Add worn player item snapshots and refresh DayZ offsets
- Resolve worn clothing from player inventory and expose it in web snapshots - Keep far entities alive through transient transform/position read misses - Refresh v1.29 health, third-person, weapon, magazine, ammo, and stat offsets - Force full skeleton cache re-probe when stale bone data is detected - Remove status styling from ESP tab toggles
This commit is contained in:
@@ -9,3 +9,7 @@ out/
|
|||||||
*.exe
|
*.exe
|
||||||
*.pdb
|
*.pdb
|
||||||
*.ilk
|
*.ilk
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Keep these specific files tracked
|
||||||
|
!README.md
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
(none)
|
(none)
|
||||||
|
item esp doesnt render past a certain distance
|
||||||
|
inventory detection
|
||||||
|
ballistic prediction
|
||||||
|
combat mode on web radar
|
||||||
|
create a grid coord system for web radar
|
||||||
Vendored
+2
-2
@@ -230,7 +230,7 @@ static void render_esp_tab(const visual_widget_filter& w, float section_height)
|
|||||||
gui->begin_group();
|
gui->begin_group();
|
||||||
{
|
{
|
||||||
begin_visual_section("Entities", section_height);
|
begin_visual_section("Entities", section_height);
|
||||||
w.checkbox("Players", "Show player ESP", g_menu->showPlayers, title_status_safe);
|
w.checkbox("Players", "Show player ESP", g_menu->showPlayers);
|
||||||
w.checkbox("Animals", "Show animal ESP", g_menu->showAnimals);
|
w.checkbox("Animals", "Show animal ESP", g_menu->showAnimals);
|
||||||
w.checkbox("Zombies", "Show infected ESP", g_menu->showZombies);
|
w.checkbox("Zombies", "Show infected ESP", g_menu->showZombies);
|
||||||
w.checkbox("Items", "Show loot ESP", g_menu->showItems);
|
w.checkbox("Items", "Show loot ESP", g_menu->showItems);
|
||||||
@@ -241,7 +241,7 @@ static void render_esp_tab(const visual_widget_filter& w, float section_height)
|
|||||||
w.checkbox("Weapon In Hand", "Show held weapon name", g_menu->showWeapon);
|
w.checkbox("Weapon In Hand", "Show held weapon name", g_menu->showWeapon);
|
||||||
w.checkbox("Health Bar", "Draw player health bar", g_menu->showHealthBar);
|
w.checkbox("Health Bar", "Draw player health bar", g_menu->showHealthBar);
|
||||||
w.checkbox("Health Number", "Draw numeric health", g_menu->showHealthNumber);
|
w.checkbox("Health Number", "Draw numeric health", g_menu->showHealthNumber);
|
||||||
w.checkbox("Skeleton Debug", "Label every named bone", g_menu->debugSkeleton, title_status_warning);
|
w.checkbox("Skeleton Debug", "Label every named bone", g_menu->debugSkeleton);
|
||||||
end_visual_section();
|
end_visual_section();
|
||||||
}
|
}
|
||||||
gui->end_group();
|
gui->end_group();
|
||||||
|
|||||||
+3
-1
@@ -1,7 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Primitive math types
|
// Primitive math types
|
||||||
@@ -129,6 +130,7 @@ struct DayZPlayerEntry {
|
|||||||
bool isAdmin = false; // model matches a known invisible/admin model path
|
bool isAdmin = false; // model matches a known invisible/admin model path
|
||||||
std::string nickname;
|
std::string nickname;
|
||||||
std::string itemInHands;
|
std::string itemInHands;
|
||||||
|
std::vector<std::string> wornItems;
|
||||||
std::string typeName;
|
std::string typeName;
|
||||||
std::string configName;
|
std::string configName;
|
||||||
std::string modelName;
|
std::string modelName;
|
||||||
|
|||||||
+43
-5
@@ -73,7 +73,9 @@ namespace Offsets {
|
|||||||
constexpr uint64_t ServerName = 0x308; // v1.29 [manual]
|
constexpr uint64_t ServerName = 0x308; // v1.29 [manual]
|
||||||
constexpr uint64_t GameVersion = 0x350; // v1.29 [manual]
|
constexpr uint64_t GameVersion = 0x350; // v1.29 [manual]
|
||||||
constexpr uint64_t MapName = 0x38; // v1.29 [manual]
|
constexpr uint64_t MapName = 0x38; // v1.29 [manual]
|
||||||
constexpr uint64_t ThirdPerson = 0x9C; // v1.29 [manual]
|
// 0x9C is the adjacent sibling DWORD — confirmed wrong by UC dumper r15 (2026-06-15).
|
||||||
|
// Correct value is MissionHeader::is_third_person_disabled at 0x74.
|
||||||
|
constexpr uint64_t ThirdPerson = 0x74; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
constexpr uint64_t Crosshair = 0xA0; // v1.29 [manual]
|
constexpr uint64_t Crosshair = 0xA0; // v1.29 [manual]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,10 +109,46 @@ namespace Offsets {
|
|||||||
constexpr uint64_t InputController = 0x7E8; // v1.29 [manual]
|
constexpr uint64_t InputController = 0x7E8; // v1.29 [manual]
|
||||||
constexpr uint64_t IsDead = 0xE2; // v1.29
|
constexpr uint64_t IsDead = 0xE2; // v1.29
|
||||||
constexpr uint64_t EntityDead = 0x15D; // v1.29 [manual]
|
constexpr uint64_t EntityDead = 0x15D; // v1.29 [manual]
|
||||||
// TODO: verify health offset for current game version.
|
// Damage manager — pointer to health/blood zone manager.
|
||||||
// Set to 0 to disable health reading; set to the correct entity offset
|
// Chain: entity+0x700 → damage manager → zone array → per-zone float health.
|
||||||
// for a float in the range 0..100 once confirmed via RE.
|
constexpr uint64_t DamageManager = 0x700; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
constexpr uint64_t Health = 0x0;
|
// Player stats hash map container. Chain: entity+0x6F0 → stats container → records.
|
||||||
|
constexpr uint64_t StatsContainer = 0x6F0; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
// Health is the Quality float at entity+0x194 (same field as Inventory::ItemQuality).
|
||||||
|
// Values are discrete increments of 0.25f: 0.25 | 0.50 | 0.75 | 1.0
|
||||||
|
// (maps to critically injured / badly injured / injured / healthy).
|
||||||
|
// Valid for both players and zombies. Read as float, multiply by 100 for display.
|
||||||
|
constexpr uint64_t Health = 0x194; // v1.29 [operator-confirmed 2026-06-17]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player stat record — offset of the float value field within a StatRecord object.
|
||||||
|
// Confirmed: PlayerStats::AddDelta at VA 0x6ABA30 — `addss xmm1,[rcx+0x2C]; movss [rcx+0x2C],xmm1; ret`
|
||||||
|
namespace PlayerStats {
|
||||||
|
constexpr uint64_t RecordValue = 0x2C; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weapon entity struct offsets (entity is already an InventoryItem with a type ptr at +0x180).
|
||||||
|
// Chain to magazine: entity+0x1B0 (ChamberedPtr) → chambered item → magazine.
|
||||||
|
namespace Weapon {
|
||||||
|
constexpr uint64_t ChamberedPtr = 0x1B0; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
constexpr uint64_t AmmoMagCount = 0x6AC; // v1.29 [UC dumper r15 2026-06-15] — loaded magazine ammo count
|
||||||
|
constexpr uint64_t AmmoCapacityA = 0x6B0; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
constexpr uint64_t AmmoCapacityB = 0x6B4; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Magazine entity struct offsets.
|
||||||
|
// AmmoTypePtr → ammo type object; AmmoCount = current rounds; MaxAmmo = capacity.
|
||||||
|
namespace Magazine {
|
||||||
|
constexpr uint64_t AmmoTypePtr = 0x20; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
constexpr uint64_t AmmoCount = 0x3B0; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
constexpr uint64_t MaxAmmo = 0x3A4; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AmmoType config object offsets (reached via Magazine::AmmoTypePtr).
|
||||||
|
namespace AmmoType {
|
||||||
|
constexpr uint64_t InitSpeed = 0x38C; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
constexpr uint64_t AirFriction = 0x3B4; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
|
constexpr uint64_t Dispersion = 0x3A4; // v1.29 [UC dumper r15 2026-06-15]
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Infected {
|
namespace Infected {
|
||||||
|
|||||||
@@ -126,10 +126,11 @@ std::vector<DayZPlayerEntry> EntityCategoryProjector::BuildPlayers(
|
|||||||
const std::vector<DayZFarEntityEntry>& far,
|
const std::vector<DayZFarEntityEntry>& far,
|
||||||
const std::vector<DayZSlowEntityEntry>& slow,
|
const std::vector<DayZSlowEntityEntry>& slow,
|
||||||
const std::unordered_map<uint32_t, std::string>& scoreboardNames,
|
const std::unordered_map<uint32_t, std::string>& scoreboardNames,
|
||||||
std::function<bool(uint64_t)> deadResolver,
|
std::function<bool(uint64_t)> deadResolver,
|
||||||
std::function<std::string(uint64_t)> heldItemResolver,
|
std::function<std::string(uint64_t)> heldItemResolver,
|
||||||
std::function<float(uint64_t)> healthResolver,
|
std::function<float(uint64_t)> healthResolver,
|
||||||
std::function<bool(uint64_t)> adminResolver)
|
std::function<bool(uint64_t)> adminResolver,
|
||||||
|
std::function<std::vector<std::string>(uint64_t)> wornClothesResolver)
|
||||||
{
|
{
|
||||||
auto all = EnumerateEntities(near, far, slow);
|
auto all = EnumerateEntities(near, far, slow);
|
||||||
|
|
||||||
@@ -159,10 +160,11 @@ std::vector<DayZPlayerEntry> EntityCategoryProjector::BuildPlayers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dead state, held item, and health — resolved via caller-provided callbacks.
|
// Dead state, held item, health, and worn clothes — resolved via caller-provided callbacks.
|
||||||
if (deadResolver) entry.isDead = deadResolver(ep.address);
|
if (deadResolver) entry.isDead = deadResolver(ep.address);
|
||||||
if (heldItemResolver)entry.itemInHands = heldItemResolver(ep.address);
|
if (heldItemResolver) entry.itemInHands = heldItemResolver(ep.address);
|
||||||
if (healthResolver) entry.health = healthResolver(ep.address);
|
if (healthResolver) entry.health = healthResolver(ep.address);
|
||||||
|
if (wornClothesResolver) entry.wornItems = wornClothesResolver(ep.address);
|
||||||
|
|
||||||
result.push_back(std::move(entry));
|
result.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ public:
|
|||||||
const std::vector<DayZFarEntityEntry>& far,
|
const std::vector<DayZFarEntityEntry>& far,
|
||||||
const std::vector<DayZSlowEntityEntry>& slow,
|
const std::vector<DayZSlowEntityEntry>& slow,
|
||||||
const std::unordered_map<uint32_t, std::string>& scoreboardNames,
|
const std::unordered_map<uint32_t, std::string>& scoreboardNames,
|
||||||
std::function<bool(uint64_t)> deadResolver,
|
std::function<bool(uint64_t)> deadResolver,
|
||||||
std::function<std::string(uint64_t)> heldItemResolver,
|
std::function<std::string(uint64_t)> heldItemResolver,
|
||||||
std::function<float(uint64_t)> healthResolver,
|
std::function<float(uint64_t)> healthResolver,
|
||||||
std::function<bool(uint64_t)> adminResolver = nullptr);
|
std::function<bool(uint64_t)> adminResolver = nullptr,
|
||||||
|
std::function<std::vector<std::string>(uint64_t)> wornClothesResolver = nullptr);
|
||||||
|
|
||||||
static std::vector<DayZAnimalEntry> BuildAnimals(
|
static std::vector<DayZAnimalEntry> BuildAnimals(
|
||||||
const std::vector<DayZNearEntityEntry>& near,
|
const std::vector<DayZNearEntityEntry>& near,
|
||||||
|
|||||||
@@ -212,10 +212,9 @@ void FarEntityListReader::ProcessEntity(VmmAccessor& me
|
|||||||
cached.entry.modelName = typeMeta.modelName;
|
cached.entry.modelName = typeMeta.modelName;
|
||||||
cached.lastSeenGeneration = m_scanGeneration;
|
cached.lastSeenGeneration = m_scanGeneration;
|
||||||
cached.failedTypeReads = 0;
|
cached.failedTypeReads = 0;
|
||||||
|
// Entity address was present in the pointer table — it is alive regardless
|
||||||
if (transformOk) {
|
// of whether the transform read succeeded this tick.
|
||||||
cached.lastSuccessfulRead = now;
|
cached.lastSuccessfulRead = now;
|
||||||
}
|
|
||||||
|
|
||||||
m_resultDirty = true;
|
m_resultDirty = true;
|
||||||
}
|
}
|
||||||
@@ -381,20 +380,25 @@ void FarEntityListReader::RefreshPositions(VmmAccessor& mem,
|
|||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
for (auto& slot : slots) {
|
for (auto& slot : slots) {
|
||||||
if (slot.vsAddr == 0) continue;
|
if (slot.vsAddr == 0) continue;
|
||||||
|
|
||||||
|
auto it = m_entities.find(slot.entityAddr);
|
||||||
|
if (it == m_entities.end()) continue;
|
||||||
|
|
||||||
|
// VS pointer resolved — entity is provably still in the game world.
|
||||||
|
// Bump the keep-alive stamp regardless of whether the position scatter
|
||||||
|
// returned a valid vector; a bad position is a transient DMA miss,
|
||||||
|
// not evidence the entity was removed from the table.
|
||||||
|
it->second.lastSuccessfulRead = now;
|
||||||
|
|
||||||
if (!MemoryValidation::IsValidVector(slot.posOut)) {
|
if (!MemoryValidation::IsValidVector(slot.posOut)) {
|
||||||
// Possibly stale VS pointer — invalidate so pass A re-reads it next time.
|
// Position unreadable this tick — keep last known position and
|
||||||
auto it = m_entities.find(slot.entityAddr);
|
// force a fresh VS re-read next pass in case the pointer moved.
|
||||||
if (it != m_entities.end())
|
it->second.visualStateAddr = 0;
|
||||||
it->second.visualStateAddr = 0;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto it = m_entities.find(slot.entityAddr);
|
it->second.entry.position = slot.posOut;
|
||||||
if (it != m_entities.end()) {
|
m_resultDirty = true;
|
||||||
it->second.entry.position = slot.posOut;
|
|
||||||
it->second.lastSuccessfulRead = now;
|
|
||||||
m_resultDirty = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -632,6 +632,55 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
|
|||||||
return hp;
|
return hp;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- wornClothesResolver ---
|
||||||
|
// Chain: entity → +0x650 (inventory) → +0x150 (clothes grid ptr)
|
||||||
|
// grid → +0x8 (items array ptr), +0xC (uint32 count)
|
||||||
|
// items[i*8] → item entity → +0x180 (type) → cleanName
|
||||||
|
auto wornClothesResolver = [&](uint64_t addr) -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> result;
|
||||||
|
|
||||||
|
uint64_t inventoryAddr = 0;
|
||||||
|
if (!m_memory.TryReadPointer(pid, addr + RuntimeOffsets::Inventory::Base, inventoryAddr)
|
||||||
|
|| !MemoryValidation::IsValidUserAddress(inventoryAddr))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
uint64_t clothesGrid = 0;
|
||||||
|
if (!m_memory.TryReadPointer(pid, inventoryAddr + Offsets::Inventory::WornClothes, clothesGrid)
|
||||||
|
|| !MemoryValidation::IsValidUserAddress(clothesGrid))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
uint64_t itemsArray = 0;
|
||||||
|
if (!m_memory.TryReadPointer(pid, clothesGrid + Offsets::Inventory::ItemPtr, itemsArray)
|
||||||
|
|| !MemoryValidation::IsValidUserAddress(itemsArray))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
uint32_t count = 0;
|
||||||
|
if (!m_memory.TryReadValue<uint32_t>(pid, clothesGrid + Offsets::Inventory::CargoGridCount, count)
|
||||||
|
|| count == 0 || count > 32)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
result.reserve(count);
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
uint64_t itemAddr = 0;
|
||||||
|
if (!m_memory.TryReadPointer(pid, itemsArray + i * sizeof(uint64_t), itemAddr)
|
||||||
|
|| !MemoryValidation::IsValidUserAddress(itemAddr))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
uint64_t typeAddr = 0;
|
||||||
|
if (!m_memory.TryReadPointer(pid, itemAddr + Offsets::Common::Type, typeAddr)
|
||||||
|
|| !MemoryValidation::IsValidUserAddress(typeAddr))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
EntityTypeMetadata meta = ReadEntityTypeMetadata(m_memory, pid, typeAddr);
|
||||||
|
std::string name;
|
||||||
|
if (!meta.cleanName.empty()) name = meta.cleanName;
|
||||||
|
else if (!meta.typeName.empty()) name = FormatEntityName(meta.typeName);
|
||||||
|
else if (!meta.configName.empty()) name = meta.configName;
|
||||||
|
if (!name.empty()) result.push_back(std::move(name));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
RuntimeUpdate update;
|
RuntimeUpdate update;
|
||||||
update.areBaseObjectsReady = true;
|
update.areBaseObjectsReady = true;
|
||||||
update.status = "Live";
|
update.status = "Live";
|
||||||
@@ -677,7 +726,8 @@ void DayZRuntimeService::RunLiveLoop(const RuntimeSession& session) {
|
|||||||
|
|
||||||
update.players = EntityCategoryProjector::BuildPlayers(
|
update.players = EntityCategoryProjector::BuildPlayers(
|
||||||
m_state.nearEntities, m_state.farEntities, m_state.slowEntities,
|
m_state.nearEntities, m_state.farEntities, m_state.slowEntities,
|
||||||
scoreboardNames, deadResolver, heldItemResolver, healthResolver, adminResolver);
|
scoreboardNames, deadResolver, heldItemResolver, healthResolver,
|
||||||
|
adminResolver, wornClothesResolver);
|
||||||
|
|
||||||
// ---- Announce newly-detected admins once each (name + how + range) ----
|
// ---- Announce newly-detected admins once each (name + how + range) ----
|
||||||
{
|
{
|
||||||
@@ -1305,9 +1355,10 @@ void DayZRuntimeService::RefreshBonesScatter(
|
|||||||
const float hy = skel.head.y - refY;
|
const float hy = skel.head.y - refY;
|
||||||
const float hz = skel.head.z - refZ;
|
const float hz = skel.head.z - refZ;
|
||||||
if (hx*hx + hy*hy + hz*hz > 9.0f) { // > 3 m from VS origin → stale pointer
|
if (hx*hx + hy*hy + hz*hz > 9.0f) { // > 3 m from VS origin → stale pointer
|
||||||
skel.valid = false;
|
skel.valid = false;
|
||||||
ed.cache->valid = false; // force re-resolve next frame
|
ed.cache->valid = false;
|
||||||
lossReason = "stale pointer (head drift > 3m)";
|
ed.cache->animClass = 0; // skip fast-path; force full re-probe
|
||||||
|
lossReason = "stale pointer (head drift > 3m)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1319,9 +1370,10 @@ void DayZRuntimeService::RefreshBonesScatter(
|
|||||||
// here causes ensureCached to re-read a fresh matBase on the next tick via
|
// here causes ensureCached to re-read a fresh matBase on the next tick via
|
||||||
// the fast 2-read path (animClass is still cached).
|
// the fast 2-read path (animClass is still cached).
|
||||||
if (skel.valid && vsOk && (skel.head.y - vsY) < 0.3f) {
|
if (skel.valid && vsOk && (skel.head.y - vsY) < 0.3f) {
|
||||||
skel.valid = false;
|
skel.valid = false;
|
||||||
ed.cache->valid = false;
|
ed.cache->valid = false;
|
||||||
lossReason = "collapsed skeleton (stale matBase)";
|
ed.cache->animClass = 0; // stale matBase implies animClass may also be bad; force full re-probe
|
||||||
|
lossReason = "collapsed skeleton (stale matBase)";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record presence / log the valid→invalid edge for diagnostics.
|
// Record presence / log the valid→invalid edge for diagnostics.
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ static json PlayerEntityJson(const DayZPlayerEntry& p, const MapInfo* map) {
|
|||||||
obj["steamId"] = "";
|
obj["steamId"] = "";
|
||||||
obj["dead"] = p.isDead;
|
obj["dead"] = p.isDead;
|
||||||
obj["handItem"] = p.itemInHands;
|
obj["handItem"] = p.itemInHands;
|
||||||
|
obj["wornItems"] = p.wornItems;
|
||||||
obj["visibleOnMap"] = true;
|
obj["visibleOnMap"] = true;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@@ -70,6 +71,7 @@ static json PlayerListEntryJson(const DayZPlayerEntry& p) {
|
|||||||
obj["visibleOnMap"] = true;
|
obj["visibleOnMap"] = true;
|
||||||
obj["dead"] = p.isDead;
|
obj["dead"] = p.isDead;
|
||||||
obj["handItem"] = p.itemInHands;
|
obj["handItem"] = p.itemInHands;
|
||||||
|
obj["wornItems"] = p.wornItems;
|
||||||
obj["distance"] = -1;
|
obj["distance"] = -1;
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user