From ee7e15d443c8f1a91cb0e5705ebff5fcd4e85d26 Mon Sep 17 00:00:00 2001 From: NovaBot <154629622+NovaBot13@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:06:09 -0500 Subject: [PATCH] [MIRROR] Improves upon setting custom materials for printed items (#615) * Improves upon setting custom materials for printed items (#81015) ## About The Pull Request This is an improvement on #80839 regarding how custom materials are set on items, it's based on the following findings. 1. `set_custom_materials()` proc already comes with an prebuilt `multiplier` var https://github.com/tgstation/tgstation/blob/1e8d511946d194f92f744f5f957a7c41683d84a6/code/game/atom/atom_materials.dm#L11 This means we can pass the machine's cost coefficiency directly to this proc rather than creating our own list of materials with their values scaled by the factor like so https://github.com/tgstation/tgstation/blob/1e8d511946d194f92f744f5f957a7c41683d84a6/code/game/machinery/autolathe.dm#L192-L193 We can instead just do `set_custom_materials(design.materials, material_cost_coefficient)` without ever needing this list, thus making code cleaner, with that the changes propogate to how `has_materials()` & `use_materials()` procs are also used as we can now use their `coefficiency` param rather than seeing it go to waste 2. All items custom materials will now always be integer values. With this `SSmaterials.FindOrCreateMaterialCombo` now has better performance because when computing the key for caching values like `1.5` or `1.7` it will become just `1` and will point to the same cache thus reducing memory usage 3. Materials are now uniformly split among all the contents of a printed item from techfab or autolathe. What this means is items like the foam ammo box printed from autolathe will have both the ammo case and their 40 bullets each set with custom materials such that their final sum becomes equal to the design cost. For info on how that's done see the documentation of `split_materials_uniformly` proc. One downside of this proc is when items have a very small amount of `custom_materials`(less than 2). In that case values like `0.8` or `1.5` or `1.7` ends up getting rounded to 1 which means you end up getting less materials when recycling than what you used for printing that item. In this case You get 0.48 iron from both the box & it's ammo instead of 0.79 iron used for printing in tier1 autolathe(Or 0.50 for tier4 autolathe). This shouldn't be an issue as you still can't make any profits from this but at least everything in the box is now recyclable. ## Changelog :cl: fix: items that contain recursive contents inside them (like foam dart boxes from autolathes) now have their custom materials set to match with its design cost rather than being nullified, meaning they are now recyclable. code: all custom materials are now integer values. Improved code for how materials are used in techfab & auto lathe for printing /:cl: * Improves upon setting custom materials for printed items --------- Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> --- code/__HELPERS/construction.dm | 60 +++++++++++++++++++ code/controllers/subsystem/materials.dm | 4 +- code/game/atom/atom_materials.dm | 4 +- code/game/machinery/autolathe.dm | 41 +++++++------ .../modules/research/machinery/_production.dm | 28 ++++----- 5 files changed, 95 insertions(+), 42 deletions(-) diff --git a/code/__HELPERS/construction.dm b/code/__HELPERS/construction.dm index 9cc7d112532..f7b0ece13f8 100644 --- a/code/__HELPERS/construction.dm +++ b/code/__HELPERS/construction.dm @@ -59,3 +59,63 @@ return null . = new target.type(target.drop_location(), amount, FALSE, target.mats_per_unit) + +/** + * divides a list of materials uniformly among all contents of the target_object reccursively + * Used to set materials of printed items with their design cost by taking into consideration their already existing materials + * e.g. if 12 iron is to be divided uniformly among 2 objects A, B who's current iron contents are 3 & 7 + * Then first we normalize those values i.e. find their weights to decide who gets an higher share of iron + * total_sum = 3 + 7 = 10, A = 3/10 = 0.3, B = 7/10 = 0.7 + * Then we finally multiply those weights with the user value of 12 we get + * A = 0.3 * 12 = 3.6, B = 0.7 * 12 = 8.4 i.e. 3.6 + 8.4 = 12!! + * Off course we round the values so we don't have to deal with floating point materials so the actual value + * ends being less but that's not an issue + * Arguments + * + * * [custom_materials][list] - the list of materials to set for the object + * * multiplier - multiplier passed to set_custom_materials + * * [target_object][atom] - the target object who's custom materials we are trying to modify + */ +/proc/split_materials_uniformly(list/custom_materials, multiplier, atom/target_object) + if(!length(target_object.contents)) //most common case where the object is just 1 thing + target_object.set_custom_materials(custom_materials, multiplier) + return + + //Step 1: Get reccursive contents of all objects, only filter obj cause that what's material container accepts + var/list/reccursive_contents = target_object.get_all_contents_type(/obj/item) + + //Step 2: find the sum of each material type per object and record their amounts into an 2D list + var/list/material_map_sum = list() + var/list/material_map_amounts = list() + for(var/atom/object as anything in reccursive_contents) + var/list/item_materials = object.custom_materials + for(var/mat as anything in custom_materials) + var/mat_amount = 1 //no materials mean we assign this default amount + if(length(item_materials)) + mat_amount = item_materials[mat] || 1 //if this object doesn't have our material type then assign a default value of 1 + + //record the sum of mats for normalizing + material_map_sum[mat] += mat_amount + //record the material amount for each item into an 2D list + var/list/mat_list_per_item = material_map_amounts[mat] + if(isnull(mat_list_per_item)) + material_map_amounts[mat] = list(mat_amount) + else + mat_list_per_item += mat_amount + + //Step 3: normalize & scale material_map_amounts with material_map_sum + for(var/mat as anything in material_map_amounts) + var/mat_sum = material_map_sum[mat] + var/list/mat_per_item = material_map_amounts[mat] + for(var/i in 1 to mat_per_item.len) + mat_per_item[i] = (mat_per_item[i] / mat_sum) * custom_materials[mat] + + //Step 4 flatten the 2D list and assign the final values to each atom + var/index = 1 + for(var/atom/object as anything in reccursive_contents) + var/list/final_material_list = list() + for(var/mat as anything in material_map_amounts) + var/list/mat_per_item = material_map_amounts[mat] + final_material_list[mat] = mat_per_item[index] + object.set_custom_materials(final_material_list, multiplier) + index += 1 diff --git a/code/controllers/subsystem/materials.dm b/code/controllers/subsystem/materials.dm index a9c807cc38a..4ae9272e970 100644 --- a/code/controllers/subsystem/materials.dm +++ b/code/controllers/subsystem/materials.dm @@ -154,13 +154,13 @@ SUBSYSTEM_DEF(materials) var/list/combo_params = list() for(var/x in materials_declaration) var/datum/material/mat = x - combo_params += "[istype(mat) ? mat.id : mat]=[materials_declaration[mat] * multiplier]" + combo_params += "[istype(mat) ? mat.id : mat]=[OPTIMAL_COST(materials_declaration[mat] * multiplier)]" sortTim(combo_params, GLOBAL_PROC_REF(cmp_text_asc)) // We have to sort now in case the declaration was not in order var/combo_index = combo_params.Join("-") var/list/combo = material_combos[combo_index] if(!combo) combo = list() for(var/mat in materials_declaration) - combo[GET_MATERIAL_REF(mat)] = materials_declaration[mat] * multiplier + combo[GET_MATERIAL_REF(mat)] = OPTIMAL_COST(materials_declaration[mat] * multiplier) material_combos[combo_index] = combo return combo diff --git a/code/game/atom/atom_materials.dm b/code/game/atom/atom_materials.dm index 803a79110a1..345a8486dd6 100644 --- a/code/game/atom/atom_materials.dm +++ b/code/game/atom/atom_materials.dm @@ -12,7 +12,7 @@ if(custom_materials && material_flags & MATERIAL_EFFECTS) //Only runs if custom materials existed at first and affected src. for(var/current_material in custom_materials) var/datum/material/custom_material = GET_MATERIAL_REF(current_material) - custom_material.on_removed(src, custom_materials[current_material] * material_modifier, material_flags) //Remove the current materials + custom_material.on_removed(src, OPTIMAL_COST(custom_materials[current_material] * material_modifier), material_flags) //Remove the current materials if(!length(materials)) custom_materials = null @@ -21,7 +21,7 @@ if(material_flags & MATERIAL_EFFECTS) for(var/current_material in materials) var/datum/material/custom_material = GET_MATERIAL_REF(current_material) - custom_material.on_applied(src, materials[current_material] * multiplier * material_modifier, material_flags) + custom_material.on_applied(src, OPTIMAL_COST(materials[current_material] * multiplier * material_modifier), material_flags) custom_materials = SSmaterials.FindOrCreateMaterialCombo(materials, multiplier) diff --git a/code/game/machinery/autolathe.dm b/code/game/machinery/autolathe.dm index 63bb36c2b4a..3047995e7b3 100644 --- a/code/game/machinery/autolathe.dm +++ b/code/game/machinery/autolathe.dm @@ -188,11 +188,9 @@ return build_count = clamp(build_count, 1, 50) - var/is_stack_recipe = ispath(design.build_path, /obj/item/stack) - var/list/materials_per_item = list() - var/material_cost_coefficient = is_stack_recipe ? 1 : creation_efficiency + var/list/materials_needed = list() for(var/datum/material/material as anything in design.materials) - var/amount_needed = design.materials[material] * material_cost_coefficient + var/amount_needed = design.materials[material] if(istext(material)) // category var/list/choices = list() for(var/datum/material/valid_candidate as anything in SSmaterials.materials_by_category[material]) @@ -215,39 +213,41 @@ if(isnull(material)) stack_trace("got passed an invalid material id: [material]") return - materials_per_item[material] = amount_needed + materials_needed[material] = amount_needed - // don't pass the coefficient of creation here, we already modify it for use with setting the custom_materials - if(!materials.has_materials(materials_per_item, multiplier = build_count)) + var/material_cost_coefficient = ispath(design.build_path, /obj/item/stack) ? 1 : creation_efficiency + if(!materials.has_materials(materials_needed, material_cost_coefficient, build_count)) say("Not enough materials to begin production.") return - var/power_use_amount = 0 - for(var/material_used in materials_per_item) - power_use_amount += materials_per_item[material_used] * 0.2 * build_count - if(!directly_use_power(power_use_amount)) + //use power + var/power = active_power_usage + for(var/material in design.materials) + power += round(design.materials[material] * material_cost_coefficient * build_count) + power = min(active_power_usage, power) + if(!directly_use_power(power)) say("Not enough power in local network to begin production.") return var/total_time = (design.construction_time * design.lathe_time_factor * build_count) ** 0.8 var/time_per_item = total_time / build_count - start_making(design, build_count, ui.user, time_per_item, materials_per_item) + start_making(design, build_count, time_per_item, material_cost_coefficient) return TRUE /// Begins the act of making the given design the given number of items /// Does not check or use materials/power/etc -/obj/machinery/autolathe/proc/start_making(datum/design/design, build_count, mob/user, build_time_per_item, list/materials_per_item) +/obj/machinery/autolathe/proc/start_making(datum/design/design, build_count, build_time_per_item, material_cost_coefficient) PROTECTED_PROC(TRUE) busy = TRUE icon_state = "autolathe_n" update_static_data_for_all_viewers() - addtimer(CALLBACK(src, PROC_REF(do_make_item), design, materials_per_item, build_time_per_item, build_count), build_time_per_item) + addtimer(CALLBACK(src, PROC_REF(do_make_item), design, material_cost_coefficient, build_time_per_item, build_count), build_time_per_item) /// Callback for start_making, actually makes the item /// Called using timers started by start_making -/obj/machinery/autolathe/proc/do_make_item(datum/design/design, list/materials_per_item, time_per_item, items_remaining) +/obj/machinery/autolathe/proc/do_make_item(datum/design/design, material_cost_coefficient, time_per_item, items_remaining) PROTECTED_PROC(TRUE) if(!items_remaining) // how @@ -259,11 +259,12 @@ finalize_build() return + var/list/design_materials = design.materials var/is_stack = ispath(design.build_path, /obj/item/stack) - if(!materials.has_materials(materials_per_item, multiplier = is_stack ? items_remaining : 1)) + if(!materials.has_materials(design_materials, material_cost_coefficient, is_stack ? items_remaining : 1)) say("Unable to continue production, missing materials.") return - materials.use_materials(materials_per_item, multiplier = is_stack ? items_remaining : 1) + materials.use_materials(design_materials, material_cost_coefficient, is_stack ? items_remaining : 1) var/turf/target = get_step(src, drop_direction) if(isclosedturf(target)) @@ -274,12 +275,10 @@ created = new design.build_path(target, items_remaining) else created = new design.build_path(target) - created.set_custom_materials(materials_per_item.Copy()) + split_materials_uniformly(design_materials, material_cost_coefficient, created) created.pixel_x = created.base_pixel_x + rand(-6, 6) created.pixel_y = created.base_pixel_y + rand(-6, 6) - for(var/atom/movable/content in created) - content.set_custom_materials(list()) // no created.forceMove(target) if(is_stack) @@ -290,7 +289,7 @@ if(!items_remaining) finalize_build() return - addtimer(CALLBACK(src, PROC_REF(do_make_item), design, materials_per_item, time_per_item, items_remaining), time_per_item) + addtimer(CALLBACK(src, PROC_REF(do_make_item), design, material_cost_coefficient, time_per_item, items_remaining), time_per_item) /// Resets the icon state and busy flag /// Called at the end of do_make_item's timer loop diff --git a/code/modules/research/machinery/_production.dm b/code/modules/research/machinery/_production.dm index 32ff4480cbf..0067b05fd14 100644 --- a/code/modules/research/machinery/_production.dm +++ b/code/modules/research/machinery/_production.dm @@ -237,20 +237,15 @@ print_quantity = clamp(print_quantity, 1, 50) var/coefficient = build_efficiency(design.build_path) - var/list/materials_per_item = list() - for(var/material in design.materials) - materials_per_item[material] = design.materials[material] * coefficient - // check if sufficient materials are available. - // don't pass coefficient here, as we already multiplied the design's materials by it for use with custom materials. - if(!materials.mat_container.has_materials(materials_per_item, multiplier = print_quantity)) + if(!materials.mat_container.has_materials(design.materials, coefficient, print_quantity)) say("Not enough materials to complete prototype[print_quantity > 1 ? "s" : ""].") return FALSE //use power var/power = active_power_usage for(var/material in design.materials) - power += round(design.materials[material] * print_quantity / 35) + power += round(design.materials[material] * coefficient * print_quantity / 35) power = min(active_power_usage, power) use_power(power) @@ -286,22 +281,22 @@ var/total_time = (design.construction_time * design.lathe_time_factor * print_quantity) ** 0.8 var/time_per_item = total_time / print_quantity - start_making(design, print_quantity, user, time_per_item, materials_per_item) + start_making(design, print_quantity, time_per_item, coefficient) return TRUE /// Begins the act of making the given design the given number of items /// Does not check or use materials/power/etc -/obj/machinery/rnd/production/proc/start_making(datum/design/design, build_count, mob/user, build_time_per_item, list/materials_per_item) +/obj/machinery/rnd/production/proc/start_making(datum/design/design, build_count, build_time_per_item, build_efficiency) PROTECTED_PROC(TRUE) busy = TRUE update_static_data_for_all_viewers() - addtimer(CALLBACK(src, PROC_REF(do_make_item), design, materials_per_item, build_time_per_item, build_count), build_time_per_item) + addtimer(CALLBACK(src, PROC_REF(do_make_item), design, build_efficiency, build_time_per_item, build_count), build_time_per_item) /// Callback for start_making, actually makes the item /// Called using timers started by start_making -/obj/machinery/rnd/production/proc/do_make_item(datum/design/design, list/materials_per_item, time_per_item, items_remaining) +/obj/machinery/rnd/production/proc/do_make_item(datum/design/design, build_efficiency, time_per_item, items_remaining) PROTECTED_PROC(TRUE) if(!items_remaining) // how @@ -314,22 +309,21 @@ return var/is_stack = ispath(design.build_path, /obj/item/stack) - if(!materials.mat_container.has_materials(materials_per_item, multiplier = is_stack ? items_remaining : 1)) + var/list/design_materials = design.materials + if(!materials.mat_container.has_materials(design_materials, build_efficiency, is_stack ? items_remaining : 1)) say("Unable to continue production, missing materials.") return - materials.use_materials(materials_per_item, multiplier = is_stack ? items_remaining : 1, action = "built", name = "[design.name]") + materials.use_materials(design_materials, build_efficiency, is_stack ? items_remaining : 1, "built", "[design.name]") var/atom/movable/created if(is_stack) created = new design.build_path(get_turf(src), items_remaining) else created = new design.build_path(get_turf(src)) - created.set_custom_materials(materials_per_item.Copy()) + split_materials_uniformly(design_materials, build_efficiency, created) created.pixel_x = created.base_pixel_x + rand(-6, 6) created.pixel_y = created.base_pixel_y + rand(-6, 6) - for(var/atom/movable/content in created) - content.set_custom_materials(list()) // no if(is_stack) items_remaining = 0 @@ -339,7 +333,7 @@ if(!items_remaining) finalize_build() return - addtimer(CALLBACK(src, PROC_REF(do_make_item), design, materials_per_item, time_per_item, items_remaining), time_per_item) + addtimer(CALLBACK(src, PROC_REF(do_make_item), design, build_efficiency, time_per_item, items_remaining), time_per_item) /// Resets the busy flag /// Called at the end of do_make_item's timer loop