From b66bf4cc36238eb23b684d1955331d86433aecc9 Mon Sep 17 00:00:00 2001 From: Khopa Date: Thu, 4 Jul 2019 19:04:31 +0200 Subject: [PATCH] Qt Map drawn with line and frontline --- qt_ui/main.py | 10 ++- qt_ui/uiconstants.py | 40 +++++++---- qt_ui/widgets/QMapControlPoint.py | 30 ++++---- qt_ui/widgets/QMapGroundObject.py | 28 ++++++++ qt_ui/windows/QLiberationMap.py | 108 +++++++++++++++++++++++++---- qt_ui/windows/QLiberationWindow.py | 44 ++++++++++-- resources/ui/misc/new.png | Bin 0 -> 852 bytes resources/ui/misc/open.png | Bin 0 -> 2073 bytes resources/ui/misc/save.png | Bin 0 -> 1187 bytes 9 files changed, 213 insertions(+), 47 deletions(-) create mode 100644 resources/ui/misc/new.png create mode 100644 resources/ui/misc/open.png create mode 100644 resources/ui/misc/save.png diff --git a/qt_ui/main.py b/qt_ui/main.py index 94aa9483..9ecec5e3 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -4,23 +4,29 @@ from time import sleep from PySide2.QtGui import QPixmap from PySide2.QtWidgets import QApplication, QLabel, QSplashScreen +from qt_ui import uiconstants from qt_ui.windows.QLiberationWindow import QLiberationWindow from userdata import persistency + if __name__ == "__main__": app = QApplication(sys.argv) + # Splash screen setup pixmap = QPixmap("../resources/ui/splash_screen.png") splash = QSplashScreen(pixmap) splash.show() + # Load stuff persistency.setup(sys.argv[1]) - sleep(2) + sleep(0.5) + uiconstants.load_icons() app.processEvents() + # Start window window = QLiberationWindow() window.show() - splash.finish(window) + sys.exit(app.exec_()) \ No newline at end of file diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index 3e0c2ed3..9aec3696 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -1,7 +1,11 @@ # URL for UI links +from typing import Dict + from PySide2.QtGui import QColor, QFont, QPixmap -URLS = { +from theater.theatergroundobject import CATEGORY_MAP + +URLS : Dict[str, str] = { "Manual": "https://github.com/shdwp/dcs_liberation/wiki/Manual", "Troubleshooting": "https://github.com/shdwp/dcs_liberation/wiki/Troubleshooting", "Modding": "https://github.com/shdwp/dcs_liberation/wiki/Modding-tutorial", @@ -10,7 +14,7 @@ URLS = { "Issues": "https://github.com/shdwp/dcs_liberation/issues" } -COLORS = { +COLORS: Dict[str, QColor] = { "red": QColor(255, 125, 125), "bright_red": QColor(200, 64, 64), "blue": QColor(164, 164, 255), @@ -18,7 +22,8 @@ COLORS = { "white": QColor(255, 255, 255), "green": QColor(128, 186, 128), "bright_green": QColor(64, 200, 64), - "black": QColor(0, 0, 0) + "black": QColor(0, 0, 0), + "black_transparent": QColor(0, 0, 0, 64) } @@ -27,13 +32,22 @@ CP_SIZE = 25 FONT = QFont("Arial", 12, weight=5, italic=True) -""" -ICONS = { - "Dawn": QPixmap("../resources/ui/daytime/dawn.png"), - "Day": QPixmap("../resources/ui/daytime/day.png"), - "Dusk": QPixmap("../resources/ui/daytime/dusk.png"), - "Night": QPixmap("../resources/ui/daytime/night.png"), - "Money": QPixmap("../resources/ui/misc/money_icon.png"), - "Ordnance": QPixmap("../resources/ui/misc/ordnance_icon.png"), -} -""" +ICONS: Dict[str, QPixmap] = {} + +def load_icons(): + + ICONS["New"] = QPixmap("../resources/ui/misc/new.png") + ICONS["Open"] = QPixmap("../resources/ui/misc/open.png") + ICONS["Save"] = QPixmap("../resources/ui/misc/save.png") + + ICONS["Dawn"] = QPixmap("../resources/ui/daytime/dawn.png") + ICONS["Day"] = QPixmap("../resources/ui/daytime/day.png") + ICONS["Dusk"] = QPixmap("../resources/ui/daytime/dusk.png") + ICONS["Night"] = QPixmap("../resources/ui/daytime/night.png") + ICONS["Money"] = QPixmap("../resources/ui/misc/money_icon.png") + ICONS["Ordnance"] = QPixmap("../resources/ui/misc/ordnance_icon.png") + + ICONS["target"] = QPixmap("../resources/ui/ground_assets/target.png") + ICONS["cleared"] = QPixmap("../resources/ui/ground_assets/cleared.png") + for category in CATEGORY_MAP.keys(): + ICONS[category] = QPixmap("../resources/ui/ground_assets/" + category + ".png") diff --git a/qt_ui/widgets/QMapControlPoint.py b/qt_ui/widgets/QMapControlPoint.py index ad4f2f97..54cfb309 100644 --- a/qt_ui/widgets/QMapControlPoint.py +++ b/qt_ui/widgets/QMapControlPoint.py @@ -14,28 +14,32 @@ class QMapControlPoint(QGraphicsRectItem): self.parent = parent self.setAcceptHoverEvents(True) self.setZValue(1) - self.setToolTip(self.model.base) + self.setToolTip(self.model.name) def paint(self, painter, option, widget=None): #super(QMapControlPoint, self).paint(painter, option, widget) - painter.save() - painter.setRenderHint(QPainter.Antialiasing) - painter.setBrush(self.brush_color) - painter.setPen(self.pen_color) - if self.isUnderMouse(): - painter.setBrush(CONST.COLORS["green"]) + if self.parent.get_display_rule("cp"): + painter.save() + painter.setRenderHint(QPainter.Antialiasing) + painter.setBrush(self.brush_color) painter.setPen(self.pen_color) - painter.drawEllipse(option.rect) + if self.isUnderMouse(): + painter.setBrush(CONST.COLORS["green"]) + painter.setPen(self.pen_color) - r = option.rect - painter.setPen(QPen(CONST.COLORS["white"], CONST.CP_SIZE/5)) - painter.setBrush(CONST.COLORS["white"]) - painter.drawLine(QLine(r.x(), r.y(), r.x()+r.width(), r.y()+r.height())) + painter.drawEllipse(option.rect) - painter.restore() + r = option.rect + painter.setPen(QPen(CONST.COLORS["white"], CONST.CP_SIZE/5)) + painter.setBrush(CONST.COLORS["white"]) + painter.drawLine(QLine(r.x()+CONST.CP_SIZE/5, r.y()+CONST.CP_SIZE/5, + r.x()+r.width()-CONST.CP_SIZE/5, + r.y()+r.height()-CONST.CP_SIZE/5)) + + painter.restore() def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent): self.update() diff --git a/qt_ui/widgets/QMapGroundObject.py b/qt_ui/widgets/QMapGroundObject.py index e69de29b..d4fd1078 100644 --- a/qt_ui/widgets/QMapGroundObject.py +++ b/qt_ui/widgets/QMapGroundObject.py @@ -0,0 +1,28 @@ +from PySide2.QtWidgets import QGraphicsRectItem + +import qt_ui.uiconstants as CONST +from theater import TheaterGroundObject, ControlPoint + + +class QMapGroundObject(QGraphicsRectItem): + + def __init__(self, parent, x: float, y: float, w: float, h: float, cp: ControlPoint, model: TheaterGroundObject): + super(QMapGroundObject, self).__init__(x, y, w, h) + self.model = model + self.cp = cp + self.parent = parent + self.setAcceptHoverEvents(True) + self.setZValue(2) + self.setToolTip(cp.name + "'s " + self.model.category) + + + def paint(self, painter, option, widget=None): + #super(QMapControlPoint, self).paint(painter, option, widget) + + if self.parent.get_display_rule("go"): + painter.save() + if not self.model.is_dead and not self.cp.captured: + painter.drawPixmap(option.rect, CONST.ICONS[self.model.category]) + else: + painter.drawPixmap(option.rect, CONST.ICONS["cleared"]) + painter.restore() diff --git a/qt_ui/windows/QLiberationMap.py b/qt_ui/windows/QLiberationMap.py index ec60770c..e6ad8084 100644 --- a/qt_ui/windows/QLiberationMap.py +++ b/qt_ui/windows/QLiberationMap.py @@ -1,10 +1,16 @@ +from typing import Dict + from PySide2.QtCore import Qt from PySide2.QtGui import QPixmap, QBrush, QColor, QWheelEvent, QPen, QFont from PySide2.QtWidgets import QWidget, QGraphicsWidget, QGraphicsView, QFrame, QGraphicsTextItem +from gen import Conflict from qt_ui.widgets.QMapControlPoint import QMapControlPoint +from qt_ui.widgets.QMapGroundObject import QMapGroundObject from qt_ui.windows.QLiberationScene import QLiberationScene from dcs import Point + +from theater import ControlPoint from userdata import persistency from game import Game import qt_ui.uiconstants as CONST @@ -12,18 +18,28 @@ import qt_ui.uiconstants as CONST class QLiberationMap(QGraphicsView): + instance = None + display_rules: Dict[str, bool] = { + "cp": True, + "go": True, + "lines": True, + "ennemy_sam_ranges": True, + "ally_sam_ranges": True + } + def __init__(self): super(QLiberationMap, self).__init__() + QLiberationMap.instance = self + + self.frontline_vector_cache = {} + self.setMinimumSize(800,600) + self.setMaximumHeight(2160) self._zoom = 0 - self.init_scene() - game = persistency.restore_game() self.loadGame(game) - - def init_scene(self): scene = QLiberationScene(self) scene.addText("Hello World") @@ -39,26 +55,80 @@ class QLiberationMap(QGraphicsView): def loadGame(self, game: Game): self.game = game + scene = self.scene() + self.reload_scene() + + def reload_scene(self): scene = self.scene() scene.clear() scene.addPixmap(QPixmap("../resources/" + self.game.theater.overview_image)) for cp in self.game.theater.controlpoints: pos = self._transform_point(cp.position) - scene.addItem(QMapControlPoint(self, pos[0]-CONST.CP_SIZE/2, pos[1]-CONST.CP_SIZE/2, CONST.CP_SIZE, CONST.CP_SIZE, cp)) + scene.addItem(QMapControlPoint(self, pos[0] - CONST.CP_SIZE / 2, pos[1] - CONST.CP_SIZE / 2, CONST.CP_SIZE, + CONST.CP_SIZE, cp)) - #e = scene.addEllipse(pos[0]-CONST.CP_SIZE/2, pos[1]-CONST.CP_SIZE/2, CONST.CP_SIZE, CONST.CP_SIZE, QPen(brush=QBrush(color=color), width=5), brush=color) + # e = scene.addEllipse(pos[0]-CONST.CP_SIZE/2, pos[1]-CONST.CP_SIZE/2, CONST.CP_SIZE, CONST.CP_SIZE, QPen(brush=QBrush(color=color), width=5), brush=color) text = scene.addText(cp.name, font=QFont("Trebuchet MS", 10, weight=5, italic=False)) - text.setPos(pos[0]+CONST.CP_SIZE, pos[1]-CONST.CP_SIZE/2) - - for go in cp.ground_objects: - pos = self._transform_point(go.position) - e = scene.addEllipse(pos[0] - CONST.CP_SIZE / 2, pos[1] - CONST.CP_SIZE / 2, CONST.CP_SIZE, - CONST.CP_SIZE, QPen(brush=QBrush(color=CONST.COLORS["green"]), width=5), brush=CONST.COLORS["green"]) + text.setPos(pos[0] + CONST.CP_SIZE, pos[1] - CONST.CP_SIZE / 2) + for ground_object in cp.ground_objects: + go_pos = self._transform_point(ground_object.position) + scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 16, 16, cp, ground_object)) + + if self.get_display_rule("lines"): + self.scene_create_lines_for_cp(cp) + + def scene_create_lines_for_cp(self, cp: ControlPoint): + scene = self.scene() + pos = self._transform_point(cp.position) + for connected_cp in cp.connected_points: + pos2 = self._transform_point(connected_cp.position) + if connected_cp.captured != cp.captured: + color = CONST.COLORS["red"] + elif connected_cp.captured and cp.captured: + color = CONST.COLORS["blue"] + else: + color = CONST.COLORS["black_transparent"] + + pen = QPen(brush=color) + pen.setColor(color) + pen.setWidth(4) + scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) + + if cp.captured and not connected_cp.captured and Conflict.has_frontline_between(cp, connected_cp): + frontline = self._frontline_vector(cp, connected_cp) + if not frontline: + continue + + frontline_pos, heading, distance = frontline + + if distance < 10000: + frontline_pos = frontline_pos.point_from_heading(heading + 180, 5000) + distance = 10000 + + start_coords = self._transform_point(frontline_pos, treshold=10) + end_coords = self._transform_point(frontline_pos.point_from_heading(heading, distance), + treshold=60) + + frontline_pen = QPen(brush=CONST.COLORS["bright_red"]) + frontline_pen.setColor(CONST.COLORS["bright_red"]) + frontline_pen.setWidth(4) + frontline_pen.setStyle(Qt.DashDotLine) + # frontline_pen.setDashPattern([0,1]) + scene.addLine(start_coords[0], start_coords[1], end_coords[0], end_coords[1], pen=frontline_pen) + def _frontline_vector(self, from_cp: ControlPoint, to_cp: ControlPoint): + # Cache mechanism to avoid performing frontline vector computation on every frame + key = str(from_cp.id) + "_" + str(to_cp.id) + if key in self.frontline_vector_cache: + return self.frontline_vector_cache[key] + else: + frontline = Conflict.frontline_vector(from_cp, to_cp, self.game.theater) + self.frontline_vector_cache[key] = frontline + return frontline def wheelEvent(self, event: QWheelEvent): @@ -101,3 +171,17 @@ class QLiberationMap(QGraphicsView): #Y += 5 return X > treshold and X or treshold, Y > treshold and Y or treshold + + @staticmethod + def set_display_rule(rule: str, value: bool): + QLiberationMap.display_rules[rule] = value + QLiberationMap.instance.reload_scene() + QLiberationMap.instance.update() + + @staticmethod + def get_display_rules() -> Dict[str, bool]: + return QLiberationMap.display_rules + + @staticmethod + def get_display_rule(rule) -> bool: + return QLiberationMap.display_rules[rule] diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index 154c757a..968080c4 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -1,10 +1,10 @@ from PySide2.QtGui import QIcon -from PySide2.QtWidgets import QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QMenuBar, QMainWindow +from PySide2.QtWidgets import QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QMenuBar, QMainWindow, QAction, QToolBar import webbrowser from qt_ui.uiconstants import URLS from qt_ui.windows.QLiberationMap import QLiberationMap - +import qt_ui.uiconstants as CONST class QLiberationWindow(QMainWindow): @@ -19,16 +19,18 @@ class QLiberationWindow(QMainWindow): self.setWindowIcon(QIcon("../resources/icon.png")) self.statusBar().showMessage('Ready') self.init_menubar() + self.init_toolbar() okButton = QPushButton("OK") cancelButton = QPushButton("Cancel") hbox = QHBoxLayout() hbox.addStretch(1) - hbox.addWidget(okButton) - hbox.addWidget(cancelButton) + """hbox.addWidget(okButton) + hbox.addWidget(cancelButton)""" self.liberation_map = QLiberationMap() + hbox.addWidget(self.liberation_map) vbox = QVBoxLayout() @@ -40,13 +42,20 @@ class QLiberationWindow(QMainWindow): self.setCentralWidget(central_widget) + def init_toolbar(self): + self.tool_bar = self.addToolBar("File") + self.tool_bar.addAction(QIcon(CONST.ICONS["New"]), "New") + self.tool_bar.addAction(QIcon(CONST.ICONS["Open"]), "Open") + self.tool_bar.addAction(QIcon(CONST.ICONS["Save"]), "Save") + + def init_menubar(self): self.menu = self.menuBar() file_menu = self.menu.addMenu("File") - file_menu.addAction("New Game") - file_menu.addAction("Open") - file_menu.addAction("Save") + file_menu.addAction(QIcon(CONST.ICONS["New"]), "New Game") + file_menu.addAction(QIcon(CONST.ICONS["Open"]), "Open") + file_menu.addAction(QIcon(CONST.ICONS["Save"]), "Save") file_menu.addAction("Save As") help_menu = self.menu.addMenu("Help") @@ -57,3 +66,24 @@ class QLiberationWindow(QMainWindow): help_menu.addAction("Contribute", lambda: webbrowser.open_new_tab(URLS["Repository"])) help_menu.addAction("Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"])) help_menu.addAction("Report an issue", lambda: webbrowser.open_new_tab(URLS["Issues"])) + + displayMenu = self.menu.addMenu("Display") + + tg_cp_visibility = QAction('Control Point', displayMenu) + tg_cp_visibility.setCheckable(True) + tg_cp_visibility.setChecked(True) + tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked())) + + tg_go_visibility = QAction('Ground Objects', displayMenu) + tg_go_visibility.setCheckable(True) + tg_go_visibility.setChecked(True) + tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked())) + + tg_line_visibility = QAction('Lines', displayMenu) + tg_line_visibility.setCheckable(True) + tg_line_visibility.setChecked(True) + tg_line_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("lines", tg_line_visibility.isChecked())) + + displayMenu.addAction(tg_go_visibility) + displayMenu.addAction(tg_cp_visibility) + displayMenu.addAction(tg_line_visibility) diff --git a/resources/ui/misc/new.png b/resources/ui/misc/new.png new file mode 100644 index 0000000000000000000000000000000000000000..12131b01008a3ec29ec69f8b3f65c4b3c15b60d6 GIT binary patch literal 852 zcmV-a1FQUrP)IE2xvpcKl890$E8eWxeF8o0mWjmIG{M&7eWO9CVRojsy7@;&dpQ!a9t0J7gPX(5I|>76wd9YM;~Wq>Ghkp6rY@= zsi|pt_x=M#qtRtve?bMn?-1>_C5DBX;{}vm&C%raD|#~il%B<2&`e?uix*V@+JPAL z6CgbQkZR2~6*sGtFK$zbV~w|k*M3m}@OjM5T^_Z^d)w}xJHF(IUAVpO;*`oW;C1B2 zbys585IO4RGepz@rv~8kI;}Y(n3!mCfS7AgQ=I?+DlNG&;!abH5Df(KZTl#;d_@48 zJKJjfX^uQ>h#Iar6M#C8w^So4*Tn<|1^_iEoENLSCA-QKz)fKe6)JLW%zjz|XlmW9 z@suY3Q`{VJZCgNKnyVajp6h`@6-N=kYKbun)^`LT3}hx^wrO>XsHzE2w#B^A1Ngov zV}~hV3>btQ09#E(0CWRWn5~q>9Op*n90161U8$i6fc^Yd5_2;F2sIc&=-PaaDX2~W zTL1&dm}fQw05ax<-uM>_{4Cgt02pUFPqb7NBbWjJ0t`O;#ZxHoL%3#1_iOo5hu6MT z0mxO4x=yW24v?>f-nYV%Jx`{y-lMT`>)O!u6_~SbsQ_#=q-z-e;1PdX7gN*|3t4aE z?!Drr!OKg0ZH#q?HK_Wx9NlHvJdYn4{+`bsH@(TacqVT~0BG|6fti$^=|8_20YcOC e{Jor>rG5iZY$c(Rl?rzN0000RP)CWtKz1rEEwa&Ubd$6s{da=&VIP1;2`BM0`Wb;D z{M-!)vd0-ix#k=pmKgFxQgYk(eOi$0QlJD2NbOI5d5567avIIj< zv@<}9dj`!tE;Q@xs8{wNhf{?zUNu9!u=J3D!C+h_ze{}K1V|htI6?j0>T@UYxhDij zhnKMl|M#(rT!AcZ}eF)wgP_d zAX10jekLr}U#(CrCA<(opAbNs2#S{v>vbjZBPMG2>Dlh2!oC#Mk@9m9Zc z2%^^3CBjkLC=!RpAFqNhVw6pZB#Q!^oqj*T-F(Vw$mLX*nO_ zNX-3DeeTpEomGUYnz~IsN92z9JUkMF)7=lf!HB}*Vw`?29z6pA^e+N~guKQ7E$%u) zC{dC3_66}y+`oN81yr%;K@{IH8|1O-3y*riL6}qGd@SEo}8X#Q_kq zZ|eG3QMpHgF(8Pc07FLLD$kpMaaUZZl&Qk=tr>Kq76&g@g}Kbh`}mD}bLg zSob6#9DIaI(-=7VVTPPm{Z+FbtPVT4Iv?2~9(pg}6TG@z0w%t}gOqRBIG>z~1AjdP zW3QR%ZYsA%*Mp?X$w>b?i;UGgQ?+sSsoXX8X`%zES@sOGBngFV;lVB@d3_10@@V;lg>dY2BN>v^RZ2^?j@k8NdQ$X;_ z_a$H}ZfeJ=gha#~If~qTHdcoJ4Lhqwh-3dbkKgY5JsSA}oWGEOlkpdDqpb%S#7{~T>2A7M zsP8QLl$cN`w?OF*Jo#7bDJpj@!Dj^fwz=d)jlRsXTKwbW85}(HH?VT^u<$rb~rBzaiB|lB=W)A?(%51dO$!?mU^ ze8O#IHd*n50bG}w!BKl4^90wps7K`#)>4RXJ_(`usyx1v$_9j`-;Pq{6K%jV&lNRO%>FPb&x$A5bBEmwG~ zsNAo;tlZJ!2;Bu-bsyf#uSL0b05WG_^}?r@soeJn(y81#_yhC8@e_i|9PXV^3ptdy z4Z_G4qZfz6FCzC2|JrSe-C>#48@AYMyIwNBFNyyFX1QPd5r93<00000NkvXXu0mjf DP*&js literal 0 HcmV?d00001 diff --git a/resources/ui/misc/save.png b/resources/ui/misc/save.png new file mode 100644 index 0000000000000000000000000000000000000000..daba865fafd22fa18e7c0488eb699b79d3554170 GIT binary patch literal 1187 zcmV;U1YG-xP)5-PJz!8#8DIjU4h)0S{a*K!i>mJVVCD^kQ5IHGsm^=1>z+FI)Tw&v zz2|;5 zYXiKvXAppke-?cB?jC^K0C4>59N#}M1b8m}xWN9m&Tq?d4;^`$3nP}(pC4rW)qR_5 zKt-??KzML0Bg?XF0WdI@VU185d=w)kA`~mw7?IVLbLY;3D!2sF6tWD`ER0DCNfO?7 zI*{j(x1rU7b}No;C~~s;IMxc|ue?cX@%rWftQD*k1h7Wfy?f^tYwL!tfLf?lf$D8h zjjsf?g7=`NgaKy-HG&AJ3Y9FwBOo3K!0;X~0^TDMCKY^$Bg7#dJRx2{O;{K}*Gh>7 ztf1Os0Gczu1&UJ#X#$l5vNZMt5(iG9=s;dTE04VeOF6W2$O}}5aXkPkPoHKY21CJ!U{_^v8Xy`U*q`vX%#{A#01X5mtL``3Wj}4_b=eEU&o)}BH zcC!Nrol+61khoCqTE#GZvCBZkunzPltylR~F5%(E(lYhTaDB;yE$K=b5S$vobLD22 zD>u8WQCC0Ls$_n}C!#5=1r@`b3$=HqY!|4(sUXN2HLsgB8jo2qp8k93?-T&>g72GH zFe_J90#zZ=ko@OU-}3936D%#>xTpG8Y6Cp=>`tC}VFC}RGg~nb1AK?~_rJly!UDH$ z-J&Q8L>2*RmC}ns$#9hnDF1_1m8Hc<-5*m|${p@{U>9F*?@YhOX2JRd6<}qE@f>YdJGB!_d$W)>^#xG@H%40#K{h zaM6gg)QA+E3nyHlDgdZft7KW$r?@xXX>%H>7ePy{I3CBnBed6yF^ELpk2<)ko3Ayf z4fFOGE