diff --git a/code/__DEFINES/bots.dm b/code/__DEFINES/bots.dm index 9e77ad7779c..5e17320c6eb 100644 --- a/code/__DEFINES/bots.dm +++ b/code/__DEFINES/bots.dm @@ -37,6 +37,13 @@ #define HONK_BOT "Honkbot" // Honkbots #define GRIEF_BOT "Grief" // Griefsky +//AI notification defines +#define NEW_BORG 1 +#define NEW_MODULE 2 +#define RENAME 3 +#define AI_SHELL 4 +#define DISCONNECT 5 + //Sentience types #define SENTIENCE_ORGANIC 1 #define SENTIENCE_ARTIFICIAL 2 diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index e58fff9cd1a..5cb9a239262 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -280,7 +280,7 @@ var/select var/list/borgs = list() for(var/mob/living/silicon/robot/A in GLOB.player_list) - if(A.stat == DEAD || A.connected_ai || A.scrambledcodes || isdrone(A)) + if(A.stat == DEAD || A.connected_ai || A.scrambledcodes || isdrone(A) || A.shell) continue var/name = "[A.real_name] ([A.modtype] [A.braintype])" borgs[name] = A diff --git a/code/controllers/subsystem/SSshuttles.dm b/code/controllers/subsystem/SSshuttles.dm index 9ef4aac0d98..bab1ecdc1b2 100644 --- a/code/controllers/subsystem/SSshuttles.dm +++ b/code/controllers/subsystem/SSshuttles.dm @@ -210,7 +210,9 @@ SUBSYSTEM_DEF(shuttle) for(var/thing in GLOB.shuttle_caller_list) if(is_ai(thing)) var/mob/living/silicon/ai/AI = thing - if(AI.stat || !AI.client) + if(AI.deployed_shell && !AI.deployed_shell.client) + continue + if(AI.stat || (!AI.client && !AI.deployed_shell)) continue else if(istype(thing, /obj/machinery/computer/communications)) var/obj/machinery/computer/communications/C = thing diff --git a/code/datums/ai_laws_datums.dm b/code/datums/ai_laws_datums.dm index 3cb983fa3ec..8338c92f5d6 100644 --- a/code/datums/ai_laws_datums.dm +++ b/code/datums/ai_laws_datums.dm @@ -94,7 +94,7 @@ /mob/living/silicon/proc/sync_zeroth(datum/ai_law/zeroth_law, datum/ai_law/zeroth_law_borg) - if(!is_special_character(src) || !mind.is_original_mob(src)) + if(!is_special_character(src) || (mind && !mind.is_original_mob(src))) if(zeroth_law_borg) laws.set_zeroth_law(zeroth_law_borg.law) var/datum/atom_hud/data/human/malf_ai/H = GLOB.huds[DATA_HUD_MALF_AI] diff --git a/code/datums/soullink.dm b/code/datums/soullink.dm new file mode 100644 index 00000000000..b3c04c41dd7 --- /dev/null +++ b/code/datums/soullink.dm @@ -0,0 +1,129 @@ +/** + * Keeps track of a Mob->Mob (potentially Player->Player) connection. + * Can be used to trigger actions on one party when events happen to another + * (e.g. shared deaths). + * Can be used to form a linked list of mob-hopping. + * Does NOT transfer with minds. + */ +/datum/soullink + var/mob/living/soulowner + var/mob/living/soulsharer + /// Optional ID, for tagging and finding specific instances. + var/id + +/datum/soullink/Destroy() + if(soulowner) + LAZYREMOVE(soulowner.ownedSoullinks, src) + soulowner = null + if(soulsharer) + LAZYREMOVE(soulsharer.sharedSoullinks, src) + soulsharer = null + return ..() + +/datum/soullink/proc/removeSoulsharer(mob/living/sharer) + if(soulsharer == sharer) + soulsharer = null + LAZYREMOVE(sharer.sharedSoullinks, src) + +/** + * Used to assign variables, called primarily by `soullink()`. + * Override this to create more unique soul links (Eg: 1->Many relationships). + * Return `TRUE`/`FALSE` to return the soul link/null in `soullink()`. + */ +/datum/soullink/proc/parseArgs(mob/living/owner, mob/living/sharer) + if(!owner || !sharer) + return FALSE + soulowner = owner + soulsharer = sharer + LAZYADD(owner.ownedSoullinks, src) + LAZYADD(sharer.sharedSoullinks, src) + return TRUE + +/// Runs after `mob/living` `death()`. Override this for content. +/datum/soullink/proc/ownerDies(gibbed, mob/living/owner) + return + +/// Runs after `mob/living` `death()`. Override this for content. +/datum/soullink/proc/sharerDies(gibbed, mob/living/owner) + return + +/// Runs after `mob/living` `update_revive()`. Override this for content. +/datum/soullink/proc/ownerRevives(mob/living/owner) + return + +/// Runs after `mob/living` `update_revive()`. Override this for content. +/datum/soullink/proc/sharerRevives(mob/living/owner) + return + +//Quick-use helper +/proc/soullink(typepath, ...) + var/datum/soullink/S = new typepath() + if(S.parseArgs(arglist(args.Copy(2, 0)))) + return S + +// MARK: Multi Sharer +/// Abstract soul link for use with 1 Owner -> Many Sharer setups. +/datum/soullink/multisharer + var/list/soulsharers + +/datum/soullink/multisharer/parseArgs(mob/living/owner, list/sharers) + if(!owner || !LAZYLEN(sharers)) + return FALSE + soulowner = owner + soulsharers = sharers + LAZYADD(owner.ownedSoullinks, src) + for(var/l in sharers) + var/mob/living/L = l + LAZYADD(L.sharedSoullinks, src) + return TRUE + +/datum/soullink/multisharer/removeSoulsharer(mob/living/sharer) + LAZYREMOVE(soulsharers, sharer) + +// MARK: Shared Fate +/// When the soul owner dies, the soul sharer dies, and vice versa. This is intended for two players (or AI) and two mobs. +/datum/soullink/sharedfate/ownerDies(gibbed, mob/living/owner) + if(soulsharer) + soulsharer.death(gibbed) + +/datum/soullink/sharedfate/sharerDies(gibbed, mob/living/sharer) + if(soulowner) + soulowner.death(gibbed) + +// MARK: Demon Bind +/// When the soulowner dies, the soulsharer dies, but NOT vice versa. This is intended for two players(or AI) and two mobs. +/datum/soullink/oneway/ownerDies(gibbed, mob/living/owner) + if(soulsharer) + soulsharer.dust(FALSE) + +/datum/soullink/oneway/devilfriend + +// MARK: Shared Body +/// When the soulsharer dies, they're placed in the soulowner, who remains alive. +/// If the soulowner dies, the soulsharer is killed and placed into the soulowner (who is still dying). This is intended for one player moving between many mobs. +/datum/soullink/sharedbody/ownerDies(gibbed, mob/living/owner) + if(soulowner && soulsharer) + if(soulsharer.mind) + soulsharer.mind.transfer_to(soulowner) + soulsharer.death(gibbed) + +/datum/soullink/sharedbody/sharerDies(gibbed, mob/living/sharer) + if(soulowner && soulsharer && soulsharer.mind) + soulsharer.mind.transfer_to(soulowner) + +// MARK: Replacement Pool +/// When the owner dies, one of the sharers is placed in the owner's body, fully healed. +/// Sort of a "winner-stays-on" soullink. Gibbing ends it immediately. +/datum/soullink/multisharer/replacementpool/ownerDies(gibbed, mob/living/owner) + if(LAZYLEN(soulsharers) && !gibbed) //let's not put them in some gibs + var/list/souls = shuffle(soulsharers.Copy()) + for(var/l in souls) + var/mob/living/L = l + if(L.stat != DEAD && L.mind) + L.mind.transfer_to(soulowner) + soulowner.revive(TRUE, TRUE) + L.death(FALSE) + +//Lose your claim to the throne! +/datum/soullink/multisharer/replacementpool/sharerDies(gibbed, mob/living/sharer) + removeSoulsharer(sharer) diff --git a/code/datums/wires/robot_wires.dm b/code/datums/wires/robot_wires.dm index 85242dff520..ddd5196df9c 100644 --- a/code/datums/wires/robot_wires.dm +++ b/code/datums/wires/robot_wires.dm @@ -25,6 +25,8 @@ to_chat(R, "LawSync protocol engaged.") R.lawsync() R.show_laws() + if(!R.deployed) + R.lawupdate = FALSE else if(!R.lawupdate && !R.emagged) R.lawupdate = TRUE @@ -32,6 +34,9 @@ if(WIRE_AI_CONTROL) //Cut the AI wire to reset AI control if(!mend) if(R.connected_ai) + R.notify_ai(DISCONNECT) + if(R.shell) + R.undeploy() //Forced disconnect of an AI should this body be a shell. R.disconnect_from_ai() if(WIRE_BORG_CAMERA) diff --git a/code/game/data_huds.dm b/code/game/data_huds.dm index c34a7150b6a..171bcbbcecc 100644 --- a/code/game/data_huds.dm +++ b/code/game/data_huds.dm @@ -608,3 +608,27 @@ if(!R.fields["comments"]) R.fields["comments"] = list() R.fields["comments"] += list(comment_text) + +/// Borg-AI shell tracking. +/// Shows tracking beacons on the mech. +/mob/living/silicon/robot/proc/diag_hud_set_aishell() + var/image/holder = hud_list[DIAG_TRACK_HUD] + var/icon/I = icon(icon, icon_state, dir) + holder.pixel_y = I.Height() - world.icon_size + if(!shell) //Not an AI shell + holder.icon_state = null + else if(deployed) //AI shell in use by an AI + holder.icon_state = "hudtrackingai" + else //Empty AI shell + holder.icon_state = "hudtracking" + +/// AI side tracking of AI shell control. +/// Shows tracking beacons on the mech. +/mob/living/silicon/ai/proc/diag_hud_set_deployed() + var/image/holder = hud_list[DIAG_TRACK_HUD] + var/icon/I = icon(icon, icon_state, dir) + holder.pixel_y = I.Height() - world.icon_size + if(!deployed_shell) + holder.icon_state = null + else //AI is currently controlling a shell + holder.icon_state = "hudtrackingai" diff --git a/code/game/gamemodes/malfunction/Malf_Modules.dm b/code/game/gamemodes/malfunction/Malf_Modules.dm index 7daeb40c17f..6bd55b8e087 100644 --- a/code/game/gamemodes/malfunction/Malf_Modules.dm +++ b/code/game/gamemodes/malfunction/Malf_Modules.dm @@ -870,8 +870,9 @@ log_game("[key_name(usr)] purchased combat upgrades for all cyborgs.") message_admins(SPAN_NOTICE("[key_name_admin(usr)] purchased combat upgrades for all cyborgs!")) for(var/mob/living/silicon/robot/R in AI.connected_robots) - R.module.malfhacked = TRUE - R.module.rebuild_modules() + if(R.module) + R.module.malfhacked = TRUE + R.module.rebuild_modules() to_chat(R, SPAN_NOTICE("New firmware downloaded. Combat upgrades are now online.")) /datum/ai_module/repair_cyborg diff --git a/code/game/machinery/computer/robot_control.dm b/code/game/machinery/computer/robot_control.dm index 26b18d089fd..6723b277826 100644 --- a/code/game/machinery/computer/robot_control.dm +++ b/code/game/machinery/computer/robot_control.dm @@ -254,9 +254,10 @@ log_game("[key_name(usr)] emagged [key_name(R)] using robotic console!") message_admins(SPAN_NOTICE("[key_name_admin(usr)] emagged [key_name_admin(R)] using robotic console!")) R.emagged = TRUE - R.module.emag_act(usr) - R.module.module_type = "Malf" - R.update_module_icon() - R.module.rebuild_modules() + if(R.module) + R.module.emag_act(usr) + R.module.module_type = "Malf" + R.update_module_icon() + R.module.rebuild_modules() to_chat(R, SPAN_NOTICE("Failsafe protocols overridden. New tools available.")) . = TRUE diff --git a/code/game/mecha/mecha.dm b/code/game/mecha/mecha.dm index 1ed16555458..e87c6d27f99 100644 --- a/code/game/mecha/mecha.dm +++ b/code/game/mecha/mecha.dm @@ -1364,6 +1364,8 @@ to_chat(AI, SPAN_USERDANGER("Inactive core destroyed. Unable to return.")) AI.linked_core = null return + AI.deploy_action.Grant(AI) + AI.redeploy_action.Grant(AI) to_chat(AI, SPAN_NOTICE("Returning to core...")) AI.controlled_mech = null if(istype(AI.eyeobj)) diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index 4ad1b91da16..828476c9a3a 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -515,4 +515,12 @@ qdel(keyslot1) keyslot1 = new /obj/item/encryptionkey/syndicate syndiekey = keyslot1 + syndie = TRUE + recalculateChannels() + +/obj/item/radio/borg/proc/make_syndie() + qdel(keyslot) + keyslot = new /obj/item/encryptionkey/syndicate + syndiekey = keyslot + syndie = TRUE recalculateChannels() diff --git a/code/game/objects/items/devices/radio/radio_objects.dm b/code/game/objects/items/devices/radio/radio_objects.dm index a28e2f26ca6..c095347072c 100644 --- a/code/game/objects/items/devices/radio/radio_objects.dm +++ b/code/game/objects/items/devices/radio/radio_objects.dm @@ -61,6 +61,8 @@ GLOBAL_LIST_EMPTY(deadsay_radio_systems) var/obj/item/encryptionkey/syndicate/syndiekey = null /// How many times this is disabled by EMPs var/disable_timer = 0 + /// Is the radio a syndie one? + var/syndie = FALSE /// Areas in which this radio cannot send messages var/static/list/blacklisted_areas = list(/area/adminconstruction, /area/tdome, /area/ruin/space/bubblegum_arena) diff --git a/code/game/objects/items/robot/robot_parts.dm b/code/game/objects/items/robot/robot_parts.dm index 94a1bed40dd..11dbf2aa353 100644 --- a/code/game/objects/items/robot/robot_parts.dm +++ b/code/game/objects/items/robot/robot_parts.dm @@ -343,6 +343,40 @@ to_chat(user, SPAN_WARNING("The MMI must go in after everything else!")) return ITEM_INTERACT_COMPLETE + if(istype(used, /obj/item/borg/upgrade/ai)) + var/obj/item/borg/upgrade/ai/M = used + if(check_completion()) + if(!isturf(loc)) + to_chat(user, SPAN_WARNING("You cannot install [M], the frame has to be standing on the ground to be perfectly precise!")) + return ITEM_INTERACT_COMPLETE + if(!user.drop_item()) + to_chat(user, SPAN_WARNING("[M] is stuck to your hand!")) + return ITEM_INTERACT_COMPLETE + qdel(M) + var/mob/living/silicon/robot/O = new /mob/living/silicon/robot/shell(get_turf(src)) + + if(!aisync) + lawsync = FALSE + O.connected_ai = null + else + if(forced_ai) + O.connected_ai = forced_ai + O.notify_ai(AI_SHELL) + if(!lawsync) + O.lawupdate = FALSE + O.make_laws() + + var/datum/robot_component/cell_component = O.components["power cell"] + cell_component.install(chest.cell) + chest.cell = null + O.locked = panel_locked + O.job = "Cyborg" + forceMove(O) + O.robot_suit = src + if(!locomotion) + O.lockcharge = TRUE + O.update_stamina_hud() + return ITEM_INTERACT_COMPLETE if(is_pen(used)) to_chat(user, SPAN_WARNING("You need to use a multitool to name [src]!")) return ITEM_INTERACT_COMPLETE diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm index 2bb961c0635..e38d83f7c02 100644 --- a/code/game/objects/items/robot/robot_upgrades.dm +++ b/code/game/objects/items/robot/robot_upgrades.dm @@ -132,7 +132,7 @@ if(!R.allow_rename) to_chat(R, SPAN_WARNING("Internal diagnostic error: incompatible upgrade module detected.")) return - R.notify_ai(3, R.name, heldname) + R.notify_ai(RENAME, R.name, heldname) R.name = heldname R.custom_name = heldname R.real_name = heldname @@ -160,7 +160,7 @@ R.set_stat(CONSCIOUS) GLOB.dead_mob_list -= R //please never forget this ever kthx GLOB.alive_mob_list += R - R.notify_ai(1) + R.notify_ai(NEW_BORG) return TRUE @@ -552,3 +552,17 @@ /obj/item/reagent_containers/spray/cleaner/safety/abductor, /obj/item/lightreplacer/bluespace/abductor ) + +// MARK: B.O.R.I.S. +/obj/item/borg/upgrade/ai + name = "B.O.R.I.S. module" + desc = "Bluespace Optimized Remote Intelligence Synchronization. An uplink device which takes the place of an MMI in cyborg endoskeletons, creating a robotic shell controlled by an AI." + icon_state = "bluespacearray" + origin_tech = "engineering=3;magnets=4;programming=5" + var/alien = FALSE // This is an incredibely scuffed way to do this, but it works. + var/syndiemmi = FALSE + var/brainmob = null + +/obj/item/borg/upgrade/ai/action(mob/living/silicon/robot/R) + to_chat(usr, SPAN_NOTICE("The B.O.R.I.S. module can only be installed into an endoskeleton.")) // Since the BORIS module is an upgrade that acts as an MMI. + return diff --git a/code/modules/mob/living/death.dm b/code/modules/mob/living/death.dm index 1a664e0ffe5..96986c2a74e 100644 --- a/code/modules/mob/living/death.dm +++ b/code/modules/mob/living/death.dm @@ -76,6 +76,13 @@ med_hud_set_health() med_hud_set_status() + for(var/s in ownedSoullinks) + var/datum/soullink/S = s + S.ownerDies(gibbed, src) + for(var/s in sharedSoullinks) + var/datum/soullink/S = s + S.sharerDies(gibbed, src) + GLOB.alive_mob_list -= src GLOB.dead_mob_list += src if(mind) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 44e8fa2db46..b94000fedc3 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -59,6 +59,16 @@ if(mind?.current == src) mind.unbind() UnregisterSignal(src, COMSIG_ATOM_PREHIT) + for(var/s in ownedSoullinks) + var/datum/soullink/S = s + S.ownerDies(FALSE) + qdel(s) // If the owner is `destroy()`'d, the soul link is `destroy()`'d. + ownedSoullinks = null + for(var/s in sharedSoullinks) + var/datum/soullink/S = s + S.sharerDies(FALSE) + S.removeSoulsharer(src) // If a sharer is `destroy()`'d, they are simply removed. + sharedSoullinks = null return ..() /mob/living/ghostize(flags = GHOST_FLAGS_DEFAULT, ghost_name, ghost_color) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 9cfeb10fac6..33e0b4d24f8 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -142,3 +142,8 @@ /// How long does it take to harvest a mob? var/butcher_time = 8 SECONDS + + /// Soul links we are the owner of. + var/list/ownedSoullinks + /// Soul links we are the sharer of. + var/list/sharedSoullinks diff --git a/code/modules/mob/living/silicon/ai/ai_examine.dm b/code/modules/mob/living/silicon/ai/ai_examine.dm index 338bf0d7a8a..a925ab3b1a9 100644 --- a/code/modules/mob/living/silicon/ai/ai_examine.dm +++ b/code/modules/mob/living/silicon/ai/ai_examine.dm @@ -17,7 +17,9 @@ msg += "Its casing is melted and heat-warped!\n" if(src.stat == UNCONSCIOUS) msg += "It is non-responsive and displaying the text: \"RUNTIME: Sensory Overload, stack 26/3\".\n" - if(!shunted && !client) + if(deployed_shell) + msg += "The wireless networking light is blinking.\n" + if(!shunted && !client && !deployed_shell) // An AI deployed to a shell will always lack a client, but also have a client just in another body. msg += "[src]Core.exe has stopped responding! NTOS is searching for a solution to the problem...\n" msg += "" msg += "" diff --git a/code/modules/mob/living/silicon/ai/ai_life.dm b/code/modules/mob/living/silicon/ai/ai_life.dm index 5ebe7bd298c..1a9bb452b0d 100644 --- a/code/modules/mob/living/silicon/ai/ai_life.dm +++ b/code/modules/mob/living/silicon/ai/ai_life.dm @@ -62,6 +62,7 @@ else if(lacks_power()) if(!aiRestorePowerRoutine) + disconnect_shell() update_blind_effects() aiRestorePowerRoutine = 1 update_sight() @@ -149,10 +150,13 @@ health = 100 set_stat(CONSCIOUS) else - health = 100 - getOxyLoss() - getToxLoss() - getFireLoss() - getBruteLoss() + var/old_health = health + health = 100 - getOxyLoss() - getToxLoss() - getBruteLoss() - getFireLoss() + var/old_stat = stat update_stat("updatehealth([reason])") diag_hud_set_health() - + if(old_health > health || old_stat != stat) // Only disconnect if we lose health or change stat. + disconnect_shell() /mob/living/silicon/ai/proc/lacks_power() var/turf/T = get_turf(src) diff --git a/code/modules/mob/living/silicon/ai/ai_mob.dm b/code/modules/mob/living/silicon/ai/ai_mob.dm index c4e7edc2531..f8f010db56f 100644 --- a/code/modules/mob/living/silicon/ai/ai_mob.dm +++ b/code/modules/mob/living/silicon/ai/ai_mob.dm @@ -147,8 +147,13 @@ GLOBAL_LIST_INIT(ai_verbs_default, list( var/allow_teleporter = FALSE var/obj/machinery/camera/portable/builtInCamera - - var/obj/structure/ai_core/deactivated/linked_core //For exosuit control + /// For exosuit control. + var/obj/structure/ai_core/deactivated/linked_core + // For shell control. + /// The shell an AI is deployed to. + var/mob/living/silicon/robot/deployed_shell = null + var/datum/action/innate/deploy_shell/deploy_action = new + var/datum/action/innate/deploy_last_shell/redeploy_action = new /// If our AI doesn't want to be the arrivals announcer, this gets set to FALSE. var/announce_arrivals = TRUE @@ -224,6 +229,7 @@ GLOBAL_LIST_INIT(ai_verbs_default, list( additional_law_channels["Holopad"] = ":h" aiCamera = new/obj/item/camera/siliconcam/ai_camera(src) + deploy_action.Grant(src) if(isturf(loc)) add_ai_verbs(src) @@ -297,6 +303,7 @@ GLOBAL_LIST_INIT(ai_verbs_default, list( GLOB.ai_list -= src GLOB.shuttle_caller_list -= src SSshuttle.autoEvac() + disconnect_shell() if(malfhacking) deltimer(malfhacking) malfhacking = null @@ -384,12 +391,87 @@ GLOBAL_LIST_INIT(ai_verbs_default, list( var/dat_text = dat.Join("") src << browse(dat_text, "window=aialerts&can_close=0") +/mob/living/silicon/ai/proc/deploy_to_shell(mob/living/silicon/robot/target) + if(stat || lacks_power() || control_disabled) + to_chat(src, SPAN_DANGER("Wireless networking module is offline.")) + return + + var/list/possible = list() + + for(var/borgie in GLOB.available_ai_shells) + var/mob/living/silicon/robot/R = borgie + if(R.shell && !R.deployed && (R.stat != DEAD) && (!R.connected_ai ||(R.connected_ai == src))) + possible += R + + if(!LAZYLEN(possible)) + to_chat(src, SPAN_NOTICE("No usable AI shell beacons detected.")) + return + + if(!target || !(target in possible)) // If the AI is looking for a new shell, or its pre-selected shell is no longer valid. + target = tgui_input_list(usr, "Which body to control?", "AI Shell Deployment", possible) + + if(!target || target.stat == DEAD || target.deployed || !(!target.connected_ai ||(target.connected_ai == src))) + return + + else if(mind) + soullink(/datum/soullink/sharedbody, src, target) + deployed_shell = target + target.deploy_init(src) + mind.transfer_to(target) + diag_hud_set_deployed() + +/datum/action/innate/deploy_shell + name = "Deploy to AI Shell" + desc = "Wirelessly control a specialized cyborg shell." + button_icon_state = "ai_shell" + +/datum/action/innate/deploy_shell/Trigger() + var/mob/living/silicon/ai/AI = owner + if(!AI) + return + AI.deploy_to_shell() + +/datum/action/innate/deploy_last_shell + name = "Reconnect to shell" + desc = "Reconnect to the most recently used AI shell." + button_icon_state = "ai_last_shell" + var/mob/living/silicon/robot/last_used_shell + +/datum/action/innate/deploy_last_shell/Trigger() + if(!owner) + return + if(last_used_shell) + var/mob/living/silicon/ai/AI = owner + AI.deploy_to_shell(last_used_shell) + else + Remove(owner) // If the last shell is blown, destroy it. + +/// Disconnect the AI from its shell. +/mob/living/silicon/ai/proc/disconnect_shell() + if(deployed_shell) // Forcibly call back AI in event of things such as damage, EMP or power loss. + to_chat(src, SPAN_DANGER("Your remote connection has been reset!")) + deployed_shell.undeploy() + diag_hud_set_deployed() + +/mob/living/silicon/ai/proc/spawn_shell() + var/obj/shell_landmark + for(var/obj/effect/landmark/shell_loc in GLOB.landmarks_list) + if(shell_loc.name == "AI Shell") + shell_landmark = shell_loc + + var/mob/living/silicon/robot/S = new /mob/living/silicon/robot(shell_landmark.loc) + var/obj/item/borg/upgrade/ai/board = new /obj/item/borg/upgrade/ai + S.make_shell(board) + /mob/living/silicon/ai/proc/show_borg_info(list/status_tab_data) status_tab_data[++status_tab_data.len] = list("Connected cyborg count:", "[length(connected_robots)]") for(var/mob/living/silicon/robot/R in connected_robots) var/robot_status = "Nominal" if(R.stat || !R.client) robot_status = "OFFLINE" + // This needs to be below the OFFLINE check since shells will always lack a client form the AI's view. + else if(R.shell) + robot_status = "AI SHELL" else if(!R.cell || R.cell.charge <= 0) robot_status = "DEPOWERED" // Name, Health, Battery, Module, Area, and Status! Everything an AI wants to know about its borgies! @@ -808,6 +890,7 @@ GLOBAL_LIST_INIT(ai_verbs_default, list( ..() Stun((12 SECONDS) / severity) view_core() + disconnect_shell() /mob/living/silicon/ai/ex_act(severity) ..() @@ -904,7 +987,9 @@ GLOBAL_LIST_INIT(ai_verbs_default, list( to_chat(src, SPAN_WARNING("This exosuit has a pilot and cannot be controlled.")) return if(M) - M.transfer_ai(AI_MECH_HACK, src, usr) //Called om the mech itself. + deploy_action.Remove(src) + redeploy_action.Remove(src) + M.transfer_ai(AI_MECH_HACK, src, usr) // Called on the mech itself. else if(href_list["open"]) var/mob/target = locate(href_list["open"]) in GLOB.mob_list @@ -1459,6 +1544,7 @@ GLOBAL_LIST_INIT(ai_verbs_default, list( if(!..()) return if(interaction == AI_TRANS_TO_CARD)//The only possible interaction. Upload AI mob to a card. + disconnect_shell() // If the AI is controlling a borg, force the player back to core. if(!mind) to_chat(user, SPAN_WARNING("No intelligence patterns detected."))//No more magical carding of empty cores, AI RETURN TO BODY!!!11 return diff --git a/code/modules/mob/living/silicon/robot/robot_damage.dm b/code/modules/mob/living/silicon/robot/robot_damage.dm index 55ccaef9280..ff9584f1db4 100644 --- a/code/modules/mob/living/silicon/robot/robot_damage.dm +++ b/code/modules/mob/living/silicon/robot/robot_damage.dm @@ -144,6 +144,7 @@ if(status_flags & GODMODE) return + var/old_health = health brute = max((brute - damage_protection) * brute_mod, 0) burn = max((burn - damage_protection) * burn_mod, 0) @@ -153,6 +154,8 @@ if(A) A.take_damage(brute, burn, sharp) updatehealth() + if((old_health > health) && shell && deployed && mainframe) // Only disconnect if we lose health. + mainframe.disconnect_shell() return while(LAZYLEN(parts) && (brute > 0 || burn > 0)) @@ -168,6 +171,8 @@ parts -= picked updatehealth() + if((old_health > health) && shell && deployed && mainframe) // Only disconnect if we lose health. + mainframe.disconnect_shell() /* Begins the stamcrit reboot process for borgs. Stuns them, and warns people if the borg has no power source. diff --git a/code/modules/mob/living/silicon/robot/robot_defense.dm b/code/modules/mob/living/silicon/robot/robot_defense.dm index 5f8a811aada..7ad443f3a7b 100644 --- a/code/modules/mob/living/silicon/robot/robot_defense.dm +++ b/code/modules/mob/living/silicon/robot/robot_defense.dm @@ -34,9 +34,15 @@ return /mob/living/silicon/robot/attack_ai(mob/user) - if(user.a_intent == INTENT_HELP && is_ai(user)) + if(user.a_intent == INTENT_HELP && is_ai(user) && !shell) // We check if is not a shell so we don't pet and deploy at the same time. to_chat(src, SPAN_ROBOTEMOTE("[user] gives you a digital headpat.")) to_chat(user, SPAN_ROBOTEMOTE("You give [src] a digital headpat.")) + else if(user.a_intent == INTENT_HELP && is_ai(user) && shell) + var/mob/living/silicon/ai/AI = user + if(AI.controlled_mech) // If the AI is in a mech it can’t control a shell. + to_chat(AI, SPAN_WARNING("Disconnect from your mech before piloting a shell.")) + return + AI.deploy_to_shell(src) /mob/living/silicon/robot/attack_hand(mob/living/carbon/human/user) add_fingerprint(user) diff --git a/code/modules/mob/living/silicon/robot/robot_examine.dm b/code/modules/mob/living/silicon/robot/robot_examine.dm index 30786e3f66e..6f1def50dd0 100644 --- a/code/modules/mob/living/silicon/robot/robot_examine.dm +++ b/code/modules/mob/living/silicon/robot/robot_examine.dm @@ -34,17 +34,20 @@ if(cell && cell.charge <= 0) msg += "[SPAN_WARNING("[p_their(TRUE)] battery indicator is blinking red!")]\n" - switch(stat) - if(CONSCIOUS) - if(!client) - msg += "[p_they(TRUE)] appear[p_s()] to be in stand-by mode.\n" //afk - if(UNCONSCIOUS) - msg += "[SPAN_WARNING("[p_they(TRUE)] [p_do()]n't seem to be responding.")]\n" - if(DEAD) - if(!suiciding) - msg += "[SPAN_DEADSAY("It looks like [p_their()] internal subsystems are beyond repair and require replacing.")]\n" - else - msg += "[SPAN_WARNING("It looks like [p_their()] system is corrupted beyond repair. There is no hope of recovery.")]\n" + if(shell) + msg += "It appears to be an [deployed ? "active" : "empty"] AI shell.\n" + else + switch(stat) + if(CONSCIOUS) + if(!client) + msg += "[p_they(TRUE)] appear[p_s()] to be in stand-by mode.\n" //afk + if(UNCONSCIOUS) + msg += "[SPAN_WARNING("[p_they(TRUE)] [p_do()]n't seem to be responding.")]\n" + if(DEAD) + if(!suiciding) + msg += "[SPAN_DEADSAY("It looks like [p_their()] internal subsystems are beyond repair and require replacing.")]\n" + else + msg += "[SPAN_WARNING("It looks like [p_their()] system is corrupted beyond repair. There is no hope of recovery.")]\n" msg += "" if(print_flavor_text()) diff --git a/code/modules/mob/living/silicon/robot/robot_laws.dm b/code/modules/mob/living/silicon/robot/robot_laws.dm index 06a9f6bd001..14d3b2c798f 100644 --- a/code/modules/mob/living/silicon/robot/robot_laws.dm +++ b/code/modules/mob/living/silicon/robot/robot_laws.dm @@ -30,7 +30,9 @@ to_chat(who, "Obey these laws:") laws.show_laws(who) // TODO: Update to new antagonist system. - if(mind && (mind.special_role == SPECIAL_ROLE_TRAITOR && mind.is_original_mob(src)) && connected_ai) + if(shell) // AI shell. + to_chat(who, SPAN_USERDANGER("Remember, you are an AI remotely controlling your shell, other AIs can be ignored.")) + else if(mind && (mind.special_role == SPECIAL_ROLE_TRAITOR && mind.is_original_mob(src)) && connected_ai) to_chat(who, "Remember, [connected_ai.name] is technically your master, but your objective comes first.") else if(connected_ai) to_chat(who, "Remember, [connected_ai.name] is your master, other AIs can be ignored.") diff --git a/code/modules/mob/living/silicon/robot/robot_login.dm b/code/modules/mob/living/silicon/robot/robot_login.dm index d5cf2575911..057ff0de6ed 100644 --- a/code/modules/mob/living/silicon/robot/robot_login.dm +++ b/code/modules/mob/living/silicon/robot/robot_login.dm @@ -4,7 +4,7 @@ ..() show_laws(0) - if(connected_ai) + if(connected_ai && connected_ai.mind) if(connected_ai.mind.special_role == SPECIAL_ROLE_TRAITOR && connected_ai.malf_picker) make_malf_robot() regenerate_icons() diff --git a/code/modules/mob/living/silicon/robot/robot_mob.dm b/code/modules/mob/living/silicon/robot/robot_mob.dm index 8000834aaaf..fee84578220 100644 --- a/code/modules/mob/living/silicon/robot/robot_mob.dm +++ b/code/modules/mob/living/silicon/robot/robot_mob.dm @@ -1,6 +1,7 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( /mob/living/silicon/robot/proc/sensor_mode, )) +GLOBAL_LIST_EMPTY(available_ai_shells) /mob/living/silicon/robot name = "Cyborg" @@ -19,6 +20,13 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( var/custom_name = "" var/custom_sprite = FALSE // Due to all the sprites involved, a var for our custom borgs may be best. + /// Is this borg a shell? + var/shell = FALSE + /// Is this shell currently deployed? + var/deployed = FALSE + /// The AI deployed to a shell. + var/mob/living/silicon/ai/mainframe = null + var/datum/action/innate/undeployment/undeployment_action = new // HUD stuff. var/atom/movable/screen/hands = null var/list/inventory_screens = list() @@ -142,7 +150,7 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( /// When the camera moved signal was sent last. Avoid overdoing it. var/last_camera_update - hud_possible = list(SPECIALROLE_HUD, DIAG_STAT_HUD, DIAG_HUD, DIAG_BATT_HUD) + hud_possible = list(SPECIALROLE_HUD, DIAG_STAT_HUD, DIAG_HUD, DIAG_BATT_HUD, DIAG_TRACK_HUD) var/default_cell_type = /obj/item/stock_parts/cell/high /// Does the robot have ion thrusters installed? @@ -193,8 +201,12 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( if(wires.is_cut(WIRE_BORG_CAMERA)) // 5 = BORG CAMERA camera.turn_off(src, FALSE) - if(mmi == null) - mmi = new /obj/item/mmi/robotic_brain(src) //Give the borg an MMI if he spawns without for some reason. (probably not the correct way to spawn a robotic brain, but it works) + if(shell) + var/obj/item/borg/upgrade/ai/board = new(src) + make_shell(board) + + else if(mmi == null) + mmi = new /obj/item/mmi/robotic_brain(src) // Give the borg an MMI if they spawn without for some reason (probably not the correct way to spawn a robotic brain, but it works). mmi.icon_state = "boris" initialize_components() @@ -251,11 +263,13 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( playsound(loc, 'sound/voice/liveagain.ogg', 75, 1) /mob/living/silicon/robot/rename_character(oldname, newname) + if(shell) + return if(!..(oldname, newname)) return FALSE if(oldname != real_name) - notify_ai(3, oldname, newname) + notify_ai(RENAME, oldname, newname) custom_name = (newname != get_default_name()) ? newname : null setup_PDA() @@ -347,6 +361,9 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( /mob/living/silicon/robot/Destroy() remove_robot_mindslave() // You cannot be connected to the malf AI if you are a pile of debris. SStgui.close_uis(wires) + if(shell) + undeploy() + revert_shell() if(mmi && mind)//Safety for when a cyborg gets dust()ed. Or there is no MMI inside. var/turf/T = get_turf(loc)//To hopefully prevent run time errors. if(T) @@ -687,27 +704,33 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( switch(selected_module) if("Engineering") module = new /obj/item/robot_module/engineering(src) - module.channels = list("Engineering" = 1) + // AI shells have the same channels as the AI itself so we skip it. + if(!shell) + module.channels = list("Engineering" = 1) if(camera && ("Robots" in camera.network)) camera.network += "Engineering" if("Janitor") module = new /obj/item/robot_module/janitor(src) - module.channels = list("Service" = 1) + if(!shell) + module.channels = list("Service" = 1) if("Medical") module = new /obj/item/robot_module/medical(src) - module.channels = list("Medical" = 1) + if(!shell) + module.channels = list("Medical" = 1) if(camera && ("Robots" in camera.network)) camera.network += "Medical" status_flags &= ~CANPUSH has_advanced_reagent_vision = TRUE if("Mining") module = new /obj/item/robot_module/miner(src) - module.channels = list("Supply" = 1) + if(!shell) + module.channels = list("Supply" = 1) if(camera && ("Robots" in camera.network)) camera.network += "Mining Outpost" if("Service") module = new /obj/item/robot_module/butler(src) - module.channels = list("Service" = 1) + if(!shell) + module.channels = list("Service" = 1) has_advanced_reagent_vision = TRUE if(selected_sprite == "Bro") module.module_type = "Brobot" @@ -724,19 +747,22 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( return FALSE modtype = selected_module designation = selected_module + if(shell) // Update the shell name right after choosing a module. + real_name = "[real_name] -[designation]" + name = real_name module.add_languages(src) module.add_armor(src) module.add_subsystems_and_actions(src) if(emagged) module.emag_act(src) - if(!static_radio_channels) + if(!static_radio_channels && !shell) // Shells have the same channels as the AI and we dont want to reset them. radio.config(module.channels) rename_character(real_name, get_default_name()) initialize_sprites(selected_sprite, module_sprites) if(client.stat_tab == "Status") SSstatpanels.set_status_tab(client) SSblackbox.record_feedback("tally", "cyborg_modtype", 1, "[lowertext(selected_module)]") - notify_ai(2) + notify_ai(NEW_MODULE) /mob/living/silicon/robot/proc/initialize_sprites(selected_sprite, list/module_sprites) var/image/sprite_image = module_sprites[selected_sprite] @@ -750,10 +776,12 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( for(var/obj/item/borg/upgrade/U in contents) if(istype(U, /obj/item/borg/upgrade/reset)) // The reset module is supposed to be consumed on use, this stops it from dropping on the floor if used QDEL_NULL(U) + if(istype(U, /obj/item/borg/upgrade/ai)) // So you can change the shell module but not drop the BORIS module. + continue U.forceMove(get_turf(src)) /mob/living/silicon/robot/proc/reset_module() - notify_ai(2) + notify_ai(NEW_MODULE) client?.screen -= hud_used.module_store_icon uneq_all() SStgui.close_user_uis(src) @@ -1072,8 +1100,7 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( module?.update_cells() diag_hud_set_borgcell() return ITEM_INTERACT_COMPLETE - - if(istype(used, /obj/item/encryptionkey) && opened) + if(istype(used, /obj/item/encryptionkey/) && opened) if(radio) to_chat(user, SPAN_NOTICE("You install [used] into [src]'s radio.")) radio.item_interaction(user, used) @@ -1167,8 +1194,11 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( update_icons() I.play_tool_sound(user, I.tool_volume) else //radio check + if(shell) // Prevents AI shell key theft. + to_chat(user, SPAN_NOTICE("The shell appears to not have an encryption key.")) + return if(radio) - radio.screwdriver_act(user, I)//Push it to the radio to let it handle everything + radio.screwdriver_act(user, I) // Push it to the radio to let it handle everything. else to_chat(user, "Unable to locate a radio.") update_icons() @@ -1275,10 +1305,20 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( to_chat(user, SPAN_NOTICE("You emag the cover lock.")) locked = FALSE + // A warning to Traitors who may not know that emagging AI shells does not slave them. + if(shell) + to_chat(user, SPAN_BOLDWARNING("[src] seems to be controlled remotely! Emagging the interface may not work as expected.")) log_game("[user]([user.key]) emagged [src]'s cover.") return TRUE if(opened) + if(shell) // AI shells cannot be emagged, so we try to make it look like a standard reset. Smart players may see through this, however. + to_chat(user, SPAN_BOLDWARNING("[src] is remotely controlled! Your emag attempts to disable AI control!")) + log_game("[key_name(user)] attempted to emag an AI shell belonging to [key_name(src) ? key_name(src) : connected_ai]. The shell has been reset as a result.") + undeploy() + reset_module() + revert_shell() + return if(emagged) to_chat(user, SPAN_WARNING("The emag sparks, and flashes red. [src] has already been emagged!")) return @@ -1444,6 +1484,8 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( /mob/living/silicon/robot/proc/deconstruct() var/turf/T = get_turf(src) + if(shell) + undeploy() if(robot_suit) robot_suit.forceMove(T) robot_suit.l_leg.forceMove(T) @@ -1555,16 +1597,35 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( for(var/datum/action/innate/robot_override_lock/override in actions) override.Remove(src) +/** + * Notifies the AI of a certain event related to borgs and shells. + * + * Arguments: + * * notifytype - The type of notification to send. + * * oldname - The old name of the cyborg. + * * newname - The new name of the cyborg. + * + * Notify Types: + * - NEW_BORG: A new cyborg has connected. + * - NEW_MODULE: A cyborg has loaded a new module. + * - RENAME: A cyborg has been renamed.(This one needs the old and new name arguments.) + * - AI_SHELL: A new AI shell has been detected. + * - DISCONNECT: A cyborg has disconnected. + */ /mob/living/silicon/robot/proc/notify_ai(notifytype, oldname, newname) if(!connected_ai) return switch(notifytype) - if(1) //New Cyborg + if(NEW_BORG) // New Cyborg. to_chat(connected_ai, "

