mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 18:22:14 +00:00
Fermichem 2.6 - Adds the HPLC: a new roundstart method of detecting purity, as well as a method of partially purifying reagents (#57858)
Adds the High-performance liquid chromatography machine - a new roundstart method of detecting purity, as well as a method of partially purifying reagents. A single machine has been added to each of the 4(5) maps - this lets people determine their purity based off the size of the green (pure portion) to the orange (impure portion) of a reagent's peak shown on the mas spectroscopy display. If a reagent is impure - it will be displayed as red. In addition to this function, the HPLC can purify reagents by selecting a range of them and pressing the purify button. This will cost some time depending on the mass of the reagent and will purify it up it's default purity (in the tweaked reagent cases - 75%, for the reagents I've yet to get to - 100%). It will also reduce the volume accordingly (so in essence you are reducing the volume of the impure parts). The sprite itself will indicate when it's running, so you don't need to be nearby it or use the UI to know when it's done
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
if(path in GLOB.fake_reagent_blacklist)
|
||||
continue
|
||||
var/datum/reagent/D = new path()
|
||||
D.mass = rand(10, 800) //This is terrible and should be removed ASAP!
|
||||
GLOB.chemical_reagents_list[path] = D
|
||||
|
||||
/proc/build_chemical_reactions_lists()
|
||||
|
||||
366
code/modules/reagents/chemistry/machinery/chem_mass_spec.dm
Normal file
366
code/modules/reagents/chemistry/machinery/chem_mass_spec.dm
Normal file
@@ -0,0 +1,366 @@
|
||||
|
||||
#define BEAKER1 1
|
||||
#define BEAKER2 2
|
||||
|
||||
/obj/machinery/chem_mass_spec
|
||||
name = "High-performance liquid chromatography machine"
|
||||
desc = {"This machine can separate reagents based on charge, meaning it can clean reagents of some of their impurities, unlike the Chem Master 3000.
|
||||
By selecting a range in the mass spectrograph certain reagents will be transferred from one beaker to another, which will clean it of any impurities up to a certain amount.
|
||||
This will not clean any inverted reagents. Inverted reagents will still be correctly detected and displayed on the scanner, however.
|
||||
\nLeft click with a beaker to add it to the input slot, Right click with a beaker to add it to the output slot. Alt + left/right click can let you quickly remove the corrisponding beaker too."}
|
||||
density = TRUE
|
||||
layer = BELOW_OBJ_LAYER
|
||||
icon = 'icons/obj/chemical.dmi'
|
||||
icon_state = "HPLC"
|
||||
base_icon_state = "HPLC"
|
||||
use_power = IDLE_POWER_USE
|
||||
idle_power_usage = 20
|
||||
resistance_flags = FIRE_PROOF | ACID_PROOF
|
||||
///If we're processing reagents or not
|
||||
var/processing_reagents = FALSE
|
||||
///Time we started processing + the delay
|
||||
var/delay_time = 0
|
||||
///How much time we've done so far
|
||||
var/progress_time = 0
|
||||
///Lower mass range - for mass selection of what will be processed
|
||||
var/lower_mass_range = 0
|
||||
///Upper_mass_range - for mass selection of what will be processed
|
||||
var/upper_mass_range = INFINITY
|
||||
///The log output to clarify how the thing works
|
||||
var/list/log = list()
|
||||
///Input reagents container
|
||||
var/obj/item/reagent_containers/beaker1
|
||||
///Output reagents container
|
||||
var/obj/item/reagent_containers/beaker2
|
||||
|
||||
/obj/machinery/chem_mass_spec/Initialize()
|
||||
. = ..()
|
||||
beaker2 = new /obj/item/reagent_containers/glass/beaker/large(src)
|
||||
ADD_TRAIT(src, DO_NOT_SPLASH, src.type)
|
||||
|
||||
/obj/machinery/chem_mass_spec/Destroy()
|
||||
QDEL_NULL(beaker1)
|
||||
QDEL_NULL(beaker2)
|
||||
return ..()
|
||||
|
||||
/* beaker swapping/attack code */
|
||||
|
||||
///Adds beaker 1
|
||||
/obj/machinery/chem_mass_spec/attackby(obj/item/item, mob/user, params)
|
||||
if(processing_reagents)
|
||||
to_chat(user, "<span class='notice'> The [src] is currently processing a batch!")
|
||||
return ..()
|
||||
if(istype(item, /obj/item/reagent_containers) && !(item.item_flags & ABSTRACT) && item.is_open_container())
|
||||
var/obj/item/reagent_containers/beaker = item
|
||||
. = TRUE //no afterattack
|
||||
if(!user.transferItemToLoc(beaker, src))
|
||||
return
|
||||
replace_beaker(user, BEAKER1, beaker)
|
||||
to_chat(user, "<span class='notice'>You add [beaker] to [src].</span>")
|
||||
updateUsrDialog()
|
||||
update_appearance()
|
||||
..()
|
||||
|
||||
///Adds beaker 2
|
||||
/obj/machinery/chem_mass_spec/attackby_secondary(obj/item/item, mob/user, params)
|
||||
if(processing_reagents)
|
||||
to_chat(user, "<span class='notice'> The [src] is currently processing a batch!")
|
||||
return
|
||||
if(istype(item, /obj/item/reagent_containers) && !(item.item_flags & ABSTRACT) && item.is_open_container())
|
||||
var/obj/item/reagent_containers/beaker = item
|
||||
if(!user.transferItemToLoc(beaker, src))
|
||||
return
|
||||
replace_beaker(user, BEAKER2, beaker)
|
||||
to_chat(user, "<span class='notice'>You add [beaker] to [src].</span>")
|
||||
updateUsrDialog()
|
||||
update_appearance()
|
||||
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
|
||||
|
||||
/obj/machinery/chem_mass_spec/AltClick(mob/living/user)
|
||||
. = ..()
|
||||
if(processing_reagents)
|
||||
to_chat(user, "<span class='notice'> The [src] is currently processing a batch!")
|
||||
return
|
||||
if(!can_interact(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
|
||||
return ..()
|
||||
replace_beaker(user, BEAKER1)
|
||||
|
||||
/obj/machinery/chem_mass_spec/alt_click_secondary(mob/living/user)
|
||||
. = ..()
|
||||
if(processing_reagents)
|
||||
to_chat(user, "<span class='notice'> The [src] is currently processing a batch!")
|
||||
return
|
||||
if(!can_interact(user) || !user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
|
||||
return
|
||||
replace_beaker(user, BEAKER2)
|
||||
|
||||
///Gee how come you get two beakers?
|
||||
/*
|
||||
* Similar to other replace beaker procs, except now there are two of them!
|
||||
* When passed a beaker along with a position define it will swap a beaker in that slot (if there is one) with the beaker the machine is bonked with
|
||||
*
|
||||
* arguments:
|
||||
* * user - The one bonking the machine
|
||||
* * target beaker - the define (BEAKER1/BEAKER2) of what position to replace
|
||||
* * new beaker - the new beaker to add/replace the slot with
|
||||
*/
|
||||
/obj/machinery/chem_mass_spec/proc/replace_beaker(mob/living/user, target_beaker, obj/item/reagent_containers/new_beaker)
|
||||
if(!user)
|
||||
return FALSE
|
||||
switch(target_beaker)
|
||||
if(BEAKER1)
|
||||
if(beaker1)
|
||||
try_put_in_hand(beaker1, user)
|
||||
beaker1 = null
|
||||
beaker1 = new_beaker
|
||||
lower_mass_range = calculate_smallest_mass()
|
||||
upper_mass_range = calculate_largest_mass()
|
||||
if(BEAKER2)
|
||||
if(beaker2)
|
||||
try_put_in_hand(beaker2, user)
|
||||
beaker2 = null
|
||||
beaker2 = new_beaker
|
||||
update_appearance()
|
||||
return TRUE
|
||||
|
||||
/* Icon code */
|
||||
|
||||
/obj/machinery/chem_mass_spec/update_icon_state()
|
||||
if(powered())
|
||||
icon_state = "HPLC_on"
|
||||
else
|
||||
icon_state = "HPLC"
|
||||
return ..()
|
||||
|
||||
/obj/machinery/chem_mass_spec/update_overlays()
|
||||
. = ..()
|
||||
if(beaker1)
|
||||
. += "HPLC_beaker1"
|
||||
if(beaker2)
|
||||
. += "HPLC_beaker2"
|
||||
if(powered())
|
||||
if(processing_reagents)
|
||||
. += "HPLC_graph_active"
|
||||
else if (length(beaker1?.reagents.reagent_list))
|
||||
. += "HPLC_graph_idle"
|
||||
|
||||
/* UI Code */
|
||||
|
||||
/obj/machinery/chem_mass_spec/ui_interact(mob/user, datum/tgui/ui)
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "MassSpec", name)
|
||||
ui.open()
|
||||
|
||||
/obj/machinery/chem_mass_spec/ui_data(mob/user)
|
||||
var/data = list()
|
||||
data["graphLowerRange"] = 0
|
||||
data["lowerRange"] = lower_mass_range
|
||||
data["upperRange"] = upper_mass_range
|
||||
data["processing"] = processing_reagents
|
||||
data["log"] = log
|
||||
data["beaker1"] = beaker1 ? TRUE : FALSE
|
||||
data["beaker2"] = beaker2 ? TRUE : FALSE
|
||||
if(processing_reagents)
|
||||
data["eta"] = delay_time - progress_time
|
||||
else
|
||||
data["eta"] = estimate_time()
|
||||
|
||||
var/beakerContents[0]
|
||||
if(beaker1 && beaker1.reagents)
|
||||
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
|
||||
var/in_range = TRUE
|
||||
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
|
||||
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
|
||||
if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
|
||||
in_range = FALSE
|
||||
beakerContents.Add(list(list("name" = inverse_reagent.name, "volume" = round(reagent.volume, 0.01), "mass" = inverse_reagent.mass, "purity" = 1-reagent.purity, "selected" = in_range, "color" = "#b60046", "type" = "Inverted")))
|
||||
data["peakHeight"] = max(data["peakHeight"], reagent.volume)
|
||||
continue
|
||||
if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
|
||||
in_range = FALSE
|
||||
///We want to be sure that the impure chem appears after the parent chem in the list so that it always overshadows pure reagents
|
||||
beakerContents.Add(list(list("name" = reagent.name, "volume" = round(reagent.volume * reagent.purity, 0.01), "mass" = reagent.mass, "purity" = reagent.purity, "selected" = in_range, "color" = "#3cf096", "type" = "Clean")))
|
||||
if(1 > reagent.purity && reagent.impure_chem)
|
||||
var/datum/reagent/impure_reagent = GLOB.chemical_reagents_list[reagent.impure_chem]
|
||||
beakerContents.Add(list(list("name" = impure_reagent.name, "volume" = round(reagent.volume * (1-reagent.purity), 0.01), "mass" = reagent.mass, "purity" = 1-reagent.purity, "selected" = in_range, "color" = "#fc9738", "type" = "Impurity")))
|
||||
data["peakHeight"] = max(data["peakHeight"], reagent.volume * (1-reagent.purity))
|
||||
data["peakHeight"] = max(data["peakHeight"], reagent.volume * reagent.purity)
|
||||
|
||||
data["beaker1CurrentVolume"] = beaker1.reagents.total_volume
|
||||
data["beaker1MaxVolume"] = beaker1.reagents.maximum_volume
|
||||
data["beaker1Contents"] = beakerContents
|
||||
data["graphUpperRange"] = calculate_largest_mass() //+10 because of the range on the peak
|
||||
|
||||
beakerContents = list()
|
||||
if(beaker2 && beaker2.reagents)
|
||||
for(var/datum/reagent/reagent in beaker2.reagents.reagent_list)
|
||||
///Normal stuff
|
||||
beakerContents.Add(list(list("name" = reagent.name, "volume" = round(reagent.volume * reagent.purity, 0.01), "mass" = reagent.mass, "purity" = reagent.purity, "color" = "#3cf096", "type" = "Clean", log = log[reagent.type])))
|
||||
///Impure stuff
|
||||
if(1 > reagent.purity && reagent.impure_chem)
|
||||
var/datum/reagent/impure_reagent = GLOB.chemical_reagents_list[reagent.impure_chem]
|
||||
beakerContents.Add(list(list("name" = impure_reagent.name, "volume" = round(reagent.volume * (1-reagent.purity), 0.01), "mass" = reagent.mass, "purity" = 1-reagent.purity, "color" = "#fc9738", "type" = "Impurity")))
|
||||
data["beaker2CurrentVolume"] = beaker2.reagents.total_volume
|
||||
data["beaker2MaxVolume"] = beaker2.reagents.maximum_volume
|
||||
data["beaker2Contents"] = beakerContents
|
||||
|
||||
return data
|
||||
|
||||
/obj/machinery/chem_mass_spec/ui_act(action, params)
|
||||
. = ..()
|
||||
if(.)
|
||||
return
|
||||
switch(action)
|
||||
if("activate")
|
||||
if(!beaker1 || !beaker2 || !is_operational)
|
||||
say("This [src] is missing an output beaker!")
|
||||
return
|
||||
if(processing_reagents)
|
||||
say("You shouldn't be seeing this message! Please report this bug to https://github.com/tgstation/tgstation/issues . Thank you!")
|
||||
stack_trace("Someone managed to break the HPLC and tried to get it to activate when it's already activated!")
|
||||
return
|
||||
processing_reagents = TRUE
|
||||
estimate_time()
|
||||
progress_time = 0
|
||||
update_appearance()
|
||||
begin_processing()
|
||||
. = TRUE
|
||||
if("leftSlider")
|
||||
if(!is_operational || processing_reagents)
|
||||
return
|
||||
var/current_center = (lower_mass_range + upper_mass_range)/2
|
||||
lower_mass_range = clamp(params["value"], calculate_smallest_mass(), current_center)
|
||||
. = TRUE
|
||||
if("rightSlider")
|
||||
if(!is_operational || processing_reagents)
|
||||
return
|
||||
var/current_center = (lower_mass_range + upper_mass_range)/2
|
||||
upper_mass_range = clamp(params["value"], current_center, calculate_largest_mass())
|
||||
. = TRUE
|
||||
if("centerSlider")
|
||||
if(!is_operational || processing_reagents)
|
||||
return
|
||||
var/current_center = (lower_mass_range + upper_mass_range)/2
|
||||
var/delta_center = current_center - params["value"]
|
||||
var/lowest = calculate_smallest_mass()
|
||||
var/highest = calculate_largest_mass()
|
||||
lower_mass_range = clamp(lower_mass_range - delta_center, lowest, highest)
|
||||
upper_mass_range = clamp(upper_mass_range - delta_center, lowest, highest)
|
||||
. = TRUE
|
||||
if("eject1")
|
||||
if(processing_reagents)
|
||||
return
|
||||
replace_beaker(usr, BEAKER1)
|
||||
. = TRUE
|
||||
if("eject2")
|
||||
if(processing_reagents)
|
||||
return
|
||||
replace_beaker(usr, BEAKER2)
|
||||
. = TRUE
|
||||
|
||||
/* processing procs */
|
||||
|
||||
///Increments time if it's progressing - if it's past time then it purifies and stops processing
|
||||
/obj/machinery/chem_mass_spec/process(delta_time)
|
||||
. = ..()
|
||||
if(!is_operational)
|
||||
return FALSE
|
||||
if(!processing_reagents)
|
||||
return TRUE
|
||||
if(progress_time >= delay_time)
|
||||
processing_reagents = FALSE
|
||||
progress_time = 0
|
||||
purify_reagents()
|
||||
end_processing()
|
||||
update_appearance()
|
||||
return TRUE
|
||||
progress_time += delta_time
|
||||
return FALSE
|
||||
|
||||
/*
|
||||
* Processing through the reagents in beaker 1
|
||||
* For all the reagents within the selected range - we will then purify them up to their initial purity (usually 75%). It will take away the relative reagent volume from the sum volume of the reagent however.
|
||||
* If there are any inverted reagents - then it will instead just create a new reagent of the inverted type. This doesn't really do anything other than change the name of it,
|
||||
* As it processes through the reagents, it saves what changes were applied to each reagent in a log var to show the results at the end
|
||||
*/
|
||||
/obj/machinery/chem_mass_spec/proc/purify_reagents()
|
||||
log = list()
|
||||
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
|
||||
//Inverse first
|
||||
var/volume = reagent.volume
|
||||
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
|
||||
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
|
||||
if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
|
||||
continue
|
||||
log += list(inverse_reagent.type = "Cannot purify inverted") //Might as well make it do something - just updates the reagent's name
|
||||
beaker2.reagents.add_reagent(reagent.inverse_chem, volume, reagtemp = beaker1.reagents.chem_temp, added_purity = 1-reagent.purity)
|
||||
beaker1.reagents.remove_reagent(reagent.type, volume)
|
||||
continue
|
||||
|
||||
if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
|
||||
continue
|
||||
|
||||
var/delta_purity = initial(reagent.purity) - reagent.purity
|
||||
if(delta_purity <= 0)//As pure as we can be - so lets not add more than we need
|
||||
log += list(reagent.type = "Can't purify over [initial(reagent.purity)*100]%")
|
||||
beaker2.reagents.add_reagent(reagent.type, volume, reagtemp = beaker1.reagents.chem_temp, added_purity = reagent.purity, added_ph = reagent.ph)
|
||||
beaker1.reagents.remove_reagent(reagent.type, volume)
|
||||
continue
|
||||
|
||||
var/product_vol = reagent.volume * (1-delta_purity)
|
||||
beaker2.reagents.add_reagent(reagent.type, product_vol, reagtemp = beaker1.reagents.chem_temp, added_purity = initial(reagent.purity), added_ph = reagent.ph)
|
||||
beaker1.reagents.remove_reagent(reagent.type, reagent.volume)
|
||||
log += list(reagent.type = "Purified to [initial(reagent.purity)*100]%")
|
||||
|
||||
/* Mass spec graph calcs */
|
||||
|
||||
///Returns the largest mass to the nearest 50 (rounded up)
|
||||
/obj/machinery/chem_mass_spec/proc/calculate_largest_mass()
|
||||
if(!beaker1?.reagents)
|
||||
return 0
|
||||
var/max_mass = 0
|
||||
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
|
||||
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
|
||||
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
|
||||
max_mass = max(max_mass, inverse_reagent.mass)
|
||||
continue
|
||||
max_mass = max(max_mass, reagent.mass)
|
||||
return CEILING(max_mass, 50)
|
||||
|
||||
///Returns the smallest mass to the nearest 50 (rounded down)
|
||||
/obj/machinery/chem_mass_spec/proc/calculate_smallest_mass()
|
||||
if(!beaker1?.reagents)
|
||||
return 0
|
||||
var/min_mass = 0
|
||||
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
|
||||
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
|
||||
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
|
||||
min_mass = min(min_mass, inverse_reagent.mass)
|
||||
continue
|
||||
min_mass = min(min_mass, reagent.mass)
|
||||
return FLOOR(min_mass, 50)
|
||||
|
||||
/*
|
||||
* Estimates how long the highlighted range will take to process
|
||||
* The time will increase based off the reagent's volume, mass and purity.
|
||||
* In most cases this is between 10 to 30s for a single reagent.
|
||||
* This is why having a higher mass for a reagent is a balancing tool.
|
||||
*/
|
||||
/obj/machinery/chem_mass_spec/proc/estimate_time()
|
||||
if(!beaker1?.reagents)
|
||||
return 0
|
||||
var/time = 0
|
||||
for(var/datum/reagent/reagent as anything in beaker1.reagents.reagent_list)
|
||||
if(reagent.inverse_chem_val > reagent.purity && reagent.inverse_chem)
|
||||
var/datum/reagent/inverse_reagent = GLOB.chemical_reagents_list[reagent.inverse_chem]
|
||||
if(inverse_reagent.mass < lower_mass_range || inverse_reagent.mass > upper_mass_range)
|
||||
continue
|
||||
time += (((inverse_reagent.mass * reagent.volume) + (inverse_reagent.mass * reagent.purity * 0.1)) * 0.003) + 10 ///Roughly 10 - 30s?
|
||||
continue
|
||||
if(reagent.mass < lower_mass_range || reagent.mass > upper_mass_range)
|
||||
continue
|
||||
var/inverse_purity = 1-reagent.purity
|
||||
time += (((reagent.mass * reagent.volume) + (reagent.mass * inverse_purity * 0.1)) * 0.0035) + 10 ///Roughly 10 - 30s?
|
||||
delay_time = time
|
||||
return delay_time
|
||||
@@ -6,7 +6,7 @@
|
||||
name = "chemical reaction tester"
|
||||
density = TRUE
|
||||
icon = 'icons/obj/chemical.dmi'
|
||||
icon_state = "HPLC"
|
||||
icon_state = "HPLC_debug"
|
||||
use_power = IDLE_POWER_USE
|
||||
idle_power_usage = 40
|
||||
resistance_flags = FIRE_PROOF | ACID_PROOF | INDESTRUCTIBLE
|
||||
|
||||
@@ -52,6 +52,8 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
|
||||
var/purity = 1
|
||||
///the purity of the reagent on creation (i.e. when it's added to a mob and it's purity split it into 2 chems; the purity of the resultant chems are kept as 1, this tracks what the purity was before that)
|
||||
var/creation_purity = 1
|
||||
///The molar mass of the reagent - if you're adding a reagent that doesn't have a recipe, just add a random number between 10 - 800. Higher numbers are "harder" but it's mostly arbitary.
|
||||
var/mass
|
||||
/// color it looks in containers etc
|
||||
var/color = "#000000" // rgb: 0, 0, 0
|
||||
///how fast the reagent is metabolized by the mob
|
||||
@@ -97,6 +99,7 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
|
||||
///The amount a robot will pay for a glass of this (20 units but can be higher if you pour more, be frugal!)
|
||||
var/glass_price
|
||||
|
||||
|
||||
/datum/reagent/New()
|
||||
SHOULD_CALL_PARENT(TRUE)
|
||||
. = ..()
|
||||
@@ -105,6 +108,8 @@ GLOBAL_LIST_INIT(name2reagent, build_name2reagent())
|
||||
material = GET_MATERIAL_REF(material)
|
||||
if(glass_price)
|
||||
AddElement(/datum/element/venue_price, glass_price)
|
||||
if(!mass)
|
||||
mass = rand(10, 800)
|
||||
|
||||
/datum/reagent/Destroy() // This should only be called by the holder, so it's already handled clearing its references
|
||||
. = ..()
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
return
|
||||
|
||||
/obj/item/reagent_containers/pre_attack_secondary(atom/target, mob/living/user, params)
|
||||
if(HAS_TRAIT(target, DO_NOT_SPLASH))
|
||||
return ..()
|
||||
if (try_splash(user, target))
|
||||
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
|
||||
|
||||
|
||||
Reference in New Issue
Block a user