The Case Dossier program (#22703)

This adds the case dossier program. A program intended for investigators
to use for their cases, with the ability to record evidence, photos,
documents, people and their notes on a case. The rest of security is
able to view cases in the program, but not edit them.
Additionally, the forensic skill was added to evidence bags and photos,
allowing a person to add the time, place and the person using them,
mainly intended to be used with the case dossier program.
Fibers and prints can now be gathered from objects inside evidence bags,
to avoid contamination.
Also fixed some bugs with taking photos of certain objects, such as
airlocks.


https://github.com/user-attachments/assets/92a880e9-9032-46d4-88c0-43d45b64f979
This commit is contained in:
Casper3667
2026-06-19 04:36:07 +00:00
committed by GitHub
parent 7d6d45fae1
commit 7a99813dd0
20 changed files with 3671 additions and 24 deletions
+99 -10
View File
@@ -7,8 +7,16 @@
icon_state = "evidenceobj"
item_state = ""
w_class = WEIGHT_CLASS_SMALL
var/obj/item/stored_item = null
/// The item stored in the evidence bag
var/obj/item/stored_item
/// The label on the evidence bag
var/label_text = ""
/// Who collected the item
var/collected_by = ""
/// Where the item was collected
var/collected_location = ""
/// WHen the item was collected
var/collected_time = ""
/obj/item/evidencebag/mechanics_hints(mob/user, distance, is_adjacent)
. += ..()
@@ -65,9 +73,9 @@
human_user.visible_message("<b>[human_user]</b> puts \the [I] into \the [src].", SPAN_NOTICE("You put \the [I] inside \the [src]."),\
"You hear a rustle as someone puts something into a plastic bag.")
store_item(I)
store_item(I, user)
/obj/item/evidencebag/proc/store_item(obj/item/I)
/obj/item/evidencebag/proc/store_item(obj/item/I, mob/user)
icon_state = "evidence"
var/mutable_appearance/MA = new(I)
MA.pixel_x = 0
@@ -75,6 +83,17 @@
MA.layer = FLOAT_LAYER
AddOverlays(list(MA, "evidence"))
var/forensic = GET_SKILL_LEVEL(user, FORENSICS_SKILL_COMPONENT)
forensic = forensic ? forensic : SKILL_LEVEL_TRAINED
// Trained people would know to add this data
if(forensic >= SKILL_LEVEL_FAMILIAR)
collected_location = get_area_display_name(get_area(I))
collected_by = user.name
collected_time = worldtime2text()
// And untrained people risk contaminating it
else if (prob(25))
I.add_fingerprint(user)
desc = "A plastic bag containing [I]."
I.forceMove(src)
stored_item = I
@@ -99,21 +118,60 @@
icon_state = "evidenceobj"
return
/obj/item/evidencebag/feedback_hints(mob/user, distance, is_adjacent)
. = ..()
if(label_text)
. += SPAN_NOTICE("It is labelled: \"[label_text]\".")
if(collected_by)
. += SPAN_NOTICE("Collected by: [collected_by].")
if(collected_location)
. += SPAN_NOTICE("Collected from: [collected_location].")
if(collected_time)
. += SPAN_NOTICE("Collected at: [collected_time].")
/obj/item/evidencebag/examine(mob/user, distance, is_adjacent, infix, suffix, show_extended)
. = ..()
if (stored_item)
if(stored_item)
examinate(user, stored_item, show_extended)
/obj/item/evidencebag/attackby(obj/item/attacking_item, mob/user)
if(attacking_item.tool_behaviour == TOOL_PEN || istype(attacking_item, /obj/item/flashlight/pen))
var/tmp_label = sanitizeSafe( tgui_input_text(user, "Enter a label for [name]", "Label", label_text, MAX_NAME_LEN), MAX_NAME_LEN )
if(length(tmp_label) > MAX_NAME_LEN)
to_chat(user, SPAN_NOTICE("The label can be at most [MAX_NAME_LEN] characters long."))
var/static/list/evidence_label_fields = list(
"Label text",
"Collected by",
"Collected location",
"Collected time"
)
var/field = tgui_input_list(user, "Which evidence label field do you want to edit?", "Evidence Label", evidence_label_fields)
if(!field)
return
var/current_value = get_evidence_label_value(field)
var/new_value = tgui_input_text(
user,
"Enter [field] for [name]",
"Evidence Label",
current_value,
MAX_NAME_LEN
)
if(isnull(new_value))
return
set_evidence_label_value(field, new_value)
if(new_value == "")
to_chat(user, SPAN_NOTICE("You clear [field]."))
else
to_chat(user, SPAN_NOTICE("You set the label to \"[tmp_label]\"."))
label_text = tmp_label
update_name_label()
to_chat(user, SPAN_NOTICE("You set [field] to \"[new_value]\"."))
return
. = ..()
/obj/item/evidencebag/proc/update_name_label(var/base_name = initial(name))
@@ -122,3 +180,34 @@
name = base_name
else
name = "[base_name] ([label_text])"
/obj/item/evidencebag/proc/get_evidence_label_value(var/field)
switch(field)
if("Label text")
return label_text
if("Collected by")
return collected_by
if("Collected location")
return collected_location
if("Collected time")
return collected_time
return ""
/obj/item/evidencebag/proc/set_evidence_label_value(var/field, var/value)
switch(field)
if("Label text")
label_text = value
update_name_label()
return TRUE
if("Collected by")
collected_by = value
return TRUE
if("Collected location")
collected_location = value
return TRUE
if("Collected time")
collected_time = value
return TRUE
return FALSE
@@ -175,6 +175,7 @@
/obj/item/forensics/sample_kit/mechanics_hints(mob/user, distance, is_adjacent)
. += ..()
. += "Click drag it onto an object to collect fiber evidence. Alternatively, click on an object with non-Help intent."
. += "Click an evidence bag to collect evidence from the object within it. Alternatively, using harm intent checks the evidence bag."
/obj/item/forensics/sample_kit/proc/can_take_sample(var/mob/user, var/atom/supplied)
return (supplied.suit_fibers && supplied.suit_fibers.len)
@@ -187,6 +188,13 @@
if(!proximity)
return
add_fingerprint(user)
// We can sample items from within an evidence bag
if(user.a_intent != I_HURT && istype(A, /obj/item/evidencebag))
var/obj/item/evidencebag/bag = A
if(bag.stored_item)
A = bag.stored_item
if(can_take_sample(user, A))
take_sample(user,A)
return 1
@@ -78,5 +78,6 @@ ABSTRACT_TYPE(/datum/modular_computer_app_presets)
/datum/computer_file/program/camera_monitor,\
/datum/computer_file/program/digitalwarrant,\
/datum/computer_file/program/records/security,\
/datum/computer_file/program/case_dossier,\
/datum/computer_file/program/guntracker,\
)
@@ -0,0 +1,299 @@
#define STATUS_OPEN "Open"
#define STATUS_SUBMITTED "Submitted"
#define STATUS_ARCHIVED "Archived"
/datum/computer_file/program/case_dossier
filename = "case_dossier"
filedesc = "Case Dossier"
extended_desc = "Official NTsec program for the handling of investigation cases."
program_icon_state = "security_record"
program_key_icon_state = "yellow_key"
color = LIGHT_COLOR_RED
size = 6
requires_ntnet = TRUE
available_on_ntnet = TRUE
// Security officers and wardens are allowed to download the program, but not edit it
required_access_download = ACCESS_SECURITY
required_access_run = ACCESS_SECURITY
usage_flags = PROGRAM_ALL_REGULAR | PROGRAM_STATIONBOUND
tgui_id = "Casedossier"
var/list/stored_cases
var/datum/investigation_case/open_case
/datum/computer_file/program/case_dossier/New(obj/item/modular_computer/comp)
. = ..()
stored_cases = GLOB.case_dossier_cases
/datum/computer_file/program/case_dossier/Destroy()
stored_cases.Cut()
open_case = null
return ..()
/// Who is permitted to edit the investigation files. Defaults to forensic staff
/datum/computer_file/program/case_dossier/proc/can_edit(mob/user)
if(!user)
return FALSE
var/obj/item/card/id/I = user.GetIdCard()
if(!I)
return FALSE
return (ACCESS_FORENSICS_LOCKERS in I.access)
/datum/computer_file/program/case_dossier/ui_data(mob/user)
var/list/data = list()
data["available_statuses"] = list(STATUS_OPEN, STATUS_SUBMITTED, STATUS_ARCHIVED)
data["can_edit"] = can_edit(user)
data["open_case"] = null
data["cases"] = list()
if(open_case)
open_case.send_photo_resources(user)
data["open_case"] = open_case.tgui_data()
if(stored_cases)
for(var/datum/investigation_case/C in stored_cases)
data["cases"] += list(C.tgui_data(TRUE))
return data
/datum/investigation_case/proc/tgui_data(var/include_contents = TRUE)
var/list/data = list()
data["id"] = "[case_id]"
data["title"] = title
data["status"] = status
data["investigator"] = investigator
data["created_at"] = created_at
data["updated_at"] = updated_at
data["tags"] = tags
data["victim_count"] = length(victims)
data["suspect_count"] = length(suspects)
data["witness_count"] = length(witnesses)
data["evidence_count"] = length(evidence_refs)
data["photo_count"] = length(photo_refs)
data["report_count"] = length(report_refs)
if(include_contents)
data["summary"] = summary
data["timeline"] = timeline
data["findings"] = findings
data["victims"] = person_list_tgui_data(victims)
data["suspects"] = person_list_tgui_data(suspects)
data["witnesses"] = person_list_tgui_data(witnesses)
data["evidence"] = evidence_list_tgui_data(evidence_refs)
data["photos"] = evidence_list_tgui_data(photo_refs)
data["reports"] = report_list_tgui_data(report_refs)
return data
/datum/computer_file/program/case_dossier/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
switch(action)
if("new_case")
create_new_case(usr)
return TRUE
if("open_case")
open_case_by_id(params["id"])
return TRUE
if("save_case")
save_open_case(usr)
return TRUE
if("edit_case_field")
if(open_case)
open_case.set_field(params["field"], params["value"])
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("edit_case_tags")
if(open_case)
open_case.set_tags_from_text(params["value"])
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("add_person")
if(open_case)
open_case.add_person(params["list"])
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("edit_person")
if(open_case)
open_case.edit_person(params["list"], params["id"], params["field"], params["value"])
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("remove_person")
if(open_case)
open_case.remove_person(params["list"], params["id"])
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("scan_evidence")
if(open_case)
open_case.scan_held_evidence(usr, usr.get_type_in_hands(/obj/item))
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("scan_photo")
if(open_case)
open_case.scan_held_photo(usr, usr.get_type_in_hands(/obj/item))
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("scan_report")
if(open_case)
open_case.scan_held_report(usr, usr.get_type_in_hands(/obj/item))
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("print_summary")
if(open_case)
open_case.print_summary(usr)
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("print_evidence_log")
if(open_case)
open_case.print_evidence_log(usr)
else
to_chat(usr, SPAN_NOTICE("No open case selected."))
return TRUE
if("set_case_status")
var/datum/investigation_case/C = find_case_by_id(params["id"])
if(C)
C.set_field("status", params["status"])
return TRUE
if("duplicate_case")
duplicate_case_by_id(params["id"], usr)
return TRUE
if("delete_case")
delete_case_by_id(params["id"])
return TRUE
if("add_manual_evidence")
if(open_case)
open_case.add_manual_evidence(usr)
return TRUE
if("edit_evidence")
if(open_case)
open_case.edit_evidence(params["id"], params["field"], params["value"])
return TRUE
if("remove_evidence")
if(open_case)
open_case.remove_evidence(params["id"])
return TRUE
if("edit_photo")
if(open_case)
open_case.edit_photo(params["id"], params["field"], params["value"])
return TRUE
if("remove_photo")
if(open_case)
open_case.remove_photo(params["id"])
return TRUE
if("remove_report")
if(open_case)
open_case.remove_report(params["id"])
return TRUE
/// Starts a new case
/datum/computer_file/program/case_dossier/proc/create_new_case(var/mob/user)
var/datum/investigation_case/C = new /datum/investigation_case(creator = user)
stored_cases += C
open_case = C
/// Finds a new case by its id
/datum/computer_file/program/case_dossier/proc/find_case_by_id(var/id)
if(!id)
return null
for(var/datum/investigation_case/C in stored_cases)
if("[C.case_id]" == "[id]")
return C
return null
/// Opens a case by its id
/datum/computer_file/program/case_dossier/proc/open_case_by_id(var/id)
var/datum/investigation_case/C = find_case_by_id(id)
if(!C)
return FALSE
open_case = C
return TRUE
/// Saves the open case
/// Right now it doesn't do much, as the program auto-saves.
/datum/computer_file/program/case_dossier/proc/save_open_case(var/mob/user)
if(!open_case)
return FALSE
open_case.touch()
if(user)
to_chat(user, SPAN_NOTICE("You save [open_case.title]."))
return TRUE
/datum/computer_file/program/case_dossier/proc/delete_case_by_id(var/id)
var/datum/investigation_case/C = find_case_by_id(id)
if(!C)
return FALSE
stored_cases -= C
if(open_case == C)
open_case = null
qdel(C)
return TRUE
/// Duplicates the case based on the id
/datum/computer_file/program/case_dossier/proc/duplicate_case_by_id(var/id, var/mob/user)
var/datum/investigation_case/C = find_case_by_id(id)
if(!C)
return FALSE
var/datum/investigation_case/N = C.copy_case()
N.title = "Copy of [C.title]"
N.created_by = user?.name
N.created_at = worldtime2text()
N.updated_at = N.created_at
stored_cases += N
open_case = N
return TRUE
#undef STATUS_OPEN
#undef STATUS_SUBMITTED
#undef STATUS_ARCHIVED
+38 -4
View File
@@ -26,7 +26,7 @@
/********
* photo *
********/
GLOBAL_VAR_INIT(photo_count, 0)
GLOBAL_VAR_INIT(photo_count, 1)
/obj/item/photo
name = "photo"
@@ -35,12 +35,26 @@ GLOBAL_VAR_INIT(photo_count, 0)
icon_state = "photo"
item_state = "paper"
w_class = WEIGHT_CLASS_TINY
var/picture_desc // Who and/or what's in the picture.
/// Who and/or what's in the picture.
var/picture_desc
/// The photo's id
var/id
var/icon/img //Big photo image
var/scribble //Scribble on the back.
/// Big photo image
var/icon/img
/// Scribble on the back.
var/scribble
/// The small size of the photo used for the icon
var/icon/tiny
/// The size of the photo
var/photo_size = 3
/// The time the photo was taken
var/time_taken
/// The place the photo was taken
var/location_taken
/// Who took the photo
var/taken_by
/// The photo's name/caption
var/caption
drop_sound = 'sound/items/drop/paper.ogg'
pickup_sound = 'sound/items/pickup/paper.ogg'
@@ -95,8 +109,10 @@ GLOBAL_VAR_INIT(photo_count, 0)
if(surface_atom == usr || surface_atom.Adjacent(usr))
if(n_name)
name = "[initial(name)] ([n_name])"
caption = n_name
else
name = initial(name)
caption = null
add_fingerprint(usr)
@@ -238,6 +254,12 @@ GLOBAL_VAR_INIT(photo_count, 0)
desc = "A one use - polaroid camera."
pictures_left = 30
// Detective cameras have build in data recording, so no need for a skill check
/obj/item/camera/detective/record_data(mob/living/user, obj/item/photo/pic)
pic.time_taken = worldtime2text()
pic.location_taken = get_area_display_name(get_area(user))
pic.taken_by = user.name
//Proc for capturing check
/mob/living/proc/can_capture_turf(turf/T)
return TRUE // DVIEW will do sanity checks, we've got no special checks.
@@ -288,9 +310,21 @@ GLOBAL_VAR_INIT(photo_count, 0)
p.pixel_x = rand(-10, 10)
p.pixel_y = rand(-10, 10)
p.photo_size = size
record_data(user, p)
return p
/// Data recorded on photos.
/obj/item/camera/proc/record_data(mob/living/user, obj/item/photo/pic)
var/forensic = GET_SKILL_LEVEL(user, FORENSICS_SKILL_COMPONENT)
forensic = forensic ? forensic : SKILL_LEVEL_TRAINED
// Trained people would know to add this data
if(forensic >= SKILL_LEVEL_FAMILIAR)
pic.time_taken = worldtime2text()
pic.location_taken = get_area_display_name(get_area(user))
pic.taken_by = user.name
/obj/item/camera/proc/printpicture(mob/user, obj/item/photo/p)
p.forceMove(user.loc)
if(!user.get_inactive_hand())