Initial commit
This commit is contained in:
622
engines/parallaction/dialogue.cpp
Normal file
622
engines/parallaction/dialogue.cpp
Normal file
@@ -0,0 +1,622 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "common/events.h"
|
||||
#include "common/debug-channels.h"
|
||||
#include "common/textconsole.h"
|
||||
#include "parallaction/exec.h"
|
||||
#include "parallaction/input.h"
|
||||
#include "parallaction/parallaction.h"
|
||||
|
||||
|
||||
|
||||
namespace Parallaction {
|
||||
|
||||
#define MAX_PASSWORD_LENGTH 7
|
||||
/*
|
||||
#define QUESTION_BALLOON_X 140
|
||||
#define QUESTION_BALLOON_Y 10
|
||||
#define QUESTION_CHARACTER_X 190
|
||||
#define QUESTION_CHARACTER_Y 80
|
||||
|
||||
#define ANSWER_CHARACTER_X 10
|
||||
#define ANSWER_CHARACTER_Y 80
|
||||
*/
|
||||
struct BalloonPositions {
|
||||
Common::Point _questionBalloon;
|
||||
Common::Point _questionChar;
|
||||
|
||||
Common::Point _answerChar;
|
||||
};
|
||||
|
||||
|
||||
|
||||
#ifdef USE_TTS
|
||||
static const int kNumberOfVoiceDatas = 65;
|
||||
#endif
|
||||
|
||||
class DialogueManager {
|
||||
|
||||
Parallaction *_vm;
|
||||
Dialogue *_dialogue;
|
||||
|
||||
bool isNpc;
|
||||
GfxObj *_questioner;
|
||||
GfxObj *_answerer;
|
||||
int _faceId;
|
||||
|
||||
int _questionerVoiceID;
|
||||
int _answererVoiceID;
|
||||
|
||||
Question *_q;
|
||||
|
||||
int _answerId;
|
||||
|
||||
int _selection, _oldSelection;
|
||||
|
||||
uint32 _mouseButtons;
|
||||
Common::Point _mousePos;
|
||||
|
||||
protected:
|
||||
BalloonPositions _ballonPos;
|
||||
struct VisibleAnswer {
|
||||
Answer *_a;
|
||||
int _index; // index into Question::_answers[]
|
||||
int _balloon;
|
||||
} _visAnswers[5];
|
||||
int _numVisAnswers;
|
||||
bool _isKeyDown;
|
||||
uint16 _downKey;
|
||||
|
||||
protected:
|
||||
Gfx *_gfx;
|
||||
BalloonManager *_balloonMan;
|
||||
|
||||
public:
|
||||
DialogueManager(Parallaction *vm, ZonePtr z);
|
||||
virtual ~DialogueManager();
|
||||
|
||||
void start();
|
||||
|
||||
bool isOver() {
|
||||
return _state == DIALOGUE_OVER;
|
||||
}
|
||||
void run();
|
||||
|
||||
ZonePtr _z;
|
||||
CommandList *_cmdList;
|
||||
|
||||
protected:
|
||||
enum DialogueState {
|
||||
DIALOGUE_START,
|
||||
RUN_QUESTION,
|
||||
RUN_ANSWER,
|
||||
NEXT_QUESTION,
|
||||
NEXT_ANSWER,
|
||||
DIALOGUE_OVER
|
||||
} _state;
|
||||
|
||||
static const int NO_ANSWER_SELECTED = -1;
|
||||
|
||||
void transitionToState(DialogueState newState);
|
||||
|
||||
bool displayQuestion();
|
||||
void displayAnswers();
|
||||
bool testAnswerFlags(Answer *a);
|
||||
virtual void addVisibleAnswers(Question *q) = 0;
|
||||
virtual int16 selectAnswer() = 0;
|
||||
|
||||
int16 selectAnswer1();
|
||||
int16 selectAnswerN();
|
||||
int16 getHoverAnswer(int16 x, int16 y);
|
||||
|
||||
void runQuestion();
|
||||
void runAnswer();
|
||||
void nextQuestion();
|
||||
void nextAnswer();
|
||||
};
|
||||
|
||||
DialogueManager::DialogueManager(Parallaction *vm, ZonePtr z) : _vm(vm), _z(z) {
|
||||
_gfx = _vm->_gfx;
|
||||
_balloonMan = _vm->_balloonMan;
|
||||
|
||||
_dialogue = _z->u._speakDialogue;
|
||||
isNpc = !_z->u._filename.empty() && _z->u._filename.compareToIgnoreCase("yourself");
|
||||
_questioner = isNpc ? _vm->_disk->loadTalk(_z->u._filename.c_str()) : _vm->_char._talk;
|
||||
_answerer = _vm->_char._talk;
|
||||
|
||||
#ifdef USE_TTS
|
||||
_answererVoiceID = _vm->_characterVoiceID;
|
||||
|
||||
if (!isNpc) {
|
||||
_questionerVoiceID = _answererVoiceID;
|
||||
} else {
|
||||
_questionerVoiceID = kNarratorVoiceID;
|
||||
|
||||
for (int i = 1; i < kNumberOfVoiceDatas; ++i) {
|
||||
if (characterVoiceDatas[i].characterName && !scumm_stricmp(characterVoiceDatas[i].characterName, _z->u._filename.c_str())) {
|
||||
_questionerVoiceID = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
_cmdList = nullptr;
|
||||
_answerId = 0;
|
||||
|
||||
_faceId = 0;
|
||||
|
||||
_q = nullptr;
|
||||
memset(_visAnswers, 0, sizeof(_visAnswers));
|
||||
_numVisAnswers = 0;
|
||||
|
||||
_selection = _oldSelection = 0;
|
||||
|
||||
_isKeyDown = false;
|
||||
_downKey = 0;
|
||||
|
||||
_mouseButtons = 0;
|
||||
|
||||
_state = DIALOGUE_START;
|
||||
}
|
||||
|
||||
void DialogueManager::start() {
|
||||
_vm->_gfx->hideFloatingLabel();
|
||||
assert(_dialogue);
|
||||
_q = _dialogue->_questions[0];
|
||||
_state = DIALOGUE_START;
|
||||
transitionToState(displayQuestion() ? RUN_QUESTION : NEXT_ANSWER);
|
||||
}
|
||||
|
||||
|
||||
DialogueManager::~DialogueManager() {
|
||||
if (isNpc) {
|
||||
delete _questioner;
|
||||
}
|
||||
_z.reset();
|
||||
}
|
||||
|
||||
void DialogueManager::transitionToState(DialogueState newState) {
|
||||
static const char *dialogueStates[] = {
|
||||
"start",
|
||||
"runquestion",
|
||||
"runanswer",
|
||||
"nextquestion",
|
||||
"nextanswer",
|
||||
"over"
|
||||
};
|
||||
|
||||
if (_state != newState) {
|
||||
debugC(3, kDebugDialogue, "DialogueManager moved to state '%s'", dialogueStates[newState]);
|
||||
|
||||
if (DebugMan.isDebugChannelEnabled(kDebugDialogue) && gDebugLevel == 9) {
|
||||
switch (newState) {
|
||||
case RUN_QUESTION:
|
||||
debug(" Q : %s", _q->_text.c_str());
|
||||
break;
|
||||
case RUN_ANSWER:
|
||||
for (int i = 0; i < _numVisAnswers; ++i) {
|
||||
debug(" A%02i: %s", i, _visAnswers[i]._a->_text.c_str());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_state = newState;
|
||||
}
|
||||
|
||||
bool DialogueManager::testAnswerFlags(Answer *a) {
|
||||
uint32 flags = _vm->getLocationFlags();
|
||||
if (a->_yesFlags & kFlagsGlobal)
|
||||
flags = g_globalFlags | kFlagsGlobal;
|
||||
return ((a->_yesFlags & flags) == a->_yesFlags) && ((a->_noFlags & ~flags) == a->_noFlags);
|
||||
}
|
||||
|
||||
void DialogueManager::displayAnswers() {
|
||||
|
||||
_vm->setTTSVoice(_answererVoiceID);
|
||||
|
||||
// create balloons
|
||||
int id;
|
||||
for (int i = 0; i < _numVisAnswers; ++i) {
|
||||
id = _balloonMan->setDialogueBalloon(_visAnswers[i]._a->_text, 1, BalloonManager::kUnselectedColor);
|
||||
assert(id >= 0);
|
||||
_visAnswers[i]._balloon = id;
|
||||
|
||||
}
|
||||
|
||||
int mood = 0;
|
||||
if (_numVisAnswers == 1) {
|
||||
mood = _visAnswers[0]._a->speakerMood();
|
||||
_balloonMan->setBalloonText(_visAnswers[0]._balloon, _visAnswers[0]._a->_text, BalloonManager::kNormalColor);
|
||||
} else
|
||||
if (_numVisAnswers > 1) {
|
||||
mood = _visAnswers[0]._a->speakerMood();
|
||||
_oldSelection = NO_ANSWER_SELECTED;
|
||||
_selection = 0;
|
||||
}
|
||||
|
||||
_faceId = _gfx->setItem(_answerer, _ballonPos._answerChar.x, _ballonPos._answerChar.y);
|
||||
_gfx->setItemFrame(_faceId, mood);
|
||||
}
|
||||
|
||||
int16 DialogueManager::selectAnswer1() {
|
||||
if (_visAnswers[0]._a->textIsNull()) {
|
||||
return _visAnswers[0]._index;
|
||||
}
|
||||
|
||||
if (_mouseButtons == kMouseLeftUp) {
|
||||
return _visAnswers[0]._index;
|
||||
}
|
||||
|
||||
return NO_ANSWER_SELECTED;
|
||||
}
|
||||
|
||||
int16 DialogueManager::selectAnswerN() {
|
||||
|
||||
_selection = _balloonMan->hitTestDialogueBalloon(_mousePos.x, _mousePos.y);
|
||||
|
||||
if (_selection != _oldSelection) {
|
||||
if (_oldSelection != NO_ANSWER_SELECTED) {
|
||||
VisibleAnswer *oldAnswer = &_visAnswers[_oldSelection];
|
||||
_balloonMan->setBalloonText(oldAnswer->_balloon, oldAnswer->_a->_text, BalloonManager::kUnselectedColor);
|
||||
}
|
||||
|
||||
if (_selection != NO_ANSWER_SELECTED) {
|
||||
VisibleAnswer *answer = &_visAnswers[_selection];
|
||||
_balloonMan->setBalloonText(answer->_balloon, answer->_a->_text, BalloonManager::kSelectedColor);
|
||||
_gfx->setItemFrame(_faceId, answer->_a->speakerMood());
|
||||
}
|
||||
}
|
||||
|
||||
_oldSelection = _selection;
|
||||
|
||||
if ((_mouseButtons == kMouseLeftUp) && (_selection != NO_ANSWER_SELECTED)) {
|
||||
return _visAnswers[_selection]._index;
|
||||
}
|
||||
|
||||
return NO_ANSWER_SELECTED;
|
||||
}
|
||||
|
||||
bool DialogueManager::displayQuestion() {
|
||||
if (_q->textIsNull()) return false;
|
||||
|
||||
#ifdef USE_TTS
|
||||
// Some dialogue exchanges involve more than 1 character, differentiated by the mood
|
||||
if (_questionerVoiceID < kNumberOfVoiceDatas - 1 && characterVoiceDatas[_questionerVoiceID + 1].characterName == nullptr) {
|
||||
_vm->setTTSVoice(_questionerVoiceID + _q->speakerMood());
|
||||
} else {
|
||||
_vm->setTTSVoice(_questionerVoiceID);
|
||||
}
|
||||
#endif
|
||||
|
||||
_balloonMan->setSingleBalloon(_q->_text, _ballonPos._questionBalloon.x, _ballonPos._questionBalloon.y, _q->balloonWinding(), BalloonManager::kNormalColor);
|
||||
_faceId = _gfx->setItem(_questioner, _ballonPos._questionChar.x, _ballonPos._questionChar.y);
|
||||
_gfx->setItemFrame(_faceId, _q->speakerMood());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DialogueManager::runQuestion() {
|
||||
if (_mouseButtons == kMouseLeftUp) {
|
||||
_gfx->freeDialogueObjects();
|
||||
transitionToState(NEXT_ANSWER);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
void DialogueManager::nextAnswer() {
|
||||
if (_q->_answers[0] == nullptr) {
|
||||
transitionToState(DIALOGUE_OVER);
|
||||
return;
|
||||
}
|
||||
|
||||
// try and check if there are any suitable answers,
|
||||
// given the current game state.
|
||||
addVisibleAnswers(_q);
|
||||
if (!_numVisAnswers) {
|
||||
// if there are no answers, then chicken out
|
||||
transitionToState(DIALOGUE_OVER);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_visAnswers[0]._a->textIsNull()) {
|
||||
// if the first answer is null (it's implied that it's the
|
||||
// only one because we already called addVisibleAnswers),
|
||||
// then jump to the next question
|
||||
_answerId = _visAnswers[0]._index;
|
||||
transitionToState(NEXT_QUESTION);
|
||||
} else {
|
||||
// at this point we are sure there are non-null answers to show
|
||||
displayAnswers();
|
||||
transitionToState(RUN_ANSWER);
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueManager::runAnswer() {
|
||||
_answerId = selectAnswer();
|
||||
if (_answerId != NO_ANSWER_SELECTED) {
|
||||
_cmdList = &_q->_answers[_answerId]->_commands;
|
||||
_gfx->freeDialogueObjects();
|
||||
transitionToState(NEXT_QUESTION);
|
||||
}
|
||||
}
|
||||
|
||||
void DialogueManager::nextQuestion() {
|
||||
_q = _dialogue->findQuestion(_q->_answers[_answerId]->_followingName);
|
||||
if (_q == nullptr) {
|
||||
transitionToState(DIALOGUE_OVER);
|
||||
} else {
|
||||
transitionToState(displayQuestion() ? RUN_QUESTION : NEXT_ANSWER);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void DialogueManager::run() {
|
||||
|
||||
// cache event data
|
||||
_mouseButtons = _vm->_input->getLastButtonEvent();
|
||||
_vm->_input->getCursorPos(_mousePos);
|
||||
_isKeyDown = _vm->_input->getLastKeyDown(_downKey);
|
||||
|
||||
switch (_state) {
|
||||
case RUN_QUESTION:
|
||||
runQuestion();
|
||||
break;
|
||||
|
||||
case NEXT_ANSWER:
|
||||
nextAnswer();
|
||||
break;
|
||||
|
||||
case NEXT_QUESTION:
|
||||
nextQuestion();
|
||||
break;
|
||||
|
||||
case RUN_ANSWER:
|
||||
runAnswer();
|
||||
break;
|
||||
|
||||
case DIALOGUE_OVER:
|
||||
break;
|
||||
|
||||
default:
|
||||
error("unknown state in DialogueManager");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
class DialogueManager_ns : public DialogueManager {
|
||||
protected:
|
||||
Parallaction_ns *_vm;
|
||||
bool _passwordChanged;
|
||||
bool _askPassword;
|
||||
|
||||
bool checkPassword() {
|
||||
return ((!scumm_stricmp(_vm->_char.getBaseName(), g_doughName) && _vm->_password.hasPrefix("1732461")) ||
|
||||
(!scumm_stricmp(_vm->_char.getBaseName(), g_donnaName) && _vm->_password.hasPrefix("1622")) ||
|
||||
(!scumm_stricmp(_vm->_char.getBaseName(), g_dinoName) && _vm->_password.hasPrefix("179")));
|
||||
}
|
||||
|
||||
void resetPassword() {
|
||||
_vm->_password.clear();
|
||||
_passwordChanged = true;
|
||||
}
|
||||
|
||||
void accumPassword(uint16 ascii) {
|
||||
if (!Common::isDigit(ascii)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_vm->_password += ascii;
|
||||
_passwordChanged = true;
|
||||
}
|
||||
|
||||
int16 askPassword() {
|
||||
|
||||
if (_isKeyDown) {
|
||||
accumPassword(_downKey);
|
||||
}
|
||||
|
||||
if (_passwordChanged) {
|
||||
_balloonMan->setBalloonText(_visAnswers[0]._balloon, _visAnswers[0]._a->_text, BalloonManager::kNormalColor);
|
||||
_passwordChanged = false;
|
||||
}
|
||||
|
||||
if ((_vm->_password.size() == MAX_PASSWORD_LENGTH) || ((_isKeyDown) && (_downKey == Common::KEYCODE_RETURN))) {
|
||||
_vm->sayText(_vm->_password, Common::TextToSpeechManager::INTERRUPT);
|
||||
if (checkPassword()) {
|
||||
return 0;
|
||||
} else {
|
||||
resetPassword();
|
||||
}
|
||||
}
|
||||
|
||||
return NO_ANSWER_SELECTED;
|
||||
}
|
||||
|
||||
public:
|
||||
DialogueManager_ns(Parallaction_ns *vm, ZonePtr z) : DialogueManager(vm, z), _vm(vm),
|
||||
_passwordChanged(false), _askPassword(false) {
|
||||
_ballonPos._questionBalloon = Common::Point(140, 10);
|
||||
_ballonPos._questionChar = Common::Point(190, 80);
|
||||
_ballonPos._answerChar = Common::Point(10, 80);
|
||||
}
|
||||
|
||||
bool canDisplayAnswer(Answer *a) {
|
||||
return testAnswerFlags(a);
|
||||
}
|
||||
|
||||
void addVisibleAnswers(Question *q) override {
|
||||
_askPassword = false;
|
||||
_numVisAnswers = 0;
|
||||
for (int i = 0; i < NUM_ANSWERS && q->_answers[i]; i++) {
|
||||
Answer *a = q->_answers[i];
|
||||
if (!canDisplayAnswer(a)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (a->_text.contains("%P")) {
|
||||
_askPassword = true;
|
||||
|
||||
_vm->sayText(a->_text.substr(0, a->_text.find('@')), Common::TextToSpeechManager::INTERRUPT);
|
||||
}
|
||||
|
||||
_visAnswers[_numVisAnswers]._a = a;
|
||||
_visAnswers[_numVisAnswers]._index = i;
|
||||
_numVisAnswers++;
|
||||
}
|
||||
|
||||
resetPassword();
|
||||
}
|
||||
|
||||
int16 selectAnswer() override {
|
||||
int ans = NO_ANSWER_SELECTED;
|
||||
if (_askPassword) {
|
||||
ans = askPassword();
|
||||
} else
|
||||
if (_numVisAnswers == 1) {
|
||||
ans = selectAnswer1();
|
||||
} else {
|
||||
ans = selectAnswerN();
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
};
|
||||
|
||||
class DialogueManager_br : public DialogueManager {
|
||||
Parallaction_br *_vm;
|
||||
|
||||
public:
|
||||
DialogueManager_br(Parallaction_br *vm, ZonePtr z) : DialogueManager(vm, z), _vm(vm) {
|
||||
_ballonPos._questionBalloon = Common::Point(0, 0);
|
||||
_ballonPos._questionChar = Common::Point(380, 80);
|
||||
_ballonPos._answerChar = Common::Point(10, 80);
|
||||
}
|
||||
|
||||
bool canDisplayAnswer(Answer *a) {
|
||||
if (!a)
|
||||
return false;
|
||||
|
||||
if (a->_hasCounterCondition) {
|
||||
_vm->testCounterCondition(a->_counterName, a->_counterOp, a->_counterValue);
|
||||
return (_vm->getLocationFlags() & kFlagsTestTrue) != 0;
|
||||
}
|
||||
|
||||
return testAnswerFlags(a);
|
||||
}
|
||||
|
||||
void addVisibleAnswers(Question *q) override {
|
||||
_numVisAnswers = 0;
|
||||
for (int i = 0; i < NUM_ANSWERS && q->_answers[i]; i++) {
|
||||
Answer *a = q->_answers[i];
|
||||
if (!canDisplayAnswer(a)) {
|
||||
continue;
|
||||
}
|
||||
_visAnswers[_numVisAnswers]._a = a;
|
||||
_visAnswers[_numVisAnswers]._index = i;
|
||||
_numVisAnswers++;
|
||||
}
|
||||
}
|
||||
|
||||
int16 selectAnswer() override {
|
||||
int16 ans = NO_ANSWER_SELECTED;
|
||||
if (_numVisAnswers == 1) {
|
||||
ans = selectAnswer1();
|
||||
} else {
|
||||
ans = selectAnswerN();
|
||||
}
|
||||
return ans;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
void Parallaction::enterDialogueMode(ZonePtr z) {
|
||||
if (!z->u._speakDialogue) {
|
||||
return;
|
||||
}
|
||||
|
||||
debugC(1, kDebugDialogue, "Parallaction::enterDialogueMode(%s)", z->u._filename.c_str());
|
||||
_dialogueMan = createDialogueManager(z);
|
||||
assert(_dialogueMan);
|
||||
_dialogueMan->start();
|
||||
_input->_inputMode = Input::kInputModeDialogue;
|
||||
}
|
||||
|
||||
void Parallaction::exitDialogueMode() {
|
||||
debugC(1, kDebugDialogue, "Parallaction::exitDialogueMode()");
|
||||
_input->_inputMode = Input::kInputModeGame;
|
||||
|
||||
setTTSVoice(_characterVoiceID);
|
||||
|
||||
/* Since the current instance of _dialogueMan must be destroyed before the
|
||||
zone commands are executed, as they may create a new instance of _dialogueMan that
|
||||
would overwrite the current, we need to save the references to the command lists.
|
||||
*/
|
||||
CommandList *_cmdList = _dialogueMan->_cmdList;
|
||||
ZonePtr z = _dialogueMan->_z;
|
||||
|
||||
// destroy the _dialogueMan here
|
||||
destroyDialogueManager();
|
||||
|
||||
// run the lists saved
|
||||
if (_cmdList) {
|
||||
_cmdExec->run(*_cmdList);
|
||||
}
|
||||
_cmdExec->run(z->_commands, z);
|
||||
}
|
||||
|
||||
void Parallaction::destroyDialogueManager() {
|
||||
// destroy the _dialogueMan here
|
||||
delete _dialogueMan;
|
||||
_dialogueMan = nullptr;
|
||||
}
|
||||
|
||||
void Parallaction::runDialogueFrame() {
|
||||
if (_input->_inputMode != Input::kInputModeDialogue) {
|
||||
return;
|
||||
}
|
||||
|
||||
_dialogueMan->run();
|
||||
|
||||
if (_dialogueMan->isOver()) {
|
||||
exitDialogueMode();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DialogueManager *Parallaction_ns::createDialogueManager(ZonePtr z) {
|
||||
return new DialogueManager_ns(this, z);
|
||||
}
|
||||
|
||||
DialogueManager *Parallaction_br::createDialogueManager(ZonePtr z) {
|
||||
return new DialogueManager_br(this, z);
|
||||
}
|
||||
|
||||
} // namespace Parallaction
|
||||
Reference in New Issue
Block a user