Initial commit
This commit is contained in:
584
engines/myst3/movie.cpp
Normal file
584
engines/myst3/movie.cpp
Normal file
@@ -0,0 +1,584 @@
|
||||
/* 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 "engines/myst3/movie.h"
|
||||
#include "engines/myst3/ambient.h"
|
||||
#include "engines/myst3/myst3.h"
|
||||
#include "engines/myst3/sound.h"
|
||||
#include "engines/myst3/state.h"
|
||||
#include "engines/myst3/subtitles.h"
|
||||
|
||||
#include "common/config-manager.h"
|
||||
|
||||
namespace Myst3 {
|
||||
|
||||
Movie::Movie(Myst3Engine *vm, uint16 id) :
|
||||
_vm(vm),
|
||||
_id(id),
|
||||
_posU(0),
|
||||
_posV(0),
|
||||
_startFrame(0),
|
||||
_endFrame(0),
|
||||
_texture(nullptr),
|
||||
_force2d(false),
|
||||
_forceOpaque(false),
|
||||
_subtitles(nullptr),
|
||||
_volume(0),
|
||||
_additiveBlending(false),
|
||||
_transparency(100) {
|
||||
|
||||
ResourceDescription binkDesc = _vm->getFileDescription("", id, 0, Archive::kMultitrackMovie);
|
||||
|
||||
if (!binkDesc.isValid())
|
||||
binkDesc = _vm->getFileDescription("", id, 0, Archive::kDialogMovie);
|
||||
|
||||
if (!binkDesc.isValid())
|
||||
binkDesc = _vm->getFileDescription("", id, 0, Archive::kStillMovie);
|
||||
|
||||
if (!binkDesc.isValid())
|
||||
binkDesc = _vm->getFileDescription("", id, 0, Archive::kMovie);
|
||||
|
||||
// Check whether the video is optional
|
||||
bool optional = false;
|
||||
if (_vm->_state->hasVarMovieOptional()) {
|
||||
optional = _vm->_state->getMovieOptional();
|
||||
_vm->_state->setMovieOptional(0);
|
||||
}
|
||||
|
||||
if (!binkDesc.isValid()) {
|
||||
if (!optional)
|
||||
error("Movie %d does not exist", id);
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
loadPosition(binkDesc.getVideoData());
|
||||
|
||||
Common::SeekableReadStream *binkStream = binkDesc.getData();
|
||||
_bink.setSoundType(Audio::Mixer::kSFXSoundType);
|
||||
_bink.loadStream(binkStream);
|
||||
_bink.setOutputPixelFormat(Texture::getRGBAPixelFormat());
|
||||
|
||||
if (binkDesc.getType() == Archive::kMultitrackMovie || binkDesc.getType() == Archive::kDialogMovie) {
|
||||
uint language = ConfMan.getInt("audio_language");
|
||||
_bink.setAudioTrack(language);
|
||||
}
|
||||
|
||||
if (ConfMan.getBool("subtitles"))
|
||||
_subtitles = Subtitles::create(_vm, id);
|
||||
|
||||
// Clear the subtitles override anyway, so that it does not end up
|
||||
// being used by the another movie at some point.
|
||||
_vm->_state->setMovieOverrideSubtitles(0);
|
||||
}
|
||||
|
||||
void Movie::loadPosition(const ResourceDescription::VideoData &videoData) {
|
||||
static const float scale = 50.0f;
|
||||
|
||||
_is3D = _vm->_state->getViewType() == kCube;
|
||||
assert(!_texture);
|
||||
|
||||
Math::Vector3d planeDirection = videoData.v1;
|
||||
planeDirection.normalize();
|
||||
|
||||
Math::Vector3d u;
|
||||
u.set(planeDirection.z(), 0.0f, -planeDirection.x());
|
||||
u.normalize();
|
||||
|
||||
Math::Vector3d v = Math::Vector3d::crossProduct(planeDirection, u);
|
||||
v.normalize();
|
||||
|
||||
Math::Vector3d planeOrigin = planeDirection * scale;
|
||||
|
||||
float left = (videoData.u - 320) * 0.003125f;
|
||||
float right = (videoData.u + videoData.width - 320) * 0.003125f;
|
||||
float top = (320 - videoData.v) * 0.003125f;
|
||||
float bottom = (320 - videoData.v - videoData.height) * 0.003125f;
|
||||
|
||||
Math::Vector3d vLeft = scale * left * u;
|
||||
Math::Vector3d vRight = scale * right * u;
|
||||
Math::Vector3d vTop = scale * top * v;
|
||||
Math::Vector3d vBottom = scale * bottom * v;
|
||||
|
||||
_pTopLeft = planeOrigin + vTop + vLeft;
|
||||
_pBottomLeft = planeOrigin + vBottom + vLeft;
|
||||
_pBottomRight = planeOrigin + vBottom + vRight;
|
||||
_pTopRight = planeOrigin + vTop + vRight;
|
||||
|
||||
_posU = videoData.u;
|
||||
_posV = videoData.v;
|
||||
}
|
||||
|
||||
void Movie::draw2d() {
|
||||
Common::Rect screenRect = Common::Rect(_bink.getWidth(), _bink.getHeight());
|
||||
screenRect.translate(_posU, _posV);
|
||||
|
||||
Common::Rect textureRect = Common::Rect(_bink.getWidth(), _bink.getHeight());
|
||||
|
||||
if (_forceOpaque)
|
||||
_vm->_gfx->drawTexturedRect2D(screenRect, textureRect, _texture);
|
||||
else
|
||||
_vm->_gfx->drawTexturedRect2D(screenRect, textureRect, _texture, (float) _transparency / 100, _additiveBlending);
|
||||
}
|
||||
|
||||
void Movie::draw3d() {
|
||||
_vm->_gfx->drawTexturedRect3D(_pTopLeft, _pBottomLeft, _pTopRight, _pBottomRight, _texture);
|
||||
}
|
||||
|
||||
void Movie::draw() {
|
||||
if (_force2d)
|
||||
return;
|
||||
|
||||
if (_is3D) {
|
||||
draw3d();
|
||||
} else {
|
||||
draw2d();
|
||||
}
|
||||
}
|
||||
|
||||
void Movie::drawOverlay() {
|
||||
if (_force2d)
|
||||
draw2d();
|
||||
|
||||
if (_subtitles) {
|
||||
_subtitles->setFrame(adjustFrameForRate(_bink.getCurFrame(), false));
|
||||
_vm->_gfx->renderWindowOverlay(_subtitles);
|
||||
}
|
||||
}
|
||||
|
||||
void Movie::drawNextFrameToTexture() {
|
||||
const Graphics::Surface *frame = _bink.decodeNextFrame();
|
||||
|
||||
if (frame) {
|
||||
if (_texture)
|
||||
_texture->update(frame);
|
||||
else if (_is3D)
|
||||
_texture = _vm->_gfx->createTexture3D(frame);
|
||||
else
|
||||
_texture = _vm->_gfx->createTexture2D(frame);
|
||||
}
|
||||
}
|
||||
|
||||
int32 Movie::adjustFrameForRate(int32 frame, bool dataToBink) {
|
||||
// The scripts give frame numbers for a framerate of 15 im/s
|
||||
// adjust the frame number according to the actual framerate
|
||||
if (_bink.getFrameRate().toInt() != 15) {
|
||||
Common::Rational rational;
|
||||
if (dataToBink) {
|
||||
rational = _bink.getFrameRate() * frame / 15;
|
||||
} else {
|
||||
rational = 15 * frame / _bink.getFrameRate();
|
||||
}
|
||||
frame = rational.toInt();
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
void Movie::setStartFrame(int32 v) {
|
||||
_startFrame = adjustFrameForRate(v, true);
|
||||
}
|
||||
|
||||
void Movie::setEndFrame(int32 v) {
|
||||
_endFrame = adjustFrameForRate(v, true);
|
||||
}
|
||||
|
||||
void Movie::pause(bool p) {
|
||||
_bink.pauseVideo(p);
|
||||
}
|
||||
|
||||
Movie::~Movie() {
|
||||
if (_texture)
|
||||
delete _texture;
|
||||
|
||||
delete _subtitles;
|
||||
}
|
||||
|
||||
void Movie::setForce2d(bool b) {
|
||||
_force2d = b;
|
||||
if (_force2d) {
|
||||
if (_is3D)
|
||||
delete _texture;
|
||||
_is3D = false;
|
||||
}
|
||||
}
|
||||
|
||||
ScriptedMovie::ScriptedMovie(Myst3Engine *vm, uint16 id) :
|
||||
Movie(vm, id),
|
||||
_condition(0),
|
||||
_conditionBit(0),
|
||||
_startFrameVar(0),
|
||||
_endFrameVar(0),
|
||||
_posUVar(0),
|
||||
_posVVar(0),
|
||||
_nextFrameReadVar(0),
|
||||
_nextFrameWriteVar(0),
|
||||
_playingVar(0),
|
||||
_enabled(false),
|
||||
_disableWhenComplete(false),
|
||||
_scriptDriven(false),
|
||||
_isLastFrame(false),
|
||||
_soundHeading(0),
|
||||
_soundAttenuation(0),
|
||||
_volumeVar(0),
|
||||
_loop(false),
|
||||
_transparencyVar(0) {
|
||||
_bink.start();
|
||||
}
|
||||
|
||||
void ScriptedMovie::draw() {
|
||||
if (!_enabled)
|
||||
return;
|
||||
|
||||
Movie::draw();
|
||||
}
|
||||
|
||||
void ScriptedMovie::drawOverlay() {
|
||||
if (!_enabled)
|
||||
return;
|
||||
|
||||
Movie::drawOverlay();
|
||||
}
|
||||
|
||||
void ScriptedMovie::update() {
|
||||
if (_startFrameVar) {
|
||||
_startFrame = _vm->_state->getVar(_startFrameVar);
|
||||
}
|
||||
|
||||
if (_endFrameVar) {
|
||||
_endFrame = _vm->_state->getVar(_endFrameVar);
|
||||
}
|
||||
|
||||
if (!_endFrame) {
|
||||
_endFrame = _bink.getFrameCount();
|
||||
}
|
||||
|
||||
if (_posUVar) {
|
||||
_posU = _vm->_state->getVar(_posUVar);
|
||||
}
|
||||
|
||||
if (_posVVar) {
|
||||
_posV = _vm->_state->getVar(_posVVar);
|
||||
}
|
||||
|
||||
if (_transparencyVar) {
|
||||
_transparency = _vm->_state->getVar(_transparencyVar);
|
||||
}
|
||||
|
||||
bool newEnabled;
|
||||
if (_conditionBit) {
|
||||
newEnabled = (_vm->_state->getVar(_condition) & (1 << (_conditionBit - 1))) != 0;
|
||||
} else {
|
||||
newEnabled = _vm->_state->evaluate(_condition);
|
||||
}
|
||||
|
||||
if (newEnabled != _enabled) {
|
||||
_enabled = newEnabled;
|
||||
|
||||
if (newEnabled) {
|
||||
if (_disableWhenComplete
|
||||
|| _bink.getCurFrame() < _startFrame
|
||||
|| _bink.getCurFrame() >= _endFrame
|
||||
|| _bink.endOfVideo()) {
|
||||
_bink.seekToFrame(_startFrame);
|
||||
_isLastFrame = false;
|
||||
}
|
||||
|
||||
if (!_scriptDriven)
|
||||
_bink.pauseVideo(false);
|
||||
|
||||
drawNextFrameToTexture();
|
||||
|
||||
} else {
|
||||
// Make sure not to pause the video twice. VideoDecoder handles pause levels.
|
||||
// The video may have already been paused if _disableWhenComplete is set.
|
||||
if (!_bink.isPaused()) {
|
||||
_bink.pauseVideo(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_enabled) {
|
||||
updateVolume();
|
||||
|
||||
if (_nextFrameReadVar) {
|
||||
int32 nextFrame = _vm->_state->getVar(_nextFrameReadVar);
|
||||
if (nextFrame > 0 && nextFrame <= (int32)_bink.getFrameCount()) {
|
||||
// Are we changing frame?
|
||||
if (_bink.getCurFrame() != nextFrame - 1) {
|
||||
// Don't seek if we just want to display the next frame
|
||||
if (_bink.getCurFrame() + 1 != nextFrame - 1) {
|
||||
_bink.seekToFrame(nextFrame - 1);
|
||||
}
|
||||
drawNextFrameToTexture();
|
||||
}
|
||||
|
||||
_vm->_state->setVar(_nextFrameReadVar, 0);
|
||||
_isLastFrame = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_scriptDriven && (_bink.needsUpdate() || _isLastFrame)) {
|
||||
bool complete = false;
|
||||
|
||||
if (_isLastFrame) {
|
||||
_isLastFrame = false;
|
||||
|
||||
if (_loop) {
|
||||
_bink.seekToFrame(_startFrame);
|
||||
drawNextFrameToTexture();
|
||||
} else {
|
||||
complete = true;
|
||||
}
|
||||
} else {
|
||||
drawNextFrameToTexture();
|
||||
_isLastFrame = _bink.getCurFrame() == (_endFrame - 1);
|
||||
}
|
||||
|
||||
if (_nextFrameWriteVar) {
|
||||
_vm->_state->setVar(_nextFrameWriteVar, _bink.getCurFrame() + 1);
|
||||
}
|
||||
|
||||
if (_disableWhenComplete && complete) {
|
||||
_bink.pauseVideo(true);
|
||||
|
||||
if (_playingVar) {
|
||||
_vm->_state->setVar(_playingVar, 0);
|
||||
} else {
|
||||
_enabled = 0;
|
||||
_vm->_state->setVar(_condition & 0x7FF, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptedMovie::updateVolume() {
|
||||
int32 volume;
|
||||
if (_volumeVar) {
|
||||
volume = _vm->_state->getVar(_volumeVar);
|
||||
} else {
|
||||
volume = _volume;
|
||||
}
|
||||
|
||||
int32 mixerVolume, balance;
|
||||
_vm->_sound->computeVolumeBalance(volume, _soundHeading, _soundAttenuation, &mixerVolume, &balance);
|
||||
_bink.setVolume(mixerVolume);
|
||||
_bink.setBalance(balance);
|
||||
}
|
||||
|
||||
ScriptedMovie::~ScriptedMovie() {
|
||||
}
|
||||
|
||||
SimpleMovie::SimpleMovie(Myst3Engine *vm, uint16 id) :
|
||||
Movie(vm, id),
|
||||
_synchronized(false) {
|
||||
_startFrame = 1;
|
||||
_endFrame = _bink.getFrameCount();
|
||||
_startEngineTick = _vm->_state->getTickCount();
|
||||
}
|
||||
|
||||
void SimpleMovie::play() {
|
||||
playStartupSound();
|
||||
|
||||
_bink.setEndFrame(_endFrame - 1);
|
||||
_bink.setVolume(_volume * Audio::Mixer::kMaxChannelVolume / 100);
|
||||
|
||||
if (_bink.getCurFrame() < _startFrame - 1) {
|
||||
_bink.seekToFrame(_startFrame - 1);
|
||||
}
|
||||
|
||||
_bink.start();
|
||||
}
|
||||
|
||||
void SimpleMovie::update() {
|
||||
uint16 scriptStartFrame = _vm->_state->getMovieScriptStartFrame();
|
||||
if (scriptStartFrame && _bink.getCurFrame() > scriptStartFrame) {
|
||||
uint16 script = _vm->_state->getMovieScript();
|
||||
|
||||
// The control variables are reset before running the script because
|
||||
// some scripts set up another movie triggered script
|
||||
_vm->_state->setMovieScriptStartFrame(0);
|
||||
_vm->_state->setMovieScript(0);
|
||||
|
||||
_vm->runScriptsFromNode(script);
|
||||
}
|
||||
|
||||
uint16 ambiantStartFrame = _vm->_state->getMovieAmbiantScriptStartFrame();
|
||||
if (ambiantStartFrame && _bink.getCurFrame() > ambiantStartFrame) {
|
||||
// Fixes bug #16491 for missing ambient sounds while a simpleMovie is playing
|
||||
// eg. the burning fire after Saavedro throws the lamp at the curtain in Tomahna (during the intro at Atrus's office)
|
||||
// and the screaming squee after trapping it in Edanna.
|
||||
// This fix is essentially replicating the code from ambientReloadCurrentNode() script command.
|
||||
// In these movies (SimpleMovie), a frame number (ambiantStartFrame) is set (before they start)
|
||||
// upon which the ambient sounds scripts should be re-evaluated,
|
||||
// because a condition and related variables to that condition will have changed
|
||||
// and therefore different ambient sounds should be played.
|
||||
//
|
||||
// NOTE: The value set for var MovieAmbiantScript (181) is not a node id -- so calling runAmbientScripts() here with it as argument does not work.
|
||||
// Instead, we just reload the current node's ambient scripts and then applySounds using the value of MovieAmbiantScript as fadeOutDelay.
|
||||
_vm->_ambient->loadNode(0, 0, 0);
|
||||
_vm->_ambient->applySounds(_vm->_state->valueOrVarValue(_vm->_state->getMovieAmbiantScript()));
|
||||
|
||||
_vm->_state->setMovieAmbiantScriptStartFrame(0);
|
||||
_vm->_state->setMovieAmbiantScript(0);
|
||||
}
|
||||
|
||||
if (!_synchronized) {
|
||||
// Play the movie according to the bink file framerate
|
||||
if (_bink.needsUpdate()) {
|
||||
drawNextFrameToTexture();
|
||||
}
|
||||
} else {
|
||||
// Draw a movie frame each two engine frames
|
||||
int targetFrame = (_vm->_state->getTickCount() - _startEngineTick) >> 1;
|
||||
if (_bink.getCurFrame() < targetFrame) {
|
||||
drawNextFrameToTexture();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SimpleMovie::endOfVideo() {
|
||||
if (!_synchronized) {
|
||||
return _bink.getTime() >= (uint)_bink.getEndTime().msecs();
|
||||
} else {
|
||||
int tickBasedEndFrame = (_vm->_state->getTickCount() - _startEngineTick) >> 1;
|
||||
return tickBasedEndFrame >= _endFrame;
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleMovie::playStartupSound() {
|
||||
int32 soundId = _vm->_state->getMovieStartSoundId();
|
||||
if (soundId) {
|
||||
uint32 volume = _vm->_state->getMovieStartSoundVolume();
|
||||
uint32 heading = _vm->_state->getMovieStartSoundHeading();
|
||||
uint32 attenuation = _vm->_state->getMovieStartSoundAttenuation();
|
||||
|
||||
_vm->_sound->playEffect(soundId, volume, heading, attenuation);
|
||||
|
||||
_vm->_state->setMovieStartSoundId(0);
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleMovie::refreshAmbientSounds() {
|
||||
uint32 engineFrames = _bink.getFrameCount() * 2;
|
||||
_vm->_ambient->playCurrentNode(100, engineFrames);
|
||||
}
|
||||
|
||||
SimpleMovie::~SimpleMovie() {
|
||||
}
|
||||
|
||||
ProjectorMovie::ProjectorMovie(Myst3Engine *vm, uint16 id, Graphics::Surface *background) :
|
||||
ScriptedMovie(vm, id),
|
||||
_background(background),
|
||||
_frame(nullptr) {
|
||||
_enabled = true;
|
||||
|
||||
for (uint i = 0; i < kBlurIterations; i++) {
|
||||
_blurTableX[i] = (uint8)(sin(2 * (float)M_PI * i / (float)kBlurIterations) * 256.0);
|
||||
_blurTableY[i] = (uint8)(cos(2 * (float)M_PI * i / (float)kBlurIterations) * 256.0);
|
||||
}
|
||||
}
|
||||
|
||||
ProjectorMovie::~ProjectorMovie() {
|
||||
if (_frame) {
|
||||
_frame->free();
|
||||
delete _frame;
|
||||
}
|
||||
|
||||
if (_background) {
|
||||
_background->free();
|
||||
delete _background;
|
||||
}
|
||||
}
|
||||
|
||||
void ProjectorMovie::update() {
|
||||
if (!_frame) {
|
||||
// First call, get the alpha channel from the bink file
|
||||
const Graphics::Surface *frame = _bink.decodeNextFrame();
|
||||
_frame = new Graphics::Surface();
|
||||
_frame->copyFrom(*frame);
|
||||
}
|
||||
|
||||
uint16 focus = _vm->_state->getProjectorBlur() / 10;
|
||||
uint16 zoom = _vm->_state->getProjectorZoom();
|
||||
uint16 backgroundX = (_vm->_state->getProjectorX() - zoom / 2) / 10;
|
||||
uint16 backgroundY = (_vm->_state->getProjectorY() - zoom / 2) / 10;
|
||||
float delta = zoom / 10.0 / _frame->w;
|
||||
|
||||
// For each pixel in the target image
|
||||
for (int i = 0; i < _frame->h; i++) {
|
||||
byte *dst = (byte *)_frame->getBasePtr(0, i);
|
||||
for (int j = 0; j < _frame->w; j++) {
|
||||
uint8 depth;
|
||||
uint16 r = 0, g = 0, b = 0;
|
||||
uint32 srcX = (uint32)(backgroundX + j * delta);
|
||||
uint32 srcY = (uint32)(backgroundY + i * delta);
|
||||
byte *src = (byte *)_background->getBasePtr(srcX, srcY);
|
||||
|
||||
// Get the depth from the background
|
||||
depth = *(src + 3);
|
||||
|
||||
// Compute the blur level from the focus point and the depth of the current point
|
||||
uint8 blurLevel = abs(focus - depth) + 1;
|
||||
|
||||
// No need to compute the effect for transparent pixels
|
||||
byte a = *(dst + 3);
|
||||
if (a != 0) {
|
||||
// The blur effect is done by mixing the color components from the pixel at (srcX, srcY)
|
||||
// and other pixels on the same row / column
|
||||
uint cnt = 0;
|
||||
for (uint k = 0; k < kBlurIterations; k++) {
|
||||
uint32 blurX = srcX + ((uint32) (blurLevel * _blurTableX[k] * delta) >> 12); // >> 12 = / 256 / 16
|
||||
uint32 blurY = srcY + ((uint32) (blurLevel * _blurTableY[k] * delta) >> 12);
|
||||
|
||||
if (blurX < 1024 && blurY < 1024) {
|
||||
byte *blur = (byte *)_background->getBasePtr(blurX, blurY);
|
||||
|
||||
r += *blur++;
|
||||
g += *blur++;
|
||||
b += *blur;
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
|
||||
// Divide the components by the number of pixels used in the blur effect
|
||||
r /= cnt;
|
||||
g /= cnt;
|
||||
b /= cnt;
|
||||
}
|
||||
|
||||
// Draw the new pixel
|
||||
*dst++ = r;
|
||||
*dst++ = g;
|
||||
*dst++ = b;
|
||||
dst++; // Keep the alpha channel from the previous frame
|
||||
}
|
||||
}
|
||||
|
||||
if (_texture)
|
||||
_texture->update(_frame);
|
||||
else if (_is3D)
|
||||
_texture = _vm->_gfx->createTexture3D(_frame);
|
||||
else
|
||||
_texture = _vm->_gfx->createTexture2D(_frame);
|
||||
}
|
||||
|
||||
} // End of namespace Myst3
|
||||
Reference in New Issue
Block a user