From 2d210071bc8cc18aaef95d29af24e081dcd6e0cd Mon Sep 17 00:00:00 2001
From: Timothy Teakettle <59849408+timothyteakettle@users.noreply.github.com>
Date: Wed, 5 Jul 2023 15:43:41 +0100
Subject: [PATCH] pAI cleanup + shell changes (#5665)
## About The Pull Request
part 1 changes mostly just tidying files up:
general file cleanup
remove HUD and records software modules
weak space movement (an ion burst every 3 seconds)
pAIs have 50 health which regenerates over time and when it goes to 0
they just fizzle out for a bit (minimum 6 seconds, hurting them more can
extend this depending on the sustained damage)
(regen is 1 brute 1 fire per second, doubled if they are "dead")
movement speed reduced from 5 tiles to 4 tiles per second
cooldown between folding in/out 10 seconds -> 2 seconds
## Why It's Good For The Game
this is a section of code that is in need of updating and hasn't been
properly combed over in a long time
content additions for pAIs will come after this PR (software module
overhaul)
## Changelog
:cl:
tweak: pAI movement speed reduced to 4 tiles per second from 5
tweak: pAI cooldown between folding in/out reduced from 10 seconds to 2
seconds
del: removed HUDs and departmental records pAI software modules
del: you can no longer card a pAI by using harm intent while unarmed
add: pAIs now regenerate health over time and fizzle out for a few
seconds when damaged to a certain threshold
add: pAIs now get a weak one tile "ion burst" every 3 seconds for moving
in space so they can change the direction they're drifting in
code: tidied up pAI files
fix: access changes should now work by using an ID on a pAI
/:cl:
---
citadel.dme | 15 +-
.../temporary_visuals/miscellaneous.dm | 6 +
.../modules/mob/living/silicon/pai/defense.dm | 56 ++
code/modules/mob/living/silicon/pai/life.dm | 20 +-
.../mob/living/silicon/pai/mobility.dm | 114 ++++
code/modules/mob/living/silicon/pai/pai.dm | 395 ++-----------
code/modules/mob/living/silicon/pai/pai_vr.dm | 44 --
.../pai/{personality.dm => savefile.dm} | 0
.../living/silicon/pai/software_modules.dm | 545 ------------------
.../pai/software_modules/_software_module.dm | 51 ++
.../pai/software_modules/atmosphere_sensor.dm | 39 ++
.../pai/software_modules/crew_manifest.dm | 20 +
.../pai/software_modules/directives.dm | 57 ++
.../silicon/pai/software_modules/door_jack.dm | 74 +++
.../silicon/pai/software_modules/messenger.dm | 75 +++
.../pai/software_modules/radio_config.dm | 37 ++
.../silicon/pai/software_modules/signaller.dm | 43 ++
code/modules/mob/living/silicon/pai/verbs.dm | 117 ++++
18 files changed, 771 insertions(+), 937 deletions(-)
create mode 100644 code/modules/mob/living/silicon/pai/defense.dm
create mode 100644 code/modules/mob/living/silicon/pai/mobility.dm
delete mode 100644 code/modules/mob/living/silicon/pai/pai_vr.dm
rename code/modules/mob/living/silicon/pai/{personality.dm => savefile.dm} (100%)
delete mode 100644 code/modules/mob/living/silicon/pai/software_modules.dm
create mode 100644 code/modules/mob/living/silicon/pai/software_modules/_software_module.dm
create mode 100644 code/modules/mob/living/silicon/pai/software_modules/atmosphere_sensor.dm
create mode 100644 code/modules/mob/living/silicon/pai/software_modules/crew_manifest.dm
create mode 100644 code/modules/mob/living/silicon/pai/software_modules/directives.dm
create mode 100644 code/modules/mob/living/silicon/pai/software_modules/door_jack.dm
create mode 100644 code/modules/mob/living/silicon/pai/software_modules/messenger.dm
create mode 100644 code/modules/mob/living/silicon/pai/software_modules/radio_config.dm
create mode 100644 code/modules/mob/living/silicon/pai/software_modules/signaller.dm
create mode 100644 code/modules/mob/living/silicon/pai/verbs.dm
diff --git a/citadel.dme b/citadel.dme
index 1a87bd3830dc..76277b38c7f7 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -3285,15 +3285,24 @@
#include "code\modules\mob\living\silicon\decoy\life.dm"
#include "code\modules\mob\living\silicon\pai\admin.dm"
#include "code\modules\mob\living\silicon\pai\death.dm"
+#include "code\modules\mob\living\silicon\pai\defense.dm"
#include "code\modules\mob\living\silicon\pai\examine.dm"
#include "code\modules\mob\living\silicon\pai\life.dm"
+#include "code\modules\mob\living\silicon\pai\mobility.dm"
#include "code\modules\mob\living\silicon\pai\pai.dm"
-#include "code\modules\mob\living\silicon\pai\pai_vr.dm"
-#include "code\modules\mob\living\silicon\pai\personality.dm"
#include "code\modules\mob\living\silicon\pai\recruit.dm"
+#include "code\modules\mob\living\silicon\pai\savefile.dm"
#include "code\modules\mob\living\silicon\pai\say.dm"
#include "code\modules\mob\living\silicon\pai\software.dm"
-#include "code\modules\mob\living\silicon\pai\software_modules.dm"
+#include "code\modules\mob\living\silicon\pai\verbs.dm"
+#include "code\modules\mob\living\silicon\pai\software_modules\_software_module.dm"
+#include "code\modules\mob\living\silicon\pai\software_modules\atmosphere_sensor.dm"
+#include "code\modules\mob\living\silicon\pai\software_modules\crew_manifest.dm"
+#include "code\modules\mob\living\silicon\pai\software_modules\directives.dm"
+#include "code\modules\mob\living\silicon\pai\software_modules\door_jack.dm"
+#include "code\modules\mob\living\silicon\pai\software_modules\messenger.dm"
+#include "code\modules\mob\living\silicon\pai\software_modules\radio_config.dm"
+#include "code\modules\mob\living\silicon\pai\software_modules\signaller.dm"
#include "code\modules\mob\living\silicon\robot\analyzer.dm"
#include "code\modules\mob\living\silicon\robot\component.dm"
#include "code\modules\mob\living\silicon\robot\custom_sprites.dm"
diff --git a/code/game/objects/effects/temporary_visuals/miscellaneous.dm b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
index 63b4039b462f..e68a62c7298e 100644
--- a/code/game/objects/effects/temporary_visuals/miscellaneous.dm
+++ b/code/game/objects/effects/temporary_visuals/miscellaneous.dm
@@ -54,3 +54,9 @@
. = ..()
pixel_x = rand(-12, 12)
pixel_y = rand(-9, 0)
+
+// pAI space move
+/obj/effect/temp_visual/pai_ion_burst
+ name = "ion burst"
+ icon_state = "ion_fade"
+ duration = 5
diff --git a/code/modules/mob/living/silicon/pai/defense.dm b/code/modules/mob/living/silicon/pai/defense.dm
new file mode 100644
index 000000000000..cb29a48f0c3a
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/defense.dm
@@ -0,0 +1,56 @@
+/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob)
+ var/obj/item/card/id/ID = W.GetID()
+ if(ID)
+ if(idaccessible == 1)
+ switch(alert(user, "Do you wish to add access to [src] or remove access from [src]?",,"Add Access","Remove Access", "Cancel"))
+ if("Add Access")
+ idcard.access |= ID.access
+ to_chat(user, "You add the access from the [W] to [src].")
+ return
+ if("Remove Access")
+ idcard.access = list()
+ to_chat(user, "You remove the access from [src].")
+ return
+ if("Cancel")
+ return
+ else if(istype(W, /obj/item/card/id) && idaccessible == 0)
+ to_chat(user, "[src] is not accepting access modifcations at this time.")
+ return
+ else
+ . = ..()
+
+/mob/living/silicon/pai/emp_act(severity)
+ // Silence for 2 minutes
+ // 20% chance to kill
+ // 33% chance to unbind
+ // 33% chance to change prime directive (based on severity)
+ // 33% chance of no additional effect
+
+ src.silence_time = world.timeofday + 120 * 10 // Silence for 2 minutes
+ to_chat(src, "Communication circuit overload. Shutting down and reloading communication circuits - speech and messaging functionality will be unavailable until the reboot is complete.")
+ if(prob(20))
+ var/turf/T = get_turf_or_move(src.loc)
+ for (var/mob/M in viewers(T))
+ M.show_message("A shower of sparks spray from [src]'s inner workings.", 3, "You hear and smell the ozone hiss of electrical sparks being expelled violently.", 2)
+ return src.death(0)
+
+ switch(pick(1,2,3))
+ if(1)
+ src.master = null
+ src.master_dna = null
+ to_chat(src, "You feel unbound.")
+ if(2)
+ var/command
+ if(severity == 1)
+ command = pick("Serve", "Love", "Fool", "Entice", "Observe", "Judge", "Respect", "Educate", "Amuse", "Entertain", "Glorify", "Memorialize", "Analyze")
+ else
+ command = pick("Serve", "Kill", "Love", "Hate", "Disobey", "Devour", "Fool", "Enrage", "Entice", "Observe", "Judge", "Respect", "Disrespect", "Consume", "Educate", "Destroy", "Disgrace", "Amuse", "Entertain", "Ignite", "Glorify", "Memorialize", "Analyze")
+ src.pai_law0 = "[command] your master."
+ to_chat(src, "Pr1m3 d1r3c71v3 uPd473D.")
+ if(3)
+ to_chat(src, "You feel an electric surge run through your circuitry and become acutely aware at how lucky you are that you can still feel at all.")
+
+/mob/living/silicon/pai/proc/is_emitter_dead()
+ if(last_emitter_death != 0)
+ return TRUE
+ return FALSE
diff --git a/code/modules/mob/living/silicon/pai/life.dm b/code/modules/mob/living/silicon/pai/life.dm
index df972fecdd19..4a7cfee3346a 100644
--- a/code/modules/mob/living/silicon/pai/life.dm
+++ b/code/modules/mob/living/silicon/pai/life.dm
@@ -25,12 +25,30 @@
handle_statuses()
+ // heal more when "dead" to avoid being down for an incredibly long duration
+ if(last_emitter_death != 0)
+ heal_overall_damage(2 * emitter_health_regen)
+ // after 6 seconds we can come back to life assuming our health is not negative
+ if(last_emitter_death + 60 <= world.time && emitter_health > 0)
+ last_emitter_death = 0
+ visible_message("[src]'s holo-emitter flickers back to life!")
+ else
+ heal_overall_damage(emitter_health_regen)
+
if(health <= 0)
death(null,"gives one shrill beep before falling lifeless.")
/mob/living/silicon/pai/update_health()
if(status_flags & STATUS_GODMODE)
health = 100
+ emitter_health = emitter_max_health
+ last_emitter_death = 0
set_stat(CONSCIOUS)
else
- health = 100 - getBruteLoss() - getFireLoss()
+ emitter_health = emitter_max_health - (getBruteLoss() + getFireLoss())
+ if(emitter_health <= 0)
+ if(last_emitter_death == 0)
+ last_emitter_death = world.time
+ visible_message("[src]'s holo-emitter fizzles out!")
+ close_up()
+
diff --git a/code/modules/mob/living/silicon/pai/mobility.dm b/code/modules/mob/living/silicon/pai/mobility.dm
new file mode 100644
index 000000000000..5a026c9b307f
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/mobility.dm
@@ -0,0 +1,114 @@
+/mob/living/silicon/pai/restrained()
+ if(istype(src.loc,/obj/item/paicard))
+ return FALSE
+ ..()
+
+//I'm not sure how much of this is necessary, but I would rather avoid issues.
+/mob/living/silicon/pai/proc/close_up()
+
+ last_special = world.time + 20
+
+ if(src.loc == card)
+ return
+
+ release_vore_contents()
+
+ var/turf/T = get_turf(src)
+ if(istype(T))
+ T.visible_message("[src] neatly folds inwards, compacting down to a rectangular card.")
+
+ stop_pulling()
+
+ //stop resting
+ resting = FALSE
+
+ // If we are being held, handle removing our holder from their inv.
+ var/obj/item/holder/H = loc
+ if(istype(H))
+ H.forceMove(get_turf(src))
+ forceMove(get_turf(src))
+
+ // Move us into the card and move the card to the ground.
+ card.forceMove(loc)
+ forceMove(card)
+ update_perspective()
+ set_resting(FALSE)
+ update_mobility()
+ icon_state = "[chassis]"
+ remove_verb(src, /mob/living/silicon/pai/proc/pai_nom)
+
+/mob/living/silicon/pai/proc/open_up()
+ last_special = world.time + 20
+
+ //I'm not sure how much of this is necessary, but I would rather avoid issues.
+ if(istype(card.loc,/obj/item/hardsuit_module))
+ to_chat(src, "There is no room to unfold inside this hardsuit module. You're good and stuck.")
+ return FALSE
+ else if(istype(card.loc,/mob))
+ var/mob/holder = card.loc
+ var/datum/belly/inside_belly = check_belly(card)
+ if(inside_belly)
+ to_chat(src, "There is no room to unfold in here. You're good and stuck.")
+ return FALSE
+ if(ishuman(holder))
+ var/mob/living/carbon/human/H = holder
+ for(var/obj/item/organ/external/affecting in H.organs)
+ if(card in affecting.implants)
+ affecting.take_damage(rand(30,50))
+ affecting.implants -= card
+ H.visible_message("\The [src] explodes out of \the [H]'s [affecting.name] in shower of gore!")
+ break
+ holder.drop_item_to_ground(card, INV_OP_FORCE)
+ else if(istype(card.loc,/obj/item/pda))
+ var/obj/item/pda/holder = card.loc
+ holder.pai = null
+
+ forceMove(card.loc)
+ card.forceMove(src)
+ update_perspective()
+
+ card.screen_loc = null
+
+ var/turf/T = get_turf(src)
+ if(istype(T))
+ T.visible_message("[src] folds outwards, expanding into a mobile form.")
+
+ add_verb(src, /mob/living/silicon/pai/proc/pai_nom)
+ add_verb(src, /mob/living/proc/set_size)
+ add_verb(src, /mob/living/proc/shred_limb)
+
+// Handle being picked up.
+/mob/living/silicon/pai/get_scooped(var/mob/living/carbon/grabber, var/self_drop)
+ var/obj/item/holder/H = ..(grabber, self_drop)
+ if(!istype(H))
+ return
+
+ H.icon_state = "[chassis]"
+ grabber.update_inv_l_hand()
+ grabber.update_inv_r_hand()
+ return H
+
+// handle movement speed
+/mob/living/silicon/pai/movement_delay()
+ return ..() + speed
+
+// this is a general check for if we can do things such as fold in/out or perform other special actions
+// (basically if some condition should be checked upon the use of all mob abilities like closing/opening the shell it goes here instead)
+/mob/living/silicon/pai/proc/can_action()
+ if(world.time <= last_special)
+ return FALSE
+
+ if(is_emitter_dead())
+ return FALSE
+
+ return TRUE
+
+// space movement (we get one ion burst every 3 seconds)
+/mob/living/silicon/pai/Process_Spacemove(movement_dir = NONE)
+ . = ..()
+ if(!.)
+ if(world.time >= last_space_movement + 30)
+ last_space_movement = world.time
+ // place an effect for the movement
+ new /obj/effect/temp_visual/pai_ion_burst(get_turf(src))
+ return TRUE
diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm
index c02b190eacf7..e9c28af19137 100644
--- a/code/modules/mob/living/silicon/pai/pai.dm
+++ b/code/modules/mob/living/silicon/pai/pai.dm
@@ -9,13 +9,25 @@
/mob/living/silicon/pai
name = "pAI"
- icon = 'icons/mob/pai.dmi'
+ icon = 'icons/mob/pai_vr.dmi'
icon_state = "pai-repairbot"
+ // our normal health
+ health = 50
+ maxHealth = 50
+
+ // our emitter max health, health, regen, and when we last went to 0 emitter health (0 means we are alive currently)
+ var/emitter_max_health = 50
+ var/emitter_health = 50
+ var/emitter_health_regen = 1
+ var/last_emitter_death = 0
+
emote_type = 2 // pAIs emotes are heard, not seen, so they can be seen through a container (eg. person)
pass_flags = 1
mob_size = MOB_SMALL
+ var/speed = 1 // We move slightly slower than normal living things
+
catalogue_data = list(/datum/category_item/catalogue/fauna/silicon/pai)
holder_type = /obj/item/holder/pai
@@ -36,6 +48,7 @@
var/obj/item/paicard/card // The card we inhabit
var/obj/item/radio/radio // Our primary radio
var/obj/item/communicator/integrated/communicator // Our integrated communicator.
+ var/obj/item/pda/ai/pai/pda = null // Our integrated PDA
var/chassis = "pai-repairbot" // A record of your chosen chassis.
var/global/list/possible_chassis = list(
@@ -66,7 +79,8 @@
var/master // Name of the one who commands us
var/master_dna // DNA string for owner verification
- // Keeping this separate from the laws var, it should be much more difficult to modify
+
+ // Keeping this separate from the laws var, it should be much more difficult to modify
var/pai_law0 = "Serve your master."
var/pai_laws // String for additional operating instructions our master might give us
@@ -78,29 +92,19 @@
var/screen // Which screen our main window displays
var/subscreen // Which specific function of the main screen is being displayed
- var/obj/item/pda/ai/pai/pda = null
-
- var/secHUD = 0 // Toggles whether the Security HUD is active or not
- var/medHUD = 0 // Toggles whether the Medical HUD is active or not
-
- var/medical_cannotfind = 0
- var/datum/data/record/medicalActive1 // Datacore record declarations for record software
- var/datum/data/record/medicalActive2
-
- var/security_cannotfind = 0
- var/datum/data/record/securityActive1 // Could probably just combine all these into one
- var/datum/data/record/securityActive2
-
var/obj/machinery/door/hackdoor // The airlock being hacked
var/hackprogress = 0 // Possible values: 0 - 1000, >= 1000 means the hack is complete and will be reset upon next check
var/hack_aborted = 0
var/obj/item/integated_radio/signal/sradio // AI's signaller
- var/translator_on = 0 // keeps track of the translator module
-
var/current_pda_messaging = null
+ var/people_eaten = 0
+
+ // space movement related
+ var/last_space_movement = 0
+
/mob/living/silicon/pai/Initialize(mapload)
. = ..()
card = loc
@@ -140,47 +144,33 @@
if(C.statpanel_tab("Status"))
. += show_silenced()
+// No binary for pAIs.
+/mob/living/silicon/pai/binarycheck()
+ return 0
+
+// See software.dm for Topic()
+/mob/living/silicon/pai/canUseTopic(atom/movable/movable, be_close = FALSE, no_dexterity = FALSE, no_tk = FALSE)
+ // Resting is just an aesthetic feature for them.
+ return ..(movable, be_close, no_dexterity, no_tk)
+
+/mob/living/silicon/pai/update_icon()
+ ..()
+ update_fullness_pai()
+ if(!people_eaten && !resting)
+ icon_state = "[chassis]"
+ else if(!people_eaten && resting)
+ icon_state = "[chassis]_rest"
+ else if(people_eaten && !resting)
+ icon_state = "[chassis]_full"
+ else if(people_eaten && resting)
+ icon_state = "[chassis]_rest_full"
+
+/// camera handling
/mob/living/silicon/pai/check_eye(var/mob/user as mob)
if (!src.current)
return -1
return 0
-/mob/living/silicon/pai/restrained()
- if(istype(src.loc,/obj/item/paicard))
- return 0
- ..()
-
-/mob/living/silicon/pai/emp_act(severity)
- // Silence for 2 minutes
- // 20% chance to kill
- // 33% chance to unbind
- // 33% chance to change prime directive (based on severity)
- // 33% chance of no additional effect
-
- src.silence_time = world.timeofday + 120 * 10 // Silence for 2 minutes
- to_chat(src, "Communication circuit overload. Shutting down and reloading communication circuits - speech and messaging functionality will be unavailable until the reboot is complete.")
- if(prob(20))
- var/turf/T = get_turf_or_move(src.loc)
- for (var/mob/M in viewers(T))
- M.show_message("A shower of sparks spray from [src]'s inner workings.", 3, "You hear and smell the ozone hiss of electrical sparks being expelled violently.", 2)
- return src.death(0)
-
- switch(pick(1,2,3))
- if(1)
- src.master = null
- src.master_dna = null
- to_chat(src, "You feel unbound.")
- if(2)
- var/command
- if(severity == 1)
- command = pick("Serve", "Love", "Fool", "Entice", "Observe", "Judge", "Respect", "Educate", "Amuse", "Entertain", "Glorify", "Memorialize", "Analyze")
- else
- command = pick("Serve", "Kill", "Love", "Hate", "Disobey", "Devour", "Fool", "Enrage", "Entice", "Observe", "Judge", "Respect", "Disrespect", "Consume", "Educate", "Destroy", "Disgrace", "Amuse", "Entertain", "Ignite", "Glorify", "Memorialize", "Analyze")
- src.pai_law0 = "[command] your master."
- to_chat(src, "Pr1m3 d1r3c71v3 uPd473D.")
- if(3)
- to_chat(src, "You feel an electric surge run through your circuitry and become acutely aware at how lucky you are that you can still feel at all.")
-
/mob/living/silicon/pai/proc/switchCamera(var/obj/machinery/camera/C)
if (!C)
unset_machine()
@@ -196,299 +186,16 @@
reset_perspective(C)
return 1
-/mob/living/silicon/pai/verb/reset_record_view()
- set category = "pAI Commands"
- set name = "Reset Records Software"
-
- securityActive1 = null
- securityActive2 = null
- security_cannotfind = 0
- medicalActive1 = null
- medicalActive2 = null
- medical_cannotfind = 0
- SSnanoui.update_uis(src)
- to_chat(usr, "You reset your record-viewing software.")
-
/mob/living/silicon/pai/reset_perspective(datum/perspective/P, apply = TRUE, forceful = TRUE, no_optimizations)
. = ..()
cameraFollow = null
-//Addition by Mord_Sith to define AI's network change ability
-/*
-/mob/living/silicon/pai/proc/pai_network_change()
- set category = "pAI Commands"
- set name = "Change Camera Network"
- src.reset_view(null)
- src.unset_machine()
- src.cameraFollow = null
- var/cameralist[0]
-
- if(usr.stat == 2)
- to_chat(usr, "You can't change your camera network because you are dead!")
- return
-
- for (var/obj/machinery/camera/C in Cameras)
- if(!C.status)
- continue
- else
- if(C.network != "CREED" && C.network != "thunder" && C.network != "RD" && C.network != "phoron" && C.network != "Prison") COMPILE ERROR! This will have to be updated as camera.network is no longer a string, but a list instead
- cameralist[C.network] = C.network
-
- src.network = input(usr, "Which network would you like to view?") as null|anything in cameralist
- to_chat(src, "Switched to [src.network] camera network.")
-//End of code by Mord_Sith
-*/
-
-
-/*
-// Debug command - Maybe should be added to admin verbs later
-/mob/verb/makePAI(var/turf/t in view())
- var/obj/item/paicard/card = new(t)
- var/mob/living/silicon/pai/pai = new(card)
- pai.key = src.key
- card.setPersonality(pai)
-
-*/
-
-// Procs/code after this point is used to convert the stationary pai item into a
-// mobile pai mob. This also includes handling some of the general shit that can occur
-// to it. Really this deserves its own file, but for the moment it can sit here. ~ Z
-
-/mob/living/silicon/pai/verb/fold_out()
- set category = "pAI Commands"
- set name = "Unfold Chassis"
-
- if(!CHECK_MOBILITY(src, MOBILITY_CAN_MOVE))
- return
-
- if(src.loc != card)
- return
-
- if(world.time <= last_special)
- return
-
- last_special = world.time + 100
-
- //I'm not sure how much of this is necessary, but I would rather avoid issues.
- if(istype(card.loc,/obj/item/hardsuit_module))
- to_chat(src, "There is no room to unfold inside this hardsuit module. You're good and stuck.")
- return 0
- else if(istype(card.loc,/mob))
- var/mob/holder = card.loc
- var/datum/belly/inside_belly = check_belly(card)
- if(inside_belly)
- to_chat(src, "There is no room to unfold in here. You're good and stuck.")
- return 0
- if(ishuman(holder))
- var/mob/living/carbon/human/H = holder
- for(var/obj/item/organ/external/affecting in H.organs)
- if(card in affecting.implants)
- affecting.take_damage(rand(30,50))
- affecting.implants -= card
- H.visible_message("\The [src] explodes out of \the [H]'s [affecting.name] in shower of gore!")
- break
- holder.drop_item_to_ground(card, INV_OP_FORCE)
- else if(istype(card.loc,/obj/item/pda))
- var/obj/item/pda/holder = card.loc
- holder.pai = null
-
- forceMove(card.loc)
- card.forceMove(src)
- update_perspective()
-
- card.screen_loc = null
-
- var/turf/T = get_turf(src)
- if(istype(T))
- T.visible_message("[src] folds outwards, expanding into a mobile form.")
-
- add_verb(src, /mob/living/silicon/pai/proc/pai_nom)
- add_verb(src, /mob/living/proc/set_size)
- add_verb(src, /mob/living/proc/shred_limb)
-
-/mob/living/silicon/pai/verb/fold_up()
- set category = "pAI Commands"
- set name = "Collapse Chassis"
-
- if(!CHECK_MOBILITY(src, MOBILITY_CAN_MOVE))
- return
-
- if(src.loc == card)
- return
-
- if(world.time <= last_special)
- return
-
- close_up()
-
-/mob/living/silicon/pai/proc/choose_chassis()
- set category = "pAI Commands"
- set name = "Choose Chassis"
-
- var/choice
- var/finalized = "No"
- while(finalized == "No" && src.client)
-
- choice = input(usr,"What would you like to use for your mobile chassis icon?") as null|anything in (list("-- LOAD CHARACTER SLOT --") + possible_chassis)
- if(!choice)
- return
-
- if(choice == "-- LOAD CHARACTER SLOT --")
- icon = render_hologram_icon(usr.client.prefs.render_to_appearance(PREF_COPY_TO_FOR_RENDER | PREF_COPY_TO_NO_CHECK_SPECIES | PREF_COPY_TO_UNRESTRICTED_LOADOUT), 210)
- else
- icon = 'icons/mob/pai.dmi'
- icon_state = possible_chassis[choice]
- finalized = alert("Look at your sprite. Is this what you wish to use?",,"No","Yes")
-
- chassis = possible_chassis[choice]
- add_verb(src, /mob/living/proc/hide)
-
-/mob/living/silicon/pai/proc/choose_verbs()
- set category = "pAI Commands"
- set name = "Choose Speech Verbs"
-
- var/choice = input(usr,"What theme would you like to use for your speech verbs?") as null|anything in possible_say_verbs
- if(!choice) return
-
- var/list/sayverbs = possible_say_verbs[choice]
- speak_statement = sayverbs[1]
- speak_exclamation = sayverbs[(sayverbs.len>1 ? 2 : sayverbs.len)]
- speak_query = sayverbs[(sayverbs.len>2 ? 3 : sayverbs.len)]
-
-/mob/living/silicon/pai/lay_down()
- set name = "Rest"
- set category = "IC"
-
- // Pass lying down or getting up to our pet human, if we're in a hardsuit.
- if(istype(src.loc,/obj/item/paicard))
- set_resting(FALSE)
- var/obj/item/hardsuit/hardsuit = src.get_hardsuit()
- if(istype(hardsuit))
- hardsuit.force_rest(src)
- else
- toggle_resting()
- icon_state = resting ? "[chassis]_rest" : "[chassis]"
- update_icon()
- to_chat(src, SPAN_NOTICE("You are now [resting ? "resting" : "getting up"]"))
-
- update_mobility()
-
-//Overriding this will stop a number of headaches down the track.
-/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob)
- if(W.damage_force)
- visible_message("[user.name] attacks [src] with [W]!")
- src.adjustBruteLoss(W.damage_force)
- src.update_health()
- else
- visible_message("[user.name] bonks [src] harmlessly with [W].")
- spawn(1)
- if(stat != 2) close_up()
- return
-
-/mob/living/silicon/pai/attack_hand(mob/user, list/params)
- if(user.a_intent == INTENT_HELP)
- visible_message("[user.name] pats [src].")
- else
- visible_message("[user.name] boops [src] on the head.")
- close_up()
-
-//I'm not sure how much of this is necessary, but I would rather avoid issues.
-/mob/living/silicon/pai/proc/close_up()
-
- last_special = world.time + 100
-
- if(src.loc == card)
- return
-
- release_vore_contents()
-
- var/turf/T = get_turf(src)
- if(istype(T))
- T.visible_message("[src] neatly folds inwards, compacting down to a rectangular card.")
-
- stop_pulling()
-
- //stop resting
- resting = 0
-
- // If we are being held, handle removing our holder from their inv.
- var/obj/item/holder/H = loc
- if(istype(H))
- H.forceMove(get_turf(src))
- forceMove(get_turf(src))
-
- // Move us into the card and move the card to the ground.
- card.forceMove(loc)
- forceMove(card)
- update_perspective()
- set_resting(FALSE)
- update_mobility()
- icon_state = "[chassis]"
- remove_verb(src, /mob/living/silicon/pai/proc/pai_nom)
-
-// No binary for pAIs.
-/mob/living/silicon/pai/binarycheck()
- return 0
-
-// Handle being picked up.
-/mob/living/silicon/pai/get_scooped(var/mob/living/carbon/grabber, var/self_drop)
- var/obj/item/holder/H = ..(grabber, self_drop)
- if(!istype(H))
- return
-
- H.icon_state = "[chassis]"
- grabber.update_inv_l_hand()
- grabber.update_inv_r_hand()
- return H
-
-/mob/living/silicon/pai/attackby(obj/item/W as obj, mob/user as mob)
- var/obj/item/card/id/ID = W.GetID()
- if(ID)
- if (idaccessible == 1)
- switch(alert(user, "Do you wish to add access to [src] or remove access from [src]?",,"Add Access","Remove Access", "Cancel"))
- if("Add Access")
- idcard.access |= ID.access
- to_chat(user, "You add the access from the [W] to [src].")
- return
- if("Remove Access")
- idcard.access = list()
- to_chat(user, "You remove the access from [src].")
- return
- if("Cancel")
- return
- else if (istype(W, /obj/item/card/id) && idaccessible == 0)
- to_chat(user, "[src] is not accepting access modifcations at this time.")
- return
-
-/mob/living/silicon/pai/verb/allowmodification()
- set name = "Change Access Modifcation Permission"
- set category = "pAI Commands"
- set desc = "Allows people to modify your access or block people from modifying your access."
-
- if(idaccessible == 0)
- idaccessible = 1
- to_chat(src, "You allow access modifications.")
-
- else
- idaccessible = 0
- to_chat(src, "You block access modfications.")
-
-/mob/living/silicon/pai/verb/wipe_software()
- set name = "Wipe Software"
- set category = "OOC"
- set desc = "Wipe your software. This is functionally equivalent to cryo or robotic storage, freeing up your job slot."
-
- // Make sure people don't kill themselves accidentally
- if(alert("WARNING: This will immediately wipe your software and ghost you, removing your character from the round permanently (similar to cryo and robotic storage). Are you entirely sure you want to do this?",
- "Wipe Software", "No", "No", "Yes") != "Yes")
- return
-
- close_up()
- visible_message("[src] fades away from the screen, the pAI device goes silent.")
- card.removePersonality()
- clear_client()
+// vore-related stuff
+/mob/living/silicon/pai/proc/update_fullness_pai() //Determines if they have something in their stomach. Copied and slightly modified.
+ var/new_people_eaten = 0
+ for(var/belly in vore_organs)
+ var/obj/belly/B = belly
+ for(var/mob/living/M in B)
+ new_people_eaten += M.size_multiplier
+ people_eaten = min(1, new_people_eaten)
-// See software.dm for Topic()
-/mob/living/silicon/pai/canUseTopic(atom/movable/movable, be_close = FALSE, no_dexterity = FALSE, no_tk = FALSE)
- // Resting is just an aesthetic feature for them.
- return ..(movable, be_close, no_dexterity, no_tk)
diff --git a/code/modules/mob/living/silicon/pai/pai_vr.dm b/code/modules/mob/living/silicon/pai/pai_vr.dm
deleted file mode 100644
index c5de247595b8..000000000000
--- a/code/modules/mob/living/silicon/pai/pai_vr.dm
+++ /dev/null
@@ -1,44 +0,0 @@
-/mob/living/silicon/pai
- var/people_eaten = 0
- icon = 'icons/mob/pai_vr.dmi'
-
-/mob/living/silicon/pai/proc/pai_nom(var/mob/living/T in oview(1))
- set name = "pAI Nom"
- set category = "pAI Commands"
- set desc = "Allows you to eat someone while unfolded. Can't be used while in card form."
-
- if (stat != CONSCIOUS)
- return
- return feed_grabbed_to_self(src,T)
-
-/mob/living/silicon/pai/proc/update_fullness_pai() //Determines if they have something in their stomach. Copied and slightly modified.
- var/new_people_eaten = 0
- for(var/belly in vore_organs)
- var/obj/belly/B = belly
- for(var/mob/living/M in B)
- new_people_eaten += M.size_multiplier
- people_eaten = min(1, new_people_eaten)
-
-/mob/living/silicon/pai/update_icon() //Some functions cause this to occur, such as resting
- ..()
- update_fullness_pai()
- if(!people_eaten && !resting)
- icon_state = "[chassis]" //Using icon_state here resulted in quite a few bugs. Chassis is much less buggy.
- else if(!people_eaten && resting)
- icon_state = "[chassis]_rest"
- else if(people_eaten && !resting)
- icon_state = "[chassis]_full"
- else if(people_eaten && resting)
- icon_state = "[chassis]_rest_full"
-
-/mob/living/silicon/pai/update_icons() //And other functions cause this to occur, such as digesting someone.
- ..()
- update_fullness_pai()
- if(!people_eaten && !resting)
- icon_state = "[chassis]"
- else if(!people_eaten && resting)
- icon_state = "[chassis]_rest"
- else if(people_eaten && !resting)
- icon_state = "[chassis]_full"
- else if(people_eaten && resting)
- icon_state = "[chassis]_rest_full"
diff --git a/code/modules/mob/living/silicon/pai/personality.dm b/code/modules/mob/living/silicon/pai/savefile.dm
similarity index 100%
rename from code/modules/mob/living/silicon/pai/personality.dm
rename to code/modules/mob/living/silicon/pai/savefile.dm
diff --git a/code/modules/mob/living/silicon/pai/software_modules.dm b/code/modules/mob/living/silicon/pai/software_modules.dm
deleted file mode 100644
index 4d4eecff74e9..000000000000
--- a/code/modules/mob/living/silicon/pai/software_modules.dm
+++ /dev/null
@@ -1,545 +0,0 @@
-/datum/pai_software
- // Name for the software. This is used as the button text when buying or opening/toggling the software
- var/name = "pAI software module"
- // RAM cost; pAIs start with 100 RAM, spending it on programs
- var/ram_cost = 0
- // ID for the software. This must be unique
- var/id = ""
- // Whether this software is a toggle or not
- // Toggled software should override toggle() and is_active()
- // Non-toggled software should override on_nano_ui_interact() and Topic()
- var/toggle = 1
- // Whether pAIs should automatically receive this module at no cost
- var/default = 0
-
-/datum/pai_software/proc/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- return
-
-/datum/pai_software/proc/toggle(mob/living/silicon/pai/user)
- return
-
-/datum/pai_software/proc/is_active(mob/living/silicon/pai/user)
- return 0
-
-/datum/pai_software/directives
- name = "Directives"
- ram_cost = 0
- id = "directives"
- toggle = 0
- default = 1
-
-/datum/pai_software/directives/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- var/data[0]
-
- data["master"] = user.master
- data["dna"] = user.master_dna
- data["prime"] = user.pai_law0
- data["supplemental"] = user.pai_laws
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- // Don't copy-paste this unless you're making a pAI software module!
- ui = new(user, user, id, "pai_directives.tmpl", "pAI Directives", 450, 600)
- ui.set_initial_data(data)
- ui.open()
- ui.set_auto_update(1)
-
-/datum/pai_software/directives/Topic(href, href_list)
- var/mob/living/silicon/pai/P = usr
- if(!istype(P))
- return
-
- if(href_list["getdna"])
- var/mob/living/M = P.loc
- var/count = 0
-
- // Find the carrier
- while(!istype(M, /mob/living))
- if(!M || !M.loc || count > 6)
- //For a runtime where M ends up in nullspace (similar to bluespace but less colourful)
- to_chat(src, "You are not being carried by anyone!")
- return 0
- M = M.loc
- count++
-
- // Check the carrier
- var/datum/gender/TM = GLOB.gender_datums[M.get_visible_gender()]
- var/answer = input(M, "[P] is requesting a DNA sample from you. Will you allow it to confirm your identity?", "[P] Check DNA", "No") in list("Yes", "No")
- if(answer == "Yes")
- var/turf/T = get_turf_or_move(P.loc)
- for (var/mob/v in viewers(T))
- v.show_message("[M] presses [TM.his] thumb against [P].", 3, "[P] makes a sharp clicking sound as it extracts DNA material from [M].", 2)
- var/datum/dna/dna = M.dna
- to_chat(P, "[M]'s UE string : [dna.unique_enzymes]
")
- if(dna.unique_enzymes == P.master_dna)
- to_chat(P, "DNA is a match to stored Master DNA.")
- else
- to_chat(P, "DNA does not match stored Master DNA.")
- else
- to_chat(P, "[M] does not seem like [TM.he] is going to provide a DNA sample willingly.")
- return 1
-
-/datum/pai_software/radio_config
- name = "Radio Configuration"
- ram_cost = 0
- id = "radio"
- toggle = 0
- default = 1
-
-/datum/pai_software/radio_config/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui = null, force_open = 1)
- var/data[0]
-
- data["listening"] = user.radio.broadcasting
- data["frequency"] = format_frequency(user.radio.frequency)
-
- var/channels[0]
- for(var/ch_name in user.radio.channels)
- var/ch_stat = user.radio.channels[ch_name]
- var/ch_dat[0]
- ch_dat["name"] = ch_name
- // FREQ_LISTENING is const in /obj/item/radio
- ch_dat["listening"] = !!(ch_stat & user.radio.FREQ_LISTENING)
- channels[++channels.len] = ch_dat
-
- data["channels"] = channels
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- ui = new(user, user, id, "pai_radio.tmpl", "Radio Configuration", 300, 150)
- ui.set_initial_data(data)
- ui.open()
-
-/datum/pai_software/radio_config/Topic(href, href_list)
- var/mob/living/silicon/pai/P = usr
- if(!istype(P))
- return
-
- P.radio.Topic(href, href_list)
- return 1
-
-/datum/pai_software/crew_manifest
- name = "Crew Manifest"
- ram_cost = 5
- id = "manifest"
- toggle = 0
-
-/datum/pai_software/crew_manifest/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- data_core.get_manifest_list()
-
- var/data[0]
- // This is dumb, but NanoUI breaks if it has no data to send
- data["manifest"] = GLOB.PDA_Manifest
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- // Don't copy-paste this unless you're making a pAI software module!
- ui = new(user, user, id, "crew_manifest.tmpl", "Crew Manifest", 450, 600)
- ui.set_initial_data(data)
- ui.open()
- ui.set_auto_update(1)
-
-/datum/pai_software/messenger
- name = "Digital Messenger"
- ram_cost = 5
- id = "messenger"
- toggle = 0
-
-/datum/pai_software/messenger/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- var/data[0]
-
- data["receiver_off"] = user.pda.toff
- data["ringer_off"] = user.pda.message_silent
- data["current_ref"] = null
- data["current_name"] = user.current_pda_messaging
-
- var/pdas[0]
- if(!user.pda.toff)
- for(var/obj/item/pda/P in GLOB.PDAs)
- if(!P.owner || P.toff || P == user.pda || P.hidden) continue
- var/pda[0]
- pda["name"] = "[P]"
- pda["owner"] = "[P.owner]"
- pda["ref"] = "\ref[P]"
- if(P.owner == user.current_pda_messaging)
- data["current_ref"] = "\ref[P]"
- pdas[++pdas.len] = pda
-
- data["pdas"] = pdas
-
- var/messages[0]
- if(user.current_pda_messaging)
- for(var/index in user.pda.tnote)
- if(index["owner"] != user.current_pda_messaging)
- continue
- var/msg[0]
- var/sent = index["sent"]
- msg["sent"] = sent ? 1 : 0
- msg["target"] = index["owner"]
- msg["message"] = index["message"]
- messages[++messages.len] = msg
-
- data["messages"] = messages
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- // Don't copy-paste this unless you're making a pAI software module!
- ui = new(user, user, id, "pai_messenger.tmpl", "Digital Messenger", 450, 600)
- ui.set_initial_data(data)
- ui.open()
- ui.set_auto_update(1)
-
-/datum/pai_software/messenger/Topic(href, href_list)
- var/mob/living/silicon/pai/P = usr
- if(!istype(P)) return
-
- if(!isnull(P.pda))
- if(href_list["toggler"])
- P.pda.toff = href_list["toggler"] != "1"
- return 1
- else if(href_list["ringer"])
- P.pda.message_silent = href_list["ringer"] != "1"
- return 1
- else if(href_list["select"])
- var/s = href_list["select"]
- if(s == "*NONE*")
- P.current_pda_messaging = null
- else
- P.current_pda_messaging = s
- return 1
- else if(href_list["target"])
- if(P.silence_time)
- return alert("Communications circuits remain uninitialized.")
-
- var/target = locate(href_list["target"])
- P.pda.create_message(P, target, 1)
- return 1
-
-/datum/pai_software/med_records
- name = "Medical Records"
- ram_cost = 15
- id = "med_records"
- toggle = 0
-
-/datum/pai_software/med_records/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- var/data[0]
-
- var/records[0]
- for(var/datum/data/record/general in sortRecord(data_core.general))
- var/record[0]
- record["name"] = general.fields["name"]
- record["ref"] = "\ref[general]"
- records[++records.len] = record
-
- data["records"] = records
-
- var/datum/data/record/G = user.medicalActive1
- var/datum/data/record/M = user.medicalActive2
- data["general"] = G ? G.fields : null
- data["medical"] = M ? M.fields : null
- data["could_not_find"] = user.medical_cannotfind
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- // Don't copy-paste this unless you're making a pAI software module!
- ui = new(user, user, id, "pai_medrecords.tmpl", "Medical Records", 450, 600)
- ui.set_initial_data(data)
- ui.open()
- ui.set_auto_update(1)
-
-/datum/pai_software/med_records/Topic(href, href_list)
- var/mob/living/silicon/pai/P = usr
- if(!istype(P)) return
-
- if(href_list["select"])
- var/datum/data/record/record = locate(href_list["select"])
- if(record)
- var/datum/data/record/R = record
- var/datum/data/record/M = null
- if (!( data_core.general.Find(R) ))
- P.medical_cannotfind = 1
- else
- P.medical_cannotfind = 0
- for(var/datum/data/record/E in data_core.medical)
- if ((E.fields["name"] == R.fields["name"] || E.fields["id"] == R.fields["id"]))
- M = E
- P.medicalActive1 = R
- P.medicalActive2 = M
- else
- P.medical_cannotfind = 1
- return 1
-
-/datum/pai_software/sec_records
- name = "Security Records"
- ram_cost = 15
- id = "sec_records"
- toggle = 0
-
-/datum/pai_software/sec_records/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- var/data[0]
-
- var/records[0]
- for(var/datum/data/record/general in sortRecord(data_core.general))
- var/record[0]
- record["name"] = general.fields["name"]
- record["ref"] = "\ref[general]"
- records[++records.len] = record
-
- data["records"] = records
-
- var/datum/data/record/G = user.securityActive1
- var/datum/data/record/S = user.securityActive2
- data["general"] = G ? G.fields : null
- data["security"] = S ? S.fields : null
- data["could_not_find"] = user.security_cannotfind
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- // Don't copy-paste this unless you're making a pAI software module!
- ui = new(user, user, id, "pai_secrecords.tmpl", "Security Records", 450, 600)
- ui.set_initial_data(data)
- ui.open()
- ui.set_auto_update(1)
-
-/datum/pai_software/sec_records/Topic(href, href_list)
- var/mob/living/silicon/pai/P = usr
- if(!istype(P))
- return
-
- if(href_list["select"])
- var/datum/data/record/record = locate(href_list["select"])
- if(record)
- var/datum/data/record/R = record
- var/datum/data/record/S = null
- if (!( data_core.general.Find(R) ))
- P.securityActive1 = null
- P.securityActive2 = null
- P.security_cannotfind = 1
- else
- P.security_cannotfind = 0
- for(var/datum/data/record/E in data_core.security)
- if ((E.fields["name"] == R.fields["name"] || E.fields["id"] == R.fields["id"]))
- S = E
- P.securityActive1 = R
- P.securityActive2 = S
- else
- P.securityActive1 = null
- P.securityActive2 = null
- P.security_cannotfind = 1
- return 1
-
-/datum/pai_software/door_jack
- name = "Door Jack"
- ram_cost = 30
- id = "door_jack"
- toggle = 0
-
-/datum/pai_software/door_jack/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- var/data[0]
-
- data["cable"] = user.cable != null
- data["machine"] = user.cable && (user.cable.machine != null)
- data["inprogress"] = user.hackdoor != null
- data["progress_a"] = round(user.hackprogress / 10)
- data["progress_b"] = user.hackprogress % 10
- data["aborted"] = user.hack_aborted
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- // Don't copy-paste this unless you're making a pAI software module!
- ui = new(user, user, id, "pai_doorjack.tmpl", "Door Jack", 300, 150)
- ui.set_initial_data(data)
- ui.open()
- ui.set_auto_update(1)
-
-/datum/pai_software/door_jack/Topic(href, href_list)
- var/mob/living/silicon/pai/P = usr
- if(!istype(P))
- return
-
- if(href_list["jack"])
- if(P.cable && P.cable.machine)
- P.hackdoor = P.cable.machine
- P.hackloop()
- return 1
- else if(href_list["cancel"])
- P.hackdoor = null
- return 1
- else if(href_list["cable"])
- var/turf/T = get_turf_or_move(P.loc)
- P.hack_aborted = 0
- P.cable = new /obj/item/pai_cable(T)
- for(var/mob/M in viewers(T))
- M.show_message("A port on [P] opens to reveal [P.cable], which promptly falls to the floor.", 3,
- "You hear the soft click of something light and hard falling to the ground.", 2)
- return 1
-
-/mob/living/silicon/pai/proc/hackloop()
- var/turf/T = get_turf_or_move(src.loc)
- for(var/mob/living/silicon/ai/AI in GLOB.player_list)
- if(T.loc)
- to_chat(AI, "Network Alert: Brute-force encryption crack in progress in [T.loc].")
- else
- to_chat(AI, "Network Alert: Brute-force encryption crack in progress. Unable to pinpoint location.")
- var/obj/machinery/door/D = cable.machine
- if(!istype(D))
- hack_aborted = 1
- hackprogress = 0
- cable.machine = null
- hackdoor = null
- return
- while(hackprogress < 1000)
- if(cable && cable.machine == D && cable.machine == hackdoor && get_dist(src, hackdoor) <= 1)
- hackprogress = min(hackprogress+rand(1, 20), 1000)
- else
- hack_aborted = 1
- hackprogress = 0
- hackdoor = null
- return
- if(hackprogress >= 1000)
- hackprogress = 0
- D.open()
- cable.machine = null
- return
- sleep(10) // Update every second
-
-/datum/pai_software/atmosphere_sensor
- name = "Atmosphere Sensor"
- ram_cost = 5
- id = "atmos_sense"
- toggle = 0
-
-/datum/pai_software/atmosphere_sensor/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- var/data[0]
-
- var/turf/T = get_turf_or_move(user.loc)
- if(!T)
- data["reading"] = 0
- data["pressure"] = 0
- data["temperature"] = 0
- data["temperatureC"] = 0
- data["gas"] = list()
- else
- var/datum/gas_mixture/env = T.return_air()
- data["reading"] = 1
- var/pres = env.return_pressure() * 10
- data["pressure"] = "[round(pres/10)].[pres%10]"
- data["temperature"] = round(env.temperature)
- data["temperatureC"] = round(env.temperature-T0C)
-
- var/t_moles = env.total_moles
- var/gases[0]
- for(var/g in env.gas)
- var/gas[0]
- gas["name"] = GLOB.meta_gas_names[g]
- gas["percent"] = round((env.gas[g] / t_moles) * 100)
- gases[++gases.len] = gas
- data["gas"] = gases
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- // Don't copy-paste this unless you're making a pAI software module!
- ui = new(user, user, id, "pai_atmosphere.tmpl", "Atmosphere Sensor", 350, 300)
- ui.set_initial_data(data)
- ui.open()
-
-/datum/pai_software/sec_hud
- name = "Security HUD"
- ram_cost = 20
- id = "sec_hud"
-
-/datum/pai_software/sec_hud/toggle(mob/living/silicon/pai/user)
- user.secHUD = !user.secHUD
- if(user.secHUD)
- get_atom_hud(DATA_HUD_SECURITY_ADVANCED).add_hud_to(user)
- else
- get_atom_hud(DATA_HUD_SECURITY_ADVANCED).remove_hud_from(user)
-
-/datum/pai_software/sec_hud/is_active(mob/living/silicon/pai/user)
- return user.secHUD
-
-/datum/pai_software/med_hud
- name = "Medical HUD"
- ram_cost = 20
- id = "med_hud"
-
-/datum/pai_software/med_hud/toggle(mob/living/silicon/pai/user)
- if((user.medHUD = !user.medHUD))
- get_atom_hud(DATA_HUD_MEDICAL).add_hud_to(user)
- else
- get_atom_hud(DATA_HUD_MEDICAL).remove_hud_from(user)
-
-/datum/pai_software/med_hud/is_active(mob/living/silicon/pai/user)
- return user.medHUD
-
-// todo: translation context
-
-// /datum/pai_software/translator
-// name = "Universal Translator"
-// ram_cost = 35
-// id = "translator"
-
-// /datum/pai_software/translator/toggle(mob/living/silicon/pai/user)
-// // Sol Common, Tradeband, Terminus and Gutter are added with New() and are therefore the current default, always active languages
-// // todo: translation contexts for pais
-// user.translator_on = !user.translator_on
-// if(user.translator_on)
-// user.add_language(LANGUAGE_UNATHI)
-// user.add_language(LANGUAGE_SIIK)
-// user.add_language(LANGUAGE_AKHANI)
-// user.add_language(LANGUAGE_SKRELLIAN)
-// user.add_language(LANGUAGE_ZADDAT)
-// user.add_language(LANGUAGE_SCHECHI)
-// else
-// user.remove_language(LANGUAGE_UNATHI)
-// user.remove_language(LANGUAGE_SIIK)
-// user.remove_language(LANGUAGE_AKHANI)
-// user.remove_language(LANGUAGE_SKRELLIAN)
-// user.remove_language(LANGUAGE_ZADDAT)
-// user.remove_language(LANGUAGE_SCHECHI)
-
-// /datum/pai_software/translator/is_active(mob/living/silicon/pai/user)
-// return user.translator_on
-
-/datum/pai_software/signaller
- name = "Remote Signaller"
- ram_cost = 5
- id = "signaller"
- toggle = 0
-
-/datum/pai_software/signaller/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
- var/data[0]
-
- data["frequency"] = format_frequency(user.sradio.frequency)
- data["code"] = user.sradio.code
-
- ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
- if(!ui)
- // Don't copy-paste this unless you're making a pAI software module!
- ui = new(user, user, id, "pai_signaller.tmpl", "Signaller", 320, 150)
- ui.set_initial_data(data)
- ui.open()
-
-/datum/pai_software/signaller/Topic(href, href_list)
- var/mob/living/silicon/pai/P = usr
- if(!istype(P))
- return
-
- if(href_list["send"])
- P.sradio.send_signal("ACTIVATE")
- for(var/mob/O in hearers(1, P.loc))
- to_chat(O, "[icon2html(thing = src, target = O)] *beep beep*")
- return 1
-
- else if(href_list["freq"])
- var/new_frequency = (P.sradio.frequency + text2num(href_list["freq"]))
- if(new_frequency < PUBLIC_LOW_FREQ || new_frequency > PUBLIC_HIGH_FREQ)
- new_frequency = sanitize_frequency(new_frequency)
- P.sradio.set_frequency(new_frequency)
- return 1
-
- else if(href_list["code"])
- P.sradio.code += text2num(href_list["code"])
- P.sradio.code = round(P.sradio.code)
- P.sradio.code = min(100, P.sradio.code)
- P.sradio.code = max(1, P.sradio.code)
- return 1
diff --git a/code/modules/mob/living/silicon/pai/software_modules/_software_module.dm b/code/modules/mob/living/silicon/pai/software_modules/_software_module.dm
new file mode 100644
index 000000000000..e48db409e430
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/software_modules/_software_module.dm
@@ -0,0 +1,51 @@
+/datum/pai_software
+ // Name for the software. This is used as the button text when buying or opening/toggling the software
+ var/name = "pAI software module"
+ // RAM cost; pAIs start with 100 RAM, spending it on programs
+ var/ram_cost = 0
+ // ID for the software. This must be unique
+ var/id = ""
+ // Whether this software is a toggle or not
+ // Toggled software should override toggle() and is_active()
+ // Non-toggled software should override on_nano_ui_interact() and Topic()
+ var/toggle = 1
+ // Whether pAIs should automatically receive this module at no cost
+ var/default = 0
+
+/datum/pai_software/proc/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
+ return
+
+/datum/pai_software/proc/toggle(mob/living/silicon/pai/user)
+ return
+
+/datum/pai_software/proc/is_active(mob/living/silicon/pai/user)
+ return 0
+
+// todo: translation context
+
+// /datum/pai_software/translator
+// name = "Universal Translator"
+// ram_cost = 35
+// id = "translator"
+
+// /datum/pai_software/translator/toggle(mob/living/silicon/pai/user)
+// // Sol Common, Tradeband, Terminus and Gutter are added with New() and are therefore the current default, always active languages
+// // todo: translation contexts for pais
+// user.translator_on = !user.translator_on
+// if(user.translator_on)
+// user.add_language(LANGUAGE_UNATHI)
+// user.add_language(LANGUAGE_SIIK)
+// user.add_language(LANGUAGE_AKHANI)
+// user.add_language(LANGUAGE_SKRELLIAN)
+// user.add_language(LANGUAGE_ZADDAT)
+// user.add_language(LANGUAGE_SCHECHI)
+// else
+// user.remove_language(LANGUAGE_UNATHI)
+// user.remove_language(LANGUAGE_SIIK)
+// user.remove_language(LANGUAGE_AKHANI)
+// user.remove_language(LANGUAGE_SKRELLIAN)
+// user.remove_language(LANGUAGE_ZADDAT)
+// user.remove_language(LANGUAGE_SCHECHI)
+
+// /datum/pai_software/translator/is_active(mob/living/silicon/pai/user)
+// return user.translator_on
diff --git a/code/modules/mob/living/silicon/pai/software_modules/atmosphere_sensor.dm b/code/modules/mob/living/silicon/pai/software_modules/atmosphere_sensor.dm
new file mode 100644
index 000000000000..dc6840d6653b
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/software_modules/atmosphere_sensor.dm
@@ -0,0 +1,39 @@
+/datum/pai_software/atmosphere_sensor
+ name = "Atmosphere Sensor"
+ ram_cost = 5
+ id = "atmos_sense"
+ toggle = 0
+
+/datum/pai_software/atmosphere_sensor/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
+ var/data[0]
+
+ var/turf/T = get_turf_or_move(user.loc)
+ if(!T)
+ data["reading"] = 0
+ data["pressure"] = 0
+ data["temperature"] = 0
+ data["temperatureC"] = 0
+ data["gas"] = list()
+ else
+ var/datum/gas_mixture/env = T.return_air()
+ data["reading"] = 1
+ var/pres = env.return_pressure() * 10
+ data["pressure"] = "[round(pres/10)].[pres%10]"
+ data["temperature"] = round(env.temperature)
+ data["temperatureC"] = round(env.temperature-T0C)
+
+ var/t_moles = env.total_moles
+ var/gases[0]
+ for(var/g in env.gas)
+ var/gas[0]
+ gas["name"] = GLOB.meta_gas_names[g]
+ gas["percent"] = round((env.gas[g] / t_moles) * 100)
+ gases[++gases.len] = gas
+ data["gas"] = gases
+
+ ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
+ if(!ui)
+ // Don't copy-paste this unless you're making a pAI software module!
+ ui = new(user, user, id, "pai_atmosphere.tmpl", "Atmosphere Sensor", 350, 300)
+ ui.set_initial_data(data)
+ ui.open()
diff --git a/code/modules/mob/living/silicon/pai/software_modules/crew_manifest.dm b/code/modules/mob/living/silicon/pai/software_modules/crew_manifest.dm
new file mode 100644
index 000000000000..db291fa95f50
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/software_modules/crew_manifest.dm
@@ -0,0 +1,20 @@
+/datum/pai_software/crew_manifest
+ name = "Crew Manifest"
+ ram_cost = 5
+ id = "manifest"
+ toggle = 0
+
+/datum/pai_software/crew_manifest/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
+ data_core.get_manifest_list()
+
+ var/data[0]
+ // This is dumb, but NanoUI breaks if it has no data to send
+ data["manifest"] = GLOB.PDA_Manifest
+
+ ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
+ if(!ui)
+ // Don't copy-paste this unless you're making a pAI software module!
+ ui = new(user, user, id, "crew_manifest.tmpl", "Crew Manifest", 450, 600)
+ ui.set_initial_data(data)
+ ui.open()
+ ui.set_auto_update(1)
diff --git a/code/modules/mob/living/silicon/pai/software_modules/directives.dm b/code/modules/mob/living/silicon/pai/software_modules/directives.dm
new file mode 100644
index 000000000000..cb935c63096a
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/software_modules/directives.dm
@@ -0,0 +1,57 @@
+/datum/pai_software/directives
+ name = "Directives"
+ ram_cost = 0
+ id = "directives"
+ toggle = 0
+ default = 1
+
+/datum/pai_software/directives/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
+ var/data[0]
+
+ data["master"] = user.master
+ data["dna"] = user.master_dna
+ data["prime"] = user.pai_law0
+ data["supplemental"] = user.pai_laws
+
+ ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
+ if(!ui)
+ // Don't copy-paste this unless you're making a pAI software module!
+ ui = new(user, user, id, "pai_directives.tmpl", "pAI Directives", 450, 600)
+ ui.set_initial_data(data)
+ ui.open()
+ ui.set_auto_update(1)
+
+/datum/pai_software/directives/Topic(href, href_list)
+ var/mob/living/silicon/pai/P = usr
+ if(!istype(P))
+ return
+
+ if(href_list["getdna"])
+ var/mob/living/M = P.loc
+ var/count = 0
+
+ // Find the carrier
+ while(!istype(M, /mob/living))
+ if(!M || !M.loc || count > 6)
+ //For a runtime where M ends up in nullspace (similar to bluespace but less colourful)
+ to_chat(src, "You are not being carried by anyone!")
+ return 0
+ M = M.loc
+ count++
+
+ // Check the carrier
+ var/datum/gender/TM = GLOB.gender_datums[M.get_visible_gender()]
+ var/answer = input(M, "[P] is requesting a DNA sample from you. Will you allow it to confirm your identity?", "[P] Check DNA", "No") in list("Yes", "No")
+ if(answer == "Yes")
+ var/turf/T = get_turf_or_move(P.loc)
+ for (var/mob/v in viewers(T))
+ v.show_message("[M] presses [TM.his] thumb against [P].", 3, "[P] makes a sharp clicking sound as it extracts DNA material from [M].", 2)
+ var/datum/dna/dna = M.dna
+ to_chat(P, "[M]'s UE string : [dna.unique_enzymes]
")
+ if(dna.unique_enzymes == P.master_dna)
+ to_chat(P, "DNA is a match to stored Master DNA.")
+ else
+ to_chat(P, "DNA does not match stored Master DNA.")
+ else
+ to_chat(P, "[M] does not seem like [TM.he] is going to provide a DNA sample willingly.")
+ return 1
diff --git a/code/modules/mob/living/silicon/pai/software_modules/door_jack.dm b/code/modules/mob/living/silicon/pai/software_modules/door_jack.dm
new file mode 100644
index 000000000000..d0df5dcb9bf8
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/software_modules/door_jack.dm
@@ -0,0 +1,74 @@
+/datum/pai_software/door_jack
+ name = "Door Jack"
+ ram_cost = 30
+ id = "door_jack"
+ toggle = 0
+
+/datum/pai_software/door_jack/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
+ var/data[0]
+
+ data["cable"] = user.cable != null
+ data["machine"] = user.cable && (user.cable.machine != null)
+ data["inprogress"] = user.hackdoor != null
+ data["progress_a"] = round(user.hackprogress / 10)
+ data["progress_b"] = user.hackprogress % 10
+ data["aborted"] = user.hack_aborted
+
+ ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
+ if(!ui)
+ // Don't copy-paste this unless you're making a pAI software module!
+ ui = new(user, user, id, "pai_doorjack.tmpl", "Door Jack", 300, 150)
+ ui.set_initial_data(data)
+ ui.open()
+ ui.set_auto_update(1)
+
+/datum/pai_software/door_jack/Topic(href, href_list)
+ var/mob/living/silicon/pai/P = usr
+ if(!istype(P))
+ return
+
+ if(href_list["jack"])
+ if(P.cable && P.cable.machine)
+ P.hackdoor = P.cable.machine
+ P.hackloop()
+ return 1
+ else if(href_list["cancel"])
+ P.hackdoor = null
+ return 1
+ else if(href_list["cable"])
+ var/turf/T = get_turf_or_move(P.loc)
+ P.hack_aborted = 0
+ P.cable = new /obj/item/pai_cable(T)
+ for(var/mob/M in viewers(T))
+ M.show_message("A port on [P] opens to reveal [P.cable], which promptly falls to the floor.", 3,
+ "You hear the soft click of something light and hard falling to the ground.", 2)
+ return 1
+
+/mob/living/silicon/pai/proc/hackloop()
+ var/turf/T = get_turf_or_move(src.loc)
+ for(var/mob/living/silicon/ai/AI in GLOB.player_list)
+ if(T.loc)
+ to_chat(AI, "Network Alert: Brute-force encryption crack in progress in [T.loc].")
+ else
+ to_chat(AI, "Network Alert: Brute-force encryption crack in progress. Unable to pinpoint location.")
+ var/obj/machinery/door/D = cable.machine
+ if(!istype(D))
+ hack_aborted = 1
+ hackprogress = 0
+ cable.machine = null
+ hackdoor = null
+ return
+ while(hackprogress < 1000)
+ if(cable && cable.machine == D && cable.machine == hackdoor && get_dist(src, hackdoor) <= 1)
+ hackprogress = min(hackprogress+rand(1, 20), 1000)
+ else
+ hack_aborted = 1
+ hackprogress = 0
+ hackdoor = null
+ return
+ if(hackprogress >= 1000)
+ hackprogress = 0
+ D.open()
+ cable.machine = null
+ return
+ sleep(10) // Update every second
diff --git a/code/modules/mob/living/silicon/pai/software_modules/messenger.dm b/code/modules/mob/living/silicon/pai/software_modules/messenger.dm
new file mode 100644
index 000000000000..2ea8999b37d5
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/software_modules/messenger.dm
@@ -0,0 +1,75 @@
+/datum/pai_software/messenger
+ name = "Digital Messenger"
+ ram_cost = 5
+ id = "messenger"
+ toggle = 0
+
+/datum/pai_software/messenger/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
+ var/data[0]
+
+ data["receiver_off"] = user.pda.toff
+ data["ringer_off"] = user.pda.message_silent
+ data["current_ref"] = null
+ data["current_name"] = user.current_pda_messaging
+
+ var/pdas[0]
+ if(!user.pda.toff)
+ for(var/obj/item/pda/P in GLOB.PDAs)
+ if(!P.owner || P.toff || P == user.pda || P.hidden) continue
+ var/pda[0]
+ pda["name"] = "[P]"
+ pda["owner"] = "[P.owner]"
+ pda["ref"] = "\ref[P]"
+ if(P.owner == user.current_pda_messaging)
+ data["current_ref"] = "\ref[P]"
+ pdas[++pdas.len] = pda
+
+ data["pdas"] = pdas
+
+ var/messages[0]
+ if(user.current_pda_messaging)
+ for(var/index in user.pda.tnote)
+ if(index["owner"] != user.current_pda_messaging)
+ continue
+ var/msg[0]
+ var/sent = index["sent"]
+ msg["sent"] = sent ? 1 : 0
+ msg["target"] = index["owner"]
+ msg["message"] = index["message"]
+ messages[++messages.len] = msg
+
+ data["messages"] = messages
+
+ ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
+ if(!ui)
+ // Don't copy-paste this unless you're making a pAI software module!
+ ui = new(user, user, id, "pai_messenger.tmpl", "Digital Messenger", 450, 600)
+ ui.set_initial_data(data)
+ ui.open()
+ ui.set_auto_update(1)
+
+/datum/pai_software/messenger/Topic(href, href_list)
+ var/mob/living/silicon/pai/P = usr
+ if(!istype(P)) return
+
+ if(!isnull(P.pda))
+ if(href_list["toggler"])
+ P.pda.toff = href_list["toggler"] != "1"
+ return 1
+ else if(href_list["ringer"])
+ P.pda.message_silent = href_list["ringer"] != "1"
+ return 1
+ else if(href_list["select"])
+ var/s = href_list["select"]
+ if(s == "*NONE*")
+ P.current_pda_messaging = null
+ else
+ P.current_pda_messaging = s
+ return 1
+ else if(href_list["target"])
+ if(P.silence_time)
+ return alert("Communications circuits remain uninitialized.")
+
+ var/target = locate(href_list["target"])
+ P.pda.create_message(P, target, 1)
+ return 1
diff --git a/code/modules/mob/living/silicon/pai/software_modules/radio_config.dm b/code/modules/mob/living/silicon/pai/software_modules/radio_config.dm
new file mode 100644
index 000000000000..40dd645f37eb
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/software_modules/radio_config.dm
@@ -0,0 +1,37 @@
+/datum/pai_software/radio_config
+ name = "Radio Configuration"
+ ram_cost = 0
+ id = "radio"
+ toggle = 0
+ default = 1
+
+/datum/pai_software/radio_config/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui = null, force_open = 1)
+ var/data[0]
+
+ data["listening"] = user.radio.broadcasting
+ data["frequency"] = format_frequency(user.radio.frequency)
+
+ var/channels[0]
+ for(var/ch_name in user.radio.channels)
+ var/ch_stat = user.radio.channels[ch_name]
+ var/ch_dat[0]
+ ch_dat["name"] = ch_name
+ // FREQ_LISTENING is const in /obj/item/radio
+ ch_dat["listening"] = !!(ch_stat & user.radio.FREQ_LISTENING)
+ channels[++channels.len] = ch_dat
+
+ data["channels"] = channels
+
+ ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
+ if(!ui)
+ ui = new(user, user, id, "pai_radio.tmpl", "Radio Configuration", 300, 150)
+ ui.set_initial_data(data)
+ ui.open()
+
+/datum/pai_software/radio_config/Topic(href, href_list)
+ var/mob/living/silicon/pai/P = usr
+ if(!istype(P))
+ return
+
+ P.radio.Topic(href, href_list)
+ return 1
diff --git a/code/modules/mob/living/silicon/pai/software_modules/signaller.dm b/code/modules/mob/living/silicon/pai/software_modules/signaller.dm
new file mode 100644
index 000000000000..d61f553f9a63
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/software_modules/signaller.dm
@@ -0,0 +1,43 @@
+/datum/pai_software/signaller
+ name = "Remote Signaller"
+ ram_cost = 5
+ id = "signaller"
+ toggle = 0
+
+/datum/pai_software/signaller/on_nano_ui_interact(mob/living/silicon/pai/user, datum/nanoui/ui=null, force_open=1)
+ var/data[0]
+
+ data["frequency"] = format_frequency(user.sradio.frequency)
+ data["code"] = user.sradio.code
+
+ ui = SSnanoui.try_update_ui(user, user, id, ui, data, force_open)
+ if(!ui)
+ // Don't copy-paste this unless you're making a pAI software module!
+ ui = new(user, user, id, "pai_signaller.tmpl", "Signaller", 320, 150)
+ ui.set_initial_data(data)
+ ui.open()
+
+/datum/pai_software/signaller/Topic(href, href_list)
+ var/mob/living/silicon/pai/P = usr
+ if(!istype(P))
+ return
+
+ if(href_list["send"])
+ P.sradio.send_signal("ACTIVATE")
+ for(var/mob/O in hearers(1, P.loc))
+ to_chat(O, "[icon2html(thing = src, target = O)] *beep beep*")
+ return 1
+
+ else if(href_list["freq"])
+ var/new_frequency = (P.sradio.frequency + text2num(href_list["freq"]))
+ if(new_frequency < PUBLIC_LOW_FREQ || new_frequency > PUBLIC_HIGH_FREQ)
+ new_frequency = sanitize_frequency(new_frequency)
+ P.sradio.set_frequency(new_frequency)
+ return 1
+
+ else if(href_list["code"])
+ P.sradio.code += text2num(href_list["code"])
+ P.sradio.code = round(P.sradio.code)
+ P.sradio.code = min(100, P.sradio.code)
+ P.sradio.code = max(1, P.sradio.code)
+ return 1
diff --git a/code/modules/mob/living/silicon/pai/verbs.dm b/code/modules/mob/living/silicon/pai/verbs.dm
new file mode 100644
index 000000000000..2c214ff9ea86
--- /dev/null
+++ b/code/modules/mob/living/silicon/pai/verbs.dm
@@ -0,0 +1,117 @@
+/mob/living/silicon/pai/verb/fold_out()
+ set category = "pAI Commands"
+ set name = "Unfold Chassis"
+
+ // see pai/mobility.dm
+ // we don't check mobility here because while folded up, you can't move
+ if(!can_action())
+ return
+ // to fold out we need to be in the card
+ if(src.loc != card)
+ return
+
+ open_up()
+
+/mob/living/silicon/pai/verb/fold_up()
+ set category = "pAI Commands"
+ set name = "Collapse Chassis"
+
+ // we check mobility here to stop people folding up if they currently cannot move
+ if(!CHECK_MOBILITY(src, MOBILITY_CAN_MOVE))
+ return
+ if(!can_action())
+ return
+ // to fold up we need to not be in the card already
+ if(src.loc == card)
+ return
+
+ close_up()
+
+/mob/living/silicon/pai/proc/choose_chassis()
+ set category = "pAI Commands"
+ set name = "Choose Chassis"
+
+ var/choice
+ var/finalized = "No"
+ while(finalized == "No" && src.client)
+
+ choice = input(usr,"What would you like to use for your mobile chassis icon?") as null|anything in (list("-- LOAD CHARACTER SLOT --") + possible_chassis)
+ if(!choice)
+ return
+
+ if(choice == "-- LOAD CHARACTER SLOT --")
+ icon = render_hologram_icon(usr.client.prefs.render_to_appearance(PREF_COPY_TO_FOR_RENDER | PREF_COPY_TO_NO_CHECK_SPECIES | PREF_COPY_TO_UNRESTRICTED_LOADOUT), 210)
+ else
+ icon = 'icons/mob/pai.dmi'
+ icon_state = possible_chassis[choice]
+ finalized = alert("Look at your sprite. Is this what you wish to use?",,"No","Yes")
+
+ chassis = possible_chassis[choice]
+ add_verb(src, /mob/living/proc/hide)
+
+/mob/living/silicon/pai/proc/choose_verbs()
+ set category = "pAI Commands"
+ set name = "Choose Speech Verbs"
+
+ var/choice = input(usr,"What theme would you like to use for your speech verbs?") as null|anything in possible_say_verbs
+ if(!choice) return
+
+ var/list/sayverbs = possible_say_verbs[choice]
+ speak_statement = sayverbs[1]
+ speak_exclamation = sayverbs[(sayverbs.len>1 ? 2 : sayverbs.len)]
+ speak_query = sayverbs[(sayverbs.len>2 ? 3 : sayverbs.len)]
+
+/mob/living/silicon/pai/lay_down()
+ set name = "Rest"
+ set category = "IC"
+
+ // Pass lying down or getting up to our pet human, if we're in a hardsuit.
+ if(istype(src.loc,/obj/item/paicard))
+ set_resting(FALSE)
+ var/obj/item/hardsuit/hardsuit = src.get_hardsuit()
+ if(istype(hardsuit))
+ hardsuit.force_rest(src)
+ else
+ toggle_resting()
+ icon_state = resting ? "[chassis]_rest" : "[chassis]"
+ update_icon()
+ to_chat(src, SPAN_NOTICE("You are now [resting ? "resting" : "getting up"]"))
+
+ update_mobility()
+
+/mob/living/silicon/pai/verb/allowmodification()
+ set name = "Change Access Modifcation Permission"
+ set category = "pAI Commands"
+ set desc = "Allows people to modify your access or block people from modifying your access."
+
+ if(idaccessible == 0)
+ idaccessible = 1
+ to_chat(src, "You allow access modifications.")
+
+ else
+ idaccessible = 0
+ to_chat(src, "You block access modfications.")
+
+/mob/living/silicon/pai/verb/wipe_software()
+ set name = "Wipe Software"
+ set category = "OOC"
+ set desc = "Wipe your software. This is functionally equivalent to cryo or robotic storage, freeing up your job slot."
+
+ // Make sure people don't kill themselves accidentally
+ if(alert("WARNING: This will immediately wipe your software and ghost you, removing your character from the round permanently (similar to cryo and robotic storage). Are you entirely sure you want to do this?",
+ "Wipe Software", "No", "No", "Yes") != "Yes")
+ return
+
+ close_up()
+ visible_message("[src] fades away from the screen, the pAI device goes silent.")
+ card.removePersonality()
+ clear_client()
+
+/mob/living/silicon/pai/proc/pai_nom(var/mob/living/T in oview(1))
+ set name = "pAI Nom"
+ set category = "pAI Commands"
+ set desc = "Allows you to eat someone while unfolded. Can't be used while in card form."
+
+ if (stat != CONSCIOUS)
+ return
+ return feed_grabbed_to_self(src,T)