From 483cc8cdd5e202b518dc2ffde25083e5d9aea165 Mon Sep 17 00:00:00 2001
From: JohnFulpWillard <53777086+JohnFulpWillard@users.noreply.github.com>
Date: Sat, 2 Dec 2023 08:36:40 -0500
Subject: [PATCH 1/3] ports swag ass fonts
---
code/__DEFINES/MC.dm | 8 +
code/__DEFINES/_tick.dm | 5 +
code/__DEFINES/chat.dm | 3 +
code/__DEFINES/fonts.dm | 7 +
code/__DEFINES/text.dm | 21 +-
code/__DEFINES/traits.dm | 2 +
code/_globalvars/traits.dm | 1 +
code/controllers/subsystem/runechat.dm | 249 +----------------
code/datums/chatmessage.dm | 280 ++++++++++++--------
code/game/say.dm | 18 +-
code/modules/balloon_alert/balloon_alert.dm | 2 +-
code/modules/mob/say.dm | 2 +-
interface/fonts/Grand9K_Pixel.ttf | Bin 0 -> 22108 bytes
interface/fonts/Pixellari.ttf | Bin 0 -> 39908 bytes
interface/fonts/SpessFont.ttf | Bin 0 -> 7692 bytes
interface/fonts/TinyUnicode.ttf | Bin 0 -> 23280 bytes
interface/fonts/VCR_OSD_Mono.ttf | Bin 0 -> 75864 bytes
interface/fonts/fonts_datum.dm | 78 ++++++
interface/fonts/grand_9k.dm | 253 ++++++++++++++++++
interface/fonts/license.txt | 13 +
interface/fonts/pixellari.dm | 252 ++++++++++++++++++
interface/fonts/spess_font.dm | 252 ++++++++++++++++++
interface/fonts/tiny_unicode.dm | 253 ++++++++++++++++++
interface/fonts/vcr_osd_mono.dm | 3 +
strings/tips.txt | 2 +
yogstation.dme | 7 +
26 files changed, 1361 insertions(+), 350 deletions(-)
create mode 100644 code/__DEFINES/fonts.dm
create mode 100644 interface/fonts/Grand9K_Pixel.ttf
create mode 100644 interface/fonts/Pixellari.ttf
create mode 100644 interface/fonts/SpessFont.ttf
create mode 100644 interface/fonts/TinyUnicode.ttf
create mode 100644 interface/fonts/VCR_OSD_Mono.ttf
create mode 100644 interface/fonts/fonts_datum.dm
create mode 100644 interface/fonts/grand_9k.dm
create mode 100644 interface/fonts/license.txt
create mode 100644 interface/fonts/pixellari.dm
create mode 100644 interface/fonts/spess_font.dm
create mode 100644 interface/fonts/tiny_unicode.dm
create mode 100644 interface/fonts/vcr_osd_mono.dm
diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm
index aa1d6214bd26..e8dd84b3a366 100644
--- a/code/__DEFINES/MC.dm
+++ b/code/__DEFINES/MC.dm
@@ -99,6 +99,14 @@
}\
/datum/controller/subsystem/##X
+#define TIMER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/timer/##X);\
+/datum/controller/subsystem/timer/##X/New(){\
+ NEW_SS_GLOBAL(SS##X);\
+ PreInit();\
+}\
+/datum/controller/subsystem/timer/##X/fire() {..() /*just so it shows up on the profiler*/} \
+/datum/controller/subsystem/timer/##X
+
#define PROCESSING_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/processing/##X);\
/datum/controller/subsystem/processing/##X/New(){\
NEW_SS_GLOBAL(SS##X);\
diff --git a/code/__DEFINES/_tick.dm b/code/__DEFINES/_tick.dm
index abafc4465c38..142d904f31a5 100644
--- a/code/__DEFINES/_tick.dm
+++ b/code/__DEFINES/_tick.dm
@@ -22,6 +22,11 @@
/// runs stoplag if tick_usage is above the limit
#define CHECK_TICK ( TICK_CHECK ? stoplag() : 0 )
+/// Checks if a sleeping proc is running before or after the master controller
+#define RUNNING_BEFORE_MASTER ( Master.last_run != null && Master.last_run != world.time )
+/// Returns true if a verb ought to yield to the MC (IE: queue up to be processed by a subsystem)
+#define VERB_SHOULD_YIELD ( TICK_CHECK || RUNNING_BEFORE_MASTER )
+
/// Returns true if tick usage is above 95, for high priority usage
#define TICK_CHECK_HIGH_PRIORITY ( TICK_USAGE > 95 )
/// runs stoplag if tick_usage is above 95, for high priority usage
diff --git a/code/__DEFINES/chat.dm b/code/__DEFINES/chat.dm
index 4c66ba43ada7..d26ce716c3e7 100644
--- a/code/__DEFINES/chat.dm
+++ b/code/__DEFINES/chat.dm
@@ -20,5 +20,8 @@
#define MESSAGE_TYPE_MENTORPM "mentorpm"
#define MESSAGE_TYPE_DONATOR "donator"
+/// Max length of chat message in characters
+#define CHAT_MESSAGE_MAX_LENGTH 110
+
/// Adds a generic box around whatever message you're sending in chat. Really makes things stand out.
#define examine_block(str) ("
" + str + "
")
diff --git a/code/__DEFINES/fonts.dm b/code/__DEFINES/fonts.dm
new file mode 100644
index 000000000000..ba799a62c9c7
--- /dev/null
+++ b/code/__DEFINES/fonts.dm
@@ -0,0 +1,7 @@
+// Font metrics bitfield
+/// Include leading A width and trailing C width in GetWidth() or in DrawText()
+#define INCLUDE_AC (1<<0)
+
+DEFINE_BITFIELD(font_flags, list(
+ "INCLUDE_AC" = INCLUDE_AC,
+))
diff --git a/code/__DEFINES/text.dm b/code/__DEFINES/text.dm
index 823a88345de6..cf35addca922 100644
--- a/code/__DEFINES/text.dm
+++ b/code/__DEFINES/text.dm
@@ -1,8 +1,25 @@
/// Prepares a text to be used for maptext. Use this so it doesn't look hideous.
#define MAPTEXT(text) {"[##text]"}
-/// Macro from Lummox used to get height from a MeasureText proc
-#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1))
+/// Prepares a text to be used for maptext, using a variable size font.
+/// Variable size font. More flexible but doesn't scale pixel perfect to BYOND icon resolutions. (May be blurry.) Can use any size in pt or px.
+#define MAPTEXT_VCR_OSD_MONO(text) {"[##text]"}
+
+/// Prepares a text to be used for maptext using a pixel font. Cleaner but less size choices.
+/// Standard size (ie: normal runechat) Use only sizing pt, multiples of 6: 6pt 12pt 18pt 24pt etc. - Not for use with px sizing
+#define MAPTEXT_GRAND9K(text) {"[##text]"}
+
+/// Prepares a text to be used for maptext using a pixel font. Cleaner but less size choices.
+/// Small size. (ie: whisper runechat) Use only size pt, multiples of 12: 12pt 24pt 48pt etc. - Not for use with px sizing
+#define MAPTEXT_TINY_UNICODE(text) {"[##text]"}
+
+/// Macro from Lummox used to get height from a MeasureText proc.
+/// resolves the MeasureText() return value once, then resolves the height, then sets return_var to that.
+#define WXH_TO_HEIGHT(measurement, return_var) \
+ do { \
+ var/_measurement = measurement; \
+ return_var = text2num(copytext(_measurement, findtextEx(_measurement, "x") + 1)); \
+ } while(FALSE);
/*
* Uses MAPTEXT to format antag points into a more appealing format
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 0cdc86824d08..b4400312bf5f 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -134,6 +134,8 @@
#define TRAIT_CRITICAL_CONDITION "critical-condition"
/// Is frozen in place
#define TRAIT_FROZEN "frozen"
+/// Is runechat for this atom/movable currently disabled, regardless of prefs or anything?
+#define TRAIT_RUNECHAT_HIDDEN "runechat_hidden"
/// trait associated to a stat value or range of
#define STAT_TRAIT "stat"
#define TRAIT_INCAPACITATED "incapacitated"
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 6b17e39cec8b..ebc3d1b0de4c 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -6,6 +6,7 @@
GLOBAL_LIST_INIT(traits_by_type, list(
/mob = list(/atom/movable = list(
"TRAIT_MOVE_PHASING" = TRAIT_MOVE_PHASING,
+ "TRAIT_RUNECHAT_HIDDEN" = TRAIT_RUNECHAT_HIDDEN,
))))
/// value -> trait name, generated on use from trait_by_type global
GLOBAL_LIST(trait_name_map)
diff --git a/code/controllers/subsystem/runechat.dm b/code/controllers/subsystem/runechat.dm
index a5193a508ad6..663bb8cf347d 100644
--- a/code/controllers/subsystem/runechat.dm
+++ b/code/controllers/subsystem/runechat.dm
@@ -1,243 +1,14 @@
-/// Controls how many buckets should be kept, each representing a tick. (30 seconds worth)
-#define BUCKET_LEN (world.fps * 1 * 30)
-/// Helper for getting the correct bucket for a given chatmessage
-#define BUCKET_POS(scheduled_destruction) (((round((scheduled_destruction - SSrunechat.head_offset) / world.tick_lag) + 1) % BUCKET_LEN) || BUCKET_LEN)
-/// Gets the maximum time at which messages will be handled in buckets, used for deferring to secondary queue
-#define BUCKET_LIMIT (world.time + TICKS2DS(min(BUCKET_LEN - (SSrunechat.practical_offset - DS2TICKS(world.time - SSrunechat.head_offset)) - 1, BUCKET_LEN - 1)))
-
-/**
- * # Runechat Subsystem
- *
- * Maintains a timer-like system to handle destruction of runechat messages. Much of this code is modeled
- * after or adapted from the timer subsystem.
- *
- * Note that this has the same structure for storing and queueing messages as the timer subsystem does
- * for handling timers: the bucket_list is a list of chatmessage datums, each of which are the head
- * of a circularly linked list. Any given index in bucket_list could be null, representing an empty bucket.
- */
-SUBSYSTEM_DEF(runechat)
+TIMER_SUBSYSTEM_DEF(runechat)
name = "Runechat"
- flags = SS_TICKER | SS_NO_INIT
- wait = 1
priority = FIRE_PRIORITY_RUNECHAT
- /// world.time of the first entry in the bucket list, effectively the 'start time' of the current buckets
- var/head_offset = 0
- /// Index of the first non-empty bucket
- var/practical_offset = 1
- /// world.tick_lag the bucket was designed for
- var/bucket_resolution = 0
- /// How many messages are in the buckets
- var/bucket_count = 0
- /// List of buckets, each bucket holds every message that has to be killed that byond tick
- var/list/bucket_list = list()
- /// Queue used for storing messages that are scheduled for deletion too far in the future for the buckets
- var/list/datum/chatmessage/second_queue = list()
-
-/datum/controller/subsystem/runechat/PreInit()
- bucket_list.len = BUCKET_LEN
- head_offset = world.time
- bucket_resolution = world.tick_lag
-
-/datum/controller/subsystem/runechat/stat_entry(msg)
- msg += "ActMsgs:[bucket_count] SecQueue:[length(second_queue)]"
- return msg
-
-/datum/controller/subsystem/runechat/get_metrics()
- . = ..()
- .["buckets"] = bucket_count
- .["second_queue"] = length(second_queue)
-
-/datum/controller/subsystem/runechat/fire(resumed = FALSE)
- // Store local references to datum vars as it is faster to access them this way
- var/list/bucket_list = src.bucket_list
-
- if (MC_TICK_CHECK)
- return
-
- // Check for when we need to loop the buckets, this occurs when
- // the head_offset is approaching BUCKET_LEN ticks in the past
- if (practical_offset > BUCKET_LEN)
- head_offset += TICKS2DS(BUCKET_LEN)
- practical_offset = 1
- resumed = FALSE
-
- // Check for when we have to reset buckets, typically from auto-reset
- if ((length(bucket_list) != BUCKET_LEN) || (world.tick_lag != bucket_resolution))
- reset_buckets()
- bucket_list = src.bucket_list
- resumed = FALSE
-
- // Store a reference to the 'working' chatmessage so that we can resume if the MC
- // has us stop mid-way through processing
- var/static/datum/chatmessage/cm
- if (!resumed)
- cm = null
-
- // Iterate through each bucket starting from the practical offset
- while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset - 1) * world.tick_lag) <= world.time)
- var/datum/chatmessage/bucket_head = bucket_list[practical_offset]
- if (!cm || !bucket_head || cm == bucket_head)
- bucket_head = bucket_list[practical_offset]
- cm = bucket_head
-
- while (cm)
- // If the chatmessage hasn't yet had its life ended then do that now
- var/datum/chatmessage/next = cm.next
- if (!cm.eol_complete)
- cm.end_of_life()
- else if (!QDELETED(cm)) // otherwise if we haven't deleted it yet, do so (this is after EOL completion)
- qdel(cm)
-
- if (MC_TICK_CHECK)
- return
-
- // Break once we've processed the entire bucket
- cm = next
- if (cm == bucket_head)
- break
-
- // Empty the bucket, check if anything in the secondary queue should be shifted to this bucket
- bucket_list[practical_offset++] = null
- var/i = 0
- for (i in 1 to length(second_queue))
- cm = second_queue[i]
- if (cm.scheduled_destruction >= BUCKET_LIMIT)
- i--
- break
-
- // Transfer the message into the bucket, performing necessary circular doubly-linked list operations
- bucket_count++
- var/bucket_pos = max(1, BUCKET_POS(cm.scheduled_destruction))
- var/datum/timedevent/head = bucket_list[bucket_pos]
- if (!head)
- bucket_list[bucket_pos] = cm
- cm.next = null
- cm.prev = null
- continue
-
- if (!head.prev)
- head.prev = head
- cm.next = head
- cm.prev = head.prev
- cm.next.prev = cm
- cm.prev.next = cm
- if (i)
- second_queue.Cut(1, i + 1)
- cm = null
-
-/datum/controller/subsystem/runechat/Recover()
- bucket_list |= SSrunechat.bucket_list
- second_queue |= SSrunechat.second_queue
-
-/datum/controller/subsystem/runechat/proc/reset_buckets()
- bucket_list.len = BUCKET_LEN
- head_offset = world.time
- bucket_resolution = world.tick_lag
-
-/**
- * Enters the runechat subsystem with this chatmessage, inserting it into the end-of-life queue
- *
- * This will also account for a chatmessage already being registered, and in which case
- * the position will be updated to remove it from the previous location if necessary
- *
- * Arguments:
- * * new_sched_destruction Optional, when provided is used to update an existing message with the new specified time
- */
-/datum/chatmessage/proc/enter_subsystem(new_sched_destruction = 0)
- // Get local references from subsystem as they are faster to access than the datum references
- var/list/bucket_list = SSrunechat.bucket_list
- var/list/second_queue = SSrunechat.second_queue
-
- // When necessary, de-list the chatmessage from its previous position
- if (new_sched_destruction)
- if (scheduled_destruction >= BUCKET_LIMIT)
- second_queue -= src
- else
- SSrunechat.bucket_count--
- var/bucket_pos = BUCKET_POS(scheduled_destruction)
- if (bucket_pos > 0)
- var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
- if (bucket_head == src)
- bucket_list[bucket_pos] = next
- if (prev != next)
- prev.next = next
- next.prev = prev
- else
- prev?.next = null
- next?.prev = null
- prev = next = null
- scheduled_destruction = new_sched_destruction
-
- // Ensure the scheduled destruction time is properly bound to avoid missing a scheduled event
- scheduled_destruction = max(CEILING(scheduled_destruction, world.tick_lag), world.time + world.tick_lag)
-
- // Handle insertion into the secondary queue if the required time is outside our tracked amounts
- if (scheduled_destruction >= BUCKET_LIMIT)
- BINARY_INSERT(src, SSrunechat.second_queue, /datum/chatmessage, src, scheduled_destruction, COMPARE_KEY)
- return
-
- // Get bucket position and a local reference to the datum var, it's faster to access this way
- var/bucket_pos = BUCKET_POS(scheduled_destruction)
-
- // Get the bucket head for that bucket, increment the bucket count
- var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
- SSrunechat.bucket_count++
-
- // If there is no existing head of this bucket, we can set this message to be that head
- if (!bucket_head)
- bucket_list[bucket_pos] = src
- return
-
- // Otherwise it's a simple insertion into the circularly doubly-linked list
- if (!bucket_head.prev)
- bucket_head.prev = bucket_head
- next = bucket_head
- prev = bucket_head.prev
- next.prev = src
- prev.next = src
-
-
-/**
- * Removes this chatmessage datum from the runechat subsystem
- */
-/datum/chatmessage/proc/leave_subsystem()
- // Attempt to find the bucket that contains this chat message
- var/bucket_pos = BUCKET_POS(scheduled_destruction)
-
- // Get local references to the subsystem's vars, faster than accessing on the datum
- var/list/bucket_list = SSrunechat.bucket_list
- var/list/second_queue = SSrunechat.second_queue
-
- // Attempt to get the head of the bucket
- var/datum/chatmessage/bucket_head
- if (bucket_pos > 0)
- bucket_head = bucket_list[bucket_pos]
-
- // Decrement the number of messages in buckets if the message is
- // the head of the bucket, or has a SD less than BUCKET_LIMIT implying it fits
- // into an existing bucket, or is otherwise not present in the secondary queue
- if(bucket_head == src)
- bucket_list[bucket_pos] = next
- SSrunechat.bucket_count--
- else if(scheduled_destruction < BUCKET_LIMIT)
- SSrunechat.bucket_count--
- else
- var/l = length(second_queue)
- second_queue -= src
- if(l == length(second_queue))
- SSrunechat.bucket_count--
-
- // Remove the message from the bucket, ensuring to maintain
- // the integrity of the bucket's list if relevant
- if(prev != next)
- prev.next = next
- next.prev = prev
- else
- prev?.next = null
- next?.prev = null
- prev = next = null
+ var/list/datum/callback/message_queue = list()
-#undef BUCKET_LEN
-#undef BUCKET_POS
-#undef BUCKET_LIMIT
+/datum/controller/subsystem/timer/runechat/fire(resumed)
+ . = ..() //poggers
+ while(message_queue.len)
+ var/datum/callback/queued_message = message_queue[message_queue.len]
+ queued_message.Invoke()
+ message_queue.len--
+ if(MC_TICK_CHECK)
+ return
diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm
index 5694bfa0fd08..90de8a8d5101 100644
--- a/code/datums/chatmessage.dm
+++ b/code/datums/chatmessage.dm
@@ -1,33 +1,38 @@
/// How long the chat message's spawn-in animation will occur for
-#define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS
+#define CHAT_MESSAGE_SPAWN_TIME (0.2 SECONDS)
/// How long the chat message will exist prior to any exponential decay
-#define CHAT_MESSAGE_LIFESPAN 5 SECONDS
+#define CHAT_MESSAGE_LIFESPAN (5 SECONDS)
/// How long the chat message's end of life fading animation will occur for
-#define CHAT_MESSAGE_EOL_FADE 0.7 SECONDS
+#define CHAT_MESSAGE_EOL_FADE (0.7 SECONDS)
+/// Grace period for fade before we actually delete the chat message
+#define CHAT_MESSAGE_GRACE_PERIOD (0.2 SECONDS)
/// Factor of how much the message index (number of messages) will account to exponential decay
-#define CHAT_MESSAGE_EXP_DECAY 0.7
+#define CHAT_MESSAGE_EXP_DECAY 0.7
/// Factor of how much height will account to exponential decay
-#define CHAT_MESSAGE_HEIGHT_DECAY 0.9
+#define CHAT_MESSAGE_HEIGHT_DECAY 0.9
/// Approximate height in pixels of an 'average' line, used for height decay
-#define CHAT_MESSAGE_APPROX_LHEIGHT 11
+#define CHAT_MESSAGE_APPROX_LHEIGHT 11
/// Max width of chat message in pixels
-#define CHAT_MESSAGE_WIDTH 96
-/// Max length of chat message in characters
-#define CHAT_MESSAGE_MAX_LENGTH 110
+#define CHAT_MESSAGE_WIDTH 112
+/// The dimensions of the chat message icons
+#define CHAT_MESSAGE_ICON_SIZE 9
+
+///Base layer of chat elements
+#define CHAT_LAYER 1
+///Highest possible layer of chat elements
+#define CHAT_LAYER_MAX 2
/// Maximum precision of float before rounding errors occur (in this context)
-#define CHAT_LAYER_Z_STEP 0.0001
+#define CHAT_LAYER_Z_STEP 0.0001
/// The number of z-layer 'slices' usable by the chat message layering
-#define CHAT_LAYER_MAX_Z (CHAT_LAYER_MAX - CHAT_LAYER) / CHAT_LAYER_Z_STEP
-/// The dimensions of the chat message icons
-#define CHAT_MESSAGE_ICON_SIZE 9
+#define CHAT_LAYER_MAX_Z (CHAT_LAYER_MAX - CHAT_LAYER) / CHAT_LAYER_Z_STEP
/**
- * # Chat Message Overlay
- *
- * Datum for generating a message overlay on the map
- */
+ * # Chat Message Overlay
+ *
+ * Datum for generating a message overlay on the map
+ */
/datum/chatmessage
- /// The visual element of the chat messsage
+ /// The visual element of the chat message
var/image/message
/// The location in which the message is appearing
var/atom/message_loc
@@ -45,18 +50,22 @@
var/datum/chatmessage/prev
/// The current index used for adjusting the layer of each sequential chat message such that recent messages will overlay older ones
var/static/current_z_idx = 0
+ /// When we started animating the message
+ var/animate_start = 0
+ /// Our animation lifespan, how long this message will last
+ var/animate_lifespan = 0
/**
- * Constructs a chat message overlay
- *
- * Arguments:
- * * text - The text content of the overlay
- * * target - The target atom to display the overlay at
- * * owner - The mob that owns this overlay, only this mob will be able to view it
- * * language - The language this message was spoken in
- * * extra_classes - Extra classes to apply to the span that holds the text
- * * lifespan - The lifespan of the message in deciseconds
- */
+ * Constructs a chat message overlay
+ *
+ * Arguments:
+ * * text - The text content of the overlay
+ * * target - The target atom to display the overlay at
+ * * owner - The mob that owns this overlay, only this mob will be able to view it
+ * * language - The language this message was spoken in
+ * * extra_classes - Extra classes to apply to the span that holds the text
+ * * lifespan - The lifespan of the message in deciseconds
+ */
/datum/chatmessage/New(text, atom/target, mob/owner, datum/language/language, list/extra_classes = list(), lifespan = CHAT_MESSAGE_LIFESPAN)
. = ..()
if (!istype(target))
@@ -68,41 +77,44 @@
INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, language, extra_classes, lifespan)
/datum/chatmessage/Destroy()
- if (owned_by)
+ if (!QDELING(owned_by))
+ if(REALTIMEOFDAY < animate_start + animate_lifespan)
+ stack_trace("Del'd before we finished fading, with [(animate_start + animate_lifespan) - REALTIMEOFDAY] time left")
+
if (owned_by.seen_messages)
LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src)
owned_by.images.Remove(message)
+
owned_by = null
message_loc = null
message = null
- leave_subsystem()
return ..()
/**
- * Calls qdel on the chatmessage when its parent is deleted, used to register qdel signal
- */
+ * Calls qdel on the chatmessage when its parent is deleted, used to register qdel signal
+ */
/datum/chatmessage/proc/on_parent_qdel()
SIGNAL_HANDLER
qdel(src)
/**
- * Generates a chat message image representation
- *
- * Arguments:
- * * text - The text content of the overlay
- * * target - The target atom to display the overlay at
- * * owner - The mob that owns this overlay, only this mob will be able to view it
- * * language - The language this message was spoken in
- * * extra_classes - Extra classes to apply to the span that holds the text
- * * lifespan - The lifespan of the message in deciseconds
- */
+ * Generates a chat message image representation
+ *
+ * Arguments:
+ * * text - The text content of the overlay
+ * * target - The target atom to display the overlay at
+ * * owner - The mob that owns this overlay, only this mob will be able to view it
+ * * language - The language this message was spoken in
+ * * extra_classes - Extra classes to apply to the span that holds the text
+ * * lifespan - The lifespan of the message in deciseconds
+ */
/datum/chatmessage/proc/generate_image(text, atom/target, mob/owner, datum/language/language, list/extra_classes, lifespan)
/// Cached icons to show what language the user is speaking
var/static/list/language_icons
// Register client who owns this message
owned_by = owner.client
- RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_qdel), src)
+ RegisterSignal(owned_by, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_qdel))
// Remove spans in the message from things like the recorder
var/static/regex/span_check = new(@"<\/?span[^>]*>", "gi")
@@ -133,6 +145,10 @@
if (!ismob(target))
extra_classes |= "small"
+ // Why are you yelling?
+ if(copytext_char(text, -2) == "!!")
+ extra_classes |= SPAN_YELL
+
var/list/prefixes
// Append radio icon if from a virtual speaker
@@ -159,26 +175,67 @@
var/tgt_color = extra_classes.Find("italics") ? target.chat_color_darkened : target.chat_color
// Approximate text height
- var/complete_text = ""
- var/mheight = WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH))
- approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
+ var/complete_text = ""
+
+ var/mheight
+ WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH), mheight)
+
+ if(!VERB_SHOULD_YIELD)
+ return finish_image_generation(mheight, target, owner, complete_text, lifespan)
+
+ var/datum/callback/our_callback = CALLBACK(src, PROC_REF(finish_image_generation), mheight, target, owner, complete_text, lifespan)
+ SSrunechat.message_queue += our_callback
+ return
+
+///finishes the image generation after the MeasureText() call in generate_image().
+///necessary because after that call the proc can resume at the end of the tick and cause overtime.
+/datum/chatmessage/proc/finish_image_generation(mheight, atom/target, mob/owner, complete_text, lifespan)
+ var/rough_time = REALTIMEOFDAY
+ approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
+ var/starting_height = target.maptext_height
// Translate any existing messages upwards, apply exponential decay factors to timers
- message_loc = get_atom_on_turf(target)
+ message_loc = isturf(target) ? target : get_atom_on_turf(target)
if (owned_by.seen_messages)
var/idx = 1
var/combined_height = approx_lines
- for(var/msg in owned_by.seen_messages[message_loc])
- var/datum/chatmessage/m = msg
- animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME)
+ for(var/datum/chatmessage/m as anything in owned_by.seen_messages[message_loc])
combined_height += m.approx_lines
+ var/time_spent = rough_time - m.animate_start
+ var/time_before_fade = m.animate_lifespan - CHAT_MESSAGE_EOL_FADE
+
// When choosing to update the remaining time we have to be careful not to update the
- // scheduled time once the EOL completion time has been set.
- var/sched_remaining = m.scheduled_destruction - world.time
- if (!m.eol_complete)
- var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
- m.enter_subsystem(world.time + remaining_time) // push updated time to runechat SS
+ // scheduled time once the EOL has been executed.
+ if (time_spent >= time_before_fade)
+ if(m.message.pixel_y < starting_height)
+ var/max_height = m.message.pixel_y + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height
+ if(max_height > 0)
+ animate(m.message, pixel_y = m.message.pixel_y + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
+ else if(mheight + starting_height >= m.message.pixel_y)
+ animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
+ continue
+
+ var/remaining_time = time_before_fade * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
+ // Ensure we don't accidentially spike alpha up or something silly like that
+ m.message.alpha = m.get_current_alpha(time_spent)
+ if (remaining_time > 0)
+ // Stay faded in for a while, then
+ animate(m.message, alpha = 255, remaining_time)
+ // Fade out
+ animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
+ m.animate_lifespan = remaining_time + CHAT_MESSAGE_EOL_FADE
+ else
+ // Your time has come my son
+ animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
+ // We run this after the alpha animate, because we don't want to interrup it, but also don't want to block it by running first
+ // Sooo instead we do this. bit messy but it fuckin works
+ if(m.message.pixel_y < starting_height)
+ var/max_height = m.message.pixel_y + m.approx_lines * CHAT_MESSAGE_APPROX_LHEIGHT - starting_height
+ if(max_height > 0)
+ animate(m.message, pixel_y = m.message.pixel_y + max_height, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
+ else if(mheight + starting_height >= m.message.pixel_y)
+ animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
// Reset z index if relevant
if (current_z_idx >= CHAT_LAYER_MAX_Z)
@@ -189,43 +246,53 @@
message.plane = RUNECHAT_PLANE
message.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA | KEEP_APART
message.alpha = 0
- message.pixel_y = owner.bound_height * 0.95
+ message.pixel_y = starting_height
+ message.pixel_x = -target.base_pixel_x
message.maptext_width = CHAT_MESSAGE_WIDTH
- message.maptext_height = mheight
+ message.maptext_height = mheight * 1.25 // We add extra because some characters are superscript, like actions
message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5
- message.maptext = complete_text
+ message.maptext = MAPTEXT(complete_text)
+
+ animate_start = rough_time
+ animate_lifespan = lifespan
// View the message
LAZYADDASSOCLIST(owned_by.seen_messages, message_loc, src)
owned_by.images |= message
+
+ // Fade in
animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME)
+ var/time_before_fade = lifespan - CHAT_MESSAGE_SPAWN_TIME - CHAT_MESSAGE_EOL_FADE
+ // Stay faded in
+ animate(alpha = 255, time = time_before_fade)
+ // Fade out
+ animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
- // Register with the runechat SS to handle EOL and destruction
- scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE)
- enter_subsystem()
+ // Register with the runechat SS to handle destruction
+ addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src), lifespan + CHAT_MESSAGE_GRACE_PERIOD, TIMER_DELETE_ME, SSrunechat)
-/**
- * Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion,
- * sets time for scheduling deletion and re-enters the runechat SS for qdeling
- *
- * Arguments:
- * * fadetime - The amount of time to animate the message's fadeout for
- */
-/datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE)
- eol_complete = scheduled_destruction + fadetime
- animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL)
- enter_subsystem(eol_complete) // re-enter the runechat SS with the EOL completion time to QDEL self
+/datum/chatmessage/proc/get_current_alpha(time_spent)
+ if(time_spent < CHAT_MESSAGE_SPAWN_TIME)
+ return (time_spent / CHAT_MESSAGE_SPAWN_TIME) * 255
+
+ var/time_before_fade = animate_lifespan - CHAT_MESSAGE_EOL_FADE
+ if(time_spent <= time_before_fade)
+ return 255
+
+ return (1 - ((time_spent - time_before_fade) / CHAT_MESSAGE_EOL_FADE)) * 255
/**
- * Creates a message overlay at a defined location for a given speaker
- *
- * Arguments:
- * * speaker - The atom who is saying this message
- * * message_language - The language that the message is said in
- * * raw_message - The text content of the message
- * * spans - Additional classes to be added to the message
- */
+ * Creates a message overlay at a defined location for a given speaker
+ *
+ * Arguments:
+ * * speaker - The atom who is saying this message
+ * * message_language - The language that the message is said in
+ * * raw_message - The text content of the message
+ * * spans - Additional classes to be added to the message
+ */
/mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, runechat_flags = NONE)
+ if(HAS_TRAIT(speaker, TRAIT_RUNECHAT_HIDDEN))
+ return
// Ensure the list we are using, if present, is a copy so we don't modify the list provided to us
spans = spans ? spans.Copy() : list()
@@ -235,11 +302,7 @@
var/atom/movable/virtualspeaker/v = speaker
speaker = v.source
spans |= "virtual-speaker"
-
- //NTSL doesn't pass a speaker when you do broadcast() since technically nothing is actually speaking.
- if(!speaker)
- return
-
+
// Ignore virtual speaker (most often radio messages) from ourself
if (originalSpeaker != src && speaker == src)
return
@@ -248,25 +311,24 @@
if(runechat_flags & EMOTE_MESSAGE)
new /datum/chatmessage(raw_message, speaker, src, message_language, list("emote", "italics"))
else
- new /datum/chatmessage(lang_treat(speaker, message_language, raw_message, spans, null, TRUE), speaker, src, message_language, spans)
-
+ new /datum/chatmessage(raw_message, speaker, src, message_language, spans)
// Tweak these defines to change the available color ranges
-#define CM_COLOR_SAT_MIN 0.6
-#define CM_COLOR_SAT_MAX 0.7
-#define CM_COLOR_LUM_MIN 0.65
-#define CM_COLOR_LUM_MAX 0.75
+#define CM_COLOR_SAT_MIN 0.6
+#define CM_COLOR_SAT_MAX 0.7
+#define CM_COLOR_LUM_MIN 0.65
+#define CM_COLOR_LUM_MAX 0.75
/**
- * Gets a color for a name, will return the same color for a given string consistently within a round.atom
- *
- * Note that this proc aims to produce pastel-ish colors using the HSL colorspace. These seem to be favorable for displaying on the map.
- *
- * Arguments:
- * * name - The name to generate a color for
- * * sat_shift - A value between 0 and 1 that will be multiplied against the saturation
- * * lum_shift - A value between 0 and 1 that will be multiplied against the luminescence
- */
+ * Gets a color for a name, will return the same color for a given string consistently within a round.atom
+ *
+ * Note that this proc aims to produce pastel-ish colors using the HSL colorspace. These seem to be favorable for displaying on the map.
+ *
+ * Arguments:
+ * * name - The name to generate a color for
+ * * sat_shift - A value between 0 and 1 that will be multiplied against the saturation
+ * * lum_shift - A value between 0 and 1 that will be multiplied against the luminescence
+ */
/datum/chatmessage/proc/colorize_string(name, sat_shift = 1, lum_shift = 1)
// seed to help randomness
var/static/rseed = rand(1,26)
@@ -303,13 +365,19 @@
if(5)
return "#[num2hex(c, 2)][num2hex(m, 2)][num2hex(x, 2)]"
-#undef CHAT_MESSAGE_SPAWN_TIME
-#undef CHAT_MESSAGE_LIFESPAN
+
+#undef CHAT_LAYER_MAX_Z
+#undef CHAT_LAYER_Z_STEP
+#undef CHAT_MESSAGE_APPROX_LHEIGHT
+#undef CHAT_MESSAGE_GRACE_PERIOD
#undef CHAT_MESSAGE_EOL_FADE
#undef CHAT_MESSAGE_EXP_DECAY
#undef CHAT_MESSAGE_HEIGHT_DECAY
-#undef CHAT_MESSAGE_APPROX_LHEIGHT
-#undef CHAT_MESSAGE_WIDTH
-#undef CHAT_LAYER_Z_STEP
-#undef CHAT_LAYER_MAX_Z
#undef CHAT_MESSAGE_ICON_SIZE
+#undef CHAT_MESSAGE_LIFESPAN
+#undef CHAT_MESSAGE_SPAWN_TIME
+#undef CHAT_MESSAGE_WIDTH
+#undef CM_COLOR_LUM_MAX
+#undef CM_COLOR_LUM_MIN
+#undef CM_COLOR_SAT_MAX
+#undef CM_COLOR_SAT_MIN
diff --git a/code/game/say.dm b/code/game/say.dm
index 45410e3f1656..88b5eec84c39 100644
--- a/code/game/say.dm
+++ b/code/game/say.dm
@@ -61,7 +61,7 @@ GLOBAL_LIST_INIT(freqtospan, list(
var/endspanpart = ""// Yogs
//Message
- var/messagepart = " [span_message("[lang_treat(speaker, message_language, raw_message, spans, message_mods)]")]"
+ var/messagepart = " [span_message("[say_emphasis(lang_treat(speaker, message_language, raw_message, spans, message_mods))]")]"
var/languageicon = ""
var/datum/language/D = GLOB.language_datum_instances[message_language]
@@ -100,6 +100,22 @@ GLOBAL_LIST_INIT(freqtospan, list(
var/spanned = attach_spans(input, spans)
return "[say_mod(input, message_mods)], \"[spanned]\""
+/// Transforms the speech emphasis mods from [/atom/movable/proc/say_emphasis] into the appropriate HTML tags. Includes escaping.
+#define ENCODE_HTML_EMPHASIS(input, char, html, varname) \
+ var/static/regex/##varname = regex("(?$1[html]>")
+
+/// Scans the input sentence for speech emphasis modifiers, notably |italics|, +bold+, and _underline_ -mothblocks
+/atom/movable/proc/say_emphasis(input)
+ ENCODE_HTML_EMPHASIS(input, "\\|", "i", italics)
+ ENCODE_HTML_EMPHASIS(input, "\\+", "b", bold)
+ ENCODE_HTML_EMPHASIS(input, "_", "u", underline)
+ var/static/regex/remove_escape_backlashes = regex("\\\\(_|\\+|\\|)", "g") // Removes backslashes used to escape text modification.
+ input = remove_escape_backlashes.Replace_char(input, "$1")
+ return input
+
+#undef ENCODE_HTML_EMPHASIS
+
/atom/movable/proc/lang_treat(atom/movable/speaker, datum/language/language, raw_message, list/spans, list/message_mods = list(), no_quote = FALSE)
if(has_language(language))
var/atom/movable/AM = speaker.GetSource()
diff --git a/code/modules/balloon_alert/balloon_alert.dm b/code/modules/balloon_alert/balloon_alert.dm
index 45c78f0f466d..891bea06d50b 100644
--- a/code/modules/balloon_alert/balloon_alert.dm
+++ b/code/modules/balloon_alert/balloon_alert.dm
@@ -58,7 +58,7 @@
balloon_alert.appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM
balloon_alert.maptext = MAPTEXT("[text]")
balloon_alert.maptext_x = (BALLOON_TEXT_WIDTH - bound_width) * -0.5
- balloon_alert.maptext_height = WXH_TO_HEIGHT(viewer_client?.MeasureText(text, null, BALLOON_TEXT_WIDTH))
+ WXH_TO_HEIGHT(viewer_client?.MeasureText(text, null, BALLOON_TEXT_WIDTH), balloon_alert.maptext_height)
balloon_alert.maptext_width = BALLOON_TEXT_WIDTH
viewer_client?.images += balloon_alert
diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm
index 5baae6ee9b54..6818e83cfb4f 100644
--- a/code/modules/mob/say.dm
+++ b/code/modules/mob/say.dm
@@ -142,7 +142,7 @@
if(key)
K = src.key
- var/spanned = say_quote(message)
+ var/spanned = say_quote(say_emphasis(message))
var/source = "[span_prefix("DEAD:")] [span_name("[(src.client.prefs.chat_toggles & GHOST_CKEY) ? "" : "([K]) "][name]")][alt_name]" // yogs - i have no clue
var/rendered = " [span_message("[emoji_parse(spanned)]")]"
log_talk(message, LOG_SAY, tag="DEAD")
diff --git a/interface/fonts/Grand9K_Pixel.ttf b/interface/fonts/Grand9K_Pixel.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..cf6fdf44e2ec74a0ebe28b4bbb21f89b66bf8420
GIT binary patch
literal 22108
zcmdU1dz4*eb>HXSx${ginY`b*6KxcbnMtCQM5V~P2tq?d6p9QpH!weXmH{
z)7UQ<8=l@m|7M;GW?d_R4<<%PUZ+Ix)PbcH;vp
zz9e$lRp9@ny_3^3r(OM~TSRtLMM_(QvSWY4H9tN6+TNG^RC+pbLF8+nyyai*{GOd6
zC!`Y!;?u76Swfp`Tu-En-*7*@qu+6OLa#glS$h3a;heO+7+O@MvvOx;3ka;XbNL&x
zv$P1(C6&ZJH;dBf_VyQDQmx9_QtjW+A$17b4U1GI={s91wBtw%ac{%kft}Z=ncw4e
z?)>i8Dkys!UW1eL9)-)XR|mz$kk?q>JmYp#py!!u=fe*Es`
z`0Tp!n#&U3Z&r@^@;<+Nd!8wCY!k2VyoeUoVO^G8W|x=sS(o3vp3^9v{XS{Mdl|rE
zd*4~FS=-{ZR^MaVahdg-+1B`GGQ@3)Wzr~KYxdpk6zd%OEY^|joi5LQ&`Zyt}{L
zN95)FoJV}kB0e?m8jJkog}8#OP(DN2H20h%XG67;9F#wo56Xw+BXU?iE)U6Pq@MI9
ztCLO1MadP(tCQiRmP{r$BnOka{d4t4>YuGYT>nh{
zuj`+xKUlx7es}$y^_lwX>tprd`fKa2s$W)rdHu{|cOUuGkq3`FaO4w5K7Qozk-LtZ
z_RpU*9U;lH`Lvg4&P0wq8zM@f1Y5Vky6tdoCjzM(>3N>amj%d^3#CsM$zoX|OJ$iX
zmld*7R>^8vBWq=ytcNZ8WrI9No-4m18|8WOd^tr9GkG68{(d0m1M(L@&wcWjK+s|NsC*3me_UQK*UJbf{ew)(d*yX9CjTgVWGAlN
zD7WIc8@tG;Oz1g2+zA{`%dm{&;@|6yT{0!#laQ7-XVu@{q1s_yhq+GpAPly^ae!6
zLKM%|$dF$oGM;^0fWotiieFMp2r#+~_&W=z_;fOWNS;XEm3%OHGWl7ly|koMEp01J
zmEKaiuk?87Sb1Ifg7Rqjp7K}AN6P=xHq>@O+rhT`+McSkSB5H=RCZNvuG~?1sPb6l
zpDI6Y-`svq`|kET+CSa?-Hy(VfsU&>c6S`=c(CKi&W)WHb?)yx)cL{AhdRI4`ID~B
zu7R#sbiJgK0-8XgL+x>9&ksj$;*K=mi^*wLvxvS^PJwKVZ
zZQix>-ahaCc@NKfYJNAib@MNoKQ;gG{4dP^_JYj|-nQV51xI_Uy=V9C=-u0UTkl=H
z_b-%%r!IWW!b1xmS$M4P)V`g4xAi^LcVtoLqB9p=x9FxtcP@Hl(RUY@7GJ#h;Nnj#
z{?U@5C8J9YFL`R|qNQgoomzVL(#MwmWZA}L*DSkz*;kh@Tz=m2L(4zE{HYadR$RW~
z))k*$@#Ko5E7z^uwsLgkohu(*Ra$k{s+(6mxa#p$CsuDyE6iuD^Kw%=-J+KLw0jTfM9Lh3aF~
zC#yfK9_?SyzoGw({>%HX?Z2V_-u_27ls3FjJ7^>y^ebf^lD_^v?iG0k$&HtOj{QXa
znFKu!Gajyb-A$oxf^l1dt&R$8$9febr!-e4{iq4LT8rJt3Ef$by_eEkpYgWZjmI{$
zt1B<#R|;Ioxvn(Ja$Lu;l-wk;d&kpuxPecq?{hM)iOSf>l~>w8jrK^C3%R=l95vf_
zN<$0qcnrsQqkP$BW9SaIV-LPBMs8n?eEuBd@Y9gDw;)%)ScDelmh|;_5&o~(e(m#fK{*4?6M{o$36e2CbcUHkieXS7W0h6AdOcS@j<_?7L_U1IZ0hdSz}d+QYuLycs<4yzZqNVMzrGt-Kj0c
zf6y(a>n7saR7_i;jd7utW-oKE(c@4AP0(I6FSVSV>%EH_MOKQplOlLtAtQ+D1>HGf?vSd=(2tZ
zmP?S|sBn;{g7MdUjJZPOfw%je(P8YKL5bpIPBIlqGU(P8V_>mT3v`?uB{XubgIkJZ
z;xcHejZ<8KUL=>PLlfC%F+}AU&1R-LOi{vS5<}qznwZr`7$;539+eD(=zCo!ORmy7Vc)mNz99R6_x=fcqK0>i5AA^ayv$-8n
zx21fB%^;IIQO_X2&E?~HHi_c9En+obBW9M8^q6&-ot$G4VS!{^haymge(S@$Z6)Sv
zj9Bl*$smKG-hg=sJvRpXX*%sFjMYz;y)cdfE~06
zoejH@GTQRU_9rw6T0A$)GdUzva8D~tgCt^jCLt5@7}T~Dp=m;CMwoM~xkMgv)Gh9V
zb!-@NUg+IuDwn{yTO(SWNe*ggu2+6i;QulQt(`Rv$|tS$(hVb&nIvnSD5#fQ6lQ
zl3AG0@UawuP5olMlwa^s#4mPFhuZY#YFpFC$2Xbz>^3)o+2yL8EL?5%B>)%u^w=d6RQSAQ>i-tHdX3%Jl9{cK9(xkd&TD+L+Qv}dl+<0bPhqUK-2SA$L
zW8a~d({_P|cf5|jvZCNPY(;~HaUZP6@DDl0e40`rHEujJ!#-1xh%R%tHAt4X8}nBV
zCq}t316cGk7vv;`oDF@lBbDM|C+tX%wxZpfHYqx&vNFhJSc3W{53#{fgr_nEevGMj
z>~FS-Y?6hGhD=kL$%g{RXG&+?jRkozENEj3e9VaRcKwAgZdNz}Z!^D{=PHIrUA!&C
zw$CHe+!D(jmsJ**Vp>P3W2pWbO`#uUwaXIO;nQ9DgFH^aI9UV?t##EU&l;_+a^dG8&
z*5M(WGphgH=d_e*osC6X(PusC$=rl20{u7|%ExZHLZT8G*33e#AUS$$WIphc@vfr=
zKG1;lI8dkh_#ofVJb`DHDR7^3ZG1>GqPFTE#%nqg0xh$*+Fb2!s{1^c%}fyUgozpW
zQd{NQkmnqEPK=ghKh9ADOZWs|z+`z}P#!uUp1+$TOkvHh8Q;7H!Euz;Sel_r7E+D+
zyV^Xb<0!g?V8~Vi%4!1&Z=lZWpY|0>#p#&U%mB2SS@KxgZ$f|iH0Y09Our^{LBL5r
z2Iyu4VwgBHpW8@`zBMtCfnu8kR0mmfkZ%f|05|rgR09t-v`wq^KIS&gpA9oqJH<5R
zR3AZ1OJA&Jd(@1IrTDOnfmo+hK}V;-OSCE#GMiagI89!4J}gs(V>qWV-(>t0=54{Y
ziuAxW*p}Z&lpCkd#==2EJ8^KH4!(nZl^*gkMYhdw&McF)QML6D_^0?FS8hDV5RZYD
zjk;lb@J%t&BE~Gk(Rf4)dr6Bhjj=*|M_7;oQk8GPf?2Q!LAgM)}1s2yd?Xt@8<
zoM8cEeGN&p0Sx@0A`f{z$k93bnHaK2%z9O`jU7qUl9$TSl5R{}%V@`qwB1U1io_dt
z`&map*8k&iLdLmRDt`BTk>)jc%u(kyi?f=yw~??}vTRP!{z3I3{tQRzbw9_%z8s>gOUao5gwBqcto8
znUCk-r1KMdjI7C6a|$i1X3(SMloBvO9kQO^%|hw%(RezEydTFMGPirtIA%wplpKtU
zm-*Pc__QXUml$7&X;(ezy4BC>_~R
z2kO#n_j_DA@`VcGfx2-gGKh_zV`t9hN=xRwe90`$9K&pCS>Ag@_YUO7Z@s7omd0!x
zmzO*B2BmfBnw=>XcS;?fO=_3keUkB?yQCoss`~5$k!g~chm4oz=H~YA$8RrA!S5|L
zSvzZ+w>`DVwzx*@1x7jTYme!?*QO8V_66<10gvT;N27f+s)kvOZ;XA2ya-2)$N@de
zwRX1NJ0Hnet_bs)u;%FcQDZ)lVeHU~rk|n9kb?mlupiG&fPGDWXpozhuwEGF=vX6-
z5%4hDW`TBKk=onBsQI;Sc_fUEGC$aakzO}@QAbkw87!mgJ;>d}HOz&ch8%6=<9bss
zeAJMU-0nu2eNODhdQdXAUBg)=Dy=kxy0UxR7zDfUA_5_lr*i=d{@MIA}8
z-Ho&PfCi&i5%#t=mCoQvHzTR8^Ta{88`K)trj4wzFmX?UzQHI@o>_>a=Xl|}Ks=1U
zBH8hLBu2SrT%iD+NRBpb%R2Pd>N!`ojBJRh
z%4|Mr#*f@*<3rbH{_$Tn6m97C^RtH*A
zTr0Fb-R46yC5dv(z@7K-0gjnbHBXwAB#B0r0m|BvqXdd67rDGC?i%!`
zr!vY0aE?VY@3{U7R=s6Wd}yuRPFVkM@w9}UC)bPFd(r8(EJY+*iFkFi
z6X1*+wISzG>D*8?36Vp;aO1W3%=~GvvavGdwSFl}OE!AQp3^Ye_)NuJ;ubO2Kooqm
z|1i7Qc+owZa;GG^gBEc@svL=W%$V~n`uy2TF590xAMZ?xY^W@SpM?ojQTsONgMKBp
zqif^Ws87t!Z0x9N&D3oPzWm1p_p8rm!wkr|_?g_jtmguEi^I&Lv#IS8s5gU+Q;g5z
z%T|2p*V*|&O)#Jvn$9tF?3U9mASZV%;D@}-%ES>j_X5k*E!NMkHQs?tp&tz6>|g%x
zoU(p5i<_nxi?KyqnWof>&%n5MM0&a&X^y22i*$<-;&5$ARH3Ox%F&HhfOFoV$8wl7
z-?Wz8y_ESOhry!!hW>~Hb6AGFqFuD_$?Tm)m(ndrPTTlW0_{SwW*?Sp+H4CLzD#Pc
zg6Nj(v-wpLA(vd3Sxr(KfcmNm`%4n$kwHN)1{|j2J+xP^1(HtMCTLuIb>RE-J|6J%
z>Hbl4E;fv!76I>VOgm-$6&-*3!q_7h+nk4<)3)SAcX_~3Xmc-v&pMA>;{HW=88BT;
zvT`;QdQL`ayMj(@TWCcc8#Qxwex8kK23qYi8u<3l!C589+12CFxf`-ttL+k)M^m3F
zcKDQ}wNIP#rr+k~nf~@mW7PSq#ecW7#!qu=Q-0Ug&$C&adfILphq)_4yX?Q+$|0Cs
zowJ_P*3^>I3Pz1@F=vyER}9f9KdxRXzwq1BlwaaA42B1lG3=6%tzB5OTM}s%6S1)Q
zcn4eS)|rjs2qXi0l*l2Xkw2;UYsSmsFV8A-%$$r`#9uQ`sb5Gm*38-2c{c5M&-|jf
zJBHD8r{@Ia61*T(gA9M1?UxbBJl{*)F1lxsT@CxUHGo
zQQg>3F<#Dip2YkmCXyN{%wN)eC1)Q>>4wj@hPnz?l^Zkl6z^2sH}7kVnd!nl((Fm%
zW`;U?A%0?umhr5
zpjpq|B+O!+X6NIbT+GJRF^jci0^7xXO~z-=`AJ?YXJ_ZuOx>2?tMil4$HjctH;gV*
zuch`a*tD^mn_B|)X0UOJ;W_6gd9B^x89;3y$M2ppb*zY3$<0r=l9VEZHTihbweYq^
z!E!`+UfP3RDHmt`0neZoY+}6G(d1K5d_|vUXdVdmZ_@XecuIs$HODeIXj(oz^6*eY
z!k=@{bCCMrmqc0;3;9=bgvWE1DN-2Fh$^9*uC;X?F)usx
zkb?tykkL}#DlzNG2@TI{ZUjojK9=`Pc0SzBTQ)rVPtTQ8J1g#3S)#=YB8Tv_eY|8FC8z~SWfr#lsJf3cAP1nT>r&2
zE02msJ|40(joH}wGzM$TM===IZPHm&Z2h=wGcjAa%rj!|m;FNB*SsP|+K-0yJpCn7
zu18cRz_>j|vwn|q(;jp^f4P?a%bUj_e2_{UJ-x`GGFxShNj@&Ze2A!_
zpt?1BAU__X6By80*NhD$hPl*7zINFVBQBrIXc^aFU_)ICNEge7
z=&e^QQ;RgsQ^5|wCeAY(agcAusW{q5X&2UcbEq&D7WCQU3^NLCX-`knC0@6a+K+v0
z_`ejF)MX50gC`Aa_74qKrra*M%W3(hIAJiiIp2^I(*K7cAM(uj#@~E`Di{@RzS{%&K<1^os8QUm*czs$DQ
z$sI-C8FN8D66eE)wMM7RWvcjBebmS3SW>jU&laeBj9UTlLERpFDd7wEi8*z-(n*^2*upiFf|369p-wwu{y->q;eQ%$
z4drgU+33TeJP*Il`#~r#kb$HZVkK=zk#cA9(ojxhRq~xsF3E!AzeBk!r>582qH;wB
zN(w{JX_t+q!BD?T&MLh!l)L3%>Aj&mPcAO^h4KQqrhN5>s+)&~w!Q$zO+$Lzq{q#A
z+)}-8YItI_dhV{_$?59lQ_~VymQu1ybKf8g}#;lV8fLxXsER246^8p5^}FS@ewCVb1vo1*gO
zsJsQWFT^XNhVe3~QIyY>T_{iDB~n$q>S_w5QM>|b1h0=8M|lF*2XN;Kl=nytrR}&r
zhBhmuQjZI9`QR!z--VwuZL*P##8$E4AHp;8VqqM+cxm
z6+a`b;#FOP_`U^4YROV2GwEhj^E59847lhj9rgbJ!q80;q