From ae5b2a018534d621c0ce7bb0a937f46781adfe5b Mon Sep 17 00:00:00 2001 From: JISAUAY Date: Thu, 13 Nov 2025 15:22:23 -0600 Subject: [PATCH] added some really bad raycasting --- .attachments/raycasting_test.png | Bin 0 -> 45028 bytes Notes.md | 6 ++ README.md | 6 ++ src/Camera.h | 14 ++-- src/Chunk.cpp | 21 ++++- src/World.cpp | 119 ++++++++++++++++++++++++++-- src/World.h | 4 + src/main.cpp | 129 +++++++++++++++++++++++++++---- 8 files changed, 268 insertions(+), 31 deletions(-) create mode 100644 .attachments/raycasting_test.png create mode 100644 Notes.md diff --git a/.attachments/raycasting_test.png b/.attachments/raycasting_test.png new file mode 100644 index 0000000000000000000000000000000000000000..5c516b707d02941ba1ada47eac3a9ddf9c2be972 GIT binary patch literal 45028 zcmeFZhf|Z$_5~V6MFBwsL6IU#kuJT1ih@Xq(t9t`kzPU~C?L{%Cn8OH?nldyN{O1H@v$N&JqZRIzwGyninHvr%o z&CRQX8D>lTYr>yfu5a|+0RYNB#NR84Jd_UsfJXr3S1+`DGIkcW6IaCQZoI2aj;|Q2 z=x9*S;fyd~3gL`Y?@-Tix$g=2P*xKA>drMvlX1Abjc*bAouD&-e1?TFKf&_-o>7s-P|Gs*8jSM0?;n~mbClWx_WBXu*Z!fkKo3Enui3ipFg{na>}}i zL>3^;a(fc40Fs^%;xfy9HL?H0Zbx5XG>G)`s>HVI6+lKh!S<(TtkG<3mw3%E-4b*F zr7|o4Q@2dIsXz?wDKWUH8Ifo~oFm zI~~^n6*W*rZvsHL73*;ywWQD^PE#p13s4sk7qSL? zqPate`5D}KT*9e}mreE$F|5zTrOL*5SHy-%E+d&TGNRKzj*A445kh?MOK7aijy%cz ztEM#}XImoZX1NM>D_T8O86wcis{n}y#IQRO@*;)V`rJlAU1Weo5OEtK-Y1C--g(!n zjDEla_@sH4==nJgonVV84*2i765_g}I_KF2=OK)+4gkQ(L9qY5*mJDQ(;xoNx;W7~ zwj^DQ?iBPJ=~Gj;TTcOa&Ow~bq{TC#Nbd@u7QcQsLJ6lGEx5Zi&qM^h{_?JeGxSDq<_O$_MZ% zo*2*_%LOa1g)FKABhZ4yk(``d0yhajRfWLG=bt>|X9z$coJyD#cjT{n|fFlorRfC2zGB{E2rm8nlXi0R?JF!qSKWVE|w_@E?~C zp#dKQ#@))n)(1P}005kr7J!Ucwg1^wMFa6v<9Z|j@J^YC;UnJrk^r7d5O-;SkExG;!50Ad@PcT- zP^G)--&%(Ma+Ftks~k>@e36~FyyX$0>Ob)jT^s2CH;Mlw%`vHFGlh~Q)ETiH9+~(E z&7lr@2|Es${>R(trXI(ftmupQ&jgtMk?;-ip@b~atoi@@GAHq6#civ)g&a1kUYZNS z_Fi5xE)Y;O`Nwv%|B`g3?^q?4mjuwii5y`fXs_=!NA>mz=)2q#FRcA#p#tEOmb*n7I)H@AZtrllAJ-C~wpjPCq&WiwrUe);}TSl>#Ng{L=cs6g-{g zV-23!(V;T5O|G|-S$aHaVZdUDle&4HSaqQ0HjO;{~a{=4YcDo&pCOXl^cKxI%4wGda~%m$Rt5$ z0HBYE`m^8(VT1<;`BlwO=E^_CeoS1pk}07n*R7fOFqR;iyl;ubG2uZ=0xk zt~Nqzk`KfIpYn*A-!HUld8(Ne|B>XsDs67n>+-jOAjF^k5iMyjfosM83P6ICXq==Z ziz{4-{Jsd_(-<+y;dIsW+YbmM;eR|x{O{E zF$t8xaa_azydM#TF>FHE^x(r4!is$kVk!PHlttf*rhE2*phOBp8x;i1_D=}M+{wWK z_4MKwE5ag|e+TN@q`m}1;0qFfk2ulxV^OvH*1eXkjEqEMAw*+eV!2=siKDE4repp) zsw0lB{oiPiI2w=p@3VCaq}0t=9M_S01iL8S*DRInoI) z5xDw=5Orl%NkIE5(Lb#{?d1Y#6_vb8Wg{K46eP{6afp=2hyJ|+Vi|mb%qxAPy0u>K zf^fzk==IZHCgn2(8J!4)I-zx_eywN6EXnKSU##0qt(Yxp!*;8Y6UEcnonS z_20{O$66ncanaUC0014a^vzNhv_2jp&EC9rxKHrl`H!8JiCPEAEC^E^C=oZ*vvCE$ zOQb?OK%>)peU9|Ml!dFuaAz$(iLlrpkcxQVJl9py?7YQN&U>HFhA=nyPqXAX-`oS< z36FLQa$x~{;vv#GGvkrA%rB+;ox=pahY|VCY_HL&Oa{K5(@DVuXzw8g`eFB4;qooH z-%mp*39ZC`HdOt;b9bf~+!aU(#{`iw3{pyu{DY#05t3~L%uI;2HvHDC!Y81`&DsG1 z#-dITc-+(;%nC+KNu^#A7dx!*uPpx2n}+5_IR4@kQIE%6Ue2T7$4@Q~S1@?oh0;n{ zX73kMSwcT~*&_!4EN4E_qY$X2w6h0uo^4(EuW;(+m{rL-5w+*m&{1H zS3SoI%p16edr4dn5QtZnQ0lgM21J2IRdqTrV5+dwX$Z=Df2M#RgEZFsIWZCd0OLQt z@lTQRI$lo>1jcXZ7lEQe&5Yz<4{5B)J-iB7{Q2K*_9N7QKa<<4Q$dRX{(G$WI@E_&@icO1yVVtpJ1`oTyD^6=Q5+ z0`h;|dd{8~%+L^+N?ZxJ^6Ez-#UnubOJbEoctkxrV1!ycA~^ex=y&XdgsI4woCiD} zQABiNh%4>>RnH2e`Ld*3=kptP0A{a=?7JbMl;RTw6pSH+rI$bq3r-mvHYP`^z-G`x z2&`oL-wC+bn(;(crn}(&T9djNA-2hXSb+y)yRvd-FJ?|*5D*^IjfK>>GPTm|+(?~` zTn!UlAi46NK0=CXFrnbB4HDS3m$RfWGtUCUUUa*_;CQ8mLd89>>uhjJq@tTmGX}m( zK8p%0VD)n8p1x{zc!!XwUkW#ncM`2RukK~Y-JVo0FR*LvLn-t7dF6$vqn;QLRvJy5G|sqo{Aac2BM{OgC2 zL_3*w8*XlJmBTwe{H^^Fv*ae#yMK{vuuU1+gOh%9eea`Q+VTup*ulw(_rngqRNMQX zl8K-*o?!P_Osh>9<2X&ApDQQSLz|aVi%VJDI^@BaT8Z*TQ9Z=p)>NgvnZB=L)r&`Q z6n&KJEvW+20W)r?BYjGD90R^Bw2Igca0%;6k3Hg)iReOavRB70c;BO%O;waeplA~y z{JjTRpKtwZv{Xxt+25;uNWowD9{veUc_L2-i|{#l{Yz6|R9=$pM38|sNB3O}?lS49 z17wQxz6fnryC*)Xe<2f}ie@GopfN0&KPNXxU-IVyU;Z7L_TGLv&3(rAFylc;jh=oS z?hqBoRq<^n{em?za(yY2prW5Td5_4`Sx@;;ayz%UK^>50e4ZUNAogo$6f=im;}Uj= z>iU8B`yAc<%B9bYG|eiVIUuflqBBBcQ!>z>VQ?_&`6Q8w-28qf{5IlOJ_bJB?(1Z`YPDXZbTNwa4N zJ}>!H<;tDQaIvANz_{Q7MQB`p3a={FZ95z`%r8m=%cauq;ZsAke|=0CgVF(q?hDmE zHNpG0nT#iBxzy6!FBcGT_otI^cc91d%l_s?^+8P`4;M%QU3wn+{v*za5;*>8V74ue zE=SB`qY)ZktPefAK$?dAsgMJLW_}-svaql~E=>$|l6E0Ks6yur&d$ynjQELSE?R1Q z^0G!{bI+(C{0xUqgW=FDsg`%+h*~sGHLK+X`_%`5c!%DNT;LvDzH^c(U}8wBrEl&t zc#jQ#SHGIyjX^1y!c;DI`32S`a-IQFy!Tjh2aIw3qC;omVThAw@yGZR&inl~y4HPx zSyXz}A6v0b7{uW6!f(?tukB|AI^c~~&B9Np>~?~?#pE1ML6K&g4E9#OG34^ZA!oUJ z|8ZOUhzjYQ>!Bw4*u!kKd6(jr=JTT5H=>OQ*CkW$H!lils;-jKc5fjL@~AQk8|Yyx=j3ptWTJLaxlyXtm$Av?NhxC)$wDyFr79zv9eOe zZ&g(XIiiDM^_={@OV!EwW7+!-4mb;wwZcVT8dhWyMlmg?^eJ|^Jf6QM~rXnNt7B_F2=FYyQk6thOjwh;` zZPU}XRy@)r%R%SMIpOw_*s$BD|z zj}FQSswHB~qiZytq#2}Jk)4jw!{<$6o(BjzuAj*uNab1t1H2-OB*0*-vphOotv&-C zL|Hg9V*M!;(o0cG#rMKG%UEG{eqneLJpGi8rQw+l=)Hg2yO43NJ?0g@w~5fXThMpM z^O<#DY}cp;IFgjfR|NXsQ2Yjz6k!bU=0@IQ@{^6T@% zC+1;hBsJT78^(3uQ75Xeb2LI0Zcm7VW#f0`uH_8HD+o=E{$8n5B=yvy&AOj=xsiKz z8G}zscIl^ppyz;>n)SC##wpPwPx~ndMWOa!&<|5eLtkN?lFcw{I z7&84Bs9JM=aB$GQhyOEgoydENMJ!lU8m3P@MmW@`)6v$uGgX$pw@)C-%O%DXhQO~5 z3u^loDMS6q@8UiB#fQ9^p4uDd?x?n5k}|woK`!fd$Un)SY$e67QdQSZ$D8}D>kA9W zHa#>I_l_zTH68EA536fTy8Xype)ug*crN%FIN&_J*zN>$YE&AQDCroo7;>ld$zqli z{%}M~>BZPyr91z8cTA6(`U$$Zs z!-F-2!IYJAVK9EXI{?EZ*IA1^Mp!Nv>Bap z?6dRBoKF8}Tprj;FNOLKUL%3HJaS()@H*$Gv=+;FoAbfsVgcO`YvKYUi!B$p z-MGx0HY|=(-QM$#GrhV|@sQr-hs{^FvVu}wrmMvhHk$h*ZTRCAw?>fx8kfMGpW}}Y z|L?N^kh=Q^SU5wYHc!3!YZs`+$W|hTx*4Yw%Dsm<*<P&~~orFgNrv9YL{4m?OHK%jJMx?6JIFk#c*KnL@dWEuO~DJiIf-x_JJ%H-KHih zJl;GH`A`F>@JHR0d`PQ06lFsBqs#WUR3e?T```Rv^yUM%H|8&X@zy-Dk_g}$Vn6a) z#~j!u-*bFiklFjZ&{*_H|8@(`20VqSJlwRLw2O?~@`Ci0jI8h2Wh%8*D-VXD>NH9^ zPT{OgX@A;#dPmS+p$k<*aqTq@Dv?RMJPP=%jmCZSojNsI^^dBhP&_OD{!g)+N=G7T#yY9O`0q3ElwO_ z0`si~w0*6F;YHC5{`Ku1gIv1J(efwW%9N$G9vHqaa^xhIQ>i!9c^pFO!7P7e5-$3` z*#G!qH#A}RL(cJ6pl03Hom#3~7kpZhpD_+!)i&P`KK-2_Y+Ptc_2zP^jm|Br6tws5 zmG+%rT9*K^B~B<3QZ)ZgzZZ059LNNAeiq;<=WzQ>ds{p3Ppi4(ptgR~oU_99biKbL z`U9s0wi_%O^4`4}?KN4s#!;m7eFMS09+;F!u>>eaTjvJ7kv;J2PDGEx-OzMa@K#bc z`%%A%E&NvVnl~~_Tz*57#m{5={Dp#b`OBA)&Qr%;>2ho*E!1w}+A>chrBW#o56HL~ zysn>Q!(vu9hWXxVeboOUpJ?dfhdh^|mNm8V#dLgTotSSn>FTXqi?nWjrGLxSEGBdJ zx&AG*j8}Ptq3!HNWoe6+_8|e}$#TQ;l7an?#Xes@LH9Rxt)mRi70KtbgamEUa~#ekky&t~h!uQsGC@Jp)H} zopEpKM7oI#WONAi5<=H3&?G?IX1aOz^}SRZy24kbGmL%#FGzdu4)r_dcH4)9zhNnj zZtK0;qcF$)q149snf=BtrlNVLW*F|dL3wntX~0{?CRKNQ(te-EzV#XGHr<#sGW$UpB{?E*yH zkGcwl(tqxBvHkv>9Yj`==AhJk(R?wGc)SY6XEYv~^<~Qy*bee3Hk+AfxqqaJND+=M znXaDcQ|m8$pPO?3L~)xmbk}3cWiDr(^L;I~Bwv4dN8;nw(yC20pNouL#qA2rMhM+q zABVAQAt_<8!Wm_^G%~w$lju;hqw>#L$ZN9Cwi!)6>gV8v4)@zXCb|~9mRBfJQ&`@- zWGaUQo?o(hzQj_?wggGT9$r8AxHju!1{gs~OPy}=Z%sDg9)mr+okU(4PBpiGD>4?& zxUol2IRT}r_u$NFi;Lq#+08pvuoh`q%~sST>U7e&Y4h<|cA?ha*QIT-Ib$%dMz0H= zbf@lSdN;TaJl?QePh3i^R$7JLh?cuPWemsmJ2{5RsnTX^!uf8)andxO%?TT#|9Z=k zAABCA^Ht8n25!qU^gz)xB#R$o6^l;5PVSHtQ02SVO@C;<9^hkF^mi{ibNc1q3(1TO zsd=qpY+|6Jnb|@Ot{4*Vad9#{m}Q~TdoXdyb7lbD(b;NOV#Gr3T$@U#XLFYa^z^XY zJFwXq`&j2!*&OUr*eXXdAVo80BDr5pgu9C0nGceD)5uQU<%f^2|6yju_c;{5eWLAn z&|pnpX1$cA>44^NeEX!qwkzyJ=((eDBz0}c{f3bkwN)37QmpILww`1~Tnh~6d&F)L zB>n2_aL{q(-Ge_=`uowniXGy!1)SoRJcT6WxA||oZDpX!?rEEDB^7Qz3>$5V+x;0c z7e*Q1$l7y^4xC}_+{gjuxwpOIp5oWxkGjg^Qe+7Fkx?D6?iC7U6L@j%xL*3!r%ki^ zhiSEV-OXBWAT3i8D1TzNnjS=bj1600<=Hk!V@_mU>pEs~V1sn|v(TROTe(;L#!anN z;XDSLZQ%z(op3o)=VHg4_w{^Cy-F{=vU(W>8VnkNl1`dAkB?`r&IJBW*5(Q36-42f zUy$dl12rcC+s4sVx)q)MD4qO?&MBB;;PEc^;C{bU?k@(Kq|(;|Ta**L<42s_O1q{c zah5wrj|53np-v2W=P31$opWk1!Gm&1X|Ex)g0&r-0zKVp32RWV)(c4WT9aV|*(XE3 zw7EA0Y(8}56w_tAspBlZp#B#mC-y)lfvv>nQ1x%4U}IZ8{)-s$%h=OFhsy6Mjhy{D zJ3`TNeft65a;Q*jfd{7w4yHI{y{Q{~%-r@HHpX%Qs%F}$<2`sHdMp_iO*S)R*(Zz+ zuOj6K^LiNXNWP7HS$S#v_~4|fRJ^HwuWXybh&=4Amcmh(bhL@URbASqwJ9a>fS-kY z0s^-)pM>GaY>ZGRXjc7`-F$BZ<`8+#GZsmA!vxnNGy->mrT0p1?K&ewYPpIHZk|{D zwj59%MG+w8aYoV+CeSo=&-eN zY%|ALDphgKC{!%Adh@sanj}ecpVcW+yz_zDTA+cY);TuMq9KhzgDdsPmJv-eo!d{T zW(dmlU~(*m)hU*E7=x>;?XVTA=eOwbEPuPb>!dOr8sbu zM{5u;hFzA2A^oe<17-xpJZG=QRAuL)C2V7#>%G~2tFuAoOyGWA>|}g2UGv+7CB#{E zWsm0%lH)!$>j}M3c)GKd%AnH2W<8;$3+L@udut(0i34=B9Ni~;bwYl#5*VQi@FUq+ z6`*HV$ZYaSxYJT~VGaf|*#pmcC6<=ul{;Kln zG|x!;@~#Kz=`F`X%R ze@rpm4u9puFD1+~8DAIOw=<@WdHa20(P^x|N>Zh0Pw~Uvf*^CpFpcd%_9mu_J5{cu z!oT76wtc(QBc7_Z8Y1TMgp*Upcy^g6 zLzma~+dHy54K_!}U)5{ZggS{zvDQpFGuPGv=-L)aDQO+^+{AY)24!uhGp8@KLIm3K zIUSjH>FwA1nRZRcWxNGIcO}lFGUdi5YcxyRZZ;iS!TGNt>@NwI1~+JF#)mw3M=qN^ z@~4P9xA#45;KjU|W66xgP!{Wv>O&v0-T1TmV&PWAD=jIbvCv*Rw=^dM3mv@Y5J-qI z%dTyFgYmBRbtBDtqqf(@13n#BK zyz?_J;_6wijEyW*b-pxqY%_AbK)vH=X>Po99*RjhYWbNr^&3gi?1K7}wcjz;8noc( zGbkOs=Y&f@e7^XWCwFR{$o_=YyIhTa2fcR?N-s3|VfLJry(cUcVqSYV!j~7kce`Fz zl$-N5@9nn!%n_r`8!FrzjI|n>Tkxzn!9;!+CO+cS`x9G?M7|+GO7jmKxW8XdZdlM=^3zdXOydlIdI? zXAP^V4X?WyOWqWVE=zCsPf~2|{-S)^p;&IHO0h#>i*xR9lWbe1(d#uBi(HuG&T0Rd zl`EcXqPsC1`QYTl|2$Z3Q@pE)*Pl%PsXc=%5*n{GAY(WcIq>KtA@N&c4p|QO0L$<7 z4Y3pz3SBd;kut#2hsFDD7Nz1n`ks9?3)@Miwrz5-Oa^9_yb>LKAM1IxU)l}A(+jGZ1pVi}x7P5zT;*zfg z>x$M*a%TNC;@_%x^&XFUWzDV@?j8-lIF=bQ-Bm9T?)R!YDl}`=1#wX9*@mCHzu=T6 zMbt+!e?uCC7GwsmvJDC6pJFu54%dopOG&L8 z=>Icw+?&++|j z^R6jYqx#aTF}C3Ka#i(l^V;n3*ls(G&znePb&O$F4hol8v1CwUX0MC~prC-onmn-d*6X;~+8!SaFRV%IYzYqFQ)wmg8i z?U~+0ZM)7$$J+TN=7y1v(yo$Eol4U?CTFZ)7@MeTT4QgO52`pmT}?|aUzo1b6SC8N zWyB$CM=ohM<;`I=t2^wdwoy_1Xq;IOByzsB&i)~Y`X}$y^j!%gIaE$gLPU4>P8bL7 z_}w$z>g?wVcbgQ7g-WT4+k;atEVJikRo5qq@Vg&38_>41P1ymy512IC(_x!g2TEpD zG;XkYG}7EJ|E@%I_9LtC=kBf1qn>*eoB^q9z=!vqbc4?z1;$t z6!F4KP4{nt>kipPNYd<`U_&p}+LGyK<0Gf5y;!pCzA$OaG|ZVP4Y;!uIb}*G z4UulHeH+RB%;3JKLz3QOChhyWpsf!ct{$`x4S!b7#a|Q-%2&FD zY9KC~8x%_Kc^4W6d}u2ydL;l9f6sc`8LK(g)n}EZ>TeTNnfO5|k;#J51CLyKYNyA4oCmwhyIzTfnSeoidTi&D_Ykkds4lC5q$(lUSZ(RAQ?-a;~bT6DF_ z4TW}NU1+~Gr-G$N&8ks{OUo2VSZ)iA$df@)61^qUH(eS}SnpfUt~D-MXB+Y= zUMNnMQaOo}mycBIc4Ewb#2QD^xlu_Pa7vrsoa6giA_y92G@~x=Sbfyem5aN-0}+~+ zy&(-eMRP_88zm~DhV0J0sbz=i10~;>3AzlaO5%CPvvu9!42-p+@32!yW-+F!(FYUA z$=40m<_9sOiyovx@_*Y)mXhwP*WOJ^ZS`tXDMc7rLzVbo>`k%?SLI}iic7})B#c!x zMm>%cJPjwpTa}`EwUMg*vw2;Kl=9asM_#Y&-yO^s&89Nkgh)#{C0aMj77T65wcrDy zx{q{T3M~gEVG3#_!;mp?F~&*?=NBj2;^OvfePnNF_W14?9#>=1mZjQoiMfV`RM2b4 zbk=JAeGqgElD(@xv!EMFQ+c@NH(m}53Mgknn973ml ze&*$SJoEE;_DVl;{;KtkhH_~2(zGh;l3WxO*It10<;=9kWKBPh zGjtY7`rbVuC$n5q24lL;QYWXt^h-Mj-S61Bo>`z7*q4+!h3`uX5X;SatfW5lu=8nY zq?&>kNpPTd^>OTr9O09n8fzKF$bDB-Rjm1duPW*WJDPTj*>7q@oD%UWpt@s9LfA#4 zYtS;sqm62TTl&69hhcn1UDi~sG|71K)7z5e%x}4Ezt%wqB%Ot!7cYObw7>qIW<%3< zd461!Y&Iy(t(yC-W@e>iyz0$K$ zVSA^;yQk@QBAq9jZmaw<-Y5LG1oD1DtTK`1{N-*0W#&Wlb+{)p_@*LwOsRI-VQzp* zNPMS2=9A(4)0I!)6LC1wr*^OWBs*n!=&s% z_FBcUQhAI|x_=6~^!zkg1gf_06Z7 zaJ$Y`+}kCYV`Efi^i8Ztl6&7A`ncth5}0m-cEv#ZOqyU{P82WF}X{;LO!O+*NWNQ6-e-rp3`+au#z|%*EfjZe z;$~;YHS+zCi?JKH;=e<0x@2Zi{eMa8d_x9)|uVAp?A;}hZsH`Ol&Z)sS6-&P)nZ0mMDQ}O5@1X#I%fU4d@OQ&^VxzHi46WFSeY*HQXdhmRhE2sQ|i^MjEW>Y+yL zz1FMk4Rvg<>bwT7hyE;ynjX4r7mW0-wCDWsOkvJNZR2i)$_4c(dqI>qgg!p@qiR2& zKkdc)L}fv1brv&;Oo%*|zZhChBjJ^0)IcLV&?;XktgL%aUAexXaWJWUlQYyaaCYjg zT*y@SFW@y;>DQ%S8EegLgFl>@MY|ktyy_i!4^-jDpAGM6)%x1!SLEQ%p+6%#R>&Xv zZ0K#eBsF)Ln#cpoS3fN3FJcuf$of_vj`XDM!T+wlNKKtuxj+k)&U$Qsa(;uB>~V^; zgVdBI_Ucs`f58kNE)jZ?i%eCyo%pSVOL0h-Y&R2{IMXp?b)?d zywcjBKH8JNA*oS1JZqWBj5jetj;vBw??Im&UKw5J8$NdqE!EDpLOST(k4gvX6@+%i zJw88bE%}!2&)7YkGu6o5m!!rDv{}AhjO+tmiIl{fSg5N~wnP*ltAO~PmFC3ij1H8Q zTa9p*uRzH~UFVP~(}Tim5W(EB_ILp|EB=fZ>vnsWp+4ppR1y+Q{JY7^B}WTXx)M<% zW2KLI26cCS%pV&;hF*vTlbM}W7CY$NyR6(+z5Wife_VFeYUsB|(WD`8FPzq-BXFx} zB6=vf8*Hfu7yM2ggN^7jjvS!HQlYYO&9GccvN%d9MdgGjP zIH<)#r158z%w&!$KjsK3({?S#w*2_$`hcO^SD%T(=~NcyGRUxi%$}eyWp<{ zXyTf6B(I(JwO*=r|i~h z%|A{EQ?J`{cTAuBjqA9KHhI3nTczH2s;rFB&lNgNsF^zgRlBD`KXn`m z!z`3D1gkSf<@Y_2o1ts6R|m1m_f}mjQ>T6p6l|A2L*RM-^kUD$-&na=_*O&Nq^z(E zeB>yo%^8URq+x8x1#gl`{j+|`l*|=;BKWS5-)WneL=eVgRav`QQ0f?EwJvj9 zWUz#lEITh#T*{eOx~C0l0AEr>SW)T2cBfw3N6PDmu29h`f$?1+&bF3Qh&m)z732#> zt!_TC^KCrQW1a?o_dM@_1v4f0R1NHTaGRoEe4B0DNmkW_<%_oz;4e>{t>Zm>&bqr; z9;OyNd&Pm6J*-Bq;}-^lLkGXgUYt+%)AN<37kfa@)ogq@1*Ln=hF-73;H;yYnXmgrLmF~Y z^)Z{}{Zf*J-D7OLXoJI##+6yaO;6tN{j!Ai)*UHa6 zW=SS5oRwS9N~B>YAsm}0&TE0m_gakWdp$ffLgLRNd=|wA1?V%tv84rhI+x#T5T9*v z-h7av$l#)j%^szsuSMCFLE`c;4j<&=jSqK!i{;f7D`ehZF0HQWWLsQ^E^=^xQo>@8 z+P4nb(N2)(QPt0iQn*^GZuND0XE%qdU)D8*u;{^^hOhaXI1R0Q?yPX10Yat@lNUO~wDkQ&Fef#Kv8U84l^hjPr_7QM zD6BImlpdGkpRByt6feet_8&}~-Tz$_rMyP2bcKan9}6!iXI;6nHHB`CQRmoA=($m{ zKiPFhxH*`;;0csWuR@XX5C;tQys!qVv~cg;oa>(pdDkcW)kXGLvMOuG>+i+f7&7Wj z?I~tobmy`b;c|Y?P?Yr>jA{9mmCTo7S`S@$OtYWfGwE}V)K&s3RU;v1hwhHavo7y! zp*}~x+VusA(HsSpOGA*0<|M}J_;VqV?mw~KdM#}DG~XSr)2|car52Nrb=aV&D{T+` zjjJDHb>Ws^*&=k?Y%|kAuS=DVp_|B~GqWnxTP?JN32?z{Hl-8v2Ll)8EQ)6Vu25w1 zb_hU^qf%+()+)I<)3c1E=sI$tX4oB$8D_zZSHIBK%Ds+<%%0!S^<58*&gN*$lH?h#ep8s{v?`w{;3!om z?IY_e+>^+xZtXFJ?^0g#LOMu@&+FFfy!$C6Rn-|-1iPp^RL5<`YBkE#sP<&$ z04>Z{OL>^089Q=5`&j;5{kk}BGC#tm-#6#X$glzn8z|{dmglu_6Dwc~5L(@7GMw3; zXyH}IL}90y1q*VaxGp6DGvBR+n8(e655VwlVGAWZcUNn^hpBcY7u<+v|9HL) z$wd#y8GJekKYyBs_NTI65NTA^dehUP(**Ws{WZR57i$WR+s%cWZ$bTEHnE}vY7*M*d4a@rvnGxwMiN(1s#Vd=lzJ2r=NSz-E7fhZNasv$9I8=c^ZW84EWFY0)$7; zy8n^RyX^y$dQm!YYA2T)(%Vz0dfg=t8TmS@SN2e-x0{at8BSbhS3d9fh@MwI!-w{n z_|>4zHRZ99X-Xw~9V*@S9&c)yrazo61xuZwvmG2}tY>J%!BZWw^M%ajsU)5eiMO$j zWhYFitbI#bc7oDt=MvcitGYQf`N$=<2%jk1`(57hMphz378Sns{!n~xhKVNqZE&@v z8n=YlMF7X|p}YiB`0ByO&IO+0Ft)h5_DL=I_xtUfrI$r4 zwjx?siW2NwjX%NbG*;S!&0hKbps1(PHt<;&$-~H3H&h>N@2_qdBl$w}YRWoaIaQvD z?I(#M-oOS+J(4b)Y`0g9lOp$Yn8jS{)K8A`yq9SnM5$cw#=5j1Y0{ziwx}Me8&JN+9yS;Ic(RL@k-WqB1F>YO;*b5miYv^dMy&Qk>Rf1* zV_?@EKRKrzdoIFQ6T*8;EqOkV4OMXDWYvN_i8KdN4hvXV|LQhl&tmMrhZmADn ze{ZYtDE$!xJ$q_4m{p34!3M?7&cjyeg-ZM|k1NBp^AdY&rE0^cHz5HhoEUYh^u-D4E4)3* zoHLY4v2=RU-5HcR;p4xUu^$`X0!gM#}rdfcCeat})i?+%917IBGo%HX{v$zPkX{dE4|R^ z>xOKVzz_O1Qq~4I-<4^hOmmv$YVVx0Ir2O-$`q;Jh|lBg%&ueZ9`)9}F(CY%-F6rZ zR!u1um(9}eD=7MXwWEcaR_8Mi>v(M~w9wS16wQnoV7p%xEXeCjR`w>XI|KHRbs2aP znKuOFge@+%iNrX3W#qwenGKg;uqp7#)!CEQo{F}+cug~hi+MDDP)TNh2S+VU-G{-;L0g#GX#PJ9a;XO4Va~F>n=d>fo%ArLWGhxycVt`0Nw;xnI(1dTuEf zRkMBT99UJwS~F?hLW3*9Qm=buhAB6h8)S`u+ECu}ic+NLssSg4o!MdT1=~k3EaI!) z`4JCi9&>X!9>?(-35@sagLlu*2C;0J!%mUp5`vPEq9^W;kD+yw`n42VjRnw4s>?fS z9S$F2Lq8a*CbZ;o(GK1Ys4jvBHE&!&1q!+@ZWNx~Xz|yS0*0FFgoT)C@D6VtKq=#5XIHyI&I+K)KoA=jaFklV`D%s&@#KIEyZ~H2WIk^$_c81$HkNm zr2ow5`TCUjE5#Hac7w@CcB6WpkKYQ1@r6ZW6f~kubH(5xhEF=1{a>Bv)imO?tJbqu zpczTDj|H3l{vIjx3vX$@BFio)cO%W{$!*a6?j?WIL87KchK5ADC$a937 zB)K+@Rf;kolS`k1YLAei;hR!ZEKuMo=P~nA=%ps zTxWH&EUi1J^K~Esw~-7$NZR)$R`$3ZJZ(~EiG9qPZSkJo^^IPtm4bd!d; z=th*(2|k`VsMmJLsklgb%J_gMtnajg#2(^?|C_TrHWUl1D=0gB};>fy~#tN#HVU5*oWUm*Kyh_r9jeVB5}t< z2*@t(v(OyQ1*imqi_ghRBv8@UpXX6YU1=Jp*|4on$tY&PNve3%^~}mbRPUAwJ7n~G z{BZOF-EE{WI6lzp%DF~j>qCm;(N7-w+Ke(vHEU`$9Y}cs^NexM1~>#bd8=M%*W zNUxwRLe}R9_tc{IDKPo#Q>|xW)Cji0spo~6ZDE?}daW;J6ei{Cg_nn7q2OKfp~}Nk zSZwe%eb4ddI3ekuH|CChZM`aOtQ|^Bu~9zVVN@CIoO&*6L#EOsKDYltA&y-dx8Az^ zKIJT^SSs2^Iqi@UTLOjey|>c$oNXny$w#l#kYPz)sKga6+9+#zg_zp^IF)GUDu3@# zR-i5Nx&*Y0&3*TQ=9gO1X+f?);OUu`|3v9Yr^8;(Bx2NaBhR}z7}wKw<#FGII(*ap zTY2kHI$M{Ul#$K!FBt*u=3$z?7{gaOdLA(6vlbP|7 z6Qz%V{JoPJdEXu25#sn$C44+O4*2^9YL;p!K_y1J?a!{dNPoP}DGg*s6gCed2yRNHopnciCr8m9bLlVR5G zI>mn2F8x}4a{f;wNeklvAaSYeyWx&(%Euj&E$i>6hruc(n0*VUN0ZVtR;Z!ZE z8e}(XL--gkV)IioX>S}#4-)Ix*^8C$T=fcySdhOIIn;X>g6-lMLd#Juc8+$Pu z#{|W0vaM%w6gwm_`gZAD-}(t@P8XEJ(YZ74e>ksuQS{j;+_pokdQe7_i&ZmOcJ(vC8|C(oZ^}X{^S35MRnZ~s%+s_%o z7&PpZ(34pq-gwPbOf5b>;IWHiSB{KqA4XIaszzDBDHQ)C_iF^~y1Zu+^uD%#uSpeF zugpGbM`UF^`gONM$%H*`oqGRNzWuF3@nF=v94W*mVq&gk@ATzuRrR}zPmsS^)W#$xMWjoj{?fYs6s@zh}4~<+;Klc4XMOe$rk@VP3 zILA1n;NZn7_x-U;?M2LV!AR*b^Ki@6o2|a~xZ~`z31YH0#&h_H^eIIs(Q_HmS?`vD z%iB|&{?I5$!wvmbesapd$?tophz}1zW>p3OGS)*=D&BENs6*Ls|9B(1`9~HF2!qkK zBAN%}`*Z5CI!udUSG8H?163w~=P~?&REJ7T5M@0SI*FjyL*QI_iu7>^DpvMmdI6)_ z`oeIkusEU+`EBw3s{ARL#Wvfa!Vj}F72|yGB;v|zZ0kTqqaL>-btOYlbh3c|v)$co zOq^~I3k{|9{5j~eVkX@lUNbJL0+IM}CjHs-a136G7zt^vf|*ur2A9Ur+bCHK>!rt0 zzv38tvGI3M#4x28CuBF@arUCBM(~I@b&DFhZ)BUUZqnGFIukn>&|WcJ`zq=EEcBzS zYxM9YoAy6jKh9Ognr3&Q>Ny~Ec+rozjcNRr`if;>A=o`b5719IjtJ)K_r|Sikob6g zfLG#vsob{0|HI26_&rL59odrZXk>GtW#74@Av&t#;g?yP(NePzF9RF7LM32UOJqMW znA9dyc9Ws_bu-M3a}_jvrV1pv zi+Eq-GOBl-21U$|_SwlgM!kh6W5foSPsAcZ{j1tFNp!DB5`0k0mnpl6utCqq!O#6x z-APKPl5X~(SwNvDL@-G{qa*Org(yj7X8f>?FIK*6Z7!V3U+$XUEL6NqI=z*IB`X0g)0}Xs3;paYz ze%v8%q05~iP0%1^sjmz|z?;M)w;#n!-I{8NE9@8uS!nj4XZJrRMx7hmEp&6qdLy(l znD-N_*fK>73x8JE?tmTwFS$=P69J04>4bm%$jjZqCAMm(sSFb9qD!p1vVE{W(KehY z*?_@xOBOkmge60V3uz53Ao0Jz^lq;Abmw=W7<_e5w)|xjm%*q3&s{VPw*{qXFZg2= z8$Xv`S11B~KblVJmNdm1>t=oLcMIQkD#bosklK0_dNmioH6F-XamIO@R#s4ytTC-)En9X^6O=rc2 z(C*8qDL`KjJC|Khai8{`T9pP@sa!W#jMFgY=zT`GRTiCnx)!m_AhT&!`0qobpLQLn zuT+G=Pqyvswq=pbN)^FJyYIE@atGXA*_eAqYCl2S^rT`+Y-73N&Be`Q-dz zWY@c>nI_02y=I2PXv}IpOhrl((ie{jifx98dbPax=3ko@vOUn34Ou z&^eFN6L`P6QNxiyITJH^6xvSQUDYD;k6D;JlUrMJ2U^Eu$W-?7-qZ_?hq21Geo;N! z%prd37Snbpn#N^6pGEw4^wU#|GJL>d=zPqgWuvJ^WfnD)Ac`EmzM{fN=$mzK*BSd( znvVJcu!C)5tw*F#6M|)Pm5!IH#jKnsc4?G#P4mI zWc6e1&+}TlmUgOP*1NhHZ^ZAZR)r=-y`g1^YmQaZ(9y?r62D7b>-nizMPtkY-ocJA ze`_=Yuvi}!Rlls<+nc&jg~^QB;#nx=uynq>b9a-AWbNM~MoI;mYv2xO!_bkSS^D?M z)P!HIGJ0*$Hqv$}Hb&sIL0TOu3`BGWI}pPezC(oL01^iU~8Q3^)3u+<+si-nJdBHhw# zfaJiXtU4`ouw0DTc(PDXSrugFF6UKq_W(@Hp_3 zlT~mGd*D@$=gmLLxk2t!h`hq!O`~Xf%O<2MmXK@tvXl)_C9CrDx7y&`dJ__JO-j2N zek!AfS@sJl7PG^-BUf?a=uzG> zIbYv;Un(QMqifW*zei>L+sZ2~%73gAsE(9D3bgsx9rE z6faZOw9S&wM-Sh|%&do2leAjZZ--E-50u?SEhtC<_kvJqyMLX2gNSu<257jZ3ZGoo zy()mYx?B$Q>{&``F>#71UlFy&^oHcEt`CG-aV=&GvXi$Zq>h4OuZC85WCTIjoMm(S zWW5;$w+XKL5f~M%O6IS4&y9A9J9<8G!y-_M6un1XVUI~X%6{V*T4E`~^g7*yDRuRd z>wg4T1yQ3-Qc8xXS|g_v53vHneP1L7CXP4Q{QMg+(JM7JNzt;R$HT?i|53yxh@HJi zn<(ZRUzqsQKP%_gk<2!7#&1iwWY~0-fA;O#J*u{QTP+c0-uP%lrBRCSyFZs+;}kz^ zI9-W%N_@nPO0L=sm5INJ7_n56jfoS>uuSlBTz}s=S+&rAX-C?eKL2uZJI%2bdUF%; zkW(&Pg|?N*#FV^y!4`MblktxSGka$L$0`Y*4)?eUyThpn!S$}HYkqF-oa8l>P#J_u zDi|Vex2eeMtqYnWPe;rT9b{(bLj0@g`rj-oTx~p@mZ6MLuti^XE#}mk*zNPx=#1}v zY0-45jlu37S;@uEUtgc3OiA4G0kpFADYkTWqm&sQAREIjN(Sub#?V3JjZH$`qjtoAYZm)i|9VF=_T&rO6hQQY(;es?FR7!wEZa6QFVyp4;G1C zml)$WKR4=D4DxNXHcF~fpwz2mqG^CrK0$@r5t#)>D&}|N(av-+rak>FU!BiT*Q=#e zBlyHbDf?Gdl`s3%lU$q#v2ypTjZ`27AN*w4h~l9g2Z-t=fpr=_vS>&$Vl+Vwpy6!KZ#{RJUU2z|6h*ZDL~6vjUjW{daFew~$ky zG89mK$|Z3s5V;g=+`zPL`JoeYm@$7Flh|B)IEF3f0ZdlY(xCvocvTAt_3SUU7*?Kp z#5%vjTF46hkjVqIhdJ3n0li<*s9h3n}E)T4!ay)=9rqEh%c&C zifbLdQSvZzTXuc+$0%`na;N2wb7oPHIZm*}!=Yt2qkS)Cm&eUso4(}SeD<%W^bF?4 zk7p<$SknF{Z0df2Ph*r0@xEdV=d#M?3%HZP9#xDhmbbVe<28eDL{XOxjU7A1Rom{R z%8;7etUF@nmhm#)_()Qq|UGQ*veIT|+Py+}FMbZwup zCaS{4yygCfGfjyaLs6EZIB$Pjshty}Uh?sDB_!+U`Y=sIv-Ox5KZDfZV^+DoS(AXv z^))ze1)0EL43#v>bXfA_ODs-5?4N-5#?=dRV^1^DHGhN}y?Qk%AseWJc{6~ui=d%; z-qfZ&AO}!&egQny1e-;mJrT>N8=x)N`@WHjHzb_bIzjY+`KVHCNn+On;1T&wYiajqK@aASCj$y@>(pL7*)+C-5!ai$B zfJ%##zjpmpg)$w8F&+bWnUN@`yR3n+XxaWiBlH_BPIQv>}|$52z-?)2b9Nmk4`R#Qxa&Q&PIH zYiHTlw0-q25WpCfqfW8@qzRtmtaRm^94LpOQ5C3XDo^zgi(Ck59z`CF{kQ_!J=T9K z-bw)O7<|0G9UXPE-+mujMwYfSG)WhzVQ+SH(Jy1F$MTLh?hD%E@Dratw6jbWjG72P zM|fafu{p0jci)6=%O{RN_CW|10Q#vOhoD%>Eb!1lcH!&OFNKj4kxl7fxE5vHt9O zRc>$ENkivyDCF!m-t6G+sDon9n~M$|2`vO`bkI*y=ZH1q@yn9c3{t63Ix}>ey^uPn z2=xQ`RBGl@Uiej?k#!jniUZcG@X2I{mM)cp_WW7G;v1qXa9Sgqw0(UWFJ^aMh;aSo zIu&KLThHS6NaMg>J&u2~Du!S*yWZ$jjZ&HmlMe(`iYcn1DP+`{EW#n)x1FK_SVea^ z%~IDP=rMa0PtRc0S7TaNO6aJC@YdJ3W2j9Eu-8l$jR+vjiLwdr5Ow!W%EzZ09+t#o zL{Z>3H-1WCch$g?$4vx3*`4o>$AK=sWJ%3vvy@dEOGYht%Sdua-k2Y`YP?qE9yJ_s9+; zHBufQrM#5Q2KgXQQBNIj?%(ukOCbxq(EBq}JO6t(&X!g5ww}R-cD4)E$;h*s+dY3n7m%w@0tSiP5mT0uQ~ z8OBxh%tx)N)}HkKGvZXJw;m5aw0i}{TZ&L@l$(6fV95wto=T#2x)#DRp~Vy~-P)zz zDDRb=ImH&I!*ufjM;!z{H~O-eT6%S-$IJbHEkKa(lT3YcIZ1`$63|Yi1pMU6Zu@w& zJz|pC>}7$9QTKimGGNV$xoY zUBC<~`iNg0ImCYeO&{+UjxD;t36w2oNL}Kqfl|iI*N`fR0IVDO##Aup{3<5~CU4L6V!HO$RpvG=G*#cXWC- z4?U2QlK52C4rLak%Z==+Hb$}RHAVZEbOF{n@schDl`Lr%1N=;!=LN23Md(ekoFdUX zOOhUlXLxiTFvAxCLL1-yOd8=e$lh~Upd^wxU=R^e_^szyc7bHl`#vpk3?JIr9M@-t zK3ogU81!ehxi<0UjD)tDx}jR>i1KbEk&0w+H|{EzHq8e?Ua zdIf&?l5M$Rq?4CAbDln~q>l>O(N+V5D=CtD6#M`|66R zS$1bL3Vxay{Oi!Vx$S(@eo)0*A&gC;e$a(sX`};0B;9;jc_N4!%G_a1Q@NGDx6&_Y zaF^KD$vdWuy-8*Xyj2$i*o3sp zneJJQ13wA2>$IBwu0Ci?Bws&gNhF)=i#z7O==v}!VywWWE*0=D2R1NNAfH<9lXY|S z9}F+BcQJI|$SL!Vsc_XlXs5VyVm%FgPpxwGC$Lagz4{}DYzx!Oz{<90-}XSGm5ez- z+3Ai)mpW}g+7yQ&NlBSWWWq{=I&vnu1nn1^iH|zL3bySdh-nO`G6)B_-=WqnP z-9F^%Ub(cE!1}Akm1_W@ri64dAD02Iw{ z7{0V3tW)3X{v08W18%>3~|L|4fd98oN8 zr%sN$V1?HaXXaK-mUXYh+G{A6l(f)iS;-W0rW8S;P6zVw7u06jg9+Agel_o_2o;db zr2SMG5zSKkX11qS>Qawl!T9rvJsK*;LQ_&4+g5THXjT+n!j3*%JBP%!)NvUMLN+-|mqJ?m-Hp&2c4@1z9y#JRsA` zYiu5&Y;xmhaAa_7bH!;7i=|!w(h;`C?gFy7G<;`q6tQ_wjY0*M2lLTC7p34*(UZbz zsz-OAD3U`$+43Ww8VSuNS#JH}pSp8X-^o<$bcc$r`s@4~g4c8TrlGAPO?HDk9VyeZ zW=cS;B2;svLW(63bu7^(`4kn@%GdApB=I3rz~tKNcNtTb89>XMKw7}#K;OpV99ZjO zZeXKjvr;Aucwk-RUBs&KlBu(nM@si$ob#R+qyfCIaiVYQe&ec9CIr|a_?aB2dtlk( zWq&0hKJ35-m)2;Mqtn^#ZOFSW(V(>yE6KF(^$@WmUGekSswz#n zQY03S`5Fq{Yzf6uKD@xi;=x+or?KIDA0Pv%YExk?dk--Hz>S$O_oOd6AA4@ zn%nGJCFaif{&Z}%mZTm~dQoa)bDlKeVT=fVEE?7STb%x{QYxcnY+CTQR;$Is2{hGh zc6&dHxyvDztM5Y)sIh9ydg;lauHM{T0y6U#1PVKs4ojrau+|0aY%c|B7=}yZ*pBB= zYLr)3e(Ppf>HLSn;*XiT9ZT%j#kPo2CgWeam6fBmW z(EnV7BhDSwopIAO1tNub{ru=E=L+g$J`?NW?HRKGVqK9_3l|k) z&e7%oqg0-=jK%N#yQ|~8QEhg9GO9;})(dtsI#ytD*GjoS)*dGP%52yDJ|k24Lppxz z?PxLhOZjq}(F;qVUOma#&c>FVu?A@mQ|a8bjXE!msAg+-TUNSUTF&=|2j~2QOA(_^ zIS+cUUIxO_CizTaEaEX99ok=hR7YO7#6r>ao;U1_ha8QA=a&5Lp>PLJ&U4Ef!W%Md ziU89sDOr{iOYeOt62GqVc)N-f=NpFUBwIQ*tT9*iU*>Cl-TK0DVr^%~kAyoxv3x0Y zpwO37mflo6rO#Mx=Vx=xJgeW^nnTV=Qe|TAgz{9gGXdyh+kDVEB)aSy3%IGg(JN;EdX%I4_$FNRAi9S zBKwEwW2q^(goRwteDQ1Wi2v6))6-L%20i(FMA1$YB~Ad%M%^)F>^06<_Q4K;mt?Iw1P#|nyTQR`!4iI; z_h9p`VV|Bx^!06I+RoT7=NY`V*h8P-Lc?)6{0-Zh%T4DNwNPJl2qUB#=AS*ei5?$u zg^8RM^l1wj&UY{|V@+}x-`i`~5LtRWk%R2{E&45=ZA1Xf)JGlIi@rEKzrcs%k(^!c z(KZF3O$`hZT`J{Sth?Gg*@~E{s>Vk%iVagC`+SC}STTCV5C zeCz&i-|>d^DSl^}eRE%*Kth$P9A3!W4`3u{$K)ni#?w>92=|1yF*Vyszohz+AknMi z$7&P6+6;_$K-$8(5mJV19r-eFiwl`0`K5EBUNuHqJl|u;wYlZ9;YX9dt0A_uKocV&}Ld2T6U> zc0an@+GjIP@{pxVntKHsh7ZcG1dU3Nh9fY_$!&HzRlL;EVsWe5-!maj{*J8b2d;B6 zA;L}rPokYXwaP5g|Mtpkkqp_ZFz?8pi_b@Us_8$p024^TZM>o`sgEN_;(QxJGzlY= zF@oAf-QfqP1$J{sB^zqnG$~QqzmGDOSi}gUSQ3x|2^OnA>yD%NfmO#ec{F@qe{LF$ z@I#pAC|J2;YJkTIW%ZUphZtc!Ib+g3e z#YkiM_jgX-9}XdqplwwBl|lnNpXpkeCBS1mk$w$1#{tkAh0?b$7V9@U7|I%U6o^R= z^sCVKvOrT01F%}*4k^%%t4KE2$J=^sGpXKPycheJ{NE4QNGPG-T<>>r!a@-g?TkT^cx;+jCBVE7?|2)8y@Ac(fE`9)2e4I=zpiOd~pCF zylP+@HgTHe3`x}?Tj0DYJH&z27B-k*3aJgC!3F|Gk8`UZPaYJ7oN&*si&0+X7jt~B zDJ|nZ#3waC#k3zQN?VGh?H^@?HFINTNW{56ybv@WwfX>wJW=;gbG2b+3%D4zUES2> zy#rjx=oe@=@1PW?7qO{>VH4he1}v4cMeSOZH{q5t{E-SkB*RmZQU63kD!b$9#nrs#p3c<` z5>J&rQOrGQ5&M!t6b&otMbt~w9uxLO?@-1LR?V4d{l7)ygk;{Z9K653YK|PdITEPv_|5ff*~`dXKv|SC}>9AX!_{y|2*e) z9Q`qL+aPYi^RAe&InwmXeSG{qvPV_+^{HS8LC)@cx~xdQV4cQ*hv*JhTs>?aH8LME z2=_v%PPtBT*ej}153EfZ$JqPlx#G9y9&KiH!sP}h5%%Ds24Fx^d{Z9$Kh5j$gUb7) zA9N4+H2L)@CRWFWUhMtdq4~7s3v8AT=cftzpQOJ)K$n2f$Al%ZSpn-cBmyi$>5Kd; z6-S6=7Ea&Tmhjwi$yHl$LG6)g411xiklsl(#(*Gis)X}1(ol^MMI5TIu#tcskn^s( zB8VLA9|*+N*ZY#$O-xiS^pq~bL@v7EYaL4@w<2t!V`{s|{5UN*4$`}C>c6@~$DrjW z$GMt_c3fExjoI2Ls*sNm?Yb0`C;oVPk(}{^S|oJrg7>2?*Q(?_et7D}!DE}Pa^hQ1 zNE9k}&*h5W`S~z<+Ho#Y_#%~?n{h5^eknq_(Ee*@=I@r@W?>FN+&BA)sDZH{hW0;3 zV%VW-qzS{{c1F>AIGh9pv}nptzShK0uO6P-`Mdit&Y%5sP~qckQ&bJ;-aq|0dz83i z{ikB9LdAcbO~ilISK7cZm>O)jsF^;p$FRh*j~4R17OFcA=lCZ&H;sk2yR zXr|-yxCOmRP{DlTuBdJ`?kSo=9F5wXpfSX0JbGr;{Dp|TS13$1q_o*F6TxdfRUP{V zDPob$ec_Aep_8?49^u9zTZs59BPiInc+^%0nnE#!B+h`{fkS5q>=?Ne#b6S)hig5PApp+D0!B)+!rWS zc38yf()@g1E(eQ6c5`G_PuLu~!`lxj9imS%ucS(~d19FuI|495}hW;3U-h(7i4i@pQ1K(hkbkgC0CQq9gWxh{4j5HBz`5OS|5|PpYQJi@2m7 zjM3?zf^=r2`H(Z;!PDG;Y63p{Wa5C@=ByGl((aiOE&lS-6V6+F$9Jn1#q_=Ufu-U* zPN>fVFYFR^@uImBkydukjWA_Cxkhw{JR&sTRIS(&>{BlaXG$9CoEPxDrXhYp1tYu! zlQaN*?Jk*JNm6occ)fyWKRbjdWHBp*ikxEa#7s(@y2>^lHhc2P-70%C2_bFS z;oF?W?l`}=?lsa-3{dfC$P>Ih4m_HX==1&GF@t&B&!X;`c9tW&eD2v;=Q^E{;}q#M z#^R;_D29{g%p%8c9cyXbVkFDY6hT}MpHIq4cKkdp`&E$sAGPVwm3@{*i=!dUH6Ym5 zJyqcN!II2Rf4sng<_jF?(vnjPTEN*!yHz4qF37h`;wW?!l{DZ7=C}F`S6BPBp3g`3 z0T`8aJw^5wCAB`I?e1YngraFBwRlH0oyp)6rCXMvm7^vFftP&5iV>gSbd&L|_vh){ z0E?~q1mfnp^z$S}G5>;qqO`btUBwkw^y>MR^$+HnaZP{ftG58=$Sm9XGNte@dv>#} z@r4;`r-R$^IbDfW+SwOs>b6FYy_Khme316rJK=Bg4XE!~u`hJgw7Z*=CM^eBV>{d9x7kLRVU*ju- zwT9z8aq%G-tgI2El326W6^Koi{_}P`@r&aAd;AvX)ukaE)+#DO@;tV-LezIu3wgo~ z`5C1Y@8+I=-n4EWuyp?b(3>LC)#J3@zIh|yC(zAsQjPVKwM8yyHWIQ_Cykh1#W}=O z>1*sW|E24KJq3h>(6pOEw`F3~`KJ^+kGYM!n*Y9>JK7#x+!&hSMy8XFxVg^kJ=;QD zR9tGAUQ6RW{}xigS%8Td>OQkij`9YlnEzVC-0ze95u!>Zx|6KIjO?k;78*befv)2u z1lG$R>=mk{ID7&sqn{;8WWT?-w77a($ITw?%|<F4j&65j7{|W|6J?6*^SX+&{G5|A@@NK8R37?Wgy@={Wpx^$AoMRb(6KO0}~A z((`xmV_dRNTxlY{OiDsr;7)jAzfCbsca5gnSDQO{YO^qEEujq^wf+tODhX4w?u5 zT0~g9&h9Z#%9dlJ!eI95$Zry#+jlb`z*YQvC;KMBM#BObhBi*+QU7AB3xcR zjYpx>f6guvDzL}MX892(xr2A{L)vAC1;+Xe$4x7zuW;HQ!6SP`7OoByK!ZksMmkN+ zhOoOZ5-(K0^x)Srr4{xOKhhf{<=Ws`p#G{Jbam*9w;33fSLizWToQ+mjHKKuwj16H*xKjHkY! zQ=p7XigX}uIU{FK1p1Syrpw}Dd-7ExOXmix-cI*jVbfU%MahKQQok66Yp_cCuNl$& z(OV9de-@MMcieql(NH3-W#0HpT{aWfhG2JRo}2HaOJS{F?kdpVAJ;4V{L0zkCeIc8 z-t-Lpbu^O@mDGor;8=rO@?A=!r5RPX69+-v?LE75E&%kNi;bZfasaoO|NI8_Qu!y7 zxrb)y%Z#0~Nn{L~`-&K_a>Eu}-|;Y3lFTMC3egu>W5X!iN8-2K{g>%dG|iDh_IVGKn5^vH^h!S~{?FSl9j5y6t8*OQUs zoD}=6;P7eBQDT&&P9}ZGRH5jQ1xcC~b5<@l;4*4(Txv{Hi#*zrOe{9l*l=KtN78?9 zv`W-EAyO!3=c1aG`@%#ztKk-&+f(X5o$~&<^dWY)uB}$ZmYtOuWc;}{FXY6A52#au-noF+ILEV_yi6nxPX zkupCr$*zHGP1!2%+}fb6e+3zTfr2(ohhTLjH0% zSv_Hu?nipd{Su%xOWy(g;%4H|x8AUA1AyV=c$2yojy$Xj`|_AU$q9ybLo4T=tYNp46h6SHJg&=+Ryv3NI7!AFg6s7fO!}J z`h*fcNK0z%2FWEan=gp9%)IUh`@i_1u|XR_jM2+L=OZ8_7l>=fH;#?V%Kh66pFCWw zC7Kgt#@<21Qd1+`dW+lhzUBNN^rRnD#~e*8ra*3G$q|9?^(m@~xy*uMuaH0SW)!I# zR>K;?)5`zZJ1->a)&NmyMUjW7_QcYs9!;~h*Q|V3FOTs)H~Lj}kVDp(vTTl99f=RS z;8X)h6Hs*5^^{N6q932bC9EC=-p=KTV5|aC$`cOUFeR`WX9OMibEL?0b2-)E6K&=X+&A`0c=_(xNlS=ult09CtSdXt~(c7|pC=jxfif$I$v2b5Ua_zSQ)t+Qrj{v32 zx?nz)qR?S8OqG6flGQw-2A!X6DC4y&E1FjxjD8>gfn7cdl;Dwrh4I>vc#~Q-E;R>S z&RXnplCCd>Iu9nH3lMFrHt+Fou2e5doC@mC|E~q`!Z{WT=(G$z^5m>ab{(ik+giE) zYiEJp1D1-3M4!;1@0}G@4Jw07}o&=n!1wCc*4yij} z_{E!y)?65(w)pKpl`3^o#krKX+cC{0^zB-kb%SDUFc6=SrhFtu#t%{IyzF4iL z%JhIDaqj>_TGo^t)|R>o+-SbQYwip3O8z36>gy#6*t-|-d|&veA!OO+RQ@~ghm1+h z@U^nZF6njXnVTcz#K15w4m$=uNjfY3+w|m@O@oBLXgBjW>4T5GWF>Un`UrW5ntmi# z_l>M1pu>79t^JYl8)4j`V?Pu5tfZ;E-z34GhmGdY&c41*3h!g&A|!|d>Wv)iA5-3H z;otxc;QH*XFzMDuEE3jD&>~U-Hvzpd%rUtW>1ox(n5vI2bMW>2a{t-o)=?nauJS|V z%7w89Ol{R%vz?_5|6nftrsRiHWN2@Dva&?m5h79WHyN(T6f&8vfj$bX$%yGi^tkn8 zf|6fiyB*1C&I1JvKzTyz%#KO*Qq<4ZaYmCILbj9WaK;A&2a%23(Us9=GTpkjjMz{# zCdK6DjkHdH@Q92_QM; z2qigwTfRaZg*)&AO-b^Z5)yDEK2au>QCua*sb15OT9$XKS+8raq)>GAtA@3j_~6<) zV3=elCUKnE6<>i#Lhbw8a0*XVR?gu(g`|SKixR_dY0iLzl7bBCATCAO9N50($@559_-6Z1F= zkc!IwENYSeDyu?pXU-ethOcY~u!0PFY`_>Jz-_U{A#8E+B6PW3E8#e`lLLYX*J?g4 z1r_L`RKB&2#KW89UC*YOQLwa$sp&P=$qi=?f3t9fUcQ%!`86{bC>B?E#u(}WCoP%! zMdDF0x_UmAEK5^sybpcbCwUs7ro?(;K8|_?BMJ&Sq=W|w!9juS7s0mn0W!A`u3-`% ze{ywDkxwsQ0cniUp9ks=7n&l*2N_JrGU5M>T#n&@AAQ+K+t-HIh0<0*J-rk^Z&>Y%*flq%uhC8*`4>pyZ|l zWrxJLhib88M7a2*-AIK*phHC@Wa>p4Q3lNj+BP#N>gxufkkDU~N)7?0J@fZNfVftq z_qy73m|K|QFl_8K^Om}*!d1VMr<)USl&|}nu)jevWLl2eE6P2`RFXq$f<-CRUOa$5 zz4z%GTgw<%R?_iCwIqjuw+B)tkG;RoTN3e7DfQ_7^M_;;KbW`l=zedrK;!d==_ep}FWbYAIsu}o~o*|7Ww9cP($)b@%T=BAXC z5WSd{PNENAF#06%{yiNeC`OAQigdUNn6(}L&b8+*_(sbbO#ZvVsp|_9A_V0;sbdU7|F1YQkQlcotgSS z%Or*R-{lPBPsrtM_RRGAJ5Fn$d) z+7^ct6S9nUb4LVz9y}#hb|!_>iA{)nuY`dV;A@+!aH{h zTb9i<=w58k&n;i~`u3;7`bV%S+s_Dvld_PI^^lV(n}~+=?Z8ae&ylEvdNRz{Tu^Ht z816$M*qT9^EVTnp+nv}xg4|WF-R7js&ZZL;H{ldA3gtuBbG>4o!IO4I&rKmaVb~rT z`YlL8)$_vr38#{ygPHH39t<=i5|A14|3sc_JXc$_c(Q((Uu_aWDXWJSygJauF8r8_ z>SkpO(ZZfK(;@0Jn;RbkuFEoxdBvN%mkp5=ANx|)-C(hJnhkcqDx?0zX!CrGcBztKawjgg-coV=u897w_Uk0hq0*4jENeUnJ1n^t4pu=%A#zB7 zQ1Jt;^gB1-s0)$ZT8?fg(tFEREo?wNlk(8~t;%RT3fa;e%MAOlYHMgd`e6veeTtcB z4K0GWCpNZ*dKf8emWva^i8?F~zo~&9*hHV2%;3^Puxj1WR(?{zy?qa|0F5%vY}OQW z%RA*};mb$+tf};Ld!Pes6hA+1>BiBQpsQh@ z_i&t$LUKstd73|ezv>uSih~}#qt0F-CgBS^^Uq55)jxC`eU3q>ykoh#=4}*xPX~N3 zCCPXbCY2^1el-loY6ECBG-9K8v>n$VV!@Dc!@(?#g@nGeV2p~ofi(R7&lXgngSmBx zX!KHli%-*h$G8B3xUtQTmbgKt1=LgLSJALKLMt@7sN)BPaH;4+r4ti#?cA(4O@##o< zFs{ZQxf^;9PCZX+N1#zVolikyTdmR2z)oMUNvngnE0ghHvp=NjC}{-TMhpwY99~>v zhY90=!QkEq>1xsN8=9yAFGEMx9F)HkxS^1lh?ID<$8%$;(|aapL>~?!?DXUyUt`Se zgpo(|#?gKq{PA5eYRVq6`DQzgXTQXn%`U}23?EhMz{K%CO`}g{4|8JxMXmQAIL6d+ zp82NiwdQSw%q}-WIcy=>B?zMECVe^G2s5lQgpU5~f;bc3Qv3=Al4zJ-esr1=(HR_e zA40B3;ab-2>NS_zD@j1k!2&wl)xmDH+_a-n%8?`pcxkeJMMFpH zX;ZRaVp}kwb|dv-S)8?73Bs!Eut?2awON&LaQa0o0(zCS=|9d2x|QG*Vmt+ zFsOnck`7d|5dt($s_;ZMV$tTQdp6+Rt5SuEzMXIIRr{^)FWKWvJ{#X3g(0&)t@Clq zezmU)1K`i*N-M->+kq}G-5E=dWd3qQ;f`%d7`4gIub?}tgDGLoB|OTFHQjwjvsxq6 zU(VG>&WY4-V^aeY`li~0|MU~%Tr-Oz>{jga= zWGQ)3=mS^^6J+=_4(S1qON|dwFMpk-!Cwdt%o=!%H~vn=_8X zC6_bv$nWzAlC z_>ATdG_i&aS##2TRWu@#x2SmH`@>W6UD8CEZo6puH)3st#G%uJR4|zMIzstm}q<%qGlB?t~Fr<~5YfF4$*S6~_(n zXkq52I|YW|5(!MEOK$l|$D(&bCv`ez$@q*RA`_%-;mX%1^f>k{>K`qwE?q_r%}}NF zgxTis&Up4ACN2EqO~;^TklS=&Y82uqK^QI>rNW31x8N z$F`R630v8!K_qml?2K5Av{DhxJE~5YQ?3{KMC+PM5nKxGPQL^qy8{uNqLStu;tm*X}&_0}|#`gMYL_S)Ut+WjX%^Nx1W? z9ev_z8c-75>0`-(w(E{g(7E zls8#12uwa2*BJ0P_4rFy_buKH^`Xl$VI>8H6lf#;tdjCpz$ZCT@nZdxTF5sp!7*S% zt0C$lC`>QJt+cwiAtos5X@R6VMo#-L_+8bqd*TFm`l9Hh{RH^WOfdZX&~dP5)DkL( z4b1mZQ5_(EyB7VyJji!NQvLi)B4e@KP}+L%yk#bV!}KXa?{_-mfJxC%LD0^XScj@n z3;hCU+hE9AQqt%^?UAbZT*K;fJ4&9}Y-y97M6X}FF1pO$J@Em_Sah$;PGdV2*TK0NI;cq z$7+ykYk3eia;S0;ii!SJul2~Yn$LzFhTK)E*VcdK#4LyUWaiCibH{M`IQk>CAB}KO zl7D?Op>wz9V2f|hcAr6}s7I!@z>dVlMy>rrVsg6IAInck3E0do;xvMSwfU65@1SbR z-b5%);JXm;LA{vjJ2Gf=X7|Jh-i^snaTQ_P8H68eP8C3dHO%e^Up07LFZ0LdcHe-S zrm3@dXB3^|%-o$80dDm}6?ZK71nXGVHGio7e{o{GX}^CoSt>>xy_^Y!)P-!z~o#H(#%GcNKo@ zj@j_Z{YKmoI2L`MFl{W**K*Zvt{uT}zTwDjDv!C&fL3xzWP28514r$VGZ3Usd;bD$ zz%zE@LF8s8|BRp1G1e4@XC~+Z%4GM4;sUAdy+0JTV0M@$rEM7GvWFMI*;5~ z6Z{7`U&x_Wvo%B1Lr-c zW#Cs@pBb)gZs)5Jrmq%NUzz%9nRL;|j6a@@OzPW_i1h;(Lhpl}a_;XI3$1bY2(f6y zP>{$&7-92?*h)OgiCnYBhUWuSdZyXo_3MxM2c6|Au_nT#ACGBGPyiQlv8zJ3alAQb z$SE^O_Ij9(W|XhA8f6nt@WZt%EJF1DiXMa(gz;>Ef{9*#4smuf1YyANC!rBO(~EuI zZ_Wzz03(~OeLy4m;=}@Y1D!-{)RP&q>`PX)8`_m4)PWrd=Xtp;>2*P`(+S#OzJ|nF zc+cJ|_c1zR_r>qiX0eR#KAOOm^F4YKvz0Vs!eUrP?rd5oo|T5Z+Nl1hJJ_VTK#|J^ z)P-?c<&s}W!0Q3gf&RA*&Ao#DUUlL&p%FALth%^RJnR5A4d_{Ypc38Ud9o$U)B zdxSad7o;h(&b<+HWd8xud&bEtKLi&mM86vR7K!aUFXPgte9a4Z$Xs+@zP-9Qqn{;y z?mcaLD*e{|Jy|h56N#jB~Y9b6n<)-~h_#(vieix^`q5s+3>E`=A%SThr z;k-t2XpF=m(&Jn(>+HJ5;2X*2NWEoIZlzqy@vC|9n?tcSHR~K9sHq2J)r=|1C%fD-E24GJfK)jv z%O;W7AOYWdvx`XjpI{l~N}4pkH9pHbMhl$Kt3%v-H=Os4Zk}*DGW(#H!dYCz7o3*v zFaViAh#6oe11^=}=11GPEW00KCN? zD=D?|ki%yrlF_hz6I%j#XWhN#W6czq9q4kNFC__48Ihf`!xz9itgK8&yO(!zQ4-Yj z7&7EgJ9<1>d#b*z9D*ICQ7A9F!Y1SM=*G@J0MMgBQ1id^a*^MoEWF~Blt4ic>#D!@ zF+AKzoTuV1xbpi#^Bfj}Ah@kePfQ+`Ez~Veb%N72ow|CTdb*2-pb+Xb}>9&Kpha?D4FwgidFI%%Nn?At| zRGLHp7f{>3LjVmf)o-@EO3}@%@a^c8HkB#tXx@GQxG!zvfath@60oDdEC@B=gu)DVgQ>F$&qVz* zm0Mnz-ag<+w0Z&Z#MX(p3wY#m`aZnNMU{i5#&EWYU|pPIS>sS{!|;1cGWvGht^Sx& zEMZ-L`=0vqt_E3%B#)a4G+F0Ko6p~n01II1>L!$E{~BpV1N*kp9V_#T(k{y2`oUXb zKqU@{Z^$Te%Xu}S7eokXBIwq@eu~tnfd^wda3egd5A!U~;veyem;kL#(sU7Xs9hW6 zb=@cZHZg6q*Mgt5)h}s1Xll}aX&k8I{*3%K@g9zMLf#`*L-Kl~6+3=*>EY%ul|90= zS5!^a_1>iv@jZw_^mmbYi!EQarH=hccgPz$YIIY)@Nn(q#JeQy(~WzVv^K7c#Za%?|GG91uiH-8^QJ} z7^nkhJUBA#R~7abPC%(x4r)~HSlp;-$go8v-(9vIon|7-@Uh!bMTC4{@1F{Q#w}tl zGlWy9F)UNdC03{((%i^Zm8e&fqD*MZBXMz{mFWJ#uG zvvByp=AdLEYQTvdL~s=S8fYmpMH`1wU6uibV@JL>Cr>L1?&k4qz>E4)t^HocrYXn!~BKYv2R6#N1lJlJ@op zw@kT$HEtIXeD>gxc5_R$AfSj~Pw8PUyLtGn_W5c5shTkJAIz>I+g(-(Ga6g@gZJfc z$w2I@vS0yv+}K6%J_VDT#&6E87KgvTd-IhvdGx;eQHgxjyK28=q(jx4jvV4!pv}pV zL@zH3Vfa!0XGIZE9v)w_6^v7Y_PNJHVX`bnps?`PYV|T32+6#I38-AXA29azJJf1$ zt@!!L-o}U4OHdMdH!L5Oo-lDMQdTZkYF*REul2>aV>e?dj!VGw);){g#xpfC#qU3W zaCPDKprVNlXB(&+;UIu(PZ)p8P&)*`>-SZ|7)m0e4u(Sl!c3&>Px|kyJ%H#hOGZB* zcYkt#qT41v+%*qRfd>Z2{tSc|$Vdk7z3uwm_Ng0ncE-B+l8Bp-a%5a#Id?p&|FGz+ z8d|pUhxJl0J>a0ArU`~)UEpHzM3t+BacJaZCV`Fs~0K_zi3}bH;=ptQ@&1o1z?^10RVgu)+2dm6VJGHlDzo!6RHyx z<)TkK?zCnw4sF_LIe3#-+%zE;r~zCJ8(@+v-}%TUr^gF z2=G&NZmBlZHK8GG8Gq4`RIJ?O1}{oFIQj#n9%H-Jphp0`$2%qZT&w3oN3Ix#%h;L zE)Ao+!<<(yf>0WEfh^0xUa30)ZNto)&XU-59#~t6s(rF-?KZl)mF)^Q8X( zqhjZDQxT{M45ndje#1((Y649kcNW^YDS6H^pSar zLDvU%oT$Ueo*$X6e0qPSX3l{0r_IUP85}*|Kc-HH?D~6NMgq-p*T6 z=0!v~Mrify-Ep<2<1EXIUX;WGHNdKV>E)v(eL8^8v@6dCrmGbC5f!JjDzs?SDr*vC zLKpv4{idy}ybsgxFZl2h$2Kp{Tj*(;mewG%g>%B#W-6F6?xiA4lA1?@+>n_XRbAnW4AimMH`oB+~ zJlGA{%^fmBJA6=|_iv&Y2l_mrYq3mu4A5}W%>TF`@V;pPO#N!nfM63L?y=*2lRB7x zjBa$hfHZa#=EG>q2B4AEk7*Efc;J%se^WQXMPJnJ#Alw#KSnzmS}JKqL+8!v=gg{! zZ<8AfP2zkQSDBXJINyFK(n0A*K&!@qc^&M`tf~w08)pZOacaby8E}HTsSJrQl@7(% zV&z8@m@JE;tGi=)Q(6SC^E{&57IAhFVV}&c z0A<;?^Bgc&;Z{eOMw~yT2t$J>bRO9LBjc0xk=`h;LpDsXn?jun6OVQx;IVqpwc68{ zQI+%FKInffWB(O|DG%5=qt(a)GOWpPuU5?Wt|#;jS%#c)5_g$vfd2imHsxgHU z;)QY?NPhB!?270cN)|QoKBDce{15yv&AQ&>f&hqu=QI}1yw#W7QI>0GO3K_rm=uu*b{{K2$KkJ0q&_ zMZM>yYF;*>CjxQ6;ixQ`F|LzYelp@EL_T{#3IOzWAUrT+mB!zGq16Yc#|Lpga|Z>d zKQVH!LlkL_7fZQ~xRDKvZ^l)R2JwwVmx162OL7f^Z*&w;gQMT zF*p^+T9!>nBYpf~RtNk1u)3*D#18xrB9n*ykCa%JnF~%AYUDlLtMu?;Fl zv(OEGwmW_s9AxE-25ANbHU}k7Y5U|HdojtEEp44Pn2bIYUQm`9ty`km{E@6kV(|-r zHjd-sX#n;3+`jKtuB6{cje;>6F)i!kNK2ym9Qzx$8Dxq5fpI88sn{TeT&O(=)Pad6 zqX|v`kOr|vV4-poy6qUoR4l*a1Sv_FbSh;~tUAmpaCV07Ghwp8ni$R&@MBy}A{K>=E*kl$qmV=p)CC8U5qgaTxf#7F7KJkcZyRi$xU4m5zf?@aL=n%#^Be?($- zx(P6tLCNb1bZBb|RnV%qol61QCEX;iGzMZNp+n3qK%wBG01wDc!Iq&ckre8wU`Ur# zjR3xF`OCPp4-**v#(WZJ_E!K%F^vG?64clN!kfeEK7-KI(!?kW%>~}L03Y2PPP~}Y zw?aNeDW{SD-7Glwz!$t#;4-z(f);WI!GaT6;J?w0*L4P;L%3&)?iFQ z)meN@)2P`&7*p~rF~z`_LN-9BUKz0F&nEEime`XLptD&t8-dhq@-nYj#UmI#5wu zHnj^{G7uwf+ULte&mG*rljCI6PFs(V`l?fB8hvh8`Ick{uH$wW=zNY|E~u69&+e|} z?Edy(Mhv;ee?unMpm8W@ZXm2RJq$^YNL!m1%Iz_8tf3qL0A?fX%ER%se{cbhLPO6}Fz5vL zKcELNi-Vt1voYqq4b|XETE2Z==*OFowOQ`*D?|sf$s0EKeOn81h`dCGXT?N%lxyfV2qy>>UMOc7gTgUeAm!g`R+OOR zq7DJ@6~D{Z1eI>zj=W+xeL6yYQ!uibm&4L_e$!M({3*WT4u~B>fo^UPKTyCjNBr-u z%t@uab^joGD*5bwphT;xq~MFJgCN=ZS>UsAyUstFcwf8P>Q_g|6LY1Y3u@DJUCr7@ zU!ThOFbSQmNaWrCszCyiOb^mEOR8J0nL4jAWFA?!Gqb#Pz~65~+t-d@WEQX?9}UZw zApds;?Ljj8=iv@mmfI%XnoLa>I$?mRT&B$u*hEzRGJAewwL6M~o{H{& zyO+PAL`oa$LyeVy`YFHcpcBs`%*}B;0_@i|#FWnZm=4S%%hVcy{vrs3G<Q2%NtEBQz-@GA_UHI&k=gM0~T_&qfzmiZB&n4#u$&Xp z5CDJsq;d0j338S8pnd9nvQB$j23m;u(Olzd2Hk@5DfI$-O77MdYx9nNz#5o8elDL`WUp2YxUdG!9Y_)R)) z&^Z4wCOxm7XJ!$SilQM9Vz7jP8061E2SQGBL)9w~DrGI?d84jfnUg3eM@kvSo zyx+6zgz&W!tgBwJI%BZq#^cbYV-fmkUsqT2$%^IfF2g9*|3;?>1^BK>`7y@2-gx73^d6E>^sB^dym~YwK%@jt7J0bI zF=+xg_)oBbCRVV1^p!4t(q1kEDQaG8*=CU2%(gwEe0f=snQUFUyZhIT{>Ok=I-AxZYSH#Cp!KN$SKh%~fu$J0>3G?;(*w-y zK&g_eTS^j_rC&6KYQz-}ap*-KD85z#44gdkL3Yn7U4sdq8mDr-^MX}=5EbsX<~hqi zW+}c5IO+i6%_{8g2oM0c9x)fier5vt|8W{M)(^dz^XXmNq;;AeG)X8+7DF;&GE~sR$ fZrpnZhE_Vl>Pe(70R`T^2LRUQ4rU!DKB@l)HtG6p literal 0 HcmV?d00001 diff --git a/Notes.md b/Notes.md new file mode 100644 index 0000000..8c5e7a5 --- /dev/null +++ b/Notes.md @@ -0,0 +1,6 @@ + +These are just some notes on the blogpost I was following. While it was great, I thought the author might want to know where some areas were lacking (i would). + +- in the definition of raycast_voxel `step` is never defined, nor explained. I'm pretty sure it's just supposed to be `step_dir` though. +- `get_voxel` returns the type `Voxel?` but `Chunk.voxels` doesn't hold a type named `Voxel` only a HashMap + diff --git a/README.md b/README.md index a64cf02..660f799 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,9 @@ Chunks now generate around the player in a square based on a render distance. Th Different voxel types are now supported in the data structures and passed to the shaders. ![a world of dirt with some stone blocks](.attachments/voxel-types.png) + +## 0.6.0 : Raycasting Placing - 11/13/25 + +When placing a block, the program now raycasts to place where the camera is looking. + +![a place of brown dirt with word Hi built out of stone.](.attachments/raycasting_test.png) diff --git a/src/Camera.h b/src/Camera.h index a545a52..fb1d2dd 100644 --- a/src/Camera.h +++ b/src/Camera.h @@ -4,7 +4,7 @@ #include #include -const float CAMERA_HEIGHT = 1.7f; +const float CAMERA_HEIGHT = 1.8f; const float GRAVITY = 18.6f; // Should be in m/s^2 but it doesn't really work out with 9.8 so we double it class Camera @@ -62,11 +62,12 @@ class Camera glm::mat4 getView() { - glm::mat4 v = glm::lookAt(this->position, - this->target + this->position, - this->worldUp); + glm::vec3 real_camera_pos = this->position; + real_camera_pos.y += CAMERA_HEIGHT; - v = glm::translate(v, glm::vec3(0.0f, -CAMERA_HEIGHT, 0.0f)); + glm::mat4 v = glm::lookAt(real_camera_pos, + real_camera_pos + this->target, + this->worldUp); return v; } @@ -92,6 +93,9 @@ class Camera } + glm::vec3 getEyePosition() const { + return position + glm::vec3(0.0f, CAMERA_HEIGHT, 0.0f); + } }; #endif diff --git a/src/Chunk.cpp b/src/Chunk.cpp index b2c35c8..0bc1864 100644 --- a/src/Chunk.cpp +++ b/src/Chunk.cpp @@ -46,9 +46,19 @@ void addFace(glm::ivec3 pos, const float *faceVertices, float voxelKind, std::ve unsigned int vIndex = vbo.size() / VERTEX_SIZE; for (int i = 0; i < 4; i++) { const float* v = &faceVertices[i * 6]; // Face data is still 6 floats per vertex - vbo.push_back(v[0] + pos.x); - vbo.push_back(v[1] + pos.y); - vbo.push_back(v[2] + pos.z); + float world_x = v[0] + pos.x; + float world_y = v[1] + pos.y; + float world_z = v[2] + pos.z; + + // Debug: Print first vertex of first face + if (vbo.empty() && i == 0) { + std::cout << "[MESH] First vertex - Local pos: (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; + std::cout << "[MESH] First vertex - World: (" << world_x << ", " << world_y << ", " << world_z << ")" << std::endl; + } + + vbo.push_back(world_x); + vbo.push_back(world_y); + vbo.push_back(world_z); vbo.push_back(v[3]); vbo.push_back(v[4]); vbo.push_back(v[5]); @@ -117,5 +127,10 @@ void Chunk::generateMesh() { } void Chunk::regenerateMesh() { + std::cout << "[REGENERATE] Regenerating mesh with " << voxels.size() << " voxels" << std::endl; + if (!voxels.empty()) { + auto first = voxels.begin(); + std::cout << "[REGENERATE] First voxel at local: (" << first->first.x << ", " << first->first.y << ", " << first->first.z << ")" << std::endl; + } this->generateMesh(); } diff --git a/src/World.cpp b/src/World.cpp index d68a724..346f578 100644 --- a/src/World.cpp +++ b/src/World.cpp @@ -1,10 +1,34 @@ #include "World.h" +#include + +// Helper function to handle negative modulo correctly +glm::ivec3 positive_modulo(glm::ivec3 value, int divisor) { + glm::ivec3 result; + result.x = ((value.x % divisor) + divisor) % divisor; + result.y = ((value.y % divisor) + divisor) % divisor; + result.z = ((value.z % divisor) + divisor) % divisor; + return result; +} + +// Helper function to correctly floor divide for chunk positions +glm::ivec3 floor_divide(glm::ivec3 value, int divisor) { + glm::ivec3 result; + result.x = (int)std::floor((float)value.x / divisor); + result.y = (int)std::floor((float)value.y / divisor); + result.z = (int)std::floor((float)value.z / divisor); + return result; +} + World::World() {} void World::set_voxel(glm::ivec3 position, std::optional kind) { - glm::ivec3 chunk_position = position / CHUNK_SIZE; - glm::ivec3 local_position = position % CHUNK_SIZE; + glm::ivec3 chunk_position = floor_divide(position, CHUNK_SIZE); + glm::ivec3 local_position = positive_modulo(position, CHUNK_SIZE); + + std::cout << "[SET_VOXEL] World pos: (" << position.x << ", " << position.y << ", " << position.z << ")" << std::endl; + std::cout << "[SET_VOXEL] Chunk pos: (" << chunk_position.x << ", " << chunk_position.y << ", " << chunk_position.z << ")" << std::endl; + std::cout << "[SET_VOXEL] Local pos: (" << local_position.x << ", " << local_position.y << ", " << local_position.z << ")" << std::endl; Chunk * chunk = get_or_create_chunk(chunk_position); if (!kind.has_value()) { @@ -22,11 +46,11 @@ Chunk* World::get_or_create_chunk(glm::ivec3 chunk_position) { // Fill in new chunk with a flat plane of voxels at y=0 std::unordered_map voxels; if (chunk_position.y == 0) { - for (int z = 0; z <= CHUNK_SIZE; z++) { - for (int y = 0; y <= CHUNK_SIZE; y++) { - for (int x = 0; x <= CHUNK_SIZE; x++) { + for (int z = 0; z < CHUNK_SIZE; z++) { + for (int y = 0; y < CHUNK_SIZE; y++) { + for (int x = 0; x < CHUNK_SIZE; x++) { glm::ivec3 offset = glm::ivec3(x, y, z); - glm::ivec3 world_voxel_position = chunk_position * 32 + offset; + glm::ivec3 world_voxel_position = chunk_position * CHUNK_SIZE + offset; if (world_voxel_position.y == 0) { voxels[offset] = VoxelKind::Dirt; @@ -42,3 +66,86 @@ Chunk* World::get_or_create_chunk(glm::ivec3 chunk_position) { } return &this->chunks[chunk_position]; } + +std::optional> World::raycast_voxel(glm::vec3 start, glm::vec3 direction, float max_dist) { + // Normalize direction to ensure consistent behavior + direction = glm::normalize(direction); + + std::cout << "[RAYCAST] Start: (" << start.x << ", " << start.y << ", " << start.z << ")" << std::endl; + std::cout << "[RAYCAST] Direction: (" << direction.x << ", " << direction.y << ", " << direction.z << ")" << std::endl; + + glm::ivec3 pos = glm::ivec3(std::floor(start.x), std::floor(start.y), std::floor(start.z)); + std::cout << "[RAYCAST] Starting voxel: (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; + + // (+1 or -1 for each axis) + glm::ivec3 step_dir = glm::sign(direction); + std::cout << "[RAYCAST] Step direction: (" << step_dir.x << ", " << step_dir.y << ", " << step_dir.z << ")" << std::endl; + + // How far to step in each axis + const float epsilon = 0.0001f; + glm::vec3 safe_dir = glm::vec3( + abs(direction.x) < epsilon ? epsilon : direction.x, + abs(direction.y) < epsilon ? epsilon : direction.y, + abs(direction.z) < epsilon ? epsilon : direction.z + ); + + glm::vec3 delta = glm::abs(1.0f / safe_dir); + + glm::vec3 fract = start - glm::vec3(pos); + glm::vec3 t_max = glm::vec3( + (step_dir.x > 0 ? (1.0f - fract.x) : fract.x) * delta.x, + (step_dir.y > 0 ? (1.0f - fract.y) : fract.y) * delta.y, + (step_dir.z > 0 ? (1.0f - fract.z) : fract.z) * delta.z + ); + + float dist = 0.0f; + glm::ivec3 last_move = glm::ivec3(0, 0, 0); + + int iterations = 0; + const int max_iterations = 100; // Safety limit + + while (dist < max_dist && iterations < max_iterations) { + iterations++; + + std::cout << "[RAYCAST] Iteration " << iterations << " at voxel (" << pos.x << ", " << pos.y << ", " << pos.z << ")" << std::endl; + + if (this->get_voxel(pos).has_value()) { + std::cout << "[RAYCAST] Hit voxel at (" << pos.x << ", " << pos.y << ", " << pos.z << ") after " << iterations << " iterations" << std::endl; + std::cout << "[RAYCAST] Returning normal: (" << -last_move.x << ", " << -last_move.y << ", " << -last_move.z << ")" << std::endl; + return std::make_pair(pos, -last_move); + } + + // step in the axis with the smallest t_max - that's the next voxel boundary + if (t_max.x < t_max.y && t_max.x < t_max.z) { + pos.x += step_dir.x; + last_move = glm::ivec3(step_dir.x, 0, 0); + dist = t_max.x; + t_max.x += delta.x; + } else if (t_max.y < t_max.z) { + pos.y += step_dir.y; + last_move = glm::ivec3(0, step_dir.y, 0); + dist = t_max.y; + t_max.y += delta.y; + } else { + pos.z += step_dir.z; + last_move = glm::ivec3(0, 0, step_dir.z); + dist = t_max.z; + t_max.z += delta.z; + } + } + + std::cout << "[RAYCAST] No voxel hit after " << iterations << " iterations, dist=" << dist << std::endl; + return std::nullopt; +} + +std::optional World::get_voxel(glm::ivec3 position) { + glm::ivec3 chunk_position = floor_divide(position, CHUNK_SIZE); + glm::ivec3 local_position = positive_modulo(position, CHUNK_SIZE); + + Chunk* chunk = this->get_or_create_chunk(chunk_position); + auto it = chunk->voxels.find(local_position); + if (it != chunk->voxels.end()) { + return it->second; + } + return std::nullopt; +} diff --git a/src/World.h b/src/World.h index 82413fd..fa51d65 100644 --- a/src/World.h +++ b/src/World.h @@ -17,6 +17,10 @@ class World void set_voxel(glm::ivec3 position, std::optional kind); + std::optional> raycast_voxel(glm::vec3 start, glm::vec3 direction, float max_dist); + + std::optional get_voxel(glm::ivec3 position); + Chunk* get_or_create_chunk(glm::ivec3 chunk_position); }; diff --git a/src/main.cpp b/src/main.cpp index 29e821a..7982996 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,7 +33,7 @@ const int VIEW_DISTANCE = 4; // In Chunks bool W_pressed, S_pressed, A_pressed, D_pressed = false; glm::mat4 projection = glm::mat4(1.0f); -Camera camera = Camera(CAMERA_SPEED, glm::vec3(0.0f, 1.0f, 3.0f)); +Camera camera = Camera(CAMERA_SPEED, glm::vec3(0.0f, 0.0f, 0.0f)); // Callback function for when the window is resized // glfw: whenever the window size changed (by OS or user resize) this callback function executes @@ -86,10 +86,6 @@ void mouse_callback(GLFWwindow * window, double xpos, double ypos) direction.y = -sin(glm::radians(pitch)); direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); camera.target = glm::normalize(direction); - - if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_PRESS) { - world.set_voxel(glm::ivec3(camera.position), VoxelKind::Stone); - } } // process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly @@ -176,6 +172,57 @@ void processInput(GLFWwindow *window) camera.speed = CAMERA_SPEED * 7.5; if(glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_RELEASE) camera.speed = CAMERA_SPEED * 20; + + static bool left_mouse_pressed = false; + static bool right_mouse_pressed = false; + + // Left click - place block + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS && !left_mouse_pressed) { + left_mouse_pressed = true; + std::cout << "\n=== LEFT CLICK (PLACE) ===" << std::endl; + std::cout << "Camera eye pos: (" << camera.getEyePosition().x << ", " << camera.getEyePosition().y << ", " << camera.getEyePosition().z << ")" << std::endl; + std::cout << "Camera target: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; + + std::optional> raycast_result = + world.raycast_voxel(camera.getEyePosition(), camera.target, 3); + + if (raycast_result.has_value()) { + auto [target_block, normal] = raycast_result.value(); + std::cout << "Hit block at: (" << target_block.x << ", " << target_block.y << ", " << target_block.z << ")" << std::endl; + std::cout << "Normal: (" << normal.x << ", " << normal.y << ", " << normal.z << ")" << std::endl; + glm::ivec3 place_pos = target_block + normal; + std::cout << "Placing at: (" << place_pos.x << ", " << place_pos.y << ", " << place_pos.z << ")" << std::endl; + world.set_voxel(place_pos, VoxelKind::Stone); + } else { + std::cout << "No block hit within range" << std::endl; + } + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_RELEASE) { + left_mouse_pressed = false; + } + + // Right click - remove block + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS && !right_mouse_pressed) { + right_mouse_pressed = true; + std::cout << "\n=== RIGHT CLICK (REMOVE) ===" << std::endl; + std::cout << "Camera eye pos: (" << camera.getEyePosition().x << ", " << camera.getEyePosition().y << ", " << camera.getEyePosition().z << ")" << std::endl; + std::cout << "Camera target: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; + + std::optional> raycast_result = + world.raycast_voxel(camera.getEyePosition(), camera.target, 3); + + if (raycast_result.has_value()) { + auto [target_block, normal] = raycast_result.value(); + std::cout << "Hit block at: (" << target_block.x << ", " << target_block.y << ", " << target_block.z << ")" << std::endl; + std::cout << "Removing block at: (" << target_block.x << ", " << target_block.y << ", " << target_block.z << ")" << std::endl; + world.set_voxel(target_block, std::nullopt); + } else { + std::cout << "No block hit within range" << std::endl; + } + } + if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_RELEASE) { + right_mouse_pressed = false; + } } int main() { @@ -189,7 +236,7 @@ int main() { glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 2. --- Create a Window --- - GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Voxel Engine 0.5.0", NULL, NULL); + GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Voxel Engine 0.6.0", NULL, NULL); if (window == NULL) { std::cerr << "Failed to create GLFW window" << std::endl; glfwTerminate(); @@ -227,6 +274,30 @@ int main() { // Setup projection matrix projection = glm::perspective(glm::radians(FOV), (float)SCR_WIDTH / (float)SCR_HEIGHT, Z_NEAR, Z_FAR); // This doesn't need to be recalculated every frame + // Setup crosshair + unsigned int crosshairVAO, crosshairVBO; + glGenVertexArrays(1, &crosshairVAO); + glGenBuffers(1, &crosshairVBO); + + glBindVertexArray(crosshairVAO); + glBindBuffer(GL_ARRAY_BUFFER, crosshairVBO); + + // Crosshair vertices in NDC (Normalized Device Coordinates: -1 to 1) + float crosshairSize = 0.02f; // Size in NDC + float crosshairVertices[] = { + // Horizontal line + -crosshairSize, 0.0f, 0.0f, + crosshairSize, 0.0f, 0.0f, + // Vertical line + 0.0f, -crosshairSize, 0.0f, + 0.0f, crosshairSize, 0.0f + }; + + glBufferData(GL_ARRAY_BUFFER, sizeof(crosshairVertices), crosshairVertices, GL_STATIC_DRAW); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + glBindVertexArray(0); + glm::mat4 view; double start_time = glfwGetTime(); double move_time = glfwGetTime(); @@ -246,15 +317,19 @@ int main() { shader.use(); - if (!debug_printed) { - std::cout << "\n=== RENDER DEBUG ===" << std::endl; - std::cout << "Camera pos: (" << camera.position.x << ", " << camera.position.y << ", " << camera.position.z << ")" << std::endl; - std::cout << "Camera target: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; - // std::cout << "Number of chunks to draw: " << world.chunks.size() << std::endl; - } + // if (!debug_printed) { + // std::cout << "\n=== RENDER DEBUG ===" << std::endl; + // std::cout << "Camera pos: (" << camera.position.x << ", " << camera.position.y << ", " << camera.position.z << ")" << std::endl; + // std::cout << "Camera target: (" << camera.target.x << ", " << camera.target.y << ", " << camera.target.z << ")" << std::endl; + // // std::cout << "Number of chunks to draw: " << world.chunks.size() << std::endl; + // } // Generate chunks around the camera - glm::ivec3 camera_chunk = glm::ivec3(camera.position) / CHUNK_SIZE; + glm::ivec3 camera_chunk = glm::ivec3( + std::floor(camera.position.x / CHUNK_SIZE), + std::floor(camera.position.y / CHUNK_SIZE), + std::floor(camera.position.z / CHUNK_SIZE) + ); // ---- PASS 1: Draw solid grey cubes ---- glEnable(GL_POLYGON_OFFSET_FILL); @@ -276,15 +351,25 @@ int main() { Chunk* chunk = world.get_or_create_chunk(chunk_pos); Mesh* chunk_mesh = chunk->getMesh(); - if (chunk_mesh) + if (chunk_mesh) { + // Debug: Log when rendering chunks with your placed block + static bool logged_placed_block = false; + if (!logged_placed_block && chunk->voxels.count(glm::ivec3(0, 1, 31)) > 0) { + std::cout << "\n[RENDER] ===== FOUND PLACED BLOCK =====" << std::endl; + std::cout << "[RENDER] Drawing chunk at pos (" << chunk_pos.x << ", " << chunk_pos.y << ", " << chunk_pos.z << ")" << std::endl; + std::cout << "[RENDER] Translation: (" << (chunk_pos.x * CHUNK_SIZE) << ", " << (chunk_pos.y * CHUNK_SIZE) << ", " << (chunk_pos.z * CHUNK_SIZE) << ")" << std::endl; + std::cout << "[RENDER] Final world position should be: (" << (chunk_pos.x * CHUNK_SIZE + 0) << ", " << (chunk_pos.y * CHUNK_SIZE + 1) << ", " << (chunk_pos.z * CHUNK_SIZE + 31) << ")" << std::endl; + logged_placed_block = true; + } chunk_mesh->draw(); + } } } } - if (!debug_printed) { - debug_printed = true; - } + // if (!debug_printed) { + // debug_printed = true; + // } glDisable(GL_POLYGON_OFFSET_FILL); @@ -315,6 +400,16 @@ int main() { // --- Reset polygon mode for next frame (good practice) --- glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + // ---- Draw Crosshair ---- + glDisable(GL_DEPTH_TEST); // Draw on top of everything + shader.use(); + shader.setMat4("u_mvp", glm::mat4(1.0f)); // Identity matrix (NDC coordinates) + shader.setVec3("u_Color", glm::vec3(1.0f, 1.0f, 1.0f)); // White + glBindVertexArray(crosshairVAO); + glDrawArrays(GL_LINES, 0, 4); // 4 vertices = 2 lines + glBindVertexArray(0); + glEnable(GL_DEPTH_TEST); + double end_time = glfwGetTime(); float time_since_last_move = end_time - move_time; if (time_since_last_move >= 0.017)