go(P76w8u`2}jwK7QTR-^371M^dZ9E>eX{gqHa@%ee
z=%3a9$cy|516#VC(Ty`#HfhPP<9pN@wIR9U9>Qkl2g%bJjy%<6O*(qvA`b~R9INDg
z*NiczGqOl#Zt+48iB!IEE3dnj#=D+J@d(k)lGj}!$8(+
z$Rwm`UDB;W>+`eFa@^R_#&dy>IXO3MF
zzQOC6@QC7~s+^1Y!iFH<{&1<4>oJIa?MGQV2e>M>&8n1L#u{w16s|NIWR?8iqRPu?Z
z6|;8d*o|S@IJRx9IbbVT`+95HNXupq!OzCq!b%al5ef)B(&5oeOnt&xHNIn9>&Eir
zk6Z2He-bGuSal_@bR%ua_Pd5QR}q#QOxR);9JQ-WX00uEa2K9^}=;=O4~=wA|^Th)6@E#~G_{7qDv{Gq##!d&1~E`k*Q|
z-r@P+l>^($UhUL+!_~I4AXTj~r(Rq31$iK2@_`14%*}?w+d&YVj7rk#0z=$^y>*OH
z)1=|sg7{xvBT#+ed?DKDL=Knjo4Waj{)LnC7D@r1nvSxzsVa+IY7hd1~sy2
zl5hJ2Jep2>{5DY2zS!t@oUQ4}%Om+a-{cvVM+xNQKegKQHa6JhWXCMS7^(T9`>{6!
z^j;L(^k!9!W1@WDYwt^PjL+gW31X2@J-PMyD}n-MKoDX)-b?}?t{ppWk21
z91&_pujNtGJrf*a#W}uTPw$4)_4X>SwBt%_sf^^iwq;IcXS((mgCt_)-ow>I!#&%Y
zv<9|&^yH?@yEFG~M_*u%rf(}qv(T#lXCwf;K-TqzM(
z%4g0>i}E_}!EgrcYB%C0jHr>@q?a+1TzbxMQOWVd)+nH}8@wdCRG6}IO<$-Q=K1P=
z&|(~SFA3y%Xk8osuIG?T>du4{=}`~2^SqS`;fTcoRyq8T8x@J=jV6+vS2Gy_nL=CGqMhR~@aK=mwgabT=M&QuLzM
zrew%^PmugE=lJ)bB72k!lrL8lBBaj~>x^vp^w6P&osIIe^lQ?rTY)Le7h7p!EG>YooI4d+B~m`sY*}LXTF5
zejP>S{yWD(?2KfMf9O#+b2o|
z?&_qz*<-o;h(lw3ad680$j@}!j~t!M)i8J4-}itOZFZlR#(-6tT-eGZk~GJYd2I|6
ziEgoCGar{R&9Jl{1V^nm?N1$@#fq&t2dg^SW
zxhI$GhLMj)>wY8AkPtgBOfP#dF+etpLfYfVT2ErXwWbFjFZ^++L(c=8^xs>DTd)~n-O7x
zt%rr$UTX8^?nqAAu|wW{2b0K&fqb#9Pc|ZJ9p6`72qH0-l+ydcJe1#f
zZFL-*qT>ipNoLK!*!u3=d7c(Q>5WK06cC4dGFL`kxx#nCr6q`Zedlb(AS8GBll0-w
z&M%!!9WC1Gs#>OBc7?hb2qAr)B+T_PYf^tuY%o?(|V9mg1
zB81M1p@n%!x=Y4d*kUoY|ZZC!Y@O#^fm&D2AE)4;V
zQQ4bn=S%u=z)c{{(0I$~p;+JaiCpj2wB#gO^j#f$%Yir3tYsZ-Noq%Ylc)?V4^TYe
zJJ=#^TRx{963L6$ZC}IhrTRv2TQINajkauoOOzbtJ?jj4Z28+7E;Cs1{k-0gq%oPyqXsC&>yQj_j_@`hHz99n6AVdpS9
z&Mo>LXbM)+!0>V&^`J&a&n~ui5f>F0gk@5AMLqLLm6)Cr-aJoCE?0j#%=wt+Yd4`e
z#%9FyIA8f=I_{7vn;V$s(qr$vCq|^MUOgJ3uEA$M)FklaU22ho7{9}VJG3I7EvW6Y
zP-_X-f$5h%Vjmu9X(~_7eIAq7Jk&GJM7c0;NPoLi^f(Dik@ziYd5Xfx=55}9U<@c#
zTVv@CAD@)`@LqRI6;J(xggd$hG~{GVJ1m6PQ7VudZ14=ZBB{>s$+7eD
z!+O$3qTbyj2RSyd>Lg`8IyrU7eY$)m`sBjmIM}#wm|@sj|HFByMXkeU=Qax5xG9vD
zkRtx0>X^T=>nZ9H-`=nl2X-HTq#
zH?M5A;8sDlp$VJxH(lomINv8^kfVu+6J`E5Gsrn67kqL-(1ZvOm4b1H?;a8lHb7Ry
z@GV`J=;7SxK3RR;R7WlPC)aGPn5O*B)O&rpyJyGo;!nc&NjGGp3DITVENhihO-tla
z$)wcT+z4%Idg9YAyyS!w+_~2i4wu|JINg!?+oepBVTJ?g^Tg$C?za9aoi*8k8ODWG
z#f}N}+SjZ$6kpx3ZdY+RYP*Q&0lweF;RbzR7)K{>=)u{;Mv(1ML7{TmhKqCP7zVQ_
zXzd>RV&CRzE<_kh&RKQ8>MfO|!%dUL>8JC|_-89}qNuV5%Ldzh$5PW`E~DSRDw3s@
zkh$%T{&ddMgJ8SD_D_`C@_+lS{ZN(Hy61yB`r66E>IlJ|w0-EfnL+YvE~Y)_Wj@VI
zpLO_fUi*$|08znF=^~;H6N#q}YtfLfuX%H(Q6a(2eL~?v8cB^G?oo$L{v-x#JhmYD
z-=Nc{8~EHyZ}?u1GRf9d>pSa9Enkw-@B8%LYY%Z_f`^MOsL*+jOv2$2nU@*I4`+;*
zU3oxfTS2*R&%I+W6Oyb(9ymy1#wXp6*nCVujEyY&JNjuFV|>^Cfx4IPeFXT)NGOWO
ziW+!wfNKv}0lp>%KR1T^5g>AYLtu~JEB-zGOM!nW@Gk}arNF-w_?H6zFakP9rwU#G3IR2IJq?{X-PMRMjCcR_7=h)Q!2y6t%5-#FQWQi6GlX
z1Qjv!0t%Q(L4}1nl#b#*1kS&&`3EQv_aL0)iN4v6esOiv0a4Y69#K^5Jiq+ZJih{F
zQ9uzK@T`ED6O_YX1mrP;k{bU109gON;s;Rx^RbSgOV5V2T|q~?Sqgn`R7A0QT1asc
z>h}WBbI5DV(gVoCoFGmP`ovJh1K|%zeh_&7HoL+U7%)3U+I-xR@vXe3>Y$|B?Ex{>
zvOxjGg=wJM@R$dDh5Uwm$6qhyKdguU&w%r|5OVCTjxoy$gZXcBD@1|Pgm|JreIvR$
zl&(Uj6xz2>RHblI5IM5Q4|E&Y@I^s+%z_ZS{%<{B0A+YAUi)k>-P^olFLoe)$Um=sW)T>$z$2Y5Ck1o;m8Z=4?E&(Nmh
zdHcV3kC%s8crWPx=fE)}jt*G?u>Wo18z?a2afz_zlq*FF+Td8Hq=x69s9MH=FtU4!
zUtwV$==B2BQ2{w@e+_v$ht*$sztFm)CgS!#e06%z`$cEKLZj
zltno@l6?dC|330JDNvV{Nf3AX>~B@t=6szJ>W)nksH9l|g@J|N`Sxdei|66@p7HO2
z?;RCV80#=J-|;t5`S-X|{x##h|A@2S1m_8Xbt-pxQzqkDf1qxm~Cr;2a+EpWiw&|2qd)6<4s-F==
zPA>vq3-SW?)3dl-*@_563;
z`=kE%GW5&veECr_@MZgWd$Yi&I1_7TvGf1N^8d|$`y1;K;TG16pOo~~nq{=?LiCNl
zikH;s{QU75_0kUMU%YaM#GGAOC-o!~c!-V><`1D(KS2R!OvYy^PwGPBG;V
zQ~Zh(bAqUaF#(0XR!#Fw|NR64QR#V!q+b}731ez-oc^VbtrU-bXgmD!m>x;BsHJ%Q
zzfi`6MiY%lXi>jc(H5(sb=*FAtwOOv9=o8WXM;XT?f9hn40u*bm&8Oh5+r^YGFGr6eISmxJZyv_x@crsa@#jakvmhi7@t*BP
z>h50-w_m*u;~PXiEU$f_PFCGTAO&WmFCj2nwVf9y-Y4Hswok57j6
zm4%f)Ek}<#Y8ot4f{MeCmcN?)p9elGoC|1>Mq4e1+pkoIiBBPDHMLv&UfYzrQC`Qk
zR|5UKPei$8h99{A=fJ;Lmw=7$zh`K~u#_e@@7^H*^J$;v1!aFZ{$phxj9I$HR1z@n
z-Y-RM)t3`o&aKVUGvC`OjW%nMRF3QsRcZtJ4PtD-o`ZgTo)79U?9YL2{%+U5RSv+I
zez9IrOMW?`^oS{4od@mXV?E#cj(_?&Xs_l0|Hnm?S~E?p)-Q+KYO0$Kk0$BXFl8%~
zR@V6}g$^GSRVo`5Qk;sos)jH|&qe7}y3w}Sd_qBGzv^p!hA6yG=be4RQ~uVub%
zQc!UcCu`sMj&FtSJD$VQ3;I9`m`)i@yPw5t{Ci`4uagv*onYveQCDb{QoG(Ku2u_R
zoB4xh(8mG?)^GV8Z(J-5=d=HD-xoB&$ph5Q@6}Iv%&-u0piEkG8y;T#IbGX`U`z;A
zx!n6#dmYRzfO60&rt)I=qVGQ%3&!M>5DscsFw`S;q|0Q~Z;Xqo=8OoU27o=n>go6P
z*(*tV+!#S|u2@FxD<7bHn?QKbkg6+lPG(tZR`}yRaOE
zs^!#`el`CO-#<1HtI7+fkhtG$es0f5&6hy}OFYPl@MYo*Jr(g@
zpbv-h;J7>|9FK6m6CA7I@>y}^mE+$^@BzX+Du5bzp{l!aDP1ZIY{*AMm47K80MZy9
zaIPATk%z_9GMD1D(&c(({UdWg$G_&sf5*>Yj>Kx}|8adK7%TBB&NoV`$u38i9x;_r
z{9FL$_j!b`@mvn~pHm0pnmGRBXb=6{nd6SM%i*?CbxkK-){P4(cCBW*EQJly2y3e#
zPr+F5^Z)PxK1-+?&4K(6ysv=MuhrwjEX50!q5a3g33L*S0~Q7qwM18n*UPmt7#Kn@
zDykB<94xD~4(dKO#sK>!V3+#ER5L2AoW2?>jUh_?M$Wr-WJ<%-zR88NkZR>gE5JvWb_0X+h-hJ4>6da$T0g^;_$D3@q1=iYPVW
zW3peu@n?MaS^wiy<)0QV9W;tW9z_9ehI^`a6Krd{B}8dRLE)WniQ0u#`=K3PW%eEdUAB7RX@OR$_{hL`33ob!w{qQ|vy2aHZaAT!k(^gIjAYUz~4`C|md%zfB63%7)
zUHY$v`{6p4F=5r9j?%*A&|9&(HW@8rsP{1b{QdLue_z(Wm)4U)sFoxH^RIk>8U^h&
zARnL^ju}_e9Ohre7sf8eL{%!1jLm;&9H2zgWCvXH4t=FxlK+AVvmlpTXsJR>dHEtb
zL{JYPy>T+|&&%dt<@M70aQX=L2@z17>NYsRx)isSEMxLN5)Oze*Tb0lQaDz78TRea
zz7L42o>-11tukm`IHvj4_>bFP0?yAWVg}{))Rx0-CF{WYqmW-MZvx6DE?z2!X%R)e
zcz@yRyr~uyJ#Mhq#uNtF($y}@{|z6|MHu4*vfU=39=wvgUaqYoId!dB;7h}C=YPYo
z`soI3z=R;GyICAfxfHiY+J=-9LdeS1G>82)eg8AZ{+~H3+?)k0PYWtllqV$;EyZmm
z%XNm<+ravn5tv)?HGHdI20)B=Ci{QnO@V!!USPmjYSrQe^$z;qF!lmsurLo4>OCBj
zE%M7^#)VN+FSQNXSCZdL+bTZcNZK!~T>h(#mGJo>kajha=w;&ojS?#6kglth|G1bd
zE=GmrIT)|tbF!c;fVDJ}MRJ0P8~jlfS;oaK!da{9vCFz;`IeV*>Jv{X(d&9#NH7)l#Ys70Nn16=)NR<#1cc
zx?wpT^sg5GhcYn(`Yaf~91v7$DGzx-x|Ak0r`*?rSZecf^0ON1fDgL}Vy|O;;>uMG
z(rVtHrPamW>YLH|opb)7eg0B9y+-RYOo4f4=u<P8f^{VgCx`2HHGWH}x0sgf<7-WnA9_+rb^sW@Fm~|6~6H{W^ph
z#&IAGA&fA;7v$Mv#sra*-6Bd=qoS&}>m)Vxe6);y=oo(~ua_%#heQxh2`Lpo{P1u?
z*#9rOuZI6{b3Aa2uL#x!plz4Ktwl!V6gIyW?%xUQ?6?r*3CtmY`|tuA0?sfW`mf>x
zUscU$mws_blJ#x#es
zU(q@5ZU9-Agho<_sLG`bw7zWKIXBuV>ob4E)qBLzelY&=S8>Ji{de8R_B|k92kaF*
z0@l8l)`}|KOjXrEMIX0e>N9cp;~js84?pYPI#ckaRT0G=+*~lu_Cx#jBYF2NZR7a_
zVS_q{pTC^qM@K5}wDll=ehTDH
zRQE}so)pL`nGPTgIsQlU-&Ng<)S=H-(3EWz)wo`xY|8arjs1m(heg!#VH^PJ)N1qn
zAnoD0)M>bfC*T)$%micr?D09#FQ#1CBBc`4DWkUMFQEBfx73*1(@zO048ip`5N|C0
z;9WRh5f_JrHgQ}CHP9ul^r}-5ed?2nKA*q7(O0(SzZ(9A!ae7+lqpI&hpMF199l$B
z37{Y827V>j+X(D`C8c5g4Nd;7M|wonE`pp8Aop@0_6T(UcRv=d6F=*W*F(6k$P9p~
zQv}uAEvEJ$Uq#Qj%-EW3$i$xPZz1}{%`I-;BtzmV*>F#8$a^>!0(2WQB&0adEvodP
zMoRfgCQ4JhRY{);bK%B{j?3aLYze@6tUYxy>V_TSN>9f5WN=~{X;H4pmYSMq3lE6gEOT2&*J%)?}hy=&elVHSH$$Hn~1F>0X|Dm
z!u`G1ukyL(sm?3CRLI1AA5h
z-xb;&EWfe3hZ~DPoN;{+Zp|C$gBCJXv<@$Ae3T#fayflhx(
zQ@pLk$~dI`7YBR>+P^j#^)r7Lp1*s4zodp6mj8g(U&8mZuW@|32J)Pe^I7`20nx
zzl+QU7{Ov{LwdOFJ-@hP8*#RswpYsA(>{{_77!e$d6Mz#zf^C3V_58BK*2
zF;&lY5#|-&kIiQUQJA@73S(WO
zN(I$&T0eB(^gH@SY&8m6)?lBcxIsZw?UaB5cArB?6CB^6zC)RW{UpA>2C)fHhJEG
zfb|e>FczQg5kZ!Zi>TcCB(0|GVQI4E&*1;o|GQ;04mU_D-5C*9Zdl+$PT=(!H@631
z`hu@`e-7TWfL&(_arrUp&GGX~u#cGp_9rVcVnyQ+OX=01qIUqqf2O~uAA>x{(FVsy
z$V>bIZ6=JV!2`~pVCS5EFHZ<6Oic(NYdgi2q6;LIOuH0yxdzXAkuQbuN0h-nwl3Hh
zAmArnX7Td*GtXE#1ayM72IMpJWSLlP_!0Qy+pKW$SU)I=YKOk|x8n`_54g4jCkNPA
zA~v3Z<2TM`g7(20-BCg0V2_AWGT3v{sTQdvc302%r|gCMUfbvh^ey2W-b&l>Z>J;9
zWUNC_3b14$f<5&
zrIJQblwXpnu4tO^afT`ty&r1R{tBI{?ar(Ps{2hAW#zHt<0_*gqCIwLC?P4kq%4N|;ok%@4OwH$S
zhV4f^*mK37U_e6k1-9Q>W&Fp*0^uALSQjF$n%`pnW9OH%v<$hy_o)_e>%#uqPlNRN
zLl&S+e(Cc98xQ*RS-|gp!0#mRoe%pDs`^KQZS7b58z=W{?PxMB9jKeeM^{WFh9%YQ
z=i&Sd$PM}f$1m~y3D?jM!`Xk_*aP%WgRkv>>^^ayA|pvbob5BTH-GqT;2Mr?vHcL>
zJM`P2T*CP(u#fCG*uyx#3VgfiwUUO=w1gfN#`nRB#%M7Sv4o#h4R?X>1(|e;sXhbn
zwRB0U`>h!BK||eYCW2s13D{$3_1ORAt`z3I}$HlS&){Sb$|Uqp!P=#Xj|P@3e-$TqU*_c-r1fYc
z@ir-yGwtHaX;a`GK%TMrdpJJ}AJGFddhz%f@YUyp?Rqj9Sxvo(fjh!z8alKdL0?1AfdziuH_u%{v7$<<^2%KMr8&6{A
z#(@mLJ$~xtG|_(th%`M5+HN5vcAgs>xBq=VWEjWA`7$5}WEJ)QOLZTX!94QVkdF0N
z-$M91bojG-10sS6JiX7a=(wy(*?>4o&yb~DR#l~6Oyv>yPID{BpPU8!#LkyNUk4j+
zgS-WJLR@h+=)ZLl{s;Rac>Q_V=s1&;L>mfZw&yCZnl}$9uJ&J?(6X6OFAl4ohgrH;F1;?hsR{nB_-~
zLj9eDHvM-Vf9n~x(=VpfSZ8c|q(xraxLr!^$*>Tr6Xc1_z;7#!h#?i1qYcO*xB_Jd
z9@xFYz7vjbh5uWhgL={_t@*>(hc8FFKdb9kG1*Wlug(i{AkKk)Djj?ys(%{j7mTUF
z1KQa?;)35ncgQ=azi>u6yPAN)oU@K|A@!Tw!o0$i)4%TgHFU9RglzWv7H@-t^OSej@`t*w;--3Cl
zA=tnEK|4VEKd%2FeZs%*%QL*j*_YpC1lXA>S+v4(H2Ey1;*GT%;F+Jn!|!<~cK#XI
zt^rB4Kuks5a_FsC-D^z~;Q>KZ8_2(ygZqR+nnBocHg~ntP7%{3rgUjJV$@5kOM_g@
zMQB65hZjHXSzzZtT&%Y8JJvreN0+5_nHu`6U@wX(xJCj$MuK{_5Vhe^DE?5umY3!fv{gh`Qri~9Gm0#
zj^#bLURj)0zCHL|@G(Kfp${s0%*&Ak?0;AGJ$Wua4}Tp0p WBCUF3
z5zdPNeS$K8?{gvkUta$_dMqzH*tHgdit~liXw-7_=oM4C4t>Sd2KS%A1AS_Ez}UqE
zm{(s8x0S4Gk=9gN0P|vB=D__KjH}^)I39pH(ju;MWjT73h%3v%@070wSpN?*xciRl|v?i90>HiMQKjU6#n_!HvS5ztM%>}QoXw+_K
z%Qy-4Yxo(kea}1b{V?p`KyLCvnX2LLZ{_V@c`n4l0_+v5Qv5wI|Ey=>9szK?nu^w=
zUy5s@nhq&gvy=0)VEfkhV)HJ5{=@GDK>G&vj&X6UV^Tq@C^H{Az_8~axFdDr83a*B9X917*!7M!_IO3V*NiBPawNJ
zGHOmsaa-wfo3!Q`=ubg=3iGGnyI1pp@2B7yz&+n3hd#0#O+ZfIZ73_hf=n+*Zy1{e
zd)Xyc-}L{XdEvcEM!X=uavs9)EASt`UoW1oE#gXNmg9e!l&Ut&8N}uwuX34Qj{ndm
z4+|=G)j7GX_}-LX(yQAq;J0BR#a;*lo~Fyevs&u#_8-CnuLnR*DyF&dOO&wJ3F-zr
zK`ecI{uj*&gS>!aoO(s=AMOKyyrY*;_o2*vN#E7Nb37ba`NZnd{Ot9QWQ|p
zA**!~`T(n)|6kDwz&s1~XKRwt6km#0TVOb%OGG&Y()wrhYj{56bqby#|M5NygbUZ7
z;c{Hzcm?x&9_GBD49uxste4ivUW(gFmrK+Q4uL({<{@0m(Rg*&DPsD?(T>Z}WI!AZ
z<^sORYX!9bCvkzi#@SWedtn)ubBEjO94iZ;ZU&CMfWE_6^_Vbf0?coJ>=j1`mMLoM
zgrBhAx*TpRSyyRpzYgq|+wm*rPC^~)6IOoN?(O%LpInd9;{>^mU@qXF<@=X126+sR
zS&-Kb^?w@h8rpWKYjA8jETk|yE}~q~CXIerqM&Kigw$cj_=S?KB!B;`t^Nnk2)e|S
zpF=v~Wc26J>r2}Gm1{`LK|!US5-H8E_8EGnuCs0utgFGrf&Y1&@Nx%vjg7H@GhEly
zDWdcl?8)+|MOIz6-r)G22q)(sx)=1H@gM#V@&T;jUbes5H&)@E%U})SY^uDv@N#$#
z2rE5>wEruz1n*fY|2X~q|Ju6}_^7I@|3Z+Ix}dFV^=q)J-TY{^wcl4^*6hjbYZgL6
z5RgR#TxuyvMa0iq+|gQV7p2y^Tdh`Hf{1{!%902q8N!m4WU`TjEc5-(eRC%-lQ)?+
zlL^Gi{PJ?&d3QVaoV%WT?z!^ud04t*N7Or&o|OU5u?K;B4!V!4O&+h_`qJTa>&-9b
z75=E|q2G`9{ayBo8@VE!l5bUx8?Nb^X2kt^BR|Kfl-98b$L6f)N6S+jva&MVC!5de
zHFz6Ie$-d78miT2Xw@5W7_6x0amSqB4D^6j#oB9-i8_xHd*Uc*i~D{S7KZ
zN0ZL+!Qm9=J?|T`r&iCp`73=(?~18yh0{?VD$;B-PnWEO(>qdE?3-af2<>oRdXB8l
z3~a0^uGdojQ{BS-Q2iZK%`FXD>(+y*uHV*L3NlX4y!jGe<i+?_w74er$-qcMto{0~nXA=oOPIXwCuZin)^yR>{LGM~5#|!Usubg(8!2B-Rs3DE=3m*Nf?Y@OejRJH4%qMCUYqQ=|4526>y^0+FZ=7m4-7f)b*gCcMat)`6(tiobe8SZ9!I4V-wcb(R`dxU_F8Ez#_9!|ucZSfoi^H|&+KZ@;f@Ebd2O5CnZGe_
z)>Y5{`qy7b8Cw|Aj|p#&rSJ;aRBfeofV7SC_RV;Vaax?Ki9DwIKzD=D{Jlk$+25!&
z9BbA){}@G*wmCz%dC?5g)szm9tye9(Jn^6ISbV-0@S`_?>_
z*F58^zL(z`qIWySig5;=C@D~}eAOjY$@UG0lI$z1Gu%mgZ~OIEU;o{2&+t1+dY*}P
zh5~0OaE1bBC~$@X(Ne${{jWb^a9mFiEDVOdLj0ic!eH1N1eXkk;Vr~T;!}Aga90pq
zLa-1B-^;*|aF_o`G!I`g8153LiSdiY0HuRrBtFQ$L=gN(>{Jw<^7dcF#^BPyFvHw`
z)QJ4aTkyk)Vr(hwA#Y()33K4@VJ`{c;8GTBQXqT~93Oy*&Y-*qgMoh7LtlxZ!7xK#
zh!6OSaGVH(;eOa3Z759C0VhR&FjRzvaIimFpRb?A7j74m34+0JzyC5ZSa{ec2E3j*
zRDnfUWH3M}%<6Xu3&8w~$zO!O4rL&*x{Gk)azQA@r7V8_3W@#XT^fLk`-Mc5ox@0(
zyzxz}4}cS?C>&-7
zjjY6<=wHl1(chmqeJr5>ASC}Wm;-PL1|0&wQosQKFMuVn8C*Qre~`D&GblEigEVnO
z^bCqEO-aG~RX|RSrvy)&Fd6^xQwzcdXur^N=J)xhz|L1*6{>7ACzYkUQcH{GeG{}*=nmw9o2?MaoP
zO{!}}dv2tEaa?D)V`%Kp-Gfj+MF;kDl6O>(sGUUMW7cdds3V
zjjr}ey3+G%^o9xVNRhw7iUY=dBE8j=Fq<#tQ`E`yBZEhIIiL8!CvzlN_ZJf80u46b=G@$UeUT6=PyY(oZ`5yMQ2^ssx_~}
z7~Cq3NdtWp@3+Vu9#`B~t2Qp>xWMMcuZ9mFF1^tHq%+vAHf{2*Tpf|_(Cc?BAHOHV
zHKkr_UD~2Hzk#u|A$++);~^~{wFT%uyvECaRB!!Xsedfr31`&J?hoCC5!nN;Om(DT
z4&6`mlkzzhxKa6WjCGJ62(FJJFuxX==
zr;&ssoIullPqJOo2bkWc@AO2HR=BXdO}1;z4=5=&57YGclR}r;v^iY*(fAWT2rsYJ
zdK1sjtFbH-&uPWB{38D`=Bkam>)(2dXQ#;R&|09w6kB{ay=dOvt}?yC5vfdeXhgci
zelA31Kz-{ew@kl`=cmN2)7y2NejM*uy4|oHd?0TDJD0K7oDX|zS?!~BuNal1UTbqG
zDK`(ZWBT0lFkfq@vt+R>H%|*V^&d&H2kz``R~z3WJh^`?U8-+ht$7VEn^Lz!ZGw&e
z(ZPwzhjDNo$Hl8Q-5F{<8w*
z&01UD@FgDXTRwBaM3SS7B>&;!g0}rT(_P=^xnga&ak%i2`VsHwA-MuQxEtt6o|h8W
zS6+pAfcfo6^Hmz2=)0pp88dpg;^e4SofA51G#rqHGZJB$l)6eI5DRtY_
zmRni<@z*0I`6>-V>2|10f9JRyO}3@c-9vPCisl2PA5VM>PLK1I<3%vh65;!yZ(H50
zGCkd(v*vx6kvo~E9jy!7bU)*3fM7gE3KRaFD&ygGOMc1vfR_sk&udeewzp_457#6)
zEH%lF1(bJiKCIlJ5v{qg7WFo%O>Z^mEKB#N+JF3X-VByjwB-$s2N`Mt+GQirI2ey;
zVUhz8f8kR7{H)xRa|zGax2KA~3T4iMufd)IyJt(}ImI!Y#JK!1WM_9*rkHfcjRj%v
zdwkVhc)>xPCA~#sd=T^Q{|jWG@CD=aD{}QPn)35{U6W+L`4ea1H=2uX4yhOHS$=mM
zZ*N0Mfpbz!XdFV@{m-_aC@qBZH?ocUjQ`?sry#kc34hsKp9Tfc)lE#_fP@iN*3
zD6Kjw9kg^HIxcIa(|E_SWOS7q(`&
zFRj*E?U0{8(XKW?hMuJeKcWNd`UTdWO7h!WH2*C00X4+C;PONY`|E!%_IF-Vk~a_Y
zXnx_usx)Uxi`sNoi`uxg6L)=5o22rP%rTPs*{LzV$kSEoVsG_X%4@jt5`FkX`0xkz
zVEQnRI)uLNFHKrY?mK4p6-x7^a7{YXL$J|=^`Tgg2jdXg`i$`FR%O+dE?9h4BserC
z+2(|^XD8Neaa_VU+yXwm-k>ors!VrYy=MB%sK+JDi|&ntzk}&BxXyAMJ*lprL{k2}
zS+1+(>OZd&u%XlqS?d$R2}%hKH6dVSTs!3ZzMY%)L+p!gr~RMoF}^`
zJ(S|GVs8a|Nm)IN=7=L#j%dHZwsUiH!J>%HncjWhs$t7}=c1e94rk_GR;{zRz~A4)
z2J9g|F7dX*UBa8ng}tB!htr%o%x_jU>Mgf(M%3u7RHV@65C~L`(wg~`bF3$3&?4q^W@^=tCU8ucqh72
zIhs-&8YSiCVX7=MFUJ~`-TN2|Jfvw5PUr(ZeQ(8Gte@SJnSB}BM~^g2tlnAqL2u}k
zt>9%-;yyKRo^S#(rC8w+oDTZDliM?0Y_H~Zd+s=_J4PihDnG3sPC#c;Nx7wApyTl{
z#Sg|k7BKat82hAsz!Q{itn;Jv=s{oBv32^~h~$FaB&&({*`t9MWIGRXdN(9lb2+ZD
zps!pSbxHPAo}Uu8%${=z=D?jq1G)4@@{Q#5*bCdu@z|N+n2fx7Mh5z
zmj{Y;CixN3JkxhDH8LHU9qyUuLyxB`+VX{q*QqjX!QFhU3e=>!uLP|}H0Pl5(nDh&
z@eT5*wpOiaJ=V0#4yC%L;=aTLUf-0sxcg2(yNv06os=d-+xIe@7ZNVGk1if?rPN>P
zJLi2DbTUt9jGO8;=9N{c4)w001?MU$=WtNW8f59Coj7)Q2&zxN$Yb*44v&L3jmpQjzIdnCoCm%}L*->6(ID)YS@H|#+a$)(H7
z-G%aEy|fqSU>>c<-q|Kw{)M<(Xjpj#Pdi$-CU@RBs8=0wc#ZTMl^-&cPdIM7QnRnZ
z+<>h+$q!>B_Hqmd+ccIxz;;;nPLun;aM#MHmY3rqb+JGGp4gYhb}mGH6x#fW53-Bc
zxP-eX#zBYYBgj~vsZFvJ95Uxk-u1+vPkDKzWvBO6t#vu=J&%;%6rcFYZGI9>A#bg#
zNXz~S&l9%zPkVVeF5HD)^e=cDjFbjcM*h(Fj(v&xR-Nh9#w6?FN@LCx*d~v9&w%4M
z+U~X+7kwFbqC~92PrSzX*@iyr-z|`P9!qvy_tL_}5sks4T{flZSgrjXkukQ-0N6S#Z6AHHXFVRh`5#9a-55D(L@+6d^
zfc^b~KrSV$CRbo?0=YA&pb~eoA7*aJK*6U-2~jEt=lctKX^_ha05{R+Ap*ck1#@9Q
zg1KN5@EA|_B;ZaUuEAyICEyZ>!id%;y2!Q(!6NAE5<&J2|E>}kepH45I}zVHV?doL!gVxE|}`V(d@9M{_VXH^Y2eYNMRK!LO$wBbV*J
zhE04F0{GT%6}CGIzEqoH|0&jbw?TgK*2!sP?_gkV#phVTE`^~vs8?r-=$uQeih;B2
zDy&UkrE@)yzYIFRuM~d@2f6xVvBD`h9p5{fgq^nh@t!p2RmZiK*;so#*Q_yHDco_5
z*(jaE2h&$644K6Kru!d@**djp){^*|G}o0lds_h7`~_>RO31d@d2+%RYvex>{+lyh
z7hw%~QYoIH!XeGR!x`>GN;8^Y*Povk4rFCtRC|klkx*E9+nFe$v5ID)=Z42;mS~_H{XOc^j64;x}&YTRK{ktnb8Ypovo4JDi@a0
zgKWK`DsO&R*#&6*08fMB@%KnviU*wmcCMu=-E}$aNico+NYaporS!nl-s7qE?}np4
z+N41mH=1|i8|r=e`?<5@*Uy|oI);0qLYmVT*9I&?Z+df7v+ilHg^AGqsGMadpI7^x@ZH3
zcV*8Ct7D8l{hny+z7#i{Yj~W9buh{Lb_N2rDf9SLC+nqfp
zt~9S`9NI&fv~1D56Hd7IZ(%t6ci8gJg`BE0nmnX&K=a;R=}xA%gSz_%sb3`Tgg4b^
zk_Tgc@5N{t%Cw&H7zrQP`AD_JPWeHemq%&w^tj8*A@!4f1KvFti}pj;_c83>
zTlZv&It<~;(5W{4hRPhA4^LZ(OL5Vb9-+L-4bDW&<-0NW*bN%4qqL=I5KIrr>|k%I
z4){Lbs5Z?kPqBaJn7JV0^$T=#RMFy3cr$%j=u|=<^zMajG=@8l>8#hTx8+T2UbK7=iPx@N8Mi&%d1bT4I1}f+0(umK
z0!e^FhUw>kZ_UxrpXw>*{pUuI_6s<-vig|LoVv-9dya2+r6hMj
zc(7<@+}=#*g>_n!2|BN9(05|anAOMj>D8t;kFWj5pzy&!!@S!BtUcF7gFnTgb?dte
z7l-8griHf!XDPgEaG#BP$)=(1+=;$-BldtgDXm}}Fu!g3)KM7L4jXuXM{rzeoelL)
zdr#;)8$4M#OkZdSqW%xh-0`cp@^t49p-Z!%6*j0Kt8S$IRBHF3(x&ggslPeN{)^y)@se>Pcz*YuYZVjHG$;l980LPEFaytC=Tnk
z?L+Y&+%L)HP3;T1Fz+789)3FKTUUxUVEU(@zHILl^WuLrYRxmN^v-W>a2Li0koCXd
zJjz*z^ww**ITJax_-*tyyo=2ICtJuAe?Xt|G;Laih*`>A<`g=Q7KIr>s6R(H3*i
zH_^9$+@>;aptN6{UN|21n+``ycWJrM23Cf{m;7n^;&|kJARIly`9rqUwm;1^
zX-DoHl8wF;F1*yA_2IzRKc0{-NpBa~anoCG55`vx#(eRQ()6NvCw?E(Sk;t1(pwZs
z8wUP;ood5DFJ$1D(%XLiUnMsOK$Ut(@FI7{QO`l>dUP}&VT+fM)wY6IlY
z=}dDZ$kIHJ$v)krv(7l2l{>lbp6K^64rOIuK{Sx!6X`qQhc*2Bg#T_^KF!Y?cp6j|
z%&CuJe)kXDH#_%Os`L8|IX4cOGg91We(itw2jnWoGNKR9Gg6oE$Jo^l9nLQ|{r*{@
zMQeHnW5H{%{cuxVs$C5c@
zoJ2pn8ThVhNU|kW8S*B17bxo9Mg!d;X&%m*OY7Ecoc`z|x=yGKJF%8n4IS02U5>rJG}C2EULnCNx|2Mjy98eNp0J{n+^*Y^6Jq3(qXPjbmIZw4@zAuzq9EIBjlSXy3p}rFhFwG!y%Xb1mP;gn~X<|
zCvcw+NP8;p^P&4@$}_Vs#$7Xt=QTigM8B%c?DLy776bat`ypH1MRH>Foj)8=qQgY4
zFik%Pve^{K*6zW+)E2CR+5R#gmuP&&7#Vh-X_zdActMxu3dn(egZbD8q(eR27*2Qq
zua}1;qVNOmDmV-GGW-ntB)`NQeI5EreVxuiYj{QDE{zQs|&4NMx^}$^r;%8
z<8W}=n6GW2bP(>vV82i>rHAxyk;}g_!+kMm$@Xt3T&Ohocg$l>9!PVZ2bu3eCE+O^
zg+m>9Q!an9<=vw)?B?l*b4gDJa>!KN)A8St)B*6Z?K4~c1UVc)_Xm}hh46#?WhHDv
z$Abs5@_4y2Q2MYfCc94o`&p()@<)CCkjjC&@QPesp}t`scJM}=pBT8m=qsS5tlU>F
zE!2UoUFoi~(f$|9`OAGHUO`^_o*eyYkGOZ5@e`#qpm4amM2EAJ*HXIl@VMmihrWYG
zhfw#Pg51?zmEyQWE`R!lb&sO+;*>Vh?T0?>t*{3=9(cHLe)DJajr>s0T2Ri{uy-{7
znBMw5*!#pND+sW0Ilf+Nx(@rD-yk^n2N_l>#h0U<^c&fys!nzOT)LNuJxl@n5{v4z
z=IgP4$#gnkb5W?#S-ypSdQPV*>qYd_$B7;&r>va4IwR*?j2&U`6aer0u@_?u#-HG_
z4A+I&N43GO)Dt)>Rw-8=v>hflm5Yz@a)Y$2ep(X`>p;_Iv0qeBooxHMTwLg*roa|@
zSeX`;u_7}^cCQEh{hdT3w66}-ZQLVbyajh`{1~?4*!%(eQR8Z~ma9SQ+1QH;ph0_3
zwW-b@Q(P(ja_>|o*f*4wQ)7SW+vp>b_hn`?+jOwkB~&Nbu7I7fe4Jx_2K~)3KTZ0N
zq&crSt~IL&35<EPa$$nMk<1XXeoGMP83?--{}V}BP*49&5kb>;BZ-GP@UUF|u$?jid{=B!
z9Ce@-wm``K=<-PN$C$r2luZxd_IfbQLcx?L^f#`9jb)XR{3#4Qr~`cmVWU-!1}Mvt
zQ2EJyr+%V3%{>umo{;mWZq80z_?Ya#7dla@nY}J{tEq#j4r{c_H9L^zpZM4rz`h5K=$YAi`(s7)3
zg9stfiw9zqL{OHVNhS1*^SgL26@--1xVsWc<4Y4f`0u&0B)(cGiAOxT&e{K{$))R=
z{m*?cnS@+4%@F8Bah?fmRZ&oT5=@yA*ZOg*swFl3{;XODCZI
zn$dKfYv!TTn;$Ti9Kb5S2~|CQmlc($U=QNK52-iB{I$CGSBIPcJ=)3B^o~BE7Wk~ioN-3I*8Bs^x7l2Z(g8k0
z&Oe}k{0rvN0?nbJi!=rE_Nf@tN|4S*;MN0Pvhz0-AMa-5(FeTfOv5VhL5DjlenfLY
zngardA$xkrGoXlt`Q1c3-$x&rjqgt(k7~jhI6Q%TW%ZQ7n|;!`65<8q-JAyGV%|L!
zX$_hG;#)uL;4r;kKMn4a#^dk6X+F`L!%`RJ%BHyVL%*CZ_2ch|`#f;FpTp!L?;d=k
z{y8w$Iga{^?}O?fg~eVh=J-Yv(%FIhR^vUhA=weAzhdsMY
literal 0
HcmV?d00001
diff --git a/Bloxstrap/Bootstrapper.cs b/Bloxstrap/Bootstrapper.cs
new file mode 100644
index 0000000..a14dda0
--- /dev/null
+++ b/Bloxstrap/Bootstrapper.cs
@@ -0,0 +1,655 @@
+using System.Diagnostics;
+using System.IO.Compression;
+using System.Security.Cryptography;
+
+using Microsoft.Win32;
+
+using Bloxstrap.Enums;
+using Bloxstrap.Dialogs.BootstrapperStyles;
+using Bloxstrap.Helpers;
+using Bloxstrap.Helpers.RSMM;
+
+namespace Bloxstrap
+{
+ public class Bootstrapper
+ {
+ private string? LaunchCommandLine;
+
+ private string VersionGuid;
+ private PackageManifest VersionPackageManifest;
+ private FileManifest VersionFileManifest;
+ private string VersionFolder;
+
+ private readonly string DownloadsFolder;
+ private readonly bool FreshInstall;
+
+ private int ProgressIncrement;
+ private bool CancelFired = false;
+
+ private static readonly HttpClient Client = new();
+
+ // in case a new package is added, you can find the corresponding directory
+ // by opening the stock bootstrapper in a hex editor
+ // TODO - there ideally should be a less static way to do this that's not hardcoded?
+ private static readonly IReadOnlyDictionary PackageDirectories = new Dictionary()
+ {
+ { "RobloxApp.zip", @"" },
+ { "shaders.zip", @"shaders\" },
+ { "ssl.zip", @"ssl\" },
+
+ { "content-avatar.zip", @"content\avatar\" },
+ { "content-configs.zip", @"content\configs\" },
+ { "content-fonts.zip", @"content\fonts\" },
+ { "content-sky.zip", @"content\sky\" },
+ { "content-sounds.zip", @"content\sounds\" },
+ { "content-textures2.zip", @"content\textures\" },
+ { "content-models.zip", @"content\models\" },
+
+ { "content-textures3.zip", @"PlatformContent\pc\textures\" },
+ { "content-terrain.zip", @"PlatformContent\pc\terrain\" },
+ { "content-platform-fonts.zip", @"PlatformContent\pc\fonts\" },
+
+ { "extracontent-luapackages.zip", @"ExtraContent\LuaPackages\" },
+ { "extracontent-translations.zip", @"ExtraContent\translations\" },
+ { "extracontent-models.zip", @"ExtraContent\models\" },
+ { "extracontent-textures.zip", @"ExtraContent\textures\" },
+ { "extracontent-places.zip", @"ExtraContent\places\" },
+ };
+
+ private static readonly string AppSettings =
+ "\n" +
+ "\n" +
+ " content\n" +
+ " http://www.roblox.com\n" +
+ "\n";
+
+ public event EventHandler PromptShutdownEvent;
+ public event ChangeEventHandler ShowSuccessEvent;
+ public event ChangeEventHandler MessageChanged;
+ public event ChangeEventHandler ProgressBarValueChanged;
+ public event ChangeEventHandler ProgressBarStyleChanged;
+ public event ChangeEventHandler CancelEnabledChanged;
+
+ private string _message;
+ private int _progress = 0;
+ private ProgressBarStyle _progressStyle = ProgressBarStyle.Marquee;
+ private bool _cancelEnabled = false;
+
+ public string Message
+ {
+ get => _message;
+
+ private set
+ {
+ if (_message == value)
+ return;
+
+ MessageChanged.Invoke(this, new ChangeEventArgs(value));
+
+ _message = value;
+ }
+ }
+
+ public int Progress
+ {
+ get => _progress;
+
+ private set
+ {
+ if (_progress == value)
+ return;
+
+ ProgressBarValueChanged.Invoke(this, new ChangeEventArgs(value));
+
+ _progress = value;
+ }
+ }
+
+ public ProgressBarStyle ProgressStyle
+ {
+ get => _progressStyle;
+
+ private set
+ {
+ if (_progressStyle == value)
+ return;
+
+ ProgressBarStyleChanged.Invoke(this, new ChangeEventArgs(value));
+
+ _progressStyle = value;
+ }
+ }
+
+ public bool CancelEnabled
+ {
+ get => _cancelEnabled;
+
+ private set
+ {
+ if (_cancelEnabled == value)
+ return;
+
+ CancelEnabledChanged.Invoke(this, new ChangeEventArgs(value));
+
+ _cancelEnabled = value;
+ }
+ }
+
+ public Bootstrapper(BootstrapperStyle bootstrapperStyle, string? launchCommandLine = null)
+ {
+ Debug.WriteLine("Initializing bootstrapper");
+
+ FreshInstall = String.IsNullOrEmpty(Program.Settings.VersionGuid);
+ LaunchCommandLine = launchCommandLine;
+ DownloadsFolder = Path.Combine(Program.BaseDirectory, "Downloads");
+ Client.Timeout = TimeSpan.FromMinutes(10);
+
+ switch (bootstrapperStyle)
+ {
+ case BootstrapperStyle.TaskDialog:
+ new TaskDialogStyle(this);
+ break;
+
+ case BootstrapperStyle.LegacyDialog:
+ Application.Run(new LegacyDialogStyle(this));
+ break;
+
+ case BootstrapperStyle.ProgressDialog:
+ Application.Run(new ProgressDialogStyle(this));
+ break;
+ }
+ }
+
+ public async Task Run()
+ {
+ if (LaunchCommandLine == "-uninstall")
+ {
+ Uninstall();
+ return;
+ }
+
+ await CheckLatestVersion();
+
+ if (!Directory.Exists(VersionFolder) || Program.Settings.VersionGuid != VersionGuid)
+ {
+ Debug.WriteLineIf(!Directory.Exists(VersionFolder), $"Installing latest version (!Directory.Exists({VersionFolder}))");
+ Debug.WriteLineIf(Program.Settings.VersionGuid != VersionGuid, $"Installing latest version ({Program.Settings.VersionGuid} != {VersionGuid})");
+
+ await InstallLatestVersion();
+ }
+
+ // yes, doing this for every start is stupid, but the death sound mod is dynamically toggleable after all
+ ApplyModifications();
+
+ if (Program.IsFirstRun)
+ Program.SettingsManager.ShouldSave = true;
+
+ if (Program.IsFirstRun || FreshInstall)
+ Register();
+
+ CheckInstall();
+
+ await StartRoblox();
+ }
+
+ private void CheckIfRunning()
+ {
+ Process[] processes = Process.GetProcessesByName("RobloxPlayerBeta");
+
+ if (processes.Length > 0)
+ PromptShutdown();
+
+ try
+ {
+ // try/catch just in case process was closed before prompt was answered
+
+ foreach (Process process in processes)
+ {
+ process.CloseMainWindow();
+ process.Close();
+ }
+ }
+ catch (Exception) { }
+ }
+
+ private async Task StartRoblox()
+ {
+ string startEventName = Program.ProjectName.Replace(" ", "") + "StartEvent";
+
+ Message = "Starting Roblox...";
+
+ // launch time isn't really required for all launches, but it's usually just safest to do this
+ LaunchCommandLine += " --launchtime=" + DateTimeOffset.Now.ToUnixTimeSeconds() + " -startEvent " + startEventName;
+ Debug.WriteLine($"Starting game client with command line '{LaunchCommandLine}'");
+
+ using (SystemEvent startEvent = new(startEventName))
+ {
+ Process.Start(Path.Combine(VersionFolder, "RobloxPlayerBeta.exe"), LaunchCommandLine);
+
+ Debug.WriteLine($"Waiting for {startEventName} event to be fired...");
+ bool startEventFired = await startEvent.WaitForEvent();
+
+ startEvent.Close();
+
+ if (startEventFired)
+ {
+ Debug.WriteLine($"{startEventName} event fired! Exiting in 5 seconds...");
+ await Task.Delay(5000);
+
+ Program.Exit();
+ }
+ }
+ }
+
+ // Bootstrapper Installing
+
+ public static void Register()
+ {
+ RegistryKey applicationKey = Registry.CurrentUser.CreateSubKey($@"Software\{Program.ProjectName}");
+
+ // new install location selected, delete old one
+ string? oldInstallLocation = (string?)applicationKey.GetValue("OldInstallLocation");
+ if (!String.IsNullOrEmpty(oldInstallLocation) && oldInstallLocation != Program.BaseDirectory)
+ {
+ try
+ {
+ if (Directory.Exists(oldInstallLocation))
+ Directory.Delete(oldInstallLocation, true);
+ }
+ catch (Exception) { }
+
+ applicationKey.DeleteValue("OldInstallLocation");
+ }
+
+ applicationKey.SetValue("InstallLocation", Program.BaseDirectory);
+ applicationKey.Close();
+
+ // set uninstall key
+ RegistryKey uninstallKey = Registry.CurrentUser.CreateSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}");
+ uninstallKey.SetValue("DisplayIcon", $"{Program.FilePath},0");
+ uninstallKey.SetValue("DisplayName", Program.ProjectName);
+ uninstallKey.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
+ uninstallKey.SetValue("InstallLocation", Program.BaseDirectory);
+ // uninstallKey.SetValue("NoModify", 1);
+ uninstallKey.SetValue("NoRepair", 1);
+ uninstallKey.SetValue("Publisher", Program.ProjectName);
+ uninstallKey.SetValue("ModifyPath", $"\"{Program.FilePath}\" -preferences");
+ uninstallKey.SetValue("UninstallString", $"\"{Program.FilePath}\" -uninstall");
+ uninstallKey.Close();
+ }
+
+ public static void CheckInstall()
+ {
+ // check if launch uri is set to our bootstrapper
+ // this doesn't go under register, so we check every launch
+ // just in case the stock bootstrapper changes it back
+
+ Protocol.Register("roblox", "Roblox", Program.FilePath);
+ Protocol.Register("roblox-player", "Roblox", Program.FilePath);
+
+ // in case the user is reinstalling
+ if (File.Exists(Program.FilePath) && Program.IsFirstRun)
+ File.Delete(Program.FilePath);
+
+ // check to make sure bootstrapper is in the install folder
+ if (!File.Exists(Program.FilePath) && Environment.ProcessPath is not null)
+ File.Copy(Environment.ProcessPath, Program.FilePath);
+ }
+
+ private void Uninstall()
+ {
+ CheckIfRunning();
+
+ // lots of try/catches here... lol
+
+ Message = $"Uninstalling {Program.ProjectName}...";
+
+ Program.SettingsManager.ShouldSave = false;
+
+ // check if stock bootstrapper is still installed
+ RegistryKey? bootstrapperKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\roblox-player");
+ if (bootstrapperKey is null)
+ {
+ Protocol.Unregister("roblox");
+ Protocol.Unregister("roblox-player");
+ }
+ else
+ {
+ // revert launch uri handler to stock bootstrapper
+
+ string bootstrapperLocation = (string?)bootstrapperKey.GetValue("InstallLocation") + "RobloxPlayerLauncher.exe";
+
+ Protocol.Register("roblox", "Roblox", bootstrapperLocation);
+ Protocol.Register("roblox-player", "Roblox", bootstrapperLocation);
+ }
+
+ try
+ {
+ // delete application key
+ Registry.CurrentUser.DeleteSubKey($@"Software\{Program.ProjectName}");
+ }
+ catch (Exception) { }
+
+ try
+ {
+ // delete installation folder
+ // (should delete everything except bloxstrap itself)
+ Directory.Delete(Program.BaseDirectory, true);
+ }
+ catch (Exception) { }
+
+ try
+ {
+ // delete uninstall key
+ Registry.CurrentUser.DeleteSubKey($@"Software\Microsoft\Windows\CurrentVersion\Uninstall\{Program.ProjectName}");
+ }
+ catch (Exception) { }
+
+ ShowSuccess($"{Program.ProjectName} has been uninstalled");
+ Program.Exit();
+ }
+
+ // Roblox Installing
+
+ private async Task CheckLatestVersion()
+ {
+ Message = "Connecting to Roblox...";
+
+ Debug.WriteLine($"Checking latest version...");
+ VersionGuid = await Client.GetStringAsync($"{Program.BaseUrlSetup}/version");
+ VersionFolder = Path.Combine(Program.BaseDirectory, "Versions", VersionGuid);
+ Debug.WriteLine($"Latest version is {VersionGuid}");
+
+ Debug.WriteLine("Getting package manifest...");
+ VersionPackageManifest = await PackageManifest.Get(VersionGuid);
+
+ Debug.WriteLine("Getting file manifest...");
+ VersionFileManifest = await FileManifest.Get(VersionGuid);
+ }
+
+ private async Task InstallLatestVersion()
+ {
+ CheckIfRunning();
+
+ if (FreshInstall)
+ Message = "Installing Roblox...";
+ else
+ Message = "Upgrading Roblox...";
+
+ Directory.CreateDirectory(Program.BaseDirectory);
+
+ CancelEnabled = true;
+
+ // i believe the original bootstrapper bases the progress bar off zip
+ // extraction progress, but here i'm doing package download progress
+
+ ProgressStyle = ProgressBarStyle.Continuous;
+
+ ProgressIncrement = (int)Math.Floor((decimal) 1 / VersionPackageManifest.Count * 100);
+ Debug.WriteLine($"Progress Increment is {ProgressIncrement}");
+
+ Directory.CreateDirectory(Path.Combine(Program.BaseDirectory, "Downloads"));
+
+ foreach (Package package in VersionPackageManifest)
+ {
+ // no await, download all the packages at once
+ DownloadPackage(package);
+ }
+
+ do
+ {
+ // wait for download to finish (and also round off the progress bar if needed)
+
+ if (Progress == ProgressIncrement * VersionPackageManifest.Count)
+ Progress = 100;
+
+ await Task.Delay(1000);
+ }
+ while (Progress != 100);
+
+ ProgressStyle = ProgressBarStyle.Marquee;
+
+ Debug.WriteLine("Finished downloading");
+
+ Directory.CreateDirectory(Path.Combine(Program.BaseDirectory, "Versions"));
+
+ foreach (Package package in VersionPackageManifest)
+ {
+ // extract all the packages at once (shouldn't be too heavy on cpu?)
+ ExtractPackage(package);
+ }
+
+ Debug.WriteLine("Finished extracting packages");
+
+ Message = "Configuring Roblox...";
+
+ string appSettingsLocation = Path.Combine(VersionFolder, "AppSettings.xml");
+ await File.WriteAllTextAsync(appSettingsLocation, AppSettings);
+
+ if (!FreshInstall)
+ {
+ // let's take this opportunity to delete any packages we don't need anymore
+ foreach (string filename in Directory.GetFiles(DownloadsFolder))
+ {
+ if (!VersionPackageManifest.Exists(package => filename.Contains(package.Signature)))
+ File.Delete(filename);
+ }
+
+ // and also to delete our old version folder
+ Directory.Delete(Path.Combine(Program.BaseDirectory, "Versions", Program.Settings.VersionGuid));
+ }
+
+ CancelEnabled = false;
+
+ Program.Settings.VersionGuid = VersionGuid;
+ }
+
+ private async void ApplyModifications()
+ {
+ // i guess we can just assume that if the hash does not match the manifest, then it's a mod
+ // probably not the best way to do this? don't think file corruption is that much of a worry here
+
+ // TODO - i'm thinking i could have a manifest on my website like rbxManifest.txt
+ // for integrity checking and to quickly fix/alter stuff (like ouch.ogg being renamed)
+ // but that probably wouldn't be great to check on every run in case my webserver ever goes down
+ // interesting idea nonetheless, might add it sometime
+
+ // TODO - i'm hoping i can take this idea of content mods much further
+ // for stuff like easily installing (community-created?) texture/shader/audio mods
+ // but for now, let's just keep it at this
+
+ string fileContentName = "ouch.ogg";
+ string fileContentLocation = "content\\sounds\\ouch.ogg";
+ string fileLocation = Path.Combine(VersionFolder, fileContentLocation);
+
+ string officialDeathSoundHash = VersionFileManifest[fileContentLocation];
+ string currentDeathSoundHash = CalculateMD5(fileLocation);
+
+ if (Program.Settings.UseOldDeathSound && currentDeathSoundHash == officialDeathSoundHash)
+ {
+ // let's get the old one!
+
+ Debug.WriteLine($"Fetching old death sound...");
+
+ var response = await Client.GetAsync($"{Program.BaseUrlApplication}/mods/{fileContentLocation}");
+
+ if (File.Exists(fileLocation))
+ File.Delete(fileLocation);
+
+ using (var fileStream = new FileStream(fileLocation, FileMode.CreateNew))
+ {
+ await response.Content.CopyToAsync(fileStream);
+ }
+ }
+ else if (!Program.Settings.UseOldDeathSound && currentDeathSoundHash != officialDeathSoundHash)
+ {
+ // who's lame enough to ever do this?
+ // well, we need to re-extract the one that's in the content-sounds.zip package
+
+ Debug.WriteLine("Fetching current death sound...");
+
+ var package = VersionPackageManifest.Find(x => x.Name == "content-sounds.zip");
+
+ if (package is null)
+ {
+ Debug.WriteLine("Failed to find content-sounds.zip package! Aborting...");
+ return;
+ }
+
+ DownloadPackage(package);
+
+ string packageLocation = Path.Combine(DownloadsFolder, package.Signature);
+ string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]);
+
+ using (ZipArchive archive = ZipFile.OpenRead(packageLocation))
+ {
+ ZipArchiveEntry? entry = archive.Entries.Where(x => x.FullName == fileContentName).FirstOrDefault();
+
+ if (entry is null)
+ {
+ Debug.WriteLine("Failed to find file entry in content-sounds.zip! Aborting...");
+ return;
+ }
+
+ if (File.Exists(fileLocation))
+ File.Delete(fileLocation);
+
+ entry.ExtractToFile(fileLocation);
+ }
+ }
+ }
+
+ private async void DownloadPackage(Package package)
+ {
+ string packageUrl = $"{Program.BaseUrlSetup}/{VersionGuid}-{package.Name}";
+ string packageLocation = Path.Combine(DownloadsFolder, package.Signature);
+ string robloxPackageLocation = Path.Combine(Program.LocalAppData, "Roblox", "Downloads", package.Signature);
+
+ if (File.Exists(packageLocation))
+ {
+ FileInfo file = new(packageLocation);
+
+ string calculatedMD5 = CalculateMD5(packageLocation);
+ if (calculatedMD5 != package.Signature)
+ {
+ Debug.WriteLine($"{package.Name} is corrupted ({calculatedMD5} != {package.Signature})! Deleting and re-downloading...");
+ file.Delete();
+ }
+ else
+ {
+ Debug.WriteLine($"{package.Name} is already downloaded, skipping...");
+ Progress += ProgressIncrement;
+ return;
+ }
+ }
+ else if (File.Exists(robloxPackageLocation))
+ {
+ // let's cheat! if the stock bootstrapper already previously downloaded the file,
+ // then we can just copy the one from there
+
+ Debug.WriteLine($"Found existing version of {package.Name} ({robloxPackageLocation})! Copying to Downloads folder...");
+ File.Copy(robloxPackageLocation, packageLocation);
+ Progress += ProgressIncrement;
+ return;
+ }
+
+ if (!File.Exists(packageLocation))
+ {
+ Debug.WriteLine($"Downloading {package.Name}...");
+
+ var response = await Client.GetAsync(packageUrl);
+
+ if (CancelFired)
+ return;
+
+ using (var fileStream = new FileStream(packageLocation, FileMode.CreateNew))
+ {
+ await response.Content.CopyToAsync(fileStream);
+ }
+
+ Debug.WriteLine($"Finished downloading {package.Name}!");
+ Progress += ProgressIncrement;
+ }
+ }
+
+ private void ExtractPackage(Package package)
+ {
+ if (CancelFired)
+ return;
+
+ string packageLocation = Path.Combine(DownloadsFolder, package.Signature);
+ string packageFolder = Path.Combine(VersionFolder, PackageDirectories[package.Name]);
+ string extractPath;
+
+ Debug.WriteLine($"Extracting {package.Name} to {packageFolder}...");
+
+ using (ZipArchive archive = ZipFile.OpenRead(packageLocation))
+ {
+ foreach (ZipArchiveEntry entry in archive.Entries)
+ {
+ if (CancelFired)
+ return;
+
+ if (entry.FullName.EndsWith(@"\"))
+ continue;
+
+ extractPath = Path.Combine(packageFolder, entry.FullName);
+
+ Debug.WriteLine($"[{package.Name}] Writing {extractPath}...");
+
+ Directory.CreateDirectory(Path.GetDirectoryName(extractPath));
+
+ if (File.Exists(extractPath))
+ File.Delete(extractPath);
+
+ entry.ExtractToFile(extractPath);
+ }
+ }
+ }
+
+ // Dialog Events
+
+ public void CancelButtonClicked()
+ {
+ CancelFired = true;
+
+ try
+ {
+ if (Program.IsFirstRun)
+ Directory.Delete(Program.BaseDirectory, true);
+ else if (Directory.Exists(VersionFolder))
+ Directory.Delete(VersionFolder, true);
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to cleanup install!\n\n{ex}");
+ }
+
+ Program.Exit();
+ }
+
+ private void ShowSuccess(string message)
+ {
+ ShowSuccessEvent.Invoke(this, new ChangeEventArgs(message));
+ }
+
+ private void PromptShutdown()
+ {
+ PromptShutdownEvent.Invoke(this, new EventArgs());
+ }
+
+ // Utilities
+
+ private static string CalculateMD5(string filename)
+ {
+ using (MD5 md5 = MD5.Create())
+ {
+ using (FileStream stream = File.OpenRead(filename))
+ {
+ byte[] hash = md5.ComputeHash(stream);
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+ }
+ }
+ }
+}
diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs
new file mode 100644
index 0000000..2da9d57
--- /dev/null
+++ b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.Designer.cs
@@ -0,0 +1,108 @@
+namespace Bloxstrap.Dialogs.BootstrapperStyles
+{
+ partial class LegacyDialogStyle
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.Message = new System.Windows.Forms.Label();
+ this.ProgressBar = new System.Windows.Forms.ProgressBar();
+ this.IconBox = new System.Windows.Forms.PictureBox();
+ this.CancelButton = new System.Windows.Forms.Button();
+ ((System.ComponentModel.ISupportInitialize)(this.IconBox)).BeginInit();
+ this.SuspendLayout();
+ //
+ // Message
+ //
+ this.Message.Location = new System.Drawing.Point(55, 23);
+ this.Message.Name = "Message";
+ this.Message.Size = new System.Drawing.Size(287, 17);
+ this.Message.TabIndex = 0;
+ this.Message.Text = "Please wait...";
+ //
+ // ProgressBar
+ //
+ this.ProgressBar.Location = new System.Drawing.Point(58, 51);
+ this.ProgressBar.MarqueeAnimationSpeed = 33;
+ this.ProgressBar.Name = "ProgressBar";
+ this.ProgressBar.Size = new System.Drawing.Size(287, 26);
+ this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee;
+ this.ProgressBar.TabIndex = 1;
+ //
+ // IconBox
+ //
+ this.IconBox.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
+ this.IconBox.ImageLocation = "";
+ this.IconBox.Location = new System.Drawing.Point(19, 16);
+ this.IconBox.Name = "IconBox";
+ this.IconBox.Size = new System.Drawing.Size(32, 32);
+ this.IconBox.TabIndex = 2;
+ this.IconBox.TabStop = false;
+ //
+ // CancelButton
+ //
+ this.CancelButton.Enabled = false;
+ this.CancelButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
+ this.CancelButton.Location = new System.Drawing.Point(271, 83);
+ this.CancelButton.Name = "CancelButton";
+ this.CancelButton.Size = new System.Drawing.Size(75, 23);
+ this.CancelButton.TabIndex = 3;
+ this.CancelButton.Text = "Cancel";
+ this.CancelButton.UseVisualStyleBackColor = true;
+ this.CancelButton.Visible = false;
+ this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click);
+ //
+ // LegacyDialogStyle
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(362, 131);
+ this.Controls.Add(this.CancelButton);
+ this.Controls.Add(this.IconBox);
+ this.Controls.Add(this.ProgressBar);
+ this.Controls.Add(this.Message);
+ this.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
+ this.MaximizeBox = false;
+ this.MaximumSize = new System.Drawing.Size(378, 170);
+ this.MinimizeBox = false;
+ this.MinimumSize = new System.Drawing.Size(378, 170);
+ this.Name = "LegacyDialogStyle";
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
+ this.Text = "LegacyDialogStyle";
+ ((System.ComponentModel.ISupportInitialize)(this.IconBox)).EndInit();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private Label Message;
+ private ProgressBar ProgressBar;
+ private PictureBox IconBox;
+ private Button CancelButton;
+ }
+}
\ No newline at end of file
diff --git a/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs
new file mode 100644
index 0000000..0a44dea
--- /dev/null
+++ b/Bloxstrap/Dialogs/BootstrapperStyles/LegacyDialogStyle.cs
@@ -0,0 +1,161 @@
+using Bloxstrap.Helpers;
+using Bloxstrap.Helpers.RSMM;
+
+namespace Bloxstrap.Dialogs.BootstrapperStyles
+{
+ // TODO - universal implementation for winforms-based styles? (to reduce duplicate code)
+
+ // example: https://youtu.be/3K9oCEMHj2s?t=35
+
+ // so this specifically emulates the 2011 version of the legacy dialog,
+ // but once winforms code is cleaned up we could also do the 2009 version too
+ // example: https://youtu.be/VpduiruysuM?t=18
+
+ public partial class LegacyDialogStyle : Form
+ {
+ private Bootstrapper? Bootstrapper;
+
+ public LegacyDialogStyle(Bootstrapper? bootstrapper = null)
+ {
+ InitializeComponent();
+
+ if (bootstrapper is not null)
+ {
+ Bootstrapper = bootstrapper;
+ Bootstrapper.PromptShutdownEvent += new EventHandler(PromptShutdown);
+ Bootstrapper.ShowSuccessEvent += new ChangeEventHandler(ShowSuccess);
+ Bootstrapper.MessageChanged += new ChangeEventHandler(MessageChanged);
+ Bootstrapper.ProgressBarValueChanged += new ChangeEventHandler(ProgressBarValueChanged);
+ Bootstrapper.ProgressBarStyleChanged += new ChangeEventHandler(ProgressBarStyleChanged);
+ Bootstrapper.CancelEnabledChanged += new ChangeEventHandler