refactor detective scan categories, fix some categories not being shown at all, remove Syndicate scan category (#93138)

## About The Pull Request

Refactor detective scan categories.
Fix some categories not being shown at all.
Remove Syndicate scan category.

<details>
<summary>TGUI</summary>
<img width="514" height="545" alt="image"
src="https://github.com/user-attachments/assets/754ce576-545e-40e5-9fad-86f19d8d2606"
/>
</details>

<details>
<summary>Paper report</summary>
<img width="460" height="609" alt="image"
src="https://github.com/user-attachments/assets/e37e1965-e90a-4e0c-8109-f4384d6a4a79"
/>
</details>


## Why It's Good For The Game

DETSCAN_CATEGORY_SETTINGS, DETSCAN_CATEGORY_HOLY,
DETSCAN_CATEGORY_ILLEGAL and DETSCAN_CATEGORY_NOTES are now shown in
detective scanner ui and printed report.

Cleaner code

## Changelog

🆑
fix: energy dagger pen report will be now shown in detective scanner and
also all items that transform will have additional note about it
/🆑

---------

Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com>
This commit is contained in:
Gaxeer
2025-10-12 06:44:56 +03:00
committed by GitHub
parent 90b6067608
commit 7468a0fbcf
13 changed files with 524 additions and 235 deletions

View File

@@ -538,7 +538,7 @@
#define COMSIG_SPEED_POTION_APPLIED "speed_potion"
#define SPEED_POTION_STOP (1<<0)
/// from /obj/item/detective_scanner/scan(): (mob/user, list/extra_data)
/// from /obj/item/detective_scanner/scan(): (mob/user, datum/detective_scanner_log/entry)
#define COMSIG_DETECTIVE_SCANNED "det_scanned"
/// from /obj/plunger_act when an object is being plungered

View File

@@ -1,6 +1,9 @@
// CATEGORY HEADERS
/// New `/datum/detective_scan_category` must be created when adding new `DETSCAN_CATEGORY`.
/// Macros are sorted by `/datum/detective_scan_category::display_order`
/// Fingerpints detected
#define DETSCAN_CATEGORY_FINGERS "Prints"
/// Displays any bloodprints found and their uefi
@@ -8,37 +11,20 @@
/// Clothing and glove fibers
#define DETSCAN_CATEGORY_FIBER "Fibers"
/// Liquids detected
#define DETSCAN_CATEGORY_DRINK "Reagents"
#define DETSCAN_CATEGORY_REAGENTS "Reagents"
/// ID Access
#define DETSCAN_CATEGORY_ACCESS "ID Access"
// The categories below do not have hard rules on what info is displayed, and are for categorizing info thematically.
/// Generic extra information category
#define DETSCAN_CATEGORY_NOTES "Additional Notes"
/// Attributes that might be illegal, but don't have ties to syndicate/aren't exclusively produced by them
#define DETSCAN_CATEGORY_ILLEGAL "Illegal Tech"
/// The emags and other in-house technology from the syndicate
#define DETSCAN_CATEGORY_SYNDIE "Syndicate Tech"
/// praise be
#define DETSCAN_CATEGORY_HOLY "Holy Data"
/// The mode that the items in, what kind of item is dispensed, etc
#define DETSCAN_CATEGORY_SETTINGS "Active Settings"
// If your category is not in this list it WILL NOT BE DISPLAYED
/// defines the order categories are displayed, with the original categories, then custom ones, then finally the extra info.
#define DETSCAN_DEFAULT_ORDER(...) list(\
DETSCAN_CATEGORY_FINGERS, \
DETSCAN_CATEGORY_BLOOD, \
DETSCAN_CATEGORY_FIBER, \
DETSCAN_CATEGORY_DRINK, \
DETSCAN_CATEGORY_ACCESS, \
DETSCAN_CATEGORY_SETTINGS, \
DETSCAN_CATEGORY_HOLY, \
DETSCAN_CATEGORY_ILLEGAL, \
DETSCAN_CATEGORY_SYNDIE, \
DETSCAN_CATEGORY_NOTES, \
)
/// praise be
#define DETSCAN_CATEGORY_HOLY "Holy Data"
/// Attributes that might be illegal, can also have ties to syndicate
#define DETSCAN_CATEGORY_ILLEGAL "Illegal Tech"
/// Generic extra information category
#define DETSCAN_CATEGORY_NOTES "Additional Notes"
/// the order departments show up in for the id scan (its sorted by red to blue on the color wheel)
#define DETSCAN_ACCESS_ORDER(...) list(\

