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"