[MIRROR] Integrated circuits for modular computers (#26196)

Integrated circuits for modular computers (#80530)

This PR integrates circuits for modular computers and a good bits of
their programs.
The peculiarity here is that modular computers have no fixed amount of
unremovable components (except the base one with just a couple ports for
now), instead, they're added and removed along with programs. With a few
exceptions (such as the messenger and signaler), for these program
circuits to work, their associated program has to be either open or in
the background.

For a reason or another, not all programs have a circuit associated to
them, still, however the programs with a circuit are still a handful.
They are:
- Nanotrasen Pay System
- Notepad
- SiliConnect
- WireCarp
- MODsuit Control
- Spectre Meter
- Direct Messenger*
- LifeConnect
- Custodial Locator
- Fission360
- Camera
- Status Display
- SignalCommander

*By the by, sending messages has a cooldown, so it shouldn't be as
spammy. If it turns out to not be enough, I can make it so messages from
circuit will be ignored by other messenger circuits.

The PR is no longer WIP.

I believe modular computers could make for some interesting setups with
circuits, since they're fairly flexible and stocked with features unlike
many other appliances, therefore also a speck more abusable, though
limits, cooldowns, logging and sanitization have been implemented to
keep it in check.

🆑
add: Modular Computers now support integrated circuits. What can be done
with them depends on the programs installed and whether they're running
(open or background).
add: Modular Consoles (the machinery) now have a small backup cell they
draw power from if the power goes out.
/🆑

Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
This commit is contained in:
SkyratBot
2024-01-21 15:59:14 +01:00
committed by GitHub
parent ab5a4d0f99
commit fdcfabefd8
60 changed files with 1286 additions and 240 deletions
@@ -110,6 +110,15 @@
///The max amount of paper that can be held at once.
var/max_paper = 30
/// The capacity of the circuit shell component of this item
var/shell_capacity = SHELL_CAPACITY_MEDIUM
/**
* Reference to the circuit shell component, because we're special and do special things with it,
* such as creating and deleting unremovable circuit comps based on the programs installed.
*/
var/datum/component/shell/shell
/datum/armor/item_modular_computer
bullet = 20
laser = 20
@@ -120,6 +129,7 @@
START_PROCESSING(SSobj, src)
if(!physical)
physical = src
add_shell_component(shell_capacity)
set_light_color(comp_light_color)
set_light_range(comp_light_luminosity)
if(looping_sound)
@@ -137,6 +147,26 @@
register_context()
update_appearance()
///Initialize the shell for this item, or the physical machinery it belongs to.
/obj/item/modular_computer/proc/add_shell_component(capacity = SHELL_CAPACITY_MEDIUM, shell_flags = NONE)
shell = physical.AddComponent(/datum/component/shell, list(new /obj/item/circuit_component/modpc), capacity, shell_flags)
RegisterSignal(shell, COMSIG_SHELL_CIRCUIT_ATTACHED, PROC_REF(on_circuit_attached))
RegisterSignal(shell, COMSIG_SHELL_CIRCUIT_REMOVED, PROC_REF(on_circuit_removed))
/obj/item/modular_computer/proc/on_circuit_attached(datum/source)
SIGNAL_HANDLER
RegisterSignal(shell.attached_circuit, COMSIG_CIRCUIT_PRE_POWER_USAGE, PROC_REF(use_power_for_circuits))
///Try to draw power from our internal cell first, before switching to that of the circuit.
/obj/item/modular_computer/proc/use_power_for_circuits(datum/source, power_usage_per_input)
SIGNAL_HANDLER
if(use_power(power_usage_per_input, check_programs = FALSE))
return COMPONENT_OVERRIDE_POWER_USAGE
/obj/item/modular_computer/proc/on_circuit_removed(datum/source)
SIGNAL_HANDLER
UnregisterSignal(shell.attached_circuit, COMSIG_CIRCUIT_PRE_POWER_USAGE)
/obj/item/modular_computer/proc/install_default_programs()
SHOULD_CALL_PARENT(FALSE)
for(var/programs in default_programs + starting_programs)
@@ -158,6 +188,7 @@
if(computer_id_slot)
QDEL_NULL(computer_id_slot)
shell = null
physical = null
return ..()
@@ -438,29 +469,32 @@
/obj/item/modular_computer/proc/turn_on(mob/user, open_ui = TRUE)
var/issynth = issilicon(user) // Robots and AIs get different activation messages.
if(atom_integrity <= integrity_failure * max_integrity)
if(issynth)
to_chat(user, span_warning("You send an activation signal to \the [src], but it responds with an error code. It must be damaged."))
else
to_chat(user, span_warning("You press the power button, but the computer fails to boot up, displaying variety of errors before shutting down again."))
if(user)
if(issynth)
to_chat(user, span_warning("You send an activation signal to \the [src], but it responds with an error code. It must be damaged."))
else
to_chat(user, span_warning("You press the power button, but the computer fails to boot up, displaying variety of errors before shutting down again."))
return FALSE
if(use_power()) // checks if the PC is powered
if(issynth)
to_chat(user, span_notice("You send an activation signal to \the [src], turning it on."))
else
to_chat(user, span_notice("You press the power button and start up \the [src]."))
if(looping_sound)
soundloop.start()
enabled = TRUE
update_appearance()
if(open_ui)
update_tablet_open_uis(user)
if(user)
if(issynth)
to_chat(user, span_notice("You send an activation signal to \the [src], turning it on."))
else
to_chat(user, span_notice("You press the power button and start up \the [src]."))
if(open_ui)
update_tablet_open_uis(user)
return TRUE
else // Unpowered
if(issynth)
to_chat(user, span_warning("You send an activation signal to \the [src] but it does not respond."))
else
to_chat(user, span_warning("You press the power button but \the [src] does not respond."))
if(user)
if(issynth)
to_chat(user, span_warning("You send an activation signal to \the [src] but it does not respond."))
else
to_chat(user, span_warning("You press the power button but \the [src] does not respond."))
return FALSE
// Process currently calls handle_power(), may be expanded in future if more things are added.
@@ -572,18 +606,22 @@
if(program.computer != src)
CRASH("tried to open program that does not belong to this computer")
if(!program || !istype(program)) // Program not found or it's not executable program.
if(isnull(program) || !istype(program)) // Program not found or it's not executable program.
if(user)
to_chat(user, span_danger("\The [src]'s screen shows \"I/O ERROR - Unable to run program\" warning."))
return FALSE
if(active_program == program)
return FALSE
// The program is already running. Resume it.
if(program in idle_threads)
active_program?.background_program()
active_program = program
program.alert_pending = FALSE
idle_threads.Remove(program)
if(open_ui)
update_tablet_open_uis(user)
INVOKE_ASYNC(src, PROC_REF(update_tablet_open_uis), user)
update_appearance(UPDATE_ICON)
return TRUE
@@ -603,10 +641,12 @@
if(!program.on_start(user))
return FALSE
active_program?.background_program()
active_program = program
program.alert_pending = FALSE
if(open_ui)
update_tablet_open_uis(user)
INVOKE_ASYNC(src, PROC_REF(update_tablet_open_uis), user)
update_appearance(UPDATE_ICON)
return TRUE
@@ -682,10 +722,11 @@
* It is separated from ui_act() to be overwritten as needed.
*/
/obj/item/modular_computer/proc/toggle_flashlight(mob/user)
if(!has_light || !internal_cell || !internal_cell.charge)
if(!has_light || !internal_cell?.charge)
return FALSE
if(!COOLDOWN_FINISHED(src, disabled_time))
balloon_alert(user, "disrupted!")
if(user)
balloon_alert(user, "disrupted!")
return FALSE
set_light_on(!light_on)
update_appearance()
@@ -0,0 +1,76 @@
///A simple circuit component compatible with stationary consoles, laptops and PDAs, independent from programs.
/obj/item/circuit_component/modpc
display_name = "Modular Computer"
desc = "Circuit for basic functions of a modular computer."
var/obj/item/modular_computer/computer
///Turns the PC on/off
var/datum/port/input/on_off
///When set, will print a piece of paper with the value as text.
var/datum/port/input/print
///Toggles lights on and off. Also RGB.
var/datum/port/input/lights
var/datum/port/input/red
var/datum/port/input/green
var/datum/port/input/blue
/obj/item/circuit_component/modpc/register_shell(atom/movable/shell)
. = ..()
if(istype(shell, /obj/item/modular_computer))
computer = shell
else if(istype(shell, /obj/machinery/modular_computer))
var/obj/machinery/modular_computer/console = shell
computer = console.cpu
/**
* Some mod pc have lights while some don't, but populate_ports()
* is called before we get to know which object this has attahed to,
* I hope you're cool with me doing it here.
*/
if(computer?.has_light)
lights = add_input_port("Toggle Lights", PORT_TYPE_SIGNAL)
red = add_input_port("Red", PORT_TYPE_NUMBER)
green = add_input_port("Green", PORT_TYPE_NUMBER)
blue = add_input_port("Blue", PORT_TYPE_NUMBER)
/obj/item/circuit_component/modpc/unregister_shell(atom/movable/shell)
computer = null
return ..()
/obj/item/circuit_component/modpc/populate_ports()
on_off = add_input_port("Turn On/Off", PORT_TYPE_SIGNAL)
print = add_input_port("Print Text", PORT_TYPE_STRING)
/obj/item/circuit_component/modpc/pre_input_received(datum/port/input/port)
if(isnull(computer))
return
if(COMPONENT_TRIGGERED_BY(print, port))
print.set_value(html_encode(trim(print.value, MAX_PAPER_LENGTH)))
else if(COMPONENT_TRIGGERED_BY(red, port))
red.set_value(clamp(red.value, 0, 255))
else if(COMPONENT_TRIGGERED_BY(blue, port))
blue.set_value(clamp(blue.value, 0, 255))
else if(COMPONENT_TRIGGERED_BY(green, port))
green.set_value(clamp(green.value, 0, 255))
/obj/item/circuit_component/modpc/input_received(datum/port/input/port)
if(isnull(computer))
return
if(COMPONENT_TRIGGERED_BY(on_off, port))
if(computer.enabled)
INVOKE_ASYNC(computer, TYPE_PROC_REF(/obj/item/modular_computer, shutdown_computer))
else
INVOKE_ASYNC(computer, TYPE_PROC_REF(/obj/item/modular_computer, turn_on))
return
if(!computer.enabled)
return
if(COMPONENT_TRIGGERED_BY(print, port))
computer.print_text(print.value)
if(lights)
if(COMPONENT_TRIGGERED_BY(lights, port))
computer.toggle_flashlight()
if(COMPONENT_TRIGGERED_BY(red, port) || COMPONENT_TRIGGERED_BY(green, port) || COMPONENT_TRIGGERED_BY(blue, port))
computer.set_flashlight_color(rgb(red.value || 0, green.value || 0, blue.value || 0))
@@ -3,23 +3,24 @@
///Draws power from its rightful source (area if its a computer, the cell otherwise)
///Takes into account special cases, like silicon PDAs through override, and nopower apps.
/obj/item/modular_computer/proc/use_power(amount = 0)
if(check_power_override())
/obj/item/modular_computer/proc/use_power(amount = 0, check_programs = TRUE)
if(check_power_override(amount))
return TRUE
if(!internal_cell)
return FALSE
if(!internal_cell.charge && (isnull(active_program) || !(active_program.program_flags & PROGRAM_RUNS_WITHOUT_POWER)))
close_all_programs()
for(var/datum/computer_file/program/programs as anything in stored_files)
if((programs.program_flags & PROGRAM_RUNS_WITHOUT_POWER) && open_program(program = programs))
return TRUE
if(internal_cell.use(amount JOULES))
return TRUE
if(!check_programs)
return FALSE
if(!internal_cell.use(amount JOULES))
internal_cell.use(min(amount JOULES, internal_cell.charge)) //drain it anyways.
return FALSE
return TRUE
internal_cell.use(min(amount JOULES, internal_cell.charge)) //drain it anyways.
if(active_program?.program_flags & PROGRAM_RUNS_WITHOUT_POWER)
return TRUE
INVOKE_ASYNC(src, PROC_REF(close_all_programs))
for(var/datum/computer_file/program/programs as anything in stored_files)
if((programs.program_flags & PROGRAM_RUNS_WITHOUT_POWER) && open_program(program = programs))
return TRUE
return FALSE
/obj/item/modular_computer/proc/give_power(amount)
if(internal_cell)
@@ -60,8 +61,8 @@
///Returns TRUE if the PC should not be using any power, FALSE otherwise.
///Checks to see if the current app allows to be ran without power, if so we'll run with it.
/obj/item/modular_computer/proc/check_power_override()
return (!internal_cell?.charge && (active_program?.program_flags & PROGRAM_RUNS_WITHOUT_POWER))
/obj/item/modular_computer/proc/check_power_override(amount)
return !amount && !internal_cell?.charge && (active_program?.program_flags & PROGRAM_RUNS_WITHOUT_POWER)
//Integrated (Silicon) tablets don't drain power, because the tablet is required to state laws, so it being disabled WILL cause problems.
/obj/item/modular_computer/pda/silicon/check_power_override()
@@ -146,7 +146,7 @@
if("PC_minimize")
if(!active_program || (!isnull(internal_cell) && !internal_cell.charge))
return
active_program.background_program()
active_program.background_program(usr)
return TRUE
if("PC_killprogram")
@@ -26,6 +26,8 @@
comp_light_luminosity = 2.3 //this is what old PDAs were set to
looping_sound = FALSE
shell_capacity = SHELL_CAPACITY_SMALL
///The item currently inserted into the PDA, starts with a pen.
var/obj/item/inserted_item = /obj/item/pen
@@ -337,6 +339,10 @@
silicon_owner = null
return ..()
///Silicons don't have the tools (or hands) to make circuits setups with their own PDAs.
/obj/item/modular_computer/pda/silicon/add_shell_component(capacity)
return
/obj/item/modular_computer/pda/silicon/turn_on(mob/user, open_ui = FALSE)
if(silicon_owner?.stat != DEAD)
return ..()
@@ -7,7 +7,8 @@
icon_state = null
icon_state_unpowered = null
icon_state_menu = null
hardware_flag = 0
hardware_flag = NONE
internal_cell = /obj/item/stock_parts/cell/crap
///The modular computer MACHINE that hosts us.
var/obj/machinery/modular_computer/machinery_computer
@@ -25,7 +26,6 @@
physical = loc
machinery_computer = loc
machinery_computer.cpu = src
internal_cell = machinery_computer.internal_cell
hardware_flag = machinery_computer.hardware_flag
steel_sheet_cost = machinery_computer.steel_sheet_cost
max_idle_programs = machinery_computer.max_idle_programs
@@ -44,12 +44,12 @@
machinery_computer = null
return ..()
/obj/item/modular_computer/processor/use_power(amount = 0)
/obj/item/modular_computer/processor/use_power(amount = 0, check_programs = TRUE)
var/obj/machinery/machine_holder = physical
if(machine_holder.powered())
machine_holder.use_power(amount)
return TRUE
return FALSE
return ..()
/obj/item/modular_computer/processor/relay_qdel()
qdel(machinery_computer)
@@ -1,3 +1,5 @@
#define CPU_INTERACTABLE(user) (cpu && !HAS_TRAIT_FROM(src, TRAIT_MODPC_INTERACTING_WITH_FRAME, REF(user)))
// Modular Computer - A machinery that is mostly just a host to the Modular Computer item.
/obj/machinery/modular_computer
name = "modular computer"
@@ -9,8 +11,6 @@
max_integrity = 300
integrity_failure = 0.5
///The power cell, null by default as we use the APC we're in
var/internal_cell = null
///A flag that describes this device type
var/hardware_flag = PROGRAM_CONSOLE
/// Amount of programs that can be ran at once
@@ -41,23 +41,42 @@
. = ..()
cpu = new(src)
cpu.screen_on = TRUE
cpu.add_shell_component(SHELL_CAPACITY_LARGE, SHELL_FLAG_USB_PORT)
update_appearance()
register_context()
/obj/machinery/modular_computer/Destroy()
QDEL_NULL(cpu)
return ..()
/obj/machinery/modular_computer/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
if(isnull(held_item))
context[SCREENTIP_CONTEXT_RMB] = "Toggle processor interaction"
return CONTEXTUAL_SCREENTIP_SET
/obj/machinery/modular_computer/attack_hand_secondary(mob/user, list/modifiers)
. = ..()
if(. == SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN)
return
if(HAS_TRAIT_FROM(src, TRAIT_MODPC_INTERACTING_WITH_FRAME, REF(user)))
REMOVE_TRAIT(src, TRAIT_MODPC_INTERACTING_WITH_FRAME, REF(user))
balloon_alert(user, "now interacting with computer")
else
ADD_TRAIT(src, TRAIT_MODPC_INTERACTING_WITH_FRAME, REF(user))
balloon_alert(user, "now interacting with frame")
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/modular_computer/examine(mob/user)
if(cpu)
return cpu.examine(user)
return ..()
. = cpu?.examine(user) || ..()
. += span_info("You can toggle interaction between computer and its machinery frame with [EXAMINE_HINT("Right-Click")] while empty-handed.")
. += span_info("Currently interacting with [EXAMINE_HINT(HAS_TRAIT_FROM(src, TRAIT_MODPC_INTERACTING_WITH_FRAME, REF(user)) ? "frame" : "computer")].")
/obj/machinery/modular_computer/attack_ghost(mob/dead/observer/user)
. = ..()
if(.)
return
if(cpu)
cpu.attack_ghost(user)
cpu?.attack_ghost(user)
/obj/machinery/modular_computer/emag_act(mob/user, obj/item/card/emag/emag_card)
if(!cpu)
@@ -98,17 +117,14 @@
/obj/machinery/modular_computer/AltClick(mob/user)
. = ..()
if(!can_interact(user))
if(CPU_INTERACTABLE(user) || !can_interact(user))
return
if(cpu)
cpu.AltClick(user)
cpu.AltClick(user)
//ATTACK HAND IGNORING PARENT RETURN VALUE
// On-click handling. Turns on the computer if it's off and opens the GUI.
/obj/machinery/modular_computer/interact(mob/user)
if(cpu)
return cpu.interact(user)
return ..()
return CPU_INTERACTABLE(user) ? cpu.interact(user) : ..()
// Modular computers can have battery in them, we handle power in previous proc, so prevent this from messing it up for us.
/obj/machinery/modular_computer/power_change()
@@ -118,30 +134,33 @@
return
return ..()
///Try to recharge our internal cell if it isn't fully charged.
/obj/machinery/modular_computer/process(seconds_per_tick)
var/obj/item/stock_parts/cell/cell = get_cell()
if(isnull(cell) || cell.percent() >= 100)
return
var/power_to_draw = idle_power_usage * seconds_per_tick * 0.5
if(!use_power_from_net(power_to_draw))
return
cell.give(power_to_draw)
/obj/machinery/modular_computer/get_cell()
return cpu?.internal_cell
/obj/machinery/modular_computer/screwdriver_act(mob/user, obj/item/tool)
if(cpu)
return cpu.screwdriver_act(user, tool)
return ..()
return CPU_INTERACTABLE(user) ? cpu.screwdriver_act(user, tool) : ..()
/obj/machinery/modular_computer/wrench_act_secondary(mob/user, obj/item/tool)
if(cpu)
return cpu.wrench_act_secondary(user, tool)
return ..()
return CPU_INTERACTABLE(user) ? cpu.wrench_act_secondary(user, tool) : ..()
/obj/machinery/modular_computer/welder_act(mob/user, obj/item/tool)
if(cpu)
return cpu.welder_act(user, tool)
return ..()
return CPU_INTERACTABLE(user) ? cpu.welder_act(user, tool) : ..()
/obj/machinery/modular_computer/attackby(obj/item/W as obj, mob/living/user)
if (cpu && !user.combat_mode && !(obj_flags & NO_DECONSTRUCTION))
return cpu.attackby(W, user)
return ..()
/obj/machinery/modular_computer/attackby(obj/item/weapon, mob/living/user)
return (CPU_INTERACTABLE(user) && !user.combat_mode) ? cpu.attackby(weapon, user) : ..()
/obj/machinery/modular_computer/attacked_by(obj/item/attacking_item, mob/living/user)
if (cpu)
return cpu.attacked_by(attacking_item, user)
return ..()
return CPU_INTERACTABLE(user) ? cpu.attacked_by(attacking_item, user) : ..()
// Stronger explosions cause serious damage to internal components
// Minor explosions are mostly mitigitated by casing.
@@ -170,6 +189,6 @@
// "Burn" damage is equally strong against internal components and exterior casing
// "Brute" damage mostly damages the casing.
/obj/machinery/modular_computer/bullet_act(obj/projectile/Proj)
if(cpu)
return cpu.bullet_act(Proj)
return ..()
return cpu?.bullet_act(Proj) || ..()
#undef CPU_INTERACTABLE