From 4bb4c7027728bfe933b5b17f54b9782ddbf82b12 Mon Sep 17 00:00:00 2001 From: SapphicOverload <93578146+SapphicOverload@users.noreply.github.com> Date: Thu, 2 Oct 2025 07:14:54 -0400 Subject: [PATCH 1/3] Language Menu + Language Scrambling Rework + Splits Kalixcian Into Multiple Languages (#4988) Replaces the language quirks with a proper language menu, where you can select which languages your character knows and how well. This also splits Kalixcian Common into two different languages, Gezenan and Zohilan, originating from the Pan-Gezena Federation and Zohil Explorat respectively as well as being widely spoken in their current and former colonies. Both have a very high (90%) mutual understanding of each other. The way not understanding a language scrambles text has also been reworked so that instead of being randomized for every message, it now uses a hash function to always scramble the same original words to the same scrambled text within a given round, making it possible to infer the meaning of certain words based on context.
Images: Zohilan to a native Gezenan speaker. For fun, try to figure out the meaning of some of the words in this paragraph. ![image](https://github.com/user-attachments/assets/2e2300c4-b47c-46a9-89b5-8afdfcfb812a) Helm console on a PGFN vessel. ![image](https://github.com/user-attachments/assets/3bffed65-3f08-44e6-ad44-197ddcf04d49) Language selection! ![image](https://github.com/user-attachments/assets/44c910e5-01d1-4544-a361-60c1239684b9) Zohilan language icon by Aquidu: lizard_blue
All of this significantly improves the level of character customization available to players, and the ability to figure out what some of the words in other languages mean using context is pretty cool and interesting. Kalixcian Common doesn't make much sense to be a single language, considering Kalixcis is the only homeworld to never unify into a single entity, so I thought it made sense to split it into Gezenan and Zohilan and give them a high mutual understanding of each other to reflect this. :cl: SapphicOverload, Aquidu add: Added a proper language selection to the character preferences menu add: Helm consoles now speak the official language of whichever faction their ship is registered with add: Splits Kalixcian Common into two separate languages (Gezenan / Zohilan) with high mutual understanding add: Admins can now set partial understanding with the language menu del: Removed languages being automatically granted from being part of a faction del: Removed language quirks del: Removed an unused language code: Refactored language text scrambling to be consistent within a given round, allowing the meaning of words to be inferred from context fix: Fixed the language menu not showing the level of understanding fix: Fixed quirks that have been removed from the game persisting in character preferences /:cl: (cherry picked from commit 72294f2ff430ea4ff1493c2528dfd235c146f040) --- code/__DEFINES/language.dm | 6 +- code/__DEFINES/preferences.dm | 11 ++ .../subsystem/processing/quirks.dm | 16 ++- code/datums/traits/negative/monolingual.dm | 4 + code/datums/traits/neutral/languages.dm | 97 ------------------ code/datums/traits/positive/trilingual.dm | 4 + code/game/atoms_movable.dm | 5 +- code/game/objects/effects/contraband.dm | 2 +- code/modules/client/preferences.dm | 87 ++++++++++++++++ code/modules/client/preferences_savefile.dm | 13 +++ .../clothing/outfits/factions/gezena.dm | 6 -- .../clothing/outfits/factions/solgov.dm | 6 -- code/modules/faction/faction_datum.dm | 5 + code/modules/language/buzzwords.dm | 15 --- code/modules/language/common.dm | 5 +- .../language/{draconic.dm => gezenan.dm} | 12 ++- code/modules/language/language.dm | 41 +++++--- code/modules/language/language_holder.dm | 6 +- code/modules/language/language_menu.dm | 12 +++ code/modules/language/moffic.dm | 2 +- code/modules/language/sign.dm | 2 +- code/modules/language/solarian.dm | 2 +- code/modules/language/teceti_unified.dm | 2 +- code/modules/language/zohilan.dm | 33 ++++++ .../mob/dead/new_player/ship_select.dm | 6 ++ code/modules/overmap/helm.dm | 5 + code/modules/shuttle/shuttle.dm | 4 + code/modules/surgery/organs/tongue.dm | 34 +++--- icons/misc/language.dmi | Bin 4699 -> 5137 bytes shiptest.dme | 7 +- .../packages/tgui/interfaces/LanguageMenu.tsx | 1 + 31 files changed, 270 insertions(+), 181 deletions(-) create mode 100644 code/datums/traits/negative/monolingual.dm delete mode 100644 code/datums/traits/neutral/languages.dm create mode 100644 code/datums/traits/positive/trilingual.dm delete mode 100644 code/modules/language/buzzwords.dm rename code/modules/language/{draconic.dm => gezenan.dm} (69%) create mode 100644 code/modules/language/zohilan.dm diff --git a/code/__DEFINES/language.dm b/code/__DEFINES/language.dm index 2ee6ba37703..6c0affaffe2 100644 --- a/code/__DEFINES/language.dm +++ b/code/__DEFINES/language.dm @@ -2,8 +2,12 @@ #define TONGUELESS_SPEECH (1<<1) #define LANGUAGE_HIDE_ICON_IF_UNDERSTOOD (1<<2) #define LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD (1<<3) +/// This language can be selected in preferences. +#define ROUNDSTART_LANGUAGE (1<<4) /// This language is signed, not spoken. -#define SIGNED_LANGUAGE (1<<4) +#define SIGNED_LANGUAGE (1<<5) +/// Sarathi do not "sss" when speaking this language +#define NO_HISS (1<<6) // LANGUAGE SOURCE DEFINES /// For use in full removal only. diff --git a/code/__DEFINES/preferences.dm b/code/__DEFINES/preferences.dm index 131af782bf1..c5b37e2694d 100644 --- a/code/__DEFINES/preferences.dm +++ b/code/__DEFINES/preferences.dm @@ -128,5 +128,16 @@ #define PROSTHETIC_AMPUTATED "amputated" #define PROSTHETIC_ROBOTIC "prosthetic" +/// You cannot speak or understand this language whatsoever. +#define LANGUAGE_UNKNOWN "Unknown (0)" +/// You cannot speak this language, but can recognize some of the words. +#define LANGUAGE_RECOGNIZED "Recognized (1)" +/// You are familiar with this language enough to sort of speak it, but cannot understand it very well. +#define LANGUAGE_FAMILIAR "Familiar (2)" +/// You are fluent in this language, and can both understand and speak it perfectly. +#define LANGUAGE_FLUENT "Fluent (3)" +/// Maximum number of additional languages that can be selected. +#define MAX_LANGUAGE_POINTS 4 + #define NOT_SYNTHETIC FALSE #define IS_SYNTHETIC TRUE diff --git a/code/controllers/subsystem/processing/quirks.dm b/code/controllers/subsystem/processing/quirks.dm index 0813bc6640b..04d796c09ba 100644 --- a/code/controllers/subsystem/processing/quirks.dm +++ b/code/controllers/subsystem/processing/quirks.dm @@ -24,16 +24,14 @@ PROCESSING_SUBSYSTEM_DEF(quirks) SetupQuirks() quirk_blacklist = list( - list("Blind","Nearsighted"), \ - list("Ageusia","Vegetarian","Deviant Tastes"), \ + list("Ananas Affinity","Ananas Aversion"), //PENTEST ADDED + list("Blind","Nearsighted"), + list("Ageusia","Vegetarian","Deviant Tastes"), list("Alcohol Tolerance","Light Drinker"), - list("Bad Touch", "Friendly"), \ - list("Self-Aware", "Congenital Analgesia"), \ - //list("(Language) Moth Pidgin", "(Language) Solarian International", "(Language) Teceti Unified Standard", "(Language) Kalixcian Common"), \ - //PENTEST EDITS BELOW - list("Ananas Affinity","Ananas Aversion"), \ - list("(Language) Moth Pidgin", "(Language) Terran International", "(Language) Teceti Unified Standard", "(Language) Draconic Common"), \ - ) + list("Bad Touch", "Friendly"), + list("Self-Aware", "Congenital Analgesia"), + list("Trilingual", "Monolingual"), + ) species_blacklist = list("Blood Deficiency" = list(SPECIES_IPC, SPECIES_JELLYPERSON, SPECIES_PLASMAMAN, SPECIES_VAMPIRE)) diff --git a/code/datums/traits/negative/monolingual.dm b/code/datums/traits/negative/monolingual.dm new file mode 100644 index 00000000000..e30b8454052 --- /dev/null +++ b/code/datums/traits/negative/monolingual.dm @@ -0,0 +1,4 @@ +/datum/quirk/monolingual + name = "Monolingual" + desc = "You're only fluent in your native language. (-2 language points)" + value = -1 diff --git a/code/datums/traits/neutral/languages.dm b/code/datums/traits/neutral/languages.dm deleted file mode 100644 index 816a518d927..00000000000 --- a/code/datums/traits/neutral/languages.dm +++ /dev/null @@ -1,97 +0,0 @@ -//Languages - not worth their own file - -// Kalixcian Common -/datum/quirk/lang_kalixcian - name = "(Language) Kalixcian Common" - desc = "You're fluent in Kalixcian Common." - value = 0 - gain_text = span_notice("You know Kalixcian Common.") - lose_text = span_danger("You forget Kalixcian Common.") - detectable = FALSE - -/datum/quirk/lang_kalixcian/add() - var/mob/living/carbon/human/knower = quirk_holder - knower.grant_language(/datum/language/kalixcian_common, source = LANGUAGE_MIND) - -/datum/quirk/lang_kalixcian/remove() - if(quirk_holder) - var/mob/living/carbon/human/knower = quirk_holder - knower.remove_language(/datum/language/kalixcian_common, source = LANGUAGE_MIND) - -// Teceti Unified Standard -/datum/quirk/lang_tuc - name = "(Language) Teceti Unified Standard" - desc = "You're fluent in Teceti Unified Standard." - value = 0 - gain_text = span_notice("You know Teceti Unified.") - lose_text = span_danger("You forget Teceti Unified.") - detectable = FALSE - -/datum/quirk/lang_tuc/add() - var/mob/living/carbon/human/knower = quirk_holder - knower.grant_language(/datum/language/teceti_unified, source = LANGUAGE_MIND) - -/datum/quirk/lang_tuc/remove() - if(quirk_holder) - var/mob/living/carbon/human/knower = quirk_holder - knower.remove_language(/datum/language/teceti_unified, source = LANGUAGE_MIND) - -// Solarian International -/datum/quirk/lang_solarian_international - name = "(Language) Solarian International" - desc = "You're fluent in Solarian International." - value = 0 - gain_text = span_notice("You know Solarian International.") - lose_text = span_danger("You forget Solarian International.") - detectable = FALSE - -/datum/quirk/lang_solarian_international/add() - var/mob/living/carbon/human/knower = quirk_holder - knower.grant_language(/datum/language/solarian_international, source = LANGUAGE_MIND) - -/datum/quirk/lang_solarian_international/remove() - if(quirk_holder) - var/mob/living/carbon/human/knower = quirk_holder - knower.remove_language(/datum/language/solarian_international, source = LANGUAGE_MIND) - -// Moth Pidgin -/datum/quirk/lang_moth - name = "(Language) Moth Pidgin" - desc = "You're fluent in Moth Pidgin." - gain_text = span_notice("You know Moth Pidgin.") - lose_text = span_danger("You forget Moth Pidgin.") - detectable = FALSE - -/datum/quirk/lang_moth/add() - var/mob/living/carbon/human/knower = quirk_holder - knower.grant_language(/datum/language/moffic, source = LANGUAGE_MIND) - -/datum/quirk/lang_moth/remove() - if(quirk_holder) - var/mob/living/carbon/human/knower = quirk_holder - knower.remove_language(/datum/language/moffic, source = LANGUAGE_MIND) - -// Sign Language -/datum/quirk/signer - name = "Signer" - desc = "You're fluent in Universal Sign Language and have translation gloves to communicate over radio." - value = 0 - gain_text = span_notice("You know Universal Sign Language.") - lose_text = span_danger("You forget Universal Sign Language.") - detectable = FALSE - -/datum/quirk/signer/add() - var/mob/living/carbon/human/knower = quirk_holder - knower.grant_language(/datum/language/sign_language, source = LANGUAGE_MIND) - // Give translation gloves so they can use sign language over radio - var/obj/item/clothing/gloves/radio/translator_gloves = new(get_turf(knower)) - if(!knower.equip_to_slot_if_possible(translator_gloves, ITEM_SLOT_GLOVES, disable_warning = TRUE)) - // If they can't equip them (already wearing gloves), put them in hands or drop nearby - if(!knower.put_in_hands(translator_gloves)) - translator_gloves.forceMove(get_turf(knower)) - to_chat(knower, span_notice("A pair of translation gloves appears at your feet.")) - -/datum/quirk/signer/remove() - if(quirk_holder) - var/mob/living/carbon/human/knower = quirk_holder - knower.remove_language(/datum/language/sign_language, source = LANGUAGE_MIND) diff --git a/code/datums/traits/positive/trilingual.dm b/code/datums/traits/positive/trilingual.dm new file mode 100644 index 00000000000..a7492c430c0 --- /dev/null +++ b/code/datums/traits/positive/trilingual.dm @@ -0,0 +1,4 @@ +/datum/quirk/trilingual + name = "Trilingual" + desc = "You're fluent in three languages. (+2 language points)" + value = 1 diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 29d69256e75..e34377749e1 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1205,7 +1205,10 @@ /// Gets a lazylist of all mutually understood languages. /atom/movable/proc/get_partially_understood_languages() - return get_language_holder().best_mutual_languages + var/datum/language_holder/our_holder = get_language_holder() + if(!our_holder.best_mutual_languages) + our_holder.calculate_best_mutual_language() + return our_holder.best_mutual_languages /// Gets a random spoken language, useful for forced speech and such. /atom/movable/proc/get_random_spoken_language() diff --git a/code/game/objects/effects/contraband.dm b/code/game/objects/effects/contraband.dm index 35f620fbf20..a70ec9ca0b0 100644 --- a/code/game/objects/effects/contraband.dm +++ b/code/game/objects/effects/contraband.dm @@ -788,7 +788,7 @@ /obj/structure/sign/poster/retro/radio name = "Radio" - desc = "A poster advertising one of Nanotrasen's earliest products, a radio. One of its main selling points was a integrated OS and two way automatic translation for Solarian Common, and Kalixcian Common, which made it a smash hit. This thing is ancient." + desc = "A poster advertising one of Nanotrasen's earliest products, a radio. One of its main selling points was a integrated OS and two way automatic translation for Solarian Common and Gezenan, which made it a smash hit. This thing is ancient." icon_state = "poster-radio70_retro" //Safety moth posters, credit to AspEv for the art which the below posters are based on and to Ayy-Robotics for the sprites. diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index bd89cf70c77..adeb1b9c571 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -163,6 +163,15 @@ GLOBAL_LIST_EMPTY(preferences_datums) //Job preferences 2.0 - indexed by job title , no key or value implies never var/list/job_preferences = list() + /// Languages this character knows besides their native language. + var/list/learned_languages = list() + + /// This character's native language. + var/datum/language/native_language = /datum/language/galactic_common + + /// Associated list with language levels of understanding and their point costs. + var/static/list/language_level_costs = list(LANGUAGE_UNKNOWN = 0, LANGUAGE_RECOGNIZED = 1, LANGUAGE_FAMILIAR = 2, LANGUAGE_FLUENT = 3) + // 0 = character settings, 1 = game preferences var/current_tab = 0 @@ -359,6 +368,18 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "Custom Job Preferences:
" dat += "Preferred AI Core Display: [preferred_ai_core_display]
" + dat += "

Languages

" + dat += "" + dat += "" + if(!learned_languages?.len) + init_learned_languages() + for(var/datum/language/lang_type as anything in learned_languages) + if(lang_type == native_language) + continue + dat += "" + dat += "" + dat += "
[get_language_point_balance()] points left.
Native Language: [initial(native_language.name)]
[initial(lang_type.name)]: [learned_languages[lang_type]]
Reset Languages
" + dat += "" dat += "

Clothing

" @@ -1502,6 +1523,24 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(SSquirks.quirk_points[q] > 0) .++ +/datum/preferences/proc/init_learned_languages() + learned_languages = list() + for(var/datum/language/lang_type as anything in subtypesof(/datum/language)) + if(initial(lang_type.flags) & ROUNDSTART_LANGUAGE) + learned_languages[lang_type] = LANGUAGE_UNKNOWN + +/datum/preferences/proc/get_language_point_balance() + var/points_balance = MAX_LANGUAGE_POINTS + for(var/datum/language/lang_type as anything in learned_languages) + if(lang_type == native_language) + continue // this should happen but just in case + points_balance -= language_level_costs[learned_languages[lang_type]] + if("Trilingual" in all_quirks) + points_balance += 2 + if("Monolingual" in all_quirks) + points_balance -= 2 + return points_balance + /datum/preferences/Topic(href, href_list, hsrc) //yeah, gotta do this I guess.. . = ..() if(href_list["close"]) @@ -2098,6 +2137,34 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(ai_core_icon) preferred_ai_core_display = ai_core_icon + if("native_language") + var/list/language_list = list() + for(var/datum/language/lang_type as anything in learned_languages) + language_list[initial(lang_type.name)] = lang_type + var/datum/language/new_lang = language_list[tgui_input_list(user, "Select a native language:", "Native Language", language_list)] + if(ispath(new_lang, /datum/language) && (initial(new_lang.flags) & ROUNDSTART_LANGUAGE)) // double-check to prevent exploits to gain codespeak or something as a native language + native_language = new_lang + learned_languages[new_lang] = LANGUAGE_UNKNOWN + + if("learned_language") + var/datum/language/selected_lang = locate(href_list["language"]) + if(selected_lang.type == native_language) // wuh oh + CRASH("[usr] attempted to change level of understanding for [selected_lang] despite it being their native language!") + if(selected_lang && (selected_lang.flags & ROUNDSTART_LANGUAGE)) // no using html exploits to learn codespeak + var/understanding = tgui_input_list(user, "Select level of understanding:", "Learn Language", language_level_costs) + if(!understanding) + return + if(!(understanding in language_level_costs)) + CRASH("[usr] attempted to set level of understanding for [selected_lang.type] to \"[understanding]\"") + var/old_value = learned_languages[selected_lang.type] + learned_languages[selected_lang.type] = understanding + if(get_language_point_balance() < 0 && understanding != LANGUAGE_UNKNOWN) // in case something breaks REAL bad, you can still disable languages to fix it + learned_languages[selected_lang.type] = old_value + to_chat(usr, span_warning("You don't have enough language points!")) + + if("reset_languages") + init_learned_languages() + if ("clientfps") var/desiredfps = input(user, "Choose your desired fps. (0 = default, 60 FPS))", "Character Preference", clientfps) as null|num //WS Edit - Client FPS Tweak - if (!isnull(desiredfps)) @@ -2566,6 +2633,26 @@ GLOBAL_LIST_EMPTY(preferences_datums) character.update_body_parts(TRUE) character.dna.update_body_size() + if(!character_setup && get_language_point_balance() < 0) + init_learned_languages() // no exploits allowed + character.grant_language(native_language) + character.get_language_holder().selected_language = native_language + for(var/datum/language/lang_type as anything in learned_languages) + if(lang_type == native_language) + continue + switch(learned_languages[lang_type]) + if(LANGUAGE_FLUENT) + character.grant_language(lang_type) + if(LANGUAGE_FAMILIAR) + character.grant_language(lang_type, SPOKEN_LANGUAGE) + character.remove_language(lang_type, UNDERSTOOD_LANGUAGE) + character.grant_partial_language(lang_type, 80) + if(LANGUAGE_RECOGNIZED) + character.remove_language(lang_type) + character.grant_partial_language(lang_type, 40) + if(LANGUAGE_UNKNOWN) + character.remove_language(lang_type) + /datum/preferences/proc/get_default_name(name_id) switch(name_id) if("ai") diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index d29c6113145..e89d7230b5a 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -438,6 +438,10 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car READ_FILE(S["body_size"], features["body_size"]) READ_FILE(S["prosthetic_limbs"], prosthetic_limbs) prosthetic_limbs ||= list(BODY_ZONE_L_ARM = PROSTHETIC_NORMAL, BODY_ZONE_R_ARM = PROSTHETIC_NORMAL, BODY_ZONE_L_LEG = PROSTHETIC_NORMAL, BODY_ZONE_R_LEG = PROSTHETIC_NORMAL) + READ_FILE(S["learned_languages"], learned_languages) + if(!learned_languages?.len) init_learned_languages() + READ_FILE(S["native_language"], native_language) + native_language ||= /datum/language/galactic_common READ_FILE(S["feature_mcolor"], features["mcolor"]) READ_FILE(S["feature_mcolor2"], features["mcolor2"]) READ_FILE(S["feature_ethcolor"], features["ethcolor"]) @@ -502,6 +506,13 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car //Quirks READ_FILE(S["all_quirks"], all_quirks) + var/list/removed_quirks = list() + for(var/quirk_name in all_quirks.Copy()) + if(!(quirk_name in SSquirks.quirks)) + all_quirks.Remove(quirk_name) + removed_quirks.Add(quirk_name) + if(removed_quirks.len) + to_chat(parent, "Some of your previously selected quirks have been removed: [english_list(removed_quirks)].") //Flavor Text S["feature_flavor_text"] >> features["flavor_text"] @@ -631,6 +642,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car WRITE_FILE(S["generic_adjective"] , generic_adjective) WRITE_FILE(S["body_size"] , features["body_size"]) WRITE_FILE(S["prosthetic_limbs"] , prosthetic_limbs) + WRITE_FILE(S["learned_languages"] , learned_languages) + WRITE_FILE(S["native_language"] , native_language) WRITE_FILE(S["feature_mcolor"] , features["mcolor"]) WRITE_FILE(S["feature_mcolor2"] , features["mcolor2"]) WRITE_FILE(S["feature_ethcolor"] , features["ethcolor"]) diff --git a/code/modules/clothing/outfits/factions/gezena.dm b/code/modules/clothing/outfits/factions/gezena.dm index ce24f187e64..9b74156b9ab 100644 --- a/code/modules/clothing/outfits/factions/gezena.dm +++ b/code/modules/clothing/outfits/factions/gezena.dm @@ -3,12 +3,6 @@ faction = FACTION_PLAYER_GEZENA // faction_icon = "bg_pgf" -/datum/outfit/job/gezena/post_equip(mob/living/carbon/human/H, visualsOnly) - . = ..() - if(visualsOnly) - return - H.grant_language(/datum/language/kalixcian_common) - //Playable Roles (put in ships): /datum/outfit/job/gezena/assistant name = "PGF - Crewman" diff --git a/code/modules/clothing/outfits/factions/solgov.dm b/code/modules/clothing/outfits/factions/solgov.dm index 5663a9655dd..d78880cd965 100644 --- a/code/modules/clothing/outfits/factions/solgov.dm +++ b/code/modules/clothing/outfits/factions/solgov.dm @@ -3,12 +3,6 @@ faction = FACTION_PLAYER_SOLCON faction_icon = "bg_solgov" -/datum/outfit/job/solgov/post_equip(mob/living/carbon/human/H, visualsOnly) - . = ..() - if(visualsOnly) - return - H.grant_language(/datum/language/solarian_international) - /datum/outfit/job/solgov/assistant name = "SolGov - Scribe" id_assignment = "Scribe" diff --git a/code/modules/faction/faction_datum.dm b/code/modules/faction/faction_datum.dm index 7d8d58c6290..30b9ad6bcf7 100644 --- a/code/modules/faction/faction_datum.dm +++ b/code/modules/faction/faction_datum.dm @@ -8,6 +8,8 @@ var/list/prefixes /// List/Typecache of factions that this faction is allowed to interact with. Non-recursive. var/list/allowed_factions = list() + /// The official language of this faction. Galactic Common by default. + var/official_language = /datum/language/galactic_common /// Theme color for this faction, currently only used for the wiki var/color = "#ffffff" /// Contrast color for this faction, used for links on the wiki @@ -68,6 +70,7 @@ /datum/faction/syndicate/suns name = FACTION_SUNS short_name = "SUNS" + official_language = /datum/language/solarian_international prefixes = PREFIX_SUNS color = "#CD94D3" @@ -79,6 +82,7 @@ /datum/faction/solgov name = FACTION_SOLCON parent_faction = /datum/faction/solgov + official_language = /datum/language/solarian_international prefixes = PREFIX_SOLCON color = "#444e5f" @@ -132,6 +136,7 @@ name = FACTION_PGF short_name = "PGF" parent_faction = /datum/faction/pgf + official_language = /datum/language/gezena_kalixcian prefixes = PREFIX_PGF color = "#359829" diff --git a/code/modules/language/buzzwords.dm b/code/modules/language/buzzwords.dm deleted file mode 100644 index ad41497b80d..00000000000 --- a/code/modules/language/buzzwords.dm +++ /dev/null @@ -1,15 +0,0 @@ -/datum/language/buzzwords - name = "Buzzwords" - desc = "A common language innate to all bugs, made by the rhythmic beating of wings." - speech_verb = "buzzes" - ask_verb = "buzzes" - exclaim_verb = "loudly buzzes" - sing_verb = "hums" - flags = LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD - key = "z" - space_chance = 0 - syllables = list( - "bzz","zzz","z","bz","bzzz","zzzz", "bzzzz", "b", "zz", "zzzzz" - ) - icon_state = "buzz" - default_priority = 90 diff --git a/code/modules/language/common.dm b/code/modules/language/common.dm index 9bcc2e07b08..ce4f0ce4a18 100644 --- a/code/modules/language/common.dm +++ b/code/modules/language/common.dm @@ -3,14 +3,15 @@ name = "Galactic Common" desc = "The common galactic tongue." key = "0" - flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD + flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD | ROUNDSTART_LANGUAGE default_priority = 100 icon_state = "galcom" mutual_understanding = list( /datum/language/solarian_international = 20, - /datum/language/kalixcian_common = 20, + /datum/language/gezena_kalixcian = 20, + /datum/language/zohil_kalixcian = 15, // similar to gezenan ) diff --git a/code/modules/language/draconic.dm b/code/modules/language/gezenan.dm similarity index 69% rename from code/modules/language/draconic.dm rename to code/modules/language/gezenan.dm index 710beb6600a..d1e1699960e 100644 --- a/code/modules/language/draconic.dm +++ b/code/modules/language/gezenan.dm @@ -1,12 +1,12 @@ -/datum/language/kalixcian_common - name = "Kalixcian Common" - desc = "The most prevalent language to come out of Kalixcis, and generally understood by all those native to it." +/datum/language/gezena_kalixcian + name = "Gezenan" + desc = "The most widely spoken Kalixcian language, and the official language of the Pan-Gezena Federation." speech_verb = "hisses" ask_verb = "hisses" exclaim_verb = "roars" sing_verb = "sings" key = "o" - flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD + flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD | ROUNDSTART_LANGUAGE | NO_HISS space_chance = 12 sentence_chance = 0 between_word_sentence_chance = 10 @@ -20,7 +20,11 @@ "ka", "ak", "ke", "ek", "ki", "ik", "ko", "ok", "ku", "uk", "ks", "sk", "sa", "as", "se", "es", "si", "is", "so", "os", "su", "us", "ss", "ss", "ra", "ar", "re", "er", "ri", "ir", "ro", "or", "ru", "ur", "rs", "sr", + "na", "an", "ne", "en", "ni", "in", "no", "on", "nu", "un", "ng", "ts", "a", "a", "e", "e", "i", "i", "o", "o", "u", "u", "s", "s" ) icon_state = "lizard" default_priority = 90 + mutual_understanding = list( + /datum/language/zohil_kalixcian = 90, // enough to sort of understand each other, but not perfectly + ) diff --git a/code/modules/language/language.dm b/code/modules/language/language.dm index b7b66dd4b23..76c9ef38388 100644 --- a/code/modules/language/language.dm +++ b/code/modules/language/language.dm @@ -2,6 +2,12 @@ #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 +/// Number of hex characters the MD5 will use to scramble text (16**SCRAMBLE_HASH_SIZE must not exceed 2**32) +#define SCRAMBLE_HASH_SIZE 4 +/// Probability check using MD5 hash +#define HASH_PROB(probability, raw_hash, hash_offset) (probability >= 100 ? TRUE : (probability / 100 >= hex2num(copytext(raw_hash, hash_offset, hash_offset + SCRAMBLE_HASH_SIZE)) / ((16**SCRAMBLE_HASH_SIZE) - 1))) +/// Pick from a list using MD5 hash (will cause problems if SCRAMBLE_HASH_SIZE is too low) +#define HASH_PICK(list, raw_hash, hash_offset) (list?.len ? list[1 + hex2num(copytext(raw_hash, hash_offset, hash_offset + SCRAMBLE_HASH_SIZE)) % list.len] : null) /// Datum based languages. Easily editable and modular. /datum/language @@ -18,6 +24,8 @@ var/list/syllables /// List of characters that will randomly be inserted between syllables. var/list/special_characters + /// Likelihood of inserting special characters between syllables. + var/special_character_chance = 0 // These modify how syllables are combined. /// Likelihood of making a new sentence after each syllable. @@ -126,7 +134,8 @@ /// 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) + var/list/partial_understanding = hearer.get_partially_understood_languages() + var/understands = hearer.has_language(type) || (partial_understanding?[type] >= 50) if((flags & LANGUAGE_HIDE_ICON_IF_UNDERSTOOD) && understands) return FALSE if((flags & LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD) && !understands) @@ -274,16 +283,16 @@ for(var/word in splittext(input, " ")) var/translate_prob = mutual_languages?[type] || 0 var/base_word = strip_outer_punctuation(word) + var/raw_hash = md5("[lowertext(base_word)]/[GLOB.round_id]") if(translate_prob > 0) // the probability of managing to understand a word is based on how common it is (+10%, -15%) // 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[lowertext(base_word)] || 1250 translate_prob += (10 * (1 - (min(commonness, 1250) / 500))) - if(prob(translate_prob)) + if(HASH_PROB(translate_prob, raw_hash, 1)) scrambled_words += word translated_index += FALSE continue - var/scrambled_word = scramble_word(base_word) scrambled_words += scrambled_word translated_index += (scrambled_word != base_word) @@ -296,11 +305,12 @@ if(!translated_index[i]) sentence += " [word]" continue + var/raw_hash = md5("[lowertext(word)]/[lowertext(scrambled_words[i - 1])]/[GLOB.round_id]") // if the last word was scrambled, always include a space - if(translated_index[i - 1] || prob(between_word_space_chance)) + if(translated_index[i - 1] || HASH_PROB(between_word_space_chance, raw_hash, 1)) sentence += " " // lastly try inserting a new sentence - else if(prob(between_word_sentence_chance)) + else if(HASH_PROB(between_word_sentence_chance, raw_hash, 1 + SCRAMBLE_HASH_SIZE)) sentence += ". " word = capitalize(word) @@ -333,30 +343,29 @@ var/add_period = FALSE word = "" while(length_char(word) < input_size) + // uses the MD5 hash to make sure each word is always scrambled to the same thing within a given round + var/raw_hash = md5("[lowertext(input)]/[lowertext(word)]/[GLOB.round_id]") // add in the last syllable's period or space first if(add_period) word += ". " else if(add_space) word += " " // insert special chars if we're not at the start of the word - else if(word && prob(1) && length(special_characters)) - word += pick(special_characters) + else if(word && HASH_PROB(special_character_chance, raw_hash, 1) && length(special_characters)) + word += HASH_PICK(special_characters, raw_hash, 1 + SCRAMBLE_HASH_SIZE) // generate the next syllable (capitalize if we just added a period) - var/next = pick_weight_recursive(syllables) + var/next = HASH_PICK(syllables, raw_hash, 1 + SCRAMBLE_HASH_SIZE * 2) word += add_period ? capitalize(next) : next // determine if the next syllable gets a period or space - add_period = prob(sentence_chance) - add_space = prob(space_chance) + add_period = HASH_PROB(sentence_chance, raw_hash, 1 + SCRAMBLE_HASH_SIZE * 3) + add_space = HASH_PROB(space_chance, raw_hash, 1 + SCRAMBLE_HASH_SIZE * 4) write_word_cache(input, word) // If they're shouting, we're shouting return (is_uppercase(input) && length_char(input) >= 2) ? uppertext(word) : word -/** - * Called from mob/living/say() - */ -/datum/language/proc/on_say(atom/movable/speaker, message, bubble_type, list/spans = list(), datum/language/language = null) - return - +#undef HASH_PICK +#undef HASH_PROB +#undef SCRAMBLE_HASH_SIZE #undef SCRAMBLE_CACHE_LEN diff --git a/code/modules/language/language_holder.dm b/code/modules/language/language_holder.dm index abaa89e71c7..d78a3b9a7d9 100644 --- a/code/modules/language/language_holder.dm +++ b/code/modules/language/language_holder.dm @@ -362,7 +362,7 @@ GLOBAL_LIST_INIT(prototype_language_holders, init_language_holder_prototypes()) spoken_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM)) /datum/language_holder/lizard/ash - selected_language = /datum/language/kalixcian_common + selected_language = /datum/language/gezena_kalixcian /datum/language_holder/monkey understood_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), @@ -381,7 +381,7 @@ GLOBAL_LIST_INIT(prototype_language_holders, init_language_holder_prototypes()) /datum/language_holder/synthetic understood_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), /datum/language/machine = list(LANGUAGE_ATOM), - /datum/language/kalixcian_common = list(LANGUAGE_ATOM), + /datum/language/gezena_kalixcian = list(LANGUAGE_ATOM), /datum/language/moffic = list(LANGUAGE_ATOM), /datum/language/rachnidian = list(LANGUAGE_ATOM), /datum/language/teceti_unified = list(LANGUAGE_ATOM), @@ -389,7 +389,7 @@ GLOBAL_LIST_INIT(prototype_language_holders, init_language_holder_prototypes()) /datum/language/sign_language = list(LANGUAGE_ATOM)) spoken_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), /datum/language/machine = list(LANGUAGE_ATOM), - /datum/language/kalixcian_common = list(LANGUAGE_ATOM), + /datum/language/gezena_kalixcian = list(LANGUAGE_ATOM), /datum/language/moffic = list(LANGUAGE_ATOM), /datum/language/teceti_unified = list(LANGUAGE_ATOM), /datum/language/solarian_international = list(LANGUAGE_ATOM), diff --git a/code/modules/language/language_menu.dm b/code/modules/language/language_menu.dm index 33b96171c9f..11d76c9e75e 100644 --- a/code/modules/language/language_menu.dm +++ b/code/modules/language/language_menu.dm @@ -23,6 +23,7 @@ var/list/data = list() var/atom/movable/speaker = language_holder.owner + var/list/partial_languages = speaker?.get_partially_understood_languages() data["languages"] = list() for(var/datum/language/language as anything in GLOB.all_languages) var/list/lang_data = list() @@ -37,6 +38,7 @@ lang_data["can_speak"] = !!speaker.has_language(language, SPOKEN_LANGUAGE) lang_data["could_speak"] = !!(language_holder.omnitongue || speaker.could_speak_language(language)) lang_data["can_understand"] = !!speaker.has_language(language, UNDERSTOOD_LANGUAGE) + lang_data["partial_understanding"] = partial_languages?[language] || 0 UNTYPED_LIST_ADD(data["languages"], lang_data) @@ -80,6 +82,14 @@ if("Both") adding_flags |= ALL + if(adding_flags & UNDERSTOOD_LANGUAGE) + var/partial_understanding = tgui_input_number(user, "Set level of understanding:", "[language_datum]", 100, 1, 100) + if(isnull(partial_understanding)) + return + if(partial_understanding < 100) + adding_flags &= ~UNDERSTOOD_LANGUAGE + language_holder.grant_partial_language(language_datum, partial_understanding) + if(LAZYACCESS(language_holder.blocked_languages, language_datum)) choice = tgui_alert(user, "Do you want to lift the blockage that's also preventing the language to be spoken or understood?", "[language_datum]", list("Yes", "No")) if(choice == "Yes") @@ -105,6 +115,8 @@ removing_flags |= ALL language_holder.remove_language(language_datum, removing_flags) + if(removing_flags & UNDERSTOOD_LANGUAGE) + language_holder.remove_partial_language(language_datum) if(is_admin) message_admins("[key_name_admin(user)] removed the [language_name] language to [key_name_admin(speaker)].") log_admin("[key_name(user)] removed the language [language_name] to [key_name(speaker)].") diff --git a/code/modules/language/moffic.dm b/code/modules/language/moffic.dm index 9c43cda6642..a74eea63210 100644 --- a/code/modules/language/moffic.dm +++ b/code/modules/language/moffic.dm @@ -4,7 +4,7 @@ speech_verb = "flutters" ask_verb = "fluffs" exclaim_verb = "floofs" - flags = LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD + flags = LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD | ROUNDSTART_LANGUAGE key = "m" space_chance = 55 syllables = list( //hollow knight diff --git a/code/modules/language/sign.dm b/code/modules/language/sign.dm index 1c2e6af2df3..37669e528d5 100644 --- a/code/modules/language/sign.dm +++ b/code/modules/language/sign.dm @@ -8,7 +8,7 @@ whisper_verb = "subtly signs" sing_verb = "rythmically signs" key = "u" - flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD | SIGNED_LANGUAGE + flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_UNDERSTOOD | ROUNDSTART_LANGUAGE | SIGNED_LANGUAGE | NO_HISS default_priority = 99 use_tone_indicators = TRUE bubble_override = "signlang" diff --git a/code/modules/language/solarian.dm b/code/modules/language/solarian.dm index 425156824e0..bbd0a931649 100644 --- a/code/modules/language/solarian.dm +++ b/code/modules/language/solarian.dm @@ -2,7 +2,7 @@ name = "Solarian International Standard" desc = "The natural fusion of the Solarian languages that survived the Night Of Fire, which gradually coalesced into a single language." key = "c" - flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD + flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD | ROUNDSTART_LANGUAGE default_priority = 90 space_chance = 20 sentence_chance = 0 diff --git a/code/modules/language/teceti_unified.dm b/code/modules/language/teceti_unified.dm index 5d2f8339961..f3a9ffb86be 100644 --- a/code/modules/language/teceti_unified.dm +++ b/code/modules/language/teceti_unified.dm @@ -4,7 +4,7 @@ speech_verb = "chirps" ask_verb = "chirps" key = "f" - flags = LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD + flags = LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD | ROUNDSTART_LANGUAGE space_chance = 40 sentence_chance = 0 between_word_sentence_chance = 20 diff --git a/code/modules/language/zohilan.dm b/code/modules/language/zohilan.dm new file mode 100644 index 00000000000..a64d15ef880 --- /dev/null +++ b/code/modules/language/zohilan.dm @@ -0,0 +1,33 @@ +/datum/language/zohil_kalixcian + name = "Zohilan" + desc = "A Kalixcian language commonly spoken in the Zohil Explorat and its former colonies in the Maxin system. Speakers of other Kalixcian languages often find it difficult to pronounce." + speech_verb = "hisses" + ask_verb = "hisses" + exclaim_verb = "roars" + sing_verb = "sings" + key = "z" + flags = TONGUELESS_SPEECH | LANGUAGE_HIDE_ICON_IF_NOT_UNDERSTOOD | ROUNDSTART_LANGUAGE | NO_HISS + space_chance = 12 + sentence_chance = 0 + between_word_sentence_chance = 10 + between_word_space_chance = 75 + additional_syllable_low = 0 + additional_syllable_high = 3 + syllables = list( + "za", "az", "ze", "ez", "zi", "iz", "zo", "oz", "zu", "uz", "zs", "sz", + "ha", "ah", "he", "eh", "hi", "ih", "ho", "oh", "hu", "uh", "hs", "sh", + "la", "al", "le", "el", "li", "il", "lo", "ol", "lu", "ul", "ls", "sl", + "ka", "ak", "ke", "ek", "ki", "ik", "ko", "ok", "ku", "uk", "ks", "sk", + "sa", "as", "se", "es", "si", "is", "so", "os", "su", "us", "ss", "ss", + "ra", "ar", "re", "er", "ri", "ir", "ro", "or", "ru", "ur", "rs", "sr", + "ta", "at", "te", "et", "ti", "it", "to", "ot", "tu", "ut", "th", "zh", + "qa", "aq", "qe", "eq", "qi", "iq", "qo", "oq", "qu", "uq", "bh", "zl", + "a", "a", "e", "e", "i", "i", "o", "o", "u", "u", "s", "s" + ) + special_characters = list("'") + special_character_chance = 20 + icon_state = "lizard-blue" + default_priority = 90 + mutual_understanding = list( + /datum/language/gezena_kalixcian = 90, // enough to sort of understand each other, but not perfectly + ) diff --git a/code/modules/mob/dead/new_player/ship_select.dm b/code/modules/mob/dead/new_player/ship_select.dm index 276e88ce611..c2fe8b46799 100644 --- a/code/modules/mob/dead/new_player/ship_select.dm +++ b/code/modules/mob/dead/new_player/ship_select.dm @@ -56,6 +56,12 @@ to_chat(spawnee, span_warning("You cannot join this ship anymore, as its join mode has changed!")) return + var/datum/faction/registered_faction = target.shuttle_port.registered_faction + var/datum/language/official_lang = initial(registered_faction.official_language) + if(official_lang != spawnee.client.prefs.native_language && spawnee.client.prefs.learned_languages[official_lang] != LANGUAGE_FLUENT && \ + tgui_alert(spawnee, "Your character does not fully understand this faction's official language ([initial(official_lang.name)]), are you sure?", "Official language", list("Yes", "No")) != "Yes") + return // pop-up warning for new players that forgot to set their + ui.close() var/datum/job/selected_job = locate(params["job"]) in target.job_slots //boots you out if you're banned from officer roles diff --git a/code/modules/overmap/helm.dm b/code/modules/overmap/helm.dm index d645c2da5c2..79441152c3a 100644 --- a/code/modules/overmap/helm.dm +++ b/code/modules/overmap/helm.dm @@ -148,6 +148,11 @@ current_ship.helms -= src current_ship = port.current_ship current_ship.helms |= src + if(port.registered_faction) + var/datum/language/official_language = port.registered_faction.official_language + var/datum/language_holder/lang_holder = get_language_holder() + grant_language(official_language) + lang_holder.selected_language = official_language /** * This proc manually rechecks that the helm computer is connected to a proper ship diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm index 9d8885f6f24..674ed536f6a 100644 --- a/code/modules/shuttle/shuttle.dm +++ b/code/modules/shuttle/shuttle.dm @@ -472,6 +472,9 @@ ///List of all stationary docking ports that spawned on the ship roundstart, used for docking to other ships. var/list/obj/docking_port/stationary/docking_points + ///The faction this shuttle is registered under. + var/datum/faction/registered_faction + /// Does this shuttle play sounds upon landing and takeoff? var/shuttle_sounds = TRUE /// The take off sound to be played @@ -536,6 +539,7 @@ /obj/docking_port/mobile/proc/load(datum/map_template/shuttle/source_template) + registered_faction = source_template.faction shuttle_areas = list() var/list/all_turfs = return_ordered_turfs(x, y, z, dir) for(var/turf/curT as anything in all_turfs) diff --git a/code/modules/surgery/organs/tongue.dm b/code/modules/surgery/organs/tongue.dm index dac77bb7e9a..302d38e7f5e 100644 --- a/code/modules/surgery/organs/tongue.dm +++ b/code/modules/surgery/organs/tongue.dm @@ -16,7 +16,8 @@ var/modifies_speech = FALSE var/static/list/languages_possible_base = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/teceti_unified, //datum/language/solarian_international, /datum/language/moffic, @@ -59,8 +60,8 @@ /obj/item/organ/tongue/lizard/handle_speech(datum/source, list/speech_args) // Sarathi tongues don't hiss when speaking Kalixcian. Or when signing. // we should make non-sarathi hiss in Kalixcian - var/datum/language/language_used = speech_args[SPEECH_LANGUAGE] - if((language_used == /datum/language/kalixcian_common) || (initial(language_used?.flags) & SIGNED_LANGUAGE)) + var/datum/language/lang_type = speech_args[SPEECH_LANGUAGE] + if(initial(lang_type.flags) & NO_HISS) return var/static/regex/lizard_hiss = new("s+", "g") @@ -95,7 +96,8 @@ var/list/phomeme_types = list("sans", "papyrus") var/static/list/languages_possible_skeleton = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/codespeak, /datum/language/monkey, /datum/language/aphasia, @@ -161,7 +163,8 @@ taste_sensitivity = 101 // Not a tongue, they can't taste shit var/static/list/languages_possible_ethereal = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/teceti_unified, /datum/language/solarian_international, /datum/language/moffic, @@ -183,7 +186,8 @@ say_mod = "flutters" var/static/list/languages_possible_moth = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/teceti_unified, /datum/language/solarian_international, /datum/language/moffic, @@ -202,7 +206,8 @@ say_mod = "chirps" var/static/list/languages_possible_kepi = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/teceti_unified, /datum/language/solarian_international, /datum/language/moffic, @@ -223,7 +228,8 @@ say_mod = "shrieks" var/static/list/languages_possible_vox = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/teceti_unified, /datum/language/solarian_international, /datum/language/moffic, @@ -245,13 +251,13 @@ say_mod = "chitters" var/static/list/languages_possible_arachnid = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/codespeak, /datum/language/monkey, /datum/language/aphasia, /datum/language/moffic, - /datum/language/rachnidian, - /datum/language/buzzwords + /datum/language/rachnidian )) /obj/item/organ/tongue/spider/Initialize(mapload) @@ -286,7 +292,8 @@ say_mod = "blorbles" var/static/list/languages_possible_slime = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/teceti_unified, /datum/language/solarian_international, /datum/language/moffic, @@ -420,7 +427,8 @@ modifies_speech = TRUE var/static/list/languages_possible_fly = typecacheof(list( /datum/language/galactic_common, - /datum/language/kalixcian_common, + /datum/language/gezena_kalixcian, + /datum/language/zohil_kalixcian, /datum/language/teceti_unified, /datum/language/solarian_international, /datum/language/moffic, diff --git a/icons/misc/language.dmi b/icons/misc/language.dmi index 9adf235b3a8b7117f3669fdd053eca92bccecde8..c6ced0ec6839fa6817944a0cc611822bcbce629f 100644 GIT binary patch delta 5119 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 delta 4678 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 IM6N<$f}}l2mjD0& diff --git a/shiptest.dme b/shiptest.dme index 03f4b126a71..f35c34b5e57 100644 --- a/shiptest.dme +++ b/shiptest.dme @@ -918,6 +918,7 @@ #include "code\datums\traits\negative\frail.dm" #include "code\datums\traits\negative\heavy_sleeper.dm" #include "code\datums\traits\negative\light_drinker.dm" +#include "code\datums\traits\negative\monolingual.dm" #include "code\datums\traits\negative\mute.dm" #include "code\datums\traits\negative\nearsighted.dm" #include "code\datums\traits\negative\paraplegic.dm" @@ -928,7 +929,6 @@ #include "code\datums\traits\neutral\bald.dm" #include "code\datums\traits\neutral\deviant_tastes.dm" #include "code\datums\traits\neutral\gunslinger.dm" -#include "code\datums\traits\neutral\languages.dm" #include "code\datums\traits\neutral\monochromatic.dm" #include "code\datums\traits\neutral\musician.dm" #include "code\datums\traits\neutral\photographer.dm" @@ -941,6 +941,7 @@ #include "code\datums\traits\positive\friendly.dm" #include "code\datums\traits\positive\light_step.dm" #include "code\datums\traits\positive\self_aware.dm" +#include "code\datums\traits\positive\trilingual.dm" #include "code\datums\votes\_vote_datum.dm" #include "code\datums\votes\custom_vote.dm" #include "code\datums\votes\restart_vote.dm" @@ -2542,11 +2543,10 @@ #include "code\modules\keybindings\focus.dm" #include "code\modules\keybindings\setup.dm" #include "code\modules\language\aphasia.dm" -#include "code\modules\language\buzzwords.dm" #include "code\modules\language\codespeak.dm" #include "code\modules\language\common.dm" -#include "code\modules\language\draconic.dm" #include "code\modules\language\drone.dm" +#include "code\modules\language\gezenan.dm" #include "code\modules\language\language.dm" #include "code\modules\language\language_holder.dm" #include "code\modules\language\language_menu.dm" @@ -2563,6 +2563,7 @@ #include "code\modules\language\teceti_unified.dm" #include "code\modules\language\vox_pidgin.dm" #include "code\modules\language\xenocommon.dm" +#include "code\modules\language\zohilan.dm" #include "code\modules\library\lib_items.dm" #include "code\modules\library\lib_machines.dm" #include "code\modules\library\random_books.dm" diff --git a/tgui/packages/tgui/interfaces/LanguageMenu.tsx b/tgui/packages/tgui/interfaces/LanguageMenu.tsx index ad58fd2163c..03577e0402a 100644 --- a/tgui/packages/tgui/interfaces/LanguageMenu.tsx +++ b/tgui/packages/tgui/interfaces/LanguageMenu.tsx @@ -70,6 +70,7 @@ const LangUnderstandIcon = (props: LanguageProps) => { borderBottom: '2px dotted rgba(255, 255, 255, 0.8)', }} > + {language.partial_understanding}% From c45643910cc550e4d9151c22ef05b657b0f34855 Mon Sep 17 00:00:00 2001 From: "Ossa88 (SYNAPSE)" Date: Thu, 30 Jul 2026 06:42:15 -0700 Subject: [PATCH 2/3] Language system hardening --- code/modules/client/preferences.dm | 34 +++++++++++++++++++ code/modules/client/preferences_savefile.dm | 9 +++++ .../code/modules/language/language_holder.dm | 32 ----------------- 3 files changed, 43 insertions(+), 32 deletions(-) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index adeb1b9c571..9bf04fad3bf 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -1614,6 +1614,18 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(balance + value < 0) to_chat(user, span_warning("Refunding this would cause you to go below your balance!")) return + // PENTEST ADDITION - START - Check if removing Trilingual or adding Monolingual would cause negative language points + if(quirk == "Trilingual" || quirk == "Monolingual") + var/temp_quirks = all_quirks.Copy() + temp_quirks -= quirk + var/old_all_quirks = all_quirks + all_quirks = temp_quirks + var/lang_balance = get_language_point_balance() + all_quirks = old_all_quirks + if(lang_balance < 0) + to_chat(user, span_warning("You cannot remove this quirk as it would leave you with insufficient language points! Reset your languages first.")) + return + // PENTEST ADDITION - END all_quirks -= quirk else var/is_positive_quirk = SSquirks.quirk_points[quirk] > 0 @@ -1623,6 +1635,18 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(balance - value < 0) to_chat(user, span_warning("You don't have enough balance to gain this quirk!")) return + // PENTEST ADDITION - START - Check if adding Monolingual or removing Trilingual would cause negative language points + if(quirk == "Trilingual" || quirk == "Monolingual") + var/temp_quirks = all_quirks.Copy() + temp_quirks += quirk + var/old_all_quirks = all_quirks + all_quirks = temp_quirks + var/lang_balance = get_language_point_balance() + all_quirks = old_all_quirks + if(lang_balance < 0) + to_chat(user, span_warning("You cannot select this quirk as it would leave you with insufficient language points! Reset your languages first.")) + return + // PENTEST ADDITION - END all_quirks += quirk SetQuirks(user) if("reset") @@ -2634,7 +2658,17 @@ GLOBAL_LIST_EMPTY(preferences_datums) character.dna.update_body_size() if(!character_setup && get_language_point_balance() < 0) + // PENTEST ADDITON - START - This is a check to prevent exploits where players can gain more languages than they should be able to. If this happens, it will reset their languages and log it to admins. + log_admin("WARNING: [parent?.ckey] spawned with negative language points. Resetting languages.") + message_admins("WARNING: [key_name_admin(parent)] spawned with negative language points. Languages have been reset.") + // PENTEST ADDITION - END init_learned_languages() // no exploits allowed + else if(get_language_point_balance() < 0) + // Pentest ADDITION - START - Additional check during character setup + log_admin("WARNING: [parent?.ckey] in character setup with negative language points. Resetting languages.") + message_admins("WARNING: [key_name_admin(parent)] in character setup with negative language points. Languages have been reset.") + init_learned_languages() + // PENTEST ADDITION - END character.grant_language(native_language) character.get_language_holder().selected_language = native_language for(var/datum/language/lang_type as anything in learned_languages) diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index e89d7230b5a..17bd8fd7046 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -610,6 +610,15 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car /datum/preferences/proc/save_character() if(!path) return FALSE + + // PENTEST ADDITION - START - Validate language points before saving + var/lang_balance = get_language_point_balance() + if(lang_balance < 0) + log_admin("WARNING: [parent?.ckey] attempted to save character with negative language points ([lang_balance]). Resetting languages.") + message_admins("WARNING: [key_name_admin(parent)] attempted to save character with negative language points ([lang_balance]). Languages have been reset.") + init_learned_languages() + // PENTEST ADDITION - END + var/savefile/S = new /savefile(path) if(!S) return FALSE diff --git a/modular_pentest/master_files/code/modules/language/language_holder.dm b/modular_pentest/master_files/code/modules/language/language_holder.dm index b8d38baf1f8..cdfaef69360 100644 --- a/modular_pentest/master_files/code/modules/language/language_holder.dm +++ b/modular_pentest/master_files/code/modules/language/language_holder.dm @@ -6,35 +6,3 @@ understood_languages = list(/datum/language/swarmer = list(LANGUAGE_ATOM)) spoken_languages = list(/datum/language/swarmer = list(LANGUAGE_ATOM)) blocked_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM)) - -/* -/datum/language_holder/human - understood_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/solarian_international = list(LANGUAGE_ATOM)) - spoken_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/solarian_international = list(LANGUAGE_ATOM)) -*/ - -/datum/language_holder/lizard - understood_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/kalixcian_common = list(LANGUAGE_ATOM)) - spoken_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/kalixcian_common = list(LANGUAGE_ATOM)) - -/datum/language_holder/moth - understood_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/moffic = list(LANGUAGE_ATOM)) - spoken_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/moffic = list(LANGUAGE_ATOM)) - -/datum/language_holder/ethereal - understood_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/kalixcian_common = list(LANGUAGE_ATOM)) - spoken_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/kalixcian_common = list(LANGUAGE_ATOM)) - -/datum/language_holder/kepori - understood_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/teceti_unified = list(LANGUAGE_ATOM)) - spoken_languages = list(/datum/language/galactic_common = list(LANGUAGE_ATOM), - /datum/language/teceti_unified = list(LANGUAGE_ATOM)) From 5daa416e96447f13b130e046cc1bac8c9d393d40 Mon Sep 17 00:00:00 2001 From: generalthrax <139387950+generalthrax@users.noreply.github.com> Date: Sat, 4 Oct 2025 22:19:22 -0700 Subject: [PATCH 3/3] Static Sectors (#5326) ## About The Pull Request Adds an additional sector that the outpost spawns in, along with a normal overmap that the usual shiptest stuff is in - [x] Create systems for every outpost - [x] Colours rough draft - [x] Remove the 20 minute no-jump cooldown at roundstart - [x] Make outpost and wild sector generate - [x] Make outpost sector select from list - [x] Make outpost sector actually spawn the matching outpost - [x] Make outpost sector star static - [x] Make outpost sector spawn no planets - [x] Colours final Unique sectors for each of the outposts - High-Pier system (Orange Dwarf, Chana) (Arrowsong Refueling Platform, CLIP controlled) - Value of Public Works system (Red Dwarf, Ecbatana) (Agni Trading Post, NGR controlled) - Minya system (Yellow Main-Sequence, Aubaine) (Installation Trifuge, Independent) - Persei-277 system (Yellow Main-Sequence, Persei-277) (Yebiri Sipili, NT controlled) These sectors do not contain much of worth other than a small amount of hazards Colors WIP in order according to above image image image image ## Why It's Good For The Game canon outposts and the static sectors theyre within for consistency. lets the standard overmap be a "wild" sector that pirates can probably more reasonably skulk around in. give outposts more presence and ic believability than being in the middle of the unwashed pirate hordes at all times ## Changelog :cl: add: Second overmap containing the outpost that you spawn in, along with standard shiptest overmap (no outpost) add: High-Pier system (CLIP), Value of Public Works system (NGR), Minya system (Indie), Persei-277 system (NT) add: Outposts now spawn only in their corresponding system add: Jump between these two sectors with a 1 minute timer with bluespace jump del: Removed 20 minute "No-Jump" cooldown /:cl: --------- Signed-off-by: generalthrax <139387950+generalthrax@users.noreply.github.com> Co-authored-by: retlaw34 <58402542+retlaw34@users.noreply.github.com> Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com> (cherry picked from commit 3e1defb0792300f6b04bf262146d1f9bee86ebbd) --- code/controllers/subsystem/overmap.dm | 103 +++++++++++++++++++----- code/modules/admin/verbs/randomverbs.dm | 2 +- code/modules/overmap/helm.dm | 8 -- 3 files changed, 85 insertions(+), 28 deletions(-) diff --git a/code/controllers/subsystem/overmap.dm b/code/controllers/subsystem/overmap.dm index 6c474ef77b2..6a8986a46bd 100644 --- a/code/controllers/subsystem/overmap.dm +++ b/code/controllers/subsystem/overmap.dm @@ -23,6 +23,9 @@ SUBSYSTEM_DEF(overmap) /// The mandatory and default star system var/datum/overmap_star_system/default_system + /// The secondary star system that allows planet spawns + var/datum/overmap_star_system/wild_system + ///Should events be processed var/events_enabled = TRUE @@ -41,8 +44,6 @@ SUBSYSTEM_DEF(overmap) //if(length(tracked_star_systems) >= 1) // CRASH("Attempted to create more than 1 star system. Having mutiple star systems is not supported.") - if(length(tracked_star_systems) >= 1) - WARNING("Attempted to create more than 1 star system. Bugs may occur as this isn't very well supported, you have been warned") tracked_star_systems += new_starsystem return new_starsystem @@ -56,7 +57,9 @@ SUBSYSTEM_DEF(overmap) dynamic_encounters = list() events = list() - default_system = create_new_star_system(new /datum/overmap_star_system/shiptest) + var/list/sector_types = pick(subtypesof(/datum/overmap_star_system/safezone)) + default_system = create_new_star_system(new sector_types) + wild_system = create_new_star_system (new /datum/overmap_star_system/shiptest) return ..() /datum/controller/subsystem/overmap/proc/spawn_new_star_system(datum/overmap_star_system/system_to_spawn=/datum/overmap_star_system) @@ -64,7 +67,6 @@ SUBSYSTEM_DEF(overmap) return create_new_star_system(system_to_spawn) return create_new_star_system(new system_to_spawn) - /datum/controller/subsystem/overmap/fire() for(var/datum/overmap_star_system/current_system as anything in tracked_star_systems) if(!current_system.encounters_refresh) @@ -345,6 +347,9 @@ SUBSYSTEM_DEF(overmap) //can our pallete be selected randomly roundstart? set to no for subtypes or if you dont change the pallete var/can_be_selected_randomly = TRUE + /// Datum type for the main outpost spawned here + var/default_outpost_type + COOLDOWN_DECLARE(dynamic_despawn_cooldown) /datum/overmap_star_system/New(generate_now=TRUE) @@ -371,7 +376,7 @@ SUBSYSTEM_DEF(overmap) if(!size) size = CONFIG_GET(number/overmap_size) if(!max_overmap_dynamic_events) - max_overmap_dynamic_events = CONFIG_GET(number/max_overmap_dynamic_events) + max_overmap_dynamic_events = isnull(max_overmap_dynamic_events) overmap_container = new/list(size, size, 0) @@ -516,24 +521,23 @@ SUBSYSTEM_DEF(overmap) /datum/overmap_star_system/proc/spawn_outpost() var/list/location = get_unused_overmap_square_in_radius(rand(4, round(size/5))) - var/datum/overmap/outpost/found_type if(fexists(OUTPOST_OVERRIDE_FILEPATH)) var/file_text = trim_right(file2text(OUTPOST_OVERRIDE_FILEPATH)) // trim_right because there's often a trailing newline var/datum/overmap/outpost/potential_type = text2path(file_text) if(!potential_type || !ispath(potential_type, /datum/overmap/outpost)) stack_trace("SSovermap found an outpost override file at [OUTPOST_OVERRIDE_FILEPATH], but was unable to find the outpost type [potential_type]!") else - found_type = potential_type + default_outpost_type = potential_type fdel(OUTPOST_OVERRIDE_FILEPATH) // don't want it to affect 2 rounds in a row. - if(!found_type) + if(!default_outpost_type) var/list/possible_types = subtypesof(/datum/overmap/outpost) for(var/datum/overmap/outpost/outpost_type as anything in possible_types) if(!initial(outpost_type.main_template)) possible_types -= outpost_type - found_type = pick(possible_types) + default_outpost_type = pick(possible_types) - var/datum/overmap/outpost/our_outpost = new found_type(location, src) + var/datum/overmap/outpost/our_outpost = new default_outpost_type(location, src) //gets rid of nearby events that casue radio interference for(var/direction as anything in GLOB.cardinals) @@ -970,11 +974,7 @@ SUBSYSTEM_DEF(overmap) override_object_colors = TRUE overmap_icon_state = "overmap" - dynamic_probabilities = list(\ - DYNAMIC_WORLD_BEACHPLANET = 10, - DYNAMIC_WORLD_SPACERUIN = 5, - DYNAMIC_WORLD_MOON = 20, - ) + max_overmap_dynamic_events = 0 /datum/overmap_star_system/zx_spectrum_pallete //main colors, used for dockable terrestrials, and background @@ -1067,9 +1067,11 @@ SUBSYSTEM_DEF(overmap) else datum_to_edit.token.add_filter("gloweffect", 5, list("type"="drop_shadow", "color"= "#808080", "size"=2, "offset"=1)) -/datum/overmap_star_system/ngr - name = "Gorlex Controlled - Ecbatana" +/datum/overmap_star_system/safezone/ngr + name = "Gorlex Controlled - Value of Public Works" starname = "Ecbatana" + startype = /datum/overmap/star/dwarf + default_outpost_type = /datum/overmap/outpost/ngr_rock //main colors, used for dockable terrestrials, and background primary_color = "#d9ad82" @@ -1086,6 +1088,69 @@ SUBSYSTEM_DEF(overmap) override_object_colors = TRUE overmap_icon_state = "overmap_dark" +/datum/overmap_star_system/safezone/clip + name = "CLIP Controlled - High-Pier" + starname = "Chana" + startype = /datum/overmap/star/dwarf/orange + default_outpost_type = /datum/overmap/outpost/clip_ocean + + //main colors, used for dockable terrestrials, and background + primary_color = "#6fa8de" + secondary_color = "#96b6d4" + + //hazard colors, used for the overmap hazards and sun + hazard_primary_color = "#d5e3f0" + hazard_secondary_color = "#96a6b5" + + //structure colors, used for ships and outposts/colonies + primary_structure_color = "#97dfe8" + secondary_structure_color = "#6fa8de" + + override_object_colors = TRUE + overmap_icon_state = "overmap_dark" + +/datum/overmap_star_system/safezone/trifuge + name = "Independent - Minya" + starname = "Aubaine" + startype = /datum/overmap/star/medium + default_outpost_type = /datum/overmap/outpost/indie_space + + //main colors, used for dockable terrestrials, and background + primary_color = "#5e5e5e" + secondary_color = "#242424" + + //hazard colors, used for the overmap hazards and sun + hazard_primary_color = "#b56060" + hazard_secondary_color = "#824242" + + //structure colors, used for ships and outposts/colonies + primary_structure_color = "#ffffff" + secondary_structure_color = "#ffffff" + + override_object_colors = TRUE + overmap_icon_state = "overmap" + +/datum/overmap_star_system/safezone/nt + name = "Nanotrasen Controlled - Persei-277" + starname = "Persei-277" + startype = /datum/overmap/star/medium + default_outpost_type = /datum/overmap/outpost/nanotrasen_ice + + //main colors, used for dockable terrestrials, and background + primary_color = "#7e8cd9" + secondary_color = "#33324a" + + //hazard colors, used for the overmap hazards and sun + hazard_primary_color = "#ededed" + hazard_secondary_color = "#7f7db0" + + //structure colors, used for ships and outposts/colonies + primary_structure_color = "#4272db" + secondary_structure_color = "#38a0eb" + + override_object_colors = TRUE + overmap_icon_state = "overmap_dark" + /datum/overmap_star_system/c64 //main colors, used for dockable terrestrials, and background @@ -1105,13 +1170,13 @@ SUBSYSTEM_DEF(overmap) //default shiptest overmap /datum/overmap_star_system/shiptest - has_outpost = TRUE + has_outpost = FALSE can_be_selected_randomly = FALSE encounters_refresh = TRUE + max_overmap_dynamic_events = 15 /datum/overmap_star_system/shiptest/create_map() . = ..() - set_station_name(starname) /datum/overmap_star_system/admin_sandbox name = "Admin Sandbox" diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index 820c5ed62a7..a564e868ad9 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -961,7 +961,7 @@ nova.size = inputed inputed = input(usr, "Choose Maximum amount of Dynamic Events", "Spawn Overmap", nova.max_overmap_dynamic_events) as num - if(!inputed) + if(isnull(inputed)) QDEL_NULL(nova) return nova.max_overmap_dynamic_events = inputed diff --git a/code/modules/overmap/helm.dm b/code/modules/overmap/helm.dm index 79441152c3a..0a3078e4a30 100644 --- a/code/modules/overmap/helm.dm +++ b/code/modules/overmap/helm.dm @@ -50,12 +50,8 @@ icon_state = "computer-solgov" deconpath = /obj/structure/frame/computer/solgov -/datum/config_entry/number/bluespace_jump_wait - default = 5 MINUTES - /obj/machinery/computer/helm/Initialize(mapload, obj/item/circuitboard/C) . = ..() - jump_allowed = world.time + CONFIG_GET(number/bluespace_jump_wait) ntnet_relay = new(src) /obj/machinery/computer/helm/examine(mob/user) @@ -72,10 +68,6 @@ if(current_ship.docked_to || current_ship.docking) say("Bluespace Jump Calibration detected interference in the local area.") return - if(world.time < jump_allowed) - var/jump_wait = DisplayTimeText(jump_allowed - world.time) - say("Bluespace Jump Calibration is currently recharging. ETA: [jump_wait].") - return message_admins("[ADMIN_LOOKUPFLW(usr)] has initiated a bluespace jump in [ADMIN_VERBOSEJMP(src)]") jump_timer = addtimer(CALLBACK(src, PROC_REF(jump_sequence), TRUE), JUMP_CHARGEUP_TIME, TIMER_STOPPABLE) if(new_system)