From af687dedb198e2a257014ee82a3513f5092f5e45 Mon Sep 17 00:00:00 2001 From: davpapp Date: Mon, 5 Mar 2018 05:20:29 -0500 Subject: [PATCH] Added garbage collection and camera rotation code --- src/CameraCalibrator.java | 40 +++++++++++++++++ src/Inventory.java | 13 ++++++ src/IronMiner.java | 62 ++++++++++++++++++++------ src/ObjectTracker.java | 31 +++++++++++-- target/classes/CameraCalibrator.class | Bin 0 -> 1975 bytes target/classes/Inventory.class | Bin 3436 -> 3805 bytes target/classes/IronMiner.class | Bin 3755 -> 5761 bytes target/classes/ObjectTracker.class | Bin 1860 -> 2746 bytes 8 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 src/CameraCalibrator.java create mode 100644 target/classes/CameraCalibrator.class diff --git a/src/CameraCalibrator.java b/src/CameraCalibrator.java new file mode 100644 index 0000000..8444054 --- /dev/null +++ b/src/CameraCalibrator.java @@ -0,0 +1,40 @@ +import java.awt.AWTException; +import java.awt.Robot; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; + +public class CameraCalibrator { + + Robot robot; + Randomizer randomizer; + + public CameraCalibrator() throws AWTException { + robot = new Robot(); + randomizer = new Randomizer(); + } + + public void rotateUntilObjectFound(String objectNameToLookFor) throws Exception { + ObjectDetector objectDetector = new ObjectDetector(); + BufferedImage screenCapture = objectDetector.captureScreenshotGameWindow(); + + ArrayList detectedObjects = objectDetector.getObjectsInImage(screenCapture, 0.30); + ArrayList detectedObjectsToLookFor = objectDetector.getObjectsOfClassInList(detectedObjects, objectNameToLookFor); + while (detectedObjectsToLookFor.size() == 0) { + randomlyRotateKeyboard(); + screenCapture = objectDetector.captureScreenshotGameWindow(); + detectedObjects = objectDetector.getObjectsInImage(screenCapture, 0.30); + objectDetector.getObjectsOfClassInList(detectedObjects, objectNameToLookFor); + } + } + + private void randomlyRotateKeyboard() throws InterruptedException { + Randomizer randomizer = new Randomizer(); + int keyPressLength = randomizer.nextGaussianWithinRange(350, 1105); + robot.keyPress(KeyEvent.VK_LEFT); + Thread.sleep(keyPressLength); + robot.keyRelease(KeyEvent.VK_LEFT); + Thread.sleep(randomizer.nextGaussianWithinRange(120, 250)); + } +} diff --git a/src/Inventory.java b/src/Inventory.java index cb9c5d4..5beaeca 100644 --- a/src/Inventory.java +++ b/src/Inventory.java @@ -44,6 +44,11 @@ public class Inventory { updateAllInventorySlots(image); } + public void updateLastSlot() throws IOException { + BufferedImage image = robot.createScreenCapture(this.inventoryRectangleToCapture); + updateLastInventorySlot(image); + } + private void updateAllInventorySlots(BufferedImage image) throws IOException { for (int row = 0; row < Constants.INVENTORY_NUM_ROWS; row++) { for (int column = 0; column < Constants.INVENTORY_NUM_COLUMNS; column++) { @@ -52,6 +57,10 @@ public class Inventory { } } + private void updateLastInventorySlot(BufferedImage image) throws IOException { + inventorySlots[Constants.INVENTORY_NUM_ROWS - 1][Constants.INVENTORY_NUM_COLUMNS - 1].updateInventorySlot(image); + } + public void updateAndWriteAllInventoryImages() throws IOException { BufferedImage image = robot.createScreenCapture(this.inventoryRectangleToCapture); ImageIO.write(image, "png", new File("/home/dpapp/Desktop/RunescapeAI/Tests/Inventory/inventory_TO_RENAME.png")); @@ -70,6 +79,10 @@ public class Inventory { return inventorySlots[row][column].getItemNameInInventorySlot(items); } + public boolean isLastSlotInInventoryFull() { + return !inventorySlots[Constants.INVENTORY_NUM_ROWS - 1][Constants.INVENTORY_NUM_COLUMNS - 1].isInventorySlotEmpty(items); + } + public boolean isInventoryFull() { // TODO: this will fail if some unexpected item shows up for (int row = 0; row < Constants.INVENTORY_NUM_ROWS; row++) { diff --git a/src/IronMiner.java b/src/IronMiner.java index 2953469..ee4daf1 100644 --- a/src/IronMiner.java +++ b/src/IronMiner.java @@ -45,16 +45,32 @@ public class IronMiner { } public void run() throws Exception { - while (true) { + long startTime = System.currentTimeMillis(); + long garbageCollectionTime = System.currentTimeMillis(); + int framesWithoutObjects = 0; + + while (((System.currentTimeMillis() - startTime) / 1000.0 / 60) < 85) { long frameStartTime = System.currentTimeMillis(); BufferedImage screenCapture = objectDetector.captureScreenshotGameWindow(); System.out.println("looking for iron ores"); - //int count = objectDetector.getObjectsInImage(screenCapture, 0.6); - ArrayList detectedObjects = objectDetector.getObjectsInImage(screenCapture, 0.60); - //ArrayList ironOres = objectDetector.getObjectsOfClassInList(detectedObjects, "ironOre"); + + ArrayList detectedObjects = objectDetector.getObjectsInImage(screenCapture, 0.30); + ArrayList ironOres = objectDetector.getObjectsOfClassInList(detectedObjects, "ironOre"); System.out.println("Found " + detectedObjects.size() + " objects."); - /*DetectedObject closestIronOre = getClosestObjectToCharacter(ironOres); + if (ironOres.size() == 0) { + framesWithoutObjects++; + System.out.println("no objects found!"); + } + else { + framesWithoutObjects = 0; + } + if (framesWithoutObjects > 50) { + CameraCalibrator cameraCalibrator = new CameraCalibrator(); + cameraCalibrator.rotateUntilObjectFound("ironOre"); + } + + DetectedObject closestIronOre = getClosestObjectToCharacter(ironOres); if (closestIronOre != null) { Rect2d boundingBox = closestIronOre.getBoundingRect2d(); ObjectTracker ironOreTracker = new ObjectTracker(screenCapture, boundingBox); @@ -62,25 +78,43 @@ public class IronMiner { cursor.moveAndLeftClickAtCoordinatesWithRandomness(closestIronOre.getCenterForClicking(), 10, 10); long miningStartTime = System.currentTimeMillis(); - int maxTimeToMine = randomizer.nextGaussianWithinRange(3500, 5000); + int maxTimeToMine = randomizer.nextGaussianWithinRange(3400, 4519); boolean objectTrackingFailure = false; - while (!objectTrackingFailure && !isTimeElapsedOverLimit(miningStartTime, maxTimeToMine)) { + boolean oreAvailable = true; + int oreLostCount = 0; + while (!objectTrackingFailure && oreLostCount < 3 && !isTimeElapsedOverLimit(miningStartTime, maxTimeToMine)) { + long trackingFrameStartTime = System.currentTimeMillis(); + screenCapture = objectDetector.captureScreenshotGameWindow(); + detectedObjects = objectDetector.getObjectsInImage(screenCapture, 0.15); + ironOres = objectDetector.getObjectsOfClassInList(detectedObjects, "ironOre"); objectTrackingFailure = ironOreTracker.update(screenCapture, boundingBox); + oreAvailable = objectDetector.isObjectPresentInBoundingBoxInImage(ironOres, boundingBox, "ironOre"); + if (!oreAvailable) { + oreLostCount++; + } + else { + oreLostCount = 0; + } + System.out.println("Tracking timespan: " + (System.currentTimeMillis() - trackingFrameStartTime)); } - }*/ + } - // TODO: change this so that we only check the last slot for an item. - // - //dropInventoryIfFull(); - System.out.println("Timespan: " + (System.currentTimeMillis() - frameStartTime)); + + // Garbage Collection + if (((System.currentTimeMillis() - garbageCollectionTime) / 1000.0 / 60) > 10) { + System.out.println("Running garbage collection."); + System.gc(); + garbageCollectionTime = System.currentTimeMillis() + randomizer.nextGaussianWithinRange(8500, 19340); + } + dropInventoryIfFull(); } } private void dropInventoryIfFull() throws Exception { - inventory.update(); - if (inventory.isInventoryFull()) { + inventory.updateLastSlot(); + if (inventory.isLastSlotInInventoryFull()) { cursorTask.optimizedDropAllItemsInInventory(cursor, inventory); } } diff --git a/src/ObjectTracker.java b/src/ObjectTracker.java index a69745c..fcb6469 100644 --- a/src/ObjectTracker.java +++ b/src/ObjectTracker.java @@ -1,7 +1,14 @@ import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import javax.imageio.ImageIO; + +import org.opencv.core.CvType; import org.opencv.core.Mat; +import org.opencv.core.MatOfByte; import org.opencv.core.Rect2d; +import org.opencv.imgcodecs.Imgcodecs; import org.opencv.tracking.Tracker; import org.opencv.tracking.TrackerBoosting; @@ -10,9 +17,9 @@ public class ObjectTracker { Tracker objectTracker; private int numberOfFramesLostFor; - public ObjectTracker(BufferedImage image, Rect2d boundingBox) { + public ObjectTracker(BufferedImage image, Rect2d boundingBox) throws IOException { this.objectTracker = TrackerBoosting.create(); - this.objectTracker.init(bufferedImageToMat(image), boundingBox); + this.objectTracker.init(BufferedImage2Mat(image), boundingBox); this.numberOfFramesLostFor = 0; } @@ -23,8 +30,9 @@ public class ObjectTracker { } - public boolean update(BufferedImage image, Rect2d boundingBox) { - boolean trackingSuccessful = objectTracker.update(bufferedImageToMat(image), boundingBox); + public boolean update(BufferedImage image, Rect2d boundingBox) throws IOException { + boolean trackingSuccessful = objectTracker.update(BufferedImage2Mat(image), boundingBox); + System.out.print("Tracking: " + trackingSuccessful + ". Counter at "); updateNumberOfFramesLostFor(trackingSuccessful); return isObjectTrackingFailure(); } @@ -45,6 +53,21 @@ public class ObjectTracker { } private boolean isObjectTrackingFailure() { + System.out.println(numberOfFramesLostFor); return numberOfFramesLostFor > 3; } + + /*private Mat bufferedImageToMat(BufferedImage sourceImage) { + Mat mat = new Mat(sourceImage.getHeight(), sourceImage.getWidth(), CvType.CV_8UC3); + byte[] data = ((DataBufferByte) sourceImage.getRaster().getDataBuffer()).getData(); + mat.put(0, 0, data); + return mat; + }*/ + + private Mat BufferedImage2Mat(BufferedImage image) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(image, "jpg", byteArrayOutputStream); + byteArrayOutputStream.flush(); + return Imgcodecs.imdecode(new MatOfByte(byteArrayOutputStream.toByteArray()), Imgcodecs.CV_LOAD_IMAGE_UNCHANGED); + } } diff --git a/target/classes/CameraCalibrator.class b/target/classes/CameraCalibrator.class new file mode 100644 index 0000000000000000000000000000000000000000..4bd12456c0f1fbbe69ad22f5e2e0088ab34e1aff GIT binary patch literal 1975 zcmaJ?T~pge6g_K<1*!l84j+M15<;*Efuu=kf(d~@piT@lK%nVI+O@qHfsq(VZbIMt z-05R~L7y|zkWMqxAJEtSf&7xDXJunP>M6p~?%u0&&%O8T>aTzP_!Gcwlng`!;`4G} zd2(Ks?3^b9*E101>aP4=PL!lmoLJ56s(c{O=(#zU&v-Vp$rr)InqEl@n4WYB?!Ntl z@&v5xT5U#KY}$70U`C)JncAf5! z&(`zmVl>#XeQi5m7$(!@xq%GSQzx)XmAWjtWv4(2$x2niimeBp?G)3gO#_z&I!{Vj z%g?|Src*&!Pyy@fdM1Vt*S8T1Nu&h&^70@kdul!JDdqS(Zm`5wZP_gK3xUC8>ZB3& zzAUPV2j%T;<*7nO&(g8`f8PrKH0E^+$-c3E_K`SUry{% z#r3#HTK#9cMp3l*VT0x?-4Yo1|GrR&g|OwSQ0)ZeQ#W~mo^zh3=ysje?%~0)en;wI zO=l}99LG%0XyWUE%`P7M_A~%%nC=e2wP@zBxmEW5-jnVBuSQM-0ED)PZ(?q}|(*vyj(NCZX0; zNl9PDW}kt&fij=TC@nT?JgCW*Z0+;g9E1 z2*(46&jwm}n9=e9XHm{9v~u($v;cjhZ_qaS7VXd8pz}2n9CmTo^BTRc=oUdA?;U&* zXyLsTZM-fbj()CKgd+q#!~mZHA7SuRpi4TCXy#j*(9&BB5qNlc>nD~%V8&e;=8(ccdM1pUQ1v#fhR{; zzc>0D67k_9Z1w;6JH9%eNqQ6#*rtbGKIzi{zdE28ns6DNk literal 0 HcmV?d00001 diff --git a/target/classes/Inventory.class b/target/classes/Inventory.class index deb5caad9dde8780703fe9a3fa583aa24c61056e..c585e04d18c8da5570989c07e323e12b5e3ffeec 100644 GIT binary patch delta 968 zcmZ8fOHWfl6#k~Y_txHCpfnbQ7O(^ail$Jc1`WiDQkx(YL7|}10tE`CMOvOd>I3nG zk7QPEj5`*xfY^YFacxZe112VJ+!%j>3*))1Qe!5WbIzP|zWKg0^P~7niTv@`+xGwt z%2lC=1%VJGVQ8@mWuQ$p!#6l^dU>0-y zljtz!h2y(y+h!vX{!?^IR}@^uHSU(0>XW#s!wnLiCL5ejIDD~ilmKp_iGeNXk;Jls zJLzaqMv}jjUa-7Z?YahymchrZ`h&$?n>0iXDrwJ!mi9U*s7F4Wd`RELdU#52GdDtn z3o?$NfNU8WQH7)YyZ%&CqYjw*^QcpxiJP#Oyf!+~sEGCy>LsU9a@KC4Cwr5e&Nd?l zEjU1R1(|9>t)@t2(t%DwW9TLSfAk?@qoSAFQ`{#jb$vVN_nR(k}(2A9|AfPBhed2qi ziIcdK$ildlNT}Mx#H~Ajfd9bg%AGNepfNX@GjlUJ=giEPwy&+`r(f?s0O-P_W%HF* zXCj6p3PErr`_)x3!c{gf|+dh^^GvrydZ5(6}DdYxey zh`>Y?&5Ry|;lnOkG7oA3$0%hBgpb#d$uoaF~ z=0UhHgaERXs1LT?!Cjug#g9+GS^@6h^|L#Hd$dp=2@Z2Uf}QN4nWqA~@u7q9Pj(-N MxliB$R~9_{4KOlxn*aa+ diff --git a/target/classes/IronMiner.class b/target/classes/IronMiner.class index 2cfed24bfb94affe95e51b98784b485a6d0c2222..8a9a8f60113dee8d10adf7da3d88dc987083de36 100644 GIT binary patch delta 3176 zcmZ`*3wRV)75>le?9A?F5|VucHUvl@Xc8Wwv4oFJfsjP((j_2IiGobBld!Ov3A>XJ zTH7s@M@w5;9ZP`oYhuDItKwGdv@c1u zWw#N06=pt%*x*xO5(WtT`H`v%WMZyL9&xC8rb!&9~4E<%Z6j-IIK5}_*<93T|8 zXgzvbYtd54j8&C=HxRx*_@+sWJy!pBwIhlBQ7<@&52 zhj2u}VM0->ZZS4`B5$nX0ephPGic2sEW@#qx9byxvg(?+neMJLdv&9;zpm3v>vj3P zMg_+Rg?SIF(^}_NofA8XPYI+xO_*Pu$5`8L)g^nhZr$D_jjOtvvzoOcyU(aNfzR68 zy|c5kFsS15_yS=@kJ+y;HWG2Y%W6p_JGU;jT1+#|S#rslbxCXUO3g@^J%*m)_ElXG zc7>TVtj1WZ=8a79D!J+8y-X%RZaO{a2!1TVDHUJBX=Z?-4>Vfu)A}-*q-KZ!NrOXl zGti#u*4CPsyy&U-I6tD^FRRF5h{@8|o8W}FaC?O`rR}PPwhNvlxj!V#a6#n$3Zj5fiu(njOxrzM9LobGfG7i?0%bPFuK}6;`rG&-7|W)FJcN1QRb1 z%Bz>!B5l_SI@re2qM;xR6H*Xf{C=*8#DU3a5KFl*0rQB zm6Ds*t!Z*Bd44jMmbO-N?gUy&2#*A88H<FEO*qaMt#^XdNyRe zBbNm;rMUAgV?_wW-#TiI?4~niX7r2|%U?5mF#hK8VVi=Ek-6cv5hh%%Qn-L}B}j-ew=$j90n9Vf$5Y ztG(Pedmc?zX$oY9Wx$7Ws#IyJv)bXODtebf(>eV&UIWWCYqLu4rs_w|`c6{TeGv*u z0XlUKE1LLeb~kti=>SXf z((41(5c(s&90qc@C9o@p-8t;zlMz3wj-X_>hlH~`Sv3gRoo(=hJ;n7W5Im2&*AL;I zXhEbvc)YhZ;va%7mfyfLk%AoVkE-W!bbZ7dQHSu!Xdy?RBlO3M{l)D3bL&LPMXZg5 za5994a(K9**xgVfH3ZxZ!9kc}KO}N?N^+9-Wlf@OZx7aLEyb!a?~sifT)X!o@>);_&cJHmcP3hw!u+ z!Z-Q(R`&j5s1$&Fn*lk8@3H|$57P{6qS@%6YmlHg)=~$y&~|L)`yoZU_@m~&EKEAgUpo(TF4MV&90G!i z96BzlqX+S04o2`Y;y+=@jRHD>pF)NQLE6hnyuflf-HaFUb5T2Wb5bwChegzmU$W%q zq7=N0Uok2N$<68hhNa_N(r@uQ@gZ{xkKp(C0}8pqqxd5`EaEy3;7{zhn5(@Lf9Cp1 zSbsbI%2I%X?!n)9H^|Pq@ps+{;UAoCDO@87TpCN@5-0Fa-s?xfWfVyg4MT@l0kJ44 zNW7w82mb=D(qRU;cx-26L%|^h|5k8RBK`rRyi9VvhO%LV;d=*TpnjN9V%H)92X^6k z`#N>%2mZsnistb8VFXy3%g-D~Ll8W2J3I<7gTRDP(8mS$^9Rm=(-oCF091~2MU>+P zRj_OED$S|0vo|?r6|4rvjhG23-#nH`V!YfA5`46fkMh?VT;$;cE|M{kyB=f%h1YFC zQWj9#j2wBRo&Zx_404K>(}I__9K_VP&VV%p!q?B34=yUWRXAzO0Lv8>x10`Vs=mQsZGT>M;5 zWlS44`zxb~EJ^HDrE=bpozCY05+^T#$t@77fqR${;Fk??fB88CzTbiA!^{)Wo>47V zE-rV5m_guqk{lExGtbhb8T=&nu~flQMPlDMns(qJHgY-LlAW%cyaM`&Q^XXuK!i9$ R#~?zrEV($!3~`iZ{vVxm5(EGM delta 1269 zcmY+D`%_e97{@<{J-f?p2aFY%SP|9>Gk|f)G$tC+8pja=%rr_GjH7#yWtN3@4X$02v4W#0+FiGw=h+&1vFc8Gnn5)LK=N20b z@_>e(N$jvReB~95E_cXa2M;RCUm1N^BNXe~(qnD4Gu19O>|&2>M|c)G8@F*RtV?7s9+W7nF2WHL-0FBdlGXgssfLT6Ni2m!08=!BLK>@|>~- zoZy2X?`uSQ%t5nez)mD<8f`0WiZFu@8CR*Cl9C`NrKQ!DEhDnx>%38gncyP>hg0g_ zV0E^}X@fJIRr_;p3rW`V1{XM}{>WLKcWv|kBX3cQa%=1O(%>rsGMVimiuu~$8}Bp} z=3BlG@|{%lG$qZ{wwe~x?l$;=A5ZSdJ*o}!8v*?CEIOvRV^!rOD;?TeCuomscIb4^ zRrz!|bl2qrGU<2*-Hq@ zeuJErVsPVqS1Kr2u43_gjtnNgk<>^Wf= zg3WmLiRXFo_{2jNO6BD*yypdJqu$}A14P7HE~|FN^UAnKb#+C&l6Q)_4dk&=o`0Js z#Z8SQpOJDUbxcyf!JBGte)aGsj~0`PFy{)*wchd+yVfC@u90`4!cVvH80Bp*!_ShpC$a1%cv2gbD&HvWxUq zmv&CwTCixiL-K5v>+B?oNjY1nW~)p)E?<2o4Q!*4ZV}nziEa>KQgc>xi#JmF9%$lY z!LlUNTB-i2c%t(AOe|fp8hkEjV0QFwj#2iZrMxaH@J*9TAcz(uBWpr>I{{_W6r%sf zD~-DkU3QrPcS~+}h(*^=^2HKa@e#2uightQGR39Q;{s-Rk0{_>$}Y9OurSI`-t|iy Nm&M8w=x2}k%RhvW5u*SA diff --git a/target/classes/ObjectTracker.class b/target/classes/ObjectTracker.class index bcee18eebf744ed7833d2cc7843ac8165f6c3c97..e4f1e3628c9ba4e47bbd377c99b4922006cf5528 100644 GIT binary patch literal 2746 zcmbVOU31e`5Ir}sQDPJljFSK+5Qr(Z12`nm7EIG(Ak=MwDWRbW{XmxGpkQ0BECJ~k z{dnka=zC}S!b97Egef!q0sT>(o+~LrA%RZ&U`bbZ@1EVWXIK8~??3(oa2h`-=o09w zR96h$xnXPiUBgxo6Bt<0?rDXF)>AbfyK)W@kMl?Y>hmo7%EbD7p2zVH>rwTot1j zx@8-M>kM?VR?IC3BrZSDjkaT2t&V~|fjyol(<+oJ;f_G;qE#~z=*EDGUhEd=&gK>p z*n>d@DS@$9BvG;$Gm}y=gkc7)+lJ;C0>`qsXk0JmzHzMB3xRzq;@FYkQ(DCVj0o)B zl;Px@<_Mhm7aBIt#gO@Sqj@gmcn{-7<#Jv&=9DH@r1$I^~w^bwc zCF_B}-j_x9-_xON2Df;VT(Z1wyXKYme`|6{7Gz4r1jc%C25%^MeN!jRuXY@xsp3tX zr3@{X-d-Sq8_jM>WTTc~?4N z`<~bi=B3Ug{QD|qF(=UDSbkUnDR$9|DN@oEe4yZ3q?;qesJJe(jJG9MvZrpS_z$KgV$_Z34(1SWb zSK7-7+`)?5&by z{tJAi;7j&>#Hra_);XE=4l@j%RD6wZs1dWt_uTV@7CEvJN3p#9JS2D3?U)ug6lNcW zyNL!i)V$@|x-nyVQ!Szn!#*jw#9`{En;*?C{y5RSCk{p474GyR!JVC4#kf+ji%(73 z%M*w4>*&uvMe+&u<{$GnzK;FJ*KqJ=zWWI>ztN-%ukkuW$3TiM2I*=D`!I}y*o$L) zJLy zc(CaZb^@oi-4HuG^*KlPG@2%l#eqV-}U9`M~kI4c*5j^d)QsY_%m6Ab8Dki75 zLEYssont}s=qJaL1|KmMBBnwbQUPLu_C8{l97zG9L0?iZ!|9)})2sF`=*urYMkaPU zD1CRN^aX}D!7`qrf~F{dGamSm?;->r_J9OFq2n03)L!m$tObyBTuE6Bk!$9eS;MFD zw&Q2SV~@Hexjv%QEBGl&;H-zD`lkXE>6_%GXv9A)yl$3ui z#qv#(+I)ugBJcJ(?sYxI>aVnm*7FjZ<}wB_L#1EwpnYv;!UQuxJ$s1#5a05&z;~XP G@BaaJ2YCem literal 1860 zcmcIk&ul;=?^djnyQhIOp%%pL{43=$H`LLW6iEZ z`=|6?sTVGlpg|zD_a6EuA>NEP!D&#WR^niJXXl&u-uJ$@^Y`nQe*##>Y5_TciTjPm zGDR^*qYPd>ZuCDZURrO>y+O4MMY|QEmq$Y5x+wU}_ zzTaBYVMiu)m85G*3y8Y{qc`JjoURC559*dFe3DkYT3MNR<&V{NNHL9 zof#ezEZ~Ab=@>o_iwKw(lez!ibH4+aARUU<$ZH6>}82| z*HvFfaxJ!*AHtOSin$vwPu4uIyc~bbjeX)k`9I2?B8;kWu9jJ4>fBfG9!;It!^yvF z?T1+$rShlPLo4&kMD3g-YbgeLhM z@=ouo{cNQ&w};Q!$MvUtKTv$as+wo(=V`@)#jFg@9VneXP&%C{byHZRpx@^g32uX+o@>xqHaGP%(o*U2v(Eqk