mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-24 05:30:05 +01:00
Multi-Z Support for Lazy Templates | Cleans up some turf flag misuse (#77786)
## About The Pull Request Adds multi-z support for lazy templates Also fixes some improper use and placement for turf flags ## Why It's Good For The Game Shadow needs/wants this for bit runner maps. Turf flags are also why lava has been generating in places it shouldnt. (inside of ruins) ## Changelog 🆑 fix: Lava can no longer occasionally generate inside of previously loaded templates and breach and/or destroy shit /🆑 --------- Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
This commit is contained in:
co-authored by
Jeremiah
LemonInTheDark
parent
a67404f5dc
commit
1b96345e44
+159
-54
@@ -114,26 +114,72 @@
|
||||
var/turfsSkipped = 0
|
||||
#endif
|
||||
|
||||
/datum/parsed_map/proc/copy()
|
||||
// Avoids duped work just in case
|
||||
build_cache()
|
||||
var/datum/parsed_map/newfriend = new()
|
||||
newfriend.original_path = original_path
|
||||
newfriend.map_format = map_format
|
||||
newfriend.key_len = key_len
|
||||
newfriend.line_len = line_len
|
||||
newfriend.grid_models = grid_models.Copy()
|
||||
newfriend.gridSets = gridSets.Copy()
|
||||
newfriend.modelCache = modelCache.Copy()
|
||||
newfriend.parsed_bounds = parsed_bounds.Copy()
|
||||
// Copy parsed bounds to reset to initial values
|
||||
newfriend.bounds = parsed_bounds.Copy()
|
||||
newfriend.turf_blacklist = turf_blacklist?.Copy()
|
||||
return newfriend
|
||||
|
||||
//text trimming (both directions) helper macro
|
||||
#define TRIM_TEXT(text) (trim_reduced(text))
|
||||
|
||||
/// Shortcut function to parse a map and apply it to the world.
|
||||
///
|
||||
/// - `dmm_file`: A .dmm file to load (Required).
|
||||
/// - `x_offset`, `y_offset`, `z_offset`: Positions representign where to load the map (Optional).
|
||||
/// - `cropMap`: When true, the map will be cropped to fit the existing world dimensions (Optional).
|
||||
/// - `measureOnly`: When true, no changes will be made to the world (Optional).
|
||||
/// - `no_changeturf`: When true, [/turf/proc/AfterChange] won't be called on loaded turfs
|
||||
/// - `x_lower`, `x_upper`, `y_lower`, `y_upper`: Coordinates (relative to the map) to crop to (Optional).
|
||||
/// - `placeOnTop`: Whether to use [/turf/proc/PlaceOnTop] rather than [/turf/proc/ChangeTurf] (Optional).
|
||||
/proc/load_map(dmm_file as file, x_offset as num, y_offset as num, z_offset as num, cropMap as num, measureOnly as num, no_changeturf as num, x_lower = -INFINITY as num, x_upper = INFINITY as num, y_lower = -INFINITY as num, y_upper = INFINITY as num, placeOnTop = FALSE as num, new_z)
|
||||
var/datum/parsed_map/parsed = new(dmm_file, x_lower, x_upper, y_lower, y_upper, measureOnly)
|
||||
if(parsed.bounds && !measureOnly)
|
||||
parsed.load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z = new_z)
|
||||
return parsed
|
||||
/**
|
||||
* Helper and recommened way to load a map file
|
||||
* - dmm_file: The path to the map file
|
||||
* - x_offset: The x offset to load the map at
|
||||
* - y_offset: The y offset to load the map at
|
||||
* - z_offset: The z offset to load the map at
|
||||
* - crop_map: If true, the map will be cropped to the world bounds
|
||||
* - measure_only: If true, the map will not be loaded, but the bounds will be calculated
|
||||
* - no_changeturf: If true, the map will not call /turf/AfterChange
|
||||
* - x_lower: The minimum x coordinate to load
|
||||
* - x_upper: The maximum x coordinate to load
|
||||
* - y_lower: The minimum y coordinate to load
|
||||
* - y_upper: The maximum y coordinate to load
|
||||
* - z_lower: The minimum z coordinate to load
|
||||
* - z_upper: The maximum z coordinate to load
|
||||
* - place_on_top: Whether to use /turf/proc/PlaceOnTop rather than /turf/proc/ChangeTurf
|
||||
* - new_z: If true, a new z level will be created for the map
|
||||
*/
|
||||
/proc/load_map(
|
||||
dmm_file,
|
||||
x_offset = 0,
|
||||
y_offset = 0,
|
||||
z_offset = 0,
|
||||
crop_map = FALSE,
|
||||
measure_only = FALSE,
|
||||
no_changeturf = FALSE,
|
||||
x_lower = -INFINITY,
|
||||
x_upper = INFINITY,
|
||||
y_lower = -INFINITY,
|
||||
y_upper = INFINITY,
|
||||
z_lower = -INFINITY,
|
||||
z_upper = INFINITY,
|
||||
place_on_top = FALSE,
|
||||
new_z = FALSE,
|
||||
)
|
||||
if(!(dmm_file in GLOB.cached_maps))
|
||||
GLOB.cached_maps[dmm_file] = new /datum/parsed_map(dmm_file)
|
||||
|
||||
var/datum/parsed_map/parsed_map = GLOB.cached_maps[dmm_file]
|
||||
parsed_map = parsed_map.copy()
|
||||
if(!measure_only && !isnull(parsed_map.bounds))
|
||||
parsed_map.load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
|
||||
return parsed_map
|
||||
|
||||
/// Parse a map, possibly cropping it.
|
||||
/datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, measureOnly=FALSE)
|
||||
/datum/parsed_map/New(tfile, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper=INFINITY, z_lower = -INFINITY, z_upper=INFINITY, measureOnly=FALSE)
|
||||
// This proc sleeps for like 6 seconds. why?
|
||||
// Is it file accesses? if so, can those be done ahead of time, async to save on time here? I wonder.
|
||||
// Love ya :)
|
||||
@@ -184,20 +230,26 @@
|
||||
CRASH("Coords before model definition in DMM")
|
||||
|
||||
var/curr_x = text2num(regexOutput[3])
|
||||
|
||||
if(curr_x < x_lower || curr_x > x_upper)
|
||||
continue
|
||||
|
||||
var/curr_y = text2num(regexOutput[4])
|
||||
if(curr_y < y_lower || curr_y > y_upper)
|
||||
continue
|
||||
|
||||
var/curr_z = text2num(regexOutput[5])
|
||||
if(curr_z < z_lower || curr_z > z_upper)
|
||||
continue
|
||||
|
||||
var/datum/grid_set/gridSet = new
|
||||
|
||||
gridSet.xcrd = curr_x
|
||||
//position of the currently processed square
|
||||
gridSet.ycrd = text2num(regexOutput[4])
|
||||
gridSet.zcrd = text2num(regexOutput[5])
|
||||
gridSet.ycrd = curr_y
|
||||
gridSet.zcrd = curr_z
|
||||
|
||||
bounds[MAP_MINX] = min(bounds[MAP_MINX], curr_x)
|
||||
bounds[MAP_MINZ] = min(bounds[MAP_MINZ], gridSet.zcrd)
|
||||
bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], gridSet.zcrd)
|
||||
bounds[MAP_MINZ] = min(bounds[MAP_MINZ], curr_y)
|
||||
bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], curr_z)
|
||||
|
||||
var/list/gridLines = splittext(regexOutput[6], "\n")
|
||||
gridSet.gridLines = gridLines
|
||||
@@ -238,16 +290,29 @@
|
||||
bounds[MAP_MAXX] = clamp(bounds[MAP_MAXX], x_lower, x_upper)
|
||||
bounds[MAP_MINY] = clamp(bounds[MAP_MINY], y_lower, y_upper)
|
||||
bounds[MAP_MAXY] = clamp(bounds[MAP_MAXY], y_lower, y_upper)
|
||||
bounds[MAP_MINZ] = clamp(bounds[MAP_MINZ], z_lower, z_upper)
|
||||
bounds[MAP_MAXZ] = clamp(bounds[MAP_MAXZ], z_lower, z_upper)
|
||||
|
||||
parsed_bounds = src.bounds
|
||||
src.key_len = key_len
|
||||
src.line_len = line_len
|
||||
|
||||
/// Load the parsed map into the world. See [/proc/load_map] for arguments.
|
||||
/datum/parsed_map/proc/load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, whitelist = FALSE, new_z)
|
||||
/// Iterates over all grid sets and returns ones with z values within the given bounds. Inclusive
|
||||
/datum/parsed_map/proc/filter_grid_sets_based_on_z_bounds(lower_z, upper_z)
|
||||
var/list/filtered_sets = list()
|
||||
for(var/datum/grid_set/grid_set as anything in gridSets)
|
||||
if(grid_set.zcrd < lower_z)
|
||||
continue
|
||||
if(grid_set.zcrd > upper_z)
|
||||
continue
|
||||
filtered_sets += grid_set
|
||||
return filtered_sets
|
||||
|
||||
/// Load the parsed map into the world. You probably want [/proc/load_map]. Keep the signature the same.
|
||||
/datum/parsed_map/proc/load(x_offset = 0, y_offset = 0, z_offset = 0, crop_map = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, z_lower = -INFINITY, z_upper = INFINITY, place_on_top = FALSE, new_z = FALSE)
|
||||
//How I wish for RAII
|
||||
Master.StartLoadingMap()
|
||||
. = _load_impl(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
|
||||
. = _load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
|
||||
Master.StopLoadingMap()
|
||||
|
||||
#define MAPLOADING_CHECK_TICK \
|
||||
@@ -262,7 +327,7 @@
|
||||
}
|
||||
|
||||
// Do not call except via load() above.
|
||||
/datum/parsed_map/proc/_load_impl(x_offset = 1, y_offset = 1, z_offset = world.maxz + 1, cropMap = FALSE, no_changeturf = FALSE, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, placeOnTop = FALSE, new_z = FALSE)
|
||||
/datum/parsed_map/proc/_load_impl(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
|
||||
PRIVATE_PROC(TRUE)
|
||||
// Tell ss atoms that we're doing maploading
|
||||
// We'll have to account for this in the following tick_checks so it doesn't overflow
|
||||
@@ -275,9 +340,9 @@
|
||||
var/sucessful = FALSE
|
||||
switch(map_format)
|
||||
if(MAP_TGM)
|
||||
sucessful = _tgm_load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
|
||||
sucessful = _tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
|
||||
else
|
||||
sucessful = _dmm_load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
|
||||
sucessful = _dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
|
||||
|
||||
// And we are done lads, call it off
|
||||
SSatoms.map_loader_stop(REF(src))
|
||||
@@ -309,7 +374,7 @@
|
||||
// In the tgm format, each gridset contains 255 lines, each line representing one tile, with 255 total gridsets
|
||||
// In the dmm format, each gridset contains 255 lines, each line representing one row of tiles, containing 255 * line length characters, with one gridset per z
|
||||
// You can think of dmm as storing maps in rows, whereas tgm stores them in columns
|
||||
/datum/parsed_map/proc/_tgm_load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
|
||||
/datum/parsed_map/proc/_tgm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
|
||||
// setup
|
||||
var/list/modelCache = build_cache(no_changeturf)
|
||||
var/space_key = modelCache[SPACE_KEY]
|
||||
@@ -330,12 +395,12 @@
|
||||
var/relative_y = first_column.ycrd
|
||||
var/highest_y = relative_y + y_relative_to_absolute
|
||||
|
||||
if(!cropMap && highest_y > world.maxy)
|
||||
if(!crop_map && highest_y > world.maxy)
|
||||
if(new_z)
|
||||
// Need to avoid improperly loaded area/turf_contents
|
||||
world.increaseMaxY(highest_y, max_zs_to_load = z_offset - 1)
|
||||
world.increase_max_y(highest_y, map_load_z_cutoff = z_offset - 1)
|
||||
else
|
||||
world.increaseMaxY(highest_y)
|
||||
world.increase_max_y(highest_y)
|
||||
expanded_y = TRUE
|
||||
|
||||
// Skip Y coords that are above the smallest of the three params
|
||||
@@ -345,7 +410,6 @@
|
||||
var/y_starting_skip = relative_y - y_skip_above
|
||||
highest_y -= y_starting_skip
|
||||
|
||||
|
||||
// Y is the LOWEST it will ever be here, so we can easily set a threshold for how low to go
|
||||
var/line_count = length(first_column.gridLines)
|
||||
var/lowest_y = relative_y - (line_count - 1) // -1 because we decrement at the end of the loop, not the start
|
||||
@@ -353,7 +417,7 @@
|
||||
|
||||
// X setup
|
||||
var/x_delta_with = x_upper
|
||||
if(cropMap)
|
||||
if(crop_map)
|
||||
// Take our smaller crop threshold yes?
|
||||
x_delta_with = min(x_delta_with, world.maxx)
|
||||
|
||||
@@ -367,33 +431,51 @@
|
||||
// If our relative x is greater then X upper, well then we've gotta limit our expansion
|
||||
var/delta = max(final_x - x_delta_with, 0)
|
||||
final_x -= delta
|
||||
if(final_x > world.maxx && !cropMap)
|
||||
if(final_x > world.maxx && !crop_map)
|
||||
if(new_z)
|
||||
// Need to avoid improperly loaded area/turf_contents
|
||||
world.increaseMaxX(final_x, max_zs_to_load = z_offset - 1)
|
||||
world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1)
|
||||
else
|
||||
world.increaseMaxX(final_x)
|
||||
world.increase_max_x(final_x)
|
||||
expanded_x = TRUE
|
||||
|
||||
var/lowest_x = max(x_lower, 1 - x_relative_to_absolute)
|
||||
|
||||
// Amount we offset the grid zcrd to get the true zcrd
|
||||
var/grid_z_offset = z_offset - 1
|
||||
var/z_upper_set = z_upper < INFINITY
|
||||
var/z_lower_set = z_lower > -INFINITY
|
||||
|
||||
// We make the assumption that the last block of turfs will have the highest embedded z in it
|
||||
var/highest_z = last_column.zcrd + z_offset - 1 // Lets not just make a new z level each time we increment maxz
|
||||
// true max zcrd
|
||||
var/map_bounds_z_max = last_column.zcrd
|
||||
var/z_upper_parsed = map_bounds_z_max + z_offset - 1
|
||||
if(z_upper_set)
|
||||
z_upper_parsed -= map_bounds_z_max - z_upper
|
||||
if(z_lower_set)
|
||||
var/offset_amount = z_lower - 1
|
||||
z_upper_parsed -= offset_amount
|
||||
grid_z_offset -= offset_amount
|
||||
|
||||
var/list/target_grid_sets = gridSets
|
||||
if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want
|
||||
target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper)
|
||||
|
||||
var/z_threshold = world.maxz
|
||||
if(highest_z > z_threshold && cropMap)
|
||||
for(var/i in z_threshold + 1 to highest_z) //create a new z_level if needed
|
||||
if(z_upper_parsed > z_threshold && crop_map)
|
||||
for(var/i in z_threshold + 1 to z_upper_parsed) //create a new z_level if needed
|
||||
world.incrementMaxZ()
|
||||
if(!no_changeturf)
|
||||
WARNING("Z-level expansion occurred without no_changeturf set, this may cause problems when /turf/AfterChange is called")
|
||||
|
||||
for(var/datum/grid_set/gset as anything in gridSets)
|
||||
for(var/datum/grid_set/gset as anything in target_grid_sets)
|
||||
var/true_xcrd = gset.xcrd + x_relative_to_absolute
|
||||
|
||||
// any cutoff of x means we just shouldn't iterate this gridset
|
||||
if(final_x < true_xcrd || lowest_x > gset.xcrd)
|
||||
continue
|
||||
|
||||
var/zcrd = gset.zcrd + z_offset - 1
|
||||
var/zcrd = gset.zcrd + grid_z_offset
|
||||
// If we're using changeturf, we disable it if we load into a z level we JUST created
|
||||
var/no_afterchange = no_changeturf || zcrd > z_threshold
|
||||
|
||||
@@ -420,7 +502,7 @@
|
||||
if(!cache)
|
||||
SSatoms.map_loader_stop(REF(src))
|
||||
CRASH("Undefined model key in DMM: [gset.gridLines[i]]")
|
||||
build_coordinate(cache, locate(true_xcrd, ycrd, zcrd), no_afterchange, placeOnTop, new_z)
|
||||
build_coordinate(cache, locate(true_xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z)
|
||||
|
||||
// only bother with bounds that actually exist
|
||||
if(!first_found)
|
||||
@@ -444,7 +526,7 @@
|
||||
/// Stanrdard loading, not used in production
|
||||
/// Doesn't take advantage of any tgm optimizations, which makes it slower but also more general
|
||||
/// Use this if for some reason your map format is messy
|
||||
/datum/parsed_map/proc/_dmm_load(x_offset, y_offset, z_offset, cropMap, no_changeturf, x_lower, x_upper, y_lower, y_upper, placeOnTop, new_z)
|
||||
/datum/parsed_map/proc/_dmm_load(x_offset, y_offset, z_offset, crop_map, no_changeturf, x_lower, x_upper, y_lower, y_upper, z_lower, z_upper, place_on_top, new_z)
|
||||
// setup
|
||||
var/list/modelCache = build_cache(no_changeturf)
|
||||
var/space_key = modelCache[SPACE_KEY]
|
||||
@@ -455,23 +537,46 @@
|
||||
var/y_relative_to_absolute = y_offset - 1
|
||||
var/x_relative_to_absolute = x_offset - 1
|
||||
var/line_len = src.line_len
|
||||
for(var/datum/grid_set/gset as anything in gridSets)
|
||||
|
||||
// Amount we offset the grid zcrd to get the true zcrd
|
||||
var/grid_z_offset = z_offset - 1
|
||||
var/z_upper_set = z_upper < INFINITY
|
||||
var/z_lower_set = z_lower > -INFINITY
|
||||
|
||||
// we now need to find the maximum z, fun!
|
||||
var/map_bounds_z_max = 1
|
||||
for(var/datum/grid_set/grid_set as anything in gridSets)
|
||||
map_bounds_z_max = max(map_bounds_z_max, grid_set.zcrd)
|
||||
|
||||
var/z_upper_parsed = map_bounds_z_max + z_offset - 1
|
||||
if(z_upper_set)
|
||||
z_upper_parsed -= map_bounds_z_max - z_upper
|
||||
if(z_lower_set)
|
||||
var/offset_amount = z_lower - 1
|
||||
z_upper_parsed -= offset_amount
|
||||
grid_z_offset -= offset_amount
|
||||
|
||||
var/list/target_grid_sets = gridSets
|
||||
if(z_lower_set || z_upper_set) // bounds are set, filter out gridsets for z levels we don't want
|
||||
target_grid_sets = filter_grid_sets_based_on_z_bounds(z_lower, z_upper)
|
||||
|
||||
for(var/datum/grid_set/gset as anything in target_grid_sets)
|
||||
var/relative_x = gset.xcrd
|
||||
var/relative_y = gset.ycrd
|
||||
var/true_xcrd = relative_x + x_relative_to_absolute
|
||||
var/ycrd = relative_y + y_relative_to_absolute
|
||||
var/zcrd = gset.zcrd + z_offset - 1
|
||||
if(!cropMap && ycrd > world.maxy)
|
||||
var/zcrd = gset.zcrd + grid_z_offset
|
||||
if(!crop_map && ycrd > world.maxy)
|
||||
if(new_z)
|
||||
// Need to avoid improperly loaded area/turf_contents
|
||||
world.increaseMaxY(ycrd, max_zs_to_load = z_offset - 1)
|
||||
world.increase_max_y(ycrd, map_load_z_cutoff = z_offset - 1)
|
||||
else
|
||||
world.increaseMaxY(ycrd)
|
||||
world.increase_max_y(ycrd)
|
||||
expanded_y = TRUE
|
||||
var/zexpansion = zcrd > world.maxz
|
||||
var/no_afterchange = no_changeturf
|
||||
if(zexpansion)
|
||||
if(cropMap)
|
||||
if(crop_map)
|
||||
continue
|
||||
else
|
||||
while (zcrd > world.maxz) //create a new z_level if needed
|
||||
@@ -508,7 +613,7 @@
|
||||
var/x_step_count = ROUND_UP(x_target / key_len)
|
||||
var/final_x = relative_x + (x_step_count - 1)
|
||||
var/x_delta_with = x_upper
|
||||
if(cropMap)
|
||||
if(crop_map)
|
||||
// Take our smaller crop threshold yes?
|
||||
x_delta_with = min(x_delta_with, world.maxx)
|
||||
if(final_x > x_delta_with)
|
||||
@@ -517,12 +622,12 @@
|
||||
x_step_count -= delta
|
||||
final_x -= delta
|
||||
x_target = x_step_count * key_len
|
||||
if(final_x > world.maxx && !cropMap)
|
||||
if(final_x > world.maxx && !crop_map)
|
||||
if(new_z)
|
||||
// Need to avoid improperly loaded area/turf_contents
|
||||
world.increaseMaxX(final_x, max_zs_to_load = z_offset - 1)
|
||||
world.increase_max_x(final_x, map_load_z_cutoff = z_offset - 1)
|
||||
else
|
||||
world.increaseMaxX(final_x)
|
||||
world.increase_max_x(final_x)
|
||||
expanded_x = TRUE
|
||||
|
||||
// We're gonna track the first and last pairs of coords we find
|
||||
@@ -553,7 +658,7 @@
|
||||
if(!cache)
|
||||
SSatoms.map_loader_stop(REF(src))
|
||||
CRASH("Undefined model key in DMM: [model_key]")
|
||||
build_coordinate(cache, locate(xcrd, ycrd, zcrd), no_afterchange, placeOnTop, new_z)
|
||||
build_coordinate(cache, locate(xcrd, ycrd, zcrd), no_afterchange, place_on_top, new_z)
|
||||
|
||||
// only bother with bounds that actually exist
|
||||
if(!first_found)
|
||||
|
||||
Reference in New Issue
Block a user