From bb689a39eb4c200c086b547503ceaa0d63dfeb07 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Thu, 29 Sep 2022 00:04:18 +0700 Subject: [PATCH] Analog scroll bar, slim scroll bar, canvas scroll panel --- build.gradle.kts | 10 +- .../client/screen/ExoSuitInventoryScreen.kt | 17 +- .../screen/panels/AnalogScrollBarPanel.kt | 179 ++++++++++++++++++ .../screen/panels/DiscreteScrollBarPanel.kt | 55 ++++-- .../screen/panels/ScrollBarConstants.kt | 14 +- .../screen/panels/ScrollbarBackgroundPanel.kt | 166 ++++++++++++++++ .../textures/gui/scroll.png | Bin 714 -> 779 bytes .../textures/gui/scroll.xcf | Bin 3009 -> 5687 bytes 8 files changed, 412 insertions(+), 29 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/AnalogScrollBarPanel.kt create mode 100644 src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ScrollbarBackgroundPanel.kt diff --git a/build.gradle.kts b/build.gradle.kts index d42fc07db..362628229 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -256,6 +256,11 @@ minecraft.runs.all { } repositories { + // If you have mod jar dependencies in ./libs, you can declare them as a repository like so: + flatDir { + dir("libs") + } + maven { url = uri("https://maven.dbotthepony.ru") } @@ -285,11 +290,6 @@ repositories { name = "Kotlin for Forge" url = uri("https://thedarkcolour.github.io/KotlinForForge/") } - - // If you have mod jar dependencies in ./libs, you can declare them as a repository like so: - flatDir { - dir("libs") - } } fun org.gradle.jvm.tasks.Jar.attachManifest() { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/ExoSuitInventoryScreen.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/ExoSuitInventoryScreen.kt index 6980f74fd..fc1cde0af 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/ExoSuitInventoryScreen.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/ExoSuitInventoryScreen.kt @@ -171,15 +171,24 @@ class ExoSuitInventoryScreen(menu: ExoSuitInventoryMenu) : MatteryScreen 4) + ScrollbarBackgroundPanel.padded(this, frame, x, + width = curiosWidth, + height = curiosHeight, alwaysShowScrollbar = true) + else + BackgroundPanel.padded(this, frame, x, + width = curiosWidth, + height = curiosHeight) x -= curiosRect.width curiosRect.x = x for ((slot, cosmetic) in menu.curiosSlots) { - val row = EditablePanel(this, curiosRect, height = AbstractSlotPanel.SIZE) + val row = EditablePanel(this, if (curiosRect is ScrollbarBackgroundPanel) curiosRect.canvas else curiosRect, height = AbstractSlotPanel.SIZE) row.dock = Dock.TOP SlotPanel(this, row, slot).dock = Dock.RIGHT diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/AnalogScrollBarPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/AnalogScrollBarPanel.kt new file mode 100644 index 000000000..ddb8648bf --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/AnalogScrollBarPanel.kt @@ -0,0 +1,179 @@ +package ru.dbotthepony.mc.otm.client.screen.panels + +import com.mojang.blaze3d.platform.InputConstants +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.client.gui.screens.Screen +import ru.dbotthepony.mc.otm.core.linearInterpolation +import kotlin.math.roundToInt + +open class AnalogScrollBarPanel( + screen: S, + parent: EditablePanel<*>?, + val maxScroll: (panel: AnalogScrollBarPanel<*>) -> Float, + val scrollCallback: (panel: AnalogScrollBarPanel<*>, oldScroll: Float, newScroll: Float) -> Unit = { _, _, _ -> }, + val smoothScrollCallback: (panel: AnalogScrollBarPanel<*>, oldScroll: Float, newScroll: Float) -> Unit = { _, _, _ -> }, + x: Float = 0f, + y: Float = 0f, + height: Float = 20f, + open val allowSmoothScroll: Boolean = true, + open var scrollStep: Float = AbstractSlotPanel.SIZE, + isSlim: Boolean = false +) : EditablePanel(screen, parent, x, y, width = if (isSlim) ScrollBarConstants.SLIM_WIDTH else ScrollBarConstants.WIDTH, height = height) { + inner class Button : EditablePanel(screen, this@AnalogScrollBarPanel, 1f, 1f, this@AnalogScrollBarPanel.width - 2f, 15f) { + var isScrolling = false + private set + + override fun innerRender(stack: PoseStack, mouseX: Float, mouseY: Float, partialTick: Float) { + if (this@AnalogScrollBarPanel.width == ScrollBarConstants.SLIM_WIDTH) { + if (isScrolling) { + ScrollBarConstants.scrollSlimBarButtonPress.render(stack, width = width, height = height) + } else if (maxScroll.invoke(this@AnalogScrollBarPanel) <= 0) { + ScrollBarConstants.scrollSlimBarButtonDisabled.render(stack, width = width, height = height) + } else if (isHovered) { + ScrollBarConstants.scrollSlimBarButtonHover.render(stack, width = width, height = height) + } else { + ScrollBarConstants.scrollSlimBarButton.render(stack, width = width, height = height) + } + } else { + if (isScrolling) { + ScrollBarConstants.scrollBarButtonPress.render(stack, width = width, height = height) + } else if (maxScroll.invoke(this@AnalogScrollBarPanel) <= 0) { + ScrollBarConstants.scrollBarButtonDisabled.render(stack, width = width, height = height) + } else if (isHovered) { + ScrollBarConstants.scrollBarButtonHover.render(stack, width = width, height = height) + } else { + ScrollBarConstants.scrollBarButton.render(stack, width = width, height = height) + } + } + } + + private var rememberScroll = 0f + private var rememberY = 0.0 + + override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean { + if (maxScroll.invoke(this@AnalogScrollBarPanel) <= 0f) { + return true + } + + if (button == InputConstants.MOUSE_BUTTON_LEFT && tryToGrabMouseInput()) { + isScrolling = true + rememberScroll = scroll + rememberY = y + } + + return true + } + + override fun mouseReleasedInner(x: Double, y: Double, button: Int): Boolean { + if (isScrolling && button == InputConstants.MOUSE_BUTTON_LEFT) { + isScrolling = false + grabMouseInput = false + return true + } + + return false + } + + override fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean { + return this@AnalogScrollBarPanel.mouseScrolledInner(x, y, scroll) + } + + override fun mouseDraggedInner(x: Double, y: Double, button: Int, xDelta: Double, yDelta: Double): Boolean { + if (isScrolling) { + val pixelsPerRow = (this@AnalogScrollBarPanel.height - height) / maxScroll.invoke(this@AnalogScrollBarPanel) + val diff = y - rememberY + + this@AnalogScrollBarPanel.scroll = rememberScroll + (diff / pixelsPerRow).roundToInt() + return true + } + + return false + } + } + + val scrollButton = Button() + + private var lastRender = System.nanoTime() + + override fun innerRender(stack: PoseStack, mouseX: Float, mouseY: Float, partialTick: Float) { + if (width == ScrollBarConstants.SLIM_WIDTH) { + ScrollBarConstants.scrollSlimBarBody.render(stack, y = 2f, height = height - 4f) + ScrollBarConstants.scrollSlimBarTop.render(stack) + ScrollBarConstants.scrollSlimBarBottom.render(stack, y = height - 2f) + } else { + ScrollBarConstants.scrollBarBody.render(stack, y = 2f, height = height - 4f) + ScrollBarConstants.scrollBarTop.render(stack) + ScrollBarConstants.scrollBarBottom.render(stack, y = height - 2f) + } + + val time = System.nanoTime() + val diff = time - lastRender + lastRender = time + + if (scroll != smoothScroll) { + smoothScroll = linearInterpolation(diff / 50_000_000.0, smoothScroll.toDouble(), scroll.toDouble()).toFloat() + } + } + + public override fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean { + this.scroll -= scroll.toFloat() * scrollStep + return true + } + + var scroll = 0f + set(value) { + val newValue = value.coerceAtLeast(0f).coerceAtMost(maxScroll.invoke(this)) + + if (newValue != field) { + val old = field + field = newValue + scrollChanged(old, newValue) + + if (!allowSmoothScroll) { + smoothScroll = newValue + } + } + } + + var smoothScroll = 0f + set(value) { + val newValue = value.coerceAtLeast(0f).coerceAtMost(maxScroll.invoke(this)) + + if (newValue != field) { + val old = field + field = newValue + smoothScrollChanged(old, newValue) + } + } + + fun hardSetScroll(scroll: Float) { + this.scroll = scroll + this.smoothScroll = scroll + } + + open fun updateScrollButtonPosition() { + val maxScroll = this.maxScroll.invoke(this) + + if (maxScroll <= 0f) { + scrollButton.y = 1f + return + } + + val availableHeight = height - scrollButton.height - 2f + scrollButton.y = 1f + availableHeight * (scroll / maxScroll) + } + + open fun scrollChanged(oldScroll: Float, newScroll: Float) { + scrollCallback.invoke(this, oldScroll, newScroll) + updateScrollButtonPosition() + } + + open fun smoothScrollChanged(oldScroll: Float, newScroll: Float) { + smoothScrollCallback.invoke(this, oldScroll, newScroll) + } + + override fun performLayout() { + super.performLayout() + updateScrollButtonPosition() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/DiscreteScrollBarPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/DiscreteScrollBarPanel.kt index 3ee5a7e42..fe6b77d2d 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/DiscreteScrollBarPanel.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/DiscreteScrollBarPanel.kt @@ -5,32 +5,45 @@ import com.mojang.blaze3d.vertex.PoseStack import net.minecraft.client.gui.screens.Screen import kotlin.math.roundToInt -open class DiscreteScrollBarPanel @JvmOverloads constructor( +open class DiscreteScrollBarPanel( screen: S, parent: EditablePanel<*>?, - val maxScroll: (panel: DiscreteScrollBarPanel) -> Int, - val scrollCallback: (panel: DiscreteScrollBarPanel, oldScroll: Int, newScroll: Int) -> Unit, + val maxScroll: (panel: DiscreteScrollBarPanel<*>) -> Int, + val scrollCallback: (panel: DiscreteScrollBarPanel<*>, oldScroll: Int, newScroll: Int) -> Unit, x: Float = 0f, y: Float = 0f, - height: Float = 20f -) : EditablePanel(screen, parent, x, y, width = ScrollBarConstants.WIDTH, height = height) { - open inner class Button : EditablePanel(screen, this@DiscreteScrollBarPanel, 1f, 1f, 12f, 15f) { + height: Float = 20f, + isSlim: Boolean = false +) : EditablePanel(screen, parent, x, y, width = if (isSlim) ScrollBarConstants.SLIM_WIDTH else ScrollBarConstants.WIDTH, height = height) { + inner class Button : EditablePanel(screen, this@DiscreteScrollBarPanel, 1f, 1f, this@DiscreteScrollBarPanel.width - 12f, 15f) { var isScrolling = false - protected set + private set override fun innerRender(stack: PoseStack, mouseX: Float, mouseY: Float, partialTick: Float) { - if (isScrolling) { - ScrollBarConstants.scrollBarButtonPress.render(stack, width = width, height = height) - } else if (maxScroll.invoke(this@DiscreteScrollBarPanel) <= 0) { - ScrollBarConstants.scrollBarButtonDisabled.render(stack, width = width, height = height) - } else if (isHovered) { - ScrollBarConstants.scrollBarButtonHover.render(stack, width = width, height = height) + if (this@DiscreteScrollBarPanel.width == ScrollBarConstants.SLIM_WIDTH) { + if (isScrolling) { + ScrollBarConstants.scrollSlimBarButtonPress.render(stack, width = width, height = height) + } else if (maxScroll.invoke(this@DiscreteScrollBarPanel) <= 0) { + ScrollBarConstants.scrollSlimBarButtonDisabled.render(stack, width = width, height = height) + } else if (isHovered) { + ScrollBarConstants.scrollSlimBarButtonHover.render(stack, width = width, height = height) + } else { + ScrollBarConstants.scrollSlimBarButton.render(stack, width = width, height = height) + } } else { - ScrollBarConstants.scrollBarButton.render(stack, width = width, height = height) + if (isScrolling) { + ScrollBarConstants.scrollBarButtonPress.render(stack, width = width, height = height) + } else if (maxScroll.invoke(this@DiscreteScrollBarPanel) <= 0) { + ScrollBarConstants.scrollBarButtonDisabled.render(stack, width = width, height = height) + } else if (isHovered) { + ScrollBarConstants.scrollBarButtonHover.render(stack, width = width, height = height) + } else { + ScrollBarConstants.scrollBarButton.render(stack, width = width, height = height) + } } } - protected var rememberScroll = 0 + private var rememberScroll = 0 private var rememberY = 0.0 override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean { @@ -77,9 +90,15 @@ open class DiscreteScrollBarPanel @JvmOverloads constructor( val scrollButton = Button() override fun innerRender(stack: PoseStack, mouseX: Float, mouseY: Float, partialTick: Float) { - ScrollBarConstants.scrollBarBody.render(stack, y = 2f, height = height - 4) - ScrollBarConstants.scrollBarTop.render(stack) - ScrollBarConstants.scrollBarBottom.render(stack, y = height - 2) + if (width == ScrollBarConstants.SLIM_WIDTH) { + ScrollBarConstants.scrollSlimBarBody.render(stack, y = 2f, height = height - 4f) + ScrollBarConstants.scrollSlimBarTop.render(stack) + ScrollBarConstants.scrollSlimBarBottom.render(stack, y = height - 2f) + } else { + ScrollBarConstants.scrollBarBody.render(stack, y = 2f, height = height - 4f) + ScrollBarConstants.scrollBarTop.render(stack) + ScrollBarConstants.scrollBarBottom.render(stack, y = height - 2f) + } } public override fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean { diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ScrollBarConstants.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ScrollBarConstants.kt index f0db8929f..5e4b1161e 100644 --- a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ScrollBarConstants.kt +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ScrollBarConstants.kt @@ -5,15 +5,25 @@ import ru.dbotthepony.mc.otm.client.render.element object ScrollBarConstants { const val WIDTH = 14f - const val TEXTURE_WIDTH = 14f + const val SLIM_WIDTH = 8f + const val TEXTURE_WIDTH = 22f const val TEXTURE_HEIGHT = 68f val scrollBarTop = WidgetLocation.SCROLL.element(0f, 45f, 14f, 2f, TEXTURE_WIDTH, TEXTURE_HEIGHT) val scrollBarBody = WidgetLocation.SCROLL.element(0f, 46f, 14f, 6f, TEXTURE_WIDTH, TEXTURE_HEIGHT) val scrollBarBottom = WidgetLocation.SCROLL.element(0f, 51f, 14f, 2f, TEXTURE_WIDTH, TEXTURE_HEIGHT) - val scrollBarButton = WidgetLocation.SCROLL.element(0f, 0f, 12f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) + val scrollBarButton = WidgetLocation.SCROLL.element(0f, 0f, 12f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) val scrollBarButtonHover = WidgetLocation.SCROLL.element(0f, 15f, 12f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) val scrollBarButtonPress = WidgetLocation.SCROLL.element(0f, 30f, 12f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) val scrollBarButtonDisabled = WidgetLocation.SCROLL.element(0f, 53f, 12f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) + + val scrollSlimBarTop = WidgetLocation.SCROLL.element(14f, 45f, 8f, 2f, TEXTURE_WIDTH, TEXTURE_HEIGHT) + val scrollSlimBarBody = WidgetLocation.SCROLL.element(14f, 46f, 8f, 6f, TEXTURE_WIDTH, TEXTURE_HEIGHT) + val scrollSlimBarBottom = WidgetLocation.SCROLL.element(14f, 51f, 8f, 2f, TEXTURE_WIDTH, TEXTURE_HEIGHT) + + val scrollSlimBarButton = WidgetLocation.SCROLL.element(14f, 0f, 6f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) + val scrollSlimBarButtonHover = WidgetLocation.SCROLL.element(14f, 15f, 6f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) + val scrollSlimBarButtonPress = WidgetLocation.SCROLL.element(14f, 30f, 6f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) + val scrollSlimBarButtonDisabled = WidgetLocation.SCROLL.element(14f, 53f, 6f, 15f, TEXTURE_WIDTH, TEXTURE_HEIGHT) } diff --git a/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ScrollbarBackgroundPanel.kt b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ScrollbarBackgroundPanel.kt new file mode 100644 index 000000000..8a6be9bb6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/mc/otm/client/screen/panels/ScrollbarBackgroundPanel.kt @@ -0,0 +1,166 @@ +package ru.dbotthepony.mc.otm.client.screen.panels + +import net.minecraft.client.gui.screens.Screen + +open class ScrollbarBackgroundPanel( + screen: S, + parent: EditablePanel<*>?, + x: Float = 0f, + y: Float = 0f, + width: Float = 30f, + height: Float = 30f, + alwaysShowScrollbar: Boolean = false, + autoResizeForScrollbar: Boolean = true, + autoMoveForScrollbar: Boolean = true, + scrollStep: Float = AbstractSlotPanel.SIZE, +) : BackgroundPanel(screen, parent, x, y, width, height) { + /** + * Whenever is to hide scrollbar when there is nothing to scroll + */ + var alwaysShowScrollbar = alwaysShowScrollbar + set(value) { + if (field != value) { + field = value + determineScrollbarVisible() + } + } + + private var lastResizedForScrollbar: Boolean = false + private var lastMovedForScrollbar: Boolean = false + + /** + * Whenever is to auto resize width when scrollbar has to be shown + * + * Has no effect if [alwaysShowScrollbar] is true + */ + var autoResizeForScrollbar = autoResizeForScrollbar + set(value) { + if (field != value) { + field = value + determineScrollbarVisible() + } + } + + /** + * Whenever is to auto move panel along X axis when scrollbar has to be shown (given no [dock] is [Dock.NONE]) + * + * Has no effect if [alwaysShowScrollbar] is true + */ + var autoMoveForScrollbar = autoMoveForScrollbar + set(value) { + if (field != value) { + field = value + determineScrollbarVisible() + } + } + + private fun getMaxScroll(it: AnalogScrollBarPanel<*>): Float { + return (canvas.childrenRectHeight - canvas.height).coerceAtLeast(0f) + } + + private fun onScrollUpdate(it: AnalogScrollBarPanel<*>, old: Float, new: Float) { + this.canvas.yOffset = -new + } + + val scrollbar = AnalogScrollBarPanel(screen, this, this::getMaxScroll, smoothScrollCallback = this::onScrollUpdate, isSlim = true, scrollStep = scrollStep) + + fun determineScrollbarVisible() { + if (alwaysShowScrollbar) { + scrollbar.visible = true + + if (lastResizedForScrollbar) { + width -= scrollbar.width + lastResizedForScrollbar = false + } + + if (lastMovedForScrollbar) { + x += scrollbar.width + lastMovedForScrollbar = false + } + } else { + if (canvas.childrenRectHeight > (height - dockPadding.top - dockPadding.bottom)) { + if (!scrollbar.visible) { + scrollbar.visible = true + + if (autoResizeForScrollbar) { + width += scrollbar.width + lastResizedForScrollbar = true + } + + if (autoMoveForScrollbar && dock == Dock.NONE) { + x -= scrollbar.width + lastMovedForScrollbar = true + } + } + } else { + if (scrollbar.visible) { + scrollbar.visible = false + + if (lastResizedForScrollbar) { + width -= scrollbar.width + lastResizedForScrollbar = false + } + + if (lastMovedForScrollbar) { + x += scrollbar.width + lastMovedForScrollbar = false + } + } + } + } + } + + val canvas = object : EditablePanel(screen, this@ScrollbarBackgroundPanel) { + init { + scissor = true + } + + override fun performLayout() { + super.performLayout() + determineScrollbarVisible() + } + + override fun mouseScrolledInner(x: Double, y: Double, scroll: Double): Boolean { + scrollbar.mouseScrolledInner(x, y, scroll) + return true + } + } + + fun addPanel(panel: EditablePanel<*>) { + panel.parent = canvas + } + + init { + scrollbar.dock = Dock.RIGHT + canvas.dock = Dock.FILL + scrollbar.visible = alwaysShowScrollbar + } + + companion object { + fun padded( + screen: S, + parent: EditablePanel<*>?, + x: Float = 0f, + y: Float = 0f, + width: Float = 30f, + height: Float = 30f, + alwaysShowScrollbar: Boolean = false, + autoResizeForScrollbar: Boolean = true, + autoMoveForScrollbar: Boolean = true, + scrollStep: Float = AbstractSlotPanel.SIZE, + ) = ScrollbarBackgroundPanel(screen, parent, x, y, width + 6f + (if (alwaysShowScrollbar) ScrollBarConstants.SLIM_WIDTH else 0f), height + 6f, alwaysShowScrollbar, autoResizeForScrollbar, autoMoveForScrollbar, scrollStep) + + fun paddedCenter( + screen: S, + parent: EditablePanel<*>?, + x: Float = 0f, + y: Float = 0f, + width: Float = 30f, + height: Float = 30f, + alwaysShowScrollbar: Boolean = false, + autoResizeForScrollbar: Boolean = true, + autoMoveForScrollbar: Boolean = true, + scrollStep: Float = AbstractSlotPanel.SIZE, + ) = ScrollbarBackgroundPanel(screen, parent, x - 3f, y - 3f - (if (alwaysShowScrollbar) ScrollBarConstants.SLIM_WIDTH / 2f else 0f), width + 6f + (if (alwaysShowScrollbar) ScrollBarConstants.SLIM_WIDTH else 0f), height + 6f, alwaysShowScrollbar, autoResizeForScrollbar, autoMoveForScrollbar, scrollStep) + } +} diff --git a/src/main/resources/assets/overdrive_that_matters/textures/gui/scroll.png b/src/main/resources/assets/overdrive_that_matters/textures/gui/scroll.png index 6c42618a7221c9fbbe498a579c5d6e6f481c706e..a5c44ed983d58adfdf7e09498a8480011c370b01 100644 GIT binary patch literal 779 zcmV+m1N8ifP)EX>4Tx04R}tkv&MmKpe$iTT81{9PA*{AwzYtAS$ApRIvyaN?V~-2a`*`ph-iL z;^HW{799LotU9+0Yt2!bCVj!sUBE>hzEl0u6Z503ls?%w0>9pG(NnPT*e1BzFVPW-reS>)G@?ahO;rb+OdNtgI` zb6DoQ#aXG=S^J*+g`tAFlHoed2ohLA5-A9fP)7w-n26D;l42lD`*9cluy{D4^000SaNLh0L z01FcU01FcV0GgZ_00007bV*G`2j&SJ1vnb_AYTIj009R{L_t(o!|jy84TK;RM28Ir z+tG`OwqYmI1= z_HNGhxmirpbUUPl8d_D=Y47H2pROAb6EP9Tyf9n4y^u*90Q$anN=zq8vXZqD|pHi?*si8z`#L>pU5w%&R@u| z7c^-|Qd}Gb*Mfr|i&X~~XI&j!1wrrw#L>w~(M3x9Us7m)5#zyeKi=JY+`R+5%_>t2 z{{*0@rk+m3g{CzsL+NnOV%jb$s2!!`HhQ&vHNa=jf9Q1_L|-@f_21 zlX!!8X4BL-?-NH@S(J&-iN|%iAn_yDC5PWQmu&X(%!r;%&l5+8g;EzwUChe5LOe|z zl@*opy*Y<}WzJiim1>=}@5x^nDySfFw^#(;&*+=-z`!lgyXN%P-pAn`sOcK7!0nRb6aNPKdZ0@`+a00006VoOIv06G9V06P65qO6m| z0U&NML&%TucI3hy~f-`Z!vN#V5jHs2JYyh zOzRwTxKN!KxAxEldnxsBD=bpcg*l~NEX9=

3RrcE|1!jPbhcd(; zh?8Y{Pqs2WUM^=R`NOOE$r&qo$RHVil$U%3KxrYJW(r^>P1a4#|yfp?jDRa z3*^6Lb%N`b?D=WQ-l!!b?=2ZCTC#7+l8Hx_?Eh@Z^dC#^Y9;C=h!Mjfc86`Uw`{$+ zqOg0-W%ioAX$DofbQcD>k=9!4tI_$><{fMAriNnNr9`m2FbrQal&h)>a}?;AbLC+z{sG8k^9X>w%J@Fq-?TR2!+@1fi=0s#~>cwq0pxt zy%P}|AE6%(J~D;c+#WL8x)E%&G^3&G4b8pU*7T6(<=Os?n7eG1J*RrI*1X9!5FD=| z2GzwaVupCOEh&lhmj8xq#DotEW4RBn&T+fc3O4+$Ex@JaUX@y2o5#cl>Ngk_B_&pr zhg3;STww19c?};~T9aFRbaHEcGhYNd7AGDN8y}G~+(DB2NR6E5c4@S&kM`(oW8X%X zjDs6AhK&GQE>BySb#mpU93ed2~b#UPgcYUJ?avAaQGw*T(BY-kPMN&o;3Jd#nA@pbR;ldb zAV+1_t!kECw+{=iKe&0!vWp`%KkUbtX90m+bOQ(^U7+|?p~FL910w#w!AGXJ{l6AL zqZOe<8r876O^E1Dms?(*?TT*=1%yWkdvn4BiDwUN$Xz!FHrz4@lzb+1co=M;BtJU% z$dvrf?UY(Nuo;c8zD*~#?sO9bHZBOzF-X zvPzXiwW^BRr7o*pp3MO)3FhgG1D-p#=aC?vz>*oUzXvFpqrzD3BU5IS+oQNRh%~n8 U;sAw3>r$udgQ)hu%n9iH1pzp(k^lez delta 324 zcmdn4b5PtlJu|mJp&~g=q0GS0kO2tzfY=4Z0D@^iEC|Gmj6gydh&h}lFff2H5QqRV z3lK|840vrA4-#QuU}lEUtQruSJrF{3)<7CJ-=HL1-`u zaui4o2$&{Ke#2EWxrO%|6ARnqg95^n`}x?JSd@TFF(wwP%^UbWGcvL1!)4gK!7^;g zn{NtyXJq1tfy;1IfMmEiAl?BQ%`>@CSbg#YVF@7SnH(j2WU@ZTl*y|^{F%6V;kvk1 Og3aSPxLIG6gAo9OH#sN(