From 9b6e752f3ea5a7c9158bf5d19a41c972797c2b51 Mon Sep 17 00:00:00 2001 From: Lucy Date: Wed, 22 Apr 2026 16:55:26 -0400 Subject: [PATCH] Makes lighting objects objects again (#95332) ## About The Pull Request Continuation of https://github.com/tgstation/tgstation/pull/78857 FOR TOOO LONG WE HAVE SUFF- Ok yeah so like, we made them overlays to save on maptick, but with threaded maptick that is potentially not an issue anymore. I'm opening this pr so I can tm it and see. If it works, it'll save about 50% of lighting object update costs, which is pretty damn good. I'm also removing the fullbright light icon state, since it is barely ever used (it was added as a clientside op, but we don't hit fullbright very much at all so it does nothing but eat my cpu time) Also also changing how SSlighting does its resolution. Rather then waiting for all sources to process, then working on corners and objects, instead we will do all the sources we had at the start, then all the sources after, and so on. The goal is to avoid churn causing the system to constantly choke. it is better to potentially double operate then it is for things to feel horrifically slow. ## Why It's Good For The Game Faster. Also you won't be able to see lights through walls anymore, so mesons will be less dumb. Can maybe bump their nightvision a bit idk we'll see. ## Current Issues to be found ## Changelog :cl: Absolucy, LemonInTheDark fix: You can no longer see lights through walls when using mesons or when ventcrawling. /:cl: --------- Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> --- code/__DEFINES/layers.dm | 2 +- code/controllers/subsystem/lighting.dm | 17 ++- code/controllers/subsystem/mapping.dm | 15 --- code/datums/lazy_template.dm | 1 + code/game/turfs/change_turf.dm | 6 +- code/game/turfs/turf.dm | 2 +- .../view_variables/reference_tracking.dm | 2 +- code/modules/lighting/lighting_corner.dm | 6 +- code/modules/lighting/lighting_object.dm | 109 ++++++++++++------ code/modules/lighting/lighting_turf.dm | 2 +- code/modules/mapping/map_template.dm | 9 +- .../camera/camera_image_capturing.dm | 4 +- .../mobile_port/shuttle_move_callbacks.dm | 10 ++ code/modules/unit_tests/unit_test.dm | 3 + icons/effects/lighting_object.dmi | Bin 2092 -> 1520 bytes 15 files changed, 114 insertions(+), 74 deletions(-) diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm index 90af29c67c0..4d368ccc670 100644 --- a/code/__DEFINES/layers.dm +++ b/code/__DEFINES/layers.dm @@ -39,7 +39,7 @@ #define POINT_PLANE 5 //---------- LIGHTING ------------- -/// Normal 1 per turf dynamic lighting underlays +/// Normal 1 per turf dynamic lighting objects #define LIGHTING_PLANE 10 /// Lighting objects that are "free floating" diff --git a/code/controllers/subsystem/lighting.dm b/code/controllers/subsystem/lighting.dm index 4717e7dcf8c..d35a58b6ab6 100644 --- a/code/controllers/subsystem/lighting.dm +++ b/code/controllers/subsystem/lighting.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(lighting) /datum/controller/subsystem/atoms, /datum/controller/subsystem/mapping, ) - wait = 2 + wait = 1 ss_flags = SS_TICKER var/static/list/sources_queue = list() // List of lighting sources queued for update. var/static/list/corners_queue = list() // List of lighting corners queued for update. @@ -38,7 +38,7 @@ SUBSYSTEM_DEF(lighting) for(var/turf/area_turf as anything in zlevel_turfs) if(area_turf.space_lit) continue - new /datum/lighting_object(area_turf) + new /atom/movable/lighting_object(null, area_turf) CHECK_TICK CHECK_TICK @@ -53,6 +53,7 @@ SUBSYSTEM_DEF(lighting) // UPDATE SOURCE QUEUE var/i = 0 + // something something cache locally for sonic speed var/list/queue = current_sources while(i < length(queue)) //we don't use for loop here because i cannot be changed during an iteration i += 1 @@ -110,7 +111,7 @@ SUBSYSTEM_DEF(lighting) while(i < length(queue)) //we don't use for loop here because i cannot be changed during an iteration i += 1 - var/datum/lighting_object/O = queue[i] + var/atom/movable/lighting_object/O = queue[i] if(QDELETED(O)) continue O.update() @@ -132,3 +133,13 @@ SUBSYSTEM_DEF(lighting) /datum/controller/subsystem/lighting/Recover() initialized = SSlighting.initialized ..() + +/// Takes a list of turfs in, and sets up static lighting for them as needed. +/// Exactly what it says on the tin. +/datum/controller/subsystem/lighting/proc/setup_static_lighting_if_needed(list/turfs) + for(var/turf/unlit as anything in turfs) + if(unlit.space_lit) + continue + var/area/loc_area = unlit.loc + if(loc_area.static_lighting) + unlit.lighting_build_overlay() diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm index b3bcc301ea6..8d98ff05bd5 100644 --- a/code/controllers/subsystem/mapping.dm +++ b/code/controllers/subsystem/mapping.dm @@ -744,11 +744,6 @@ ADMIN_VERB(load_away_mission, R_FUN, "Load Away Mission", "Load a specific away if(contain_turfs) build_area_turfs(z_value, filled_with_space) - // And finally, misc global generation - - // We'll have to update this if offsets change, because we load lowest z to highest z - generate_lighting_appearance_by_z(z_value) - /datum/controller/subsystem/mapping/proc/build_area_turfs(z_level, space_guaranteed) // If we know this is filled with default tiles, we can use the default area // Faster @@ -785,11 +780,6 @@ ADMIN_VERB(load_away_mission, R_FUN, "Load Away Mission", "Load a specific away z_level_to_lowest_plane_offset[level_to_update.z_value] = plane_offset z_level_to_stack[level_to_update.z_value] = z_stack - // This can be affected by offsets, so we need to update it - // PAIN - for(var/i in 1 to length(z_list)) - generate_lighting_appearance_by_z(i) - var/old_max = max_plane_offset max_plane_offset = max(max_plane_offset, plane_offset) if(max_plane_offset == old_max) @@ -875,11 +865,6 @@ ADMIN_VERB(load_away_mission, R_FUN, "Load Away Mission", "Load a specific away CRASH("Attempted to lazy load a template key that does not exist: '[template_key]'") return target.lazy_load() -/proc/generate_lighting_appearance_by_z(z_level) - if(length(GLOB.default_lighting_underlays_by_z) < z_level) - GLOB.default_lighting_underlays_by_z.len = z_level - GLOB.default_lighting_underlays_by_z[z_level] = mutable_appearance(LIGHTING_ICON, "transparent", z_level * 0.01, null, LIGHTING_PLANE, 255, RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM, offset_const = GET_Z_PLANE_OFFSET(z_level)) - /// Returns true if the map we're playing on is on a planet /datum/controller/subsystem/mapping/proc/is_planetary() return current_map.planetary diff --git a/code/datums/lazy_template.dm b/code/datums/lazy_template.dm index 3faefc0cc78..9fe85cbcbcd 100644 --- a/code/datums/lazy_template.dm +++ b/code/datums/lazy_template.dm @@ -104,6 +104,7 @@ loaded_atom_movables |= thing SSatoms.InitializeAtoms(loaded_areas + loaded_atom_movables + loaded_turfs) + SSlighting.setup_static_lighting_if_needed(loaded_turfs) SSmachines.setup_template_powernets(loaded_cables) SSair.setup_template_machinery(loaded_atmospherics) diff --git a/code/game/turfs/change_turf.dm b/code/game/turfs/change_turf.dm index fa55c13fe40..33afb7c7b29 100644 --- a/code/game/turfs/change_turf.dm +++ b/code/game/turfs/change_turf.dm @@ -158,8 +158,12 @@ GLOBAL_LIST_INIT(blacklisted_automated_baseturfs, typecacheof(list( if(SSlighting.initialized) // Space tiles should never have lighting objects if(!space_lit) + if(old_lighting_object) + lighting_object = old_lighting_object + vis_contents += lighting_object // Should have a lighting object if we never had one - lighting_object = old_lighting_object || new /datum/lighting_object(src) + else + new /atom/movable/lighting_object(null, src) else if (old_lighting_object) qdel(old_lighting_object, force = TRUE) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index c6debdb5ce9..b6a0b8c3867 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -67,7 +67,7 @@ GLOBAL_LIST_EMPTY(station_turfs) var/tmp/lighting_corners_initialised = FALSE ///Our lighting object. - var/tmp/datum/lighting_object/lighting_object + var/tmp/atom/movable/lighting_object/lighting_object ///Lighting Corner datums. var/tmp/datum/lighting_corner/lighting_corner_NE var/tmp/datum/lighting_corner/lighting_corner_SE diff --git a/code/modules/admin/view_variables/reference_tracking.dm b/code/modules/admin/view_variables/reference_tracking.dm index 5f3afd380f7..cccb97f6c9b 100644 --- a/code/modules/admin/view_variables/reference_tracking.dm +++ b/code/modules/admin/view_variables/reference_tracking.dm @@ -19,6 +19,7 @@ GLOBAL_ALIST_EMPTY(reftracker_skip_typecache_b) /icon, /matrix, /regex, + /atom/movable/lighting_object, // only contains turf and MA refs /atom/movable/mirage_holder, /atom/movable/render_step/emissive_blocker, /datum/armor, @@ -31,7 +32,6 @@ GLOBAL_ALIST_EMPTY(reftracker_skip_typecache_b) /datum/greyscale_layer, /datum/icon_transformer, /datum/instrument_key, - /datum/lighting_object, // only contains turf and MA refs /datum/movespeed_modifier, /datum/painting, /datum/paper_input, diff --git a/code/modules/lighting/lighting_corner.dm b/code/modules/lighting/lighting_corner.dm index 4c35a8f2bb3..1323f45f1e6 100644 --- a/code/modules/lighting/lighting_corner.dm +++ b/code/modules/lighting/lighting_corner.dm @@ -141,7 +141,7 @@ return #endif - var/datum/lighting_object/lighting_object = master_NE?.lighting_object + var/atom/movable/lighting_object/lighting_object = master_NE?.lighting_object if (lighting_object && !lighting_object.needs_update) lighting_object.needs_update = TRUE SSlighting.objects_queue += lighting_object @@ -163,10 +163,6 @@ self_destruct_if_idle() - -/datum/lighting_corner/dummy/New() - return - /datum/lighting_corner/Destroy(force) if (!force) return QDEL_HINT_LETMELIVE diff --git a/code/modules/lighting/lighting_object.dm b/code/modules/lighting/lighting_object.dm index c5ce42e45ed..85bcd6c0c31 100644 --- a/code/modules/lighting/lighting_object.dm +++ b/code/modules/lighting/lighting_object.dm @@ -1,32 +1,50 @@ -/datum/lighting_object - ///the underlay we are currently applying to our turf to apply light - var/mutable_appearance/current_underlay - +/atom/movable/lighting_object + name = "" + anchored = TRUE + plane = LIGHTING_PLANE + icon = LIGHTING_ICON + icon_state = null + color = null //we manually set color in init instead + appearance_flags = RESET_COLOR | RESET_ALPHA | RESET_TRANSFORM + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + invisibility = INVISIBILITY_LIGHTING + move_resist = INFINITY ///whether we are already in the SSlighting.objects_queue list var/needs_update = FALSE ///the turf that our light is applied to var/turf/affected_turf -// Global list of lighting underlays, indexed by z level -GLOBAL_LIST_EMPTY(default_lighting_underlays_by_z) - -/datum/lighting_object/New(turf/source) - if(!isturf(source)) - qdel(src, force=TRUE) - stack_trace("a lighting object was assigned to [source], a non turf! ") - return +/atom/movable/lighting_object/Initialize(mapload, turf/affected_turf) + if(!isnull(loc)) + if(isturf(loc)) + affected_turf = loc + moveToNullspace() + stack_trace("a lighting object was improperly initialized - they should have a null loc, with the affected turf being the second argument") + else + qdel(src, force = TRUE) + CRASH("a lighting object tried to be spawned for a non-turf!") + if(!isturf(affected_turf)) + qdel(src, force = TRUE) + CRASH("a lighting object was assigned to [affected_turf], a non turf!") . = ..() - current_underlay = new(GLOB.default_lighting_underlays_by_z[source.z]) + verbs.Cut() - affected_turf = source + src.affected_turf = affected_turf + layer = affected_turf.z * 0.01 + if(SSmapping.max_plane_offset) + // generates the offset lighting plane to use. NOTE: this assumes the turf lighting + // plane is ALWAYS offsettable which is technically dependent on a plane master var. + // checking for that is slower and this is hot enough that this is a worthwhile risk to take + plane = LIGHTING_PLANE - (PLANE_RANGE * GET_Z_PLANE_OFFSET(affected_turf.z)) if (affected_turf.lighting_object) qdel(affected_turf.lighting_object, force = TRUE) stack_trace("a lighting object was assigned to a turf that already had a lighting object!") affected_turf.lighting_object = src + affected_turf.vis_contents += src // Default to fullbright, so things can "see" if they use view() before we update affected_turf.luminosity = 1 @@ -38,18 +56,20 @@ GLOBAL_LIST_EMPTY(default_lighting_underlays_by_z) needs_update = TRUE SSlighting.objects_queue += src -/datum/lighting_object/Destroy(force) +/atom/movable/lighting_object/Destroy(force) if (!force) return QDEL_HINT_LETMELIVE SSlighting.objects_queue -= src if (isturf(affected_turf)) + affected_turf.vis_contents -= src affected_turf.lighting_object = null affected_turf.luminosity = 1 - affected_turf.underlays -= current_underlay affected_turf = null return ..() -/datum/lighting_object/proc/update() +/atom/movable/lighting_object/proc/update() + var/turf/affected_turf = src.affected_turf + // To the future coder who sees this and thinks // "Why didn't he just use a loop?" // Well my man, it's because the loop performed like shit. @@ -60,8 +80,6 @@ GLOBAL_LIST_EMPTY(default_lighting_underlays_by_z) var/static/datum/lighting_corner/dummy/dummy_lighting_corner = new - var/turf/affected_turf = src.affected_turf - #ifdef VISUALIZE_LIGHT_UPDATES affected_turf.add_atom_colour(COLOR_BLUE_LIGHT, ADMIN_COLOUR_PRIORITY) animate(affected_turf, 10, color = null) @@ -84,20 +102,12 @@ GLOBAL_LIST_EMPTY(default_lighting_underlays_by_z) var/set_luminosity = max > 1e-6 #endif - var/mutable_appearance/current_underlay = src.current_underlay - affected_turf.underlays -= current_underlay - if(red_corner.cache_r & green_corner.cache_r & blue_corner.cache_r & alpha_corner.cache_r && \ - (red_corner.cache_g + green_corner.cache_g + blue_corner.cache_g + alpha_corner.cache_g + \ - red_corner.cache_b + green_corner.cache_b + blue_corner.cache_b + alpha_corner.cache_b == 8)) - //anything that passes the first case is very likely to pass the second, and addition is a little faster in this case - current_underlay.icon_state = "lighting_transparent" - current_underlay.color = null - else if(!set_luminosity) - current_underlay.icon_state = "lighting_dark" - current_underlay.color = null + if(!set_luminosity) + icon_state = "lighting_dark" + color = null else - current_underlay.icon_state = null - current_underlay.color = list( + icon_state = null + color = list( red_corner.cache_r, red_corner.cache_g, red_corner.cache_b, 00, green_corner.cache_r, green_corner.cache_g, green_corner.cache_b, 00, blue_corner.cache_r, blue_corner.cache_g, blue_corner.cache_b, 00, @@ -105,7 +115,34 @@ GLOBAL_LIST_EMPTY(default_lighting_underlays_by_z) 00, 00, 00, 01 ) - // Of note. Most of the cost in this proc is here, I think because color matrix'd underlays DO NOT cache well, which is what adding to underlays does - // We use underlays because objects on each tile would fuck with maptick. if that ever changes, use an object for this instead - affected_turf.underlays += current_underlay - affected_turf.luminosity = set_luminosity + luminosity = set_luminosity + +// Variety of overrides so the overlays don't get affected by weird things. + +/atom/movable/lighting_object/ex_act(severity) + return FALSE + +/atom/movable/lighting_object/singularity_act() + return + +/atom/movable/lighting_object/singularity_pull() + return + +/atom/movable/lighting_object/blob_act() + return + +/atom/movable/lighting_object/on_changed_z_level(turf/old_turf, turf/new_turf, same_z_layer, notify_contents = TRUE) + SHOULD_CALL_PARENT(FALSE) + return + +/atom/movable/lighting_object/wash(clean_types) + SHOULD_CALL_PARENT(FALSE) // lighting objects are dirty, confirmed + return + +// Override here to prevent things accidentally moving around overlays. +/atom/movable/lighting_object/forceMove(atom/destination, no_tp = FALSE, harderforce = FALSE) + if(harderforce) + return ..() + +/atom/movable/lighting_object/ref_search_details() + return "[text_ref(src)] (turf: [affected_turf ? "[affected_turf.type] @ [AREACOORD(affected_turf)]" : "null"] needs_update: [needs_update ? "true" : "false"])" diff --git a/code/modules/lighting/lighting_turf.dm b/code/modules/lighting/lighting_turf.dm index 949d9b59b8f..f3feed46bcf 100644 --- a/code/modules/lighting/lighting_turf.dm +++ b/code/modules/lighting/lighting_turf.dm @@ -14,7 +14,7 @@ if (lighting_object) qdel(lighting_object, force=TRUE) //Shitty fix for lighting objects persisting after death - new /datum/lighting_object(src) + new /atom/movable/lighting_object(null, src) // Used to get a scaled lumcount. /turf/proc/get_lumcount(minlum = 0, maxlum = 1) diff --git a/code/modules/mapping/map_template.dm b/code/modules/mapping/map_template.dm index 0f6ff85eed9..a0a309849dc 100644 --- a/code/modules/mapping/map_template.dm +++ b/code/modules/mapping/map_template.dm @@ -90,14 +90,7 @@ return SSatoms.InitializeAtoms(areas + turfs + movables, returns_created_atoms ? created_atoms : null) - - for(var/turf/unlit as anything in turfs) - if(unlit.space_lit) - continue - var/area/loc_area = unlit.loc - if(!loc_area.static_lighting) - continue - unlit.lighting_build_overlay() + SSlighting.setup_static_lighting_if_needed(turfs) // NOTE, now that Initialize and LateInitialize run correctly, do we really // need these two below? diff --git a/code/modules/photography/camera/camera_image_capturing.dm b/code/modules/photography/camera/camera_image_capturing.dm index 7a4cc5857a0..21ad4a51d4b 100644 --- a/code/modules/photography/camera/camera_image_capturing.dm +++ b/code/modules/photography/camera/camera_image_capturing.dm @@ -41,7 +41,7 @@ atoms += new /obj/effect/appearance_clone(newT, T.loc) if(T.lighting_object) var/obj/effect/appearance_clone/lighting_overlay = new(newT) - lighting_overlay.appearance = T.lighting_object.current_underlay + lighting_overlay.appearance = T.lighting_object.appearance lighting_overlay.underlays += backdrop lighting_overlay.blend_mode = BLEND_MULTIPLY lighting += lighting_overlay @@ -61,7 +61,7 @@ atoms += T if(T.lighting_object) var/obj/effect/appearance_clone/lighting_overlay = new(T) - lighting_overlay.appearance = T.lighting_object.current_underlay + lighting_overlay.appearance = T.lighting_object.appearance lighting_overlay.underlays += backdrop lighting_overlay.blend_mode = BLEND_MULTIPLY lighting += lighting_overlay diff --git a/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm b/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm index 2eee98a0d6f..245d1cb3485 100644 --- a/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm +++ b/code/modules/shuttle/mobile_port/shuttle_move_callbacks.dm @@ -78,6 +78,13 @@ All ShuttleMove procs go here if(rotation) shuttleRotate(rotation, params = ALL) //see shuttle_rotate.dm + + // if we have a lighting object that needs to be updated + if(lighting_object?.needs_update) + lighting_object.update() + lighting_object.needs_update = FALSE + SSlighting.objects_queue -= lighting_object + SEND_SIGNAL(src, COMSIG_TURF_AFTER_SHUTTLE_MOVE, oldT) return TRUE @@ -402,6 +409,9 @@ All ShuttleMove procs go here /************************************Misc move procs************************************/ +/atom/movable/lighting_object/onShuttleMove() + return FALSE + /obj/docking_port/mobile/hypotheticalShuttleMove(rotation, move_mode, obj/docking_port/mobile/moving_dock) . = ..() if(moving_dock == src) diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index 3814df5ef38..74f41bf1088 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -279,6 +279,9 @@ GLOBAL_VAR_INIT(focused_tests, focused_tests()) /obj/effect/decal/cleanable/blood/trail, //Should not exist outside of ethereals /obj/item/stock_parts/power_store/cell/ethereal, + // Abstract type, controlled by turfs + // Literally errors on creation/deletion + /atom/movable/lighting_object, ) // Everything that follows is a typesof() check. diff --git a/icons/effects/lighting_object.dmi b/icons/effects/lighting_object.dmi index d2e503958723fba64ad288e8819ffbcdcb87afc0..f2d5960d91b0d07332dc9e8cbe9e46687dd4b294 100644 GIT binary patch literal 1520 zcmXX`dpOg39G*OGL*}45m1{1?C0Pjzn>5uLCUP>D$mK{8=F*hQqC#s{E?Pu8N3?|0)doKCz0+z z4FHaVyex1FEpgj{l5+KoS1d9lCI}Z66&n_L83e)?d-kxvX%-OKDVRtd(U^bHFVa}G zyR_DA8!CtP*bz7SW@$=C6Cw5%ban4%a%lE>Chx%fC~o%psehFCkBxF@WemBhtE2!+ z!g`sYMKr&`1Vz&5lS5F`TEe;-b zAr=R*->0H;(v8${WujZr6iO=DFCW2XU}pbdN!c|x1!ouQs7B$z{7~?pxe2=T-BF`r z&v~`p4a@6#2JZ3)BIKjsMHFv-``Yli&i1j6kd;FKm#K(LJN{c$x0@z?C4>BCV&dm!9D>(Kg#z#cZ-h34Q=MS9{JevnH~)4UF?9C5cG3Ds9h3ML4|BK?o63Db&z zQ<{gBLwXbEtzK+kw$16{bQB}-wrLmDIGUrD)A6|V0q12cu#Z_*C2`8Ho?=s7u>Dl6 z>0mV)^PK5A6h%FRgq>B?`6+>&oDKN>r8SEqRcMZYzh`>3B4On2*<1l zN8z5n0}NZgeJYV7mhqvydk=NX_c4z2_~GLkE?}v7%tr0EZQq5 zS{NpnIiUF|B@}32p|`ylnd>;F$7l!)6KvQ<=ZoS~m<){RsNqf}Xt#zi{00{>s;gX? zz{M5RdMyb$Nz=KM&r~43NnORg2qoy8Jn1{L9;dZ{zjJN<*C(GO?$f`p)Yq(T+2s^n zC<{|FIvzto2y$1H8EvBiz`DrGC_h-5D=G@Lt!Hvso5YZ$vF9dx+_ZO;PMFul@D17` zDB}sdZ{O}3{jArDycDr!4Qxx&sEU!c4ok~N(}==VSZa%6`SitigJpH=AI zV6A|(W!27Yn_oCWdq6@qm^pFt92xTC!ZX+`JaUSLK0aGw|9mo~F*X28-OkdHmFJbq zWl*6W*;>k=wR<*gZSBs=^_QTMRhNCs8ydK&Jq`V-_k zd#qO$IjO?afY(WY84n4t$JyIT2k0DF;f6^6y(BlKg<&0Ksf_-A-7BKpZECKSpB-5ICja$+HX@t*~`RnCsi5IMH|CZMv^U{_S ze>+JMLod00D_7qvMgpkql}NQ_U$LczMAFOhG7>_4Q?h{_a!5je3?xL-eTlcojkRS3 dDa=zAs}QFX`Gp?grN9>sLOHo2Ya9bo{{t&B=Mw+` literal 2092 zcmc(g`#;ltAIHD5cCck=Q-r1_r`3gWm^Q}|x+GRkR}M3mV=Z$?4x>4QmWOgXBqJ%x z6&KUt#*J)p=pcvWCMI83S-OqXB8s`^dfflR{ln+|{^j-ld|r?D^ZkB&E_=8;E1`Cy z002;Ob#bK0GhKe{kZ^gNHH*c_6F2#kZ@gn@Tu5vrD?T#%8~`MiUwYYu>N3F{ow53E zm^KkmpYUp*;XkTUVT!tXyUdGuSDrk{r0T~|mMZq#`y|=b@^*yi`u=ejL$vU`-6|@* z=ZoE4-2tc0VY7$~r?G;|2b6XaZq#KtNAcAySReh1&OE8jnz(6C8)fd6xtTs&xV~t5 z5OZ$j8{`paiG(%y^aB7Qz}3UHdT@W|x%LIuU;a@}F zGeH7KSR&j(A4W?;zbPDrgp}Ta64HTVLr$_o6|~ z-brw>w+}qsdpd3dxB5rxO7J(+u>!F=sD5<$SKv&>Js^<28{km8%)oc+(+VI{sw8C& zhGgQ`+~RLIBly>?vWF*e9#dM8-j+tkVZOmeZX?di_R#Xia)-r%N*IXV#TG--Hc{Y5 z;QOJUCMn#IQi3+3oY0SY7{NWs83;*40mw{_;&jDy+7hCRvXIxjOzh%b)n7fyZ7u7- z-Rbuft8-Pc6bH|H9FMs7q6PY?Dr6jgk1$=-lWll~7s9d%~Bv`T%ZVejo#% zfAcSw9%P?O)4tmdSofozI4uQ(oBPzUXS$heAG$G&zuc>5d#2xt9sgDo?r~i-s)4G~w4`oUY$)%m1ZT;`NsJ zz}XRBOejq?K@j_3jg%Q!wWP>;ChRvi%}*!orYdaFg0xwZQ@4b}ZQwSj2E?FBtx7Zd z?f)5_DD-2IL93ZER+GIO4trjM6(UQ5w+va2PmNnMYDraU0R(5K74;aQ@|+^x?&qa& zBH`sdq`4uD#@jdK?=t3tcvrH86AH!JP{xb3_6!%Bn%y32AOFVHGIdX!{~dE%iLb`W zWf9*4&V^IeE+G5jGS)co>stjMfx+CL&9;vw{NzkJx1K$h){X%T|NFo-?HP7g=qq4O7Z1UElu zZoTh1TsF2(ty`%z0kr( ztqccZ8N$-c!t8AA%l`o#L_Z+sWSYbHIMWPIu6j>xQq%LYrTN3rT0^Z-#K^h?Z)=PF zmTH!}c!fMP4-C8&lPbS#b>?sWnoJvY!;wX!z#E1g_PUiOLn-kMikl02}gNnO?0Bsg7@ahKq`} zh`Od9-Y7nT|0D3kGL0ngE*v#_qicStjK(W!+jqf9a(`UztR+ZWMWo8s;S>I*sd7!x zYAy(}mrz~zV$-5< z0f`xjB;$@vMjEO`m6Z7%_vHEUtjL0PuuY@Qc(ous=dyM;w4L0!0S?y79*1_xe!zk- z(j5hNN9w~y2kSh@HYM<^9=E6DCZLBJL&c*CPr4)OdX3 z4g`x4hjqin=ejYmmVZIM1~2(n>g*9*hcwRy!2o*5VzEuap$Ujq0I+|coUEhip%n4z ztfq;DjxuY}PGUz9l+sxUaZeu!yr{wQN&6{8>pYQet4v8wUHHE7Vwkg1+`5s{D=Sv! z40d8|1ygJO)_6Kz(`y&(dCmH)6au7`9O08n1`*&U%(u-}w%EkBp+APyg{-j!QBd!` zuWOu9czwQHS*PTy{qiH!Ta$l8F0Z$spFdEk?8_AV$JYkWpWuxCUkfLiJ8iaHv89`6 exe;*ji}C9rbAz$~yAk>K54bwHJ2p7bGyV%Xp49&U