diff --git a/code/controllers/subsystem/processing/manufacturing.dm b/code/controllers/subsystem/processing/manufacturing.dm
new file mode 100644
index 00000000000..8bc9c6af5d5
--- /dev/null
+++ b/code/controllers/subsystem/processing/manufacturing.dm
@@ -0,0 +1,4 @@
+PROCESSING_SUBSYSTEM_DEF(manufacturing)
+ name = "Manufacturing Processing"
+ wait = 1 SECONDS
+ stat_tag = "MN"
diff --git a/code/datums/components/crafting/crafting.dm b/code/datums/components/crafting/crafting.dm
index 54f77d4579e..bf13b7cd5ba 100644
--- a/code/datums/components/crafting/crafting.dm
+++ b/code/datums/components/crafting/crafting.dm
@@ -21,6 +21,8 @@
var/display_craftable_only = FALSE
var/display_compact = FALSE
var/forced_mode = FALSE
+ /// crafting flags we ignore when considering a recipe
+ var/ignored_flags = NONE
/* This is what procs do:
get_environment - gets a list of things accessable for crafting by user
@@ -205,16 +207,16 @@
if(!check_tools(crafter, recipe, contents))
return ", missing tool."
+ var/considered_flags = recipe.crafting_flags & ~(ignored_flags)
-
- if((recipe.crafting_flags & CRAFT_ONE_PER_TURF) && (locate(recipe.result) in dest_turf))
+ if((considered_flags & CRAFT_ONE_PER_TURF) && (locate(recipe.result) in dest_turf))
return ", already one here!"
- if(recipe.crafting_flags & CRAFT_CHECK_DIRECTION)
- if(!valid_build_direction(dest_turf, crafter.dir, is_fulltile = (recipe.crafting_flags & CRAFT_IS_FULLTILE)))
+ if(considered_flags & CRAFT_CHECK_DIRECTION)
+ if(!valid_build_direction(dest_turf, crafter.dir, is_fulltile = (considered_flags & CRAFT_IS_FULLTILE)))
return ", won't fit here!"
- if(recipe.crafting_flags & CRAFT_ON_SOLID_GROUND)
+ if(considered_flags & CRAFT_ON_SOLID_GROUND)
if(isclosedturf(dest_turf))
return ", cannot be made on a wall!"
@@ -222,7 +224,7 @@
if(!locate(/obj/structure/thermoplastic) in dest_turf) // for tram construction
return ", must be made on solid ground!"
- if(recipe.crafting_flags & CRAFT_CHECK_DENSITY)
+ if(considered_flags & CRAFT_CHECK_DENSITY)
for(var/obj/object in dest_turf)
if(object.density && !(object.obj_flags & IGNORE_DENSITY) || object.obj_flags & BLOCKS_CONSTRUCTION)
return ", something is in the way!"
@@ -703,3 +705,20 @@
if(recipe == potential_recipe)
return TRUE
return FALSE
+
+/datum/component/personal_crafting/machine
+ ignored_flags = CRAFT_CHECK_DENSITY
+
+/datum/component/personal_crafting/machine/get_environment(atom/crafter, list/blacklist = null, radius_range = 1)
+ . = list()
+ for(var/atom/movable/content in crafter.contents)
+ if((content.flags_1 & HOLOGRAM_1) || (blacklist && (content.type in blacklist)))
+ continue
+ if(isitem(content))
+ var/obj/item/item = content
+ if(item.item_flags & ABSTRACT) //let's not tempt fate, shall we?
+ continue
+ . += content
+
+/datum/component/personal_crafting/machine/check_tools(atom/source, datum/crafting_recipe/recipe, list/surroundings)
+ return TRUE
diff --git a/code/datums/components/crafting/structures.dm b/code/datums/components/crafting/structures.dm
index c4a9b48ec36..090ec31ce22 100644
--- a/code/datums/components/crafting/structures.dm
+++ b/code/datums/components/crafting/structures.dm
@@ -74,3 +74,14 @@
)
category = CAT_STRUCTURE
crafting_flags = CRAFT_CHECK_DENSITY | CRAFT_MUST_BE_LEARNED
+
+/datum/crafting_recipe/manucrate
+ name = "Manufacturing Storage Unit"
+ result = /obj/machinery/power/manufacturing/storagebox
+ tool_behaviors = list(TOOL_SCREWDRIVER, TOOL_WELDER)
+ time = 6 SECONDS
+ reqs = list(
+ /obj/item/stack/sheet/iron = 10,
+ )
+ category = CAT_STRUCTURE
+ crafting_flags = CRAFT_CHECK_DENSITY
diff --git a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
index 80c71e2b859..4db8ffd123f 100644
--- a/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
+++ b/code/game/objects/items/circuitboards/machines/machine_circuitboards.dm
@@ -1715,3 +1715,65 @@
req_components = list(
/datum/stock_part/servo = 1,
)
+
+/obj/item/circuitboard/machine/manucrafter
+ name = /obj/machinery/power/manufacturing/crafter::name
+ greyscale_colors = CIRCUIT_COLOR_ENGINEERING
+ build_path = /obj/machinery/power/manufacturing/crafter
+ req_components = list(
+ /obj/item/stack/sheet/iron = 5,
+ /datum/stock_part/servo = 1,
+ )
+
+/obj/item/circuitboard/machine/manulathe
+ name = /obj/machinery/power/manufacturing/lathe::name
+ greyscale_colors = CIRCUIT_COLOR_ENGINEERING
+ build_path = /obj/machinery/power/manufacturing/lathe
+ req_components = list(
+ /obj/item/stack/sheet/iron = 5,
+ /datum/stock_part/servo = 1,
+ )
+
+/obj/item/circuitboard/machine/manucrusher
+ name = /obj/machinery/power/manufacturing/crusher::name
+ greyscale_colors = CIRCUIT_COLOR_ENGINEERING
+ build_path = /obj/machinery/power/manufacturing/crusher
+ req_components = list(
+ /obj/item/stack/sheet/iron = 5,
+ /datum/stock_part/servo = 1,
+ )
+
+/obj/item/circuitboard/machine/manuunloader
+ name = /obj/machinery/power/manufacturing/unloader::name
+ greyscale_colors = CIRCUIT_COLOR_ENGINEERING
+ build_path = /obj/machinery/power/manufacturing/unloader
+ req_components = list(
+ /obj/item/stack/sheet/iron = 5,
+ /datum/stock_part/servo = 1,
+ )
+
+/obj/item/circuitboard/machine/manusorter
+ name = /obj/machinery/power/manufacturing/sorter::name
+ greyscale_colors = CIRCUIT_COLOR_ENGINEERING
+ build_path = /obj/machinery/power/manufacturing/sorter
+ req_components = list(
+ /obj/item/stack/sheet/iron = 5,
+ /datum/stock_part/scanning_module = 1,
+ )
+
+/obj/item/circuitboard/machine/manusmelter
+ name = /obj/machinery/power/manufacturing/smelter::name
+ greyscale_colors = CIRCUIT_COLOR_ENGINEERING
+ build_path = /obj/machinery/power/manufacturing/smelter
+ req_components = list(
+ /obj/item/stack/sheet/iron = 5,
+ /datum/stock_part/micro_laser = 1,
+ )
+
+/obj/item/circuitboard/machine/manurouter
+ name = /obj/machinery/power/manufacturing/router::name
+ greyscale_colors = CIRCUIT_COLOR_ENGINEERING
+ build_path = /obj/machinery/power/manufacturing/router
+ req_components = list(
+ /obj/item/stack/sheet/iron = 5,
+ )
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 38976bcfdd6..5c2dcbe34d6 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -270,7 +270,7 @@ GLOBAL_LIST_EMPTY(station_turfs)
* * type_list - are we checking for types of atoms to ignore and not physical atoms
*/
/turf/proc/is_blocked_turf(exclude_mobs = FALSE, source_atom = null, list/ignore_atoms, type_list = FALSE)
- if(density)
+ if((!isnull(source_atom) && !CanPass(source_atom, get_dir(src, source_atom))) || density)
return TRUE
for(var/atom/movable/movable_content as anything in contents)
diff --git a/code/modules/manufactorio/_manufacturing.dm b/code/modules/manufactorio/_manufacturing.dm
new file mode 100644
index 00000000000..236c05dd869
--- /dev/null
+++ b/code/modules/manufactorio/_manufacturing.dm
@@ -0,0 +1,123 @@
+#define MANUFACTURING_FAIL_FULL -1
+#define MANUFACTURING_FAIL 0
+#define MANUFACTURING_SUCCESS 1
+
+#define POCKET_INPUT "Input"
+#define POCKET_OUTPUT "Output"
+
+#define MANUFACTURING_TURF_LAG_LIMIT 10 // max items on a turf before we consider it full
+
+/obj/machinery/power/manufacturing
+ icon = 'icons/obj/machines/manufactorio.dmi'
+ name = "base manufacture receiving type"
+ desc = "this shouldnt exist"
+ density = TRUE
+ /// Do we add the simple_rotation component and a text that we are powered by cable? Also allows unwrenching
+ var/may_be_moved = TRUE
+ /// Allow taking in mobs from conveyors?
+ var/allow_mob_bump_intake = FALSE
+
+/obj/machinery/power/manufacturing/Initialize(mapload)
+ . = ..()
+ if(may_be_moved)
+ AddComponent(/datum/component/simple_rotation)
+ if(anchored)
+ connect_to_network()
+
+/obj/machinery/power/manufacturing/examine(mob/user)
+ . = ..()
+ if(may_be_moved)
+ . += "It receives power via cable, but certain buildings do not need power."
+ . += length(contents - circuit) ? "It contains:" : "Its empty."
+ for(var/atom/movable/thing as anything in contents - circuit)
+ var/text = thing.name
+ var/obj/item/stack/possible_stack = thing
+ if(istype(possible_stack))
+ text = "[possible_stack.amount] [text]"
+ . += text
+
+
+/obj/machinery/power/manufacturing/Bumped(atom/movable/bumped_atom) //attempt to put in whatever is pushed into us via conveyor
+ . = ..()
+ if((!allow_mob_bump_intake && ismob(bumped_atom)) || !anchored) //only uncomment if youre brave
+ return
+ var/conveyor = locate(/obj/machinery/conveyor) in bumped_atom.loc
+ if(isnull(conveyor))
+ return
+ receive_resource(bumped_atom, bumped_atom.loc, get_dir(src, bumped_atom))
+
+/obj/machinery/power/manufacturing/wrench_act(mob/living/user, obj/item/tool)
+ . = ..()
+ if(!may_be_moved)
+ return
+ default_unfasten_wrench(user, tool)
+ if(anchored)
+ connect_to_network()
+ else
+ disconnect_from_network()
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/power/manufacturing/screwdriver_act(mob/living/user, obj/item/tool)
+ if(default_deconstruction_screwdriver(user, icon_state, icon_state, tool))
+ return ITEM_INTERACT_SUCCESS
+ return ITEM_INTERACT_BLOCKING
+
+/obj/machinery/power/manufacturing/crowbar_act(mob/living/user, obj/item/tool)
+ . = ITEM_INTERACT_BLOCKING
+ if(default_deconstruction_crowbar(tool))
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/power/manufacturing/proc/generate_io_overlays(direction, color, offsets_override)
+ var/list/dir_offset
+ if(islist(offsets_override))
+ dir_offset = offsets_override
+ else
+ dir_offset = dir2offset(direction)
+ dir_offset[1] *= 32
+ dir_offset[2] *= 32
+ var/image/nonemissive = image(icon='icons/obj/doors/airlocks/station/overlays.dmi', icon_state="unres_[direction]")
+ nonemissive.pixel_x = dir_offset[1]
+ nonemissive.pixel_y = dir_offset[2]
+ nonemissive.color = color
+ var/mutable_appearance/emissive = emissive_appearance(nonemissive.icon, nonemissive.icon_state, offset_spokesman = src, alpha = nonemissive.alpha)
+ emissive.pixel_y = nonemissive.pixel_y
+ emissive.pixel_x = nonemissive.pixel_x
+ return list(nonemissive, emissive)
+
+/// Returns whatever object it may output, or null if it cant do that
+/obj/machinery/power/manufacturing/proc/request_resource()
+
+
+/obj/machinery/power/manufacturing/proc/receive_resource(atom/movable/receiving, atom/from, receive_dir)
+ CRASH("Unimplemented!") //check can_receive_resource here
+
+//use dir please
+/obj/machinery/power/manufacturing/proc/send_resource(atom/movable/sending, atom/what_or_dir)
+ if(isobj(what_or_dir))
+ var/obj/machinery/power/manufacturing/target = what_or_dir
+ return target.receive_resource(sending, src, get_step(src, what_or_dir))
+ var/turf/next_turf = isturf(what_or_dir) ? what_or_dir : get_step(src, what_or_dir)
+ var/obj/machinery/power/manufacturing/manufactury = locate(/obj/machinery/power/manufacturing) in next_turf
+ if(!isnull(manufactury))
+ if(!manufactury.anchored)
+ return MANUFACTURING_FAIL
+ return manufactury.receive_resource(sending, src, isturf(what_or_dir) ? get_dir(src, what_or_dir) : what_or_dir)
+ if(next_turf.is_blocked_turf(exclude_mobs = TRUE, source_atom = sending))
+ return MANUFACTURING_FAIL
+ if(length(next_turf.contents) >= MANUFACTURING_TURF_LAG_LIMIT)
+ return MANUFACTURING_FAIL_FULL
+ if(isnull(sending))
+ return MANUFACTURING_SUCCESS // for the sake of being used as a check
+ if(isnull(sending.loc) || !sending.Move(next_turf, get_dir(src, next_turf)))
+ sending.forceMove(next_turf)
+ return MANUFACTURING_SUCCESS
+
+/// Checks if this stack (if not a stack does not do anything) can merge WITHOUT creating two stacks in contents
+/obj/machinery/power/manufacturing/proc/may_merge_in_contents(obj/item/stack/stack)
+ if(!istype(stack))
+ return
+ for(var/obj/item/stack/other in contents - circuit)
+ if(!other.can_merge(stack))
+ continue
+ if(other.amount + stack.amount <= other.max_amount)
+ return other
diff --git a/code/modules/manufactorio/machines/crafter.dm b/code/modules/manufactorio/machines/crafter.dm
new file mode 100644
index 00000000000..ee794d29301
--- /dev/null
+++ b/code/modules/manufactorio/machines/crafter.dm
@@ -0,0 +1,139 @@
+/obj/machinery/power/manufacturing/crafter
+ name = "manufacturing assembling machine"
+ desc = "Assembles (crafts) the set recipe until it runs out of resources. Inputs irrelevant to the recipe are ignored."
+ icon_state = "crafter"
+ circuit = /obj/item/circuitboard/machine/manucrafter
+ /// power used per process() spent crafting
+ var/power_cost = 5 KILO WATTS
+ /// our output, if the way out was blocked is held here
+ var/atom/movable/withheld
+ /// current recipe
+ var/datum/crafting_recipe/recipe
+ /// crafting component
+ var/datum/component/personal_crafting/machine/craftsman
+ /// current timer for our crafting
+ var/craft_timer
+ /// do we use cooking recipes instead
+ var/cooking = FALSE
+
+/obj/machinery/power/manufacturing/crafter/Initialize(mapload)
+ . = ..()
+ craftsman = AddComponent(/datum/component/personal_crafting/machine)
+
+/obj/machinery/power/manufacturing/crafter/examine(mob/user)
+ . = ..()
+ . += span_notice("It is currently manufacturing [isnull(recipe) ? "nothing. Use a multitool to set it" : recipe.name].")
+ if(isnull(recipe))
+ return
+ . += span_notice("It needs:")
+ for(var/valid_type in recipe.reqs)
+ // Check if they're datums, specifically reagents.
+ var/datum/reagent/reagent_ingredient = valid_type
+ if(istype(reagent_ingredient))
+ var/amount = recipe.reqs[reagent_ingredient]
+ . += "[amount] unit[amount > 1 ? "s" : ""] of [initial(reagent_ingredient.name)]"
+
+ var/atom/ingredient = valid_type
+ var/amount = recipe.reqs[ingredient]
+
+ . += "[amount > 1 ? ("[amount]" + " of") : "a"] [initial(ingredient.name)]"
+
+/obj/machinery/power/manufacturing/crafter/update_overlays()
+ . = ..()
+ . += generate_io_overlays(dir, COLOR_ORANGE)
+ for(var/target_dir in GLOB.cardinals - dir)
+ . += generate_io_overlays(target_dir, COLOR_MODERATE_BLUE)
+
+/obj/machinery/power/manufacturing/crafter/proc/valid_for_recipe(obj/item/checking)
+ . = FALSE
+ for(var/requirement_path in recipe.reqs)
+ if(!ispath(checking.type, requirement_path) || recipe.blacklist.Find(checking.type))
+ continue
+ return TRUE
+
+/obj/machinery/power/manufacturing/crafter/proc/contains_type(path)
+ . = FALSE
+ for(var/content in contents - circuit)
+ if(!istype(content, path))
+ continue
+ return TRUE
+
+/obj/machinery/power/manufacturing/crafter/receive_resource(obj/receiving, atom/from, receive_dir)
+ if(isnull(recipe) || !isitem(receiving) || surplus() < power_cost)
+ return MANUFACTURING_FAIL
+ if(receive_dir == dir || !valid_for_recipe(receiving))
+ return MANUFACTURING_FAIL
+ if(!may_merge_in_contents(receiving) && contains_type(receiving.type))
+ return MANUFACTURING_FAIL_FULL
+ receiving.Move(src, get_dir(receiving, src))
+ START_PROCESSING(SSmanufacturing, src)
+ return MANUFACTURING_SUCCESS
+
+/obj/machinery/power/manufacturing/crafter/multitool_act(mob/living/user, obj/item/tool)
+ . = NONE
+ var/list/unavailable = list()
+ for(var/datum/crafting_recipe/potential_recipe as anything in cooking ? GLOB.cooking_recipes : GLOB.crafting_recipes)
+ if(craftsman.is_recipe_available(potential_recipe, user))
+ continue
+ var/obj/result = initial(potential_recipe.result)
+ if(istype(result) && initial(result.anchored))
+ continue
+ unavailable += potential_recipe
+ var/result = tgui_input_list(usr, "Recipe", "Select Recipe", (cooking ? GLOB.cooking_recipes : GLOB.crafting_recipes) - unavailable)
+ if(isnull(result) || result == recipe || !user.can_perform_action(src))
+ return ITEM_INTERACT_FAILURE
+ var/dump_target = get_step(src, get_dir(src, user))
+ for(var/atom/movable/thing as anything in contents - circuit)
+ thing.Move(dump_target)
+ recipe = result
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/power/manufacturing/crafter/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone == withheld)
+ withheld = null
+
+/obj/machinery/power/manufacturing/crafter/atom_destruction(damage_flag)
+ . = ..()
+ withheld?.Move(drop_location(src))
+
+/obj/machinery/power/manufacturing/crafter/Destroy()
+ . = ..()
+ recipe = null
+ craftsman = null
+ QDEL_NULL(withheld)
+
+/obj/machinery/power/manufacturing/crafter/process(seconds_per_tick)
+ if(!isnull(withheld) && !send_resource(withheld, dir))
+ return
+ if(!isnull(craft_timer))
+ if(surplus() >= power_cost)
+ add_load()
+ else
+ deltimer(craft_timer)
+ craft_timer = null
+ say("Power failure!")
+ return
+ if(isnull(recipe) || !craftsman.check_contents(src, recipe, craftsman.get_surroundings(src)))
+ return
+ flick_overlay_view(mutable_appearance(icon, "crafter_printing"), recipe.time)
+ craft_timer = addtimer(CALLBACK(src, PROC_REF(craft), recipe), recipe.time, TIMER_STOPPABLE)
+
+/obj/machinery/power/manufacturing/crafter/proc/craft(datum/crafting_recipe/recipe)
+ if(QDELETED(src))
+ return
+ craft_timer = null
+ var/atom/movable/result = craftsman.construct_item(src, recipe)
+ if(istype(result))
+ if(isitem(result))
+ result.pixel_x += rand(-4, 4)
+ result.pixel_y += rand(-4, 4)
+ result.Move(src)
+ send_resource(result, dir)
+ else
+ say(result)
+
+/obj/machinery/power/manufacturing/crafter/cooker
+ name = "manufacturing cooking machine" // maybe this shouldnt be available dont wanna make chef useless, though otherwise it would need a sprite
+ desc = "Cooks the set recipe until it runs out of resources. Inputs irrelevant to the recipe are ignored."
+ cooking = TRUE
diff --git a/code/modules/manufactorio/machines/crusher.dm b/code/modules/manufactorio/machines/crusher.dm
new file mode 100644
index 00000000000..f0f18c10ae8
--- /dev/null
+++ b/code/modules/manufactorio/machines/crusher.dm
@@ -0,0 +1,83 @@
+/obj/machinery/power/manufacturing/crusher //todo make it work for other stuff
+ name = "manufacturing crusher"
+ desc = "Crushes any item put into it, boulders and such. Materials below a sheet are stored in the machine."
+ icon_state = "crusher"
+ circuit = /obj/item/circuitboard/machine/manucrusher
+ /// power used to crush
+ var/crush_cost = 3 KILO WATTS
+ /// how much can we hold
+ var/capacity = 5
+ /// withheld output because output is either blocked or full
+ var/atom/movable/withholding
+ /// list of held mats
+ var/list/obj/item/stack/held_mats = list()
+
+/obj/machinery/power/manufacturing/crusher/update_overlays()
+ . = ..()
+ . += generate_io_overlays(dir, COLOR_ORANGE) // OUT - stuff in it
+ . += generate_io_overlays(REVERSE_DIR(dir), COLOR_MODERATE_BLUE) // IN - to crush
+
+/obj/machinery/power/manufacturing/crusher/Destroy()
+ . = ..()
+ QDEL_NULL(withholding)
+
+/obj/machinery/power/manufacturing/crusher/atom_destruction(damage_flag)
+ withholding?.Move(drop_location())
+ return ..()
+
+/obj/machinery/power/manufacturing/crusher/receive_resource(obj/receiving, atom/from, receive_dir)
+ if(istype(receiving, /obj/item/stack/ore) || receiving.resistance_flags & INDESTRUCTIBLE || !isitem(receiving) || surplus() < crush_cost || receive_dir != REVERSE_DIR(dir))
+ return MANUFACTURING_FAIL
+ if(!may_merge_in_contents(receiving) && length(contents - circuit) >= capacity)
+ return MANUFACTURING_FAIL_FULL
+ receiving.Move(src, get_dir(receiving, src))
+ START_PROCESSING(SSmanufacturing, src)
+ return MANUFACTURING_SUCCESS
+
+/obj/machinery/power/manufacturing/crusher/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone == withholding)
+ withholding = null
+
+/obj/machinery/power/manufacturing/crusher/process(seconds_per_tick) //noot functional
+ if(!isnull(withholding) && !send_resource(withholding, dir))
+ return
+ for(var/material in held_mats)
+ if(held_mats[material] >= 1)
+ var/new_amount = floor(held_mats[material])
+ held_mats[material] -= new_amount
+ if(held_mats[material] <= 0)
+ held_mats -= material
+ withholding = new material(null, new_amount)
+ return
+ var/list/poor_saps = contents - circuit
+ if(!length(poor_saps))
+ return PROCESS_KILL
+ if(surplus() < crush_cost)
+ return
+ var/obj/victim = poor_saps[length(poor_saps)]
+ if(istype(victim)) //todo handling for other things
+ if(!length(victim.custom_materials))
+ add_load(crush_cost)
+ victim.atom_destruction()
+ for(var/obj/object in victim.contents+victim)
+ for(var/datum/material/possible_mat as anything in object.custom_materials)
+ var/quantity = object.custom_materials[possible_mat]
+ object.set_custom_materials(object.custom_materials.Copy() - possible_mat, 1)
+ var/type_to_use = istype(victim, /obj/item/boulder) ? possible_mat.ore_type : possible_mat.sheet_type
+ if(quantity < SHEET_MATERIAL_AMOUNT)
+ if(!(type_to_use in held_mats))
+ held_mats[type_to_use] = quantity / SHEET_MATERIAL_AMOUNT
+ continue
+ held_mats[type_to_use] += quantity / SHEET_MATERIAL_AMOUNT
+ continue
+ var/obj/item/stack/sheet/new_item = new type_to_use(src, quantity / SHEET_MATERIAL_AMOUNT)
+ if(!send_resource(new_item, dir))
+ withholding = new_item
+ return
+ else if(isliving(victim))
+ var/mob/living/poor_sap = victim
+ poor_sap.adjustBruteLoss(95, TRUE)
+ if(!send_resource(poor_sap, dir))
+ withholding = poor_sap
+ return
diff --git a/code/modules/manufactorio/machines/debug.dm b/code/modules/manufactorio/machines/debug.dm
new file mode 100644
index 00000000000..7c21cf4e989
--- /dev/null
+++ b/code/modules/manufactorio/machines/debug.dm
@@ -0,0 +1,18 @@
+/obj/loop_spawner
+ name = "testing loop spawner"
+ icon = 'icons/obj/machines/mining_machines.dmi'
+ icon_state = "unloader"
+ anchored = TRUE
+ color = COLOR_PURPLE
+ /// directions we can output to right now
+ var/to_spawn = /obj/item/screwdriver
+ /// the subsystem to process us
+ var/subsystem_to_process_us = /datum/controller/subsystem/processing/obj
+
+/obj/loop_spawner/Initialize(mapload)
+ . = ..()
+ var/datum/controller/subsystem/processing/subsystem = locate(subsystem_to_process_us) in Master.subsystems
+ START_PROCESSING(subsystem, src)
+
+/obj/loop_spawner/process(seconds_per_tick)
+ new to_spawn(get_step(src, dir))
diff --git a/code/modules/manufactorio/machines/lathe.dm b/code/modules/manufactorio/machines/lathe.dm
new file mode 100644
index 00000000000..2669e851b93
--- /dev/null
+++ b/code/modules/manufactorio/machines/lathe.dm
@@ -0,0 +1,145 @@
+/obj/machinery/power/manufacturing/lathe // this is a heavily gutted autolathe
+ name = "manufacturing lathe"
+ desc = "Lathes the set recipe until it runs out of resources. Only accepts sheets or other kinds of material stacks."
+ icon_state = "lathe"
+ circuit = /obj/item/circuitboard/machine/manulathe
+ /// power cost for lathing
+ var/power_cost = 5 KILO WATTS
+ /// design id we print
+ var/design_id
+ ///The container to hold materials
+ var/datum/component/material_container/materials
+ //looping sound for printing items
+ var/datum/looping_sound/lathe_print/print_sound
+ ///Designs related to the autolathe
+ var/datum/techweb/autounlocking/stored_research
+ /// timer id of printing
+ var/busy = FALSE
+ /// our output, if the way out was blocked is held here
+ var/atom/movable/withheld
+
+/obj/machinery/power/manufacturing/lathe/Initialize(mapload)
+ . = ..()
+ print_sound = new(src, FALSE)
+ materials = AddComponent( \
+ /datum/component/material_container, \
+ SSmaterials.materials_by_category[MAT_CATEGORY_ITEM_MATERIAL], \
+ SHEET_MATERIAL_AMOUNT * MAX_STACK_SIZE * 2, \
+ MATCONTAINER_EXAMINE|MATCONTAINER_NO_INSERT, \
+ )
+ if(!GLOB.autounlock_techwebs[/datum/techweb/autounlocking/autolathe])
+ GLOB.autounlock_techwebs[/datum/techweb/autounlocking/autolathe] = new /datum/techweb/autounlocking/autolathe
+ stored_research = GLOB.autounlock_techwebs[/datum/techweb/autounlocking/autolathe]
+
+/obj/machinery/power/manufacturing/lathe/examine(mob/user)
+ . = ..()
+ var/datum/design/design
+ if(!isnull(design_id))
+ design = SSresearch.techweb_design_by_id(design_id)
+ . += span_notice("It is set to print [!isnull(design) ? design.name : "nothing, set with a multitool"].")
+ if(isnull(design))
+ return
+ . += span_notice("It needs:")
+ for(var/valid_type in design.materials)
+ var/atom/ingredient = valid_type
+ var/amount = design.materials[ingredient] / SHEET_MATERIAL_AMOUNT
+
+ . += "[amount] sheets of [initial(ingredient.name)]"
+
+/obj/machinery/power/manufacturing/lathe/update_overlays()
+ . = ..()
+ . += generate_io_overlays(dir, COLOR_ORANGE) // OUT - stuff in it
+ . += generate_io_overlays(REVERSE_DIR(dir), COLOR_MODERATE_BLUE) // IN - to crush
+
+/obj/machinery/power/manufacturing/lathe/Destroy()
+ . = ..()
+ stored_research = null
+ QDEL_NULL(print_sound)
+ materials = null
+ QDEL_NULL(withheld)
+
+/obj/machinery/power/manufacturing/lathe/atom_destruction(damage_flag)
+ withheld?.Move(drop_location())
+ return ..()
+
+/obj/machinery/power/manufacturing/lathe/receive_resource(atom/movable/receiving, atom/from, receive_dir)
+ if(!isstack(receiving) || receiving.resistance_flags & INDESTRUCTIBLE || receive_dir != REVERSE_DIR(dir))
+ return MANUFACTURING_FAIL
+ materials.insert_item(receiving)
+ return MANUFACTURING_SUCCESS
+
+/obj/machinery/power/manufacturing/lathe/multitool_act(mob/living/user, obj/item/tool)
+ . = ..()
+ var/list/name_to_id = list()
+ for(var/id in stored_research.researched_designs)
+ var/datum/design/design = SSresearch.techweb_design_by_id(id)
+ name_to_id[design.name] = id
+ var/result = tgui_input_list(user, "Select Design", "Select Design", sort_list(name_to_id))
+ if(isnull(result))
+ return ITEM_INTERACT_FAILURE
+ design_id = name_to_id[result]
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/power/manufacturing/lathe/process()
+ if(!isnull(withheld) && !send_resource(withheld, dir))
+ return
+
+ var/datum/design/design = SSresearch.techweb_design_by_id(design_id)
+ if(isnull(design) || !(design.build_type & AUTOLATHE))
+ return
+ if(surplus() < power_cost)
+ finalize_build()
+ return
+ //check for materials required. For custom material items decode their required materials
+ var/list/materials_needed = list()
+ for(var/material in design.materials)
+ var/amount_needed = design.materials[material]
+ if(istext(material)) // category
+ for(var/datum/material/valid_candidate as anything in SSmaterials.materials_by_category[material])
+ if(materials.get_material_amount(valid_candidate) < amount_needed)
+ continue
+ material = valid_candidate
+ break
+ if(isnull(material))
+ return
+ materials_needed[material] = amount_needed
+
+ if(!materials.has_materials(materials_needed))
+ return
+
+ var/craft_time = (design.construction_time * design.lathe_time_factor) ** 0.8
+ flick_overlay_view(mutable_appearance(icon, "crafter_printing"), craft_time)
+ print_sound.start()
+ add_load(power_cost)
+ busy = addtimer(CALLBACK(src, PROC_REF(do_make_item), design, materials_needed), craft_time, TIMER_UNIQUE | TIMER_STOPPABLE | TIMER_DELETE_ME)
+
+/obj/machinery/power/manufacturing/lathe/proc/do_make_item(datum/design/design, list/materials_needed)
+ finalize_build()
+ if(surplus() < power_cost)
+ return
+
+ var/is_stack = ispath(design.build_path, /obj/item/stack)
+ if(!materials.has_materials(materials_needed))
+ return
+ materials.use_materials(materials_needed)
+
+ var/atom/movable/created
+ if(is_stack)
+ var/obj/item/stack/stack_item = initial(design.build_path)
+ created = new stack_item(null, 1)
+ else
+ created = new design.build_path(null)
+ split_materials_uniformly(materials_needed, target_object = created)
+ if(isitem(created))
+ created.pixel_x = created.base_pixel_x + rand(-6, 6)
+ created.pixel_y = created.base_pixel_y + rand(-6, 6)
+ SSblackbox.record_feedback("nested tally", "lathe_printed_items", 1, list("[type]", "[created.type]"))
+
+ if(!send_resource(created, dir))
+ withheld = created
+
+
+/obj/machinery/power/manufacturing/lathe/proc/finalize_build()
+ print_sound.stop()
+ deltimer(busy)
+ busy = null
diff --git a/code/modules/manufactorio/machines/router.dm b/code/modules/manufactorio/machines/router.dm
new file mode 100644
index 00000000000..7c57a930bd3
--- /dev/null
+++ b/code/modules/manufactorio/machines/router.dm
@@ -0,0 +1,66 @@
+/obj/machinery/power/manufacturing/router // Basically a splitter
+ name = "manufacturing router"
+ desc = "Distributes input to 3 output directions equally. Stacks are split, and you may toggle outputs with a multitool. May not receive from other routers."
+ allow_mob_bump_intake = TRUE
+ icon_state = "splitter"
+ circuit = /obj/item/circuitboard/machine/manurouter
+ /// outputs disabled with a multitool
+ var/list/disabled_dirs = list()
+ /// directions we can output to right now
+ var/list/directions
+
+/obj/machinery/power/manufacturing/router/Initialize(mapload)
+ . = ..()
+ directions = GLOB.cardinals.Copy()
+
+/obj/machinery/power/manufacturing/router/multitool_act(mob/living/user, obj/item/tool)
+ . = ..()
+ var/to_toggle = get_dir(src, user)
+ if(!(to_toggle in GLOB.cardinals))
+ balloon_alert(user, "stand inline!")
+ return ITEM_INTERACT_FAILURE
+ if(to_toggle in disabled_dirs)
+ disabled_dirs -= to_toggle
+ else
+ disabled_dirs += to_toggle
+ update_appearance(UPDATE_OVERLAYS)
+ balloon_alert(user, "toggled output")
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/power/manufacturing/router/update_overlays()
+ . = ..()
+ for(var/direction in GLOB.cardinals)
+ var/variant
+ if(disabled_dirs.Find(direction))
+ variant = "bl"
+ else
+ variant = (direction == dir) ? "in" : "out"
+ var/image/new_overlay = image(icon, "splitter_[variant]", layer = layer+0.001, dir = direction)
+ . += new_overlay
+
+/obj/machinery/power/manufacturing/router/receive_resource(obj/receiving, atom/from, receive_dir)
+ if(istype(from, /obj/machinery/power/manufacturing/router))
+ return MANUFACTURING_FAIL
+ var/list/filtered = directions - receive_dir - disabled_dirs
+ if(!length(filtered))
+ directions = GLOB.cardinals.Copy()
+ for(var/target in filtered)
+ directions -= target
+ if(isstack(receiving))
+ receiving = handle_stack(receiving, receive_dir)
+ if(send_resource(receiving, target))
+ dir = receive_dir
+ update_appearance(UPDATE_OVERLAYS) // im sorry
+ return MANUFACTURING_SUCCESS
+ return MANUFACTURING_FAIL_FULL
+
+/obj/machinery/power/manufacturing/router/proc/handle_stack(obj/item/stack/stack, direction)
+ . = stack
+ var/potential_output_count = length(GLOB.cardinals - direction - disabled_dirs)
+ if(potential_output_count <= 1)
+ return
+ var/split_amount = round(stack.amount / potential_output_count, 1)
+ if(stack.amount == potential_output_count)
+ return
+ var/atom/movable/new_stack = stack.split_stack(amount = min(stack.amount, split_amount))
+ return new_stack
diff --git a/code/modules/manufactorio/machines/smelter.dm b/code/modules/manufactorio/machines/smelter.dm
new file mode 100644
index 00000000000..1a7beca66f4
--- /dev/null
+++ b/code/modules/manufactorio/machines/smelter.dm
@@ -0,0 +1,59 @@
+/obj/machinery/power/manufacturing/smelter
+ name = "manufacturing smelter"
+ desc = "Pretty much incinerates whatever is put into it. Refines ore (not boulders)."
+ icon_state = "smelter"
+ circuit = /obj/item/circuitboard/machine/manusmelter
+ /// power used to smelt
+ var/power_cost = 4 KILO WATTS
+ /// our output, if the way out was blocked is held here
+ var/atom/movable/withheld
+
+/obj/machinery/power/manufacturing/smelter/update_overlays()
+ . = ..()
+ . += generate_io_overlays(dir, COLOR_ORANGE) // OUT - stuff in it
+ . += generate_io_overlays(REVERSE_DIR(dir), COLOR_MODERATE_BLUE) // IN - to crush
+
+/obj/machinery/power/manufacturing/smelter/receive_resource(obj/receiving, atom/from, receive_dir)
+ if(!isitem(receiving) || surplus() < power_cost || receive_dir != REVERSE_DIR(dir))
+ return MANUFACTURING_FAIL
+ var/list/stacks = contents - circuit
+ if(!may_merge_in_contents(receiving) && length(stacks) >= 5)
+ return MANUFACTURING_FAIL_FULL
+ receiving.Move(src, get_dir(receiving, src))
+ START_PROCESSING(SSmanufacturing, src)
+ return MANUFACTURING_SUCCESS
+
+/obj/machinery/power/manufacturing/smelter/Destroy()
+ . = ..()
+ QDEL_NULL(withheld)
+
+/obj/machinery/power/manufacturing/smelter/atom_destruction(damage_flag)
+ withheld?.Move(drop_location())
+ return ..()
+
+/obj/machinery/power/manufacturing/smelter/process(seconds_per_tick)
+ var/list/stacks = contents - circuit
+ if(!length(stacks))
+ return
+
+ var/list/stacks_preprocess = contents - circuit
+ var/obj/item/stack/ore/ore = stacks_preprocess[length(stacks_preprocess)]
+ if(isnull(ore))
+ return
+ if(isnull(withheld) && surplus() >= power_cost)
+ icon_state="smelter_on"
+ add_load(power_cost)
+ if(istype(ore))
+ var/obj/item/stack/new_stack = new ore.refined_type(null, min(5, ore.amount), FALSE)
+ new_stack.moveToNullspace()
+ ore.use(min(5, ore.amount))
+ ore = new_stack
+ else
+ ore.fire_act(1400)
+ withheld = ore
+ else if(surplus() < power_cost)
+ icon_state = "smelter"
+ if(send_resource(withheld, dir))
+ withheld = null // nullspace thumbs down
+ if(!length(contents - circuit))
+ return PROCESS_KILL //we finished
diff --git a/code/modules/manufactorio/machines/sorter.dm b/code/modules/manufactorio/machines/sorter.dm
new file mode 100644
index 00000000000..7d06f74e6fa
--- /dev/null
+++ b/code/modules/manufactorio/machines/sorter.dm
@@ -0,0 +1,149 @@
+/obj/machinery/power/manufacturing/sorter
+ icon_state = "router"
+ name = "conveyor sort-router"
+ desc = "Pushes things on it to its sides following set criteria, set via multitool."
+ layer = BELOW_OPEN_DOOR_LAYER
+ density = FALSE
+ interaction_flags_atom = INTERACT_ATOM_ATTACK_HAND
+ circuit = /obj/item/circuitboard/machine/manurouter
+ /// for mappers; filter path = list(direction, value), otherwise a list of initialized filters
+ var/list/sort_filters = list()
+ /// dir to push to if there is no criteria
+ var/dir_if_not_met
+ /// timer id of the thing that makes stuff move
+ var/delay_timerid
+ /// max filters
+ var/max_filters = 10
+
+/obj/machinery/power/manufacturing/sorter/Initialize(mapload)
+ . = ..()
+ if(isnull(dir_if_not_met))
+ dir_if_not_met = dir
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = PROC_REF(on_entered),
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+ for(var/i in 1 to length(sort_filters))
+ var/creating_type = sort_filters[i]
+ var/list/values = sort_filters[creating_type]
+ var/datum/sortrouter_filter/new_type = new creating_type(src)
+ new_type.dir_target = values[1]
+ new_type.value = values[2]
+ sort_filters[i] = new_type
+ START_PROCESSING(SSobj, src)
+
+/obj/machinery/power/manufacturing/sorter/Destroy()
+ . = ..()
+ QDEL_LIST(sort_filters)
+
+/obj/machinery/power/manufacturing/sorter/multitool_act(mob/living/user, obj/item/tool)
+ . = ..()
+ ui_interact(user)
+
+/obj/machinery/power/manufacturing/sorter/receive_resource(atom/movable/receiving, atom/from, receive_dir)
+ if(length(loc.contents) >= MANUFACTURING_TURF_LAG_LIMIT)
+ return MANUFACTURING_FAIL_FULL
+ receiving.Move(loc)
+ return MANUFACTURING_SUCCESS
+
+
+/obj/machinery/power/manufacturing/sorter/ui_data(mob/user)
+ . = list()
+ .["unmet_dir"] = dir_if_not_met
+ .["filters"] = list()
+ for(var/datum/sortrouter_filter/sorting as anything in sort_filters)
+ .["filters"] += list(list(
+ "name" = sorting.return_name(),
+ "ref" = REF(sorting),
+ "inverted" = sorting.inverted,
+ "dir" = sorting.dir_target,
+ ))
+
+/obj/machinery/power/manufacturing/sorter/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("del_filter")
+ var/datum/sortrouter_filter/filter = locate(params["ref"])
+ if(isnull(filter))
+ return
+ sort_filters -= filter
+ qdel(filter)
+ return TRUE
+ if("new_filter")
+ if(length(sort_filters) >= max_filters)
+ return
+ var/static/list/filter_by_name
+ if(!length(filter_by_name))
+ filter_by_name = list()
+ for(var/datum/sortrouter_filter/to_do as anything in subtypesof(/datum/sortrouter_filter))
+ filter_by_name[initial(to_do.name)] = to_do
+ filter_by_name = sort_list(filter_by_name)
+ var/target_type = tgui_input_list(usr, "Select a filter", "New Filter", filter_by_name)
+ if(isnull(target_type)|| !usr.can_perform_action(src, ALLOW_SILICON_REACH))
+ return
+ target_type = filter_by_name[target_type]
+ sort_filters += new target_type(src)
+ return TRUE
+ if("rotate")
+ var/datum/sortrouter_filter/filter = locate(params["ref"])
+ if(isnull(filter))
+ return
+ var/next_ind = GLOB.cardinals.Find(filter.dir_target) + 1
+ filter.dir_target = GLOB.cardinals[WRAP(next_ind, 1, 5)]
+ return TRUE
+ if("rotate_unmet")
+ var/next_ind = GLOB.cardinals.Find(dir_if_not_met) + 1
+ dir_if_not_met = GLOB.cardinals[WRAP(next_ind, 1, 5)]
+ return TRUE
+ if("edit")
+ var/datum/sortrouter_filter/filter = locate(params["ref"])
+ if(isnull(filter))
+ return
+ filter.edit(usr)
+ return TRUE
+ if("shift")
+ var/datum/sortrouter_filter/filter = locate(params["ref"])
+ if(isnull(filter))
+ return
+ var/next_ind = WRAP(sort_filters.Find(filter) + text2num(params["amount"]), 1, length(sort_filters)+1)
+ sort_filters -= filter
+ sort_filters.Insert(next_ind, filter)
+ return TRUE
+
+/obj/machinery/power/manufacturing/sorter/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "ManufacturingSorter")
+ ui.open()
+
+/obj/machinery/power/manufacturing/sorter/proc/send_nomobs(atom/movable/moving, dir)
+ var/mutable_appearance/operate = mutable_appearance(icon, "router_operate")
+ operate.dir = dir
+ flick_overlay_view(operate, 1 SECONDS)
+ return ismob(moving) ? moving.Move(get_step(src,dir), dir) : send_resource(moving, dir)
+
+/obj/machinery/power/manufacturing/sorter/process()
+ if(delay_timerid || !length(loc?.contents - 1))
+ return
+ launch_everything()
+
+/obj/machinery/power/manufacturing/sorter/proc/on_entered(datum/source, atom/movable/mover)
+ SIGNAL_HANDLER
+ if(mover == src || !istype(mover) || mover.anchored || delay_timerid)
+ return
+ delay_timerid = addtimer(CALLBACK(src, PROC_REF(launch_everything)), 0.2 SECONDS)
+
+/obj/machinery/power/manufacturing/sorter/proc/launch_everything()
+ delay_timerid = null
+ var/turf/where_we_at = get_turf(src)
+ for(var/atom/movable/mover as anything in where_we_at.contents)
+ if(mover.anchored)
+ continue
+ for(var/datum/sortrouter_filter/sorting as anything in sort_filters)
+ if(sorting.meets_conditions(mover) == sorting.inverted)
+ continue
+ send_nomobs(mover, sorting.dir_target)
+ return
+ send_nomobs(mover, dir_if_not_met)
diff --git a/code/modules/manufactorio/machines/sorter_filters.dm b/code/modules/manufactorio/machines/sorter_filters.dm
new file mode 100644
index 00000000000..cb7e31cc41e
--- /dev/null
+++ b/code/modules/manufactorio/machines/sorter_filters.dm
@@ -0,0 +1,120 @@
+/datum/sortrouter_filter
+ /// name of the filter shown in UI
+ var/name
+ /// if it meets criteria, item is pushed to this direction
+ var/dir_target = NORTH
+ /// value of our filter, checked by us
+ var/value = ""
+ /// is our output inverted? checked by sorter
+ var/inverted = FALSE
+ /// the sorter we belong to
+ var/obj/machinery/power/manufacturing/sorter/sorter
+
+/datum/sortrouter_filter/New(sorter)
+ . = ..()
+ if(isnull(sorter))
+ return
+ src.sorter = sorter
+
+/datum/sortrouter_filter/Destroy()
+ . = ..()
+ if(isnull(sorter))
+ return
+ sorter = null
+
+/datum/sortrouter_filter
+
+/datum/sortrouter_filter/proc/return_name()
+ return name
+
+/datum/sortrouter_filter/proc/edit(mob/user)
+ to_chat(user, "This filter is not editable.")
+
+/datum/sortrouter_filter/proc/meets_conditions(atom/checking)
+
+/datum/sortrouter_filter/is_stack
+ name = "input is stack"
+
+/datum/sortrouter_filter/is_stack/meets_conditions(atom/checking)
+ return isstack(checking)
+
+/datum/sortrouter_filter/is_ore
+ name = "input is ore"
+
+/datum/sortrouter_filter/is_ore/meets_conditions(atom/checking)
+ return istype(checking, /obj/item/stack/ore)
+
+/datum/sortrouter_filter/is_mail
+ name = "input is mail"
+
+/datum/sortrouter_filter/is_mail/meets_conditions(atom/checking)
+ return istype(checking, /obj/item/mail)
+
+/datum/sortrouter_filter/is_tagged
+ name = "input is tagged X"
+
+/datum/sortrouter_filter/is_tagged/edit(mob/user)
+ var/target = tgui_input_list(user, "Select a tag", "Tag", sort_list(GLOB.TAGGERLOCATIONS))
+ if(isnull(target) || !user.can_perform_action(sorter, ALLOW_SILICON_REACH))
+ return
+ value = GLOB.TAGGERLOCATIONS.Find(target)
+
+/datum/sortrouter_filter/is_tagged/return_name()
+ return "input is tagged [value ? GLOB.TAGGERLOCATIONS[value] : ""]"
+
+/datum/sortrouter_filter/is_tagged/meets_conditions(checking)
+ var/obj/item/delivery/mail_or_delivery = checking
+ var/sort_tag
+ if(istype(checking, /obj/item/delivery) || istype(checking, /obj/item/mail))
+ sort_tag = mail_or_delivery.sort_tag
+
+ return value == sort_tag
+
+/datum/sortrouter_filter/name_contains
+ name = "input's name contains"
+
+/datum/sortrouter_filter/name_contains/edit(mob/user)
+ var/target = tgui_input_text(user, "What should it contain?", "Name", value, 12)
+ if(isnull(target)|| !user.can_perform_action(sorter, ALLOW_SILICON_REACH))
+ return
+ value = target
+
+/datum/sortrouter_filter/name_contains/return_name()
+ return "input's name contains [value]"
+
+/datum/sortrouter_filter/name_contains/meets_conditions(atom/checking)
+ return findtext(LOWER_TEXT(checking.name), value)
+
+/datum/sortrouter_filter/is_path_specific
+ name = "input is specific item"
+ /// are we currently listening for an item to set as our filter?
+ var/currently_listening = FALSE
+
+/datum/sortrouter_filter/is_path_specific/edit(mob/user)
+ name = initial(name)
+ if(!currently_listening)
+ name = "awaiting item"
+ to_chat(user, "Hit the sorter with the item of choice to set the filter.")
+ sorter.balloon_alert(user, "awaiting item!")
+ currently_listening = TRUE
+ RegisterSignal(sorter, COMSIG_ATOM_ATTACKBY, PROC_REF(sorter_hit))
+ else
+ currently_listening = FALSE
+ UnregisterSignal(sorter, COMSIG_ATOM_ATTACKBY)
+
+/datum/sortrouter_filter/is_path_specific/proc/sorter_hit(datum/source, obj/item/attacking_item, user, params)
+ currently_listening = FALSE
+ value = attacking_item.type
+ name = attacking_item.name
+ sorter.balloon_alert(user, "filter set")
+ UnregisterSignal(sorter, COMSIG_ATOM_ATTACKBY)
+ return COMPONENT_NO_AFTERATTACK
+
+/datum/sortrouter_filter/is_path_specific/meets_conditions(atom/checking)
+ return checking.type == value
+
+/datum/sortrouter_filter/is_path_specific/subtypes
+ name = "input is specific kind of item"
+
+/datum/sortrouter_filter/is_path_specific/subtypes/meets_conditions(atom/checking)
+ return istype(checking.type, value)
diff --git a/code/modules/manufactorio/machines/storagebox.dm b/code/modules/manufactorio/machines/storagebox.dm
new file mode 100644
index 00000000000..21957871cf8
--- /dev/null
+++ b/code/modules/manufactorio/machines/storagebox.dm
@@ -0,0 +1,46 @@
+/obj/machinery/power/manufacturing/storagebox
+ name = "manufacturing storage unit"
+ desc = "Its basically a box. Receives resources (if anchored). Needs a machine to take stuff out of without dumping everything out."
+ icon_state = "box"
+ /// how much can we hold
+ var/max_stuff = 16
+
+/obj/machinery/power/manufacturing/request_resource() //returns last inserted item
+ var/list/real_contents = contents - circuit
+ if(!length(real_contents))
+ return
+ return (real_contents)[length(real_contents)]
+
+/obj/machinery/power/manufacturing/storagebox/receive_resource(atom/movable/receiving, atom/from, receive_dir)
+ if(iscloset(receiving) && length(receiving.contents))
+ return MANUFACTURING_FAIL
+ if(!may_merge_in_contents(receiving) && length(contents - circuit) >= max_stuff)
+ return MANUFACTURING_FAIL_FULL
+ receiving.Move(src,receive_dir)
+ return MANUFACTURING_SUCCESS
+
+/obj/machinery/power/manufacturing/storagebox/container_resist_act(mob/living/user)
+ . = ..()
+ user.Move(drop_location())
+
+/obj/machinery/power/manufacturing/storagebox/screwdriver_act(mob/living/user, obj/item/tool)
+ . = NONE
+ balloon_alert(user, "disassembling...")
+ if(!do_after(user, 5 SECONDS, src))
+ return ITEM_INTERACT_FAILURE
+ atom_destruction()
+ return ITEM_INTERACT_SUCCESS
+
+/obj/machinery/power/manufacturing/storagebox/atom_destruction(damage_flag)
+ new /obj/item/stack/sheet/iron(drop_location(), 10)
+ dump_inventory_contents()
+ return ..()
+
+/obj/machinery/power/manufacturing/storagebox/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ if(user.combat_mode)
+ return
+ balloon_alert(user, "dumping..")
+ if(!do_after(user, 1.25 SECONDS, src))
+ return
+ dump_inventory_contents()
diff --git a/code/modules/manufactorio/machines/unloader.dm b/code/modules/manufactorio/machines/unloader.dm
new file mode 100644
index 00000000000..982c3358268
--- /dev/null
+++ b/code/modules/manufactorio/machines/unloader.dm
@@ -0,0 +1,78 @@
+/obj/machinery/power/manufacturing/unloader
+ name = "manufacturing crate unloader"
+ desc = "Unloads crates (and ore boxes) passed into it, ejecting the empty crate to the side and its contents forwards. Use a multitool to flip the crate output."
+ icon = 'icons/obj/machines/mining_machines.dmi'
+ icon_state = "unloader-corner"
+ circuit = /obj/item/circuitboard/machine/manuunloader
+ /// power used per attempt to unload a crate
+ var/power_to_unload_crate = 2 KILO WATTS
+ /// whether the side we output unloaded crates is flipped
+ var/flip_side = FALSE
+
+/obj/machinery/power/manufacturing/unloader/update_overlays()
+ . = ..()
+ . += generate_io_overlays(dir, COLOR_ORANGE) // OUT - stuff in it
+ . += generate_io_overlays(REVERSE_DIR(dir), COLOR_MODERATE_BLUE) // IN - crate
+ . += generate_io_overlays(turn(dir, flip_side ? 90 : -90), COLOR_ORANGE) // OUT -- empty crate
+
+/obj/machinery/power/manufacturing/unloader/request_resource() //returns held crate if someone wants to do that for some reason
+ var/list/real_contents = contents - circuit
+ if(!length(real_contents))
+ return
+ return (real_contents)[1]
+
+/obj/machinery/power/manufacturing/unloader/multitool_act(mob/living/user, obj/item/tool)
+ . = ..()
+ balloon_alert(user, "flipped")
+ flip_side = !flip_side
+ update_appearance()
+
+/obj/machinery/power/manufacturing/unloader/receive_resource(obj/receiving, atom/from, receive_dir)
+ if(surplus() < power_to_unload_crate || receive_dir != REVERSE_DIR(dir))
+ return MANUFACTURING_FAIL
+ var/list/real_contents = contents - circuit
+ if(length(real_contents))
+ return MANUFACTURING_FAIL_FULL
+
+ var/obj/structure/closet/as_closet = receiving
+ var/obj/structure/ore_box/as_orebox = receiving
+ if(istype(as_closet))
+ if(!as_closet.can_open())
+ return MANUFACTURING_FAIL
+ else if(!istype(as_orebox))
+ return MANUFACTURING_FAIL
+ receiving.Move(src, get_dir(receiving, src))
+ START_PROCESSING(SSfastprocess, src)
+ return MANUFACTURING_SUCCESS
+
+/obj/machinery/power/manufacturing/unloader/process(seconds_per_tick)
+ var/list/real_contents = contents - circuit
+ if(!length(real_contents))
+ return PROCESS_KILL
+ if(surplus() < power_to_unload_crate)
+ return
+ add_load(power_to_unload_crate)
+ var/obj/structure/closet/closet = real_contents[1]
+ if(istype(closet))
+ return unload_crate(closet)
+ else
+ return unload_orebox(closet)
+
+/obj/machinery/power/manufacturing/unloader/proc/unload_crate(obj/structure/closet/closet)
+ if (!closet.contents_initialized)
+ closet.contents_initialized = TRUE
+ closet.PopulateContents()
+ SEND_SIGNAL(closet, COMSIG_CLOSET_CONTENTS_INITIALIZED)
+ for(var/atom/thing as anything in closet.contents)
+ if(ismob(thing))
+ continue
+ send_resource(thing, dir)
+ if(!length(closet.contents) && send_resource(closet, turn(dir, flip_side ? 90 : -90)))
+ closet.open(force = TRUE)
+ return PROCESS_KILL
+
+/obj/machinery/power/manufacturing/unloader/proc/unload_orebox(obj/structure/ore_box/box)
+ for(var/atom/thing as anything in box.contents)
+ send_resource(thing, dir)
+ if(!length(box.contents) && send_resource(box, turn(dir, flip_side ? 90 : -90)))
+ return PROCESS_KILL
diff --git a/code/modules/recycling/conveyor.dm b/code/modules/recycling/conveyor.dm
index b4580a8bc23..44d9631a609 100644
--- a/code/modules/recycling/conveyor.dm
+++ b/code/modules/recycling/conveyor.dm
@@ -34,15 +34,18 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
var/flipped = FALSE
/// Are we currently conveying items?
var/conveying = FALSE
- //Direction -> if we have a conveyor belt in that direction
+ ///Direction -> if we have a conveyor belt in that direction
var/list/neighbors
+ /// are we operating in wire power mode
+ var/wire_mode = FALSE
+ /// weakref to attached cable if wire mode
+ var/datum/weakref/attached_wire_ref
/obj/machinery/conveyor/Initialize(mapload, new_dir, new_id)
. = ..()
AddElement(/datum/element/footstep_override, priority = STEP_SOUND_CONVEYOR_PRIORITY)
AddElement(/datum/element/give_turf_traits, string_list(list(TRAIT_TURF_IGNORE_SLOWDOWN)))
register_context()
-
if(new_dir)
setDir(new_dir)
if(new_id)
@@ -58,6 +61,9 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
AddElement(/datum/element/connect_loc, loc_connections)
update_move_direction()
LAZYADD(GLOB.conveyors_by_id[id], src)
+ if(wire_mode)
+ update_cable()
+ START_PROCESSING(SSmachines, src)
/obj/machinery/conveyor/examine(mob/user)
. = ..()
@@ -66,6 +72,7 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
. += "\nLeft-click with a wrench to rotate."
. += "Left-click with a screwdriver to invert its direction."
. += "Right-click with a screwdriver to flip its belt around."
+ . += "Left-click with a multitool to toggle whether this conveyor receives power via cable. Toggling connects and disconnects."
. += "Using another conveyor belt assembly on this will place a new conveyor belt in the direction this one is pointing."
/obj/machinery/conveyor/add_context(atom/source, list/context, obj/item/held_item, mob/user)
@@ -80,6 +87,9 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
context[SCREENTIP_CONTEXT_LMB] = "Invert conveyor belt"
context[SCREENTIP_CONTEXT_RMB] = "Flip conveyor belt"
return CONTEXTUAL_SCREENTIP_SET
+ if(held_item?.tool_behaviour == TOOL_MULTITOOL)
+ context[SCREENTIP_CONTEXT_LMB] = "Toggle conveyor belt wire mode"
+ return CONTEXTUAL_SCREENTIP_SET
/obj/machinery/conveyor/centcom_auto
id = "round_end_belt"
@@ -118,6 +128,7 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
/obj/machinery/conveyor/Destroy()
set_operating(FALSE)
LAZYREMOVE(GLOB.conveyors_by_id[id], src)
+ attached_wire_ref = null
return ..()
/obj/machinery/conveyor/vv_edit_var(var_name, var_value)
@@ -295,7 +306,16 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
inverted = !inverted
update_move_direction()
to_chat(user, span_notice("You set [src]'s direction [inverted ? "backwards" : "back to default"]."))
-
+ else if(attacking_item.tool_behaviour == TOOL_MULTITOOL)
+ attacking_item.play_tool_sound(src)
+ wire_mode = !wire_mode
+ update_cable()
+ power_change()
+ if(wire_mode)
+ START_PROCESSING(SSmachines, src)
+ else
+ STOP_PROCESSING(SSmachines, src)
+ to_chat(user, span_notice("You set [src]'s wire mode [wire_mode ? "on" : "off"]."))
else if(istype(attacking_item, /obj/item/stack/conveyor))
// We should place a new conveyor belt machine on the output turf the conveyor is pointing to.
var/turf/target_turf = get_step(get_turf(src), forwards)
@@ -334,10 +354,51 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
return
user.Move_Pulled(src)
+/obj/machinery/conveyor/powered(chan = power_channel, ignore_use_power = FALSE)
+ if(!wire_mode)
+ return ..()
+ var/datum/powernet/powernet = get_powernet()
+ if(!isnull(powernet))
+ return clamp(powernet.avail-powernet.load, 0, powernet.avail) >= active_power_usage
+ return ..()
+
/obj/machinery/conveyor/power_change()
. = ..()
update()
+/obj/machinery/conveyor/process()
+ if(!wire_mode)
+ return PROCESS_KILL
+ if(isnull(attached_wire_ref))
+ update_cable()
+ return
+ var/datum/powernet/powernet = get_powernet()
+ if(isnull(powernet))
+ return
+ if(powered())
+ powernet.load += active_power_usage
+ else
+ power_change()
+
+
+/obj/machinery/conveyor/proc/update_cable()
+ if(!wire_mode)
+ attached_wire_ref = null
+ return
+ var/turf/our_turf = get_turf(src)
+ attached_wire_ref = WEAKREF(locate(/obj/structure/cable) in our_turf)
+ if(attached_wire_ref)
+ return power_change()
+
+/obj/machinery/conveyor/proc/get_powernet()
+ if(!wire_mode)
+ return
+ var/obj/structure/cable/cable = attached_wire_ref.resolve()
+ if(isnull(cable))
+ attached_wire_ref = null
+ return
+ return cable.powernet
+
// Conveyor switch
/obj/machinery/conveyor_switch
name = "conveyor switch"
diff --git a/code/modules/research/designs/machine_designs.dm b/code/modules/research/designs/machine_designs.dm
index d07deca2926..55f8436c453 100644
--- a/code/modules/research/designs/machine_designs.dm
+++ b/code/modules/research/designs/machine_designs.dm
@@ -1257,3 +1257,73 @@
RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
)
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE | DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO | DEPARTMENT_BITFLAG_SERVICE
+
+/datum/design/board/manulathe
+ name = /obj/machinery/power/manufacturing/lathe::name
+ desc = "The circuit board for this machine."
+ id = "manulathe"
+ build_path = /obj/machinery/power/manufacturing/lathe
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design/board/manucrafter
+ name = /obj/machinery/power/manufacturing/crafter::name
+ desc = "The circuit board for this machine."
+ id = "manucrafter"
+ build_path = /obj/machinery/power/manufacturing/crafter
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design/board/manucrusher
+ name = /obj/machinery/power/manufacturing/crusher::name
+ desc = "The circuit board for this machine."
+ id = "manucrusher"
+ build_path = /obj/machinery/power/manufacturing/crusher
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design/board/manurouter
+ name = /obj/machinery/power/manufacturing/router::name
+ desc = "The circuit board for this machine."
+ id = "manurouter"
+ build_path = /obj/machinery/power/manufacturing/router
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design/board/manusorter
+ name = /obj/machinery/power/manufacturing/sorter::name
+ desc = "The circuit board for this machine."
+ id = "manusorter"
+ build_path = /obj/machinery/power/manufacturing/sorter
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design/board/manuunloader
+ name = /obj/machinery/power/manufacturing/unloader::name
+ desc = "The circuit board for this machine."
+ id = "manuunloader"
+ build_path = /obj/machinery/power/manufacturing/unloader
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
+
+/datum/design/board/manusmelter
+ name = /obj/machinery/power/manufacturing/smelter::name
+ desc = "The circuit board for this machine."
+ id = "manusmelter"
+ build_path = /obj/machinery/power/manufacturing/smelter
+ category = list(
+ RND_CATEGORY_MACHINE + RND_SUBCATEGORY_MACHINE_ENGINEERING
+ )
+ departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_CARGO
diff --git a/code/modules/research/techweb/nodes/engi_nodes.dm b/code/modules/research/techweb/nodes/engi_nodes.dm
index 0d8572130fb..fa1be5bb155 100644
--- a/code/modules/research/techweb/nodes/engi_nodes.dm
+++ b/code/modules/research/techweb/nodes/engi_nodes.dm
@@ -148,6 +148,13 @@
"light_tube",
"crossing_signal",
"guideway_sensor",
+ "manuunloader",
+ "manusmelter",
+ "manucrusher",
+ "manucrafter",
+ "manulathe",
+ "manusorter",
+ "manurouter",
)
/datum/techweb_node/energy_manipulation
diff --git a/icons/obj/machines/manufactorio.dmi b/icons/obj/machines/manufactorio.dmi
new file mode 100644
index 00000000000..95bbd8fccb0
Binary files /dev/null and b/icons/obj/machines/manufactorio.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index ed7d09bb8fe..9d5d9d705a1 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -758,6 +758,7 @@
#include "code\controllers\subsystem\processing\fishing.dm"
#include "code\controllers\subsystem\processing\greyscale.dm"
#include "code\controllers\subsystem\processing\instruments.dm"
+#include "code\controllers\subsystem\processing\manufacturing.dm"
#include "code\controllers\subsystem\processing\obj.dm"
#include "code\controllers\subsystem\processing\plumbing.dm"
#include "code\controllers\subsystem\processing\processing.dm"
@@ -4576,6 +4577,17 @@
#include "code\modules\mafia\roles\town\town_killing.dm"
#include "code\modules\mafia\roles\town\town_protective.dm"
#include "code\modules\mafia\roles\town\town_support.dm"
+#include "code\modules\manufactorio\_manufacturing.dm"
+#include "code\modules\manufactorio\machines\crafter.dm"
+#include "code\modules\manufactorio\machines\crusher.dm"
+#include "code\modules\manufactorio\machines\debug.dm"
+#include "code\modules\manufactorio\machines\lathe.dm"
+#include "code\modules\manufactorio\machines\router.dm"
+#include "code\modules\manufactorio\machines\smelter.dm"
+#include "code\modules\manufactorio\machines\sorter.dm"
+#include "code\modules\manufactorio\machines\sorter_filters.dm"
+#include "code\modules\manufactorio\machines\storagebox.dm"
+#include "code\modules\manufactorio\machines\unloader.dm"
#include "code\modules\mapfluff\centcom\nuke_ops.dm"
#include "code\modules\mapfluff\ruins\generic.dm"
#include "code\modules\mapfluff\ruins\lavaland_ruin_code.dm"
diff --git a/tgui/packages/tgui/interfaces/ManufacturingSorter.tsx b/tgui/packages/tgui/interfaces/ManufacturingSorter.tsx
new file mode 100644
index 00000000000..9ae38a3e5b6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ManufacturingSorter.tsx
@@ -0,0 +1,113 @@
+import { BooleanLike } from 'common/react';
+
+import { useBackend } from '../backend';
+import { Box, Button, Icon, LabeledList, Section, Stack } from '../components';
+import { Window } from '../layouts';
+
+type Data = {
+ filters: Filter[];
+ unmet_dir: number;
+};
+
+function dir2icon(dir) {
+ switch (dir) {
+ case 1:
+ return 'arrow-up';
+ case 2:
+ return 'arrow-down';
+ case 4:
+ return 'arrow-right';
+ case 8:
+ return 'arrow-left';
+ default:
+ return 'arrow-up';
+ }
+}
+
+type Filter = {
+ name: string;
+ ref: string;
+ inverted: BooleanLike;
+ dir: number;
+};
+
+export function ManufacturingSorter(props) {
+ const { act, data } = useBackend();
+ const { filters, unmet_dir } = data;
+
+ return (
+
+
+
+
+ act('new_filter')}
+ >
+ New filter
+
+ }
+ >
+
+ {filters.map((filter, i) => (
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ If no criteria is met, outputting to:
+
+
+
+
+
+
+
+
+
+ );
+}