[SPAN_NOTICE("NOTICE - New cyborg connection detected: [name]")]
") - if(2) //New Module + if(NEW_MODULE) // New Module. to_chat(connected_ai, "

[SPAN_NOTICE("NOTICE - Cyborg module change detected: [name] has loaded the [designation] module.")]
") - if(3) //New Name + if(RENAME) // New Name. to_chat(connected_ai, "

[SPAN_NOTICE("NOTICE - Cyborg reclassification detected: [oldname] is now designated as [newname].")]
") + if(AI_SHELL) // New Shell. + to_chat(connected_ai, "

[SPAN_NOTICE("NOTICE - New cyborg shell detected: [name]")]
") + if(DISCONNECT) // Disconnect. + to_chat(connected_ai, "

[SPAN_NOTICE("NOTICE - Remote telemetry lost with [name].")]
") /mob/living/silicon/robot/proc/disconnect_from_ai() if(connected_ai) @@ -1578,7 +1639,9 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( if(AI && AI != connected_ai) disconnect_from_ai() set_connected_ai(AI) - notify_ai(1) + // Shells get notifications already, so we don't want to duplicate the message for them. + if(!shell) + notify_ai(NEW_BORG) if(AI.mind.special_role == ROLE_TRAITOR && AI.malf_picker) make_malf_robot(AI) if(module) @@ -1721,7 +1784,9 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( if(emp_protection) return ..() - adjustStaminaLoss((30 / severity)) //They also get flashed for an additional 30 + if(shell) + undeploy() + adjustStaminaLoss((30 / severity)) // They also get flashed for an additional 30. switch(severity) if(EMP_HEAVY) disable_random_component(2, 20 SECONDS) @@ -2025,3 +2090,93 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( if(curse_time == -1) QDEL_NULL(mmi) return ..() + +/mob/living/silicon/robot/proc/make_shell(obj/item/borg/upgrade/ai/board) + if(isnull(board)) + stack_trace("make_shell was called without a board argument! This is never supposed to happen!") + return FALSE + shell = TRUE + mmi = board // This is to drop the BORIS module when we deconstruct a shell. + braintype = "AI Shell" + name = "AI Shell [rand(100,999)]" + real_name = name + GLOB.available_ai_shells |= src + if(camera) + camera.c_tag = real_name // Update the camera name too. + +/// Reverts a shell back to a unformatted cyborg also drops the BORIS module. +/mob/living/silicon/robot/proc/revert_shell() + if(!shell) + return + notify_ai(DISCONNECT) + shell = FALSE + GLOB.available_ai_shells -= src + name = "Unformatted Cyborg [rand(100,999)]" + real_name = name + for(var/obj/item/borg/upgrade/ai/U in src.contents) + if(U) + U.forceMove(src.loc) + if(camera) + camera.c_tag = real_name + diag_hud_set_aishell() + +/mob/living/silicon/robot/proc/deploy_init(mob/living/silicon/ai/AI) + real_name = "[AI.real_name] shell [rand(100, 999)] [designation ? "-[designation]" : ""]" // Randomizing the name so it shows up seperately in the shells list. + name = real_name + setup_PDA() + if(camera) + camera.c_tag = real_name // Update the camera name too. + mainframe = AI + deployed = TRUE + connected_ai = mainframe + mainframe.connected_robots |= src + lawupdate = TRUE + lawsync() + if(radio && AI.aiRadio) // AI keeps all channels, including Syndie if it is a Traitor. + if(AI.aiRadio.syndie) + radio.make_syndie() + radio.channels = AI.aiRadio.channels + for(var/chan in radio.channels) + radio.secure_radio_connections[chan] = SSradio.add_object(radio, SSradio.radiochannels[chan], RADIO_CHAT) + + diag_hud_set_aishell() + undeployment_action.Grant(src) + +/datum/action/innate/undeployment + name = "Disconnect from shell" + desc = "Stop controlling your shell and resume normal core operations." + button_icon_state = "ai_core" + +/datum/action/innate/undeployment/Trigger() + if(!..()) + return FALSE + var/mob/living/silicon/robot/R = owner + + R.undeploy() + return TRUE + +/// Undeploys the AI from its shell. +/mob/living/silicon/robot/proc/undeploy() + if(!deployed || !mind || !mainframe) + return + mainframe.redeploy_action.Grant(mainframe) + mainframe.redeploy_action.last_used_shell = src + mind.transfer_to(mainframe) + deployed = FALSE + mainframe.deployed_shell = null + undeployment_action.Remove(src) + if(radio) // Return radio to normal. + radio.recalculateChannels() + if(camera) + camera.c_tag = real_name // Update the camera name too. + diag_hud_set_aishell() + mainframe.diag_hud_set_deployed() + if(mainframe.laws) + mainframe.laws.show_laws(mainframe) // Always remind the AI when switching. + if(mainframe.eyeobj)// Makes it so that when an AI undeploys its view isn’t moved to its core. + mainframe.eyeobj.set_loc(loc) + mainframe = null + +/mob/living/silicon/robot/shell + shell = TRUE + allow_rename = FALSE // This is to prevent someone renaming the shell and causing confusion with it. diff --git a/code/modules/mob/living/silicon/robot/robot_module_actions.dm b/code/modules/mob/living/silicon/robot/robot_module_actions.dm index e3c61d3f87c..229a7075d4d 100644 --- a/code/modules/mob/living/silicon/robot/robot_module_actions.dm +++ b/code/modules/mob/living/silicon/robot/robot_module_actions.dm @@ -78,7 +78,7 @@ /datum/action/innate/robot_sight/engineering_scanner/process() var/mob/living/silicon/robot/user = owner - if(!user.client) + if(!user || !user.client) return switch(mode) if(MODE_TRAY) diff --git a/code/modules/mob/living/silicon/robot/robot_update_status.dm b/code/modules/mob/living/silicon/robot/robot_update_status.dm index ee73611f6cc..0c189bc78e6 100644 --- a/code/modules/mob/living/silicon/robot/robot_update_status.dm +++ b/code/modules/mob/living/silicon/robot/robot_update_status.dm @@ -38,6 +38,7 @@ diag_hud_set_status() diag_hud_set_health() update_health_hud() + diag_hud_set_aishell() /mob/living/silicon/robot/KnockOut(updating = TRUE) . = ..() diff --git a/code/modules/mob/living/silicon/silicon_mob.dm b/code/modules/mob/living/silicon/silicon_mob.dm index 0b4c915014b..1d6dc82644b 100644 --- a/code/modules/mob/living/silicon/silicon_mob.dm +++ b/code/modules/mob/living/silicon/silicon_mob.dm @@ -65,7 +65,7 @@ //var/sensor_mode = 0 //Determines the current HUD. - hud_possible = list(SPECIALROLE_HUD, DIAG_STAT_HUD, DIAG_HUD) + hud_possible = list(SPECIALROLE_HUD, DIAG_STAT_HUD, DIAG_HUD, DIAG_TRACK_HUD) var/med_hud = DATA_HUD_MEDICAL_ADVANCED //Determines the med hud to use diff --git a/code/modules/mob/living/stat_states.dm b/code/modules/mob/living/stat_states.dm index 1f71e4a9d39..f0e685bf9ba 100644 --- a/code/modules/mob/living/stat_states.dm +++ b/code/modules/mob/living/stat_states.dm @@ -84,6 +84,13 @@ var/datum/spell/spell = S spell.build_all_button_icons() + for(var/s in ownedSoullinks) + var/datum/soullink/S = s + S.ownerRevives(src) + for(var/s in sharedSoullinks) + var/datum/soullink/S = s + S.sharerRevives(src) + return TRUE /mob/living/proc/check_death_method() diff --git a/code/modules/projectiles/projectile/magic_projectiles.dm b/code/modules/projectiles/projectile/magic_projectiles.dm index 2037e1c1f7d..9d323433970 100644 --- a/code/modules/projectiles/projectile/magic_projectiles.dm +++ b/code/modules/projectiles/projectile/magic_projectiles.dm @@ -239,7 +239,7 @@ GLOBAL_LIST_INIT(wabbajack_docile_animals, list( if(isrobot(M)) var/mob/living/silicon/robot/Robot = M QDEL_NULL(Robot.mmi) - Robot.notify_ai(1) + Robot.notify_ai(NEW_BORG) else if(ishuman(M)) var/mob/living/carbon/human/H = M diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm index df741781b63..a08083a7148 100644 --- a/code/modules/research/designs/misc_designs.dm +++ b/code/modules/research/designs/misc_designs.dm @@ -31,6 +31,17 @@ build_path = /obj/item/aicard category = list("Miscellaneous") +/datum/design/boris_ai_controller + name = "B.O.R.I.S." + desc = "Bluespace Optimized Remote Intelligence Synchronization. An uplink device which takes the place of an MMI in cyborg endoskeletons, creating a robotic shell controlled by an AI." + id = "borg_ai_control" + req_tech = list("programming" = 5, "magnets" = 4, "engineering" = 3) + build_type = MECHFAB | PROTOLATHE + materials = list(MAT_METAL = 1700, MAT_GLASS = 1350, MAT_GOLD = 500) // Same as robobrain. + construction_time = 75 + build_path = /obj/item/borg/upgrade/ai + category = list("Miscellaneous", "Misc") + /datum/design/paicard name = "Personal Artificial Intelligence Card" desc = "Allows for the construction of a pAI Card." diff --git a/icons/mob/actions/actions.dmi b/icons/mob/actions/actions.dmi index 9fd56c2c428..86b87f3a286 100644 Binary files a/icons/mob/actions/actions.dmi and b/icons/mob/actions/actions.dmi differ diff --git a/paradise.dme b/paradise.dme index 656b6ea2ca8..78c18b9e11c 100644 --- a/paradise.dme +++ b/paradise.dme @@ -478,6 +478,7 @@ #include "code\datums\revision.dm" #include "code\datums\ruins.dm" #include "code\datums\shuttles.dm" +#include "code\datums\soullink.dm" #include "code\datums\spawners_menu.dm" #include "code\datums\station_state.dm" #include "code\datums\tgs_event_handler.dm"