mirror of
				https://github.com/TomHarte/CLK.git
				synced 2025-10-30 14:16:04 +00:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			2018-03-03
			...
			2018-03-07
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a942e1319b | ||
|  | 9abc020818 | ||
|  | 2dade8d353 | ||
|  | dfcc502a88 | ||
|  | 1c6faaae88 | ||
|  | 35c8a0dd8c | ||
|  | 38feedaf6a | 
| @@ -252,6 +252,7 @@ class ConcreteMachine: | |||||||
| 								default: *cycle.value = 0xff; break; | 								default: *cycle.value = 0xff; break; | ||||||
| 								case 0x52: | 								case 0x52: | ||||||
| 									// Read AY data. | 									// Read AY data. | ||||||
|  | 									update_audio(); | ||||||
| 									ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1)); | 									ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1)); | ||||||
| 									*cycle.value = ay_.get_data_output(); | 									*cycle.value = ay_.get_data_output(); | ||||||
| 									ay_.set_control_lines(GI::AY38910::ControlLines(0)); | 									ay_.set_control_lines(GI::AY38910::ControlLines(0)); | ||||||
| @@ -289,12 +290,14 @@ class ConcreteMachine: | |||||||
| 								break; | 								break; | ||||||
| 								case 0x50: | 								case 0x50: | ||||||
| 									// Set AY address. | 									// Set AY address. | ||||||
|  | 									update_audio(); | ||||||
| 									ay_.set_control_lines(GI::AY38910::BC1); | 									ay_.set_control_lines(GI::AY38910::BC1); | ||||||
| 									ay_.set_data_input(*cycle.value); | 									ay_.set_data_input(*cycle.value); | ||||||
| 									ay_.set_control_lines(GI::AY38910::ControlLines(0)); | 									ay_.set_control_lines(GI::AY38910::ControlLines(0)); | ||||||
| 								break; | 								break; | ||||||
| 								case 0x51: | 								case 0x51: | ||||||
| 									// Set AY data. | 									// Set AY data. | ||||||
|  | 									update_audio(); | ||||||
| 									ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR)); | 									ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR)); | ||||||
| 									ay_.set_data_input(*cycle.value); | 									ay_.set_data_input(*cycle.value); | ||||||
| 									ay_.set_control_lines(GI::AY38910::ControlLines(0)); | 									ay_.set_control_lines(GI::AY38910::ControlLines(0)); | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
|  |  | ||||||
| #include "ZX8081.hpp" | #include "ZX8081.hpp" | ||||||
|  |  | ||||||
|  | #include "../../Components/AY38910/AY38910.hpp" | ||||||
| #include "../../Processors/Z80/Z80.hpp" | #include "../../Processors/Z80/Z80.hpp" | ||||||
| #include "../../Storage/Tape/Tape.hpp" | #include "../../Storage/Tape/Tape.hpp" | ||||||
| #include "../../Storage/Tape/Parsers/ZX8081.hpp" | #include "../../Storage/Tape/Parsers/ZX8081.hpp" | ||||||
| @@ -18,6 +19,8 @@ | |||||||
| #include "../Utility/MemoryFuzzer.hpp" | #include "../Utility/MemoryFuzzer.hpp" | ||||||
| #include "../Utility/Typer.hpp" | #include "../Utility/Typer.hpp" | ||||||
|  |  | ||||||
|  | #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" | ||||||
|  |  | ||||||
| #include "Keyboard.hpp" | #include "Keyboard.hpp" | ||||||
| #include "Video.hpp" | #include "Video.hpp" | ||||||
|  |  | ||||||
| @@ -31,6 +34,11 @@ namespace { | |||||||
| 	const unsigned int ZX8081ClockRate = 3250000; | 	const unsigned int ZX8081ClockRate = 3250000; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // TODO: | ||||||
|  | //	Quiksilva sound support: | ||||||
|  | //  7FFFh.W   PSG index | ||||||
|  | //  7FFEh.R/W PSG data | ||||||
|  |  | ||||||
| namespace ZX8081 { | namespace ZX8081 { | ||||||
|  |  | ||||||
| enum ROMType: uint8_t { | enum ROMType: uint8_t { | ||||||
| @@ -50,14 +58,18 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 	public: | 	public: | ||||||
| 		ConcreteMachine() : | 		ConcreteMachine() : | ||||||
| 			z80_(*this), | 			z80_(*this), | ||||||
| 			tape_player_(ZX8081ClockRate) { | 			tape_player_(ZX8081ClockRate), | ||||||
|  | 			ay_(audio_queue_), | ||||||
|  | 			speaker_(ay_) { | ||||||
| 			set_clock_rate(ZX8081ClockRate); | 			set_clock_rate(ZX8081ClockRate); | ||||||
|  | 			speaker_.set_input_rate(static_cast<float>(ZX8081ClockRate) / 2.0f); | ||||||
| 			clear_all_keys(); | 			clear_all_keys(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { | 		forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { | ||||||
| 			HalfCycles previous_counter = horizontal_counter_; | 			const HalfCycles previous_counter = horizontal_counter_; | ||||||
| 			horizontal_counter_ += cycle.length; | 			horizontal_counter_ += cycle.length; | ||||||
|  | 			time_since_ay_update_ += cycle.length; | ||||||
|  |  | ||||||
| 			if(previous_counter < vsync_start_ && horizontal_counter_ >= vsync_start_) { | 			if(previous_counter < vsync_start_ && horizontal_counter_ >= vsync_start_) { | ||||||
| 				video_->run_for(vsync_start_ - previous_counter); | 				video_->run_for(vsync_start_ - previous_counter); | ||||||
| @@ -94,7 +106,7 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 				return Cycles(0); | 				return Cycles(0); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			uint16_t address = cycle.address ? *cycle.address : 0; | 			const uint16_t address = cycle.address ? *cycle.address : 0; | ||||||
| 			bool is_opcode_read = false; | 			bool is_opcode_read = false; | ||||||
| 			switch(cycle.operation) { | 			switch(cycle.operation) { | ||||||
| 				case CPU::Z80::PartialMachineCycle::Output: | 				case CPU::Z80::PartialMachineCycle::Output: | ||||||
| @@ -106,6 +118,15 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 						if(vsync_) line_counter_ = 0; | 						if(vsync_) line_counter_ = 0; | ||||||
| 						set_vsync(false); | 						set_vsync(false); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
|  | 					// The below emulates the ZonX AY expansion device. | ||||||
|  | 					if(is_zx81) { | ||||||
|  | 						if((address&0xef) == 0xcf) { | ||||||
|  | 							ay_set_register(*cycle.value); | ||||||
|  | 						} else if((address&0xef) == 0x0f) { | ||||||
|  | 							ay_set_data(*cycle.value); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
| 				break; | 				break; | ||||||
|  |  | ||||||
| 				case CPU::Z80::PartialMachineCycle::Input: { | 				case CPU::Z80::PartialMachineCycle::Input: { | ||||||
| @@ -121,6 +142,13 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
|  |  | ||||||
| 						value &= ~(tape_player_.get_input() ? 0x00 : 0x80); | 						value &= ~(tape_player_.get_input() ? 0x00 : 0x80); | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
|  | 					// The below emulates the ZonX AY expansion device. | ||||||
|  | 					if(is_zx81) { | ||||||
|  | 						if((address&0xef) == 0x0f) { | ||||||
|  | 							value &= ay_read_data(); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
| 					*cycle.value = value; | 					*cycle.value = value; | ||||||
| 				} break; | 				} break; | ||||||
|  |  | ||||||
| @@ -144,7 +172,7 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 					} | 					} | ||||||
| 					if(has_latched_video_byte_) { | 					if(has_latched_video_byte_) { | ||||||
| 						std::size_t char_address = static_cast<std::size_t>((address & 0xfe00) | ((latched_video_byte_ & 0x3f) << 3) | line_counter_); | 						std::size_t char_address = static_cast<std::size_t>((address & 0xfe00) | ((latched_video_byte_ & 0x3f) << 3) | line_counter_); | ||||||
| 						uint8_t mask = (latched_video_byte_ & 0x80) ? 0x00 : 0xff; | 						const uint8_t mask = (latched_video_byte_ & 0x80) ? 0x00 : 0xff; | ||||||
| 						if(char_address < ram_base_) { | 						if(char_address < ram_base_) { | ||||||
| 							latched_video_byte_ = rom_[char_address & rom_mask_] ^ mask; | 							latched_video_byte_ = rom_[char_address & rom_mask_] ^ mask; | ||||||
| 						} else { | 						} else { | ||||||
| @@ -159,10 +187,10 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 				case CPU::Z80::PartialMachineCycle::ReadOpcode: | 				case CPU::Z80::PartialMachineCycle::ReadOpcode: | ||||||
| 					// Check for use of the fast tape hack. | 					// Check for use of the fast tape hack. | ||||||
| 					if(use_fast_tape_hack_ && address == tape_trap_address_) { | 					if(use_fast_tape_hack_ && address == tape_trap_address_) { | ||||||
| 						uint64_t prior_offset = tape_player_.get_tape()->get_offset(); | 						const uint64_t prior_offset = tape_player_.get_tape()->get_offset(); | ||||||
| 						int next_byte = parser_.get_next_byte(tape_player_.get_tape()); | 						const int next_byte = parser_.get_next_byte(tape_player_.get_tape()); | ||||||
| 						if(next_byte != -1) { | 						if(next_byte != -1) { | ||||||
| 							uint16_t hl = z80_.get_value_of_register(CPU::Z80::Register::HL); | 							const uint16_t hl = z80_.get_value_of_register(CPU::Z80::Register::HL); | ||||||
| 							ram_[hl & ram_mask_] = static_cast<uint8_t>(next_byte); | 							ram_[hl & ram_mask_] = static_cast<uint8_t>(next_byte); | ||||||
| 							*cycle.value = 0x00; | 							*cycle.value = 0x00; | ||||||
| 							z80_.set_value_of_register(CPU::Z80::Register::ProgramCounter, tape_return_address_ - 1); | 							z80_.set_value_of_register(CPU::Z80::Register::ProgramCounter, tape_return_address_ - 1); | ||||||
| @@ -187,7 +215,7 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 					if(address < ram_base_) { | 					if(address < ram_base_) { | ||||||
| 						*cycle.value = rom_[address & rom_mask_]; | 						*cycle.value = rom_[address & rom_mask_]; | ||||||
| 					} else { | 					} else { | ||||||
| 						uint8_t value = ram_[address & ram_mask_]; | 						const uint8_t value = ram_[address & ram_mask_]; | ||||||
|  |  | ||||||
| 						// If this is an M1 cycle reading from above the 32kb mark and HALT is not | 						// If this is an M1 cycle reading from above the 32kb mark and HALT is not | ||||||
| 						// currently active, latch for video output and return a NOP. Otherwise, | 						// currently active, latch for video output and return a NOP. Otherwise, | ||||||
| @@ -210,12 +238,15 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if(typer_) typer_->run_for(cycle.length); | 			if(typer_) typer_->run_for(cycle.length); | ||||||
|  |  | ||||||
| 			return HalfCycles(0); | 			return HalfCycles(0); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		forceinline void flush() { | 		forceinline void flush() { | ||||||
| 			video_->flush(); | 			video_->flush(); | ||||||
|  | 			if(is_zx81) { | ||||||
|  | 				update_audio(); | ||||||
|  | 				audio_queue_.perform(); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		void setup_output(float aspect_ratio) override final { | 		void setup_output(float aspect_ratio) override final { | ||||||
| @@ -231,7 +262,7 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		Outputs::Speaker::Speaker *get_speaker() override final { | 		Outputs::Speaker::Speaker *get_speaker() override final { | ||||||
| 			return nullptr; | 			return is_zx81 ? &speaker_ : nullptr; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		void run_for(const Cycles cycles) override final { | 		void run_for(const Cycles cycles) override final { | ||||||
| @@ -301,7 +332,7 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
|  |  | ||||||
| 		// Obtains the system ROMs. | 		// Obtains the system ROMs. | ||||||
| 		bool set_rom_fetcher(const std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::string &machine, const std::vector<std::string> &names)> &roms_with_names) override { | 		bool set_rom_fetcher(const std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::string &machine, const std::vector<std::string> &names)> &roms_with_names) override { | ||||||
| 			auto roms = roms_with_names( | 			const auto roms = roms_with_names( | ||||||
| 				"ZX8081", | 				"ZX8081", | ||||||
| 				{ | 				{ | ||||||
| 					"zx80.rom",	"zx81.rom", | 					"zx80.rom",	"zx81.rom", | ||||||
| @@ -320,8 +351,8 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// MARK: - Keyboard | 		// MARK: - Keyboard | ||||||
| 		void set_key_state(uint16_t key, bool isPressed) override final { | 		void set_key_state(uint16_t key, bool is_pressed) override final { | ||||||
| 			if(isPressed) | 			if(is_pressed) | ||||||
| 				key_states_[key >> 8] &= static_cast<uint8_t>(~key); | 				key_states_[key >> 8] &= static_cast<uint8_t>(~key); | ||||||
| 			else | 			else | ||||||
| 				key_states_[key >> 8] |= static_cast<uint8_t>(key); | 				key_states_[key >> 8] |= static_cast<uint8_t>(key); | ||||||
| @@ -443,6 +474,34 @@ template<bool is_zx81> class ConcreteMachine: | |||||||
| 		inline void update_sync() { | 		inline void update_sync() { | ||||||
| 			video_->set_sync(vsync_ || hsync_); | 			video_->set_sync(vsync_ || hsync_); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// MARK: - Audio | ||||||
|  | 		Concurrency::DeferringAsyncTaskQueue audio_queue_; | ||||||
|  | 		GI::AY38910::AY38910 ay_; | ||||||
|  | 		Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_; | ||||||
|  | 		HalfCycles time_since_ay_update_; | ||||||
|  | 		inline void ay_set_register(uint8_t value) { | ||||||
|  | 			update_audio(); | ||||||
|  | 			ay_.set_control_lines(GI::AY38910::BC1); | ||||||
|  | 			ay_.set_data_input(value); | ||||||
|  | 			ay_.set_control_lines(GI::AY38910::ControlLines(0)); | ||||||
|  | 		} | ||||||
|  | 		inline void ay_set_data(uint8_t value) { | ||||||
|  | 			update_audio(); | ||||||
|  | 			ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR)); | ||||||
|  | 			ay_.set_data_input(value); | ||||||
|  | 			ay_.set_control_lines(GI::AY38910::ControlLines(0)); | ||||||
|  | 		} | ||||||
|  | 		inline uint8_t ay_read_data() { | ||||||
|  | 			update_audio(); | ||||||
|  | 			ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1)); | ||||||
|  | 			const uint8_t value = ay_.get_data_output(); | ||||||
|  | 			ay_.set_control_lines(GI::AY38910::ControlLines(0)); | ||||||
|  | 			return value; | ||||||
|  | 		} | ||||||
|  | 		inline void update_audio() { | ||||||
|  | 			speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2))); | ||||||
|  | 		} | ||||||
| }; | }; | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -460,12 +460,35 @@ int main(int argc, char *argv[]) { | |||||||
|  |  | ||||||
| 				// deliberate fallthrough... | 				// deliberate fallthrough... | ||||||
| 				case SDL_KEYUP: { | 				case SDL_KEYUP: { | ||||||
| 					KeyboardMachine::Machine *keyboard_machine = machine->keyboard_machine(); | 					const bool is_pressed = event.type == SDL_KEYDOWN; | ||||||
| 					if(!keyboard_machine) break; |  | ||||||
|  |  | ||||||
|  | 					KeyboardMachine::Machine *const keyboard_machine = machine->keyboard_machine(); | ||||||
|  | 					if(keyboard_machine) { | ||||||
| 						Inputs::Keyboard::Key key = Inputs::Keyboard::Key::Space; | 						Inputs::Keyboard::Key key = Inputs::Keyboard::Key::Space; | ||||||
| 						if(!KeyboardKeyForSDLScancode(event.key.keysym.scancode, key)) break; | 						if(!KeyboardKeyForSDLScancode(event.key.keysym.scancode, key)) break; | ||||||
| 					keyboard_machine->get_keyboard().set_key_pressed(key, event.type == SDL_KEYDOWN); | 						keyboard_machine->get_keyboard().set_key_pressed(key, is_pressed); | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  |  | ||||||
|  | 					JoystickMachine::Machine *const joystick_machine = machine->joystick_machine(); | ||||||
|  | 					if(joystick_machine) { | ||||||
|  | 						std::vector<std::unique_ptr<Inputs::Joystick>> &joysticks = joystick_machine->get_joysticks(); | ||||||
|  | 						if(!joysticks.empty()) { | ||||||
|  | 							switch(event.key.keysym.scancode) { | ||||||
|  | 								case SDL_SCANCODE_LEFT:		joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Left, is_pressed);	break; | ||||||
|  | 								case SDL_SCANCODE_RIGHT:	joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Right, is_pressed);	break; | ||||||
|  | 								case SDL_SCANCODE_UP:		joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Up, is_pressed);	break; | ||||||
|  | 								case SDL_SCANCODE_DOWN:		joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Down, is_pressed);	break; | ||||||
|  | 								case SDL_SCANCODE_SPACE:	joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput::Fire, is_pressed);	break; | ||||||
|  | 								case SDL_SCANCODE_A:		joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput(Inputs::Joystick::DigitalInput::Fire, 0), is_pressed);	break; | ||||||
|  | 								case SDL_SCANCODE_S:		joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput(Inputs::Joystick::DigitalInput::Fire, 1), is_pressed);	break; | ||||||
|  | 								default: { | ||||||
|  | 									const char *key_name = SDL_GetKeyName(event.key.keysym.sym); | ||||||
|  | 									joysticks[0]->set_digital_input(Inputs::Joystick::DigitalInput(key_name[0]), is_pressed); | ||||||
|  | 								} break; | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
| 				} break; | 				} break; | ||||||
|  |  | ||||||
| 				default: break; | 				default: break; | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ It currently contains emulations of the: | |||||||
| * Acorn Electron; | * Acorn Electron; | ||||||
| * Amstrad CPC; | * Amstrad CPC; | ||||||
| * Atari 2600; | * Atari 2600; | ||||||
|  | * ColecoVision; | ||||||
| * Commodore Vic-20 (and Commodore 1540/1); | * Commodore Vic-20 (and Commodore 1540/1); | ||||||
| * MSX 1; | * MSX 1; | ||||||
| * Oric 1/Atmos; and | * Oric 1/Atmos; and | ||||||
| @@ -43,6 +44,7 @@ If your machine has a 4k monitor and a 96Khz audio output? Then you'll get a 4k | |||||||
| ||| | ||| | ||||||
| ||| | ||| | ||||||
| ||| | ||| | ||||||
|  | ||| | ||||||
|  |  | ||||||
| | 1:1 Pixel Copying | Correct Aspect Ratio, Filtered | | | 1:1 Pixel Copying | Correct Aspect Ratio, Filtered | | ||||||
| |---|---| | |---|---| | ||||||
| @@ -65,7 +67,7 @@ Cycle-accurate emulation for the supported target machines is fairly trite; this | |||||||
|  |  | ||||||
| Self-ratings: | Self-ratings: | ||||||
| * the Electron, Oric and Vic-20 are pretty much perfect; | * the Electron, Oric and Vic-20 are pretty much perfect; | ||||||
| * the ZX80, ZX81 and MSX 1 are very strong; | * the ZX80, ZX81, ColecoVision and MSX 1 are very strong; | ||||||
| * the Amstrad CPC has known accuracy deficiencies in its 8272 and 6845; | * the Amstrad CPC has known accuracy deficiencies in its 8272 and 6845; | ||||||
| * the Atari 2600 has some known accuracy deficiencies in its TIA; | * the Atari 2600 has some known accuracy deficiencies in its TIA; | ||||||
| * the C-1540(/1) is locked in reading mode and doesn't yet support writing. | * the C-1540(/1) is locked in reading mode and doesn't yet support writing. | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								READMEImages/CompositePresentsDonkeyKong.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								READMEImages/CompositePresentsDonkeyKong.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								READMEImages/NaivePresentsDonkeyKong.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								READMEImages/NaivePresentsDonkeyKong.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 311 B | 
		Reference in New Issue
	
	Block a user