diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm
index f6db4f65d4..9e29ba9998 100644
--- a/code/controllers/configuration.dm
+++ b/code/controllers/configuration.dm
@@ -60,6 +60,7 @@ var/list/gamemode_cache = list()
var/humans_need_surnames = 0
var/allow_random_events = 0 // enables random events mid-round when set to 1
var/allow_ai = 1 // allow ai job
+ var/allow_ai_drones = 0 // allow ai controlled drones
var/hostedby = null
var/respawn = 1
var/guest_jobban = 1
@@ -400,6 +401,9 @@ var/list/gamemode_cache = list()
if ("allow_ai")
config.allow_ai = 1
+ if ("allow_ai_drones")
+ config.allow_ai_drones = 1
+
// if ("authentication")
// config.enable_authentication = 1
diff --git a/code/game/machinery/computer/ai_core.dm b/code/game/machinery/computer/ai_core.dm
index fa491c20e7..0e68f1e7a3 100644
--- a/code/game/machinery/computer/ai_core.dm
+++ b/code/game/machinery/computer/ai_core.dm
@@ -200,14 +200,16 @@ GLOBAL_LIST_BOILERPLATE(all_deactivated_AI_cores, /obj/structure/AIcore/deactiva
if(!istype(transfer) || locate(/mob/living/silicon/ai) in src)
return
+ if(transfer.controlling_drone)
+ transfer.controlling_drone.release_ai_control("Unit control lost. Core transfer completed.")
transfer.aiRestorePowerRoutine = 0
transfer.control_disabled = 0
transfer.aiRadio.disabledAi = 0
transfer.loc = get_turf(src)
transfer.create_eyeobj()
transfer.cancel_camera()
- user << "Transfer successful: [transfer.name] placed within stationary core."
- transfer << "You have been transferred into a stationary core. Remote device connection restored."
+ to_chat(user, "Transfer successful: [transfer.name] placed within stationary core.")
+ to_chat(transfer, "You have been transferred into a stationary core. Remote device connection restored.")
if(card)
card.clear()
diff --git a/code/game/objects/items/devices/aicard.dm b/code/game/objects/items/devices/aicard.dm
index b57c67bac0..7e288dbadc 100644
--- a/code/game/objects/items/devices/aicard.dm
+++ b/code/game/objects/items/devices/aicard.dm
@@ -64,20 +64,24 @@
add_attack_logs(user,carded_ai,"Purged from AI Card")
flush = 1
carded_ai.suiciding = 1
- carded_ai << "Your power has been disabled!"
+ to_chat(carded_ai, "Your power has been disabled!")
while (carded_ai && carded_ai.stat != 2)
+ if(carded_ai.controlling_drone && prob(carded_ai.oxyloss)) //You feel it creeping? Eventually will reach 100, resulting in the second half of the AI's remaining life being lonely.
+ carded_ai.controlling_drone.release_ai_control("Unit lost. Integrity too low to maintain connection.")
carded_ai.adjustOxyLoss(2)
carded_ai.updatehealth()
sleep(10)
flush = 0
if (href_list["radio"])
carded_ai.aiRadio.disabledAi = text2num(href_list["radio"])
- carded_ai << "Your Subspace Transceiver has been [carded_ai.aiRadio.disabledAi ? "disabled" : "enabled"]!"
- user << "You [carded_ai.aiRadio.disabledAi ? "disable" : "enable"] the AI's Subspace Transceiver."
+ to_chat(carded_ai, "Your Subspace Transceiver has been [carded_ai.aiRadio.disabledAi ? "disabled" : "enabled"]!")
+ to_chat(user, "You [carded_ai.aiRadio.disabledAi ? "disable" : "enable"] the AI's Subspace Transceiver.")
if (href_list["wireless"])
carded_ai.control_disabled = text2num(href_list["wireless"])
- carded_ai << "Your wireless interface has been [carded_ai.control_disabled ? "disabled" : "enabled"]!"
- user << "You [carded_ai.control_disabled ? "disable" : "enable"] the AI's wireless interface."
+ to_chat(carded_ai, "Your wireless interface has been [carded_ai.control_disabled ? "disabled" : "enabled"]!")
+ to_chat(user, "You [carded_ai.control_disabled ? "disable" : "enable"] the AI's wireless interface.")
+ if(carded_ai.control_disabled && carded_ai.controlling_drone)
+ carded_ai.controlling_drone.release_ai_control("Unit control terminated at intellicore port.")
update_icon()
return 1
@@ -94,12 +98,12 @@
icon_state = "aicard"
/obj/item/device/aicard/proc/grab_ai(var/mob/living/silicon/ai/ai, var/mob/living/user)
- if(!ai.client)
- user << "ERROR: AI [ai.name] is offline. Unable to transfer."
+ if(!ai.client && !ai.controlling_drone)
+ to_chat(user, "ERROR: AI [ai.name] is offline. Unable to transfer.")
return 0
if(carded_ai)
- user << "Transfer failed: Existing AI found on remote device. Remove existing AI to install a new one."
+ to_chat(user, "Transfer failed: Existing AI found on remote device. Remove existing AI to install a new one.")
return 0
if(!user.IsAdvancedToolUser() && isanimal(user))
@@ -108,7 +112,9 @@
return 0
user.visible_message("\The [user] starts transferring \the [ai] into \the [src]...", "You start transferring \the [ai] into \the [src]...")
- ai << "\The [user] is transferring you into \the [src]!"
+ to_chat(ai, "\The [user] is transferring you into \the [src]!")
+ if(ai.controlling_drone)
+ to_chat(ai.controlling_drone, "\The [user] is transferring you into \the [src]!")
if(do_after(user, 100))
if(istype(ai.loc, /turf/))
@@ -124,11 +130,13 @@
ai.control_disabled = 1
ai.aiRestorePowerRoutine = 0
carded_ai = ai
+ if(ai.controlling_drone)
+ ai.controlling_drone.release_ai_control("Unit control lost.")
if(ai.client)
- ai << "You have been transferred into a mobile core. Remote access lost."
+ to_chat(ai, "You have been transferred into a mobile core. Remote access lost.")
if(user.client)
- user << "Transfer successful: [ai.name] extracted from current device and placed within mobile core."
+ to_chat(ai, "Transfer successful: [ai.name] extracted from current device and placed within mobile core.")
ai.canmove = 1
update_icon()
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 927216190a..60b552013d 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -685,6 +685,9 @@ var/list/ai_verbs_default = list(
card.grab_ai(src, user)
else if(istype(W, /obj/item/weapon/wrench))
+ if(user == controlling_drone)
+ to_chat(user, "The drone's subsystems resist your efforts to tamper with your bolts.")
+ return
if(anchored)
playsound(src, W.usesound, 50, 1)
user.visible_message("\The [user] starts to unbolt \the [src] from the plating...")
diff --git a/code/modules/mob/living/silicon/ai/death.dm b/code/modules/mob/living/silicon/ai/death.dm
index 534117cee0..3bcbd7057b 100644
--- a/code/modules/mob/living/silicon/ai/death.dm
+++ b/code/modules/mob/living/silicon/ai/death.dm
@@ -3,6 +3,10 @@
if(stat == DEAD)
return
+ if(controlling_drone)
+ controlling_drone.release_ai_control("WARNING: Primary control loop failure. Session terminated.")
+ . = ..(gibbed)
+
if(src.eyeobj)
src.eyeobj.setLoc(get_turf(src))
diff --git a/code/modules/mob/living/silicon/ai/life.dm b/code/modules/mob/living/silicon/ai/life.dm
index 413cf49250..c8e7dfdada 100644
--- a/code/modules/mob/living/silicon/ai/life.dm
+++ b/code/modules/mob/living/silicon/ai/life.dm
@@ -8,6 +8,8 @@
if (src.stat!=CONSCIOUS)
src.cameraFollow = null
src.reset_view(null)
+ if(controlling_drone)
+ controlling_drone.release_ai_control("WARNING: Primary control loop failure. Session terminated.")
src.updatehealth()
diff --git a/code/modules/mob/living/silicon/robot/drone/drone.dm b/code/modules/mob/living/silicon/robot/drone/drone.dm
index d102f9e7cb..599eacdfa9 100644
--- a/code/modules/mob/living/silicon/robot/drone/drone.dm
+++ b/code/modules/mob/living/silicon/robot/drone/drone.dm
@@ -19,7 +19,7 @@ var/list/mob_hat_cache = list()
return mob_hat_cache[key]
/mob/living/silicon/robot/drone
- name = "drone"
+ name = "maintenance drone"
real_name = "drone"
icon = 'icons/mob/robots.dmi'
icon_state = "repairbot"
@@ -57,6 +57,8 @@ var/list/mob_hat_cache = list()
var/obj/item/hat
var/hat_x_offset = 0
var/hat_y_offset = -13
+ var/serial_number = 0
+ var/name_override = 0
holder_type = /obj/item/weapon/holder/drone
@@ -71,6 +73,7 @@ var/list/mob_hat_cache = list()
return FALSE
/mob/living/silicon/robot/drone/construction
+ name = "construction drone"
icon_state = "constructiondrone"
law_type = /datum/ai_laws/construction_drone
module_type = /obj/item/weapon/robot_module/drone/construction
@@ -96,6 +99,7 @@ var/list/mob_hat_cache = list()
remove_language("Robot Talk")
add_language("Robot Talk", 0)
add_language("Drone Talk", 1)
+ serial_number = rand(0,999)
//They are unable to be upgraded, so let's give them a bit of a better battery.
cell.maxcharge = 10000
@@ -128,14 +132,22 @@ var/list/mob_hat_cache = list()
name = real_name
/mob/living/silicon/robot/drone/updatename()
- real_name = "maintenance drone ([rand(100,999)])"
+ if(name_override)
+ return
+ if(controlling_ai)
+ real_name = "remote drone ([controlling_ai])"
+ else
+ real_name = "[initial(name)] ([serial_number])"
name = real_name
/mob/living/silicon/robot/drone/updateicon()
overlays.Cut()
if(stat == 0)
- overlays += "eyes-[icon_state]"
+ if(controlling_ai)
+ overlays += "eyes-[icon_state]-ai"
+ else
+ overlays += "eyes-[icon_state]"
else
overlays -= "eyes"
if(hat) // Let the drones wear hats.
@@ -214,15 +226,18 @@ var/list/mob_hat_cache = list()
return
if(emagged)
- to_chat(user, "\The [user] attempts to load subversive software into you, but your hacked subroutines ignore the attempt.")
+ to_chat(src, "\The [user] attempts to load subversive software into you, but your hacked subroutines ignore the attempt.")
to_chat(user, "You attempt to subvert [src], but the sequencer has no effect.")
return
to_chat(user, "You swipe the sequencer across [src]'s interface and watch its eyes flicker.")
- to_chat(user, "You feel a sudden burst of malware loaded into your execute-as-root buffer. Your tiny brain methodically parses, loads and executes the script.")
- message_admins("[key_name_admin(user)] emagged drone [key_name_admin(src)]. Laws overridden.")
- log_game("[key_name(user)] emagged drone [key_name(src)]. Laws overridden.")
+ if(controlling_ai)
+ to_chat(src, "\The [user] loads some kind of subversive software into the remote drone, corrupting its lawset but luckily sparing yours.")
+ else
+ to_chat(src, "You feel a sudden burst of malware loaded into your execute-as-root buffer. Your tiny brain methodically parses, loads and executes the script.")
+
+ log_game("[key_name(user)] emagged drone [key_name(src)][controlling_ai ? " but AI [key_name(controlling_ai)] is in remote control" : " Laws overridden"].")
var/time = time2text(world.realtime,"hh:mm:ss")
lawchanges.Add("[time] : [user.name]([user.key]) emagged [name]([key])")
@@ -235,9 +250,10 @@ var/list/mob_hat_cache = list()
var/datum/gender/TU = gender_datums[user.get_visible_gender()]
set_zeroth_law("Only [user.real_name] and people [TU.he] designate[TU.s] as being such are operatives.")
- src << "Obey these laws:"
- laws.show_laws(src)
- src << "ALERT: [user.real_name] [TU.is] your new master. Obey your new laws and [TU.his] commands."
+ if(!controlling_ai)
+ to_chat(src, "Obey these laws:")
+ laws.show_laws(src)
+ to_chat(src, "ALERT: [user.real_name] is your new master. Obey your new laws and \his commands.")
return 1
//DRONE LIFE/DEATH
@@ -263,26 +279,41 @@ var/list/mob_hat_cache = list()
return
..()
+/mob/living/silicon/robot/drone/death(gibbed)
+ if(controlling_ai)
+ release_ai_control("WARNING: remote system failure. Connection timed out.")
+ . = ..(gibbed)
+
//DRONE MOVEMENT.
/mob/living/silicon/robot/drone/Process_Spaceslipping(var/prob_slip)
return 0
//CONSOLE PROCS
/mob/living/silicon/robot/drone/proc/law_resync()
+
+ if(controlling_ai)
+ to_chat(src, "Someone issues a remote law reset order for this unit, but you disregard it.")
+ return
+
if(stat != 2)
if(emagged)
- src << "You feel something attempting to modify your programming, but your hacked subroutines are unaffected."
+ to_chat(src, "You feel something attempting to modify your programming, but your hacked subroutines are unaffected.")
else
- src << "A reset-to-factory directive packet filters through your data connection, and you obediently modify your programming to suit it."
+ to_chat(src, "A reset-to-factory directive packet filters through your data connection, and you obediently modify your programming to suit it.")
full_law_reset()
show_laws()
/mob/living/silicon/robot/drone/proc/shut_down()
+
+ if(controlling_ai && mind.special_role)
+ to_chat(src, "Someone issued a remote kill order for this unit, but you disregard it.")
+ return
+
if(stat != 2)
if(emagged)
- src << "You feel a system kill order percolate through your tiny brain, but it doesn't seem like a good idea to you."
+ to_chat(src, "You feel a system kill order percolate through [controlling_ai ? "the drones" : "your"] tiny brain, but it doesn't seem like a good idea to [controlling_ai ? "it" : "you"].")
else
- src << "You feel a system kill order percolate through your tiny brain, and you obediently destroy yourself."
+ to_chat(src, "You feel a system kill order percolate through [controlling_ai ? "the drones" : "your"] tiny brain, and [controlling_ai ? "it" : "you"] obediently destroy[controlling_ai ? "s itself" : " yourself"].")
death()
/mob/living/silicon/robot/drone/proc/full_law_reset()
@@ -291,6 +322,21 @@ var/list/mob_hat_cache = list()
clear_ion_laws(1)
laws = new law_type
+/mob/living/silicon/robot/drone/show_laws(var/everyone = 0)
+ if(!controlling_ai)
+ return..()
+ to_chat(src, "Obey these laws:")
+ controlling_ai.laws_sanity_check()
+ controlling_ai.laws.show_laws(src)
+
+/mob/living/silicon/robot/drone/robot_checklaws()
+ set category = "Silicon Commands"
+ set name = "State Laws"
+
+ if(!controlling_ai)
+ return ..()
+ controlling_ai.subsystem_law_manager()
+
//Reboot procs.
/mob/living/silicon/robot/drone/proc/request_player()
@@ -348,14 +394,6 @@ var/list/mob_hat_cache = list()
..()
flavor_text = "It's a bulky construction drone stamped with a Sol Central glyph."
-/mob/living/silicon/robot/drone/construction/updatename()
- real_name = "construction drone ([rand(100,999)])"
- name = real_name
-
/mob/living/silicon/robot/drone/mining/init()
..()
flavor_text = "It's a bulky mining drone stamped with a Grayson logo."
-
-/mob/living/silicon/robot/drone/mining/updatename()
- real_name = "mining drone ([rand(100,999)])"
- name = real_name
diff --git a/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm b/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
index e9589622a5..fdb9c09eb5 100644
--- a/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
+++ b/code/modules/mob/living/silicon/robot/drone/drone_manufacturer.dm
@@ -75,20 +75,22 @@
if(!produce_drones || !config.allow_drone_spawn || count_drones() >= config.max_maint_drones)
return
- if(!player || !istype(player.mob,/mob/observer/dead))
+ if(player && !istype(player.mob,/mob/observer/dead))
return
- announce_ghost_joinleave(player, 0, "They have taken control over a maintenance drone.")
visible_message("\The [src] churns and grinds as it lurches into motion, disgorging a shiny new drone after a few moments.")
flick("h_lathe_leave",src)
+ drone_progress = 0
time_last_drone = world.time
- if(player.mob && player.mob.mind) player.mob.mind.reset()
- var/mob/living/silicon/robot/drone/new_drone = new drone_type(get_turf(src))
- new_drone.transfer_personality(player)
- new_drone.master_fabricator = src
- drone_progress = 0
+ var/mob/living/silicon/robot/drone/new_drone = new drone_type(get_turf(src))
+ if(player)
+ announce_ghost_joinleave(player, 0, "They have taken control over a maintenance drone.")
+ if(player.mob && player.mob.mind) player.mob.mind.reset()
+ new_drone.transfer_personality(player)
+
+ return new_drone
/mob/observer/dead/verb/join_as_drone()
diff --git a/code/modules/mob/living/silicon/robot/drone/drone_remote_control.dm b/code/modules/mob/living/silicon/robot/drone/drone_remote_control.dm
new file mode 100644
index 0000000000..77abb0e59d
--- /dev/null
+++ b/code/modules/mob/living/silicon/robot/drone/drone_remote_control.dm
@@ -0,0 +1,104 @@
+/mob/living/silicon/ai
+ var/mob/living/silicon/robot/drone/controlling_drone
+
+/mob/living/silicon/robot/drone
+ var/mob/living/silicon/ai/controlling_ai
+
+/mob/living/silicon/robot/drone/attack_ai(var/mob/living/silicon/ai/user)
+
+ if(!istype(user) || controlling_ai || !config.allow_drone_spawn || !config.allow_ai_drones)
+ return
+
+ if(client || key)
+ to_chat(user, "You cannot take control of an autonomous, active drone.")
+ return
+
+ if(health < -35 || emagged)
+ to_chat(user, "WARNING: connection timed out.")
+ return
+
+ user.controlling_drone = src
+ user.teleop = src
+ radio.channels = user.aiRadio.keyslot2.channels
+ controlling_ai = user
+ verbs += /mob/living/silicon/robot/drone/proc/release_ai_control_verb
+ local_transmit = FALSE
+ languages = controlling_ai.languages.Copy()
+ speech_synthesizer_langs = controlling_ai.speech_synthesizer_langs.Copy()
+ stat = CONSCIOUS
+ if(user.mind)
+ user.mind.transfer_to(src)
+ else
+ key = user.key
+ updatename()
+ to_chat(src, "You have shunted your primary control loop into \a [initial(name)]. Use the Release Control verb to return to your core.")
+
+/obj/machinery/drone_fabricator/attack_ai(var/mob/living/silicon/ai/user as mob)
+
+ if(!istype(user) || user.controlling_drone || !config.allow_drone_spawn || !config.allow_ai_drones)
+ return
+
+ if(stat & NOPOWER)
+ to_chat(user, "\The [src] is unpowered.")
+ return
+
+ if(!produce_drones)
+ to_chat(user, "\The [src] is disabled.")
+ return
+
+ if(drone_progress < 100)
+ to_chat(user, "\The [src] is not ready to produce a new drone.")
+ return
+
+ if(count_drones() >= config.max_maint_drones)
+ to_chat(user, "The drone control subsystems are tasked to capacity; they cannot support any more drones.")
+ return
+
+ var/mob/living/silicon/robot/drone/new_drone = create_drone()
+ user.controlling_drone = new_drone
+ user.teleop = new_drone
+ new_drone.radio.channels = user.aiRadio.keyslot2.channels
+ new_drone.controlling_ai = user
+ new_drone.verbs += /mob/living/silicon/robot/drone/proc/release_ai_control_verb
+ new_drone.local_transmit = FALSE
+ new_drone.languages = new_drone.controlling_ai.languages.Copy()
+ new_drone.speech_synthesizer_langs = new_drone.controlling_ai.speech_synthesizer_langs.Copy()
+
+ if(user.mind)
+ user.mind.transfer_to(new_drone)
+ else
+ new_drone.key = user.key
+ new_drone.updatename()
+
+ to_chat(new_drone, "You have shunted your primary control loop into \a [initial(new_drone.name)]. Use the Release Control verb to return to your core.")
+
+/mob/living/silicon/robot/drone/proc/release_ai_control_verb()
+ set name = "Release Control"
+ set desc = "Release control of a remote drone."
+ set category = "Silicon Commands"
+
+ release_ai_control("Remote session terminated.")
+
+/mob/living/silicon/robot/drone/proc/release_ai_control(var/message = "Connection terminated.")
+
+ if(controlling_ai)
+ if(mind)
+ mind.transfer_to(controlling_ai)
+ else
+ controlling_ai.key = key
+ to_chat(controlling_ai, "[message]")
+ controlling_ai.controlling_drone = null
+ controlling_ai.teleop = null
+ controlling_ai = null
+
+ radio.channels = module.channels
+ verbs -= /mob/living/silicon/robot/drone/proc/release_ai_control_verb
+ languages = initial(languages)
+ speech_synthesizer_langs = initial(speech_synthesizer_langs)
+ remove_language("Robot Talk")
+ add_language("Robot Talk", 0)
+ add_language("Drone Talk", 1)
+ local_transmit = TRUE
+ full_law_reset()
+ updatename()
+ death()
diff --git a/html/changelogs/Mechoid - AI_Drones.yml b/html/changelogs/Mechoid - AI_Drones.yml
new file mode 100644
index 0000000000..df2c200589
--- /dev/null
+++ b/html/changelogs/Mechoid - AI_Drones.yml
@@ -0,0 +1,7 @@
+
+author: Mechoid
+
+delete-after: True
+
+changes:
+ - rscadd: "Allow AIs to create and take control of mindless drones from fabricators."
diff --git a/icons/mob/robots.dmi b/icons/mob/robots.dmi
index 8b9a9c9cc4..fbbc875bc6 100644
Binary files a/icons/mob/robots.dmi and b/icons/mob/robots.dmi differ
diff --git a/polaris.dme b/polaris.dme
index f9d457a941..8cd6d29bc7 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -1872,6 +1872,7 @@
#include "code\modules\mob\living\silicon\robot\drone\drone_damage.dm"
#include "code\modules\mob\living\silicon\robot\drone\drone_items.dm"
#include "code\modules\mob\living\silicon\robot\drone\drone_manufacturer.dm"
+#include "code\modules\mob\living\silicon\robot\drone\drone_remote_control.dm"
#include "code\modules\mob\living\silicon\robot\drone\drone_say.dm"
#include "code\modules\mob\living\silicon\robot\robot_modules\event.dm"
#include "code\modules\mob\living\silicon\robot\robot_modules\station.dm"