diff --git a/code/__defines/nifsoft.dm b/code/__defines/nifsoft.dm index 06d1b0c7aa..daf63425ea 100644 --- a/code/__defines/nifsoft.dm +++ b/code/__defines/nifsoft.dm @@ -36,9 +36,10 @@ #define NIF_HEATSINK 29 #define NIF_COMPLIANCE 30 #define NIF_SIZECHANGE 31 +#define NIF_SOULCATCHER 32 // Must be equal to the highest number above -#define TOTAL_NIF_SOFTWARE 31 +#define TOTAL_NIF_SOFTWARE 32 ////////////////////// // NIF flag list hints @@ -83,6 +84,8 @@ #define NIF_O_APCCHARGE 0x2 #define NIF_O_PRESSURESEAL 0x4 #define NIF_O_HEATSINKS 0x8 +#define NIF_O_SCMYSELF 0x10 //Soulcatcher stuff +#define NIF_O_SCOTHERS 0x20 /////////////////// // applies_to flags diff --git a/code/game/jobs/job/job.dm b/code/game/jobs/job/job.dm index 6add65ce55..dd265203d6 100644 --- a/code/game/jobs/job/job.dm +++ b/code/game/jobs/job/job.dm @@ -54,7 +54,7 @@ if(COMPANY_OPPOSED) loyalty = 0.70 //give them an account in the station database - var/money_amount = (rand(5,50) + rand(5, 50)) * loyalty * economic_modifier * ECO_MODIFIER + var/money_amount = (rand(15,40) + rand(15,40)) * loyalty * economic_modifier * ECO_MODIFIER //VOREStation Edit - Smoothed peaks. var/datum/money_account/M = create_account(H.real_name, money_amount, null) if(H.mind) var/remembered_info = "" diff --git a/code/modules/client/preference_setup/vore/08_nif.dm b/code/modules/client/preference_setup/vore/08_nif.dm index 52586d3bcf..62ebbdbeac 100644 --- a/code/modules/client/preference_setup/vore/08_nif.dm +++ b/code/modules/client/preference_setup/vore/08_nif.dm @@ -2,6 +2,7 @@ /datum/preferences var/obj/item/device/nif/nif_path var/nif_durability + var/list/nif_savedata // Definition of the stuff for NIFs /datum/category_item/player_setup_item/vore/nif @@ -11,10 +12,12 @@ /datum/category_item/player_setup_item/vore/nif/load_character(var/savefile/S) S["nif_path"] >> pref.nif_path S["nif_durability"] >> pref.nif_durability + S["nif_savedata"] >> pref.nif_savedata /datum/category_item/player_setup_item/vore/nif/save_character(var/savefile/S) S["nif_path"] << pref.nif_path S["nif_durability"] << pref.nif_durability + S["nif_savedata"] << pref.nif_savedata /datum/category_item/player_setup_item/vore/nif/sanitize_character() if(pref.nif_path && !ispath(pref.nif_path)) //We have at least a text string that should be a path. @@ -23,19 +26,23 @@ pref.nif_path = null //Kill! WARNING("Loaded a NIF but it was an invalid path, [pref.real_name]") - if(ispath(pref.nif_path) && !pref.nif_durability) //How'd you lose this? + if(ispath(pref.nif_path) && isnull(pref.nif_durability)) //How'd you lose this? pref.nif_durability = initial(pref.nif_path.durability) //Well, have a new one, my bad. WARNING("Loaded a NIF but with no durability, [pref.real_name]") + if(!islist(pref.nif_savedata)) + pref.nif_savedata = list() + /datum/category_item/player_setup_item/vore/nif/copy_to_mob(var/mob/living/carbon/human/character) //If you had a NIF... if((character.type == /mob/living/carbon/human) && ispath(pref.nif_path) && pref.nif_durability) - new pref.nif_path(character,pref.nif_durability) + new pref.nif_path(character,pref.nif_durability,pref.nif_savedata) //And now here's the trick. We wipe these so that if they die, they lose the NIF. //Backup implants will start saving this again periodically, and so will cryo'ing out. pref.nif_path = null pref.nif_durability = null + pref.nif_savedata = null var/savefile/S = new /savefile(pref.path) if(!S) WARNING ("Couldn't load NIF save savefile? [pref.real_name]") S.cd = "/character[pref.default_slot]" diff --git a/code/modules/mob/living/carbon/human/emote_vr.dm b/code/modules/mob/living/carbon/human/emote_vr.dm index 274ddd2904..4b3091b440 100644 --- a/code/modules/mob/living/carbon/human/emote_vr.dm +++ b/code/modules/mob/living/carbon/human/emote_vr.dm @@ -13,6 +13,42 @@ if ("awoo") message = "awoos loudly. AwoooOOOOoooo!" m_type = 2 + if ("nsay") + if(!nif) + to_chat(src,"You can't use *nsay without a NIF.") + return 1 + var/datum/nifsoft/soulcatcher/SC = nif.imp_check(NIF_SOULCATCHER) + if(!SC) + to_chat(src,"You need the Soulcatcher software to use *nme.") + return 1 + if(!SC.brainmob.mind) + to_chat(src,"You need a loaded mind to use *nme.") + return 1 + var/nifmessage = sanitize(input("Type a message to say.") as text|null) + if(nifmessage) + nifmessage = "\[\icon[nif.big_icon]NIF\] [src] speaks, \"[nifmessage]\"" + to_chat(SC.brainmob,nifmessage) + to_chat(src,nifmessage) + return 1 + + if ("nme") + if(!nif) + to_chat(src,"You can't use *nme without a NIF.") + return 1 + var/datum/nifsoft/soulcatcher/SC = nif.imp_check(NIF_SOULCATCHER) + if(!SC) + to_chat(src,"You need the Soulcatcher software to use *nme.") + return 1 + if(!SC.brainmob.mind) + to_chat(src,"You need a loaded mind to use *nme.") + return 1 + var/nifmessage = sanitize(input("Type an action to perform.") as text|null) + if(nifmessage) + nifmessage = "\[\icon[nif.big_icon]NIF\] [src] [nifmessage]" + to_chat(SC.brainmob,nifmessage) + to_chat(src,nifmessage) + return 1 + if ("flip") var/danger = 1 //Base 1% chance to break something. var/list/involved_parts = list(BP_L_LEG, BP_R_LEG, BP_L_FOOT, BP_R_FOOT) diff --git a/code/modules/nifsoft/nif.dm b/code/modules/nifsoft/nif.dm index e8f4976f48..8dc8d9c52e 100644 --- a/code/modules/nifsoft/nif.dm +++ b/code/modules/nifsoft/nif.dm @@ -1,3 +1,14 @@ +/* ////////////////////////////// +The NIF has a proc API shared with NIFSofts, and you should not really ever +directly interact with this API. Procs like install(), uninstall(), etc should +not be directly called. If you want to install a new NIFSoft, pass the NIF in +the constructor for a new instance of the NIFSoft. If you want to force a NIFSoft +to be uninstalled, use imp_check to get a reference to it, and call +uninstall() only on the return value of that. + +You can also set the stat of a NIF to NIF_TEMPFAIL without any issues to disable it. +*/ ////////////////////////////// + //Holder on humans to prevent having to 'find' it every time /mob/living/carbon/human/var/obj/item/device/nif/nif @@ -45,17 +56,22 @@ "seems to be daydreaming", "focuses elsewhere for a moment") -//Constructor comes with a free AR HUD -/obj/item/device/nif/New(var/newloc,var/wear) - ..(newloc) + var/list/save_data - //Required for AR stuff. - nif_hud = new(src) +//Constructor comes with a free AR HUD +/obj/item/device/nif/New(var/newloc,var/wear,var/list/load_data) + ..(newloc) //First one to spawn in the game, make a big icon if(!big_icon) big_icon = new(icon,icon_state = "nif_full") + //Required for AR stuff. + nif_hud = new(src) + + //Put loaded data here if we loaded any + save_data = islist(load_data) ? load_data.Copy() : list() + //If given a human on spawn (probably from persistence) if(ishuman(newloc)) var/mob/living/carbon/human/H = newloc @@ -233,7 +249,8 @@ var/percent_done = (world.time - (install_done - (30 MINUTES))) / (30 MINUTES) - human.client.screen.Add(global_hud.whitense) //This is the camera static + if(human.client) + human.client.screen.Add(global_hud.whitense) //This is the camera static switch(percent_done) //This is 0.0 to 1.0 kinda percent. //Connecting to optical nerves @@ -242,19 +259,20 @@ //Mapping brain if(0.2 to 0.9) - if(prob(99)) return TRUE + if(prob(98)) return TRUE var/incident = rand(1,3) switch(incident) if(1) var/message = pick(list( - "Your head throbs around your new implant.", - "The skin around your recent surgery itches.", - "A wave of nausea overtakes you as the world seems to spin.", - "The floor suddenly seems to come up at you.", - "There's a throbbing lump of ice behind your eyes.", - "A wave of pain shoots down your neck." + "Your head throbs around your new implant!", + "The skin around your recent surgery itches!", + "A wave of nausea overtakes you as the world seems to spin!", + "The floor suddenly seems to come up at you!", + "There's a throbbing lump of ice behind your eyes!", + "A wave of pain shoots down your neck!" )) - to_chat(human,"[message]") + human.adjustHalLoss(35) + human.custom_pain(message,35) if(2) human.Weaken(5) to_chat(human,"A wave of weakness rolls over you.") diff --git a/code/modules/nifsoft/nif_statpanel.dm b/code/modules/nifsoft/nif_statpanel.dm index 23b5aca0b1..579dc44ec9 100644 --- a/code/modules/nifsoft/nif_statpanel.dm +++ b/code/modules/nifsoft/nif_statpanel.dm @@ -5,18 +5,22 @@ /mob/living/carbon/human/proc/SetupNifStat() var/nif_status = "" + var/nif_percent = round((nif.durability/initial(nif.durability))*100) switch(nif.stat) if(NIF_WORKING) - nif_status = "Operating Normally" + if(nif_percent < 20) + nif_status = "Service Needed Soon" + else + nif_status = "Operating Normally" if(NIF_POWFAIL) nif_status = "Insufficient Energy!" if(NIF_TEMPFAIL) - nif_status = "Needs Maintenance!" + nif_status = "System Failure!" if(NIF_INSTALLING) nif_status = "Adapting To User" else nif_status = "Unknown - Error" - nif_status += " ([round((nif.durability/initial(nif.durability))*100)]%)" + nif_status += " (Condition: [nif_percent]%)" stat("NIF Status", nif_status) if(nif.stat == NIF_WORKING) diff --git a/code/modules/nifsoft/nifsoft.dm b/code/modules/nifsoft/nifsoft.dm index 0703adfab4..fdccca0bce 100644 --- a/code/modules/nifsoft/nifsoft.dm +++ b/code/modules/nifsoft/nifsoft.dm @@ -1,3 +1,6 @@ +//Please see the comment above the main NIF definition before +//trying to call any of these procs directly. + //A single piece of NIF software /datum/nifsoft var/name = "Prototype" diff --git a/code/modules/nifsoft/software/01_vision.dm b/code/modules/nifsoft/software/01_vision.dm index d222cd67dd..c58377f957 100644 --- a/code/modules/nifsoft/software/01_vision.dm +++ b/code/modules/nifsoft/software/01_vision.dm @@ -172,8 +172,9 @@ life() if((. = ..())) var/mob/living/carbon/human/H = nif.human - H.client.screen |= global_hud.meson H.sight |= SEE_TURFS + if(H.client) + H.client.screen |= global_hud.meson /datum/nifsoft/material name = "Material Scanner" @@ -219,8 +220,9 @@ if((. = ..())) var/mob/living/carbon/human/H = nif.human H.sight |= SEE_MOBS - H.client.screen |= global_hud.thermal H.see_invisible = SEE_INVISIBLE_NOLIGHTING + if(H.client) + H.client.screen |= global_hud.thermal /datum/nifsoft/nightvis name = "Low-Light Amp" @@ -244,6 +246,7 @@ if((. = ..())) var/mob/living/carbon/human/H = nif.human H.see_in_dark += 7 - H.client.screen |= global_hud.nvg H.see_invisible = SEE_INVISIBLE_NOLIGHTING + if(H.client) + H.client.screen |= global_hud.nvg diff --git a/code/modules/nifsoft/software/13_soulcatcher.dm b/code/modules/nifsoft/software/13_soulcatcher.dm new file mode 100644 index 0000000000..f1eac1bef3 --- /dev/null +++ b/code/modules/nifsoft/software/13_soulcatcher.dm @@ -0,0 +1,258 @@ +//These two also have NIF FLAG representations. These are the local setting representations. +#define NIF_SC_CATCHING_ME 0x1 +#define NIF_SC_CATCHING_OTHERS 0x2 +//These are purely local setings flags, without global representation. +#define NIF_SC_ALLOW_EARS 0x4 +#define NIF_SC_ALLOW_EYES 0x8 + +/////////// +// Soulcatcher - Like a posibrain, sorta! +/datum/nifsoft/soulcatcher + name = "Soulcatcher" + desc = "A mind storage and processing system capable of capturing and supporting a single human-intelligence mind on body death, into a small VR space." + list_pos = NIF_SOULCATCHER + cost = 1265 + wear = 1 + p_drain = 0.01 + + var/setting_flags = (NIF_SC_CATCHING_ME|NIF_SC_CATCHING_OTHERS|NIF_SC_ALLOW_EARS|NIF_SC_ALLOW_EYES) + var/mob/living/carbon/brain/caught_soul/brainmob + var/inside_flavor = "A small completely white room with a couch, and a window to what seems to be the outside world. A small sign in the corner says 'Configure Me'." + + New() + ..() + brainmob = new(nif) + brainmob.nif = nif + brainmob.container = src + brainmob.stat = 0 + brainmob.silent = FALSE + dead_mob_list -= src.brainmob + load_settings() + + Destroy() + if(brainmob) + brainmob.container = null + qdel(brainmob) + ..() + + install() + if((. = ..())) + nif.set_flag(NIF_O_SCMYSELF,NIF_FLAGS_OTHER) + nif.set_flag(NIF_O_SCOTHERS,NIF_FLAGS_OTHER) + + uninstall() + if((. = ..())) + nif.clear_flag(NIF_O_SCMYSELF,NIF_FLAGS_OTHER) + nif.clear_flag(NIF_O_SCOTHERS,NIF_FLAGS_OTHER) + + activate() + if((. = ..())) + show_settings(nif.human) + spawn(0) + deactivate() + + deactivate() + if((. = ..())) + return TRUE + + stat_text() + return "Change Settings[brainmob.mind ? " (Mind Stored)" : ""]" + + proc/save_settings() + nif.save_data["[list_pos]"] = inside_flavor + return TRUE + + proc/load_settings() + var/load = nif.save_data["[list_pos]"] + if(load) + inside_flavor = load + return TRUE + + proc/show_settings(var/mob/living/carbon/human/H) + var/settings_list = list( + "Catching You \[[setting_flags & NIF_SC_CATCHING_ME ? "Enabled" : "Disabled"]\]" = NIF_SC_CATCHING_ME, + "Catching Others \[[setting_flags & NIF_SC_CATCHING_OTHERS ? "Enabled" : "Disabled"]\]" = NIF_SC_CATCHING_OTHERS, + "Ext. Hearing \[[setting_flags & NIF_SC_ALLOW_EARS ? "Enabled" : "Disabled"]\]" = NIF_SC_ALLOW_EARS, + "Ext. Vision \[[setting_flags & NIF_SC_ALLOW_EYES ? "Enabled" : "Disabled"]\]" = NIF_SC_ALLOW_EYES, + "Design Inside", + "Erase Contents") + var/choice = input(nif.human,"Select a setting to modify:","Soulcatcher NIFSoft") as null|anything in settings_list + if(choice in settings_list) + switch(choice) + if("Design Inside") + var/new_flavor = input(nif.human, "Type what the prey sees after being 'caught'. This will be \ + printed after an intro ending with: \"Around you, you see...\" to the prey. If you already \ + have prey, this will be printed to them after \"Your surroundings change to...\". Limit 2048 char.", \ + "VR Environment", html_decode(inside_flavor)) as message + new_flavor = sanitize(new_flavor) + inside_flavor = new_flavor + nif.notify("Updating VR environment...") + to_chat(brainmob,"Your surroundings change to...\n[inside_flavor]") + save_settings() + return TRUE + + if("Erase Contents") + if(brainmob && brainmob.mind) + var/warning = alert(nif.human,"Are you SURE you want to erase [brainmob.mind.name]?","Erase Mind","CANCEL","DELETE","CANCEL") + if(warning == "DELETE") + brainmob.ghostize() + qdel(brainmob) + brainmob = new(nif) + nif.notify("Mind deleted!",TRUE) + return TRUE + else + nif.notify("No mind to delete!",TRUE) + return TRUE + + //Must just be a flag without special handling then. + else + var/flag = settings_list[choice] + return toggle_setting(flag) + + proc/toggle_setting(var/flag) + setting_flags ^= flag + + //Special treatment + switch(flag) + if(NIF_SC_CATCHING_ME) + if(setting_flags & NIF_SC_CATCHING_ME) + nif.set_flag(NIF_O_SCMYSELF,NIF_FLAGS_OTHER) + else + nif.clear_flag(NIF_O_SCMYSELF,NIF_FLAGS_OTHER) + if(NIF_SC_CATCHING_OTHERS) + if(setting_flags & NIF_SC_CATCHING_OTHERS) + nif.set_flag(NIF_O_SCOTHERS,NIF_FLAGS_OTHER) + else + nif.clear_flag(NIF_O_SCOTHERS,NIF_FLAGS_OTHER) + if(NIF_SC_ALLOW_EARS) + if(setting_flags & NIF_SC_ALLOW_EARS) + brainmob.ext_deaf = FALSE + to_chat(brainmob,"\[\icon[nif.big_icon]NIF\] Your access to your host's auditory sense has been unrestricted.") + else + brainmob.ext_deaf = TRUE + to_chat(brainmob,"\[\icon[nif.big_icon]NIF\] Your access to your host's auditory sense has been restricted.") + if(NIF_SC_ALLOW_EYES) + if(setting_flags & NIF_SC_ALLOW_EYES) + brainmob.ext_blind = FALSE + to_chat(brainmob,"\[\icon[nif.big_icon]NIF\] Your access to your host's visual sense has been unrestricted.") + else + brainmob.ext_blind = TRUE + to_chat(brainmob,"\[\icon[nif.big_icon]NIF\] Your access to your host's visual sense has been restricted.") + + return TRUE + + proc/catch_mob(var/mob/living/carbon/human/H) + if(istype(H)) + if(H == nif.human) //Reset our permissions to be permissive to the owner. + brainmob.ext_deaf = FALSE + brainmob.ext_blind = FALSE + brainmob.parent_mob = TRUE + + brainmob.dna = H.dna + brainmob.timeofhostdeath = H.timeofdeath + brainmob.stat = 0 + if(H.mind) + H.mind.transfer_to(brainmob) + brainmob.name = brainmob.mind.name + var/message = "Your vision fades in a haze of static, before returning.\nAround you, you see...\n[inside_flavor]" + to_chat(brainmob,message) + nif.notify("Mind loaded into VR space: [brainmob.name]") + to_chat(nif.human,"Your occupant's messages/actions can only be seen by you, and you can \ + send messages that only they can hear/see by 'say'ing either '*nsay' or '*nme'.") + return TRUE + + return FALSE + +//////////////// +//The caught mob +/mob/living/carbon/brain/caught_soul + name = "recorded mind" + desc = "A mind recorded and being played on digital hardware." + use_me = 1 + var/ext_deaf = FALSE //Forbidden from 'ear' access on host + var/ext_blind = FALSE //Forbidden from 'eye' access on host + var/parent_mob = FALSE //If we've captured our owner + var/obj/item/device/nif/nif + +/mob/living/carbon/brain/caught_soul/Destroy() + nif = null + ..() + +/mob/living/carbon/brain/caught_soul/Life() + if(!client) return + + . = ..() + + if(parent_mob) return + //If they're blinded + if(ext_blind) + eye_blind = 5 + client.screen.Remove(global_hud.whitense) + overlay_fullscreen("blind", /obj/screen/fullscreen/blind) + else + eye_blind = 0 + clear_fullscreens() + client.screen.Add(global_hud.whitense) + + //If they're deaf + if(ext_deaf) + ear_deaf = 5 + else + ear_deaf = 0 + +/mob/living/carbon/brain/caught_soul/hear_say() + if(ext_deaf || !client) + return FALSE + ..() + +/mob/living/carbon/brain/caught_soul/show_message() + if(ext_blind || !client) + return FALSE + ..() + +/mob/living/carbon/brain/caught_soul/say(var/message) + if(parent_mob) return ..() + if(silent) return FALSE + to_chat(nif.human,"\[\icon[nif.big_icon]NIF\] [name] speaks, \"[message]\"") + to_chat(src,"\[\icon[nif.big_icon]NIF\] [name] speaks, \"[message]\"") + +/mob/living/carbon/brain/caught_soul/emote(var/act,var/m_type=1,var/message = null) + if(parent_mob) return ..() + if(silent) return FALSE + if (act == "me") + if(silent) + return + if (src.client) + if (client.prefs.muted & MUTE_IC) + src << "You cannot send IC messages (muted)." + return + if (stat) + return + if(!(message)) + return + return custom_emote(m_type, message) + else + return FALSE + +/mob/living/carbon/brain/caught_soul/custom_emote(var/m_type, var/message) + if(silent) return FALSE + to_chat(nif.human,"\[\icon[nif.big_icon]NIF\] [name] [message]") + to_chat(src,"\[\icon[nif.big_icon]NIF\] [name] [message]") + +/////////////////// +//The catching hook +/hook/death/proc/nif_soulcatcher(var/mob/living/carbon/human/H) + if(!istype(H)) return TRUE //Hooks must return TRUE + + if(H.nif && H.nif.flag_check(NIF_O_SCMYSELF,NIF_FLAGS_OTHER)) //They are caught in their own NIF + var/datum/nifsoft/soulcatcher/SC = H.nif.imp_check(NIF_SOULCATCHER) + if(!SC.brainmob.mind) //As long as they don't have one + SC.catch_mob(H,TRUE) + else if(ishuman(H.loc)) //Died in someone + var/mob/living/carbon/human/HP = H.loc + if(HP.nif && HP.nif.flag_check(NIF_O_SCOTHERS,NIF_FLAGS_OTHER)) + var/datum/nifsoft/soulcatcher/SC = HP.nif.imp_check(NIF_SOULCATCHER) + if(!SC.brainmob.mind) //As long as they don't have one + SC.catch_mob(H,FALSE) + + return TRUE \ No newline at end of file diff --git a/code/modules/nifsoft/software/15_misc.dm b/code/modules/nifsoft/software/15_misc.dm index cc8d6b8b10..91ea175338 100644 --- a/code/modules/nifsoft/software/15_misc.dm +++ b/code/modules/nifsoft/software/15_misc.dm @@ -31,7 +31,7 @@ life() if((. = ..())) var/mob/living/carbon/human/H = nif.human - if(apc && (get_dist(H,apc) <= 1) && H.nutrition < 450) + if(apc && (get_dist(H,apc) <= 1) && H.nutrition < 440) // 440 vs 450, life() happens before we get here so it'll never be EXACTLY 450 H.nutrition = min(H.nutrition+10, 450) apc.drain_power(7000/450*10) //This is from the large rechargers. No idea what the math is. return TRUE diff --git a/code/modules/vore/persist/persist_vr.dm b/code/modules/vore/persist/persist_vr.dm index bddd9bc7b9..eb124f58f6 100644 --- a/code/modules/vore/persist/persist_vr.dm +++ b/code/modules/vore/persist/persist_vr.dm @@ -15,7 +15,7 @@ // Handle people leaving due to round ending. /hook/roundend/proc/persist_locations() - for(var/mob/Player in player_list) + for(var/mob/Player in human_mob_list) if(!Player.mind || isnewplayer(Player)) continue // No mind we can do nothing, new players we care not for else if(Player.stat == DEAD) @@ -233,9 +233,11 @@ if(nif) prefs.nif_path = nif.type prefs.nif_durability = nif.durability + prefs.nif_savedata = nif.save_data.Copy() else prefs.nif_path = null prefs.nif_durability = null + prefs.nif_savedata = null var/datum/category_group/player_setup_category/vore_cat = prefs.player_setup.categories_by_name["VORE"] var/datum/category_item/player_setup_item/vore/nif/nif_prefs = vore_cat.items_by_name["NIF Data"] diff --git a/vorestation.dme b/vorestation.dme index 41f8895e95..8325921369 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -1972,6 +1972,7 @@ #include "code\modules\nifsoft\software\01_vision.dm" #include "code\modules\nifsoft\software\05_health.dm" #include "code\modules\nifsoft\software\10_combat.dm" +#include "code\modules\nifsoft\software\13_soulcatcher.dm" #include "code\modules\nifsoft\software\14_commlink.dm" #include "code\modules\nifsoft\software\15_misc.dm" #include "code\modules\organs\blood.dm"