Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds flavor text and records #1825

Merged
merged 1 commit into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions code/__DEFINES/~monkestation/span.dm
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
#define span_ratvar(str) ("<span class='ratvar'>" + str + "</span>")

#define REQUEST_MENTORHELP "request_mentorhelp"

#define span_italics(str) ("<span class='italics'>" + str + "</span>")
3 changes: 3 additions & 0 deletions code/__HELPERS/pronouns.dm
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
/datum/proc/p_theyve(capitalized, temp_gender)
. = p_they(capitalized, temp_gender) + "'" + copytext_char(p_have(temp_gender), 3)

/datum/proc/p_Theyve(capitalized, temp_gender)
. = p_They(capitalized, temp_gender) + "'" + copytext_char(p_have(temp_gender), 3)

/datum/proc/p_theyre(capitalized, temp_gender)
. = p_they(capitalized, temp_gender) + "'" + copytext_char(p_are(temp_gender), 2)

Expand Down
2 changes: 2 additions & 0 deletions monkestation/code/__DEFINES/_module_defines.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// How much flavor text gets displayed before cutting off.
#define EXAMINE_FLAVOR_MAX_DISPLAYED 65
2 changes: 2 additions & 0 deletions monkestation/code/__DEFINES/antag_defines.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// Whether the antagonist can see exploitable info on people they examine.
#define FLAG_CAN_SEE_EXPOITABLE_INFO (1<<1)
4 changes: 4 additions & 0 deletions monkestation/code/__DEFINES/signals.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// Sent from [/mob/living/examine] late, after the first signal is sent, but BEFORE flavor text handling,
/// for when you prefer something guaranteed to appear at the bottom of the stack
/// (Flavor text should stay at the very very bottom though)
#define COMSIG_LIVING_LATE_EXAMINE "late_examine"
11 changes: 11 additions & 0 deletions monkestation/code/__HELPERS/text_helpers.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// -- Text helpers. --
/// Provides a preview of [string] up to [len - 3], after which it appends "..." if it pasts the length.
/proc/TextPreview(string, len = 40)
var/char_len = length_char(string)
if(char_len <= len)
if(!char_len)
return "\[...\]"
else
return string
else
return "[copytext_char(string, 1, len - 3)]..."
2 changes: 2 additions & 0 deletions monkestation/code/modules/antagonists/_common/antag_datum.dm
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/datum/antagonist
/// Allows antags to check exploitable info
antag_flags = FLAG_CAN_SEE_EXPOITABLE_INFO
///The list of keys that are valid to see our antag hud/of huds we can see
var/list/hud_keys

Expand Down
11 changes: 11 additions & 0 deletions monkestation/code/modules/client/preferences/_preferences.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Used to apply preferences at the very end of applying preferences, quirks, clothing, etc.
/datum/preferences/proc/after_prefs_transfer(mob/living/carbon/human/target)
for (var/datum/preference/preference as anything in get_preferences_in_priority_order())
if (preference.savefile_identifier != PREFERENCE_CHARACTER)
continue

preference.after_apply_to_human(target, src, read_preference(preference.type))

/// See above. Called at the very end of player initialization.
/datum/preference/proc/after_apply_to_human(mob/living/carbon/human/target, datum/preferences/prefs, value)
return
162 changes: 162 additions & 0 deletions monkestation/code/modules/client/preferences/multiline_preferences.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#define MAX_FLAVOR_LEN 2048

/datum/preference/multiline_text
abstract_type = /datum/preference/multiline_text
can_randomize = FALSE
var/max_length = MAX_FLAVOR_LEN

/datum/preference/multiline_text/deserialize(input, datum/preferences/preferences)
return STRIP_HTML_SIMPLE("[input]", max_length)

/datum/preference/multiline_text/serialize(input)
return STRIP_HTML_SIMPLE(input, max_length)

/datum/preference/multiline_text/is_valid(value)
return istext(value) && !isnull(STRIP_HTML_SIMPLE(value, max_length))

/datum/preference/multiline_text/create_default_value()
return null

/datum/preference/multiline_text/compile_constant_data()
return list("maximum_length" = max_length)

/// Preferences that add onto flavor text datum
/datum/preference/multiline_text/flavor_datum
abstract_type = /datum/preference/multiline_text/flavor_datum
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
priority = PREFERENCE_PRIORITY_NAMES

