Files
Bubberstation/code/modules/wiremod/components/atom/remotecam.dm
Ghom 778ed9f1ab The death or internal/external organ pathing (ft. fixed fox ears and recoloring bodypart overlays with dye sprays) (#87434)
## About The Pull Request
This PR kills the abstract internal and external typepaths for organs,
now replaced by an EXTERNAL_ORGAN flag to distinguish the two kinds.

This PR also fixes fox ears (from #87162, no tail is added) and
mushpeople's caps (they should be red, the screenshot is a tad
outdated).

And yes, you can now use a hair dye spray to recolor body parts like
most tails, podpeople hair, mushpeople caps and cat ears. The process
can be reversed by using the spray again.

## Why It's Good For The Game
Time-Green put some effort during the last few months to untie functions
and mechanics from external/internal organ pathing. Now, all that this
pathing is good for are a few typechecks, easily replaceable with
bitflags.

Also podpeople and mushpeople need a way to recolor their "hair". This
kind of applies to fish tails from the fish infusion, which colors can't
be selected right now. The rest is just there if you ever want to
recolor your lizard tail for some reason.

Proof of testing btw (screenshot taken before mushpeople cap fix, right
side has dyed body parts, moth can't be dyed, they're already fabolous):

![immagine](https://github.com/user-attachments/assets/2bb625c9-9233-42eb-b9b8-e0bd6909ce89)

## Changelog

🆑
code: Removed internal/external pathing from organs in favor of a bit
flag. Hopefully this shouldn't break anything about organs.
fix: Fixed invisible fox ears.
fix: Fixed mushpeople caps not being colored red by default.
add: You can now dye most tails, podpeople hair, mushpeople caps etc.
with a hair dye spray.
/🆑
2024-10-30 08:03:02 +01:00

439 lines
16 KiB
Plaintext

#define REMOTECAM_RANGE_FAR 7
#define REMOTECAM_RANGE_NEAR 2
#define REMOTECAM_ENERGY_USAGE_NEAR 0.003 * STANDARD_CELL_CHARGE //Normal components have 0.001 * STANDARD_CELL_CHARGE, this is expensive to livestream footage
#define REMOTECAM_ENERGY_USAGE_FAR 0.008 * STANDARD_CELL_CHARGE //Far range vision should be expensive, crank this up 8 times
#define REMOTECAM_EMP_RESET 90 SECONDS
/**
* # Remote Camera Component
*
* Attaches a camera for surveillance-on-the-go.
*/
/obj/item/circuit_component/remotecam
display_name = "Camera Abstract Type"
desc = "This is the abstract parent type - do not use this directly!"
category = "Entity"
circuit_flags = CIRCUIT_NO_DUPLICATES
/// Starts the cameraa
var/datum/port/input/start
/// Stops the program.
var/datum/port/input/stop
/// Camera range flag (near/far)
var/datum/port/input/camera_range
/// The network to use
var/datum/port/input/network
/// Allow camera range to be set or not
var/camera_range_settable = TRUE
/// Used only for the BCI shell type, as the COMSIG_MOVABLE_MOVED signal need to be assigned to the user mob, not the shell circuit
var/camera_signal_move_override = FALSE
/// Camera object
var/obj/machinery/camera/shell_camera = null
/// The shell storing the parent circuit
var/atom/movable/shell_parent = null
/// The shell's type (used for prefix naming)
var/camera_prefix = "Camera"
/// Camera random ID
var/c_tag_random = 0
/// Used to store the current process state
var/current_camera_state = FALSE
/// Used to store the current cameranet state
var/current_cameranet_state = TRUE
/// Used to store the camera emp state
var/current_camera_emp = FALSE
/// Used to store the camera emp timer id
var/current_camera_emp_timer_id
/// Used to store the last string used for the camera name
var/current_camera_name = ""
/// Used to store the current camera range setting (near/far)
var/current_camera_range = 0
/// Used to store the last string used for the camera network
var/current_camera_network = ""
/obj/item/circuit_component/remotecam/get_ui_notices()
. = ..()
if(camera_range_settable)
. += create_ui_notice("Energy Usage For Near (0) Range: [display_energy(REMOTECAM_ENERGY_USAGE_NEAR)] Per [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock")
. += create_ui_notice("Energy Usage For Far (1) Range: [display_energy(REMOTECAM_ENERGY_USAGE_FAR)] Per [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock")
else
. += create_ui_notice("Energy Usage While Active: [display_energy(current_camera_range > 0 ? REMOTECAM_ENERGY_USAGE_FAR : REMOTECAM_ENERGY_USAGE_NEAR)] Per [DisplayTimeText(COMP_CLOCK_DELAY)]", "orange", "clock")
/obj/item/circuit_component/remotecam/populate_ports()
start = add_input_port("Start", PORT_TYPE_SIGNAL)
stop = add_input_port("Stop", PORT_TYPE_SIGNAL)
if(camera_range_settable)
camera_range = add_input_port("Camera Range", PORT_TYPE_NUMBER, default = 0)
network = add_input_port("Network", PORT_TYPE_STRING, default = "ss13")
if(camera_range_settable)
current_camera_range = camera_range.value
c_tag_random = rand(1, 999)
/obj/item/circuit_component/remotecam/register_shell(atom/movable/shell)
shell_parent = shell
stop_process()
/obj/item/circuit_component/remotecam/unregister_shell(atom/movable/shell)
stop_process()
remove_camera()
shell_parent = null
/obj/item/circuit_component/remotecam/Destroy()
stop_process()
remove_camera()
shell_parent = null
return ..()
/obj/item/circuit_component/remotecam/input_received(datum/port/input/port)
if(!shell_parent || !shell_camera)
return
update_camera_name_network()
if(COMPONENT_TRIGGERED_BY(start, port))
start_process()
cameranet_add()
current_camera_state = TRUE
else if(COMPONENT_TRIGGERED_BY(stop, port))
stop_process()
close_camera() //Instantly turn off the camera
current_camera_state = FALSE
/**
* Initializes the camera
*/
/obj/item/circuit_component/remotecam/proc/init_camera()
shell_camera.desc = "This camera belongs in a circuit. If you see this, tell a coder!"
shell_camera.AddElement(/datum/element/empprotection, EMP_PROTECT_ALL)
shell_camera.use_power = NO_POWER_USE
shell_camera.start_active = TRUE
shell_camera.internal_light = FALSE
current_camera_name = ""
if(camera_range_settable)
current_camera_range = camera_range.value
current_cameranet_state = TRUE
current_camera_emp = FALSE
current_camera_network = ""
close_camera()
update_camera_range()
update_camera_name_network()
if(current_camera_state)
start_process()
update_camera_location()
else
cameranet_remove() //Remove camera from global cameranet until user activates the camera first
if(!camera_signal_move_override)
RegisterSignal(shell_parent, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
RegisterSignal(shell_parent, COMSIG_ATOM_EMP_ACT, PROC_REF(set_camera_emp))
/**
* Remove the camera
*/
/obj/item/circuit_component/remotecam/proc/remove_camera()
if(!shell_camera)
return
if(!camera_signal_move_override)
UnregisterSignal(shell_parent, COMSIG_MOVABLE_MOVED)
UnregisterSignal(shell_parent, COMSIG_ATOM_EMP_ACT)
if(current_camera_emp)
deltimer(current_camera_emp_timer_id)
current_camera_emp = FALSE
cameranet_add() //Readd camera to cameranet before deleting camera
QDEL_NULL(shell_camera)
/**
* Close the camera state (only if it's already active)
*/
/obj/item/circuit_component/remotecam/proc/close_camera()
if(shell_camera?.camera_enabled)
shell_camera.toggle_cam(null, 0)
/**
* Set the camera range
*/
/obj/item/circuit_component/remotecam/proc/update_camera_range()
shell_camera.setViewRange(current_camera_range > 0 ? REMOTECAM_RANGE_FAR : REMOTECAM_RANGE_NEAR)
/**
* Updates the camera name and network
*/
/obj/item/circuit_component/remotecam/proc/update_camera_name_network()
if(!parent || !parent.display_name || parent.display_name == "")
shell_camera.c_tag = "[camera_prefix]: unspecified #[c_tag_random]"
current_camera_name = ""
else if(current_camera_name != parent.display_name)
current_camera_name = parent.display_name
var/new_cam_name = reject_bad_name(current_camera_name, allow_numbers = TRUE, ascii_only = FALSE, strict = TRUE, cap_after_symbols = FALSE)
//Set camera name using parent circuit name
if(new_cam_name)
shell_camera.c_tag = "[camera_prefix]: [new_cam_name] #[c_tag_random]"
else
shell_camera.c_tag = "[camera_prefix]: unspecified #[c_tag_random]"
if(!network.value || network.value == "")
shell_camera.network = list("ss13")
current_camera_network = ""
else if(current_camera_network != network.value)
current_camera_network = network.value
var/new_net_name = LOWER_TEXT(sanitize(current_camera_network))
//Set camera network string
if(new_net_name)
shell_camera.network = list("[new_net_name]")
else
shell_camera.network = list("ss13")
/**
* Update the chunk for the camera (if enabled)
*/
/obj/item/circuit_component/remotecam/proc/update_camera_location(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
SIGNAL_HANDLER
if(current_camera_state && current_cameranet_state)
GLOB.cameranet.updatePortableCamera(shell_camera, 0.5 SECONDS)
/**
* Add camera from global cameranet
*/
/obj/item/circuit_component/remotecam/proc/cameranet_add()
if(current_cameranet_state)
return
GLOB.cameranet.cameras += shell_camera
GLOB.cameranet.addCamera(shell_camera)
current_cameranet_state = TRUE
/**
* Remove camera from global cameranet
*/
/obj/item/circuit_component/remotecam/proc/cameranet_remove()
if(!current_cameranet_state)
return
GLOB.cameranet.removeCamera(shell_camera)
GLOB.cameranet.cameras -= shell_camera
current_cameranet_state = FALSE
/**
* Set the camera as emp'd
*/
/obj/item/circuit_component/remotecam/proc/set_camera_emp(datum/source, severity, protection)
SIGNAL_HANDLER
if(current_camera_emp)
return
if(!prob(150 / severity))
return
current_camera_emp = TRUE
close_camera()
current_camera_emp_timer_id = addtimer(CALLBACK(src, PROC_REF(remove_camera_emp)), REMOTECAM_EMP_RESET, TIMER_STOPPABLE)
for(var/mob/M as anything in GLOB.player_list)
if (M.client?.eye == shell_camera)
M.reset_perspective(null)
to_chat(M, span_warning("The screen bursts into static!"))
/**
* Restore emp'd camera
*/
/obj/item/circuit_component/remotecam/proc/remove_camera_emp()
current_camera_emp = FALSE
/**
* Adds the component to the SSclock_component process list
*
* Starts draining cell per second while camera is active
*/
/obj/item/circuit_component/remotecam/proc/start_process()
START_PROCESSING(SSclock_component, src)
/**
* Removes the component to the SSclock_component process list
*
* Stops draining cell per second
*/
/obj/item/circuit_component/remotecam/proc/stop_process()
STOP_PROCESSING(SSclock_component, src)
/**
* Handle power usage and camera state updating
*
* This is the generic abstract proc - subtypes with specialized logic should use their own copy of process()
*/
/obj/item/circuit_component/remotecam/process(seconds_per_tick)
if(!shell_parent || !shell_camera)
return PROCESS_KILL
//Camera is currently emp'd
if (current_camera_emp)
close_camera()
return
var/obj/item/stock_parts/power_store/cell = parent.get_cell()
//If cell doesn't exist, or we ran out of power
if(!cell?.use(current_camera_range > 0 ? REMOTECAM_ENERGY_USAGE_FAR : REMOTECAM_ENERGY_USAGE_NEAR))
close_camera()
return
if(camera_range_settable)
//If the camera range has changed, update camera range
if(!camera_range.value != !current_camera_range)
current_camera_range = camera_range.value
update_camera_range()
//Set the camera state (if state has been changed)
if(current_camera_state ^ shell_camera.camera_enabled)
shell_camera.toggle_cam(null, 0)
/obj/item/circuit_component/remotecam/bci
display_name = "BCI Camera"
desc = "Digitizes user's sight for surveillance-on-the-go. User must have fully functional eyes for digitizer to work. Camera range input is either 0 (near) or 1 (far). Network field is used for camera network."
category = "BCI"
camera_prefix = "BCI"
required_shells = list(/obj/item/organ/cyberimp/bci)
/// BCIs are organs, and thus the signal must be assigned ONLY when the shell has been installed in a mob - otherwise the camera will never update position
camera_signal_move_override = TRUE
/// Store the BCI owner as a variable, so we can remove the move signal if the user was gibbed/destroyed while the BCI is still installed
var/mob/living/carbon/bciuser = null
/obj/item/circuit_component/remotecam/drone
display_name = "Remote Camera"
desc = "Capture the surrounding environment for surveillance-on-the-go. Camera range input is either 0 (near) or 1 (far). Network field is used for camera network."
camera_prefix = "Drone"
/obj/item/circuit_component/remotecam/airlock
display_name = "Peephole Camera"
desc = "A peephole camera that captures both sides of the airlock. Network field is used for camera network."
camera_prefix = "Airlock"
/// Hardcode camera to near range
camera_range_settable = FALSE
current_camera_range = 0
/obj/item/circuit_component/remotecam/polaroid
display_name = "Camera Stream Add-On"
desc = "Relays a polaroid camera's feed as a digital stream for surveillance-on-the-go. The camera stream will not work if stored inside of a container like a backpack/box. Network field is used for camera network."
camera_prefix = "Polaroid"
/// Hardcode camera to near range
camera_range_settable = FALSE
current_camera_range = 0
/obj/item/circuit_component/remotecam/bci/register_shell(atom/movable/shell)
. = ..()
if(!istype(shell_parent, /obj/item/organ/cyberimp/bci))
return
shell_camera = new /obj/machinery/camera (shell_parent)
init_camera()
RegisterSignal(shell_parent, COMSIG_ORGAN_IMPLANTED, PROC_REF(on_organ_implanted))
RegisterSignal(shell_parent, COMSIG_ORGAN_REMOVED, PROC_REF(on_organ_removed))
var/obj/item/organ/cyberimp/bci/bci = shell_parent
if(bci.owner) //If somehow the camera was added while shell is already installed inside a mob, assign signals
if(bciuser) //This should never happen... But if it does, unassign move signal from old mob
UnregisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
bciuser = bci.owner
RegisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
/obj/item/circuit_component/remotecam/bci/unregister_shell(atom/movable/shell)
if(shell_camera)
if(bciuser)
UnregisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
bciuser = null
UnregisterSignal(shell_parent, list(COMSIG_ORGAN_IMPLANTED, COMSIG_ORGAN_REMOVED))
return ..()
/obj/item/circuit_component/remotecam/bci/Destroy()
if(shell_camera)
if(bciuser)
UnregisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
bciuser = null
UnregisterSignal(shell_parent, list(COMSIG_ORGAN_IMPLANTED, COMSIG_ORGAN_REMOVED))
return ..()
/obj/item/circuit_component/remotecam/bci/proc/on_organ_implanted(datum/source, mob/living/carbon/owner)
SIGNAL_HANDLER
if(bciuser)
return
bciuser = owner
RegisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
/obj/item/circuit_component/remotecam/bci/proc/on_organ_removed(datum/source, mob/living/carbon/owner)
SIGNAL_HANDLER
if(!bciuser)
return
UnregisterSignal(bciuser, COMSIG_MOVABLE_MOVED, PROC_REF(update_camera_location))
bciuser = null
/obj/item/circuit_component/remotecam/drone/register_shell(atom/movable/shell)
. = ..()
if(!istype(shell_parent, /mob/living/circuit_drone))
return
current_camera_state = FALSE //Always reset camera state for built-in shell components
shell_camera = new /obj/machinery/camera (shell_parent)
init_camera()
/obj/item/circuit_component/remotecam/airlock/register_shell(atom/movable/shell)
. = ..()
if(!istype(shell_parent, /obj/machinery/door/airlock))
return
current_camera_state = FALSE //Always reset camera state for built-in shell components
shell_camera = new /obj/machinery/camera (shell_parent)
init_camera()
/obj/item/circuit_component/remotecam/polaroid/register_shell(atom/movable/shell)
. = ..()
if(!istype(shell_parent, /obj/item/camera))
return
current_camera_state = FALSE //Always reset camera state for built-in shell components
shell_camera = new /obj/machinery/camera (shell_parent)
init_camera()
/obj/item/circuit_component/remotecam/bci/process(seconds_per_tick)
if(!shell_parent || !shell_camera)
return PROCESS_KILL
//Camera is currently emp'd
if (current_camera_emp)
close_camera()
return
var/obj/item/organ/cyberimp/bci/bci = shell_parent
//If shell is not currently inside a head, or user is currently blind, or user is dead
if(!bci.owner || bci.owner.is_blind() || bci.owner.stat >= UNCONSCIOUS)
close_camera()
return
var/obj/item/stock_parts/power_store/cell = parent.get_cell()
//If cell doesn't exist, or we ran out of power
if(!cell?.use(current_camera_range > 0 ? REMOTECAM_ENERGY_USAGE_FAR : REMOTECAM_ENERGY_USAGE_NEAR))
close_camera()
return
//If owner is nearsighted, set camera range to short (if it wasn't already)
if(bci.owner.is_nearsighted_currently())
if(current_camera_range)
current_camera_range = 0
update_camera_range()
//Else if the camera range has changed, update camera range
else if(!camera_range.value != !current_camera_range)
current_camera_range = camera_range.value
update_camera_range()
//Set the camera state (if state has been changed)
if(current_camera_state ^ shell_camera.camera_enabled)
shell_camera.toggle_cam(null, 0)
/obj/item/circuit_component/remotecam/polaroid/process(seconds_per_tick)
if(!shell_parent || !shell_camera)
return PROCESS_KILL
//Camera is currently emp'd
if (current_camera_emp)
close_camera()
return
//If camera is stored inside of bag or something, turn it off
if(shell_parent.loc.atom_storage)
close_camera()
return
var/obj/item/stock_parts/power_store/cell = parent.get_cell()
//If cell doesn't exist, or we ran out of power
if(!cell?.use(REMOTECAM_ENERGY_USAGE_NEAR))
close_camera()
return
//Set the camera state (if state has been changed)
if(current_camera_state ^ shell_camera.camera_enabled)
shell_camera.toggle_cam(null, 0)
#undef REMOTECAM_RANGE_FAR
#undef REMOTECAM_RANGE_NEAR
#undef REMOTECAM_ENERGY_USAGE_NEAR
#undef REMOTECAM_ENERGY_USAGE_FAR
#undef REMOTECAM_EMP_RESET