Forensics Datum (#17796)

* forensics datum initial work

* typo fix

* glove recursion fix

* missing bloodtype on organ data

* removed frustrating and unneeded b_type var from humans

* no leaky

* listcheck

* documenting

* documentation
This commit is contained in:
Will
2025-06-05 23:30:11 -07:00
committed by GitHub
parent be6680da62
commit ae718f07e3
62 changed files with 603 additions and 400 deletions
+108 -28
View File
@@ -8,37 +8,117 @@ var/const/FINGERPRINT_COMPLETE = 6
/proc/is_complete_print(var/print)
return stringpercent(print) <= FINGERPRINT_COMPLETE
/atom/var/list/suit_fibers
/// Forensics: Returns the object's forensic information datum. If none exists, it makes it.
/atom/proc/init_forensic_data()
RETURN_TYPE(/datum/forensics_crime)
SHOULD_NOT_OVERRIDE(TRUE)
if(!forensic_data)
forensic_data = new()
return forensic_data
/atom/proc/add_fibers(mob/living/carbon/human/M)
if(M.gloves && istype(M.gloves,/obj/item/clothing/gloves))
var/obj/item/clothing/gloves/G = M.gloves
if(G.transfer_blood) //bloodied gloves transfer blood to touched objects
if(add_blood(G.bloody_hands_mob)) //only reduces the bloodiness of our gloves if the item wasn't already bloody
/// Forensics: Handles most forensic investigation actions while touching an object. Including fingerprints, stray fibers from clothing, and bloody hands smearing objects. Returns true if a fingerprint was made.
/atom/proc/add_fingerprint(mob/living/M as mob, ignoregloves = FALSE)
SHOULD_NOT_OVERRIDE(TRUE)
if(isnull(M) || isAI(M) || isnull(M.key))
return FALSE
//Fibers from worn clothing get transfered along with fingerprints~
var/datum/forensics_crime/C = init_forensic_data()
C.add_fibres(M)
// bloodied gloves and hands transfer blood to touched objects. Blood does not transfer if we are already bloody.
if(!forensic_data?.has_blooddna())
var/mob/living/carbon/human/H = M
if(ishuman(M) && H.gloves && istype(H.gloves,/obj/item/clothing/gloves))
var/obj/item/clothing/gloves/G = H.gloves
if(G.transfer_blood)
forensic_data.merge_blooddna(G.forensic_data)
G.transfer_blood--
else if(M.bloody_hands)
if(add_blood(M.bloody_hands_mob))
else if(M.bloody_hands)
forensic_data.merge_blooddna(M.forensic_data)
M.bloody_hands--
if(!suit_fibers) suit_fibers = list()
var/fibertext
var/item_multiplier = istype(src,/obj/item)?1.2:1
var/suit_coverage = 0
if(M.wear_suit)
fibertext = "Material from \a [M.wear_suit]."
if(prob(10*item_multiplier) && !(fibertext in suit_fibers))
suit_fibers += fibertext
suit_coverage = M.wear_suit.body_parts_covered
//He has no prints!
if(mFingerprints in M.mutations)
if(C.get_lastprint() != M.key)
C.add_hiddenprints(M)
C.set_lastprint(M.key)
return FALSE
if(M.w_uniform && (M.w_uniform.body_parts_covered & ~suit_coverage))
fibertext = "Fibers from \a [M.w_uniform]."
if(prob(15*item_multiplier) && !(fibertext in suit_fibers))
suit_fibers += fibertext
//Smudge up dem prints some if it's just a mob
if(!ishuman(M))
if(C.get_lastprint() != M.key)
C.add_hiddenprints(M)
C.set_lastprint(M.key)
return TRUE
var/mob/living/carbon/human/H = M
//Now, deal with gloves.
if (H.gloves && H.gloves != src)
C.add_hiddenprints(M)
H.gloves.add_fingerprint(M,ignoregloves)
//Deal with gloves the pass finger/palm prints.
if(!ignoregloves)
if(H.gloves && H.gloves != src)
if(istype(H.gloves, /obj/item/clothing/gloves))
var/obj/item/clothing/gloves/G = H.gloves
if(!prob(G.fingerprint_chance))
return 0
return C.add_prints(H)
/// Forensics: Adds an admin investigation fingerprint, even if no actual fingerprints are made. Used even if the action is done with a weapon as a way of logging actions for admins.
/atom/proc/add_hiddenprint(mob/living/M as mob)
SHOULD_NOT_OVERRIDE(TRUE)
if(isnull(M))
return
if(isnull(M.key))
return
init_forensic_data().add_hiddenprints(M)
/// Forensics: Adds blood dna to an object, this also usually gives the object a bloody overlay, but that is handled by the object itself. Returns true if this is the first time this dna is being added to this object.
/atom/proc/add_blooddna(var/datum/dna/dna_data,var/mob/M)
SHOULD_NOT_OVERRIDE(TRUE)
return init_forensic_data().add_blooddna(dna_data,M)
/// Forensics: Adds blood dna to an object, this version uses an organ's more restricted dna datum, but it still has all the information needed. Returns true if this is the first time this dna is being added to this object.
/atom/proc/add_blooddna_organ(var/datum/organ_data/dna_data)
SHOULD_NOT_OVERRIDE(TRUE)
return init_forensic_data().add_blooddna_organ(dna_data)
/// Forensics: Adds fibres from suits or gloves
/atom/proc/add_fibres(mob/living/carbon/human/M)
SHOULD_NOT_OVERRIDE(TRUE)
init_forensic_data().add_fibres(M)
/// Forensics: Transfers both our normal and hidden fingerprints to the specified object, handles the forensics datum creation itself.
/atom/proc/transfer_fingerprints_to(var/atom/transfer_to)
SHOULD_NOT_OVERRIDE(TRUE)
if(!forensic_data)
return
var/datum/forensics_crime/C = transfer_to.init_forensic_data()
C.merge_prints(forensic_data)
C.merge_hiddenprints(forensic_data)
/// Forensics: Transfers our blood dna to the specified object, handles the forensics datum creation itself.
/atom/proc/transfer_blooddna_to(var/atom/transfer_to)
if(!forensic_data)
return
transfer_to.init_forensic_data().merge_blooddna(forensic_data)
/// Forensics: Transfers our stray fibers to the specified object, handles the forensics datum creation itself.
/atom/proc/transfer_fibres_to(var/atom/transfer_to)
SHOULD_NOT_OVERRIDE(TRUE)
if(!forensic_data)
return
transfer_to.init_forensic_data().merge_fibres(forensic_data)
/// Forensics: Adds gunshot residue from firing boolets
/atom/proc/add_gunshotresidue(var/obj/item/ammo_casing/shell)
init_forensic_data().add_gunshotresidue(shell.caliber)
if(M.gloves && (M.gloves.body_parts_covered & ~suit_coverage))
fibertext = "Material from a pair of [M.gloves.name]."
if(prob(20*item_multiplier) && !(fibertext in suit_fibers))
suit_fibers += "Material from a pair of [M.gloves.name]."
/datum/data/record/forensic
name = "forensic data"
@@ -48,9 +128,9 @@ var/const/FINGERPRINT_COMPLETE = 6
uid = "\ref [A]"
fields["name"] = sanitize(A.name)
fields["area"] = sanitize("[get_area(A)]")
fields["fprints"] = A.fingerprints ? A.fingerprints.Copy() : list()
fields["fibers"] = A.suit_fibers ? A.suit_fibers.Copy() : list()
fields["blood"] = A.blood_DNA ? A.blood_DNA.Copy() : list()
fields["fprints"] = A.forensic_data?.get_prints().Copy()
fields["fibers"] = A.forensic_data?.get_fibres().Copy()
fields["blood"] = A.forensic_data?.get_blooddna().Copy()
fields["time"] = world.time
/datum/data/record/forensic/proc/merge(var/datum/data/record/other)
-2
View File
@@ -1,6 +1,5 @@
/mob
var/bloody_hands = 0
var/mob/living/carbon/human/bloody_hands_mob
var/track_blood = 0
var/list/feet_blood_DNA
var/track_blood_type
@@ -8,7 +7,6 @@
/obj/item/clothing/gloves
var/transfer_blood = 0
var/mob/living/carbon/human/bloody_hands_mob
/obj/item/clothing/shoes/
var/track_blood = 0
+12 -10
View File
@@ -6,7 +6,7 @@
/obj/item/sample/Initialize(mapload, var/atom/supplied)
. = ..()
if(supplied)
if(supplied && supplied.forensic_data)
copy_evidence(supplied)
name = "[initial(name)] (\the [supplied])"
@@ -16,9 +16,10 @@
icon_state = "fingerprint1"
/obj/item/sample/proc/copy_evidence(var/atom/supplied)
if(supplied.suit_fibers && supplied.suit_fibers.len)
evidence = supplied.suit_fibers.Copy()
supplied.suit_fibers.Cut()
var/list/fibre_data = supplied.forensic_data.get_fibres()
if(fibre_data && fibre_data.len)
evidence = fibre_data.Copy()
supplied.forensic_data.clear_fibres()
/obj/item/sample/proc/merge_evidence(var/obj/item/sample/supplied, var/mob/user)
if(!supplied.evidence || !supplied.evidence.len)
@@ -116,10 +117,11 @@
return 0
/obj/item/sample/print/copy_evidence(var/atom/supplied)
if(supplied.fingerprints && supplied.fingerprints.len)
for(var/print in supplied.fingerprints)
evidence[print] = supplied.fingerprints[print]
supplied.fingerprints.Cut()
var/list/print_data = supplied.forensic_data.get_prints()
if(print_data && print_data.len)
for(var/print in print_data)
evidence[print] = print_data[print]
supplied.forensic_data.clear_prints()
/obj/item/forensics/sample_kit
name = "fiber collection kit"
@@ -130,7 +132,7 @@
var/evidence_path = /obj/item/sample/fibers
/obj/item/forensics/sample_kit/proc/can_take_sample(var/mob/user, var/atom/supplied)
return (supplied.suit_fibers && supplied.suit_fibers.len)
return supplied.forensic_data?.has_fibres()
/obj/item/forensics/sample_kit/proc/take_sample(var/mob/user, var/atom/supplied)
var/obj/item/sample/S = new evidence_path(get_turf(user), supplied)
@@ -155,4 +157,4 @@
evidence_path = /obj/item/sample/print
/obj/item/forensics/sample_kit/powder/can_take_sample(var/mob/user, var/atom/supplied)
return (supplied.fingerprints && supplied.fingerprints.len)
return supplied.forensic_data?.has_prints()
+16 -13
View File
@@ -34,11 +34,12 @@
to_chat(user, span_notice("Done printing."))
// to_chat(user, span_notice("[M]'s Fingerprints: [md5(M.dna.uni_identity)]"))
if(reveal_blood && M.blood_DNA && M.blood_DNA.len)
if(reveal_blood && M.forensic_data?.has_blooddna())
to_chat(user, span_notice("Blood found on [M]. Analysing..."))
spawn(15)
for(var/blood in M.blood_DNA)
to_chat(user, span_notice("Blood type: [M.blood_DNA[blood]]\nDNA: [blood]"))
var/list/blooddna = M.forensic_data.get_blooddna()
for(var/blood in blooddna)
to_chat(user, span_notice("Blood type: [blooddna[blood]]\nDNA: [blood]"))
return
/obj/item/detective_scanner/afterattack(atom/A as obj|turf, mob/user, proximity)
@@ -67,7 +68,7 @@
return 0
//General
if ((!A.fingerprints || !A.fingerprints.len) && !A.suit_fibers && !A.blood_DNA)
if (!A.forensic_data?.has_prints() && !A.forensic_data?.has_fibres() && !A.forensic_data.has_blooddna())
user.visible_message("\The [user] scans \the [A] with \a [src], the air around [user.gender == MALE ? "him" : "her"] humming[prob(70) ? " gently." : "."]" ,\
span_warning("Unable to locate any fingerprints, materials, fibers, or blood on [A]!"),\
"You hear a faint hum of electrical equipment.")
@@ -80,14 +81,15 @@
return
//PRINTS
if(A.fingerprints && A.fingerprints.len)
to_chat(user, span_notice("Isolated [A.fingerprints.len] fingerprints:"))
if(A.forensic_data?.has_prints())
to_chat(user, span_notice("Isolated [A.forensic_data?.get_prints().len] fingerprints:"))
if(!reveal_incompletes)
to_chat(user, span_warning("Rapid Analysis Imperfect: Scan samples with H.R.F.S. equipment to determine nature of incomplete prints."))
var/list/complete_prints = list()
var/list/incomplete_prints = list()
for(var/i in A.fingerprints)
var/print = A.fingerprints[i]
var/list/print_data = A.forensic_data?.get_prints()
for(var/i in print_data)
var/print = print_data[i]
if(stringpercent(print) <= FINGERPRINT_COMPLETE)
complete_prints += print
else
@@ -107,21 +109,22 @@
//FIBERS
if(A.suit_fibers && A.suit_fibers.len)
if(A.forensic_data?.has_fibres())
to_chat(user,span_notice("Fibers/Materials detected.[reveal_fibers ? " Analysing..." : " Acquisition of fibers for H.R.F.S. analysis advised."]"))
flick("[icon_state]1",src)
if(reveal_fibers && do_after(user, 5 SECONDS))
to_chat(user, span_notice("Apparel samples scanned:"))
for(var/sample in A.suit_fibers)
for(var/sample in A.forensic_data.get_fibres())
to_chat(user, " - " + span_notice("[sample]"))
//Blood
if (A.blood_DNA && A.blood_DNA.len)
if (A.forensic_data?.has_blooddna())
to_chat(user, span_notice("Blood detected.[reveal_blood ? " Analysing..." : " Acquisition of swab for H.R.F.S. analysis advised."]"))
if(reveal_blood && do_after(user, 5 SECONDS))
flick("[icon_state]1",src)
for(var/blood in A.blood_DNA)
to_chat(user, "Blood type: " + span_warning("[A.blood_DNA[blood]]") + " DNA: " + span_warning("[blood]"))
var/list/blood_data = A.forensic_data.get_blooddna()
for(var/blood in blood_data)
to_chat(user, "Blood type: " + span_warning("[blood_data[blood]]") + " DNA: " + span_warning("[blood]"))
user.visible_message("\The [user] scans \the [A] with \a [src], the air around [user.gender == MALE ? "him" : "her"] humming[prob(70) ? " gently." : "."]" ,\
span_notice("You finish scanning \the [A]."),\
+6 -6
View File
@@ -59,7 +59,7 @@
return
user.visible_message("[user] swabs [H]'s palm for a sample.")
sample_type = "GSR"
gsr = H.gunshot_residue
gsr = H.forensic_data?.get_gunshotresidue()
else
return
@@ -80,7 +80,7 @@
add_fingerprint(user)
var/list/choices = list()
if(A.blood_DNA)
if(A.forensic_data?.has_blooddna())
choices |= "Blood"
if(istype(A, /obj/item/clothing))
choices |= "Gunshot Residue"
@@ -99,16 +99,16 @@
var/sample_type
if(choice == "Blood")
if(!A.blood_DNA || !A.blood_DNA.len) return
dna = A.blood_DNA.Copy()
if(!A.forensic_data?.has_blooddna()) return
dna = A.forensic_data?.get_blooddna().Copy()
sample_type = "blood"
else if(choice == "Gunshot Residue")
var/obj/item/clothing/B = A
if(!istype(B) || !B.gunshot_residue)
if(!istype(B) || !B.forensic_data?.get_gunshotresidue())
to_chat(user, span_warning("There is no residue on \the [A]."))
return
gsr = B.gunshot_residue
gsr = B.forensic_data?.get_gunshotresidue()
sample_type = "residue"
if(sample_type)