mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 10:43:20 +00:00
[MIRROR] Some ZAS tweaks and minor optimizations (#9544)
Co-authored-by: Heroman3003 <31296024+Heroman3003@users.noreply.github.com> Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0473c33bcc
commit
a7e4ef7dad
@@ -36,11 +36,11 @@
|
||||
|
||||
//Convenience function for atoms to update turfs they occupy
|
||||
/atom/movable/proc/update_nearby_tiles(need_rebuild)
|
||||
if(!air_master)
|
||||
if(!SSair)
|
||||
return 0
|
||||
|
||||
for(var/turf/simulated/turf in locs)
|
||||
air_master.mark_for_update(turf)
|
||||
SSair.mark_for_update(turf)
|
||||
|
||||
return 1
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/*
|
||||
|
||||
Overview:
|
||||
Connections are made between turfs by air_master.connect(). They represent a single point where two zones converge.
|
||||
Connections are made between turfs by SSair.connect(). They represent a single point where two zones converge.
|
||||
|
||||
Class Vars:
|
||||
A - Always a simulated turf.
|
||||
@@ -60,19 +60,19 @@ Class Procs:
|
||||
|
||||
/connection/New(turf/simulated/A, turf/simulated/B)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(air_master.has_valid_zone(A))
|
||||
//ASSERT(air_master.has_valid_zone(B))
|
||||
ASSERT(HAS_VALID_ZONE(A))
|
||||
//ASSERT(HAS_VALID_ZONE(B))
|
||||
#endif
|
||||
src.A = A
|
||||
src.B = B
|
||||
zoneA = A.zone
|
||||
if(!istype(B))
|
||||
mark_space()
|
||||
edge = air_master.get_edge(A.zone,B)
|
||||
edge = SSair.get_edge(A.zone,B)
|
||||
edge.add_connection(src)
|
||||
else
|
||||
zoneB = B.zone
|
||||
edge = air_master.get_edge(A.zone,B.zone)
|
||||
edge = SSair.get_edge(A.zone,B.zone)
|
||||
edge.add_connection(src)
|
||||
|
||||
/connection/proc/mark_direct()
|
||||
@@ -108,7 +108,7 @@ Class Procs:
|
||||
erase()
|
||||
return
|
||||
|
||||
var/block_status = air_master.air_blocked(A,B)
|
||||
var/block_status = SSair.air_blocked(A,B)
|
||||
if(block_status & AIR_BLOCKED)
|
||||
//to_world("Blocked connection.")
|
||||
erase()
|
||||
@@ -133,7 +133,7 @@ Class Procs:
|
||||
return
|
||||
else
|
||||
edge.remove_connection(src)
|
||||
edge = air_master.get_edge(A.zone, B)
|
||||
edge = SSair.get_edge(A.zone, B)
|
||||
edge.add_connection(src)
|
||||
zoneA = A.zone
|
||||
|
||||
@@ -155,7 +155,7 @@ Class Procs:
|
||||
//to_world("Zones changed, \...")
|
||||
if(A.zone && B.zone)
|
||||
edge.remove_connection(src)
|
||||
edge = air_master.get_edge(A.zone, B.zone)
|
||||
edge = SSair.get_edge(A.zone, B.zone)
|
||||
edge.add_connection(src)
|
||||
zoneA = A.zone
|
||||
zoneB = B.zone
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Overview:
|
||||
These are what handle gas transfers between zones and into space.
|
||||
They are found in a zone's edges list and in air_master.edges.
|
||||
They are found in a zone's edges list and in SSair.edges.
|
||||
Each edge updates every air tick due to their role in gas transfer.
|
||||
They come in two flavors, /connection_edge/zone and /connection_edge/unsimulated.
|
||||
As the type names might suggest, they handle inter-zone and spacelike connections respectively.
|
||||
@@ -82,7 +82,7 @@ Class Procs:
|
||||
/connection_edge/proc/contains_zone(zone/Z)
|
||||
|
||||
/connection_edge/proc/erase()
|
||||
air_master.remove_edge(src)
|
||||
SSair.remove_edge(src)
|
||||
//to_world("[type] Erased.")
|
||||
|
||||
/connection_edge/proc/tick()
|
||||
@@ -169,19 +169,19 @@ Class Procs:
|
||||
if(equiv)
|
||||
if(direct)
|
||||
erase()
|
||||
air_master.merge(A, B)
|
||||
SSair.merge(A, B)
|
||||
return
|
||||
else
|
||||
A.air.equalize(B.air)
|
||||
air_master.mark_edge_sleeping(src)
|
||||
SSair.mark_edge_sleeping(src)
|
||||
|
||||
air_master.mark_zone_update(A)
|
||||
air_master.mark_zone_update(B)
|
||||
SSair.mark_zone_update(A)
|
||||
SSair.mark_zone_update(B)
|
||||
|
||||
/connection_edge/zone/recheck()
|
||||
// Edges with only one side being vacuum need processing no matter how close.
|
||||
if(!A.air.compare(B.air, vacuum_exception = 1))
|
||||
air_master.mark_edge_active(src)
|
||||
SSair.mark_edge_active(src)
|
||||
|
||||
//Helper proc to get connections for a zone.
|
||||
/connection_edge/zone/proc/get_connected_zone(zone/from)
|
||||
@@ -233,16 +233,16 @@ Class Procs:
|
||||
|
||||
if(equiv)
|
||||
A.air.copy_from(air)
|
||||
air_master.mark_edge_sleeping(src)
|
||||
SSair.mark_edge_sleeping(src)
|
||||
|
||||
air_master.mark_zone_update(A)
|
||||
SSair.mark_zone_update(A)
|
||||
|
||||
/connection_edge/unsimulated/recheck()
|
||||
// Edges with only one side being vacuum need processing no matter how close.
|
||||
// Note: This handles the glaring flaw of a room holding pressure while exposed to space, but
|
||||
// does not specially handle the less common case of a simulated room exposed to an unsimulated pressurized turf.
|
||||
if(!A.air.compare(air, vacuum_exception = 1))
|
||||
air_master.mark_edge_active(src)
|
||||
SSair.mark_edge_active(src)
|
||||
|
||||
/proc/ShareHeat(datum/gas_mixture/A, datum/gas_mixture/B, connecting_tiles)
|
||||
//This implements a simplistic version of the Stefan-Boltzmann law.
|
||||
|
||||
@@ -16,7 +16,7 @@ Class Procs:
|
||||
Preferable to accessing the connection directly because it checks validity.
|
||||
|
||||
place(connection/c, d)
|
||||
Called by air_master.connect(). Sets the connection in the specified direction to c.
|
||||
Called by SSair.connect(). Sets the connection in the specified direction to c.
|
||||
|
||||
update_all()
|
||||
Called after turf/update_air_properties(). Updates the validity of all connections on this turf.
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
var/datum/controller/subsystem/air/air_master
|
||||
|
||||
var/tick_multiplier = 2
|
||||
|
||||
/*
|
||||
|
||||
Overview:
|
||||
The air controller does everything. There are tons of procs in here.
|
||||
|
||||
Class Vars:
|
||||
zones - All zones currently holding one or more turfs.
|
||||
edges - All processing edges.
|
||||
|
||||
tiles_to_update - Tiles scheduled to update next tick.
|
||||
zones_to_update - Zones which have had their air changed and need air archival.
|
||||
active_hotspots - All processing fire objects.
|
||||
|
||||
active_zones - The number of zones which were archived last tick. Used in debug verbs.
|
||||
next_id - The next UID to be applied to a zone. Mostly useful for debugging purposes as zones do not need UIDs to function.
|
||||
|
||||
Class Procs:
|
||||
|
||||
mark_for_update(turf/T)
|
||||
Adds the turf to the update list. When updated, update_air_properties() will be called.
|
||||
When stuff changes that might affect airflow, call this. It's basically the only thing you need.
|
||||
|
||||
add_zone(zone/Z) and remove_zone(zone/Z)
|
||||
Adds zones to the zones list. Does not mark them for update.
|
||||
|
||||
air_blocked(turf/A, turf/B)
|
||||
Returns a bitflag consisting of:
|
||||
AIR_BLOCKED - The connection between turfs is physically blocked. No air can pass.
|
||||
ZONE_BLOCKED - There is a door between the turfs, so zones cannot cross. Air may or may not be permeable.
|
||||
|
||||
has_valid_zone(turf/T)
|
||||
Checks the presence and validity of T's zone.
|
||||
May be called on unsimulated turfs, returning 0.
|
||||
|
||||
merge(zone/A, zone/B)
|
||||
Called when zones have a direct connection and equivalent pressure and temperature.
|
||||
Merges the zones to create a single zone.
|
||||
|
||||
connect(turf/simulated/A, turf/B)
|
||||
Called by turf/update_air_properties(). The first argument must be simulated.
|
||||
Creates a connection between A and B.
|
||||
|
||||
mark_zone_update(zone/Z)
|
||||
Adds zone to the update list. Unlike mark_for_update(), this one is called automatically whenever
|
||||
air is returned from a simulated turf.
|
||||
|
||||
equivalent_pressure(zone/A, zone/B)
|
||||
Currently identical to A.air.compare(B.air). Returns 1 when directly connected zones are ready to be merged.
|
||||
|
||||
get_edge(zone/A, zone/B)
|
||||
get_edge(zone/A, turf/B)
|
||||
Gets a valid connection_edge between A and B, creating a new one if necessary.
|
||||
|
||||
has_same_air(turf/A, turf/B)
|
||||
Used to determine if an unsimulated edge represents a specific turf.
|
||||
Simulated edges use connection_edge/contains_zone() for the same purpose.
|
||||
Returns 1 if A has identical gases and temperature to B.
|
||||
|
||||
remove_edge(connection_edge/edge)
|
||||
Called when an edge is erased. Removes it from processing.
|
||||
|
||||
*/
|
||||
|
||||
//
|
||||
// The rest of the air subsystem is defined in air.dm
|
||||
//
|
||||
|
||||
/datum/controller/subsystem/air
|
||||
//Geometry lists
|
||||
var/list/zones = list()
|
||||
var/list/edges = list()
|
||||
//Geometry updates lists
|
||||
var/list/tiles_to_update = list()
|
||||
var/list/zones_to_update = list()
|
||||
var/list/active_fire_zones = list()
|
||||
var/list/active_hotspots = list()
|
||||
var/list/active_edges = list()
|
||||
|
||||
var/active_zones = 0
|
||||
var/current_cycle = 0
|
||||
var/next_id = 1 //Used to keep track of zone UIDs.
|
||||
|
||||
/datum/controller/subsystem/air/proc/add_zone(zone/z)
|
||||
zones.Add(z)
|
||||
z.name = "Zone [next_id++]"
|
||||
mark_zone_update(z)
|
||||
|
||||
/datum/controller/subsystem/air/proc/remove_zone(zone/z)
|
||||
zones.Remove(z)
|
||||
zones_to_update.Remove(z)
|
||||
|
||||
/datum/controller/subsystem/air/proc/air_blocked(turf/A, turf/B)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(isturf(A))
|
||||
ASSERT(isturf(B))
|
||||
#endif
|
||||
var/ablock = A.c_airblock(B)
|
||||
if(ablock == BLOCKED) return BLOCKED
|
||||
return ablock | B.c_airblock(A)
|
||||
|
||||
/datum/controller/subsystem/air/proc/has_valid_zone(turf/simulated/T)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(istype(T))
|
||||
#endif
|
||||
return istype(T) && T.zone && !T.zone.invalid
|
||||
|
||||
/datum/controller/subsystem/air/proc/merge(zone/A, zone/B)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(istype(A))
|
||||
ASSERT(istype(B))
|
||||
ASSERT(!A.invalid)
|
||||
ASSERT(!B.invalid)
|
||||
ASSERT(A != B)
|
||||
#endif
|
||||
if(A.contents.len < B.contents.len)
|
||||
A.c_merge(B)
|
||||
mark_zone_update(B)
|
||||
else
|
||||
B.c_merge(A)
|
||||
mark_zone_update(A)
|
||||
|
||||
/datum/controller/subsystem/air/proc/connect(turf/simulated/A, turf/simulated/B)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(istype(A))
|
||||
ASSERT(isturf(B))
|
||||
ASSERT(A.zone)
|
||||
ASSERT(!A.zone.invalid)
|
||||
//ASSERT(B.zone)
|
||||
ASSERT(A != B)
|
||||
#endif
|
||||
|
||||
var/block = air_master.air_blocked(A,B)
|
||||
if(block & AIR_BLOCKED) return
|
||||
|
||||
var/direct = !(block & ZONE_BLOCKED)
|
||||
var/space = !istype(B)
|
||||
|
||||
if(!space)
|
||||
if(min(A.zone.contents.len, B.zone.contents.len) < ZONE_MIN_SIZE || (direct && (equivalent_pressure(A.zone,B.zone) || current_cycle == 0)))
|
||||
merge(A.zone,B.zone)
|
||||
return
|
||||
|
||||
var/a_to_b = get_dir(A,B)
|
||||
var/b_to_a = get_dir(B,A)
|
||||
|
||||
if(!A.connections) A.connections = new
|
||||
if(!B.connections) B.connections = new
|
||||
|
||||
if(A.connections.get(a_to_b)) return
|
||||
if(B.connections.get(b_to_a)) return
|
||||
if(!space)
|
||||
if(A.zone == B.zone) return
|
||||
|
||||
|
||||
var/connection/c = new /connection(A,B)
|
||||
|
||||
A.connections.place(c, a_to_b)
|
||||
B.connections.place(c, b_to_a)
|
||||
|
||||
if(direct) c.mark_direct()
|
||||
|
||||
/datum/controller/subsystem/air/proc/mark_for_update(turf/T)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(isturf(T))
|
||||
#endif
|
||||
if(T.needs_air_update) return
|
||||
tiles_to_update |= T
|
||||
#ifdef ZASDBG
|
||||
T.add_overlay(mark)
|
||||
#endif
|
||||
T.needs_air_update = 1
|
||||
|
||||
/datum/controller/subsystem/air/proc/mark_zone_update(zone/Z)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(istype(Z))
|
||||
#endif
|
||||
if(Z.needs_update) return
|
||||
zones_to_update.Add(Z)
|
||||
Z.needs_update = 1
|
||||
|
||||
/datum/controller/subsystem/air/proc/mark_edge_sleeping(connection_edge/E)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(istype(E))
|
||||
#endif
|
||||
if(E.sleeping) return
|
||||
active_edges.Remove(E)
|
||||
E.sleeping = 1
|
||||
|
||||
/datum/controller/subsystem/air/proc/mark_edge_active(connection_edge/E)
|
||||
#ifdef ZASDBG
|
||||
ASSERT(istype(E))
|
||||
#endif
|
||||
if(!E.sleeping) return
|
||||
active_edges.Add(E)
|
||||
E.sleeping = 0
|
||||
|
||||
/datum/controller/subsystem/air/proc/equivalent_pressure(zone/A, zone/B)
|
||||
return A.air.compare(B.air)
|
||||
|
||||
/datum/controller/subsystem/air/proc/get_edge(zone/A, zone/B)
|
||||
|
||||
if(istype(B))
|
||||
for(var/connection_edge/zone/edge in A.edges)
|
||||
if(edge.contains_zone(B)) return edge
|
||||
var/connection_edge/edge = new/connection_edge/zone(A,B)
|
||||
edges.Add(edge)
|
||||
edge.recheck()
|
||||
return edge
|
||||
else
|
||||
for(var/connection_edge/unsimulated/edge in A.edges)
|
||||
if(has_same_air(edge.B,B)) return edge
|
||||
var/connection_edge/edge = new/connection_edge/unsimulated(A,B)
|
||||
edges.Add(edge)
|
||||
edge.recheck()
|
||||
return edge
|
||||
|
||||
/datum/controller/subsystem/air/proc/has_same_air(turf/A, turf/B)
|
||||
if(A.oxygen != B.oxygen) return 0
|
||||
if(A.nitrogen != B.nitrogen) return 0
|
||||
if(A.phoron != B.phoron) return 0
|
||||
if(A.carbon_dioxide != B.carbon_dioxide) return 0
|
||||
if(A.temperature != B.temperature) return 0
|
||||
return 1
|
||||
|
||||
/datum/controller/subsystem/air/proc/remove_edge(connection_edge/E)
|
||||
edges.Remove(E)
|
||||
if(!E.sleeping) active_edges.Remove(E)
|
||||
@@ -8,12 +8,12 @@
|
||||
/*
|
||||
if(!check_rights(R_DEBUG)) return
|
||||
|
||||
var/result = air_master.Tick()
|
||||
var/result = SSair.Tick()
|
||||
if(result)
|
||||
to_chat(src, "Successfully Processed.")
|
||||
|
||||
else
|
||||
to_chat(src, "Failed to process! ([air_master.tick_progress])")
|
||||
to_chat(src, "Failed to process! ([SSair.tick_progress])")
|
||||
*/
|
||||
|
||||
/client/proc/Zone_Info(turf/T as null|turf)
|
||||
|
||||
@@ -58,7 +58,7 @@ If it gains pressure too slowly, it may leak or just rupture instead of explodin
|
||||
fuel_objs.Cut()
|
||||
|
||||
if(!fire_tiles.len)
|
||||
air_master.active_fire_zones.Remove(src)
|
||||
SSair.active_fire_zones.Remove(src)
|
||||
|
||||
/zone/proc/remove_liquidfuel(var/used_liquid_fuel, var/remove_fire=0)
|
||||
if(!fuel_objs.len)
|
||||
@@ -94,7 +94,7 @@ If it gains pressure too slowly, it may leak or just rupture instead of explodin
|
||||
return 1
|
||||
|
||||
fire = new(src, fl)
|
||||
air_master.active_fire_zones |= zone
|
||||
SSair.active_fire_zones |= zone
|
||||
|
||||
var/obj/effect/decal/cleanable/liquid_fuel/fuel = locate() in src
|
||||
zone.fire_tiles |= src
|
||||
@@ -191,7 +191,7 @@ If it gains pressure too slowly, it may leak or just rupture instead of explodin
|
||||
set_light(3, 1, color)
|
||||
|
||||
firelevel = fl
|
||||
air_master.active_hotspots.Add(src)
|
||||
SSair.active_hotspots.Add(src)
|
||||
|
||||
/obj/fire/proc/fire_color(var/env_temperature)
|
||||
var/temperature = max(4000*sqrt(firelevel/vsc.fire_firelevel_multiplier), env_temperature)
|
||||
@@ -209,7 +209,7 @@ If it gains pressure too slowly, it may leak or just rupture instead of explodin
|
||||
|
||||
T.fire = null
|
||||
loc = null
|
||||
air_master.active_hotspots.Remove(src)
|
||||
SSair.active_hotspots.Remove(src)
|
||||
|
||||
|
||||
/turf/simulated/var/fire_protection = 0 //Protects newly extinguished tiles from being overrun again.
|
||||
|
||||
@@ -41,9 +41,8 @@
|
||||
if(istype(unsim, /turf/simulated))
|
||||
|
||||
var/turf/simulated/sim = unsim
|
||||
if(air_master.has_valid_zone(sim))
|
||||
|
||||
air_master.connect(sim, src)
|
||||
if(HAS_VALID_ZONE(sim))
|
||||
SSair.connect(sim, src)
|
||||
|
||||
// CHOMPAdd
|
||||
#define GET_ZONE_NEIGHBOURS(T, ret) \
|
||||
@@ -240,7 +239,7 @@
|
||||
if(verbose) to_world("Connecting to [sim.zone]")
|
||||
#endif
|
||||
|
||||
SSair.connect(src, sim) // CHOMPEdit
|
||||
SSair.connect(src, sim)
|
||||
|
||||
|
||||
#ifdef ZASDBG
|
||||
@@ -268,7 +267,7 @@
|
||||
//At this point, a zone should have happened. If it hasn't, don't add more checks, fix the bug.
|
||||
|
||||
for(var/turf/T in postponed)
|
||||
SSair.connect(src, T) // CHOMPEdit
|
||||
SSair.connect(src, T)
|
||||
|
||||
/turf/proc/post_update_air_properties()
|
||||
if(connections) connections.update_all()
|
||||
@@ -324,7 +323,7 @@
|
||||
/turf/simulated/return_air()
|
||||
if(zone)
|
||||
if(!zone.invalid)
|
||||
air_master.mark_zone_update(zone)
|
||||
SSair.mark_zone_update(zone)
|
||||
return zone.air
|
||||
else
|
||||
if(!air)
|
||||
|
||||
@@ -56,7 +56,7 @@ Class Procs:
|
||||
/zone/var/list/graphic_remove = list()
|
||||
|
||||
/zone/New()
|
||||
air_master.add_zone(src)
|
||||
SSair.add_zone(src)
|
||||
air.temperature = TCMB
|
||||
air.group_multiplier = 1
|
||||
air.volume = CELL_VOLUME
|
||||
@@ -65,7 +65,7 @@ Class Procs:
|
||||
#ifdef ZASDBG
|
||||
ASSERT(!invalid)
|
||||
ASSERT(istype(T))
|
||||
ASSERT(!air_master.has_valid_zone(T))
|
||||
ASSERT(!HAS_VALID_ZONE(T))
|
||||
#endif
|
||||
|
||||
var/datum/gas_mixture/turf_air = T.return_air()
|
||||
@@ -75,7 +75,7 @@ Class Procs:
|
||||
if(T.fire)
|
||||
var/obj/effect/decal/cleanable/liquid_fuel/fuel = locate() in T
|
||||
fire_tiles.Add(T)
|
||||
air_master.active_fire_zones |= src
|
||||
SSair.active_fire_zones |= src
|
||||
if(fuel) fuel_objs += fuel
|
||||
if(air.graphic)
|
||||
T.update_graphic(air.graphic)
|
||||
@@ -122,11 +122,11 @@ Class Procs:
|
||||
if(E.contains_zone(into))
|
||||
continue //don't need to rebuild this edge
|
||||
for(var/turf/T in E.connecting_turfs)
|
||||
air_master.mark_for_update(T)
|
||||
SSair.mark_for_update(T)
|
||||
|
||||
/zone/proc/c_invalidate()
|
||||
invalid = 1
|
||||
air_master.remove_zone(src)
|
||||
SSair.remove_zone(src)
|
||||
#ifdef ZASDBG
|
||||
for(var/turf/simulated/T in contents)
|
||||
T.dbg(invalid_zone)
|
||||
@@ -141,7 +141,7 @@ Class Procs:
|
||||
T.update_graphic(graphic_remove = air_graphic) //we need to remove the overlays so they're not doubled when the zone is rebuilt
|
||||
//T.dbg(invalid_zone)
|
||||
T.needs_air_update = 0 //Reset the marker so that it will be added to the list.
|
||||
air_master.mark_for_update(T)
|
||||
SSair.mark_for_update(T)
|
||||
|
||||
/zone/proc/add_tile_air(datum/gas_mixture/tile_air)
|
||||
//air.volume += CELL_VOLUME
|
||||
@@ -152,7 +152,7 @@ Class Procs:
|
||||
air.group_multiplier = contents.len+1
|
||||
|
||||
/zone/proc/tick()
|
||||
if(air.temperature >= PHORON_FLASHPOINT && !(src in air_master.active_fire_zones) && air.check_combustability() && contents.len)
|
||||
if(air.temperature >= PHORON_FLASHPOINT && !(src in SSair.active_fire_zones) && air.check_combustability() && contents.len)
|
||||
var/turf/T = pick(contents)
|
||||
if(istype(T))
|
||||
T.create_fire(vsc.fire_firelevel_multiplier)
|
||||
|
||||
@@ -15,7 +15,7 @@ Every air tick:
|
||||
|
||||
Important Functions:
|
||||
|
||||
air_master.mark_for_update(turf)
|
||||
SSair.mark_for_update(turf)
|
||||
When stuff happens, call this. It works on everything. You basically don't need to worry about any other
|
||||
functions besides CanPass().
|
||||
|
||||
|
||||
Reference in New Issue
Block a user