diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm
index 8548b2be..cabc67f6 100644
--- a/code/__DEFINES/logging.dm
+++ b/code/__DEFINES/logging.dm
@@ -35,6 +35,7 @@
#define LOG_GAME (1 << 12)
#define LOG_ADMIN_PRIVATE (1 << 13)
#define LOG_ASAY (1 << 14)
+#define LOG_CLONING (1 << 15)
//Individual logging panel pages
#define INDIVIDUAL_ATTACK_LOG (LOG_ATTACK)
diff --git a/code/__DEFINES/machines.dm b/code/__DEFINES/machines.dm
index ec719794..0edb4239 100644
--- a/code/__DEFINES/machines.dm
+++ b/code/__DEFINES/machines.dm
@@ -100,6 +100,10 @@
#define NUKE_ON_TIMING 2
#define NUKE_ON_EXPLODING 3
+//cloning defines. These are flags.
+#define CLONING_SUCCESS (1<<0)
+#define CLONING_DELETE_RECORD (1<<1)
+
//these flags are used to tell the DNA modifier if a plant gene cannot be extracted or modified.
#define PLANT_GENE_REMOVABLE (1<<0)
#define PLANT_GENE_EXTRACTABLE (1<<1)
diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm
index 30740956..b6b4efa0 100644
--- a/code/__HELPERS/_logging.dm
+++ b/code/__HELPERS/_logging.dm
@@ -58,6 +58,10 @@
if (CONFIG_GET(flag/log_game))
WRITE_LOG(GLOB.world_game_log, "GAME: [text]")
+/proc/log_cloning(text, mob/initiator)
+ if(CONFIG_GET(flag/log_cloning))
+ WRITE_LOG(GLOB.world_cloning_log, "CLONING: [text]")
+
/proc/log_access(text)
if (CONFIG_GET(flag/log_access))
WRITE_LOG(GLOB.world_game_log, "ACCESS: [text]")
diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm
index deffd25b..bf4ff55f 100644
--- a/code/_globalvars/logging.dm
+++ b/code/_globalvars/logging.dm
@@ -26,6 +26,8 @@ GLOBAL_VAR(query_debug_log)
GLOBAL_PROTECT(query_debug_log)
GLOBAL_VAR(world_job_debug_log)
GLOBAL_PROTECT(world_job_debug_log)
+GLOBAL_VAR(world_cloning_log)
+GLOBAL_PROTECT(world_cloning_log)
GLOBAL_LIST_EMPTY(bombers)
GLOBAL_PROTECT(bombers)
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index 9a1a083b..ccfa668b 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -39,6 +39,8 @@
/datum/config_entry/flag/log_game // log game events
+/datum/config_entry/flag/log_cloning // log cloning actions.
+
/datum/config_entry/flag/log_vote // log voting
/datum/config_entry/flag/log_whisper // log client whisper
diff --git a/code/datums/dna.dm b/code/datums/dna.dm
index 43ec9e54..466a5406 100644
--- a/code/datums/dna.dm
+++ b/code/datums/dna.dm
@@ -371,7 +371,7 @@
updateappearance(icon_update=0)
if(LAZYLEN(mutation_index))
- dna.mutation_index = mutation_index
+ dna.mutation_index = mutation_index.Copy()
domutcheck()
if(mrace || newfeatures || ui)
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index f633b5cf..5dd0b34d 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -62,6 +62,9 @@
var/unconvertable = FALSE
var/late_joiner = FALSE
+ var/last_death = 0
+
+
var/force_escaped = FALSE // Set by Into The Sunset command of the shuttle manipulator
var/list/learned_recipes //List of learned recipe TYPES.
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 9e8756bb..78953609 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -598,6 +598,14 @@
/atom/proc/multitool_act(mob/living/user, obj/item/I)
return
+///Check if the multitool has an item in it's data buffer
+/atom/proc/multitool_check_buffer(user, obj/item/I, silent = FALSE)
+ if(!istype(I, /obj/item/multitool))
+ if(user && !silent)
+ to_chat(user, "[I] has no data buffer!")
+ return FALSE
+ return TRUE
+
/atom/proc/screwdriver_act(mob/living/user, obj/item/I)
SEND_SIGNAL(src, COMSIG_ATOM_SCREWDRIVER_ACT, user, I)
diff --git a/code/game/machinery/cloning.dm b/code/game/machinery/cloning.dm
index 855ede0e..a70c1068 100644
--- a/code/game/machinery/cloning.dm
+++ b/code/game/machinery/cloning.dm
@@ -19,7 +19,7 @@
circuit = /obj/item/circuitboard/machine/clonepod
var/heal_level //The clone is released once its health reaches this level.
- var/obj/machinery/computer/cloning/connected = null //So we remember the connected clone machine.
+ var/obj/machinery/computer/cloning/connected //So we remember the connected clone machine.
var/mess = FALSE //Need to clean out it if it's full of exploded clone.
var/attempting = FALSE //One clone attempt at a time thanks
var/speed_coeff
@@ -53,7 +53,10 @@
update_icon()
/obj/machinery/clonepod/Destroy()
+ var/mob/living/mob_occupant = occupant
go_out()
+ if(mob_occupant)
+ log_cloning("[key_name(mob_occupant)] ejected from [src] at [AREACOORD(src)] due to Destroy().")
QDEL_NULL(radio)
QDEL_NULL(countdown)
if(connected)
@@ -74,6 +77,23 @@
if(heal_level > 100)
heal_level = 100
+/obj/machinery/clonepod/attack_hand(mob/user)
+ . = ..()
+ if(.)
+ return
+ user.examinate(src)
+
+/obj/machinery/clonepod/attack_ai(mob/user)
+ return attack_hand(user)
+
+/obj/machinery/clonepod/examine(mob/user)
+ . = ..()
+ . += "The linking device can be scanned with a multitool."
+ if(in_range(user, src) || isobserver(user))
+ . += "The status display reads: Cloning speed at [speed_coeff*50]%.
Predicted amount of cellular damage: [100-heal_level]%."
+ if(efficiency > 5)
+ . += "Pod has been upgraded to support autoprocessing and apply beneficial mutations."
+
//The return of data disks?? Just for transferring between genetics machine/cloning machine.
//TO-DO: Make the genetics machine accept them.
/obj/item/disk/data
@@ -102,7 +122,7 @@
//Clonepod
/obj/machinery/clonepod/examine(mob/user)
- ..()
+ . = ..()
var/mob/living/mob_occupant = occupant
if(mess)
to_chat(user, "It's filled with blood and viscera. You swear you can see it moving...")
@@ -129,44 +149,54 @@
return examine(user)
//Start growing a human clone in the pod!
-/obj/machinery/clonepod/proc/growclone(ckey, clonename, ui, mutation_index, mindref, datum/species/mrace, list/features, factions, list/quirks)
+/obj/machinery/clonepod/proc/growclone(clonename, ui, mutation_index, mindref, last_death, blood_type, datum/species/mrace, list/features, factions, list/quirks, empty)
if(panel_open)
return FALSE
if(mess || attempting)
return FALSE
- clonemind = locate(mindref) in SSticker.minds
- if(!istype(clonemind)) //not a mind
- return FALSE
- if(!QDELETED(clonemind.current))
- if(clonemind.current.stat != DEAD) //mind is associated with a non-dead body
+
+ if(!empty) //Doesn't matter if we're just making a copy
+ clonemind = locate(mindref) in SSticker.minds
+ if(!istype(clonemind)) //not a mind
return FALSE
- if(clonemind.current.suiciding) // Mind is associated with a body that is suiciding.
+ if(clonemind.last_death != last_death) //The soul has advanced, the record has not.
return FALSE
- if(clonemind.active) //somebody is using that mind
- if( ckey(clonemind.key)!=ckey )
+ if(!QDELETED(clonemind.current))
+ if(clonemind.current.stat != DEAD) //mind is associated with a non-dead body
+ return NONE
+ if(clonemind.current.suiciding) // Mind is associated with a body that is suiciding.
+ return NONE
+ if(!clonemind.active)
+ // get_ghost() will fail if they're unable to reenter their body
+ var/mob/dead/observer/G = clonemind.get_ghost()
+ if(!G)
+ return NONE
+ if(G.suiciding) // The ghost came from a body that is suiciding.
+ return NONE
+ if(clonemind.damnation_type) //Can't clone the damned.
+ INVOKE_ASYNC(src, .proc/horrifyingsound)
+ mess = TRUE
+ icon_state = "pod_g"
+ update_icon()
return FALSE
- else
- // get_ghost() will fail if they're unable to reenter their body
- var/mob/dead/observer/G = clonemind.get_ghost()
- if(!G)
- return FALSE
- if(G.suiciding) // The ghost came from a body that is suiciding.
- return FALSE
- if(clonemind.damnation_type) //Can't clone the damned.
- INVOKE_ASYNC(src, .proc/horrifyingsound)
- mess = TRUE
- update_icon()
- return FALSE
attempting = TRUE //One at a time!!
countdown.start()
var/mob/living/carbon/human/H = new /mob/living/carbon/human(src)
- H.hardset_dna(ui, mutation_index, H.real_name, null, mrace, features)
+ H.hardset_dna(ui, mutation_index, H.real_name, blood_type, mrace, features)
- if(prob(50 - efficiency*10)) //Chance to give a bad mutation.
- H.easy_randmut(NEGATIVE+MINOR_NEGATIVE) //100% bad mutation. Can be cured with mutadone.
+ if(!HAS_TRAIT(H, TRAIT_RADIMMUNE))//dont apply mutations if the species is Mutation proof.
+ if(efficiency > 2)
+ var/list/unclean_mutations = (GLOB.not_good_mutations|GLOB.bad_mutations)
+ H.dna.remove_mutation_group(unclean_mutations)
+ if(efficiency > 5 && prob(20))
+ H.easy_randmut(POSITIVE)
+ if(efficiency < 3 && prob(50))
+ var/mob/M = H.easy_randmut(NEGATIVE+MINOR_NEGATIVE)
+ if(ismob(M))
+ H = M
H.silent = 20 //Prevents an extreme edge case where clones could speak if they said something at exactly the right moment.
occupant = H
@@ -184,15 +214,16 @@
ADD_TRAIT(H, TRAIT_NOCRITDAMAGE, "cloning")
H.Unconscious(80)
- clonemind.transfer_to(H)
+ if(!empty)
+ clonemind.transfer_to(H)
- if(grab_ghost_when == CLONER_FRESH_CLONE)
- H.grab_ghost()
- to_chat(H, "Consciousness slowly creeps over you as your body regenerates.
So this is what cloning feels like?")
+ if(grab_ghost_when == CLONER_FRESH_CLONE)
+ H.grab_ghost()
+ to_chat(H, "Consciousness slowly creeps over you as your body regenerates.
So this is what cloning feels like?")
- if(grab_ghost_when == CLONER_MATURE_CLONE)
- H.ghostize(TRUE) //Only does anything if they were still in their old body and not already a ghost
- to_chat(H.get_ghost(TRUE), "Your body is beginning to regenerate in a cloning pod. You will become conscious when it is complete.")
+ if(grab_ghost_when == CLONER_MATURE_CLONE)
+ H.ghostize(TRUE) //Only does anything if they were still in their old body and not already a ghost
+ to_chat(H.get_ghost(TRUE), "Your body is beginning to regenerate in a cloning pod. You will become conscious when it is complete.")
if(H)
H.faction |= factions
@@ -206,7 +237,7 @@
H.suiciding = FALSE
attempting = FALSE
- return TRUE
+ return CLONING_SUCCESS
//Grow clones to maturity then kick them out. FREELOADERS
/obj/machinery/clonepod/process()
@@ -215,16 +246,20 @@
if(!is_operational()) //Autoeject if power is lost
if(mob_occupant)
go_out()
+ log_cloning("[key_name(mob_occupant)] ejected from [src] at [AREACOORD(src)] due to power loss.")
+
mob_occupant.apply_vore_prefs()
+
connected_message("Clone Ejected: Loss of power.")
else if(mob_occupant && (mob_occupant.loc == src))
- if((mob_occupant.stat == DEAD) || (mob_occupant.suiciding) || mob_occupant.hellbound) //Autoeject corpses and suiciding dudes.
+ if(mob_occupant && (mob_occupant.stat == DEAD) || (mob_occupant.suiciding) || mob_occupant.hellbound) //Autoeject corpses and suiciding dudes.
connected_message("Clone Rejected: Deceased.")
if(internal_radio)
SPEAK("The cloning has been \
aborted due to unrecoverable tissue failure.")
go_out()
+ log_cloning("[key_name(mob_occupant)] ejected from [src] at [AREACOORD(src)] after suiciding.")
mob_occupant.apply_vore_prefs()
else if(mob_occupant.cloneloss > (100 - heal_level))
@@ -272,10 +307,13 @@
BP.attach_limb(mob_occupant)
go_out()
+ log_cloning("[key_name(mob_occupant)] completed cloning cycle in [src] at [AREACOORD(src)].")
mob_occupant.apply_vore_prefs()
else if (!mob_occupant || mob_occupant.loc != src)
occupant = null
+ if (!mess && !panel_open)
+ icon_state = "pod_0"
use_power(200)
update_icon()
@@ -290,6 +328,8 @@
return
if(istype(W, /obj/item/multitool))
+ if(!multitool_check_buffer(user, W))
+ return
var/obj/item/multitool/P = W
if(istype(P.buffer, /obj/machinery/computer/cloning))
@@ -316,10 +356,13 @@
to_chat(user, "Error: Pod has no occupant.")
return
else
+ add_fingerprint(user)
connected_message("Emergency Ejection")
SPEAK("An emergency ejection of the current clone has occurred. Survival not guaranteed.")
to_chat(user, "You force an emergency ejection. ")
go_out()
+ log_cloning("[key_name(user)] manually ejected [key_name(mob_occupant)] from [src] at [AREACOORD(src)].")
+ log_combat(user, mob_occupant, "ejected", W, "from [src]")
mob_occupant.apply_vore_prefs()
else
return ..()
@@ -329,6 +372,9 @@
return
to_chat(user, "You corrupt the genetic compiler.")
malfunction()
+ add_fingerprint(user)
+ log_cloning("[key_name(user)] emagged [src] at [AREACOORD(src)], causing it to malfunction.")
+ log_combat(user, src, "emagged", null, occupant ? "[occupant] inside, killing them via malfunction." : null)
//Put messages in the connected computer's temp var for display.
/obj/machinery/clonepod/proc/connected_message(message)
@@ -381,6 +427,7 @@
unattached_flesh.Cut()
occupant = null
+ clonemind = null
/obj/machinery/clonepod/proc/malfunction()
var/mob/living/mob_occupant = occupant
@@ -391,13 +438,14 @@
mess = TRUE
maim_clone(mob_occupant) //Remove every bit that's grown back so far to drop later, also destroys bits that haven't grown yet
update_icon()
- if(mob_occupant.mind != clonemind)
+ if(clonemind && mob_occupant.mind != clonemind)
clonemind.transfer_to(mob_occupant)
mob_occupant.grab_ghost() // We really just want to make you suffer.
flash_color(mob_occupant, flash_color="#960000", flash_time=100)
to_chat(mob_occupant, "Agony blazes across your consciousness as your body is torn apart.
Is this what dying is like? Yes it is.")
playsound(src.loc, 'sound/machines/warning-buzzer.ogg', 50, 0)
SEND_SOUND(mob_occupant, sound('sound/hallucinations/veryfar_noise.ogg',0,1,50))
+ log_cloning("[key_name(mob_occupant)] destroyed within [src] at [AREACOORD(src)] due to malfunction.")
QDEL_IN(mob_occupant, 40)
/obj/machinery/clonepod/relaymove(mob/user)
@@ -416,11 +464,14 @@
SPEAK(Gibberish("Exposure to electromagnetic fields has caused the ejection of, ERROR: John Doe, prematurely." ,0))
mob_occupant.apply_vore_prefs()
go_out()
+ log_cloning("[key_name(mob_occupant)] ejected from [src] at [AREACOORD(src)] due to EMP pulse.")
/obj/machinery/clonepod/ex_act(severity, target)
..()
- if(!QDELETED(src))
+ if(!QDELETED(src) && occupant)
+ var/mob/living/mob_occupant = occupant
go_out()
+ log_cloning("[key_name(mob_occupant)] ejected from [src] at [AREACOORD(src)] due to explosion.")
/obj/machinery/clonepod/handle_atom_del(atom/A)
if(A == occupant)
@@ -436,7 +487,9 @@
/obj/machinery/clonepod/deconstruct(disassembled = TRUE)
if(occupant)
+ var/mob/living/mob_occupant = occupant
go_out()
+ log_cloning("[key_name(mob_occupant)] ejected from [src] at [AREACOORD(src)] due to deconstruction.")
..()
/obj/machinery/clonepod/proc/maim_clone(mob/living/carbon/human/H)
diff --git a/code/game/machinery/computer/cloning.dm b/code/game/machinery/computer/cloning.dm
index 039152f5..dede5724 100644
--- a/code/game/machinery/computer/cloning.dm
+++ b/code/game/machinery/computer/cloning.dm
@@ -7,25 +7,24 @@
icon_keyboard = "med_key"
circuit = /obj/item/circuitboard/computer/cloning
req_access = list(ACCESS_HEADS) //ONLY USED FOR RECORD DELETION RIGHT NOW.
- var/obj/machinery/dna_scannernew/scanner = null //Linked scanner. For scanning.
+ var/obj/machinery/dna_scannernew/scanner //Linked scanner. For scanning.
var/list/pods //Linked cloning pods
var/temp = "Inactive"
var/scantemp_ckey
var/scantemp = "Ready to Scan"
var/menu = 1 //Which menu screen to display
+ var/list/records = list()
var/datum/data/record/active_record = null
var/obj/item/disk/data/diskette = null //Mostly so the geneticist can steal everything.
+
var/loading = 0 // Nice loading text
var/autoprocess = 0
- var/list/records = list()
light_color = LIGHT_COLOR_BLUE
/obj/machinery/computer/cloning/Initialize()
. = ..()
updatemodules(TRUE)
- var/obj/item/circuitboard/computer/cloning/board = circuit
- records = board.records
/obj/machinery/computer/cloning/Destroy()
@@ -39,7 +38,7 @@
if(pods)
for(var/P in pods)
var/obj/machinery/clonepod/pod = P
- if(pod.occupant && pod.clonemind == mind)
+ if(pod.occupant && mind && pod.clonemind == mind)
return null
if(pod.is_operational() && !(pod.occupant || pod.mess))
return pod
@@ -60,6 +59,9 @@
else if(!. && pod.is_operational() && !(pod.occupant || pod.mess) && pod.efficiency > 5)
. = pod
+/proc/grow_clone_from_record(obj/machinery/clonepod/pod, datum/data/record/R, empty)
+ return pod.growclone(R.fields["name"], R.fields["UI"], R.fields["SE"], R.fields["mindref"], R.fields["last_death"], R.fields["blood_type"], R.fields["mrace"], R.fields["features"], R.fields["factions"], R.fields["quirks"], empty)
+
/obj/machinery/computer/cloning/process()
if(!(scanner && LAZYLEN(pods) && autoprocess))
return
@@ -68,16 +70,19 @@
scan_occupant(scanner.occupant)
for(var/datum/data/record/R in records)
- var/obj/machinery/clonepod/pod = GetAvailableEfficientPod(R.fields["mind"])
+ var/obj/machinery/clonepod/pod = GetAvailableEfficientPod(R.fields["mindref"])
if(!pod)
return
if(pod.occupant)
- continue //how though?
+ continue
- if(pod.growclone(R.fields["ckey"], R.fields["name"], R.fields["UI"], R.fields["SE"], R.fields["mind"], R.fields["mrace"], R.fields["features"], R.fields["factions"], R.fields["quirks"]))
+ var/result = grow_clone_from_record(pod, R)
+ if(result & CLONING_SUCCESS)
temp = "[R.fields["name"]] => Cloning cycle in progress..."
+ log_cloning("Cloning of [key_name(R.fields["mindref"])] automatically started via autoprocess - [src] at [AREACOORD(src)]. Pod: [pod] at [AREACOORD(pod)].")
+ if(result & CLONING_DELETE_RECORD)
records -= R
/obj/machinery/computer/cloning/proc/updatemodules(findfirstcloner)
@@ -132,7 +137,9 @@
to_chat(user, "You insert [W].")
playsound(src, 'sound/machines/terminal_insert_disc.ogg', 50, 0)
src.updateUsrDialog()
- else if(istype(W, /obj/item/multitool))
+ else if(W.tool_behaviour == TOOL_MULTITOOL)
+ if(!multitool_check_buffer(user, W))
+ return
var/obj/item/multitool/P = W
if(istype(P.buffer, /obj/machinery/clonepod))
@@ -201,6 +208,7 @@
if(scanner_occupant)
dat += "Start Scan"
+ dat += "Body-Only Scan"
dat += "
[src.scanner.locked ? "Unlock Scanner" : "Lock Scanner"]"
else
dat += "Start Scan"
@@ -228,8 +236,11 @@
if (!src.active_record)
dat += "Record not found."
else
- dat += "