View File

@@ -114,10 +114,10 @@
/datum/component/transforming/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_ITEM_ATTACK_SELF, COMSIG_ITEM_SHARPEN_ACT, COMSIG_DETECTIVE_SCANNED))
/datum/component/transforming/proc/on_scan(datum/source, mob/user, list/extra_data)
/datum/component/transforming/proc/on_scan(datum/source, mob/user, datum/detective_scanner_log/entry)
SIGNAL_HANDLER
LAZYADD(extra_data[DETSCAN_CATEGORY_NOTES], "Readings suggest some form of state changing.")
entry.add_data_entry(DETSCAN_CATEGORY_NOTES, "Readings suggest some form of state changing.")
/*
* Called on [COMSIG_ITEM_ATTACK_SELF].

View File

@@ -0,0 +1,115 @@
GLOBAL_LIST_INIT_TYPED(detective_scan_categories, /datum/detective_scan_category, initialize_detective_scan_categories())
/proc/initialize_detective_scan_categories()
var/list/categories = list()
for(var/datum/detective_scan_category/category_path as anything in subtypesof(/datum/detective_scan_category))
var/datum/detective_scan_category/existing_category = categories[category_path::id]
if(!isnull(existing_category))
stack_trace("`[category_path]` has duplicate id - `[category_path::id]` of `[existing_category.type]`")
continue
categories[category_path::id] = new category_path
return categories
/datum/detective_scan_category
/// Category ID. Must be defined in `code/__DEFINES/security.dm`
var/id = "no id"
/// Name of scan category. Used in UIs and in paper
var/name = "no name"
/// Order the data with this category will be sorted by
var/display_order = 0
/// Fontawesome icon used in TGUI near this category data entry
var/ui_icon = "question"
/// Fontawesome icon color used in TGUI near this category data entry
var/ui_icon_color = "white"
/// Generates report data used in `/datum/detective_scanner_log/proc/generate_report_text()`
/datum/detective_scan_category/proc/generate_report_data(list/data)
SHOULD_NOT_OVERRIDE(TRUE)
var/list/report_text = list()
report_text += "<dt><b>[name]</b></dt><dd>"
for(var/entry in data)
report_text += format_report_entry(entry, data[entry])
report_text += "</dd>"
return report_text
/// Formats entered log data. Can be used to have unique formating per each category
/datum/detective_scan_category/proc/format_report_entry(entry, entry_associated_value)
return "[entry]<br>"
/datum/detective_scan_category/fingers
id = DETSCAN_CATEGORY_FINGERS
name = "Fingerprints"
display_order = 1
ui_icon = "fingerprint"
ui_icon_color = "yellow"
/datum/detective_scan_category/blood
id = DETSCAN_CATEGORY_BLOOD
name = "Blood DNA, Type"
display_order = 2
ui_icon = "droplet"
ui_icon_color = "red"
/datum/detective_scan_category/blood/format_report_entry(entry, entry_associated_value)
return "[entry], [entry_associated_value]<br>"
/datum/detective_scan_category/fiber
id = DETSCAN_CATEGORY_FIBER
name = "Fibers"
display_order = 3
ui_icon = "shirt"
ui_icon_color = "green"
/datum/detective_scan_category/drink
id = DETSCAN_CATEGORY_REAGENTS
name = "Reagents"
display_order = 4
ui_icon = "flask"
ui_icon_color = "blue"
/datum/detective_scan_category/drink/format_report_entry(entry, entry_associated_value)
return "<b>[entry]</b>: [entry_associated_value] u.<br>"
/datum/detective_scan_category/access
id = DETSCAN_CATEGORY_ACCESS
name = "ID Access"
display_order = 5
ui_icon = "id-card"
ui_icon_color = "blue"
/datum/detective_scan_category/access/format_report_entry(entry, entry_associated_value)
var/list/associated_value_list = entry_associated_value
return "<b>[entry]</b>: [associated_value_list]<br>"
/datum/detective_scan_category/setting
id = DETSCAN_CATEGORY_SETTINGS
name = "Active settings"
display_order = 6
ui_icon = "wrench"
ui_icon_color = "orange"
/datum/detective_scan_category/holy
id = DETSCAN_CATEGORY_HOLY
name = "Holy data"
display_order = 7
ui_icon = "book-bible"
ui_icon_color = "brown"
/datum/detective_scan_category/illegal
id = DETSCAN_CATEGORY_ILLEGAL
name = "Illegal tech"
display_order = 8
ui_icon = "handcuffs"
ui_icon_color = "red"
/datum/detective_scan_category/notes
id = DETSCAN_CATEGORY_NOTES
name = "Additional notes"
ui_icon = "clipboard"
ui_icon_color = "yellow"
display_order = 9

View File

@@ -0,0 +1,25 @@
/proc/cmp_detective_scanner_data_entry(datum/detective_scanner_data_entry/a, datum/detective_scanner_data_entry/b)
return cmp_numeric_asc(a.display_order, b.display_order)
/datum/detective_scanner_data_entry
/// Category this data entry is related to
var/category
/// Order this entry will be displayed in TGUI and paper report
var/display_order = 0
/// List of data for this entry. Displayed in UIs and paper report
var/list/data = list()
/datum/detective_scanner_data_entry/New(category, display_order, data)
src.category = category
src.display_order = display_order
if(!isnull(data))
src.data += data
/datum/detective_scanner_data_entry/ui_data(mob/user)
var/list/ui_data = list()
ui_data["category"] = category
ui_data["data"] = data
return ui_data
/datum/detective_scanner_data_entry/proc/add_data(data)
src.data += data

View File

@@ -0,0 +1,65 @@
/datum/detective_scanner_log
/// Name of the scanned atom
var/scan_target
/// Time the scan was performed at
var/scan_time
/// `data_entries` is an assoc list, which can't use `BINARY_INSERT`
/// And to not perform sorting pipeline every time new data is added,
/// this var will be utilized to only sort list when it's required
var/sorted = TRUE
/// Scan data for current log
var/list/data_entries = list()
/datum/detective_scanner_log/ui_data(mob/user)
var/list/ui_data = list()
ui_data["scanTarget"] = scan_target
ui_data["scanTime"] = scan_time
sort_data_entries()
var/list/data_entries_ui_data = list()
for(var/key,value in data_entries)
var/datum/detective_scanner_data_entry/entry = value
UNTYPED_LIST_ADD(data_entries_ui_data, entry.ui_data(user))
ui_data["dataEntries"] = data_entries_ui_data
return ui_data
/// Adds new data entry to `data_entries` or updates existing one
/// Entries will be not sorted after using it
/// Returns TRUE if `data_entries` can be unsorted
/datum/detective_scanner_log/proc/add_data_entry(scan_category_id, data)
var/datum/detective_scan_category/category = GLOB.detective_scan_categories[scan_category_id]
if(isnull(category))
stack_trace("scan_category_id - `[scan_category_id]` with no corresponding `/datum/detective_scan_category`")
category = GLOB.detective_scan_categories[DETSCAN_CATEGORY_NOTES]
var/datum/detective_scanner_data_entry/data_entry = data_entries[category.id]
if(!isnull(data_entry))
data_entry.add_data(data)
return
data_entries[scan_category_id] = new /datum/detective_scanner_data_entry(scan_category_id, category.display_order, data)
sorted = (length(data_entries) <= 1)
/// Sorts the `data_entries` list if it's considered not sorted
/datum/detective_scanner_log/proc/sort_data_entries()
if(!sorted)
sortTim(data_entries, GLOBAL_PROC_REF(cmp_detective_scanner_data_entry), TRUE)
sorted = TRUE
/// Return text that will be used in printed paper report
/// Called in `/obj/item/detective_scanner/proc/print_report()`
/datum/detective_scanner_log/proc/generate_report_text()
var/list/report_text = list()
report_text += "<h2>[capitalize(scan_target)] scan at [scan_time]</h2><dr>"
if(!length(data_entries))
report_text += "No forensic traces found."
else
sort_data_entries()
for(var/log_category,data_entry in data_entries)
var/datum/detective_scanner_data_entry/data_entry_datum = data_entry
report_text += GLOB.detective_scan_categories[log_category].generate_report_data(data_entry_datum.data)
report_text += "</dl><hr>"
return report_text

View File

@@ -44,49 +44,12 @@
var/frNum = ++forensicPrintCount
report_paper.name = "FR-[frNum] 'Forensic Record'"
var/list/report_text = list("<H1>Forensic Record - (FR-[frNum])</H1><HR>")
var/list/report_text = list("<h1>Forensic Record - (FR-[frNum])</h1><hr>")
for(var/list/log in log_data)
report_text += "<H2>[capitalize(log["scan_target"])] scan at [log["scan_time"]]</H2><DL>"
for(var/datum/detective_scanner_log/log_entry as anything in log_data)
report_text += log_entry.generate_report_text()
if(!log[DETSCAN_CATEGORY_FIBER] && !log[DETSCAN_CATEGORY_BLOOD] && !log[DETSCAN_CATEGORY_FINGERS] && !log[DETSCAN_CATEGORY_DRINK] && !log[DETSCAN_CATEGORY_ACCESS])
report_text += "No forensic traces found.<HR>"
continue
if(log[DETSCAN_CATEGORY_FIBER])
report_text += "<DT><B>[DETSCAN_CATEGORY_FIBER]</B></DT><DD>"
for(var/fibers in log[DETSCAN_CATEGORY_FIBER])
report_text += fibers + "<BR>"
report_text += "</DD>"
if(log[DETSCAN_CATEGORY_BLOOD])
report_text += "<DT><B>[DETSCAN_CATEGORY_BLOOD]</B></DT><DD>"
for(var/blood in log[DETSCAN_CATEGORY_BLOOD])
report_text += "[blood], [log[DETSCAN_CATEGORY_BLOOD][blood]]<BR>"
report_text += "</DD>"
if(log[DETSCAN_CATEGORY_FINGERS])
report_text += "<DT><B>[DETSCAN_CATEGORY_FINGERS]</B></DT><DD>"
for(var/fingers in log[DETSCAN_CATEGORY_FINGERS])
report_text += fingers + "<BR>"
report_text += "</DD>"
if(log[DETSCAN_CATEGORY_DRINK])
report_text += "<DT><B>[DETSCAN_CATEGORY_DRINK]</B></DT><DD>"
for(var/reagent in log[DETSCAN_CATEGORY_DRINK])
report_text += "<B>[reagent]</B>: [log[DETSCAN_CATEGORY_DRINK][reagent]] u.<BR>"
report_text += "</DD>"
if(log[DETSCAN_CATEGORY_ACCESS])
report_text += "<DT><B>[DETSCAN_CATEGORY_ACCESS]</B></DT><DD>"
for(var/region in log[DETSCAN_CATEGORY_ACCESS])
var/list/access_list = log[DETSCAN_CATEGORY_ACCESS][region]
report_text += "<B>[region]</B>: [access_list.Join(", ")]<BR>"
report_text += "</DD>"
report_text += "</DL><HR>"
report_text += "<H1>Notes:</H1><BR>"
report_text += "<h1>Notes:</h1><br>"
report_paper.add_raw_text(report_text.Join())
report_paper.update_appearance()
@@ -149,35 +112,38 @@
// GATHER INFORMATION
var/list/log_entry_data = list()
var/datum/detective_scanner_log/log_entry = new
// Start gathering
log_entry_data["scan_target"] = scanned_atom.name
log_entry_data["scan_time"] = station_time_timestamp()
log_entry.scan_target = scanned_atom.name
log_entry.scan_time = station_time_timestamp()
var/list/atom_fibers = GET_ATOM_FIBRES(scanned_atom)
if(length(atom_fibers))
log_entry_data[DETSCAN_CATEGORY_FIBER] = atom_fibers.Copy()
log_entry.add_data_entry(DETSCAN_CATEGORY_FIBER, atom_fibers.Copy())
var/list/blood = GET_ATOM_BLOOD_DNA(scanned_atom)
if(length(blood))
log_entry_data[DETSCAN_CATEGORY_BLOOD] = blood.Copy()
log_entry.add_data_entry(DETSCAN_CATEGORY_BLOOD, blood.Copy())
if(ishuman(scanned_atom))
var/mob/living/carbon/human/scanned_human = scanned_atom
if(!scanned_human.gloves)
LAZYADD(log_entry_data[DETSCAN_CATEGORY_FINGERS], md5(scanned_human.dna?.unique_identity))
log_entry.add_data_entry(
DETSCAN_CATEGORY_FINGERS,
rustg_hash_string(RUSTG_HASH_MD5, scanned_human.dna?.unique_identity)
)
else if(!ismob(scanned_atom))
var/list/atom_fingerprints = GET_ATOM_FINGERPRINTS(scanned_atom)
if(length(atom_fingerprints))
log_entry_data[DETSCAN_CATEGORY_FINGERS] = atom_fingerprints.Copy()
log_entry.add_data_entry(DETSCAN_CATEGORY_FINGERS, atom_fingerprints.Copy())
// Only get reagents from non-mobs.
for(var/datum/reagent/present_reagent as anything in scanned_atom.reagents?.reagent_list)
LAZYADD(log_entry_data[DETSCAN_CATEGORY_DRINK], list(present_reagent.name = present_reagent.volume))
log_entry.add_data_entry(DETSCAN_CATEGORY_REAGENTS, list(present_reagent.name = present_reagent.volume))
// Get blood data from the blood reagent.
if(!istype(present_reagent, /datum/reagent/blood))
@@ -188,10 +154,7 @@
if(!blood_DNA || !blood_type)
continue
// Add to our copied blood list instead of the original
if(!log_entry_data[DETSCAN_CATEGORY_BLOOD])
log_entry_data[DETSCAN_CATEGORY_BLOOD] = list()
LAZYSET(log_entry_data[DETSCAN_CATEGORY_BLOOD], blood_DNA, blood_type)
log_entry.add_data_entry(DETSCAN_CATEGORY_BLOOD, list(blood_DNA = blood_type))
if(istype(scanned_atom, /obj/item/card/id))
var/obj/item/card/id/user_id = scanned_atom
@@ -202,14 +165,17 @@
var/list/access_names = list()
for(var/access_num in access_in_region)
access_names += SSid_access.get_access_desc(access_num)
LAZYADD(log_entry_data[DETSCAN_CATEGORY_ACCESS], region)
LAZYADD(log_entry_data[DETSCAN_CATEGORY_ACCESS][region], english_list(access_names))
log_entry.add_data_entry(DETSCAN_CATEGORY_ACCESS, list("[region]" = english_list(access_names)))
// sends it off to be modified by the items
SEND_SIGNAL(scanned_atom, COMSIG_DETECTIVE_SCANNED, user, log_entry_data)
SEND_SIGNAL(scanned_atom, COMSIG_DETECTIVE_SCANNED, user, log_entry)
// Perform sorting now, because probably this will be never modified
log_entry.sort_data_entries()
stoplag(3 SECONDS)
log_data += list(log_entry_data)
log_data += log_entry
return TRUE
/obj/item/detective_scanner/click_alt(mob/living/user)
@@ -217,7 +183,7 @@
/obj/item/detective_scanner/examine(mob/user)
. = ..()
if(LAZYLEN(log_data) && !scanner_busy)
if(length(log_data) && !scanner_busy)
. += span_notice("Alt-click to clear scanner logs.")
@@ -228,8 +194,28 @@
ui.open()
/obj/item/detective_scanner/ui_data(mob/user)
var/list/logs = list()
for(var/datum/detective_scanner_log/log as anything in log_data)
UNTYPED_LIST_ADD(logs, log.ui_data(user))
var/list/data = list()
data["log_data"] = log_data
data["logs"] = logs
return data
/obj/item/detective_scanner/ui_static_data(mob/user)
var/list/categories = list()
for(var/key,value in GLOB.detective_scan_categories)
var/datum/detective_scan_category/category = value
var/list/category_data = list()
category_data["name"] = category.name
category_data["uiIcon"] = category.ui_icon
category_data["uiIconColor"] = category.ui_icon_color
categories[category.id] = category_data
var/list/data = list()
data["categories"] = categories
return data
/obj/item/detective_scanner/ui_act(action, params, datum/tgui/ui)
@@ -251,7 +237,7 @@
balloon_alert(ui.user, "log deleted")
ui.send_update()
if("print")
if(!LAZYLEN(log_data))
if(!length(log_data))
balloon_alert(ui.user, "no logs!")
return
if(scanner_busy)
@@ -263,7 +249,7 @@
addtimer(CALLBACK(src, PROC_REF(safe_print_report)), 3 SECONDS)
/obj/item/detective_scanner/proc/clear_logs(mob/living/user)
if(!LAZYLEN(log_data))
if(!length(log_data))
balloon_alert(user, "no logs!")
return CLICK_ACTION_BLOCKING
if(scanner_busy)

View File

@@ -445,9 +445,10 @@
/datum/embedding/edagger_active
embed_chance = 100
/obj/item/pen/edagger/proc/on_scan(datum/source, mob/user, list/extra_data)
/obj/item/pen/edagger/proc/on_scan(datum/source, mob/user, datum/detective_scanner_log/entry)
SIGNAL_HANDLER
LAZYADD(extra_data[DETSCAN_CATEGORY_ILLEGAL], "Hard-light generator detected.")
entry.add_data_entry(DETSCAN_CATEGORY_ILLEGAL, "Hard-light generator detected.")
/obj/item/pen/survival
name = "survival pen"

View File

@@ -4198,8 +4198,11 @@
#include "code\modules\deathmatch\deathmatch_modifier.dm"
#include "code\modules\debugging\debugger.dm"
#include "code\modules\debugging\tracy.dm"
#include "code\modules\detectivework\detective_scan_category.dm"
#include "code\modules\detectivework\evidence.dm"
#include "code\modules\detectivework\scanner.dm"
#include "code\modules\detectivework\detective_scanner_data\detective_scanner_data_entry.dm"
#include "code\modules\detectivework\detective_scanner_data\detective_scanner_log_entry.dm"
#include "code\modules\discord\accountlink.dm"
#include "code\modules\discord\discord_embed.dm"
#include "code\modules\discord\discord_link_record.dm"

View File

@@ -1,145 +0,0 @@
import {
Box,
Button,
Icon,
LabeledList,
NoticeBox,
Section,
} from 'tgui-core/components';
import { capitalizeFirst } from 'tgui-core/string';
import { useBackend } from '../backend';
import { Window } from '../layouts';
type ForensicScannerData = {
log_data: LogData[];
};
type LogData = {
scan_target: string;
scan_time: string;
Prints: Record<string, string>;
Fibers: Record<string, string>;
Blood: Record<string, string>;
Reagents: Record<string, number>;
'ID Access': Record<string, string[]>;
};
export const ForensicScanner = (props) => {
const { act, data } = useBackend<ForensicScannerData>();
const { log_data = [] } = data;
return (
<Window width={512} height={512}>
<Window.Content>
{log_data.length === 0 ? (
<NoticeBox>Log empty.</NoticeBox>
) : (
<Section
title="Scan history"
fill
scrollable
buttons={
<>
<Button.Confirm
icon="trash"
color="danger"
onClick={() => act('clear')}
>
Clear logs
</Button.Confirm>
<Button icon="print" onClick={() => act('print')}>
Print report
</Button>
</>
}
>
{log_data
.map((log, index) => (
<ForensicLog key={index} log={log} index={index} />
))
.reverse()}
</Section>
)}
</Window.Content>
</Window>
);
};
const ForensicLog = ({ log, index }: { log: LogData; index: number }) => {
const { act } = useBackend<ForensicScannerData>();
return (
<Section
title={`${capitalizeFirst(log.scan_target)} scan at ${log.scan_time}`}
buttons={
<Button
icon="trash"
color="transparent"
onClick={() => act('delete', { index })}
/>
}
>
{!log.Prints && !log.Fibers && !log.Blood && !log.Reagents ? (
<Box opacity={0.5}>No forensic traces found.</Box>
) : (
<LabeledList>
{log.Fibers && Object.keys(log.Fibers).length > 0 && (
<LabeledList.Item label="Fibers">
{Object.keys(log.Fibers).map((fibers) => (
<Box key={fibers} py={0.5}>
<Icon name="shirt" mr={1} color="green" />
{fibers}
</Box>
))}
</LabeledList.Item>
)}
{log.Prints && Object.values(log.Prints).length > 0 && (
<LabeledList.Item label="Fingerprints">
{Object.values(log.Prints).map((print) => (
<Box
key={print}
py={0.5}
style={{ textTransform: 'uppercase' }}
>
<Icon name="fingerprint" mr={1} color="yellow" />
{print}
</Box>
))}
</LabeledList.Item>
)}
{log.Blood && Object.keys(log.Blood).length > 0 && (
<LabeledList.Item label="Blood DNA, Type">
{Object.keys(log.Blood).map((dna) => (
<Box key={dna} py={0.5} style={{ textTransform: 'uppercase' }}>
<Icon name="droplet" mr={1} color="red" />
{`${dna}, ${log.Blood[dna]}`}
</Box>
))}
</LabeledList.Item>
)}
{log.Reagents && Object.keys(log.Reagents)?.length > 0 && (
<LabeledList.Item label="Reagents">
<LabeledList>
{Object.keys(log.Reagents).map((reagent) => (
<LabeledList.Item key={reagent} label={reagent}>
{`${log.Reagents[reagent]} u.`}
</LabeledList.Item>
))}
</LabeledList>
</LabeledList.Item>
)}
{log['ID Access'] && Object.keys(log['ID Access'])?.length > 0 && (
<LabeledList.Item label="ID Access">
<LabeledList>
{Object.keys(log['ID Access']).map((region) => (
<LabeledList.Item key={region} label={region}>
{log['ID Access'][region]}
</LabeledList.Item>
))}
</LabeledList>
</LabeledList.Item>
)}
</LabeledList>
)}
</Section>
) as any;
};

View File

@@ -0,0 +1,181 @@
import { Box, Button, Icon, LabeledList, Section } from 'tgui-core/components';
import { capitalizeFirst } from 'tgui-core/string';
import { useBackend } from '../../backend';
import type { DataEntry, ForensicScannerData } from './types';
type ForensicLogsProps = {
dataEntries: DataEntry[];
scanTarget: string;
scanTime: string;
index: number;
};
export function ForensicLogs(props: ForensicLogsProps) {
const { act, data } = useBackend<ForensicScannerData>();
const { categories } = data;
const { dataEntries, scanTarget, scanTime, index } = props;
return (
<Section
title={`${capitalizeFirst(scanTarget)} scan at ${scanTime} `}
buttons={
<Button
icon="trash"
color="transparent"
onClick={() => act('delete', { index })}
/>
}
>
{dataEntries.length === 0 ? (
<Box opacity={0.5}>No forensic traces found.</Box>
) : (
<LabeledList>
{dataEntries.map((dataEntry) => {
const category = categories[dataEntry.category];
return (
<LabeledList.Item key={category.name} label={category.name}>
<ForensicLog
logCategoryId={dataEntry.category}
log={dataEntry.data}
iconName={category.uiIcon}
iconColor={category.uiIconColor}
/>
</LabeledList.Item>
);
})}
</LabeledList>
)}
</Section>
);
}
type ForensicLogProps = {
logCategoryId: string;
log: Record<string, string>;
iconName: string;
iconColor: string;
};
function ForensicLog(props: ForensicLogProps) {
const { logCategoryId, log, iconName, iconColor } = props;
if (logCategoryId === 'Fingerprints')
return (
<PrintsLogFormatter log={log} iconName={iconName} iconColor={iconColor} />
);
if (logCategoryId === 'Reagents')
return (
<ReagentsLogFormatter
log={log}
iconName={iconName}
iconColor={iconColor}
/>
);
if (logCategoryId === 'Blood')
return (
<BloodLogFormatter log={log} iconName={iconName} iconColor={iconColor} />
);
if (logCategoryId === 'ID_Access')
return (
<IdAccessLogFormatter
log={log}
iconName={iconName}
iconColor={iconColor}
/>
);
return (
<DefaultLogFormatter log={log} iconName={iconName} iconColor={iconColor} />
);
}
type LogFormatterProprs = {
log: Record<string, string>;
iconName: string;
iconColor: string;
};
function DefaultLogFormatter(props: LogFormatterProprs) {
const { log, iconName, iconColor } = props;
return (
<>
{Object.entries(log).map(([key, value]) => (
<Box key={key} py={0.5}>
<Icon name={iconName} mr={1} color={iconColor} />
{value}
</Box>
))}
</>
);
}
function BloodLogFormatter(props: LogFormatterProprs) {
const { log, iconName, iconColor } = props;
return (
<>
{Object.entries(log).map(([key, value]) => (
<Box key={key} py={0.5} style={{ textTransform: 'uppercase' }}>
<Icon name={iconName} mr={1} color={iconColor} />
{`${key}, ${value}`}
</Box>
))}
</>
);
}
function PrintsLogFormatter(props: LogFormatterProprs) {
const { log, iconName, iconColor } = props;
return (
<>
{Object.entries(log).map(([key, value]) => (
<Box key={key} py={0.5} style={{ textTransform: 'uppercase' }}>
<Icon name={iconName} mr={1} color={iconColor} />
{value}
</Box>
))}
</>
);
}
function ReagentsLogFormatter(props: LogFormatterProprs) {
const { log, iconName, iconColor } = props;
return (
<LabeledList>
{Object.keys(log).map((reagent) => (
<LabeledList.Item
key={reagent}
label={
<>
<Icon name={iconName} mr={1} color={iconColor} />
{reagent}
</>
}
>
{`${log[reagent]} u.`}
</LabeledList.Item>
))}
</LabeledList>
);
}
function IdAccessLogFormatter(props: LogFormatterProprs) {
const { log, iconName, iconColor } = props;
return (
<LabeledList>
{Object.keys(log).map((region) => (
<LabeledList.Item
key={region}
label={
<>
<Icon name={iconName} mr={1} color={iconColor} />
{region}
</>
}
>
{log[region]}
</LabeledList.Item>
))}
</LabeledList>
);
}

View File

@@ -0,0 +1,51 @@
import { Button, NoticeBox, Section } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { Window } from '../../layouts';
import { ForensicLogs } from './ForensicLogs';
import type { ForensicScannerData } from './types';
export function ForensicScanner() {
const { act, data } = useBackend<ForensicScannerData>();
const { logs = [] } = data;
return (
<Window width={512} height={512}>
<Window.Content>
{logs.length === 0 ? (
<NoticeBox>Log empty.</NoticeBox>
) : (
<Section
title="Scan history"
fill
scrollable
buttons={
<>
<Button.Confirm
icon="trash"
color="danger"
onClick={() => act('clear')}
>
Clear logs
</Button.Confirm>
<Button icon="print" onClick={() => act('print')}>
Print report
</Button>
</>
}
>
{logs
.map((log, index) => (
<ForensicLogs
key={index}
dataEntries={log.dataEntries}
scanTarget={log.scanTarget}
scanTime={log.scanTime}
index={index}
/>
))
.reverse()}
</Section>
)}
</Window.Content>
</Window>
);
}

View File

@@ -0,0 +1,21 @@
export type ForensicScannerData = {
logs: LogEntry[];
categories: Record<string, ForensicScannerCategory>;
};
export type LogEntry = {
scanTarget: string;
scanTime: string;
dataEntries: DataEntry[];
};
export type DataEntry = {
category: string;
data: Record<string, string>;
};
export type ForensicScannerCategory = {
name: string;
uiIcon: string;
uiIconColor: string;
};