Files
Bubberstation/code/game/atom/atom_materials.dm
GhomandGitHub 6eb2d1674a Implementing materials checks for techweb designs. (#96257)
## About The Pull Request
In similar fashion to what was done with crafting recipes last year,
this year it's time for techweb designs and printed items to be audited.
This is mostly just about consistency, we've a lot of items that can be
printed by protolathes, autolathes, circuit printers and techfab etc.
However they almost all (except most stacks, mainly) have custom
materials that do not match in one way or another with the materials
used by the design, which is what this PR is for.

"But items printed from lathes etc. already get the mats used to make
them." Yes, they do, however that isn't the case for items of the same
type that were spawned in some other way (cargo shuttle, space/maints
loot, mapped, admins), this create a subtle discrepancy. It isn't a huge
deal (in spite of the size of this PR, ton of designs), but given that I
have done something similar with crafting recipes before, I may as well
give it a second arc of some sort and bring things to completion. And
fix a few possible oversights.

TL;DR consistency and stuff

## Why It's Good For The Game
Consistency, unit test checks to make it harder not to be consistent in
the future. Still has a few TODOs like:

- [x] Fixed newly printed, fully charged RCDs costing less than the RCD
cartridges required to fully charge one. EDIT: I had to tweak the newly
added RDD as well because it suffered from the same fundamental issue.
- [x] Fixed plates being made of iron and yet shattering like ceramic
ones. A new subtype for metallic ones has been made.

## Changelog

🆑
refactor: Refactored a few things with techweb designs (the ones for
autolathes, protolathes, circuit printers, mechfabs etc.) to make sure
that the materials of items that can be made from these designs more
closely match the materials used to make them.
fix: Lizard fries no longer need a plate to be made, like all other
treats that used to require plates in a distant past.
balance: Tweaked the materials cost of RCD, RDD and RCD cartridges.
image: Oven trays now have a more metallic hue.
balance: Plates printed printed from lathes won't shatter like ceramic
ones, in virtue of them being made out of iron instead.
/🆑
2026-07-06 00:13:02 -07:00

686 lines
29 KiB
Plaintext

/atom
/// The custom materials this atom is made of, used by a lot of things like furniture, walls, and floors (if I finish the functionality, that is.)
/// The list referenced by this var can be shared by multiple objects and should not be directly modified. Instead, use [set_custom_materials][/atom/proc/set_custom_materials].
var/list/datum/material/custom_materials
/// Bitfield for how the atom handles materials.
var/material_flags = NONE
/// Modifier that raises/lowers the effect of the amount of a material, prevents small and easy to get items from being death machines.
var/material_modifier = 1
/// List of material slots to be used to control material behaviors instead of default ones
var/list/datum/material_slot/material_slots = null
/// Sets the custom materials for an atom. This is what you want to call, since most of the ones below are mainly internal.
/atom/proc/set_custom_materials(list/materials, multiplier = 1)
SHOULD_NOT_OVERRIDE(TRUE)
// Easy way to know no changes are being made.
if((custom_materials == materials) && multiplier == 1)
return
var/replace_mats = length(materials)
if(length(custom_materials))
remove_material_effects(replace_mats)
if(!replace_mats)
return
initialize_materials(materials, multiplier)
/**
* The second part of set_custom_materials(), which handles applying the new materials
* It is a separate proc because Initialize calls may make use of this since they should've no prior materials to remove.
*/
/atom/proc/initialize_materials(list/materials, multiplier = 1)
SHOULD_NOT_OVERRIDE(TRUE)
if(multiplier != 1)
materials = materials.Copy() //avoid editing the original list since it may be cached somewhere (likely in the materials subsystem).
for(var/current_material in materials)
materials[current_material] *= multiplier
//Let's be sure that there are absolutely no materials in the list that aren't positive.
var/list/nonpos_mats
for(var/mat in materials)
if(materials[mat] <= 0)
LAZYADD(nonpos_mats, "[mat] = [materials[mat]]")
materials -= mat
if(length(nonpos_mats))
stack_trace("materials with non-positive values found in [type]: [english_list(nonpos_mats, and_text = ", ")]")
if(!length(materials))
return
sortTim(materials, GLOBAL_PROC_REF(cmp_numeric_dsc), associative = TRUE)
apply_material_effects(materials)
///proc responsible for applying material effects when setting materials.
/atom/proc/apply_material_effects(list/materials)
SHOULD_CALL_PARENT(TRUE)
if(material_flags & MATERIAL_EFFECTS)
var/list/material_effects = get_material_effects_list(materials)
finalize_material_effects(material_effects)
custom_materials = SSmaterials.get_material_set_cache(materials)
/// Proc responsible for removing material effects when setting materials.
/atom/proc/remove_material_effects(replace_mats = TRUE)
SHOULD_CALL_PARENT(TRUE)
//Only runs if custom materials existed at first and affected src.
if(material_flags & MATERIAL_EFFECTS)
var/list/material_effects = get_material_effects_list(custom_materials)
finalize_remove_material_effects(material_effects)
if(!replace_mats)
custom_materials = null
/atom/proc/get_material_effects_list(list/materials)
SHOULD_NOT_OVERRIDE(TRUE)
PRIVATE_PROC(TRUE)
var/list/material_effects = list()
var/index = 1
for(var/current_material in materials)
var/datum/material/material = SSmaterials.get_material(current_material)
material_effects[material] = list(
MATERIAL_LIST_OPTIMAL_AMOUNT = OPTIMAL_COST(materials[current_material] * material_modifier),
MATERIAL_LIST_MULTIPLIER = get_material_multiplier(material, materials, index),
)
index++
if(material_slots)
configure_material_slots(material_effects)
return material_effects
/// Add MATERIAL_LIST_SLOTS entries to material effects
/atom/proc/configure_material_slots(list/datum/material/material_effects)
for (var/slot_index in 1 to length(material_slots))
var/slot_type = material_slots[slot_index]
var/datum/material/material = null
// The slot has a specific material assigned to it
if (material_slots[slot_type])
material = SSmaterials.get_material(material_slots[slot_type])
// Slots were unset, abort
if (!material_effects[material])
continue
else if (slot_index <= length(material_effects)) // Otherwise, go by index
material = material_effects[slot_index]
if (!material)
continue
var/list/effects_list = material_effects[material]
if (!effects_list[MATERIAL_LIST_SLOTS])
effects_list[MATERIAL_LIST_SLOTS] = list()
var/list/effects_slots = effects_list[MATERIAL_LIST_SLOTS]
effects_slots[slot_type] = TRUE
/atom/proc/set_material_slot(slot_type, new_material)
if (material_slots[slot_type])
var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
var/datum/material/material = SSmaterials.get_material(material_slots[slot_type])
// Not present/initialized
if (material && custom_materials[material])
var/list/materials_slots = get_slots_of_material(material)
var/slot_sum = 0
if (length(materials_slots) > 1)
for (var/other_slot_type in materials_slots)
var/datum/material_slot/other_slot = SSmaterials.material_slots[other_slot_type]
slot_sum += other_slot.material_amount
slot.on_removed(src, material, custom_materials[material] * (slot_sum > 0 ? slot.material_amount / slot_sum : 1), get_material_multiplier(material, custom_materials, custom_materials.Find(material)))
// Don't store materials directly, only their IDs
if (istype(new_material, /datum/material))
var/datum/material/as_material = new_material
material_slots[slot_type] = as_material.id
else
material_slots[slot_type] = new_material
var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
var/datum/material/material = istype(new_material, /datum/material) ? new_material : SSmaterials.get_material(new_material)
if (!material || !custom_materials[material])
return
var/list/materials_slots = get_slots_of_material(material)
var/slot_sum = 0
if (length(materials_slots) > 1)
for (var/other_slot_type in materials_slots)
var/datum/material_slot/other_slot = SSmaterials.material_slots[other_slot_type]
slot_sum += other_slot.material_amount
slot.on_applied(src, material, custom_materials[material] * (slot_sum > 0 ? slot.material_amount / slot_sum : 1), get_material_multiplier(material, custom_materials, custom_materials.Find(material)))
/atom/proc/set_material_slots(list/new_slots)
if (length(material_slots))
for (var/slot_type, material_id in material_slots)
var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
var/datum/material/material = SSmaterials.get_material(material_id)
// Not present/initialized
if (!material || !custom_materials[material])
continue
var/list/materials_slots = get_slots_of_material(material)
var/slot_sum = 0
if (length(materials_slots) > 1)
for (var/other_slot_type in materials_slots)
var/datum/material_slot/other_slot = SSmaterials.material_slots[other_slot_type]
slot_sum += other_slot.material_amount
slot.on_removed(src, material, custom_materials[material] * (slot_sum > 0 ? slot.material_amount / slot_sum : 1), get_material_multiplier(material, custom_materials, custom_materials.Find(material)))
if (!length(new_slots))
material_slots = null
return
material_slots = new_slots.Copy()
for (var/slot_type, material_id in material_slots)
var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
var/datum/material/material = SSmaterials.get_material(material_id)
if (!material || !custom_materials[material])
continue
var/list/materials_slots = get_slots_of_material(material)
var/slot_sum = 0
if (length(materials_slots) > 1)
for (var/other_slot_type in materials_slots)
var/datum/material_slot/other_slot = SSmaterials.material_slots[other_slot_type]
slot_sum += other_slot.material_amount
slot.on_applied(src, material, custom_materials[material] * (slot_sum > 0 ? slot.material_amount / slot_sum : 1), get_material_multiplier(material, custom_materials, custom_materials.Find(material)))
/**
* A proc that can be used to selectively control the stat changes and effects from a material without affecting the others.
*
* For example, we can have items made of two different materials, with the primary contributing a good 1.2 multiplier
* and the second a meager 0.3.
*
* The GET_MATERIAL_MODIFIER macro will handles some modifications where the minimum should be 1 if above 1 and the maximum
* be 1 if below 1. Just don't return negative values.
*/
/atom/proc/get_material_multiplier(datum/material/custom_material, list/materials, index)
if (!length(material_slots))
return 1 / length(materials)
// Slots usually account for multipliers in their own behaviors, so unless overriden it should just be 1
for (var/slot_type in material_slots)
if (material_slots[slot_type] == custom_material.id)
return 1
return 1 / length(materials)
///Called by apply_material_effects(). It ACTUALLY handles applying effects common to all atoms (depending on material flags)
/atom/proc/finalize_material_effects(list/materials)
SHOULD_CALL_PARENT(TRUE)
var/total_alpha = 0
var/list/colors = list()
var/mat_length = length(materials)
var/datum/material/main_material = materials[1]//the material with the highest amount (after calculations)
var/main_mat_amount = materials[main_material][MATERIAL_LIST_OPTIMAL_AMOUNT]
var/main_mat_mult = materials[main_material][MATERIAL_LIST_MULTIPLIER]
var/do_main_material = TRUE
for(var/datum/material/custom_material as anything in materials)
var/list/deets = materials[custom_material]
var/mat_amount = deets[MATERIAL_LIST_OPTIMAL_AMOUNT]
var/multiplier = deets[MATERIAL_LIST_MULTIPLIER]
var/do_effects = TRUE
var/from_slot = FALSE
if(!isnull(material_slots) && length(deets[MATERIAL_LIST_SLOTS]))
from_slot = TRUE
var/slot_sum = 0
// A material is in multiple slots, we need to cut it up between them
if(length(deets[MATERIAL_LIST_SLOTS]) > 1)
for(var/slot_type in deets[MATERIAL_LIST_SLOTS])
var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
slot_sum += slot.material_amount
for(var/slot_type in deets[MATERIAL_LIST_SLOTS])
var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
var/slot_amt = mat_amount
if (slot_sum > 0)
slot_amt *= slot.material_amount / slot_sum
do_effects &= slot.on_applied(src, custom_material, slot_amt, multiplier)
if(!do_effects && custom_material == main_material)
do_main_material = FALSE
if(do_effects)
apply_single_mat_effect(custom_material, mat_amount, multiplier)
custom_material.on_applied(src, mat_amount, multiplier, from_slot = from_slot)
//Prevent changing things with pre-set colors, to keep colored toolboxes their looks for example
if(material_flags & (MATERIAL_COLOR|MATERIAL_GREYSCALE))
gather_material_color(custom_material, colors, mat_amount, multicolor = mat_length > 1)
var/added_alpha = custom_material.alpha * (custom_material.alpha / 255)
total_alpha += GET_MATERIAL_MODIFIER(added_alpha, multiplier)
if(do_main_material)
apply_main_material_effects(main_material, main_mat_amount, main_mat_mult)
if(material_flags & (MATERIAL_COLOR|MATERIAL_GREYSCALE))
var/previous_alpha = alpha
alpha *= (total_alpha / length(materials))/255
if(alpha < previous_alpha * 0.9)
opacity = FALSE
if(material_flags & MATERIAL_GREYSCALE)
var/config_path = get_material_greyscale_config(main_material.type, greyscale_config)
//Make sure that we've no less than the expected amount
//expected_colors is zero for paths, the value is assigned when reading the json files.
var/datum/greyscale_config/config = SSgreyscale.configurations["[config_path || greyscale_config]"]
var/colors_len = length(colors)
if(config.expected_colors > colors_len)
var/list/filled_colors = colors.Copy()
for(var/index in colors_len to config.expected_colors - 1)
filled_colors += pick(colors)
colors = filled_colors
set_greyscale(colors, config_path)
else if(length(colors))
mix_material_colors(colors)
if(material_flags & MATERIAL_ADD_PREFIX)
var/prefixes = get_material_prefixes(materials)
name = "[prefixes] [name]"
SEND_SIGNAL(src, COMSIG_ATOM_FINALIZE_MATERIAL_EFFECTS, materials, main_material)
/**
* A proc used by both finalize_material_effects() and finalize_remove_material_effects() to get the colors
* that will later be applied to or removed from the atom
*/
/atom/proc/gather_material_color(datum/material/material, list/colors, amount, multicolor = FALSE)
SHOULD_CALL_PARENT(TRUE)
if(!material.color) //the material has no color. Nevermind
return
var/color_to_add = material.color
var/istext = istext(color_to_add)
if(istext)
if(material.alpha != 255)
color_to_add += num2hex(material.alpha, 2)
else
if(multicolor || material_flags & MATERIAL_GREYSCALE)
color_to_add = material.greyscale_color || color_matrix2color_hex(material.color)
if(material.greyscale_color)
color_to_add += num2hex(material.alpha, 2)
else
color_to_add = color_to_full_rgba_matrix(color_to_add)
color_to_add[20] *= (material.alpha / 255) // multiply the constant alpha of the color matrix
colors[color_to_add] += amount
/// Manages mixing, adding or removing the material colors from the atom in absence of the MATERIAL_GREYSCALE flag.
/atom/proc/mix_material_colors(list/colors, remove = FALSE)
SHOULD_NOT_OVERRIDE(TRUE)
var/color_len = length(colors)
if(!color_len)
return
var/mixcolor = colors[1]
var/amount_divisor = colors[mixcolor]
for(var/i in 2 to length(colors))
var/color_to_add = colors[i]
if(islist(color_to_add))
color_to_add = color_matrix2color_hex(color_to_add)
var/mix_amount = colors[color_to_add]
amount_divisor += mix_amount
mixcolor = BlendRGB(mixcolor, color_to_add, mix_amount/amount_divisor)
if(remove)
remove_atom_colour(FIXED_COLOUR_PRIORITY, mixcolor)
else
add_atom_colour(mixcolor, FIXED_COLOUR_PRIORITY)
///Returns the prefixes to attach to the atom when setting materials, from a list argument.
/atom/proc/get_material_prefixes(list/materials)
var/list/mat_names = list()
for(var/datum/material/material as anything in materials)
mat_names |= material.name
return mat_names.Join("-")
///Returns a string like "plasma, paper and glass" from a list of materials
/atom/proc/get_material_english_list(list/materials)
var/list/mat_names = list()
for(var/datum/material/material as anything in materials)
mat_names += material.name
return english_list(mat_names)
///Searches for a subtype of config_type that is to be used in its place for specific materials (like shimmering gold for cleric maces)
/atom/proc/get_material_greyscale_config(mat_type, config_type)
SHOULD_NOT_OVERRIDE(TRUE)
if(!config_type)
return
for(var/datum/greyscale_config/path as anything in subtypesof(config_type))
if(mat_type == initial(path.material_skin))
return path
///Apply material effects of a single material.
/atom/proc/apply_single_mat_effect(datum/material/material, amount, multiplier)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_APPLY, material, amount, multiplier)
// Derived and not optional, so this needs to be on base and not in the property code itself
var/beauty_modifier = material.get_property(MATERIAL_BEAUTY)
if(beauty_modifier)
AddElement(/datum/element/beauty, beauty_modifier * amount)
if(beauty_modifier >= 0.15 && HAS_TRAIT(src, TRAIT_FISHING_BAIT))
AddElement(/datum/element/shiny_bait)
if((material_flags & MATERIAL_AFFECT_STATISTICS) && uses_integrity)
change_material_integrity(material, amount, multiplier)
///A proc for material effects that only the main material (which the atom's primarly composed of) should apply.
/atom/proc/apply_main_material_effects(datum/material/main_material, amount, multiplier)
SHOULD_CALL_PARENT(TRUE)
if(main_material.texture_layer_icon_state && (material_flags & MATERIAL_COLOR))
ADD_KEEP_TOGETHER(src, MATERIAL_SOURCE(main_material))
add_filter("material_texture_[main_material.name]", 1, layering_filter(icon = main_material.cached_texture_filter_icon, blend_mode = BLEND_INSET_OVERLAY))
main_material.on_main_applied(src, amount, multiplier)
///Called by remove_material_effects(). It ACTUALLY handles removing effects common to all atoms (depending on material flags)
/atom/proc/finalize_remove_material_effects(list/materials)
var/list/colors = list()
var/datum/material/main_material = get_master_material()
var/mat_length = length(materials)
var/main_mat_amount = materials[main_material][MATERIAL_LIST_OPTIMAL_AMOUNT]
var/main_mat_mult = materials[main_material][MATERIAL_LIST_MULTIPLIER]
var/do_main_material = TRUE
for(var/datum/material/custom_material as anything in materials)
var/list/deets = materials[custom_material]
var/mat_amount = deets[MATERIAL_LIST_OPTIMAL_AMOUNT]
var/multiplier = deets[MATERIAL_LIST_MULTIPLIER]
var/do_effects = TRUE
var/from_slot = FALSE
if(!isnull(material_slots) && length(deets[MATERIAL_LIST_SLOTS]))
from_slot = TRUE
var/slot_sum = 0
// A material is in multiple slots, we need to cut it up between them
if(length(deets[MATERIAL_LIST_SLOTS]) > 1)
for(var/slot_type in deets[MATERIAL_LIST_SLOTS])
var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
slot_sum += slot.material_amount
for(var/slot_type in deets[MATERIAL_LIST_SLOTS])
var/datum/material_slot/slot = SSmaterials.material_slots[slot_type]
var/slot_amt = mat_amount
if (slot_sum > 0)
slot_amt *= slot.material_amount / slot_sum
do_effects &= slot.on_removed(src, custom_material, mat_amount, multiplier)
if(!do_effects && custom_material == main_material)
do_main_material = FALSE
if(do_effects)
remove_single_mat_effect(custom_material, mat_amount, multiplier)
custom_material.on_removed(src, mat_amount, multiplier, from_slot = from_slot)
if(material_flags & MATERIAL_COLOR)
gather_material_color(custom_material, colors, mat_amount, multicolor = mat_length > 1)
if(do_main_material)
remove_main_material_effects(main_material, main_mat_amount, main_mat_mult)
if(material_flags & (MATERIAL_GREYSCALE|MATERIAL_COLOR))
if(material_flags & MATERIAL_COLOR)
mix_material_colors(colors, remove = TRUE)
else
set_greyscale(initial(greyscale_colors), initial(greyscale_config))
alpha = initial(alpha)
opacity = initial(opacity)
if(material_flags & MATERIAL_ADD_PREFIX)
name = initial(name)
// Ensure that we restore armor zero'd out by zero multipliers, as we don't have anything to go off other than our initial values
if((material_flags & MATERIAL_AFFECT_STATISTICS) && uses_integrity && initial(armor_type))
var/datum/armor/inital_armor = get_armor_by_type(initial(armor_type))
for (var/armor_id in ARMOR_LIST_ALL)
var/initial_rating = inital_armor.get_rating(armor_id)
if (get_armor_rating(armor_id) == 0 && initial_rating != 0)
set_armor_rating(armor_id, initial_rating)
SEND_SIGNAL(src, COMSIG_ATOM_FINALIZE_REMOVE_MATERIAL_EFFECTS, materials, main_material)
///Remove material effects of a single material.
/atom/proc/remove_single_mat_effect(datum/material/material, amount, multiplier)
SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(src, COMSIG_ATOM_SINGLE_MATERIAL_EFFECT_REMOVE, material, amount, multiplier)
var/beauty_modifier = material.get_property(MATERIAL_BEAUTY)
if(beauty_modifier)
RemoveElement(/datum/element/beauty, beauty_modifier * amount)
if(beauty_modifier >= 0.15 && HAS_TRAIT(src, TRAIT_FISHING_BAIT))
RemoveElement(/datum/element/shiny_bait)
if((material_flags & MATERIAL_AFFECT_STATISTICS) && uses_integrity)
change_material_integrity(material, amount, multiplier, removing = TRUE)
///A proc to remove the material effects previously applied by the (ex-)main material
/atom/proc/remove_main_material_effects(datum/material/main_material, amount, multipier)
SHOULD_CALL_PARENT(TRUE)
if(main_material.texture_layer_icon_state)
remove_filter("material_texture_[main_material.name]")
REMOVE_KEEP_TOGETHER(src, MATERIAL_SOURCE(main_material))
main_material.on_main_removed(src, amount, multipier)
///Remove the old effects, change the material_modifier variable, and then reapply all the effects.
/atom/proc/change_material_modifier(new_value)
SHOULD_NOT_OVERRIDE(TRUE)
remove_material_effects()
material_modifier = new_value
apply_material_effects(custom_materials)
///For enabling and disabling material effects from an item (mainly VV)
/atom/proc/toggle_material_flags(new_flags)
SHOULD_NOT_OVERRIDE(TRUE)
if(material_flags & MATERIAL_EFFECTS && !(new_flags & MATERIAL_EFFECTS))
remove_material_effects()
else if(!(material_flags & MATERIAL_EFFECTS) && new_flags & MATERIAL_EFFECTS)
apply_material_effects()
material_flags = new_flags
/// Applies changes to integrity and armor from a material
/atom/proc/change_material_integrity(datum/material/material, amount, multiplier, removing = FALSE)
var/base_modifier = material.get_property(MATERIAL_INTEGRITY)
var/integrity_mod = GET_MATERIAL_MODIFIER(base_modifier, multiplier)
var/integrity_change = removing ? floor(max_integrity / integrity_mod) : ceil(max_integrity * integrity_mod)
modify_max_integrity(integrity_change)
var/list/armor_mods = material.get_armor_modifiers(multiplier)
// Invert if we're removing our material
if (removing)
for (var/armor_type, value in armor_mods)
if (value != 0) // Needs to be restored to initial values in finalize effects, sorry
armor_mods[armor_type] = 1 / value
set_armor(get_armor().generate_new_with_multipliers(armor_mods))
/// Tries to fetch a material matching a specific slot
/atom/proc/get_material_from_slot(slot_type)
var/mat_type = material_slots?[slot_type]
if (mat_type)
return SSmaterials.get_material(mat_type)
/// Fetches a copy of all material slots.
/atom/proc/get_material_slots()
return material_slots?.Copy()
/// Returns TRUE if this atom utilizes material slots
/atom/proc/has_material_slots()
return !!length(material_slots)
/// Lists all slots in which a material is present
/atom/proc/get_slots_of_material(datum/material/material)
. = list()
for (var/slot_type, material_id in material_slots)
if (material_id == istype(material) ? material.id : material)
. += slot_type
return .
/**
* Returns the material composition of the atom.
*
* Used when recycling items, specifically to turn alloys back into their component mats.
*
* Exists because I'd need to add a way to un-alloy alloys or otherwise deal
* with people converting the entire stations material supply into alloys.
*
* Arguments:
* - flags: A set of flags determining how exactly the materials are broken down.
*/
/atom/proc/get_material_composition(flags)
. = list()
var/list/cached_materials = custom_materials
for(var/mat in cached_materials)
var/datum/material/material = SSmaterials.get_material(mat)
var/list/material_comp = material.return_composition(cached_materials[mat], flags)
for(var/comp_mat in material_comp)
.[comp_mat] += material_comp[comp_mat]
/**
* Fetches a list of all of the materials this object has of the desired type. Returns null if there is no valid materials of the type
*
* Arguments:
* - [required_material][/datum/material]: The type of material we are checking for
* - mat_amount: The minimum required amount of material
*/
/atom/proc/has_material_type(datum/material/required_material, mat_amount = 0)
var/list/cached_materials = custom_materials
if(!length(cached_materials))
return null
var/materials_of_type
for(var/current_material in cached_materials)
if(cached_materials[current_material] < mat_amount)
continue
var/datum/material/material = SSmaterials.get_material(current_material)
if(!istype(material, required_material))
continue
LAZYSET(materials_of_type, material, cached_materials[current_material])
return materials_of_type
/// Gets the most common material in the object.
/atom/proc/get_master_material()
return length(custom_materials) ? SSmaterials.get_material(custom_materials[1]) : null //materials are sorted by amount, the first is always the main one
/// Gets the total amount of materials in this atom.
/atom/proc/get_custom_material_amount()
return isnull(custom_materials) ? 0 : counterlist_sum(custom_materials)
/**
* Returns a list with the sum of the materials of the source and its contents for each mat type.
* If contents_type is set, only consider contained objects of that type (plus src).
* Any object that has any of the traits in skip_traits will also be skipped.
*/
/atom/proc/get_contents_custom_materials(contents_type, list/skip_traits)
var/list/contained = ispath(contents_type) ? get_all_contents_type(contents_type) : get_all_contents()
if(skip_traits && !islist(skip_traits))
skip_traits = list(skip_traits)
var/list/all_mats = list()
for(var/atom/atom as anything in contained)
if(!length(atom.custom_materials))
continue
if(skip_traits)
var/skip = FALSE
for(var/trait in skip_traits)
if(HAS_TRAIT(atom, trait))
skip = TRUE
break
if(skip)
continue
for(var/material in atom.custom_materials)
all_mats[material] += atom.custom_materials[material]
return all_mats
/// A simple proc that iterates through each material that the object is made of and spawns some stacks based on their amount and associated sheet/ore type.
/atom/proc/drop_custom_materials(multiplier = 1)
for(var/datum/material/material as anything in custom_materials)
var/stack_type = material.sheet_type || material.ore_type
if(!stack_type)
continue
var/amount_to_spawn = FLOOR(custom_materials[material] / SHEET_MATERIAL_AMOUNT * multiplier, 1)
if(amount_to_spawn > 0)
new stack_type(loc, amount_to_spawn)
/**
* A bit of leeway when comparing the amount of material of two items.
* This was made to test the material composition of items spawned via crafting/processable component and an items of the same type spawned
* via other means, since small portion of materials can be lost when rounding down values to the nearest integers and we can't do much about it.
* (eg. a slab of meat worth 100 mat points is cut in three cutlets, each 33, with the remaining 1 percent lost to rounding)
*
* right now it's 3 points per 100 units of a material.
*
*/
#define COMPARISION_ACCEPTABLE_MATERIAL_DEVIATION 0.03
/// Compares the materials of two items to see if they're roughly the same. Primarily used in crafting and processing unit tests.
/atom/proc/compare_materials(atom/target)
if(custom_materials == target.custom_materials) // SSmaterials caches the combinations so we don't have to run more complex checks
return TRUE
if(length(custom_materials) != length(target.custom_materials))
return FALSE
for(var/mat in custom_materials)
var/enemy_amount = target.custom_materials[mat]
if(!enemy_amount) //we couldn't find said material, early return so we won't perform a division by zero
return FALSE
var/ratio_difference = abs((custom_materials[mat] / enemy_amount) - 1)
if(ratio_difference > COMPARISION_ACCEPTABLE_MATERIAL_DEVIATION)
return FALSE
return TRUE
/**
* Returns a string with the materials and their respective amounts written in a way that reflects how it's displayed in the code
* (eg. [list(/datum/material/meat = 100, /datum/material/plastic = 10)]). Also used in several unit tests.
* Not to be confused with get_material_english_list()
* Arguments:
* * as_sheets: returns the text in terms of sheets, e.g "[list(/datum/material/titanium = SHEET_MATERIAL_AMOUNT * 2)]"
*/
/atom/proc/transcribe_materials_list(list/mats_list, as_sheets = TRUE)
if(!mats_list)
if(!custom_materials)
return "null"
mats_list = custom_materials
var/text = "list("
var/index = 1
var/mats_len = length(mats_list)
for(var/datum/material/mat as anything in mats_list)
var/amount_string = as_sheets ? transcribe_mat_value_as_sheet(mats_list[mat]) : "[mats_list[mat]]"
text += "[mat.type] = " + amount_string
if(index < mats_len)
text += ", "
index++
text += ")"
return text
/proc/transcribe_mat_value_as_sheet(value)
var/amount = sheets_from_value(value)
switch(amount)
if(0 to 0.09)
return "SMALL_MATERIAL_AMOUNT * " + num2text(amount * 10)
if(0.1)
return "SMALL_MATERIAL_AMOUNT"
if(0.11 to 0.49)
return "SMALL_MATERIAL_AMOUNT * " + num2text(amount * 10)
if(0.5)
return "HALF_SHEET_MATERIAL_AMOUNT"
if(1)
return "SHEET_MATERIAL_AMOUNT"
else
return "SHEET_MATERIAL_AMOUNT * " + num2text(amount)
/// Convert a raw material amount into
/// "SHEET_MATERIAL_AMOUNT", or "* N", with rounding rules.
/proc/sheets_from_value(value, sheet_amount = SHEET_MATERIAL_AMOUNT)
if(!value)
return 0
// If value is small, do NOT try rounding to nearest 0 or 5. percentage error becomes huge.
var/final_value
if(value < sheet_amount)
// Use exact amount for small-value materials (0.1, 0.25, 0.55, etc)
final_value = value
else
// Large values: round to nearest 0 or 5
var/nearest5_value = round(value / 5) * 5
var/max_error = value * COMPARISION_ACCEPTABLE_MATERIAL_DEVIATION // 3%
if(abs(nearest5_value - value) <= max_error)
final_value = nearest5_value
else
final_value = value
var/final_sheet_multiplier = final_value / sheet_amount
return final_sheet_multiplier
#undef COMPARISION_ACCEPTABLE_MATERIAL_DEVIATION