/datum/preference/multiline_text/flavor_datum/apply_to_human(mob/living/carbon/human/target, value)
if(!length(value) || istype(target, /mob/living/carbon/human/dummy)) // Don't stick flavor text on dummies
return

var/datum/flavor_text/our_flavor = target.linked_flavor || add_or_get_mob_flavor_text(target)
if(isnull(our_flavor))
return

add_to_flavor_datum(our_flavor, value)

/datum/preference/multiline_text/flavor_datum/proc/add_to_flavor_datum(datum/flavor_text/our_flavor, value)
SHOULD_CALL_PARENT(FALSE)
stack_trace("add_to_flavor_datum not implemented for [type]")

/datum/preference/multiline_text/flavor_datum/flavor
savefile_key = "flavor_text"

/datum/preference/multiline_text/flavor_datum/flavor/add_to_flavor_datum(datum/flavor_text/our_flavor, value)
our_flavor.flavor_text = value

/datum/preference/multiline_text/flavor_datum/silicon
savefile_key = "silicon_text"

/datum/preference/multiline_text/flavor_datum/silicon/add_to_flavor_datum(datum/flavor_text/our_flavor, value)
our_flavor.silicon_text = value

/datum/preference/multiline_text/flavor_datum/exploitable
savefile_key = "exploitable_info"

/datum/preference/multiline_text/flavor_datum/exploitable/add_to_flavor_datum(datum/flavor_text/our_flavor, value)
our_flavor.expl_info = value

/// Preferences that add onto crew records
/datum/preference/multiline_text/record
abstract_type = /datum/preference/multiline_text/record
savefile_identifier = PREFERENCE_CHARACTER
category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
priority = PREFERENCE_PRIORITY_NAMES
max_length = MAX_MESSAGE_LEN

/datum/preference/multiline_text/record/New()
. = ..()
// This is here to catch people who have preferences assigned before the manifest is built (IE: roundstart players)
RegisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED, PROC_REF(on_new_player_joined))
RegisterSignal(SSticker, COMSIG_TICKER_ROUND_STARTING, PROC_REF(unregister_join_sig))
// Confusingly, roundstart character setup goes "create characters" -> "assign quirks" -> "build manifest" -> "transfer clients in"
// while latejoin character setup is "create charater" -> "transfer clients in" -> "inject manifest entry" -> "assign quirks"

/datum/preference/multiline_text/record/proc/on_new_player_joined(datum/source, mob/living/carbon/human/joined, rank)
SIGNAL_HANDLER

apply_to_human_records(joined)

/datum/preference/multiline_text/record/proc/unregister_join_sig()
SIGNAL_HANDLER

UnregisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED)
UnregisterSignal(SSticker, COMSIG_TICKER_ROUND_STARTING)

/datum/preference/multiline_text/record/apply_to_human(mob/living/carbon/human/target, value)
return

/datum/preference/multiline_text/record/after_apply_to_human(mob/living/carbon/human/target, datum/preferences/prefs, value)
apply_to_human_records(target, prefs, value)

/datum/preference/multiline_text/record/proc/apply_to_human_records(mob/living/carbon/human/joined, datum/preferences/prefs, value)
if(!ishuman(joined) || istype(joined, /mob/living/carbon/human/dummy)) // Fairly certain this is redundant but let's just be safe
return

prefs ||= joined.client?.prefs
if(isnull(prefs))
CRASH("[type] was applied to a mob ([joined]) without prefs.")

value ||= prefs.read_preference(type)
if(!length(value))
return // valid

var/datum/record/crew/associated_record = find_record(joined.real_name)
if(isnull(associated_record))
if(length(GLOB.manifest?.general))
stack_trace("[type] was applied to a mob ([joined], [joined.key]) before their record was created.")
return

add_to_record(associated_record, value)

/datum/preference/multiline_text/record/proc/add_to_record(datum/record/crew/associated_record, value)
SHOULD_CALL_PARENT(FALSE)
stack_trace("add_to_record not implemented for [type]")

/datum/preference/multiline_text/record/general
savefile_key = "general_records"

