diff --git a/_maps/map_files/cyberiad/cyberiad.dmm b/_maps/map_files/cyberiad/cyberiad.dmm
index dd810edfe60..c66079312ed 100644
--- a/_maps/map_files/cyberiad/cyberiad.dmm
+++ b/_maps/map_files/cyberiad/cyberiad.dmm
@@ -6977,7 +6977,7 @@
"cEi" = (/obj/machinery/light{dir = 4},/obj/machinery/atmospherics/pipe/simple/hidden/supply,/turf/simulated/floor/plasteel{dir = 4; icon_state = "blue"},/area/medical/patients_rooms)
"cEj" = (/turf/simulated/wall/r_wall,/area/medical/patient_b)
"cEk" = (/obj/structure/closet/l3closet/scientist,/obj/machinery/light{dir = 8},/turf/simulated/floor/plasteel{dir = 4; icon_state = "warning"},/area/toxins/xenobiology)
-"cEl" = (/obj/structure/table,/obj/item/weapon/storage/box/monkeycubes,/obj/machinery/atmospherics/pipe/simple/hidden/supply,/obj/item/weapon/storage/box/monkeycubes,/obj/item/weapon/storage/box/monkeycubes,/obj/item/weapon/storage/box/monkeycubes,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
+"cEl" = (/obj/structure/table,/obj/machinery/atmospherics/pipe/simple/hidden/supply,/obj/item/weapon/storage/box/monkeycubes,/obj/item/weapon/storage/box/monkeycubes,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cEm" = (/obj/structure/table,/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,/obj/item/stack/sheet/mineral/plasma{amount = 8; layer = 2.9},/obj/machinery/media/receiver/boombox,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cEn" = (/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{dir = 6},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cEo" = (/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{dir = 4},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
@@ -7098,7 +7098,7 @@
"cGz" = (/obj/structure/disposalpipe/segment{dir = 1; icon_state = "pipe-c"},/turf/simulated/floor/plating,/area/maintenance/asmaint)
"cGA" = (/obj/machinery/door/poddoor{density = 0; icon_state = "pdoor0"; id_tag = "xenobio1"; name = "Containment Blast Doors"; opacity = 0},/obj/machinery/door/window/northleft{base_state = "right"; dir = 8; icon_state = "right"; name = "Containment Pen"; req_access_txt = "55"},/turf/simulated/floor/engine,/area/toxins/xenobiology)
"cGB" = (/obj/structure/cable{d1 = 1; d2 = 2; icon_state = "1-2"; tag = ""},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
-"cGC" = (/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,/obj/machinery/hologram/holopad,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
+"cGC" = (/obj/machinery/atmospherics/pipe/manifold/hidden/scrubbers{dir = 8; initialize_directions = 11; level = 1},/obj/structure/disposalpipe/segment{dir = 4},/obj/machinery/computer/camera_advanced/xenobio,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cGD" = (/obj/machinery/camera{c_tag = "Xenobiology Module East"; dir = 8; network = list("Research","SS13"); pixel_y = -22},/obj/machinery/firealarm{dir = 4; pixel_x = 24},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cGE" = (/turf/simulated/floor/plating/airless,/obj/structure/cable{d1 = 1; d2 = 2; icon_state = "1-2"; pixel_y = 0; tag = ""},/turf/simulated/floor/plating/airless/catwalk{tag = "icon-catwalk3"; icon_state = "catwalk3"},/area/solar/starboard)
"cGF" = (/turf/simulated/floor/plating/airless,/obj/structure/cable{icon_state = "0-2"; d2 = 2},/turf/simulated/floor/plating/airless/catwalk{tag = "icon-catwalk3"; icon_state = "catwalk3"},/area/solar/port)
@@ -7260,14 +7260,14 @@
"cJF" = (/obj/machinery/air_sensor{frequency = 1441; id_tag = "n2o_sensor"},/turf/simulated/floor/engine/n20,/area/atmos)
"cJG" = (/obj/machinery/portable_atmospherics/canister/sleeping_agent/roomfiller{valve_open = 0; volume = 1e+006},/turf/simulated/floor/engine/n20,/area/atmos)
"cJH" = (/obj/machinery/light/small{dir = 4},/turf/simulated/floor/engine/n20,/area/atmos)
-"cJI" = (/obj/machinery/atmospherics/pipe/manifold/hidden/scrubbers{dir = 8; initialize_directions = 11; level = 1},/obj/structure/disposalpipe/segment{dir = 4},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
-"cJJ" = (/obj/machinery/atmospherics/pipe/simple/hidden/supply,/obj/structure/disposalpipe/segment{dir = 4; icon_state = "pipe-c"},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
+"cJI" = (/obj/machinery/atmospherics/pipe/simple/hidden/supply,/obj/structure/disposalpipe/segment{dir = 4; icon_state = "pipe-c"},/obj/machinery/computer/camera_advanced/xenobio,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
+"cJJ" = (/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers,/obj/structure/stool/bed/chair/comfy/blue{dir = 1},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cJK" = (/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{dir = 4},/obj/structure/disposalpipe/segment{dir = 4},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cJL" = (/obj/item/weapon/extinguisher,/obj/item/weapon/extinguisher{pixel_x = 4; pixel_y = 4},/obj/structure/table/glass,/obj/machinery/atmospherics/unary/vent_scrubber{dir = 8; on = 1; scrub_N2O = 1; scrub_Toxins = 1},/obj/structure/sign/deathsposal{pixel_y = -32},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cJM" = (/obj/structure/rack,/turf/simulated/floor/plating,/area/maintenance/asmaint2)
"cJN" = (/turf/simulated/floor/plating/airless/catwalk{tag = "icon-catwalk0"; icon_state = "catwalk0"},/area/space)
"cJO" = (/obj/structure/rack,/obj/effect/spawner/lootdrop/maintenance,/obj/machinery/light/small{dir = 4},/turf/simulated/floor/plating,/area/maintenance/asmaint)
-"cJP" = (/obj/machinery/atmospherics/pipe/simple/hidden/supply,/obj/structure/disposalpipe/segment,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
+"cJP" = (/obj/machinery/atmospherics/pipe/simple/hidden/supply,/obj/structure/disposalpipe/segment,/obj/structure/stool/bed/chair/comfy/blue{dir = 1},/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
"cJQ" = (/obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{dir = 4; level = 1},/obj/machinery/atmospherics/pipe/simple/hidden/supply{dir = 4; level = 1},/turf/simulated/floor/plasteel{dir = 5; icon_state = "whitegreencorner"},/area/medical/virology)
"cJR" = (/obj/structure/table,/obj/item/device/healthanalyzer,/obj/effect/spawner/lootdrop/maintenance,/turf/simulated/floor/plating,/area/maintenance/asmaint)
"cJS" = (/obj/machinery/atmospherics/pipe/manifold/hidden/supply{level = 1},/obj/structure/disposalpipe/segment,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
@@ -8730,6 +8730,7 @@
"dlT" = (/obj/machinery/atmospherics/unary/vent_scrubber{on = 1; scrub_N2O = 1; scrub_Toxins = 1},/obj/machinery/power/treadmill{dir = 8},/obj/structure/cable/yellow,/obj/machinery/treadmill_monitor{id = "Cell 2"; pixel_y = -32},/turf/simulated/floor/plasteel,/area/security/prison/cell_block/A)
"dlU" = (/obj/structure/stool/bed,/obj/machinery/flasher{id = "Cell 2"; pass_flags = 0; pixel_x = 0; pixel_y = -26},/turf/simulated/floor/plasteel{icon_state = "red"; dir = 4},/area/security/prison/cell_block/A)
"dlV" = (/obj/machinery/power/treadmill{dir = 4},/obj/machinery/treadmill_monitor{on = 1; pixel_y = -32; redeem_immediately = 1},/obj/structure/cable/yellow,/turf/simulated/floor/plasteel,/area/security/prison/cell_block/C)
+"dlW" = (/obj/machinery/hologram/holopad,/turf/simulated/floor/plasteel{icon_state = "white"},/area/toxins/xenobiology)
(1,1,1) = {"
bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
@@ -8905,8 +8906,8 @@ bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbi
bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikaahbikbikbikafxaabczocAkczqbikczocAkczqaabczocAkczqaabbikbikaabaabaabaabaabbikbikbikbikbikaabbikbikczscwWcvKcBecxjcBfcBgcBhcBicBjcBicBkcBicBlcBmcxfcBncztcztczxcBocBpcBqcBrcBscBtcBucBvcBwcBxcBycBzcBAcBBcBCcBDcBDcBEcBFcBGcBHcBIcBJcBKcBLcBMcrecBOcHucHtcDPcDPcDPcDPcDPcDPcHvcoAcEhclUclUclUclUclUclUcjmbEJcjmcFfclUclUcoAcoAcoAcoAcusbGycoAbikbikckPcxOcBZcxOcCacxRcqUcCbcCccCdcCecqUcqUcCgcqUcChckPbikaabaabcwGcwGcCscwGcwGbikbwobxXcvzcyJcHycHxbwobikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikaabbikczocAkczqaabczocAkczqbikczocAkczqbikbikbikbikbikbikaabaabaabbikaabbikaabaabbikbikcybcCicvKcCjcCkcCkcClcvKcvKcvKcvKcvKcvKcvKcvKcvKcvKcvKcvKcvKcCmcCncCmcCocCmcrmcCpcCqcFbcCtcCtcCtcCucCvcBCcCwcCxcCycCzcCAcCBcBIcCCcCDcCEcCFcHzcCHcHAcCJaabcCKcCKcCKcCKcCKcHBcoAcHDcHCcIGcHHcIHctackycjocBWckuckycBXcCPcBYcCRcCQcoAcusbGycoAbikbikckPcxOcxOcxOcAQcCTczbcCUcCVcCWcCXcCZcCZcDacqUcDbckPaabaabbikbikbikcIKbikbikbikbwobxXcvzcyJbxXbxXbwobikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikacGacGafxaabbikczocAkczqaabczocAkczqbikczocAkczqaabbikbikbikbikbikbikbikbikaabcDccDdcDdcDdcDdcybcDecwWcDfcDgcDhcDhcDicDjcDkcDlcDlcDlcDlcDmcDncDocDncDncDpcDpcDncDqcDrcDscDtcDtctYcDvcDwcDxcDycDzcDzcDAcDBcDCcDDcDEcDFcCAcDGcBIcDHcDIcDJcDKcILcDMcIMcDOcDPcDQcDRcDScDTcCKcHBcoDcEhclUcvscvqcIOcINcxLckxckwdcrclUcCSczVcuqczVcDUcoAcusbGycoAcoAbikckPcwAcwAcwAcwAcEkcqUcCbcElcEmcCecqUcEncEocEocEpcEqcXYaabbikbikbikbikbikbikbikbwobxXcGybxXbxXcIPbwoaabbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
-bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikacGbikaabbikaabaabcEraabaabaabcEraabbikaabcEraabbikbikbikbikbikbikbikbikcDccDccDccEscEtcEucEvcEwcExcEycDfcDfcEzcEAcvKcvKcEBcECcECcECcECcEDcEEcEFcEEcEEcEEcDtcDtcEGcEHcEIcEJcEKcDzcELcDwcDzcDzcDzcDzcEMcBCcENcEOcCycDFcCAcDFcBIcDHcDIcEPcEQcERcEScETcEUaabcEVcEWcDTcEXcCKcHBcoDcHDcHCcxKcxJczVcxMclUcIIcDVdcsclUcDWcDYczVcEbcEaclUcusbGycITcoAbikckPcwtcwucwvcFjcARczbcFkcJJcJIcJKcJKcFocFpcFqcJLckPaabaabaabbikbikbikbikbikbikbwocJMcvzbwobwobwobwobwoaabaabaabcJNcJNcJNcJNcJNcJNaabbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
-bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikafxaabcFscFtcFtcFucFvcFwcFwcFwcFvcFwcFwcFwcFvcFwcFwcFxcFtcFtcFtcFtcFtcFycFzcFAcFBcFCcFDcFEcFFcFGcfRcwWcFHcDfcvKcvKcvKcFIcEBcECcFJcFKcFLcyrcFNcDucFPcFOcFQcFScFTcSpcFVcFWcFXcFScFYcFZcDwcGacDzcDzcDzcGbcBCcGccGccDFcGdcGecGfcGgcGhcGicGjcGkcGlcGmcGncDOcGocDQcGpcDTcDTcCKcHBcoDcEhclUcyMcyLcyNcFfclUcnkclTcFfclUcyQcyScyRcyTcyTclUcusbGycJOcoAbikckPcxOcxPcxOcGAcxRcqUcGBcJPcGCcqUcqUcGDcwAcwAcwAckPbikbikaabaabbikbikbikbikbikbwocyKcvzbwobikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
+bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikacGbikaabbikaabaabcEraabaabaabcEraabbikaabcEraabbikbikbikbikbikbikbikbikcDccDccDccEscEtcEucEvcEwcExcEycDfcDfcEzcEAcvKcvKcEBcECcECcECcECcEDcEEcEFcEEcEEcEEcDtcDtcEGcEHcEIcEJcEKcDzcELcDwcDzcDzcDzcDzcEMcBCcENcEOcCycDFcCAcDFcBIcDHcDIcEPcEQcERcEScETcEUaabcEVcEWcDTcEXcCKcHBcoDcHDcHCcxKcxJczVcxMclUcIIcDVdcsclUcDWcDYczVcEbcEaclUcusbGycITcoAbikckPcwtcwucwvcFjcARczbcFkcJIcGCcJKcJKcFocFpcFqcJLckPaabaabaabbikbikbikbikbikbikbwocJMcvzbwobwobwobwobwoaabaabaabcJNcJNcJNcJNcJNcJNaabbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
+bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikafxaabcFscFtcFtcFucFvcFwcFwcFwcFvcFwcFwcFwcFvcFwcFwcFxcFtcFtcFtcFtcFtcFycFzcFAcFBcFCcFDcFEcFFcFGcfRcwWcFHcDfcvKcvKcvKcFIcEBcECcFJcFKcFLcyrcFNcDucFPcFOcFQcFScFTcSpcFVcFWcFXcFScFYcFZcDwcGacDzcDzcDzcGbcBCcGccGccDFcGdcGecGfcGgcGhcGicGjcGkcGlcGmcGncDOcGocDQcGpcDTcDTcCKcHBcoDcEhclUcyMcyLcyNcFfclUcnkclTcFfclUcyQcyScyRcyTcyTclUcusbGycJOcoAbikckPcxOcxPcxOcGAcxRcqUcGBcJPcJJcqUdlWcGDcwAcwAcwAckPbikbikaabaabbikbikbikbikbikbwocyKcvzbwobikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikacGbikaabbikaabaabcGFaabbikaabcGFaabbikaabcGFaabbikbikbikbikbikbikbikbikcDccDccDccGGcGHcFMcDdcGJcfRcGKcExcExcExcExcGLcExcFGcECcGMcGNcGOcGPcGQcGRcGScGTcGUcGVcGWcGXcGYcGZcHacHbcFYcELcDwcHccHdcDzcDzcHecBCcHfcHgcDFcHhcCAcHicHjcHkcHlcHmcHncHocHpcHocHqaabcCKcCKcCKcCKcCKcHBcoAcEhclUcIJcyVcyYcyXcEecEccEgcEfcHwcGucHZczVcIRcIQclUcusbGycJRcoAbikckPcxOcxOcxOcFjcHEcHFcHGcJScHIcCZcCZcCZcHJcHKcHLcHMbikbikbikaabaabbikbikbikbikbwobwocvzbwobikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikacGacGafxaabaabczocHOczqbikczocHOczqbikczocHOczqaabbikbikbikbikbikbikbikaabaabcDccDdcDdcDdcDdcHPcHQcHRcfRcHScfRcfRcEBcHTcfRcECcHUcHVcGOcHWcFNcHXcHYcIacIacIbcIccIdcIecIfbRFcIbcIhcIicIjcIkcIlcImcIncIocIpcIqcGfcIrcIscItcIucIvcIwcIxcIycIzcIAcIBcICcIDcDPcDQcIEcIFcIFcCKcHBcoAcEhclUclUclUclUclUcJhcANcBRcJQcMccBScMecMdcMCcBVclUcuscGqcGrcGrcGrcJTcJTcJTcJTcJTcJUcKTcKbcKUcIUczbcIVczbcIWczbcIXcHMbikbikbikbikaabaabbikbikbikbikbwocvzbwoaabaabaabaabaabaabbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
bikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikczocHOczqaabczocHOczqbikczocHOczqbikbikbikaabbikbikbikbikaabaabaabbikbikaabaabcJbcJbcJbcJbcJbcJbcJbcJccJbcJbcECcJdcJecGOcHWcFNcJfcFNcJgcLhcDtcJicJjcFWcJkcFRcDtcJmcDzcDwcJncJocJpcIncJqcJrcJscJtcJucJvcJwcJtcJxcJycJzcJAcDFcJBcJCcJDcJEaabcEVcJFcJGcJHcCKcHBcoDcKWcKVcKVcKVcKXclUcFacyLcCLcFecyScCMcyTcyTcyScCNclUcuscGvbVibGycKYcKZcoAbikbikckPcJVcJWcJXcJYcJVcJWcJZcKacJVcJWcLackPaabaabaabaabaabaabaabaabaabaabbwocvzbwobwobwobikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikdjcbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbikbik
diff --git a/code/__DEFINES/stat.dm b/code/__DEFINES/stat.dm
index 4ab2ba43203..fe780c5d154 100644
--- a/code/__DEFINES/stat.dm
+++ b/code/__DEFINES/stat.dm
@@ -39,5 +39,17 @@
#define SHUTTLE_CALL 2
#define SHUTTLE_DOCKED 3
#define SHUTTLE_STRANDED 4
-#define SHUTTLE_ESCAPE 5
-#define SHUTTLE_ENDGAME 6
\ No newline at end of file
+#define SHUTTLE_ESCAPE 5
+#define SHUTTLE_ENDGAME 6
+
+
+/*
+ Logic
+*/
+// State When to Use Example
+#define LOGIC_OFF 0 //Use this for when you want it to stay off (continuous signal, lever)
+#define LOGIC_ON 1 //Use this for when you want it to stay on (continuous signal, lever)
+#define LOGIC_FLICKER 2 //Use this for when you want it to turn on and then turn off (buttons, clocks)
+
+//Logic-related stuff (Misc. defines for logic things, to keep it organized or something)
+#define LOGIC_FLICKER_TIME 10 //number of deciseconds LOGIC_TEMP_ON will remain active before turning off
diff --git a/code/controllers/communications.dm b/code/controllers/communications.dm
index fd3fa222c82..43bee48f267 100644
--- a/code/controllers/communications.dm
+++ b/code/controllers/communications.dm
@@ -202,6 +202,7 @@ var/const/RADIO_CLEANBOT = "10"
var/const/RADIO_FLOORBOT = "11"
var/const/RADIO_MEDBOT = "12"
var/const/RADIO_MAGNETS = "radio_magnet"
+var/const/RADIO_LOGIC = "radio_logic"
var/global/datum/controller/radio/radio_controller
diff --git a/code/game/machinery/buttons.dm b/code/game/machinery/buttons.dm
index b38830b4b44..0934959d20f 100644
--- a/code/game/machinery/buttons.dm
+++ b/code/game/machinery/buttons.dm
@@ -1,3 +1,8 @@
+
+//////////////////////////////////////
+// Driver Button //
+//////////////////////////////////////
+
/obj/machinery/driver_button
name = "mass driver button"
icon = 'icons/obj/objects.dmi'
@@ -5,11 +10,17 @@
desc = "A remote control switch for a mass driver."
var/id_tag = "default"
var/active = 0
- settagwhitelist = list("id_tag")
+ settagwhitelist = list("id_tag", "logic_id_tag")
anchored = 1.0
use_power = 1
idle_power_usage = 2
active_power_usage = 4
+ var/range = 7
+
+ var/datum/radio_frequency/radio_connection
+ var/frequency = 0
+ var/logic_id_tag = "default" //Defines the ID tag to send logic signals to, so you don't have to unlink from doors and stuff
+ var/logic_connect = 0 //Set this to allow the button to send out logic signals when pressed in addition to normal stuff
/obj/machinery/driver_button/New(turf/loc, var/w_dir=null)
..()
@@ -22,6 +33,123 @@
pixel_x = 25
if(WEST)
pixel_x = -25
+ if(radio_controller)
+ set_frequency(frequency)
+
+/obj/machinery/driver_button/initialize()
+ ..()
+ set_frequency(frequency)
+
+/obj/machinery/driver_button/proc/set_frequency(new_frequency)
+ radio_controller.remove_object(src, frequency)
+ frequency = new_frequency
+ radio_connection = radio_controller.add_object(src, frequency, RADIO_LOGIC)
+ return
+
+/obj/machinery/driver_button/Destroy()
+ if(radio_controller)
+ radio_controller.remove_object(src,frequency)
+ return ..()
+
+
+/obj/machinery/driver_button/attack_ai(mob/user as mob)
+ return src.attack_hand(user)
+
+/obj/machinery/driver_button/attackby(obj/item/weapon/W, mob/user as mob, params)
+
+ if(istype(W, /obj/item/device/detective_scanner))
+ return
+
+ if(istype(W, /obj/item/device/multitool))
+ update_multitool_menu(user)
+ return 1
+
+ if(istype(W, /obj/item/weapon/wrench))
+ playsound(get_turf(src), 'sound/items/Ratchet.ogg', 50, 1)
+ if(do_after(user, 30, target = src))
+ user << "You detach \the [src] from the wall."
+ new/obj/item/mounted/frame/driver_button(get_turf(src))
+ qdel(src)
+ return 1
+
+ return src.attack_hand(user)
+
+/obj/machinery/driver_button/multitool_menu(var/mob/user, var/obj/item/device/multitool/P)
+ return {"
+
+ - ID Tag: [format_tag("ID Tag","id_tag")]
+ - Logic Connection: [logic_connect ? "On" : "Off"]
+ - Logic ID Tag: [format_tag("Logic ID Tag", "logic_id_tag")]
+
"}
+
+/obj/machinery/driver_button/attack_hand(mob/user as mob)
+
+ src.add_fingerprint(usr)
+ if(stat & (NOPOWER|BROKEN))
+ return
+ if(active)
+ return
+ add_fingerprint(user)
+
+ use_power(5)
+
+ launch_sequence()
+
+ return
+
+/obj/machinery/driver_button/proc/launch_sequence()
+ active = 1
+ icon_state = "launcheract"
+
+ if(logic_connect)
+ if(!radio_connection) //can't output without this
+ return
+
+ if(logic_id_tag == null) //Don't output to an undefined id_tag
+ return
+
+ var/datum/signal/signal = new
+ signal.transmission_method = 1 //radio signal
+ signal.source = src
+
+ signal.data = list(
+ "tag" = logic_id_tag,
+ "sigtype" = "logic",
+ "state" = LOGIC_FLICKER, //Buttons are a FLICKER source, since they only register as ON when you press it, then turn OFF after you release
+ )
+
+ radio_connection.post_signal(src, signal, filter = RADIO_LOGIC)
+
+ for(var/obj/machinery/door/poddoor/M in range(src,range))
+ if (M.id_tag == src.id_tag && !M.protected)
+ spawn()
+ M.open()
+
+ sleep(20)
+
+ for(var/obj/machinery/mass_driver/M in range(src,range))
+ if(M.id_tag == src.id_tag)
+ M.drive()
+
+ sleep(50)
+
+ for(var/obj/machinery/door/poddoor/M in range(src,range))
+ if (M.id_tag == src.id_tag && !M.protected)
+ spawn()
+ M.close()
+ return
+
+ icon_state = "launcherbtt"
+ active = 0
+
+/obj/machinery/driver_button/multitool_topic(var/mob/user,var/list/href_list,var/obj/O)
+ ..()
+ if("toggle_logic" in href_list)
+ logic_connect = !logic_connect
+
+//////////////////////////////////////
+// Ignition Switch //
+//////////////////////////////////////
/obj/machinery/ignition_switch
name = "ignition switch"
@@ -35,6 +163,46 @@
idle_power_usage = 2
active_power_usage = 4
+/obj/machinery/ignition_switch/attack_ai(mob/user as mob)
+ return src.attack_hand(user)
+
+/obj/machinery/ignition_switch/attackby(obj/item/weapon/W, mob/user as mob, params)
+ return src.attack_hand(user)
+
+/obj/machinery/ignition_switch/attack_hand(mob/user as mob)
+
+ if(stat & (NOPOWER|BROKEN))
+ return
+ if(active)
+ return
+
+ use_power(5)
+
+ active = 1
+ icon_state = "launcheract"
+
+ for(var/obj/machinery/sparker/M in world)
+ if (M.id == src.id)
+ spawn( 0 )
+ M.spark()
+
+ for(var/obj/machinery/igniter/M in world)
+ if(M.id == src.id)
+ use_power(50)
+ M.on = !( M.on )
+ M.icon_state = text("igniter[]", M.on)
+
+ sleep(50)
+
+ icon_state = "launcherbtt"
+ active = 0
+
+ return
+
+//////////////////////////////////////
+// Flasher Button //
+//////////////////////////////////////
+
/obj/machinery/flasher_button
name = "flasher button"
desc = "A remote control switch for a mounted flasher."
@@ -47,6 +215,10 @@
idle_power_usage = 2
active_power_usage = 4
+//////////////////////////////////////
+// Crematorium Switch //
+//////////////////////////////////////
+
/obj/machinery/crema_switch
desc = "Burn baby burn!"
name = "crematorium igniter"
diff --git a/code/game/machinery/constructable_frame.dm b/code/game/machinery/constructable_frame.dm
index 8886c865722..49cc616d174 100644
--- a/code/game/machinery/constructable_frame.dm
+++ b/code/game/machinery/constructable_frame.dm
@@ -878,4 +878,50 @@ obj/item/weapon/circuitboard/rdserver
req_components = list(
/obj/item/weapon/stock_parts/matter_bin = 1,
/obj/item/weapon/stock_parts/manipulator = 1,
- /obj/item/weapon/stock_parts/console_screen = 1)
\ No newline at end of file
+ /obj/item/weapon/stock_parts/console_screen = 1)
+
+
+//Selectable mode board, like vending machine boards
+/obj/item/weapon/circuitboard/logic_gate
+ name = "circuit board (Logic Connector)"
+ build_path = /obj/machinery/logic_gate
+ board_type = "machine"
+ origin_tech = "programming=1" //This stuff is pretty much the absolute basis of programming, so it's mostly useless for research
+ req_components = list(/obj/item/stack/cable_coil = 1)
+
+ var/list/names_paths = list(
+ "NOT Gate" = /obj/machinery/logic_gate/not,
+ "OR Gate" = /obj/machinery/logic_gate/or,
+ "AND Gate" = /obj/machinery/logic_gate/and,
+ "NAND Gate" = /obj/machinery/logic_gate/nand,
+ "NOR Gate" = /obj/machinery/logic_gate/nor,
+ "XOR Gate" = /obj/machinery/logic_gate/xor,
+ "XNOR Gate" = /obj/machinery/logic_gate/xnor,
+ "STATUS Gate" = /obj/machinery/logic_gate/status,
+ "CONVERT Gate" = /obj/machinery/logic_gate/convert
+ )
+
+/obj/item/weapon/circuitboard/logic_gate/New()
+ ..()
+ if(build_path == /obj/machinery/logic_gate) //If we spawn the base type board (determined by the base type machine as the build path), become a random gate board
+ var/new_path = names_paths[pick(names_paths)]
+ set_type(new_path)
+
+/obj/item/weapon/circuitboard/logic_gate/attackby(obj/item/I, mob/user, params)
+ if(istype(I, /obj/item/weapon/screwdriver))
+ set_type(null, user)
+
+/obj/item/weapon/circuitboard/logic_gate/proc/set_type(typepath, mob/user)
+ var/new_name = "Logic Base"
+ if(!typepath)
+ new_name = input("Circuit Setting", "What would you change the board setting to?") in names_paths
+ typepath = names_paths[new_name]
+ else
+ for(var/name in names_paths)
+ if(names_paths[name] == typepath)
+ new_name = name
+ break
+ build_path = typepath
+ name = "circuit board ([new_name])"
+ if(user)
+ user << "You set the board to [new_name]."
diff --git a/code/game/machinery/door_control.dm b/code/game/machinery/door_control.dm
index d2172bd9294..8c419216d07 100644
--- a/code/game/machinery/door_control.dm
+++ b/code/game/machinery/door_control.dm
@@ -131,74 +131,3 @@
icon_state = "doorctrl-p"
else
icon_state = "doorctrl0"
-
-/obj/machinery/driver_button/var/range = 7
-
-/obj/machinery/driver_button/attack_ai(mob/user as mob)
- return src.attack_hand(user)
-
-/obj/machinery/driver_button/attackby(obj/item/weapon/W, mob/user as mob, params)
-
- if(istype(W, /obj/item/device/detective_scanner))
- return
-
- if(istype(W, /obj/item/device/multitool))
- update_multitool_menu(user)
- return 1
-
- if(istype(W, /obj/item/weapon/wrench))
- playsound(get_turf(src), 'sound/items/Ratchet.ogg', 50, 1)
- if(do_after(user, 30, target = src))
- to_chat(user, "You detach \the [src] from the wall.")
- new/obj/item/mounted/frame/driver_button(get_turf(src))
- qdel(src)
- return 1
-
- return src.attack_hand(user)
-
-/obj/machinery/driver_button/multitool_menu(var/mob/user, var/obj/item/device/multitool/P)
- return {"
-
- - [format_tag("ID Tag","id_tag")]
-
"}
-
-/obj/machinery/driver_button/attack_hand(mob/user as mob)
-
- src.add_fingerprint(usr)
- if(stat & (NOPOWER|BROKEN))
- return
- if(active)
- return
- add_fingerprint(user)
-
- use_power(5)
-
- launch_sequence()
-
- return
-
-/obj/machinery/driver_button/proc/launch_sequence()
- active = 1
- icon_state = "launcheract"
-
- for(var/obj/machinery/door/poddoor/M in range(src,range))
- if (M.id_tag == src.id_tag && !M.protected)
- spawn()
- M.open()
-
- sleep(20)
-
- for(var/obj/machinery/mass_driver/M in range(src,range))
- if(M.id_tag == src.id_tag)
- M.drive()
-
- sleep(50)
-
- for(var/obj/machinery/door/poddoor/M in range(src,range))
- if (M.id_tag == src.id_tag && !M.protected)
- spawn()
- M.close()
- return
-
- icon_state = "launcherbtt"
- active = 0
\ No newline at end of file
diff --git a/code/game/machinery/igniter.dm b/code/game/machinery/igniter.dm
index 25989db9e0f..d6e41d29fa2 100755
--- a/code/game/machinery/igniter.dm
+++ b/code/game/machinery/igniter.dm
@@ -114,39 +114,3 @@
return
spark()
..(severity)
-
-/obj/machinery/ignition_switch/attack_ai(mob/user as mob)
- return src.attack_hand(user)
-
-/obj/machinery/ignition_switch/attackby(obj/item/weapon/W, mob/user as mob, params)
- return src.attack_hand(user)
-
-/obj/machinery/ignition_switch/attack_hand(mob/user as mob)
-
- if(stat & (NOPOWER|BROKEN))
- return
- if(active)
- return
-
- use_power(5)
-
- active = 1
- icon_state = "launcheract"
-
- for(var/obj/machinery/sparker/M in world)
- if (M.id == src.id)
- spawn( 0 )
- M.spark()
-
- for(var/obj/machinery/igniter/M in world)
- if(M.id == src.id)
- use_power(50)
- M.on = !( M.on )
- M.icon_state = text("igniter[]", M.on)
-
- sleep(50)
-
- icon_state = "launcherbtt"
- active = 0
-
- return
\ No newline at end of file
diff --git a/code/game/machinery/lightswitch.dm b/code/game/machinery/lightswitch.dm
index f83a92ff3e7..41e1587ecd4 100644
--- a/code/game/machinery/lightswitch.dm
+++ b/code/game/machinery/lightswitch.dm
@@ -11,9 +11,27 @@
var/area/area = null
var/otherarea = null
// luminosity = 1
+ settagwhitelist = list("logic_id_tag")
+ var/light_connect = 1 //Allows the switch to control lights in its associated areas. When set to 0, using the switch won't affect the lights.
+ var/datum/radio_frequency/radio_connection
+ var/frequency = 0
+ var/logic_id_tag = "default" //Defines the ID tag to send logic signals to.
+ var/logic_connect = 0 //Set this to allow the switch to send out logic signals.
-/obj/machinery/light_switch/New()
+
+/obj/machinery/light_switch/New(turf/loc, var/w_dir=null)
..()
+ switch(w_dir)
+ if(NORTH)
+ pixel_y = 25
+ if(SOUTH)
+ pixel_y = -25
+ if(EAST)
+ pixel_x = 25
+ if(WEST)
+ pixel_x = -25
+ if(radio_controller)
+ set_frequency(frequency)
spawn(5)
src.area = src.loc.loc
@@ -26,7 +44,20 @@
src.on = src.area.lightswitch
updateicon()
+/obj/machinery/light_switch/initialize()
+ ..()
+ set_frequency(frequency)
+/obj/machinery/light_switch/proc/set_frequency(new_frequency)
+ radio_controller.remove_object(src, frequency)
+ frequency = new_frequency
+ radio_connection = radio_controller.add_object(src, frequency, RADIO_LOGIC)
+ return
+
+/obj/machinery/light_switch/Destroy()
+ if(radio_controller)
+ radio_controller.remove_object(src,frequency)
+ return ..()
/obj/machinery/light_switch/proc/updateicon()
if(stat & NOPOWER)
@@ -41,22 +72,55 @@
if(..(user, 1))
to_chat(user, "A light switch. It is [on? "on" : "off"].")
-
/obj/machinery/light_switch/attack_hand(mob/user)
on = !on
+ updateicon()
- area.lightswitch = on
- area.updateicon()
+ if(light_connect)
+ area.lightswitch = on
+ area.updateicon()
- for(var/obj/machinery/light_switch/L in area)
- L.on = on
- L.updateicon()
+ if(logic_connect && powered(LIGHT)) //Don't bother sending a signal if we aren't set to send them or we have no power to send with.
+ handle_output()
- area.power_change()
+ if(light_connect)
+ for(var/obj/machinery/light_switch/L in area)
+ L.on = on
+ L.updateicon()
+
+ area.power_change()
+
+/obj/machinery/light_switch/proc/handle_output()
+ if(!radio_connection) //can't output without this
+ return
+
+ if(logic_id_tag == null) //Don't output to an undefined id_tag
+ return
+
+ var/datum/signal/signal = new
+ signal.transmission_method = 1 //radio signal
+ signal.source = src
+
+ //Light switches are continuous signal sources, since they register as ON or OFF and stay that way until adjusted again
+ if(on)
+ signal.data = list(
+ "tag" = logic_id_tag,
+ "sigtype" = "logic",
+ "state" = LOGIC_ON,
+ )
+ else
+ signal.data = list(
+ "tag" = logic_id_tag,
+ "sigtype" = "logic",
+ "state" = LOGIC_OFF,
+ )
+
+ radio_connection.post_signal(src, signal, filter = RADIO_LOGIC)
+ if(on)
+ use_power(5, LIGHT) //Use a tiny bit of power every time we send an ON signal. Draws from the local APC's lighting circuit, since this is a LIGHT switch.
/obj/machinery/light_switch/power_change()
-
if(!otherarea)
if(powered(LIGHT))
stat &= ~NOPOWER
@@ -71,3 +135,40 @@
return
power_change()
..(severity)
+
+/obj/machinery/light_switch/process()
+ if(logic_connect && powered(LIGHT)) //We won't send signals while unpowered, but the last signal will remain valid for anything that received it before we went dark
+ handle_output()
+
+/obj/machinery/light_switch/attackby(obj/item/weapon/W as obj, mob/user as mob, params)
+ if(istype(W, /obj/item/device/detective_scanner))
+ return
+
+ if(istype(W, /obj/item/device/multitool))
+ update_multitool_menu(user)
+ return 1
+
+ if(istype(W, /obj/item/weapon/wrench))
+ playsound(get_turf(src), 'sound/items/Ratchet.ogg', 50, 1)
+ if(do_after(user, 30, target = src))
+ user << "You detach \the [src] from the wall."
+ new/obj/item/mounted/frame/light_switch(get_turf(src))
+ qdel(src)
+ return 1
+
+ return src.attack_hand(user)
+
+/obj/machinery/light_switch/multitool_menu(var/mob/user, var/obj/item/device/multitool/P)
+ return {"
+ "}
+
+/obj/machinery/light_switch/multitool_topic(var/mob/user,var/list/href_list,var/obj/O)
+ ..()
+ if("toggle_light_connect" in href_list)
+ light_connect = !light_connect
+ if("toggle_logic" in href_list)
+ logic_connect = !logic_connect
diff --git a/code/game/objects/items/mountable_frames/buttons_switches.dm b/code/game/objects/items/mountable_frames/buttons_switches.dm
new file mode 100644
index 00000000000..b1e3fabbb86
--- /dev/null
+++ b/code/game/objects/items/mountable_frames/buttons_switches.dm
@@ -0,0 +1,23 @@
+/obj/item/mounted/frame/driver_button
+ name = "mass driver button frame"
+ desc = "Used for repairing or building mass driver buttons"
+ icon = 'icons/obj/objects.dmi'
+ icon_state = "launcherbtt_frame"
+ mount_reqs = list("simfloor")
+ sheets_refunded = 1
+
+/obj/item/mounted/frame/driver_button/do_build(turf/on_wall, mob/user)
+ new /obj/machinery/driver_button(get_turf(user), get_dir(user, on_wall))
+ qdel(src)
+
+/obj/item/mounted/frame/light_switch
+ name = "light switch frame"
+ desc = "Used for repairing or building light switches"
+ icon = 'icons/obj/power.dmi'
+ icon_state = "light-p"
+ mount_reqs = list("simfloor", "nospace")
+ sheets_refunded = 1
+
+/obj/item/mounted/frame/light_switch/do_build(turf/on_wall, mob/user)
+ new /obj/machinery/light_switch(get_turf(user), get_dir(user, on_wall))
+ qdel(src)
\ No newline at end of file
diff --git a/code/game/objects/items/mountable_frames/driver_button.dm b/code/game/objects/items/mountable_frames/driver_button.dm
deleted file mode 100644
index 7c888b48c95..00000000000
--- a/code/game/objects/items/mountable_frames/driver_button.dm
+++ /dev/null
@@ -1,10 +0,0 @@
-/obj/item/mounted/frame/driver_button
- name = "mass driver button frame"
- desc = "Used for repairing or building mass driver buttons"
- icon = 'icons/obj/objects.dmi'
- icon_state = "launcherbtt_frame"
- mount_reqs = list("simfloor")
-
-/obj/item/mounted/frame/driver_button/do_build(turf/on_wall, mob/user)
- new /obj/machinery/driver_button(get_turf(user), get_dir(user, on_wall))
- qdel(src)
\ No newline at end of file
diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm
index 21680521d34..7cb53eaed5f 100644
--- a/code/game/objects/items/stacks/sheets/sheet_types.dm
+++ b/code/game/objects/items/stacks/sheets/sheet_types.dm
@@ -73,6 +73,7 @@ var/global/list/datum/stack_recipe/metal_recipes = list ( \
), 4), \
null, \
new/datum/stack_recipe("mass driver button frame", /obj/item/mounted/frame/driver_button, 1, time = 50, one_per_turf = 0, on_floor = 1), \
+ new/datum/stack_recipe("light switch frame", /obj/item/mounted/frame/light_switch, 1, time = 50, one_per_turf = 0, on_floor = 1), \
null, \
new/datum/stack_recipe("grenade casing", /obj/item/weapon/grenade/chem_grenade), \
new/datum/stack_recipe("light fixture frame", /obj/item/mounted/frame/light_fixture, 2), \
diff --git a/code/modules/assembly/signaler.dm b/code/modules/assembly/signaler.dm
index fe33be50f48..d6d6352f333 100644
--- a/code/modules/assembly/signaler.dm
+++ b/code/modules/assembly/signaler.dm
@@ -18,149 +18,150 @@
var/datum/radio_frequency/radio_connection
var/airlock_wire = null
- New()
- ..()
- if(!radio_controller)
- spawn(40)
- set_frequency(frequency)
- else
- spawn(1) // For things that set the frequency directly
- set_frequency(frequency)
+/obj/item/device/assembly/signaler/New()
+ ..()
+ if(radio_controller)
+ set_frequency(frequency)
+
+/obj/item/device/assembly/signaler/initialize()
+ if(radio_controller)
+ set_frequency(frequency)
+
+/obj/item/device/assembly/signaler/Destroy()
+ if(radio_controller)
+ radio_controller.remove_object(src,frequency)
+ return ..()
+
+/obj/item/device/assembly/signaler/describe()
+ return "\The [src]'s power light is [receiving?"on":"off"]"
+
+/obj/item/device/assembly/signaler/activate()
+ if(cooldown > 0) return 0
+ cooldown = 2
+ spawn(10)
+ process_cooldown()
+
+ signal()
+ return 1
+
+/obj/item/device/assembly/signaler/update_icon()
+ if(holder)
+ holder.update_icon()
+ return
+
+/obj/item/device/assembly/signaler/interact(mob/user as mob, flag1)
+ var/t1 = "-------"
+// if ((src.b_stat && !( flag1 )))
+// t1 = text("-------
\nGreen Wire: []
\nRed Wire: []
\nBlue Wire: []
\n", (src.wires & 4 ? text("Cut Wire", src) : text("Mend Wire", src)), (src.wires & 2 ? text("Cut Wire", src) : text("Mend Wire", src)), (src.wires & 1 ? text("Cut Wire", src) : text("Mend Wire", src)))
+// else
+// t1 = "-------" Speaker: [src.listening ? "Engaged" : "Disengaged"]
+ var/dat = {"
+
+ "}
+ if(!flag1)
+ dat += {"
+ Send Signal
+ Reciever is [receiving?"on":"off"]
+ "}
+ dat += {"
+ Frequency/Code for signaler:
+ Frequency:
+ -
+ -
+ [format_frequency(src.frequency)]
+ +
+ +
+
+ Code:
+ -
+ -
+ [src.code]
+ +
+ +
+ [t1]
+
+ "}
+ user << browse(dat, "window=radio")
+ onclose(user, "radio")
+ return
+
+
+/obj/item/device/assembly/signaler/Topic(href, href_list)
+ ..()
+
+ if(!usr.canmove || usr.stat || usr.restrained() || !in_range(loc, usr))
+ usr << browse(null, "window=radio")
+ onclose(usr, "radio")
return
- Destroy()
- if(radio_controller)
- radio_controller.remove_object(src,frequency)
- return ..()
+ if (href_list["freq"])
+ var/new_frequency = (frequency + text2num(href_list["freq"]))
+ if(new_frequency < RADIO_LOW_FREQ || new_frequency > RADIO_HIGH_FREQ)
+ new_frequency = sanitize_frequency(new_frequency, RADIO_LOW_FREQ, RADIO_HIGH_FREQ)
+ set_frequency(new_frequency)
- describe()
- return "\The [src]'s power light is [receiving?"on":"off"]"
-
- activate()
- if(cooldown > 0) return 0
- cooldown = 2
- spawn(10)
- process_cooldown()
+ if(href_list["code"])
+ src.code += text2num(href_list["code"])
+ src.code = round(src.code)
+ src.code = min(100, src.code)
+ src.code = max(1, src.code)
+ if(href_list["receive"])
+ receiving = !receiving
+ if(href_list["send"])
+ spawn( 0 )
signal()
- return 1
- update_icon()
- if(holder)
- holder.update_icon()
- return
-
- interact(mob/user as mob, flag1)
- var/t1 = "-------"
-// if ((src.b_stat && !( flag1 )))
-// t1 = text("-------
\nGreen Wire: []
\nRed Wire: []
\nBlue Wire: []
\n", (src.wires & 4 ? text("Cut Wire", src) : text("Mend Wire", src)), (src.wires & 2 ? text("Cut Wire", src) : text("Mend Wire", src)), (src.wires & 1 ? text("Cut Wire", src) : text("Mend Wire", src)))
-// else
-// t1 = "-------" Speaker: [src.listening ? "Engaged" : "Disengaged"]
- var/dat = {"
-
-
- Send Signal
- Reciever is [receiving?"on":"off"]
- Frequency/Code for signaler:
- Frequency:
- -
- -
- [format_frequency(src.frequency)]
- +
- +
-
- Code:
- -
- -
- [src.code]
- +
- +
- [t1]
- "}
- user << browse(dat, "window=radio")
- onclose(user, "radio")
- return
-
-
- Topic(href, href_list)
- ..()
-
- if(!usr.canmove || usr.stat || usr.restrained() || !in_range(loc, usr))
- usr << browse(null, "window=radio")
- onclose(usr, "radio")
- return
-
- if (href_list["freq"])
- var/new_frequency = (frequency + text2num(href_list["freq"]))
- if(new_frequency < RADIO_LOW_FREQ || new_frequency > RADIO_HIGH_FREQ)
- new_frequency = sanitize_frequency(new_frequency, RADIO_LOW_FREQ, RADIO_HIGH_FREQ)
- set_frequency(new_frequency)
-
- if(href_list["code"])
- src.code += text2num(href_list["code"])
- src.code = round(src.code)
- src.code = min(100, src.code)
- src.code = max(1, src.code)
- if(href_list["receive"])
- receiving = !receiving
-
- if(href_list["send"])
- spawn( 0 )
- signal()
-
- if(usr)
- attack_self(usr)
-
- return
-
-
- proc/signal()
- if(!radio_connection) return
-
- var/datum/signal/signal = new
- signal.source = src
- signal.encryption = code
- signal.data["message"] = "ACTIVATE"
- radio_connection.post_signal(src, signal)
-
- var/time = time2text(world.realtime,"hh:mm:ss")
- var/turf/T = get_turf(src)
- if(usr)
- lastsignalers.Add("[time] : [usr.key] used [src] @ location ([T.x],[T.y],[T.z]) : [format_frequency(frequency)]/[code]")
-
- return
-
-
- pulse(var/radio = 0)
- if(src.connected && src.wires)
- connected.Pulse(src)
- else
- return ..(radio)
-
-
- receive_signal(datum/signal/signal)
- if( !receiving || !signal )
- return 0
-
- if(signal.encryption != code)
- return 0
- if(!(src.wires & WIRE_RADIO_RECEIVE)) return 0
-
- pulse(1)
- for(var/mob/O in hearers(1, src.loc))
- O.show_message(text("\icon[] *beep* *beep*", src), 3, "*beep* *beep*", 2)
- return
-
-
- proc/set_frequency(new_frequency)
- if(!radio_controller)
- sleep(20)
- if(!radio_controller)
- return
- radio_controller.remove_object(src, frequency)
- frequency = new_frequency
- radio_connection = radio_controller.add_object(src, frequency, RADIO_CHAT)
+ if(usr)
+ attack_self(usr)
+
+ return
+
+/obj/item/device/assembly/signaler/proc/signal()
+ if(!radio_connection) return
+
+ var/datum/signal/signal = new
+ signal.source = src
+ signal.encryption = code
+ signal.data["message"] = "ACTIVATE"
+ radio_connection.post_signal(src, signal)
+
+ var/time = time2text(world.realtime,"hh:mm:ss")
+ var/turf/T = get_turf(src)
+ if(usr)
+ lastsignalers.Add("[time] : [usr.key] used [src] @ location ([T.x],[T.y],[T.z]) : [format_frequency(frequency)]/[code]")
+
+ return
+
+/obj/item/device/assembly/signaler/pulse(var/radio = 0)
+ if(src.connected && src.wires)
+ connected.Pulse(src)
+ else
+ return ..(radio)
+
+/obj/item/device/assembly/signaler/receive_signal(datum/signal/signal)
+ if( !receiving || !signal )
+ return 0
+
+ if(signal.encryption != code)
+ return 0
+
+ if(!(src.wires & WIRE_RADIO_RECEIVE)) return 0
+ pulse(1)
+
+ for(var/mob/O in hearers(1, src.loc))
+ O.show_message(text("\icon[] *beep* *beep*", src), 3, "*beep* *beep*", 2)
+ return
+
+/obj/item/device/assembly/signaler/proc/set_frequency(new_frequency)
+ if(!radio_controller)
+ sleep(20)
+ if(!radio_controller)
return
+ radio_controller.remove_object(src, frequency)
+ frequency = new_frequency
+ radio_connection = radio_controller.add_object(src, frequency, RADIO_CHAT)
+ return
// Embedded signaller used in anomalies.
/obj/item/device/assembly/signaler/anomaly
diff --git a/code/modules/logic/converter.dm b/code/modules/logic/converter.dm
new file mode 100644
index 00000000000..635bbf87176
--- /dev/null
+++ b/code/modules/logic/converter.dm
@@ -0,0 +1,143 @@
+
+//////////////////////////////////
+// Converter Gate //
+//////////////////////////////////
+
+/*
+ This gate is special enough to warrant its own file, as it overrides a lot of the logic_gate procs as well as adds a new one.
+ - CONVERT Gates convert signaler and logic signals, to allow logic gates to actually be used in tandem with assemblies and station equipment like doors.
+ - While technically a mono-input device, the input and output are actually completely different types of signals, and thus incompatible without this gate.
+ - A signaler must be attached manually before the gate is fully functional, and will retain any frequency and code settings it had when attached.
+ - You can adjust these settings through a link in the multitool menu, but the receiving mode is automatically controlled by the converter.
+ - While attached, the ability to manually send the signal on the signaler through its menu is also disabled, to avoid messing up the logic system.
+*/
+
+//CONVERT Gate
+/obj/machinery/logic_gate/convert
+ name = "CONVERT Gate"
+ desc = "Converts signals between radio and logic types, allowing for signaller input/output from logic systems."
+ icon_state = "logic_convert"
+ mono_input = 1
+
+ var/logic_output = 0 //When set to 1, the logic signal is the output. When set to 0, the logic signal is the input.
+ var/obj/item/device/assembly/signaler/attached_signaler = null
+
+/obj/machinery/logic_gate/convert/handle_logic()
+ output_state = input1_state
+ return
+
+/obj/machinery/logic_gate/convert/attackby(obj/item/O, mob/user, params)
+ if(tamperproof) //Extra precaution to avoid people attaching/removing signalers from tamperproofed converters
+ return
+ if(istype(O, /obj/item/device/assembly/signaler))
+ var/obj/item/device/assembly/signaler/S = O
+ if(S.secured)
+ user << "The [S] is already secured."
+ return
+ if(attached_signaler)
+ user << "There is already a device attached, remove it first."
+ return
+ user.unEquip(S)
+ S.forceMove(src)
+ S.holder = src
+ S.toggle_secure()
+ if(logic_output) //Make sure we are set to receive if the converter is set to output logic, and send if the converter is set to accept logic input
+ S.receiving = 1
+ else
+ S.receiving = 0
+ attached_signaler = S
+ user << "You attach \the [S] to the I/O connection port and secure it."
+ return
+ if(attached_signaler && istype(O, /obj/item/weapon/screwdriver)) //Makes sure we remove the attached signaler before we can open up and deconstruct the machine
+ var/obj/item/device/assembly/signaler/S = attached_signaler
+ attached_signaler = null
+ S.forceMove(get_turf(src))
+ S.holder = null
+ S.toggle_secure()
+ user << "You unsecure and detach \the [S] from the I/O connection port."
+ return
+ ..()
+
+/obj/machinery/logic_gate/convert/multitool_menu(var/mob/user, var/obj/item/device/multitool/P)
+ var/logic_state_string
+ var/menu_contents = {"
+
+ "}
+ if(logic_output)
+ switch(output_state)
+ if(LOGIC_OFF)
+ logic_state_string = "OFF"
+ if(LOGIC_ON)
+ logic_state_string = "ON"
+ if(LOGIC_FLICKER)
+ logic_state_string = "FLICKER"
+ else
+ logic_state_string = "ERROR: UNKNOWN STATE"
+ menu_contents += {"
+ - Output: [format_tag("ID Tag","output_id_tag")]
+ - Output State: [logic_state_string]
+ "}
+ else
+ switch(input1_state)
+ if(LOGIC_OFF)
+ logic_state_string = "OFF"
+ if(LOGIC_ON)
+ logic_state_string = "ON"
+ if(LOGIC_FLICKER)
+ logic_state_string = "FLICKER"
+ else
+ logic_state_string = "ERROR: UNKNOWN STATE"
+ menu_contents += {"
+ - Input: [format_tag("ID Tag","input1_id_tag")]
+ - Input State: [logic_state_string]
+ "}
+ menu_contents += {"
+ - Logic Signal Designation: [logic_output ? "Output" : "Input"]
+ "}
+ if(attached_signaler)
+ menu_contents += "- Adjust Signaler Settings
"
+ else
+ menu_contents += "- NO SIGNALER ATTACHED!
"
+ menu_contents += {"
+
+ "}
+ return menu_contents
+
+/obj/machinery/logic_gate/convert/multitool_topic(var/mob/user,var/list/href_list,var/obj/O)
+ ..()
+ if("toggle_logic" in href_list)
+ logic_output = !logic_output
+ if(attached_signaler) //If we have a signaler attached, make sure we update it to send/receive when we change the logic signal desgination
+ if(logic_output)
+ attached_signaler.receiving = 1
+ else
+ attached_signaler.receiving = 0
+ if(("adjust_signaler" in href_list) && attached_signaler) //Make sure that we have a signaler attached to handle this one, otherwise ignore this command
+ attached_signaler.interact(user, 1)
+ update_multitool_menu(user)
+
+/obj/machinery/logic_gate/convert/receive_signal(datum/signal/signal, receive_method, receive_param)
+ if(logic_output)
+ if(attached_signaler)
+ attached_signaler.receive_signal(signal)
+ return
+ else
+ ..()
+
+/obj/machinery/logic_gate/convert/handle_output()
+ if(logic_output)
+ ..()
+ else
+ if(attached_signaler && (output_state == LOGIC_ON || output_state == LOGIC_FLICKER))
+ attached_signaler.signal()
+ return
+
+/obj/machinery/logic_gate/convert/proc/process_activation(var/obj/item/device/D)
+ if(!logic_output) //Don't bother if our input is a logic signal
+ return
+ if(D == attached_signaler) //Ignore if we somehow got called by a device that isn't what we have attached
+ input1_state = LOGIC_FLICKER
+ spawn(LOGIC_FLICKER_TIME)
+ if(input1_state == LOGIC_FLICKER)
+ input1_state = LOGIC_OFF
+ return
diff --git a/code/modules/logic/dual_input.dm b/code/modules/logic/dual_input.dm
new file mode 100644
index 00000000000..03bfa4f1688
--- /dev/null
+++ b/code/modules/logic/dual_input.dm
@@ -0,0 +1,99 @@
+
+//////////////////////////////////
+// Dual-Input Gates //
+//////////////////////////////////
+
+
+// OR Gate
+/obj/machinery/logic_gate/or
+ name = "OR Gate"
+ desc = "Outputs ON when at least one input is ON."
+ icon_state = "logic_or"
+
+/obj/machinery/logic_gate/or/handle_logic()
+ if(input1_state == LOGIC_ON || input1_state == LOGIC_FLICKER || input2_state == LOGIC_ON || input2_state == LOGIC_FLICKER)
+ if(input1_state == LOGIC_ON || input2_state == LOGIC_ON) //continuous signal takes priority in determining what to output
+ output_state = LOGIC_ON
+ else
+ output_state = LOGIC_FLICKER
+ else //Both inputs were off, so input is off
+ output_state = LOGIC_OFF
+ return
+
+// AND Gate
+/obj/machinery/logic_gate/and
+ name = "AND Gate"
+ desc = "Outputs ON only when both inputs are ON."
+ icon_state = "logic_and"
+
+/obj/machinery/logic_gate/and/handle_logic()
+ if((input1_state == LOGIC_ON || input1_state == LOGIC_FLICKER) && (input2_state == LOGIC_ON || input2_state == LOGIC_FLICKER))
+ if(input1_state == LOGIC_ON && input2_state == LOGIC_ON) //only output a continuous signal when both inputs are continuous signals
+ output_state = LOGIC_ON
+ else
+ output_state = LOGIC_FLICKER
+ else //At least one input was off, so output is off
+ output_state = LOGIC_OFF
+ return
+
+// NAND Gate
+/obj/machinery/logic_gate/nand
+ name = "NAND Gate"
+ desc = "Outputs OFF only when both inputs are ON."
+ output_state = LOGIC_ON
+ icon_state = "logic_nand"
+
+/obj/machinery/logic_gate/nand/handle_logic()
+ if((input1_state == LOGIC_ON || input1_state == LOGIC_FLICKER) && (input2_state == LOGIC_ON || input2_state == LOGIC_FLICKER))
+ output_state = LOGIC_OFF //This can only output continuous signals
+ else //At least one input was ON/FLICKER, so output is off
+ output_state = LOGIC_OFF
+ return
+
+// NOR Gate
+/obj/machinery/logic_gate/nor
+ name = "NOR Gate"
+ desc = "Outputs OFF when at least one input is ON."
+ icon_state = "logic_nor"
+ output_state = LOGIC_ON
+
+/obj/machinery/logic_gate/nor/handle_logic()
+ if(input1_state == LOGIC_OFF ||input2_state == LOGIC_OFF)
+ output_state = LOGIC_ON //This can only output continuous signals
+ else //Both inputs are ON, so output is OFF
+ output_state = LOGIC_OFF
+ return
+
+// XOR Gate
+/obj/machinery/logic_gate/xor
+ name = "XOR Gate"
+ desc = "Outputs ON when only one input is ON."
+ icon_state = "logic_xor"
+
+/obj/machinery/logic_gate/xor/handle_logic()
+ if((input1_state == LOGIC_ON || input1_state == LOGIC_FLICKER) || input2_state == LOGIC_OFF) //Only input1 is ON/FLICKER, so output matches input1
+ output_state = input1_state
+ else if((input2_state == LOGIC_ON || input2_state == LOGIC_FLICKER) || input1_state == LOGIC_OFF) //Only input2 is ON/FLICKER, so output matches input2
+ output_state = input2_state
+ else //Both inputs are ON or OFF, so output is OFF
+ output_state = LOGIC_OFF
+ return
+
+// XNOR Gate
+/obj/machinery/logic_gate/xnor
+ name = "XNOR Gate"
+ desc = "Outputs ON when both inputs are ON or OFF."
+ icon_state = "logic_xnor"
+ output_state = LOGIC_ON
+
+/obj/machinery/logic_gate/xnor/handle_logic()
+ if((input1_state == LOGIC_ON || input1_state == LOGIC_FLICKER) && (input2_state == LOGIC_ON || input2_state == LOGIC_FLICKER)) //Both inputs are ON/FLICKER
+ if(input1_state == LOGIC_ON && input2_state == LOGIC_ON) //Only continuous signal when both inputs are ON
+ output_state = LOGIC_ON
+ else //If at least one input is FLICKER, output FLICKER
+ output_state = LOGIC_FLICKER
+ else if(input1_state == LOGIC_OFF && input2_state == LOGIC_OFF) //Both inputs are OFF
+ output_state = LOGIC_ON //Always continuous in this case
+ else //Only one input is ON/FLICKER
+ output_state = LOGIC_OFF
+ return
diff --git a/code/modules/logic/logic_base.dm b/code/modules/logic/logic_base.dm
new file mode 100644
index 00000000000..b126b1af214
--- /dev/null
+++ b/code/modules/logic/logic_base.dm
@@ -0,0 +1,283 @@
+
+/obj/machinery/logic_gate
+ name = "Logic Base"
+ desc = "This does nothing except connect to things. Highly illogical, report to a coder at once if you see this in-game."
+ icon = 'icons/obj/computer3.dmi'
+ icon_state = "serverframe"
+ density = 1
+ anchored = 1
+
+ settagwhitelist = list("input1_id_tag", "input2_id_tag", "output_id_tag")
+
+ var/tamperproof = 0 //if set, will make the machine unable to be destroyed, adjusted, etc. via in-game interaction (USE ONLY FOR MAPPING STUFF)
+ var/mono_input = 0 //if set, will ignore input2
+
+ var/datum/radio_frequency/radio_connection
+ var/frequency = 0
+
+ /*
+ Some notes on Input/Output:
+ - Multiple things can be linked to the same input or output tag, just like how wires can connect multiple sources and receivers.
+ - For inputs, only the last signal received BEFORE a process() call will be used in the logic handling.
+ - Input states are updated immediately whenever an input signal is received, so it is possible to update multiple times within a single process cycle.
+ - This means if you have multiple connected inputs, but the last signal received before the process() call is OFF, it won't matter if the others are both ON.
+ - For this reason, please set up your logic properly. You can theoretically chain these infinitely, so there's no need to link multiple things to a single input.
+ - For outputs, the signal will attempt to be sent out every process() call, to ensure newly connected things are updated within one process cycle
+ - Connecting an output to multiple inputs should not cause issues, as long as you don't have multiple connections to a given input (see previous notes on inputs).
+ - The output state is determined immediately preceeding the signal broadcast, using the input states at the time of the process() call, not when a signal is received.
+ - Because of how the process cycle works, it is possible that it may take multiple cycles for a signal to fully propogate through a logic chain.
+ - This is because machines attempt to process in the order they were added to the scheduler.
+ - Building the logic gates at the end of the chain first may cause delays in signal propogation.
+ If you take all this into consideration when linking and using logic machinery, you should have no unexpected issues with input/output. Your design flaws are on you though.
+ */
+
+ var/input1_id_tag = null
+ var/input1_state = LOGIC_OFF
+ var/input2_id_tag = null
+ var/input2_state = LOGIC_OFF
+ var/output_id_tag = null
+ var/output_state = LOGIC_OFF
+
+/obj/machinery/logic_gate/New()
+ if(tamperproof) //doing this during New so we don't have to worry about forgetting to set these vars during editting / defining
+ unacidable = 1
+ ..()
+ if(radio_controller)
+ set_frequency(frequency)
+ component_parts = list()
+ var/obj/item/weapon/circuitboard/logic_gate/LG = new(null)
+ LG.set_type(type)
+ component_parts += LG
+ component_parts += new /obj/item/stack/cable_coil(null, 1)
+
+/obj/machinery/logic_gate/initialize()
+ ..()
+ set_frequency(frequency)
+
+/obj/machinery/logic_gate/proc/set_frequency(new_frequency)
+ radio_controller.remove_object(src, frequency)
+ frequency = new_frequency
+ radio_connection = radio_controller.add_object(src, frequency, RADIO_LOGIC)
+ return
+
+/obj/machinery/logic_gate/Destroy()
+ if(radio_controller)
+ radio_controller.remove_object(src,frequency)
+ return ..()
+
+/obj/machinery/logic_gate/process()
+ handle_logic()
+ handle_output() //All output will send for at least one cycle, and will attempt to send every cycle. Hopefully this won't be too taxing.
+ return
+
+/obj/machinery/logic_gate/proc/handle_logic()
+ return
+
+/obj/machinery/logic_gate/proc/handle_output()
+ if(!radio_connection) //can't output without this
+ return
+
+ if(output_id_tag == null) //Don't output to an undefined id_tag
+ return
+
+ var/datum/signal/signal = new
+ signal.transmission_method = 1 //radio signal
+ signal.source = src
+
+ signal.data = list(
+ "tag" = output_id_tag,
+ "sigtype" = "logic",
+ "state" = output_state,
+ )
+
+ radio_connection.post_signal(src, signal, filter = RADIO_LOGIC)
+
+/obj/machinery/logic_gate/receive_signal(datum/signal/signal, receive_method, receive_param)
+ if(!signal.data["tag"] || ((signal.data["tag"] != input1_id_tag) && (signal.data["tag"] != input2_id_tag)) || (signal.data["sigtype"] != "logic"))
+ //If the signal lacks tag data, the signal's tag data doesn't match either input id tag, or is not a "logic" signal, ignore it since it's not for us
+ return
+
+ if(signal.data["tag"] == input1_id_tag) //If the signal is for input1
+ if(signal.data["state"] == input1_state) //If we already match, ignore the new signal since nothing changes
+ return
+ if(signal.data["state"] == LOGIC_OFF) //Shut it down and keep it off
+ input1_state = LOGIC_OFF
+ return
+ if(signal.data["state"] == LOGIC_ON) //Turn it on and keep it on
+ input1_state = LOGIC_ON
+ return
+ if(signal.data["state"] == LOGIC_FLICKER) //Turn it on then turn it off
+ if(input1_state == LOGIC_ON) //An existing continuous ON state overrides new flicker signals
+ return
+ input1_state = LOGIC_FLICKER
+ spawn(LOGIC_FLICKER_TIME)
+ if(input1_state == LOGIC_FLICKER) //Make sure we didn't get a new continuous signal set while we waited (those take priority)
+ input1_state = LOGIC_OFF
+ return
+
+ //Now, you may be wondering why I included those returns.
+ //The answer is "If you link both inputs to the same source, you're an idiot and deserve to have it break", so yeah. Deal with it.
+
+ if(mono_input)
+ //We only care about input1, so if we didn't receive a signal for that, we're done.
+ return
+
+ if(signal.data["tag"] == input2_id_tag) //If the signal is for input2 (reaching this point assumes mono_input is not set)
+ if(signal.data["state"] == input2_state) //If we already match, ignore the new signal since nothing changes
+ return
+ if(signal.data["state"] == LOGIC_OFF) //Shut it down and keep it off
+ input2_state = LOGIC_OFF
+ return
+ if(signal.data["state"] == LOGIC_ON) //Turn it on and keep it on
+ input2_state = LOGIC_ON
+ return
+ if(signal.data["state"] == LOGIC_FLICKER) //Turn it on then turn it off
+ if(input2_state == LOGIC_ON) //An existing continuous ON state overrides new flicker signals
+ return
+ input2_state = LOGIC_FLICKER
+ spawn(LOGIC_FLICKER_TIME)
+ if(input2_state == LOGIC_FLICKER) //Make sure we didn't get a new continuous signal set while we waited (those take priority)
+ input2_state = LOGIC_OFF
+ return
+
+/obj/machinery/logic_gate/multitool_menu(var/mob/user, var/obj/item/device/multitool/P)
+ var/input1_state_string
+ var/input2_state_string
+ var/output_state_string
+
+ switch(input1_state)
+ if(LOGIC_OFF)
+ input1_state_string = "OFF"
+ if(LOGIC_ON)
+ input1_state_string = "ON"
+ if(LOGIC_FLICKER)
+ input1_state_string = "FLICKER"
+ else
+ input1_state_string = "ERROR: UNKNOWN STATE"
+
+ switch(input2_state)
+ if(LOGIC_OFF)
+ input2_state_string = "OFF"
+ if(LOGIC_ON)
+ input2_state_string = "ON"
+ if(LOGIC_FLICKER)
+ input2_state_string = "FLICKER"
+ else
+ input2_state_string = "ERROR: UNKNOWN STATE"
+
+ switch(output_state)
+ if(LOGIC_OFF)
+ output_state_string = "OFF"
+ if(LOGIC_ON)
+ output_state_string = "ON"
+ if(LOGIC_FLICKER)
+ output_state_string = "FLICKER"
+ else
+ output_state_string = "ERROR: UNKNOWN STATE"
+ var/menu_contents = {"
+
+ - Input: [format_tag("ID Tag","input1_id_tag")]
+ - Input State: [input1_state_string]
+ "}
+ if(!mono_input)
+ menu_contents = {"
+ - Input 1: [format_tag("ID Tag","input1_id_tag")]
+ - Input 1 State: [input1_state_string]
+ - Input 2: [format_tag("ID Tag","input2_id_tag")]
+ - Input 2 State: [input2_state_string]
+ "}
+ menu_contents += {"
+ - Output: [format_tag("ID Tag","output_id_tag")]
+ - Output State: [output_state_string]
+
+ "}
+ return menu_contents
+
+/obj/machinery/logic_gate/convert/multitool_topic(var/mob/user,var/list/href_list,var/obj/O)
+ ..()
+ update_multitool_menu(user)
+
+/obj/machinery/logic_gate/attackby(obj/item/O, mob/user, params)
+ if(tamperproof)
+ user << "The [src] appears to be tamperproofed! You can't interact with it!"
+ return 0
+ if(istype(O, /obj/item/device/multitool))
+ update_multitool_menu(user)
+ return 1
+ if(istype(O, /obj/item/weapon/screwdriver))
+ panel_open = !panel_open
+ user << "You [panel_open ? "open" : "close"] the access panel."
+ return 1
+ if(panel_open && istype(O, /obj/item/weapon/crowbar))
+ default_deconstruction_crowbar(O)
+ return 1
+
+//////////////////////////////////////
+// Attack procs //
+//////////////////////////////////////
+
+/obj/machinery/logic_gate/attack_ai(mob/user)
+ if(tamperproof)
+ user << "The [src] appears to be tamperproofed! You can't interface with it!"
+ return 0
+ add_hiddenprint(user)
+ return ui_interact(user)
+
+/obj/machinery/logic_gate/attack_ghost(mob/user)
+ if(tamperproof)
+ user << "The [src] appears to be tamperproofed! You can't haunt it!"
+ return 0
+ return ui_interact(user)
+
+/obj/machinery/logic_gate/attack_hand(mob/user)
+ if(tamperproof)
+ user << "The [src] appears to be tamperproofed! You can't interact with it!"
+ return 0
+ . = ..()
+ if(.)
+ return 0
+ return interact(user)
+
+/obj/machinery/logic_gate/attack_alien(mob/user) //No xeno logic, that's too silly.
+ user << "The [src] appears to be too complex! You can't comprehend it and back off in fear!"
+ return 0
+
+/obj/machinery/logic_gate/attack_animal(mob/user) //No animal logic either.
+ user << "The [src] appears to be beyond your comprehension! You can't fathom it!"
+ return 0
+
+/obj/machinery/logic_gate/attack_slime(mob/user) //No slime logic. Seriously.
+ user << "The [src] appears to be beyond your gelatinous understanding! You ignore it!"
+ return 0
+
+/obj/machinery/logic_gate/emp_act(severity)
+ if(tamperproof)
+ return 0
+ ..()
+
+/obj/machinery/logic_gate/ex_act(severity)
+ if(tamperproof)
+ return 0
+ ..()
+
+/obj/machinery/logic_gate/blob_act()
+ if(tamperproof)
+ return 0
+ ..()
+
+/obj/machinery/logic_gate/singularity_act()
+ if(tamperproof)
+ //This is some top-level tamperproofing right here, that's for sure. It can even defy a singularity!
+ return 0
+ ..()
+
+/obj/machinery/logic_gate/bullet_act()
+ if(tamperproof)
+ return 0
+ ..()
+
+/obj/machinery/logic_gate/tesla_act(var/power)
+ if(tamperproof)
+ tesla_zap(src, 3, power) //If we're tamperproof, we'll just bounce the full shock of the tesla zap we got hit by, so it continues on normally without diminishing
+ return 0
+ ..()
\ No newline at end of file
diff --git a/code/modules/logic/mono_input.dm b/code/modules/logic/mono_input.dm
new file mode 100644
index 00000000000..58b76232fa0
--- /dev/null
+++ b/code/modules/logic/mono_input.dm
@@ -0,0 +1,62 @@
+
+//////////////////////////////////
+// Mono-Input Gates //
+//////////////////////////////////
+
+//NOT Gate
+/obj/machinery/logic_gate/not
+ name = "NOT Gate"
+ desc = "Accepts one input and outputs the reverse state."
+ icon_state = "logic_not"
+ mono_input = 1 //NOT Gates are the simplest logic gate because they only utilize one input.
+ output_state = LOGIC_ON //Starts with an active output, since the input will be OFF at start
+/*
+ A quick note regarding NOT Gates:
+ - Connecting multiple things to the input of a NOT Gate can cause weird behaviour due to updating both when it receives a signal and when it calls process().
+ - This means it will attempt to output once for every logic machine connected to its input's own process() call.
+ - It will then attempt to output an additional time based on the current state when it comes to its own process() call.
+ - For this reason, it is HIGHLY RECOMMENDED that you only connect a single signal source to the input of a NOT Gate to avoid signal spasms.
+ - Connecting multiple things to the output of a NOT Gate should not cause this unusual behavior.
+*/
+/obj/machinery/logic_gate/not/handle_logic() //Our output will always be a continuous signal, even with a FLICKER, it just will update the output when the FLICKER ends
+ if(input1_state == LOGIC_ON) //Output is OFF while input is ON
+ output_state = LOGIC_OFF
+ else if(input1_state == LOGIC_FLICKER) //Output is OFF while input is FLICKER, then output returns to ON when input returns to OFF
+ output_state = LOGIC_OFF
+ spawn(LOGIC_FLICKER_TIME + 1) //Call handle_logic again after this delay (the input should update from the spawn(LOGIC_FLICKER_TIME) in receive_signal() by then)
+ handle_logic()
+ else //Output is ON while input is OFF
+ output_state = LOGIC_ON
+ handle_output()
+ return
+
+//STATUS Gate
+/obj/machinery/logic_gate/status
+ name = "Status Gate"
+ desc = "Accepts one input and outputs the same state, showing a colored light based on current state."
+ icon_state = "logic_status"
+ mono_input = 1 //STATUS Gate doesn't actually perform logic operations, but instead acts as a testing conduit.
+
+/*
+ STATUS Gates are largely a diagnostics tool, but I'm sure someone will still make a logic gate rave with them anyways.
+ - There is no need to actually connect an output for these to work, they just need an input to sample from.
+ - STATUS Gates attempt to update their lights whenever they receive a signal.
+*/
+
+/obj/machinery/logic_gate/status/receive_signal(datum/signal/signal, receive_method, receive_params)
+ ..()
+ handle_logic() //STATUS Gate calls handle_logic() when it receives a signal to update its light and output_state
+
+/obj/machinery/logic_gate/status/handle_logic()
+ output_state = input1_state //Output is equal to input, since it is simply a connection with an attached light
+ if(output_state == LOGIC_OFF) //Red light when OFF
+ set_light(2,2,"#ff0000")
+ return
+ if(output_state == LOGIC_ON) //Green light when ON
+ set_light(2,2,"#009933")
+ return
+ if(output_state == LOGIC_FLICKER) //Orange light when FLICKER, then update after LOGIC_FLICKER_TIME + 1 to reflect the changed state
+ set_light(2,2,"#ff9900")
+ spawn(LOGIC_FLICKER_TIME + 1)
+ handle_logic()
+ return
diff --git a/code/modules/research/designs/autolathe_designs.dm b/code/modules/research/designs/autolathe_designs.dm
index 355f26af6da..234b23800e6 100644
--- a/code/modules/research/designs/autolathe_designs.dm
+++ b/code/modules/research/designs/autolathe_designs.dm
@@ -638,4 +638,12 @@
build_type = AUTOLATHE
materials = list(MAT_METAL = 150, MAT_GLASS = 125)
build_path = /obj/item/weapon/hand_labeler
+ category = list("initial", "Electronics")
+
+/datum/design/logic_board
+ name = "Logic Circuit"
+ id = "logic_board"
+ build_type = AUTOLATHE
+ materials = list(MAT_METAL = 50, MAT_GLASS = 50)
+ build_path = /obj/item/weapon/circuitboard/logic_gate
category = list("initial", "Electronics")
\ No newline at end of file
diff --git a/html/changelogs/AutoChangeLog-pr-4041.yml b/html/changelogs/AutoChangeLog-pr-4041.yml
new file mode 100644
index 00000000000..5f208e673c7
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-4041.yml
@@ -0,0 +1,6 @@
+author: FalseIncarnate
+delete-after: True
+changes:
+ - rscadd: "Adds logic gates, for doing illogical things in a logical manner."
+ - rscadd: "Adds buildable light switches, for all your light toggling needs."
+ - bugfix: "Fixes a resource duplication bug with mass driver buttons."
diff --git a/html/changelogs/AutoChangeLog-pr-4075.yml b/html/changelogs/AutoChangeLog-pr-4075.yml
new file mode 100644
index 00000000000..bd13595c356
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-4075.yml
@@ -0,0 +1,5 @@
+author: Fox McCloud
+delete-after: True
+changes:
+ - rscadd: "Maps in the slime management console to Xenobio"
+ - tweak: "Reduces Xenobio monkey box count from 4 to 2"
diff --git a/icons/obj/computer3.dmi b/icons/obj/computer3.dmi
index 7c87ec36102..30705076d62 100644
Binary files a/icons/obj/computer3.dmi and b/icons/obj/computer3.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 986b83d97b3..75cdb9919c3 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -727,7 +727,7 @@
#include "code\game\objects\items\devices\radio\radio.dm"
#include "code\game\objects\items\mountable_frames\air_alarm.dm"
#include "code\game\objects\items\mountable_frames\apc_frame.dm"
-#include "code\game\objects\items\mountable_frames\driver_button.dm"
+#include "code\game\objects\items\mountable_frames\buttons_switches.dm"
#include "code\game\objects\items\mountable_frames\fire_alarm.dm"
#include "code\game\objects\items\mountable_frames\frames.dm"
#include "code\game\objects\items\mountable_frames\intercom.dm"
@@ -1319,6 +1319,10 @@
#include "code\modules\lighting\lighting_process.dm"
#include "code\modules\lighting\lighting_system.dm"
#include "code\modules\lighting\lighting_turf.dm"
+#include "code\modules\logic\converter.dm"
+#include "code\modules\logic\dual_input.dm"
+#include "code\modules\logic\logic_base.dm"
+#include "code\modules\logic\mono_input.dm"
#include "code\modules\media\jukebox.dm"
#include "code\modules\media\machinery.dm"
#include "code\modules\media\mediamanager.dm"