diff --git a/_maps/map_levels/140x140/tradeport.dmm b/_maps/map_levels/140x140/tradeport.dmm
index c81a0ba456b..bf81ac22046 100644
--- a/_maps/map_levels/140x140/tradeport.dmm
+++ b/_maps/map_levels/140x140/tradeport.dmm
@@ -10698,7 +10698,7 @@
/obj/item/storage/backpack/rig{
pixel_x = -7
},
-/obj/item/universal_translator/ear/all{
+/obj/item/universal_translator/ear/adaptive{
pixel_x = 11;
pixel_y = -3
},
diff --git a/_maps/map_levels/192x192/tradeport.dmm b/_maps/map_levels/192x192/tradeport.dmm
index 21adf748248..28bae4c1729 100644
--- a/_maps/map_levels/192x192/tradeport.dmm
+++ b/_maps/map_levels/192x192/tradeport.dmm
@@ -8862,7 +8862,7 @@
/obj/item/storage/backpack/rig{
pixel_x = -7
},
-/obj/item/universal_translator/ear/all{
+/obj/item/universal_translator/ear/adaptive{
pixel_x = 11;
pixel_y = -3
},
diff --git a/citadel.dme b/citadel.dme
index 19499b43a5a..f5df3f60f28 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -189,6 +189,7 @@
#include "code\__DEFINES\languages\flags.dm"
#include "code\__DEFINES\languages\ids.dm"
#include "code\__DEFINES\languages\legacy.dm"
+#include "code\__DEFINES\languages\translation.dm"
#include "code\__DEFINES\mapping\multiz.dm"
#include "code\__DEFINES\misc\message_ranges.dm"
#include "code\__DEFINES\mobs\grab.dm"
@@ -317,6 +318,7 @@
#include "code\__HELPERS\unsorted\contents.dm"
#include "code\__HELPERS\vfx\appearance_cloning.dm"
#include "code\__HELPERS\vfx\color_animations.dm"
+#include "code\__HELPERS\vfx\flick.dm"
#include "code\__HELPERS\vfx\emissives.dm"
#include "code\__HELPERS\vfx\see_anywhere.dm"
#include "code\__HELPERS\vfx\shake_camera.dm"
@@ -481,6 +483,7 @@
#include "code\controllers\subsystem\dbcore\query.dm"
#include "code\controllers\subsystem\job\_job.dm"
#include "code\controllers\subsystem\job\_legacy_job_stuff.dm"
+#include "code\controllers\subsystem\job\jexp.dm"
#include "code\controllers\subsystem\job\job_manager.dm"
#include "code\controllers\subsystem\job\joining.dm"
#include "code\controllers\subsystem\job\roundstart.dm"
@@ -1363,7 +1366,6 @@
#include "code\game\objects\items\devices\spy_bug.dm"
#include "code\game\objects\items\devices\suit_cooling.dm"
#include "code\game\objects\items\devices\t_scanner.dm"
-#include "code\game\objects\items\devices\taperecorder.dm"
#include "code\game\objects\items\devices\text_to_speech.dm"
#include "code\game\objects\items\devices\traitordevices.dm"
#include "code\game\objects\items\devices\transfer_valve.dm"
@@ -1389,6 +1391,9 @@
#include "code\game\objects\items\devices\radio\intercom.dm"
#include "code\game\objects\items\devices\radio\jammer.dm"
#include "code\game\objects\items\devices\radio\radio.dm"
+#include "code\game\objects\items\devices\tape_recorder\cassette_tape.dm"
+#include "code\game\objects\items\devices\tape_recorder\tape_iterator.dm"
+#include "code\game\objects\items\devices\tape_recorder\tape_recorder.dm"
#include "code\game\objects\items\id_cards\cards.dm"
#include "code\game\objects\items\id_cards\sprite_stacks_cit.dm"
#include "code\game\objects\items\id_cards\station_ids.dm"
@@ -2552,42 +2557,44 @@
#include "code\modules\keybindings\keybind\mob.dm"
#include "code\modules\keybindings\keybind\movement.dm"
#include "code\modules\keybindings\keybind\robot.dm"
-#include "code\modules\language\abstract.dm"
-#include "code\modules\language\animal.dm"
-#include "code\modules\language\core.dm"
-#include "code\modules\language\event.dm"
-#include "code\modules\language\galactic.dm"
+#include "code\modules\language\hivemind.dm"
#include "code\modules\language\holder.dm"
#include "code\modules\language\language.dm"
-#include "code\modules\language\monkey.dm"
-#include "code\modules\language\silicons.dm"
-#include "code\modules\language\antagonist\borer.dm"
-#include "code\modules\language\antagonist\changeling.dm"
-#include "code\modules\language\antagonist\cult.dm"
-#include "code\modules\language\antagonist\swarmbot.dm"
-#include "code\modules\language\antagonist\xenomorph.dm"
-#include "code\modules\language\species\_species.dm"
-#include "code\modules\language\species\adherent.dm"
-#include "code\modules\language\species\akula.dm"
-#include "code\modules\language\species\alraune.dm"
-#include "code\modules\language\species\altevian.dm"
-#include "code\modules\language\species\birdsong.dm"
-#include "code\modules\language\species\celestial.dm"
-#include "code\modules\language\species\diona.dm"
-#include "code\modules\language\species\human.dm"
-#include "code\modules\language\species\moth.dm"
-#include "code\modules\language\species\naramadi.dm"
-#include "code\modules\language\species\phoronoid.dm"
-#include "code\modules\language\species\shadekin.dm"
-#include "code\modules\language\species\skrell.dm"
-#include "code\modules\language\species\tajaran.dm"
-#include "code\modules\language\species\teshari.dm"
-#include "code\modules\language\species\unathi.dm"
-#include "code\modules\language\species\vasilissan.dm"
-#include "code\modules\language\species\vox.dm"
-#include "code\modules\language\species\vulpkanin.dm"
-#include "code\modules\language\species\zaddat.dm"
-#include "code\modules\language\species\zorren.dm"
+#include "code\modules\language\translation.dm"
+#include "code\modules\language\languages\abstract.dm"
+#include "code\modules\language\languages\animal.dm"
+#include "code\modules\language\languages\core.dm"
+#include "code\modules\language\languages\event.dm"
+#include "code\modules\language\languages\galactic.dm"
+#include "code\modules\language\languages\monkey.dm"
+#include "code\modules\language\languages\silicons.dm"
+#include "code\modules\language\languages\antagonist\borer.dm"
+#include "code\modules\language\languages\antagonist\changeling.dm"
+#include "code\modules\language\languages\antagonist\cult.dm"
+#include "code\modules\language\languages\antagonist\swarmbot.dm"
+#include "code\modules\language\languages\antagonist\xenomorph.dm"
+#include "code\modules\language\languages\species\_species.dm"
+#include "code\modules\language\languages\species\adherent.dm"
+#include "code\modules\language\languages\species\akula.dm"
+#include "code\modules\language\languages\species\alraune.dm"
+#include "code\modules\language\languages\species\altevian.dm"
+#include "code\modules\language\languages\species\birdsong.dm"
+#include "code\modules\language\languages\species\celestial.dm"
+#include "code\modules\language\languages\species\diona.dm"
+#include "code\modules\language\languages\species\human.dm"
+#include "code\modules\language\languages\species\moth.dm"
+#include "code\modules\language\languages\species\naramadi.dm"
+#include "code\modules\language\languages\species\phoronoid.dm"
+#include "code\modules\language\languages\species\shadekin.dm"
+#include "code\modules\language\languages\species\skrell.dm"
+#include "code\modules\language\languages\species\tajaran.dm"
+#include "code\modules\language\languages\species\teshari.dm"
+#include "code\modules\language\languages\species\unathi.dm"
+#include "code\modules\language\languages\species\vasilissan.dm"
+#include "code\modules\language\languages\species\vox.dm"
+#include "code\modules\language\languages\species\vulpkanin.dm"
+#include "code\modules\language\languages\species\zaddat.dm"
+#include "code\modules\language\languages\species\zorren.dm"
#include "code\modules\library\lib_items.dm"
#include "code\modules\library\lib_machines.dm"
#include "code\modules\library\lib_readme.dm"
@@ -2970,8 +2977,8 @@
#include "code\modules\mob\living\silicon\login.dm"
#include "code\modules\mob\living\silicon\say.dm"
#include "code\modules\mob\living\silicon\silicon.dm"
-#include "code\modules\mob\living\silicon\silicon_vr.dm"
#include "code\modules\mob\living\silicon\subystems.dm"
+#include "code\modules\mob\living\silicon\translation.dm"
#include "code\modules\mob\living\silicon\ai\ai.dm"
#include "code\modules\mob\living\silicon\ai\ai_movement.dm"
#include "code\modules\mob\living\silicon\ai\ai_remote_control.dm"
@@ -2986,6 +2993,7 @@
#include "code\modules\mob\living\silicon\ai\logout.dm"
#include "code\modules\mob\living\silicon\ai\malf.dm"
#include "code\modules\mob\living\silicon\ai\perspective.dm"
+#include "code\modules\mob\living\silicon\ai\translation.dm"
#include "code\modules\mob\living\silicon\decoy\death.dm"
#include "code\modules\mob\living\silicon\decoy\decoy.dm"
#include "code\modules\mob\living\silicon\decoy\life.dm"
@@ -3019,6 +3027,7 @@
#include "code\modules\mob\living\silicon\robot\robot_movement.dm"
#include "code\modules\mob\living\silicon\robot\robot_remote_control.dm"
#include "code\modules\mob\living\silicon\robot\syndicate.dm"
+#include "code\modules\mob\living\silicon\robot\translation.dm"
#include "code\modules\mob\living\silicon\robot\dogborg\dog_modules_vr.dm"
#include "code\modules\mob\living\silicon\robot\dogborg\dog_sleeper_vr.dm"
#include "code\modules\mob\living\silicon\robot\drone\_defines.dm"
diff --git a/code/__DEFINES/_bitfields.dm b/code/__DEFINES/_bitfields.dm
index 2b2b7df589f..c12f2a04557 100644
--- a/code/__DEFINES/_bitfields.dm
+++ b/code/__DEFINES/_bitfields.dm
@@ -1,6 +1,11 @@
-#define DEFINE_BITFIELD(v, f) /datum/bitfield/##v { \
+#define DEFINE_BITFIELD(v, f) /datum/bitfield/single/##v { \
flags = ##f; \
variable = #v; \
}
+#define DEFINE_SHARED_BITFIELD(n, v, f) /datum/bitfield/multi/##n { \
+ flags = ##f; \
+ variables = ##v; \
+}
+
#define BITFIELD(thing) #thing = thing
diff --git a/code/__DEFINES/_cooldowns.dm b/code/__DEFINES/_cooldowns.dm
index 8dfa20ac996..1f844d0fbcf 100644
--- a/code/__DEFINES/_cooldowns.dm
+++ b/code/__DEFINES/_cooldowns.dm
@@ -23,8 +23,10 @@
// INDEXES FOR VAR COOLDOWNS - DO NOT USE UPPERCASE, DO NOT USE cooldown_, APPENDS ADDED AUTOMATICALLY
// INDEXES FOR TIMER COOLDOWNS - Must be unique!
-#define COOLDOWN_SONAR_PULSE "sonar_pulse"
-#define COOLDOWN_POWER_DRAIN_WARNING "power_drain_warning"
+#define CD_INDEX_SONAR_PULSE "sonar_pulse"
+#define CD_INDEX_POWER_DRAIN_WARNING "power_drain_warning"
+#define CD_INDEX_TAPE_TRANSLATION "tape_translation"
+#define CD_INDEX_TAPE_PRINT "tape_print"
//TIMER COOLDOWN MACROS
diff --git a/code/__DEFINES/languages/flags.dm b/code/__DEFINES/languages/flags.dm
index 479abaad645..8dd270e78dc 100644
--- a/code/__DEFINES/languages/flags.dm
+++ b/code/__DEFINES/languages/flags.dm
@@ -1,34 +1,90 @@
-//! ## LANGUAGE_FLAGS
-/// Language is available if the speaker is whitelisted.
-#define WHITELISTED (1<<0)
+//! /datum/language/var/language_flags
+/// Language is available if the speaker is whitelisted, or if granted by a species.
+#define LANGUAGE_WHITELISTED (1<<0)
/// Language can only be acquired by spawning or an admin.
-#define RESTRICTED (1<<1)
+#define LANGUAGE_RESTRICTED (1<<1)
/// Language has a significant non-verbal component. Speech is garbled without line-of-sight.
-#define NONVERBAL (1<<2)
+#define LANGUAGE_NONVERBAL (1<<2)
/// Language is completely non-verbal. Speech is displayed through emotes for those who can understand.
-#define SIGNLANG (1<<3)
+#define LANGUAGE_SIGNLANG (1<<3)
/// Broadcast to all mobs with this language.
-#define HIVEMIND (1<<4)
+#define LANGUAGE_HIVEMIND (1<<4)
/// Do not add to general languages list.
-#define NONGLOBAL (1<<5)
+#define LANGUAGE_NONGLOBAL (1<<5)
/// All mobs can be assumed to speak and understand this language. (audible emotes)
-#define INNATE (1<<6)
+#define LANGUAGE_INNATE (1<<6)
/// Do not show the "\The [speaker] talks into \the [radio]" message
-#define NO_TALK_MSG (1<<7)
+#define LANGUAGE_NO_TALK_MSG (1<<7)
/// No stuttering, slurring, or other speech problems.
-#define NO_STUTTER (1<<8)
+#define LANGUAGE_NO_STUTTER (1<<8)
/// Language is not based on vision or sound. //Todo: Add this into the say code and use it for the rootspeak languages.
-#define ALT_TRANSMIT (1<<9)
+#define LANGUAGE_ALT_TRANSMIT (1<<9)
DEFINE_BITFIELD(language_flags, list(
- BITFIELD(WHITELISTED),
- BITFIELD(RESTRICTED),
- BITFIELD(NONVERBAL),
- BITFIELD(SIGNLANG),
- BITFIELD(HIVEMIND),
- BITFIELD(NONGLOBAL),
- BITFIELD(INNATE),
- BITFIELD(NO_TALK_MSG),
- BITFIELD(NO_STUTTER),
- BITFIELD(ALT_TRANSMIT),
+ BITFIELD(LANGUAGE_WHITELISTED),
+ BITFIELD(LANGUAGE_RESTRICTED),
+ BITFIELD(LANGUAGE_NONVERBAL),
+ BITFIELD(LANGUAGE_SIGNLANG),
+ BITFIELD(LANGUAGE_HIVEMIND),
+ BITFIELD(LANGUAGE_NONGLOBAL),
+ BITFIELD(LANGUAGE_INNATE),
+ BITFIELD(LANGUAGE_NO_TALK_MSG),
+ BITFIELD(LANGUAGE_NO_STUTTER),
+ BITFIELD(LANGUAGE_ALT_TRANSMIT),
+))
+
+//! translation_class on /datum/language and /datum/translation_context as well as all translation operations
+/// common galactic languages; most translators can instantly translate this
+#define TRANSLATION_CLASS_LEVEL_1 (1<<0)
+/// high end translators can translate these; you're looking at science/cargo; low level learning translators learn this
+#define TRANSLATION_CLASS_LEVEL_2 (1<<1)
+/// extremely high end translators can translate these; high end learning translators learn this; anything above is probably adminbus
+#define TRANSLATION_CLASS_LEVEL_3 (1<<2)
+/// xenomorphs, etc
+#define TRANSLATION_CLASS_MONSTER (1<<18)
+/// simpler animal languages
+#define TRANSLATION_CLASS_ANIMAL (1<<19)
+/// magical languages
+#define TRANSLATION_CLASS_ARCANE (1<<20)
+/// encrypted languages ; usually conjunction with machine class
+#define TRANSLATION_CLASS_ENCRYPTED (1<<21)
+/// tribal/native languages
+#define TRANSLATION_CLASS_TRIBAL (1<<22)
+/// machine-like languages
+#define TRANSLATION_CLASS_MACHINE (1<<23)
+
+/// core galactic species
+#define TRANSLATION_CLASS_DEFAULT_CORE_RACE (TRANSLATION_CLASS_LEVEL_1)
+/// standard galactic species
+#define TRANSLATION_CLASS_DEFAULT_STANDARD_RACE (TRANSLATION_CLASS_LEVEL_1)
+/// people's snowflakes that wanna be super speshul and be mYsTerIoUS
+#define TRANSLATION_CLASS_DEFAULT_RARE_RACE (TRANSLATION_CLASS_LEVEL_2)
+/// native species
+#define TRANSLATION_CLASS_DEFAULT_NATIVE_RACE (TRANSLATION_CLASS_LEVEL_3 | TRANSLATION_CLASS_TRIBAL)
+/// core alt langauges for galaxy
+#define TRANSLATION_CLASS_DEFAULT_CORE_LANGUAGE (TRANSLATION_CLASS_LEVEL_1)
+
+/// what cyborgs get by default
+#define TRANSLATION_CLASSES_CYBORG_SPEAKS (TRANSLATION_CLASS_LEVEL_1)
+/// what ais get by default
+#define TRANSLATION_CLASSES_AI_SPEAKS (TRANSLATION_CLASS_LEVEL_1)
+/// what all universal translators get intrinsically
+#define TRANSLATION_CLASSES_STANDARD_TRANSLATE (TRANSLATION_CLASS_LEVEL_1)
+/// what adaptive universal translators can learn
+#define TRANSLATION_CLASSES_STANDARD_ADAPTIVE (TRANSLATION_CLASS_LEVEL_2)
+
+DEFINE_SHARED_BITFIELD(translation_classes, list(
+ "translation_class",
+ "translation_class_forbid",
+ "translation_class_learn",
+ "translation_class_learn_forbid",
+), list(
+ BITFIELD(TRANSLATION_CLASS_LEVEL_1),
+ BITFIELD(TRANSLATION_CLASS_LEVEL_2),
+ BITFIELD(TRANSLATION_CLASS_LEVEL_3),
+ BITFIELD(TRANSLATION_CLASS_MONSTER),
+ BITFIELD(TRANSLATION_CLASS_ANIMAL),
+ BITFIELD(TRANSLATION_CLASS_ENCRYPTED),
+ BITFIELD(TRANSLATION_CLASS_TRIBAL),
+ BITFIELD(TRANSLATION_CLASS_MACHINE),
))
diff --git a/code/__DEFINES/languages/translation.dm b/code/__DEFINES/languages/translation.dm
new file mode 100644
index 00000000000..0755ca4e4a2
--- /dev/null
+++ b/code/__DEFINES/languages/translation.dm
@@ -0,0 +1,5 @@
+//! adaptive translation stuff
+//! don't mess with this unless you know math
+
+/// above this point we're considered perfect, stop doing expensive computations
+#define TRANSLATION_CONTEXT_PERFECT_THRESHOLD 0.98
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 4e9855b8309..3f0e09718eb 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -568,3 +568,16 @@ GLOBAL_LIST_EMPTY(##LIST_NAME);\
#define WINDOW_STATE_CROWBRARED_IN 2
/// window is secured to frame
#define WINDOW_STATE_SECURED_TO_FRAME 3
+
+//! cassette tapes
+//* data list
+#define CASSETTE_TAPE_DATA_MESSAGE 1
+#define CASSETTE_TAPE_DATA_NAME 2
+#define CASSETTE_TAPE_DATA_LANGUAGE 3
+#define CASSETTE_TAPE_DATA_DELAY 4
+#define CASSETTE_TAPE_DATA_OPCODE 5
+/// list length for data reads
+#define CASSETTE_TAPE_DATA_LEN 5
+//* metadata
+#define CASSETTE_METADATA_LANGUAGE "%"
+#define CASSETTE_METADATA_NAME "^"
diff --git a/code/__HELPERS/animations.dm b/code/__HELPERS/animations.dm
index 6a8cc78538d..ea9fedb2c31 100644
--- a/code/__HELPERS/animations.dm
+++ b/code/__HELPERS/animations.dm
@@ -1,7 +1,3 @@
-/proc/remove_images_from_clients(image/I, list/show_to)
- for(var/client/C in show_to)
- C.images -= I
- qdel(I)
/proc/fade_out(image/I, list/show_to)
animate(I, alpha = 0, time = 0.5 SECONDS, easing = EASE_IN)
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 3b2c346d512..4cc7b616b97 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -369,26 +369,6 @@
return FALSE
return TRUE
-/proc/flick_overlay(image/I, list/show_to, duration, gc_after)
- for(var/client/C in show_to)
- C.images += I
- spawn(duration)
- if(gc_after)
- qdel(I)
- for(var/client/C in show_to)
- C.images -= I
-
-/**
- * Wrapper for flick_overlay, flicks to everyone who can see the target atom.
- */
-/proc/flick_overlay_view(image/I, atom/target, duration, gc_after)
- var/list/viewing = list()
- for(var/m in viewers(target))
- var/mob/M = m
- if(M.client)
- viewing += M.client
- flick_overlay(I, viewing, duration, gc_after)
-
/proc/isInSight(atom/A, atom/B)
var/turf/Aturf = get_turf(A)
var/turf/Bturf = get_turf(B)
diff --git a/code/__HELPERS/lists/associations.dm b/code/__HELPERS/lists/associations.dm
index 62e565f26a2..cf4c390fcab 100644
--- a/code/__HELPERS/lists/associations.dm
+++ b/code/__HELPERS/lists/associations.dm
@@ -24,6 +24,13 @@
flat_list[flat_list[i]] = TRUE
return flat_list
+/proc/make_associative_inplace_keep_values(list/flat_list)
+ for(var/i in 1 to length(flat_list))
+ if(!isnull(flat_list[flat_list[i]]))
+ continue
+ flat_list[flat_list[i]] = TRUE
+ return flat_list
+
/proc/assoc_list_strip_value_inplace(list/input)
for(var/i in 1 to length(input))
input[input[i]] = null
diff --git a/code/__HELPERS/vfx/flick.dm b/code/__HELPERS/vfx/flick.dm
new file mode 100644
index 00000000000..47df576b353
--- /dev/null
+++ b/code/__HELPERS/vfx/flick.dm
@@ -0,0 +1,20 @@
+/// Removes an image from a client's `.images`. Useful as a callback.
+/proc/remove_image_from_client(image/image, client/remove_from)
+ remove_from?.images -= image
+
+/proc/remove_images_from_clients(image/I, list/show_to)
+ for(var/client/C in show_to)
+ C.images -= I
+
+/proc/flick_overlay(image/I, list/show_to, duration)
+ for(var/client/C in show_to)
+ C.images += I
+ addtimer(CALLBACK(GLOBAL_PROC, /proc/remove_images_from_clients, I, show_to), duration, TIMER_CLIENT_TIME)
+
+/proc/flick_overlay_view(image/I, atom/target, duration) //wrapper for the above, flicks to everyone who can see the target atom
+ var/list/viewing = list()
+ for(var/m in viewers(target))
+ var/mob/M = m
+ if(M.client)
+ viewing += M.client
+ flick_overlay(I, viewing, duration)
diff --git a/code/_globals/bitfields.dm b/code/_globals/bitfields.dm
index 409b9fa8ec2..4ff36ae26f7 100644
--- a/code/_globals/bitfields.dm
+++ b/code/_globals/bitfields.dm
@@ -1,18 +1,27 @@
GLOBAL_LIST_INIT(bitfields, generate_bitfields())
/datum/bitfield
+ /// An associative list of the readable flag and its true value
+ var/list/flags
+
+/datum/bitfield/single
/// The variable name that contains the bitfield
var/variable
- /// An associative list of the readable flag and its true value
- var/list/flags
+/datum/bitfield/multi
+ /// our variable names
+ var/list/variables
/// Turns /datum/bitfield subtypes into a list for use in debugging
/proc/generate_bitfields()
var/list/bitfields = list()
- for (var/_bitfield in subtypesof(/datum/bitfield))
- var/datum/bitfield/bitfield = new _bitfield
+ for (var/_bitfield in subtypesof(/datum/bitfield/single))
+ var/datum/bitfield/single/bitfield = new _bitfield
bitfields[bitfield.variable] = bitfield.flags
+ for (var/_bitfield in subtypesof(/datum/bitfield/multi))
+ var/datum/bitfield/multi/bitfield = new _bitfield
+ for(var/n in bitfield.variables)
+ bitfields[n] = bitfield.flags
// sue me i haven't slept in 18 hours
// tl;dr convert these into DEFINE_BITFIELD's later
#define FLAG(flag) "[#flag]" = flag
diff --git a/code/_globals/regexes.dm b/code/_globals/regexes.dm
index 7e571576213..5b3938deab8 100644
--- a/code/_globals/regexes.dm
+++ b/code/_globals/regexes.dm
@@ -17,3 +17,5 @@ GLOBAL_DATUM_INIT(angular_brackets, /regex, regex(@"[<>]", "g"))
GLOBAL_DATUM_INIT(filename_forbidden_chars, /regex, regex(@{""|[\\\n\t/?%*:|<>]|\.\."}, "g"))
GLOBAL_PROTECT(filename_forbidden_chars)
// had to use the OR operator for quotes instead of putting them in the character class because it breaks the syntax highlighting otherwise.
+
+GLOBAL_DATUM_INIT(multi_space_splitter, /regex, regex("\[ \]+"))
diff --git a/code/controllers/subsystem/characters/languages.dm b/code/controllers/subsystem/characters/languages.dm
index 93cdf4ca7d2..7921019e36d 100644
--- a/code/controllers/subsystem/characters/languages.dm
+++ b/code/controllers/subsystem/characters/languages.dm
@@ -23,7 +23,7 @@
language_lookup[L.id] = L
language_names[L.name] = L
language_paths[path] = L
- if(!(L.language_flags & NONGLOBAL) && L.key)
+ if(!(L.language_flags & LANGUAGE_NONGLOBAL) && L.key)
language_keys[L.key] = L
tim_sort(language_names, /proc/cmp_auto_compare, TRUE)
diff --git a/code/controllers/subsystem/job/jexp.dm b/code/controllers/subsystem/job/jexp.dm
new file mode 100644
index 00000000000..141086fa28f
--- /dev/null
+++ b/code/controllers/subsystem/job/jexp.dm
@@ -0,0 +1,19 @@
+/**
+ * job experience system
+ */
+
+//! wip
+
+/**
+ * immediate check for if someone has jexp bypass
+ *
+ * may return incorrect answer if datbase hasn't loaded; use has_jexp_bypass_blocking() to ensure it's loaded.
+ */
+/client/proc/has_jexp_bypass()
+ SHOULD_NOT_SLEEP(TRUE)
+ return !!(admin_datums[ckey] || (database.player_flags & PLAYER_FLAG_JEXP_EXEMPT))
+
+/client/proc/has_jexp_bypass_blocking()
+ database.block_on_available()
+ return !!(admin_datums[ckey] || (database.player_flags & PLAYER_FLAG_JEXP_EXEMPT))
+
diff --git a/code/controllers/subsystem/sound/soundbyte_manager.dm b/code/controllers/subsystem/sound/soundbyte_manager.dm
index 4dfd35886ed..4953195cca4 100644
--- a/code/controllers/subsystem/sound/soundbyte_manager.dm
+++ b/code/controllers/subsystem/sound/soundbyte_manager.dm
@@ -68,6 +68,8 @@
* defaults to alias itself if not found, so passing in a valid sound datum is fine
*/
/datum/controller/subsystem/sounds/proc/fetch_asset(alias)
+ if(!istext(alias))
+ return alias
return fetch_soundbyte(alias)?.get_asset() || alias
/**
diff --git a/code/datums/soundbytes/_soundbyte.dm b/code/datums/soundbytes/_soundbyte.dm
index 5fe21374d7a..718b468671f 100644
--- a/code/datums/soundbytes/_soundbyte.dm
+++ b/code/datums/soundbytes/_soundbyte.dm
@@ -9,6 +9,13 @@
* todo: in hindsight i'm shit at writing sound backend, this works but it could be a lot better.
* todo: "is_sfx" only works to put typepaths & alias in for compiled in sounds. maybe we can have runtime soundbytes later? if needed.
*
+ * this stuff is obviously expensive and shouldn't be used at a whim
+ *
+ * todo: refactor soundbytes; they should be resolved via get_sfx global proc
+ * todo: and automatically register their sounds in global list by id
+ * todo: have var/direct_access for this; global list lookup for sound asset is WAY cheaper than
+ * todo: subsystem resolve, use that in cases of short sounds like terminal clicks.
+ *
* currently holds:
* - path/file - required for preload
* - name - defaults to filename
diff --git a/code/game/atoms/atoms.dm b/code/game/atoms/atoms.dm
index ec91495183d..2e7c090478f 100644
--- a/code/game/atoms/atoms.dm
+++ b/code/game/atoms/atoms.dm
@@ -814,7 +814,7 @@
/// message is the message output to anyone who can hear.
/// deaf_message (optional) is what deaf people will see.
/// hearing_distance (optional) is the range, how many tiles away the message can be heard.
-/atom/proc/audible_message(var/message, var/deaf_message, var/hearing_distance)
+/atom/proc/audible_message(var/message, var/deaf_message, var/hearing_distance, datum/language/lang)
var/range = hearing_distance || world.view
var/list/hear = get_mobs_and_objs_in_view_fast(get_turf(src),range,remote_ghosts = FALSE)
@@ -826,13 +826,16 @@
var/obj/O = obj
O.show_message(message, 2, deaf_message, 1)
+ var/no_runechat = FALSE
for(var/mob in hearing_mobs)
var/mob/M = mob
var/msg = message
+ if(lang && !(lang.name in M.languages))
+ msg = lang.scramble(msg)
M.show_message(msg, 2, deaf_message, 1)
heard_to_floating_message += M
- INVOKE_ASYNC(src, /atom/movable/proc/animate_chat, (message ? message : deaf_message), null, FALSE, heard_to_floating_message, 30)
-
+ if(!no_runechat)
+ INVOKE_ASYNC(src, /atom/movable/proc/animate_chat, (message ? message : deaf_message), null, FALSE, heard_to_floating_message, 30)
/atom/movable/proc/dropInto(var/atom/destination)
while(istype(destination))
@@ -870,16 +873,22 @@
/atom/proc/GenerateTag()
return
-/atom/proc/atom_say(message)
+// todo: refactor this shit to be unified saycode, christ
+/atom/proc/atom_say(message, datum/language/L)
if(!message)
return
var/list/speech_bubble_hearers = list()
+ var/no_runechat = FALSE
for(var/mob/M in get_hearers_in_view(MESSAGE_RANGE_COMBAT_LOUD, src))
- M.show_message("[src] [atom_say_verb], \"[message]\"", 2, null, 1)
+ var/processed = message
+ if(L && !(L.name in M.languages))
+ processed = L.scramble(message)
+ no_runechat = TRUE
+ M.show_message("[src] [L?.speech_verb || atom_say_verb], \"[processed]\"", 2, null, 1)
if(M.client)
speech_bubble_hearers += M.client
- if(length(speech_bubble_hearers))
+ if(length(speech_bubble_hearers) && !no_runechat)
var/image/I = generate_speech_bubble(src, "[bubble_icon][say_test(message)]", FLY_LAYER)
I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
INVOKE_ASYNC(GLOBAL_PROC, /.proc/flick_overlay, I, speech_bubble_hearers, 30)
diff --git a/code/game/objects/items/devices/tape_recorder/cassette_tape.dm b/code/game/objects/items/devices/tape_recorder/cassette_tape.dm
new file mode 100644
index 00000000000..a14009fef18
--- /dev/null
+++ b/code/game/objects/items/devices/tape_recorder/cassette_tape.dm
@@ -0,0 +1,295 @@
+/**
+ * this is a horror
+ * we use the typepaths as enums...
+ */
+/datum/cassette_opcode
+/datum/cassette_opcode/next_is_audible_emote
+/datum/cassette_opcode/next_is_direct_broadcast
+
+/**
+ * cassette tapes; capable of storing spoken audio and their languages
+ *
+ * all access must be through getters/setters due to how ridiculously synchronized
+ * and packed this has to be to store player speech.
+ */
+/obj/item/cassette_tape
+ name = "tape"
+ desc = "A magnetic tape that can hold up to ten minutes of content."
+ icon = 'icons/obj/device.dmi'
+ icon_state = "tape_white"
+ item_state = "analyzer"
+ w_class = ITEMSIZE_TINY
+ matter = list(MAT_STEEL=20, "glass"=5)
+ force = 1
+ throw_force = 0
+
+ /**
+ * this is a horror
+ * - text = message
+ * - positive number = delay of this many deciseconds after
+ * - negative number = list index of metadata
+ * - typepath = /datum/cassette_opcode opcode
+ *
+ * anything that modifies a message (e.g. delay) comes BEFORE a message.
+ *
+ * i'm sorry.
+ */
+ var/list/reel
+ /**
+ * names, language ids go in here
+ * prefix ^ = name
+ * prefix % = language
+ */
+ var/list/metadata
+ /// lookup for language id to metadata index
+ var/list/language_lookup
+ /// lookup for verbatim names to metadata indeix
+ var/list/name_lookup
+ /// active iterators
+ VAR_PRIVATE/list/datum/cassette_tape_iterator/iterators
+ /// active recording iterator
+ var/datum/cassette_tape_iterator/write/recording_lock
+ /// active translation iterator, or otherwise an iterator being used to rewrite our contents entirely
+ var/datum/cassette_tape_iterator/translator/translation_lock
+ /// max capacity in messages
+ var/capacity_messages = 1800 // it would take you 30 minutes of nonstop chatter to deplete this
+ /// used capacity in messages
+ var/used_messages = 0
+ /// max capacity in deciseconds to avoid people spamming these things for no reason
+ var/capacity_time = 30 MINUTES
+ /// used time
+ var/used_time = 0
+ /// last world.time of "tick"
+ var/last_recording_tick
+ /// last world.time of tangible messages
+ var/last_message_time
+ /// last language id
+ var/last_language_id
+ /// last speaker name
+ var/last_speaker_name
+ /// data list length - [text, speaker name, language id, opcode]
+ var/const/data_list_length = 4
+
+ /// did someone ruin this tape by unraveling the tape?
+ var/ruined = FALSE
+
+/obj/item/cassette_tape/Destroy()
+ kill_iterators()
+ return ..()
+
+/obj/item/cassette_tape/proc/wipe()
+ kill_iterators()
+ reel = null
+ metadata = null
+ used_messages = 0
+ used_time = 0
+ last_recording_tick = null
+ last_language_id = null
+ last_speaker_name = null
+ language_lookup = null
+ name_lookup = null
+
+/obj/item/cassette_tape/update_overlays()
+ . = ..()
+ if(ruined)
+ . += "ribbonoverlay"
+
+/obj/item/cassette_tape/fire_act()
+ ruin()
+
+/obj/item/cassette_tape/attack_self(mob/user)
+ . = ..()
+ if(user.a_intent != INTENT_HARM)
+ return
+ if(!ruined)
+ to_chat(user, "You pull out all the tape!")
+ ruin()
+
+/obj/item/cassette_tape/proc/ruin()
+ ruined = TRUE
+ update_icon()
+
+/obj/item/cassette_tape/proc/fix()
+ ruined = FALSE
+ update_icon()
+
+/obj/item/cassette_tape/attackby(obj/item/I, mob/user, params)
+ if(ruined && I.is_screwdriver())
+ to_chat(user, "You start winding the tape back in...")
+ playsound(src, I.tool_sound, 50, 1)
+ if(do_after(user, 120 * I.tool_speed, target = src))
+ to_chat(user, "You wound the tape back in.")
+ fix()
+ return
+ else if(istype(I, /obj/item/pen))
+ if(loc == user && !user.incapacitated())
+ var/new_name = input(user, "What would you like to label the tape?", "Tape labeling") as null|text
+ if(isnull(new_name))
+ return
+ new_name = sanitizeSafe(new_name)
+ if(new_name)
+ name = "tape - '[new_name]'"
+ to_chat(user, "You label the tape '[new_name]'.")
+ else
+ var/old_name = name
+ name = "tape"
+ to_chat(user, "You scratch off '[old_name]' from the label.")
+ return
+ ..()
+
+/**
+ * returns an iterator for reading
+ * returns null on fail, otherwise a read iterator
+ */
+/obj/item/cassette_tape/proc/iterator()
+ if(translation_lock)
+ return
+ var/datum/cassette_tape_iterator/I = new
+ I.tape = src
+ LAZYADD(iterators, I)
+ I._init() // to first pos
+ return I
+
+/obj/item/cassette_tape/proc/kill_iterators()
+ QDEL_LIST_NULL(iterators)
+ QDEL_NULL(recording_lock)
+
+/obj/item/cassette_tape/proc/iterator_killed(datum/cassette_tape_iterator/I)
+ if(I == recording_lock)
+ recording_lock = null
+ if(iterators)
+ iterators -= I
+
+/**
+ * try to lock this tape for recording
+ * returns null on fail, otherwise a write-capable iterator
+ */
+/obj/item/cassette_tape/proc/obtain_recording_lock()
+ if(translation_lock)
+ return
+ if(recording_lock)
+ return recording_lock
+ if(full())
+ return
+ LAZYINITLIST(reel)
+ LAZYINITLIST(metadata)
+ LAZYINITLIST(language_lookup)
+ LAZYINITLIST(name_lookup)
+ recording_lock = new /datum/cassette_tape_iterator/write
+ recording_lock.tape = src
+ recording_lock.current_language_id = last_language_id
+ recording_lock.current_speaker_name = last_speaker_name
+ return recording_lock
+
+/**
+ * locks tape for operation via translator
+ * kicks off all read/write iterators
+ * return null on fail, otherwise a translation iterator
+ */
+/obj/item/cassette_tape/proc/lock_for_translation()
+ kill_iterators()
+ if(translation_lock)
+ return
+ translation_lock = new /datum/cassette_tape_iterator/translator
+ translation_lock.tape = src
+ return translation_lock
+
+/obj/item/cassette_tape/proc/full()
+ return used_messages >= capacity_messages || used_time >= capacity_time
+
+/obj/item/cassette_tape/proc/increment_messages()
+ ++used_messages
+ if(full())
+ kill_iterators()
+
+
+/**
+ * returns index in metadata
+ */
+/obj/item/cassette_tape/proc/inject_latest_name(name)
+ var/index = name_lookup[name]
+ if(!index)
+ metadata += "^[name]"
+ name_lookup[name] = index = metadata.len
+ last_speaker_name = name
+ reel += -index
+
+/**
+ * returns index in metadata
+ */
+/obj/item/cassette_tape/proc/inject_latest_language(id)
+ var/index = language_lookup[id]
+ if(!index)
+ metadata += "%[id]"
+ language_lookup[id] = index = metadata.len
+ last_language_id = id
+ reel += -index
+
+//Random colour tapes
+/obj/item/cassette_tape/random/Initialize(mapload)
+ . = ..()
+ icon_state = "tape_[pick("white", "blue", "red", "yellow", "purple")]"
+
+/obj/item/cassette_tape/premade
+ /**
+ * JSON: [[message, delay, speaker name, language id, opcode], ...]
+ * message mandatory, rest can be skipped
+ * delay defaults to 2 seconds, rest defaults to last value
+ * if no last value, defaults to "Unknown" name and common language
+ */
+ var/list/preformatted_data
+
+/obj/item/cassette_tape/premade/Initialize(mapload)
+ . = ..()
+ inject(preformatted_data)
+ preformatted_data = null
+
+/obj/item/cassette_tape/premade/proc/inject(str)
+ wipe()
+ var/list/decoded
+ if(!str)
+ return
+ if(!islist(str))
+ decoded = safe_json_decode(str)
+ else
+ decoded = str
+ if(!length(str))
+ return
+ last_recording_tick = 0
+ last_language_id = LANGUAGE_ID_COMMON
+ last_speaker_name = "Unknown"
+ var/msg
+ var/delay
+ var/name
+ var/lang
+ var/opcode
+ language_lookup = list()
+ name_lookup = list()
+ for(var/list/L in decoded)
+ var/dec_len = length(L)
+ msg = L[1]
+ if(dec_len >= 2)
+ delay = L[2]
+ else
+ delay = 1 SECONDS
+ if(dec_len >= 3)
+ name = L[3]
+ else
+ name = null
+ if(dec_len >= 4)
+ lang = L[4]
+ else
+ lang = null
+ if(dec_len >= 5)
+ opcode = text2path(L[5])
+ ASSERT(ispath(opcode, /datum/cassette_opcode))
+ else
+ opcode = null
+ if(opcode)
+ reel += opcode
+ if(name && name != last_speaker_name)
+ inject_latest_name(name)
+ if(lang && lang != last_language_id)
+ inject_latest_language(lang)
+ reel += delay // delay after
+ reel += msg
diff --git a/code/game/objects/items/devices/tape_recorder/tape_iterator.dm b/code/game/objects/items/devices/tape_recorder/tape_iterator.dm
new file mode 100644
index 00000000000..d831f604d6c
--- /dev/null
+++ b/code/game/objects/items/devices/tape_recorder/tape_iterator.dm
@@ -0,0 +1,305 @@
+/**
+ * allows iteration of a cassette tape
+ *
+ * it is YOUR job to monitor for qdels!
+ */
+/datum/cassette_tape_iterator
+ /// tape
+ var/obj/item/cassette_tape/tape
+ /// index in reel
+ var/reel_index
+ /// current speaker name
+ var/current_speaker_name
+ /// current language id
+ var/current_language_id
+ /// current language datum to save on lookups
+ var/datum/language/current_language
+ //? we stage things because we have to be one *ahead* of the playback so delays are done properly.
+ /// staged message
+ var/staged_msg
+ /// staged opcode
+ var/staged_opcode
+ /// staged delay
+ var/staged_delay
+ /// default delay
+ var/static/default_delay = 1 SECONDS
+
+/datum/cassette_tape_iterator/Destroy()
+ tape?.iterator_killed(src)
+ tape = null // lol if you try to access after you get a runtime
+ return ..()
+
+//? a note on iterators
+//? key here of knowing how this works is
+//? staged vars are flushed every step
+//? when advancing, we read what's before a message, then stage it
+//? peek, and next, always emit staged vars
+//? when initializing, we read up to the first delay WITHOUT setting staged
+//? that's how we emit an "empty" delay without a message.
+//? if we get two opcodes/language changes/etc, we don't care,
+//? because the only sync information we need are messages,
+//? and the first and second delays.
+
+/**
+ * returns delay
+ */
+/datum/cassette_tape_iterator/proc/_advance()
+ PRIVATE_PROC(TRUE)
+ // prep
+ if(!reel_index)
+ return
+ var/read
+ var/index = reel_index
+ var/list/reel = tape.reel
+ staged_msg = null
+ staged_opcode = null
+ staged_delay = default_delay
+ // scan
+ while(index <= reel.len)
+ read = reel[index]
+ if(istext(read))
+ staged_msg = read
+ // flush
+ reel_index = index + 1
+ return
+ else if(isnum(read))
+ if(read >= 0)
+ staged_delay = read
+ else
+ var/metadata = tape.metadata[-read]
+ var/val = copytext(metadata, 2)
+ switch(metadata[1])
+ if(CASSETTE_METADATA_NAME)
+ current_speaker_name = val
+ if(CASSETTE_METADATA_LANGUAGE)
+ current_language_id = val
+ current_language = SScharacters.resolve_language_id(val)
+ else if(ispath(read, /datum/cassette_opcode))
+ staged_opcode = read
+ index++
+ // we moved past
+ reel_index = null
+
+/datum/cassette_tape_iterator/proc/_init()
+ // reset
+ if(!tape.reel.len)
+ reel_index = null
+ return
+ reel_index = 1
+ current_speaker_name = "Unknown"
+ current_language_id = LANGUAGE_ID_COMMON
+ current_language = SScharacters.resolve_language_id(LANGUAGE_ID_COMMON)
+ staged_msg = null
+ staged_opcode = null
+ staged_delay = 1 SECONDS
+ // read up to the first delay; if there isn't any, we just read up to the first message
+ var/read
+ var/index = reel_index
+ var/list/reel = tape.reel
+ while(index <= reel.len)
+ read = reel[index]
+ if(istext(read))
+ staged_msg = read
+ reel_index = index + 1
+ return
+ else if(isnum(read))
+ if(read >= 0)
+ staged_delay = read
+ reel_index = index + 1
+ return
+ else
+ var/metadata = tape.metadata[-read]
+ var/val = copytext(metadata, 2)
+ switch(metadata[1])
+ if(CASSETTE_METADATA_NAME)
+ current_speaker_name = val
+ if(CASSETTE_METADATA_LANGUAGE)
+ current_language_id = val
+ current_language = SScharacters.resolve_language_id(val)
+ else if(ispath(read, /datum/cassette_opcode))
+ staged_opcode = read
+ index++
+ // we moved past
+ reel_index = null
+
+/**
+ * dumps next data to a returned list and advances; returns null if end
+ * [message, speaker name, language datum, delay, opcode]
+ * delay is to be used to delay after read.
+ * null name means it's a noise in the room
+ * null message means it's just a delay (or maybe read the opcode)
+ */
+/datum/cassette_tape_iterator/proc/next_slow()
+ if(!reel_index)
+ return
+ . = list(
+ staged_msg,
+ current_speaker_name,
+ current_language,
+ staged_delay,
+ staged_opcode
+ )
+ _advance()
+
+/**
+ * dumps next data to a provided list and advances
+ * [message, speaker name, language datum, delay, opcode]
+ * delay is to be used to delay after read.
+ * null name means it's a noise in the room
+ * null message means it's just a delay (or maybe read the opcode)
+ * returns false if nothing's there
+ */
+/datum/cassette_tape_iterator/proc/next_fast(list/L)
+ if(!reel_index)
+ return FALSE
+ L[CASSETTE_TAPE_DATA_DELAY] = staged_delay
+ L[CASSETTE_TAPE_DATA_LANGUAGE] = current_language
+ L[CASSETTE_TAPE_DATA_NAME] = current_speaker_name
+ L[CASSETTE_TAPE_DATA_OPCODE] = staged_opcode
+ L[CASSETTE_TAPE_DATA_MESSAGE] = staged_msg
+ _advance()
+ return TRUE
+
+/**
+ * dumps next data to a returned list without advancing; returns null if end
+ * [message, speaker name, language datum, delay, opcode]
+ * delay is to be used to delay after read.
+ * null name means it's a noise in the room
+ * null message means it's just a delay (or maybe read the opcode)
+ */
+/datum/cassette_tape_iterator/proc/peek_slow()
+ if(!reel_index)
+ return
+ . = list(
+ staged_msg,
+ current_speaker_name,
+ current_language,
+ staged_delay,
+ staged_opcode
+ )
+
+/**
+ * dumps next data to a provided list without advancing
+ * [message, speaker name, language datum, delay, opcode]
+ * delay is to be used to delay after read.
+ * null name means it's a noise in the room
+ * null message means it's just a delay (or maybe read the opcode)
+ * returns false if nothing's there
+ */
+/datum/cassette_tape_iterator/proc/peek_fast(list/L)
+ if(!reel_index)
+ return FALSE
+ L[CASSETTE_TAPE_DATA_DELAY] = staged_delay
+ L[CASSETTE_TAPE_DATA_LANGUAGE] = current_language
+ L[CASSETTE_TAPE_DATA_NAME] = current_speaker_name
+ L[CASSETTE_TAPE_DATA_OPCODE] = staged_opcode
+ L[CASSETTE_TAPE_DATA_MESSAGE] = staged_msg
+ return TRUE
+
+/**
+ * used to **write** to a tape.
+ */
+/datum/cassette_tape_iterator/write
+
+/**
+ * expects **raw** message
+ */
+/datum/cassette_tape_iterator/write/proc/write_speech(message, speaker_name, language_id, override_pause)
+ if(language_id != tape.last_language_id)
+ tape.inject_latest_language(language_id)
+ if(speaker_name != tape.last_speaker_name)
+ tape.inject_latest_name(speaker_name)
+ tape.reel += world.time - tape.last_message_time
+ tape.last_message_time = world.time
+ tape.reel += message
+ tape.increment_messages()
+
+/**
+ * expects **raw** message
+ *
+ * speaker_name to null for actions that are heard by the recorder not associated to anyone
+ */
+/datum/cassette_tape_iterator/write/proc/write_emote(message, speaker_name, override_pause)
+ if(speaker_name != tape.last_speaker_name)
+ tape.inject_latest_name(speaker_name)
+ tape.reel += world.time - tape.last_message_time
+ tape.last_message_time = world.time
+ tape.reel += /datum/cassette_opcode/next_is_audible_emote
+ tape.reel += message
+ tape.increment_messages()
+
+/datum/cassette_tape_iterator/write/proc/tick(dt)
+ tape.used_time += world.time - tape.last_recording_tick
+ tape.last_recording_tick = world.time
+
+/datum/cassette_tape_iterator/write/proc/mark_start()
+ tape.last_message_time = world.time
+ tape.last_recording_tick = world.time
+
+/datum/cassette_tape_iterator/write/proc/mark_stop()
+ if(!tape.reel.len)
+ // bro we haven't written anything
+ return
+ if(tape.reel[tape.reel.len] == "Jumping to next recording segment...")
+ return
+ if(tape.last_language_id != LANGUAGE_ID_COMMON)
+ tape.inject_latest_language(LANGUAGE_ID_COMMON)
+ if(tape.last_speaker_name != "\[Recorder\]")
+ tape.inject_latest_name("\[Recorder\]")
+ tape.reel += world.time - tape.last_message_time
+ tape.last_message_time = world.time
+ tape.reel += /datum/cassette_opcode/next_is_direct_broadcast
+ tape.reel += "Jumping to next recording segment..."
+ tape.used_time += world.time - tape.last_recording_tick
+ tape.last_recording_tick = world.time
+
+/**
+ * used to translate a tape
+ */
+/datum/cassette_tape_iterator/translator
+
+/**
+ * returns lines translated
+ */
+/datum/cassette_tape_iterator/translator/proc/translate(datum/translation_context/context)
+ // ez
+ . = 0
+ var/list/reel = tape.reel
+ var/list/metadata = tape.metadata
+ var/static/common_str = "[CASSETTE_METADATA_LANGUAGE][LANGUAGE_ID_COMMON]"
+ var/common_index = metadata.Find(common_str)
+ var/translating = FALSE
+ var/ignore_next = FALSE
+ if(!common_index)
+ metadata += common_str
+ common_index = metadata.len
+ tape.language_lookup["[LANGUAGE_ID_COMMON]"] = common_index
+ for(var/i in 1 to reel.len)
+ var/read = reel[i]
+ if(isnum(read) && read > 0)
+ var/mdstr = metadata[read]
+ if(mdstr == common_str)
+ translating = FALSE
+ continue
+ if(mdstr[1] == CASSETTE_METADATA_LANGUAGE)
+ var/id = copytext(mdstr, 2)
+ var/datum/language/L = SScharacters.resolve_language_id(id)
+ if(context.can_translate(L, require_perfect = TRUE))
+ reel[i] = common_index
+ translating = TRUE
+ else
+ translating = FALSE
+ else if(istext(read))
+ if(ignore_next)
+ ignore_next = FALSE
+ else if(translating)
+ ++.
+ else if(ispath(read, /datum/cassette_opcode))
+ switch(read)
+ if(/datum/cassette_opcode/next_is_audible_emote)
+ ignore_next = TRUE
+ if(/datum/cassette_opcode/next_is_direct_broadcast)
+ ignore_next = TRUE
+
+// todo: compactor
+// todo: transformer/mapper
diff --git a/code/game/objects/items/devices/tape_recorder/tape_recorder.dm b/code/game/objects/items/devices/tape_recorder/tape_recorder.dm
new file mode 100644
index 00000000000..d49a4efb150
--- /dev/null
+++ b/code/game/objects/items/devices/tape_recorder/tape_recorder.dm
@@ -0,0 +1,461 @@
+/obj/item/tape_recorder
+ name = "universal recorder"
+ desc = "A device that can record to cassette tapes, and play them. It automatically translates the content in playback."
+ icon_state = "taperecorder_empty"
+ item_state = "analyzer"
+ icon = 'icons/obj/device.dmi'
+ w_class = ITEMSIZE_SMALL
+ slot_flags = SLOT_BELT
+ throw_force = 2
+ throw_speed = 4
+ throw_range = 20
+ matter = list(MAT_STEEL = 60, MAT_GLASS = 30)
+
+ /// inserted tape
+ var/obj/item/cassette_tape/tape = /obj/item/cassette_tape/random
+ /// obtained lock on the tape, whether read or write
+ var/datum/cassette_tape_iterator/tape_iterator
+ /// are we in writing mode? null if we shouldn't be doing anything
+ var/write_lock
+
+ /// are we playing?
+ var/playing = FALSE
+ /// timerid for play tick
+ var/play_timerid
+ /// how long a delay has to be before we skip it
+ var/play_skip_threshold = 10 SECONDS
+
+ /// are we recording?
+ var/recording = FALSE
+
+
+/obj/item/tape_recorder/Initialize(mapload)
+ . = ..()
+ if(ispath(tape))
+ tape = new tape(src)
+ update_icon()
+ listening_objects += src
+
+/obj/item/tape_recorder/Destroy()
+ stop_everything()
+ listening_objects -= src
+ if(tape)
+ QDEL_NULL(tape)
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/obj/item/tape_recorder/attackby(obj/item/I, mob/user, params)
+ if(istype(I, /obj/item/cassette_tape))
+ if(tape)
+ to_chat(user, "There's already a tape inside.")
+ return
+ if(!user.attempt_insert_item_for_installation(I, src))
+ return
+ tape = I
+ to_chat(user, "You insert [I] into [src].")
+ update_icon()
+ return
+ ..()
+
+/obj/item/tape_recorder/fire_act()
+ if(tape)
+ tape.ruin() //Fires destroy the tape
+ return ..()
+
+/obj/item/tape_recorder/attack_hand(mob/user)
+ if(user.get_inactive_held_item() == src)
+ if(tape)
+ eject()
+ return
+ ..()
+
+/obj/item/tape_recorder/proc/stop_recording(mob/user, silent)
+ if(!recording)
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] isn't recording."))
+ return
+ var/datum/cassette_tape_iterator/write/writer = tape_iterator
+ if(!writer)
+ recording = FALSE
+ CRASH("no writer?")
+ writer.mark_stop()
+ _release_lock()
+ recording = FALSE
+ if(user && !silent)
+ to_chat(user, SPAN_NOTICE("Recording stopped."))
+ update_icon()
+
+/obj/item/tape_recorder/proc/start_recording(mob/user, silent)
+ if(!tape)
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("There's no tape in [src]!"))
+ return
+ if(tape.ruined)
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] makes a scratchy noise."))
+ return
+ if(recording)
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] is already recording."))
+ return
+ if(playing)
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] can't both record and play at the same time."))
+ return
+ if(is_busy())
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] is busy!"))
+ return
+ if(obj_flags & EMAGGED)
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] makes a scratchy noise."))
+ return
+ if(tape.full())
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("The tape is full!"))
+ return
+ if(!_write_lock())
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("The tape is busy."))
+ return
+ var/datum/cassette_tape_iterator/write/writer = tape_iterator
+ writer.mark_start()
+ if(user && !silent)
+ to_chat(user, SPAN_NOTICE("Recording started."))
+ recording = TRUE
+ update_icon()
+
+/obj/item/tape_recorder/proc/stop_playing(mob/user, silent, ending)
+ if(!silent)
+ if(ending)
+ audible_message("[SPAN_BOLD("[src]")]: End of recording.")
+ else
+ audible_message("[SPAN_BOLD("[src]")]: Playback stopped.")
+
+ if(play_timerid)
+ deltimer(play_timerid)
+ _release_lock()
+ playing = FALSE
+ update_icon()
+
+ if(obj_flags & EMAGGED)
+ tape.ruin()
+ audible_message("Tape Recorder: This tape recorder will self-destruct in... Five.")
+ addtimer(CALLBACK(src, /atom/proc/audible_message, "Tape Recorder: Four."), 1 SECONDS)
+ addtimer(CALLBACK(src, /atom/proc/audible_message, "Tape Recorder: Three."), 2 SECONDS)
+ addtimer(CALLBACK(src, /atom/proc/audible_message, "Tape Recorder: Two."), 3 SECONDS)
+ addtimer(CALLBACK(src, /atom/proc/audible_message, "Tape Recorder: One."), 4 SECONDS)
+ addtimer(CALLBACK(src, /obj/item/tape_recorder/proc/explode), 5 SECONDS)
+
+/obj/item/tape_recorder/proc/start_playing(mob/user, silent)
+ if(!tape)
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("There's no tape in [src]."))
+ return
+ if(tape.ruined)
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] makes a scratchy noise."))
+ return
+ if(is_recording())
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] can't playback while recording."))
+ return
+ if(is_playing())
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] is already playing!"))
+ return
+ if(!_read_lock())
+ if(user && !silent)
+ to_chat(user, SPAN_WARNING("[src] failed to lock the tape for reading."))
+ return
+ if(!silent)
+ audible_message("[SPAN_BOLD("[src]")]: Playback started.")
+ playing = TRUE
+ update_icon()
+ _play_next()
+
+/obj/item/tape_recorder/proc/_play_next()
+ PRIVATE_PROC(TRUE)
+ var/datum/cassette_tape_iterator/reader = tape_iterator
+ var/list/got = reader.next_slow()
+ if(!got)
+ stop_playing(ending = TRUE)
+ return
+ var/msg = got[CASSETTE_TAPE_DATA_MESSAGE]
+ if(msg)
+ var/datum/language/L = got[CASSETTE_TAPE_DATA_LANGUAGE]
+ var/speaker = got[CASSETTE_TAPE_DATA_NAME]
+ var/opcode = got[CASSETTE_TAPE_DATA_OPCODE]
+ switch(opcode)
+ if(/datum/cassette_opcode/next_is_audible_emote)
+ audible_message("[SPAN_BOLD("[src]")]: *[SPAN_BOLD("-someone-")] [msg]*")
+ if(/datum/cassette_opcode/next_is_direct_broadcast)
+ audible_message("[SPAN_BOLD("[src]")]: [msg]")
+ else
+ audible_message("[SPAN_BOLD("[src]")]: [speaker] [L.speech_verb], [msg]", L)
+ // delays
+ var/delay = got[CASSETTE_TAPE_DATA_DELAY]
+ if(delay > play_skip_threshold)
+ audible_message("[SPAN_BOLD("[src]")]: Skipping [round(delay * 0.1)] seconds of silence.")
+ delay = 3 SECONDS
+ play_timerid = addtimer(CALLBACK(src, .proc/_play_next), delay, TIMER_STOPPABLE)
+
+/obj/item/tape_recorder/verb/playback_memory()
+ set name = "Playback Tape"
+ set category = "Object"
+
+ if(usr.incapacitated())
+ return
+ start_playing(usr)
+
+/obj/item/tape_recorder/proc/is_playing()
+ return playing
+
+/obj/item/tape_recorder/proc/is_recording()
+ return recording
+
+/obj/item/tape_recorder/proc/is_busy()
+ return playing || recording
+
+/obj/item/tape_recorder/proc/_release_lock()
+ PRIVATE_PROC(TRUE)
+ if(tape_iterator)
+ QDEL_NULL(tape_iterator)
+ return TRUE
+ return FALSE
+
+/obj/item/tape_recorder/proc/_lock_deleted(datum/source)
+ PRIVATE_PROC(TRUE)
+ if(source == tape_iterator)
+ tape_iterator = null
+ else
+ stack_trace("lock deleted called with something that isn't ours?")
+
+/obj/item/tape_recorder/proc/_read_lock()
+ PRIVATE_PROC(TRUE)
+ ASSERT(!tape_iterator)
+ tape_iterator = tape.iterator()
+ return !!tape_iterator
+
+/obj/item/tape_recorder/proc/_write_lock()
+ PRIVATE_PROC(TRUE)
+ ASSERT(!tape_iterator)
+ tape_iterator = tape.obtain_recording_lock()
+ return !!tape_iterator
+
+/obj/item/tape_recorder/proc/has_read_lock()
+ return write_lock == FALSE
+
+/obj/item/tape_recorder/proc/has_write_lock()
+ return write_lock == TRUE
+
+/obj/item/tape_recorder/proc/has_lock()
+ return !isnull(write_lock)
+
+/obj/item/tape_recorder/verb/eject()
+ set name = "Eject Tape"
+ set category = "Object"
+
+ if(usr.incapacitated())
+ return
+ if(!tape)
+ to_chat(usr, "There's no tape in \the [src].")
+ return
+ if(obj_flags & EMAGGED)
+ to_chat(usr, "The tape seems to be stuck inside.")
+ return
+ to_chat(usr, "You remove [tape] from [src].")
+ stop_everything()
+ usr.grab_item_from_interacted_with(tape, src)
+ tape = null
+ update_icon()
+
+/obj/item/tape_recorder/proc/stop_everything(mob/user, silent)
+ stop_playing(user, silent)
+ stop_recording(user, silent)
+
+/obj/item/tape_recorder/process(delta_time)
+ if(!is_recording())
+ return PROCESS_KILL
+ var/datum/cassette_tape_iterator/write/writer = tape_iterator
+ writer.tick(delta_time)
+
+//! todo : saycode refactor
+/obj/item/tape_recorder/hear_talk(mob/living/M as mob, msg, var/verb="says", datum/language/speaking=null)
+ if(!recording)
+ return
+ var/datum/cassette_tape_iterator/write/writer = tape_iterator
+ if(speaking.id == LANGUAGE_ID_NOISE)
+ writer.write_emote(msg, M.name)
+ else
+ writer.write_speech(msg, M.name, speaking.id)
+
+/obj/item/tape_recorder/see_emote(mob/M as mob, text, var/emote_type)
+ if(emote_type != 2) //only hearable emotes
+ return
+ if(!recording)
+ return
+ var/datum/cassette_tape_iterator/write/writer = tape_iterator
+ writer.write_emote(text, M.name)
+
+/obj/item/tape_recorder/show_message(msg, type, alt, alt_type)
+ var/recordedtext
+ if (msg && type == 2) //must be hearable
+ recordedtext = msg
+ else if (alt && alt_type == 2)
+ recordedtext = alt
+ else
+ return
+ if(!recording)
+ return
+ var/datum/cassette_tape_iterator/write/writer = tape_iterator
+ writer.write_emote(recordedtext)
+//! end
+
+/obj/item/tape_recorder/emag_act(var/remaining_charges, var/mob/user)
+ if(obj_flags & EMAGGED)
+ obj_flags |= EMAGGED
+ recording = 0
+ to_chat(user, "PZZTTPFFFT")
+ update_icon()
+ return TRUE
+ else
+ to_chat(user, "It is already emagged!")
+
+/obj/item/tape_recorder/proc/explode()
+ var/turf/T = get_turf(loc)
+ if(ismob(loc))
+ var/mob/M = loc
+ to_chat(M, "\The [src] explodes!")
+ if(T)
+ T.hotspot_expose(700,125)
+ explosion(T, light_impact_range = 3)
+ qdel(src)
+
+/obj/item/tape_recorder/verb/record()
+ set name = "Start Recording"
+ set category = "Object"
+
+ if(usr.incapacitated())
+ return
+ start_recording(usr)
+
+/obj/item/tape_recorder/verb/stop()
+ set name = "Stop"
+ set category = "Object"
+
+ if(usr.incapacitated())
+ return
+ if(!is_busy())
+ to_chat(usr, SPAN_WARNING("[src] isn't doing anything right now."))
+ stop_everything(usr)
+
+/obj/item/tape_recorder/verb/wipe_tape()
+ set name = "Wipe Tape"
+ set category = "Object"
+
+ if(usr.incapacitated())
+ return
+ if(obj_flags & EMAGGED)
+ to_chat(usr, "The tape recorder makes a scratchy noise.")
+ return
+ if(!tape)
+ to_chat(usr, SPAN_WARNING("The tape recorder has no tape inside."))
+ return
+ if(tape.ruined)
+ to_chat(usr, "The tape recorder makes a scratchy noise.")
+ return
+ if(is_busy())
+ to_chat(usr, SPAN_WARNING("The tape recorder is busy!"))
+ return
+ tape.wipe()
+ to_chat(usr, SPAN_WARNING("You wipe the tape."))
+
+/obj/item/tape_recorder/verb/print_transcript()
+ set name = "Print Transcript"
+ set category = "Object"
+
+ if(usr.incapacitated())
+ return
+ if(!tape)
+ to_chat(usr, "There's no tape!")
+ return
+ if(tape.ruined)
+ to_chat(usr, "The tape recorder makes a scratchy noise.")
+ return
+ if(obj_flags & EMAGGED)
+ to_chat(usr, "The tape recorder makes a scratchy noise.")
+ return
+ if(!print_ready())
+ to_chat(usr, "The recorder can't print that fast!")
+ return
+ if(is_busy())
+ to_chat(usr, "You can't print the transcript while playing or recording!")
+ return
+ if(!_read_lock())
+ to_chat(usr, SPAN_WARNING("The tape is busy!"))
+ return
+
+ to_chat(usr, "Transcript printed.")
+ var/obj/item/paper/P = new /obj/item/paper(get_turf(src))
+ var/list/constructed = list()
+ constructed += "
Transcript:
"
+ var/list/head = list()
+ head.len = CASSETTE_TAPE_DATA_LEN
+ var/datum/cassette_tape_iterator/reader = tape_iterator
+ var/msg
+ var/name
+ var/datum/language/lang
+ var/delay
+ var/current_time = 0
+ var/opcode
+ while(reader.next_fast(head))
+ msg = head[CASSETTE_TAPE_DATA_MESSAGE]
+ name = head[CASSETTE_TAPE_DATA_NAME]
+ delay = head[CASSETTE_TAPE_DATA_DELAY]
+ opcode = head[CASSETTE_TAPE_DATA_OPCODE]
+ current_time += delay
+ switch(opcode)
+ if(/datum/cassette_opcode/next_is_audible_emote)
+ // constructed += "*[name]* --- [msg] ---"
+ constructed += "[time2text(current_time, "mm:ss")] (unrecognizable / not speech)"
+ if(/datum/cassette_opcode/next_is_direct_broadcast)
+ constructed += "[time2text(current_time, "mm:ss")]
[msg]"
+ else
+ lang = head[3]
+ if(lang.id == LANGUAGE_ID_COMMON)
+ constructed += "[time2text(current_time, "mm:ss")] [name]: [msg]"
+ else
+ constructed += "[time2text(current_time, "mm:ss")] [name]: (unknown language)"
+ P.info = constructed.Join("
")
+ P.name = "Transcript"
+ print_cooldown()
+ QDEL_NULL(tape_iterator)
+
+/obj/item/tape_recorder/proc/print_ready()
+ return !TIMER_COOLDOWN_CHECK(src, CD_INDEX_TAPE_PRINT)
+
+/obj/item/tape_recorder/proc/print_cooldown()
+ TIMER_COOLDOWN_START(src, CD_INDEX_TAPE_PRINT, 30 SECONDS)
+
+/obj/item/tape_recorder/attack_self(mob/user)
+ if(is_playing())
+ stop_playing(user)
+ else if(is_recording())
+ stop_recording(user)
+ else
+ start_recording(user)
+
+/obj/item/tape_recorder/update_icon_state()
+ . = ..()
+ if(!tape)
+ icon_state = "taperecorder_empty"
+ else if(recording)
+ icon_state = "taperecorder_recording"
+ else if(playing)
+ icon_state = "taperecorder_playing"
+ else
+ icon_state = "taperecorder_idle"
+
+/obj/item/tape_recorder/empty
+ tape = null
diff --git a/code/game/objects/items/devices/translator.dm b/code/game/objects/items/devices/translator.dm
index fa487bef4b2..3e51c3194f8 100644
--- a/code/game/objects/items/devices/translator.dm
+++ b/code/game/objects/items/devices/translator.dm
@@ -6,27 +6,107 @@
icon_state = "translator"
w_class = ITEMSIZE_NORMAL
origin_tech = list(TECH_DATA = 3, TECH_ENGINEERING = 3)
+ /// our translation context; set to path to start as path
+ var/datum/translation_context/context = /datum/translation_context/simple/universal_translator
+
var/mult_icons = 1 //Changes sprite when it translates
var/visual = 1 //If you need to see to get the message
var/audio = 0 //If you need to hear to get the message
- var/omni = 0 //cit change - if the language translated doesn't need machine_understands
var/listening = 0
var/datum/language/langset
+ /// allow us to translate tapes
+ var/cassette_translation = TRUE
+ /// allow knowledge transfering
+ var/allow_knowledge_transfer = TRUE
+
+/obj/item/universal_translator/Initialize(mapload)
+ . = ..()
+ if(ispath(context))
+ set_context(context)
+
+/obj/item/universal_translator/Destroy()
+ if(context)
+ QDEL_NULL(context)
+ return ..()
+
+/obj/item/universal_translator/proc/set_context(datum/translation_context/context_or_path)
+ if(istype(context, /datum/translation_context/variable/learning))
+ var/datum/translation_context/variable/learning/CTX = context
+ CTX.on_train = null
+ if(ispath(context_or_path))
+ context_or_path = new context_or_path
+ context = context_or_path
+ if(istype(context, /datum/translation_context/variable/learning))
+ var/datum/translation_context/variable/learning/CTX = context
+ CTX.on_train = CALLBACK(src, .proc/on_learn)
+
+/obj/item/universal_translator/proc/on_learn(datum/translation_context/context, datum/language/L, old_efficiency)
+ if(old_efficiency)
+ return
+ if(!ismob(loc))
+ return
+ to_chat(loc, SPAN_NOTICE("New language detected. Beginning translation network training."))
+
+/obj/item/universal_translator/examine(mob/user)
+ . = ..()
+ if(cassette_translation)
+ . += SPAN_NOTICE("Use a cassette tape on this to translate the tape's contents where possible.")
+
+/obj/item/universal_translator/attackby(obj/item/I, mob/user)
+ if(istype(I, /obj/item/cassette_tape))
+ if(TIMER_COOLDOWN_CHECK(src, CD_INDEX_TAPE_TRANSLATION))
+ user.action_feedback(SPAN_WARNING("[src] cannot translate tapes that fast."), src)
+ return CLICKCHAIN_DO_NOT_PROPAGATE
+ var/obj/item/cassette_tape/T = I
+ var/datum/cassette_tape_iterator/translator/iter = T.lock_for_translation()
+ if(!iter)
+ user.action_feedback(SPAN_WARNING("Could not lock [T] for translation."), src)
+ return CLICKCHAIN_DO_NOT_PROPAGATE
+ var/amt = iter.translate(context)
+ user.action_feedback(SPAN_NOTICE("Translated [amt] lines on [T]. Processor cooling down for 10 seconds."), src)
+ TIMER_COOLDOWN_START(src, CD_INDEX_TAPE_TRANSLATION, 10 SECONDS)
+ return CLICKCHAIN_DO_NOT_PROPAGATE
+ return ..()
+
+/obj/item/universal_translator/attack(mob/living/M, mob/living/user, target_zone, attack_modifier)
+ if(user.a_intent == INTENT_HARM)
+ return ..()
+ if(!isrobot(M))
+ return ..()
+ if(!istype(context, /datum/translation_context/variable))
+ user.action_feedback(SPAN_WARNING("[src] does not have a variable translation matrix."), src)
+ return
+ if(!allow_knowledge_transfer)
+ user.action_feedback(SPAN_WARNING("[src] doesn't have a data port."), src)
+ return
+ var/mob/living/silicon/robot/R = M
+ var/datum/translation_context/variable/theirs = R.translation_context
+ if(!istype(theirs))
+ user.action_feedback(SPAN_WARNING("[R] does not have a variable translation matrix."), src)
+ return
+ var/datum/translation_context/variable/ours = context
+ ours.copy_knowledge(theirs)
/obj/item/universal_translator/attack_self(mob/user)
if(!listening) //Turning ON
- langset = input(user,"Translate to which of your languages?","Language Selection") as null|anything in user.languages
- if(langset)
- if(langset && ((langset.language_flags & NONVERBAL) || (langset.language_flags & HIVEMIND) || (!langset.machine_understands)))
- //Nonverbal means no spoken words to translate, so I didn't see the need to remove it.
- to_chat(user, "\The [src] cannot output that language.")
- return
- else
- listening = 1
- listening_objects |= src
- if(mult_icons)
- icon_state = "[initial(icon_state)]1"
- to_chat(user, "You enable \the [src], translating into [langset.name].")
+ var/list/allowed = list()
+ for(var/datum/language/L in user.languages)
+ if(L.language_flags & (LANGUAGE_NONVERBAL | LANGUAGE_HIVEMIND))
+ continue
+ if(!context.can_translate(L))
+ continue
+ allowed += L
+ if(!length(allowed))
+ to_chat(user, SPAN_WARNING("There's no language you know that this can translate to."))
+ return
+ langset = input(user,"Translate to which of your languages?","Language Selection") as null|anything in allowed
+ if(!langset)
+ return
+ listening = 1
+ listening_objects |= src
+ if(mult_icons)
+ icon_state = "[initial(icon_state)]1"
+ to_chat(user, "You enable \the [src], translating into [langset.name].")
else //Turning OFF
listening = 0
listening_objects -= src
@@ -51,12 +131,9 @@
if(!language)
return //Borgs were causing runtimes when passing language=null
- if (language && (language.language_flags & NONVERBAL))
+ if (language && (language.language_flags & LANGUAGE_NONVERBAL))
return //Not gonna translate sign language
- if (!language.machine_understands && !omni) // cit change - omni check
- return //Any other languages that it can't translate.
-
if (visual && ((L.sdisabilities & SDISABILITY_NERVOUS) || L.eye_blind))
return //Can't see the screen, don't get the message
@@ -67,15 +144,10 @@
//I'll just assume they don't look at the screen in that case
//They don't understand the spoken language we're translating FROM
- if(!L.say_understands(speaker,language))
+ if(!L.say_understands(speaker, language))
//They understand the output language
if(L.say_understands(null,langset))
- to_chat(L, "[src] translates, \"[message]\"")
-
- //They don't understand the output language
- /*else
- to_chat(L, "[src] translates, \"[langset.scramble(message)]\"")
- */ //not much of a 'universal translator' with this else clause
+ to_chat(L, "[src] translates, \"[context.attempt_translation(language, speaker, message)]\"")
//Let's try an ear-worn version
/obj/item/universal_translator/ear
@@ -86,13 +158,23 @@
slot_flags = SLOT_EARS
visual = 0
audio = 1
+ context = /datum/translation_context/simple/universal_translator
-/obj/item/universal_translator/all
- name = "handheld omni-translator"
- desc = "This handy device appears to translate the languages it hears into onscreen text for a user, but upgraded with more languages. Somehow."
- omni = 1
+/obj/item/universal_translator/adaptive
+ name = "handheld adaptive translator"
+ desc = "This handheld translator has been upgraded with a positronic accelerator, allowing it to \
+ slowly learn certain languages that wasn't originally included."
+ context = /datum/translation_context/variable/learning/universal_translator
-/obj/item/universal_translator/ear/all
- name = "omni-translator earpiece"
- desc = "This handy device appears to translate the languages it hears into another language for a user, but upgraded with more languages. Somehow."
- omni = 1
+/obj/item/universal_translator/ear/adaptive
+ name = "omni-translator earpiece"
+ desc = "This earpiece translator has been upgraded with a positronic accelerator, allowing it to \
+ slowly learn certain languages that wasn't originally included."
+ context = /datum/translation_context/variable/learning/universal_translator
+
+/datum/translation_context/simple/universal_translator
+ translation_class = TRANSLATION_CLASSES_STANDARD_TRANSLATE
+
+/datum/translation_context/variable/learning/universal_translator
+ translation_class = TRANSLATION_CLASSES_STANDARD_TRANSLATE
+ translation_class_learn = TRANSLATION_CLASSES_STANDARD_ADAPTIVE
diff --git a/code/game/objects/items/robot/robot_upgrades.dm b/code/game/objects/items/robot/robot_upgrades.dm
index 50f92a77958..c9fd42993a5 100644
--- a/code/game/objects/items/robot/robot_upgrades.dm
+++ b/code/game/objects/items/robot/robot_upgrades.dm
@@ -181,7 +181,7 @@
to_chat(usr, "There's no mounting point for the module!")
return FALSE
-/obj/item/borg/upgrade/syndicate/
+/obj/item/borg/upgrade/syndicate
name = "scrambled equipment module"
desc = "Unlocks new and often deadly module specific items of a robot"
icon_state = "cyborg_upgrade3"
@@ -199,8 +199,8 @@
return TRUE
/obj/item/borg/upgrade/language
- name = "language module"
- desc = "Used to let cyborgs other than clerical or service speak a variety of languages."
+ name = "adaptive translation module"
+ desc = "Upgrades a cyborg's language processing unit with an adaptive translation module."
icon_state = "cyborg_upgrade3"
item_state = "cyborg_upgrade"
@@ -208,31 +208,14 @@
if(..())
return FALSE
- R.add_language(LANGUAGE_SOL_COMMON, 1)
- R.add_language(LANGUAGE_TRADEBAND, 1)
- R.add_language(LANGUAGE_UNATHI, 1)
- R.add_language(LANGUAGE_SIIK, 1)
- R.add_language(LANGUAGE_AKHANI, 1)
- R.add_language(LANGUAGE_SKRELLIAN, 1)
- R.add_language(LANGUAGE_SKRELLIANFAR, 0)
- R.add_language(LANGUAGE_GUTTER, 1)
- R.add_language(LANGUAGE_SCHECHI, 1)
- R.add_language(LANGUAGE_ROOTLOCAL, 1)
- R.add_language(LANGUAGE_TERMINUS, 1)
- R.add_language(LANGUAGE_ZADDAT, 1)
- R.add_language(LANGUAGE_AKULA, 1)
- R.add_language(LANGUAGE_ADHERENT, 1) //Cephalopod Activity language, Omni Translators have it so borgs should too
- R.add_language(LANGUAGE_BIRDSONG, 1) //Languages moved from robot/upgrades_vr - Papalus
- R.add_language(LANGUAGE_SAGARU, 1)
- R.add_language(LANGUAGE_CANILUNZT, 1)
- R.add_language(LANGUAGE_DAEMON, 1)
- R.add_language(LANGUAGE_ENOCHIAN, 1)
- R.add_language(LANGUAGE_SLAVIC, 1)
- R.add_language(LANGUAGE_SQUEAKISH, 1)
+ if(istype(R.translation_context, /datum/translation_context/simple/silicons))
+ qdel(R.translation_context)
+ R.translation_context = new /datum/translation_context/variable/learning/silicons
+ R.sync_translation_context()
return TRUE
-//Robot resizing module, moved from robot/upgrades_vr - Papalus
+//Robot resizing module, moved from robot/upgrades_vr - Papalus
/obj/item/borg/upgrade/sizeshift
name = "robot size alteration module"
desc = "Using technology similar to one used in sizeguns, allows cyborgs to adjust their own size as neccesary."
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index c49e94a39f8..7a9df088475 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -192,7 +192,7 @@ var/global/floorIsLava = 0
body += "
Languages:
"
var/f = 1
for(var/datum/language/L as anything in SScharacters.all_languages())
- if(!(L.language_flags & INNATE))
+ if(!(L.language_flags & LANGUAGE_INNATE))
if(!f) body += " | "
else f = 0
if(L in M.languages)
diff --git a/code/modules/admin/verbs/debug/reestablish_db_connection.dm b/code/modules/admin/verbs/debug/reestablish_db_connection.dm
index df89e7d1ceb..f40e05f1cfc 100644
--- a/code/modules/admin/verbs/debug/reestablish_db_connection.dm
+++ b/code/modules/admin/verbs/debug/reestablish_db_connection.dm
@@ -30,4 +30,4 @@
message_admins("Database connection re-established")
message_admins("Reloading client database data...")
for(var/client/C in GLOB.clients)
- C.database?.Load()
+ C.database?.load()
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 5e83b651306..83532c51954 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -293,7 +293,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
// resolve database data
// this is down here because player_lookup won't have an entry for us until log_client_to_db() runs!!
database = new(ckey)
- database.LogConnect()
+ database.log_connect()
if (byond_version >= 512)
if (!byond_build || byond_build < 1386)
diff --git a/code/modules/client/database.dm b/code/modules/client/database.dm
index 82b8297641e..f1dcd867fd1 100644
--- a/code/modules/client/database.dm
+++ b/code/modules/client/database.dm
@@ -27,28 +27,28 @@
src.ckey = ckey
if(!src.ckey)
return
- Load()
+ load()
/**
* async
*/
-/datum/client_dbdata/proc/Load()
+/datum/client_dbdata/proc/load()
if(!SSdbcore.Connect())
if(isnull(available))
available = FALSE
return FALSE
- INVOKE_ASYNC(src, .proc/Load_Async)
+ INVOKE_ASYNC(src, .proc/load_blocking)
return TRUE
-/datum/client_dbdata/proc/Load_Async()
+/datum/client_dbdata/proc/load_blocking()
// allow admin proccalls - there's no args here.
var/was_proccall = !!IsAdminAdvancedProcCall()
var/old_usr = usr
usr = null
- _Load_Lock(was_proccall)
+ _load_lock(was_proccall)
usr = old_usr
-/datum/client_dbdata/proc/_Load_Lock(was_proccall)
+/datum/client_dbdata/proc/_load_lock(was_proccall)
if(IsAdminAdvancedProcCall())
return
if(loading)
@@ -56,10 +56,10 @@
CRASH("loading is still locked; why are we loading this fast?")
return
loading = TRUE
- _Load()
+ _load()
loading = FALSE
-/datum/client_dbdata/proc/_Load()
+/datum/client_dbdata/proc/_load()
if(IsAdminAdvancedProcCall())
return
var/datum/db_query/lookup
@@ -103,9 +103,9 @@
CRASH("failed to lookup player row on id [lookup_pid] even though id was present in player_lookup.")
else
qdel(lookup)
- RegisterNewPlayer(lookup_firstseen, lookup_id)
+ register_new_player(lookup_firstseen, lookup_id)
-/datum/client_dbdata/proc/RegisterNewPlayer(migrate_firstseen, lookup_id)
+/datum/client_dbdata/proc/register_new_player(migrate_firstseen, lookup_id)
if(IsAdminAdvancedProcCall())
return
// new person!
@@ -149,7 +149,7 @@
/**
* async
*/
-/datum/client_dbdata/proc/Save()
+/datum/client_dbdata/proc/save()
set waitfor = FALSE
// why are we in here if we're write locked?
if(saving)
@@ -164,11 +164,11 @@
usr = null
// don't lock; if something is spamming our flags they probably shouldn't be
saving = TRUE
- _Save()
+ _save()
saving = FALSE
usr = old_usr
-/datum/client_dbdata/proc/_Save()
+/datum/client_dbdata/proc/_save()
qdel(SSdbcore.ExecuteQuery(
"UPDATE [format_table_name("player")] SET flags = :flags WHERE id = :id",
list(
@@ -180,11 +180,11 @@
/**
* async
*/
-/datum/client_dbdata/proc/LogConnect()
+/datum/client_dbdata/proc/log_connect()
set waitfor = FALSE
- UpdateLastSeen()
+ update_last_seen()
-/datum/client_dbdata/proc/UpdateLastSeen()
+/datum/client_dbdata/proc/update_last_seen()
// don't interrupt
if(!block_on_available())
return FALSE
diff --git a/code/modules/integrated_electronics/subtypes/input.dm b/code/modules/integrated_electronics/subtypes/input.dm
index 5c0daa7d660..b06588cdfc2 100644
--- a/code/modules/integrated_electronics/subtypes/input.dm
+++ b/code/modules/integrated_electronics/subtypes/input.dm
@@ -942,6 +942,11 @@
push_data()
activate_pin(2)
+GLOBAL_DATUM_INIT(circuit_translation_context, /datum/translation_context/simple/integrated_circuit, new)
+
+/datum/translation_context/simple/integrated_circuit
+ translation_class = TRANSLATION_CLASS_LEVEL_1
+
/obj/item/integrated_circuit/input/microphone
name = "microphone"
desc = "Useful for spying on people, or for voice-activated machines."
@@ -959,8 +964,11 @@
activators = list("on message received" = IC_PINTYPE_PULSE_OUT, "on translation" = IC_PINTYPE_PULSE_OUT)
spawn_flags = IC_SPAWN_DEFAULT|IC_SPAWN_RESEARCH
power_draw_per_use = 5
+ /// translation context
+ var/datum/translation_context/translation_context
/obj/item/integrated_circuit/input/microphone/Initialize(mapload)
+ translation_context = GLOB.circuit_translation_context
. = ..()
listening_objects |= src
@@ -972,8 +980,10 @@
var/translated = FALSE
if(M && msg)
if(speaking)
- if(!speaking.machine_understands)
+ if(!translation_context.can_translate(speaking, M))
msg = speaking.scramble(msg)
+ else
+ msg = translation_context.attempt_translation(speaking, M, msg)
if(!istype(speaking, /datum/language/common) && !istype(speaking, /datum/language/noise))
translated = TRUE
set_pin_data(IC_OUTPUT, 1, M.GetVoice())
@@ -1021,7 +1031,7 @@
var/signlang = FALSE
if(M && msg)
if(speaking)
- if(!((speaking.language_flags & NONVERBAL) || (speaking.language_flags & SIGNLANG)))
+ if(!((speaking.language_flags & LANGUAGE_NONVERBAL) || (speaking.language_flags & LANGUAGE_SIGNLANG)))
signlang = FALSE
msg = speaking.scramble(msg, my_langs)
else
diff --git a/code/modules/jobs/job.dm b/code/modules/jobs/job.dm
index 371f4b982cc..b68cfb51a7b 100644
--- a/code/modules/jobs/job.dm
+++ b/code/modules/jobs/job.dm
@@ -279,6 +279,8 @@
return (available_in_days(C) == 0) // Available in 0 days = available right now = player is old enough to play.
/datum/job/proc/available_in_days(client/C)
+ if(C.has_jexp_bypass())
+ return 0
if(C && config_legacy.use_age_restriction_for_jobs && isnum(C.player_age) && isnum(minimal_player_age))
return max(0, minimal_player_age - C.player_age)
return 0
diff --git a/code/modules/language/hivemind.dm b/code/modules/language/hivemind.dm
new file mode 100644
index 00000000000..75400816136
--- /dev/null
+++ b/code/modules/language/hivemind.dm
@@ -0,0 +1,17 @@
+/**
+ * todo: finish; these are just stubs
+ *
+ * Hivemind contexts control how languages with special sending methods process speech.
+ * They don't necessarily have to be for actual hiveminds;
+ * "broadcast to someone you're grabbing/have a brain parasite in" is just as valid.
+ *
+ * todo: probably need raw say args (??) and retval for if we should pass through to normal say
+ * todo: this probably needs a saycode rewrite to be sane
+ */
+/datum/hivemind_context
+
+/datum/hivemind_context/proc/broadcast(atom/movable/speaker, message, ...)
+
+/datum/hivemind_context/proc/get_listeners()
+
+/datum/hivemind_context/proc/send(atom/movable/listener, message, ...)
diff --git a/code/modules/language/language.dm b/code/modules/language/language.dm
index 0e345923c3f..39168abfa43 100644
--- a/code/modules/language/language.dm
+++ b/code/modules/language/language.dm
@@ -22,6 +22,8 @@
// todo: sort languages into categories
/// category in UI
var/category = "General"
+ /// translation classes
+ var/translation_class = NONE
/// 'says', 'hisses', 'farts'.
var/speech_verb = "says"
/// Used when sentence ends in a ?
@@ -30,7 +32,7 @@
var/exclaim_verb = "exclaims"
/// Optional. When not specified speech_verb + quietly/softly is used instead.
var/whisper_verb
- /// list of emotes that might be displayed if this language has NONVERBAL or SIGNLANG language_flags
+ /// list of emotes that might be displayed if this language has LANGUAGE_NONVERBAL or LANGUAGE_SIGNLANG language_flags
var/signlang_verb = list("signs", "gestures")
/// CSS style to use for strings in this language.
var/colour = "body"
@@ -44,12 +46,11 @@
var/list/syllables
/// Likelihood of getting a space in the random scramble string
var/list/space_chance = 55
- /// Whether machines can parse and understand this language
- var/machine_understands = 1
/// List of languages that can /somehwat/ understand it, format is: name = chance of understanding a word
var/list/partial_understanding
var/list/scramble_cache = list()
+ // todo: icon state support, shorthand is kind of jarring to see in chat
/// The shorthand name of this language, used for the indicators in chat. Should be only a handful of all-caps letters.
var/shorthand = "???"
@@ -115,7 +116,7 @@
return stars(input)
// If the input is cached already, move it to the end of the cache and return it
- if(input in scramble_cache)
+ if(scramble_cache[input])
var/n = scramble_cache[input]
scramble_cache -= input
scramble_cache[input] = n
@@ -197,7 +198,7 @@
if(name != "Noise") // Audible Emotes
if(ishuman(speaker))
var/mob/living/carbon/human/H = speaker
- if(H.species.has_organ[O_VOICE] && !(language_flags & SIGNLANG) && !(language_flags & NONVERBAL)) // Does the species need a voicebox? Is the language even spoken?
+ if(H.species.has_organ[O_VOICE] && !(language_flags & LANGUAGE_SIGNLANG) && !(language_flags & LANGUAGE_NONVERBAL)) // Does the species need a voicebox? Is the language even spoken?
var/obj/item/organ/internal/voicebox/vocal = H.internal_organs_by_name[O_VOICE]
if(!vocal || vocal.is_broken() || vocal.mute)
return FALSE
@@ -247,7 +248,7 @@
if(speaking.can_speak_special(src))
if(universal_speak)
return 1
- if(speaking && (speaking.language_flags & INNATE))
+ if(speaking && (speaking.language_flags & LANGUAGE_INNATE))
return 1
if(speaking in src.languages)
return 1
@@ -268,7 +269,7 @@
var/dat = "Known Languages
"
for(var/datum/language/L in languages)
- if(!(L.language_flags & NONGLOBAL))
+ if(!(L.language_flags & LANGUAGE_NONGLOBAL))
dat += "[L.name] ([get_language_prefix()][L.key])
[L.desc]
"
src << browse(dat, "window=checklanguage")
@@ -281,7 +282,7 @@
dat += "Current default language: [default_language] - reset
"
for(var/datum/language/L in languages)
- if(!(L.language_flags & NONGLOBAL))
+ if(!(L.language_flags & LANGUAGE_NONGLOBAL))
if(L == default_language)
dat += "[L.name] ([get_language_prefix()][L.key]) - default - reset
[L.desc]
"
else if (can_speak(L))
diff --git a/code/modules/language/abstract.dm b/code/modules/language/languages/abstract.dm
similarity index 97%
rename from code/modules/language/abstract.dm
rename to code/modules/language/languages/abstract.dm
index 728983dfcab..64e81672f18 100644
--- a/code/modules/language/abstract.dm
+++ b/code/modules/language/languages/abstract.dm
@@ -4,7 +4,7 @@
name = "Noise"
desc = "Noises"
key = ""
- language_flags = RESTRICTED|NONGLOBAL|INNATE|NO_TALK_MSG|NO_STUTTER
+ language_flags = LANGUAGE_RESTRICTED|LANGUAGE_NONGLOBAL|LANGUAGE_INNATE|LANGUAGE_NO_TALK_MSG|LANGUAGE_NO_STUTTER
/datum/language/noise/format_message(message, verb)
return "[message]"
@@ -33,8 +33,7 @@
exclaim_verb = "shrieks"
colour = "attack"
key = "r" // Radda radda radda
- language_flags = RESTRICTED|NONGLOBAL
- machine_understands = 0
+ language_flags = LANGUAGE_RESTRICTED|LANGUAGE_NONGLOBAL
syllables = list (
"radda", "fea","vea","vei","veh","vee","feh","fa","soa","su","sua","sou","se","seh","twa","twe","twi",
"ahm","lea","lee","nae","nah","pa","pau","fae","fai","soh","mou","ahe","ll","ea","ai","thi",
diff --git a/code/modules/language/animal.dm b/code/modules/language/languages/animal.dm
similarity index 75%
rename from code/modules/language/animal.dm
rename to code/modules/language/languages/animal.dm
index 006ee661d43..1730ff3c264 100644
--- a/code/modules/language/animal.dm
+++ b/code/modules/language/languages/animal.dm
@@ -1,13 +1,13 @@
/datum/language/corgi
id = LANGUAGE_ID_DOG
name = "Dog"
+ translation_class = TRANSLATION_CLASS_ANIMAL
desc = "Woof woof woof."
speech_verb = "barks"
ask_verb = "woofs"
exclaim_verb = "howls"
+ language_flags = LANGUAGE_RESTRICTED
key = null // demoted
- language_flags = RESTRICTED
- machine_understands = 0
space_chance = 100
syllables = list("bark", "woof", "bowwow", "yap", "arf")
shorthand = "DOG"
@@ -15,13 +15,13 @@
/datum/language/cat
id = LANGUAGE_ID_CAT
name = "Cat"
+ translation_class = TRANSLATION_CLASS_ANIMAL
desc = "Meow meow meow."
speech_verb = "meows"
ask_verb = "mrowls"
exclaim_verb = "yowls"
- key = null // demoted
- language_flags = RESTRICTED
- machine_understands = 0
+ language_flags = LANGUAGE_RESTRICTED
+ key = null // demoted
space_chance = 100
syllables = list("meow", "mrowl", "purr", "meow", "meow", "meow")
shorthand = "CAT"
@@ -29,13 +29,13 @@
/datum/language/mouse
id = LANGUAGE_ID_MOUSE
name = "Mouse"
+ translation_class = TRANSLATION_CLASS_ANIMAL
desc = "Squeak squeak. *Nibbles on cheese*"
speech_verb = "squeaks"
ask_verb = "squeaks"
exclaim_verb = "squeaks"
+ language_flags = LANGUAGE_RESTRICTED
key = null // demoted
- language_flags = RESTRICTED
- machine_understands = 0
space_chance = 100
syllables = list("squeak") // , "gripes", "oi", "meow")
shorthand = "MSE"
@@ -43,13 +43,13 @@
/datum/language/bird
id = LANGUAGE_ID_BIRD
name = "Bird"
+ translation_class = TRANSLATION_CLASS_ANIMAL
desc = "Chirp chirp, give me food"
speech_verb = "chirps"
ask_verb = "tweets"
exclaim_verb = "squawks"
+ language_flags = LANGUAGE_RESTRICTED
key = null // demoted
- language_flags = RESTRICTED
- machine_understands = 0
space_chance = 100
syllables = list("chirp", "squawk", "tweet")
shorthand = "BIRD"
diff --git a/code/modules/language/antagonist/borer.dm b/code/modules/language/languages/antagonist/borer.dm
similarity index 92%
rename from code/modules/language/antagonist/borer.dm
rename to code/modules/language/languages/antagonist/borer.dm
index 35a1377f59f..066db06e8ad 100644
--- a/code/modules/language/antagonist/borer.dm
+++ b/code/modules/language/languages/antagonist/borer.dm
@@ -7,8 +7,7 @@
exclaim_verb = "sings"
colour = "alien"
key = "" //!! THIS DOES NOT HAVE A LANGUAGE KEY IF YOU SOMEHOW MIRACULOUSLY GET ANTAGS WORKING ON RP BEFORE WE TRANSITION TO SS14 IN LIKE 2025 PUT ONE IN.
- machine_understands = 0
- language_flags = RESTRICTED | HIVEMIND
+ language_flags = LANGUAGE_RESTRICTED | LANGUAGE_HIVEMIND
/datum/language/corticalborer/broadcast(var/mob/living/speaker,var/message,var/speaker_mask)
diff --git a/code/modules/language/antagonist/changeling.dm b/code/modules/language/languages/antagonist/changeling.dm
similarity index 87%
rename from code/modules/language/antagonist/changeling.dm
rename to code/modules/language/languages/antagonist/changeling.dm
index b204eff4da2..77991471c67 100644
--- a/code/modules/language/antagonist/changeling.dm
+++ b/code/modules/language/languages/antagonist/changeling.dm
@@ -5,8 +5,7 @@
speech_verb = "says"
colour = "changeling"
key = "g"
- machine_understands = 0
- language_flags = RESTRICTED | HIVEMIND
+ language_flags = LANGUAGE_RESTRICTED | LANGUAGE_HIVEMIND
/datum/language/ling/broadcast(var/mob/living/speaker,var/message,var/speaker_mask)
diff --git a/code/modules/language/antagonist/cult.dm b/code/modules/language/languages/antagonist/cult.dm
similarity index 90%
rename from code/modules/language/antagonist/cult.dm
rename to code/modules/language/languages/antagonist/cult.dm
index 38fa1b31fd2..46a0356bbdc 100644
--- a/code/modules/language/antagonist/cult.dm
+++ b/code/modules/language/languages/antagonist/cult.dm
@@ -1,15 +1,15 @@
/datum/language/cultcommon
id = LANGUAGE_ID_CULT
name = LANGUAGE_CULT
+ translation_class = TRANSLATION_CLASS_ARCANE
desc = "The chants of the occult, the incomprehensible."
speech_verb = "intones"
ask_verb = "intones"
exclaim_verb = "chants"
colour = "cult"
key = "f"
- language_flags = RESTRICTED
+ language_flags = LANGUAGE_RESTRICTED
space_chance = 100
- machine_understands = 0
syllables = list("ire","ego","nahlizet","certum","veri","jatkaa","mgar","balaq", "karazet", "geeri", \
"orkan", "allaq", "sas'so", "c'arta", "forbici", "tarem", "n'ath", "reth", "sh'yro", "eth", "d'raggathnor", \
"mah'weyh", "pleggh", "at", "e'ntrath", "tok-lyr", "rqa'nap", "g'lt-ulotf", "ta'gh", "fara'qha", "fel", "d'amar det", \
@@ -27,6 +27,5 @@
exclaim_verb = "chants"
colour = "cult"
key = "y"
- machine_understands = 0
- language_flags = RESTRICTED | HIVEMIND
+ language_flags = LANGUAGE_RESTRICTED | LANGUAGE_HIVEMIND
diff --git a/code/modules/language/antagonist/swarmbot.dm b/code/modules/language/languages/antagonist/swarmbot.dm
similarity index 70%
rename from code/modules/language/antagonist/swarmbot.dm
rename to code/modules/language/languages/antagonist/swarmbot.dm
index 769c9b30399..170118079c9 100644
--- a/code/modules/language/antagonist/swarmbot.dm
+++ b/code/modules/language/languages/antagonist/swarmbot.dm
@@ -1,13 +1,13 @@
/datum/language/swarmbot
id = LANGUAGE_ID_SWARMBOT
name = LANGUAGE_SWARMBOT
+ translation_class = TRANSLATION_CLASS_MACHINE | TRANSLATION_CLASS_ENCRYPTED
desc = "A confusing mechanical language spoken by some form of ancient machine."
speech_verb = "clatters"
ask_verb = "chatters"
exclaim_verb = "shrieks"
colour = "changeling"
key = "_"
- machine_understands = 0
- language_flags = NO_STUTTER | RESTRICTED
+ language_flags = LANGUAGE_NO_STUTTER | LANGUAGE_RESTRICTED
syllables = list("^", "v", "-", ".", "~")
space_chance = 60
diff --git a/code/modules/language/antagonist/xenomorph.dm b/code/modules/language/languages/antagonist/xenomorph.dm
similarity index 81%
rename from code/modules/language/antagonist/xenomorph.dm
rename to code/modules/language/languages/antagonist/xenomorph.dm
index 75f34bb9b1d..8d885ea2cea 100644
--- a/code/modules/language/antagonist/xenomorph.dm
+++ b/code/modules/language/languages/antagonist/xenomorph.dm
@@ -1,14 +1,14 @@
/datum/language/xenocommon
id = LANGUAGE_ID_XENOMORPH
name = SPECIES_XENO
+ translation_class = TRANSLATION_CLASS_MONSTER | TRANSLATION_CLASS_LEVEL_3
colour = "alien"
desc = "The common tongue of the xenomorphs."
speech_verb = "hisses"
ask_verb = "hisses"
exclaim_verb = "hisses"
key = "u"
- machine_understands = 0
- language_flags = RESTRICTED
+ language_flags = LANGUAGE_RESTRICTED
syllables = list("sss","sSs","SSS")
shorthand = "XENO"
@@ -20,9 +20,8 @@
ask_verb = "hisses"
exclaim_verb = "hisses"
colour = "alien"
- machine_understands = 0
key = "x"
- language_flags = RESTRICTED | HIVEMIND
+ language_flags = LANGUAGE_RESTRICTED | LANGUAGE_HIVEMIND
/datum/language/xenos/check_special_condition(var/mob/other)
diff --git a/code/modules/language/core.dm b/code/modules/language/languages/core.dm
similarity index 79%
rename from code/modules/language/core.dm
rename to code/modules/language/languages/core.dm
index 86dff292627..202cc531b4a 100644
--- a/code/modules/language/core.dm
+++ b/code/modules/language/languages/core.dm
@@ -2,28 +2,30 @@
/datum/language/common
id = LANGUAGE_ID_COMMON
name = LANGUAGE_GALCOM
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_LANGUAGE
desc = "The common galactic tongue, engineered for cross-species communication."
speech_verb = "says"
exclaim_verb = list("exclaims","shouts","yells")
whisper_verb = "whispers"
key = "0"
- language_flags = RESTRICTED
+ language_flags = LANGUAGE_RESTRICTED
syllables = list(
"vol", "zum", "coo","zoo","bi","do","ooz","ite","og","re","si","ite","ish",
"ar","at","on","ee","east","ma","da", "rim")
partial_understanding = list(LANGUAGE_SKRELLIAN = 30, LANGUAGE_SOL_COMMON = 30)
- shorthand = "GC"
+ shorthand = null // shorthand disabled - unnecessarily spammy.
/datum/language/machine
id = LANGUAGE_ID_EAL
name = LANGUAGE_EAL
+ translation_class = (TRANSLATION_CLASS_DEFAULT_CORE_LANGUAGE | TRANSLATION_CLASS_MACHINE)
desc = "An efficient language of encoded tones developed by positronics."
speech_verb = "whistles"
ask_verb = "chirps"
exclaim_verb = "whistles loudly"
colour = "changeling"
key = "6"
- language_flags = NO_STUTTER
+ language_flags = LANGUAGE_NO_STUTTER
syllables = list("beep","beep","beep","beep","beep","boop","boop","boop","bop","bop","dee","dee","doo","doo","hiss","hss","buzz","buzz","bzz","ksssh","keey","wurr","wahh","tzzz","shh","shk")
space_chance = 10
shorthand = "EAL"
@@ -37,11 +39,12 @@
/datum/language/sign
id = LANGUAGE_ID_SIGN
name = LANGUAGE_SIGN
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_LANGUAGE
desc = "A sign language commonly used for those who are deaf or mute."
signlang_verb = list("gestures")
colour = "say_quote"
key = "s"
- language_flags = SIGNLANG|NO_STUTTER|NONVERBAL
+ language_flags = LANGUAGE_SIGNLANG|LANGUAGE_NO_STUTTER|LANGUAGE_NONVERBAL
shorthand = "SIGN"
/datum/language/sign/can_speak_special(var/mob/speaker) // TODO: If ever we make external organs assist languages, convert this over to the new format
diff --git a/code/modules/language/event.dm b/code/modules/language/languages/event.dm
similarity index 92%
rename from code/modules/language/event.dm
rename to code/modules/language/languages/event.dm
index 488790f613e..6ed218ebe2f 100644
--- a/code/modules/language/event.dm
+++ b/code/modules/language/languages/event.dm
@@ -9,7 +9,7 @@
exclaim_verb = "shouts"
colour = "warning"
key = "]"
- language_flags = RESTRICTED
+ language_flags = LANGUAGE_RESTRICTED
syllables = list("chan","ange","thi","se")
//for your antag purposes.
@@ -21,9 +21,8 @@
ask_verb = "asks"
exclaim_verb = "shouts"
colour = "deadsay"
+ language_flags = LANGUAGE_RESTRICTED
key = "\["
- machine_understands = 0
- language_flags = RESTRICTED
syllables = list("rus","zem","ave","groz","ski","ska","ven","konst","pol","lin","svy",
"danya","da","mied","zan","das","krem","myka","cyka","blyat","to","st","no","na","ni",
"ko","ne","en","po","ra","li","on","byl","cto","eni","ost","ol","ego","ver","stv","pro")
diff --git a/code/modules/language/galactic.dm b/code/modules/language/languages/galactic.dm
similarity index 97%
rename from code/modules/language/galactic.dm
rename to code/modules/language/languages/galactic.dm
index 87e3442a6bb..8db85d44d59 100644
--- a/code/modules/language/galactic.dm
+++ b/code/modules/language/languages/galactic.dm
@@ -2,6 +2,7 @@
/datum/language/trader
id = LANGUAGE_ID_TRADEBAND
name = LANGUAGE_TRADEBAND
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_LANGUAGE
desc = "Maintained by the various trading cartels in major systems, this elegant, structured language is used for bartering and bargaining."
speech_verb = "enunciates"
colour = "say_quote"
@@ -18,12 +19,12 @@
// Criminal language.
/datum/language/gutter
name = LANGUAGE_GUTTER
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_LANGUAGE // fuck you you lost your machine_understands immunity
desc = "Gutter originated as a Thieves' Cant of sorts during the early colonization era. The language eventually spread from the cartels and triads to the disenfranchised people of the Bowl."
speech_verb = "growls"
colour = "rough"
key = "3"
space_chance = 45
- machine_understands = FALSE
partial_understanding = list(LANGUAGE_GALCOM = 10, LANGUAGE_TRADEBAND = 20, LANGUAGE_SOL_COMMON = 20)
shorthand = "GUT"
syllables = list (
diff --git a/code/modules/language/monkey.dm b/code/modules/language/languages/monkey.dm
similarity index 69%
rename from code/modules/language/monkey.dm
rename to code/modules/language/languages/monkey.dm
index acbe281c350..cf0414b180a 100644
--- a/code/modules/language/monkey.dm
+++ b/code/modules/language/languages/monkey.dm
@@ -2,42 +2,42 @@
id = LANGUAGE_ID_CHIMPANZEE
key = null
name = "Chimpanzee"
+ translation_class = TRANSLATION_CLASS_LEVEL_3
desc = "Ook ook ook."
speech_verb = "chimpers"
ask_verb = "chimpers"
exclaim_verb = "screeches"
syllables = list("ook","eek")
- language_flags = RESTRICTED
- machine_understands = 0
+ language_flags = LANGUAGE_RESTRICTED
shorthand = "MONK"
/datum/language/skrell/monkey
id = LANGUAGE_ID_NEAERA
key = null
name = SPECIES_MONKEY_SKRELL
+ translation_class = TRANSLATION_CLASS_LEVEL_3
desc = "Squik squik squik."
syllables = list("hiss","gronk")
- language_flags = RESTRICTED
- machine_understands = 0
+ language_flags = LANGUAGE_RESTRICTED
shorthand = "NEA"
/datum/language/unathi/monkey
id = LANGUAGE_ID_STOK
key = null
name = SPECIES_MONKEY_UNATHI
+ translation_class = TRANSLATION_CLASS_LEVEL_3
desc = "Hiss hiss hiss."
syllables = list("squick","croak")
- language_flags = RESTRICTED
- machine_understands = 0
+ language_flags = LANGUAGE_RESTRICTED
shorthand = "STOK"
/datum/language/tajaran/monkey
id = LANGUAGE_ID_FARWA
key = null
name = SPECIES_MONKEY_TAJ
+ translation_class = TRANSLATION_CLASS_LEVEL_3
desc = "Meow meow meow."
syllables = list("meow","mew")
- language_flags = RESTRICTED
- machine_understands = 0
+ language_flags = LANGUAGE_RESTRICTED
shorthand = "FAR"
diff --git a/code/modules/language/silicons.dm b/code/modules/language/languages/silicons.dm
similarity index 95%
rename from code/modules/language/silicons.dm
rename to code/modules/language/languages/silicons.dm
index 4fec93b4049..b5d12308eea 100644
--- a/code/modules/language/silicons.dm
+++ b/code/modules/language/languages/silicons.dm
@@ -7,8 +7,7 @@
ask_verb = "queries"
exclaim_verb = "declares"
key = "b"
- machine_understands = 0
- language_flags = RESTRICTED | HIVEMIND
+ language_flags = LANGUAGE_RESTRICTED | LANGUAGE_HIVEMIND
var/drone_only
/datum/language/binary/broadcast(var/mob/living/speaker,var/message,var/speaker_mask)
@@ -67,6 +66,5 @@
exclaim_verb = "transmits"
colour = "say_quote"
key = "d"
- machine_understands = 0
- language_flags = RESTRICTED | HIVEMIND
+ language_flags = LANGUAGE_RESTRICTED | LANGUAGE_HIVEMIND
drone_only = 1
diff --git a/code/modules/language/species/_species.dm b/code/modules/language/languages/species/_species.dm
similarity index 100%
rename from code/modules/language/species/_species.dm
rename to code/modules/language/languages/species/_species.dm
diff --git a/code/modules/language/species/adherent.dm b/code/modules/language/languages/species/adherent.dm
similarity index 83%
rename from code/modules/language/species/adherent.dm
rename to code/modules/language/languages/species/adherent.dm
index 8447860f5c7..db5bb5a131e 100644
--- a/code/modules/language/species/adherent.dm
+++ b/code/modules/language/languages/species/adherent.dm
@@ -1,13 +1,14 @@
/datum/language/adherent
id = LANGUAGE_ID_ADHERENT
name = LANGUAGE_ADHERENT
+ translation_class = TRANSLATION_CLASS_MACHINE | TRANSLATION_CLASS_ENCRYPTED
desc = "The mellifluous wind chime tones of the Vigil's formal shared language."
speech_verb = "chimes"
ask_verb = "rings"
exclaim_verb = "peals"
colour = "adherent"
key = "P"
- language_flags = WHITELISTED
+ language_flags = LANGUAGE_WHITELISTED
syllables = list("\[Ab\]", "\[Bb\]", "\[Cb\]", "\[Db\]", "\[Eb\]", "\[Fb\]",
"\[Gb\]", "\[A#\]", "\[B#\]", "\[C#\]", "\[D#\]", "\[E#\]", "\[F#\]",
"\[G#\]", "\[A\]", "\[B\]", "\[C\]", "\[D\]", "\[E\]", "\[F\]", "\[G\]",
diff --git a/code/modules/language/species/akula.dm b/code/modules/language/languages/species/akula.dm
similarity index 93%
rename from code/modules/language/species/akula.dm
rename to code/modules/language/languages/species/akula.dm
index 4151d01f4bc..98b637b3c13 100644
--- a/code/modules/language/species/akula.dm
+++ b/code/modules/language/languages/species/akula.dm
@@ -1,6 +1,7 @@
/datum/language/akula
id = LANGUAGE_ID_AKULA
name = LANGUAGE_AKULA
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "The language of the Akula consists of a wildly melodic combination of hard and soft consonants working in flowing conjuncture with one another."
speech_verb = "cants"
ask_verb = "inflects"
diff --git a/code/modules/language/species/alraune.dm b/code/modules/language/languages/species/alraune.dm
similarity index 90%
rename from code/modules/language/species/alraune.dm
rename to code/modules/language/languages/species/alraune.dm
index ea7a7fb1a0a..21433202df5 100644
--- a/code/modules/language/species/alraune.dm
+++ b/code/modules/language/languages/species/alraune.dm
@@ -2,6 +2,7 @@
/datum/language/vernal
id = LANGUAGE_ID_VERNAL
name = LANGUAGE_VERNAL
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "An imperceptibly complex and nuanced language, often confused for the natural sounds of the forest."
signlang_verb = list("sways")
speech_verb = "rustles"
diff --git a/code/modules/language/species/altevian.dm b/code/modules/language/languages/species/altevian.dm
similarity index 88%
rename from code/modules/language/species/altevian.dm
rename to code/modules/language/languages/species/altevian.dm
index f5cad6600b3..81e1522d3ed 100644
--- a/code/modules/language/species/altevian.dm
+++ b/code/modules/language/languages/species/altevian.dm
@@ -1,6 +1,7 @@
/datum/language/squeakish
id = LANGUAGE_ID_ALTEVIAN
name = LANGUAGE_SQUEAKISH
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "A language native to the Altevians, it has been adopted by other rodent faring species over time."
key = "E"
colour = "squeakish"
@@ -9,5 +10,4 @@
exclaim_verb = "squeaks loudly"
syllables = list ("sque", "uik", "squeak", "squee", "eak", "eek", "uek", "squik",
"squeek", "sq", "eek", "squeee", "ee", "ek", "ak", "ueak", "squea")
- machine_understands = 1
shorthand = "SQ"
diff --git a/code/modules/language/species/birdsong.dm b/code/modules/language/languages/species/birdsong.dm
similarity index 81%
rename from code/modules/language/species/birdsong.dm
rename to code/modules/language/languages/species/birdsong.dm
index 1aff162859c..64ebd2ebdc3 100644
--- a/code/modules/language/species/birdsong.dm
+++ b/code/modules/language/languages/species/birdsong.dm
@@ -1,6 +1,7 @@
/datum/language/birdsong
id = LANGUAGE_ID_BIRDSONG
name = LANGUAGE_BIRDSONG
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "A language primarily spoken by Narvians"
speech_verb = "chirps"
colour = "birdsongc"
diff --git a/code/modules/language/species/celestial.dm b/code/modules/language/languages/species/celestial.dm
similarity index 81%
rename from code/modules/language/species/celestial.dm
rename to code/modules/language/languages/species/celestial.dm
index 7da73f10873..12ece6b9cdf 100644
--- a/code/modules/language/species/celestial.dm
+++ b/code/modules/language/languages/species/celestial.dm
@@ -1,6 +1,8 @@
/datum/language/demon
id = LANGUAGE_ID_DAEDAL_DREMACHIR
name = LANGUAGE_DAEMON
+ //! set to common due to overwhelming playerbase saturation.
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "The language spoken by the demons of Infernum, it's composed of deep chanting. It's rarely spoken off of Infernum due to the volume one has to exert."
speech_verb = "chants"
ask_verb = "croons"
@@ -9,12 +11,13 @@
key = "n"
syllables = list("viepn","e","bag","docu","kar","xlaqf","raa","qwos","nen","ty","von","kytaf","xin","ty","ka","baak","hlafaifpyk","znu","agrith","na'ar","uah","plhu","six","fhler","bjel","scee","lleri",
"dttm","aggr","uujl","hjjifr","wwuthaav",)
- machine_understands = FALSE
shorthand = "DEM"
/datum/language/angel
id = LANGUAGE_ID_DAEDAL_AURIL
name = LANGUAGE_ENOCHIAN
+ //! trialling putting it on rare; put it to common if everyone uses it
+ translation_class = TRANSLATION_CLASS_DEFAULT_RARE_RACE
desc = "The graceful language spoken by angels, composed of quiet hymns. Formally, Angels sing it."
speech_verb = "sings"
ask_verb = "hums"
@@ -23,5 +26,4 @@
key = "i"
syllables = list("salve","sum","loqui","operatur","iusta","et","permittit","facere","effercio","pluribus","enim","hoc",
"mihi","wan","six","tartu")
- machine_understands = FALSE
shorthand = "ANG"
diff --git a/code/modules/language/species/diona.dm b/code/modules/language/languages/species/diona.dm
similarity index 87%
rename from code/modules/language/species/diona.dm
rename to code/modules/language/languages/species/diona.dm
index 422e025b70b..0bc6e810003 100644
--- a/code/modules/language/species/diona.dm
+++ b/code/modules/language/languages/species/diona.dm
@@ -1,14 +1,14 @@
/datum/language/diona_local
id = LANGUAGE_ID_DIONA
name = LANGUAGE_ROOTLOCAL
+ translation_class = TRANSLATION_CLASS_DEFAULT_RARE_RACE
desc = "A complex language known instinctively by Dionaea, 'spoken' by emitting modulated radio waves. This version uses high frequency waves for quick communication at short ranges."
speech_verb = "creaks and rustles"
ask_verb = "creaks"
exclaim_verb = "rustles"
colour = "soghun"
key = "q"
- machine_understands = 0
- language_flags = RESTRICTED
+ language_flags = LANGUAGE_RESTRICTED
syllables = list("hs","zt","kr","st","sh")
shorthand = "DIO"
@@ -22,5 +22,4 @@
name = LANGUAGE_ROOTGLOBAL
desc = "A complex language known instinctively by Dionaea, 'spoken' by emitting modulated radio waves. This version uses low frequency waves for slow communication at long ranges."
key = "w"
- machine_understands = 0
- language_flags = RESTRICTED | HIVEMIND
+ language_flags = LANGUAGE_RESTRICTED | LANGUAGE_HIVEMIND
diff --git a/code/modules/language/species/human.dm b/code/modules/language/languages/species/human.dm
similarity index 98%
rename from code/modules/language/species/human.dm
rename to code/modules/language/languages/species/human.dm
index a7fe942ca41..0cfc81493ca 100644
--- a/code/modules/language/species/human.dm
+++ b/code/modules/language/languages/species/human.dm
@@ -1,6 +1,7 @@
/datum/language/human
id = LANGUAGE_ID_HUMAN
name = LANGUAGE_SOL_COMMON
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_RACE
desc = "A bastardized hybrid of many languages, including Chinese, English, French, and more; it is the common language of the Sol system."
speech_verb = "says"
whisper_verb = "whispers"
@@ -22,6 +23,7 @@
/datum/language/slavic
id = LANGUAGE_ID_HUMAN_SLAVIC
name = LANGUAGE_SLAVIC
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_RACE
desc = "Was established in 2122 by the short-lived United Slavic Confederation on Earth."
colour = "solcom"
key = "G"
diff --git a/code/modules/language/species/moth.dm b/code/modules/language/languages/species/moth.dm
similarity index 95%
rename from code/modules/language/species/moth.dm
rename to code/modules/language/languages/species/moth.dm
index 14902ab0191..e1b2173aae8 100644
--- a/code/modules/language/species/moth.dm
+++ b/code/modules/language/languages/species/moth.dm
@@ -1,6 +1,7 @@
/datum/language/species/moth
id = LANGUAGE_ID_LUINIMMA
name = LANGUAGE_LUINIMMA
+ translation_class = TRANSLATION_CLASS_DEFAULT_RARE_RACE
desc = "The language of the Dnin-Nepids, Luinimma sounds like a quick, tonal series of chitters, chirps, and clicks to \
non-speakers."
speech_verb = "chitters"
@@ -35,7 +36,6 @@
"z"
)
space_chance = 35
- machine_understands = FALSE
partial_understanding = list()
shorthand = "LUI"
diff --git a/code/modules/language/species/naramadi.dm b/code/modules/language/languages/species/naramadi.dm
similarity index 87%
rename from code/modules/language/species/naramadi.dm
rename to code/modules/language/languages/species/naramadi.dm
index f299d04730a..803eb002262 100644
--- a/code/modules/language/species/naramadi.dm
+++ b/code/modules/language/languages/species/naramadi.dm
@@ -2,6 +2,7 @@
id = LANGUAGE_ID_NARAMADI
// todo: papalus go replace the sergal text
name = LANGUAGE_SAGARU
+ translation_class = TRANSLATION_CLASS_DEFAULT_RARE_RACE
desc = "The dominant language of the Sergal homeworld, Vilous. It consists of aggressive low-pitched hissing and throaty growling."
speech_verb = "snarls"
colour = "sergal"
diff --git a/code/modules/language/species/phoronoid.dm b/code/modules/language/languages/species/phoronoid.dm
similarity index 87%
rename from code/modules/language/species/phoronoid.dm
rename to code/modules/language/languages/species/phoronoid.dm
index 87db4dcfacc..e7e8bbc7810 100644
--- a/code/modules/language/species/phoronoid.dm
+++ b/code/modules/language/languages/species/phoronoid.dm
@@ -1,6 +1,7 @@
/datum/language/bones
id = LANGUAGE_ID_PHORONOID
name = LANGUAGE_BONES
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "The language of skeletons, characterised by clunks and clatters. Native language of Phoronoids."
speech_verb = "rattles"
ask_verb = "clinks"
diff --git a/code/modules/language/species/shadekin.dm b/code/modules/language/languages/species/shadekin.dm
similarity index 79%
rename from code/modules/language/species/shadekin.dm
rename to code/modules/language/languages/species/shadekin.dm
index 869f91924ee..a19d49c909e 100644
--- a/code/modules/language/species/shadekin.dm
+++ b/code/modules/language/languages/species/shadekin.dm
@@ -7,5 +7,4 @@
exclaim_verb = "mars"
colour = "changeling"
key = "m"
- machine_understands = FALSE
- language_flags = WHITELISTED | HIVEMIND
+ language_flags = LANGUAGE_WHITELISTED | LANGUAGE_HIVEMIND
diff --git a/code/modules/language/species/skrell.dm b/code/modules/language/languages/species/skrell.dm
similarity index 89%
rename from code/modules/language/species/skrell.dm
rename to code/modules/language/languages/species/skrell.dm
index ef0d5e756fa..5c8b65ae122 100644
--- a/code/modules/language/species/skrell.dm
+++ b/code/modules/language/languages/species/skrell.dm
@@ -1,6 +1,7 @@
/datum/language/skrell
id = LANGUAGE_ID_SKRELL
name = LANGUAGE_SKRELLIAN
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_RACE
desc = "A set of warbles and hums, the language itself a complex mesh of both melodic and rhythmic components, exceptionally capable of conveying intent and emotion of the speaker."
speech_verb = "warbles"
ask_verb = "warbles"
@@ -16,6 +17,7 @@
/datum/language/skrellfar
id = LANGUAGE_ID_SKRELL_ALT
name = LANGUAGE_SKRELLIANFAR
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_RACE
desc = "The most common language among the Skrellian Far Kingdoms. Has an even higher than usual concentration of inaudible phonemes."
speech_verb = "warbles"
ask_verb = "warbles"
@@ -24,7 +26,7 @@
colour = "skrellfar"
key = "p"
space_chance = 30
- language_flags = WHITELISTED
+ language_flags = LANGUAGE_WHITELISTED
syllables = list("qr","qrr","xuq","qil","quum","xuqm","vol","xrim","zaoo","qu-uu","qix","qoo","zix", "...", "oo", "q", "nq", "x", "xq", "ll", "...", "...", "...") //should sound like there's holes in it
shorthand = "SKRLFR"
diff --git a/code/modules/language/species/tajaran.dm b/code/modules/language/languages/species/tajaran.dm
similarity index 90%
rename from code/modules/language/species/tajaran.dm
rename to code/modules/language/languages/species/tajaran.dm
index a5487245548..64b70f7496d 100644
--- a/code/modules/language/species/tajaran.dm
+++ b/code/modules/language/languages/species/tajaran.dm
@@ -1,6 +1,7 @@
/datum/language/tajaran
id = LANGUAGE_ID_TAJARAN
name = LANGUAGE_SIIK
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_RACE
desc = "The most prevalant language of Meralar, composed of expressive yowls and chirps. Native to the Tajaran."
speech_verb = "mrowls"
ask_verb = "mrowls"
@@ -24,13 +25,14 @@
/datum/language/tajaranakhani
id = LANGUAGE_ID_TAJARAN_ALT
name = LANGUAGE_AKHANI
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_RACE
desc = "The language of the sea-faring Njarir'Akhan Tajaran. Borrowing some elements from Siik, the language is distinctly more structured."
speech_verb = "chatters"
ask_verb = "mrowls"
exclaim_verb = "wails"
colour = "akhani"
key = "h"
- language_flags = WHITELISTED
+ language_flags = LANGUAGE_WHITELISTED
syllables = list("mrr","rr","marr","tar","ahk","ket","hal","kah","dra","nal","kra","vah","dar","hrar", "eh",
"ara","ka","zar","mah","ner","zir","mur","hai","raz","ni","ri","nar","njar","jir","ri","ahn","kha","sir",
"kar","yar","kzar","rha","hrar","err","fer","rir","rar","yarr","arr","ii'r","jar","kur","ran","rii","ii",
@@ -40,12 +42,13 @@
/datum/language/tajsign
id = LANGUAGE_ID_TAJARAN_SIGN
name = LANGUAGE_ALAI
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_RACE
desc = "A standardized Tajaran sign language that was developed in Zarraya and gradually adopted by other nations, incorporating \
hand gestures and movements of the ears and tail."
signlang_verb = list("gestures with their hands", "gestures with their ears and tail", "gestures with their ears, tail and hands")
colour = "tajaran"
key = "l"
- language_flags = WHITELISTED | SIGNLANG | NO_STUTTER | NONVERBAL
+ language_flags = LANGUAGE_WHITELISTED | LANGUAGE_SIGNLANG | LANGUAGE_NO_STUTTER | LANGUAGE_NONVERBAL
shorthand = "TAJSIGN"
/datum/language/tajsign/can_speak_special(var/mob/speaker) // TODO: If ever we make external organs assist languages, convert this over to the new format
diff --git a/code/modules/language/species/teshari.dm b/code/modules/language/languages/species/teshari.dm
similarity index 90%
rename from code/modules/language/species/teshari.dm
rename to code/modules/language/languages/species/teshari.dm
index e0895bd3f74..ca7e4d81bcc 100644
--- a/code/modules/language/species/teshari.dm
+++ b/code/modules/language/languages/species/teshari.dm
@@ -1,6 +1,7 @@
/datum/language/teshari
id = LANGUAGE_ID_TESHARI
name = LANGUAGE_SCHECHI
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "A trilling language spoken by the diminutive Teshari."
speech_verb = "chirps"
ask_verb = "chirrups"
diff --git a/code/modules/language/species/unathi.dm b/code/modules/language/languages/species/unathi.dm
similarity index 94%
rename from code/modules/language/species/unathi.dm
rename to code/modules/language/languages/species/unathi.dm
index 43dee7bdd8d..b40bc5d6ed7 100644
--- a/code/modules/language/species/unathi.dm
+++ b/code/modules/language/languages/species/unathi.dm
@@ -1,6 +1,7 @@
/datum/language/unathi
id = LANGUAGE_ID_UNATHI
name = LANGUAGE_UNATHI
+ translation_class = TRANSLATION_CLASS_DEFAULT_CORE_RACE
desc = "The common language of the Moghes Hegemony, composed of sibilant hisses and rattles. Spoken natively by Unathi."
speech_verb = "hisses"
ask_verb = "hisses"
diff --git a/code/modules/language/species/vasilissan.dm b/code/modules/language/languages/species/vasilissan.dm
similarity index 93%
rename from code/modules/language/species/vasilissan.dm
rename to code/modules/language/languages/species/vasilissan.dm
index 58049ab06d1..750d58d0fbd 100644
--- a/code/modules/language/species/vasilissan.dm
+++ b/code/modules/language/languages/species/vasilissan.dm
@@ -1,6 +1,7 @@
/datum/language/bug
id = LANGUAGE_ID_VASILISSAN
name = LANGUAGE_VESPINAE
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "A jarring and clicky language developed and used by Vasilissans, it is designed for use with mouthparts and as a result has become a common language for various arthropod species."
speech_verb = "clicks"
ask_verb = "chitters"
diff --git a/code/modules/language/species/vox.dm b/code/modules/language/languages/species/vox.dm
similarity index 89%
rename from code/modules/language/species/vox.dm
rename to code/modules/language/languages/species/vox.dm
index bb65d4503a6..38f59714ef9 100644
--- a/code/modules/language/species/vox.dm
+++ b/code/modules/language/languages/species/vox.dm
@@ -1,16 +1,16 @@
/datum/language/vox
id = LANGUAGE_ID_VOX
name = LANGUAGE_VOX
+ translation_class = NONE
desc = "The common tongue of the various Vox ships making up the Shoal. It sounds like chaotic shrieking to everyone else."
speech_verb = "shrieks"
ask_verb = "creels"
exclaim_verb = "SHRIEKS"
colour = "vox"
key = "5"
- language_flags = WHITELISTED
+ language_flags = LANGUAGE_WHITELISTED
syllables = list("ti","ti","ti","hi","hi","ki","ki","ki","ki","ya","ta","ha","ka","ya","chi","cha","kah", \
"SKRE","AHK","EHK","RAWK","KRA","AAA","EEE","KI","II","KRI","KA")
- machine_understands = 0
shorthand = "VOX"
/datum/language/vox/get_random_name()
diff --git a/code/modules/language/species/vulpkanin.dm b/code/modules/language/languages/species/vulpkanin.dm
similarity index 93%
rename from code/modules/language/species/vulpkanin.dm
rename to code/modules/language/languages/species/vulpkanin.dm
index c6afdde3308..63f7880900e 100644
--- a/code/modules/language/species/vulpkanin.dm
+++ b/code/modules/language/languages/species/vulpkanin.dm
@@ -1,6 +1,7 @@
/datum/language/vulpkanin
id = LANGUAGE_ID_VULPKANIN
name = LANGUAGE_CANILUNZT
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "The guttural language spoken and utilized by the inhabitants of Vazzend system, composed of growls, barks, yaps, and heavy utilization of ears and tail movements. Vulpkanin speak this language with ease."
speech_verb = "rrrfts"
ask_verb = "rurs"
diff --git a/code/modules/language/species/zaddat.dm b/code/modules/language/languages/species/zaddat.dm
similarity index 87%
rename from code/modules/language/species/zaddat.dm
rename to code/modules/language/languages/species/zaddat.dm
index c23ec6d44a2..162c346e74c 100644
--- a/code/modules/language/species/zaddat.dm
+++ b/code/modules/language/languages/species/zaddat.dm
@@ -1,6 +1,7 @@
/datum/language/zaddat
id = LANGUAGE_ID_ZADDAT
name = LANGUAGE_ZADDAT
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "A harsh buzzing language created by the Zaddat following their exodus from their homeworld."
speech_verb = "buzzes"
ask_verb = "buzzes"
diff --git a/code/modules/language/species/zorren.dm b/code/modules/language/languages/species/zorren.dm
similarity index 92%
rename from code/modules/language/species/zorren.dm
rename to code/modules/language/languages/species/zorren.dm
index e70a17379c9..6115b31d279 100644
--- a/code/modules/language/species/zorren.dm
+++ b/code/modules/language/languages/species/zorren.dm
@@ -1,6 +1,7 @@
/datum/language/terminus
id = LANGUAGE_ID_TERMINUS
name = LANGUAGE_TERMINUS
+ translation_class = TRANSLATION_CLASS_DEFAULT_STANDARD_RACE
desc = "A group of languages spoken by the Zorren with a certain degree of mutual intelligibility under each other. Evidence shows it was a single lingua franca which has diverged into many sub languages due to isolation."
speech_verb = "mentions"
exclaim_verb = "insinuates"
diff --git a/code/modules/language/translation.dm b/code/modules/language/translation.dm
new file mode 100644
index 00000000000..7ab756e454d
--- /dev/null
+++ b/code/modules/language/translation.dm
@@ -0,0 +1,287 @@
+/**
+ * handles language translation
+ *
+ * individual context defines are scattered throughout the code
+ * use find all implementations to find them!
+ */
+/datum/translation_context
+
+/**
+ * returns if we can translate something
+ * partial/wrong translations are still translations
+ *
+ * @params
+ * - L - the language
+ * - speaker - guy talking, optional
+ * - require_perfect - must be perfect translation; used by tape recorders
+ */
+/datum/translation_context/proc/can_translate(datum/language/L, atom/movable/speaker, require_perfect)
+ CRASH("not implemented")
+
+/**
+ * translates a message
+ * **warning**: translation doesn't always handle mid-message spans/formatting
+ * we split words using the " " space character.
+ *
+ * should have **zero** side effects
+ *
+ * @return the translated message
+ */
+/datum/translation_context/proc/translate(datum/language/L, atom/movable/speaker, msg)
+ PROTECTED_PROC(TRUE) // please use attempt_translation.
+ CRASH("not implemented")
+
+/**
+ * attempts to translate a message
+ * has possible side effects like learning the language gradually
+ *
+ * @params
+ * - L - the language
+ * - speaker - guy talking, optional
+ * - msg - the message to translate
+ * - require_perfect - must be perfect translate; used by tape recorders
+ */
+/datum/translation_context/proc/attempt_translation(datum/language/L, atom/movable/speaker, msg, require_perfect)
+ CRASH("not implemented")
+
+/**
+ * todo: for future implementation
+ * directly modifies
+ */
+
+/**
+ * directly translates languages; it either succeeds or fails
+ */
+/datum/translation_context/simple
+ /// classes we can translate directly
+ var/translation_class = NONE
+ /// classes we cannot translate; overrides translation_class
+ var/translation_class_forbid = NONE
+ /// lazy assoclist of ids we can translate (secondary lookup); overrides all
+ var/list/translated_ids
+
+/datum/translation_context/simple/New()
+ ..()
+ if(has_typelist(NAMEOF(src, translated_ids)))
+ translated_ids = get_typelist(NAMEOF(src, translated_ids))
+ else
+ translated_ids = typelist(src, make_associative_inplace_keep_values(translated_ids))
+
+/datum/translation_context/simple/can_translate(datum/language/L, atom/movable/speaker, require_perfect)
+ if(translated_ids?[L.id])
+ return TRUE
+ return (translation_class & L.translation_class) && !(translation_class_forbid & L.translation_class)
+
+/datum/translation_context/simple/attempt_translation(datum/language/L, atom/movable/speaker, msg, require_perfect)
+ if(!can_translate(L, speaker))
+ return null
+ return translate(L, speaker, msg)
+
+/datum/translation_context/simple/translate(datum/language/L, atom/movable/speaker, msg)
+ return msg
+
+/**
+ * variable language translator
+ *
+ * todo: efficiency "sharing" for similar languages
+ *
+ * it can either fully translate,
+ * fail to translate,
+ * or partially translate
+ */
+/datum/translation_context/variable
+ /// classes we can translate directly
+ var/translation_class = NONE
+ /// classes we cannot translate; overrides translation_class
+ var/translation_class_forbid = NONE
+ /// lazy assoclist of ids we can translate (secondary lookup); overrides all; also serves as efficiency;
+ /// set to 1 or don't set anything for normal perfect behaviour
+ var/list/translated_ids
+ /// default efficiency
+ var/translate_factor = 0
+ /// are translated ids cloned? if not, we shouldn't modify directly due to typelists
+ var/translated_list_detached = FALSE
+
+/datum/translation_context/variable/New()
+ ..()
+ if(has_typelist(NAMEOF(src, translated_ids)))
+ translated_ids = get_typelist(NAMEOF(src, translated_ids))
+ else
+ for(var/id in translated_ids)
+ if(!isnull(translated_ids[id]))
+ continue
+ translated_ids[id] = translate_factor
+ translated_ids = typelist(src, make_associative_inplace_keep_values(translated_ids))
+
+/datum/translation_context/variable/Destroy()
+ translated_ids = null
+ return ..()
+
+/datum/translation_context/variable/proc/detach_translated_ids()
+ if(translated_list_detached)
+ return
+ translated_ids = translated_ids.Copy()
+ translated_list_detached = TRUE
+
+/datum/translation_context/variable/attempt_translation(datum/language/L, atom/movable/speaker, msg, require_perfect)
+ if((translation_class & L.translation_class) && !(translation_class_forbid & L.translation_class))
+ return translate(L, speaker, msg)
+ var/effective = translated_ids?[L.id]
+ if(!effective)
+ return L.scramble(msg)
+ if(effective >= TRANSLATION_CONTEXT_PERFECT_THRESHOLD)
+ return translate(L, speaker, msg)
+ if(require_perfect)
+ return null
+ return imperfect_translation(L, speaker, msg, effective)
+
+/datum/translation_context/variable/translate(datum/language/L, atom/movable/speaker, msg)
+ return msg // perfect translations
+
+/datum/translation_context/variable/can_translate(datum/language/L, atom/movable/speaker, require_perfect)
+ if((translation_class & L.translation_class) && !(translation_class_forbid & L.translation_class))
+ return TRUE
+ return require_perfect? (translated_ids?[L.id] >= TRANSLATION_CONTEXT_PERFECT_THRESHOLD) : (!!translated_ids?[L.id])
+
+/datum/translation_context/variable/proc/imperfect_translation(datum/language/L, atom/movable/speaker, msg, efficiency)
+ //! all aboard the marakov train
+ var/effective_efficiency = clamp(efficiency, 0, 1)
+ var/current_efficiency = effective_efficiency * 100
+ var/translating = prob(current_efficiency)
+ var/list/words = splittext(msg, " ")
+ for(var/i in 1 to length(words))
+ var/str = words[i]
+ if(!str)
+ continue // genuinely don't care
+ if(translating)
+ // yay!
+ // don't touch the string
+ // see if we should stop translating
+ if(prob(current_efficiency))
+ // success; drop efficiency
+ // current_efficiency *= effective_efficiency
+ //! or don't because this is brutal enough just see how rimworld works lmao
+ else
+ // oops! failed to keep going; penalize the next one too
+ current_efficiency = (effective_efficiency ** 2) * 100
+ translating = FALSE
+ else
+ // nay!
+ // scrmable string
+ words[i] = L.scramble_word(str)
+ // see if we should translate again
+ if(prob(current_efficiency))
+ // yes!
+ current_efficiency = effective_efficiency * 100
+ translating = TRUE
+ return jointext(words, " ")
+
+/**
+ * copy all our knowledge to another if it can receive it
+ */
+/datum/translation_context/variable/proc/copy_knowledge(datum/translation_context/variable/other)
+ other.receive_knowledge(src)
+
+/**
+ * copy all our knowledge to another if it can receive it
+ */
+/datum/translation_context/variable/proc/receive_knowledge(datum/translation_context/variable/giver)
+ // typelists: if the lists are the same, don't bother
+ if(giver.translated_ids == translated_ids)
+ return
+ // split list
+ detach_translated_ids()
+ // copy
+ for(var/id in giver.translated_ids)
+ var/efficiency = giver.translated_ids[id]
+ if(efficiency >= translated_ids[id])
+ continue
+ translated_ids[id] = efficiency
+
+/**
+ * variable language translator
+ *
+ * it can either fully translate,
+ * fail to translate,
+ * or partially translate.
+ *
+ * it will learn over time supported languages.
+ */
+/datum/translation_context/variable/learning
+ /// classes we automatically try to learn
+ var/translation_class_learn = NONE
+ /// classes we cannot automatically try to learn
+ var/translation_class_learn_forbid = NONE
+ /// lazy assoclist of ids we can learn (secondary lookup); also serves as efficiency override
+ var/list/learnable_ids
+ /// learning factor; 1, faster = up, slower = down; 0 to inf.
+ var/learn_factor = 1
+ /// callback to call with (src, datum/language/L, old_efficiency) on successful training
+ var/datum/callback/on_train
+
+/datum/translation_context/variable/learning/New()
+ ..()
+ if(has_typelist(NAMEOF(src, learnable_ids)))
+ learnable_ids = get_typelist(NAMEOF(src, learnable_ids))
+ else
+ for(var/id in learnable_ids)
+ if(!isnull(learnable_ids[id]))
+ continue
+ learnable_ids[id] = learn_factor
+ learnable_ids = typelist(src, make_associative_inplace_keep_values(learnable_ids))
+
+/datum/translation_context/variable/learning/Destroy()
+ learnable_ids = null
+ on_train = null
+ return ..()
+
+/datum/translation_context/variable/learning/can_translate(datum/language/L, atom/movable/speaker, require_perfect)
+ if(require_perfect)
+ return ..()
+ if((translation_class_learn & L.translation_class) && !(translation_class_learn_forbid & L.translation_class))
+ return TRUE
+ if(learnable_ids?[L.id])
+ return TRUE
+ return ..()
+
+/datum/translation_context/variable/learning/attempt_translation(datum/language/L, atom/movable/speaker, msg)
+ . = ..()
+ var/efficiency = learnable_ids?[L.id]
+ if(efficiency)
+ train(L, speaker, msg, efficiency)
+ else if((translation_class_learn & L.translation_class) && !(translation_class_learn_forbid & L.translation_class))
+ train(L, speaker, msg, learn_factor)
+
+/datum/translation_context/variable/learning/proc/train(datum/language/L, atom/movable/speaker, msg)
+ // split list
+ detach_translated_ids()
+ var/old_efficiency = ((L.translation_class & translation_class) && !(L.translation_class & translation_class_forbid) && 1) || translated_ids[L.id] || 0
+ if(old_efficiency >= TRANSLATION_CONTEXT_PERFECT_THRESHOLD)
+ // good enough don't bother
+ return
+ var/list/words = splittext(msg, GLOB.multi_space_splitter)
+ // short sentences suck, but long sentences only get you so far
+ // if anyone codedives and abuses this, well, frankly:
+ // don't make me bring out the actual math and lag the server to make
+ // literally everyone miserable. just don't. roleplay it out.
+ var/amt = length(words)
+ var/gain = log((amt * 0.1) + 1) * 0.01
+ var/new_efficiency = clamp(old_efficiency + gain, 0, 1)
+ if(old_efficiency >= new_efficiency)
+ return // how
+ translated_ids[L.id] = new_efficiency
+ on_train?.Invoke(src, L, old_efficiency)
+
+/**
+ * translates everything
+ */
+/datum/translation_context/omni
+
+/datum/translation_context/omni/can_translate(datum/language/L, atom/movable/speaker, require_perfect)
+ return TRUE
+
+/datum/translation_context/omni/translate(datum/language/L, atom/movable/speaker, msg)
+ return msg
+
+/datum/translation_context/omni/attempt_translation(datum/language/L, atom/movable/speaker, msg)
+ return translate(L, speaker, msg)
diff --git a/code/modules/mob/action_feedback.dm b/code/modules/mob/action_feedback.dm
index 180f232f69b..e96bcb76462 100644
--- a/code/modules/mob/action_feedback.dm
+++ b/code/modules/mob/action_feedback.dm
@@ -21,7 +21,7 @@
*/
/mob/proc/bubble_action_feedback(msg, atom/target)
// for now, just wrapper for chat action feedback
- action_feedback(SPAN_NOTICE(msg))
+ action_feedback("[target]: [SPAN_NOTICE(msg)]")
/**
* gives feedback to us and someone else
diff --git a/code/modules/mob/animations.dm b/code/modules/mob/animations.dm
index be847de994c..f6615ae7c27 100644
--- a/code/modules/mob/animations.dm
+++ b/code/modules/mob/animations.dm
@@ -272,7 +272,7 @@ note dizziness decrements automatically in the mob's Life() proc.
I.dir = dir
// Show the overlay to the clients
- flick_overlay(I, viewing, 5, TRUE) // 5 ticks/half a second
+ flick_overlay(I, viewing, 5) // 5 ticks/half a second
// Set the direction of the icon animation.
var/direction = get_dir(src, A)
diff --git a/code/modules/mob/hear_say.dm b/code/modules/mob/hear_say.dm
index fcfcff6c2ad..335e87d2150 100644
--- a/code/modules/mob/hear_say.dm
+++ b/code/modules/mob/hear_say.dm
@@ -25,14 +25,15 @@
return
//non-verbal languages are garbled if you can't see the speaker. Yes, this includes if they are inside a closet.
- if (language && (language.language_flags & NONVERBAL))
+ if (language && (language.language_flags & LANGUAGE_NONVERBAL))
if (!speaker || (src.sdisabilities & SDISABILITY_NERVOUS || src.blinded) || !(speaker in view(src)))
message = stars(message)
- if(!(language && (language.language_flags & INNATE))) // skip understanding checks for INNATE languages
+
+ if(!(language && (language.language_flags & LANGUAGE_INNATE))) // skip understanding checks for LANGUAGE_INNATE languages
if(!say_understands(speaker,language))
if(language)
- message = language.scramble(message, languages)
+ message = language_scramble(language, message)
else
message = stars(message)
@@ -57,7 +58,7 @@
message = "[message]"
if(is_deaf())
- if(!language || !(language.language_flags & INNATE)) // INNATE is the flag for audible-emote-language, so we don't want to show an "x talks but you cannot hear them" message if it's set
+ if(!language || !(language.language_flags & LANGUAGE_INNATE)) // LANGUAGE_INNATE is the flag for audible-emote-language, so we don't want to show an "x talks but you cannot hear them" message if it's set
if(speaker == src)
to_chat(src, "You cannot hear yourself speak!")
else
@@ -66,7 +67,7 @@
var/message_to_send = null
if(language)
//Hivemind languages already say their names. Also, no indicator if you don't know the language.
- if(client && !(language.language_flags & HIVEMIND) && say_understands(speaker, language) && client.is_preference_enabled(/datum/client_preference/language_indicator))
+ if(client && !(language.language_flags & LANGUAGE_HIVEMIND) && say_understands(speaker, language) && language.shorthand && client.is_preference_enabled(/datum/client_preference/language_indicator))
verb += " ([language.shorthand])"
message_to_send = "[speaker_name][alt_name] [track][language.format_message(message, verb)]"
else
@@ -87,6 +88,17 @@
if(has_AI()) // Won't happen if no ai_holder exists or there's a player inside w/o autopilot active.
ai_holder.on_hear_say(speaker, message)
+/mob/proc/language_scramble(datum/language/L, str)
+ return L.scramble(str, languages)
+
+//! "silicons, why are you putting this here?"
+//? Because saycode needs a full rewrite so I'm shoving stuff in here until we do
+//? as if I don't, it's just harder to find later.
+/mob/living/silicon/language_scramble(datum/language/L, str)
+ // todo: this still does the "unknown language" highlighting potentially, oops
+ var/translated = translation_context.attempt_translation(L, msg = str)
+ return isnull(translated)? L.scramble(str, languages) : translated
+
/mob/proc/on_hear_say(var/message)
to_chat(src, message)
if(teleop)
@@ -156,14 +168,14 @@
var/track = null
//non-verbal languages are garbled if you can't see the speaker. Yes, this includes if they are inside a closet.
- if (language && (language.language_flags & NONVERBAL))
+ if (language && (language.language_flags & LANGUAGE_NONVERBAL))
if (!speaker || (src.sdisabilities & SDISABILITY_NERVOUS || src.blinded) || !(speaker in view(src)))
message = stars(message)
- if(!(language && (language.language_flags & INNATE))) // skip understanding checks for INNATE languages
+ if(!(language && (language.language_flags & LANGUAGE_INNATE))) // skip understanding checks for LANGUAGE_INNATE languages
if(!say_understands(speaker,language))
if(language)
- message = language.scramble(message, languages)
+ message = language_scramble(language, message)
else
message = stars(message)
@@ -242,7 +254,7 @@
var/formatted
if(language)
- if(client && !(language.language_flags & HIVEMIND) && say_understands(speaker, language) && client.is_preference_enabled(/datum/client_preference/language_indicator))
+ if(client && !(language.language_flags & LANGUAGE_HIVEMIND) && say_understands(speaker, language) && language.shorthand && client.is_preference_enabled(/datum/client_preference/language_indicator))
verb += " ([language.shorthand])"
formatted = "[language.format_message_radio(message, verb)][part_c]"
else
diff --git a/code/modules/mob/living/autohiss.dm b/code/modules/mob/living/autohiss.dm
index 2bbfdd0ad0a..c018ebd7715 100644
--- a/code/modules/mob/living/autohiss.dm
+++ b/code/modules/mob/living/autohiss.dm
@@ -9,7 +9,7 @@
if(!maps.basic)
return message
- if(L.language_flags & NO_STUTTER) // Currently prevents EAL, Sign language, and emotes from autohissing
+ if(L.language_flags & LANGUAGE_NO_STUTTER) // Currently prevents EAL, Sign language, and emotes from autohissing
return message
if(maps.exempt && (L.name in maps.exempt))
return message
diff --git a/code/modules/mob/living/carbon/brain/say.dm b/code/modules/mob/living/carbon/brain/say.dm
index b6ad7e1dd29..95185c01bb8 100644
--- a/code/modules/mob/living/carbon/brain/say.dm
+++ b/code/modules/mob/living/carbon/brain/say.dm
@@ -29,7 +29,7 @@
else
message = Gibberish(message, (emp_damage*6))//scrambles the message, gets worse when emp_damage is higher
- if(speaking && speaking.language_flags & HIVEMIND)
+ if(speaking && speaking.language_flags & LANGUAGE_HIVEMIND)
speaking.broadcast(src,trim(message))
return
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 6691432c035..4984e72bdc4 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -143,8 +143,9 @@
var/show_ssd
var/mob/living/carbon/human/H = src
var/datum/gender/T = GLOB.gender_datums[H.get_visible_gender()] // make sure to cast to human before using get_gender() or get_visible_gender()!
- if(istype(H)) show_ssd = H.species.show_ssd
- if(show_ssd && !client && !teleop)
+ if(istype(H))
+ show_ssd = H.species.show_ssd
+ if(show_ssd && !client && !teleop && (!istype(H) || !H.override_ssd))
M.visible_message(SPAN_NOTICE("[M] shakes [src] trying to wake [T.him] up!"),
SPAN_NOTICE("You shake [src], but [T.he] [T.does] not respond... Maybe [T.he] [T.has] S.S.D?"))
else if(lying || src.sleeping)
diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm
index 8eeae66c016..9dd65ca1e0c 100644
--- a/code/modules/mob/living/carbon/human/human_defines.dm
+++ b/code/modules/mob/living/carbon/human/human_defines.dm
@@ -259,3 +259,6 @@
* Only used for camera monitors currently.
*/
var/obj/machinery/machine_visual
+
+ // ignore ssd status
+ var/override_ssd = FALSE
diff --git a/code/modules/mob/living/carbon/human/human_powers.dm b/code/modules/mob/living/carbon/human/human_powers.dm
index 9637379e974..22071eeb26e 100644
--- a/code/modules/mob/living/carbon/human/human_powers.dm
+++ b/code/modules/mob/living/carbon/human/human_powers.dm
@@ -189,13 +189,13 @@
for(var/mob/living/carbon/alien/diona/D in src)
nymphs++
D.forceMove(T)
- transfer_languages(src, D, WHITELISTED|RESTRICTED)
+ transfer_languages(src, D, LANGUAGE_WHITELISTED|LANGUAGE_RESTRICTED)
D.setDir(pick(NORTH, SOUTH, EAST, WEST))
if(nymphs < number_of_resulting_nymphs)
for(var/i in nymphs to (number_of_resulting_nymphs - 1))
var/mob/M = new /mob/living/carbon/alien/diona(T)
- transfer_languages(src, M, WHITELISTED|RESTRICTED)
+ transfer_languages(src, M, LANGUAGE_WHITELISTED|LANGUAGE_RESTRICTED)
M.setDir(pick(NORTH, SOUTH, EAST, WEST))
@@ -308,10 +308,10 @@
if(!get_turf(src))
to_chat(src, SPAN_WARNING("Not from here you can't."))
return
- if(TIMER_COOLDOWN_CHECK(src, COOLDOWN_SONAR_PULSE))
+ if(TIMER_COOLDOWN_CHECK(src, CD_INDEX_SONAR_PULSE))
to_chat(src, SPAN_WARNING("You need to wait some more to do that!"))
return
- TIMER_COOLDOWN_START(src, COOLDOWN_SONAR_PULSE, 2 SECONDS)
+ TIMER_COOLDOWN_START(src, CD_INDEX_SONAR_PULSE, 2 SECONDS)
visible_message(
SPAN_WARNING("[src] emits a quiet click."),
diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm
index 33b70f6c914..85f7b486424 100644
--- a/code/modules/mob/living/carbon/human/life.dm
+++ b/code/modules/mob/living/carbon/human/life.dm
@@ -1040,7 +1040,7 @@
if(status_flags & GODMODE) return 0
//SSD check, if a logged player is awake put them back to sleep!
- if(species.get_ssd(src) && !client && !teleop && !temporary_form)
+ if(species.get_ssd(src) && !client && !teleop && !override_ssd && !temporary_form)
Sleeping(2)
if(stat == DEAD) //DEAD. BROWN BREAD. SWIMMING WITH THE SPESS CARP
blinded = 1
diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm
index ce5d7e3c199..082c3324786 100644
--- a/code/modules/mob/living/say.dm
+++ b/code/modules/mob/living/say.dm
@@ -209,8 +209,8 @@ var/list/channel_to_radio_key = new
if(speaking && speaking == SScharacters.resolve_language_name("Noise"))
message = copytext_char(message,2)
- //HIVEMIND languages always send to all people with that language
- if(speaking && (speaking.language_flags & HIVEMIND))
+ //LANGUAGE_HIVEMIND languages always send to all people with that language
+ if(speaking && (speaking.language_flags & LANGUAGE_HIVEMIND))
speaking.broadcast(src,trim(message))
return 1
@@ -219,7 +219,7 @@ var/list/channel_to_radio_key = new
return
//Self explanatory.
- if(is_muzzled() && !(speaking && (speaking.language_flags & SIGNLANG)))
+ if(is_muzzled() && !(speaking && (speaking.language_flags & LANGUAGE_SIGNLANG)))
to_chat(src, "You're muzzled and cannot speak!")
return
@@ -250,7 +250,7 @@ var/list/channel_to_radio_key = new
w_not_heard = "[speaking.speech_verb] something [w_adverb]"
//For speech disorders (hulk, slurring, stuttering)
- if(!(speaking && (speaking.language_flags & NO_STUTTER || speaking.language_flags & SIGNLANG)))
+ if(!(speaking && (speaking.language_flags & LANGUAGE_NO_STUTTER || speaking.language_flags & LANGUAGE_SIGNLANG)))
var/list/message_data = list(message, verb, whispering)
if(handle_speech_problems(message_data))
message = message_data[1]
@@ -289,7 +289,7 @@ var/list/channel_to_radio_key = new
if(speaking)
message_range = speaking.get_talkinto_msg_range(message)
var/msg
- if(!speaking || !(speaking.language_flags & NO_TALK_MSG))
+ if(!speaking || !(speaking.language_flags & LANGUAGE_NO_TALK_MSG))
msg = "\The [src] talks into \the [used_radios[1]]"
for(var/mob/living/M in hearers(7, src))
if((M != src) && msg)
@@ -318,11 +318,11 @@ var/list/channel_to_radio_key = new
//Handle nonverbal and sign languages here
if (speaking)
- if (speaking.language_flags & SIGNLANG)
+ if (speaking.language_flags & LANGUAGE_SIGNLANG)
log_say("(SIGN) [message]", src)
return say_signlang(message, pick(speaking.signlang_verb), speaking)
- if (speaking.language_flags & NONVERBAL)
+ if (speaking.language_flags & LANGUAGE_NONVERBAL)
if (prob(30))
src.custom_emote(1, "[pick(speaking.signlang_verb)].")
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index a13f2e753cf..715c6fe7e0e 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -59,6 +59,7 @@ var/list/ai_verbs_default = list(
can_be_antagged = TRUE
status_flags = CANSTUN|CANPARALYSE|CANPUSH
catalogue_data = list(/datum/category_item/catalogue/fauna/silicon/ai)
+ translation_context = /datum/translation_context/variable/learning/silicons // ai gets the gamer context by default
/// The network we have access to.
var/list/network = list(NETWORK_DEFAULT)
@@ -67,7 +68,7 @@ var/list/ai_verbs_default = list(
var/viewalerts = FALSE
/// Default is assigned when AI is created.
var/icon/holo_icon
- var/list/connected_robots = list()
+ var/list/mob/living/silicon/robot/connected_robots = list()
var/obj/item/pda/ai/aiPDA = null
var/obj/item/communicator/aiCommunicator = null
var/obj/item/multitool/aiMulti = null
@@ -180,28 +181,6 @@ var/list/ai_verbs_default = list(
//Languages
add_language("Robot Talk", 1)
- add_language(LANGUAGE_GALCOM, 1)
- add_language(LANGUAGE_SOL_COMMON, 1)
- add_language(LANGUAGE_UNATHI, 1)
- add_language(LANGUAGE_SIIK, 1)
- add_language(LANGUAGE_AKHANI, 1)
- add_language(LANGUAGE_SKRELLIAN, 1)
- add_language(LANGUAGE_SKRELLIANFAR, 0)
- add_language(LANGUAGE_TRADEBAND, 1)
- add_language(LANGUAGE_GUTTER, 1)
- add_language(LANGUAGE_EAL, 1)
- add_language(LANGUAGE_SCHECHI, 1)
- add_language(LANGUAGE_SIGN, 1)
- add_language(LANGUAGE_ROOTLOCAL, 1)
- add_language(LANGUAGE_TERMINUS, 1)
- add_language(LANGUAGE_ZADDAT, 1)
- add_language(LANGUAGE_BIRDSONG, 1)
- add_language(LANGUAGE_SAGARU, 1)
- add_language(LANGUAGE_CANILUNZT, 1)
- add_language(LANGUAGE_DAEMON, 1)
- add_language(LANGUAGE_ENOCHIAN, 1)
- add_language(LANGUAGE_SQUEAKISH, 1)
- add_language(LANGUAGE_AKULA, 1)
if(!safety)//Only used by AIize() to successfully spawn an AI.
if (!B)//If there is no player/brain inside.
diff --git a/code/modules/mob/living/silicon/ai/translation.dm b/code/modules/mob/living/silicon/ai/translation.dm
new file mode 100644
index 00000000000..7d9db17fe59
--- /dev/null
+++ b/code/modules/mob/living/silicon/ai/translation.dm
@@ -0,0 +1,25 @@
+/mob/living/silicon/ai/sync_translation_context(datum/language/L)
+ if(!length(connected_robots))
+ return
+ if(!istype(translation_context, /datum/translation_context/variable/learning/silicons))
+ return
+ var/datum/translation_context/variable/learning/silicons/ours = translation_context
+ if(!L)
+ // full pull + push
+ for(var/mob/living/silicon/robot/R as anything in connected_robots)
+ if(!istype(R.translation_context, /datum/translation_context/variable/learning/silicons))
+ return
+ var/datum/translation_context/variable/learning/silicons/theirs = R.translation_context
+ theirs.copy_knowledge(ours)
+ ours.copy_knowledge(theirs)
+ return
+ // partial sync - give, not receive
+ if(!ours.translated_list_detached)
+ return // full sync didn't happen yet
+ for(var/mob/living/silicon/robot/R as anything in connected_robots)
+ if(!istype(R.translation_context, /datum/translation_context/variable/learning/silicons))
+ return
+ var/datum/translation_context/variable/learning/silicons/theirs = R.translation_context
+ if(!theirs.translated_list_detached)
+ return // full sync didn't happen yet
+ theirs.translated_ids[L.id] = max(ours.translated_ids[L.id], theirs.translated_ids[L.id])
diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm
index b911dc9a4c2..111e8027616 100644
--- a/code/modules/mob/living/silicon/pai/pai.dm
+++ b/code/modules/mob/living/silicon/pai/pai.dm
@@ -111,14 +111,6 @@
card.radio = new /obj/item/radio(src.card)
radio = card.radio
- //Default languages without universal translator software
- add_language(LANGUAGE_SOL_COMMON, 1)
- add_language(LANGUAGE_TRADEBAND, 1)
- add_language(LANGUAGE_GUTTER, 1)
- add_language(LANGUAGE_EAL, 1)
- add_language(LANGUAGE_TERMINUS, 1)
- add_language(LANGUAGE_SIGN, 0)
-
verbs += /mob/living/silicon/pai/proc/choose_chassis
verbs += /mob/living/silicon/pai/proc/choose_verbs
diff --git a/code/modules/mob/living/silicon/pai/software_modules.dm b/code/modules/mob/living/silicon/pai/software_modules.dm
index 1ebe82d13ea..4d4eecff74e 100644
--- a/code/modules/mob/living/silicon/pai/software_modules.dm
+++ b/code/modules/mob/living/silicon/pai/software_modules.dm
@@ -471,31 +471,34 @@
/datum/pai_software/med_hud/is_active(mob/living/silicon/pai/user)
return user.medHUD
-/datum/pai_software/translator
- name = "Universal Translator"
- ram_cost = 35
- id = "translator"
+// todo: translation context
-/datum/pai_software/translator/toggle(mob/living/silicon/pai/user)
- // Sol Common, Tradeband, Terminus and Gutter are added with New() and are therefore the current default, always active languages
- user.translator_on = !user.translator_on
- if(user.translator_on)
- user.add_language(LANGUAGE_UNATHI)
- user.add_language(LANGUAGE_SIIK)
- user.add_language(LANGUAGE_AKHANI)
- user.add_language(LANGUAGE_SKRELLIAN)
- user.add_language(LANGUAGE_ZADDAT)
- user.add_language(LANGUAGE_SCHECHI)
- else
- user.remove_language(LANGUAGE_UNATHI)
- user.remove_language(LANGUAGE_SIIK)
- user.remove_language(LANGUAGE_AKHANI)
- user.remove_language(LANGUAGE_SKRELLIAN)
- user.remove_language(LANGUAGE_ZADDAT)
- user.remove_language(LANGUAGE_SCHECHI)
+// /datum/pai_software/translator
+// name = "Universal Translator"
+// ram_cost = 35
+// id = "translator"
-/datum/pai_software/translator/is_active(mob/living/silicon/pai/user)
- return user.translator_on
+// /datum/pai_software/translator/toggle(mob/living/silicon/pai/user)
+// // Sol Common, Tradeband, Terminus and Gutter are added with New() and are therefore the current default, always active languages
+// // todo: translation contexts for pais
+// user.translator_on = !user.translator_on
+// if(user.translator_on)
+// user.add_language(LANGUAGE_UNATHI)
+// user.add_language(LANGUAGE_SIIK)
+// user.add_language(LANGUAGE_AKHANI)
+// user.add_language(LANGUAGE_SKRELLIAN)
+// user.add_language(LANGUAGE_ZADDAT)
+// user.add_language(LANGUAGE_SCHECHI)
+// else
+// user.remove_language(LANGUAGE_UNATHI)
+// user.remove_language(LANGUAGE_SIIK)
+// user.remove_language(LANGUAGE_AKHANI)
+// user.remove_language(LANGUAGE_SKRELLIAN)
+// user.remove_language(LANGUAGE_ZADDAT)
+// user.remove_language(LANGUAGE_SCHECHI)
+
+// /datum/pai_software/translator/is_active(mob/living/silicon/pai/user)
+// return user.translator_on
/datum/pai_software/signaller
name = "Remote Signaller"
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index edae7160744..dd064996d6a 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -170,8 +170,12 @@
spark_system.attach(src)
add_language("Robot Talk", 1)
- add_language(LANGUAGE_GALCOM, 1)
- add_language(LANGUAGE_EAL, 1)
+ // todo: translation contexts on language holder?
+ // this is messy
+ for(var/datum/language/L as anything in SScharacters.all_languages())
+ if(!(L.translation_class & TRANSLATION_CLASSES_CYBORG_SPEAKS))
+ continue
+ add_language(L, TRUE)
wires = new(src)
@@ -246,8 +250,8 @@
if(!cell)
return 0
- if(!TIMER_COOLDOWN_CHECK(src, COOLDOWN_POWER_DRAIN_WARNING))
- TIMER_COOLDOWN_START(src, COOLDOWN_POWER_DRAIN_WARNING, 2 SECONDS)
+ if(!TIMER_COOLDOWN_CHECK(src, CD_INDEX_POWER_DRAIN_WARNING))
+ TIMER_COOLDOWN_START(src, CD_INDEX_POWER_DRAIN_WARNING, 2 SECONDS)
to_chat(src, SPAN_DANGER("Warning: Abnormal usage on power channel [rand(11, 29)] detected!"))
return cell.drain_energy(actor, amount, flags)
diff --git a/code/modules/mob/living/silicon/robot/translation.dm b/code/modules/mob/living/silicon/robot/translation.dm
new file mode 100644
index 00000000000..090ed37c12b
--- /dev/null
+++ b/code/modules/mob/living/silicon/robot/translation.dm
@@ -0,0 +1,21 @@
+/mob/living/silicon/robot/sync_translation_context(datum/language/L)
+ if(!connected_ai)
+ return
+ if(!istype(connected_ai.translation_context, /datum/translation_context/variable/learning/silicons))
+ return
+ if(!istype(translation_context, /datum/translation_context/variable/learning/silicons))
+ return
+ var/datum/translation_context/variable/learning/silicons/ours = translation_context
+ var/datum/translation_context/variable/learning/silicons/theirs = connected_ai.translation_context
+ if(!L)
+ // expensive full sync
+ theirs.copy_knowledge(ours)
+ ours.copy_knowledge(theirs)
+ to_chat(src, SPAN_NOTICE("Adaptive translator synced to master AI."))
+ return
+ // partial sync - give, not receive
+ if(!theirs.translated_list_detached || !ours.translated_list_detached)
+ return // full sync didn't happen yet
+ if(!theirs.translated_ids[L.id])
+ to_chat(connected_ai, SPAN_NOTICE("New language received from connected units: [L.name]. Adaptive translation started."))
+ theirs.translated_ids[L.id] = max(ours.translated_ids[L.id], theirs.translated_ids[L.id])
diff --git a/code/modules/mob/living/silicon/say.dm b/code/modules/mob/living/silicon/say.dm
index 74354bd16a5..62c250132df 100644
--- a/code/modules/mob/living/silicon/say.dm
+++ b/code/modules/mob/living/silicon/say.dm
@@ -56,6 +56,8 @@
return 1
if (istype(other, /mob/living/carbon/brain))
return 1
+ if(translation_context.can_translate(speaking, require_perfect = TRUE))
+ return TRUE
return ..()
//For holopads only. Usable by AI.
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index 4453fba0b14..79633f587c3 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -48,11 +48,15 @@
var/hudmode = null
+ /// our translation context
+ var/datum/translation_context/translation_context = /datum/translation_context/simple/silicons
+
/mob/living/silicon/Initialize(mapload)
silicon_mob_list |= src
. = ..()
add_language(LANGUAGE_GALCOM)
set_default_language(SScharacters.resolve_language_name(LANGUAGE_GALCOM))
+ create_translation_context()
init_id()
init_subsystems()
@@ -223,7 +227,7 @@
dat += "Current default language: [default_language] - reset
"
for(var/datum/language/L in languages)
- if(!(L.language_flags & NONGLOBAL))
+ if(!(L.language_flags & LANGUAGE_NONGLOBAL))
var/default_str
if(L == default_language)
default_str = " - default - reset"
@@ -399,3 +403,12 @@
/mob/living/silicon/get_bullet_impact_effect_type(var/def_zone)
return BULLET_IMPACT_METAL
+
+//! Topic
+/mob/living/silicon/Topic(href, href_list)
+ . = ..()
+ if(.)
+ return
+ if(href_list["ooc_notes"])
+ src.Examine_OOC()
+ return TRUE
diff --git a/code/modules/mob/living/silicon/silicon_vr.dm b/code/modules/mob/living/silicon/silicon_vr.dm
deleted file mode 100644
index 624406bd8bd..00000000000
--- a/code/modules/mob/living/silicon/silicon_vr.dm
+++ /dev/null
@@ -1,5 +0,0 @@
-/mob/living/silicon/Topic(href, href_list) //For Robots and pAI's. And possibly AI's too.
- if(href_list["ooc_notes"])
- src.Examine_OOC()
- return 1
- return ..()
diff --git a/code/modules/mob/living/silicon/translation.dm b/code/modules/mob/living/silicon/translation.dm
new file mode 100644
index 00000000000..f546d07c359
--- /dev/null
+++ b/code/modules/mob/living/silicon/translation.dm
@@ -0,0 +1,27 @@
+/datum/translation_context/simple/silicons
+ translation_class = TRANSLATION_CLASSES_STANDARD_TRANSLATE
+
+/datum/translation_context/variable/learning/silicons
+ translation_class = TRANSLATION_CLASSES_STANDARD_TRANSLATE
+ translation_class_learn = TRANSLATION_CLASSES_STANDARD_TRANSLATE
+
+/**
+ * @params
+ * - L - language to sync; if null, syncs all
+ */
+/mob/living/silicon/proc/sync_translation_context(datum/language/L)
+ return
+
+/mob/living/silicon/proc/translation_train_hook(datum/translation_context/context, datum/language/L, old_efficiency)
+ if(!old_efficiency)
+ to_chat(src, SPAN_NOTICE("Identifying new language . . ."))
+ to_chat(src, SPAN_NOTICE("New language identified: [L.name]. Registering and beginning adaptive translation."))
+ sync_translation_context(L)
+
+/mob/living/silicon/proc/create_translation_context(path = translation_context)
+ if(istype(translation_context))
+ qdel(translation_context)
+ translation_context = new path
+ if(istype(translation_context, /datum/translation_context/variable/learning/silicons))
+ var/datum/translation_context/variable/learning/silicons/CTX = translation_context
+ CTX.on_train = CALLBACK(src, .proc/translation_train_hook)
diff --git a/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer.dm b/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer.dm
index 6acf1fc84a9..59b357283fb 100644
--- a/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer.dm
+++ b/code/modules/mob/living/simple_mob/subtypes/animal/borer/borer.dm
@@ -238,7 +238,7 @@
return emote(copytext(message, 2))
var/datum/language/L = parse_language(message)
- if(L && L.language_flags & HIVEMIND)
+ if(L && L.language_flags & LANGUAGE_HIVEMIND)
L.broadcast(src,trim(copytext(message,3)), src.true_name)
return
diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm
index 092b98f44e3..1eb6e8b8dbe 100644
--- a/code/modules/mob/say.dm
+++ b/code/modules/mob/say.dm
@@ -77,7 +77,7 @@
return 1
return 0
- if(speaking.language_flags & INNATE)
+ if(speaking.language_flags & LANGUAGE_INNATE)
return 1
//Language check.
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index 04ca0f3f031..462e10aefd9 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -73,19 +73,6 @@
//Languages
add_language("Robot Talk", 1)
- add_language(LANGUAGE_GALCOM, 1)
- add_language(LANGUAGE_SOL_COMMON, 1)
- add_language(LANGUAGE_UNATHI, 1)
- add_language(LANGUAGE_SIIK, 1)
- add_language(LANGUAGE_AKHANI, 1)
- add_language(LANGUAGE_SKRELLIAN, 1)
- add_language(LANGUAGE_TRADEBAND, 1)
- add_language(LANGUAGE_GUTTER, 1)
- add_language(LANGUAGE_EAL, 1)
- add_language(LANGUAGE_SCHECHI, 1)
- add_language(LANGUAGE_SIGN, 1)
- add_language(LANGUAGE_TERMINUS, 1)
- add_language(LANGUAGE_ZADDAT, 0)
// Lorefolks say it may be so.
if(LANGUAGE_ROOTGLOBAL in languages)
diff --git a/code/modules/preferences/preference_setup/background/language.dm b/code/modules/preferences/preference_setup/background/language.dm
index eefdb2c9627..6ec0c6c75ff 100644
--- a/code/modules/preferences/preference_setup/background/language.dm
+++ b/code/modules/preferences/preference_setup/background/language.dm
@@ -66,7 +66,7 @@
var/datum/character_species/CS = prefs.character_species_datum()
for(var/id in extraneous)
var/datum/language/L = SScharacters.resolve_language_id(id)
- if((L.language_flags & WHITELISTED) && (!(L in CS.whitelist_languages)) && !config.check_alien_whitelist(ckey(L.name), prefs.client_ckey))
+ if((L.language_flags & LANGUAGE_WHITELISTED) && (!(L in CS.whitelist_languages)) && !config.check_alien_whitelist(ckey(L.name), prefs.client_ckey))
errors?.Add(SPAN_WARNING("[L.id] is a whitelisted language."))
return FALSE
return TRUE
diff --git a/code/modules/preferences/preference_setup/helpers/language_pick.dm b/code/modules/preferences/preference_setup/helpers/language_pick.dm
index 8044c26e8b3..c2b1c4432e9 100644
--- a/code/modules/preferences/preference_setup/helpers/language_pick.dm
+++ b/code/modules/preferences/preference_setup/helpers/language_pick.dm
@@ -17,7 +17,7 @@
to_chat(user, SPAN_WARNING("You cannot select another language!"))
return TRUE
var/datum/character_species/CS = character_species_datum()
- if((L.language_flags & WHITELISTED) && !((L.id in CS.whitelist_languages) || config.check_alien_whitelist(ckey(L.name), client_ckey)))
+ if((L.language_flags & LANGUAGE_WHITELISTED) && !((L.id in CS.whitelist_languages) || config.check_alien_whitelist(ckey(L.name), client_ckey)))
to_chat(user, SPAN_WARNING("[L.name] is a whitelisted language!"))
return FALSE
var/list/current = get_character_data(CHARACTER_DATA_LANGUAGES)
@@ -67,7 +67,7 @@ GLOBAL_LIST_EMPTY(language_picker_active)
var/list/built = list()
var/list/categories = list("General")
for(var/datum/language/L as anything in SScharacters.all_languages())
- if(L.language_flags & RESTRICTED)
+ if(L.language_flags & LANGUAGE_RESTRICTED)
continue
built[++built.len] = list(
"id" = L.id,
diff --git a/code/modules/research/designs/misc.dm b/code/modules/research/designs/misc.dm
index cb5d6fdbc94..a559af8ae49 100644
--- a/code/modules/research/designs/misc.dm
+++ b/code/modules/research/designs/misc.dm
@@ -113,7 +113,7 @@
id = "translator_all"
req_tech = list(TECH_DATA = 5, TECH_ENGINEERING = 5)
materials = list(MAT_STEEL = 3000, MAT_GLASS = 3000, MAT_GOLD = 500, MAT_SILVER = 500)
- build_path = /obj/item/universal_translator/all
+ build_path = /obj/item/universal_translator/adaptive
sort_string = "TAACC"
/datum/design/item/general/ear_translator_all
@@ -121,7 +121,7 @@
id = "ear_translator_all"
req_tech = list(TECH_DATA = 6, TECH_ENGINEERING = 6) //dude what hte fuck lmao
materials = list(MAT_STEEL = 2000, MAT_GLASS = 2000, MAT_GOLD = 2000, MAT_SILVER = 2000)
- build_path = /obj/item/universal_translator/ear/all
+ build_path = /obj/item/universal_translator/ear/adaptive
sort_string = "TAACD"
/datum/design/item/advmop
diff --git a/code/modules/vore/eating/living_vr.dm b/code/modules/vore/eating/living_vr.dm
index 4e14a3a3823..596f81033ff 100644
--- a/code/modules/vore/eating/living_vr.dm
+++ b/code/modules/vore/eating/living_vr.dm
@@ -759,6 +759,7 @@
/mob/living/Topic(href, href_list) //Can't find any instances of Topic() being overridden by /mob/living in polaris' base code, even though /mob/living/carbon/human's Topic() has a ..() call
if(href_list["vore_prefs"])
display_voreprefs(usr)
+ return TRUE
return ..()
/mob/living/proc/display_voreprefs(mob/user) //Called by Topic() calls on instances of /mob/living (and subtypes) containing vore_prefs as an argument