From eabb9e4b20a19941d211aadf9c2632371a6b80d1 Mon Sep 17 00:00:00 2001 From: CHOMPStation2StaffMirrorBot <94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:45:41 -0700 Subject: [PATCH] [MIRROR] Circuitry cloning implementation (#11536) Co-authored-by: Aura Dusklight <46622484+NovaDusklight@users.noreply.github.com> --- .../integrated_electronics/core/assemblies.dm | 40 ++ .../core/circuit_serialization.dm | 492 ++++++++++++++++++ .../integrated_electronics/core/printer.dm | 289 +++++++++- .../research/tg/designs/circuitry_designs.dm | 13 + .../tg/techwebs/nodes/circuit_nodes.dm | 1 + .../tgui/interfaces/ICAssembly/Plane.tsx | 30 +- .../tgui/interfaces/ICAssembly/index.tsx | 12 + .../tgui/interfaces/ICAssembly/types.ts | 7 + tgui/packages/tgui/interfaces/ICExport.tsx | 63 +++ tgui/packages/tgui/interfaces/ICPrinter.tsx | 57 +- vorestation.dme | 1 + 11 files changed, 968 insertions(+), 37 deletions(-) create mode 100644 code/modules/integrated_electronics/core/circuit_serialization.dm create mode 100644 tgui/packages/tgui/interfaces/ICExport.tsx diff --git a/code/modules/integrated_electronics/core/assemblies.dm b/code/modules/integrated_electronics/core/assemblies.dm index b522e74e6d..3b6cd5ffd2 100644 --- a/code/modules/integrated_electronics/core/assemblies.dm +++ b/code/modules/integrated_electronics/core/assemblies.dm @@ -18,6 +18,7 @@ var/locked = FALSE // If true, the assembly cannot be opened with a crowbar var/obj/item/card/id/locked_by = null // The ID that locked this assembly var/obj/item/card/id/access_card = null // ID card for door access + var/list/component_positions = list() // Stores circuit positions as list of lists: list("ref" = ref, "x" = x, "y" = y) /obj/item/electronic_assembly/Initialize(mapload) @@ -86,11 +87,18 @@ data["battery_max"] = round(battery?.maxcharge, 0.1) data["net_power"] = net_power / CELLRATE + // Include export data - the UI component will handle displaying it if needed + data["export_data"] = serialize_electronic_assembly() + data["assembly_name"] = name + var/list/circuits = list() for(var/obj/item/integrated_circuit/circuit in contents) UNTYPED_LIST_ADD(circuits, circuit.tgui_data(user, ui, state)) data["circuits"] = circuits + // Include component positions for UI restoration + data["component_positions"] = component_positions + return data /obj/item/electronic_assembly/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) @@ -98,6 +106,14 @@ return TRUE switch(action) + if("export_circuit") + if(!LAZYLEN(contents)) + to_chat(ui.user, span_warning("There's nothing in the [src] to export!")) + return TRUE + var/datum/tgui/window = new(ui.user, src, "ICExport", "Circuit Export") + window.open() + return TRUE + // Actual assembly actions if("rename") rename(ui.user) @@ -171,6 +187,30 @@ C.remove(ui.user) return TRUE + if("update_component_position") + var/obj/item/integrated_circuit/C = locate(params["ref"]) in contents + if(!istype(C)) + return FALSE + + var/new_x = params["x"] + var/new_y = params["y"] + if(!isnum(new_x) || !isnum(new_y)) + return FALSE + + // Find existing position entry or create new one + var/found = FALSE + for(var/list/pos_data in component_positions) + if(pos_data["ref"] == REF(C)) + pos_data["x"] = new_x + pos_data["y"] = new_y + found = TRUE + break + + if(!found) + UNTYPED_LIST_ADD(component_positions, list("ref" = REF(C), "x" = new_x, "y" = new_y)) + + return TRUE + return FALSE // End TGUI diff --git a/code/modules/integrated_electronics/core/circuit_serialization.dm b/code/modules/integrated_electronics/core/circuit_serialization.dm new file mode 100644 index 0000000000..8046fac386 --- /dev/null +++ b/code/modules/integrated_electronics/core/circuit_serialization.dm @@ -0,0 +1,492 @@ +/** + * Serialization/Deserialization for Integrated Circuits + * + * These functions handle converting assemblies to JSON format for export/import + */ +// Common prefixes that can be stripped to reduce JSON size +#define ASSEMBLY_PREFIX "/obj/item/electronic_assembly/" +#define CIRCUIT_PREFIX "/obj/item/integrated_circuit/" + +/** + * Strips the common assembly prefix to reduce JSON size + * @param type_path The full assembly type path + * @return The shortened path without the common prefix + */ +/obj/item/electronic_assembly/proc/strip_assembly_prefix(type_path) + if(!type_path) + return "" + var/type_string = "[type_path]" + if(findtext(type_string, ASSEMBLY_PREFIX) == 1) + return copytext(type_string, length(ASSEMBLY_PREFIX) + 1) + return type_string // Return as-is if prefix not found (for backward compatibility) + +/** + * Strips the common circuit prefix to reduce JSON size + * @param type_path The full circuit type path + * @return The shortened path without the common prefix + */ +/obj/item/electronic_assembly/proc/strip_circuit_prefix(type_path) + if(!type_path) + return "" + var/type_string = "[type_path]" + if(findtext(type_string, CIRCUIT_PREFIX) == 1) + return copytext(type_string, length(CIRCUIT_PREFIX) + 1) + return type_string // Return as-is if prefix not found (for backward compatibility) + +/** + * Restores the full assembly path from a shortened one + * @param shortened_path The shortened assembly path + * @return The full assembly type path + */ +/obj/item/integrated_circuit_printer/proc/restore_assembly_prefix(shortened_path) + if(!shortened_path) + return "" + var/path_string = "[shortened_path]" + // If it already contains the full path, return as-is (backward compatibility) + if(findtext(path_string, "/obj/item/electronic_assembly/") == 1) + return path_string + // Otherwise, add the prefix + return ASSEMBLY_PREFIX + path_string + +/** + * Restores the full circuit path from a shortened one + * @param shortened_path The shortened circuit path + * @return The full circuit type path + */ +/obj/item/integrated_circuit_printer/proc/restore_circuit_prefix(shortened_path) + if(!shortened_path) + return "" + var/path_string = "[shortened_path]" + // If it already contains the full path, return as-is (backward compatibility) + if(findtext(path_string, "/obj/item/integrated_circuit/") == 1) + return path_string + // Otherwise, add the prefix + return CIRCUIT_PREFIX + path_string + +/** + * Serializes this electronic assembly into a JSON string + * + * @return JSON string representation of the assembly + */ +/obj/item/electronic_assembly/proc/serialize_electronic_assembly() + if(!istype(src)) + return "Invalid assembly" + + var/list/assembly_data = list( + "n" = name, // Shortened: name + "d" = desc, // Shortened: desc + "t" = src.strip_assembly_prefix("[type]"), // Shortened: type (strip common prefix) + "c" = detail_color, // Shortened: color + "components" = list(), // Keep full name for clarity + "connections" = list() // Keep full name for clarity + ) + + // Create a lookup table for component indices + var/list/component_indices = list() + var/component_index = 1 + + // First pass: serialize components and build index lookup + for(var/obj/item/integrated_circuit/IC in contents) + component_indices[REF(IC)] = component_index + + var/list/component_data = list( + "i" = component_index, // Shortened key: index + "t" = src.strip_circuit_prefix("[IC.type]") // Shortened key: type (strip common prefix) + ) + + // Only include custom name if it differs from the default + if(IC.displayed_name != IC.name) + component_data["n"] = IC.displayed_name // Shortened key: name + + // Include position data if available + for(var/list/pos_data in component_positions) + if(pos_data["ref"] == REF(IC)) + component_data["x"] = pos_data["x"] // x position + component_data["y"] = pos_data["y"] // y position + break + + // Serialize pin data that has non-null values (both inputs and outputs) + var/list/pin_data_list = list() + + // Serialize input pins + for(var/i = 1, i <= length(IC.inputs), i++) + var/datum/integrated_io/input_pin = IC.inputs[i] + if(input_pin.data != null) + var/pin_data = null + // Handle different data types appropriately + if(isnum(input_pin.data) || istext(input_pin.data)) + pin_data = input_pin.data + else if(islist(input_pin.data)) + var/list/original_list = input_pin.data + pin_data = list() + for(var/item in original_list) + pin_data += item + else + pin_data = "[input_pin.data]" // Convert other types to text + + // Store pin type, index and data + UNTYPED_LIST_ADD(pin_data_list, list("t" = "i", "i" = i, "d" = pin_data)) + + // Serialize output pins + for(var/i = 1, i <= length(IC.outputs), i++) + var/datum/integrated_io/output_pin = IC.outputs[i] + if(output_pin.data != null) + var/pin_data = null + // Handle different data types appropriately + if(isnum(output_pin.data) || istext(output_pin.data)) + pin_data = output_pin.data + else if(islist(output_pin.data)) + var/list/original_list = output_pin.data + pin_data = list() + for(var/item in original_list) + pin_data += item + else + pin_data = "[output_pin.data]" // Convert other types to text + + // Store pin type, index and data + UNTYPED_LIST_ADD(pin_data_list, list("t" = "o", "i" = i, "d" = pin_data)) + + // Only include pins if there's actual data + if(length(pin_data_list) > 0) + component_data["p"] = pin_data_list // Shortened key: pins (inputs and outputs) + + UNTYPED_LIST_ADD(assembly_data["components"], component_data) + component_index++ + + // Second pass: serialize connections using the component indices (avoid duplicates by only processing outputs) + var/list/recorded_connections = list() // Track connections to avoid duplicates + + for(var/obj/item/integrated_circuit/IC in contents) + var/source_component_index = component_indices[REF(IC)] + + // Check output connections (only process outputs to avoid duplicates) + for(var/i = 1, i <= IC.outputs.len, i++) + var/datum/integrated_io/output_pin = IC.outputs[i] + for(var/datum/integrated_io/linked_pin in output_pin.linked) + var/target_component_index = component_indices[REF(linked_pin.holder)] + if(target_component_index) + var/target_pin_type = "u" // u = unknown + var/target_pin_index = 0 + + if(linked_pin in linked_pin.holder.inputs) + target_pin_type = "i" // i = input + target_pin_index = linked_pin.holder.inputs.Find(linked_pin) + else if(linked_pin in linked_pin.holder.outputs) + target_pin_type = "o" // o = output + target_pin_index = linked_pin.holder.outputs.Find(linked_pin) + else if(linked_pin in linked_pin.holder.activators) + target_pin_type = "a" // a = activator + target_pin_index = linked_pin.holder.activators.Find(linked_pin) + + if(target_pin_index > 0) + // Create unique connection identifier to prevent duplicates + var/connection_id = "[source_component_index].o[i]->[target_component_index].[target_pin_type][target_pin_index]" + + if(!(connection_id in recorded_connections)) + recorded_connections += connection_id + + // Ultra-compact connection format: [sc, spt, spi, tc, tpt, tpi] + var/list/connection = list( + "sc" = source_component_index, // source_component -> sc + "spt" = "o", // source_pin_type -> spt (always "o" for output) + "spi" = i, // source_pin_index -> spi + "tc" = target_component_index, // target_component -> tc + "tpt" = target_pin_type, // target_pin_type -> tpt + "tpi" = target_pin_index // target_pin_index -> tpi + ) + UNTYPED_LIST_ADD(assembly_data["connections"], connection) + + // Check activator connections + for(var/i = 1, i <= IC.activators.len, i++) + var/datum/integrated_io/activate/activator_pin = IC.activators[i] + for(var/datum/integrated_io/linked_pin in activator_pin.linked) + var/target_component_index = component_indices[REF(linked_pin.holder)] + if(target_component_index) + var/target_pin_type = "u" // u = unknown + var/target_pin_index = 0 + + if(linked_pin in linked_pin.holder.inputs) + target_pin_type = "i" // i = input + target_pin_index = linked_pin.holder.inputs.Find(linked_pin) + else if(linked_pin in linked_pin.holder.outputs) + target_pin_type = "o" // o = output + target_pin_index = linked_pin.holder.outputs.Find(linked_pin) + else if(linked_pin in linked_pin.holder.activators) + target_pin_type = "a" // a = activator + target_pin_index = linked_pin.holder.activators.Find(linked_pin) + + if(target_pin_index > 0) + // Create unique connection identifier to prevent duplicates + var/connection_id = "[source_component_index].a[i]->[target_component_index].[target_pin_type][target_pin_index]" + + if(!(connection_id in recorded_connections)) + recorded_connections += connection_id + + // Ultra-compact connection format: [sc, spt, spi, tc, tpt, tpi] + var/list/connection = list( + "sc" = source_component_index, // source_component -> sc + "spt" = "a", // source_pin_type -> spt (always "a" for activator) + "spi" = i, // source_pin_index -> spi + "tc" = target_component_index, // target_component -> tc + "tpt" = target_pin_type, // target_pin_type -> tpt + "tpi" = target_pin_index // target_pin_index -> tpi + ) + UNTYPED_LIST_ADD(assembly_data["connections"], connection) + + return json_encode(assembly_data) + +/** + * Deserializes a JSON string into a list of components to add to an assembly + * + * @param json_data The JSON string to deserialize + * @return List of information needed to recreate the assembly + */ +/obj/item/integrated_circuit_printer/proc/deserialize_electronic_assembly(json_data) + if(!json_data) + return null + + // Add safety check for maximum size + if(length(json_data) > 50000) + return null + + // Safety check for minimum viable JSON + if(length(json_data) < 10) + return null + + if(copytext(json_data, 1, 2) != "{" || copytext(json_data, length(json_data)) != "}") + return null + + var/list/assembly_data + try + // Use built-in html_decode to handle all HTML entities + var/cleaned_json = html_decode(json_data) + + assembly_data = json_decode(cleaned_json) + catch + return null + + if(!assembly_data || !islist(assembly_data)) + return null + + // Validate required fields exist + if(!assembly_data["components"] || !islist(assembly_data["components"])) + return null + + return assembly_data + +/** + * Creates an electronic assembly from deserialized data + * + * @param assembly_data The deserialized assembly data + * @param override_type Whether to override the assembly type + * @param custom_type Custom assembly type path if overriding + * @return The created assembly or null if failed + */ +/obj/item/integrated_circuit_printer/proc/create_assembly_from_data(list/assembly_data, override_type = FALSE, custom_type = null) + if(!assembly_data || !islist(assembly_data)) + return null + + var/obj/item/electronic_assembly/assembly + + // Determine assembly type + if(override_type && custom_type) + var/custom_path = text2path(custom_type) + if(custom_path && ispath(custom_path, /obj/item/electronic_assembly)) + assembly = new custom_path() + + // Use original assembly type if not overriding (use shortened key "t" for type) + if(!assembly && assembly_data["t"]) + var/restored_path = src.restore_assembly_prefix(assembly_data["t"]) + var/original_path = text2path(restored_path) + if(original_path && ispath(original_path, /obj/item/electronic_assembly)) + assembly = new original_path() + + // Default to medium assembly + if(!assembly) + assembly = new /obj/item/electronic_assembly/medium() + + // Set basic properties (use shortened keys) + if(assembly_data["n"]) // "n" for name + assembly.name = assembly_data["n"] + if(assembly_data["d"]) // "d" for desc + assembly.desc = assembly_data["d"] + if(assembly_data["c"]) // "c" for color + assembly.detail_color = assembly_data["c"] + + // Open assembly for component insertion + assembly.opened = TRUE + + return assembly + +/** + * Adds components to an assembly from deserialized data + * + * @param assembly The assembly to add components to + * @param assembly_data The deserialized assembly data + * @param available_components List of component types available for creation + * @return List of created components indexed by their original index + */ +/obj/item/integrated_circuit_printer/proc/add_components_to_assembly(obj/item/electronic_assembly/assembly, list/assembly_data, list/available_components) + if(!assembly || !assembly_data || !assembly_data["components"]) + return null + + var/list/created_components = list() + var/list/components_list = assembly_data["components"] + + // Create each component + var/component_index = 0 + for(var/component_data in components_list) + component_index++ + + if(!islist(component_data)) + continue + + // Use shortened keys: "t" for type, "i" for index + if(!component_data["t"] || !component_data["i"]) + continue + + var/restored_type_path = src.restore_circuit_prefix(component_data["t"]) + var/component_type_path = text2path(restored_type_path) + if(!component_type_path || !ispath(component_type_path, /obj/item/integrated_circuit)) + continue + + if(available_components && !(component_type_path in available_components)) + continue + + var/obj/item/integrated_circuit/IC = new component_type_path() + + if(component_data["n"]) + IC.displayed_name = component_data["n"] + + // Set pin data - use shortened key "p" for pins (both inputs and outputs) + if(component_data["p"] && islist(component_data["p"])) + for(var/list/pin_data in component_data["p"]) + // Only support ultra-compact format with pin type indicators + var/pin_type = pin_data["t"] // Pin type: "i" = input, "o" = output + var/pin_index = pin_data["i"] // Pin index + var/pin_value = pin_data["d"] // Pin data + + if(!pin_type || !pin_index || pin_value == null) + continue + + var/datum/integrated_io/target_pin = null + + if(pin_type == "i") + if(pin_index <= length(IC.inputs)) + target_pin = IC.inputs[pin_index] + else if(pin_type == "o") + if(pin_index <= length(IC.outputs)) + target_pin = IC.outputs[pin_index] + else + continue + + if(target_pin) + target_pin.write_data_to_pin(pin_value) + + // Add component to assembly + IC.forceMove(assembly) + assembly.force_add_circuit(IC) + + // Store position data if available + if(component_data["x"] != null && component_data["y"] != null) + // Store position in assembly for later UI restoration + UNTYPED_LIST_ADD(assembly.component_positions, list( + "ref" = REF(IC), + "x" = component_data["x"], + "y" = component_data["y"] + )) + else + // Set default positions in a grid layout if no position data + var/default_x = ((component_index - 1) % 4) * 200 + 50 // 4 components per row, 200px apart + var/default_y = ((component_index - 1) / 4) * 150 + 50 // 150px between rows + UNTYPED_LIST_ADD(assembly.component_positions, list( + "ref" = REF(IC), + "x" = default_x, + "y" = default_y + )) + + // Store component by its original index for wiring + created_components["[component_data["i"]]"] = IC + + return created_components + +/** + * Restores wiring connections between components (ultra-compact format only) + * + * @param assembly_data The deserialized assembly data + * @param created_components List of created components indexed by original index + */ +/obj/item/integrated_circuit_printer/proc/restore_component_wiring(list/assembly_data, list/created_components) + if(!assembly_data["connections"] || !islist(assembly_data["connections"])) + return + + for(var/connection in assembly_data["connections"]) + if(!connection || !islist(connection)) + continue + + // Support both old and new connection formats + var/source_comp_index, source_pin_type, source_pin_index + var/target_comp_index, target_pin_type, target_pin_index + + // Try ultra-compact format first (new format) + if(connection["sc"]) + source_comp_index = connection["sc"] // source_component + source_pin_type = connection["spt"] // source_pin_type + source_pin_index = connection["spi"] // source_pin_index + target_comp_index = connection["tc"] // target_component + target_pin_type = connection["tpt"] // target_pin_type + target_pin_index = connection["tpi"] // target_pin_index + + if(!source_comp_index || !target_comp_index || !source_pin_index || !target_pin_index) + continue + + var/source_key = "[source_comp_index]" + var/target_key = "[target_comp_index]" + + var/obj/item/integrated_circuit/source_IC = created_components[source_key] + var/obj/item/integrated_circuit/target_IC = created_components[target_key] + + if(!source_IC || !target_IC) + continue + + var/datum/integrated_io/source_pin + var/datum/integrated_io/target_pin + + // Get the appropriate pin based on single-letter type + switch(source_pin_type) + if("i") + if(source_pin_index <= source_IC.inputs.len) + source_pin = source_IC.inputs[source_pin_index] + if("o") + if(source_pin_index <= source_IC.outputs.len) + source_pin = source_IC.outputs[source_pin_index] + if("a") + if(source_pin_index <= source_IC.activators.len) + source_pin = source_IC.activators[source_pin_index] + + switch(target_pin_type) + if("i") + if(target_pin_index <= target_IC.inputs.len) + target_pin = target_IC.inputs[target_pin_index] + if("o") + if(target_pin_index <= target_IC.outputs.len) + target_pin = target_IC.outputs[target_pin_index] + if("a") + if(target_pin_index <= target_IC.activators.len) + target_pin = target_IC.activators[target_pin_index] + + if(source_pin && target_pin) + // Allow multiple outputs to connect to the same input + // Only prevent truly identical connections (same source pin to same target pin) + var/connection_exists = FALSE + + // Check if this exact pin-to-pin connection already exists + if(target_pin in source_pin.linked) + connection_exists = TRUE + + if(!connection_exists) + source_pin.linked |= target_pin + target_pin.linked |= source_pin + +#undef ASSEMBLY_PREFIX +#undef CIRCUIT_PREFIX diff --git a/code/modules/integrated_electronics/core/printer.dm b/code/modules/integrated_electronics/core/printer.dm index 6781b90d79..879610f773 100644 --- a/code/modules/integrated_electronics/core/printer.dm +++ b/code/modules/integrated_electronics/core/printer.dm @@ -12,11 +12,29 @@ 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. (Not implemented) -// var/static/list/recipe_list = list() - var/obj/item/electronic_assembly/assembly_to_clone = null // Not implemented x3 + 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 @@ -141,20 +159,20 @@ if(ispath(path, /obj/item/integrated_circuit)) var/obj/item/integrated_circuit/IC = path - if((initial(IC.spawn_flags) & IC_SPAWN_RESEARCH) && (!(initial(IC.spawn_flags) & IC_SPAWN_DEFAULT)) && !upgraded) + 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((initial(E.max_complexity) + initial(E.max_components)) / 4) + cost = round((E::max_complexity + E::max_components) / 4) else var/obj/item/I = path - cost = initial(I.w_class) + cost = I::w_class items.Add(list(list( - "name" = initial(O.name), - "desc" = initial(O.desc), + "name" = O::name, + "desc" = O::desc, "can_build" = can_build, "cost" = cost, "path" = path, @@ -174,8 +192,9 @@ data["metal_per_sheet"] = metal_per_sheet data["debug"] = debug data["upgraded"] = upgraded - data["can_clone"] = can_clone - data["assembly_to_clone"] = assembly_to_clone + 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 @@ -186,6 +205,17 @@ 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)) @@ -195,10 +225,10 @@ if(ispath(build_type, /obj/item/electronic_assembly)) var/obj/item/electronic_assembly/E = build_type - cost = round( (initial(E.max_complexity) + initial(E.max_components) ) / 4) + cost = round( (E::max_complexity + E::max_components ) / 4) else var/obj/item/I = build_type - cost = initial(I.w_class) + cost = I::w_class var/in_some_category = FALSE for(var/category in SScircuit.circuit_fabricator_recipe_list) @@ -221,6 +251,240 @@ 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" @@ -241,7 +505,6 @@ icon_state = "upgrade_disk_illegal" origin_tech = list(TECH_ENGINEERING = 3, TECH_DATA = 4, TECH_ILLEGAL = 1) -// To be implemented later. /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." diff --git a/code/modules/research/tg/designs/circuitry_designs.dm b/code/modules/research/tg/designs/circuitry_designs.dm index f0d985dfbb..f76a946bae 100644 --- a/code/modules/research/tg/designs/circuitry_designs.dm +++ b/code/modules/research/tg/designs/circuitry_designs.dm @@ -24,6 +24,19 @@ ) departmental_flags = DEPARTMENT_BITFLAG_SCIENCE +/datum/design_techweb/custom_circuit_printer_upgrade_clone + name = "Integrated circuit printer upgrade - circuit cloner" + desc = "Allows the integrated circuit printer to clone existing circuit assemblies" + id = "ic_printer_upgrade_clone" + // req_tech = list(TECH_ENGINEERING = 3, TECH_DATA = 4) + build_type = PROTOLATHE + materials = list(MAT_STEEL = 2000) + build_path = /obj/item/disk/integrated_circuit/upgrade/clone + category = list( + RND_CATEGORY_CIRCUITRY + ) + departmental_flags = DEPARTMENT_BITFLAG_SCIENCE + /datum/design_techweb/wirer name = "Custom wirer tool" id = "wirer" diff --git a/code/modules/research/tg/techwebs/nodes/circuit_nodes.dm b/code/modules/research/tg/techwebs/nodes/circuit_nodes.dm index 79a43a58a0..b696de2ed6 100644 --- a/code/modules/research/tg/techwebs/nodes/circuit_nodes.dm +++ b/code/modules/research/tg/techwebs/nodes/circuit_nodes.dm @@ -23,6 +23,7 @@ design_ids = list( "assembly-implant", "ic_printer_upgrade_adv", + "ic_printer_upgrade_clone", ) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_1_POINTS) diff --git a/tgui/packages/tgui/interfaces/ICAssembly/Plane.tsx b/tgui/packages/tgui/interfaces/ICAssembly/Plane.tsx index b5c0337428..c9024d5a29 100644 --- a/tgui/packages/tgui/interfaces/ICAssembly/Plane.tsx +++ b/tgui/packages/tgui/interfaces/ICAssembly/Plane.tsx @@ -290,10 +290,30 @@ const Circuit = ( onPortRightClick, } = props; - const [pos, setPos] = useSharedState(`component-pos-${circuit.ref}`, { - x: 0, - y: 0, - }); + const { act, data } = useBackend(); + + // Find stored position for this circuit + const storedPosition = data.component_positions?.find( + (pos) => pos.ref === circuit.ref, + ); + const initialPosition = storedPosition + ? { x: storedPosition.x, y: storedPosition.y } + : { x: 0, y: 0 }; + + const [pos, setPos] = useSharedState( + `component-pos-${circuit.ref}`, + initialPosition, + ); + + const handleComponentMoved = (val) => { + setPos(val); + // Also notify the backend to track position for export/import + act('update_component_position', { + ref: circuit.ref, + x: val.x, + y: val.y, + }); + }; return ( setPos(val)} + onComponentMoved={handleComponentMoved} onPortUpdated={onPortUpdated} onPortLoaded={onPortLoaded} onPortMouseDown={onPortMouseDown} diff --git a/tgui/packages/tgui/interfaces/ICAssembly/index.tsx b/tgui/packages/tgui/interfaces/ICAssembly/index.tsx index a6b939fd15..922db4b1ea 100644 --- a/tgui/packages/tgui/interfaces/ICAssembly/index.tsx +++ b/tgui/packages/tgui/interfaces/ICAssembly/index.tsx @@ -35,6 +35,18 @@ export const ICAssembly = (props) => { onClick={() => act('rename')} /> + +