From 67eef8efa7bcf338c5fa1f4785ed39ee19247dc1 Mon Sep 17 00:00:00 2001
From: Geeves <22774890+Geevies@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:04:35 +0000
Subject: [PATCH] Smooth Progress Bar (#23093)
* Progress bars now use smooth, colour-changing fills and support visual
themes.
https://github.com/user-attachments/assets/ac493f6d-dce7-4b11-a105-985a076f4904
Ported from https://github.com/NebulaSS13/Nebula/pull/1574, which ported
from the original PR:
https://github.com/ParadiseSS13/Paradise/pull/15695
### Asset Licenses
The following assets that **have not** been created by myself are
included in this PR:
| Path | Original Author | License |
| --- | --- | --- |
|
| [dearmochi](https://github.com/dearmochi) (ParadiseSS13) | Creative
Commons 3.0 BY-SA |
AI usage disclosure: Ported using GPT 5.6 Sol.
---
code/__HELPERS/unsorted.dm | 16 +-
code/datums/progressbar.dm | 230 ++++++++++++++------
html/changelogs/geeves-progress_bar.yml | 6 +
icons/effects/progress_bar/default.dmi | Bin 0 -> 312 bytes
icons/effects/progress_bar/default_slim.dmi | Bin 0 -> 307 bytes
icons/effects/progress_bar/warning.dmi | Bin 0 -> 315 bytes
icons/effects/progress_bar/warning_slim.dmi | Bin 0 -> 304 bytes
icons/effects/progressbar.dmi | Bin 1013 -> 0 bytes
8 files changed, 173 insertions(+), 79 deletions(-)
create mode 100644 html/changelogs/geeves-progress_bar.yml
create mode 100644 icons/effects/progress_bar/default.dmi
create mode 100644 icons/effects/progress_bar/default_slim.dmi
create mode 100644 icons/effects/progress_bar/warning.dmi
create mode 100644 icons/effects/progress_bar/warning_slim.dmi
delete mode 100644 icons/effects/progressbar.dmi
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 847ea28a4e6..92fa0a10669 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -585,8 +585,9 @@ Turf and target are seperate in case you want to teleport some distance from a t
* * display_progress - Boolean, if the progress bar is shown
* * extra_checks - A `/datum/callback` that is invoked to perform extra checks and validate that the action can continue to be performed,
* if it returns `FALSE` or an algebraic equivalent the action is aborted
+ * * progressbar_type - The progress bar theme to display
*/
-/proc/do_mob(mob/user, mob/target, delay = 30, needhand = TRUE, display_progress = TRUE, datum/callback/extra_checks) //This is quite an ugly solution but i refuse to use the old request system.
+/proc/do_mob(mob/user, mob/target, delay = 30, needhand = TRUE, display_progress = TRUE, datum/callback/extra_checks, progressbar_type = /datum/progressbar/default) //This is quite an ugly solution but i refuse to use the old request system.
if(!user || !target)
stack_trace("do_mob called without either an user or a target!")
return FALSE
@@ -600,7 +601,7 @@ Turf and target are seperate in case you want to teleport some distance from a t
var/atom/loc_check = target
for(var/i = 0; !isturf(loc_check.loc) && i < 5; i++)
loc_check = target.loc
- progbar = new(user, delay, loc_check)
+ progbar = new progressbar_type(user, delay, loc_check)
var/endtime = world.time + delay
var/starttime = world.time
@@ -621,7 +622,7 @@ Turf and target are seperate in case you want to teleport some distance from a t
break
if (progbar)
- qdel(progbar)
+ progbar.end_progress()
/**
* Timed actions involving one mob user and (optionally) one target.
@@ -635,17 +636,18 @@ Turf and target are seperate in case you want to teleport some distance from a t
* * do_flags: Flags that determine what the user and target can and cannot do, defined in [mobs.dm]. Defaults to DO_DEFAULT.
* * incapacitation_flags: Incapacitation flags that determines if the user can be incapacitated. Defaults to INCAPACITATION_DEFAULT.
* * extra_checks: Optional extra checks, that uses a callback. See [datum/callback].
+ * * progressbar_type: The progress bar theme to display.
*
*/
-/proc/do_after(mob/user, delay, atom/target, do_flags = DO_DEFAULT, incapacitation_flags = INCAPACITATION_DEFAULT, datum/callback/extra_checks)
- return !do_after_detailed(user, delay, target, do_flags, incapacitation_flags, extra_checks)
+/proc/do_after(mob/user, delay, atom/target, do_flags = DO_DEFAULT, incapacitation_flags = INCAPACITATION_DEFAULT, datum/callback/extra_checks, progressbar_type = /datum/progressbar/default)
+ return !do_after_detailed(user, delay, target, do_flags, incapacitation_flags, extra_checks, progressbar_type)
/**
* See [/proc/do_after]
* Returns the exact error, defined in [mobs.dm] for custom error messages.
* Overlaps with do_flags, with some extra error messages available.
*/
-/proc/do_after_detailed(mob/user, delay, atom/target, do_flags = DO_DEFAULT, incapacitation_flags = INCAPACITATION_DEFAULT, datum/callback/extra_checks)
+/proc/do_after_detailed(mob/user, delay, atom/target, do_flags = DO_DEFAULT, incapacitation_flags = INCAPACITATION_DEFAULT, datum/callback/extra_checks, progressbar_type = /datum/progressbar/default)
if(!delay)
return FALSE
@@ -686,7 +688,7 @@ Turf and target are seperate in case you want to teleport some distance from a t
var/datum/progressbar/progbar
if ((do_flags & DO_SHOW_PROGRESS) && user.client && (user.client.prefs.toggles_secondary & PROGRESS_BARS))
var/progbar_pos = (do_flags & DO_PLACE_PROGRESSBAR_ON_USER) ? user : (target || user)
- progbar = new(user, delay, progbar_pos)
+ progbar = new progressbar_type(user, delay, progbar_pos)
SEND_SIGNAL(user, COMSIG_DO_AFTER_BEGAN)
diff --git a/code/datums/progressbar.dm b/code/datums/progressbar.dm
index 4ba6b03362d..70842f81dea 100644
--- a/code/datums/progressbar.dm
+++ b/code/datums/progressbar.dm
@@ -1,25 +1,45 @@
-#define PROGRESSBAR_HEIGHT 6
-#define PROGRESSBAR_ANIMATION_TIME 5
-
/datum/progressbar
- ///The progress bar visual element.
+ /// The icon file containing the backdrop, mask, and fill states for this theme.
+ var/icon = 'icons/effects/progress_bar/default.dmi'
+ /// The total height in pixels of the themed progress bar.
+ var/height = 7
+ /// The width in pixels of the theme's fill sprite.
+ var/fill_width = 22
+ /// The time taken for the progress bar to appear.
+ var/animation_time_appear = 0.5 SECONDS
+ /// The time taken for the progress bar to fade away.
+ var/animation_time_fade = 0.5 SECONDS
+ /// The time taken for stacked progress bars to shift down.
+ var/animation_time_shift = 0.5 SECONDS
+
+ /// The visual element containing the backdrop and fill.
var/image/bar
- ///The target where this progress bar is applied and where it is shown.
+ /// The backdrop visual element.
+ var/image/backdrop
+ /// The fill visual element.
+ var/image/fill
+ /// The target where this progress bar is applied and where it is shown.
var/atom/bar_loc
- ///The mob whose client sees the progress bar.
+ /// The mob whose client sees the progress bar.
var/mob/user
- ///The client seeing the progress bar.
+ /// The client seeing the progress bar.
var/client/user_client
- ///Effectively the number of steps the progress bar will need to do before reaching completion.
+ /// The number of steps the progress bar needs to reach completion.
var/goal = 1
- ///Control check to see if the progress was interrupted before reaching its goal.
+ /// The most recently displayed progress value.
var/last_progress = 0
- ///Variable to ensure smooth visual stacking on multiple progress bars.
+ /// The progress bar's position in the stack over its target.
var/listindex = 0
- ///The type of our last value for bar_loc, for debugging
+ /// The type of our last value for bar_loc, for debugging.
var/location_type
- ///Where to draw the progress bar above the icon
+ /// Horizontal offset needed to centre the bar over oversized icons.
+ var/offset_x
+ /// Vertical offset needed to place the bar over oversized icons.
var/offset_y
+ /// Whether the bar has started its ending animation.
+ var/stopping = FALSE
+ /// Whether this bar has already been removed from the user's stack.
+ var/removed_from_stack = FALSE
/datum/progressbar/New(mob/User, goal_number, atom/target)
. = ..()
@@ -27,33 +47,30 @@
stack_trace("Invalid target [target] passed in")
qdel(src)
return
- if(QDELETED(User) || !istype(User))
+ if (QDELETED(User) || !istype(User))
stack_trace("/datum/progressbar created with [isnull(User) ? "null" : "invalid"] user")
qdel(src)
return
- if(!isnum(goal_number))
+ if (!isnum(goal_number))
stack_trace("/datum/progressbar created with [isnull(User) ? "null" : "invalid"] goal_number")
qdel(src)
return
+
goal = goal_number
bar_loc = target
location_type = bar_loc.type
+ user = User
var/list/icon_offsets = target.get_oversized_icon_offsets()
- var/offset_x = icon_offsets["x"]
+ offset_x = icon_offsets["x"]
offset_y = icon_offsets["y"]
-
- bar = image('icons/effects/progressbar.dmi', bar_loc, "prog_bar_0", pixel_x = offset_x)
- // SET_PLANE_EXPLICIT(bar, ABOVE_HUD_PLANE, User)
- bar.plane = HUD_PLANE
- bar.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
- user = User
+ init_images()
LAZYADDASSOCLIST(user.progressbars, bar_loc, src)
var/list/bars = user.progressbars[bar_loc]
- listindex = bars.len
+ listindex = length(bars)
- if(user.client)
+ if (user.client)
user_client = user.client
add_prog_bar_image_to_client()
@@ -62,111 +79,180 @@
RegisterSignal(user, COMSIG_MOB_LOGIN, PROC_REF(on_user_login))
RegisterSignal(bar_loc, COMSIG_QDELETING, PROC_REF(on_bar_loc_delete))
-
/datum/progressbar/Destroy()
- if(user)
+ if (user)
UnregisterSignal(user, list(COMSIG_QDELETING, COMSIG_MOB_LOGOUT, COMSIG_MOB_LOGIN))
- if(isliving(user))
- var/mob/living/L = user
- if(L.stamina_bar == src)
- L.stamina_bar = null
+ if (isliving(user))
+ var/mob/living/living_user = user
+ if (living_user.stamina_bar == src)
+ living_user.stamina_bar = null
- for(var/pb in user.progressbars[bar_loc])
- var/datum/progressbar/progress_bar = pb
- if(progress_bar == src || progress_bar.listindex <= listindex)
- continue
- progress_bar.listindex--
-
- progress_bar.bar.pixel_y = world.icon_size + offset_y + (PROGRESSBAR_HEIGHT * (progress_bar.listindex - 1))
- var/dist_to_travel = world.icon_size + offset_y + (PROGRESSBAR_HEIGHT * (progress_bar.listindex - 1)) - PROGRESSBAR_HEIGHT
- animate(progress_bar.bar, pixel_y = dist_to_travel, time = PROGRESSBAR_ANIMATION_TIME, easing = SINE_EASING)
-
- LAZYREMOVEASSOC(user.progressbars, bar_loc, src)
+ remove_from_stack()
user = null
- if(user_client)
+ if (user_client)
clean_user_client()
- if(bar_loc)
+ if (bar_loc)
UnregisterSignal(bar_loc, COMSIG_QDELETING)
bar_loc = null
+
bar = null
+ backdrop = null
+ fill = null
return ..()
+/// Initializes the holder, backdrop, and masked fill images used by the theme.
+/datum/progressbar/proc/init_images()
+ bar = image('icons/effects/effects.dmi', bar_loc, "nothing", HUD_ABOVE_ITEM_LAYER, pixel_x = offset_x)
+ bar.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ bar.alpha = 0
+ bar.plane = HUD_PLANE
+ bar.appearance_flags = KEEP_TOGETHER | APPEARANCE_UI_IGNORE_ALPHA
-///Called right before the user's Destroy()
+ backdrop = image(icon, bar_loc, "backdrop")
+ fill = image(icon, bar_loc, "fill")
+ fill.filters = filter(type = "alpha", icon = icon(icon, "mask"), x = -fill_width)
+ bar.overlays += list(backdrop, fill)
+
+/// Removes this bar from its stack and smoothly shifts any bars above it down.
+/datum/progressbar/proc/remove_from_stack()
+ if (removed_from_stack || !user || !bar_loc)
+ return
+
+ var/list/bars = user.progressbars?[bar_loc]
+ if (bars)
+ for (var/pb in bars)
+ var/datum/progressbar/progress_bar = pb
+ if (progress_bar == src || progress_bar.listindex <= listindex)
+ continue
+ progress_bar.listindex--
+ progress_bar.shift_down()
+ LAZYREMOVEASSOC(user.progressbars, bar_loc, src)
+
+ removed_from_stack = TRUE
+
+/// Shifts this progress bar down one position in its stack.
+/datum/progressbar/proc/shift_down()
+ animate(bar,
+ pixel_y = world.icon_size + offset_y + (height * (listindex - 1)),
+ time = animation_time_shift,
+ easing = SINE_EASING | EASE_OUT,
+ flags = ANIMATION_PARALLEL
+ )
+
+/// Called right before the user's Destroy().
/datum/progressbar/proc/on_user_delete(datum/source)
SIGNAL_HANDLER
- user.progressbars = null //We can simply nuke the list and stop worrying about updating other prog bars if the user itself is gone.
+ user.progressbars = null
+ removed_from_stack = TRUE
user = null
qdel(src)
-
-///Called right before the bar_loc's Destroy()
+/// Called right before the bar_loc's Destroy().
/datum/progressbar/proc/on_bar_loc_delete(datum/source)
SIGNAL_HANDLER
qdel(src)
-///Removes the progress bar image from the user_client and nulls the variable, if it exists.
+/// Removes the progress bar image from the current client.
/datum/progressbar/proc/clean_user_client(datum/source)
SIGNAL_HANDLER
- if(!user_client) //Disconnected, already gone.
+ if (!user_client)
return
user_client.images -= bar
user_client = null
-
-///Called by user's Login(), it transfers the progress bar image to the new client.
+/// Transfers the progress bar image to the user's new client after login.
/datum/progressbar/proc/on_user_login(datum/source)
SIGNAL_HANDLER
- if(user_client)
- if(user_client == user.client) //If this was not client handling I'd condemn this sanity check. But clients are fickle things.
+ if (user_client)
+ if (user_client == user.client)
return
clean_user_client()
- if(!user.client) //Clients can vanish at any time, the bastards.
+ if (!user.client)
return
user_client = user.client
add_prog_bar_image_to_client()
-
-///Adds a smoothly-appearing progress bar image to the player's screen.
+/// Adds a smoothly appearing progress bar image to the player's client.
/datum/progressbar/proc/add_prog_bar_image_to_client()
bar.pixel_y = 0
bar.alpha = 0
user_client.images += bar
- animate(bar, pixel_y = world.icon_size + offset_y + (PROGRESSBAR_HEIGHT * (listindex - 1)), alpha = 255, time = PROGRESSBAR_ANIMATION_TIME, easing = SINE_EASING)
+ animate(bar,
+ pixel_y = world.icon_size + offset_y + (height * (listindex - 1)),
+ alpha = 255,
+ time = animation_time_appear,
+ easing = SINE_EASING | EASE_OUT
+ )
-
-///Updates the progress bar image visually.
+/// Updates the progress bar's masked fill.
/datum/progressbar/proc/update(progress)
+ if (QDELETED(bar_loc))
+ qdel(src)
+ return
+
progress = clamp(progress, 0, goal)
- if(progress == last_progress)
+ if (progress == last_progress)
return
last_progress = progress
- bar.icon_state = "prog_bar_[round(((progress / goal) * 100), 5)]"
+ refresh_fill()
+/// Removes and re-adds the fill so its filtered appearance updates immediately.
+/datum/progressbar/proc/refresh_fill()
+ if (!fill || !bar)
+ return
+ bar.overlays -= fill
+ update_fill(clamp(last_progress / goal, 0, 1))
+ bar.overlays += fill
-///Called on progress end, be it successful or a failure. Wraps up things to delete the datum and bar.
+/// Updates the fill image for the current theme.
+/datum/progressbar/proc/update_fill(fraction)
+ UNLINT(fill?.filters[1]?.x = -fill_width * (1 - fraction))
+ if (!stopping)
+ fill.color = rgb_gradient(fraction, 0, "#cc0033", 0.25, "#cc6633", 0.5, "#d1cc33", 0.75, "#00cc33")
+ else if (last_progress == goal)
+ fill.color = COLOR_YELLOW
+
+/// Ends the progress bar and schedules its deletion after the fade animation.
/datum/progressbar/proc/end_progress()
- if(last_progress != goal)
- bar.icon_state = "[bar.icon_state]_fail"
+ if (stopping)
+ return
+ stopping = TRUE
+ refresh_fill()
+ remove_from_stack()
+ animate(bar, alpha = 0, time = animation_time_fade, flags = ANIMATION_PARALLEL)
+ QDEL_IN(src, animation_time_fade)
- animate(bar, alpha = 0, time = PROGRESSBAR_ANIMATION_TIME)
-
- QDEL_IN(src, PROGRESSBAR_ANIMATION_TIME)
-
-///Progress bars are very generic, and what hangs a ref to them depends heavily on the context in which they're used
-///So let's make hunting harddels easier yeah?
+/// Progress bars are generic, so record the target type to make harddel debugging easier.
/datum/progressbar/dump_harddel_info()
- if(harddel_deets_dumped)
+ if (harddel_deets_dumped)
return
harddel_deets_dumped = TRUE
return "Owner's type: [location_type]"
-#undef PROGRESSBAR_ANIMATION_TIME
-#undef PROGRESSBAR_HEIGHT
+/datum/progressbar/default
+
+/datum/progressbar/default/slim
+ icon = 'icons/effects/progress_bar/default_slim.dmi'
+ height = 5
+
+/// A warning theme that pulses between yellow and red after its halfway point.
+/datum/progressbar/warning
+ icon = 'icons/effects/progress_bar/warning.dmi'
+
+/datum/progressbar/warning/update_fill(fraction)
+ . = ..()
+ if (fraction <= 0.5)
+ fill.color = COLOR_YELLOW
+ else
+ fill.color = rgb_gradient(fraction * 10, 0.5, COLOR_YELLOW, 0.5, COLOR_RED, "loop")
+
+/datum/progressbar/warning/slim
+ icon = 'icons/effects/progress_bar/warning_slim.dmi'
+ height = 5
diff --git a/html/changelogs/geeves-progress_bar.yml b/html/changelogs/geeves-progress_bar.yml
new file mode 100644
index 00000000000..97ed45da856
--- /dev/null
+++ b/html/changelogs/geeves-progress_bar.yml
@@ -0,0 +1,6 @@
+author: Geeves
+
+delete-after: True
+
+changes:
+ - rscadd: "Progress bars now use smooth, colour-changing fills and support visual themes."
diff --git a/icons/effects/progress_bar/default.dmi b/icons/effects/progress_bar/default.dmi
new file mode 100644
index 0000000000000000000000000000000000000000..45d9865068034b895ad4a83e22f0ec337e6c60e3
GIT binary patch
literal 312
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|ei2$Dv*8>L*2nmVk>6n9;N!wQN@
zzkdlX`1tjSmbb3fxija3H-s8oG=A_%=e&>RNrs}H-W?XkLB^Mjy%gp=nsj7Rh(fTY
zrlFCy
literal 0
HcmV?d00001
diff --git a/icons/effects/progress_bar/default_slim.dmi b/icons/effects/progress_bar/default_slim.dmi
new file mode 100644
index 0000000000000000000000000000000000000000..9db0f7f7de85ac38b4dacba77d5cd3766c3d456f
GIT binary patch
literal 307
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|e(Ey(i*8>L*2nmVk>6rZg|KH5i
z8pxeCZCcH@=l6jM7)yfuf*Bm1-ADs+s;WXFN?cNllZ!G7N;32F7#J$%1cwzAm45#c
zT=4Ph6D@CDt#fD22X6>9xM=*~k!lj|9Xh*>RX4AY&`3FPbY|F;8MC<(4~sri6H6BknKEtWF%APdOcL<~hvc{K3eO#3X*aV!g@+pot8gu6{1-oD!ML*2nmVk=`j5N{~yQ(
z3Ky{D0!aqOk|4ie28U-i(tw<*s*s2hm(=3qqRfJl%=|nChKf1CVFg8{-@gPGeEj-E
z%Uf6L+?n&i8$t~(8b5fXbKXbuBtub8?+y#&Amhu%UJ7#_O*%3uL?KvH)6mj-sm6YX
z&TeDX%_}4{QcfJ588&6cY_7z^qR-UC(uG5&Oq)5m*?Z!qrq%PgZ}c);w9tQGYbUP^
zbVZ=2i(`mKXL5oB>*55F3Ct`L8Nyhdyb2a@JTjI(+<0(7pu+`mpiB=_6vMTI%K?uA
z3zopr
E06Xn&r~m)}
literal 0
HcmV?d00001
diff --git a/icons/effects/progress_bar/warning_slim.dmi b/icons/effects/progress_bar/warning_slim.dmi
new file mode 100644
index 0000000000000000000000000000000000000000..e7fd0aa1bbf9f520a1adc23371a4d11c5a4baa25
GIT binary patch
literal 304
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0J3?w7mbKU|ep#Yx{*8>L*2nmVk=`j5N{~yQ(
z3Ky{D0!aqOk|4ie28U-i(tw<*s*s2hm(=3qqRfJl%=|nChKf1CVFg8{-@gPGeEj-E
z%Uf6L+?n&i8$t~(8b5fXbKXbuBtub8?+y#&Amhu%UJ7#_O*%3uL?KvH)6mj-sm6YX
z&TeDX%_}4{QcfJ588&6cY_7z^qR-UC(uG5&Oq)5m*?Z!qrq%PgZ}c);w9tQGYbUP^
zbcLIzi(`mKXL5oB>*53v9v%S~rU+3E&4Z0f9p`g+*x5pY94_zzWqO(pa?bE|?saZH
t*uq%G8W5=<*bvI7eTO0V4c7-|hEI1yf2m#1w+5QX;OXk;vd$@?2>>DcYXJZN
literal 0
HcmV?d00001
diff --git a/icons/effects/progressbar.dmi b/icons/effects/progressbar.dmi
deleted file mode 100644
index f055a07ba1492a36912ed6b9fdbf1b8e278e4864..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1013
zcmV{hG&VIm%m6bYAt(O;Gt6c)|CuuY%rgN0Gc#uYW|{wws`2W@R{!XX_Jw>-00001
zbW%=J06^y0W&i*H`+8JZbVOxyV{&P5bZKvH004NLt=78=!Y~vD;JJBzP}a&<)8%MEhWm?j3)Zra)=6xBl?EerTz7
z2YY{}C!ZDnI3odCOJc@_05D+)6(Cd$p#p@8A=H3SGlUutYKG7NLc!i*uz0b$M%=72C~2n#@1FoXplEEvKP5S9#K2?$Hhi~pB6mdowp
z_0xF(AJ~Na{wENT0007PNklv0t6x9nR}0$u{oB{)U_tBr^7Wp8-tS=Ve1xxX
z-y8q{0001B9=)5H{`uJ5-8Y;M*c?^b)l|-F?d&Gl;Cg;4=6BBDl=2j8a8{3Rk1G9o
zF6Xs=ej99XJwF%oJLhjpc@8#MJOQU0F!(#_e897F000000DyV)Zf5%DV|RB0{QrPW
zP`^g)YbxY-&)>v(3N~n&zdH%r;k*0yZ6VJ=0slz9|8;|Kevb3n{QXIMK4A7IVDNX;
z`G9BV00000008so-OTjQ$L{XF;e0>}>emO`*IksSV1q~J?<(_C^Zc{^J+JNG_fejM
z4IZ7puguTQ^F0B*-@)Gb2w&m8IRF3v006)|dN(uu^Rc_TZ#W;Y3+mTm`9QR@@89>8{ynek_dho%&(F>C-!4?<=d1Gpvp)fYzoX6vJUa&f00000
zm`Cqsrhh(mcQ?TA3tYCZhf4dJR<^qz8{C|q*3Lh){l36u|Nd6#-}B0T|6_xj^YhyI
zZ*9LX@Y|n&!QWBm1D>4&000000L)|d=H>a_+u)-1H8r)b{{qWTx&hbydv5CA{|A25
j4Ve82nEe|t^Iw5?ZsBHuRElq(00000NkvXXu0mjf6jA0r