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
🆑 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)
/🆑
This commit is contained in:
Joshua Kidder
2025-07-30 17:37:38 -04:00
committed by Roxy
parent 371069ae18
commit ff7ee72146
17 changed files with 631 additions and 163 deletions
@@ -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)
@@ -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)
@@ -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
@@ -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)
+2 -1
View File
@@ -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))
+2 -8
View File
@@ -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)
+6 -7
View File
@@ -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)
+340 -16
View File
@@ -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]"
+1 -1
View File
@@ -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()
@@ -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
+15 -8
View File
@@ -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
+16 -11
View File
@@ -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)
+13 -10
View File
@@ -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
@@ -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
+132 -35
View File
@@ -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 (
<Window title="Ore Silo" width={620} height={600}>
<Window.Content>
<Window.Content className="OreSilo">
<Stack vertical fill>
<Stack.Item>
<Tabs fluid>
@@ -84,7 +104,12 @@ export const OreSilo = (props: any) => {
onRemove={(index) => act('remove', { id: index })}
/>
) : null}
{currentTab === Tab.Logs ? <LogsList logs={logs!} /> : null}
{currentTab === Tab.Logs && (
<>
<RestrictButton />
<LogsList logs={logs!} />
</>
)}
</Stack.Item>
<Stack.Item>
<Section fill>
@@ -148,7 +173,7 @@ const MachineDisplay = (props: MachineProps) => {
<Box className="FabricatorRecipe">
<Box
className={
machine.onHold
machine.on_hold
? classes([
'FabricatorRecipe__Title',
'FabricatorRecipe__Title--disabled',
@@ -168,7 +193,7 @@ const MachineDisplay = (props: MachineProps) => {
<Tooltip
content={
machine.onHold
machine.on_hold
? `Resume ${machine.name} usage.`
: `Put ${machine.name} on hold.`
}
@@ -182,7 +207,7 @@ const MachineDisplay = (props: MachineProps) => {
onPause();
}}
>
<Icon name={machine.onHold ? 'circle-play' : 'circle-pause'} />
<Icon name={machine.on_hold ? 'circle-play' : 'circle-pause'} />
</Box>
</Tooltip>
<Tooltip content={`Disconnect ${machine.name}.`}>
@@ -206,14 +231,32 @@ type LogsListProps = {
logs: Log[];
};
const RestrictButton = () => {
const { act, data } = useBackend<Data>();
const { id_required } = data;
return (
<Box align="center">
<Button
position="relative"
className="__RestrictButton"
color={id_required ? 'bad' : 'good'}
onClick={() => act('toggle_restrict')}
>
{id_required ? 'Disable ID Requirement' : 'Enable ID Requirement'}
</Button>
</Box>
);
};
const LogsList = (props: LogsListProps) => {
const { logs } = props;
return logs.length > 0 ? (
<Section fill scrollable pr={1} height="100%">
<Section fill scrollable pr={1} align="center">
<Divider />
<VirtualList>
{logs.map((log, index) => (
<LogEntry key={index} log={log} />
<LogEntry key={index} {...log} />
))}
</VirtualList>
</Section>
@@ -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<Data>();
const { banned_users } = data;
return (
<Section
title={`${capitalize(log.action)}: x${Math.abs(log.amount)} ${log.noun}`}
>
<LabeledList>
<LabeledList.Item label="Time">{log.time}</LabeledList.Item>
<LabeledList.Item label="Machine">
{capitalize(log.machineName)}
</LabeledList.Item>
<LabeledList.Item label="Location">{log.areaName}</LabeledList.Item>
<LabeledList.Item
label="Materials"
color={log.amount > 0 ? 'good' : 'bad'}
>
{log.rawMaterials}
</LabeledList.Item>
</LabeledList>
</Section>
<Stack align="center" className="__UserItem">
<Stack.Item className="__Name">{name}</Stack.Item>
<Stack.Item className="__Assignment">{assignment}</Stack.Item>
{!id_read_failure && !silicon_override && (
<Stack.Item>
<Button
className="__AntiRoboticistButton" // we have fun here
color={banned_users.includes(account_id) ? 'bad' : 'good'}
onClick={() => act('toggle_ban', { user_data: props })}
>
{banned_users.includes(account_id) ? 'Unban' : 'Ban'} User?
</Button>
</Stack.Item>
)}
</Stack>
);
};
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 (
<Collapsible
title={`${action.toUpperCase()} ${formatAmount(action, amount)} ${noun}, [${user_data.name} | ${user_data.assignment.toUpperCase()}]`}
>
<Section className="__LogEntry">
<LabeledList>
<LabeledList.Item label="Time">{time}</LabeledList.Item>
<LabeledList.Item label="Machine">
{capitalize(machine_name)}
</LabeledList.Item>
<LabeledList.Item label="Location">{area_name}</LabeledList.Item>
<LabeledList.Item
label="Materials"
color={amount > 0 ? 'good' : 'bad'}
>
{raw_materials}
</LabeledList.Item>
<LabeledList.Item label="User">
<UserItem {...user_data} />
</LabeledList.Item>
</LabeledList>
</Section>
</Collapsible>
);
};
@@ -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;
}
}
}
}
+1
View File
@@ -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');