mirror of
https://github.com/a2x/cs2-dumper.git
synced 2025-10-07 16:30:01 +08:00
Print field types
This commit is contained in:
@@ -22,14 +22,25 @@ impl FileBuilder for CppBuilder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_variable(&mut self, output: &mut dyn Write, name: &str, value: usize) -> Result<()> {
|
||||
write!(
|
||||
output,
|
||||
" constexpr std::ptrdiff_t {} = {:#X};\n",
|
||||
name, value
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
match comment {
|
||||
Some(comment) => write!(
|
||||
output,
|
||||
" constexpr std::ptrdiff_t {} = {:#X}; // {}\n",
|
||||
name, value, comment
|
||||
),
|
||||
None => write!(
|
||||
output,
|
||||
" constexpr std::ptrdiff_t {} = {:#X};\n",
|
||||
name, value
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
|
@@ -19,10 +19,21 @@ impl FileBuilder for CSharpBuilder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_variable(&mut self, output: &mut dyn Write, name: &str, value: usize) -> Result<()> {
|
||||
write!(output, " public const nint {} = {:#X};\n", name, value)?;
|
||||
|
||||
Ok(())
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
match comment {
|
||||
Some(comment) => write!(
|
||||
output,
|
||||
" public const nint {} = {:#X}; // {}\n",
|
||||
name, value, comment
|
||||
),
|
||||
None => write!(output, " public const nint {} = {:#X};\n", name, value),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
|
@@ -2,8 +2,18 @@ use std::io::{Result, Write};
|
||||
|
||||
pub trait FileBuilder {
|
||||
fn extension(&mut self) -> &str;
|
||||
|
||||
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()>;
|
||||
|
||||
fn write_namespace(&mut self, output: &mut dyn Write, name: &str) -> Result<()>;
|
||||
fn write_variable(&mut self, output: &mut dyn Write, name: &str, value: usize) -> Result<()>;
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()>;
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()>;
|
||||
}
|
||||
|
@@ -25,7 +25,13 @@ impl FileBuilder for JsonFileBuilder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_variable(&mut self, _output: &mut dyn Write, name: &str, value: usize) -> Result<()> {
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
_output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
_comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
if let Some(json_as_map) = self.json.as_object_mut() {
|
||||
let namespace_entry = json_as_map
|
||||
.entry(self.namespace.clone())
|
||||
|
@@ -47,15 +47,25 @@ impl FileBuilder for FileBuilderEnum {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_variable(&mut self, output: &mut dyn Write, name: &str, value: usize) -> Result<()> {
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
FileBuilderEnum::CppBuilder(builder) => builder.write_variable(output, name, value),
|
||||
FileBuilderEnum::CSharpBuilder(builder) => builder.write_variable(output, name, value),
|
||||
FileBuilderEnum::CppBuilder(builder) => {
|
||||
builder.write_variable(output, name, value, comment)
|
||||
}
|
||||
FileBuilderEnum::CSharpBuilder(builder) => {
|
||||
builder.write_variable(output, name, value, comment)
|
||||
}
|
||||
FileBuilderEnum::JsonFileBuilder(builder) => {
|
||||
builder.write_variable(output, name, value)
|
||||
builder.write_variable(output, name, value, comment)
|
||||
}
|
||||
FileBuilderEnum::RustFileBuilder(builder) => {
|
||||
builder.write_variable(output, name, value)
|
||||
builder.write_variable(output, name, value, comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -24,10 +24,21 @@ impl FileBuilder for RustFileBuilder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_variable(&mut self, output: &mut dyn Write, name: &str, value: usize) -> Result<()> {
|
||||
write!(output, " pub const {}: usize = {:#X};\n", name, value)?;
|
||||
|
||||
Ok(())
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
match comment {
|
||||
Some(comment) => write!(
|
||||
output,
|
||||
" pub const {}: usize = {:#X}; // {}\n",
|
||||
name, value, comment
|
||||
),
|
||||
None => write!(output, " pub const {}: usize = {:#X};\n", name, value),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use crate::builder::FileBuilderEnum;
|
||||
use crate::dumpers::Entry;
|
||||
use crate::error::Result;
|
||||
use crate::remote::Process;
|
||||
|
||||
@@ -24,14 +25,14 @@ pub fn dump_interfaces(builders: &mut Vec<FileBuilderEnum>, process: &Process) -
|
||||
while interface_registry_ptr != 0 {
|
||||
let interface_ptr = process.read_memory::<usize>(interface_registry_ptr)?;
|
||||
|
||||
let interface_version_name_ptr =
|
||||
let interface_version_ptr =
|
||||
process.read_memory::<usize>(interface_registry_ptr + 0x8)?;
|
||||
|
||||
let interface_version_name = process.read_string(interface_version_name_ptr, 64)?;
|
||||
let interface_version = process.read_string(interface_version_ptr)?;
|
||||
|
||||
log::info!(
|
||||
" -> Found '{}' @ {:#X} ({} + {:#X})",
|
||||
interface_version_name,
|
||||
log::debug!(
|
||||
" └─ '{}' @ {:#X} ({} + {:#X})",
|
||||
interface_version,
|
||||
interface_ptr,
|
||||
module_name,
|
||||
interface_ptr - module.address()
|
||||
@@ -40,7 +41,11 @@ pub fn dump_interfaces(builders: &mut Vec<FileBuilderEnum>, process: &Process) -
|
||||
entries
|
||||
.entry(module_name.replace(".", "_"))
|
||||
.or_default()
|
||||
.push((interface_version_name, interface_ptr - module.address()));
|
||||
.push(Entry {
|
||||
name: interface_version.clone(),
|
||||
value: interface_ptr - module.address(),
|
||||
comment: None,
|
||||
});
|
||||
|
||||
interface_registry_ptr =
|
||||
process.read_memory::<usize>(interface_registry_ptr + 0x10)?;
|
||||
@@ -48,7 +53,7 @@ pub fn dump_interfaces(builders: &mut Vec<FileBuilderEnum>, process: &Process) -
|
||||
}
|
||||
}
|
||||
|
||||
for builder in builders.iter_mut() {
|
||||
for builder in builders {
|
||||
generate_file(builder, "interfaces", &entries)?;
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,13 @@ pub mod interfaces;
|
||||
pub mod offsets;
|
||||
pub mod schemas;
|
||||
|
||||
pub type Entries = BTreeMap<String, Vec<(String, usize)>>;
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
pub value: usize,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
pub type Entries = BTreeMap<String, Vec<Entry>>;
|
||||
|
||||
pub fn generate_file(
|
||||
builder: &mut FileBuilderEnum,
|
||||
@@ -27,11 +33,16 @@ pub fn generate_file(
|
||||
|
||||
let len = entries.len();
|
||||
|
||||
for (i, entry) in entries.iter().enumerate() {
|
||||
builder.write_namespace(&mut file, entry.0)?;
|
||||
for (i, pair) in entries.iter().enumerate() {
|
||||
builder.write_namespace(&mut file, pair.0)?;
|
||||
|
||||
for (name, value) in entry.1 {
|
||||
builder.write_variable(&mut file, name, *value)?;
|
||||
for entry in pair.1 {
|
||||
builder.write_variable(
|
||||
&mut file,
|
||||
&entry.name,
|
||||
entry.value,
|
||||
entry.comment.as_deref(),
|
||||
)?;
|
||||
}
|
||||
|
||||
builder.write_closure(&mut file, i == len - 1)?;
|
||||
|
@@ -2,6 +2,7 @@ use std::fs::File;
|
||||
|
||||
use crate::builder::FileBuilderEnum;
|
||||
use crate::config::{Config, Operation};
|
||||
use crate::dumpers::Entry;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::remote::Process;
|
||||
|
||||
@@ -48,15 +49,13 @@ pub fn dump_offsets(builders: &mut Vec<FileBuilderEnum>, process: &Process) -> R
|
||||
}
|
||||
}
|
||||
|
||||
let sanitized_module_name = signature.module.replace(".", "_");
|
||||
|
||||
let (name, value) = if let Some(offset) = offset {
|
||||
log::info!(" -> Found '{}' @ {:#X}", signature.name, offset);
|
||||
log::debug!(" └─ '{}' @ {:#X}", signature.name, offset);
|
||||
|
||||
(signature.name, offset as usize)
|
||||
} else {
|
||||
log::info!(
|
||||
" -> Found '{}' @ {:#X} ({} + {:#X})",
|
||||
log::debug!(
|
||||
" └─ '{}' @ {:#X} ({} + {:#X})",
|
||||
signature.name,
|
||||
address,
|
||||
signature.module,
|
||||
@@ -67,12 +66,16 @@ pub fn dump_offsets(builders: &mut Vec<FileBuilderEnum>, process: &Process) -> R
|
||||
};
|
||||
|
||||
entries
|
||||
.entry(sanitized_module_name)
|
||||
.entry(signature.module.replace(".", "_"))
|
||||
.or_default()
|
||||
.push((name, value));
|
||||
.push(Entry {
|
||||
name,
|
||||
value,
|
||||
comment: None,
|
||||
});
|
||||
}
|
||||
|
||||
for builder in builders.iter_mut() {
|
||||
for builder in builders {
|
||||
generate_file(builder, "offsets", &entries)?;
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use crate::builder::FileBuilderEnum;
|
||||
use crate::dumpers::Entry;
|
||||
use crate::error::Result;
|
||||
use crate::remote::Process;
|
||||
use crate::sdk::SchemaSystem;
|
||||
@@ -9,29 +10,42 @@ pub fn dump_schemas(builders: &mut Vec<FileBuilderEnum>, process: &Process) -> R
|
||||
let schema_system = SchemaSystem::new(&process)?;
|
||||
|
||||
for type_scope in schema_system.type_scopes()? {
|
||||
log::info!("Generating files for {}...", type_scope.module_name()?);
|
||||
let module_name = type_scope.module_name()?;
|
||||
|
||||
log::info!("Generating files for {}...", module_name);
|
||||
|
||||
let mut entries = Entries::new();
|
||||
|
||||
for class in type_scope.classes()? {
|
||||
log::info!(" [{}]", class.name());
|
||||
log::debug!(" {}", class.name());
|
||||
|
||||
for field in class.fields()? {
|
||||
log::info!(" [{}] = {:#X}", field.name()?, field.offset()?);
|
||||
let field_name = field.name()?;
|
||||
let field_offset = field.offset()?;
|
||||
let type_name = field.r#type()?.name()?;
|
||||
|
||||
log::debug!(
|
||||
" └─ {} = {:#X} // {}",
|
||||
field_name,
|
||||
field_offset,
|
||||
type_name
|
||||
);
|
||||
|
||||
entries
|
||||
.entry(class.name().replace("::", "_"))
|
||||
.or_default()
|
||||
.push((field.name()?, field.offset()? as usize));
|
||||
.push(Entry {
|
||||
name: field_name,
|
||||
value: field_offset as usize,
|
||||
comment: Some(type_name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if entries.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for builder in builders.iter_mut() {
|
||||
generate_file(builder, &type_scope.module_name()?, &entries)?;
|
||||
if !entries.is_empty() {
|
||||
for builder in builders.iter_mut() {
|
||||
generate_file(builder, &module_name, &entries)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
68
src/error.rs
68
src/error.rs
@@ -1,56 +1,40 @@
|
||||
use std::io;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
use serde_json::Error as SerdeError;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use windows::core::Error as WindowsError;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Buffer size mismatch: expected {0}, got {1}")]
|
||||
BufferSizeMismatch(usize, usize),
|
||||
|
||||
#[error("Invalid magic: {0:#X}")]
|
||||
InvalidMagic(u32),
|
||||
IOError(io::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
IOError(#[from] io::Error),
|
||||
|
||||
#[error("Module not found")]
|
||||
ModuleNotFound,
|
||||
|
||||
#[error("Pattern not found")]
|
||||
PatternNotFound,
|
||||
|
||||
#[error("Process not found")]
|
||||
ProcessNotFound,
|
||||
SectionNotFound,
|
||||
SerdeError(serde_json::Error),
|
||||
Utf8Error(std::string::FromUtf8Error),
|
||||
WindowsError(windows::core::Error),
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Self::IOError(err)
|
||||
}
|
||||
}
|
||||
#[error("Serde error: {0}")]
|
||||
SerdeError(#[from] SerdeError),
|
||||
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(err: std::string::FromUtf8Error) -> Self {
|
||||
Self::Utf8Error(err)
|
||||
}
|
||||
}
|
||||
#[error("UTF-8 error: {0}")]
|
||||
Utf8Error(#[from] FromUtf8Error),
|
||||
|
||||
impl From<windows::core::Error> for Error {
|
||||
fn from(err: windows::core::Error) -> Self {
|
||||
Self::WindowsError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BufferSizeMismatch(expected, actual) => write!(
|
||||
fmt,
|
||||
"Buffer size mismatch: expected {}, got {}",
|
||||
expected, actual
|
||||
),
|
||||
Self::InvalidMagic(magic) => write!(fmt, "Invalid magic: {:#X}", magic),
|
||||
Self::IOError(err) => write!(fmt, "IO error: {}", err),
|
||||
Self::ModuleNotFound => write!(fmt, "Module not found"),
|
||||
Self::PatternNotFound => write!(fmt, "Pattern not found"),
|
||||
Self::ProcessNotFound => write!(fmt, "Process not found"),
|
||||
Self::SectionNotFound => write!(fmt, "Section not found"),
|
||||
Self::SerdeError(err) => write!(fmt, "Serde error: {}", err),
|
||||
Self::Utf8Error(err) => write!(fmt, "UTF-8 error: {}", err),
|
||||
Self::WindowsError(err) => write!(fmt, "Windows error: {}", err),
|
||||
}
|
||||
}
|
||||
#[error("Windows error: {0}")]
|
||||
WindowsError(#[from] WindowsError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
22
src/main.rs
22
src/main.rs
@@ -33,24 +33,38 @@ struct Args {
|
||||
|
||||
#[arg(short, long)]
|
||||
schemas: bool,
|
||||
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
SimpleLogger::new().init().unwrap();
|
||||
|
||||
let Args {
|
||||
all,
|
||||
interfaces,
|
||||
offsets,
|
||||
schemas,
|
||||
verbose,
|
||||
} = Args::parse();
|
||||
|
||||
let log_level = if verbose {
|
||||
log::LevelFilter::Debug
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
};
|
||||
|
||||
SimpleLogger::new()
|
||||
.with_level(log_level)
|
||||
.without_timestamps()
|
||||
.init()
|
||||
.unwrap();
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
fs::create_dir_all("generated")?;
|
||||
|
||||
let process = Process::new("cs2.exe")?;
|
||||
|
||||
fs::create_dir_all("generated")?;
|
||||
|
||||
let mut builders: Vec<FileBuilderEnum> = vec![
|
||||
FileBuilderEnum::CppBuilder(CppBuilder),
|
||||
FileBuilderEnum::CSharpBuilder(CSharpBuilder),
|
||||
|
@@ -11,6 +11,7 @@ use crate::error::{Error, Result};
|
||||
|
||||
use super::Module;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Process {
|
||||
process_id: u32,
|
||||
process_handle: HANDLE,
|
||||
@@ -157,13 +158,14 @@ impl Process {
|
||||
self.write_memory_raw(address, &value as *const _ as *const _, mem::size_of::<T>())
|
||||
}
|
||||
|
||||
pub fn read_string(&self, address: usize, length: usize) -> Result<String> {
|
||||
let mut buffer: Vec<u8> = vec![0; length];
|
||||
pub fn read_string(&self, address: usize) -> Result<String> {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
self.read_memory_raw(address, buffer.as_mut_ptr() as *mut _, length)?;
|
||||
|
||||
if let Some(end) = buffer.iter().position(|&x| x == 0) {
|
||||
buffer.truncate(end);
|
||||
for i in 0.. {
|
||||
match self.read_memory::<u8>(address + i) {
|
||||
Ok(byte) if byte != 0 => buffer.push(byte),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(buffer)?)
|
||||
|
@@ -2,6 +2,7 @@ pub use schema_class_field_data::SchemaClassFieldData;
|
||||
pub use schema_class_info::SchemaClassInfo;
|
||||
pub use schema_system::SchemaSystem;
|
||||
pub use schema_system_type_scope::SchemaSystemTypeScope;
|
||||
pub use schema_type::SchemaType;
|
||||
pub use schema_type_declared_class::SchemaTypeDeclaredClass;
|
||||
pub use utl_ts_hash::UtlTsHash;
|
||||
|
||||
@@ -9,5 +10,6 @@ pub mod schema_class_field_data;
|
||||
pub mod schema_class_info;
|
||||
pub mod schema_system;
|
||||
pub mod schema_system_type_scope;
|
||||
pub mod schema_type;
|
||||
pub mod schema_type_declared_class;
|
||||
pub mod utl_ts_hash;
|
||||
|
@@ -1,6 +1,8 @@
|
||||
use crate::error::Result;
|
||||
use crate::remote::Process;
|
||||
|
||||
use super::SchemaType;
|
||||
|
||||
pub struct SchemaClassFieldData<'a> {
|
||||
process: &'a Process,
|
||||
address: usize,
|
||||
@@ -12,9 +14,15 @@ impl<'a> SchemaClassFieldData<'a> {
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Result<String> {
|
||||
let name_ptr = self.process.read_memory::<usize>(self.address)?;
|
||||
self.process
|
||||
.read_string(self.process.read_memory::<usize>(self.address)?)
|
||||
}
|
||||
|
||||
self.process.read_string(name_ptr, 64)
|
||||
pub fn r#type(&self) -> Result<SchemaType> {
|
||||
Ok(SchemaType::new(
|
||||
self.process,
|
||||
self.process.read_memory::<usize>(self.address + 0x8)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> Result<u16> {
|
||||
|
@@ -28,13 +28,13 @@ impl<'a> SchemaClassInfo<'a> {
|
||||
|
||||
let fields: Vec<SchemaClassFieldData> = (0..count as usize)
|
||||
.filter_map(|i| {
|
||||
let field = self
|
||||
let address = self
|
||||
.process
|
||||
.read_memory::<usize>(self.address + 0x28)
|
||||
.ok()?
|
||||
+ (i * 0x20);
|
||||
|
||||
(field != 0).then(|| SchemaClassFieldData::new(self.process, field))
|
||||
(address != 0).then(|| SchemaClassFieldData::new(self.process, address))
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@@ -35,6 +35,6 @@ impl<'a> SchemaSystemTypeScope<'a> {
|
||||
}
|
||||
|
||||
pub fn module_name(&self) -> Result<String> {
|
||||
self.process.read_string(self.address + 0x8, 256)
|
||||
self.process.read_string(self.address + 0x8)
|
||||
}
|
||||
}
|
||||
|
18
src/sdk/schema_type.rs
Normal file
18
src/sdk/schema_type.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use crate::error::Result;
|
||||
use crate::remote::Process;
|
||||
|
||||
pub struct SchemaType<'a> {
|
||||
process: &'a Process,
|
||||
address: usize,
|
||||
}
|
||||
|
||||
impl<'a> SchemaType<'a> {
|
||||
pub fn new(process: &'a Process, address: usize) -> Self {
|
||||
Self { process, address }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Result<String> {
|
||||
self.process
|
||||
.read_string(self.process.read_memory::<usize>(self.address + 0x8)?)
|
||||
}
|
||||
}
|
@@ -12,8 +12,7 @@ impl<'a> SchemaTypeDeclaredClass<'a> {
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Result<String> {
|
||||
let name_ptr = self.process.read_memory::<usize>(self.address + 0x8)?;
|
||||
|
||||
self.process.read_string(name_ptr, 64)
|
||||
self.process
|
||||
.read_string(self.process.read_memory::<usize>(self.address + 0x8)?)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user