Files
lew 11a9a2d644 Multi-Languages: a complete Saycode rewrite (#22637)
Rewrites Saycode and Langchat to add support for multiple languages in
one message, including audible emotes.

<img width="1139" height="338" alt="image"
src="https://github.com/user-attachments/assets/25e26932-7a6e-4c54-ab74-56fffb92ecad"
/>

Here's some samples to explain how to mix languages.

<img width="422" height="26" alt="image"
src="https://github.com/user-attachments/assets/e1b176cc-8625-4dc9-83c8-a053d3f310e6"
/>

`Languages ,2 can be mixed ,3 like this.`

<img width="540" height="21" alt="image"
src="https://github.com/user-attachments/assets/19156c67-4670-4d7a-84d7-26e527de2676"
/>

`!explains, ,2Emotes work too. ,0The text will get auto-quoted.`

<img width="592" height="18" alt="image"
src="https://github.com/user-attachments/assets/cfc31c5c-2383-41c8-82be-b36836339321"
/>

`,3Most languages ,0can be ,2mixed ,1arbitrarily, ,3any number of
,0times.`

<img width="636" height="20" alt="image"
src="https://github.com/user-attachments/assets/388b4f9d-192a-4374-ac31-bbd4e4e5dfe4"
/>

`,2Emotes. ,eAnd he nods. ,3They don't have to come first anymore.`

There are some exceptions. The exceptions are currently anything with
any of the flags `SIGNLANG`, `HIVEMIND`, `PRESSUREPROOF`,
`KNOWONLYHEAR`. Exceptions work the same as current languages do: they
must be the first language in the message. If so, they prevent switching
into any other language mid-message; if they're not first, they just
wont trigger.

They're exceptions currently because there's not really a clean way that
I or the people I asked for help on this one to make them look nice.
`SIGNLANG` for example doesn't scramble text, it shows it's own `
gestures a lengthy message.` text for those that don't understand. Could
we just replace every instance of sign language with that if somebody
doesn't understand? Probably. It would look pretty awful though. e.g.
`Alina Eskelinen says, "Hello." Alina Eskelinen gestures a short
message.`

This definitely needs testmerging because it more or less rewrites the
entire pipeline surrounding `say`. The `say` code itself had to be
rewritten to support the multiple languages, as well as all the existing
plumbing for listeners receiving messages. In return though, it's
significantly more straightforward and hopefully by extension easier for
people to add to in the future.

Primarily, instead of having four different `hear_say`, `hear_radio`,
`hear_sleep`, etc., routes for messages to come through, every single
audible message is received by `hear_message`, which is responsible for
figuring out how clear the message is (is the radio damaged? is it a
whisper we're eavesdropping on?), who needs to receive it in their
chatbox, formatting it correctly for each listener, and finally if any
npc or object within range needs to react to it in some way, like a
parrot or a mech.

changelog:
- rscadd: "Adds code-switching: you can now speak in multiple languages
in the same message."
- rscadd: "Adds audible emotes to the language list. They can be
triggered with ,e."
  - rscdel: "Removes SSrunechat."
- refactor: "Rewrote langchat in order to support multiple languages and
partial comprehension."
- refactor: "Rewrote a vast majority of all saycode and the code
responsible for displaying saytext to clients."
- bugfix: "Sleeping mobs are no longer able to understand all
languages."
- bugfix: "Langchat now correctly shows the appropriate comprehension
for all viewers rather than all viewers sharing the comprehension of the
last viewer."
- bugfix: "Languages which are supposed to be invisible when not
understood no longer appear as scrambled overhead text."

Forgive me whoever has to review this. Biggest areas that have room for
error is stuff like a borer inside someone's head, and Dionae stuff. Old
langchat had odd exceptions for those and I was forced to rewrite it
entirely, but I think I got it all back to how it was working before.
2026-06-11 10:47:26 +00:00

321 lines
14 KiB
Plaintext

/// START specific to SSmachinery
#define START_PROCESSING_MACHINE(machine, flag)\
if(!istype(machine, /obj/structure/machinery)) CRASH("A non-machine [log_info_line(machine)] was queued to process on the machinery subsystem.");\
machine.processing_flags |= flag;\
START_PROCESSING(SSmachinery, machine)
/// STOP specific to SSmachinery
#define STOP_PROCESSING_MACHINE(machine, flag)\
machine.processing_flags &= ~flag;\
if(machine.processing_flags == 0){\
machine.datum_flags &= ~DF_ISPROCESSING;\
SSmachinery.processing -= machine}
//! ## Timing subsystem
/**
* Don't run if there is an identical unique timer active
*
* if the arguments to addtimer are the same as an existing timer, it doesn't create a new timer,
* and returns the id of the existing timer
*/
#define TIMER_UNIQUE (1<<0)
///For unique timers: Replace the old timer rather then not start this one
#define TIMER_OVERRIDE (1<<1)
/**
* Timing should be based on how timing progresses on clients, not the server.
*
* Tracking this is more expensive,
* should only be used in conjuction with things that have to progress client side, such as
* animate() or sound()
*/
#define TIMER_CLIENT_TIME (1<<2)
///Timer can be stopped using deltimer()
#define TIMER_STOPPABLE (1<<3)
///prevents distinguishing identical timers with the wait variable
///
///To be used with TIMER_UNIQUE
#define TIMER_NO_HASH_WAIT (1<<4)
///Loops the timer repeatedly until qdeleted
///
///In most cases you want a subsystem instead, so don't use this unless you have a good reason
#define TIMER_LOOP (1<<5)
///Delete the timer on parent datum Destroy() and when deltimer'd
#define TIMER_DELETE_ME (1<<6)
///Empty ID define
#define TIMER_ID_NULL -1
/// Used to trigger object removal from a processing list
#define PROCESS_KILL 26
// -- SSatoms stuff --
// Technically this check will fail if someone loads a map mid-round, but that's not enabled right now.
///TRUE if the INITIAL SSatoms initialization has finished, aka the atoms are initialized and mapload has finished
#define SSATOMS_IS_PROBABLY_DONE (SSatoms.initialized == INITIALIZATION_INNEW_REGULAR)
///type and all subtypes should always immediately call Initialize in New()
#define INITIALIZE_IMMEDIATE(X) ##X/New(loc, ...){\
..();\
if(!(flags_1 & INITIALIZED_1)) {\
var/previous_initialized_value = SSatoms.initialized;\
SSatoms.initialized = INITIALIZATION_INNEW_MAPLOAD;\
args[1] = TRUE;\
SSatoms.InitAtom(src, FALSE, args);\
SSatoms.initialized = previous_initialized_value;\
}\
}
/* Initialization subsystem */
///New should not call Initialize
#define INITIALIZATION_INSSATOMS 0
///New should call Initialize(TRUE)
#define INITIALIZATION_INNEW_MAPLOAD 2
///New should call Initialize(FALSE)
#define INITIALIZATION_INNEW_REGULAR 1
///Nothing happens.
#define INITIALIZE_HINT_NORMAL 0
/**
* call LateInitialize at the end of all atom Initalization
*
* The item will be added to the late_loaders list, this is iterated over after
* initalization of subsystems is complete and calls LateInitalize on the atom
* see [this file for the LateIntialize proc](atom.html#proc/LateInitialize)
*
* It's worth noting that LateInitialize will allow the persistence subsystem to apply content,
* which might be needed during the objects init process supplied via Init arguments,
* as the init by the persistence subsystems doesn't provide init arguments.
* Persistence subsystem instanciates object -> LateInit is returned
* -> Persistence subsystem applies content -> LateInit is called to finish the whole init process.
*/
#define INITIALIZE_HINT_LATELOAD 1
///Call qdel on the atom after intialization
#define INITIALIZE_HINT_QDEL 2
// -- SSoverlays --
#define CUT_OVERLAY_IN(ovr, time) addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, CutOverlays), ovr), time, TIMER_STOPPABLE | TIMER_CLIENT_TIME)
#define ATOM_USING_SSOVERLAY(atom) (atom.atom_overlay_cache || atom.atom_protected_overlay_cache)
// -- SSticker --
#define ROUND_IS_STARTED (SSticker.current_state >= GAME_STATE_PLAYING)
// -- SSeffects --
// tick/process() return vals.
#define EFFECT_CONTINUE 0 // Keep processing.
#define EFFECT_HALT 1 // Stop processing, but don't qdel.
#define EFFECT_DESTROY 2 // qdel.
// Effect helpers.
#define QUEUE_EFFECT(effect) if (!(effect.datum_flags & DF_ISPROCESSING)) {effect.datum_flags |= DF_ISPROCESSING; SSeffects.effect_systems += effect;}
#define QUEUE_VISUAL(visual) if (!(visual.datum_flags & DF_ISPROCESSING)) {visual.datum_flags |= DF_ISPROCESSING; SSeffects.visuals += visual;}
#define STOP_EFFECT(effect) SSeffects.effect_systems -= effect;
#define STOP_VISUAL(visual) SSeffects.visuals -= visual;
// -- SSfalling --
#define ADD_FALLING_ATOM(atom) if (!atom.multiz_falling) { atom.multiz_falling = 1; SSfalling.falling[atom] = 0; }
// -- SSlistener --
#define GET_LISTENERS(id) (id ? SSlistener.listeners["[id]"] : null)
// Connection prefixes for player-editable fields
#define WP_ELECTRONICS "elec_"
// -- SSicon_cache --
#define LIGHT_FIXTURE_CACHE(icon,state,color) SSicon_cache.light_fixture_cache["[icon]_[state]_[color]"] || (SSicon_cache.light_fixture_cache["[icon]_[state]_[color]"] = SSicon_cache.generate_color_variant(icon,state,color))
// -- SSmob_ai --
#define MOB_START_THINKING(mob) if (!mob.thinking_enabled) { SSmob_ai.processing += mob; mob.on_think_enabled(); mob.thinking_enabled = TRUE; }
#define MOB_STOP_THINKING(mob) SSmob_ai.processing -= mob; mob.on_think_disabled(); mob.thinking_enabled = FALSE;
#define MOB_SHIFT_TO_FAST_THINKING(mob) if(!mob.is_fast_processing) { SSmob_ai.processing -= mob; SSmob_fast_ai.processing += mob; mob.is_fast_processing = TRUE; }
#define MOB_SHIFT_TO_NORMAL_THINKING(mob) if(mob.is_fast_processing) { SSmob_fast_ai.processing -= mob; SSmob_ai.processing += mob; mob.is_fast_processing = FALSE; }
// - SSrecords --
#define RECORD_GENERAL BITFLAG(0)
#define RECORD_MEDICAL BITFLAG(1)
#define RECORD_SECURITY BITFLAG(2)
#define RECORD_LOCKED BITFLAG(3)
#define RECORD_WARRANT BITFLAG(4)
#define RECORD_VIRUS BITFLAG(5)
#define RECORD_SHUTTLE_MANIFEST BITFLAG(6)
// - SSjobs --
// departments
// If we rename any of these, also change them in ship_locations.dm define.
#define DEPARTMENT_COMMAND "Command"
#define DEPARTMENT_COMMAND_SUPPORT "Command Support"
#define DEPARTMENT_SECURITY "Security"
#define DEPARTMENT_ENGINEERING "Engineering"
#define DEPARTMENT_MEDICAL "Medical"
#define DEPARTMENT_SCIENCE "Science"
#define DEPARTMENT_CARGO "Operations"
#define DEPARTMENT_SERVICE "Service"
#define DEPARTMENT_CIVILIAN "Civilian"
#define DEPARTMENT_EQUIPMENT "Equipment"
#define DEPARTMENT_MISCELLANEOUS "Miscellaneous"
#define DEPARTMENT_OFFSHIP "Off-ship"
#define DEPARTMENTS_LIST_INIT list(\
DEPARTMENT_COMMAND = list(),\
DEPARTMENT_COMMAND_SUPPORT = list(),\
DEPARTMENT_SECURITY = list(),\
DEPARTMENT_ENGINEERING = list(),\
DEPARTMENT_MEDICAL = list(),\
DEPARTMENT_SCIENCE = list(),\
DEPARTMENT_CARGO = list(),\
DEPARTMENT_SERVICE = list(),\
DEPARTMENT_CIVILIAN = list(),\
DEPARTMENT_EQUIPMENT = list(),\
DEPARTMENT_MISCELLANEOUS = list(),\
DEPARTMENT_OFFSHIP = list(),\
)
// job roles within departments
#define JOBROLE_DEFAULT 0 // This is the default "job role", no special meaning.
#define JOBROLE_SUPERVISOR (1 << 0) // Indicates that the job is a supervisory position, i.e a head of department.
#define SIMPLEDEPT(dept) list(dept = JOBROLE_DEFAULT)
#define ASSET_CROSS_ROUND_CACHE_DIRECTORY "tmp/assets"
//! ### SS initialization hints
/**
* Negative values incidate a failure or warning of some kind, positive are good.
* 0 and 1 are unused so that TRUE and FALSE are guarenteed to be invalid values.
*/
/// Subsystem failed to initialize entirely. Print a warning, log, and disable firing.
#define SS_INIT_FAILURE -2
/// The default return value which must be overriden. Will succeed with a warning.
#define SS_INIT_NONE -1
/// Subsystem initialized sucessfully.
#define SS_INIT_SUCCESS 2
/// If your system doesn't need to be initialized (by being disabled or something)
#define SS_INIT_NO_NEED 3
/// Succesfully initialized, BUT do not announce it to players (generally to hide game mechanics it would otherwise spoil)
#define SS_INIT_NO_MESSAGE 4
/// The timer key used to know how long subsystem initialization takes
#define SS_INIT_TIMER_KEY "ss_init"
//! ### SS initialization load orders
// Subsystem init_order, from highest priority to lowest priority
// Subsystems shutdown in the reverse of the order they initialize in
// The numbers just define the ordering, they are meaningless otherwise.
#define INIT_ORDER_PERSISTENT_CONFIGURATION 101 //Aurora snowflake config handling
#define INIT_ORDER_PROFILER 101
#define INIT_ORDER_GARBAGE 99
#define INIT_ORDER_DBCORE 95
#define INIT_ORDER_AUTH 94 //Admin permissions should be loaded early on
#define INIT_ORDER_SOUNDS 83
#define INIT_ORDER_SENTRY 79
#define INIT_ORDER_DISCORD 78
#define INIT_ORDER_JOBS 65 // Must init before atoms, to set up properly the dynamic job lists.
#define INIT_ORDER_AI_CONTROLLERS 55 //So the controller can get the ref
#define INIT_ORDER_TICKER 55
#define INIT_ORDER_SEEDS 52 // More aurora snowflake, needs to load before the atoms init as it generates images for seeds that are used
#define INIT_ORDER_MISC_FIRST 51 //Another aurora snowflake system? Who would have guessed... Anyways, need to load before mapping or global HUDs are not ready when atoms request them
#define INIT_ORDER_MAPPING 50 //This is the ATLAS subsystem
#define INIT_ORDER_PARALLAX 49 // Parallax image cache generation. Must run before ghosts are able to join. Another aurora snowflake code, run after mapping or it runtimes
#define INIT_ORDER_EARLY_ASSETS 48
#define INIT_ORDER_SPATIAL_GRID 43
#define INIT_ORDER_ECONOMY 40
#define INIT_ORDER_MAPFINALIZE 32 // Asteroid generation, another aurora snowflake, must run before the atoms init
#define INIT_ORDER_PERSISTENCE 31 // Persistence subsystem, requires map load
#define INIT_ORDER_ATOMS 30
#define INIT_ORDER_MACHINES 20
#define INIT_ORDER_DEFAULT 0
#define INIT_ORDER_TIMER 1
#define INIT_ORDER_AIR -1
#define INIT_ORDER_AWAY_MAPS -2 //Loading away sites and exoplanets, should start after air, must initialize before ghost roles in order for their spawnpoints to work
#define INIT_ORDER_GHOSTROLES -2.1 //Ghost roles must initialize before SS_INIT_MISC due to some roles (matriarch drones) relying on the assumption that this SS is initialized.
#define INIT_ORDER_MISC -2.2 //Aurora snowflake, Subsystems without an explicitly set initialization order start here
#define INIT_ORDER_CODEX -3 // Codex subsystem. Should be initialized after chemistry and cooking recipes.
#define INIT_ORDER_VOTE -4
#define INIT_ORDER_ASSETS -5
#define INIT_ORDER_ICON_UPDATE -5.5 //Yet another aurora snowflake, Icon update queue flush. Should run before overlays, probably before smoothing the icon too
#define INIT_ORDER_VISCONTENTS -5.6 // Queued vis_contents updates.
#define INIT_ORDER_ICON_SMOOTHING -6
#define INIT_ORDER_OVERLAY -7
#define INIT_ORDER_WEATHER -9
#define INIT_ORDER_ODYSSEY -15
#define INIT_ORDER_LIGHTING -20
#define INIT_ORDER_ZCOPY -21 //Aurora snowflake, Z-mimic flush. Should run after SSoverlay & SSicon_smooth so it copies the smoothed sprites.
#define INIT_ORDER_PATH -50
#define INIT_ORDER_STATPANELS -97
#define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init.
// Subsystem fire priority, from lowest to highest priority
// If the subsystem isn't listed here it's either DEFAULT or PROCESS (if it's a processing subsystem child)
#define FIRE_PRIORITY_PING 10
#define FIRE_PRIORITY_GARBAGE 15
#define FIRE_PRIORITY_DATABASE 16
#define FIRE_PRIORITY_ASSETS 20
#define FIRE_PRIORITY_NPC_MOVEMENT 21
#define FIRE_PRIORITY_NPC_ACTIONS 22
#define FIRE_PRIORITY_PATHFINDING 23
#define FIRE_PRIORITY_PROCESS 25
#define FIRE_PRIORITY_THROWING 25
#define FIRE_PRIORITY_SPACEDRIFT 30
#define FIRE_PRIORITY_DEFAULT 50
#define FIRE_PRIORITY_MOBS 100
#define FIRE_PRIORITY_TGUI 110
#define FIRE_PRIORITY_STATPANEL 390
#define FIRE_PRIORITY_CHAT 400
#define FIRE_PRIORITY_TIMER 700
#define FIRE_PRIORITY_SOUND_LOOPS 800
#define FIRE_PRIORITY_SPEECH_CONTROLLER 900
#define FIRE_PRIORITY_DELAYED_VERBS 950
/**
Create a new timer and add it to the queue.
* Arguments:
* * callback the callback to call on timer finish
* * wait deciseconds to run the timer for
* * flags flags for this timer, see: code\__DEFINES\subsystems.dm
* * timer_subsystem the subsystem to insert this timer into
*/
#define addtimer(args...) _addtimer(args, file = __FILE__, line = __LINE__)
// Subsystem delta times or tickrates, in seconds. I.e, how many seconds in between each process() call for objects being processed by that subsystem.
// Only use these defines if you want to access some other objects processing seconds_per_tick, otherwise use the seconds_per_tick that is sent as a parameter to process()
#define SSMOBS_DT (SSmobs.wait/10)
/* AURORA SHIT */
// Don't display in processes panel.
#define SS_NO_DISPLAY 128
#define SS_IS_RUNNING(subsystem) (subsystem.can_fire && (GAME_STATE & subsystem.runlevels))
// Vote subsystem counting methods
/// First past the post. One selection per person, and the selection with the most votes wins.
#define VOTE_COUNT_METHOD_SINGLE 1
/// Approval voting. Any number of selections per person, and the selection with the most votes wins.
#define VOTE_COUNT_METHOD_MULTI 2
/// The choice with the most votes wins. Ties are broken by the first choice to reach that number of votes.
#define VOTE_WINNER_METHOD_SIMPLE "Simple"
/// The winning choice is selected randomly based on the number of votes each choice has.
#define VOTE_WINNER_METHOD_WEIGHTED_RANDOM "Weighted Random"
/// There is no winner for this vote.
#define VOTE_WINNER_METHOD_NONE "None"