[MIRROR] Circuitry cloning implementation (#11536)

Co-authored-by: Aura Dusklight <46622484+NovaDusklight@users.noreply.github.com>
This commit is contained in:
CHOMPStation2StaffMirrorBot
2025-08-29 19:45:41 -07:00
committed by GitHub
parent bc177617ef
commit eabb9e4b20
11 changed files with 968 additions and 37 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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."

View File

@@ -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"

View File

@@ -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)

View File

@@ -290,10 +290,30 @@ const Circuit = (
onPortRightClick,
} = props;
const [pos, setPos] = useSharedState(`component-pos-${circuit.ref}`, {
x: 0,
y: 0,
const { act, data } = useBackend<Data>();
// 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 (
<CircuitComponent
@@ -301,7 +321,7 @@ const Circuit = (
gridMode
x={pos.x}
y={pos.y}
onComponentMoved={(val) => setPos(val)}
onComponentMoved={handleComponentMoved}
onPortUpdated={onPortUpdated}
onPortLoaded={onPortLoaded}
onPortMouseDown={onPortMouseDown}

View File

@@ -35,6 +35,18 @@ export const ICAssembly = (props) => {
onClick={() => act('rename')}
/>
</Stack.Item>
<Stack.Item>
<Button
color="transparent"
width={2.5}
height={2}
textAlign="center"
icon="file-export"
tooltip="Export Circuit"
tooltipPosition="bottom-start"
onClick={() => act('export_circuit')}
/>
</Stack.Item>
<Stack.Item>
<Button
color="transparent"

View File

@@ -7,6 +7,13 @@ export type Data = {
battery_max: number;
net_power: number;
circuits: CircuitData[];
component_positions: ComponentPosition[];
};
export type ComponentPosition = {
ref: string;
x: number;
y: number;
};
export type CircuitData = {

View File

@@ -0,0 +1,63 @@
import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts';
import { Box, Button, Section, Stack, TextArea } from 'tgui-core/components';
type Data = {
export_data: string;
assembly_name: string;
};
const downloadCircuitFile = (data: string, filename: string) => {
const blob = new Blob([data], {
type: 'application/json',
});
Byond.saveBlob(blob, filename, '.json');
};
export const ICExport = (props) => {
const { data, act } = useBackend<Data>();
const { export_data, assembly_name } = data;
const handleDownload = () => {
const filename = assembly_name
? `circuit_${assembly_name}`
: 'circuit_export';
downloadCircuitFile(export_data, filename);
};
return (
<Window width={600} height={350}>
<Window.Content>
<Section title="Circuit Export Data">
<Stack vertical>
<Stack.Item>
<TextArea
height="40vh"
width="100%"
value={export_data || 'No data available'}
/>
</Stack.Item>
<Stack.Item>
<Stack>
<Stack.Item>
<Button
icon="download"
content="Download as File"
onClick={handleDownload}
tooltip="Download circuit data as a JSON file"
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
Note: You can download the circuit as a file or copy to clipboard.
Use the circuit printer&apos;s import function to load the
circuit.
</Stack.Item>
</Stack>
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -33,7 +33,7 @@ type item = {
};
export const ICPrinter = (props) => {
const { data } = useBackend<Data>();
const { data, act } = useBackend<Data>();
const { metal, max_metal, metal_per_sheet, upgraded, can_clone } = data;
@@ -43,11 +43,13 @@ export const ICPrinter = (props) => {
<Stack fill vertical>
<Stack.Item>
<Section fill title="Status">
<Stack vertical>
<Stack.Item>
<LabeledList>
<LabeledList.Item label="Metal">
<ProgressBar value={metal} maxValue={max_metal}>
{metal / metal_per_sheet} / {max_metal / metal_per_sheet}{' '}
sheets
{metal / metal_per_sheet} /{' '}
{max_metal / metal_per_sheet} sheets
</ProgressBar>
</LabeledList.Item>
<LabeledList.Item label="Circuits Available">
@@ -57,10 +59,27 @@ export const ICPrinter = (props) => {
{can_clone ? 'Available' : 'Unavailable'}
</LabeledList.Item>
</LabeledList>
<Box mt={1}>
</Stack.Item>
{!!can_clone && (
<Stack.Item>
<Button
icon="file-import"
content="Import Circuit"
onClick={() => {
act('import_circuit', {
override_type: false,
custom_type: null,
});
}}
tooltip="Import a circuit design from a JSON file"
/>
</Stack.Item>
)}
<Stack.Item>
Note: A red component name means that the printer must be
upgraded to create that component.
</Box>
</Stack.Item>
</Stack>
</Section>
</Stack.Item>
<Stack.Item>

View File

@@ -3011,6 +3011,7 @@
#include "code\modules\integrated_electronics\core\integrated_circuit.dm"
#include "code\modules\integrated_electronics\core\pins.dm"
#include "code\modules\integrated_electronics\core\printer.dm"
#include "code\modules\integrated_electronics\core\circuit_serialization.dm"
#include "code\modules\integrated_electronics\core\tools.dm"
#include "code\modules\integrated_electronics\core\assemblies\circuit_bug.dm"
#include "code\modules\integrated_electronics\core\assemblies\clothing.dm"