From 2922d36500b51ae20fceffebc1523318acdf62f4 Mon Sep 17 00:00:00 2001 From: SyncIt21 <110812394+SyncIt21@users.noreply.github.com> Date: Tue, 30 May 2023 02:00:16 +0530 Subject: [PATCH] Refactors for material container, ammo box & recycler (#75422) **1. Material Container Refactors** a. `/datum/component/material_container/proc/insert_item()` - Will now do stack spliting i.e. it will consume as many sheets from a stack as possible and leave out the rest, It was moved from a player interaction feature in `user_insert()` to this low level allowing many things to take advantage of it - Will now delete the item for you if it could salvage any materials from it, you don't have to do it explicitly anymore if insertion was successfull (i.e. this proc returns an non zero value) If you inserted a stack and not all of it's sheets were inserted from the above point then you still have to check it explicitly - Will now invoke `after_insert` if any materials were salvaged b. `datum/component/material_container/proc/user_insert() ` - Will now split the stack by the requested amount making precise insertion work again & Fixes #72288 - will now consume all contents inside of the object reccursively, this means items like ammo boxes will no longer have to adjust their custom materials based on how much ammo they contain because `user_insert()` will loop through all its contents and salvage the metal of every bullet inside the box contents so this means https://github.com/tgstation/tgstation/blob/9686971c76fb6300939c9c191c635d2a852a143c/code/modules/projectiles/boxes_magazines/_box_magazine.dm#L206 has been removed. **The Problem with this proc** take `/obj/item/ammo_box/foambox/riot` for example. it has 40 darts each having 1125 worth of iron, this proc will add the total iron of all the bullets to the box custom material so the box custom materials would become `5000(base iron of box. see the definition of /obj/item/ammo_box/foambox/riot) + 45000(40 bullets each having 1125 worth of iron) = 50000 iron` What happens when you throw this ammo box in an recycler? The recycler will recycle this box(Now 50000 worth of iron) AND the iron of each of it's 40 bullets thus yielding `50000(iron from box because of update_custom_materials()) + 45000(40 bullets each having 1125 worth of iron) = 95000 iron` ` because of this single proc we got `95000 - 50000 = 45000 extra iron` from thin air **The Solution?** Remove this proc and set a constant custom material value for the ammo box(it's now 5000 computed see code) AND allow the material container to loop through every bullet in the box and salvage iron from them. This would yield `5000(base iron of box. see the definition of /obj/item/ammo_box/foambox/riot) + 45000(40 bullets each having 1125 worth of iron) = 50000 iron` From both box & bullets combined and not just from the box alone Fixes #43570 Fixes #57548 This also allows you to do cool stuff like fill your bag with iron, glass, whatever and dump them in the autolathe/ore silo by attacking the machine with your bag rather than pulling out & inserting each item individually so hey convinience **2. Recycler patches** - Recycler will stop consuming items when it runs out of power mid recycling(which can happen if it recycles a large amount of items). It used to previously run through the list of items without breaking so even when power was lost, it still did it's job - Recycler will now Properly recycle all the contents inside an atom. **The Problem** Say we have 2 Items - Backpack - Glass sheet inside Backpack If we process the items in the following order while deleting each item that is processed first "Backpack" then "Glass Sheet" then when "Backpack" is fully recycled and "Deleted" since the "Glass Sheet" is inside the "Backpack" it get's deleted as well, so when we actually try to recycle the "Glass Sheet" next, nothing happens because it was deleted when we deleted the "Backpack". **The Solution** Recycle the items in the reverse order first recycle the "Glass Sheet" delete it & then the "Back Pack" so we don't deal with deleted items. So you should see more materials come out when you put stuff inside storage mediums & throw them in the recycler - Recycler will consume only half the power when it's deleting items that can't be recycled(no material was salvaged). just for convinience --- code/datums/components/material_container.dm | 212 ++++++++++++------ code/game/machinery/recycler.dm | 48 ++-- code/modules/mining/machine_processing.dm | 3 +- code/modules/mining/machine_redemption.dm | 15 +- code/modules/mining/machine_stacking.dm | 1 - .../boxes_magazines/_box_magazine.dm | 28 +-- 6 files changed, 189 insertions(+), 118 deletions(-) diff --git a/code/datums/components/material_container.dm b/code/datums/components/material_container.dm index 07c347b31b1..0895383d496 100644 --- a/code/datums/components/material_container.dm +++ b/code/datums/components/material_container.dm @@ -116,82 +116,164 @@ examine_texts += span_notice("It has [amt] units of [lowertext(M.name)] stored.") /// Proc that allows players to fill the parent with mats -/datum/component/material_container/proc/on_attackby(datum/source, obj/item/I, mob/living/user) +/datum/component/material_container/proc/on_attackby(datum/source, obj/item/weapon, mob/living/user) SIGNAL_HANDLER - var/list/tc = allowed_item_typecache - if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode) - return - if(I.item_flags & ABSTRACT) - return - if((I.flags_1 & HOLOGRAM_1) || (I.item_flags & NO_MAT_REDEMPTION) || (tc && !is_type_in_typecache(I, tc))) - if(!(mat_container_flags & MATCONTAINER_SILENT)) - to_chat(user, span_warning("[parent] won't accept [I]!")) - return - . = COMPONENT_NO_AFTERATTACK - var/datum/callback/pc = precondition - if(pc && !pc.Invoke(user)) - return - var/material_amount = get_item_material_amount(I, mat_container_flags) - if(!material_amount) - to_chat(user, span_warning("[I] does not contain sufficient materials to be accepted by [parent].")) - return - if(!has_space(material_amount)) - if(isstack(I)) - //figure out how much space is left - var/space_left = max_amount - total_amount - //figure out the amount of sheets that can fit that space - var/obj/item/stack/stack_to_split = I - var/material_per_sheet = material_amount / stack_to_split.amount - var/sheets_to_insert = round(space_left / material_per_sheet) - if(!sheets_to_insert) - to_chat(user, span_warning("[parent] can't hold any more of [I] sheets.")) - return - //split the amount we don't need off - INVOKE_ASYNC(stack_to_split, TYPE_PROC_REF(/obj/item/stack, split_stack), user, stack_to_split.amount - sheets_to_insert) - else - to_chat(user, span_warning("[I] contains more materials than [parent] has space to hold.")) - return - user_insert(I, user, mat_container_flags) + user_insert(weapon, user) -/// Proc used for when player inserts materials + return COMPONENT_NO_AFTERATTACK + +/** + * inserts an item from the players hand into the container. Loops through all the contents inside reccursively + * Arguments + * * held_item - the item to insert + * * user - the mob inserting this item + * * breakdown_flags - how this item and all it's contents inside are broken down during insertion. This is unique to the machine doing the insertion + */ /datum/component/material_container/proc/user_insert(obj/item/held_item, mob/living/user, breakdown_flags = mat_container_flags) set waitfor = FALSE - var/requested_amount - var/active_held = user.get_active_held_item() // differs from I when using TK - if(isstack(held_item) && precise_insertion) - var/atom/current_parent = parent - var/obj/item/stack/item_stack = held_item - 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(held_item) || QDELETED(user) || QDELETED(src)) - return - if(parent != current_parent || user.get_active_held_item() != active_held) - return - if(!user.temporarilyRemoveItemFromInventory(held_item)) - to_chat(user, span_warning("[held_item] is stuck to you and cannot be placed into [parent].")) + . = 0 + + //differs from held_item when using TK + var/active_held = user.get_active_held_item() + //don't attack the machine + if(!(mat_container_flags & MATCONTAINER_ANY_INTENT) && user.combat_mode) return - var/inserted = insert_item(held_item, stack_amt = requested_amount, breakdown_flags= mat_container_flags) - if(inserted) - to_chat(user, span_notice("You insert a material total of [inserted] into [parent].")) - qdel(held_item) - if(after_insert) - after_insert.Invoke(held_item, last_inserted_id, inserted) - else if(held_item == active_held) - user.put_in_active_hand(held_item) + //user defined conditions + if(precondition && !precondition.Invoke(user)) + return + + //loop through all contents inside this atom and salvage their material as well but in reverse so we don't delete parents before processing their children + var/list/contents = held_item.get_all_contents_type(/obj/item) + for(var/i = length(contents); i >= 1 ; i--) + var/obj/item/target = contents[i] + + //not a solid sub type + if(target.item_flags & ABSTRACT) + if(target == active_held) //was this the original item in the players hand? put it back because we coudn't salvage it + user.put_in_active_hand(target) + continue + //item is either not real, not allowed for redemption, not in the allowed types + if((target.flags_1 & HOLOGRAM_1) || (target.item_flags & NO_MAT_REDEMPTION) || (allowed_item_typecache && !is_type_in_typecache(target, allowed_item_typecache))) + if(!(mat_container_flags & MATCONTAINER_SILENT)) + to_chat(user, span_warning("[parent] won't accept [target]!")) + if(target == active_held) //was this the original item in the players hand? put it back because we coudn't salvage it + user.put_in_active_hand(target) + continue + + //if stack, check if we want to read precise amount of sheets to insert + var/obj/item/stack/item_stack = null + if(isstack(target) && precise_insertion) + var/atom/current_parent = parent + item_stack = target + 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) || 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 = 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 + //if it was split then item_stack has the reference to the original stack/item + var/original_item = was_stack_split ? item_stack : target + //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) + to_chat(user, span_warning("[held_item] is stuck to you and cannot be placed into [parent].")) + return + + //insert the item + var/inserted = insert_item(target, breakdown_flags = mat_container_flags) + if(inserted > 0) + . += inserted + + //stack was either split by the container(!QDELETED(target) means the container only consumed a part of it) or by the player, put whats left back of the original stack back in players hand + if((!QDELETED(target) || was_stack_split)) + + //stack was split by player and that portion was not fully consumed, merge whats left back with the original stack + if(!QDELETED(target) && was_stack_split) + var/obj/item/stack/inserting_stack = target + 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(!isnull(original_item)) + user.put_in_active_hand(original_item) + + to_chat(user, span_notice("You insert a material total of [inserted] into [parent].")) + else + //decode the error & print it + var/error_msg + if(inserted == -2) + error_msg = "[parent] has insufficient space to accept the [target]" + else + error_msg = "[target] has insufficient materials to be accepted by [parent]" + to_chat(user, span_warning(error_msg)) + + //player split the stack by the requested amount but even that split amount could not be salvaged. merge it back with the original + if(!isnull(item_stack) && was_stack_split) + var/obj/item/stack/inserting_stack = target + 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(!isnull(original_item)) + user.put_in_active_hand(original_item) + +/** + * Splits a stack. we don't use /obj/item/stack/proc/split_stack because Byond complains that should only be called asynchronously. + * This proc is also more faster because it doesn't deal with mobs, copying evidences or refreshing atom storages + */ +/datum/component/material_container/proc/split_stack(obj/item/stack/target, amount) + if(!target.use(amount, TRUE, FALSE)) + return null + + . = new target.type(target.drop_location(), amount, FALSE, target.mats_per_unit) + target.loc.atom_storage?.refresh_views() + + target.is_zero_amount(delete_if_zero = TRUE) /// Proc specifically for inserting items, returns the amount of materials entered. -/datum/component/material_container/proc/insert_item(obj/item/I, multiplier = 1, stack_amt, breakdown_flags = mat_container_flags) - if(QDELETED(I)) - return FALSE - +/datum/component/material_container/proc/insert_item(obj/item/weapon, multiplier = 1, breakdown_flags = mat_container_flags) + if(QDELETED(weapon)) + return -1 multiplier = CEILING(multiplier, 0.01) - var/material_amount = get_item_material_amount(I, breakdown_flags) - if(!material_amount || !has_space(material_amount)) - return FALSE + var/obj/item/target = weapon - last_inserted_id = insert_item_materials(I, multiplier, breakdown_flags) - return material_amount + var/material_amount = get_item_material_amount(target, breakdown_flags) * multiplier + if(!material_amount) + return -1 + var/obj/item/stack/item_stack + if(isstack(weapon) && !has_space(material_amount)) //not enugh space split and feed as many sheets possible + item_stack = weapon + var/space_left = max_amount - total_amount + if(!space_left) + return -2 + var/material_per_sheet = material_amount / item_stack.amount + var/sheets_to_insert = round(space_left / material_per_sheet) + if(!sheets_to_insert) + return -2 + target = split_stack(item_stack, sheets_to_insert) + material_amount = get_item_material_amount(target, breakdown_flags) * multiplier + if(!has_space(material_amount)) + return -2 + + last_inserted_id = insert_item_materials(target, multiplier, breakdown_flags) + if(!isnull(last_inserted_id)) + if(after_insert) + after_insert.Invoke(target, last_inserted_id, material_amount) + 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 0 /** * Inserts the relevant materials from an item into this material container. diff --git a/code/game/machinery/recycler.dm b/code/game/machinery/recycler.dm index bcb6d4b3f65..2b4ebe3a921 100644 --- a/code/game/machinery/recycler.dm +++ b/code/game/machinery/recycler.dm @@ -150,35 +150,44 @@ if(obj_flags & EMAGGED) for(var/CRUNCH in crunchy_nom) // Eat them and keep going because we don't care about safety. if(isliving(CRUNCH)) // MMIs and brains will get eaten like normal items + if(!is_operational) //we ran out of power after recycling a large amount to living stuff, time to stop + break crush_living(CRUNCH) use_power(active_power_usage) else // Stop processing right now without eating anything. emergency_stop() return - for(var/nommed in nom) - recycle_item(nommed) - use_power(active_power_usage) - if(nom.len && sound) - playsound(src, item_recycle_sound, (50 + nom.len*5), TRUE, nom.len, ignore_walls = (nom.len - 10)) // As a substitute for playing 50 sounds at once. - if(not_eaten) - playsound(src, 'sound/machines/buzz-sigh.ogg', (50 + not_eaten*5), FALSE, not_eaten, ignore_walls = (not_eaten - 10)) // Ditto. - if(!ismob(morsel)) - qdel(morsel) -/obj/machinery/recycler/proc/recycle_item(obj/item/I) - var/obj/item/grown/log/L = I - if(istype(L)) + /** + * we process the list in reverse so that atoms without parents/contents are deleted first & their parents are deleted next & so on. + * this is the reverse order in which get_all_contents() returns it's list + * if we delete an atom containing stuff then all its stuff are deleted with it as well so we will end recycling deleted items down the list and gain nothing from them + */ + for(var/i = length(nom); i >= 1; i--) + if(!is_operational) //we ran out of power after recycling a large amount to items, time to stop + break + use_power(active_power_usage / (recycle_item(nom[i]) ? 1 : 2)) //recycling stuff that produces no material takes just half the power + if(nom.len && sound) + playsound(src, item_recycle_sound, (50 + nom.len * 5), TRUE, nom.len, ignore_walls = (nom.len - 10)) // As a substitute for playing 50 sounds at once. + if(not_eaten) + playsound(src, 'sound/machines/buzz-sigh.ogg', (50 + not_eaten * 5), FALSE, not_eaten, ignore_walls = (not_eaten - 10)) // Ditto. + +/obj/machinery/recycler/proc/recycle_item(obj/item/weapon) + . = FALSE + var/obj/item/grown/log/wood = weapon + if(istype(wood)) var/seed_modifier = 0 - if(L.seed) - seed_modifier = round(L.seed.potency / 25) - new L.plank_type(loc, 1 + seed_modifier) + if(wood.seed) + seed_modifier = round(wood.seed.potency / 25) + new wood.plank_type(loc, 1 + seed_modifier) + . = TRUE else var/datum/component/material_container/materials = GetComponent(/datum/component/material_container) - var/material_amount = materials.get_item_material_amount(I, BREAKDOWN_FLAGS_RECYCLER) - if(material_amount) - materials.insert_item(I, material_amount, multiplier = (amount_produced / 100), breakdown_flags=BREAKDOWN_FLAGS_RECYCLER) + var/retrived = materials.insert_item(weapon, multiplier = (amount_produced / 100), breakdown_flags=BREAKDOWN_FLAGS_RECYCLER) + if(retrived > 0) //item was salvaged i.e. deleted materials.retrieve_all() - qdel(I) + return TRUE + qdel(weapon) /obj/machinery/recycler/proc/emergency_stop() playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE) @@ -192,7 +201,6 @@ update_appearance() /obj/machinery/recycler/proc/crush_living(mob/living/L) - L.forceMove(loc) if(issilicon(L)) diff --git a/code/modules/mining/machine_processing.dm b/code/modules/mining/machine_processing.dm index fe15d628fd2..869779f6c36 100644 --- a/code/modules/mining/machine_processing.dm +++ b/code/modules/mining/machine_processing.dm @@ -171,8 +171,7 @@ if(!materials.has_space(material_amount)) unload_mineral(O) else - materials.insert_item(O, breakdown_flags=BREAKDOWN_FLAGS_ORE_PROCESSOR) - qdel(O) + materials.insert_item(O, breakdown_flags = BREAKDOWN_FLAGS_ORE_PROCESSOR) if(mineral_machine) mineral_machine.updateUsrDialog() diff --git a/code/modules/mining/machine_redemption.dm b/code/modules/mining/machine_redemption.dm index fcfc7d416ac..f3a4278c2fe 100644 --- a/code/modules/mining/machine_redemption.dm +++ b/code/modules/mining/machine_redemption.dm @@ -75,9 +75,6 @@ if(gathered_ore.refined_type == null) return - if(gathered_ore?.refined_type) - points += gathered_ore.points * point_upgrade * gathered_ore.amount - var/material_amount = mat_container.get_item_material_amount(gathered_ore, BREAKDOWN_FLAGS_ORM) if(!material_amount) @@ -89,10 +86,14 @@ else var/list/stack_mats = gathered_ore.get_material_composition(BREAKDOWN_FLAGS_ORM) var/mats = stack_mats & mat_container.materials - var/amount = gathered_ore.amount - mat_container.insert_item(gathered_ore, ore_multiplier, breakdown_flags=BREAKDOWN_FLAGS_ORM) //insert it - materials.silo_log(src, "smelted", amount, gathered_ore.name, mats) - qdel(gathered_ore) + var/ore_amount = gathered_ore.amount + var/ore_points= gathered_ore.points + var/ore_name = gathered_ore.name + var/refined_type = gathered_ore?.refined_type + if(mat_container.insert_item(gathered_ore, ore_multiplier, breakdown_flags = BREAKDOWN_FLAGS_ORM) > 0) //increase points only if insertion was successfull + if(refined_type) + points += ore_points * point_upgrade * ore_amount + materials.silo_log(src, "smelted", ore_amount, ore_name, mats) SEND_SIGNAL(src, COMSIG_ORM_COLLECTED_ORE) diff --git a/code/modules/mining/machine_stacking.dm b/code/modules/mining/machine_stacking.dm index 0ea2ddbcba4..366791c6a01 100644 --- a/code/modules/mining/machine_stacking.dm +++ b/code/modules/mining/machine_stacking.dm @@ -142,7 +142,6 @@ if (length(matlist)) var/inserted = materials.mat_container.insert_item(inp) materials.silo_log(src, "collected", inserted, "sheets", matlist) - qdel(inp) return // No silo attached process to internal storage diff --git a/code/modules/projectiles/boxes_magazines/_box_magazine.dm b/code/modules/projectiles/boxes_magazines/_box_magazine.dm index f5550ba5ed4..3783e464bc1 100644 --- a/code/modules/projectiles/boxes_magazines/_box_magazine.dm +++ b/code/modules/projectiles/boxes_magazines/_box_magazine.dm @@ -31,10 +31,6 @@ var/multiload = TRUE ///Whether the magazine should start with nothing in it var/start_empty = FALSE - ///cost of all the bullets in the magazine/box - var/list/bullet_cost - ///cost of the materials in the magazine/box itself - var/list/base_cost /// If this and ammo_band_icon aren't null, run update_ammo_band(). Is the color of the band, such as blue on the detective's Iceblox. var/ammo_band_color @@ -45,9 +41,7 @@ /obj/item/ammo_box/Initialize(mapload) . = ..() - if(!bullet_cost) - base_cost = SSmaterials.FindOrCreateMaterialCombo(custom_materials, 0.1) - bullet_cost = SSmaterials.FindOrCreateMaterialCombo(custom_materials, 0.9 / max_ammo) + custom_materials = SSmaterials.FindOrCreateMaterialCombo(custom_materials, 0.1) if(!start_empty) top_off(starting=TRUE) update_icon_state() @@ -87,7 +81,7 @@ for(var/i in max(1, stored_ammo.len) to max_ammo) stored_ammo += new round_check(src) - update_ammo_count() + update_appearance() ///gets a round from the magazine, if keep is TRUE the round will stay in the gun /obj/item/ammo_box/proc/get_round(keep = FALSE) @@ -141,7 +135,7 @@ if(!did_load || !multiload) break if(num_loaded) - AM.update_ammo_count() + AM.update_appearance() if(isammocasing(A)) var/obj/item/ammo_casing/AC = A if(give_round(AC, replace_spent)) @@ -153,7 +147,7 @@ if(!silent) to_chat(user, span_notice("You load [num_loaded] shell\s into \the [src]!")) playsound(src, 'sound/weapons/gun/general/mag_bullet_insert.ogg', 60, TRUE) - update_ammo_count() + update_appearance() return num_loaded @@ -167,11 +161,6 @@ A.bounce_away(FALSE, NONE) playsound(src, 'sound/weapons/gun/general/mag_bullet_insert.ogg', 60, TRUE) to_chat(user, span_notice("You remove a round from [src]!")) - update_ammo_count() - -/// Updates the materials and appearance of this ammo box -/obj/item/ammo_box/proc/update_ammo_count() - update_custom_materials() update_appearance() /obj/item/ammo_box/update_desc(updates) @@ -202,13 +191,6 @@ ammo_band_image.appearance_flags = RESET_COLOR|KEEP_APART overlays += ammo_band_image -/// Updates the amount of material in this ammo box according to how many bullets are left in it. -/obj/item/ammo_box/proc/update_custom_materials() - var/temp_materials = custom_materials.Copy() - for(var/material in bullet_cost) - temp_materials[material] = (bullet_cost[material] * stored_ammo.len) + base_cost[material] - set_custom_materials(temp_materials) - ///Count of number of bullets in the magazine /obj/item/ammo_box/magazine/proc/ammo_count(countempties = TRUE) var/boolets = 0 @@ -233,4 +215,4 @@ /obj/item/ammo_box/magazine/handle_atom_del(atom/A) stored_ammo -= A - update_ammo_count() + update_appearance()