/datum/preference/multiline_text/record/general/add_to_record(datum/record/crew/associated_record, value)
var/fake_author = "Record Database"
for(var/datum/medical_note/existing_note as anything in associated_record.medical_notes)
if(existing_note.author == fake_author)
existing_note.content = value
return

var/datum/medical_note/new_note = new(fake_author, value)
new_note.time = "Past record"
associated_record.medical_notes += new_note

/datum/preference/multiline_text/record/medical
savefile_key = "medical_records"

/datum/preference/multiline_text/record/medical/add_to_record(datum/record/crew/associated_record, value)
var/fake_author = "Medical Database"
for(var/datum/medical_note/existing_note as anything in associated_record.medical_notes)
if(existing_note.author == fake_author)
existing_note.content = value
return

var/datum/medical_note/new_note = new(fake_author, value)
new_note.time = "Past record"
associated_record.medical_notes += new_note

/datum/preference/multiline_text/record/security
savefile_key = "security_records"

/datum/preference/multiline_text/record/security/add_to_record(datum/record/crew/associated_record, value)
var/fake_author = "Security Database"
for(var/datum/crime/existing_note as anything in associated_record.crimes)
if(existing_note.author == fake_author)
existing_note.details = value
return

var/datum/crime/new_crime = new("Notes / Past infractions", value, fake_author, "Indetermined")
new_crime.time = "Past record"
new_crime.valid = FALSE // This makes it so the record is printed as "REDACTED", which I think is cool
associated_record.crimes += new_crime

#undef MAX_FLAVOR_LEN
88 changes: 88 additions & 0 deletions monkestation/code/modules/flavor_text/flavor_examine.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/// -- Extension of examine, examine_more, and flavortext code. --
/mob
/// Last time a client was connected to this mob.
var/last_connection_time = 0

/mob/Logout()
. = ..()
last_connection_time = world.time

/**
* Flavor text and Personal Records On Examine INS AND OUTS (implementation by mrmelbert from MapleStation)
* - Admin ghosts, when examining, are given a list of buttons for all the records of a player.
* (This can probably be moved to examine_more if it's too annoying)
* - When you examine yourself, you will always see your own records and flavor text, no matter what.
* - When another person examines you, the following happens:
* > If your face is covered (by helmet or mask), they will not see your favor text or records, unless you're wearing your ID.
* > If you are wearing another player's ID (In disguise as another active player), they will see the other player's records and flavor instead.
* > If you are not wearing another player's ID (if you are unknown, or wearing a non-player's ID), no records or flavor text will show up as if none were set.
* > If you do not have any flavor text or records set, nothing special happens. The examine is normal.
*
* - Flavor text is displayed to other players without any pre-requisites. It displays [EXAMINE_FLAVOR_MAX_DISPLAYED] (65 by default) characters before being trimmed.
* - Exploitive information is displayed via link to antagonists with the proper flags.
*
* Bonus: If you are not connected to the server and someone examines you...
* an AFK timer is shown to the examiner, which displays how long you have been disconnected for.
*/

// Carbon and human examine don't call parent
// so we need to replicate this across all three
// Really I should be using the signal but at least this guarantees order
/mob/living/examine(mob/user)
. = ..()
. += late_examine(user)

/mob/living/carbon/examine(mob/user)
. = ..()
. += late_examine(user)

/mob/living/carbon/human/examine(mob/user)
. = ..()
. += late_examine(user)

/// Mob level examining that happens after the main beef of examine is done
/mob/living/proc/late_examine(mob/user)
. = list()
SEND_SIGNAL(src, COMSIG_LIVING_LATE_EXAMINE, user, .)

// Who's identity are we dealing with? In most cases it's the same as [src], but it could be disguised people, or null.
var/datum/flavor_text/known_identity = get_visible_flavor(user)
var/expanded_examine = ""

if(known_identity)
expanded_examine += known_identity.format_flavor_for_examine(user)

if(linked_flavor && user.client?.holder && isAdminObserver(user))
// Formatted output list of records.
var/admin_line = ""

if(linked_flavor.flavor_text)
admin_line += "<a href='?src=[REF(linked_flavor)];flavor_text=1'>\[FLA\]</a>"
if(linked_flavor.expl_info)
admin_line += "<a href='?src=[REF(linked_flavor)];exploitable_info=1'>\[EXP\]</a>"
if(known_identity != linked_flavor)
admin_line += "\nThey are currently [isnull(known_identity) ? "disguised and have no visible flavor":"visible as the flavor text of [known_identity.name]"]."

