From 3043bf536d4a58912e637c4c8a7e8c8938f565c7 Mon Sep 17 00:00:00 2001 From: Kano <89972582+kano-dot@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:03:01 +0300 Subject: [PATCH] Adds whiteboards and gives a new UI to barsigns and its subtypes (#22133) ## About PR Sprites made by @KingOfThePing! Adds whiteboards to following areas: - CE and RD office (portable) - Expedition EVA storage (portable) - Intrepid flight deck (portable) - Xenoarcheology presentation room - Engineering breakroom - Kitchen (as an alternative to holo-menu) Whiteboards are also available in cargo console in the recreation tab As an addition whiteboards can be mapped in with preset overlays and written contents This PR also changes sign selection list menu for barsigns and its subtypes with a tgui image menu ## Images image
video sample

https://github.com/user-attachments/assets/24de419c-e155-4623-9d08-0ff6c0b7d228

### Asset Licenses The following assets that **have not** been created by myself are included in this PR: | Path | Original Author | License | | --- | --- | --- | | icons/obj/whiteboard.dmi & icons/obj/whiteboard_portable.dmi | KingOfThePing | CC BY-SA 3.0 | | sound/effects/whiteboard_scribble_1.ogg, sound/effects/whiteboard_scribble_2.ogg, sound/effects/whiteboard_scribble_3.ogg | freesound_community (https://pixabay.com/sound-effects/film-special-effects-marker-whiteboard-squeak03-83059/) | Pixabay Licence | --- aurorastation.dme | 1 + code/__DEFINES/sound.dm | 1 + code/game/objects/structures/barsign.dm | 115 +++++-- code/game/objects/structures/whiteboard.dm | 293 ++++++++++++++++++ code/game/sound/sound_keys/sound_keys.dm | 8 + code/modules/cargo/items/recreation.dm | 28 ++ html/changelogs/kano-dot-whiteboards.yml | 5 + icons/obj/whiteboard.dmi | Bin 0 -> 4723 bytes icons/obj/whiteboard_portable.dmi | Bin 0 -> 615 bytes maps/sccv_horizon/sccv_horizon.dmm | 95 ++++-- sound/effects/whiteboard_scribble_1.ogg | Bin 0 -> 23514 bytes sound/effects/whiteboard_scribble_2.ogg | Bin 0 -> 23712 bytes sound/effects/whiteboard_scribble_3.ogg | Bin 0 -> 51564 bytes .../tgui/interfaces/OverlayChoice.tsx | 115 +++++++ 14 files changed, 605 insertions(+), 56 deletions(-) create mode 100644 code/game/objects/structures/whiteboard.dm create mode 100644 html/changelogs/kano-dot-whiteboards.yml create mode 100644 icons/obj/whiteboard.dmi create mode 100644 icons/obj/whiteboard_portable.dmi create mode 100644 sound/effects/whiteboard_scribble_1.ogg create mode 100644 sound/effects/whiteboard_scribble_2.ogg create mode 100644 sound/effects/whiteboard_scribble_3.ogg create mode 100644 tgui/packages/tgui/interfaces/OverlayChoice.tsx diff --git a/aurorastation.dme b/aurorastation.dme index a8f148a0ceb..36a4ac3d44a 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -1496,6 +1496,7 @@ #include "code\game\objects\structures\watercloset.dm" #include "code\game\objects\structures\weapon_rack.dm" #include "code\game\objects\structures\weapons_rack.dm" +#include "code\game\objects\structures\whiteboard.dm" #include "code\game\objects\structures\windoor_assembly.dm" #include "code\game\objects\structures\window.dm" #include "code\game\objects\structures\barricades\_barricade.dm" diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm index 33692f821ef..07eaff88630 100644 --- a/code/__DEFINES/sound.dm +++ b/code/__DEFINES/sound.dm @@ -183,3 +183,4 @@ #define SFX_TRAY_HIT "tray_hit" #define SFX_WIELD "wield" #define SFX_PLASTIC_DRY_FIRE "plastic_dry_fire" +#define SFX_WHITEBOARD_SCRIBBLE "whiteboard_scribble" diff --git a/code/game/objects/structures/barsign.dm b/code/game/objects/structures/barsign.dm index de0f8a0c465..06cb54b0e3e 100644 --- a/code/game/objects/structures/barsign.dm +++ b/code/game/objects/structures/barsign.dm @@ -1,3 +1,8 @@ +/// Lists of data passed over to tgui, obtained from numerous singletons. List contains lists of: "display_name", "icon", "icon_name", "singleton_path" +GLOBAL_LIST_EMPTY(barsign_overlay_cache) +GLOBAL_LIST_EMPTY(kitchensign_overlay_cache) +GLOBAL_LIST_EMPTY(marketsign_overlay_cache) + /obj/structure/sign/double/barsign icon = 'icons/obj/barsigns.dmi' icon_state = "off" @@ -9,21 +14,86 @@ var/currently_on = FALSE var/on_icon_state var/off_icon_state = "off" + /// Local reference of the global list associated with this object's singletons. + var/list/target_cache = list() + +/obj/structure/sign/double/barsign/mechanics_hints(mob/user, distance, is_adjacent) + . += ..() + . += "You can Alt Click to toggle power of \the [src]." + +/obj/structure/sign/double/barsign/Initialize() + . = ..() + set_overlay_cache() + +/obj/structure/sign/double/barsign/proc/get_target_cache() + return GLOB.barsign_overlay_cache + +/obj/structure/sign/double/barsign/proc/set_overlay_cache() + target_cache = get_target_cache() + + if(!length(target_cache)) + var/list/singleton_instances = GET_SINGLETON_SUBTYPE_MAP(choice_types) + for(var/i in singleton_instances) + var/singleton/sign/double/bar/S = GET_SINGLETON(i) + var/overlay_image = image(icon = icon, icon_state = S.icon_state) + + target_cache.Add(list(list( + "display_name" = S.name, + "icon" = icon2base64(getFlatIcon(overlay_image, no_anim = TRUE)), // expensive but we only do it once + "icon_name" = S.icon_state, + "singleton_path" = "[S.type]" + ))) /obj/structure/sign/double/barsign/attackby(obj/item/attacking_item, mob/user) if(cult) return ..() var/obj/item/card/id/card = attacking_item.GetID() if(istype(card)) - if(check_access(card)) - set_sign() - to_chat(user, SPAN_NOTICE("You change the sign.")) - else + if(!check_access(card)) to_chat(user, SPAN_WARNING("Access denied.")) - return + return + ui_interact(user) return ..() +/obj/structure/sign/double/barsign/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "OverlayChoice", capitalize_first_letters(name)) + ui.open() + +/obj/structure/sign/double/barsign/ui_static_data(mob/user) + var/list/data = list() + data["contents"] = target_cache + return data + +/obj/structure/sign/double/barsign/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + var/mob/user = ui.user + add_fingerprint(user) + switch(action) + if("select_overlay") + var/choice = params["singleton_path"] + INVOKE_ASYNC(src, PROC_REF(set_sign), choice) + + SStgui.close_uis(src) + + . = TRUE + +/obj/structure/sign/double/barsign/proc/set_sign(choice) + var/singleton/sign/double/chosen_singleton = text2path(choice) + + name = chosen_singleton.name + desc = chosen_singleton.desc + desc_extended = chosen_singleton.desc_extended + icon_state = chosen_singleton.icon_state + currently_on = TRUE + on_icon_state = icon_state + update_icon() + /obj/structure/sign/double/barsign/AltClick(mob/user) // Alt-click a sign with an empty hand to power or depower it, if it has the associated "[name]-off" state in the .dmi file. if(!on_icon_state) to_chat(user, SPAN_WARNING("The sign is already off!")) @@ -35,30 +105,7 @@ icon_state = off_icon_state currently_on = FALSE -/obj/structure/sign/double/barsign/proc/get_sign_choices() - var/list/sign_choices = GET_SINGLETON_SUBTYPE_MAP(choice_types) - return sign_choices - -/obj/structure/sign/double/barsign/proc/set_sign() - var/list/sign_choices = get_sign_choices() - - var/list/sign_index = list() - for(var/sign in sign_choices) - var/singleton/sign/double/B = GET_SINGLETON(sign) - sign_index["[B.name]"] = B - - var/sign_choice = tgui_input_list(usr, "What should the sign be changed to?", "Bar Sign", sign_index) - if(!sign_choice) - return - var/singleton/sign/double/signselect = sign_index[sign_choice] - - name = signselect.name - desc = signselect.desc - desc_extended = signselect.desc_extended - icon_state = signselect.icon_state - currently_on = TRUE - on_icon_state = icon_state - update_icon() +// ---- Kitchen sign /obj/structure/sign/double/barsign/kitchensign icon = 'icons/obj/kitchensigns.dmi' @@ -66,9 +113,14 @@ req_access = list(ACCESS_GALLEY) choice_types = /singleton/sign/double/kitchen +/obj/structure/sign/double/barsign/kitchensign/get_target_cache() + return GLOB.kitchensign_overlay_cache + /obj/structure/sign/double/barsign/kitchensign/mirrored // Visible from the other end of the sign. pixel_x = -32 +// ---- Market sign + /obj/structure/sign/double/barsign/marketsign icon = 'icons/obj/marketsigns.dmi' icon_state = "off" @@ -76,9 +128,8 @@ req_one_access = list(ACCESS_CARGO, ACCESS_JANITOR, ACCESS_ROBOTICS, ACCESS_MINING, ACCESS_PARAMEDIC, ACCESS_HYDROPONICS, ACCESS_GALLEY, ACCESS_LIBRARY) choice_types = /singleton/sign/double/market -/obj/structure/sign/double/barsign/marketsign/set_sign() - . = ..() - off_icon_state = "[icon_state]-off" +/obj/structure/sign/double/barsign/marketsign/get_target_cache() + return GLOB.marketsign_overlay_cache /obj/structure/sign/double/barsign/marketsign/mirrored // Visible from the other end of the sign. pixel_x = -32 diff --git a/code/game/objects/structures/whiteboard.dm b/code/game/objects/structures/whiteboard.dm new file mode 100644 index 00000000000..ec92486fa7a --- /dev/null +++ b/code/game/objects/structures/whiteboard.dm @@ -0,0 +1,293 @@ +/// List of data passed over to tgui, obtained from whiteboard singletons. List contains lists of: "display_name", "icon", "icon_name", "singleton_path" +GLOBAL_LIST_EMPTY(whiteboard_overlay_cache) + +// ---- large whiteboard, 64x32 +/obj/structure/whiteboard + name = "whiteboard" + desc = "A whiteboard in classic design. Often associated with its sibling glassboard, perfect material to draw incomprehensible equations." + icon = 'icons/obj/whiteboard.dmi' + icon_state = "whiteboard" + layer = OBJ_LAYER + /// Singleton path to whiteboard overlays. + var/overlay_choices_singleton = /singleton/whiteboard + /// Written things on the whiteboard. + var/written_contents + /// Is someone currently drawing things on the whiteboard? + var/busy = FALSE + /// Used to prevent pixel-shift mechanic spam. + var/currently_pixel_shifting = FALSE + /// For mapping purposes. Define this as one of the overlay icon_state's name (e.g. overlay_1). + var/preset_overlay + +/obj/structure/whiteboard/mechanics_hints(mob/user, distance, is_adjacent) + . += ..() + . += "You can write or edit things on [src] with a pen." + . += "You can ALT Click to pixel-shift [src] to the wall. Only works for north direction when wheel brakes are locked!" + . += "You can engage/release wheel brakes with the help of a wrench." + +/obj/structure/whiteboard/examine(mob/user, distance, is_adjacent, infix, suffix, show_extended) + . = ..() + if(written_contents) + to_chat(user, EXAMINE_BLOCK("It reads: \"[written_contents]\".")) + +/obj/structure/whiteboard/Initialize() + . = ..() + if(!length(GLOB.whiteboard_overlay_cache)) + var/list/singleton_instances = GET_SINGLETON_SUBTYPE_MAP(overlay_choices_singleton) + for(var/i in singleton_instances) + var/singleton/whiteboard/S = GET_SINGLETON(i) + var/overlay_image = image(icon = icon, icon_state = S.icon_state) + + GLOB.whiteboard_overlay_cache.Add(list(list( + "display_name" = S.name, + "icon" = icon2base64(getFlatIcon(overlay_image)), // expensive but we only do it once + "icon_name" = S.icon_state, + "singleton_path" = "[S.type]" + ))) + + if(preset_overlay) + AddOverlays(preset_overlay) + +/obj/structure/whiteboard/Move() + . = ..() + if(has_gravity()) + playsound(src, 'sound/effects/roll.ogg', 50, 1) + +/obj/structure/whiteboard/AltClick(mob/user) + if(!anchored) + to_chat(user, SPAN_WARNING("You need the wheels locked first in order to secure [src] to a wall!")) + return + + if(currently_pixel_shifting) + return + + currently_pixel_shifting = TRUE + if(!do_after(user, 0.5 SECOND)) + currently_pixel_shifting = FALSE + return + currently_pixel_shifting = FALSE + animate(src, pixel_y = pixel_y == 0 ? 12 : 0, time = 0.2 SECOND) + balloon_alert(user, "shifted") + +/obj/structure/whiteboard/attackby(obj/item/attacking_item, mob/user) + if(busy) + to_chat(user, SPAN_WARNING("Someone is already drawing things on this board!")) + return + + var/obj/item/pen/P = attacking_item + if(istype(P)) + to_chat(user, SPAN_NOTICE("You start contemplating what to draw.")) + handle_drawing(user) + + else if(attacking_item.tool_behaviour == TOOL_WRENCH) + animate(src, pixel_y = 0, time = 0.2 SECOND) // if we were pixel-shifted, return to our original state + anchored = !anchored + attacking_item.play_tool_sound(get_turf(src), 75) + balloon_alert(user, "[anchored ? "secure" : "unsecure"]") + user.visible_message("[user] [anchored ? "locks" : "unlocks"] [src]'s wheels.", \ + "You [anchored ? "locked" : "unlocked"] [src]'s wheels.", \ + "You hear a click.") + + else + to_chat(user, SPAN_WARNING("This is the wrong tool to manifest your inland empire, maybe a pen would be more fitting?")) + +/obj/structure/whiteboard/proc/handle_drawing(mob/user) + busy = TRUE + // Text input + var/new_written_contents = tgui_input_text(user, "Write your text here", "Whiteboard Contents", default = written_contents, multiline = TRUE) + if(new_written_contents == written_contents) // if untouched, cancel it + to_chat(user, SPAN_WARNING("You withdraw your pen in thought, [src] remains untouched.")) + busy = FALSE + return + + if(!new_written_contents) //if empty, erase the board + to_chat(user, SPAN_NOTICE("You erase the contents on [src].")) + ClearOverlays() + desc = initial(desc) + written_contents = null + busy = FALSE + return + + written_contents = new_written_contents + ui_interact(user) + +/obj/structure/whiteboard/proc/finalize_drawing(mob/user, choice) + playsound(get_turf(src), SFX_WHITEBOARD_SCRIBBLE, 60, FALSE) + if(do_after(user, 2 SECONDS)) + update_icon(choice) + else + ClearOverlays() + written_contents = null + +/obj/structure/whiteboard/update_icon(choice) + ClearOverlays() + AddOverlays(choice) + +// ---- UI code + +/obj/structure/whiteboard/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "OverlayChoice", capitalize_first_letters(name)) + ui.open() + +/obj/structure/whiteboard/ui_static_data(mob/user) + var/list/data = list() + data["contents"] = GLOB.whiteboard_overlay_cache + return data + +/obj/structure/whiteboard/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + var/mob/user = ui.user + add_fingerprint(user) + switch(action) + if("select_overlay") + var/choice = params["choice"] + INVOKE_ASYNC(src, PROC_REF(finalize_drawing), user, choice) + busy = FALSE + + SStgui.close_uis(src) + + . = TRUE + +/obj/structure/whiteboard/ui_close(mob/user, datum/tgui/ui) + . = ..() + + if(busy) // if the ui was closed while we were still busy, it means no overlay were chosen + busy = FALSE + written_contents = null + ClearOverlays() + to_chat(user, SPAN_WARNING("You withdraw your pen in thought, [src] remains untouched.")) + +// ---- Portable whiteboard, 32x32 +/obj/item/portable_whiteboard + name = "portable whiteboard" + desc = "A compact whiteboard folded into a portable frame, ready to be deployed on any unexpected seminar!" + icon = 'icons/obj/whiteboard_portable.dmi' + icon_state = "whiteboard_case" + item_state = "whiteboard_case" + contained_sprite = TRUE + force = 18 + throw_speed = 1 + throw_range = 4 + w_class = WEIGHT_CLASS_BULKY + attack_verb = list("attacked", "bashed", "battered", "bludgeoned", "whacked") + drop_sound = 'sound/items/drop/toolbox.ogg' + pickup_sound = 'sound/items/pickup/toolbox.ogg' + /// Used to keep track of folding process to prevent spams. + var/currently_unfolding = FALSE + +/obj/item/portable_whiteboard/mechanics_hints(mob/user, distance, is_adjacent) + . += ..() + . += "You can ALT Click to deploy [src]. It must be on the floor first!" + +/obj/item/portable_whiteboard/AltClick(mob/user) + if(loc == user) + to_chat(user, SPAN_WARNING("[src] needs to be standing on the floor in order to be unfolded!")) + return + + if(currently_unfolding) + return + + currently_unfolding = TRUE + if(!do_after(user, 3 SECONDS)) + currently_unfolding = FALSE + return + + new /obj/structure/whiteboard/portable(get_turf(src)) + playsound(get_turf(src), 'sound/items/storage/briefcase.ogg', 50) + qdel(src) + +/obj/structure/whiteboard/portable + name = "portable whiteboard" + desc = "A compact whiteboard folded into a portable frame, ready to be deployed on any unexpected seminar!" + icon = 'icons/obj/whiteboard_portable.dmi' + anchored = FALSE + /// Used to keep track of folding process to prevent spams. + var/currently_folding = FALSE + +/obj/structure/whiteboard/portable/AltClick(mob/user) + if(currently_folding) + return + + currently_folding = TRUE + if(!do_after(user, 3 SECONDS)) + currently_folding = FALSE + return + + new /obj/item/portable_whiteboard(get_turf(src)) + playsound(get_turf(src), 'sound/items/storage/briefcase.ogg', 50) + qdel(src) + +/obj/structure/whiteboard/portable/update_icon(choice) + ClearOverlays() + var/image/overlay_image = image(icon = 'icons/obj/whiteboard.dmi', icon_state = choice) + overlay_image.transform *= 0.5 + overlay_image.pixel_x = -16 + overlay_image.pixel_y = 2 + AddOverlays(overlay_image) + +// ---- Whiteboard overlay singletons + +/singleton/whiteboard + var/name = "placeholder" + var/icon_state = "" + +/singleton/whiteboard/bar_chart + name = "Bar chart" + icon_state = "overlay_1" + +/singleton/whiteboard/pie_chart + name = "Pie chart" + icon_state = "overlay_2" + +/singleton/whiteboard/spacetime_diagram + name = "Spacetime diagram" + icon_state = "overlay_3" + +/singleton/whiteboard/geometric_chart + name = "Geometric chart" + icon_state = "overlay_4" + +/singleton/whiteboard/scribbles + name = "Scribbles" + icon_state = "overlay_5" + +/singleton/whiteboard/gun_diagram + name = "Gun diagram" + icon_state = "overlay_6" + +/singleton/whiteboard/circle_chart + name = "Circle chart" + icon_state = "overlay_7" + +/singleton/whiteboard/display_chart + name = "Display chart" + icon_state = "overlay_8" + +/singleton/whiteboard/todays_menu + name = "Today's menu" + icon_state = "overlay_9" + +/singleton/whiteboard/calculations + name = "Calculations" + icon_state = "overlay_10" + +/singleton/whiteboard/erased_board + name = "Erased board" + icon_state = "overlay_11" + +/singleton/whiteboard/wavelenghth + name = "Wavelength" + icon_state = "overlay_12" + +/singleton/whiteboard/grafitti + name = "Grafitti" + icon_state = "overlay_13" + +/singleton/whiteboard/archeological + name = "Archeological" + icon_state = "overlay_14" diff --git a/code/game/sound/sound_keys/sound_keys.dm b/code/game/sound/sound_keys/sound_keys.dm index e5dc716c81d..a39cc5ba6c1 100644 --- a/code/game/sound/sound_keys/sound_keys.dm +++ b/code/game/sound/sound_keys/sound_keys.dm @@ -904,3 +904,11 @@ 'sound/effects/creatures/robot_talk_1.ogg', 'sound/effects/creatures/robot_talk_2.ogg' ) + +/datum/sound_effect/whiteboard_scribble + key = SFX_WHITEBOARD_SCRIBBLE + file_paths = list( + 'sound/effects/whiteboard_scribble_1.ogg', + 'sound/effects/whiteboard_scribble_2.ogg', + 'sound/effects/whiteboard_scribble_3.ogg' + ) diff --git a/code/modules/cargo/items/recreation.dm b/code/modules/cargo/items/recreation.dm index efb4f7703c2..e0a16ff59a1 100644 --- a/code/modules/cargo/items/recreation.dm +++ b/code/modules/cargo/items/recreation.dm @@ -382,3 +382,31 @@ container_type = "crate" groupable = TRUE spawn_amount = 1 + +/singleton/cargo_item/whiteboard + category = "recreation" + name = "whiteboard" + supplier = "orion" + description = "A standard whiteboard." + price = 100 + items = list( + /obj/structure/whiteboard + ) + access = 0 + container_type = "crate" + groupable = TRUE + spawn_amount = 1 + +/singleton/cargo_item/whiteboard_portable + category = "recreation" + name = "portable whiteboard" + supplier = "orion" + description = "A portable whiteboard, smaller than the standard alternatives." + price = 75 + items = list( + /obj/item/portable_whiteboard + ) + access = 0 + container_type = "crate" + groupable = TRUE + spawn_amount = 1 diff --git a/html/changelogs/kano-dot-whiteboards.yml b/html/changelogs/kano-dot-whiteboards.yml new file mode 100644 index 00000000000..1873dbc6d7a --- /dev/null +++ b/html/changelogs/kano-dot-whiteboards.yml @@ -0,0 +1,5 @@ +author: Kano +delete-after: True +changes: + - rscadd: "Added whiteboards, sprites by KingOfThePing." + - qol: "Gave barsigns and its subtypes a tgui menu." diff --git a/icons/obj/whiteboard.dmi b/icons/obj/whiteboard.dmi new file mode 100644 index 0000000000000000000000000000000000000000..b8dc84dc56828680776b5207b6a34f1fea7c2d01 GIT binary patch literal 4723 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 O1TfGwfmCZdMEnOEFYa>y literal 0 HcmV?d00001 diff --git a/icons/obj/whiteboard_portable.dmi b/icons/obj/whiteboard_portable.dmi new file mode 100644 index 0000000000000000000000000000000000000000..89de8607dffd4d5a0a60e3977c9db786a02f4a6e GIT binary patch literal 615 zcmV-t0+{`YP)fFDZ*Bkp zc$`yKaB_9`^iy#0_2eo`Eh^5;&r`5fFwryM;w;ZhDainGjE%TBGg33tGfE(w;*!LY zR3KBSJR`FtH7P%_C`E~jGbOXA7$|7S#hF%=n41b=V^JEPoLHPns`~h%41^XF;gNLXF#sUIO6)fUpNjwh0dz@3K~z|U?UrG7gCGom-N*r^>;S9V0ZQWm?BM=q z8rGpfGagX;w!N3HzvPvV2$7^|?&hf_ywnJKLXAMt2+krnPM{F8U98~J_wTv$kqU0> zI>ZxVhv;$ypsB!&e#my(4;phCvIKDilfaolJ7=uUi(oIX-TY7=f!~A)R~><#zbsQ5 z!PU$U`n5KK@RS$9;0SgSEUA^1j1Vou3t;4Cy6_4ShX{6ikQ69GH&IWE?oJ^cfs43B zpyz0oJox-z=0iVgnN18CxMbp$O9*Ix6zq7cKxsN^*xY%nAmEj)-EBm26RHylJSDI zLWZ*VhXr5?1%5)S$(TQtLc!)3dDbo=v<8JFGf@x(K}Z1OofjwgUm+^03>AWBTHy0a zO-x)JHyuN