diff --git a/code/__DEFINES/language.dm b/code/__DEFINES/language.dm index 937fc987aa6..1ae7d898203 100644 --- a/code/__DEFINES/language.dm +++ b/code/__DEFINES/language.dm @@ -1,7 +1,20 @@ -#define NO_STUTTER 1 -#define TONGUELESS_SPEECH 2 -#define LANGUAGE_HIDE_ICON_IF_UNDERSTOOD 4 -#define LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD 8 +// Language flags +/// Language is never stuttered +#define LANGUAGE_NO_STUTTER (1<<0) +/// Language is speakable without a tongue +#define LANGUAGE_TONGUELESS_SPEECH (1<<1) +/// Language icon is hidden if the language is understood +#define LANGUAGE_HIDE_ICON_IF_UNDERSTOOD (1<<2) +/// Language icon is hidden if the language is not understood +#define LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD (1<<3) + +// Returns from display_icon_type +/// Display no icon for this language +#define DISPLAY_LANGUAGE_ICON_NONE 0 +/// Display the full icon for this language +#define DISPLAY_LANGUAGE_ICON_FULL 1 +/// Display a blended icon for this language, indicating partial understanding +#define DISPLAY_LANGUAGE_ICON_PARTIAL 2 // LANGUAGE SOURCE DEFINES /// For use in full removal only. diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm index cf555170b68..dce1ca6791d 100644 --- a/code/datums/chatmessage.dm +++ b/code/datums/chatmessage.dm @@ -64,7 +64,7 @@ * * extra_classes - Extra classes to apply to the span that holds the text * * lifespan - The lifespan of the message in deciseconds */ -/datum/chatmessage/New(text, atom/target, mob/owner, datum/language/language, list/extra_classes = list(), lifespan = CHAT_MESSAGE_LIFESPAN) +/datum/chatmessage/New(text, atom/target, mob/owner, datum/language/language, list/extra_classes = list(), lifespan = CHAT_MESSAGE_LIFESPAN, list/message_mods) . = ..() if (!istype(target)) CRASH("Invalid target given for chatmessage") @@ -72,7 +72,7 @@ stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner") qdel(src) return - INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, language, extra_classes, lifespan) + INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, language, extra_classes, lifespan, message_mods) /datum/chatmessage/Destroy() if (!QDELING(owned_by)) @@ -109,8 +109,9 @@ * * language - The language this message was spoken in * * extra_classes - Extra classes to apply to the span that holds the text * * lifespan - The lifespan of the message in deciseconds + * * message_mods - All mods from whatever message triggered this (if applicable) */ -/datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, datum/language/language, list/extra_classes, lifespan) +/datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, datum/language/language, list/extra_classes, lifespan, list/message_mods) /// Cached icons to show what language the user is speaking var/static/list/language_icons @@ -171,12 +172,16 @@ // Append language icon if the language uses one var/datum/language/language_instance = GLOB.language_datum_instances[language] - if (language_instance?.display_icon(owner)) - var/icon/language_icon = LAZYACCESS(language_icons, language) + var/language_icon_type = language_instance?.display_icon_type(owner, message_mods) || DISPLAY_LANGUAGE_ICON_NONE + if(language_icon_type != DISPLAY_LANGUAGE_ICON_NONE) + var/language_icon_key = "[language][language_icon_type == DISPLAY_LANGUAGE_ICON_PARTIAL ? "-partial" : ""]" + var/icon/language_icon = LAZYACCESS(language_icons, language_icon_key) if (isnull(language_icon)) language_icon = icon(language_instance.icon, icon_state = language_instance.icon_state) + if(language_icon_type == DISPLAY_LANGUAGE_ICON_PARTIAL) + language_icon.Blend(icon('icons/ui/chat/language.dmi', "unknown"), ICON_OVERLAY) language_icon.Scale(CHAT_MESSAGE_ICON_SIZE, CHAT_MESSAGE_ICON_SIZE) - LAZYSET(language_icons, language, language_icon) + LAZYSET(language_icons, language_icon_key, language_icon) LAZYADD(prefixes, "\icon[language_icon]") text = "[prefixes?.Join(" ")][text]" @@ -319,30 +324,34 @@ * * raw_message - The text content of the message * * spans - Additional classes to be added to the message */ -/mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, runechat_flags = NONE) +/mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, runechat_flags = NONE, list/message_mods) if(SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(speaker, TRAIT_BYPASS_MEASURES)) return if(HAS_TRAIT(speaker, TRAIT_RUNECHAT_HIDDEN)) return // Ensure the list we are using, if present, is a copy so we don't modify the list provided to us - spans = spans ? spans.Copy() : list() + var/list/classes = (runechat_flags & EMOTE_MESSAGE) ? list("emote", "italics") : LAZYCOPY(spans) // Check for virtual speakers (aka hearing a message through a radio) - var/atom/movable/originalSpeaker = speaker - if (istype(speaker, /atom/movable/virtualspeaker)) - var/atom/movable/virtualspeaker/v = speaker - speaker = v.source - spans |= "virtual-speaker" + var/atom/movable/original_speaker = speaker + if (!(runechat_flags & EMOTE_MESSAGE) && istype(speaker, /atom/movable/virtualspeaker)) + var/atom/movable/virtualspeaker/virtual_speaker = speaker + speaker = virtual_speaker.source + classes |= "virtual-speaker" // Ignore virtual speaker (most often radio messages) from ourselves - if (originalSpeaker != src && speaker == src) + if (original_speaker != src && speaker == src) return // Display visual above source - if(runechat_flags & EMOTE_MESSAGE) - new /datum/chatmessage(raw_message, speaker, src, message_language, list("emote", "italics")) - else - new /datum/chatmessage(raw_message, speaker, src, message_language, spans) + new /datum/chatmessage( + text = raw_message, + target = speaker, + owner = src, + language = message_language, + extra_classes = classes, + message_mods = message_mods, + ) #undef CHAT_LAYER_MAX_Z #undef CHAT_LAYER_Z_STEP diff --git a/code/datums/quirks/negative_quirks/csl.dm b/code/datums/quirks/negative_quirks/csl.dm index 68a8f58be68..752a7d2dc31 100644 --- a/code/datums/quirks/negative_quirks/csl.dm +++ b/code/datums/quirks/negative_quirks/csl.dm @@ -10,13 +10,14 @@ lose_text = span_notice("Common starts to click for you.") medical_record_text = "Patient is CSL." /// What language typepath is our primary language? - var/native_language + VAR_PRIVATE/native_language /datum/quirk/csl/add(client/client_source) - if(iscarbon(quirk_holder)) - quirk_holder.remove_language(/datum/language/common, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES) - else - quirk_holder.remove_language(/datum/language/common, UNDERSTOOD_LANGUAGE, LANGUAGE_ATOM) + quirk_holder.remove_language(/datum/language/common, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES) + quirk_holder.remove_language(/datum/language/common, UNDERSTOOD_LANGUAGE, LANGUAGE_ATOM) + if(length(quirk_holder.get_understood_languages()) < 1) + quirk_holder.grant_language(/datum/language/uncommon, SPOKEN_LANGUAGE|UNDERSTOOD_LANGUAGE, LANGUAGE_QUIRK) + quirk_holder.grant_partial_language(/datum/language/common, text2num(client_source?.prefs?.read_preference(/datum/preference/choiced/csl_strength)) || 90, type) RegisterSignal(quirk_holder, COMSIG_SPECIES_GAIN, PROC_REF(reremove_common)) RegisterSignal(quirk_holder, COMSIG_MOB_SAY, PROC_REF(translate_parts)) @@ -30,6 +31,7 @@ return quirk_holder.remove_partial_language(/datum/language/common, type) + quirk_holder.remove_language(/datum/language/uncommon, SPOKEN_LANGUAGE|UNDERSTOOD_LANGUAGE, LANGUAGE_QUIRK) var/mob/living/carbon/carbon_quirk_holder = quirk_holder if(istype(carbon_quirk_holder) && carbon_quirk_holder.dna.species) // only give back common if they're a species that should speak it @@ -78,10 +80,9 @@ if(say_args[SPEECH_FORCED] || isnull(native_language) || quirk_holder.mob_mood?.sanity > 75) return - // init this list if nothing else has - LAZYINITLIST(say_args[SPEECH_MODS][LANGUAGE_MUTUAL_BONUS]) // force speak language, add mutual bonuses so everyone else can understand say_args[SPEECH_LANGUAGE] = native_language + say_args[SPEECH_MODS][LANGUAGE_MUTUAL_BONUS] ||= list() say_args[SPEECH_MODS][LANGUAGE_MUTUAL_BONUS][native_language] = max(round(8 * sqrt(quirk_holder.mob_mood?.sanity), 5), say_args[SPEECH_MODS][LANGUAGE_MUTUAL_BONUS][native_language]) /datum/quirk_constant_data/csl diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 5475913e1c7..2ffa7e29ac9 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1598,6 +1598,10 @@ /atom/movable/proc/has_language(language, flags_to_check) return get_language_holder().has_language(language, flags_to_check) +/// Checks if atom has the language. If spoken is true, only checks if atom can speak the language. +/atom/movable/proc/has_partial_language(language) + return get_language_holder().has_partial_language(language) + /// Checks if atom can speak the language. /atom/movable/proc/can_speak_language(language) return get_language_holder().can_speak_language(language) @@ -1623,6 +1627,10 @@ /atom/movable/proc/get_random_spoken_language() return get_language_holder().get_random_spoken_language() +/// Gets a list of all understood languages, excluding any blocked languages +/atom/movable/proc/get_understood_languages() as /list + return get_language_holder().get_understood_languages() || list() + /// Copies all languages into the supplied atom/language holder. Source should be overridden when you /// do not want the language overwritten by later atom updates or want to avoid blocked languages. /atom/movable/proc/copy_languages(datum/language_holder/from_holder, source_override) diff --git a/code/game/say.dm b/code/game/say.dm index c707f90f9c9..51f48cc8bca 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -155,6 +155,7 @@ GLOBAL_LIST_INIT(freqtospan, list( listened += hearing_movable do_tts_message(tts_message_to_use, message_language, message_mods, tts_filter, listened) + /atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, radio_freq_name, radio_freq_color, list/spans, list/message_mods = list(), visible_name = FALSE) //This proc uses [] because it is faster than continually appending strings. Thanks BYOND. //Basic span @@ -174,8 +175,10 @@ GLOBAL_LIST_INIT(freqtospan, list( var/languageicon = "" if(!message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]) var/datum/language/dialect = GLOB.language_datum_instances[message_language] - if(istype(dialect) && dialect.display_icon(src)) - languageicon = "[dialect.get_icon()] " + var/dialect_icon_type = dialect?.display_icon_type(src, message_mods) || DISPLAY_LANGUAGE_ICON_NONE + if(dialect_icon_type != DISPLAY_LANGUAGE_ICON_NONE) + var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat) + languageicon = sheet.icon_tag("language-[dialect.icon_state][dialect_icon_type == DISPLAY_LANGUAGE_ICON_PARTIAL ? "-partial" : ""]") + " " // The actual message part. var/messagepart = speaker.generate_messagepart(raw_message, spans, message_mods) diff --git a/code/modules/asset_cache/assets/chat.dm b/code/modules/asset_cache/assets/chat.dm index 890f05afe39..b5b0e596bca 100644 --- a/code/modules/asset_cache/assets/chat.dm +++ b/code/modules/asset_cache/assets/chat.dm @@ -1,17 +1,23 @@ /datum/asset/spritesheet_batched/chat name = "chat" + /// Default file for language icons + var/default_language_file = 'icons/ui/chat/language.dmi' /datum/asset/spritesheet_batched/chat/create_spritesheets() insert_all_icons("emoji", EMOJI_SET) // pre-loading all lanugage icons also helps to avoid meta - insert_all_icons("language", 'icons/ui/chat/language.dmi') + insert_all_icons("language", default_language_file) // catch languages which are pulling icons from another file - for(var/datum/language/L as anything in subtypesof(/datum/language)) - var/icon = initial(L.icon) - if (icon != 'icons/ui/chat/language.dmi') - var/icon_state = initial(L.icon_state) + for(var/datum/language/lang_type as anything in subtypesof(/datum/language)) + var/icon = initial(lang_type.icon) + var/icon_state = initial(lang_type.icon_state) + if (icon != default_language_file) insert_icon("language-[icon_state]", uni_icon(icon, icon_state)) + var/datum/universal_icon/partial_icon = uni_icon(icon, icon_state) + partial_icon.blend_icon(uni_icon(default_language_file, "unknown"), ICON_OVERLAY) + insert_icon("language-[icon_state]-partial", partial_icon) + var/datum/universal_icon/byond_member = uni_icon('icons/ui/chat/member_content.dmi', "blag") byond_member.scale(16, 16) insert_icon("byond_member", byond_member) diff --git a/code/modules/client/preferences/language.dm b/code/modules/client/preferences/language.dm index c9f69c6e205..f822bdf323d 100644 --- a/code/modules/client/preferences/language.dm +++ b/code/modules/client/preferences/language.dm @@ -94,13 +94,13 @@ should_update_preview = FALSE /datum/preference/choiced/csl_strength/create_default_value() - return "90%" + return init_possible_values()[1] /datum/preference/choiced/csl_strength/is_accessible(datum/preferences/preferences) return ..() && (/datum/quirk/csl::name in preferences.all_quirks) /datum/preference/choiced/csl_strength/init_possible_values() - return list("90%", "75%", "50%", "33%", "25%", "10%") + return list("75%", "50%", "25%") /datum/preference/choiced/csl_strength/apply_to_human(mob/living/carbon/human/target, value) return diff --git a/code/modules/language/_language.dm b/code/modules/language/_language.dm index 65b107ee598..2c558e33f33 100644 --- a/code/modules/language/_language.dm +++ b/code/modules/language/_language.dm @@ -110,19 +110,19 @@ most_common_cache.Cut() last_sentence_cache.Cut() -/// Checks whether we should display the language icon to the passed hearer. -/datum/language/proc/display_icon(atom/movable/hearer) - var/understands = hearer.has_language(src.type) - if((flags & LANGUAGE_HIDE_ICON_IF_UNDERSTOOD) && understands) - return FALSE - if((flags & LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD) && !understands) - return FALSE - return TRUE +/// Returns what icon to display when speaking this language +/datum/language/proc/display_icon_type(atom/movable/hearer, list/message_mods) + if(hearer.has_language(type)) + if(flags & LANGUAGE_HIDE_ICON_IF_UNDERSTOOD) + return DISPLAY_LANGUAGE_ICON_NONE -/// Returns the icon to display in the chat window when speaking this language. -/datum/language/proc/get_icon() - var/datum/asset/spritesheet_batched/sheet = get_asset_datum(/datum/asset/spritesheet_batched/chat) - return sheet.icon_tag("language-[icon_state]") + else + if(hearer.has_partial_language(type) || LAZYACCESSASSOC(message_mods, LANGUAGE_MUTUAL_BONUS, type)) + return DISPLAY_LANGUAGE_ICON_PARTIAL + if(flags & LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD) + return DISPLAY_LANGUAGE_ICON_NONE + + return DISPLAY_LANGUAGE_ICON_FULL /// Simple helper for getting a default firstname lastname /datum/language/proc/default_name(gender = NEUTER) @@ -259,12 +259,12 @@ var/list/translated_index = list() for(var/word in splittext(input, " ")) var/translate_prob = mutual_languages?[type] || 0 - var/base_word = strip_outer_punctuation(word) + var/base_word = strip_outer_punctuation(html_decode(word)) if(translate_prob > 0) - // the probability of managing to understand a word is based on how common it is (+10%, -15%) + // the probability of managing to understand a word is based on how common it is (up to +20%, though -5% for very uncommon words) // 1000 words in the list, so words outside the list are just treated as "the 1250th most common word" var/commonness = GLOB.most_common_words_frequency[LOWER_TEXT(base_word)] || 1250 - translate_prob += (10 * (1 - (min(commonness, 1250) / 500))) + translate_prob += max(20 - (commonness / 1000) * 20, -5) if(prob(translate_prob)) scrambled_words += word translated_index += FALSE diff --git a/code/modules/language/_language_holder.dm b/code/modules/language/_language_holder.dm index c4207f40f07..c72e830d135 100644 --- a/code/modules/language/_language_holder.dm +++ b/code/modules/language/_language_holder.dm @@ -232,6 +232,10 @@ Key procs return language in langs_to_check +/// Checks if you have partial understanding of the language passed. +/datum/language_holder/proc/has_partial_language(language) + return LAZYACCESS(best_mutual_languages, language) + /// Checks if you can speak the language. Tongue limitations should be supplied as an argument. /datum/language_holder/proc/can_speak_language(language) var/can_speak_language_path = omnitongue || owner.could_speak_language(language) @@ -272,6 +276,10 @@ Key procs else return /datum/language/common +/// Returns a list of languages we understand +/datum/language_holder/proc/get_understood_languages() + return assoc_to_keys(understood_languages) - assoc_to_keys(blocked_understanding) + /// Opens a language menu reading from the language holder. /datum/language_holder/proc/open_language_menu(mob/user) if(!language_menu) diff --git a/code/modules/language/carptongue.dm b/code/modules/language/carptongue.dm index f3a376287ec..6215c618863 100644 --- a/code/modules/language/carptongue.dm +++ b/code/modules/language/carptongue.dm @@ -3,7 +3,7 @@ desc = "Various fishy sounds that might make sense to space carps." key = "c" icon_state = "fish" - flags = NO_STUTTER|TONGUELESS_SPEECH + flags = LANGUAGE_NO_STUTTER|LANGUAGE_TONGUELESS_SPEECH sentence_chance = 0 space_chance = 0 between_word_sentence_chance = 0 diff --git a/code/modules/language/codespeak.dm b/code/modules/language/codespeak.dm index 8cc8c1c460a..55af1b060a7 100644 --- a/code/modules/language/codespeak.dm +++ b/code/modules/language/codespeak.dm @@ -3,7 +3,7 @@ desc = "Syndicate operatives can use a series of codewords to convey complex information, while sounding like random concepts and drinks to anyone listening in." key = "t" default_priority = 0 - flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD + flags = LANGUAGE_TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD icon_state = "codespeak" always_use_default_namelist = TRUE // No syllables anyways diff --git a/code/modules/language/common.dm b/code/modules/language/common.dm index bf015a04e2f..7cd79359c44 100644 --- a/code/modules/language/common.dm +++ b/code/modules/language/common.dm @@ -3,7 +3,7 @@ name = "Galactic Common" desc = "The common galactic tongue." key = "0" - flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD + flags = LANGUAGE_TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD default_priority = 100 space_chance = 20 sentence_chance = 0 diff --git a/code/modules/language/draconic.dm b/code/modules/language/draconic.dm index 76cc7350f60..628587b7db9 100644 --- a/code/modules/language/draconic.dm +++ b/code/modules/language/draconic.dm @@ -2,7 +2,7 @@ name = "Draconic" desc = "The common language of lizard-people, composed of sibilant hisses and rattles." key = "o" - flags = TONGUELESS_SPEECH + flags = LANGUAGE_TONGUELESS_SPEECH space_chance = 12 sentence_chance = 0 between_word_sentence_chance = 10 diff --git a/code/modules/language/drone.dm b/code/modules/language/drone.dm index 373f504a135..3b220119e76 100644 --- a/code/modules/language/drone.dm +++ b/code/modules/language/drone.dm @@ -3,7 +3,7 @@ desc = "A heavily encoded damage control coordination stream, with special flags for hats." spans = list(SPAN_ROBOT) key = "d" - flags = NO_STUTTER + flags = LANGUAGE_NO_STUTTER syllables = list(".", "|") // ...|..||.||||.|.||.|.|.|||.||| space_chance = 0 diff --git a/code/modules/language/machine.dm b/code/modules/language/machine.dm index 63bec30c6f8..ab08d141237 100644 --- a/code/modules/language/machine.dm +++ b/code/modules/language/machine.dm @@ -3,7 +3,7 @@ desc = "An efficient language of encoded tones developed by synthetics and cyborgs." spans = list(SPAN_ROBOT) key = "6" - flags = NO_STUTTER + flags = LANGUAGE_NO_STUTTER syllables = list( "beep", "beep", "beep", "beep", "beep", "boop", "boop", "boop", "bop", "bop", "dee", "dee", "doo", "doo", "hiss", "hss", "buzz", diff --git a/code/modules/language/spinwarder.dm b/code/modules/language/spinwarder.dm index 8c3877f38c0..7e4f12f738c 100644 --- a/code/modules/language/spinwarder.dm +++ b/code/modules/language/spinwarder.dm @@ -10,7 +10,7 @@ between_word_space_chance = 75 additional_syllable_low = 0 additional_syllable_high = 0 - flags = TONGUELESS_SPEECH + flags = LANGUAGE_TONGUELESS_SPEECH syllables = list( "v", "od", "noy", "ned", "ele", "dn", "ey", "da", "ny", "et", "mes", "yat", "che", "bu", "pol", "ye", "eli", "nyy", "se", "god", "ya", "dom", "ash", "zav", diff --git a/code/modules/language/uncommon.dm b/code/modules/language/uncommon.dm index 43b878e8341..060f6c02cb1 100644 --- a/code/modules/language/uncommon.dm +++ b/code/modules/language/uncommon.dm @@ -2,7 +2,7 @@ name = "Galactic Uncommon" desc = "The second-most spoken Human language." key = "!" - flags = TONGUELESS_SPEECH + flags = LANGUAGE_TONGUELESS_SPEECH space_chance = 20 sentence_chance = 0 between_word_sentence_chance = 10 diff --git a/code/modules/mob/living/carbon/carbon_say.dm b/code/modules/mob/living/carbon/carbon_say.dm index e1dc3d27992..35ed43d8fde 100644 --- a/code/modules/mob/living/carbon/carbon_say.dm +++ b/code/modules/mob/living/carbon/carbon_say.dm @@ -12,4 +12,4 @@ // the tower of babel needs to bypass the tongue language restrictions without giving omnitongue return HAS_MIND_TRAIT(src, TRAIT_TOWER_OF_BABEL) || spoken_with.could_speak_language(language_path) - return initial(language_path.flags) & TONGUELESS_SPEECH + return initial(language_path.flags) & LANGUAGE_TONGUELESS_SPEECH diff --git a/icons/ui/chat/language.dmi b/icons/ui/chat/language.dmi index 7cb84ee3af9..3a8a6148f44 100644 Binary files a/icons/ui/chat/language.dmi and b/icons/ui/chat/language.dmi differ