From ff7ee72146e20eb08e20d437428e0c3d3e31f49b Mon Sep 17 00:00:00 2001 From: Joshua Kidder <49173900+Metekillot@users.noreply.github.com> Date: Sun, 27 Jul 2025 01:17:51 -0400 Subject: [PATCH] Ore silo logging & access refactor; UI updates, single-target bans, more robust logging information, more robust access control (#91142) ## About The Pull Request # More robust logging ## Ore silo logs have now been refactored in the UI to display: - Number of sheets is now the relative unit when displaying a given log. - Instead of `100 iron` being displayed when removing one sheet, it just says `1 iron` - Instead of `25 iron` being displayed when using a quarter sheet, it just says `0.25 iron` - All information from ID_DATA(log_user) now sent to tgui backend - The items rendered to ore silo users are: - Name on ID, job on ID - If the ID's bank account (if one is registered) is one of the ore silo's banned users - If the user for a given entry was wearing a chameleon card, they will always appear unbanned - NOTE: The bank account ID # is (currently) not shown to players using the ore silo. - Full log information is rendered within a dropdown; the dropdown one-liner shows - Action (deposit, eject, item created) - Amount (deposit/eject? amount of material used. item created? number of items created.) - Either name of material (if deposit/eject) or the name of items crafted - The name of the user who performed a given operation (if wearing an unbanned ID) or ID_READ_FAILURE (if ore silo ID requirement has been disabled and the person is not wearing an ID) - As name, but instead, the job of the ID (or ID_READ_FAILURE) ![image](https://github.com/user-attachments/assets/bb992a81-8db3-4a92-ad3a-917239fc407a) # Access control improvements ## Single-user bans - Anyone with QM access (not silicons) can now ban/unban a user from a given log from using the ore silo - Bans are associated to bank account IDs. - Wearers of chameleon cards bypass any ban restrictions. - Anyone with QM access on their worn card bypasses ban restrictions. - Silicons bypass ban restrictions. - QM access requirement is removed if the ore silo is emagged. - Silicons can ban/unban people if the ore silo is emagged. ## Worn ID requirement - Enabled at roundstart, can be disabled by anyone with QM access (not silicons) - If enabled, you must be wearing an ID with a bank account associated to it to use ore silo materials. - Wearers of chameleon cards bypass this restriction (so-called ID requirement free thinkers wearing chameleon ID cards) - QM access requirement to toggle removed if emagged. - Silicons can toggle this on/off if the ore silo is emagged. # Access control radio notifications ## Access control operations reported on the radio - Any operations for access control are reported on radio channels. - Currently, the policy is always the default. - In the future, the ore silo UI will allow the quartermaster to modify what operations are reported on what channels (petty QM broadcasting ban reports on Common) - Current default policy: - Reported on COMMON channel: - Anyone but the Captain attempts to ban someone with QM access from the ore silo (nice try dumbass) - Ore silo ID requirement toggling - Reported on COMMAND channel: - Common channel reports. - Per-user banning/unbanning. - Anyone without QM access attempts to ban/unban someone. - Anyone without QM access attempts to toggle the restriction for ID. - Silicons attempting to tamper with the ID requirement restriction - Silicons attempting to tamper with the ban/unbanned user list - A ban attempt failing because a given log entry had a user with no bank ID. - Reported on SECURITY channel: - Common channel reports. - Per-user banning/unbanning. - Anyone without QM access tampering with the silo. - Reported on SUPPLY channel: - Command channel reports. - Reporting to the radio is disabled if the ore silo is emagged. ## Modifications to the remote_materials component - Strictly encompass the behavior for connecting/disconnecting ore silos to material receptacles (RCDs, machines, etc) into procs on the component, instead of handling it all over the place ## Why It's Good For The Game Gives people with ore silo access more fine grained control over ore silo use without having to resort to heavy-handed fabricator lockouts Makes the logging on the ore silo more robust so we can make sure we kill the right Roboticist for using all the materials Offers an avenue for sidestepping all of this with a chameleon card or emag if a given traitor (organic or otherwise) is particularly opposed to DRM mats. https://github.com/user-attachments/assets/effd2c63-509c-4d33-992f-837a0d62b935 ## Changelog :cl: Bisar add: The ore silo has had a significant expansion to its logging capabilities. add: The ore silo now allows any ID with Quartermaster access (NOT SILICONS!) to ban/unban specific users from the silo. add: The ore silo now has a toggle (on by default) that a user of ore silo materials has an ID with an associated bank account. This can be toggled by anyone with Quartermaster access (NOT SILICONS!) add: NanoTrasen discounts any reports that Syndicate contraband (cryptographic sequencer, agent card) can be used to circumvent any protocols instituted on the ore silo access control routines. add: The ore silo now announces operations to ban/unban users and enable/disable ID requirements on radio channels (check the Github for actions reported to what channels.) add: The tgui interface for ore silo log entries has been reworked. refactor: The code for logging a given access to ore silo materials has been significantly refactored. qol: Ore silo log entries now display materials spent in terms of sheets rather than the obfuscated absolute-units previously display (1 iron ejected instead of -100 iron, 0.25 used in a craft instead of -25 iron) /:cl: --- code/__DEFINES/dcs/signals/signals_object.dm | 6 + .../subsystem/networks/id_access.dm | 69 ++-- .../components/material/material_container.dm | 20 +- .../components/material/remote_materials.dm | 47 +-- code/game/machinery/_machinery.dm | 3 +- code/game/objects/items/rcd/RHD.dm | 10 +- code/modules/mining/machine_redemption.dm | 13 +- code/modules/mining/machine_silo.dm | 356 +++++++++++++++++- code/modules/mob/living/living.dm | 2 +- .../mob/living/silicon/robot/robot_model.dm | 2 +- .../modules/research/machinery/_production.dm | 23 +- .../modules/vehicles/mecha/mech_fabricator.dm | 27 +- .../modules/wiremod/core/component_printer.dm | 23 +- .../wiremod/core/integrated_circuit.dm | 2 +- tgui/packages/tgui/interfaces/OreSilo.tsx | 167 ++++++-- .../tgui/styles/interfaces/OreSilo.scss | 23 ++ tgui/packages/tgui/styles/main.scss | 1 + 17 files changed, 631 insertions(+), 163 deletions(-) create mode 100644 tgui/packages/tgui/styles/interfaces/OreSilo.scss diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 06e41ec007e..997e4fb88e0 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -47,6 +47,12 @@ /// Can't use tools on the machine #define COMPONENT_CANT_USE_MACHINE_TOOLS (1<<1) +#define COMSIG_ORE_SILO_PERMISSION_CHECKED "ore_silo_permission_checked" + /// The ore silo is not allowed to be used + #define COMPONENT_ORE_SILO_DENY (1<<0) + /// The ore silo is allowed to be used + #define COMPONENT_ORE_SILO_ALLOW (1<<1) + ///from obj/machinery/iv_drip/IV_attach(target, usr) : (attachee) #define COMSIG_IV_ATTACH "iv_attach" ///from obj/machinery/iv_drip/IV_detach() : (detachee) diff --git a/code/controllers/subsystem/networks/id_access.dm b/code/controllers/subsystem/networks/id_access.dm index 027ab454e42..5215de4a6b2 100644 --- a/code/controllers/subsystem/networks/id_access.dm +++ b/code/controllers/subsystem/networks/id_access.dm @@ -534,62 +534,63 @@ SUBSYSTEM_DEF(id_access) ) as /alist var/alist/returned_record = alist( - "Name" = null, - "Age" = null, - "Assignment" = null, - "Account ID" = null, - "Account Holder" = null, - "Account Assignment" = null, - "Accesses" = null, + "name" = null, + "age" = null, + "assignment" = null, + "account_id" = null, + "account_holder" = null, + "account_assignment" = null, + "accesses" = null, ) . = returned_record if(isnull(target_of_record)) - .["Name"] = ID_READ_FAILURE - .["Age"] = ID_READ_FAILURE - .["Assignment"] = ID_READ_FAILURE - .["Account ID"] = ID_READ_FAILURE - .["Account Holder"] = ID_READ_FAILURE - .["Account Assignment"] = ID_READ_FAILURE + .["name"] = ID_READ_FAILURE + .["age"] = ID_READ_FAILURE + .["assignment"] = ID_READ_FAILURE + .["account_id"] = ID_READ_FAILURE + .["account_holder"] = ID_READ_FAILURE + .["account_assignment"] = ID_READ_FAILURE + .["accesses"] = ID_READ_FAILURE .[ID_READ_FAILURE] = ID_READ_FAILURE return . var/mob/living/target = astype(target_of_record, /mob/living) if(target) - if(!issilicon(target)) + if(!issilicon(target) && !isdrone(target)) . = __in_character_record_id_information(astype(target.get_idcard(), /obj/item/card/id/advanced)) return . - .["Name"] = target.name - .["Age"] = "INSPECT MANUFACTURER MANIFEST" - .["Account ID"] = 0 - .["Account Holder"] = "NO ACCOUNT." - .["Account Assignment"] = "NO ACCOUNT." - .["Bank Account"] = "N/A" - .["Assignment"] = target.mind?.assigned_role?.title + .["name"] = target.name + .["age"] = "INSPECT MANUFACTURER MANIFEST" + .["assignment"] = 0 + .["account_id"] = "NO ACCOUNT." + .["account_holder"] = "NO ACCOUNT." + .["account_assignment"] = "N/A" + .["accesses"] = target.mind?.assigned_role?.title .[SILICON_OVERRIDE] = SILICON_OVERRIDE return . var/obj/item/card/id/advanced/id_card = astype(target_of_record, /obj/item/card/id/advanced) if(id_card) - .["Name"] = id_card.registered_name || "Unknown" - .["Age"] = id_card.registered_age || "Unknown" - .["Assignment"] = id_card.assignment || "Unassigned" - .["Accesses"] = id_card.access + .["name"] = id_card.registered_name || "Unknown" + .["age"] = id_card.registered_age || "Unknown" + .["assignment"] = id_card.assignment || "Unassigned" + .["accesses"] = id_card.access var/datum/bank_account/id_account = id_card.registered_account if(istype(id_card, /obj/item/card/id/advanced/chameleon) && !bypass_chameleon) // Generate a bogey record based only on the ID card // Generates a random bank account number every time as a 'spot the thread' for anyone who // went through records for this entry for whatever reason. - .["Account ID"] = rand(111111, 999999) - .["Account Holder"] = .["Name"] - .["Account Assignment"] = .["Assignment"] + .["account_id"] = rand(111111, 999999) + .["account_holder"] = .["name"] + .["account_assignment"] = .["assignment"] .[CHAMELEON_OVERRIDE] = CHAMELEON_OVERRIDE return . if(!id_account) - .["Account ID"] = 0 - .["Account Holder"] = "NO ACCOUNT." - .["Account Assignment"] = "NO ACCOUNT." + .["account_id"] = 0 + .["account_holder"] = "NO ACCOUNT." + .["account_assignment"] = "NO ACCOUNT." return . - .["Account ID"] = id_account.account_id - .["Account Holder"] = id_account.account_holder - .["Account Assignment"] = id_account.account_job?.title || "Unassigned" + .["account_id"] = id_account.account_id + .["account_holder"] = id_account.account_holder + .["account_assignment"] = id_account.account_job?.title || "Unassigned" return . else . = ID_DATA(null) diff --git a/code/datums/components/material/material_container.dm b/code/datums/components/material/material_container.dm index 3774d655f42..6b6d26e7e44 100644 --- a/code/datums/components/material/material_container.dm +++ b/code/datums/components/material/material_container.dm @@ -158,11 +158,11 @@ * Arguments: * - [source][/obj/item]: The source of the materials we are inserting. * - multiplier: The multiplier for the materials extract from this item being inserted. - * - context: the atom performing the operation, this is the last argument sent in COMSIG_MATCONTAINER_ITEM_CONSUMED - * and is used mostly for silo logging, the silo resends this signal on the context to give it a + * - context: the atom performing the operation, is used mostly for silo logging, the silo resends this signal on the context to give it a * chance to process the item + * - user_data: in the form rendered by ID_DATA(user), for material logging (and if this component is connected to a silo, also for permission checking) */ -/datum/component/material_container/proc/insert_item_materials(obj/item/source, multiplier = 1, atom/context = parent) +/datum/component/material_container/proc/insert_item_materials(obj/item/source, multiplier = 1, atom/context = parent, alist/user_data) var/primary_mat var/max_mat_value = 0 var/material_amount = 0 @@ -180,7 +180,7 @@ mats_consumed[MAT] = mat_amount material_amount += mat_amount if(length(mats_consumed)) - SEND_SIGNAL(src, COMSIG_MATCONTAINER_ITEM_CONSUMED, source, primary_mat, mats_consumed, material_amount, context) + SEND_SIGNAL(src, COMSIG_MATCONTAINER_ITEM_CONSUMED, source, primary_mat, mats_consumed, material_amount, context, user_data) return primary_mat //=================================================================================== @@ -226,8 +226,9 @@ * - multiplier: The multiplier for the materials being inserted * - context: the atom performing the operation, this is the last argument sent in COMSIG_MATCONTAINER_ITEM_CONSUMED and is used mostly for silo logging * * - delete_item: should we delete the item after its materials are consumed. does not apply to stacks if they were split due to lack of space + * * - user_data - in the form rendered by ID_DATA(user), for material logging (and if this component is connected to a silo, also for permission checking) */ -/datum/component/material_container/proc/insert_item(obj/item/weapon, multiplier = 1, atom/context = parent, delete_item = TRUE) +/datum/component/material_container/proc/insert_item(obj/item/weapon, multiplier = 1, atom/context = parent, delete_item = TRUE, alist/user_data) if(QDELETED(weapon)) return MATERIAL_INSERT_ITEM_NO_MATS multiplier = CEILING(multiplier, 0.01) @@ -252,7 +253,7 @@ material_amount = OPTIMAL_COST(material_amount) //do the insert - var/last_inserted_id = insert_item_materials(target, multiplier, context) + var/last_inserted_id = insert_item_materials(target, multiplier, context, user_data = user_data) if(!isnull(last_inserted_id)) if(delete_item || target != weapon) //we could have split the stack ourselves qdel(target) //item gone @@ -389,7 +390,7 @@ is_stack = TRUE //we typically don't want to consume bags, boxes but only their contents. so we skip processing - inserted = !target_item.atom_storage ? insert_item(target_item, 1, context, is_stack) : 0 + inserted = !target_item.atom_storage ? insert_item(target_item, 1, context, is_stack, user_data = ID_DATA(user)) : 0 if(inserted > 0) . += inserted inserted /= SHEET_MATERIAL_AMOUNT // display units inserted as sheets for improved readability @@ -679,8 +680,9 @@ * [material][datum/material]: type of sheets present in this container to extract * [target][atom]: drop location * [atom][context]: context - the atom performing the operation, this is the last argument sent in COMSIG_MATCONTAINER_SHEETS_RETRIEVED and is used mostly for silo logging + * user_data - in the form rendered by ID_DATA(user), for material logging (and if this component is connected to a silo, also for permission checking) */ -/datum/component/material_container/proc/retrieve_sheets(sheet_amt, datum/material/material, atom/target = null, atom/context = parent) +/datum/component/material_container/proc/retrieve_sheets(sheet_amt, datum/material/material, atom/target = null, atom/context = parent, alist/user_data) //do we support sheets of this material if(!material.sheet_type) return 0 //Add greyscale sheet handling here later @@ -709,7 +711,7 @@ use_amount_mat(new_sheets.amount * SHEET_MATERIAL_AMOUNT, material) sheet_amt -= new_sheets.amount //send signal - SEND_SIGNAL(src, COMSIG_MATCONTAINER_SHEETS_RETRIEVED, new_sheets, context) + SEND_SIGNAL(src, COMSIG_MATCONTAINER_SHEETS_RETRIEVED, new_sheets, context, user_data) //no point merging anything into an already full stack if(new_sheets.amount == new_sheets.max_amount) continue diff --git a/code/datums/components/material/remote_materials.dm b/code/datums/components/material/remote_materials.dm index b74347cf6a0..6a6d38cd787 100644 --- a/code/datums/components/material/remote_materials.dm +++ b/code/datums/components/material/remote_materials.dm @@ -63,8 +63,7 @@ handles linking back and forth. if (connect_to_silo) silo = GLOB.ore_silo_default if (silo) - silo.ore_connected_machines += src - mat_container = silo.materials + silo.connect_receptacle(src, parent) if(!(mat_container_flags & MATCONTAINER_NO_INSERT)) RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_insert)) RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(on_secondary_insert)) @@ -76,7 +75,8 @@ handles linking back and forth. if(silo) allow_standalone = FALSE disconnect() - mat_container = null + if(mat_container) + mat_container = null return ..() @@ -121,9 +121,7 @@ handles linking back and forth. return UnregisterSignal(parent, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY)) - silo.ore_connected_machines -= src - silo = null - mat_container = null + silo.disconnect_receptacle(src, parent) if (allow_standalone) _MakeLocal() @@ -143,8 +141,7 @@ handles linking back and forth. var/obj/machinery/ore_silo/new_silo = M.buffer var/datum/component/material_container/new_container = new_silo.GetComponent(/datum/component/material_container) if (silo) - silo.ore_connected_machines -= src - silo.holds -= src + silo.disconnect_receptacle(src, parent) else if (mat_container) //transfer all mats to silo. whatever cannot be transfered is dumped out as sheets if(mat_container.total_amount()) @@ -155,9 +152,7 @@ handles linking back and forth. new_container.materials[mat] += mat_amount mat_container.materials[mat] = 0 qdel(mat_container) - silo = new_silo - silo.ore_connected_machines += src - mat_container = new_container + silo.connect_receptacle(src, parent) if(!(mat_container_flags & MATCONTAINER_NO_INSERT)) RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_insert)) RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(on_secondary_insert)) @@ -210,8 +205,9 @@ handles linking back and forth. * - The silo in not on hold * Arguments * * check_hold - should we check if the silo is on hold + * * user_data - in the form rendered by ID_DATA(user); used as a reference for silo bans and access checks */ -/datum/component/remote_materials/proc/can_use_resource(check_hold = TRUE) +/datum/component/remote_materials/proc/can_use_resource(check_hold = TRUE, alist/user_data) var/atom/movable/movable_parent = parent if (!istype(movable_parent)) return FALSE @@ -221,6 +217,8 @@ handles linking back and forth. if(check_hold && on_hold()) //silo on hold movable_parent.say("Mineral access is on hold, please contact the quartermaster.") return FALSE + if(SEND_SIGNAL(movable_parent, COMSIG_ORE_SILO_PERMISSION_CHECKED, user_data, movable_parent) & COMPONENT_ORE_SILO_DENY) + return FALSE return TRUE /** @@ -233,9 +231,10 @@ handles linking back and forth. * multiplier- each mat unit is scaled by this value then rounded after it is scaled by coefficient. This value is your print quatity e.g. printing multiple items * action- For logging only. e.g. build, create, i.e. the action you are trying to perform * name- For logging only. the design you are trying to build e.g. matter bin, etc. + * user_data - in the form rendered by ID_DATA(user), for material logging and (if this component is connected to a silo), permission checking */ -/datum/component/remote_materials/proc/use_materials(list/mats, coefficient = 1, multiplier = 1, action = "build", name = "design") - if(!can_use_resource()) +/datum/component/remote_materials/proc/use_materials(list/mats, coefficient = 1, multiplier = 1, action = "build", name = "design", alist/user_data) + if(!can_use_resource(user_data = user_data)) return 0 var/amount_consumed = mat_container.use_materials(mats, coefficient, multiplier) @@ -244,7 +243,7 @@ handles linking back and forth. var/list/scaled_mats = list() for(var/i in mats) scaled_mats[i] = OPTIMAL_COST(OPTIMAL_COST(mats[i] * coefficient) * multiplier) - silo.silo_log(parent, action, -multiplier, name, scaled_mats) + silo.silo_log(parent, action, -multiplier, name, scaled_mats, user_data) return amount_consumed @@ -255,16 +254,17 @@ handles linking back and forth. * [material_ref][datum/material]- The material type you are trying to eject * eject_amount- how many sheets to eject * [drop_target][atom]- optional where to drop the sheets. null means it is dropped at this components parent location + * user_data - in the form rendered by ID_DATA(user), for material logging and (if this component is connected to a silo), permission checking */ -/datum/component/remote_materials/proc/eject_sheets(datum/material/material_ref, eject_amount, atom/drop_target = null) - if(!can_use_resource()) +/datum/component/remote_materials/proc/eject_sheets(datum/material/material_ref, eject_amount, atom/drop_target = null, alist/user_data) + if(!can_use_resource(user_data = user_data)) return 0 var/atom/movable/movable_parent = parent if(isnull(drop_target)) drop_target = movable_parent.drop_location() - return mat_container.retrieve_sheets(eject_amount, material_ref, target = drop_target, context = parent) + return mat_container.retrieve_sheets(eject_amount, material_ref, target = drop_target, context = parent, user_data = user_data) /** * Insert an item into the mat container, helper proc to insert items with the correct context @@ -272,9 +272,14 @@ handles linking back and forth. * Arguments * * obj/item/weapon - the item you are trying to insert * * multiplier - the multiplier applied on the materials consumed + * * user_data - an alist in the form rendered by ID_DATA(user), for logging who/where/when the item was inserted */ -/datum/component/remote_materials/proc/insert_item(obj/item/weapon, multiplier = 1) - if(!can_use_resource(FALSE)) +/datum/component/remote_materials/proc/insert_item(obj/item/weapon, multiplier = 1, alist/user_data) + // Inserting materials automatically shouldn't be permission-restricted + if(!islist(user_data)) + user_data = ID_DATA(null) + user_data[SILICON_OVERRIDE] = SILICON_OVERRIDE + if(!can_use_resource(FALSE, user_data)) return MATERIAL_INSERT_ITEM_FAILURE - return mat_container.insert_item(weapon, multiplier, parent) + return mat_container.insert_item(weapon, multiplier, parent, user_data = user_data) diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm index f224db8f484..ff0cef96087 100644 --- a/code/game/machinery/_machinery.dm +++ b/code/game/machinery/_machinery.dm @@ -636,7 +636,8 @@ if((machine_stat & (NOPOWER|BROKEN)) && !(interaction_flags_machine & INTERACT_MACHINE_OFFLINE)) // Check if the machine is broken, and if we can still interact with it if so return FALSE - if(SEND_SIGNAL(user, COMSIG_TRY_USE_MACHINE, src) & COMPONENT_CANT_USE_MACHINE_INTERACT) + var/try_use_signal = SEND_SIGNAL(user, COMSIG_TRY_USE_MACHINE, src) | SEND_SIGNAL(src, COMSIG_TRY_USE_MACHINE, user) + if(try_use_signal & COMPONENT_CANT_USE_MACHINE_INTERACT) return FALSE if(isAdminGhostAI(user)) diff --git a/code/game/objects/items/rcd/RHD.dm b/code/game/objects/items/rcd/RHD.dm index c7cc3bd0520..b303f300c6b 100644 --- a/code/game/objects/items/rcd/RHD.dm +++ b/code/game/objects/items/rcd/RHD.dm @@ -190,10 +190,6 @@ update_appearance() return TRUE else - if(silo_mats.on_hold()) - if(user) - balloon_alert(user, "silo on hold!") - return FALSE if(!silo_mats.mat_container) if(user) balloon_alert(user, "no silo detected!") @@ -203,7 +199,7 @@ if(user) balloon_alert(user, "not enough silo material!") return FALSE - silo_mats.use_materials(list(/datum/material/iron = SILO_USE_AMOUNT), multiplier = amount, action = "build", name = "consume") + silo_mats.use_materials(list(/datum/material/iron = SILO_USE_AMOUNT), multiplier = amount, action = "build", name = "consume", user_data = ID_DATA(user)) return TRUE /obj/item/construction/ui_static_data(mob/user) @@ -265,9 +261,7 @@ else . = matter >= amount else - if(silo_mats.on_hold()) - if(user) - balloon_alert(user, "silo on hold!") + if(!silo_mats.can_use_resource(user_data = ID_DATA(user))) return FALSE . = silo_mats.mat_container.has_enough_of_material(/datum/material/iron, amount * SILO_USE_AMOUNT) if(!. && user) diff --git a/code/modules/mining/machine_redemption.dm b/code/modules/mining/machine_redemption.dm index 31e8cca2ea9..465bfff2708 100644 --- a/code/modules/mining/machine_redemption.dm +++ b/code/modules/mining/machine_redemption.dm @@ -297,7 +297,7 @@ var/mob/living/user = usr user_id_card = user.get_idcard(TRUE) if(isnull(user_id_card)) - to_chat(usr, span_warning("No valid ID detected.")) + say("No ID card found.") return FALSE //we have points @@ -310,8 +310,8 @@ if("Release") if(!mat_container) return - if(materials.on_hold()) - to_chat(usr, span_warning("Mineral access is on hold, please contact the quartermaster.")) + if(!materials.can_use_resource(user_data = ID_DATA(usr))) + return else if(!allowed(usr)) //Check the ID inside, otherwise check the user to_chat(usr, span_warning("Required access not found.")) else @@ -327,13 +327,12 @@ var/desired = text2num(params["sheets"]) var/sheets_to_remove = round(min(desired, 50, stored_amount)) - materials.eject_sheets(mat, sheets_to_remove, get_step(src, output_dir)) + materials.eject_sheets(mat, sheets_to_remove, get_step(src, output_dir), user_data = ID_DATA(usr)) return TRUE if("Smelt") if(!mat_container) return - if(materials.on_hold()) - to_chat(usr, span_warning("Mineral access is on hold, please contact the quartermaster.")) + if(!materials.can_use_resource(user_data = ID_DATA(usr))) return var/alloy_id = params["id"] var/datum/design/alloy = stored_research.isDesignResearchedID(alloy_id) @@ -345,7 +344,7 @@ var/amount = round(min(text2num(params["sheets"]), 50, can_smelt_alloy(alloy))) if(amount < 1) //no negative mats return - materials.use_materials(alloy.materials, multiplier = amount, action = "released", name = "sheets") + materials.use_materials(alloy.materials, multiplier = amount, action = "released", name = "sheets", user_data = ID_DATA(usr)) var/output if(ispath(alloy.build_path, /obj/item/stack/sheet)) output = new alloy.build_path(src, amount) diff --git a/code/modules/mining/machine_silo.dm b/code/modules/mining/machine_silo.dm index 1c376172e65..1af23cd8d57 100644 --- a/code/modules/mining/machine_silo.dm +++ b/code/modules/mining/machine_silo.dm @@ -1,3 +1,28 @@ +// Always announce this action +#define ALWAYS_ANNOUNCE (ALL) +// Announced when someone tries to ban someone without QM access +#define BAN_ATTEMPT_FAILURE_NO_ACCESS (1<<1) +// Announced when someone tries to ban someone with QM access without being the Captain +#define BAN_ATTEMPT_FAILURE_CHALLENGING_DA_CHIEF (1<<2) +// Announced when a silicon tries to ban someone +#define BAN_ATTEMPT_FAILURE_SOULLESS_MACHINE (1<<3) +// Announced when a user is banned from the ore silo +#define BAN_CONFIRMATION (1<<4) +// Announced when a user is unbanned from the ore silo +#define UNBAN_CONFIRMATION (1<<5) +// Announced when a suspicious(chameleon ID worn by user) log is Among the ore silo logs and someone tries to ban them +#define FAILED_OPERATION_SUSPICIOUS (1<<6) +// Announced when a user tries to ban someone without a bank account ID +#define FAILED_OPERATION_NO_BANK_ID (1<<7) +// Announced when a user tries to unrestrict the ore silo without QM access +#define UNRESTRICT_FAILURE_NO_ACCESS (1<<8) +// Announced when a silicon tries to unrestrict the ore silo +#define UNRESTRICT_FAILURE_SOULLESS_MACHINE (1<<9) +// Announced when a user removes the worn ID(with valid bank account) requirement from the ore silo +#define UNRESTRICT_CONFIRMATION (1<<10) +// Announced when a user restricts the ore silo to require a valid ID with bank account +#define RESTRICT_CONFIRMATION (1<<11) + /obj/machinery/ore_silo name = "ore silo" desc = "An all-in-one bluespace storage and transmission system for the station's mineral distribution needs." @@ -8,16 +33,42 @@ interaction_flags_machine = INTERACT_MACHINE_WIRES_IF_OPEN|INTERACT_MACHINE_ALLOW_SILICON|INTERACT_MACHINE_OPEN_SILICON processing_flags = NONE + /// By default, an ore silo requires you to be wearing an ID to pull materials from it. + var/ID_required = TRUE /// List of all connected components that are on hold from accessing materials. var/list/holds = list() /// List of all components that are sharing ores with this silo. var/list/datum/component/remote_materials/ore_connected_machines = list() /// Material Container var/datum/component/material_container/materials + /// A list of names of bank account IDs that are banned from using this ore silo. + var/list/banned_users = list() + ///The machine's internal radio, used to broadcast alerts. + var/obj/item/radio/radio + ///The channels we announce over + var/list/radio_channels = list( + RADIO_CHANNEL_COMMON = NONE, + RADIO_CHANNEL_COMMAND = NONE, + RADIO_CHANNEL_SUPPLY = NONE, + RADIO_CHANNEL_SECURITY = NONE, + ) + var/static/alist/announcement_messages = alist( + BAN_ATTEMPT_FAILURE_NO_ACCESS = "ACCESS ENFORCEMENT FAILURE: $SILO_USER_NAME lacks supply command authority.", + BAN_ATTEMPT_FAILURE_CHALLENGING_DA_CHIEF = "ACCESS ENFORCEMENT FAILURE: $SILO_USER_NAME attempting subversion of supply command authority.", + BAN_ATTEMPT_FAILURE_SOULLESS_MACHINE = "$SILO_USER_NAME INTERFACE_EXCEPTION -> BANNED_USERS+=\[$TARGET_NAME\] => NO_OP", + BAN_CONFIRMATION = "ACCESS ENFORCEMENT CONFIRMATION\[$SILO_USER_NAME\]: $TARGET_NAME banned from ore silo access.", + UNBAN_CONFIRMATION = "ACCESS ENFORCEMENT CONFIRMATION\[$SILO_USER_NAME\]: $TARGET_NAME unbanned from ore silo access.", + FAILED_OPERATION_SUSPICIOUS = "NULL_ACCOUNT_RESOLVE_PTR_#?", + FAILED_OPERATION_NO_BANK_ID = "ACCESS ENFORCEMENT FAILURE: No account ID found. Please contact a banker.", + UNRESTRICT_FAILURE_NO_ACCESS = "ID ACCESS REQUIREMENT ENFORCED: $SILO_USER_NAME lacks supply command authority; ID ACCESS REQUIREMENT REMOVAL FAILED.", + UNRESTRICT_FAILURE_SOULLESS_MACHINE = "$SILO_USER_NAME INTERFACE_EXCEPTION -> ID_ACCESS_REQUIREMENT = !ID_ACCESS_REQUIREMENT => NO_OP", + RESTRICT_CONFIRMATION = "ID ACCESS REQUIREMENT ROUTINE STARTED: $SILO_USER_NAME has enforced ID read requirement for this ore silo.", + UNRESTRICT_CONFIRMATION = "ID ACCESS REQUIREMENT ROUTINE SUSPENDED: $SILO_USER_NAME has removed ID read requirement for this ore silo.", + RESTRICT_FAILURE = "ID ACCESS REQUIREMENT ROUTINE FAILED TO START: $SILO_USER_NAME()" + ) /obj/machinery/ore_silo/Initialize(mapload) . = ..() - materials = AddComponent( \ /datum/component/material_container, \ SSmaterials.materials_by_category[MAT_CATEGORY_SILO], \ @@ -32,6 +83,27 @@ if (!GLOB.ore_silo_default && mapload && is_station_level(z)) GLOB.ore_silo_default = src register_context() + setup_radio() + configure_default_announcements_policy() + +/obj/machinery/ore_silo/emag_act(mob/living/user) + if(obj_flags & EMAGGED) + return FALSE + obj_flags |= EMAGGED + return TRUE + +/obj/machinery/ore_silo/proc/setup_radio() + radio = new(src) + radio.subspace_transmission = TRUE + radio.canhear_range = 0 + radio.set_listening(FALSE) + radio.keyslot = new + radio.keyslot.channels[RADIO_CHANNEL_COMMON] = TRUE + radio.keyslot.channels[RADIO_CHANNEL_COMMAND] = TRUE + radio.keyslot.channels[RADIO_CHANNEL_SUPPLY] = TRUE + radio.keyslot.channels[RADIO_CHANNEL_SECURITY] = TRUE + radio.recalculateChannels() + /obj/machinery/ore_silo/Destroy() if (GLOB.ore_silo_default == src) @@ -69,17 +141,17 @@ context[SCREENTIP_CONTEXT_LMB] = "Deconstruct" return CONTEXTUAL_SCREENTIP_SET -/obj/machinery/ore_silo/proc/on_item_consumed(datum/component/material_container/container, obj/item/item_inserted, last_inserted_id, mats_consumed, amount_inserted, atom/context) +/obj/machinery/ore_silo/proc/on_item_consumed(datum/component/material_container/container, obj/item/item_inserted, last_inserted_id, mats_consumed, amount_inserted, atom/context, alist/user_data) SIGNAL_HANDLER - silo_log(context, "deposited", amount_inserted, item_inserted.name, mats_consumed) + silo_log(context, "DEPOSIT", amount_inserted, item_inserted.name, mats_consumed, user_data) SEND_SIGNAL(context, COMSIG_SILO_ITEM_CONSUMED, container, item_inserted, last_inserted_id, mats_consumed, amount_inserted) -/obj/machinery/ore_silo/proc/log_sheets_ejected(datum/component/material_container/container, obj/item/stack/sheet/sheets, atom/context) +/obj/machinery/ore_silo/proc/log_sheets_ejected(datum/component/material_container/container, obj/item/stack/sheet/sheets, atom/context, alist/user_data) SIGNAL_HANDLER - silo_log(context, "ejected", -sheets.amount, "[sheets.singular_name]", sheets.custom_materials) + silo_log(context, "EJECT", -sheets.amount * SHEET_MATERIAL_AMOUNT, "[sheets.singular_name]", sheets.custom_materials, user_data) /obj/machinery/ore_silo/screwdriver_act(mob/living/user, obj/item/tool) . = ITEM_INTERACT_BLOCKING @@ -96,6 +168,61 @@ balloon_alert(user, "saved to multitool buffer") return ITEM_INTERACT_SUCCESS + +/** + * The logic for disconnecting a remote receptacle (RCD, fabricator, etc.) is collected here for sanity's sake + * rather than being on specific types. Serves to agnosticize the remote_materials component somewhat rather than + * snowflaking code for silos into the component. + * * receptacle - The datum/component/remote_materials component that is getting connected. + * * physical_receptacle - the actual object in the game world that was connected to our material supply. Typed as atom/movable for + * future-proofing against anything that may conceivably one day have remote silo access, such as a cyborg, an implant, structures, vehicles, + * and so-on. + */ +/obj/machinery/ore_silo/proc/connect_receptacle(datum/component/remote_materials/receptacle, atom/movable/physical_receptacle) + ore_connected_machines += receptacle + receptacle.mat_container = src.materials + receptacle.silo = src + RegisterSignal(physical_receptacle, COMSIG_ORE_SILO_PERMISSION_CHECKED, PROC_REF(check_permitted)) + +/** + * The logic for disconnecting a remote receptacle (RCD, fabricator, etc.) is collected here for sanity's sake + * rather than being on specific types. Cleans up references to us and to the receptacle. + * * receptacle - The datum/component/remote_materials component that is getting destroyed. + * * physical_receptacle - the actual object in the game world that was connected to our material supply. Typed as atom/movable for + * future-proofing against anything that may conceivably one day have remote silo access, such as a cyborg, an implant, structures, vehicles, + * and so-on. + */ +/obj/machinery/ore_silo/proc/disconnect_receptacle(datum/component/remote_materials/receptacle, atom/movable/physical_receptacle) + ore_connected_machines -= receptacle + receptacle.mat_container = null + receptacle.silo = null + holds -= receptacle + UnregisterSignal(physical_receptacle, COMSIG_ORE_SILO_PERMISSION_CHECKED) + +/obj/machinery/ore_silo/proc/check_permitted(datum/source, alist/user_data, atom/movable/physical_receptacle) + SIGNAL_HANDLER + + if(!ID_required) + return COMPONENT_ORE_SILO_ALLOW + if(!islist(user_data)) + // Just allow to salvage the situation + . = COMPONENT_ORE_SILO_ALLOW + CRASH("Invalid data passed to check_permitted") + if(user_data[SILICON_OVERRIDE] || user_data[CHAMELEON_OVERRIDE] || astype(user_data["accesses"], /list)?.Find(ACCESS_QM)) + return COMPONENT_ORE_SILO_ALLOW + if(user_data[ID_READ_FAILURE]) + physical_receptacle.say("SILO ERR: ID interface failure. Please contact the Head of Personnel.") + return COMPONENT_ORE_SILO_DENY + if(!user_data["account_id"] || !isnum(user_data["account_id"])) + if(prob(5)) + physical_receptacle.say("SILO ERR: Bank account ID not found. Initiating anti-communist silo-access policy.") + physical_receptacle.say("SILO ERR: No account ID found. Please contact Head of Personnel.") + return COMPONENT_ORE_SILO_DENY + if(banned_users.Find(user_data["account_id"])) + physical_receptacle.say("SILO ERR: You are banned from using this ore silo.") + return COMPONENT_ORE_SILO_DENY + return COMPONENT_ORE_SILO_ALLOW + /obj/machinery/ore_silo/ui_assets(mob/user) return list( get_asset_datum(/datum/asset/spritesheet_batched/sheetmaterials) @@ -104,7 +231,7 @@ /obj/machinery/ore_silo/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, "OreSilo") + ui = new(user, src, "OreSilo", "Ore Silo Control") ui.open() /obj/machinery/ore_silo/ui_static_data(mob/user) @@ -122,7 +249,7 @@ list( "icon" = icon2base64(icon(initial(parent.icon), initial(parent.icon_state), frame = 1)), "name" = parent.name, - "onHold" = !!holds[remote], + "on_hold" = !!holds[remote], "location" = get_area_name(parent, TRUE), ) ) @@ -131,15 +258,18 @@ for(var/datum/ore_silo_log/entry as anything in GLOB.silo_access_logs[REF(src)]) data["logs"] += list( list( - "rawMaterials" = entry.get_raw_materials(""), - "machineName" = entry.machine_name, - "areaName" = entry.area_name, + "raw_materials" = entry.get_raw_materials(""), + "machine_name" = entry.machine_name, + "area_name" = entry.area_name, "action" = entry.action, "amount" = entry.amount, "time" = entry.timestamp, "noun" = entry.noun, + "user_data" = entry.user_data, ) ) + data["banned_users"] = banned_users + data["ID_required"] = ID_required return data @@ -194,9 +324,199 @@ if(isnull(amount)) return - materials.retrieve_sheets(amount, ejecting, drop_location()) + materials.retrieve_sheets(amount, ejecting, drop_location(), user_data = ID_DATA(ui.user)) return TRUE + if("toggle_ban") + var/list/banned_user_data = params["user_data"] + attempt_ban_toggle(usr, banned_user_data) + return TRUE + + if("toggle_restrict") + attempt_toggle_restrict(usr) +/** + * Called from the ore silo's UI, when someone attempts to (un)ban a user from using the ore silo. + * The person doing the banning should have at least QM access. Unless this is emagged. Not modifiable by silicons unless emagged. + * Anyone but the Captain attempting to ban someone with QM access from the ore silo gets what is essentially a glorified version + * of the permission denied result. + * * user - The person who clicked the ban button in the UI. + * * target_user_data - Data in the form rendered from ID_DATA(target), passed into the ore silo logs by whatever the target did such + * as removing/adding sheets, printing items, etc + */ +/obj/machinery/ore_silo/proc/attempt_ban_toggle(mob/living/user, list/target_user_data) + if(!istype(user) || !istype(target_user_data)) + CRASH("Bad arguments passed to [callee]") + var/emagged = obj_flags & EMAGGED + if((isAI(user) || iscyborg(user) || isdrone(user)) && !emagged) + to_chat(user, span_danger("A scroll of red text occludes your vision: ACCESS ENFORCEMENT _disabled_ for SILICON INTERFACE.")) + user.flash_act(intensity = 1, affect_silicon = TRUE) + handle_access_action_feedback( + BAN_ATTEMPT_FAILURE_SOULLESS_MACHINE, + ID_DATA(user), + target_user_data + ) + return + + var/target_bank_id = target_user_data["account_id"] + var/target_is_banned = banned_users.Find(target_bank_id) + // Agent card bypasses bans but we want to specially handle them anyway + // so a bunch of random account IDs don't fill the list and do something + // like ban people who haven't joined the round yet + var/haxxor_card_ban_immunity = !isnull(target_user_data[CHAMELEON_OVERRIDE]) + + to_chat(user, span_warning("You press the button to [target_is_banned ? "un" : ""]ban [target_user_data["name"]]'s account...")) + // No feedback if emagged + if(emagged) + if(!haxxor_card_ban_immunity && isnum(target_bank_id)) + target_is_banned ? banned_users.Remove(target_bank_id) : banned_users.Add(target_bank_id) + return + + var/alist/silo_user_data = ID_DATA(user) + var/list/silo_user_accesses = astype(silo_user_data["accesses"], /list) + var/list/target_user_accesses = astype(target_user_data["accesses"], /list) + + + // Even though QM bypasses the access check (or rather always pases) + // perhaps the Captain would pre-emptively ban them right before a demotion + if(target_user_accesses?.Find(ACCESS_QM) && !silo_user_accesses?.Find(ACCESS_CAPTAIN)) + handle_access_action_feedback( + BAN_ATTEMPT_FAILURE_CHALLENGING_DA_CHIEF, + silo_user_data, + target_user_data + ) + return + if(!silo_user_accesses?.Find(ACCESS_QM)) + handle_access_action_feedback( + BAN_ATTEMPT_FAILURE_NO_ACCESS, + silo_user_data, + target_user_data + ) + return + if(haxxor_card_ban_immunity) + handle_access_action_feedback( + FAILED_OPERATION_SUSPICIOUS, + silo_user_data, + target_user_data + ) + return + if(!target_bank_id || !isnum(target_bank_id)) + handle_access_action_feedback( + FAILED_OPERATION_NO_BANK_ID, + silo_user_data, + target_user_data + ) + return + if(target_is_banned) + banned_users.Remove(target_bank_id) + handle_access_action_feedback( + UNBAN_CONFIRMATION, + silo_user_data, + target_user_data + ) + return + // If we got here, we are banning the user + banned_users.Add(target_bank_id) + handle_access_action_feedback( + BAN_CONFIRMATION, + silo_user_data, + target_user_data + ) +/** + * Called from the ore silo tgui interface, for when someone attempts to restrict or unrestrict the ore silo from requiring + * an ID with an attached bank account (or, a chameleon ID, or, being a silicon) + * user - the person who tried to toggle the ore silo's access restriction. Needs to be someone with QM access, unless the + * silo is emagged. Shouldn't allow silicons to toggle this unless the silo is emagged. + * + */ +/obj/machinery/ore_silo/proc/attempt_toggle_restrict(mob/living/user) + if(!istype(user)) + CRASH("No user to check toggle attempt restrictions. .ID_required is unchanged.") + var/emagged = obj_flags & EMAGGED + if(emagged) + ID_required = !ID_required + return + var/alist/silo_user_data = ID_DATA(user) + var/is_a_robot = silo_user_data[SILICON_OVERRIDE] + if(is_a_robot) + handle_access_action_feedback( + UNRESTRICT_FAILURE_SOULLESS_MACHINE, + silo_user_data, + null + ) + return + var/list/user_accesses = astype(silo_user_data["accesses"], /list) + if(!user_accesses?.Find(ACCESS_QM)) + handle_access_action_feedback( + UNRESTRICT_FAILURE_NO_ACCESS, + silo_user_data, + null + ) + return + ID_required = !ID_required + handle_access_action_feedback( + ID_required ? RESTRICT_CONFIRMATION : UNRESTRICT_CONFIRMATION, + silo_user_data, + null) + +// I must sacrifice the line diff to the gods of readable code +/// Set up the default announcement policy for actions +/// radio_channels[channel_name_key] = policy_bitmask +/// where channel_name_key is one of RADIO_CHANNEL_(COMMON|COMMAND|SECURITY|SUPPLY) +/// and policy_bitmask is a bitmask of actions that will be announced on that channel +/// by default +/obj/machinery/ore_silo/proc/configure_default_announcements_policy() + + radio_channels[RADIO_CHANNEL_COMMON] = BAN_ATTEMPT_FAILURE_CHALLENGING_DA_CHIEF + radio_channels[RADIO_CHANNEL_COMMON] |= RESTRICT_CONFIRMATION + radio_channels[RADIO_CHANNEL_COMMON] |= UNRESTRICT_CONFIRMATION + + // start off with the common channel bitmask policy as a base + radio_channels[RADIO_CHANNEL_COMMAND] = radio_channels[RADIO_CHANNEL_COMMON] + radio_channels[RADIO_CHANNEL_COMMAND] |= BAN_ATTEMPT_FAILURE_NO_ACCESS + radio_channels[RADIO_CHANNEL_COMMAND] |= BAN_ATTEMPT_FAILURE_SOULLESS_MACHINE + radio_channels[RADIO_CHANNEL_COMMAND] |= UNRESTRICT_FAILURE_NO_ACCESS + radio_channels[RADIO_CHANNEL_COMMAND] |= UNRESTRICT_FAILURE_SOULLESS_MACHINE + radio_channels[RADIO_CHANNEL_COMMAND] |= FAILED_OPERATION_NO_BANK_ID + radio_channels[RADIO_CHANNEL_COMMAND] |= BAN_CONFIRMATION + radio_channels[RADIO_CHANNEL_COMMAND] |= UNBAN_CONFIRMATION + + // Security channel is used for security-related announcements + // but gets less information than command to avoid over-informing them without + // QM involvement + radio_channels[RADIO_CHANNEL_SECURITY] = radio_channels[RADIO_CHANNEL_COMMON] + radio_channels[RADIO_CHANNEL_SECURITY] |= BAN_ATTEMPT_FAILURE_NO_ACCESS + radio_channels[RADIO_CHANNEL_SECURITY] |= UNRESTRICT_FAILURE_NO_ACCESS + radio_channels[RADIO_CHANNEL_SECURITY] |= BAN_CONFIRMATION + radio_channels[RADIO_CHANNEL_SECURITY] |= UNBAN_CONFIRMATION + + // Supply channel has the same policy by default as the command channel + // due to their usual purview of the ore silo + radio_channels[RADIO_CHANNEL_SUPPLY] = radio_channels[RADIO_CHANNEL_COMMAND] + +/obj/machinery/ore_silo/proc/handle_access_action_feedback(action, alist/silo_user_data, list/target_user_data = null) + var/message = announcement_messages[action] + message = replacetext(message, "$TARGET_NAME", target_user_data?["name"]) + message = replacetext(message, "$SILO_USER_NAME", silo_user_data["name"]) + say(message) + for(var/channel in radio_channels) + // Key is the channel name, value is the bitmask of announced actions + if(action & radio_channels[channel]) + var/say_cooldown_adherence_timer = 1 SECONDS * radio_channels.Find(channel) // * 1, * 2, * 3, etc. + addtimer(CALLBACK(radio, TYPE_PROC_REF(/obj/item, talk_into), src, message, channel), say_cooldown_adherence_timer) + +#undef ALWAYS_ANNOUNCE +#undef BAN_ATTEMPT_FAILURE_NO_ACCESS +#undef BAN_ATTEMPT_FAILURE_CHALLENGING_DA_CHIEF +#undef BAN_ATTEMPT_FAILURE_SOULLESS_MACHINE +#undef BAN_CONFIRMATION +#undef UNBAN_CONFIRMATION +#undef FAILED_OPERATION_SUSPICIOUS +#undef FAILED_OPERATION_NO_BANK_ID +#undef UNRESTRICT_FAILURE_NO_ACCESS +#undef UNRESTRICT_FAILURE_SOULLESS_MACHINE +#undef UNRESTRICT_CONFIRMATION +#undef RESTRICT_CONFIRMATION + /** * Creates a log entry for depositing/withdrawing from the silo both ingame and in text based log * @@ -206,12 +526,13 @@ * - amount: The amount of sheets/objects deposited/withdrawn by this action. Positive for depositing, negative for withdrawing. * - noun: Name of the object the action was performed with (sheet, units, ore...) * - [mats][list]: Assoc list in format (material datum = amount of raw materials). Wants the actual amount of raw (iron, glass...) materials involved in this action. If you have 10 metal sheets each worth 100 iron you would pass a list with the iron material datum = 1000 + * - user_data - ID_DATA(user), includes details (not currently) rendered to the player, such as bank account #, see the proc on SSid_access */ -/obj/machinery/ore_silo/proc/silo_log(obj/machinery/M, action, amount, noun, list/mats) +/obj/machinery/ore_silo/proc/silo_log(obj/machinery/M, action, amount, noun, list/mats, alist/user_data) if (!length(mats)) return - var/datum/ore_silo_log/entry = new(M, action, amount, noun, mats) + var/datum/ore_silo_log/entry = new(M, action, amount, noun, mats, user_data) var/list/datum/ore_silo_log/logs = GLOB.silo_access_logs[REF(src)] if(!LAZYLEN(logs)) GLOB.silo_access_logs[REF(src)] = logs = list(entry) @@ -236,8 +557,9 @@ var/amount ///List of individual materials used in the action var/list/materials + var/alist/user_data -/datum/ore_silo_log/New(obj/machinery/M, _action, _amount, _noun, list/mats=list()) +/datum/ore_silo_log/New(obj/machinery/M, _action, _amount, _noun, list/mats=list(), alist/user_data) timestamp = station_time_timestamp() machine_name = M.name area_name = get_area_name(M, TRUE) @@ -245,6 +567,7 @@ amount = _amount noun = _noun materials = mats.Copy() + src.user_data = user_data var/list/data = list( "machine_name" = machine_name, "area_name" = AREACOORD(M), @@ -253,10 +576,11 @@ "noun" = noun, "raw_materials" = get_raw_materials(""), "direction" = amount < 0 ? "withdrawn" : "deposited", + "user_data" = user_data, ) logger.Log( LOG_CATEGORY_SILO, - "[machine_name] in \[[AREACOORD(M)]\] [action] [abs(amount)]x [noun] | [get_raw_materials("")]", + "[machine_name] in \[[AREACOORD(M)]\] [action] [abs(amount)]x [noun] | [get_raw_materials("")] | [user_data["name"]]", data, ) @@ -288,7 +612,7 @@ var/list/msg = list() for(var/key in materials) var/datum/material/M = key - var/val = round(materials[key]) + var/val = round(materials[key]) / SHEET_MATERIAL_AMOUNT msg += separator separator = ", " msg += "[amount < 0 ? "-" : "+"][val] [M.name]" diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 2cb869f5e78..e120a26837c 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -667,7 +667,7 @@ /** * Returns the access list for this mob */ -/mob/living/proc/get_access() +/mob/living/proc/get_access() as /list var/list/access_list = list() SEND_SIGNAL(src, COMSIG_MOB_RETRIEVE_SIMPLE_ACCESS, access_list) var/obj/item/card/id/id = get_idcard() diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm index e9920b4298f..fd54327b13c 100644 --- a/code/modules/mob/living/silicon/robot/robot_model.dm +++ b/code/modules/mob/living/silicon/robot/robot_model.dm @@ -218,7 +218,7 @@ if(!to_stock) //Nothing for us in the silo continue - storage_datum.energy += charger.materials.use_materials(list(GET_MATERIAL_REF(storage_datum.mat_type) = to_stock), action = "resupplied", name = "units") + storage_datum.energy += charger.materials.use_materials(list(GET_MATERIAL_REF(storage_datum.mat_type) = to_stock), action = "resupplied", name = "units", user_data = ID_DATA(robot)) charger.balloon_alert(robot, "+ [to_stock]u [initial(storage_datum.mat_type.name)]") playsound(charger, 'sound/items/weapons/gun/general/mag_bullet_insert.ogg', 50, vary = FALSE) return diff --git a/code/modules/research/machinery/_production.dm b/code/modules/research/machinery/_production.dm index bce895917c2..808a36782fe 100644 --- a/code/modules/research/machinery/_production.dm +++ b/code/modules/research/machinery/_production.dm @@ -303,7 +303,7 @@ say("No power to dispense sheets") return - materials.eject_sheets(material, amount) + materials.eject_sheets(material_ref = material, eject_amount = amount, user_data = ID_DATA(usr)) return TRUE if("build") @@ -338,7 +338,7 @@ var/coefficient = build_efficiency(design.build_path) //check for materials - if(!materials.can_use_resource()) + if(!materials.can_use_resource(user_data = ID_DATA(usr))) return if(!materials.mat_container.has_materials(design.materials, coefficient, print_quantity)) say("Not enough materials to complete prototype[print_quantity > 1 ? "s" : ""].") @@ -368,7 +368,7 @@ target_location = get_turf(src) else target_location = get_turf(src) - addtimer(CALLBACK(src, PROC_REF(do_make_item), design, print_quantity, build_time_per_item, coefficient, charge_per_item, target_location), build_time_per_item) + addtimer(CALLBACK(src, PROC_REF(do_make_item), design, print_quantity, build_time_per_item, coefficient, charge_per_item, target_location, ID_DATA(usr)), build_time_per_item) return TRUE @@ -382,8 +382,16 @@ * * material_cost_coefficient - the cost efficiency to print 1 design * * charge_per_item - the amount of power to print 1 item * * turf/target - the location to drop the printed item on + * * user_data - ID_DATA(user), see the proc on SSid_access, served for logging */ -/obj/machinery/rnd/production/proc/do_make_item(datum/design/design, items_remaining, build_time_per_item, material_cost_coefficient, charge_per_item, turf/target) +/obj/machinery/rnd/production/proc/do_make_item( + datum/design/design, + items_remaining, + build_time_per_item, + material_cost_coefficient, + charge_per_item, + turf/target, + alist/user_data) PROTECTED_PROC(TRUE) if(!items_remaining) // how @@ -409,8 +417,7 @@ finalize_build() return - if(!materials.can_use_resource()) - say("Unable to continue production, materials on hold.") + if(!materials.can_use_resource(user_data = user_data)) finalize_build() return @@ -420,7 +427,7 @@ say("Unable to continue production, missing materials.") finalize_build() return - materials.use_materials(design_materials, material_cost_coefficient, is_stack ? items_remaining : 1, "built", "[design.name]") + materials.use_materials(design_materials, material_cost_coefficient, is_stack ? items_remaining : 1, "built", "[design.name]", user_data = user_data) var/atom/movable/created if(is_stack) @@ -454,7 +461,7 @@ if(!items_remaining) finalize_build() return - addtimer(CALLBACK(src, PROC_REF(do_make_item), design, items_remaining, build_time_per_item, material_cost_coefficient, charge_per_item, target), build_time_per_item) + addtimer(CALLBACK(src, PROC_REF(do_make_item), design, items_remaining, build_time_per_item, material_cost_coefficient, charge_per_item, target, user_data), build_time_per_item) /// Resets the busy flag /// Called at the end of do_make_item's timer loop diff --git a/code/modules/vehicles/mecha/mech_fabricator.dm b/code/modules/vehicles/mecha/mech_fabricator.dm index d60f3befe54..6292fed5e39 100644 --- a/code/modules/vehicles/mecha/mech_fabricator.dm +++ b/code/modules/vehicles/mecha/mech_fabricator.dm @@ -230,8 +230,10 @@ if(!length(queue)) return FALSE - var/datum/design/D = queue[1] - if(build_part(D, verbose)) + var/alist/queue_record = queue[1] + var/datum/design/D = queue_record["design"] + var/alist/user_data = queue_record["user"] + if(build_part(D, verbose, user_data)) remove_from_queue(1) return TRUE @@ -244,8 +246,9 @@ * Uses materials. * * D - Design datum to attempt to print. * * verbose - Whether the machine should use say() procs. Set to FALSE to disable the machine saying reasons for failure to build. + * * user_data - ID_DATA(user), see the proc on SSid_access */ -/obj/machinery/mecha_part_fabricator/proc/build_part(datum/design/D, verbose = TRUE) +/obj/machinery/mecha_part_fabricator/proc/build_part(datum/design/D, verbose = TRUE, alist/user_data) if(!D || length(D.reagents_list)) return FALSE @@ -254,16 +257,14 @@ if(verbose) say("No access to material storage, please contact the quartermaster.") return FALSE - if (rmat.on_hold()) - if(verbose) - say("Mineral access is on hold, please contact the quartermaster.") + if (!rmat.can_use_resource(user_data = user_data)) return FALSE if(!materials.has_materials(D.materials, component_coeff)) if(verbose) say("Not enough resources. Processing stopped.") return FALSE - rmat.use_materials(D.materials, component_coeff, 1, "built", "[D.name]") + rmat.use_materials(D.materials, component_coeff, 1, "built", "[D.name]", user_data) being_built = D build_finish = world.time + get_construction_time_w_coeff(initial(D.construction_time)) build_start = world.time @@ -331,13 +332,14 @@ * * Returns TRUE if successful and FALSE if the design was not added to the queue. * * D - Datum design to add to the queue. + * user_data - user data in the form rendered by ID_DATA(user), see the proc on SSidaccess */ -/obj/machinery/mecha_part_fabricator/proc/add_to_queue(datum/design/D) +/obj/machinery/mecha_part_fabricator/proc/add_to_queue(datum/design/D, alist/user_data) if(!istype(queue)) queue = list() if(D) - queue[++queue.len] = D + queue[++queue.len] = alist("design" = D, "user" = user_data) return TRUE return FALSE @@ -422,8 +424,9 @@ var/offset = 0 - for(var/datum/design/design in queue) + for(var/alist/queue_item in queue) offset += 1 + var/datum/design/design = queue_item["design"] data["queue"] += list(list( "jobId" = top_job_id + offset, @@ -444,6 +447,8 @@ switch(action) if("build") + if(!rmat.can_use_resource(user_data = ID_DATA(usr))) + return var/designs = params["designs"] if(!islist(designs)) @@ -461,7 +466,7 @@ if(!(design.build_type & MECHFAB) || design.id != design_id) continue - add_to_queue(design) + add_to_queue(design, ID_DATA(usr)) if(params["now"]) if(process_queue) diff --git a/code/modules/wiremod/core/component_printer.dm b/code/modules/wiremod/core/component_printer.dm index ed06086e258..7797d6c8b62 100644 --- a/code/modules/wiremod/core/component_printer.dm +++ b/code/modules/wiremod/core/component_printer.dm @@ -69,7 +69,7 @@ /obj/machinery/component_printer/base_item_interaction(mob/living/user, obj/item/tool, list/modifiers) //to allow quick recycling of circuits if(istype(tool, /obj/item/circuit_component)) - var/amount_inserted = materials.insert_item(tool) + var/amount_inserted = materials.insert_item(tool, user_data = ID_DATA(user)) if(amount_inserted) to_chat(user, span_notice("[tool] worth [amount_inserted / SHEET_MATERIAL_AMOUNT] sheets of material was consumed by [src]")) @@ -115,20 +115,24 @@ update_static_data_for_all_viewers() -/obj/machinery/component_printer/proc/print_component(typepath) +/** + * typepath - the type path of the component to be printed + * user_data - data in the form rendered by ID_DATA(user), for print logging, see the proc on SSid_access +*/ +/obj/machinery/component_printer/proc/print_component(typepath, alist/user_data) var/design_id = current_unlocked_designs[typepath] var/datum/design/design = SSresearch.techweb_design_by_id(design_id) if (!(design.build_type & COMPONENT_PRINTER)) return - if (materials.on_hold()) + if (!materials.can_use_resource(user_data = user_data)) return if (!materials.mat_container.has_materials(design.materials, efficiency_coeff)) return - materials.use_materials(design.materials, efficiency_coeff, 1, "printed", "[design.name]") + materials.use_materials(design.materials, efficiency_coeff, 1, "printed", "[design.name]", user_data) return new design.build_path(drop_location()) /obj/machinery/component_printer/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) @@ -146,8 +150,7 @@ if (!(design.build_type & COMPONENT_PRINTER)) return TRUE - if (materials.on_hold()) - say("Mineral access is on hold, please contact the quartermaster.") + if (!materials.can_use_resource(user_data = ID_DATA(usr))) return TRUE if (!materials.mat_container.has_materials(design.materials, efficiency_coeff)) @@ -386,6 +389,7 @@ . = ..() if (.) return + var/obj/item/card/id/advanced/user_card = astype(usr, /mob/living)?.get_idcard() switch (action) if ("print") @@ -396,22 +400,21 @@ var/list/design = scanned_designs[design_id] - if (materials.on_hold()) - say("Mineral access is on hold, please contact the quartermaster.") + if (!materials.can_use_resource(user_card)) return TRUE if (!materials.mat_container.has_materials(design["materials"], efficiency_coeff)) say("Not enough materials.") return TRUE - materials.use_materials(design["materials"], efficiency_coeff, 1, design["name"], design["materials"]) + materials.use_materials(design["materials"], efficiency_coeff, 1, design["name"], design["materials"], user_card) print_module(design) balloon_alert_to_viewers("printed [design["name"]]") if ("remove_mat") var/datum/material/material = locate(params["ref"]) var/amount = text2num(params["amount"]) // SAFETY: eject_sheets checks for valid mats - materials.eject_sheets(material, amount) + materials.eject_sheets(material, amount, user_data = ID_DATA(usr)) return TRUE diff --git a/code/modules/wiremod/core/integrated_circuit.dm b/code/modules/wiremod/core/integrated_circuit.dm index bbd1303dd05..459ff570172 100644 --- a/code/modules/wiremod/core/integrated_circuit.dm +++ b/code/modules/wiremod/core/integrated_circuit.dm @@ -684,7 +684,7 @@ GLOBAL_LIST_EMPTY_TYPED(integrated_circuits, /obj/item/integrated_circuit) if(!printer) balloon_alert(ui.user, "linked printer not found!") return - component = printer.print_component(component_path) + component = printer.print_component(component_path, user_data = ID_DATA(usr)) if(!component) balloon_alert(ui.user, "failed to make the component!") return diff --git a/tgui/packages/tgui/interfaces/OreSilo.tsx b/tgui/packages/tgui/interfaces/OreSilo.tsx index 7c5a7553256..a7ad68bd379 100644 --- a/tgui/packages/tgui/interfaces/OreSilo.tsx +++ b/tgui/packages/tgui/interfaces/OreSilo.tsx @@ -1,6 +1,9 @@ import { useState } from 'react'; import { Box, + Button, + Collapsible, + Divider, Icon, Image, LabeledList, @@ -11,7 +14,7 @@ import { Tooltip, VirtualList, } from 'tgui-core/components'; -import { classes } from 'tgui-core/react'; +import { BooleanLike, classes } from 'tgui-core/react'; import { capitalize } from 'tgui-core/string'; import { useBackend } from '../backend'; @@ -22,18 +25,32 @@ import type { Material } from './Fabrication/Types'; type Machine = { name: string; icon: string; - onHold: boolean; + on_hold: boolean; location: string; }; +type UserData = { + name: string; + age: number; + assignment: string; + account_id: number; + account_holder: string; + account_assignment: string; + accesses: string[]; + chamelon_override: string | null; + silicon_override: string | null; + id_read_failure: string | null; +}; + type Log = { - rawMaterials: string; - machineName: string; - areaName: string; + raw_materials: string; + machine_name: string; + area_name: string; action: string; amount: number; time: string; noun: string; + user_data: UserData; }; enum Tab { @@ -46,6 +63,9 @@ type Data = { materials: Material[]; machines: Machine[]; logs: Log[]; + // Banned users is a list of bank account datum IDs + banned_users: number[]; + id_required: BooleanLike; }; export const OreSilo = (props: any) => { @@ -56,7 +76,7 @@ export const OreSilo = (props: any) => { return ( - + @@ -84,7 +104,12 @@ export const OreSilo = (props: any) => { onRemove={(index) => act('remove', { id: index })} /> ) : null} - {currentTab === Tab.Logs ? : null} + {currentTab === Tab.Logs && ( + <> + + + + )}
@@ -148,7 +173,7 @@ const MachineDisplay = (props: MachineProps) => { { { onPause(); }} > - + @@ -206,14 +231,32 @@ type LogsListProps = { logs: Log[]; }; +const RestrictButton = () => { + const { act, data } = useBackend(); + const { id_required } = data; + return ( + + + + ); +}; + const LogsList = (props: LogsListProps) => { const { logs } = props; return logs.length > 0 ? ( -
+
+ {logs.map((log, index) => ( - + ))}
@@ -222,29 +265,83 @@ const LogsList = (props: LogsListProps) => { ); }; -type LogProps = { - log: Log; -}; - -const LogEntry = (props: LogProps) => { - const { log } = props; +const UserItem = (props: UserData) => { + const { + name, + age, + assignment, + account_id, + account_holder, + account_assignment, + accesses, + chamelon_override, + silicon_override, + id_read_failure, + } = props; + const { act, data } = useBackend(); + const { banned_users } = data; return ( -
- - {log.time} - - {capitalize(log.machineName)} - - {log.areaName} - 0 ? 'good' : 'bad'} - > - {log.rawMaterials} - - -
+ + {name} + {assignment} + {!id_read_failure && !silicon_override && ( + + + + )} + + ); +}; + +const formatAmount = (action: string, amount: number) => { + const isSheetAction = action === 'EJECT' || action === 'DEPOSIT'; + const rawAmount = Math.abs(amount); + if (!isSheetAction) { + return rawAmount; + } + const proportionalAmount = rawAmount / 100; + return ` ${proportionalAmount} `; +}; + +const LogEntry = (props: Log) => { + const { + raw_materials, + machine_name, + area_name, + action, + amount, + time, + noun, + user_data, + } = props; + return ( + +
+ + {time} + + {capitalize(machine_name)} + + {area_name} + 0 ? 'good' : 'bad'} + > + {raw_materials} + + + + + +
+
); }; diff --git a/tgui/packages/tgui/styles/interfaces/OreSilo.scss b/tgui/packages/tgui/styles/interfaces/OreSilo.scss new file mode 100644 index 00000000000..024e95af316 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/OreSilo.scss @@ -0,0 +1,23 @@ +.OreSilo { + .__LogEntry { + font-size: 1rem; + outline-style: ridge; + outline-width: 1px; + background-blend-mode: screen; + .__UserItem { + font-family: 'Helvetica'; + font-kerning: auto; + text-rendering: optimizeLegibility; + .__Name { + outline: ridge black; + outline-offset: 1.5px; + margin-right: 1.5px; + } + .__Assignment { + outline: ridge black; + outline-offset: 1.5px; + letter-spacing: 1.1px; + } + } + } +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 472fe4c8a06..320122c24f1 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -37,6 +37,7 @@ @include meta.load-css('./interfaces/NtosNotepad.scss'); @include meta.load-css('./interfaces/NuclearBomb.scss'); @include meta.load-css('./interfaces/Orbit.scss'); +@include meta.load-css('./interfaces/OreSilo.scss'); @include meta.load-css('./interfaces/Paper.scss'); @include meta.load-css('./interfaces/PreferencesMenu.scss'); @include meta.load-css('./interfaces/RequestManager.scss');