Files
Bubberstation/code/datums/components/shell.dm
Pickle-Coding c1f11f26ce Converts arbitrary energy units to the joule. Fixes conservation of energy issues relating to charging cells. (#81579)
## About The Pull Request
Removes all arbitrary energy and power units in the codebase. Everything
is replaced with the joule and watt, with 1 = 1 joule, or 1 watt if you
are going to multiply by time. This is a visible change, where all
arbitrary energy units you see in the game will get proper prefixed
units of energy.

With power cells being converted to the joule, charging one joule of a
power cell will require one joule of energy.

The grid will now store energy, instead of power. When an energy usage
is described as using the watt, a power to energy conversion based on
the relevant subsystem's timing (usually multiplying by seconds_per_tick
or applying power_to_energy()) is needed before adding or removing from
the grid. Power usages that are described as the watt is really anything
you would scale by time before applying the load. If it's described as a
joule, no time conversion is needed. Players will still read the grid as
power, having no visible change.

Machines that dynamically use power with the use_power() proc will
directly drain from the grid (and apc cell if there isn't enough)
instead of just tallying it up on the dynamic power usages for the area.
This should be more robust at conserving energy as the surplus is
updated on the go, preventing charging cells from nothing.

APCs no longer consume power for the dynamic power usage channels. APCs
will consume power for static power usages. Because static power usages
are added up without checking surplus, static power consumption will be
applied before any machine processes. This will give a more truthful
surplus for dynamic power consumers.

APCs will display how much power it is using for charging the cell. APC
cell charging applies power in its own channel, which gets added up to
the total. This will prevent invisible power usage you see when looking
at the power monitoring console.

After testing in MetaStation, I found roundstart power consumption to be
around 406kW after all APCs get fully charged. During the roundstart APC
charge rush, the power consumption can get as high as over 2MW (up to
25kW per roundstart APC charging) as long as there's that much
available.

Because of the absurd potential power consumption of charging APCs near
roundstart, I have changed how APCs decide to charge. APCs will now
charge only after all other machines have processed in the machines
processing subsystem. This will make sure APC charging won't disrupt
machines taking from the grid, and should stop APCs getting their power
drained due to others demanding too much power while charging. I have
removed the delays for APC charging too, so they start charging
immediately whenever there's excess power. It also stops them turning
red when a small amount of cell gets drained (airlocks opening and shit
during APC charge rush), as they immediately become fully charged
(unless too much energy got drained somehow) before changing icon.

Engineering SMES now start at 100% charge instead of 75%. I noticed
cells were draining earlier than usual after these changes, so I am
making them start maxed to try and combat that.

These changes will fix all conservation of energy issues relating to
charging powercells.
## Why It's Good For The Game
Closes #73438
Closes #75789
Closes #80634
Closes #82031

Makes it much easier to interface with the power system in the codebase.
It's more intuitive. Removes a bunch of conservation of energy issues,
making energy and power much more meaningful. It will help the
simulation remain immersive as players won't encounter energy
duplication so easily. Arbitrary energy units getting replaced with the
joule will also tell people more meaningful information when reading it.
APC charging will feel more snappy.
## Changelog
🆑
fix: Fixes conservation of energy issues relating to charging
powercells.
qol: APCs will display how much power they are using to charge their
cell. This is accounted for in the power monitoring console.
qol: All arbitrary power cell energy units you see are replaced with
prefixed joules.
balance: As a consequence of the conservation of energy issues getting
fixed, the power consumption for charging cells is now very significant.
balance: APCs only use surplus power from the grid after every machine
processes when charging, preventing APCs from causing others to
discharge while charging.
balance: Engineering SMES start at max charge to combat the increased
energy loss due to conservation of energy fixes.
/🆑

---------

Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
2024-03-23 16:58:56 +01:00

394 lines
14 KiB
Plaintext

/// Makes an atom a shell that is able to take in an attached circuit.
/datum/component/shell
dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
/// The circuitboard attached to this shell
var/obj/item/integrated_circuit/attached_circuit
/// Flags containing what this shell can do
var/shell_flags = NONE
/// The capacity of the shell.
var/capacity = INFINITY
/// A list of components that cannot be removed
var/list/obj/item/circuit_component/unremovable_circuit_components
/// Whether the shell is locked or not
var/locked = FALSE
// The variables below are used only for anchored shells
/// The amount of power used in the last minute
var/power_used_in_minute = 0
/// The cooldown time to reset the power_used_in_minute to 0
COOLDOWN_DECLARE(power_used_cooldown)
/// The maximum power that the shell can use in a minute before entering overheating and destroying itself.
var/max_power_use_in_minute = 20000
/datum/component/shell/Initialize(unremovable_circuit_components, capacity, shell_flags, starting_circuit)
. = ..()
if(!ismovable(parent))
return COMPONENT_INCOMPATIBLE
src.shell_flags = shell_flags || src.shell_flags
src.capacity = capacity || src.capacity
set_unremovable_circuit_components(unremovable_circuit_components)
if(starting_circuit)
attach_circuit(starting_circuit)
/datum/component/shell/RegisterWithParent()
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
RegisterSignal(parent, COMSIG_ATOM_ATTACK_GHOST, PROC_REF(on_attack_ghost))
if(!(shell_flags & SHELL_FLAG_CIRCUIT_UNMODIFIABLE))
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_MULTITOOL), PROC_REF(on_multitool_act))
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attack_by))
if(!(shell_flags & SHELL_FLAG_CIRCUIT_UNREMOVABLE))
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER), PROC_REF(on_screwdriver_act))
RegisterSignal(parent, COMSIG_OBJ_DECONSTRUCT, PROC_REF(on_object_deconstruct))
if(shell_flags & SHELL_FLAG_REQUIRE_ANCHOR)
RegisterSignal(parent, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(on_set_anchored))
RegisterSignal(parent, COMSIG_ATOM_USB_CABLE_TRY_ATTACH, PROC_REF(on_atom_usb_cable_try_attach))
RegisterSignal(parent, COMSIG_MOVABLE_CIRCUIT_LOADED, PROC_REF(on_load))
/datum/component/shell/proc/set_unremovable_circuit_components(list/components)
if(unremovable_circuit_components)
QDEL_LIST(unremovable_circuit_components)
unremovable_circuit_components = list()
for(var/obj/item/circuit_component/circuit_component as anything in components)
add_unremovable_circuit_component(circuit_component)
/datum/component/shell/proc/add_unremovable_circuit_component(obj/item/circuit_component/component)
if(ispath(component))
component = new component()
component.removable = FALSE
component.set_circuit_size(0)
RegisterSignal(component, COMSIG_CIRCUIT_COMPONENT_SAVE, PROC_REF(save_component))
unremovable_circuit_components += component
/datum/component/shell/proc/save_component(datum/source, list/objects)
SIGNAL_HANDLER
objects += parent
/datum/component/shell/proc/on_load(datum/source, obj/item/integrated_circuit/circuit, list/components)
SIGNAL_HANDLER
var/list/components_in_list = list()
for(var/obj/item/circuit_component/component as anything in components)
components_in_list += component.type
for(var/obj/item/circuit_component/component as anything in unremovable_circuit_components)
if(component.type in components_in_list)
continue
var/new_type = component.type
components += new new_type()
set_unremovable_circuit_components(components)
attach_circuit(circuit)
/datum/component/shell/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_ATOM_ATTACKBY,
COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER),
COMSIG_ATOM_TOOL_ACT(TOOL_MULTITOOL),
COMSIG_OBJ_DECONSTRUCT,
COMSIG_MOVABLE_SET_ANCHORED,
COMSIG_ATOM_EXAMINE,
COMSIG_ATOM_ATTACK_GHOST,
COMSIG_ATOM_USB_CABLE_TRY_ATTACH,
COMSIG_MOVABLE_CIRCUIT_LOADED,
))
QDEL_NULL(attached_circuit)
/datum/component/shell/Destroy(force)
QDEL_LIST(unremovable_circuit_components)
return ..()
/datum/component/shell/proc/on_object_deconstruct()
SIGNAL_HANDLER
if(!attached_circuit)
return
if(attached_circuit.admin_only)
return
if(shell_flags & SHELL_FLAG_CIRCUIT_UNREMOVABLE)
return
remove_circuit()
/datum/component/shell/proc/on_attack_ghost(datum/source, mob/dead/observer/ghost)
SIGNAL_HANDLER
if(!is_authorized(ghost))
return
if(attached_circuit)
INVOKE_ASYNC(attached_circuit, TYPE_PROC_REF(/datum, ui_interact), ghost)
/datum/component/shell/proc/on_examine(atom/movable/source, mob/user, list/examine_text)
SIGNAL_HANDLER
if(!is_authorized(user))
return
if(!attached_circuit)
examine_text += span_notice("There is no integrated circuit attached.")
return
examine_text += span_notice("There is an integrated circuit attached. Use a multitool to access the wiring. Use a screwdriver to remove it from [source].")
examine_text += span_notice("The cover panel to the integrated circuit is [locked? "locked" : "unlocked"].")
var/obj/item/stock_parts/cell/cell = attached_circuit.cell
examine_text += span_notice("The charge meter reads [cell ? round(cell.percent(), 1) : 0]%.")
if (shell_flags & SHELL_FLAG_USB_PORT)
examine_text += span_notice("There is a <b>USB port</b> on the front.")
if(shell_flags & SHELL_FLAG_REQUIRE_ANCHOR)
examine_text += span_notice("The shell does not require a battery to function and will draw from the area's APC whenever possible.")
if(!source.anchored)
examine_text += span_danger("<b>The integrated circuit is non-functional whilst the shell is unanchored.</b>")
/**
* Called when the shell is anchored.
*
* Only applies if the shell has SHELL_FLAG_REQUIRE_ANCHOR.
* Disables the integrated circuit if unanchored, otherwise enable the circuit.
*/
/datum/component/shell/proc/on_set_anchored(atom/movable/source, previous_value)
SIGNAL_HANDLER
attached_circuit?.set_on(source.anchored)
/**
* Called when an item hits the parent. This is the method to add the circuitboard to the component.
*/
/datum/component/shell/proc/on_attack_by(atom/source, obj/item/item, mob/living/attacker)
SIGNAL_HANDLER
if(!is_authorized(attacker))
return
if(istype(item, /obj/item/stock_parts/cell))
source.balloon_alert(attacker, "can't put cell in directly!")
return
if(istype(item, /obj/item/inducer))
var/obj/item/inducer/inducer = item
INVOKE_ASYNC(inducer, TYPE_PROC_REF(/obj/item, attack_atom), attached_circuit, attacker, list())
return COMPONENT_NO_AFTERATTACK
if(attached_circuit)
if(attached_circuit.owner_id && item == attached_circuit.owner_id.resolve())
set_locked(!locked)
source.balloon_alert(attacker, "[locked ? "locked" : "unlocked"] [source]")
return COMPONENT_NO_AFTERATTACK
if(!attached_circuit.owner_id && isidcard(item))
source.balloon_alert(attacker, "owner id set for [item]")
attached_circuit.owner_id = WEAKREF(item)
return COMPONENT_NO_AFTERATTACK
if(istype(item, /obj/item/circuit_component))
attached_circuit.add_component_manually(item, attacker)
return COMPONENT_NO_AFTERATTACK
if(!istype(item, /obj/item/integrated_circuit))
return
var/obj/item/integrated_circuit/logic_board = item
. = COMPONENT_NO_AFTERATTACK
if(logic_board.shell) // I'll be surprised if this ever happens
return
if(attached_circuit)
source.balloon_alert(attacker, "there is already a circuitboard inside!")
return
if(logic_board.current_size > capacity)
source.balloon_alert(attacker, "this is too large to fit into [parent]!")
return
logic_board.inserter_mind = WEAKREF(attacker.mind)
attach_circuit(logic_board, attacker)
/// Sets whether the shell is locked or not
/datum/component/shell/proc/set_locked(new_value)
locked = new_value
attached_circuit?.set_locked(new_value)
/datum/component/shell/proc/on_multitool_act(atom/source, mob/user, obj/item/tool)
SIGNAL_HANDLER
if(!is_authorized(user))
return
if(!attached_circuit)
return
if(locked)
if(shell_flags & SHELL_FLAG_ALLOW_FAILURE_ACTION)
return
source.balloon_alert(user, "it's locked!")
return ITEM_INTERACT_BLOCKING
attached_circuit.interact(user)
return ITEM_INTERACT_BLOCKING
/**
* Called when a screwdriver is used on the parent. Removes the circuitboard from the component.
*/
/datum/component/shell/proc/on_screwdriver_act(atom/source, mob/user, obj/item/tool)
SIGNAL_HANDLER
if(!is_authorized(user))
return
if(!attached_circuit)
return
if(locked)
if(shell_flags & SHELL_FLAG_ALLOW_FAILURE_ACTION)
return
source.balloon_alert(user, "it's locked!")
return ITEM_INTERACT_BLOCKING
tool.play_tool_sound(parent)
source.balloon_alert(user, "you unscrew [attached_circuit] from [parent].")
remove_circuit()
return ITEM_INTERACT_BLOCKING
/**
* Checks for when the circuitboard moves. If it moves, removes it from the component.
*/
/datum/component/shell/proc/on_circuit_moved(obj/item/integrated_circuit/circuit, atom/old_loc)
SIGNAL_HANDLER
if(circuit.loc != parent)
remove_circuit()
/**
* Checks for when the circuitboard deletes so that it can be unassigned.
*/
/datum/component/shell/proc/on_circuit_delete(datum/source)
SIGNAL_HANDLER
remove_circuit()
/datum/component/shell/proc/on_circuit_add_component_manually(atom/source, obj/item/circuit_component/added_comp, mob/living/user)
SIGNAL_HANDLER
if(locked)
source.balloon_alert(user, "it's locked!")
return COMPONENT_CANCEL_ADD_COMPONENT
if(attached_circuit.current_size + added_comp.circuit_size > capacity)
source.balloon_alert(user, "it won't fit!")
return COMPONENT_CANCEL_ADD_COMPONENT
/datum/component/shell/proc/override_power_usage(datum/source, power_to_use)
SIGNAL_HANDLER
if(COOLDOWN_FINISHED(src, power_used_cooldown))
power_used_in_minute = 0
var/area/location = get_area(parent)
if(!location.powered(AREA_USAGE_EQUIP))
return
if(power_used_in_minute > max_power_use_in_minute)
explosion(parent, light_impact_range = 1, explosion_cause = attached_circuit)
if(attached_circuit)
remove_circuit()
return
location.apc?.terminal?.use_energy(power_to_use, channel = AREA_USAGE_EQUIP)
power_used_in_minute += power_to_use
COOLDOWN_START(src, power_used_cooldown, 1 MINUTES)
return COMPONENT_OVERRIDE_POWER_USAGE
/**
* Attaches a circuit to the parent. Doesn't do any checks to see for any existing circuits so that should be done beforehand.
*/
/datum/component/shell/proc/attach_circuit(obj/item/integrated_circuit/circuitboard, mob/living/user)
var/atom/movable/parent_atom = parent
if(user && !user.transferItemToLoc(circuitboard, parent_atom))
return
locked = FALSE
attached_circuit = circuitboard
SEND_SIGNAL(src, COMSIG_SHELL_CIRCUIT_ATTACHED)
if(!(shell_flags & SHELL_FLAG_CIRCUIT_UNREMOVABLE) && !circuitboard.admin_only)
RegisterSignal(circuitboard, COMSIG_MOVABLE_MOVED, PROC_REF(on_circuit_moved))
if(shell_flags & SHELL_FLAG_REQUIRE_ANCHOR)
RegisterSignal(circuitboard, COMSIG_CIRCUIT_PRE_POWER_USAGE, PROC_REF(override_power_usage))
RegisterSignal(circuitboard, COMSIG_QDELETING, PROC_REF(on_circuit_delete))
for(var/obj/item/circuit_component/to_add as anything in unremovable_circuit_components)
to_add.forceMove(attached_circuit)
attached_circuit.add_component(to_add)
RegisterSignal(circuitboard, COMSIG_CIRCUIT_ADD_COMPONENT_MANUALLY, PROC_REF(on_circuit_add_component_manually))
if(attached_circuit.display_name != "")
parent_atom.name = "[initial(parent_atom.name)] ([attached_circuit.display_name])"
attached_circuit.set_locked(FALSE)
if((shell_flags & SHELL_FLAG_CIRCUIT_UNREMOVABLE) || circuitboard.admin_only)
circuitboard.moveToNullspace()
else if(circuitboard.loc != parent_atom)
circuitboard.forceMove(parent_atom)
attached_circuit.set_shell(parent_atom)
// call after set_shell() sets on to true
if(shell_flags & SHELL_FLAG_REQUIRE_ANCHOR)
attached_circuit.set_on(parent_atom.anchored)
/**
* Removes the circuit from the component. Doesn't do any checks to see for an existing circuit so that should be done beforehand.
*/
/datum/component/shell/proc/remove_circuit()
// remove_current_shell() also turns off the circuit
attached_circuit.remove_current_shell()
UnregisterSignal(attached_circuit, list(
COMSIG_MOVABLE_MOVED,
COMSIG_QDELETING,
COMSIG_CIRCUIT_ADD_COMPONENT_MANUALLY,
COMSIG_CIRCUIT_PRE_POWER_USAGE,
))
if(attached_circuit.loc == parent || (!QDELETED(attached_circuit) && attached_circuit.loc == null))
var/atom/parent_atom = parent
attached_circuit.forceMove(parent_atom.drop_location())
for(var/obj/item/circuit_component/to_remove as anything in unremovable_circuit_components)
attached_circuit.remove_component(to_remove)
to_remove.moveToNullspace()
attached_circuit.set_locked(FALSE)
SEND_SIGNAL(src, COMSIG_SHELL_CIRCUIT_REMOVED)
attached_circuit = null
/datum/component/shell/proc/on_atom_usb_cable_try_attach(atom/source, obj/item/usb_cable/usb_cable, mob/user)
SIGNAL_HANDLER
if(!is_authorized(user))
return
if (!(shell_flags & SHELL_FLAG_USB_PORT))
source.balloon_alert(user, "this shell has no usb ports")
return COMSIG_CANCEL_USB_CABLE_ATTACK
if (isnull(attached_circuit))
source.balloon_alert(user, "no circuit inside")
return COMSIG_CANCEL_USB_CABLE_ATTACK
if(attached_circuit.locked)
source.balloon_alert(user, "circuit is locked!")
return COMSIG_CANCEL_USB_CABLE_ATTACK
usb_cable.attached_circuit = attached_circuit
return COMSIG_USB_CABLE_CONNECTED_TO_CIRCUIT
/**
* Determines if a user is authorized to see the existance of this shell. Returns false if they are not
*
* Arguments:
* * user - The user to check if they are authorized
*/
/datum/component/shell/proc/is_authorized(mob/user)
if((shell_flags & SHELL_FLAG_CIRCUIT_UNREMOVABLE) && (shell_flags & SHELL_FLAG_CIRCUIT_UNMODIFIABLE))
return FALSE
if(attached_circuit?.admin_only)
if(check_rights_for(user.client, R_VAREDIT))
return TRUE
return FALSE
return TRUE