- Deixe el Abu Simbel ací per a anar fent proves de càrrega
- [NEW] Mòdul zx_tape, encara no carrega correctament les cintes. - [CHG] Gestió completa del só pasada a la ULA, i de la pantalla a zxscreen
This commit is contained in:
BIN
abusimbel.tap
Normal file
BIN
abusimbel.tap
Normal file
Binary file not shown.
63
main.cpp
63
main.cpp
@@ -5,18 +5,11 @@
|
|||||||
#include "z80debug.h"
|
#include "z80debug.h"
|
||||||
#include "zx_ula.h"
|
#include "zx_ula.h"
|
||||||
#include "zx_screen.h"
|
#include "zx_screen.h"
|
||||||
|
#include "zx_tape.h"
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
uint8_t memory[65536];
|
uint8_t memory[65536];
|
||||||
uint32_t t = 0;
|
|
||||||
uint16_t ts = 0;
|
|
||||||
uint8_t ft = 0;
|
|
||||||
uint32_t fps=0;
|
|
||||||
uint32_t fps_time=0;
|
|
||||||
|
|
||||||
|
|
||||||
uint8_t test = 0;
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
@@ -33,6 +26,8 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
zx_ula::sound_init();
|
zx_ula::sound_init();
|
||||||
|
|
||||||
|
zx_tape::load("abusimbel.tap");
|
||||||
|
|
||||||
bool should_exit = false;
|
bool should_exit = false;
|
||||||
SDL_Event e;
|
SDL_Event e;
|
||||||
|
|
||||||
@@ -44,27 +39,22 @@ int main(int argc, char *argv[])
|
|||||||
if (z80debug::debugging()) {
|
if (z80debug::debugging()) {
|
||||||
if ((e.type==SDL_WINDOWEVENT) && ((e.window.event==SDL_WINDOWEVENT_SHOWN) || (e.window.event==SDL_WINDOWEVENT_EXPOSED))) {
|
if ((e.type==SDL_WINDOWEVENT) && ((e.window.event==SDL_WINDOWEVENT_SHOWN) || (e.window.event==SDL_WINDOWEVENT_EXPOSED))) {
|
||||||
z80debug::refresh();
|
z80debug::refresh();
|
||||||
zxscreen::refresh();
|
zxscreen::refresh(0, true);
|
||||||
}
|
}
|
||||||
if (e.type == SDL_KEYDOWN) {
|
if (e.type == SDL_KEYDOWN) {
|
||||||
if (e.key.keysym.scancode==SDL_SCANCODE_ESCAPE) {
|
if (e.key.keysym.scancode==SDL_SCANCODE_ESCAPE) {
|
||||||
should_exit=true; break;
|
should_exit=true; break;
|
||||||
} else if (e.key.keysym.scancode==SDL_SCANCODE_F10) {
|
} else if (e.key.keysym.scancode==SDL_SCANCODE_F10) {
|
||||||
t += z80::step();
|
const uint8_t dt = z80::step();
|
||||||
if (t>=69888) { t=0; z80::interrupt(); }
|
|
||||||
z80debug::refresh();
|
z80debug::refresh();
|
||||||
zxscreen::refresh();
|
zxscreen::refresh(dt, true);
|
||||||
} else if (e.key.keysym.scancode==SDL_SCANCODE_F11) {
|
} else if (e.key.keysym.scancode==SDL_SCANCODE_F11) {
|
||||||
t += z80debug::next();
|
const uint8_t dt = z80debug::next();
|
||||||
fps=0;
|
zxscreen::refresh(dt, true);
|
||||||
fps_time = SDL_GetTicks();
|
|
||||||
zxscreen::refresh();
|
|
||||||
} else if (e.key.keysym.scancode==SDL_SCANCODE_F5) {
|
} else if (e.key.keysym.scancode==SDL_SCANCODE_F5) {
|
||||||
t += z80::step();
|
const uint8_t dt = z80::step();
|
||||||
z80debug::cont();
|
z80debug::cont();
|
||||||
fps=0;
|
zxscreen::refresh(dt);
|
||||||
fps_time = SDL_GetTicks();
|
|
||||||
zxscreen::refresh();
|
|
||||||
} else if (e.key.keysym.scancode==SDL_SCANCODE_RETURN) {
|
} else if (e.key.keysym.scancode==SDL_SCANCODE_RETURN) {
|
||||||
z80debug::executeConsole();
|
z80debug::executeConsole();
|
||||||
} else if (e.key.keysym.scancode==SDL_SCANCODE_BACKSPACE) {
|
} else if (e.key.keysym.scancode==SDL_SCANCODE_BACKSPACE) {
|
||||||
@@ -78,7 +68,10 @@ int main(int argc, char *argv[])
|
|||||||
if (e.type == SDL_KEYDOWN) {
|
if (e.type == SDL_KEYDOWN) {
|
||||||
if (e.key.keysym.scancode==SDL_SCANCODE_F8) {
|
if (e.key.keysym.scancode==SDL_SCANCODE_F8) {
|
||||||
z80debug::stop();
|
z80debug::stop();
|
||||||
zxscreen::refresh();
|
zxscreen::refresh(0, true);
|
||||||
|
}
|
||||||
|
if (e.key.keysym.scancode==SDL_SCANCODE_F12) {
|
||||||
|
zx_tape::play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,30 +79,12 @@ int main(int argc, char *argv[])
|
|||||||
if (!z80debug::debugging()) {
|
if (!z80debug::debugging()) {
|
||||||
if (z80debug::isbreak(z80::getPC(), 9)) {
|
if (z80debug::isbreak(z80::getPC(), 9)) {
|
||||||
z80debug::stop();
|
z80debug::stop();
|
||||||
zxscreen::refresh();
|
zxscreen::refresh(0, true);
|
||||||
} else {
|
} else {
|
||||||
uint8_t t_states = z80::step();
|
uint8_t dt = z80::step();
|
||||||
t += t_states;
|
zx_tape::update(dt);
|
||||||
ts += t_states;
|
zx_ula::sound_update(dt);
|
||||||
if (ts>=400) {
|
zxscreen::refresh(dt);
|
||||||
ts-=400;
|
|
||||||
zx_ula::sound_update();
|
|
||||||
}
|
|
||||||
if (t>=69888) {
|
|
||||||
ft++;
|
|
||||||
if (ft==16) { ft=0; zxscreen::flash(); }
|
|
||||||
/*
|
|
||||||
fps++;
|
|
||||||
if (SDL_GetTicks() - fps_time >= 1000) {
|
|
||||||
printf("FPS: %i\n", fps);
|
|
||||||
fps = 0;
|
|
||||||
fps_time = SDL_GetTicks();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
t=0;
|
|
||||||
zxscreen::refresh();
|
|
||||||
z80::interrupt();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ namespace zxscreen
|
|||||||
SDL_Renderer *ren = nullptr;
|
SDL_Renderer *ren = nullptr;
|
||||||
SDL_Texture *tex = nullptr;
|
SDL_Texture *tex = nullptr;
|
||||||
|
|
||||||
bool _flash = false;
|
uint32_t t_screen = 0;
|
||||||
|
uint8_t t_flash = 0;
|
||||||
void flash() { _flash = not _flash; }
|
bool flash = false;
|
||||||
|
|
||||||
void show()
|
void show()
|
||||||
{
|
{
|
||||||
@@ -23,11 +23,18 @@ namespace zxscreen
|
|||||||
win = SDL_CreateWindow("ZX Spectrum Screen", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 352, 288, SDL_WINDOW_RESIZABLE);
|
win = SDL_CreateWindow("ZX Spectrum Screen", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 352, 288, SDL_WINDOW_RESIZABLE);
|
||||||
ren = SDL_CreateRenderer(win, -1, 0);
|
ren = SDL_CreateRenderer(win, -1, 0);
|
||||||
tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 352, 288);
|
tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 352, 288);
|
||||||
refresh();
|
refresh(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh()
|
void refresh(const uint8_t dt, const bool force_refresh)
|
||||||
{
|
{
|
||||||
|
t_screen += dt;
|
||||||
|
if (t_screen>=69888) {
|
||||||
|
t_flash++;
|
||||||
|
if (t_flash==16) { t_flash=0; flash = !flash; }
|
||||||
|
t_screen=0;
|
||||||
|
} else if (!force_refresh) return;
|
||||||
|
|
||||||
const uint8_t* memory = z80::getMem();
|
const uint8_t* memory = z80::getMem();
|
||||||
const uint8_t border_color = zx_ula::get_border_color();
|
const uint8_t border_color = zx_ula::get_border_color();
|
||||||
//memory+=0x4000;
|
//memory+=0x4000;
|
||||||
@@ -53,7 +60,7 @@ namespace zxscreen
|
|||||||
uint16_t address = 0x4000 | (x&0x1f) | ((y&0x7)<<8) | ((y&0x38)<<2) | ((y&0xc0)<<5);
|
uint16_t address = 0x4000 | (x&0x1f) | ((y&0x7)<<8) | ((y&0x38)<<2) | ((y&0xc0)<<5);
|
||||||
uint8_t block = memory[address];
|
uint8_t block = memory[address];
|
||||||
uint8_t c1 = color&0x7, c2 = (color>>3)&0x7;
|
uint8_t c1 = color&0x7, c2 = (color>>3)&0x7;
|
||||||
if ((color&0x80) && _flash) { c1=c2; c2=color&0x7; }
|
if ((color&0x80) && flash) { c1=c2; c2=color&0x7; }
|
||||||
for (int i=0;i<8;++i)
|
for (int i=0;i<8;++i)
|
||||||
{
|
{
|
||||||
*(pixels++)=(block&0x80) ? palette[c1] : palette[c2];
|
*(pixels++)=(block&0x80) ? palette[c1] : palette[c2];
|
||||||
@@ -71,5 +78,7 @@ namespace zxscreen
|
|||||||
SDL_UnlockTexture(tex);
|
SDL_UnlockTexture(tex);
|
||||||
SDL_RenderCopy(ren, tex, NULL, NULL);
|
SDL_RenderCopy(ren, tex, NULL, NULL);
|
||||||
SDL_RenderPresent(ren);
|
SDL_RenderPresent(ren);
|
||||||
|
|
||||||
|
if (t_screen==0) z80::interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
namespace zxscreen
|
namespace zxscreen
|
||||||
{
|
{
|
||||||
void show();
|
void show();
|
||||||
void refresh();
|
void refresh(const uint8_t dt, const bool force_refresh=false);
|
||||||
void flash();
|
|
||||||
}
|
}
|
||||||
|
|||||||
212
zx_tape.cpp
Normal file
212
zx_tape.cpp
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
#include "zx_tape.h"
|
||||||
|
#include "zx_ula.h"
|
||||||
|
#include <vector>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
namespace zx_tape
|
||||||
|
{
|
||||||
|
#define PULSE_PILOT 0
|
||||||
|
#define PULSE_SYNC1 1
|
||||||
|
#define PULSE_SYNC2 2
|
||||||
|
#define PULSE_DATA 3
|
||||||
|
#define PULSE_CHECKSUM 4
|
||||||
|
#define PULSE_SYNC3 5
|
||||||
|
#define PULSE_WAIT 6
|
||||||
|
|
||||||
|
#define PULSE_LEN_ZERO 855
|
||||||
|
#define PULSE_LEN_ONE 1710
|
||||||
|
|
||||||
|
#define PULSE_LEN_PILOT 2168
|
||||||
|
#define PULSE_LEN_SYNC1 667
|
||||||
|
#define PULSE_LEN_SYNC2 735
|
||||||
|
#define PULSE_LEN_SYNC3 954
|
||||||
|
|
||||||
|
#define PULSE_COUNT_PILOT_HEADER 8063
|
||||||
|
#define PULSE_COUNT_PILOT_DATA 3223
|
||||||
|
|
||||||
|
struct block_t
|
||||||
|
{
|
||||||
|
uint16_t length;
|
||||||
|
uint8_t flag;
|
||||||
|
uint8_t *data;
|
||||||
|
uint8_t checksum;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool playing = false;
|
||||||
|
bool loaded = false;
|
||||||
|
|
||||||
|
std::vector<block_t> blocks;
|
||||||
|
uint8_t current_block = 0;
|
||||||
|
uint16_t block_pos=0;
|
||||||
|
uint8_t current_bit=0;
|
||||||
|
|
||||||
|
uint8_t current_section = PULSE_PILOT;
|
||||||
|
uint16_t current_pulse = 0;
|
||||||
|
uint32_t pulse_pos = 0;
|
||||||
|
uint8_t pulse_level = 0;
|
||||||
|
|
||||||
|
void load(const char* filename)
|
||||||
|
{
|
||||||
|
//[TODO] Free memory that might be taken by previous tape
|
||||||
|
|
||||||
|
FILE *f = fopen(filename, "rb");
|
||||||
|
if (!f) return;
|
||||||
|
while (!feof(f))
|
||||||
|
{
|
||||||
|
block_t block;
|
||||||
|
fread(&block.length, 2, 1, f);
|
||||||
|
fread(&block.flag, 1, 1, f);
|
||||||
|
block.length -= 2; // substract flag and checksum
|
||||||
|
block.data = (uint8_t *)malloc(block.length);
|
||||||
|
fread(block.data, block.length, 1, f);
|
||||||
|
fread(&block.checksum, 1, 1, f);
|
||||||
|
blocks.push_back(block);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
playing = false;
|
||||||
|
rewind();
|
||||||
|
}
|
||||||
|
|
||||||
|
void play()
|
||||||
|
{
|
||||||
|
if (!loaded) return;
|
||||||
|
playing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop()
|
||||||
|
{
|
||||||
|
playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rewind()
|
||||||
|
{
|
||||||
|
if (!loaded) return;
|
||||||
|
current_block = block_pos = current_bit = 0;
|
||||||
|
current_section = PULSE_PILOT;
|
||||||
|
current_pulse = pulse_pos = 0;
|
||||||
|
pulse_level = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(const uint8_t dt)
|
||||||
|
{
|
||||||
|
if (!playing) return;
|
||||||
|
|
||||||
|
pulse_pos += dt;
|
||||||
|
|
||||||
|
if (current_section == PULSE_PILOT)
|
||||||
|
{
|
||||||
|
const uint16_t pulse_count = blocks[current_block].flag<128 ? PULSE_COUNT_PILOT_HEADER : PULSE_COUNT_PILOT_DATA;
|
||||||
|
if (pulse_pos >= PULSE_LEN_PILOT )
|
||||||
|
{
|
||||||
|
pulse_pos -= PULSE_LEN_PILOT;
|
||||||
|
current_pulse++;
|
||||||
|
pulse_level = pulse_level?0:1;
|
||||||
|
if (current_pulse >= pulse_count)
|
||||||
|
{
|
||||||
|
current_pulse = 0;
|
||||||
|
pulse_level = 1;
|
||||||
|
current_section = PULSE_SYNC1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_section == PULSE_SYNC1)
|
||||||
|
{
|
||||||
|
if (pulse_pos >= PULSE_LEN_SYNC1)
|
||||||
|
{
|
||||||
|
pulse_pos -= PULSE_LEN_SYNC1;
|
||||||
|
pulse_level = 0;
|
||||||
|
current_section = PULSE_SYNC2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_section == PULSE_SYNC2)
|
||||||
|
{
|
||||||
|
if (pulse_pos >= PULSE_LEN_SYNC2)
|
||||||
|
{
|
||||||
|
pulse_pos -= PULSE_LEN_SYNC2;
|
||||||
|
pulse_level = 1;
|
||||||
|
current_section = PULSE_DATA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_section == PULSE_DATA)
|
||||||
|
{
|
||||||
|
const uint8_t datum = blocks[current_block].data[block_pos];
|
||||||
|
const uint16_t pulse_len = (datum & (0x80>>current_bit)) == 0 ? PULSE_LEN_ZERO : PULSE_LEN_ONE;
|
||||||
|
if (pulse_pos >= pulse_len)
|
||||||
|
{
|
||||||
|
pulse_pos -= pulse_len;
|
||||||
|
pulse_level--;
|
||||||
|
if (pulse_level>=2)
|
||||||
|
{
|
||||||
|
pulse_level = 1;
|
||||||
|
current_bit++;
|
||||||
|
if (current_bit>=8)
|
||||||
|
{
|
||||||
|
current_bit = 0;
|
||||||
|
block_pos++;
|
||||||
|
if (block_pos>=blocks[current_block].length)
|
||||||
|
{
|
||||||
|
block_pos = 0;
|
||||||
|
current_section = PULSE_CHECKSUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_section == PULSE_CHECKSUM)
|
||||||
|
{
|
||||||
|
const uint8_t datum = blocks[current_block].checksum;
|
||||||
|
const uint16_t pulse_len = (datum & (0x80>>current_bit)) == 0 ? PULSE_LEN_ZERO : PULSE_LEN_ONE;
|
||||||
|
if (pulse_pos >= pulse_len)
|
||||||
|
{
|
||||||
|
pulse_pos -= pulse_len;
|
||||||
|
pulse_level--;
|
||||||
|
if (pulse_level>=2)
|
||||||
|
{
|
||||||
|
pulse_level = 1;
|
||||||
|
current_bit++;
|
||||||
|
if (current_bit>=8)
|
||||||
|
{
|
||||||
|
current_bit = 0;
|
||||||
|
current_section = PULSE_SYNC3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_section == PULSE_SYNC3)
|
||||||
|
{
|
||||||
|
if (pulse_pos >= PULSE_LEN_SYNC3)
|
||||||
|
{
|
||||||
|
pulse_pos -= PULSE_LEN_SYNC3;
|
||||||
|
pulse_level = 0;
|
||||||
|
current_section = PULSE_WAIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_section == PULSE_WAIT)
|
||||||
|
{
|
||||||
|
pulse_level = 0;
|
||||||
|
if (pulse_pos >= 3500000)
|
||||||
|
{
|
||||||
|
current_section = PULSE_PILOT;
|
||||||
|
pulse_level = 1;
|
||||||
|
current_block++;
|
||||||
|
if (current_block>=blocks.size())
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
rewind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zx_ula::set_ear(pulse_level);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
zx_tape.h
Normal file
11
zx_tape.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace zx_tape
|
||||||
|
{
|
||||||
|
void load(const char* filename);
|
||||||
|
void play();
|
||||||
|
void stop();
|
||||||
|
void rewind();
|
||||||
|
void update(const uint8_t dt);
|
||||||
|
}
|
||||||
12
zx_ula.cpp
12
zx_ula.cpp
@@ -11,7 +11,7 @@ namespace zx_ula
|
|||||||
{
|
{
|
||||||
const uint8_t *keys = SDL_GetKeyboardState(NULL);
|
const uint8_t *keys = SDL_GetKeyboardState(NULL);
|
||||||
const uint8_t h_addr = (port>>8);
|
const uint8_t h_addr = (port>>8);
|
||||||
uint8_t result = 0xff;
|
uint8_t result = ear ? 0xbf : 0xff;
|
||||||
if (!(h_addr & ~0xfe))
|
if (!(h_addr & ~0xfe))
|
||||||
{
|
{
|
||||||
result &= ~(
|
result &= ~(
|
||||||
@@ -97,6 +97,7 @@ namespace zx_ula
|
|||||||
|
|
||||||
uint8_t get_border_color() { return border_color; }
|
uint8_t get_border_color() { return border_color; }
|
||||||
uint8_t get_ear() { return ear; }
|
uint8_t get_ear() { return ear; }
|
||||||
|
void set_ear(const uint8_t val) { ear = val; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ namespace zx_ula
|
|||||||
SDL_AudioDeviceID sdlAudioDevice;
|
SDL_AudioDeviceID sdlAudioDevice;
|
||||||
uint8_t sound_buffer[1024];
|
uint8_t sound_buffer[1024];
|
||||||
uint16_t sound_pos;
|
uint16_t sound_pos;
|
||||||
|
uint16_t t_sound=0;
|
||||||
|
|
||||||
void audioCallback(void * userdata, uint8_t * stream, int len)
|
void audioCallback(void * userdata, uint8_t * stream, int len)
|
||||||
{
|
{
|
||||||
@@ -128,8 +130,12 @@ namespace zx_ula
|
|||||||
SDL_PauseAudioDevice(sdlAudioDevice, 1);
|
SDL_PauseAudioDevice(sdlAudioDevice, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void sound_update()
|
void sound_update(const uint8_t dt)
|
||||||
{
|
{
|
||||||
sound_buffer[sound_pos++] = ear*128;
|
t_sound += dt;
|
||||||
|
if (t_sound>=400) {
|
||||||
|
t_sound-=400;
|
||||||
|
sound_buffer[sound_pos++] = ear*128;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
3
zx_ula.h
3
zx_ula.h
@@ -9,9 +9,10 @@ namespace zx_ula
|
|||||||
|
|
||||||
uint8_t get_border_color();
|
uint8_t get_border_color();
|
||||||
uint8_t get_ear();
|
uint8_t get_ear();
|
||||||
|
void set_ear(const uint8_t val);
|
||||||
|
|
||||||
void sound_init();
|
void sound_init();
|
||||||
void sound_enable();
|
void sound_enable();
|
||||||
void sound_disable();
|
void sound_disable();
|
||||||
void sound_update();
|
void sound_update(const uint8_t dt);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user