moves the material container files inside the datum/materials folder and the food folder inside module/food_and_drinks. (#96526)

## About The Pull Request
Title.

## Why It's Good For The Game
In the case of materials. the two folders had roughly the same names,
and material_containers rely on materials to work anyway so they belong
inside the materials folder, instead of a separate directory with the
same name (minus an s).

As for food, the food items have filepaths that differ too much from the
corresponding recipe files, despite the two being closely connected as
far as features and game mechanics go, and that makes moving back and
forth between these files a pain in the ass with a standard file
manager, workspace viewer etc.

## Changelog
N/A
This commit is contained in:
Ghom
2026-06-16 14:43:26 +12:00
committed by GitHub
parent 60e9c30034
commit 564939d751
34 changed files with 33 additions and 33 deletions
@@ -0,0 +1,801 @@
/*!
This datum should be used for handling mineral contents of machines and whatever else is supposed to hold minerals and make use of them.
Variables:
amount - raw amount of the mineral this container is holding, calculated by the defined value SHEET_MATERIAL_AMOUNT=SHEET_MATERIAL_AMOUNT.
max_amount - max raw amount of mineral this container can hold.
sheet_type - type of the mineral sheet the container handles, used for output.
parent - object that this container is being used by, used for output.
MAX_STACK_SIZE - size of a stack of mineral sheets. Constant.
*/
//The full item was consumed
#define MATERIAL_INSERT_ITEM_SUCCESS 1
/datum/material_container
///Parent to which this container is attached to
VAR_PRIVATE/atom/movable/parent
/// The maximum amount of materials this material container can contain
var/max_amount
/// Map of material ref -> amount
var/list/materials //Map of key = material ref | Value = amount
/// The list of materials that this material container can accept
var/list/allowed_materials
/// The typecache of things that this material container can accept
var/list/allowed_item_typecache
/// Whether or not this material container allows specific amounts from sheets to be inserted
var/precise_insertion = FALSE
/// The material container flags. See __DEFINES/construction/materials.dm.
var/mat_container_flags
/// Signals that are registered with this contained
var/list/registered_signals
/// Sets up the proper signals and fills the list of materials with the appropriate references.
/datum/material_container/New(
target,
list/init_mats,
max_amt = 0,
_mat_container_flags = NONE,
list/allowed_mats = init_mats,
list/allowed_items,
list/container_signals
)
parent = target
materials = list()
max_amount = max(0, max_amt)
mat_container_flags = _mat_container_flags
allowed_materials = allowed_mats || list()
if(allowed_items)
if(ispath(allowed_items) && allowed_items == /obj/item/stack)
allowed_item_typecache = GLOB.typecache_stack
else
allowed_item_typecache = typecacheof(allowed_items)
//Make the assoc list material reference -> amount
for(var/mat in init_mats)
var/mat_ref = SSmaterials.get_material(mat)
if(isnull(mat_ref))
continue
var/mat_amt = init_mats[mat]
if(isnull(mat_amt))
mat_amt = 0
materials[mat_ref] += mat_amt
//all user handled signals
if(length(container_signals))
for(var/signal in container_signals)
parent.RegisterSignal(src, signal, container_signals[signal])
//can we insert into this container
if(!(mat_container_flags & MATCONTAINER_NO_INSERT))
//to insert stuff into the container
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION, PROC_REF(on_item_insert))
RegisterSignal(parent, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY, PROC_REF(on_secondary_insert))
//screen tips for inserting items
parent.flags_1 |= HAS_CONTEXTUAL_SCREENTIPS_1
RegisterSignal(parent, COMSIG_ATOM_REQUESTING_CONTEXT_FROM_ITEM, PROC_REF(on_requesting_context_from_item))
//to see available materials
if(mat_container_flags & MATCONTAINER_EXAMINE)
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
//drop sheets when the object is deconstructed but not deleted
RegisterSignal(parent, COMSIG_OBJ_DECONSTRUCT, PROC_REF(drop_sheets))
/datum/material_container/Destroy(force)
materials = null
allowed_materials = null
return ..()
/datum/material_container/proc/drop_sheets()
SIGNAL_HANDLER
retrieve_all()
/datum/material_container/proc/on_examine(datum/source, mob/user, list/examine_texts)
SIGNAL_HANDLER
for(var/I in materials)
var/datum/material/M = I
var/amt = materials[I] / SHEET_MATERIAL_AMOUNT
if(amt)
examine_texts += span_notice("It has [amt] sheets of [LOWER_TEXT(M.name)] stored.")
/datum/material_container/vv_edit_var(var_name, var_value)
var/old_flags = mat_container_flags
. = ..()
if(var_name == NAMEOF(src, mat_container_flags) && parent)
if(!(old_flags & MATCONTAINER_EXAMINE) && mat_container_flags & MATCONTAINER_EXAMINE)
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
else if(old_flags & MATCONTAINER_EXAMINE && !(mat_container_flags & MATCONTAINER_EXAMINE))
UnregisterSignal(parent, COMSIG_ATOM_EXAMINE)
if(old_flags & MATCONTAINER_NO_INSERT && !(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))
else if(!(old_flags & MATCONTAINER_NO_INSERT) && mat_container_flags & MATCONTAINER_NO_INSERT)
UnregisterSignal(parent, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY))
/**
* 3 Types of Procs
* Material Insertion : Insert materials into the container
* Material Validation : Checks how much materials are available, Extracts materials from items if the container can hold them
* Material Removal : Removes material from the container
*
* Each Proc further belongs to a specific category
* LOW LEVEL: Procs that are used internally & should not be used anywhere else unless you know what your doing
* MID LEVEL: Procs that can be used by machines(like recycler, stacking machines) to bypass majority of checks
* HIGH LEVEL: Procs that can be used by anyone publicly and guarantees safety checks & limits
*/
//================================Material Insertion procs==============================
//======================================LOW LEVEL=========================================
/**
* Inserts the relevant materials from an item into this material container.
* This low level proc should not be used directly by anyone
*
* 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, 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/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
var/list/item_materials = source.get_material_composition(mat_container_flags)
var/list/mats_consumed = list()
for(var/MAT in item_materials)
if(!can_hold_material(MAT))
continue
var/mat_amount = OPTIMAL_COST(item_materials[MAT] * multiplier)
materials[MAT] += mat_amount
if(item_materials[MAT] > max_mat_value)
max_mat_value = item_materials[MAT]
primary_mat = MAT
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, user_data)
return primary_mat
//===================================================================================
//===============================MID LEVEL===================================================
/**
* For inserting an amount of material. Use this to add materials to the container directly
*
* Arguments:
* - amt: amount of said material to insert
* - mat: the material type to insert
*/
/datum/material_container/proc/insert_amount_mat(amt, datum/material/mat)
if(amt <= 0)
return 0
amt = OPTIMAL_COST(amt)
if(!has_space(amt))
return 0
var/total_amount_saved = total_amount()
if(mat)
if(!istype(mat))
mat = SSmaterials.get_material(mat)
materials[mat] += amt
else
var/num_materials = length(materials)
if(!num_materials)
return 0
amt /= num_materials
for(var/i in materials)
materials[i] += amt
return (total_amount() - total_amount_saved)
/**
* Proc specifically for inserting items, use this when you want to insert any item into the container
* this bypasses most of the material flag checks so much be used by machines like recycler, stacking machine etc that
* does not care for such checks
*
* Arguments:
* - [weapon][obj/item]: the item you are trying to insert
* - 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/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)
var/obj/item/target = weapon
var/material_amount = OPTIMAL_COST(get_item_material_amount(target) * multiplier)
if(!material_amount)
return MATERIAL_INSERT_ITEM_NO_MATS
var/obj/item/stack/item_stack
if(isstack(weapon) && !has_space(material_amount)) //not enough space split and feed as many sheets possible
item_stack = weapon
var/space_left = max_amount - total_amount()
if(!space_left)
return MATERIAL_INSERT_ITEM_NO_SPACE
var/material_per_sheet = material_amount / item_stack.amount
var/sheets_to_insert = floor(space_left / material_per_sheet)
if(!sheets_to_insert)
return MATERIAL_INSERT_ITEM_NO_SPACE
target = fast_split_stack(item_stack, sheets_to_insert)
material_amount = get_item_material_amount(target) * multiplier
material_amount = OPTIMAL_COST(material_amount)
//do the insert
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
return material_amount
else if(!isnull(item_stack) && item_stack != target) //insertion failed, merge the split stack back into the original
var/obj/item/stack/inserting_stack = target
item_stack.add(inserting_stack.amount)
qdel(inserting_stack)
return MATERIAL_INSERT_ITEM_FAILURE
//============================================================================================
//===================================HIGH LEVEL===================================================
/**
* inserts an item from the players hand into the container. Loops through all the contents inside recursively
* Does all explicit checking for mat flags & callbacks to check if insertion is valid
* This proc is what you should be using for almost all cases
*
* Arguments:
* * held_item - the item to insert
* * user - the mob inserting this item
* * context - the atom performing the operation, this is the last argument sent in COMSIG_MATCONTAINER_ITEM_CONSUMED and is used mostly for silo logging
*/
/datum/material_container/proc/user_insert(obj/item/held_item, mob/living/user, atom/context = parent)
set waitfor = FALSE
. = 0
//All items that do not have any contents
var/list/obj/item/items = list(held_item)
//is this the first item we are ever processing
var/first_checks = TRUE
//list of items to delete
var/list/obj/item/to_delete = list()
//The status of the last insert attempt
var/inserted = 0
//All messages to be displayed to chat
var/list/chat_msgs = list()
//differs from held_item when using TK
var/obj/item/active_held = user.get_active_held_item()
//omni tools can act as any tool so get its real behaviour
active_held = active_held.get_proxy_attacker_for(held_item)
while(items.len)
//no point inserting more items
if(inserted == MATERIAL_INSERT_ITEM_NO_SPACE)
break
//Pop the 1st item out from the list
var/obj/item/target_item = items[1]
items -= target_item
//e.g. projectiles inside bullets are not objects
if(!istype(target_item))
continue
//can't allow abstract, hologram items
if((target_item.item_flags & ABSTRACT) || (target_item.flags_1 & HOLOGRAM_1))
continue
//user defined conditions
if(SEND_SIGNAL(src, COMSIG_MATCONTAINER_PRE_USER_INSERT, target_item, user) & MATCONTAINER_BLOCK_INSERT)
continue
//item is either indestructible, not allowed for redemption or not in the allowed types
if((target_item.resistance_flags & INDESTRUCTIBLE) || (target_item.item_flags & NO_MAT_REDEMPTION) || (allowed_item_typecache && !is_type_in_typecache(target_item, allowed_item_typecache)))
if(!(mat_container_flags & MATCONTAINER_SILENT))
var/list/status_data = chat_msgs["[MATERIAL_INSERT_ITEM_FAILURE]"] || list()
var/list/item_data = status_data[target_item.name] || list()
item_data["item"] = target_item
item_data["count"] += 1
status_data[target_item.name] = item_data
chat_msgs["[MATERIAL_INSERT_ITEM_FAILURE]"] = status_data
if(target_item.resistance_flags & INDESTRUCTIBLE)
if(target_item != active_held) //move it out of any storage medium its in so it doesn't get consumed with its parent, but only if that storage medium is not our hand
target_item.forceMove(get_turf(context))
continue
//storage items usually come here
//this is so players can insert items from their bags into machines for convinience
if(!target_item.atom_storage || !target_item.contents.len)
continue
//at this point we can check if we have enough for all items & other stuff
if(first_checks)
//duffle bags needs to be unzipped
if(target_item.atom_storage?.locked)
if(!(mat_container_flags & MATCONTAINER_SILENT))
to_chat(user, span_warning("[target_item] has its storage locked"))
return
//anything that isn't a stack cannot be split so find out if we have enough space, we don't want to consume half the contents of an object & leave it in a broken state
//for duffle bags and other storage items we can check for space 1 item at a time
if(!isstack(target_item) && !target_item.atom_storage)
var/total_amount = 0
for(var/obj/item/weapon as anything in target_item.get_all_contents_type(/obj/item))
total_amount += get_item_material_amount(weapon)
if(!has_space(total_amount))
if(!(mat_container_flags & MATCONTAINER_SILENT))
to_chat(user, span_warning("[parent] does not have enough space for [target_item]!"))
return
first_checks = FALSE
//if stack, check if we want to read precise amount of sheets to insert
var/obj/item/stack/item_stack = null
if(isstack(target_item) && precise_insertion)
var/atom/current_parent = parent
item_stack = target_item
var/requested_amount = tgui_input_number(user, "How much do you want to insert?", "Inserting [item_stack.singular_name]s", item_stack.amount, item_stack.amount)
if(!requested_amount || QDELETED(target_item) || QDELETED(user) || QDELETED(src))
continue
if(parent != current_parent || user.get_active_held_item() != active_held)
continue
if(requested_amount != item_stack.amount) //only split if its not the whole amount
target_item = fast_split_stack(item_stack, requested_amount) //split off the requested amount
requested_amount = 0
//is this item a stack and was it split by the player?
var/was_stack_split = !isnull(item_stack) && item_stack != target_item
//if it was split then item_stack has the reference to the original stack/item
var/obj/item/original_item = was_stack_split ? item_stack : target_item
//if this item is not the one the player is holding then don't remove it from their hand
if(original_item != active_held)
original_item = null
if(!isnull(original_item) && !user.temporarilyRemoveItemFromInventory(original_item)) //remove from hand(if split remove the original stack else the target)
return
//insert the item
var/item_name = target_item.name
var/item_count = 1
var/is_stack = FALSE
if(isstack(target_item))
var/obj/item/stack/the_stack = target_item
item_name = the_stack.singular_name
item_count = the_stack.amount
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, user_data = ID_DATA(user)) : 0
if(inserted > 0)
. += inserted
inserted /= SHEET_MATERIAL_AMOUNT // display units inserted as sheets for improved readability
//collect all messages to print later
var/list/status_data = chat_msgs["[MATERIAL_INSERT_ITEM_SUCCESS]"] || list()
var/list/item_data = status_data[item_name] || list()
item_data["item"] = target_item
item_data["count"] += item_count
item_data["amount"] += inserted
status_data[item_name] = item_data
chat_msgs["[MATERIAL_INSERT_ITEM_SUCCESS]"] = status_data
//delete the item or merge stacks if any left over
if(is_stack)
//player split it & machine further split that due to lack of space? merge with remaining stack
if(!QDELETED(target_item) && was_stack_split)
var/obj/item/stack/inserting_stack = target_item
item_stack.add(inserting_stack.amount)
qdel(inserting_stack)
//was this the original item in the players hand? put what's left back in the player's hand
if(!QDELETED(original_item))
user.put_in_active_hand(original_item)
//skip processing children & other stuff. irrelevant for stacks
continue
//queue the object for deletion
to_delete += target_item
else
//collect all messages to print later
var/list/status_data = chat_msgs["[inserted]"] || list()
var/list/item_data = status_data[item_name] || list()
item_data["item"] = target_item
item_data["count"] += item_count
status_data[item_name] = item_data
chat_msgs["[inserted]"] = status_data
//player split the stack by the requested amount but even that split amount could not be salvaged. merge it back with the original
if(was_stack_split)
var/obj/item/stack/inserting_stack = target_item
item_stack.add(inserting_stack.amount)
qdel(inserting_stack)
//was this the original item in the players hand? put it back because we coudn't salvage it
if(!QDELETED(original_item))
user.put_in_active_hand(original_item)
//we can stop here as remaining items will fail to insert as well
if(inserted == MATERIAL_INSERT_ITEM_NO_SPACE)
break
//we failed to process the item so don't bother going into its contents
//but if we are dealing with storage items like bags, boxes etc then we make a exception
if(!target_item.atom_storage)
continue
//If any mats were consumed we can proceed to delete the parent
//If it has children then we will process them first in the 2nd round
//This is done so we don't delete the children when the parent is consumed
//We only do this on the 1st iteration so we don't re-iterate through its children again
if(target_item.contents.len)
if(target_item.atom_storage?.locked) //can't access contents of locked storage(like duffle bags)
continue
//process children
items += target_item.contents
//we now summarize the chat msgs collected
if(!(mat_container_flags & MATCONTAINER_SILENT))
for(var/status in chat_msgs)
var/list/status_data = chat_msgs[status]
for(var/item_name in status_data)
//read the params
var/list/chat_data = status_data[item_name]
var/obj/item/item = chat_data["item"]
var/count = chat_data["count"]
var/amount = chat_data["amount"]
//decode the message
switch(text2num(status))
if(MATERIAL_INSERT_ITEM_SUCCESS) //no problems full item was consumed
if(isstack(item))
var/obj/item/stack/stack = item
var/sheets = min(count, amount) //minimum between sheets inserted vs sheets consumed(values differ for alloys)
if (sheets > 1)
to_chat(user, span_notice("[sheets] [stack.singular_name][plural_s(stack.singular_name)] were added to [parent]."))
else
to_chat(user, span_notice("The [stack.singular_name] was added to [parent]."))
else
if (count > 1)
to_chat(user, span_notice("[count] [item.name][plural_s(item.name)], worth [amount] sheet\s, were added to [parent]."))
else
to_chat(user, span_notice("\The [item], worth [amount] sheet\s, [item.p_were()] added to [parent]."))
if(MATERIAL_INSERT_ITEM_NO_SPACE) //no space
to_chat(user, span_warning("[parent] has no space to accept \the [item]!"))
if(MATERIAL_INSERT_ITEM_NO_MATS) //no materials inside these items
if(isstack(item))
var/obj/item/stack/stack = item
to_chat(user, span_warning("The [stack.singular_name] has no materials that can be accepted by [parent]!"))
else
to_chat(user, span_warning("\The [item][count > 1 ? "[plural_s(item.name)] have" : " [item.p_have()]"] no materials that can be accepted by [parent]!"))
if(MATERIAL_INSERT_ITEM_FAILURE) //could be because the material type was not accepted or other stuff
if (isstack(item))
var/obj/item/stack/stack = item
to_chat(user, span_warning("The [stack.singular_name] was rejected by [parent]!"))
else
to_chat(user, span_warning("\The [item][count > 1 ? "[plural_s(item.name)] were" : " [item.p_were()]"] rejected by [parent]!"))
//finally delete the items
for(var/obj/item/deleting as anything in to_delete)
if(!QDELETED(deleting)) //deleting parents also delete their children so we check
qdel(deleting)
/datum/material_container/proc/on_item_insert(datum/source, mob/living/user, obj/item/weapon)
SIGNAL_HANDLER
// Don't insert material items with left click
if (isstack(weapon))
return attempt_insert(user, weapon)
/datum/material_container/proc/on_secondary_insert(datum/source, mob/living/user, obj/item/weapon)
SIGNAL_HANDLER
return attempt_insert(user, weapon)
/// Proc that allows players to fill the parent with mats
/datum/material_container/proc/attempt_insert(mob/living/user, obj/item/weapon)
//Allows you to attack the machine with iron sheets for e.g.
if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode)
return
if(ismachinery(parent))
var/obj/machinery/machine = parent
if(machine.machine_stat || machine.panel_open)
return
user_insert(weapon, user)
return ITEM_INTERACT_SUCCESS
//===============================================================================================
//======================================Material Validation=======================================
//=========================================LOW LEVEL===================================
/**
* Proc that returns TRUE if the container has space
*
* Arguments:
* * amt - can this container hold this much amount of materials
*/
/datum/material_container/proc/has_space(amt = 0)
return (total_amount() + amt) <= max_amount
/**
* The default check for whether we can add materials to this material container.
*
* Arguments:
* - [mat][/atom/material]: The material we are checking for insertability.
*/
/datum/material_container/proc/can_hold_material(datum/material/mat)
if(mat in allowed_materials)
return TRUE
if(istype(mat) && ((mat.id in allowed_materials) || (mat.type in allowed_materials)))
allowed_materials += mat // This could get messy with passing lists by ref... but if you're doing that the list expansion is probably being taken care of elsewhere anyway...
return TRUE
if(SEND_SIGNAL(src, COMSIG_MATCONTAINER_MAT_CHECK, mat) & MATCONTAINER_ALLOW_MAT)
allowed_materials += mat
return TRUE
return FALSE
//========================================================================================
//===================================MID LEVEL=============================================
/**
* Returns the amount of a specific material in this container.
*
* Arguments:
* -[mat][datum/material] : the material type to check for 3 cases
* a) If it's an path its ref is retrieved
* b) If it's text then its an category material & there is no way to deal with it so return 0
* c) If normal material proceeds as usual
*/
/datum/material_container/proc/get_material_amount(datum/material/mat)
if(!istype(mat))
mat = SSmaterials.get_material(mat)
return materials[mat]
/**
* Returns the amount of material relevant to this container;
* if this container does not support glass, any glass in 'I' will not be taken into account
*
* Arguments:
* - [I][obj/item]: the item whos materials must be retrieved
*/
/datum/material_container/proc/get_item_material_amount(obj/item/item)
if(!istype(item) || !item.custom_materials)
return 0
var/material_amount = 0
var/list/item_materials = item.get_material_composition(mat_container_flags)
for(var/MAT in item_materials)
if(!can_hold_material(MAT))
continue
material_amount += item_materials[MAT]
return material_amount
//================================================================================================
//=========================================HIGH LEVEL==========================================
/// returns the total amount of material in the container
/datum/material_container/proc/total_amount()
. = 0
for(var/i in materials)
. += get_material_amount(i)
/**
* Returns TRUE if you have enough of the specified material.
*
* Arguments:
* - [req_mat][datum/material]: the material to check for
* - amount: how much material do we need
*/
/datum/material_container/proc/has_enough_of_material(datum/material/req_mat, amount = 1)
return get_material_amount(req_mat) >= OPTIMAL_COST(amount)
/**
* Checks if its possible to afford a certain amount of materials. Takes a dictionary of materials.
* coefficient can be thought of as the machines efficiency & multiplier as the print quantity
*
* Arguments:
* - mats: list of materials(key=material, value= 1 unit of material) to check for
* - coefficient: scaling applied to 1 unit of material in the mats list
* - multiplier: how many units(after scaling) do we require
*/
/datum/material_container/proc/has_materials(list/mats, coefficient = 1, multiplier = 1)
if(!length(mats))
return FALSE
for(var/x in mats) //Loop through all required materials
var/wanted = OPTIMAL_COST(mats[x] * coefficient) * multiplier
if(!has_enough_of_material(x, wanted))//Not a category, so just check the normal way
return FALSE
return TRUE
//==========================================================================================================
//================================================Material Usage============================================
//==================================================LOW LEVEL=======================================
/**
* Uses an amount of a specific material, effectively removing it.
*
* Arguments:
* - amt: amount of said material to use
* - [mat][datum/material]: type of mat to use
*/
/datum/material_container/proc/use_amount_mat(amt, datum/material/mat)
//round amount
amt = OPTIMAL_COST(amt)
//get ref if necessary
if(!istype(mat))
mat = SSmaterials.get_material(mat)
//check if sufficient is available
if(materials[mat] < amt)
return 0
//consume & return amount consumed
materials[mat] -= amt
return amt
//==============================================================================================
//=========================================MID LEVEL==========================================
/**
* For consuming a dictionary of materials.
*
* Arguments:
* - mats: map of materials to consume(key = material type, value = amount)
* - coefficient: how much fraction of unit material in the mats list must be consumed. This is usually your machines efficiency
* - multiplier: how many units of material in the mats list(after each unit is multiplied and rounded with coefficient) must be consumed, This is usually your print quantity
*/
/datum/material_container/proc/use_materials(list/mats, coefficient = 1, multiplier = 1)
if(!mats || !length(mats))
return FALSE
var/amount_removed = 0
for(var/i in mats)
amount_removed += use_amount_mat(OPTIMAL_COST(mats[i] * coefficient) * multiplier, i)
return amount_removed
//============================================================================================
//===========================================HIGH LEVEL=======================================
/**
* For spawning stacks (mineral sheets or ore) at a specific location. Used by machines to output sheets.
*
* Arguments:
* stack_amt: number of sheets to extract
* [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_STACK_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/material_container/proc/retrieve_stack(stack_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
if(!can_hold_material(material))
return 0
//requested amount greater than available amount or just an invalid value
stack_amt = min(floor(materials[material] / SHEET_MATERIAL_AMOUNT), stack_amt)
if(stack_amt <= 0)
return 0
//auto drop location
if(!target)
var/atom/parent_atom = parent
target = parent_atom.drop_location()
if(!target)
return 0
//eject sheets based on available amount after each iteration
var/count = 0
while(stack_amt > 0)
var/type_to_retrieve = material.sheet_type || material.ore_type
//don't merge yet. we need to do stuff with it first
var/obj/item/stack/new_stack = new type_to_retrieve(target, min(stack_amt, MAX_STACK_SIZE), FALSE)
if(istype(new_stack, /obj/item/stack/sheet))
var/obj/item/stack/sheet/new_sheets = new_stack
new_sheets.manufactured = TRUE
count += new_stack.amount
//use material & deduct work needed
use_amount_mat(new_stack.amount * SHEET_MATERIAL_AMOUNT, material)
stack_amt -= new_stack.amount
//send signal
SEND_SIGNAL(src, COMSIG_MATCONTAINER_STACK_RETRIEVED, new_stack, context, user_data)
//no point merging anything into an already full stack
if(new_stack.amount == new_stack.max_amount)
continue
//now we can merge since we are done with it
for(var/obj/item/stack/item_stack in target)
if(item_stack == new_stack || item_stack.type != type_to_retrieve) //don't merge with self or different type
continue
//speed merge
var/merge_amount = min(item_stack.amount, new_stack.max_amount - new_stack.get_amount())
item_stack.use(merge_amount)
new_stack.add(merge_amount)
break
return count
/**
* Proc to get all the materials and dump them as sheets
*
* Arguments:
* - target: drop location of the sheets
* - context: the atom which is ejecting the sheets. Used mostly in silo logging
*/
/datum/material_container/proc/retrieve_all(target = null, atom/context = parent)
var/result = 0
for(var/MAT in materials)
result += retrieve_stack(amount2sheet(materials[MAT]), MAT, target, context, user_data = ID_DATA(null))
return result
//============================================================================================
/datum/material_container/ui_static_data(mob/user)
var/list/data = list()
data["SHEET_MATERIAL_AMOUNT"] = SHEET_MATERIAL_AMOUNT
return data
/// List format is list(material_name = list(amount = ..., ref = ..., etc.))
/datum/material_container/ui_data(mob/user)
var/list/data = list()
for(var/datum/material/material as anything in materials)
var/amount = materials[material]
data += list(list(
"name" = material.name,
"ref" = REF(material),
"amount" = amount,
"color" = material.greyscale_color || material.color
))
return data
/**
* Adds context sensitivy directly to the material container file for screentips
* Arguments:
* * source - refers to item that will display its screentip
* * context - refers to, in this case, an item in the users hand hovering over the material container, such as an autolathe
* * held_item - refers to the item that has materials accepted by the material container
* * user - refers to user who will see the screentip when the proper context and tool are there
*/
/datum/material_container/proc/on_requesting_context_from_item(datum/source, list/context, obj/item/held_item, mob/living/user)
SIGNAL_HANDLER
if(isnull(held_item))
return NONE
if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode)
return NONE
if(held_item.item_flags & ABSTRACT)
return NONE
if((held_item.flags_1 & HOLOGRAM_1) || (held_item.item_flags & NO_MAT_REDEMPTION) || (allowed_item_typecache && !is_type_in_typecache(held_item, allowed_item_typecache)))
return NONE
var/list/item_materials = held_item.get_material_composition(mat_container_flags)
if(!length(item_materials))
return NONE
for(var/material in item_materials)
if(can_hold_material(material))
continue
return NONE
if (isstack(held_item))
context[SCREENTIP_CONTEXT_LMB] = "Insert stack"
context[SCREENTIP_CONTEXT_RMB] = "Insert"
return CONTEXTUAL_SCREENTIP_SET
#undef MATERIAL_INSERT_ITEM_SUCCESS
@@ -0,0 +1,293 @@
/*
This component allows machines to connect remotely to a material container
(namely an /obj/machinery/ore_silo) elsewhere. It offers optional graceful
fallback to a local material storage in case remote storage is unavailable, and
handles linking back and forth.
*/
/datum/remote_materials
// Three possible states:
// 1. silo exists, materials is parented to silo
// 2. silo is null, materials is parented to parent
// 3. silo is null, materials is null
///Movable this connection is attached to
var/atom/movable/parent
///The silo machine this container is connected to
var/obj/machinery/ore_silo/silo
///Material container. the value is either the silo or local
var/datum/material_container/mat_container
///Should we create a local storage if we can't connect to silo
var/allow_standalone
///Local size of container when silo = null
var/local_size = INFINITY
///Flags used for the local material container(exceptions for item insert & intent flags)
var/mat_container_flags = NONE
///List of signals to hook onto the local container
var/list/mat_container_signals
///Callback for round start silo connections. Have to cancel this if the parent gets destroyed before round start
VAR_PRIVATE/datum/callback/connection
/datum/remote_materials/New(
parent,
mapload,
allow_standalone = TRUE,
force_connect = FALSE,
mat_container_flags = NONE,
list/mat_container_signals = null,
)
src.parent = parent
src.allow_standalone = allow_standalone
src.mat_container_flags = mat_container_flags
src.mat_container_signals = mat_container_signals
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_MULTITOOL), PROC_REF(OnMultitool))
var/turf/T = get_turf(parent)
var/connect_to_silo = FALSE
if(force_connect || (mapload && is_station_level(T.z)))
connect_to_silo = TRUE
if(mapload) // wait for silo to initialize during mapload
connection = CALLBACK(src, PROC_REF(_PrepareStorage), connect_to_silo)
SSticker.OnRoundstart(connection)
else //directly register in round
_PrepareStorage(connect_to_silo)
/**
* Internal proc. prepares local storage if onnect_to_silo = FALSE
*
* Arguments
* connect_to_silo- if true connect to global silo. If not successfull then go to local storage
* only if allow_standalone = TRUE, else you a null mat_container
*/
/datum/remote_materials/proc/_PrepareStorage(connect_to_silo)
PRIVATE_PROC(TRUE)
connection = null
if (connect_to_silo)
silo = GLOB.ore_silo_default
if (silo)
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))
if (!mat_container && allow_standalone)
_MakeLocal()
/datum/remote_materials/Destroy()
if(connection)
LAZYREMOVE(SSticker.round_start_events, connection)
connection = null
if(silo)
allow_standalone = FALSE
disconnect()
QDEL_NULL(mat_container)
return ..()
/datum/remote_materials/proc/_MakeLocal()
PRIVATE_PROC(TRUE)
silo = null
mat_container = new ( \
parent, \
SSmaterials.get_materials_by_flag(MATERIAL_SILO_STORED), \
local_size, \
mat_container_flags, \
container_signals = mat_container_signals, \
allowed_items = /obj/item/stack \
)
/// Adds/Removes this connection from the silo
/datum/remote_materials/proc/toggle_holding()
if(isnull(silo))
return
if(!silo.holds[src])
silo.holds[src] = TRUE
else
silo.holds -= src
/**
* Sets the storage size for local materials when not linked with silo
* Arguments
*
* * size - the new size for local storage. measured in SHEET_MATERIAL_SIZE units
*/
/datum/remote_materials/proc/set_local_size(size)
local_size = size
if (!silo && mat_container)
mat_container.max_amount = size
///Disconnects this component from the silo
/datum/remote_materials/proc/disconnect()
if(isnull(silo))
return
UnregisterSignal(parent, list(COMSIG_ATOM_ITEM_INTERACTION, COMSIG_ATOM_ITEM_INTERACTION_SECONDARY))
silo.disconnect_receptacle(src, parent)
if (allow_standalone)
_MakeLocal()
/datum/remote_materials/proc/OnMultitool(datum/source, mob/user, obj/item/multitool/M)
SIGNAL_HANDLER
. = NONE
if (!QDELETED(M.buffer) && istype(M.buffer, /obj/machinery/ore_silo))
if (silo == M.buffer)
to_chat(user, span_warning("[parent] is already connected to [silo]!"))
return ITEM_INTERACT_BLOCKING
if(!check_z_level(M.buffer))
to_chat(user, span_warning("[parent] is too far away to get a connection signal!"))
return ITEM_INTERACT_BLOCKING
var/obj/machinery/ore_silo/new_silo = M.buffer
var/datum/material_container/new_container = new_silo.materials
if (silo)
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())
for(var/datum/material/mat as anything in mat_container.materials)
var/mat_amount = mat_container.materials[mat]
if(!mat_amount || !new_container.has_space(mat_amount) || !new_container.can_hold_material(mat))
continue
new_container.materials[mat] += mat_amount
mat_container.materials[mat] = 0
qdel(mat_container)
new_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))
to_chat(user, span_notice("You connect [parent] to [silo] from the multitool's buffer."))
return ITEM_INTERACT_SUCCESS
/datum/remote_materials/proc/on_item_insert(datum/source, mob/living/user, obj/item/target)
SIGNAL_HANDLER
// Only insert stacks with left click
if(isstack(target))
return attempt_insert(user, target)
/datum/remote_materials/proc/on_secondary_insert(datum/source, mob/living/user, obj/item/target)
SIGNAL_HANDLER
return attempt_insert(user, target)
/// Insert mats into silo
/datum/remote_materials/proc/attempt_insert(mob/living/user, obj/item/target)
if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode)
return
if(silo)
mat_container.user_insert(target, user, parent)
return ITEM_INTERACT_SUCCESS
/**
* Checks if the param silo is in the same level as this components parent i.e. connected machine, rcd, etc
*
* Arguments
* silo_to_check- Is this components parent in the same Z level as this param silo. If null
* then check this components connected silo
*
* Returns true if both are on the station or same z level
*/
/datum/remote_materials/proc/check_z_level(obj/silo_to_check = silo)
if(isnull(silo_to_check))
return FALSE
return is_valid_z_level(get_turf(silo_to_check), get_turf(parent))
/// returns TRUE if this connection put on hold by the silo
/datum/remote_materials/proc/on_hold()
return check_z_level() ? silo.holds[src] : FALSE
/**
* Check if this connection can use any materials from the silo
* Returns true only if
* - The parent is of type movable atom
* - A mat container is actually present
* - 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/remote_materials/proc/can_use_resource(check_hold = TRUE, alist/user_data)
var/atom/movable/movable_parent = parent
if (!istype(movable_parent))
return FALSE
if (!mat_container) //no silolink & local storage not supported
movable_parent.say("No access to material storage, please contact the quartermaster.")
return FALSE
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
/**
* Use materials from either the silo(if connected) or from the local storage. If silo then this action
* is logged else not e.g. action="build" & name="matter bin" means you are trying to build a matter bin
*
* Arguments
* [mats][list]- list of materials to use
* coefficient- each mat unit is scaled by this value then rounded. This value if usually your machine efficiency e.g. upgraded protolathe has reduced costs
* 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/remote_materials/proc/use_materials(list/mats, coefficient = 1, multiplier = 1, action = "processed", 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)
if (silo)//log only if silo is linked
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, user_data)
return amount_consumed
/**
* Ejects the given material ref and logs it
*
* Arguments
* [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/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_stack(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
*
* 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/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, user_data = user_data)