Flatpacker & machine frame enhancements (#91752)

## About The Pull Request

**1. Makes the machine creation process from both efficient.**
The machine frame no longer creates & deletes the machines old component
parts before moving in the new parts. It instead now passes its parts
directly to the circuit boards `apply_default_parts()` meaning the old
parts are never created and the new ones get applied directly. This also
mean it does not have to again call `RefreshParts()` as the machines
`Initialize()` proc will do that for us by default so the performance is
just faster now.
Flatpack box also benefits from this and it will need it as stated below

- Fixes #86514. As the circuit board is passed to the machine's
`Initialize()` proc during reconstruction so it sets it's
`all_products_free` from the board both during mapload & when player
constructed from either machine frame & flatpack

**2. Flatpack enhancements** 
- The flatpack box now passes its stored circuit board into the machine
meaning any changes made like with a screw driver or such are preserved
in the newly created machine rather than the board getting deleted & the
machine creating the default board
- Flatpacker now accept custom component parts that replaces its stored
circuit board default parts. This implements

https://github.com/tgstation/tgstation/pull/91670#issuecomment-2977259973
meaning instead of the vendor created from the flatpacker starting out
empty it now accepts the refill canister and will package both of them
together. Then during deployment that stored canister is stored back in
the vendor meaning instead of starting out empty you get to carry over
all your previous stock

Depends on https://github.com/tgstation/tgstation/pull/91512 because of
named arg conflict. Keep this drafted till that gets merged

## Changelog
🆑
qol: flatpacker now accepts custom component parts(like vendor refill
canister) along with the circuit board so you can carry over your
previous stock during deployment rather than starting out empty
fix: flatpack boxes now passes its stored circuit board into the machine
thus preserving its properties instead of creating a default board in
the machine
fix: vending machines reconstructed on station from circuits loaded off
station(having free products) will continue to sell free products
code: machine construction via flatpacker/machine frame is faster
performance wise
/🆑
This commit is contained in:
SyncIt21
2025-07-11 08:58:11 +02:00
committed by GitHub
parent 1b87ca48db
commit 2d41569c65
24 changed files with 192 additions and 67 deletions
+10 -2
View File
@@ -160,12 +160,20 @@
fire = 50
acid = 70
///Needed by machine frame & flatpacker i.e the named arg board
/obj/machinery/New(loc, obj/item/circuitboard/board, ...)
if(istype(board))
circuit = board
return ..()
/obj/machinery/Initialize(mapload)
. = ..()
SSmachines.register_machine(src)
if(ispath(circuit, /obj/item/circuitboard))
circuit = new circuit(src)
if(istype(circuit))
circuit.apply_default_parts(src)
if(processing_flags & START_PROCESSING_ON_INIT)
@@ -1191,10 +1199,10 @@
/obj/machinery/examine_more(mob/user)
. = ..()
if(HAS_TRAIT(user, TRAIT_RESEARCH_SCANNER) && component_parts)
. += display_parts(user, TRUE)
. += display_parts(user)
//called on machinery construction (i.e from frame to machinery) but not on initialization
/obj/machinery/proc/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/proc/on_construction(mob/user)
return
/**
@@ -17,7 +17,7 @@
link_alerts()
return ..()
/obj/machinery/computer/station_alert/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/computer/station_alert/on_construction(mob/user)
. = ..()
link_alerts()
+91 -11
View File
@@ -19,6 +19,8 @@
var/datum/component/material_container/materials
/// The inserted board
var/obj/item/circuitboard/machine/inserted_board
/// List of components that need to be packed along with the circuitboard
var/list/obj/item/flatpacked_components = list()
/// Materials needed to print this board
var/list/needed_mats = list()
/// The highest tier of this board
@@ -44,12 +46,18 @@
/obj/machinery/flatpacker/Destroy()
materials = null
QDEL_NULL(inserted_board)
QDEL_LIST(flatpacked_components)
. = ..()
/obj/machinery/flatpacker/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = NONE
if(!QDELETED(inserted_board))
context[SCREENTIP_CONTEXT_CTRL_LMB] = "Eject board"
if(!isnull(held_item) && (held_item.type in inserted_board.flatpack_components))
context[SCREENTIP_CONTEXT_LMB] = "Insert flatpack component"
return CONTEXTUAL_SCREENTIP_SET
. = CONTEXTUAL_SCREENTIP_SET
if(!isnull(held_item))
@@ -78,6 +86,18 @@
. += span_notice("It can be [EXAMINE_HINT("pried")] apart")
if(!QDELETED(inserted_board))
. += span_notice("The board can be ejected via [EXAMINE_HINT("Ctrl Click")]")
if(length(inserted_board.flatpack_components))
var/list/obj/item/to_insert
for(var/obj/item/component as anything in inserted_board.flatpack_components)
var/inserted = get_flatpack_component_count(component)
var/required = inserted_board.req_components[component]
if(inserted == required)
continue
LAZYADDASSOC(to_insert, get_flatpack_component_name(component), "[inserted]/[required]")
if(length(to_insert))
. += span_warning("The following components must be inserted by hand before packaging")
for(var/component_name in to_insert)
. += span_warning("[component_name]:[to_insert[component_name]]")
/obj/machinery/flatpacker/update_overlays()
. = ..()
@@ -85,6 +105,36 @@
if(!QDELETED(inserted_board))
. += mutable_appearance(icon, "[base_icon_state]_c")
/**
* Returns the name of this component. Vending canistors & maybe other types in the future require special parsing
*
* Arguments
* * obj/item/component - the component typepath we are trying to get the name
*/
/obj/machinery/flatpacker/proc/get_flatpack_component_name(obj/item/component)
PRIVATE_PROC(TRUE)
if(ispath(type, /obj/item/vending_refill))
var/obj/item/vending_refill/canister = component
return "\improper [canister::machine_name] restocking unit"
return component::name
/**
* Returns count of inserted flatpack component parts
*
* Arguments
* * obj/item/type - the component type we are trying to count
*/
/obj/machinery/flatpacker/proc/get_flatpack_component_count(obj/item/type)
PRIVATE_PROC(TRUE)
. = 0
for(var/obj/item/test as anything in flatpacked_components)
if(test.type == type)
. += 1
/obj/machinery/flatpacker/Exited(atom/movable/gone, direction)
. = ..()
if(gone == inserted_board)
@@ -92,6 +142,8 @@
needed_mats.Cut()
print_tier = 1
update_appearance(UPDATE_OVERLAYS)
if(gone in flatpacked_components)
flatpacked_components -= gone
/obj/machinery/flatpacker/RefreshParts()
. = ..()
@@ -133,10 +185,11 @@
* Otherwise, the typepath is created in nullspace and fetches materials from the initialized one, then deleted.
*
* Args:
* part_type - Typepath of the item we are trying to find the costs of
* costs - Assoc list we modify and return
* * part_type - Typepath of the item we are trying to find the costs of
* * costs - Assoc list we modify and return
* * count - the number of parts to compute the cost of
*/
/obj/machinery/flatpacker/proc/analyze_cost(part_type, costs)
/obj/machinery/flatpacker/proc/analyze_cost(part_type, costs, count)
PRIVATE_PROC(TRUE)
var/comp_type = part_type
@@ -159,7 +212,7 @@
mat_list = null_comp.custom_materials
for(var/atom/mat as anything in mat_list)
CREATE_AND_INCREMENT(costs, mat.type, mat_list[mat] * inserted_board.req_components[part_type])
CREATE_AND_INCREMENT(costs, mat.type, mat_list[mat] * count)
if(null_comp)
qdel(null_comp)
@@ -183,7 +236,10 @@
//compute the needed mats from its stock parts
for(var/type as anything in inserted_board.req_components)
needed_mats = analyze_cost(type, needed_mats)
//these don't count to the final cost as they have to inserted manually
if(type in inserted_board.flatpack_components)
continue
needed_mats = analyze_cost(type, needed_mats, inserted_board.req_components[type])
// 5 sheets of iron and 5 of cable coil
CREATE_AND_INCREMENT(needed_mats, /datum/material/iron, (SHEET_MATERIAL_AMOUNT * 5 + (SHEET_MATERIAL_AMOUNT / 20)))
@@ -191,6 +247,17 @@
update_appearance(UPDATE_OVERLAYS)
return ITEM_INTERACT_SUCCESS
else if(!QDELETED(inserted_board) && (attacking_item.type in inserted_board.flatpack_components))
if(get_flatpack_component_count(attacking_item.type) == inserted_board.req_components[attacking_item.type])
balloon_alert(user, "max count reached!")
return ITEM_INTERACT_BLOCKING
if(!user.transferItemToLoc(attacking_item, src))
to_chat(user, span_warning("[attacking_item] is stuck in hand!"))
return ITEM_INTERACT_BLOCKING
LAZYADD(flatpacked_components, attacking_item)
return ITEM_INTERACT_SUCCESS
return ..()
@@ -237,16 +304,20 @@
var/atom/build = initial(inserted_board.build_path)
var/disableReason = ""
var/has_materials = materials.has_materials(needed_mats, creation_efficiency)
if(!has_materials)
disableReason += "Not enough materials. "
if(print_tier > max_part_tier)
disableReason += "This design is too advanced for this machine. "
disableReason = "This design is too advanced for this machine. "
else if(!materials.has_materials(needed_mats, creation_efficiency))
disableReason = "Not enough materials. "
else
for(var/obj/item/component as anything in inserted_board.flatpack_components)
var/diff = inserted_board.req_components[component] - get_flatpack_component_count(component)
if(diff)
disableReason = "Please insert [diff] [get_flatpack_component_name(component)]"
break
design = list(
"name" = initial(build.name),
"requiredMaterials" = cost_mats,
"icon" = icon2base64(icon(initial(build.icon), initial(build.icon_state), frame = 1)),
"canPrint" = has_materials && print_tier <= max_part_tier,
"disableReason" = disableReason
)
.["design"] = design
@@ -266,6 +337,10 @@
if(print_tier > max_part_tier)
say("Design too complex.")
return
for(var/obj/item/component as anything in inserted_board.flatpack_components)
if(inserted_board.req_components[component] != get_flatpack_component_count(component))
say("Not enough [get_flatpack_component_name(component)].")
return
if(!materials.has_materials(needed_mats, creation_efficiency))
say("Not enough materials to begin production.")
return
@@ -313,7 +388,9 @@
busy = FALSE
materials.use_materials(needed_mats, creation_efficiency)
new /obj/item/flatpack(drop_location(), board)
var/obj/item/flatpack/box = new (drop_location(), board)
for(var/obj/item/component as anything in flatpacked_components)
component.forceMove(box)
SStgui.update_uis(src)
@@ -322,6 +399,9 @@
return CLICK_ACTION_BLOCKING
try_put_in_hand(inserted_board, user)
var/drop = drop_location()
for(var/obj/item/component as anything in flatpacked_components)
component.forceMove(drop)
return CLICK_ACTION_SUCCESS
+7 -12
View File
@@ -446,22 +446,17 @@
return FALSE
tool.play_tool_sound(src)
var/obj/machinery/new_machine = new circuit.build_path(loc)
// Prevent us from dropping stuff thanks to /Exited
var/obj/item/circuitboard/machine/leaving_circuit = circuit
components -= leaving_circuit
leaving_circuit.replacement_parts = components
// Build the machine with the replacement parts
circuit = null
var/obj/machinery/new_machine = new leaving_circuit.build_path(loc, board = leaving_circuit)
if(istype(new_machine))
new_machine.clear_components()
// Set anchor state
new_machine.set_anchored(anchored)
// Prevent us from dropping stuff thanks to /Exited
var/obj/item/circuitboard/machine/leaving_circuit = circuit
circuit = null
// Assign the circuit & parts & move them all at once into the machine
// no need to separately move circuit board as its already part of the components list
new_machine.circuit = leaving_circuit
new_machine.component_parts = components
for (var/obj/new_part in components)
new_part.forceMove(new_machine)
//Inform machine that its finished & cleanup
new_machine.RefreshParts()
new_machine.on_construction(user)
components = null
qdel(src)
+1 -1
View File
@@ -57,7 +57,7 @@
GLOB.navbeacons["[new_turf?.z]"] += src
return ..()
/obj/machinery/navbeacon/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/navbeacon/on_construction(mob/user)
var/turf/our_turf = loc
if(!isfloorturf(our_turf))
return
+1 -1
View File
@@ -79,7 +79,7 @@
QDEL_NULL(cell)
return..()
/obj/machinery/space_heater/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/space_heater/on_construction()
set_panel_open(TRUE)
QDEL_NULL(cell)
@@ -76,13 +76,23 @@ micro-manipulator, console screen, beaker, Microlaser, matter bin, power cells.
/// Default replacements for req_components, to be used in apply_default_parts instead of req_components types
/// Example: list(/obj/item/stock_parts/matter_bin = /obj/item/stock_parts/matter_bin/super)
var/list/def_components
/// List of atoms/datums to replace the default components placed inside the machine
var/list/replacement_parts
/// Components that need to be flatpacked along with the circuitboard so as to replace the defaults
var/list/obj/item/flatpack_components
// Applies the default parts defined by the circuit board when the machine is created
/obj/item/circuitboard/machine/apply_default_parts(obj/machinery/machine)
if(!req_components)
return
/**
* Converts req_components map into a linear list with its datum components resolved
*
* Arguments
* * obj/machinery/machine - if not null adds the parts to the machine directly & will not return anything
*/
/obj/item/circuitboard/machine/proc/flatten_component_list(obj/machinery/machine)
SHOULD_NOT_OVERRIDE(TRUE)
. = ..()
. = NONE
if(QDELETED(machine))
. = list()
for(var/comp_path in req_components)
var/comp_amt = req_components[comp_path]
@@ -99,10 +109,40 @@ micro-manipulator, console screen, beaker, Microlaser, matter bin, power cells.
if (isnull(stock_part_datum))
CRASH("[comp_path] didn't have a matching stock part datum")
for (var/_ in 1 to comp_amt)
machine.component_parts += stock_part_datum
if(!.)
machine.component_parts += stock_part_datum
else
. += stock_part_datum
else
for(var/component in 1 to comp_amt)
machine.component_parts += new comp_path(machine)
for(var/_ in 1 to comp_amt)
if(!.)
machine.component_parts += new comp_path(machine)
else
. += comp_path
/**
* Applies the default component parts for this machine
*
* Arguments
* * obj/machinery/machine - the machine to apply the default parts to
*/
/obj/item/circuitboard/machine/apply_default_parts(obj/machinery/machine)
if(!req_components && !length(replacement_parts))
return
. = ..()
if(replacement_parts)
for(var/part in replacement_parts)
if(ispath(part, /obj/item))
part = new part(machine)
else if(ismovable(part))
var/atom/movable/thing = part
thing.forceMove(machine)
machine.component_parts += part
replacement_parts = null
else
flatten_component_list(machine)
machine.RefreshParts()
@@ -715,6 +715,7 @@
build_path = typepath
name = "[vending_names_paths[build_path]] Vendor"
req_components = list(initial(typepath.refill_canister) = 1)
flatpack_components = list(initial(typepath.refill_canister))
/obj/item/circuitboard/machine/vendor/apply_default_parts(obj/machinery/machine)
for(var/typepath in vending_names_paths)
+17 -2
View File
@@ -71,10 +71,25 @@
return ITEM_INTERACT_BLOCKING
new /obj/effect/temp_visual/mook_dust(loc)
var/obj/machinery/new_machine = new board.build_path(loc)
var/obj/item/circuitboard/machine/leaving_circuit = board
if(contents.len > 1)
leaving_circuit.replacement_parts = leaving_circuit.flatten_component_list()
for(var/obj/item/flatpack_component in src)
if(flatpack_component == leaving_circuit)
continue
for(var/i in 1 to leaving_circuit.replacement_parts.len)
var/obj/item/machine_component = leaving_circuit.replacement_parts[i]
if(!ispath(machine_component, /obj/item))
continue
if(flatpack_component.type == machine_component)
leaving_circuit.replacement_parts[i] = flatpack_component
break
board = null
var/obj/machinery/new_machine = new leaving_circuit.build_path(loc, board = leaving_circuit)
new_machine.on_construction(user)
loc.visible_message(span_warning("[src] deploys!"))
playsound(src, 'sound/machines/terminal/terminal_eject.ogg', 70, TRUE)
new_machine.on_construction(user, from_flatpack = TRUE)
qdel(src)
return ITEM_INTERACT_SUCCESS
+1 -1
View File
@@ -43,7 +43,7 @@
engine_state = ENGINE_UNWRENCHED
anchored = FALSE
/obj/machinery/power/shuttle_engine/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/power/shuttle_engine/on_construction(mob/user)
. = ..()
if(anchored)
connect_to_shuttle(port = SSshuttle.get_containing_shuttle(src)) //connect to a new ship, if needed
@@ -541,7 +541,7 @@
PIPING_FORWARD_SHIFT(pipe_overlay, piping_layer, 2)
return pipe_overlay
/obj/machinery/atmospherics/on_construction(mob/user, obj_color, set_layer = PIPING_LAYER_DEFAULT, from_flatpack = FALSE)
/obj/machinery/atmospherics/on_construction(mob/user, obj_color, set_layer = PIPING_LAYER_DEFAULT)
if(can_unwrench)
add_atom_colour(obj_color, FIXED_COLOUR_PRIORITY)
set_pipe_color(obj_color)
@@ -125,7 +125,7 @@
airs[i] = null
return ..()
/obj/machinery/atmospherics/components/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/atmospherics/components/on_construction(mob/user)
. = ..()
update_parents()
@@ -67,7 +67,7 @@
return FALSE
. = ..()
/obj/machinery/atmospherics/components/unary/thermomachine/on_construction(mob/user, obj_color, set_layer, from_flatpack = FALSE)
/obj/machinery/atmospherics/components/unary/thermomachine/on_construction(mob/user, obj_color, set_layer)
var/obj/item/circuitboard/machine/thermomachine/board = circuit
if(board)
piping_layer = board.pipe_layer
@@ -13,7 +13,7 @@
/obj/machinery/atmospherics/components/unary/set_init_directions()
initialize_directions = dir
/obj/machinery/atmospherics/components/unary/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/atmospherics/components/unary/on_construction(mob/user)
..()
update_appearance()
@@ -64,7 +64,7 @@
AddElement(/datum/element/elevation, pixel_shift = 8)
register_context()
/obj/machinery/portable_atmospherics/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/portable_atmospherics/on_construction(mob/user)
. = ..()
set_anchored(FALSE)
+1 -1
View File
@@ -37,7 +37,7 @@
WARNING("[src] couldnt find a Quartermaster/Storage (aka cargobay) area on the station, and as such it has set the supplypod landingzone to the area it resides in.")
landingzone = get_area(src)
/obj/machinery/computer/cargo/express/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/computer/cargo/express/on_construction(mob/user)
. = ..()
packin_up()
+1 -1
View File
@@ -69,7 +69,7 @@
update_static_data(user)
return TRUE
/obj/machinery/computer/cargo/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/computer/cargo/on_construction(mob/user)
. = ..()
circuit.configure_machine(src)
@@ -201,7 +201,7 @@
linked_venue = null
return ..()
/obj/machinery/restaurant_portal/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/restaurant_portal/on_construction(mob/user)
. = ..()
circuit.configure_machine(src)
+1 -1
View File
@@ -102,7 +102,7 @@
DropFuel()
return ..()
/obj/machinery/power/port_gen/pacman/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/power/port_gen/pacman/on_construction(mob/user)
var/obj/item/circuitboard/machine/pacman/our_board = circuit
if(our_board.high_production_profile)
icon_state = "portgen1_0"
+1 -1
View File
@@ -118,7 +118,7 @@
/// The port this is connected to.
var/obj/machinery/power/smes/connector/connected_port
/obj/machinery/power/smesbank/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/power/smesbank/on_construction(mob/user)
. = ..()
set_anchored(FALSE)
@@ -6,7 +6,7 @@
may_be_remote_controlled = TRUE
var/static/list/connections = list(COMSIG_TURF_ADDED_TO_SHUTTLE = PROC_REF(on_loc_added_to_shuttle))
/obj/machinery/computer/shuttle/custom_shuttle/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/computer/shuttle/custom_shuttle/on_construction(mob/user)
circuit.configure_machine(src)
if(!shuttleId)
AddElement(/datum/element/connect_loc, connections)
@@ -48,7 +48,7 @@
zlink_range = 1
var/static/list/connections = list(COMSIG_TURF_ADDED_TO_SHUTTLE = PROC_REF(on_loc_added_to_shuttle))
/obj/machinery/computer/camera_advanced/shuttle_docker/custom/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/computer/camera_advanced/shuttle_docker/custom/on_construction(mob/user)
circuit.configure_machine(src)
if(!shuttleId)
AddElement(/datum/element/connect_loc, connections)
+1 -1
View File
@@ -183,7 +183,7 @@
update_appearance()
/obj/machinery/computer/tram_controls/on_construction(mob/user, from_flatpack = FALSE)
/obj/machinery/computer/tram_controls/on_construction(mob/user)
. = ..()
var/obj/item/circuitboard/computer/tram_controls/my_circuit = circuit
split_mode = my_circuit.split_mode
-13
View File
@@ -323,19 +323,6 @@ GLOBAL_LIST_EMPTY(vending_machines_to_restock)
for(var/obj/item/vending_refill/installed_refill in component_parts)
restock(installed_refill)
/obj/machinery/vending/on_construction(mob/user, from_flatpack = FALSE)
if (!from_flatpack)
return
// When built from a flatpack, empty our canister upon construction
for(var/obj/item/vending_refill/installed_refill in component_parts)
for (var/item_sold in installed_refill.products)
installed_refill.products[item_sold] = 0
for (var/item_sold in installed_refill.contraband)
installed_refill.contraband[item_sold] = 0
for (var/item_sold in installed_refill.premium)
installed_refill.premium[item_sold] = 0
RefreshParts()
/obj/machinery/vending/on_deconstruction(disassembled)
if(refill_canister)
return ..()
+1 -2
View File
@@ -29,7 +29,6 @@ type Design = {
name: string;
icon: string;
requiredMaterials: Material[];
canPrint: BooleanLike;
disableReason?: string;
};
@@ -145,7 +144,7 @@ const BoardPreview = (props: BoardPreviewProps) => {
icon="cog"
fontSize={1.2}
textAlign="center"
disabled={!design || !design.canPrint}
disabled={!design || design.disableReason !== ''}
tooltip={design.disableReason}
tooltipPosition="bottom"
onClick={() => onPrint()}