diff --git a/aurorastation.dme b/aurorastation.dme
index e21a90788ed..1e134ff5ce5 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -1053,6 +1053,7 @@
#include "code\game\objects\items\devices\taperecorder.dm"
#include "code\game\objects\items\devices\traitordevices.dm"
#include "code\game\objects\items\devices\transfer_valve.dm"
+#include "code\game\objects\items\devices\tvcamera.dm"
#include "code\game\objects\items\devices\uplink.dm"
#include "code\game\objects\items\devices\uplink_random_lists.dm"
#include "code\game\objects\items\devices\versebook.dm"
diff --git a/code/__DEFINES/machinery.dm b/code/__DEFINES/machinery.dm
index 4dd82657e11..2e57c309280 100644
--- a/code/__DEFINES/machinery.dm
+++ b/code/__DEFINES/machinery.dm
@@ -82,6 +82,7 @@
#define NETWORK_THIRD_DECK "Third Deck"
#define NETWORK_INTREPID "Intrepid"
#define NETWORK_CANARY "Canary"
+#define NETWORK_NEWS "News"
// Those networks can only be accessed by pre-existing terminals. AIs and new terminals can't use them.
diff --git a/code/game/jobs/job/civilian.dm b/code/game/jobs/job/civilian.dm
index 235c2380399..c86ed1cd4e7 100644
--- a/code/game/jobs/job/civilian.dm
+++ b/code/game/jobs/job/civilian.dm
@@ -238,7 +238,8 @@
messengerbag_faction = /obj/item/storage/backpack/messenger/nt
backpack_contents = list(
- /obj/item/clothing/accessory/badge/press = 1
+ /obj/item/clothing/accessory/badge/press = 1,
+ /obj/item/device/tvcamera = 1
)
/datum/outfit/job/journalistf
@@ -253,7 +254,8 @@
tablet = /obj/item/modular_computer/handheld/preset/civilian/librarian
backpack_contents = list(
- /obj/item/clothing/accessory/badge/press/independent = 1
+ /obj/item/clothing/accessory/badge/press/independent = 1,
+ /obj/item/device/tvcamera = 1
)
diff --git a/code/game/machinery/camera/presets.dm b/code/game/machinery/camera/presets.dm
index 2608195c214..0e402ce2db2 100644
--- a/code/game/machinery/camera/presets.dm
+++ b/code/game/machinery/camera/presets.dm
@@ -77,6 +77,9 @@ var/global/list/engineering_networks = list(
/obj/machinery/camera/network/thunder
network = list(NETWORK_THUNDER)
+/obj/machinery/camera/network/news
+ network = list(NETWORK_NEWS)
+
// EMP
/obj/machinery/camera/emp_proof/Initialize()
diff --git a/code/game/objects/items/devices/tvcamera.dm b/code/game/objects/items/devices/tvcamera.dm
new file mode 100644
index 00000000000..9146459ac83
--- /dev/null
+++ b/code/game/objects/items/devices/tvcamera.dm
@@ -0,0 +1,151 @@
+/obj/item/device/tvcamera
+ name = "press camera drone"
+ desc = "An Ingi Usang Entertainment Co. livestreaming press camera drone. Weapon of choice for war correspondents and reality show cameramen. It does not appear to have any internal memory storage."
+ icon_state = "camcorder"
+ item_state = "camcorder"
+ w_class = ITEMSIZE_LARGE
+ slot_flags = SLOT_BELT
+ var/channel = "General News Feed"
+ var/obj/machinery/camera/network/news/camera
+ var/obj/item/device/radio/radio
+
+/obj/item/device/tvcamera/Destroy()
+ GLOB.listening_objects -= src
+ QDEL_NULL(camera)
+ QDEL_NULL(radio)
+ . = ..()
+
+/obj/item/device/tvcamera/Initialize()
+ camera = new(src)
+ camera.c_tag = channel
+ camera.status = FALSE
+ radio = new(src)
+ radio.get_frequency(FALSE)
+ radio.set_frequency(ENT_FREQ)
+ GLOB.listening_objects += src
+ . = ..()
+
+/obj/item/device/tvcamera/examine(mob/user)
+ . = ..()
+ to_chat(user, "Video feed is currently: [camera.status ? "Online" : "Offline"]")
+ to_chat(user, "Audio feed is currently: [radio.get_broadcasting() ? "Online" : "Offline"]")
+
+/obj/item/device/tvcamera/attack_self(mob/user)
+ add_fingerprint(user)
+ user.set_machine(src)
+ var/dat = list()
+ dat += "Channel name is: [channel ? channel : "unidentified broadcast"]
"
+ dat += "Video streaming is: [camera.status ? "Online" : "Offline"]
"
+ dat += "Microphone is: [radio.get_broadcasting() ? "Online" : "Offline"]
"
+ dat += "Sound is being broadcasted on frequency: [format_frequency(radio.get_frequency())] ([get_frequency_name(radio.get_frequency())])
"
+ var/datum/browser/popup = new(user, "Press Camera Drone", "EyeBuddy", 300, 390, src)
+ popup.set_content(jointext(dat,null))
+ popup.open()
+
+/obj/item/device/tvcamera/Topic(bred, href_list, state = physical_state)
+ if(..())
+ return 1
+ if(href_list["channel"])
+ var/nc = tgui_input_text(usr, "Select new channel name", "Channel Name", channel)
+ if(nc)
+ channel = nc
+ camera.c_tag = channel
+ to_chat(usr, SPAN_WARNING("New channel name: '[channel]' has been set."))
+ if(href_list["video"])
+ camera.set_status(!camera.status)
+ if(camera.status)
+ balloon_alert(usr, SPAN_NOTICE("Video streaming: Activated. Broadcasting on channel: [channel]"))
+ playsound(src.loc, 'sound/machines/ping.ogg', 50, 1)
+ else
+ balloon_alert(usr, SPAN_NOTICE("Video streaming: Deactivated."))
+ playsound(src.loc, 'sound/machines/buzz-sigh.ogg', 50, 1)
+ update_icon()
+ if(href_list["sound"])
+ radio.set_broadcasting(!radio.get_broadcasting())
+ if(radio.get_broadcasting())
+ balloon_alert(usr, SPAN_NOTICE("Audio streaming: Activated. Broadcasting on frequency: [format_frequency(radio.get_frequency())]."))
+ playsound(src.loc, 'sound/machines/ping.ogg', 50, 1)
+ else
+ balloon_alert(usr, SPAN_NOTICE("Audio streaming: Deactivated."))
+ playsound(src.loc, 'sound/machines/buzz-sigh.ogg', 50, 1)
+ if(!href_list["close"])
+ attack_self(usr)
+
+/obj/item/device/tvcamera/update_icon()
+ ..()
+ if(camera.status)
+ icon_state = "camcorder_on"
+ item_state = "camcorder_on"
+ else
+ icon_state = "camcorder"
+ item_state = "camcorder"
+ var/mob/living/carbon/human/H = loc
+ if(istype(H))
+ H.update_inv_r_hand(0)
+ H.update_inv_l_hand()
+
+/* Assembly by a roboticist */
+/obj/item/robot_parts/head/attackby(var/obj/item/device/assembly/S, mob/user as mob)
+ if(!istype(S, /obj/item/device/assembly/infra))
+ ..()
+ return
+ var/obj/item/tv_assembly/A = new(user)
+ qdel(S)
+ user.put_in_hands(A)
+ to_chat(user, SPAN_NOTICE("You add the infrared sensor to the robot head."))
+ qdel(src)
+
+/* Using camcorder icon as I can't sprite.
+Using robohead because of restricting to roboticist */
+/obj/item/tv_assembly
+ name = "tv camera assembly"
+ desc = "A robotic head with an infrared sensor inside."
+ icon = 'icons/obj/robot_parts.dmi'
+ icon_state = "head"
+ item_state = "head"
+ var/buildstep = 0
+ w_class = ITEMSIZE_LARGE
+
+/obj/item/tv_assembly/attackby(var/obj/item/W, var/mob/user)
+ switch(buildstep)
+ if(0)
+ if(istype(W, /obj/item/robot_parts/robot_component/camera))
+ to_chat(user, SPAN_NOTICE("You add the camera module to [src]."))
+ qdel(W)
+ desc = "This TV camera assembly has a camera module."
+ buildstep++
+ if(1)
+ if(istype(W, /obj/item/device/taperecorder))
+ qdel(W)
+ buildstep++
+ to_chat(user, SPAN_NOTICE("You add the tape recorder to [src]."))
+ desc = "This TV camera assembly has a camera and audio module."
+ return
+ if(2)
+ if(W.iscoil())
+ var/obj/item/stack/cable_coil/C = W
+ if(!C.use(3))
+ to_chat(user, SPAN_NOTICE("You need three cable coils to wire the devices."))
+ ..()
+ return
+ buildstep++
+ to_chat(user, SPAN_NOTICE("You wire the assembly."))
+ desc = "This TV camera assembly has wires sticking out."
+ return
+ if(3)
+ if(W.iswirecutter())
+ to_chat(user, SPAN_NOTICE("You trim the wires."))
+ buildstep++
+ desc = "This TV camera assembly needs casing."
+ return
+ if(4)
+ if(istype(W, /obj/item/stack/material/steel))
+ var/obj/item/stack/material/steel/S = W
+ if(S.use(1))
+ buildstep++
+ to_chat(user, SPAN_NOTICE("You encase the assembly."))
+ var/turf/T = get_turf(src)
+ new /obj/item/device/tvcamera(T)
+ qdel(src)
+ return
+ ..()
diff --git a/code/game/objects/structures/crates_lockers/closets/secure/service.dm b/code/game/objects/structures/crates_lockers/closets/secure/service.dm
index 633833462ec..69f9e25aadf 100644
--- a/code/game/objects/structures/crates_lockers/closets/secure/service.dm
+++ b/code/game/objects/structures/crates_lockers/closets/secure/service.dm
@@ -56,3 +56,20 @@
new /obj/item/clothing/suit/caution(src)
new /obj/item/storage/box/lights/mixed(src)
new /obj/item/storage/box/mousetraps(src)
+
+// Journalist's Locker
+/obj/structure/closet/secure_closet/journalist
+ name = "journalist's closet"
+ desc = "It's a storage unit for a journalist's gear."
+ req_access = list(ACCESS_JOURNALIST)
+
+/obj/structure/closet/secure_closet/journalist/fill()
+ new /obj/item/storage/photo_album(src)
+ new /obj/item/device/camera_film(src)
+ new /obj/item/device/camera(src)
+ new /obj/item/paper_scanner(src)
+ new /obj/item/folder/white(src)
+ new /obj/item/storage/secure/briefcase(src)
+ new /obj/item/clipboard(src)
+ new /obj/item/folder/red(src)
+ new /obj/item/device/tvcamera(src)
diff --git a/code/modules/modular_computers/file_system/programs/security/camera.dm b/code/modules/modular_computers/file_system/programs/security/camera.dm
index 3ac28c56857..bf134b2549c 100644
--- a/code/modules/modular_computers/file_system/programs/security/camera.dm
+++ b/code/modules/modular_computers/file_system/programs/security/camera.dm
@@ -8,7 +8,7 @@
return
switch(network)
- if(NETWORK_THUNDER)
+ if(NETWORK_THUNDER, NETWORK_NEWS)
return FALSE
if(NETWORK_REACTOR,NETWORK_ENGINEERING,NETWORK_ENGINEERING_OUTPOST,NETWORK_ALARM_ATMOS,NETWORK_ALARM_FIRE,NETWORK_ALARM_POWER)
return ACCESS_ENGINE
@@ -223,3 +223,21 @@
networks.Add(list(list("tag" = NETWORK_ERT, "has_access" = 1)))
networks.Add(list(list("tag" = NETWORK_CRESCENT, "has_access" = 1)))
return networks
+
+/datum/computer_file/program/camera_monitor/news
+ filename = "idcammon"
+ filedesc = "Journalism Camera Monitoring"
+ extended_desc = "This program allows remote access to station's camera system. Some camera networks may have additional access requirements. This version has has a connection to news networks."
+ size = 6
+ nanomodule_path = /datum/nano_module/camera_monitor/news
+ required_access_download = null
+ usage_flags = PROGRAM_ALL
+
+/datum/nano_module/camera_monitor/news
+ name = "Journalism Camera Monitoring Program"
+
+/datum/computer_file/program/camera_monitor/news/modify_networks_list(var/list/networks)
+ networks = list()
+ networks.Add(list(list("tag" = NETWORK_NEWS, "has_access" = 1)))
+ networks.Add(list(list("tag" = NETWORK_THUNDER, "has_access" = 1)))
+ return networks
diff --git a/html/changelogs/SimpleMaroon-cameradrone.yml b/html/changelogs/SimpleMaroon-cameradrone.yml
new file mode 100644
index 00000000000..d3992c1414d
--- /dev/null
+++ b/html/changelogs/SimpleMaroon-cameradrone.yml
@@ -0,0 +1,43 @@
+################################
+# Example Changelog File
+#
+# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
+#
+# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
+# When it is, any changes listed below will disappear.
+#
+# Valid Prefixes:
+# bugfix
+# wip (For works in progress)
+# tweak
+# soundadd
+# sounddel
+# rscadd (general adding of nice things)
+# rscdel (general deleting of nice things)
+# imageadd
+# imagedel
+# maptweak
+# spellcheck (typo fixes)
+# experiment
+# balance
+# admin
+# backend
+# security
+# refactor
+#################################
+
+# Your name.
+author: LonelyHowls, SimpleMaroon
+
+# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
+delete-after: True
+
+# Any changes you've made. See valid prefix list above.
+# INDENT WITH TWO SPACES. NOT TABS. SPACES.
+# SCREW THIS UP AND IT WON'T WORK.
+# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries.
+# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog.
+changes:
+ - rscadd: "Ports a journalist camera to Aurora! This allows the journalist to broadcast their antics to the wider crew. This item can also be built by machinists."
+ - rscadd: "Adds a new News network. Currently only used for cameras."
+ - rscadd: "To properly implement cameras, allows any arbitrary individual to DL the camera application onto their modular computer of choice. Only the News network will be available by default."
diff --git a/icons/mob/belt.dmi b/icons/mob/belt.dmi
index 882722c1ac0..f2b52800870 100644
Binary files a/icons/mob/belt.dmi and b/icons/mob/belt.dmi differ
diff --git a/icons/mob/items/device/lefthand_device.dmi b/icons/mob/items/device/lefthand_device.dmi
index a18d90369d3..a143a1d38c1 100644
Binary files a/icons/mob/items/device/lefthand_device.dmi and b/icons/mob/items/device/lefthand_device.dmi differ
diff --git a/icons/mob/items/device/righthand_device.dmi b/icons/mob/items/device/righthand_device.dmi
index 4bc86f5adc1..777c0de18c3 100644
Binary files a/icons/mob/items/device/righthand_device.dmi and b/icons/mob/items/device/righthand_device.dmi differ
diff --git a/icons/obj/device.dmi b/icons/obj/device.dmi
index f1feebd653a..164424eb7de 100644
Binary files a/icons/obj/device.dmi and b/icons/obj/device.dmi differ
diff --git a/maps/sccv_horizon/code/sccv_horizon.dm b/maps/sccv_horizon/code/sccv_horizon.dm
index b6c6606496b..4a2a3941c87 100644
--- a/maps/sccv_horizon/code/sccv_horizon.dm
+++ b/maps/sccv_horizon/code/sccv_horizon.dm
@@ -60,7 +60,8 @@
NETWORK_FIRST_DECK,
NETWORK_SECOND_DECK,
NETWORK_THIRD_DECK,
- NETWORK_INTREPID
+ NETWORK_INTREPID,
+ NETWORK_NEWS
)
shuttle_docked_message = "Attention all hands: the shift change preparations are over. It will start in approximately %ETA%."
diff --git a/tgui/packages/tgui/interfaces/CameraMonitoring.tsx b/tgui/packages/tgui/interfaces/CameraMonitoring.tsx
index 460b16de42b..5ad1beb83c0 100644
--- a/tgui/packages/tgui/interfaces/CameraMonitoring.tsx
+++ b/tgui/packages/tgui/interfaces/CameraMonitoring.tsx
@@ -94,24 +94,28 @@ export const ShowNetworkCameras = (props, context) => {
value={searchTerm}
/>
}>
- {data.cameras
- .filter(
- (c) => c.name?.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1
- )
- .map((camera) => (
-