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');