Files
CHOMPStation2/code/modules/integrated_electronics/core/printer.dm
2025-08-30 04:45:41 +02:00

513 lines
18 KiB
Plaintext

/obj/item/integrated_circuit_printer
name = "integrated circuit printer"
desc = "A portable(ish) machine made to print tiny modular circuitry out of metal."
icon = 'icons/obj/integrated_electronics/electronic_tools.dmi'
icon_state = "circuit_printer"
w_class = ITEMSIZE_LARGE
var/metal = 0
var/max_metal = 100
var/metal_per_sheet = 10 // One sheet equals this much metal.
var/debug = FALSE // If true, metal is infinite.
var/upgraded = FALSE // When hit with an upgrade disk, will turn true, allowing it to print the higher tier circuits.
var/illegal_upgraded = FALSE // When hit with an illegal upgrade disk, will turn true, allowing it to print the illegal circuits.
var/can_clone = FALSE // Same for above, but will allow the printer to duplicate a specific assembly.
var/dirty_items = FALSE
// Printing state variables
var/is_printing = FALSE // If true, printer is busy cloning.
var/print_end_time = 0 // World time when printing will finish
var/obj/item/electronic_assembly/queued_assembly = null // The assembly being cloned.
/obj/item/integrated_circuit_printer/proc/finish_printing()
if(!queued_assembly)
is_printing = FALSE
return
// Drop the assembly on the ground
queued_assembly.forceMove(get_turf(src))
playsound(src, 'sound/machines/ding.ogg', 50, TRUE)
visible_message(span_notice("[src] beeps as it finishes printing '[queued_assembly.name]'."))
// Clear printing state
queued_assembly = null
is_printing = FALSE
print_end_time = 0
/obj/item/integrated_circuit_printer/all_upgrades
upgraded = TRUE
illegal_upgraded = TRUE
can_clone = TRUE
/obj/item/integrated_circuit_printer/illegal
illegal_upgraded = TRUE
can_clone = TRUE
/obj/item/integrated_circuit_printer/upgraded
upgraded = TRUE
can_clone = TRUE
/obj/item/integrated_circuit_printer/debug
name = "fractal integrated circuit printer"
desc = "A portable(ish) machine that makes modular circuitry seemingly out of thin air."
upgraded = TRUE
illegal_upgraded = TRUE
can_clone = TRUE
debug = TRUE
/obj/item/integrated_circuit_printer/attack_robot(mob/user as mob)
if(Adjacent(user))
return tgui_interact(user)
else
return ..()
/obj/item/integrated_circuit_printer/attackby(var/obj/item/O, var/mob/user)
if(istype(O,/obj/item/stack/material))
var/obj/item/stack/material/stack = O
if(stack.material.name == MAT_STEEL)
if(debug)
to_chat(user, span_warning("\The [src] does not need any material."))
return
var/num = min((max_metal - metal) / metal_per_sheet, stack.get_amount())
if(num < 1)
to_chat(user, span_warning("\The [src] is too full to add more metal."))
return
if(stack.use(max(1, round(num)))) // We don't want to create stacks that aren't whole numbers
to_chat(user, span_notice("You add [num] sheet\s to \the [src]."))
metal += num * metal_per_sheet
attack_self(user)
return TRUE
if(istype(O,/obj/item/integrated_circuit))
to_chat(user, span_notice("You insert the circuit into \the [src]."))
user.unEquip(O)
metal = min(metal + O.w_class, max_metal)
qdel(O)
attack_self(user)
return TRUE
if(istype(O,/obj/item/disk/integrated_circuit/upgrade/advanced))
if(upgraded)
to_chat(user, span_warning("\The [src] already has this upgrade."))
return TRUE
to_chat(user, span_notice("You install \the [O] into \the [src]."))
upgraded = TRUE
dirty_items = TRUE
attack_self(user)
return TRUE
if(istype(O,/obj/item/disk/integrated_circuit/upgrade/illegal))
if(illegal_upgraded)
to_chat(user, span_warning("\The [src] already has this upgrade."))
return TRUE
to_chat(user, span_notice("You install \the [O] into \the [src]."))
illegal_upgraded = TRUE
dirty_items = TRUE
attack_self(user)
return TRUE
if(istype(O,/obj/item/disk/integrated_circuit/upgrade/clone))
if(can_clone)
to_chat(user, span_warning("\The [src] already has this upgrade."))
return TRUE
to_chat(user, span_notice("You install \the [O] into \the [src]."))
can_clone = TRUE
attack_self(user)
return TRUE
return ..()
/obj/item/integrated_circuit_printer/vv_edit_var(var_name, var_value)
// Gotta update the static data in case an admin VV's the upgraded var for some reason..!
if(var_name == "upgraded")
dirty_items = TRUE
return ..()
/obj/item/integrated_circuit_printer/attack_self(var/mob/user)
tgui_interact(user)
/obj/item/integrated_circuit_printer/tgui_state(mob/user)
return GLOB.tgui_physical_state
/obj/item/integrated_circuit_printer/tgui_interact(mob/user, datum/tgui/ui)
if(dirty_items)
update_tgui_static_data(user, ui)
dirty_items = FALSE
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ICPrinter", name) // 500, 600
ui.open()
/obj/item/integrated_circuit_printer/tgui_static_data(mob/user)
var/list/data = ..()
var/list/categories = list()
for(var/category in SScircuit.circuit_fabricator_recipe_list)
var/list/cat_obj = list(
"name" = category,
"items" = null
)
if(cat_obj["name"] == "Illegal Parts" && !illegal_upgraded)
continue
var/list/circuit_list = SScircuit.circuit_fabricator_recipe_list[category]
var/list/items = list()
for(var/path in circuit_list)
var/obj/O = path
var/can_build = TRUE
if(ispath(path, /obj/item/integrated_circuit))
var/obj/item/integrated_circuit/IC = path
if((IC::spawn_flags & IC_SPAWN_RESEARCH) && (!(IC::spawn_flags & IC_SPAWN_DEFAULT)) && !upgraded)
can_build = FALSE
var/cost = 1
if(ispath(path, /obj/item/electronic_assembly))
var/obj/item/electronic_assembly/E = path
cost = round((E::max_complexity + E::max_components) / 4)
else
var/obj/item/I = path
cost = I::w_class
items.Add(list(list(
"name" = O::name,
"desc" = O::desc,
"can_build" = can_build,
"cost" = cost,
"path" = path,
)))
cat_obj["items"] = items
categories.Add(list(cat_obj))
data["categories"] = categories
return data
/obj/item/integrated_circuit_printer/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
var/list/data = ..()
data["metal"] = metal
data["max_metal"] = max_metal
data["metal_per_sheet"] = metal_per_sheet
data["debug"] = debug
data["upgraded"] = upgraded
data["can_clone"] = can_clone && !is_printing // Can not clone while printing
data["is_printing"] = is_printing
data["print_time_remaining"] = is_printing ? max(0, print_end_time - world.time) : 0
return data
/obj/item/integrated_circuit_printer/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
if(..())
return TRUE
add_fingerprint(ui.user)
switch(action)
if("import_circuit")
if(!can_clone)
to_chat(ui.user, span_warning("This printer requires a clone upgrade disk to import circuit designs!"))
return TRUE
if(is_printing) // Should not be possible to reach here.
to_chat(ui.user, span_warning("The printer is busy! Please wait for the current print job to finish."))
return TRUE
handle_circuit_import(ui.user)
return TRUE
if("build")
var/build_type = text2path(params["build"])
if(!build_type || !ispath(build_type))
return 1
var/cost = 1
if(ispath(build_type, /obj/item/electronic_assembly))
var/obj/item/electronic_assembly/E = build_type
cost = round( (E::max_complexity + E::max_components ) / 4)
else
var/obj/item/I = build_type
cost = I::w_class
var/in_some_category = FALSE
for(var/category in SScircuit.circuit_fabricator_recipe_list)
if(build_type in SScircuit.circuit_fabricator_recipe_list[category])
in_some_category = TRUE
break
if(!in_some_category)
return
if(!debug)
if(!Adjacent(ui.user))
to_chat(ui.user, span_notice("You are too far away from \the [src]."))
if(metal - cost < 0)
to_chat(ui.user, span_warning("You need [cost] metal to build that!."))
return 1
metal -= cost
var/obj/item/built = new build_type(get_turf(loc))
ui.user.put_in_hands(built)
to_chat(ui.user, span_notice("[capitalize(built.name)] printed."))
playsound(src, 'sound/items/jaws_pry.ogg', 50, TRUE)
return TRUE
/**
* Imports a circuit design from JSON data
* This uses the same logic as the vrdb/html vore belly imports
*
* @param user The user importing the circuit
* @param circuit_data JSON string containing the circuit data
*/
/obj/item/integrated_circuit_printer/proc/check_interactivity(mob/user)
return user.Adjacent(src)
/obj/item/integrated_circuit_printer/proc/handle_circuit_import(mob/user)
if(!user || user.stat || user.restrained() || !Adjacent(user))
return
var/input_file = input(user, "Please choose a circuit JSON file to import.", "Import Circuit") as file
if(!input_file)
return
var/file_size = length(file2text(input_file))
if(file_size > 1048576 / 4) // quarter of a megabyte.
to_chat(user, span_warning("File too large! Circuit files must be smaller than 1MB. Your file is [num2text(file_size)] bytes."))
return
if(file_size < 10)
to_chat(user, span_warning("This doesn't appear to be a valid circuit file."))
return
var/input_data
try
input_data = file2text(input_file)
catch(var/exception/e)
to_chat(user, span_warning("Failed to read file: [e]. Please ensure you selected a valid text/JSON file."))
return
if(!input_data || length(input_data) < 10)
to_chat(user, span_warning("The selected file is empty or unreadable. Please select a valid circuit JSON file."))
return
// Basic JSON validation.
if(!findtext(input_data, "{"))
// If it doesn't contain basic JSON characters, it's likely not a JSON file
to_chat(user, span_warning("Invalid file format! Please select a JSON file containing circuit data. (File appears to be binary or non-text format)"))
return
// Additional validation to prevent binary files..
if(findtext(input_data, "\xFF\xD8\xFF") || findtext(input_data, "\x89PNG") || findtext(input_data, "GIF89a") || findtext(input_data, "GIF87a"))
to_chat(user, span_warning("Invalid file type! You selected an image file. Please select a JSON text file containing circuit data."))
return
// Check if the input is Base64 encoded and decode it
if(length(input_data) > 0 && !findtext(input_data, "{"))
// If it doesn't contain '{' it's likely Base64 encoded JSON
var/decoded_data = rustg_decode_base64(input_data)
if(decoded_data && length(decoded_data) > 0)
input_data = decoded_data
else
to_chat(user, span_warning("Unable to decode file data. Please select a valid circuit JSON file."))
return
import_circuit(user, input_data, FALSE, null)
/obj/item/integrated_circuit_printer/proc/import_circuit(mob/user, circuit_data, override_type = FALSE, custom_type = null)
if(!circuit_data)
to_chat(user, span_warning("No circuit data provided!"))
return
// Add safety check before deserializing
if(length(circuit_data) > 100000) // Reduced from 50KB to be more conservative
to_chat(user, span_warning("Circuit data is too large to process!"))
return
// Additional safety checks for malformed data
if(length(circuit_data) < 20) // Increase minimum size
to_chat(user, span_warning("Circuit data is too small to be valid."))
return
// Validate that this looks like circuit JSON data
if(!findtext(circuit_data, "components") && !findtext(circuit_data, "assembly"))
to_chat(user, span_warning("This doesn't appear to be valid circuit data."))
return
// Deserialize the circuit data with enhanced error handling
var/list/assembly_data = null
try
assembly_data = deserialize_electronic_assembly(circuit_data)
catch(var/exception/e)
to_chat(user, span_warning("Failed to process circuit data: [e]. The file may be corrupted or not a valid circuit export."))
return
if(!assembly_data)
to_chat(user, span_warning("Invalid circuit data! Please select a valid circuit export file (.json) created by the circuit export system."))
return
// Validate that the assembly data has required fields
if(!islist(assembly_data) || !assembly_data["components"])
to_chat(user, span_warning("Invalid circuit format!"))
return
// Check if we have enough metal to build all components
var/total_cost = 0
var/total_complexity = 0
var/list/available_components = list()
var/list/components_to_create = list()
// Build list of available components
for(var/category in SScircuit.circuit_fabricator_recipe_list)
if(category == "Illegal Parts" && !illegal_upgraded)
continue
var/list/circuit_list = SScircuit.circuit_fabricator_recipe_list[category]
for(var/path in circuit_list)
available_components += path
// Check each component and calculate costs
for(var/list/component_data in assembly_data["components"])
// Support both old "type" and new "t" format for component type
var/component_type = component_data["type"] || component_data["t"]
// Support both old "name" and new "n" format for component name
var/component_name = component_data["name"] || component_data["n"] || "Unknown Component"
if(!component_type)
to_chat(user, span_warning("Component missing type information. Skipping."))
continue
// Handle both shortened and full paths flexibly
var/build_type = null
build_type = text2path(component_type)
if(!build_type || !ispath(build_type, /obj/item/integrated_circuit))
// Try with circuit prefix (for new shortened paths)
var/full_path = "/obj/item/integrated_circuit/[component_type]"
build_type = text2path(full_path)
if(!build_type || !ispath(build_type, /obj/item/integrated_circuit))
to_chat(user, span_warning("Unknown component type: [component_type]. Skipping."))
continue
// Check if this component is available
if(!(build_type in available_components))
to_chat(user, span_warning("Component '[component_name]' ([build_type]) is not available in this printer. Skipping."))
continue
// Check if component requires upgrades
if(ispath(build_type, /obj/item/integrated_circuit))
var/obj/item/integrated_circuit/IC = build_type
var/spawn_flags = IC::spawn_flags
// Component requires upgrades only if it has IC_SPAWN_RESEARCH but NOT IC_SPAWN_DEFAULT
if((spawn_flags & IC_SPAWN_RESEARCH) && !(spawn_flags & IC_SPAWN_DEFAULT) && !upgraded)
to_chat(user, span_warning("Component '[component_name]' requires printer upgrades. Skipping."))
continue
// Calculate cost
var/cost = 1
if(ispath(build_type, /obj/item/electronic_assembly))
var/obj/item/electronic_assembly/E = build_type
cost = round((E::max_complexity + E::max_components) / 4)
else
var/obj/item/I = build_type
cost = I::w_class
// Calculate complexity for printing time
var/complexity = 1
if(ispath(build_type, /obj/item/integrated_circuit))
var/obj/item/integrated_circuit/IC = build_type
complexity = IC::complexity
total_cost += cost
total_complexity += complexity
UNTYPED_LIST_ADD(components_to_create, list(
"type" = build_type,
"data" = component_data,
"cost" = cost
))
if(!LAZYLEN(components_to_create))
to_chat(user, span_warning("No valid components found in the circuit data!"))
return
// Check if we have enough metal
if(!debug && (total_cost / 2) > metal)
to_chat(user, span_warning("Not enough metal! Need [total_cost / 2] units, have [metal] units."))
return
// Calculate assembly cost based on w_class (1 metal per size level)
var/assembly_w_class = assembly_data["w_class"] || ITEMSIZE_SMALL // Default to SMALL if not specified
var/assembly_cost = assembly_w_class * 10
total_cost += assembly_cost
// Final metal check with assembly cost
if(!debug && (total_cost / 2) > metal)
to_chat(user, span_warning("Not enough metal! Need [total_cost / 2] units (including assembly), have [metal] units."))
return
if(!debug)
metal = max(0, metal - (total_cost / 2))
// Calculate printing time based on actual complexity (1 minute per 120 complexity = 5 deciseconds per complexity)
var/print_time = total_complexity * 5
// Create the assembly
var/obj/item/electronic_assembly/assembly = create_assembly_from_data(assembly_data, override_type, custom_type)
if(!assembly)
to_chat(user, span_warning("Failed to create assembly!"))
if(!debug)
metal += total_cost
return
// Add components to assembly
var/list/created_components = add_components_to_assembly(assembly, assembly_data, available_components)
if(!created_components || !LAZYLEN(created_components))
to_chat(user, span_warning("Failed to add components to assembly! No components were created."))
qdel(assembly)
if(!debug)
metal += total_cost
return
// Restore wiring, and appearance
restore_component_wiring(assembly_data, created_components)
assembly.update_icon()
// Start the printing process
queued_assembly = assembly
is_printing = TRUE
print_end_time = world.time + print_time
// Use addtimer instead of processing for efficiency
addtimer(CALLBACK(src, PROC_REF(finish_printing)), print_time, TIMER_DELETE_ME)
var/print_minutes = round(print_time / 600, 0.1) // Convert to minutes for display
to_chat(user, span_notice("Printing '[assembly.name]' with [LAZYLEN(created_components)] component\s. Estimated completion time: [print_minutes] minute\s."))
playsound(src, 'sound/machines/click.ogg', 50, TRUE)
return TRUE
// FUKKEN UPGRADE DISKS
/obj/item/disk/integrated_circuit/upgrade
name = "integrated circuit printer upgrade disk"
desc = "Install this into your integrated circuit printer to enhance it."
icon = 'icons/obj/integrated_electronics/electronic_tools.dmi'
icon_state = "upgrade_disk"
item_state = "card-id"
w_class = ITEMSIZE_SMALL
origin_tech = list(TECH_ENGINEERING = 3, TECH_DATA = 4)
/obj/item/disk/integrated_circuit/upgrade/advanced
name = "integrated circuit printer upgrade disk - advanced designs"
desc = "Install this into your integrated circuit printer to enhance it. This one adds new, advanced designs to the printer."
/obj/item/disk/integrated_circuit/upgrade/illegal
name = "integrated circuit printer upgrade disk - illegal designs"
desc = "Install this into your integrated circuit printer to enhance it. This one adds new, but illegal designs to the printer."
icon_state = "upgrade_disk_illegal"
origin_tech = list(TECH_ENGINEERING = 3, TECH_DATA = 4, TECH_ILLEGAL = 1)
/obj/item/disk/integrated_circuit/upgrade/clone
name = "integrated circuit printer upgrade disk - circuit cloner"
desc = "Install this into your integrated circuit printer to enhance it. This one allows the printer to duplicate assemblies."
icon_state = "upgrade_disk_clone"
origin_tech = list(TECH_ENGINEERING = 5, TECH_DATA = 6)