From 371e079e44b05e44ac5a4f24a72b939ccdaeb2be Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Wed, 11 Dec 2024 19:52:57 -0600 Subject: [PATCH 1/6] Silverscale blood runtime fixes and tweaks (#628) From 06ad29ced93bfe944a08e09cd4563bc4f4311c1d Mon Sep 17 00:00:00 2001 From: Blacklist897 <149209377+Blacklist897@users.noreply.github.com> Date: Mon, 23 Dec 2024 07:27:38 +1300 Subject: [PATCH 2/6] localnode has been repaired (#634) With oswalds apparent death, localnode has convinced shrike to allow it to merge with the lavaland orbit fragment (better desc, updated sprites) --- .../localnode_equipment/localnode_core.dm | 2 +- .../localnode_equipment/sprites/localnode.dmi | Bin 1259 -> 1237 bytes .../sprites/localnode_inhand_lh.dmi | Bin 1495 -> 1544 bytes .../sprites/localnode_inhand_rh.dmi | Bin 1499 -> 1480 bytes 4 files changed, 1 insertion(+), 1 deletion(-) diff --git a/maplestation_modules/story_content/localnode_equipment/localnode_core.dm b/maplestation_modules/story_content/localnode_equipment/localnode_core.dm index 206bbfd19e02..34f4d9d19818 100644 --- a/maplestation_modules/story_content/localnode_equipment/localnode_core.dm +++ b/maplestation_modules/story_content/localnode_equipment/localnode_core.dm @@ -6,7 +6,7 @@ /obj/item/localnode name = "LocalNode#4248" - desc = "It looks like a basketball sized blue orb, however it looks like it had bits broken off with a hammer a few times before covering it with superglue and rolling it in a box of computer parts" + desc = "A strange blue orb, humming with alien power and smelling strongly of ozone" icon = 'maplestation_modules/story_content/localnode_equipment/sprites/localnode.dmi' icon_state = "localnode" lefthand_file = 'maplestation_modules/story_content/localnode_equipment/sprites/localnode_inhand_lh.dmi' diff --git a/maplestation_modules/story_content/localnode_equipment/sprites/localnode.dmi b/maplestation_modules/story_content/localnode_equipment/sprites/localnode.dmi index 9702c29c532fedc513f976e0ce951c138690b699..d576678b9131bac2b86f53300188e673cf626bf3 100644 GIT binary patch delta 1217 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 QdjJ3c07*qoM6N<$g5A5s;Q#;t delta 1481 zcmV;)1vdJ~3)>5jB!6*wR9JLGWpiV4X>fFDZ*BkpcmXAhISzmz5CCiT6&vl1S8HQ5 zhC=>8WEB#G1c-iL)NW>qDRG<^E^t~oToj`54hQV38rW@A(pkC|m?m@R^h-=KQO3gh zOZKyQCYx(h6YZ000FgNq4<7KJ{7!f;Qo5F$^$;64q;Fwv|Xdt>VC2>U5Ka7uTQ|J5x*>u4{ zF>Z5PdHR?`5+;wKBG8rbDzJ_68Y?Y#=bUj#*%nIZUE0F=lIGld&rN&2-#Pc()7~<$ z!h@jz6o3Ly0DlTV0Vn_kpa2wr0xT(9T;165u~EB)G~4i??W_ow>Z6SQZuP#t{_Ma& z@A3ga2@7TuHZ1l{>=|LM(Y3X;@bWha4ro&ZGm{?CPwM-19hc`$bPo=IhBF>+GkTp^ z^P|VGtpYHkxi8$UM~rc6-JK^_Ws&emH;uCGT5tgN*1h* z-yh33inOdzKh7c8^d*p3!EQ}UGY^0xsZjf((>kx>hTH&1+7$*#x;`=j3X5ByYAAi~ zJdA*f&VOYGfFt4E9#)a=4_~E7=MavZRg=0Z_On|6p&7BjF#VcR_ikIC1Mq%oe#zni z@F;lfDevNtD1Z;2-3WV*S3|}3BOto84JuC#f>skwMgTkUPk;BRxNe;M_{134%35F^ zfSKOE0|3thzzBtc#^BMkmy$uA_azkXYaw-Bf`2@~6JzUne*$1M8m4;6S2x+(@W%@? z7iU+%ouc?aw_x(~0MK1Phzd!%Je$mXs^8QQ`b;^Q{Rf&-VX)pHtB3y*<`)_U+n+aoi&3!|D7@;-Y`c(DNZ`v6`Ri&^6oFpiQL^xa@$IVHXz z02hAAbz-sSBce;koW_=8@@4^6LQnt-Kz{)!00p1`6o3Ly017|>C;$bZ0Cp6CLBXs> z-C=S2?FL}$7i$9O*MPnyZHJsma(v5 zIQhuCR5kO3fRoSD$93WDf|29k=Ix)30F+h*FzNM3^fXz8o+FT*guwTp^58{44}T_- z5kMthJ&ypC>i%&klQ)y&HLrSrwE>cFiIaB=T?3ih81e{B@#xp2{*U(kVF;j&2Y{#1 zXDEd~ttb0VyT{<~>IS%0nrwXlF8PNZU_dhT8ho624Bq+SIjA|e6GW>c;t{~_d;pv{ zbXW<)b&&eWcO?0q`ety}=0z;jp(fd7ty1229|s>zCl?30HqNSt>7 jdkqRe0Vn_kSkZA4_PUQ>kLy-e00000NkvXXu0mjf#oM^L From 43fe884ddc922d422e4b8ef2b99bf141708fb255 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:28:24 -0600 Subject: [PATCH 3/6] Adds partial language understanding (#630) --- code/_globalvars/lists/flavor_misc.dm | 9 + code/controllers/subsystem/discord.dm | 7 +- code/datums/brain_damage/mild.dm | 4 +- code/game/atoms_movable.dm | 4 + .../telecomms/computers/logbrowser.dm | 2 +- code/game/say.dm | 2 +- code/modules/language/_language.dm | 191 ++++++++++++++---- code/modules/language/_language_holder.dm | 12 ++ code/modules/language/beachbum.dm | 5 + code/modules/language/codespeak.dm | 10 +- code/modules/language/common.dm | 5 + code/modules/language/uncommon.dm | 5 + .../clock_cult/clock_language.dm | 2 +- .../code/modules/language/highdraconic.dm | 18 +- 14 files changed, 207 insertions(+), 69 deletions(-) diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm index 38072aaec01e..cf803d1c4519 100644 --- a/code/_globalvars/lists/flavor_misc.dm +++ b/code/_globalvars/lists/flavor_misc.dm @@ -307,3 +307,12 @@ GLOBAL_LIST_INIT(status_display_state_pictures, list( "blank", "shuttle", )) + +GLOBAL_LIST_INIT(most_common_words, init_common_words()) + +/proc/init_common_words() + . = list() + var/i = 1 + for(var/word in world.file2list("strings/1000_most_common.txt")) + .[word] = i + i += 1 diff --git a/code/controllers/subsystem/discord.dm b/code/controllers/subsystem/discord.dm index 7efdbfcda6a5..ccfa60e09c5f 100644 --- a/code/controllers/subsystem/discord.dm +++ b/code/controllers/subsystem/discord.dm @@ -43,9 +43,6 @@ SUBSYSTEM_DEF(discord) /// People who have tried to verify this round already var/list/reverify_cache - /// Common words list, used to generate one time tokens - var/list/common_words - /// The file where notification status is saved var/notify_file = file("data/notify.json") @@ -53,7 +50,6 @@ SUBSYSTEM_DEF(discord) var/enabled = FALSE /datum/controller/subsystem/discord/Initialize() - common_words = world.file2list("strings/1000_most_common.txt") reverify_cache = list() // Check for if we are using TGS, otherwise return and disables firing if(world.TgsAvailable()) @@ -156,7 +152,7 @@ SUBSYSTEM_DEF(discord) // While there's a collision in the token, generate a new one (should rarely happen) while(not_unique) //Column is varchar 100, so we trim just in case someone does us the dirty later - one_time_token = trim("[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]-[pick(common_words)]", 100) + one_time_token = trim("[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]", 100) not_unique = find_discord_link_by_token(one_time_token, timebound = TRUE) @@ -298,4 +294,3 @@ SUBSYSTEM_DEF(discord) if (length(discord_mention_extraction_regex.group) == 1) return discord_mention_extraction_regex.group[1] return null - diff --git a/code/datums/brain_damage/mild.dm b/code/datums/brain_damage/mild.dm index 513f56840b56..97001f177f19 100644 --- a/code/datums/brain_damage/mild.dm +++ b/code/datums/brain_damage/mild.dm @@ -191,8 +191,6 @@ gain_text = span_warning("You lose your grasp on complex words.") lose_text = span_notice("You feel your vocabulary returning to normal again.") - var/static/list/common_words = world.file2list("strings/1000_most_common.txt") - /datum/brain_trauma/mild/expressive_aphasia/handle_speech(datum/source, list/speech_args) var/message = speech_args[SPEECH_MESSAGE] if(message) @@ -212,7 +210,7 @@ word = copytext(word, 1, suffix_foundon) word = html_decode(word) - if(lowertext(word) in common_words) + if(GLOB.most_common_words[lowertext(word)]) new_message += word + suffix else if(prob(30) && message_split.len > 2) diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 71bfdfcab4df..87c5394d09a1 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1521,6 +1521,10 @@ /atom/movable/proc/get_random_understood_language() return get_language_holder().get_random_understood_language() +/// Gets a list of all mutually understood languages. +/atom/movable/proc/get_mutually_understood_languages() + return get_language_holder().get_mutually_understood_languages() + /// Gets a random spoken language, useful for forced speech and such. /atom/movable/proc/get_random_spoken_language() return get_language_holder().get_random_spoken_language() diff --git a/code/game/machinery/telecomms/computers/logbrowser.dm b/code/game/machinery/telecomms/computers/logbrowser.dm index e202a508ecf0..546262b044a7 100644 --- a/code/game/machinery/telecomms/computers/logbrowser.dm +++ b/code/game/machinery/telecomms/computers/logbrowser.dm @@ -59,7 +59,7 @@ message_out = "\"[message_in]\"" else if(!user.has_language(language)) // Language unknown: scramble - message_out = "\"[language_instance.scramble(message_in)]\"" + message_out = "\"[language_instance.scramble_sentence(message_in, user.get_mutually_understood_languages())]\"" else message_out = "(Unintelligible)" packet_out["message"] = message_out diff --git a/code/game/say.dm b/code/game/say.dm index 3a8eb748b6b1..0075e0d2a801 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -213,7 +213,7 @@ GLOBAL_LIST_INIT(freqtospan, list( if(!has_language(language)) var/datum/language/dialect = GLOB.language_datum_instances[language] - raw_message = dialect.scramble(raw_message) + raw_message = dialect.scramble_sentence(raw_message, get_mutually_understood_languages()) return raw_message diff --git a/code/modules/language/_language.dm b/code/modules/language/_language.dm index 3876720cbd44..f68405920c24 100644 --- a/code/modules/language/_language.dm +++ b/code/modules/language/_language.dm @@ -1,5 +1,7 @@ -/// maximum of 50 specific scrambled lines per language +/// Last 50 spoken (uncommon) words will be cached before we start cycling them out (re-randomizing them) #define SCRAMBLE_CACHE_LEN 50 +/// Last 20 spoken sentences will be cached before we start cycling them out (re-randomizing them) +#define SENTENCE_CACHE_LEN 20 /// Datum based languages. Easily editable and modular. /datum/language @@ -18,13 +20,23 @@ var/list/special_characters /// Likelihood of making a new sentence after each syllable. var/sentence_chance = 5 + /// Likelihood of making a new sentence after each word. + var/between_word_sentence_chance = 0 /// Likelihood of getting a space in the random scramble string var/space_chance = 55 + /// Likelyhood of getting a space between words + var/between_word_space_chance = 100 /// Spans to apply from this language var/list/spans /// Cache of recently scrambled text /// This allows commonly reused words to not require a full re-scramble every time. var/list/scramble_cache = list() + /// Cache of recently spoken sentences + /// So if one person speaks over the radio, everyone hears the same thing. + var/list/last_sentence_cache = list() + /// The 1000 most common words get permanently cached + var/list/most_common_cache = list() + /// The language that an atom knows with the highest "default_priority" is selected by default. var/default_priority = 0 /// If TRUE, when generating names, we will always use the default human namelist, even if we have syllables set. @@ -45,6 +57,11 @@ /// What char to place in between randomly generated names var/random_name_spacer = " " + /// Assoc Lazylist of other language types that would have a degree of mutual understanding with this language. + /// For example, you could do `list(/datum/language/common = 50)` to say that this language has a 50% chance to understand common words + /// And yeah if you give a 100% chance, they can basically just understand the language + var/list/mutual_understanding + /// 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) @@ -109,56 +126,144 @@ return result -/datum/language/proc/check_cache(input) - var/lookup = scramble_cache[input] - if(lookup) +/// Checks the word cache for a word +/datum/language/proc/read_word_cache(input) + SHOULD_NOT_OVERRIDE(TRUE) + if(most_common_cache[input]) + return most_common_cache[input] + + . = scramble_cache[input] + if(. && scramble_cache[1] != input) + // bumps it to the top of the cache scramble_cache -= input - scramble_cache[input] = lookup - . = lookup + scramble_cache[input] = . + return . -/datum/language/proc/add_to_cache(input, scrambled_text) +/// Adds a word to the cache +/datum/language/proc/write_word_cache(input, scrambled_text) + SHOULD_NOT_OVERRIDE(TRUE) + if(GLOB.most_common_words[lowertext(input)]) + most_common_cache[input] = scrambled_text + return // Add it to cache, cutting old entries if the list is too long scramble_cache[input] = scrambled_text if(scramble_cache.len > SCRAMBLE_CACHE_LEN) - scramble_cache.Cut(1, scramble_cache.len-SCRAMBLE_CACHE_LEN-1) + scramble_cache.Cut(1, scramble_cache.len - SCRAMBLE_CACHE_LEN + 1) -/datum/language/proc/scramble(input) +/// Checks the sentence cache for a sentence +/datum/language/proc/read_sentence_cache(input) + SHOULD_NOT_OVERRIDE(TRUE) + . = last_sentence_cache[input] + if(. && last_sentence_cache[1] != input) + // bumps it to the top of the cache (don't anticipate this happening often) + last_sentence_cache -= input + last_sentence_cache[input] = . + return . - if(!length(syllables)) - return stars(input) +/// Adds a sentence to the cache, though the sentence should be modified with a key +/datum/language/proc/write_sentence_cache(input, key, result_scramble) + SHOULD_NOT_OVERRIDE(TRUE) + // Add to the cache (the cache being an assoc list of assoc lists), cutting old entries if the list is too long + LAZYSET(last_sentence_cache[input], key, result_scramble) + if(last_sentence_cache.len > SENTENCE_CACHE_LEN) + last_sentence_cache.Cut(1, last_sentence_cache.len - SENTENCE_CACHE_LEN + 1) + +/// Goes through the input and removes any punctuation from the end of the string. +/proc/strip_punctuation(input) + var/static/list/bad_punctuation = list("!", "?", ".", "~", ";", ":", "-") + var/last_char = copytext_char(input, -1) + while(last_char in bad_punctuation) + input = copytext(input, 1, -1) + last_char = copytext_char(input, -1) + + return trim_right(input) + +/// Find what punctuation is at the end of the input, returns it. +/proc/find_last_punctuation(input) + . = copytext_char(input, -3) + if(. == "...") + return . + . = copytext_char(input, -2) + if(. in list("!!", "??", "..", "?!", "!?")) + return . + . = copytext_char(input, -1) + if(. in list("!", "?" ,".", "~", ";", ":", "-")) + return . + return "" + +/// Scrambles a sentence in this language. +/// Takes into account any languages the hearer knows that has mutual understanding with this language. +/datum/language/proc/scramble_sentence(input, list/mutual_languages) + var/cache_key = "[mutual_languages?[type] || 0]-understanding" + var/list/cache = read_sentence_cache(cache_key) + if(cache?[cache_key]) + return cache[cache_key] + + var/list/real_words = splittext(input, " ") + var/list/scrambled_words = list() + for(var/word in real_words) + var/translate_prob = mutual_languages?[type] || 0 + if(translate_prob > 0) + var/base_word = lowertext(strip_punctuation(word)) + // the probability of managing to understand a word is based on how common it is + // 1000 words in the list, so words outside the list are just treated as "the 1500th most common word" + var/commonness = GLOB.most_common_words[base_word] || 1500 + translate_prob += (translate_prob * 0.2 * (1 - (min(commonness, 1500) / 500))) + if(prob(translate_prob)) + scrambled_words += base_word + continue + + scrambled_words += scramble_word(word) + // start building the word. first word is capitalized and otherwise untouched + . = capitalize(popleft(scrambled_words)) + for(var/word in scrambled_words) + if(prob(between_word_sentence_chance)) + . += ". " + else if(prob(between_word_space_chance)) + . += " " + + . += word + + // scrambling the words will drop punctuation, so re-add it at the end + . += find_last_punctuation(trim_right(input)) + + write_sentence_cache(input, cache_key, .) + + return . + +/// Scrambles a single word in this language. +/datum/language/proc/scramble_word(input) // If the input is cached already, move it to the end of the cache and return it - var/lookup = check_cache(input) - if(lookup) - return lookup - - var/input_size = length_char(input) - var/scrambled_text = "" - var/capitalize = TRUE - - while(length_char(scrambled_text) < input_size) - var/next = (length(scrambled_text) && length(special_characters) && prob(1)) ? pick(special_characters) : pick_weight_recursive(syllables) - if(capitalize) - next = capitalize(next) - capitalize = FALSE - scrambled_text += next - var/chance = rand(100) - if(chance <= sentence_chance) - scrambled_text += ". " - capitalize = TRUE - else if(chance > sentence_chance && chance <= space_chance) - scrambled_text += " " - - scrambled_text = trim(scrambled_text) - var/ending = copytext_char(scrambled_text, -1) - if(ending == ".") - scrambled_text = copytext_char(scrambled_text, 1, -2) - var/input_ending = copytext_char(input, -1) - if(input_ending in list("!","?",".")) - scrambled_text += input_ending - - add_to_cache(input, scrambled_text) - - return scrambled_text + . = read_word_cache(input) + if(.) + return . + + if(!length(syllables)) + . = stars(input) + + else + var/input_size = length_char(input) + var/add_space = FALSE + var/add_period = FALSE + . = "" + while(length_char(.) < input_size) + // add in the last syllable's period or space first + if(add_period) + . += ". " + else if(add_space) + . += " " + // generate the next syllable (capitalize if we just added a period) + var/next = (. && length(special_characters) && prob(1)) ? pick(special_characters) : pick_weight_recursive(syllables) + if(add_period) + next = capitalize(next) + . += next + // determine if the next syllable gets a period or space + add_period = prob(sentence_chance) + add_space = prob(space_chance) + + write_word_cache(input, .) + + return . #undef SCRAMBLE_CACHE_LEN diff --git a/code/modules/language/_language_holder.dm b/code/modules/language/_language_holder.dm index b48a1ab1530a..f061ed2bab35 100644 --- a/code/modules/language/_language_holder.dm +++ b/code/modules/language/_language_holder.dm @@ -176,6 +176,18 @@ Key procs /datum/language_holder/proc/get_random_understood_language() return pick(understood_languages) +/// Gets a list of all mutually understood languages. +/datum/language_holder/proc/get_mutually_understood_languages() + var/list/mutual_languages = list() + for(var/language_type in understood_languages) + var/datum/language/language_instance = GLOB.language_datum_instances[language_type] + for(var/mutual_language_type in language_instance.mutual_understanding) + // add it to the list OR override it if it's a stronger mutual understanding + if(!mutual_languages[mutual_language_type] || mutual_languages[mutual_language_type] < language_instance.mutual_understanding[mutual_language_type]) + mutual_languages[mutual_language_type] = language_instance.mutual_understanding[mutual_language_type] + + return mutual_languages + /// Gets a random spoken language, useful for forced speech and such. /datum/language_holder/proc/get_random_spoken_language() return pick(spoken_languages) diff --git a/code/modules/language/beachbum.dm b/code/modules/language/beachbum.dm index bd319e717ffd..eb2447ded187 100644 --- a/code/modules/language/beachbum.dm +++ b/code/modules/language/beachbum.dm @@ -19,3 +19,8 @@ ) icon_state = "beach" always_use_default_namelist = TRUE + + mutual_understanding = list( + /datum/language/common = 50, + /datum/language/uncommon = 30, + ) diff --git a/code/modules/language/codespeak.dm b/code/modules/language/codespeak.dm index 242095b3bb7f..7c2657c7b285 100644 --- a/code/modules/language/codespeak.dm +++ b/code/modules/language/codespeak.dm @@ -7,10 +7,10 @@ icon_state = "codespeak" always_use_default_namelist = TRUE // No syllables anyways -/datum/language/codespeak/scramble(input) - var/lookup = check_cache(input) - if(lookup) - return lookup +/datum/language/codespeak/scramble_sentence(input, list/mutual_languages) + . = read_word_cache(input) + if(.) + return . . = "" var/list/words = list() @@ -29,4 +29,4 @@ if(input_ending in endings) . += input_ending - add_to_cache(input, .) + write_word_cache(input, .) diff --git a/code/modules/language/common.dm b/code/modules/language/common.dm index 6bad808fef26..764375c4a0d3 100644 --- a/code/modules/language/common.dm +++ b/code/modules/language/common.dm @@ -55,3 +55,8 @@ "his", "ing", "ion", "ith", "not", "ome", "oul", "our", "sho", "ted", "ter", "tha", "the", "thi", ), ) + + mutual_understanding = list( + /datum/language/beachbum = 33, + /datum/language/uncommon = 20, + ) diff --git a/code/modules/language/uncommon.dm b/code/modules/language/uncommon.dm index 117ed1c76fd1..58c1d5bba2cb 100644 --- a/code/modules/language/uncommon.dm +++ b/code/modules/language/uncommon.dm @@ -14,3 +14,8 @@ ) icon_state = "galuncom" default_priority = 90 + + mutual_understanding = list( + /datum/language/common = 33, + /datum/language/beachbum = 20, + ) diff --git a/maplestation_modules/code/modules/antagonists/advanced_cult/clock_cult/clock_language.dm b/maplestation_modules/code/modules/antagonists/advanced_cult/clock_cult/clock_language.dm index 1b14aed539cd..68a1c4bcf9da 100644 --- a/maplestation_modules/code/modules/antagonists/advanced_cult/clock_cult/clock_language.dm +++ b/maplestation_modules/code/modules/antagonists/advanced_cult/clock_cult/clock_language.dm @@ -11,7 +11,7 @@ spans = list(SPAN_ROBOT) icon_state = "ratvar" -/datum/language/ratvarian/scramble(input) +/datum/language/ratvarian/scramble_sentence(input, list/mutual_languages) return text2ratvar(input) /// Regexes used to add ratvarian styling to rot13 english diff --git a/maplestation_modules/code/modules/language/highdraconic.dm b/maplestation_modules/code/modules/language/highdraconic.dm index b671914981b3..6c1e2a6cb57a 100644 --- a/maplestation_modules/code/modules/language/highdraconic.dm +++ b/maplestation_modules/code/modules/language/highdraconic.dm @@ -17,18 +17,18 @@ icon_state = "lizardred" default_priority = 85 -// So I wrote a few unit tests for /tg/ that rely on Lizards not knowing what high draconic is. -// And since rewriting them is out of the questions, Lizards don't know high draconic in unit tests. -#ifndef UNIT_TESTS + mutual_understanding = list( + /datum/language/draconic = 66, + ) -// Edit to the base lizard language holder - lizards can understand high draconic. -/datum/language_holder/lizard - understood_languages = list( - /datum/language/common = list(LANGUAGE_ATOM), - /datum/language/draconic = list(LANGUAGE_ATOM), - /datum/language/impdraconic = list(LANGUAGE_ATOM), +/datum/language/draconic + mutual_understanding = list( + /datum/language/impdraconic = 66, ) +// TG unit test compliance (out of laziness) +#ifndef UNIT_TESTS + // Edit to the silverscale language holder - silverscales can speak high draconic. /datum/language_holder/lizard/silver understood_languages = list( From 92af1259d34743c6e4e6e53284073891b708c16d Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Sun, 22 Dec 2024 12:31:19 -0600 Subject: [PATCH 4/6] Language Menu (like the one in the preferences) Rework (#631) --- code/_globalvars/lists/mobs.dm | 2 +- .../subsystem/processing/quirks.dm | 2 +- .../datums/quirks/neutral_quirks/foreigner.dm | 2 + .../quirks/positive_quirks/bilingual.dm | 2 + code/game/atoms_movable.dm | 2 +- code/modules/client/preferences/language.dm | 2 + code/modules/client/preferences_savefile.dm | 5 +- code/modules/mob/living/living_say.dm | 3 + .../code/datums/quirks/good.dm | 9 +- .../modules/client/preferences/languages.dm | 344 +++++++------ .../code/modules/language/isatoan.dm | 1 - .../code/modules/language/japanese.dm | 2 +- .../PreferencesMenu/_LanguagePicker.tsx | 460 +++++++++++++----- .../tgui/interfaces/PreferencesMenu/data.ts | 4 +- 14 files changed, 568 insertions(+), 272 deletions(-) diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm index 8d35f2192062..3f1ffe94f02e 100644 --- a/code/_globalvars/lists/mobs.dm +++ b/code/_globalvars/lists/mobs.dm @@ -90,7 +90,7 @@ GLOBAL_LIST_INIT_TYPED(language_datum_instances, /datum/language, init_language_ /// List if all language typepaths learnable, IE, those with keys GLOBAL_LIST_INIT(all_languages, init_all_languages()) // /List of language prototypes to reference, assoc "name" = typepath -GLOBAL_LIST_INIT(language_types_by_name, init_language_types_by_name()) +GLOBAL_LIST_INIT_TYPED(language_types_by_name, /datum/language, init_language_types_by_name()) /proc/init_language_prototypes() var/list/lang_list = list() diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index 79f03a1a8506..bd9785551edf 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -19,7 +19,7 @@ GLOBAL_LIST_INIT_TYPED(quirk_blacklist, /list/datum/quirk, list( list(/datum/quirk/social_anxiety, /datum/quirk/mute), list(/datum/quirk/mute, /datum/quirk/softspoken), list(/datum/quirk/poor_aim, /datum/quirk/bighands), - list(/datum/quirk/bilingual, /datum/quirk/foreigner), + // list(/datum/quirk/bilingual, /datum/quirk/foreigner), list(/datum/quirk/spacer_born, /datum/quirk/item_quirk/settler), list(/datum/quirk/photophobia, /datum/quirk/nyctophobia), list(/datum/quirk/item_quirk/settler, /datum/quirk/freerunning), diff --git a/code/datums/quirks/neutral_quirks/foreigner.dm b/code/datums/quirks/neutral_quirks/foreigner.dm index da317a7e66a4..1d252efc920e 100644 --- a/code/datums/quirks/neutral_quirks/foreigner.dm +++ b/code/datums/quirks/neutral_quirks/foreigner.dm @@ -1,3 +1,4 @@ +/* /datum/quirk/foreigner name = "Foreigner" desc = "You're not from around here. You don't know Galactic Common!" @@ -19,3 +20,4 @@ human_holder.remove_blocked_language(/datum/language/common) if(ishumanbasic(human_holder)) human_holder.remove_language(/datum/language/uncommon) +*/ diff --git a/code/datums/quirks/positive_quirks/bilingual.dm b/code/datums/quirks/positive_quirks/bilingual.dm index 408a952cfe18..13c75d57bdec 100644 --- a/code/datums/quirks/positive_quirks/bilingual.dm +++ b/code/datums/quirks/positive_quirks/bilingual.dm @@ -1,3 +1,4 @@ +/* /datum/quirk/bilingual name = "Bilingual" desc = "Over the years you've picked up an extra language!" @@ -26,3 +27,4 @@ return to_chat(quirk_holder, span_boldnotice("You are already familiar with the quirk in your preferences, so you learned Galactic Uncommon instead.")) quirk_holder.grant_language(language_type, source = LANGUAGE_QUIRK) +*/ diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 87c5394d09a1..dfc72301fff0 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1467,7 +1467,7 @@ */ /// Gets or creates the relevant language holder. For mindless atoms, gets the local one. For atom with mind, gets the mind one. -/atom/movable/proc/get_language_holder() +/atom/movable/proc/get_language_holder() as /datum/language_holder RETURN_TYPE(/datum/language_holder) if(QDELING(src)) CRASH("get_language_holder() called on a QDELing atom, \ diff --git a/code/modules/client/preferences/language.dm b/code/modules/client/preferences/language.dm index 637c4542da27..9879f4c73de9 100644 --- a/code/modules/client/preferences/language.dm +++ b/code/modules/client/preferences/language.dm @@ -1,3 +1,4 @@ +/* /datum/preference/choiced/language category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_key = "language" @@ -33,3 +34,4 @@ /datum/preference/choiced/language/apply_to_human(mob/living/carbon/human/target, value) return +*/ diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index c97a26c5396f..6a7dcaa119e1 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -5,7 +5,7 @@ // You do not need to raise this if you are adding new values that have sane defaults. // Only raise this value when changing the meaning/format/name/layout of an existing value // where you would want the updater procs below to run -#define SAVEFILE_VERSION_MAX 45 +#define SAVEFILE_VERSION_MAX 45.1 /* SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn @@ -111,6 +111,9 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car data_to_migrate = list(INFO_RESKIN = save_data?["pride_pin"]), ) + if (current_version < 45.1) + migrate_quirks_to_language_menu(save_data) + /// checks through keybindings for outdated unbound keys and updates them /datum/preferences/proc/check_keybindings() if(!parent) diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index 747d2154c7ae..f5f67a39b620 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -158,6 +158,9 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( return language = message_mods[LANGUAGE_EXTENSION] || get_selected_language() + if(!language) + message_mods[MODE_CUSTOM_SAY_EMOTE] ||= "makes \a [pick("strange", "weird", "bizarre", "peculiar", "odd", "unusual", "curious")] sound" + message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] = TRUE var/succumbed = FALSE diff --git a/maplestation_modules/code/datums/quirks/good.dm b/maplestation_modules/code/datums/quirks/good.dm index a1c65f47beb2..a205c49834fd 100644 --- a/maplestation_modules/code/datums/quirks/good.dm +++ b/maplestation_modules/code/datums/quirks/good.dm @@ -4,14 +4,8 @@ /datum/quirk/jolly value = 3 -/datum/quirk/bilingual - icon = FA_ICON_GLOBE_EUROPE - value = 0 - desc = "Over the years you've picked up an extra language! \ - (Made redundant by the Language Picker - use it instead.)" - medical_record_text = "Patient is bilingual speaks multiple languages." - // New quirks +/* /// Trilingual quirk - Gives the owner a language. /datum/quirk/trilingual name = "Trilingual" @@ -59,6 +53,7 @@ /datum/quirk/trilingual/remove() if(added_language && !QDELETED(quirk_holder)) quirk_holder.get_language_holder().remove_language(added_language, ALL, LANGUAGE_QUIRK) +*/ /datum/quirk/no_appendix name = "Appendicitis Survivor" diff --git a/maplestation_modules/code/modules/client/preferences/languages.dm b/maplestation_modules/code/modules/client/preferences/languages.dm index 1254cc2e0452..b14600bdb78d 100644 --- a/maplestation_modules/code/modules/client/preferences/languages.dm +++ b/maplestation_modules/code/modules/client/preferences/languages.dm @@ -1,191 +1,247 @@ // -- Language preference and UI. -/// Simple define to denote no language. -#define NO_LANGUAGE "No Language" - -/datum/preference/choiced/language - savefile_key = "bilingual_language" - -// Stores a typepath of a language, or "No language" when passed a null / invalid language. -/datum/preference/additional_language +// These defines are used in the UI be careful updating them. +#define ADD_SPOKEN_LANGUAGE "Add spoken language" +#define ADD_UNDERSTOOD_LANGUAGE "Add understood language" +#define REMOVE_SPOKEN_LANGUAGE "Remove spoken language" +#define REMOVE_UNDERSTOOD_LANGUAGE "Remove understood language" + +/datum/preferences/proc/migrate_quirks_to_language_menu(list/save_data) + var/datum/preference_middleware/language/update = locate() in middleware + var/datum/preference/languages/language_pref = GLOB.preference_entries[/datum/preference/languages] + + // random quirks + if("Bilingual" in all_quirks) + var/picked_lang = GLOB.language_types_by_name[save_data["bilingual_language"]]?.type + if(picked_lang && (picked_lang in language_pref.selectable_languages)) + update.add_language_to_user(picked_lang, ADD_SPOKEN_LANGUAGE) + update.add_language_to_user(picked_lang, ADD_UNDERSTOOD_LANGUAGE) + + if("Trilingual" in all_quirks) + pass() // nothing to do about this + + if("Foreigner" in all_quirks) + update.add_language_to_user(/datum/language/uncommon, ADD_SPOKEN_LANGUAGE) + update.add_language_to_user(/datum/language/uncommon, ADD_UNDERSTOOD_LANGUAGE) + update.add_language_to_user(/datum/language/common, REMOVE_SPOKEN_LANGUAGE) + update.add_language_to_user(/datum/language/common, REMOVE_UNDERSTOOD_LANGUAGE) + + // the old prefs + var/other_lang = text2path(save_data["language"]) + if(other_lang && (other_lang in language_pref.selectable_languages)) + update.add_language_to_user(other_lang, ADD_SPOKEN_LANGUAGE) + update.add_language_to_user(other_lang, ADD_UNDERSTOOD_LANGUAGE) + +/datum/preference/languages savefile_key = "language" savefile_identifier = PREFERENCE_CHARACTER priority = PREFERENCE_PRIORITY_NAMES // needs to happen after species, so name works can_randomize = FALSE -/datum/preference/additional_language/deserialize(input, datum/preferences/preferences) - if(input == NO_LANGUAGE) - return NO_LANGUAGE - if("Trilingual" in preferences.all_quirks) - return NO_LANGUAGE - if("Bilingual" in preferences.all_quirks) - return NO_LANGUAGE - - var/datum/language/lang_to_add = check_input_path(input) - if(!ispath(lang_to_add, /datum/language)) - return NO_LANGUAGE - - var/datum/species/species = preferences.read_preference(/datum/preference/choiced/species) - var/banned = initial(lang_to_add.banned_from_species) - var/req = initial(lang_to_add.required_species) - if((banned && ispath(species, banned)) || (req && !ispath(species, req))) - return NO_LANGUAGE - - return lang_to_add - -/datum/preference/additional_language/serialize(input) - return check_input_path(input) || NO_LANGUAGE - -/datum/preference/additional_language/create_default_value() - return NO_LANGUAGE - -/datum/preference/additional_language/is_valid(value) - return !!check_input_path(value) - -/// Checks if our passed input is valid -/// Returns NO LANGUAGE if passed NO LANGUAGE (truthy value) -/// Returns null if the input was invalid (falsy value) -/// Returns a language typepath if the input was a valid path (truthy value) -/datum/preference/additional_language/proc/check_input_path(input) - if(input == NO_LANGUAGE) - return NO_LANGUAGE - - var/path_form = istext(input) ? text2path(input) : input - // sometimes we deserialize with a text string that is a path, as they're saved as string in our json save - // other times we recieve a full typepath, likely from write preference - // we need to support either case just to be inclusive, so here we are var/path_form = istext(input) ? text2path(input) : input - if(!ispath(path_form, /datum/language)) - return null + /// List of languages you can pick. + /// You only need to add languagues here that are not spoken by selectable roundstart species. + var/list/selectable_languages = list( + /datum/language/common, + /datum/language/impdraconic, + /datum/language/isatoa, + /datum/language/piratespeak, + /datum/language/shadowtongue, + /datum/language/uncommon, + /datum/language/yangyu, + // these should be auto filled + /datum/language/moffic, + /datum/language/nekomimetic, + /datum/language/draconic, + /datum/language/skrell, + // these are iffy + /datum/language/voltaic, + /datum/language/calcic, + ) + /// Languages not rendered in the UI under any circumstances. + var/list/dont_show_languages = list( + /datum/language/aphasia, + /datum/language/codespeak, + /datum/language/drone, + /datum/language/xenocommon, + ) + /// Max # of languages you can add to your character. + var/max_spoken_languages = 1 + /// Max # of languages you can understand. + var/max_understood_languages = 2 - var/datum/language/lang_instance = GLOB.language_datum_instances[path_form] - // MAYBE accessed when language datums aren't created so this is just a sanity check - if(istype(lang_instance) && !lang_instance.available_as_pref) +/datum/preference/languages/create_default_value() + return null + +/datum/preference/languages/deserialize(list/input, datum/preferences/preferences) + if(!islist(input)) return null - return path_form + var/datum/species/species = GLOB.species_prototypes[preferences.read_preference(/datum/preference/choiced/species)] + var/datum/language_holder/species_holder = GLOB.prototype_language_holders[species.species_language_holder] + var/list/sanitized_input = list() + for(var/key in input) + for(var/lang_text in input[key]) + var/lang = istext(lang_text) ? text2path(lang_text) : lang_text + if(!ispath(lang, /datum/language)) + continue + switch(key) + if(ADD_SPOKEN_LANGUAGE) + if(!(lang in selectable_languages)) + continue + if(lang in species_holder.spoken_languages) + continue + if(length(sanitized_input[key]) >= max_spoken_languages) + continue + + if(ADD_UNDERSTOOD_LANGUAGE) + if(!(lang in selectable_languages)) + continue + if(lang in species_holder.understood_languages) + continue + if(length(sanitized_input[key]) >= max_understood_languages) + continue + + if(REMOVE_SPOKEN_LANGUAGE) + if(!(lang in species_holder.spoken_languages)) + continue + + if(REMOVE_UNDERSTOOD_LANGUAGE) + if(!(lang in species_holder.understood_languages)) + continue + + LAZYADD(sanitized_input[key], lang) + + return sanitized_input + +/datum/preference/languages/is_valid(value) + return islist(value) || isnull(value) + +/datum/preference/languages/apply_to_human(mob/living/carbon/human/target, value) + if(isdummy(target) || !islist(value)) + return + + // this needs to be delayed because it's tied to the mind (and we probably don't have that created yet) + if(target.mind) + add_mind_languages(target, value) + else + RegisterSignals(target, list(COMSIG_MOB_MIND_TRANSFERRED_INTO, COMSIG_MOB_MIND_INITIALIZED), PROC_REF(comsig_add_mind_languages), TRUE) + // this is fine to do now, though + remove_species_languages(target, value) -/datum/preference/additional_language/apply_to_human(mob/living/carbon/human/target, value) - if(value == NO_LANGUAGE) +/datum/preference/languages/proc/comsig_add_mind_languages(mob/living/carbon/human/target) + SIGNAL_HANDLER + + if(!target.client) return - target.grant_language(value, ALL, LANGUAGE_PREF) + UnregisterSignal(target, list(COMSIG_MOB_MIND_TRANSFERRED_INTO, COMSIG_MOB_MIND_INITIALIZED)) + add_mind_languages(target, target.client.prefs.read_preference(/datum/preference/languages)) -/datum/language - // Vars used in determining valid languages for the language preferences. - /// Whether this language is available as a pref. - var/available_as_pref = FALSE - /// The 'base species' of the language, the lizard to the draconic. - /// Players cannot select this language in the preferences menu if they already have this species set. - var/datum/species/banned_from_species - /// The 'required species' of the language, languages that require you be a certain species to know. - /// Players cannot select this language in the preferences menu if they do not have this species set. - var/datum/species/required_species +/datum/preference/languages/proc/add_mind_languages(mob/living/carbon/human/target, list/value = list()) + if(QDELETED(target)) + return + for(var/lang in value[ADD_SPOKEN_LANGUAGE]) + target.grant_language(lang, SPOKEN_LANGUAGE, LANGUAGE_MIND) + for(var/lang in value[ADD_UNDERSTOOD_LANGUAGE]) + target.grant_language(lang, UNDERSTOOD_LANGUAGE, LANGUAGE_MIND) -/datum/language/skrell - available_as_pref = TRUE - banned_from_species = /datum/species/skrell +/datum/preference/languages/proc/remove_species_languages(mob/living/carbon/human/target, list/value = list()) + if(QDELETED(target)) + return + for(var/lang in value[REMOVE_SPOKEN_LANGUAGE]) + target.remove_language(lang, SPOKEN_LANGUAGE, LANGUAGE_SPECIES) + for(var/lang in value[REMOVE_UNDERSTOOD_LANGUAGE]) + target.remove_language(lang, UNDERSTOOD_LANGUAGE, LANGUAGE_SPECIES) -/datum/language/draconic - available_as_pref = TRUE - banned_from_species = /datum/species/lizard +/datum/preference_middleware/language + action_delegations = list( + "set_language" = PROC_REF(set_language), + ) -/datum/language/impdraconic - available_as_pref = TRUE - banned_from_species = /datum/species/lizard/silverscale // already know it (though this check should be deharcoded) - required_species = /datum/species/lizard +/datum/preference_middleware/language/proc/add_language_to_user(lang_type, lang_key) + var/datum/preference/languages/language_pref = GLOB.preference_entries[/datum/preference/languages] + var/list/existing = preferences.read_preference(/datum/preference/languages) || list() -/datum/language/nekomimetic - available_as_pref = TRUE - banned_from_species = /datum/species/human/felinid + if(lang_key == ADD_SPOKEN_LANGUAGE && length(existing[ADD_SPOKEN_LANGUAGE]) >= language_pref.max_spoken_languages) + return FALSE + if(lang_key == ADD_UNDERSTOOD_LANGUAGE && length(existing[ADD_UNDERSTOOD_LANGUAGE]) >= language_pref.max_understood_languages) + return FALSE -/datum/language/moffic - available_as_pref = TRUE - banned_from_species = /datum/species/moth + if((lang_key == ADD_SPOKEN_LANGUAGE || lang_key == ADD_UNDERSTOOD_LANGUAGE) && !(lang_type in language_pref.selectable_languages)) + return FALSE -/datum/language/uncommon - available_as_pref = TRUE + LAZYADD(existing[lang_key], lang_type) -/datum/language/piratespeak - available_as_pref = TRUE + preferences.write_preference(GLOB.preference_entries[/datum/preference/languages], existing) -/datum/language/yangyu - available_as_pref = TRUE - banned_from_species = /datum/species/ornithid + return TRUE -/datum/language/shadowtongue - available_as_pref = TRUE +/datum/preference_middleware/language/proc/remove_language_from_user(lang_type, lang_key) + var/list/existing = preferences.read_preference(/datum/preference/languages) || list() -/datum/preference_middleware/language - action_delegations = list( - "set_language" = PROC_REF(set_language), - ) + LAZYREMOVE(existing[lang_key], lang_type) + + preferences.write_preference(GLOB.preference_entries[/datum/preference/languages], existing) + + return TRUE /datum/preference_middleware/language/proc/set_language(list/params, mob/user) - var/datum/preference/additional_language/language_pref = GLOB.preference_entries[/datum/preference/additional_language] if(params["deselecting"]) - preferences.update_preference(language_pref, NO_LANGUAGE) + remove_language_from_user(text2path(params["lang_type"]), params["lang_key"]) return TRUE var/lang_path = text2path(params["lang_type"]) - var/datum/species/current_species = preferences.read_preference(/datum/preference/choiced/species) - var/datum/language/lang_to_add = GLOB.language_datum_instances[lang_path] - if(!istype(lang_to_add)) - to_chat(user, span_warning("Invalid language.")) - return TRUE - if(!lang_to_add.available_as_pref) - to_chat(user, span_warning("That language is not available.")) - return TRUE - // Sanity checking - Buttons are disabled in UI but you can never rely on that - if(lang_to_add.banned_from_species && ispath(current_species, lang_to_add.banned_from_species)) - to_chat(user, span_warning("Invalid language for current species.")) - return TRUE - if(lang_to_add.required_species && !ispath(current_species, lang_to_add.required_species)) - to_chat(user, span_warning("Language requires another species.")) - return TRUE - - preferences.update_preference(language_pref, lang_path) + if(GLOB.language_datum_instances[lang_path]) + add_language_to_user(lang_path, params["lang_key"]) return TRUE +/datum/preference_middleware/language/on_new_character(mob/user) + preferences.update_static_data(user) + +/datum/preference_middleware/language/get_ui_static_data(mob/user) + var/list/data = list() + + var/datum/species/species = GLOB.species_prototypes[preferences.read_preference(/datum/preference/choiced/species)] + var/datum/language_holder/species_holder = GLOB.prototype_language_holders[species.species_language_holder] + data["spoken_languages"] = assoc_to_keys(species_holder.spoken_languages) + data["understood_languages"] = assoc_to_keys(species_holder.understood_languages) + data["partial_languages"] = species_holder.get_mutually_understood_languages() + + return data + /datum/preference_middleware/language/get_ui_data(mob/user) var/list/data = list() - data["selected_lang"] = preferences.read_preference(/datum/preference/additional_language) - data["selected_species"] = preferences.read_preference(/datum/preference/choiced/species) - data["pref_name"] = preferences.read_preference(/datum/preference/name/real_name) - data["trilingual"] = ("Trilingual" in preferences.all_quirks) - data["bilingual"] = ("Bilingual" in preferences.all_quirks) + var/list/selected_languages = preferences.read_preference(/datum/preference/languages) + data["pref_spoken_languages"] = selected_languages?[ADD_SPOKEN_LANGUAGE] || list() + data["pref_understood_languages"] = selected_languages?[ADD_UNDERSTOOD_LANGUAGE] || list() + data["pref_unspoken_languages"] = selected_languages?[REMOVE_SPOKEN_LANGUAGE] || list() + data["pref_ununderstood_languages"] = selected_languages?[REMOVE_UNDERSTOOD_LANGUAGE] || list() return data /datum/preference_middleware/language/get_constant_data() var/list/data = list() - var/list/base_languages = list() - var/list/bonus_languages = list() + + var/datum/preference/languages/language_pref = GLOB.preference_entries[/datum/preference/languages] + + data["base_languages"] = list() for(var/found_language in GLOB.language_datum_instances) - var/datum/language/found_instance = GLOB.language_datum_instances[found_language] - if(!found_instance.available_as_pref) + if(found_language in language_pref.dont_show_languages) continue - var/list/lang_data = list() - lang_data["name"] = found_instance.name lang_data["type"] = found_language + lang_data["unlocked"] = (found_language in language_pref.selectable_languages) + lang_data["name"] = GLOB.language_datum_instances[found_language].name + lang_data["desc"] = GLOB.language_datum_instances[found_language].desc + UNTYPED_LIST_ADD(data["base_languages"], lang_data) + + data["max_spoken_languages"] = language_pref.max_spoken_languages + data["max_understood_languages"] = language_pref.max_understood_languages - var/datum/species/banned_species = found_instance.banned_from_species - if(banned_species) - lang_data["incompatible_with"] = list("name" = initial(banned_species.name), "type" = banned_species) - var/datum/species/required_species = found_instance.required_species - if(required_species) - lang_data["requires"] = list("name" = initial(required_species.name), "type" = required_species) - - // Having a required species makes it a bonus language, otherwise it's a base language - if(found_instance.required_species) - UNTYPED_LIST_ADD(bonus_languages, lang_data) - else - UNTYPED_LIST_ADD(base_languages, lang_data) - - data["base_languages"] = base_languages - data["bonus_languages"] = bonus_languages - data["blacklisted_species"] = list() return data -#undef NO_LANGUAGE +#undef ADD_SPOKEN_LANGUAGE +#undef ADD_UNDERSTOOD_LANGUAGE +#undef REMOVE_SPOKEN_LANGUAGE +#undef REMOVE_UNDERSTOOD_LANGUAGE diff --git a/maplestation_modules/code/modules/language/isatoan.dm b/maplestation_modules/code/modules/language/isatoan.dm index 5d55fde3b55c..63e57ae91d7e 100644 --- a/maplestation_modules/code/modules/language/isatoan.dm +++ b/maplestation_modules/code/modules/language/isatoan.dm @@ -15,7 +15,6 @@ icon_state = "mu" icon = 'maplestation_modules/icons/misc/language.dmi' default_priority = 80 - available_as_pref = TRUE /datum/language_holder/isatoa understood_languages = list( diff --git a/maplestation_modules/code/modules/language/japanese.dm b/maplestation_modules/code/modules/language/japanese.dm index bb633e4660ec..b47a0d3baaa7 100644 --- a/maplestation_modules/code/modules/language/japanese.dm +++ b/maplestation_modules/code/modules/language/japanese.dm @@ -13,7 +13,7 @@ ) icon_state = "torii" icon = 'maplestation_modules/icons/misc/language.dmi' - default_priority = 94 + default_priority = 90 /datum/language_holder/yangyu understood_languages = list( diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/_LanguagePicker.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/_LanguagePicker.tsx index bdfdca673d21..c8e34294f3a8 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/_LanguagePicker.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/_LanguagePicker.tsx @@ -4,164 +4,396 @@ import { useBackend } from '../../backend'; import { Box, Button, - Dimmer, + Flex, NoticeBox, Section, Stack, + Tooltip, } from '../../components'; import { ServerPreferencesFetcher } from './ServerPreferencesFetcher'; type typePath = string; +type languagePath = typePath; + type Data = { pref_name: string; - selected_species: typePath; - selected_lang: typePath | string; - trilingual: BooleanLike; - bilingual: BooleanLike; + spoken_languages: languagePath[]; + understood_languages: languagePath[]; + partial_languages: Record[]; + pref_spoken_languages: languagePath[]; + pref_understood_languages: languagePath[]; + pref_unspoken_languages: languagePath[]; + pref_ununderstood_languages: languagePath[]; }; -type Species = { +export type Language = { name: string; - type: typePath; + desc: string; + type: languagePath; + unlocked: BooleanLike; }; -export type Language = { - name: string; - type: typePath; - incompatible_with: Species | null; - requires: Species | null; +enum LanguageState { + DEFAULT, + DISABLED, + ENABLED, + NONE, +} + +const StateToIcon = { + [LanguageState.DEFAULT]: 'square-check-o', + [LanguageState.DISABLED]: 'square-minus-o', + [LanguageState.ENABLED]: 'square-plus-o', + [LanguageState.NONE]: 'square-o', +}; + +const StateToColor = { + [LanguageState.DEFAULT]: 'good', + [LanguageState.DISABLED]: 'bad', + [LanguageState.ENABLED]: 'average', + [LanguageState.NONE]: 'default', +}; + +const StateToTooltipSpeech = { + [LanguageState.DEFAULT]: 'You innately speak this.', + [LanguageState.DISABLED]: 'You have opted out of speaking this.', + [LanguageState.ENABLED]: 'You have chosen to speak this.', + [LanguageState.NONE]: 'You do not speak this.', +}; + +const StateToTooltipUnderstanding = { + [LanguageState.DEFAULT]: 'You innately understand this.', + [LanguageState.DISABLED]: 'You have opted out of understanding this.', + [LanguageState.ENABLED]: 'You have chosen to understand this.', + [LanguageState.NONE]: 'You do not understand this.', +}; + +const get_spoken_language_state = ( + langtype: languagePath, + data: Data, +): LanguageState => { + if (data.pref_unspoken_languages.includes(langtype)) { + return LanguageState.DISABLED; + } + if (data.pref_spoken_languages.includes(langtype)) { + return LanguageState.ENABLED; + } + if (data.spoken_languages.includes(langtype)) { + return LanguageState.DEFAULT; + } + return LanguageState.NONE; }; -// Fake an ispath() check to determine if this species can learn this language -const isPickable = (lang: Language, species: typePath): boolean => { - if (lang.incompatible_with && species.includes(lang.incompatible_with.type)) { - return false; +const get_understood_language_state = ( + langtype: languagePath, + data: Data, +): LanguageState => { + if (data.pref_ununderstood_languages.includes(langtype)) { + return LanguageState.DISABLED; + } + if (data.pref_understood_languages.includes(langtype)) { + return LanguageState.ENABLED; + } + if (data.understood_languages.includes(langtype)) { + return LanguageState.DEFAULT; } - if (lang.requires && !species.includes(lang.requires.type)) { - return false; + return LanguageState.NONE; +}; + +// Returns the keys for the spoken language button action based on the given language and data +const get_spoken_button_keys = (langtype: languagePath, data: Data) => { + if (data.spoken_languages.includes(langtype)) { + return { + lang_key: 'Remove spoken language', // Corresponds to DM defines + deselecting: data.pref_unspoken_languages.includes(langtype), + }; } - return true; + return { + lang_key: 'Add spoken language', // Corresponds to DM defines + deselecting: data.pref_spoken_languages.includes(langtype), + }; }; -const getLanguageTooltip = (lang: Language): string => { - if (lang.incompatible_with && lang.requires) { - return `This language cannot be selected by - the "${lang.incompatible_with.name}" species and requires - the "${lang.requires.name}" species.`; +// Returns the keys for the understood language button action based on the given language and data +const get_understood_button_keys = (langtype: languagePath, data: Data) => { + if (data.understood_languages.includes(langtype)) { + return { + lang_key: 'Remove understood language', // Corresponds to DM defines + deselecting: data.pref_ununderstood_languages.includes(langtype), + }; } - if (lang.incompatible_with) { - return `This language cannot be selected by - the "${lang.incompatible_with.name}" species.`; + return { + lang_key: 'Add understood language', // Corresponds to DM defines + deselecting: data.pref_understood_languages.includes(langtype), + }; +}; + +const partial_understanding_percent = (langtype: languagePath, data: Data) => { + if (!data.partial_languages[langtype]) { + return null; } - if (lang.requires) { - return `This language requires - the "${lang.requires.name}" species.`; + const all_understood_combined = data.understood_languages + .concat(data.pref_understood_languages) + .filter((item) => !data.pref_ununderstood_languages.includes(item)); + if (all_understood_combined.includes(langtype)) { + return ( + + + {`${data.partial_languages[langtype]}%`} + + + ); } - return ''; + return {data.partial_languages[langtype]}%; }; -const LanguageStack = (props: { - language: Language; - selected_lang: typePath; - selected_species: typePath; +const LanguageRow = (props: { + displayed_language: Language; + spoken_cap: number; + understood_cap: number; }) => { - const { act } = useBackend(); - const { language, selected_species } = props; - const { name, type } = language; - const pickable = isPickable(language, selected_species); + const { act, data } = useBackend(); + const { displayed_language, spoken_cap, understood_cap } = props; + const { + spoken_languages, + pref_spoken_languages, + pref_unspoken_languages, + understood_languages, + pref_understood_languages, + pref_ununderstood_languages, + } = data; + + const lang_type = displayed_language.type; + + const spoken_state = get_spoken_language_state(lang_type, data); + const understood_state = get_understood_language_state(lang_type, data); + const ignore_spoken_cap = spoken_languages + .concat(pref_spoken_languages) + .concat(pref_unspoken_languages) + .includes(lang_type); + + const ignore_undersood_cap = understood_languages + .concat(pref_understood_languages) + .concat(pref_ununderstood_languages) + .includes(lang_type); + + // name - spoken - understood - partial understanding percent return ( - - - {name} - - - + + {displayed_language.desc ? ( + + + {displayed_language.name} + + + ) : ( + {displayed_language.name} + )} + + +