Upgrades the research console to TGUI and adds the material silo (#22987)

This updates the research console to be a TGUI and fixes several bugs
with it.
It now uses a material silo for materials, while the acid for the
circuit imprinter is still given directly to the machine.
Mech fabricators now also need to be linked to a research console.
It is also capable of connecting to multiple fabricators and distribute
tasks between them.

The sprite originates from TG station.

Unit tests were also made to ensure there is no fabricators missing
consoles and no console missing material silos.

<img width="1200" height="800" alt="image"
src="https://github.com/user-attachments/assets/b2076036-18e5-4518-9af1-3ac3698b3924"
/>
<img width="1200" height="800" alt="image"
src="https://github.com/user-attachments/assets/e954e1e4-cf2d-4472-8386-10d8993ac375"
/>
<img width="1200" height="800" alt="image"
src="https://github.com/user-attachments/assets/df690b93-c7ff-49a3-a1b3-002f6807981b"
/>
<img width="1200" height="800" alt="image"
src="https://github.com/user-attachments/assets/ff10ef88-e1d9-4b6f-9643-309d919a601f"
/>
This commit is contained in:
Casper3667
2026-08-06 09:34:34 +00:00
committed by GitHub
parent 42bba7fa3c
commit 2f5d7e8422
46 changed files with 3094 additions and 2026 deletions
@@ -797,3 +797,8 @@
new /obj/item/storage/box/unique/tea/messa(src)
new /obj/item/reagent_containers/toothpaste(src)
new /obj/item/reagent_containers/food/drinks/flask/vacuumflask/mouthwash(src)
/obj/structure/closet/crate/tool/machinist/fill()
new /obj/item/reagent_containers/weldpack(src)
new /obj/item/portable_whiteboard(src)
new /obj/item/paint_sprayer(src)
@@ -1,506 +0,0 @@
/obj/structure/machinery/mecha_part_fabricator
name = "synthetic fabricator"
desc = "A general purpose fabricator that can be used to fabricate equipment for synthetics or exosuits."
icon = 'icons/obj/machinery/robotics_fabricator.dmi'
icon_state = "fab"
density = TRUE
anchored = TRUE
idle_power_usage = 20
active_power_usage = 5000
req_access = list(ACCESS_ROBOTICS)
component_types = list(
/obj/item/circuitboard/mechfab,
/obj/item/stock_parts/matter_bin = 2,
/obj/item/stock_parts/manipulator,
/obj/item/stock_parts/micro_laser,
/obj/item/stock_parts/console_screen
)
manufacturer = "hephaestus"
/**
* A `/list` of enqueued `/datum/design` to be printed, processed in the queue
*/
var/list/datum/design/queue = list()
/**
* How much efficient (or inefficient) the protolathe is at manufacturing things
*/
var/mat_efficiency = 1
/**
* The production speed of this specific protolathe, a factor
*/
var/production_speed = 1
var/list/materials = list(MATERIAL_STEEL = 0, MATERIAL_GLASS = 0, MATERIAL_GOLD = 0, MATERIAL_SILVER = 0, MATERIAL_DIAMOND = 0, MATERIAL_PHORON = 0, MATERIAL_URANIUM = 0, MATERIAL_PLASTEEL = 0, MATERIAL_ALUMINIUM = 0, MATERIAL_LEAD = 0)
var/res_max_amount = 200000
var/datum/research/files
/// The looping sound for production.
var/datum/looping_sound/synth_fab/fab_loop
var/list/categories = list()
var/category = null
var/sync_message = ""
var/limb_manufacturer
///The timer id for the build callback, if we're building something
var/build_callback_timer
/obj/structure/machinery/mecha_part_fabricator/upgrade_hints(mob/user, distance, is_adjacent)
. += ..()
. += "- Upgraded <b>matter bins</b> will increase material storage capacity."
. += SPAN_NOTICE(" - The current storage capacity is <b>[res_max_amount / 2000]</b> sheets")
. += "- Upgraded <b>micro-lasers</b> will increase fabrication speed."
. += SPAN_NOTICE(" - The current speed increase is <b>[round((1 - (1 / production_speed)) * 100)]%</b>")
. += "- Upgraded <b>manipulators</b> will improve material use efficiency."
. += SPAN_NOTICE(" - The current cost reduction is <b>[round((1 - mat_efficiency) * 100)]%</b>")
/obj/structure/machinery/mecha_part_fabricator/antagonist_hints(mob/user, distance, is_adjacent)
. += ..()
. += "Click-drag someone with long hair onto this machine to start feeding it their hair! This will hurt and piss them off!"
/obj/structure/machinery/mecha_part_fabricator/Initialize()
. = ..()
files = new /datum/research(src) //Setup the research data holder.
fab_loop = new(src)
limb_manufacturer = GLOB.basic_robolimb.company
update_categories()
update_icon()
/// Make sure the machine starts the round properly synced.
/obj/structure/machinery/mecha_part_fabricator/LateInitialize()
. = ..()
sync()
/obj/structure/machinery/mecha_part_fabricator/update_icon()
ClearOverlays()
update_fab_audio()
if(panel_open)
AddOverlays("[icon_state]_panel")
if(!(stat & (NOPOWER|BROKEN)))
AddOverlays(emissive_appearance(icon, "[icon_state]_lights"))
AddOverlays("[icon_state]_lights")
if(build_callback_timer)
AddOverlays("[icon_state]_working")
AddOverlays(emissive_appearance(icon, "[icon_state]_lights_working"))
AddOverlays("[icon_state]_lights_working")
/// Starts and stops the necessary audio.
/obj/structure/machinery/mecha_part_fabricator/proc/update_fab_audio()
if(!fab_loop)
return
if(build_callback_timer)
fab_loop.start()
else
fab_loop.stop()
/obj/structure/machinery/mecha_part_fabricator/dismantle()
SSmaterials.normalize_material_amounts(materials)
for(var/f in materials.Copy())
eject_materials(f, materials[f])
//Stop the queue building if you're dismantling
deltimer(build_callback_timer)
..()
/obj/structure/machinery/mecha_part_fabricator/RefreshParts()
..()
res_max_amount = 0
for(var/obj/item/stock_parts/matter_bin/M in component_parts)
res_max_amount += M.rating * 100000 // 200k -> 600k
var/T = 0
for(var/obj/item/stock_parts/manipulator/M in component_parts)
T += M.rating
mat_efficiency = 1 - (T - 1) / 4 // 1 -> 0.5
for(var/obj/item/stock_parts/micro_laser/M in component_parts) // Not resetting T is intended; speed is affected by both
T += M.rating
production_speed = T / 2 // 1 -> 3
/obj/structure/machinery/mecha_part_fabricator/attack_hand(var/mob/user)
if(..())
return
if(!allowed(user))
to_chat(user, SPAN_WARNING("Access denied."))
return
do_hair_pull(user)
// Yes I'm doing this after do_hair_pull() intentionally, you can totally fail the skill check and still get your hair stuck in a blender.
var/cancelled = FALSE
SEND_SIGNAL(user, COMSIG_USE_MECH_FAB, &cancelled)
if (cancelled)
return
ui_interact(user)
/obj/structure/machinery/mecha_part_fabricator/ui_interact(mob/user, datum/tgui/ui)
. = ..()
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "SyntheticFabricator", "Synthetic Fabricator", 1200, 800)
ui.open()
/obj/structure/machinery/mecha_part_fabricator/ui_data(mob/user)
var/list/data = list()
data["manufacturer"] = manufacturer
var/datum/design/current = queue.len ? queue[1] : null
if(current)
data["current"] = current.name
data["queue"] = get_queue_names()
data["buildable"] = get_build_options()
data["category"] = category
data["available_categories"] = list()
for(var/possible_category in categories)
data["available_categories"] += list(list("name" = possible_category))
if(GLOB.fabricator_robolimbs)
var/list/T = list()
for(var/A in GLOB.fabricator_robolimbs)
var/datum/robolimb/R = GLOB.fabricator_robolimbs[A]
T += list(list("name" = R.company, "type" = A))
data["manufacturers"] = T
data["selected_manufacturer"] = limb_manufacturer
data["materials"] = get_materials()
data["maximum_resource_amount"] = res_max_amount
data["sync"] = sync_message
//If we have something in the queue that we're trying to build, show the time left if the build is undergoing, otherwise null
//the logic of displaying it is handled client-side
if(current)
if(build_callback_timer)
data["timeleft"] = round(timeleft(build_callback_timer))
else
data["timeleft"] = null
return data
/obj/structure/machinery/mecha_part_fabricator/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("build")
var/path = text2path(params["build"])
add_to_queue(path)
. = TRUE
if("remove")
remove_from_queue(text2num(params["remove"]))
. = TRUE
if("category")
if(params["category"] in categories)
category = params["category"]
. = TRUE
if("manufacturer")
if(params["manufacturer"] in GLOB.fabricator_robolimbs)
limb_manufacturer = params["manufacturer"]
. = TRUE
if("eject")
eject_materials(params["eject"], text2num(params["amount"]))
. = TRUE
if("sync")
sync()
. = TRUE
else
sync_message = ""
if(.)
playsound(src, 'sound/machines/synthfab/synthfab_button.ogg', 50)
/obj/structure/machinery/mecha_part_fabricator/attackby(obj/item/attacking_item, mob/user)
if(build_callback_timer)
to_chat(user, SPAN_NOTICE("\The [src] is busy. Please wait for completion of previous operation."))
return TRUE
if(default_deconstruction_screwdriver(user, attacking_item))
return TRUE
if(default_deconstruction_crowbar(user, attacking_item))
return TRUE
if(default_part_replacement(user, attacking_item))
return TRUE
if(!istype(attacking_item, /obj/item/stack/material))
return ..()
var/obj/item/stack/material/M = attacking_item
if(!M.material)
return ..()
var/material = SSmaterials.material_to_path(M.material, FALSE)
if(!(material in list(MATERIAL_STEEL, MATERIAL_GLASS, MATERIAL_GOLD, MATERIAL_SILVER, MATERIAL_DIAMOND, MATERIAL_PHORON, MATERIAL_URANIUM, MATERIAL_PLASTEEL, MATERIAL_ALUMINIUM, MATERIAL_LEAD)))
to_chat(user, SPAN_WARNING("\The [src] cannot hold [M.material.name]."))
return TRUE
var/sname = "[M.name]"
SSmaterials.normalize_material_amounts(materials)
if((materials[material] || 0) + SHEET_MATERIAL_AMOUNT <= res_max_amount)
if(M.amount >= 1)
var/count = 0
var/mutable_appearance/MA = mutable_appearance(icon, "material_insertion")
MA.color = M.material.icon_colour
//first play the insertion animation
flick_overlay_view(MA, 1 SECONDS)
//now play the progress bar animation
flick_overlay_view(mutable_appearance(icon, "fab_progress"), 1 SECONDS)
while((materials[material] || 0) + SHEET_MATERIAL_AMOUNT <= res_max_amount && M.amount >= 1)
SSmaterials.add_material_amount(materials, material, M.perunit)
M.use(1)
count++
to_chat(user, SPAN_NOTICE("You insert [count] [sname] into \the [src]."))
//Wake up, we have things to do (maybe)
handle_queue()
else
to_chat(user, SPAN_NOTICE("\The [src] cannot hold more [sname]."))
return TRUE
/obj/structure/machinery/mecha_part_fabricator/mouse_drop_receive(atom/dropped, mob/user, params)
var/mob/living/carbon/human/target = dropped
if (!istype(target) || target.buckled_to || get_dist(user, src) > 1 || get_dist(user, target) > 1 || user.stat || istype(user, /mob/living/silicon/ai))
return
if(target == user)
return
src.add_fingerprint(user)
var/target_loc = target.loc
if(target != user && !user.restrained() && !user.stat && !user.weakened && !user.stunned && !user.paralysis)
if(isskrell(target) || isunathi(target))
return
for(var/obj/item/protection in list(target.head))
if(protection && (protection.flags_inv & BLOCKHAIR))
return
var/datum/sprite_accessory/hair/hair_style = GLOB.hair_styles_list[target.h_style]
if(hair_style.length < 4)
return
user.visible_message(SPAN_WARNING("[user] starts feeding [target]'s hair into \the [src]!"), SPAN_WARNING("You start feeding [target]'s hair into \the [src]!"))
if(!do_after(user, 5 SECONDS, target, DO_UNIQUE))
return
if(target_loc != target.loc)
return
if(target != user && !user.restrained() && !user.stat && !user.weakened && !user.stunned && !user.paralysis)
user.visible_message(SPAN_WARNING("[user] feeds the [target]'s hair into the [src] and flicks it on!"), SPAN_ALERT("You turn the [src] on!"))
do_hair_pull(target)
user.attack_log += "\[[time_stamp()]\] <span class='warning'>Has fed [target.name]'s ([target.ckey]) hair into a [src].</span>"
target.attack_log += "\[[time_stamp()]\] <font color='orange'>Has had their hair fed into [src] by [user.name] ([user.ckey])</font>"
msg_admin_attack("[key_name_admin(user)] fed [key_name_admin(target)] in a [src]. (<A href='byond://?_src_=holder;adminplayerobservecoodjump=1;X=[user.x];Y=[user.y];Z=[user.z]'>JMP</a>)",ckey=key_name(user),ckey_target=key_name(target))
else
return
if(!do_after(user, 3.5 SECONDS, target, DO_UNIQUE))
return
if(target_loc != target.loc)
return
if(target != user && !user.restrained() && !user.stat && !user.weakened && !user.stunned && !user.paralysis)
user.visible_message(SPAN_ALERT("[user] starts tugging on [target]'s head as the [src] keeps running!"), SPAN_ALERT("You start tugging on [target]'s head!"))
do_hair_pull(target)
spawn(10)
user.visible_message(SPAN_ALERT("[user] stops the [src] and leaves [target] resting as they are."), SPAN_ALERT("You turn the [src] off and let go of [target]."))
/obj/structure/machinery/mecha_part_fabricator/emag_act(var/remaining_charges, var/mob/user)
switch(emagged)
if(0)
emagged = 0.5
visible_message("[icon2html(src, viewers(get_turf(src)))] <b>[src]</b> beeps: \"DB error \[Code 0x00F1\]\"")
sleep(10)
visible_message("[icon2html(src, viewers(get_turf(src)))] <b>[src]</b> beeps: \"Attempting auto-repair\"")
sleep(15)
visible_message("[icon2html(src, viewers(get_turf(src)))] <b>[src]</b> beeps: \"User DB corrupted \[Code 0x00FA\]. Truncating data structure...\"")
sleep(30)
visible_message("[icon2html(src, viewers(get_turf(src)))] <b>[src]</b> beeps: \"User DB truncated. Please contact your [SSatlas.current_map.company_name] system operator for future assistance.\"")
req_access = null
emagged = 1
return 1
if(0.5)
visible_message("[icon2html(src, viewers(get_turf(src)))] <b>[src]</b> beeps: \"DB not responding \[Code 0x0003\]...\"")
if(1)
visible_message("[icon2html(src, viewers(get_turf(src)))] <b>[src]</b> beeps: \"No records in User DB\"")
/**
* Adds a design to the queue
*
* * path: The path of the design to add
*/
/obj/structure/machinery/mecha_part_fabricator/proc/add_to_queue(path)
var/datum/design/D = files.known_designs[path]
if(!D)
return
queue += D
//Wake up, we have things to do
handle_queue()
/**
* Removes a design from the queue
*
* * index: The index of the design to remove
*/
/obj/structure/machinery/mecha_part_fabricator/proc/remove_from_queue(index)
queue.Cut(index, index + 1)
//If we're removing the first thing in the queue, stop the build timer
if(index == 1)
deltimer(build_callback_timer)
build_callback_timer = null
//Wake up, we have things to do
handle_queue()
/**
* Handle the construction queue
*/
/obj/structure/machinery/mecha_part_fabricator/proc/handle_queue()
//No work to do or already busy, stop
if(!length(queue) || build_callback_timer)
//If we don't have a build in progress, revert to idle power usage
if(!build_callback_timer)
update_use_power(POWER_USE_IDLE)
update_icon()
return
//If there's no power, there's no building
if(stat & NOPOWER)
queue = list()
return
//Get the first design in the queue
var/datum/design/D = queue[1]
//If we can build it, process the request
if(can_build(D))
update_use_power(POWER_USE_ACTIVE)
//Add the callback timer for printing, and remove the design from the queue
build_callback_timer = addtimer(CALLBACK(src, PROC_REF(build), D), (D.time / production_speed), TIMER_UNIQUE|TIMER_STOPPABLE)
//No queue removal here because we show how long it takes to build in the UI
else
visible_message(SPAN_NOTICE("[icon2html(src, viewers(get_turf(src)))] \The [src] flashes: Insufficient materials to complete build: [D.name]."))
remove_from_queue(1)
update_icon()
/obj/structure/machinery/mecha_part_fabricator/proc/can_build(var/datum/design/D)
SSmaterials.normalize_material_amounts(materials)
for(var/M in D.materials)
var/material = SSmaterials.material_to_path(M, FALSE)
if(!material)
material = M
if((materials[material] || 0) < D.materials[M] * mat_efficiency)
return 0
return 1
/**
* Builds the given design, assuming all the necessary conditions are met
*
* * design_to_build: The design to build
*/
/obj/structure/machinery/mecha_part_fabricator/proc/build(datum/design/design_to_build)
//Consume the materials
SSmaterials.normalize_material_amounts(materials)
for(var/M in design_to_build.materials)
SSmaterials.remove_material_amount(materials, M, design_to_build.materials[M] * mat_efficiency)
intent_message(MACHINE_SOUND)
if(design_to_build.build_path)
var/loc_offset = get_step(src, dir)
var/obj/new_item = design_to_build.Fabricate(loc_offset, src)
visible_message("\The <b>[src]</b> pings, indicating that \the [new_item] is complete.", "You hear a ping.", intent_message = PING_SOUND)
if(mat_efficiency != 1)
if(new_item.matter && new_item.matter.len > 0)
for(var/i in new_item.matter)
new_item.matter[i] = new_item.matter[i] * mat_efficiency
//We finished building, clear the timer and remove the thing from the queue
build_callback_timer = null
remove_from_queue(1)
//Do the queue handling for the next item, or to stop
handle_queue()
/obj/structure/machinery/mecha_part_fabricator/proc/get_queue_names()
. = list()
for(var/i = 1 to queue.len)
var/datum/design/D = queue[i]
. += list(list("name" = D.name, "time" = get_design_time(D), "index" = i))
/obj/structure/machinery/mecha_part_fabricator/proc/get_build_options()
. = list()
for(var/path in files.known_designs)
var/datum/design/D = files.known_designs[path]
if(!D.build_path || !(D.build_type & MECHFAB) || !D.category || (D.category != category))
continue
. += list(list("name" = D.name, "desc" = D.desc, "type" = D.type, "category" = D.category, "resources" = get_design_resourses(D), "time" = get_design_time(D)))
/obj/structure/machinery/mecha_part_fabricator/proc/get_design_resourses(var/datum/design/D)
var/list/F = list()
for(var/T in D.materials)
F += "[SSmaterials.material_display_name(T)]: [D.materials[T] * mat_efficiency]"
return english_list(F, and_text = ", ")
/obj/structure/machinery/mecha_part_fabricator/proc/get_design_time(var/datum/design/D)
return time2text(round(10 * D.time / production_speed), "mm:ss")
/obj/structure/machinery/mecha_part_fabricator/proc/update_categories()
categories = list()
for(var/path in files.known_designs)
var/datum/design/D = files.known_designs[path]
if(!D.build_path || !(D.build_type & MECHFAB) || !D.category)
continue
categories |= D.category
if(!category || !(category in categories))
category = categories[1]
/obj/structure/machinery/mecha_part_fabricator/proc/get_materials()
. = list()
SSmaterials.normalize_material_amounts(materials)
for(var/T in materials)
. += list(list("name" = SSmaterials.material_display_name(T), "amount" = materials[T]))
/obj/structure/machinery/mecha_part_fabricator/proc/eject_materials(var/material, var/amount)
if(!amount)
return
SSmaterials.normalize_material_amounts(materials)
material = SSmaterials.material_to_path(material, FALSE)
if(!material)
return
var/stack_type = SSmaterials.material_stack_type(material)
if(!stack_type)
return
var/real_amount = min(round(amount / SHEET_MATERIAL_AMOUNT), round((materials[material] || 0) / SHEET_MATERIAL_AMOUNT))
if(real_amount < 1)
return
var/obj/item/stack/material/S = new stack_type(loc, real_amount)
S.update_icon()
SSmaterials.remove_material_amount(materials, material, real_amount * SHEET_MATERIAL_AMOUNT)
/obj/structure/machinery/mecha_part_fabricator/proc/sync()
sync_message = "Error: no console found."
for(var/obj/structure/machinery/computer/rdconsole/RDC in get_area(src))
if(!RDC.sync)
continue
for(var/id in RDC.files.known_tech)
var/datum/tech/T = RDC.files.known_tech[id]
files.AddTech2Known(T)
files.RefreshResearch()
sync_message = "Sync complete."
update_categories()