if(admin_line)
expanded_examine += "ADMIN EXAMINE: [ADMIN_LOOKUPFLW(src)] - [admin_line]\n"

// if the mob doesn't have a client, show how long they've been disconnected for.
if(!client && last_connection_time && stat != DEAD)
var/formatted_afk_time = span_bold("[round((world.time - last_connection_time) / (60*60), 0.1)]")
expanded_examine += span_italics("\n[p_Theyve()] been unresponsive for [formatted_afk_time] minute(s).\n")

if(length(expanded_examine))
expanded_examine = span_info(expanded_examine)
. += expanded_examine

// This isn't even an extension of examine_more this is the only definition for /human/examine_more, isn't that neat?
/mob/living/examine_more(mob/user)
. = ..()
var/datum/flavor_text/known_identity = get_visible_flavor(user)

if(known_identity)
. += span_info(known_identity.format_flavor_for_examine(user, FALSE))
else if(ishuman(src))
// I hate this istype src but it's easier to handle this here
// Not all mobs should say "YOU CAN'T MAKE OUT DETAILS OF THIS PERSON"
. += span_smallnoticeital("You can't make out any details of this individual.\n")
73 changes: 73 additions & 0 deletions monkestation/code/modules/flavor_text/flavor_helpers.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// -- Extra helper procs for humans. --

/* Determine if the current mob's real identity is visible.
* This probably has a lot of edge cases that will get missed but we can find those later.
* (There's gotta be a helper proc for this that already exists in the code, right?)
*
* returns a reference to a mob -
* - returns SRC if [src] isn't disguised, or is wearing their id / their name is visible
* - returns another mob if [src] is disguised as someone that exists in the world
* returns null otherwise.
*/
/mob/living/proc/get_visible_flavor(mob/examiner)
RETURN_TYPE(/datum/flavor_text)

var/datum/flavor_text/found_flavor = linked_flavor
// Simple animals, basic animals, anything that's not a human/silicon is lumped under "simple"
if(found_flavor?.linked_species != "simple" || HAS_TRAIT(src, TRAIT_UNKNOWN))
return null

return found_flavor

/mob/living/carbon/human/get_visible_flavor(mob/examiner)
// your identity is always known to you
if(examiner == src)
return linked_flavor

var/shown_name = get_visible_name()
if(shown_name == "Unknown" || HAS_TRAIT(src, TRAIT_UNKNOWN)) // Redundant, but just in case
return null

var/datum/flavor_text/found_flavor
// the important check - if the visible name is our flavor text name, display our flavor text
// if the visible name is not, however, we may be in disguise - so grab the corresponding flavor text from our global list
if(shown_name == linked_flavor?.name || findtext(shown_name, linked_flavor?.name))
found_flavor = linked_flavor
else
found_flavor = GLOB.flavor_texts[shown_name]

// if you are not the species linked to the flavor text we found, you are not recognizable
if(found_flavor?.linked_species != dna?.species.id)
return null

return found_flavor

/mob/living/silicon/get_visible_flavor(mob/examiner)
if(examiner == src)
return linked_flavor

var/datum/flavor_text/found_flavor = linked_flavor
if(found_flavor?.linked_species != "silicon" || HAS_TRAIT(src, TRAIT_UNKNOWN))
return null

return found_flavor

/mob/proc/check_med_hud_and_access()
return FALSE

/mob/living/silicon/check_med_hud_and_access()
return TRUE

/mob/living/carbon/human/check_med_hud_and_access()
var/list/access = wear_id?.GetAccess()
return HAS_TRAIT(src, TRAIT_MEDICAL_HUD) && (ACCESS_MEDICAL in access)

/mob/proc/check_sec_hud_and_access()
return FALSE

/mob/living/silicon/check_sec_hud_and_access()
return TRUE

/mob/living/carbon/human/check_sec_hud_and_access()
var/list/access = wear_id?.GetAccess()
return HAS_TRAIT(src, TRAIT_SECURITY_HUD) && (ACCESS_SECURITY in access)
Loading
Loading