From d573f6cca4989d00484a40c17374cbd8e24e1c40 Mon Sep 17 00:00:00 2001 From: niklascfw Date: Sun, 13 Sep 2026 21:10:58 +0200 Subject: [PATCH] Add a Windows desktop installer for PC-based OmniNX installs. This lets mass deployments and modchip setups apply the same install/update flow from a PC, with generated deletion lists kept in sync with the payload headers. --- .github/workflows/build-desktop.yml | 44 ++ .gitignore | 5 + desktop/OmniNX.DesktopInstaller.sln | 18 + .../OmniNX.DesktopInstaller/Assets/app.ico | Bin 0 -> 60150 bytes .../Assets/logo-128.png | Bin 0 -> 13498 bytes .../Data/DeletionLists.Generated.cs | 410 ++++++++++++ desktop/OmniNX.DesktopInstaller/MainForm.cs | 581 ++++++++++++++++++ .../Models/InstallModels.cs | 54 ++ .../OmniNX.DesktopInstaller.csproj | 25 + desktop/OmniNX.DesktopInstaller/Program.cs | 11 + .../Services/BackupService.cs | 118 ++++ .../Services/ConfigIniService.cs | 132 ++++ .../Services/DeletionService.cs | 187 ++++++ .../Services/ExtrasService.cs | 97 +++ .../Services/GiteaClient.cs | 100 +++ .../Services/InstallEngine.cs | 186 ++++++ .../Services/PathUtils.cs | 133 ++++ .../Services/VersionDetectService.cs | 79 +++ .../Services/ZipExtractor.cs | 40 ++ desktop/OmniNX.DesktopInstaller/app.manifest | 16 + tools/generate_deletion_lists.py | 76 +++ 21 files changed, 2312 insertions(+) create mode 100644 .github/workflows/build-desktop.yml create mode 100644 desktop/OmniNX.DesktopInstaller.sln create mode 100644 desktop/OmniNX.DesktopInstaller/Assets/app.ico create mode 100644 desktop/OmniNX.DesktopInstaller/Assets/logo-128.png create mode 100644 desktop/OmniNX.DesktopInstaller/Data/DeletionLists.Generated.cs create mode 100644 desktop/OmniNX.DesktopInstaller/MainForm.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Models/InstallModels.cs create mode 100644 desktop/OmniNX.DesktopInstaller/OmniNX.DesktopInstaller.csproj create mode 100644 desktop/OmniNX.DesktopInstaller/Program.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/BackupService.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/ConfigIniService.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/DeletionService.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/ExtrasService.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/GiteaClient.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/InstallEngine.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/PathUtils.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/VersionDetectService.cs create mode 100644 desktop/OmniNX.DesktopInstaller/Services/ZipExtractor.cs create mode 100644 desktop/OmniNX.DesktopInstaller/app.manifest create mode 100644 tools/generate_deletion_lists.py diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml new file mode 100644 index 0000000..a8fca44 --- /dev/null +++ b/.github/workflows/build-desktop.yml @@ -0,0 +1,44 @@ +name: Build Desktop Installer + +on: + push: + paths: + - 'desktop/**' + - 'source/deletion_lists_*.h' + - 'tools/generate_deletion_lists.py' + - '.github/workflows/build-desktop.yml' + workflow_dispatch: + +jobs: + build: + name: Build (${{ matrix.rid }}) + runs-on: windows-latest + strategy: + matrix: + rid: [win-x64, win-arm64] + + steps: + - uses: actions/checkout@v5 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Generate deletion lists + run: python tools/generate_deletion_lists.py + + - name: Publish + run: > + dotnet publish desktop/OmniNX.DesktopInstaller/OmniNX.DesktopInstaller.csproj + -c Release + -r ${{ matrix.rid }} + -p:SelfContained=false + -p:PublishSingleFile=true + -o desktop/out/${{ matrix.rid }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: OmniNX-Desktop-Installer-${{ matrix.rid }} + path: desktop/out/${{ matrix.rid }}/OmniNX-Desktop-Installer.exe diff --git a/.gitignore b/.gitignore index 4c4d784..a862fca 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ Thumbs.db # Vendored TegraExplorer tree source/TegraExplorer/ +# Desktop installer (.NET) +desktop/**/bin/ +desktop/**/obj/ +desktop/out/ + diff --git a/desktop/OmniNX.DesktopInstaller.sln b/desktop/OmniNX.DesktopInstaller.sln new file mode 100644 index 0000000..71f8e9b --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller.sln @@ -0,0 +1,18 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OmniNX.DesktopInstaller", "OmniNX.DesktopInstaller\OmniNX.DesktopInstaller.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/desktop/OmniNX.DesktopInstaller/Assets/app.ico b/desktop/OmniNX.DesktopInstaller/Assets/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..c1db33164c59dcdc487bad8aab698f752b446eb4 GIT binary patch literal 60150 zcmafYRZty4*X?j{cXtVH!JR{Z1b2eF1$Vat1b26b;4Z--xVr@>Sa5f{`Tp-d+=pBB zcXiG5p8e3PtJh4gwFdy805|{x1Nhe{fEf?~nEz))@!uF80RVWR0f3$Tzp)Y;0Ei$1 z02ut=_!BMw#1H}i1oGdwjSB##S^qQsPon^ALIEJc1OUR76uzJ$5h4A1h$<~5q4Mwk zpBXU_;Qn37uiuRT0F)*zA*SY$<(jA^m86U_)%vfS1mZNxJfgl`iQat<@BcKN#B)!>dgIaRdEmb&Cy%f<2-c zhOdYaT^|mJbQMBksw(eLDwa$hDtF?nRPpxgE;7>|VZ(4_dYp|S@psTGNvSpkc4ZZL z$yYMN!YS=Xk>tlF2O*6=H};PLtW}tPVyh=_`H{jzs4hv0v z%&NU>e~9#8)6vrDZ$yjBU{JwD!Q0dTQX_-X=2|=E0I&6U3zczCrlG2C(PXW%aZmUs z7w`uqgOLzf(u{<3*g$y9uu=>>=({kQ&}b++I0c*$mdml-g$kG16}YEtdQ5dCl=A7< zYH_c=IF05UweFU;awdh_NM=4=NiF$kxGCK|i_*|q0X zk0YlK&O)p$qC50t0aq&{_W6%ed>-iq{e8lgh0)PH zlQ(r7!#;hCGNEww4fS#=W1rgJh~e#piiF~(E^#V$^b+3s(`elf<*Nv!@&O!p(*~QDgKAh&R$+O002MvUqUyfdPk}45f1vF zyHBpUrFOE_j%^sWCV2(J!6N2IEb`cO&M#|6%Tz_QL@eILrf64b6PutCaib1IDBIX@ zfh!ihQdPJi_}{dvkP51GVPGuWEzB(@_q?Bn(zA{5ab|AhX%qRc{Wht1o@Cn_5CU5fDQsRf3EE&}v;l0z&yTVJ`mqp@L-ai5<)Y{ckck+%;p+X4 z3>zjCFjrvKodBYF$IA|(IyIFwnEIOpEw>5ZW1y&xQ^D68^|7*~v*sR^PKh}=U8F|x z+{e5f4}>nt73PQtij5d;f$3TFui42)W%|AFjK4-rQ~t9^txAoBG$)KinTa zbY8!AG`{piNZebz*;m(@WDdnoz?V8IgD{tp+iQfS{YC7%d|wZ3i5?;x3-ldz+0l*; z7VI=TCGFFEBOafHBTGxeWw*S|hf*fe2r{HCxI6pkQlhp5EVzfaz->|={!msWs$QA0 z?v9A%4%JioPvGv_cV+yxP-KYbniSK4r(s--YdhccYhANWZg0q6tFekX%E73DEwhbI zM<&;zM8(G1tA-zswG#<#Dz7{uH(daW<*0KXyd8N1Etf$MvZzo7L=I8C=kKizqFkzq zr}VFSoUHr0jZc`C+4_P$*#zV3HNr!xU$uT4Y+ZuQey6z8^mQ&$qU3j{Hr>4}39VKL{G!=36d38pR-?usfp*;U{@NZi*>L{JcK*P3k&AbDl7HQ~@ z31j;dc3~PN5zGuftE_gvn)MMJzbSX7sfE< zqdA+Xs-#L=6dO|F1d(g2=jC@6b3sUumigkX)W=UGoyU7q;g@epjudYlJ=+Y=GqVtRlQ9<;YY^ezXTvF_1yn0)y62*w;B)_6yq~?U8`{quH}~)^6Rn!Y zh`&PmDwIHr2rbRRMs4S<+IY4vIHK%cOCPzPHX2rojbw^>6pj~lX(@UT5UF$h9#Vd6 zjwK`Fpi@|$M2#6Pj=pMSRoa<}nJlo{Cen>u;%8$72s|gZA?U7S@zQO&R5N>VPF3az zdOBSax;;u;Selo>_uS+J_gm>9`p6Q%r4<#sPjGxdC+Y5bAZ$(1Me#s?|gLhw0!@Me`U(Pr7OS}Z<0|xN1JZ# z%qZNBJx>Jkvv- zxP4`mRhv+3zRNgF9mm;08%vH!)RCV$6FdBUJl4COuiIQmq4CIAB2-&6-zdI4Zs1oT zm-ZVYfDY}XK~rXY{!D@YeMWE`)iPN}Z6fe?D$Rr~izgwUraYSe%wm1NyisPVO9}4; zX|Fz3T&uRmaNmAxv@~W{3RFF4{rPy&)f)-{Y35yB9*t!#v-tYEmfT_2*C8@aY-FA# zaw$7Siq#EGi|gM)*oxNp*pbrp`SCvrV+(CJ)U|odupS!+R{jeAB2xU5EUo7}Gb3AYo$bx$7p}R-1}!46Wp7-31M82orcn7=FVd zBBT|7Dz>tTYVGdQ?s7Xj=lY7eLGj>;;2VoUT$TKdITePFp?@q-M`L}wg7@vt&`dNB z)MtMCnr+`5JnPX<|M=utGUxWEIO-^)qK>hLMPY#Qk6GS%pT_drAE)(~3z=6_6R%ye zT=}hQ<1pPrT-mscNg8!qZW^l!aNcz4hX$6a*Yg8eL-@+)@{hZLq2taWq6eWe?^tr9 zN1wZLj<=`K-l0RAf~MHD7`Oiy8So#a%>F+zKm+vnPyV9e|5paAYkMZ)EG)dQYb-1X z3y-7pviC8fjTTT~V2BI^PZSmeA{nbmz>0%FP6eCpLJ1!1taNq=P$q#s-*BRo=V2rz zmA)!dQ3WE<595J*0<_xO+sZdOH_6o(*82b|*~yXn$-~3+GmZP#v#YcA`+K+tbaIy` z8gg3g-5X|St0D~Ws6RejTC9j7VHZbli9}hVDg-AFVY;nrT2q%2>vpd_rbod+xTSS8 zi~c*dH)Ac|bI(WwX7*A;l7N91RPJ08Ik)v3DeOR%kwhc~(BXnwj(*l3kWKt;tnI_q z?Wm&ZTzO|g`DHv4EP{emd~P*_gQG`%tl6>6d>7T}_g1)*dHb~?)E_7ZD34*p4_Cs) zF9sv9-_Dy)Zw_BZ>KzuDI(&Wiw(AvxKu`$gN`*gC+IDygO?ysNTT7=UkiaWU^crF7m2dY*2Vcx*OHm|v40yzz;^hH-$R$LtTO zc5?i3Gr8?Uh|nEKGL!1RC!uRP;o(E2)#oCDv3#o1f1L!N1bgI^p^-KoCd0!YqQ5l` z8>o_MNWHdzpgcfg(YYJ}q4H^r3{Wc4nd4G&9C<q7cs|t>@0~_*Gbx3 z&xkpfW8r^RvXldp-ak!-(21_aRDlsT8pCKiuPRYmF6|yE(`i&b#^uxC>muk{qvv@1 zy6)0wthIMD3q-+Y)>4FN4Hq(>$%!rqaWpwiaQC=E4f>$Im)!y%22jNz{LOEj8eI?{ z91W027?%<OLc2O=3aaLJ=hB85urT#E{NxC*;rG z_5Q+&h31wHxg#*mAiOZrY(1ojAR3CcU-Us_v!%w0G+wo+8M&KEmEPO%4cpyQu2Du$ z$P|cx4|Flt7UA%(tbFu z5IpZck8sH8#HfP`Sz&|^u?&E=C%_wy0=&D|<%mH3sc@kDriW zETfQ!Gp!b%*4vCO38lPglkz#fx^wrjYicPc;&nXecpm;LdDtSOyY}(uC1cgF5Z7)x zbAm(7eM&f6wxB3yB&DAN1uf(Y3tCQOs+z99bdN16x?2>{Q^%PACt|KGf0(&1Miy>y z5vtxRMs{vb?-lxfeUFx+Ng1K7Hc!K(Kc&wQXe-g@u;2znchmfZZugSNi%?X4D&`Kq z4hb=GC-Wc<%mc-=oQ|iTD;fPc5==WwTzmM zdxo-V7nj3UmaSHnQVR0r$H!GF5|1Qb*tC2*Z`TZ;_gm20NpP}jFmad2;DxmKWgWu4 zdI|d`UWYC6eld)Hjr^Rqc2o}ZzpwrH@MVC=3IZo7z`Z}wZ3u^)vt}U4DKS5+@h)MW zbfne>L{Ux&8^*lvs&Y{6=Avp@zA18kvf2-E)f^7=M8v=LxwtWRB|?^rQuc2-Pl8M_ z9|z?H(qi5|3&UH6gb_UqBM6*$1MGHBo1xK{6w z5TT?vBRTS|n5@ifN{q|+|AJgp%p9^Y&)P-qb~!Wz?ZCd5S9ver)%*K<{4KzV07+^*L_&vrca1Wq zpDTz!`r_u>$wqIe5Cn(7d=9N8Jfch5le&z+PtR=icdJWty8X*|Xs+NG+N$|GsX<2yg zuT$l+?{!FXMAr5$g8KG=gaDP(hftUo1ImY6!BtDOhed$|=R<)RT4LxP2OO>D>nWhO zBtkh}ZZ*ZZPkrE`H7|akClnNrO}K#r}{H5#$wU)*KvbBzHi5K zSWL4!T5)UNcjoBA!N~Mttzy%rfp5>)zUy61G4sm>07)x#cCb*8gOyq&oR3K|i%+nc zp>IDI4`pMKJ%*PbUn?Bnrdj`CR`J zo8JN0e6@a-q$cyq5JDxSmxY4l((-0yHQiFTO1;9boo|dNV-g&D`FfjzrD7(Ws)S_t z4!NJyY@eD71UEC8#NVGj{5;tR;$hVb)9TE;9Q?WXm>vwaxU=hv7HCR#>81P{(JMy} zSmH0gUK$&C@SrXtQnCl>hSDeC7Y6&k+20=d{=9zF&>v=;l$xh0&ZGH^zqWSZz(j)W zQGqPGz*6&$*|)jz!4`fEVF{MS?~h%&?}EB+SL>`|ehzy1(07J{bm_>^B8Np3+{pYR zgXH@A#B+{8w2<7-$(!%x&d*J1g$F);&S$4V=(&1>iCr(Bh??oesStO0>o6#lVvQ0l z2ga*=?bhcgOcw$#RI0pE@|M1D=DjxEhoz%~1MVtUA;I?sLpq|%Dav@eB{MNU^_Z(_ zNTf!B&=HW-*pxEyCid(_UOp`+%7x-M6;ZBxEEZ2VZnuPg6E&GjRuDCL2p<3kR3U5pI%&BL4uVA0e{k$W{_0)bg9ws=An7>zyknB(?453o>d9I zj3+cazV12|zbsJjxvJnO zAi252Lxa#>w~_j%S`|LkD9rLiKgAw5SLxhV&b;p~_3Tym6EaR7EDh-7$%S7eUFb8< z)G9q-C;>`Q01!JC+6)NwZhI|Am{>#fj>Xc=e=wSvWA-VpaPLLyqZWI4|B*sUb2)Z) zu02|qx$Uvg=HFvWBc_!X7AYbZp89(Ehc6|nbr9a(6sKkfYUf@snEcE}syu>o z;uuR3b$48`&FcTQjSLDWLpdVC~G@*l^5DSQ!hGDZRC*yPe3cdfGKJd#@sUu zy+#vPd9g)vFW#e6M;nGWfME$Bh|M-ZECea$Ko9|(!h_{7-sqWXNo3Pss7qnZ{Ng`I zrJ>Z_l*@<&_DGGW(Zj_BDNTE8KFM znprLf9n|+G1GlhzaEP=L6gynw*oDy0d4DouTp-5x0gIC~tu))+iINbK42aY_&&a8>L*(~0!wXzM4vA=7oCruLxFKO|@Ia7cT6nOSjM$zQ z`?Z(?uIU(Ytnukitn9>{L<$#QSMzxh>*L=j=c}^03rark>DO<|XF?y1Mb-=SHcfg1 zns5PVB}@!Ca7jSo@YKcj!B) z;BM-VEDQgOp9KHQga5}*mRmY*001uHfBodCj%AYCLetw_*H1eQbS~k537SL-3WTuc zFo{3kF+UU(e5U$D@{x8>qL6St?TZBY7~&3g;fz#N=@x{D3r&bSupqCLo?@2bdfwUH zRb#!);{1MRLq~l*iGri!=i+sxdPQ6L8DZD?$zv8=m-S}T`%*O83Rlu);FE#%g+h`a z+L1qb7uw4x+WasK#xp0u_35A?92hbRMQ!qq4c=uFg66^u5V#*L6h}XgV)gYZ6#v;b zU0np7mGy}pVp@+F%`K?Z!)$?zu zKEX$G61Z%3eZhPFugF&ba#vF1dkhb;EL@%@^0(tBb2sjPV{nx`Re+E#HxVgT1F+5b z07OIEm0Hqs5%2bw{}gSFIVdzkNovbbwqOOMUB>zU_kz2IAirs?} z1_3TCv19vie+o0Lx4F*JLepS5vI|P!K}Xh> zKE5TMN9Ue!#1QPqd$#LfaVQc!LG!ry2up-OBO#W7hfxZQn39}E6fDL}xVG5$vNv39 zS{rF;>DrmUU{=$e%HPF6fDYGwM?J@(4mLUIA))!9KRAvBl*37`Q(4S5SRE=71eXDv zB{^0NJ2ZrSF#Rgv1X?|Oe$N;)p{*%=^|TLDESDdC!F+^qt09u`76Vh@n-9cwErEs*yYb$e3WpdH9U6`Ign8gr(L21%D(C8nE7u<6&9r_!n zFyT2TYuIIT4Q7$3{~g3kPBIX_aOY~uHf+HI#H7cxDraLaUKfDdZC(Nfg!nT|LFlP;75ytzFJ^>MLHN$ z!e(QrrK*<186Yk69$7(OoZI4jno~ZIlQ@sp$bY2_J|a}&K#POj#R0|+L|I}D94~Va z5nMu@keU4qXG&(hrvWZ*?^>$n2y=XQ*ey>`Txf?-C~)z#aC|Y68N1!!zJDB&Y>@9} zEr&fJ4FS)*Ajq`gsNM&3I`o%15;*+%;3V}EOCAZs+9{mnOR{zl`T=sdbQ>#Gj6-j9 z!YT+S2`c%VrHAhNd|~07c_BM_L8>3cCbVp9tD5Waa$xylyFeJ*P>aIE@O8h=d%Q(@ zsXzcvV>SE-t$ovRK!B_l>lKpEmaIW`n15@3ctW@;)vNAf67j)ruuEr_)qmr-#c5zuZa4M9p|{4Emom(+ zLbx7D&5dxjPq$bh#gbIbMsCjj`p13`l5w2*kobuHH2Y$^UIWJz|lUFM`%wh=GX}(B$a(A*GW`+?vOy$nyh#>F-+A zuN`mva!yMKA00wrXpetzuijkyi(K8pz=$m5JcN#oC+Uzc9%+)qOL0{lKox%K>N?o3 zU(h@?ekv*)pVbgigxW0e$PRS%plD8`o9fV0XnTRHs)-KDrq84)Eu*z_k|klJH^u-X zLa)u2fXkN#+z=DRc}3iwrarzTwC~+P!MfdfBC^Bj&i|Tm)R>z7Q3P*J2P?v9$D7kL z5}@ATDriTX5{J^n1$HUp zFM3WQyJlS2O@so>JiiLjws_hAB7fN0Y;)S#v3YPCQqUq%DZC9W-0C zy1OYj+TkfezIPf2r3Y3_=u~Ql@CfAd4 zSK^O5^Q`rbzwf>lknbXV-Bf*^QN*vDhjo8f*le*)smb~66l!?yxixHe6GE>a_2I%; z_wrx=6lt9EUSL6l?V-NVbrzlu_`C`bu;?0WDtA6kAr`}{RaxtMCnJv)rguXpJ*^5{ ztsy<@t6QJwb>HM7%Gd&Kdae}wYL5Ga#$sFHJ;o0V+5+V_r{@ zy{3bfLH0D!t2%0us>wSP8x-}!WcNL4FNQ1B+T9IetFDZNtY2KmdDMtOAPT)Fw}Er<9T7;H zkRkPcoi8F)lolT#0=H+N%)HMq+ttWE>zLHFHikRp?ik37P4L?QwKcNyMNfhAV-H7{ z@h`d+`*Hr#O-y*ng0k;b54CTZu!43+y3`<1XaD8nZRY-!NVDF=E%$vjXD_OL*#fFCw{9WB>t+Z_v2Kj_1&Y%RK zsxQ1zq1C^Ivdp_fDpl=TRoJqwyEK(QI|so>@ZqTaP{Xl8q1Rr6>S3MDPzTXq!J+_# z6czI7pDBtUZ7d((4};{v4gfWr?lOPs=4V zIBg_ecG)o^q=w83)OJ%G%HY7jlBd~tgZ=j8pg&N(nr4$mJ4l4L`FbpX_TkFb4&tgtn>0!vckeD3Sa_^dI0S;*SPl!s5_uc z@4ja}=2XVy%xCb1#FP1h?nQ_7;Gu;x-c|{@eW)}NVp<$)ODW?#JpjkMWOqMQ_|C-ifr#B{S^212x+16$d| zM;BAMzMDRmvvh06NjVv+8KEgpEj4gNS+X!tnP3VS+m-rW^0fFvL~U#g_QKUAyHB5h z4|mkvTfDgbW_G>e()Z$j<3vtf=dM)4}iBgFzy);MyhNee^c!I8{G6`XWEDcN&jj|aH4YV zv&`+0fF>FR=#8S5qP@o*c?4u}h?1rj_&>!u6Y%i4jL7wr<>yESHP=jR*i^AsphO z{wB2Z9<|PCg{L;h!H2wa_{awAlG(oDVM9VUB5$F(E7YRQUws6(E&_D;ZPa!BI1Fw# zys}0*_y6rYrL`S3+5C&}c<#5EOpS}-S zv?hZFJ2?mt1M89+k?NvSTbV!TdwZ3!@tOrckQQ= z;3UK>=6X0qzL*-_7R^bozo;YUK4CmdZ8(;>L*&8&dSry$N<5w28}31efy|OuEdQ*v zpF9^5Sm9VoU94OQTG`BSz57LtZ;ZA{-xXs?r*D!hu!7v+M{eh1;B)Pykp6eTIsd>CLhG9~1-k#C|#o`VW`r_FV+nZzgzT zEhiG!=U+CE@bj!UyhjCY(h|82SJc|gI_-3BHyh2jou38*-Oa8`xEY4i+ZBH?#1V)F zMoOf^XN!VxHP%_y8DlWn5Uti`VBAP{Q}M6vrj~XlbS8P2a&%H!f84-5GO*#gj23g6 zvtUx94=&@-FqJ34kuTFYnJ|yNJ6Yg4%CT{NP$|*lMH6kk_FVIPb7*imd#+{PTn;|j zPn8&UVz#YzU65`3!J&Gi3MJ>zk*q_qO^L1W>tpIV!(I^lHx4MPYI~3tkCSZSnl37{ z=axnN-?LCI<=pWO_awOUns}!%5|R6Xfmz4DN{#cL{g{HdH>P(V2PGTF^KoNE=#i^i ztlaOz5P(YRA(Z~}eYPRr#BTeI?AtrCvDNJv_0dc7Fwd6#1btY(qd&;s{(iSqnJuBl zVgbwgVh=aR>tKE0g?qA@)N6BiQY+Kf%iY+E(wm}4T)nsXVv_5nBY;LIGr^q`0$*%9 zf`tp~o#GKQ$zQERw8(uH17t=Dd~qB&QM_2oUV7d-J=szf#Fi@+gW_NgD>aZSCL3}u zE~I-$d7W%iz7_;yeav$#L5>j^W@F2fK+*iS^_Lr+KQks|c}gImp#?Z8mC#POTlyw& za}4+0A0GE4t9qiF`lsbD(gbB}CGSBzF(N?Ug0escQLHZ`Z&Sg0TXV}Y2T zz3dv=(rAE)`n-336Bb#m%7)m&LbhCoxj{DxQwiZjD@7wr;^5xO7Zdl@HgZ26K;gdWD*7DtO= z10Ti*d|x-umvny>6Y+hq$EtBxZOB}t{+D zCOQHe>f@IBLU%b`PH8sXT4R&eM%$H(x?%&YNIS;X&^%GrbE~c2;@8)aNuwi?4yIdS zTP`%gN2^npU5usza0lYI${2B@ARBnY?>`5mk_Sf31RPg_ZaOo`8;Ng4^>|4E=fTly@yS;0{Pnz!juHNjR(U_J2(9SCAg^NI4g{WIIy$)1iB%#dKATtXc&(`C)S z?1W)oxiDCDTe>QVNC4kkHm^LUXE^GXP zJ9+3QhzwANZ;yC_5+=lS$O75iwlzuSZCPK1=Y9oq|Lu(@xE(rZYEeudJkHnegjmlD=;tMc;WmIrt3{-NmMkh^^9;BE99 z(c91JF^lFXj1;8y{h1!=owD+>%< z^+pX)g)tKZSS(MGB(i%;r)^=lE8npMMbavW^|LE7H(%t|y$pW!6%4+Y`KMcQ|EUhg z#Kc7M-c@znoTNWl$V6%*q2(Xg8|U`~5lGbs@3?Xkt*V5FxLP-!jmdx0TrEx5_POJ{ z2$~Lzif6;#N-Jmh~WsC^vqjqnlb z5d$Gon#qzg8e*^mw+raXVqnbQVki%R5Gp|gDsMarX2c2%!}_>!Q*#bZ_v31TgXhBb z^HXmN+E#23P2BbKuWT0$cb2KLhJ`XKLr&Wt6OHLSbLJ~@nU?=gkc(TO*ak(8j0u%j z`cpY|%$s$j`$GrC9$LjWzGMbuvf_s+ZDvxFnQ;#}|$Q2?`4;t$qC~ zkl(hUe*0>rBK0EaKmpA{@c_jMP*n6vvzSjxa>dljtG!c@S?b@;>wjtzOv$JEfAap3I z`kbgJskh&ykj>9$IWjH`ImiRxV}bz#M+L;wQsVxGEeE7gy!Z$5aQ)Kz-oI1!JCMMZ ztWdLIebOVG)?w?CyFTws&V4&eM5>8inEgD7K+FqGUjlT1q!d@Zg-Cs7e+ykt zbDnJd${(E~`n>f_K@YZ!Q=tHT7}QHx^s!fIqsv2z0w4@8LBUYwwFKY({e2n-pzEstW0)38%-8p8};$peO~4xq(JN_Oy0Qv>+9Hmh=0}C>0ZYw#$mH%fr=Ac zATSe6bAiF2aB~Z*Un-%E0Z7XrSrZzHk3tu#kYVO+_!c6^Zaj3q)QB3`uWB?0lPUxE zv|b|oD=h>RZcPAv+-yUa6A&Z(jP5q8uu}yaH}Pm#@lOj5^nBO*i~Haj{=8Amsa`}Y zrB)z=44BtkG%i0teNL~&jb6oyE!`!KJRdG+?}G^u|ZoxUGkf*%4aLYRDk z$PV6BfO2qO^G|u&ZbE-7{2AC-?CjuDjuEGYd;v>cqfw~6IEf()+9Y-;x7Zr%2&!e- zi%N`B3vNa-1d*4)RoUv7=}d~!wcO8+!Ne6|zBtYdjRBac*x<9&u3Ou;Tv=A`N)#t8 zaQlGPW@Nkul^1WEj)s>32IsfRwO;vb_vVwokrP07 z`0Fb@K+el-<*ouGL+9R5NdVSP^V4y&k-1)T`cHeEHmmi?1!F2Ju}>tCjFfD&~2{Ahf}Ep$-(vX4QK5Y$czWYO;83cH2E}~p1DWQ-e*_5 zK3xx`Bj&&VynnKU$WQsMLT+X_!8RugHx+Z;ZP&jI_x)6hnwaQTWTEI$YEIlj3O{nU$it?HTn_3_~m=37!_HT+JU zgp7o~MEI!m9VQ?V5H(d=X7sZ{{@NaGV}aSzPpsw$Qb3b@8;^x(a$RTw8QxOWA9eQk zoeDI(e=U*mUcn!__L{vtKA$SYdC^~Qt4|;9g-EP~QrYEg1jN3biR_GxCN(wmNB;ea zwtl1r-n1PW8hQ8tiih$aOi%7OHPZgMIL%LRpa(7TK}a4~k_r~O7Vi%hPFnBj z>rRlt6|smSt-aD*>CNi3&WZgUG4-R%XH-JLsAj>xN*P{%f0|a1mN>l{)&>Qx&~1J) z6M&o$QRIFceZbIzEwh_e5k#o00PtG;@E!V2`T|qWyWIOYA%#%ZdEE|!hgXFa)y-Au zD2+PT%I&VTAy6M3grbv5aDat6xTDUc%v#g!?E2k*1b>;ppLr4sb*Z*X_Tiz6&zBgb zRM*pp3=w;^+#0T8cj~RSswndwPFX!4r5;56bA;0mB#b<40ZJ=O-=+{=?9NOcE6*or z8KAq0VtLrhe<&Xxocm@sRGk|i<>wx>K^fvI&|INrH5bSI2#&4zFeS=x-t9sDlUa*3 zn<;Z(N~;!@1it)-zdyHeyQNhLoB#Tm$2=yU{@_GabzSON!$61`L`vgugwuV1oIKM4 zG!jSQa$7K2g?}+}yw@`%Tf5N2Pz+V#`Qc)=`@AO*{(bV_i>@i+Q4;N)%4U5Yb4S8$ zHA~;L?)PO_0Y1BT$31s+fkA2#{)&)X#v7(&I+yI$Cl+(4NN)!E0-(R*8GiU&e#E7V zjsZ>wCgJTHIxd%4u12Akt_81i5+~IU^*>kLWCLr$Q$llOx9v>9LW_!Tmp@R4fVvqC z(Y((F-1be~0|b)4!F`CyD&aYbZ-et1KPR=}`oGxP%5diCd%Jyj<331r+gNwMI5K0B z-S{eX925O3=Sb#+_%CH~7U0ypVidJ<%w#caH{>^kMqJE?Pq15$GKAQff#tZ_KrBv0seHS@oD7 z#5X0(h3{_Q*Q|BXK-j7hUdmEsNKW1RO-O$#+K4 zA%*Ja`TPGohv0=VG&EIE`CyU?WUjFk`r7^#cj|qWME`d2N@eiO1Z2e}o&DRtYju+E zkiBNFGe_>=Hc#qC8`S?H?lXte~k!R`Q2vXWMeJzq&h-iH+!3e^?o=+6PEs zOrfcjzJqTV_)C%T3Sc*OHa$Ms%Z^Ql~#6rC9cq9kdk97_(q`7LrYHr^}|v)iC*F zbIt4XUN5~R>N0vOVsLL*$(;X~^3D3=x$^|zmeX~2zNbU8xHGUF>wLskbNj_cIn+j9 zj2|f~N7AS@Ud?Y^1>_UR5hDG|{pd*OeAB-@w_Dhx*8e=$&a4@ak)ucCtTIChm2xA` zc*FP5)_J>a-EOo8CSL#xx2#TOU5+_Fduy>iamw_jPeWfuG^f6*^(E}} z#QNP2Q9oLI;k6t=iG5$ZoCC+UalOlnYzfU6=?bYvnD3xZ8nS$p%jXOKayWu-v$zjt zT(~YW2yHoMcwU4Fu{vr@wF}bqK2v@RT6!e~L=KrV>~N~w0mz2i)W}g3G5%xF5a`ei zTD@&t9;|!Xl6DiuYmV$)$HEdv?psve}t4@pk=_MQPKM zc$I0rQ*aw@vm;HB!Mg(i3Sc6RNR7fr)>ufR^+P4QOQb7=XtV-{1=I&X! z4*cwVYIg@Twk2vdG=IT=V#p&|ZZ4MJFAz89yFSkhs_)7t$1i^*je#Jz| z1QyAPI@iDGWX4wW1nJVLiuB>^22?Mi95le~R&JWA=NnbtiyXiUJ;tR2kkqp@{0^t3 zO)+v>961;e!+pYg2cF!jNtS6n{SVvwUsbXj*ieLtg>SbE=Ut+re+}-M`qHX?6VzKg zOI&_JE@QmE$$YalpUfGtUMW^5f#X zUPrNM!PEsPM&B~{-Zw4nvGMO=m|G_--qDKp0t{ge8@kT38(wF&*QNP0L6?z^Ncjcp z)XLv9AWA9{Q9UPu3dt#!|h6e5JQ~YmuVFxu(QFES$ZPDSoMPE^%JPH@@j|;*OHJ8(l z6|(nO{cKk*N-c<17*R?^4DB(1TG6C2N-j zGp?jD8k~?b{&U^yMe5iP>}9;;BW%f#7_x?SDc#n*5zPmROBsFjnJ1q=OGM(_uA@3a z_nliWue5c!6SlKkbPUZs_vS@-=M-S4F&S6Z?3Nb%6cm}0bHx6TT$JxOopYl~Z> z;y?e~98E?HQNwj_4#!U(-gn`kEDx&Mxn_I4xG%xo`@Eyje;BdJzvTffRAP5qTbBc9 zN?4B(7Q(9T7s&S}b&3>BC_f`yk7ane`Ry$38d@E`oR$DS6epohhD(*2lS|o+CFTm4 z4CH&C^Dzvej1!%Y;C^J`zg_w`4E!?rbWvu%#T!FZ(CszFWWM!4wD)!4@R3)@{Os{9 z9Vm*M#S@(^nZyVl$u85z=Zt-Ut4|S=Ne#OucrX?I(DTJu0m)LOYsZUB@9nnG_S(BD z*x{@sd>JIu*Ks|rdQ_eJl4eDuVjPX$-$q6;Mp2MeZYRf&{&G~ml|fzN++0hz zA^_t*{UCkR_`Hb#XP-$`!dRU1-d*ytI+cc7K7R&hx=BDG6XV#tX<;GgWv%$qVKAI_ zI8eoee@}x%qnxz-5OQR#`jjuai9g`=6@0Nf4v}3PauKbHV5@GlW z<@AwAYQ{hz=XCS6umQF za0CsIeaW`K1?s%2dg$Je14{mW#gVxcTAX0|{j&Fk4hiCPd2>jGC=A{Fe`q_4sJenK zZQqN#J3)d34I12Cg1cL=;O_43?(XjHPOuQ%-QBrxY5wnDJ?gc3){`?igQ|1ZuD7ao zz0WRs*BsY^U*57sUM60FDgwowTD&^;V+{RWz3WP2m(}Knp^H5{rcddd@1I_S)g7Xk z3MsC;n(o2gi=si%s@@iBv*geD>t{_(_3p(ZKa%F8pHjE>lhn5Au38msRjrb0&f8vt z{>tJ({_#iC@%v_&b(0H|ZB2&nnZS6ZYMISHoB08pcB_x^{xx}ib&Zjlp^eCxX?rT- z27zpkr}-8OLAjF*7?0DxbOAgwoNA(Ox}GSYK0y34N$dO~UQ4nt`xWH)MyE+1YjTlP z@;*eE-+I#DpIfj3ZVSab+g=m>u`~$*k8_c|dz>*xU9Wv)jxSZ;WhXOh)rhYnSTpf2 zW2kZhFl|0k(Km`A3XkXX^**EMvK4{bmq@7y6$$NKZsW<|jE2;GS*w+|O(O`nksz1` z_~l)sGZnk(_O*0P8}aDa3y1o)_aZ`l%kj|b!pm`^o|SVg1)kM~79e0a*6x{o2g&sq zLMX$ZiSA(euVR?!aVOWCLo}ZSTZs!&wA6LBD4l8$0_xa-4^>p0ICMpTlE;E_2gd?+ zIzw)((2*e;|DaKVtlg}(S0O-#JF7g|k36<{3)7$wt}nn>j~;+f>6I&1@x%N3z%@z+~5BQ}n6%a$6E>tun!SPQ|4X>L*`22PM;8lVLs#osWo6tc{1Baa0h zE!-TR&5(ECG@^X-K#(=x_z9893d%CQE2%W4r+vWZ`mtialS3JQn5wugILB#u=d>wQ zj{|a)x=5Xn`XU%pMl4>`-!BP|x|-VVVm{9`8Rd2& z0LqjT%Ka(1Gzb|tHgH9TQsu|v*xKHj*{{c~OBU_}@-1j;d65V`94ggXWV{D$qJ3}uUZ6?kf|69z0aw@7U*iQy&&B%N~;Ex za~=c~TLLg`m?G)Eo4ybE=W#Z}4;zOH=Sc>yHO^lsxq%zyZvMcGWxrHk?G|;Cz*mGm zC;Vm&9{iDpc!N!eK)d6D%My-Ik>aQFae91kR@GyK-B(T!=K>_mGbT5NcpehKd{TU) zG>7*h&DCq1{(s#Z2CJ&}ihCayx2EaJ@uxKsaT&=8wSN*lXyLD62>mTZ-q@hcf<8U( zVb)3rY&84$Oo&8CjozU8MR>1ug>PYdfNy?yTPfJDR*FLV+MI)~c%`u&ukOD449JqT zkJu{G2(VJYtUc6r#rqr`T6nh#G}MBgWl67bs0tlB^+O%<0VF42PJ$#I>~rthZL=`4 zczM8jwmet7oacVE!SN&VzZE0E=&v9Ern2E;q~zSjUXanK0j+h5kT3{X1v`X2wT_km7DmD3EEladMu-y;r)CIvG&Zua zcovvo=R*wCJdG-2RVI8a;k2!9a>zv`U!wrtIAs3z2C#PP-8~=V+z304ho!sI$U$9$6B+Ls_WR2Q^|!M`&WbhxrY~C**9R?XUCH!8QvPZFR+bbHi`GagHc+PAe`)8Z zKhrp@t~0l?5uSMzkkl4P_2!0v!I29>z1O5-KPuC1i!W?{xGrlycF1dUe>qh%uBhSQ zP}&gvpgvf4KUIH+?QyLTj3W49&uCvEl){xGd7uZNtn%Hhli@@a5#iCXf*c~St8o{4 z-tNfkvLR=}VSR}bIy(J^gx^UCPpLGG(IR-8ZWB@<2o-@OIQZ>6?uP+CwhAvr!2KhY zj}XywWT(uaxt)hIN|92;j`D+nJV&p^GfC;CdCIgeyjG|g5DAYJV>wOiuUShm7nXg>kWd-#ik&z+ga7IS?lSw?F@FoLLDXapPHB@qh zmtg5Aw{oIC%}|8ctAM$e=AZhylKDxINlT$umZq`kK z0;h9(UMaxHoEajc?nnyCN-I=A!ATHOyX@5v`7%Fss=pky2i>au2rJR%CcU~esd9Yz ziv^5@>%#7rEw?^n(8QJvqs>!9T{s0?^zv`A3Rq^%UuEZwPGV>LC8Az_n6Co#R-{nP z7WTOb*0hmOBgk4eYKa#5(EpHc47@^=XbqD^d&3)Tt4-W!pxH?Y5s{1IXYlb^yV~cW0$lP{gGpai_wd zvE}n-vR_zCW#Opsd4;nR)vcDj02~hIk~p;q;23=^KR9EdIMaT}#lL!_ciPtNdK%)9>i} z?)pr?#_d;O2Z3IL9V_yCkG|{vD`HrGQb>EVY$u8ZXxoK*3u^sju&leNB3HfW;|{a> zvI(F>;x*-j9F4w@X}fjtx|qWa4A);DHeT9~J2O_y!S${U4W^W64pf@1bnUjWS@ulR zyuC6r^7B-+ym|6y$@I<-MnpHK@MCyc7Hw}cqnUe$@>?cQVyL@M??oUjK^j z2XD4&7#hm=Lcto8*r#0<@*Tz-AUA1oT!Gu(YM*kKMrQ&EiYAAf zHiBl%f6Mor#PDT;N|^*t5FinFr_0ah`y^ZT7eFX7@>)@&=t;K)rTZAH*{?qX?%+R$ zJz8S~KT~yeOyN3AU`P(S#>b5=LUc}pY5b29;SnYJMpL|}`ovwYNOBIcUSu5Wo?eci zRZQ9}o6!rfk_c@=3}Lhr3p4yzb>GD+*S)T7`sZ;!#QpLL61&K(va)+$U~gWTH4CLh zJ=$lfA&CVCo?wR%@+$c(JSVR!a0lj9KzT}ZGq4IM2FOX;8#I5bg_aWnscLBY(lo~0-o&k=Zf1Y(iT)(CGkST! z3M+ku=J_*jiMM_3b$XL_6D@GvXHD9Ul2W6ZFHqYh=?QP(O)iHZ=j=V<4jutb4Q>g9 zI9dT0HLI-@u8YHKF${uukhobGkI zzAIhE0uv^mOTg=bcodb1lUC0fGRV@&er>lgP6d+RbixE2?_jnR?fLq~P8B&8Wf8<1 z*=S2~K*0sas-C-@pF1y7YOuKG#%|)#LvB`9y14M(;GI7{5T0+3N8J4mtTBb*$|;~_ z)I0Rk=yUj#q7tVghwU6zc_h7;-b@jZT2vWA%H_W~0-eLhT7g!XWDY+klg{3+jmW+h zSqZ?R2>a>wtQ`jY?7I{T+Ru@x|KpIA=Pnd~NxFvG-vv3snQf%Q~=e@O+Q? zoiKHIXRVks7qUa^4ei-TxLEQY8nrYcJEJ4XP&SL-H`;?cZ=d!3y*Xf)~u~F+YU+Y zW!{=U3#C{k(8qMz+jLs9k!xuMjKYM_vKa4qC1Jwb;+k z_?F(P2Oe*))~N`DdL-DZh>%#VNp86xn={YLTW{<8tQRq7KPq^9@87pK5eK{9!x2pW z`h%Fi!V&^va5c;Pm{|K{=;@0zl1dd-Yl%YhScvjI_}@FaoQ{S~Zy=u}+osy(iH_S} zf}Ujtncy3@0L^>nd*(~5zLO#)|CUJ+Pb^T(czJ8_wBd)7()#+U7h;Y|%x|`c8)M(x z*ju|BdA(fqXBbchdqpRd8*9A&{6pfP?G=~W$rL{*{smbZTALgi9rXdoN-CLt!tQkj zu>lq>E4@FH5&@q(2XY^uc)ne;t;Ura7ukQ3OWqgz+bdig!NX*A1!=6+FL}XA+?4I7 zU+N#AhZKr-ihgj_1dws6<#lBEyXNQ#*1n&8^Lgk8{kavpLR`FRwAsiczRZroEzJ7d z{8@}*%8xLV>e|4{13;Pz9V@uhvJ(%xq*`q$C~&YcyU!IacGAsVb7HaY=M;^{wjb2l z58hT5MIQG#AD+=fp2rm9Rk!&%wd-Sa0Fvf%!si3r<>${_kK4%vA?Mn9OHV~&$2;qv ze}1m}A9_Y8A15nWvaTxWI*|G=$?5&Op}ELBN%!glW|E~%uFH%JyV)^sF+~Ie4EQ7! zl?w(Bo`@zdbg^>@W+dBB`fllK8LQH6A0j?*N%(RV zsd;~+UTDfQfaE4!=QmP)?XA5vm#qYzEpOjJCc*V>k!jFZlJ_%xd-jc*KqqqW_3%c( z2R|zZnD%&8QgxxY%tw+h-U3aaQR{CtJ~LOZHV6BV*+!Y$DI0(@aKu)qGs}bB+V{=X zdQj4?Cz582AZ||z>sH}l>;9k*w{_aF3HE)lceCZ=WjNe)W!)@cuU-%9JAex_<^_Yu zVef)Hb=*IW7T*PvfLVSzT! zoyZPcVMP%;m&9)`Mpqtk$$LhbqBQy-UE&YxLqR!4%XcOuv{k^3ec|3y4hZzXEYDpS3BCKFEt%1XWZ;sxush=vPmYBHix))K_7}o!uK&2RJzKx=Xvx zvkVbH7N=Ldx^u4~(PuffXM;e9ZNE+?uKiK*gQNUN1-Ux2KYG(%=1ClD=%~=2Rc7z_ zG@tmOeFqwX#oETyzUo@9w{SGIJLVFwr@wJiUU5s38~;!m{n(k#voESmvQTk-J7)Pv zgc3OEmDuKiY#}Id;RIvJ@Ll4v7#FZFLim`)RGBNr;C85P_=h%&Cz*bpQJ&}!9=BIs zH9Go!2(%nTy8`kI{?#x(-&Eluej0-nuUyCV-TuOduKYRuk|eDtbnyV~Mw+gnorG|W zeS#%$wtJj0U88I$I?{ALjn6eN zTkg3>hWA`Un+NHdHO@&p1}>*hsvg0aJK3tx)GwNGpSFUyY-8PG)^TaW>_{JM-Dxqm zW~i%p@}MevppUv?^mNj+MRJ(Yul#kzG*_}TL9k}N8dBO{qWpWF)pntW-Grvog0;{5 z;FC4nHPBB*_QT@nt+XTHE9SDbe*V5Xt9bfj@uWbDd`l8C1)wYg7(HFY_(hQCPtUAP z`3NTk@UjI`!}XrGTZb^u7pOPHxt$h zxdj+^_UCKrqCyRfd+@QQL#mbe7W2?eo>Fv<-W@U1M?~dw@R{#PE7;Y&ewDMeR_Xdz zA9*0XJFWUuT>kX_ihh3+D`S1Mwupl@|MF9wjwF5_zymh2(4&CGo+ddCL`HiQSF!Wq zb?1lnhRg~A3eFF`&lrvR&)?SA9!{{-SYdA+Om-tv>!dGRP#KX?h>=yVDK%J4IT|Cn(m{%zpg~KH(a|1B##4 zGT_SsN_ju)(YrwB+l>S}#WNs}*|FV1-B2073T8N|A^<2-5ubc~)2<$e{2(cJ zi2zC37v67VB>gUP)jA^vC{TZhTk20{q6$6t+E3e8X zCuCvQ{R4?GGNjs7@T1yQ^gTtS%>zxeeW%AW@o~oti$(%dtj~3j!pf85f|1%sl-v+8 zNmOo^&v%hSX+fTggtvKu_ho7@&JK=)BK}DLwdYKJ**9tJ)+UB=-92ckr50XbD_}W- zW>NcCu?n})_iQxG-2Pg)Q;8cZeHhy_~) zI?z?RtsrJftEi>ieMa-`t1U=f~IH>-I$LkT3#Va}Cty_uMZBAzuR&ht7zTacDiGM_d;0K~L#oOj`Q9?zSFb%EcauA~ZQYVM zQaxH=gI0x`qb!Nk$D#rP<&h%t7<{2vvk~T?!ElZTjqv?>9$>Ln(vhYiFSF?zjU8c! zNY+6`mT;PEjy*fGfgSpUX=WfP&B&b`&k8tPJ(D0qv@M-_ruINF%i}HdUORahpQ`e8h<+ zKdqE1g~^=Wr5@0qjjsLib}Zu+uEP?usTSB+iKB|F;|i7b9LDFnn>|A?NOT;0!BS|B1yK8tgS*IR2zqcGvAu+(1IjYjf1emTQqwpy8YiXP?t-S#S>J>N!ak@JDbP=ZJmAUaR)1}H?GCm$_!+xa%*fE! z5FG1xmY}wyWT|v=s&#ZsNZdN#)0ODdw4XO1f<6eOg;+&i9ztS#g5bxx7HmCVOL%ZM z*qCezeoA7v8xfOU=MG<8x>)I>s-2s^uzfzK}+%EY<-pHt?rA>9)>40MlRDd zX`Blxcq7{7>FUp+jAu*`U!0m)JTl<#UKHw8-VmHjiqv$KO?%kEqjGE`zozX>rSnEv zl}>#(W$gvN8=QemOa*Ll)|E;^m&-IdVzH@vA#aCGBA zUnB!FnmEIh9q;#Jl=BXM#)abG#O8v~vTM7Y{2s`y!Kzr)p_tG*SKl70ZD2zP^n`jj znC4+5+BKbOCg=ONY%pg(Y)5Cs+ybXjN>qdxR-jIpxzuj!U)sS2k3XB8vxe#dhA>e- zr8?LoCZvYnlrO>9IdTwDm*RRa{o&!MqP@k{8lI2Vwk^QfL~V7Wji-!UMl{0bqO+#K zo&;zCfu)|?(MaIfXbU(YZ&T}KPT5PzQ*G<0ML+lwBGWA#Q=NBhZ8f`8=~K;S{O*T^5!`JNz2f3z27Bf^#r=bOS}%y`~&uv zK26+xir(ore2}?q-}u6gkxyoDi4FP)^qrbroU)bZa;$ct1^8BL+${8-kEsy(>zJ7Y zWyBzp?$h2Ml{!DJ#d$nxnT?PsmeMiBpu=ZzTN&tEaeR9++iJa>b5n?YS}D;yVR9D3wN0Rgt}@L_KbpJsJ{;9od0vI`mRN6z!NDMXX!`|oM^yROf5 zRe4`z4+PaZGTH5I2Vd_nfQ;f!)um1daz_|rmK@*+{qDooEK6PG&CzIob_SR*ny&;> zzn4}XuLI3jPjIitHcG>!h29uK#zCZRhSUvNi`m|5n89Cnm0D3GHJaD&pRtpri> z&UnQT{&kdPZ+0BCK1OTXY@b&oD@sHV&x?x^Qb7(Sh5bYmwD?EN`mbnDelQJEey2mR zZXnNV)z2H!KZFdscCp%7*(9LjgA63oF`o*M5R3mCMd zzvg;FkB877Z@R60*6g3rhBBi~fqw z4!gCxryx&*{V%@0TqXdLf_haa9!XpI;@ZbzC@S0h-zL@McJ`yl-ttb?De70;9dR_s zI#eS|(Wwb7%{@ab3E`0F%UYa(vU^K1HT|4Ay^f&M8@xkAAtw*Ti@t%I&#;x zG$_ZD;-bAqQs+yC0n86^Qb+(O2bIB;cV@f4KaeuoARjW@aM_|VNS(=B<10Mklo8d4 z!?Gk(M>k?obc}%Z#6}K+J(#qe6@i@@II|fXN8_hUhxyKP!kTxF(k)!QO}o490ht8W z8Ax#yWI;!$kLU83f_7)TstX#i6~Jhvg}m*>Xl1!%U^%a_gkF$0sU%0_)`@Zt+i3fv z=`)d<$IsZ9*ys%?A^E>0GXP4_VK$_J5FR^eF<3pSC4PR&Ph7`uliD=u_^Me+>Ks4! zTOG&$pnd*7`g6Zh@_}0Q6H6+M)&A{&CzpVquYrL7kV`raWuNzQgtGoOx#XHf`5$tr z0`!>O=B{brZ(^wFfRd1iK?xT@3I~@48O9YE2K?hgEB!bega8MTkhouGY&?$7F4vLY zd75<9WHw5VCSoQ&V$UlQ>fjv1(oz&HjJ(XhpkML(Pk>hoIDN+~uI) zr^NV(f{|W$ijD9QU(+X~qg+KYP=!P{B9n(pa92^M>g5CAP@*V0Zvx;X)_+kJqDgsJ zv4*NbTf@{Tk$-g}6KpI0M@b5K;HqH8QC@S&*1cIi|1SvNQ+y*4@-+KTN2Mi#S*IE4 zT06fx@GXiXP%|Y=5=dKOfNmHDr4;qeAQW7xS&2+K%1I7HDX2I*SldxY)E+$kk0BDSRwo5NqldxrG_kQVnKbqhJZqS1HB!|x%X3#~879`n>|%~p z7wg73Igp=j{w|matYT6@QyLb+Htu{wZs8NP?w(Qfs3dxdHnl}q;=xbQ0Ah%LC7T4$ z!679nnot@14gYaQ9A|p!a$1#$F5{10eh#0C4qVnkW0nkzQrvYHx9pqbnGL}8r@9RM z1DfG*+m!W<)bo7g3MWb!A8bnRj}XlSMsY~ylX>CL%@bvBY~px&{p_~Pg}le{oN2bK z4W(wX`>GveWheqL63h|uPz{Hv6u?Io$&3!m4a#5*9?7Qn)CP{xP^%lL8H+VU`6aQh zEGzu2AoF0W`5}# z+hTU1%V$OK&73Qc#t$<`EN>DeCvdocwS^^h zN=o=XT6i|NG5GBuF?bQekW^;z#9JK3AfMyarRR~KV14_|hog?&?XzNV5`p!l2;pGf z6kr6RoG(Yl3mPiD`e}7->nVt23|7!o6n{>23gXJM+Uy)@d@g7q1(_2oo~%RGU2anT7ZGi1ndd^*X9y64~}*E()M#X0q4OU4R*z{HVI!rZUr>HU55+ zh*pFi&*m8f+Oyr)7~h1W>a{9VZliO_V#r~~NOYgigM`?!XG z+VAFoV}i?@f=j{t6fOF)uU_-4_6~Jdp}l0V-n)o%Rmf=Yq%U4VT#trLMiEplUvw%d zA=s++iIRt4F$f9CK@F4EN3AK(%}6CfXn&WDr#O@00LbVO-d_g{OzCoFXwP1JY5*SB z7T3vUzT{rl0=?J<_FjgW==6x6TB;)1aF0Rw?_Ne*fOGp;`DIq#n_kOOS2Q0{QIhvr zx-zW|eXZ*=;S`o_tm>)(6TWr)%p}iL@z_kuQ%{f_qDX>YmioM~SIEgG#EEa^!3dFM z0!>svevOyI9sAS_dva4{;6dZ&OcmV98xI>s32x9^vwM5s4@{X+=4NDu zG)5+a#lW`IJ|`_etYC#>fqb7`%6d20X~^~Jd+ffE@lsr)HxFTlG!H^>-jdO`^!{k8 zeOGEHrD|DunFh=4LbPUiX_*w3MN4JYF~jH8Eqy| zwlI>yxDmYd!M97vk6s|s6mWCrf3_{nRc^Z}U`+iIJat>V)o;$~N1H?i=U?(1>T~IU2rUkhQz!&y3jd(=&Rpn&pNZ>jb5tXTNQRfPWne6Yy*Zw9ZjiLkblbbR z)mxe&(j;ld3;ADKv52d3`t32Tg}Lx^!Q6OX@I82-cbZ$l$}k?Fs7)C5*%}~^ZTkzB zitQ*Yzi0lJkEuY%Zp?I;TO<=C%K#UoZ`ngPp^E8q!X`DC8G{eOCRAgl=ILYeq$(@> zt0C{-Bct_KmYBYYWDnWe`9gX*qhJc+9*^!7zi@#oN zLlw9(KvH9hOSsgc`uz2JEHy)s)s1$q>mxOxJ-f&(@E3!lr%JpcBZvnbKX_FL0D! z5%6#Xf5+}_BEDNgx{`s3L+gF?WS{YQyZ4ol@G^}y8pC0?&FjLs%j>u{`_T@n6xOhH zr}UY(v|Qz|OvG?JA^*oEWC^ll9wN9CX9nQb-#?bsEb0029XXoqU-iCa0-m$YF}eov z1k)!on-6`spHJI+X|0;cb$l5psP)4VW;dBMRL}0__o^!%#N&ryRapH@;LjGGIJ`9K z?LG7U&|owRI3p|y1L7cp#ZqL@U@{=ypGc-Ab#JpbMB6^H)s(NN4FM*}`p`T?*Wcfh z9eenGxI3V{8w#!}+qd1Ql<4^f#lOTDEPLgpV8}7`>NzDef3g0%$WA zGYp!0Vk*FNplV(TQfj!P>dOp+fFokR_lMCQH7{@vNudKQKzWDH8V<1@zI zuIpmus8Ko;!XGr*CjMH+kIfrRFR7kU{$sUS@F+}rR+XDD#5tmFu8n&RU;mQA6+Jng&9Wr26F!V=vS|MFJT5F8uV(z-CxRWBpI9gv@;d*{F1;F2A z=9I4)_qc5GutFF%1IuQ9SLsflHB=2&2h*22R#P$cmnG5EPw0m)MMtIBoX*`kZkonZWhKp7lV@6 zl0JodG)~4#f*3(|iDIkw^?bM;k-t7A45^!FFvwN_CJy6c3dSE>QwW)_hn&X6J1-{J9WCg#i z^k2e%#TM3242gW|lRcCw=6+(Yp0Ra$hokUWx5A1lPM^rlF;iLPLcZKG3Ui)edVBc{ z!Qae^UhFx@12>N7q>J^Jmulb*UlB0JXf4(7I;$;}?6&nFK+=XuNH^SZatcAStOG7t~3F+QYq_-H{3VN9LYOlHE1B|1S0*lu~n$!i2L8=i|O_ zz@at46PR7Fzy>Ds!oA^Tj#7Z|^q3~k9Fm$u34hueYf?rm1YQuq)?V_(AC7HL0E(`bkbTM!ci9(^2U1s>o49uSO zujTe=_{ZiQv&3o*0ov%4E(u(j9AcNY59_u=TmM+Sa2cnIz3m$i``i{zq;t!7+*M2g z5@7GltY71|%|5tVeQ*B-Oi1`2Bf*LJA-4^~5}duhRkV=z+bLr!?~WR$;oIdL%k*hWxPD`seX;baVTK_Dw{Z9sn~4=?^^9czUeZ?$|;_$$q-g zguo}lEZgF*aT2HTO-N^Pgw=Aq(=|R!$$b9AHvO_27-m_K8RikQ;rkTg+;|=nf~bJh z^nM{YgW60aJBWA0!;Y@+p}D|XO8~e`6f@&OJ^1)C{mrE2mZ`VfQ_qma4CTE3 zG2A^m|Dm5EUsuvwZyR=+URC?XIjlDO!YGVAcvYto_lgqs1_iFn&+bMks?-;@m@xED z*Ou~4EF2&)qjrr2lZ(X2cNLx&T|DrgnGzksQ-?b;|d}7 zz+av4zzk}CT(@6`pD{qsqLQgw3s*--a@H(y{dO7E{P6KGXuP0@L6GJxED|RB(bcrm zMI?S2>tx$)Lh;rRL642$YrMpMUMr;`kHH!kw8y23-cv*|&j09N)Qm2h<4N)GGG3^% zbfk{v`+bgG$*$=3D=nzTg#d&0)t0FVy_T6XAq<}Sg&%#J;)gC@&+8t|UBstBVzG=3 z9P1_|AaIv}{*zkp-kwVy4lX5bZ2ajX!DCXvlM~2WxnZ?mBse;PY-%VP7DN9HL zAEc#5Zj67m_%$dXF!_I#jX6Ki-W`#=?f;cHy|8V67xhe0qltcpim1S zvhyzwtvN9_24jowJg*!e;(EU^kH zCfySXst^mOGYvwM2=HH54x3#*8~#-)uQ_q-ihkl3vTgs6a1Y$@I1ggmhKge%NkB~J zWD)K`|4R!06#`KasStmk#tmPeMo=3@%$-yKK!XBghr$Ns7m@*>-H4a3pv*k;8@lV# zE+>08=}squ z3%musK|xc)G}qmX6lmFsEgoS4FvPq1sr~WL%rt)lGvSNns7+8866L{y1b=P@xnFR# zspquuXx-mlXk2IfMbvkn!cJK~d&TJq@UqE=OAV5#DyKz5m$VGd8F%cShS$8zXyGWS zKA0?8C^0qt!=It9jp}lmp|F98X2QLVm&ep#B@!Y?gDz6^3o~;bWAn#Vf%pN*AyKso zM(^B95d7$3vY-Ug4fo(QWh^K-m)JSt^i)rjhxm@os^Lk8#~J4fz9c^ht-1nIagq|P z9;OlyBSe}eWc)Q@oO(~VEZ^rCx>m3kd~I+)k$5y7a70G$a{|BgbX(2aLK>R6p6{WuP4%;{~*G)5=K3{HHYt(bg$qBa-_ zk=hz=BvRxj>-MA4n?^ccRiy3!9F(kzW^_V*e$xey$$1wjjNaQ-4VQ02B;nX`V&9l( z3#05Iny;Oo8F~GWeV=rPXV&lJyPlUJUHT_y91WSU49xcft;6+-8@m7T{4Nmq)lI%o zHPxtL3xafe|86?j(q-j)uEib&s;brH|eKto2 z?$L$;ZueV(=sV*W(vAE(r4GUAX{#|46v2Z90E7XZnhD0G6EV+t)bQN zJOoyLE~5Fs%1bL-po{8{X<1FaqJjr8yuK^0^T}?dgvEFz zA}2^Ri8CU-Z+OO=y6?Bw5Bd%A8;*tAZ>tc%6!!B<(ZJ*qkxG9BHd%Ig$*;*L#}8)H z8@lY9ckdqae6a}+E&kb@&{psaB*B9G^%9mn@`&Li9OVlH8JsW1L07FKC#nqAHeF?w zjW78YhbYNKT)+$wLqnqQ8UAvOHAt+ZD%K4(&(EQxXt9l4c61rc>vOU4=ML0wQVZJuvjWj(J_U;bM*78EC^;44a~NpTnA2^^Q)?1s2H( z?8%?J&Gq8$w`_IyaS>9d8K+;4q*i~yAhBOA*PC77ND7ik7a~*RjitHv7C_qJ1Hqkl zf4sD=gKD?L5GH%Yn{OLO?!`(+1t|>I}W^GT}!;)Id&y*#kWs|}(G3vk55_~xPsr?-Y zsxZEfi2sS8hsHOd{w4E4R%T}7@BN(7c(KQ8K1?lR&dPvlD=w0vB* zzLafwv(g?`lU)o-nJ2M$ffe;NRPp>y`N8+FcO>r{mO4`{6)gccU2zzP@ymL^h==B zu+GKbn%*B;)W5xYcsDf!!y(4dNfoKN{U(gPYiD5}*1hXcb-=K^P7fsK1EnOjEK_B{ zfllp;x9TzzSq`7T6E}~h>M<({ai)yg9NSj6$$=@Le4}>|Tv|b*>!Q|kEF5r~Sq~Z+ zr%^?oWw~wdQu4)mc!2_PO;&kT21H%?$GgA!EE9sAe!86r_?qH&KidLYoT8PGr=pT! zkTLX}W|m_b=eSd%2xqH&3w?mvHDiQ4ssDS@PK=!er53+O` zJGm+ zQL(3tt%AZQdfuW)+npIw^s7mu&xqJ;>?S|8I{4CcxD)*T6BngBY9hWiqH^vZ)&yGI zxK-CEY}b?5F5U3O<64+A?z+Mz0+&dV1;oRaWs@b+;(@G6HFd_myGl;2e+8$Cdv60w z{GlKdsa5hsos+S5FB9BVd8fe@`0S$*_2W#3=lW1Y5_uXO=P(K}VJyA%VXoJ8G}|`; z=^<2MH53JnJ`1&RSK^285aZ<8+rQY$iE4h8_Ef=g2cs-~a9DHg&%yl0V(^Fz18CSn6dle59o-+4~nju|%I0EgZtY zfHPn>OkEl#?tH0)=ZXI{b=W)Rwy-~dvKm_p{ex*V^9OPJ!2(4U$J;waia`}=1%R`G z7PIcL$(FdCe27xub#7btgS$}lIhhu`X51hCtoa21v3qqoP9XW0 z@2ziN*o`jEAi8X8;DycVjy%lpcpXdT?Yex$6>z8O>(HP?%4~og8BlB`{`F;13 z4`UmB7l<*p{->TNq##t@Ij_!mPyR{zuC_^@#_rJ#lFCP>#Q`f&T8!Uvdg^WGk_yJN z6q|aN)VuxxN*#Z9V9bkV^4C#p1AnfdUn9M^HY4HlX5?MJ@B7Wk&cmk#nM)e(BfFVU zwju=r@SAmI>tLDjM5O1jc=z-yAoNu@>;@Z7=1b_Z1#{MrJfaKj2Qj-noFtX(E=N_` zFNn6de8@@`-5z`Q;}_Sy4X;a%3la6K!QuX;NupvlYP$3moXl9EBYWK5__f_JhGgvu zC<7-Mh1U=gzMflVzcjoqc>VXi>Oc@lb2K3>#6+4Y`)fq?Vj}v&QR)aS1O{P+RckIj z0fg>N;$4LA$HUXbc7PdB1$}^rsKada5WJ%g!o0efixxJnns}l+j&k^|K`K^hoTK#O zM2i*jvKdfB%W^@>wghDtWB4ftC<0?oLd2byY_4})M-s8RAIkpPDjnt~k24elDy~z@ zf4z1VVF%7n^A8JVg!3Q0a;HDGY3Nen<2>G7x!>*wz!FymjxTXz&%Y_PA7U7lUMksA zcuI2R%CvIEsnxB32_7NYzuYv>K$=_Yo~jJoSD7RTx!&6>0P2gsYu|i^N`Ub>dd+eYwfg9AucEZ#e*up`aKDhWze)+-2&HIH ztPYD%q6skIgrR+_y1IQox7atg{jEQ`>6}x-=l*+?`yByYgrN>5jc!6K8k+>h5{{vu7E~MM3G~r_ENT`urr$tXPA| zBHdI(xnhX!9=~JJtKdJLabEta-<-mb{wn*hjcQa#Qq?hGPWJuZzT3X%Qxl4o3sek! z7#&aETBf-?VZ9PVw}6Ee?X&L4`lf#o^nL$)gKz%L#|kgb`wuFevrRi**%k_oxgsfV zwT@3;^IBo%&LO*;PwYTmH~UXstMca@JZa_80Fo7bJ3jEmOLBA9@50gnOOydnLH^QU z8L8TC()ME^@k>260aMJ7R1pG#wiawu0UbSof7`!aDxCNEPerdha|M95y59=v`&Js9 zTl&<{g{Q0;JaOw;oyYv-jZ{v=Hkhfp>JYD0ViC=MYe1J%=M{K3Vb z8!oy(y6$I36<`0~d{nCt6qA(hhRG-p0nP$kWC8kCF5(BE;sh7l0wnI1q9D9Xe1EkN|z0~cc6^{_w1`wmO^V@$P%)RH3kot-~X#-x->b^+J zYCQ!h*^EpCB?7p}s@^PjJ#vSi`}N6(hR>w7{>r8nu3|_811YaN`@tXF(cX>zx5~PJs9U zoHAt>gkuY42pm8J(gX$1_$~*1%}<^gyyCRYATIZont9243- zVzp67L$d{oKuB|@GoylkorA&OpR%?q4!)S~OW4L3pv*+4XZQHzW%$q;I}{Lv;)*^I z)m4JlR_g)PVLbwaCS3p%=b@XLd~Q+V{s@4jWBUDAL!nANv9IQZ1nmksHjkl6YV2DDem_B|iuf4TP_ zJoNg_AT>-K?TQ&Kzz&L3)_nL|-;P9CV&^jejDYydp5PG>a3hax%S4{a`y^}T3Bhm# zCl;c-Tt-`Ee)*Y8`hRuOL{}Vq5lXd*e=H#!OIl?XBo zG75-So>6aeqpOU0?vhy7P(2Kv!yWLNC`N(UR1o^qN7nQ&QVH8 zQz6(g$m<9@hTiOu?|$Hlp(}oUqyS6DWJ8BG>LCJQ%K%V(?51tlbO^E>ud9Vis1}F3{swhlL0J@_7KIqN}%%0mQ^YCNo_h z@s(4obs-3*RkfZplH|=B*R>TMh18dHNv`;76AL6{MpuW;zWS&ieQm$v!uzf|1c0lK zKesWS$iMi&@1q~>e?aik{XTVMa}tFdul~!!f7`?-K;n|skqM*-3=d2S#IE}KVdk3D zX^#q?Ei%3Mplx`;ctaUL1ZK1MpW*g8dA1u`YQ=bpp&(u+^oQQ?qsza11SHCpsy6Xf zG5{UlI@sx;lX>oTj6xBL>gk^N`AXKHFT8)uG@j@~LKr}=Y zL?~WXva)SDf28^+K3xtGD*z-6WqjzxFDd)X?Z2+P-wFX~fGx~wn;qgE_LDET$Q3}^ z2NYN&3`{1Fe|`HpxA1?LmDZ6+@D?$ESo$lI>D7mS;WI@DgR~z9c^iA@Mv?t5wEQ*{4YLmXy?;+8J^I^ z5^1hY3@VjgS0VK;aml|EH^#7pTR)GI`Nm?E-(l3<|96KQTKM7$O%JBH%G7jDo9i8Y z-lbt?Y6uqwq5!dA9ZWQI_jyZ;_n&_#fJQ4hy#+`@INtULemmTByAI^@OdvQ_vqaQL z2{&mApd_s+qRHNK)zX18-Z#?@EqN#owX~gmZyAz_Z2Mlh ztnl;0x9^&NC#W+U65IN3T49e<>JMN5HFGUK-&o>H!}Anx)ul5WEP76bxBuaEtVmTb z+1P=+u8sVoFL)xHv8i=o!LTw=uE;u*34@pK=J3j|?g7Y9quqty8V0}&QmWG&`pHv5 zVGHs7_>IODm7~hBu5BxntknRtTAZUHgAtUuBq@+9THVpjnftDOwDA3-Hg&myc(IPQ zvv1{q(y#-s-Bb9@2M_3c?a@Fo*_^lt&3s8!KBV;DBewICbsb6#7;x4GzURw-esX`b z^wn!hCEeo=N_`sBgTsFLSaHsKLeBLmGNi}}Lp8C_l6@%UYS;fxQy*A@C- zMU&O8+jxt>4p17=l9#V5{_Z2^clN#3LBQ#0-dp8P5ycKTbI>dgXzi z*4q8m&$FVD48+^-Q(p{y14!h@#5cn0nX8F6wcAG0$1sGSbe?pW0Yw6IWNpU_PpbU) zKd`YK>VG~??r2N?l@75fCgIBuo*4c9xHEjVzGw>Lj6GE?7+PqTwNgLz7p6;k2?S30 zAdVo~&t(SR^z)Z|;ZSkKMU~$5@k64D{R@@AGa>}IC^VftzWMjp zFApESh9+b;NJd?}zFYv=3eduJn=+(#Ld$n!;l^h&Cb<;Q1#izXCXRV{O&B0{Ne~F_DA`03TAS%dZmF{r=_la}d8;@R=Cf1BE2&6Ru@PgewcTV)4Z-&v( zK*+jNDRF=eS$9qLz1#oa(!pDPdK>`TDy+VOHGud9dwU=E+v3D-Ad1w8hR{-S4|i!J zJHN@b632+0rb3of2mkb>FNV)P^jqkar)aW`79isM9p}x_SD(31cl0Q6j)(K^$5UY# z(@~Pz55xk3oCRsG5I{QU{NpZ+9=ZJEE2h#jmi*#ureo(%{*T{#<}pz|uM{~S#DEJ~ zbxmgd(BEIlUw-OZ0G3v+lF$q*jsc|g{@L#7vgaW?bIygLwHbD&js}EUmBrU8lWzos zPohdcfC6*TnvRY@F8JCX^Y>o)695eN*5r1yvCQZ%Tz1;KGLK%PRA*O6%+SJ!HzX;4 z|0IJC<;=fi9+Z3r1SJj%iRKW6EJ070%`XY~cZdBhH}7vB0pL~?@dL2KIR_F(aV~WuG?mcH zm=92u*g;77d2N(HtYg*Djm%Adf1~ivGe3e@Cm6xQwXxDb1Rw==g{O-beddH<@k;?r zrXWcT2SCG0d`bQnpC$--P;9~Mtm>MG&KK_S^S?Oo!^Nlm^9hV@pQ&X;iI6F~+bOq2 zhyLRDLLmxC5C|DC7g;){*{)}93kv`Dp{+O^c%hlczY$g(1Bkh#XZASia}gA4$3~Sn za0Qx4{3f~kB^y7zwqZQK=87~|pkmo+VFzRa+wtnNrf~MB_KqvOR)kZhjWLIC93EW! z%z5rFr<|2lE?}6DrQXi3@&~Y#Y6?i=(jf?gMTLIn1QXo*e}jcz@4tn879~~}q(#9%)#9>>U zclp=o*5A+9Szj?bZluvwCHf$-^#PEx5G4hZ$@uz;Z(TQZ!LjSu!qSIUh|QlS`2ce} z_d8=D?|0gfHa}=VDq!V7hl5Pn?lAM>13`HHVf)y@SFT-_IlF=wK>W*W|K4vFr%Z;p z5Uk>edxgFgH)R!j_D5K)S6BO8d5o%U{JLFcgyN)77qaQdD({+emgeuh>cj0Uzqbiu zvop&7>-7En*B>|Jb=g?Lm*+#>{GoJFSIeBq7BW#7=!=f~dH(9}?IVySE2QExQQ-+{ zM%TeVyUp%-s*=GaArTu8L86cmOtP7m9|(#Uyzfvq@XEEIWt+1rh5;}Gq&(X7&<|QG z3@BgHRaI-(cx-U2A(k;RP-Xw6>y2ds+wt-XDu2=U_5={Fm`tuV#vLNb8iR|Ti7x%_ zS)Nh82#1wryd@I=X{j%PUBr~_Lf69Q0{MUMeSiMGzkVKC%L=LZOjLLxJ+<@j|My~k zx6e*-LyN(>!xv>hQGr!vqRqT=yI(y2@QvNT>zBogfGitR8h-|ms{dA{E`de|}Bh zf5~DM^cuK`tB4*66JUY3$f}M>=y>Xeu<*-$b}PPc*WUq1yvWOnA%2zVnUnd@dH3eu z^-#IwYGTk#is#iXZpPzK?DJ!=&B!F;UHjffKs8awXgs5iOUQB{`wpc z&yN$=R**Kx4w=<9*)wy}zpfZueTRY@Dkw!-kr0H%Qs*S&-Tk{~ihntNeTjzVkL7C9 zpR z%?o8_?Q*m}>0dYGCa#-9ZXodB6hLsI+0SN;_upT?R{Z;?wiOPC#(Jsw@nrzXOU!Jt z$APS%2JX%7jM^8k*3PTyqix_Zz)b?<5I#XR33NV5B) zJI^~)_bmiEI|n-!JQdjA9dcs$_*Lg)h1q|Jz+y-KhKF3>9sBn`7BZ`aRLpgdQeY6Q zDE732y6%kmh09M~U%*SoLn|4_DwjB9qP$GEY_$CW9I_?yP3=2QSwKU{J<=qgQfCb` zimF(z0AR4vVa7#%@v7hbT!2L@^$xFXj2)6C#-j!E|I_`{jR=@uMp(;F@S`qyfx-- z9j-Z7p=d)QWdN>X0j&t1()HVhb^f7aqO!hi&Qqjzkn*_1z;} zVelc_|LS}ID`pOq)%VWq?q9l`_dRW|uviQUE)a?r7>Rf55h%9svYpu?C>l?H#5ghl z#6-Knc1Izb0Syf(HacFRw?x#f^7fHr>!trrn>8o?G!js*^r{(@=+J28*AYF{e6YrH%;(?>dhYb!~4VCzBro)7l(@Ab6ONflq^@SlyxeMX6`Ua zXD4mw@{1lFFD-Tad;l>=ReIwc4h%H_=bO}dSGTtR2t507v(`BS!?hs+NGq5(U*hPl z%fDB;cN_0K5H&Oz$LC&2jNk8hSMZ^W7Du~%aW?uFSXQP?{0W#00D`iEpspz}(>FRU z>114nHckv6@y>feuUc=5sS>e;hKko-A71J=it7(g)JZ#z>$iu4xQLY>`0Bx1Ub20! zKHKR2ZM;J;LZP$VND6dJ_lzyG$N%GwaPuQM8eGJzwV?(%@9`Rj3=s?K^#E&Z<;5q) zX?Ww(Yg+q1Wz7x48MBOp)+$&)G)m5l zLWNGn8@4p!x(({{(cI3j(hgck{4%3S{%tZ!zxEtQPu%wV()l*p7;}{7rSjK*&-eTj zF2A%md9J105}%Z*-d(to@Fl&L0B8jlS(&!kbkf;rHT{km#z_RmgeB{4IxF(yzZ?|6 zwsfxPl_QAfx0d?VjD`e&bxgi7-eWgE;|7;3EVXN+jj=>|DXt*c=3`grqc8qOJ~O=_ z#exyzp{fpM*m+D4TLkw^XWc!mH#s!s7#{|}3<7%nS(_f?n5~fW<9DK(=I&RQ={9-N z5tsyEsZgnsq#S&r$+LaTM@0_N*kSA$KpOw=oiZhwIuo##;}=j{oj=lKp0v}--!$NqeDB41cIfr_kJPqlqmAW49HS>aVcp#`@Bht>#r56;8CYQ1^Zb%D2T&4Zm6s+? z@rHRZ5aXbsrp(?MNLDi)V~o8XoGt*e+FBdv&R!rAP8aD&z5FBEt#>408jcWL7#;~Ri@;lJ$IoX@y8On%tUW^>TEOBNqZlAY6DME% zBc(c)G|R1$K1c}{tv6}T_JHawMF_?a<6sEK4?m08?qd6V*n3oE z@r^{{SKcS}sHW=zl&`87iU7t_)c+>z>rY=+aiWbjMvo-3Ph}>~>G;4oFWP;+xkb*Q z04ew&B=a<$tJFZ{FXyG8VN@&4he^$5u@3ONVS3isaXcPpyo!M+n6f4pEf6pL(_~$> z5ybNwOZ=Lf0Ts-l!dF6oas(JwwD?u)1{dc?@eFOWQGrC|X9qoX?jf1Oe|u?u?cG@N zi>)FP5DNqzI9XP_66KwVlU_zz$r^JBC8i6~HDj~NVT={Vm;odqKxKLg-4kZQu?4BB zdsNrwH>%%{e=p1W0Cm?eAZ!_?)2Icnz9z0PIFf$aXrl^cwOeQAyyr9O@ZbEb(6xq@ z+(HHN4TQl&034_k(@(%sHOp(~sH)6`LG=bDX$l>~fV!p+jK`ymEdxlnlL|T@lcp#U za8<%hqmW$Qr=c6UG- zK%&M;H^bQZl+bo%xU}3RCF)`NsqItZVj#*7*ik$}8}CFU`TPXcW%l~&*|f)(kIGpN zrI`1jd;ktem$M^6yuW4n@=@jakDxt}_yQcm`JDiHgt4^%#)kp8&a8km2w{CPM58Uc z>vH+4WBPRt6sZ#PWu)0zmFE?htp#%Z_tdp-qm4?4km;PUy?5}>ZnB#m97OqiC{R9> zht$bj8Ko*o{P+NX#u7hm-|%m#k&&e-T&5fmz|;D(l1rF5L!gQThyDwg^c zkaS)*tzaKs>R0Em)SfRdks~GxQhLng*Z3p?`MqG3sOt<2=iR5eC#tB9stfDYcU3Zn zN+v`l2Nno4#OAgot5n((Qu^ATzAhnxp)=2y`GEkYppL+Jowowy_U(+uM)$~_+x{*E5#yc8im7lyd zw>{1~4`GjJR4Y?M`gFSE@_e~<&dcWmBt0s?3K>PbR33=Z`?0ANF+3c?Lih zq}O<_i>t9in}JELdcC;Zy@by<>Z{c7A=- zpgI|dq+Cll9vGiIfSNE~V;`W@9%zoL7gF&mN&$x2uwZrHq%`0KA;7Rc*Ed|IaVL08-BjE3hULjA*kWcH;E>U7lRm|JOCS2JV@lTEe@=_I0Min+3aJe42(}6 zKrzR{$@+dJW?B)}Yvj?WP4{gCspSt1iixVa6hH{10Mk9y51Th_W0gnh=yTAS?lsJN zzH=LG_la2r>q1u;^r20hP?OlEoH(FK96%KMogKcu@u!f<>vOcpdleT7wOdt|v;L4M z8E9~R7>rgch@VJ7L<3O~G7fI+aVZBjxxqjQL&}NG)eWTL2dH!MRsO`MYbvhN$0)N7cY?N3G!;ij= zRR@%aNrs}##=&l<-i}>AHzDs$WExrwMdS-Ch&5tu(V`^toU^j~_29OLd%rAac z6^ei{9475wHA|CUjt-%Aa-dx7Wg(m8r~DY zOyA@X|9~G~YS()=qCzf2vENT)aU_Tr>J&z47aoE2Iwl$a$;*-d*QXEQfmg2r0MoP9 zZU}l{ZDfUD;$5p7-CQapzD^HS{7N{&s7-=;0j8M1q=0-M2&~Hej}6Ah37G49lxz+# z6I8r4N`jHRkeb?oVJ}Hn8*0LVi4(`6)36g(Q7N1tky19l*a|SVwZIs2A3*X1xq&xyz8|D@r4a<6 zv5g;}PVF+F^z|AFeO0#en-u}1_e*oJUBu{`1T}H>y_#Rp#!3%LEAIq)@|;705C46E zJNTCe7j;*NT%Xb)l~(DO)}T}l0)WM5Wx5L;ZYWTH|M(Tf>%YCNNO3_$q7Z1(T9bu5 zkP}7HNO6s8PgKQE?Rnv9Vy?|=sm~(-N_Ib~GQ*Hmd*KkGmE_;J7{Jh2?`k!+3?QZ9 z!VsZvF+(eWxkmC+)lXfL!!Qm$0EnXt!@B;f?k7Tk%>$WE&}!@LTN*+es}@u#kAJqm z-ADdtK6dS;#q|$nH@L(?oDZep5DSG^lZ=U4Sp_(-&UB+=(Yzq~jiE6Nav-V-$(L-Y)pb9Nwg%WI)sBWDr@^_rH$?)?cN@R1O(7TVb?HYoH1xAki{N8D?;1^^M*KK(|AIs{&F( zm;sRQZ~KYwn~&XiQ+|`ftPCy)nYAyu)a8YixmW*d9E#V$_1?Zj6I3WiMdKMk*0>y}D1LN$U)0jVc8)JlWTp!-FO+FkP_xGpekl%bQGF=4>E;UYf`yhZM z0$7#&LP`jp_2I;^WCHHY4+Z$^iNDL;@U2aSxv$dRuecci!1T`EMLMQKII@*b2HJas7HHUxh$QeG4fKpnQ#pRQVTT%F&Fd4?OVl70dft8)59D0BIif>n|+O4zM7u z%WhiUjH%&Fhg3@_z3^DYqo@d;JQHTZ^v-C}OH134fRzvwJHo&Yq%&*zJAL|>YM0L+ z90k+i@^6MjI$-02I06ZS$p<0zF~tOGrD#NLaDMR%$;p=E7!Ah31V~=kq8Fc~umBKMt<*Wz z#NWyxDBw7q(pi@7$uKiF|6n}Kaj|~dSXQVc>4u$e*4pvl>_@JC!R`CQgY&v8bcH?! z`94DfY$0%PiXfCG_DB-$m4fN$F`0+|wlw$00}l+}yzjE|GFnZIZ2$nxn7wYKd?*K1 zoyjMN7XpomxtYW-&%<;lEJEymYL)p{J(lWY4p0VO|7(zFXqjGXD~!DckZ{@P%@?j! zL;XOuN5#b#!`S%oKZa+PwUL5h$I#%sFsw1q>ukL_=3UuX8nm(EQQ_p%!IbqgyPx_; zxBbVrFX~P!gG)fl_)v-JEm9E*Ozmh3aRkyP^fE4>8@@X)f6E!W3S!)*M#-z)qhp<)6^^LvlZ6s?r<=Hys>_fZHyL`PCn?&CWoJ#{pj^i6?Z#j z`-mw-*k_=%k<@tx0F>ItvAs_Mh>IA0N9czOefit5XB2Mv{?;+^6VR%7Ce0S?n6^eZ zbwdTtS)po)-b{kmh6PL0G;Isi&H09o!^-|^=D$E$1q)g9)QtdStV4jnxR?OR1G*yg z>+=gRIyqKD;I)KZ*G&V$A5HsO5CLJ{+L;^o=-wG~O2^t*@hCg_ls9$mZrKn1-@`cK z;-BRvuV28x0BaVXl>!*mPTtb|aey$yI;(<72wuC>w-+36bok`uUq^h^2qk4a{mrzE z4_6)2fha9tYmQnXu8laAu9os>8xy{?SkXE@r`#a);%9m*4vi_s#}HNu0`!G1KX#rX zWr#ThQIZ^?%9VOec2TMPcvSOut;DZ8Ujt%^e}sU@f_taJTW9NI;*)I#utHH%-x=EL z*`RaxFF&GBy87mDlOr}N7KTEJ3V~>&qEecF?BTDp$ps1w3y2r`9bLwscV1Yy=#Y)= zg2yk0N}YEjrIf@BTyxtEo$3PEDoQkqk=X3Z;us`}bCfE>igo7&NmyJd$@j5a{KQX2 z_go{#xO&3$3y+}<^zSK~lKmb>SK}W&p(3fkz^kDw7 zk4@*|(#O;2!Kfetg7RtYT@Ds5uqdygPb`+;sn4i7tXH0I)pN7C)NeH;Dfv4{`SAK* zQ*Q9h1<4B-bBqrINCs+OdhBnKUjjBA0235zw6gQ5=I=TizxI3$O2psDUL$1?MV2?% z}8zuZQE{9Nc_6?EEVcmJbaR5%j7!ytfoKGKrv)DV?(9Z-24Oph&M58W6Y86f&oBxOxZkm z&v$QBCtmZ1+{Pd1ii%-Kp*1QYd>~PYMI+u4JLw~}lZ7FI&}1i>?ELHe@WOq!3!l6B zbd1X0uN`0f%go&ALn1C*p8#VSp;HnX%b8fFK!n$JL)@I|WWTVP3%3Ku^5cL2rl z*cAvEHxnR!5^===_uRaflqLlhj&t*piBTc(tL*qHX|F1rDwQr)f`W^;{T4u(ER?lS zi4>^%5VaZvN6H1g@aOFQUX=1P+ZZ!cRO~4gtlqKh36};RyJ=pu+qZTNsUwv9A_m|) zB5>l(SE(S2b>sZJ~ZO1b+gem`J128(Clc^$|+em(p*=K~0mRO9BU!+lPxukf$mWYbrVbARy-0 zToi$FA#=9F`h}$dE;-?n{Egq*PdHq-+(}%?^`?9EDR%801Lqp{nk|R|$l#l>3m^Jk+V^;1+|%LYnS1_3g+UNyK~6)ZzR!}{)4;2r zo=;Kft!Dzcs9-vKRL7go!aM&HkfEipt8V~s4%POVs=gNhMGO>%I9huPHmh&6MRIU_ zU&7lNwGO^DxBg=1k$)YaBQN>d;LPp9Vt&wqi@x)G12`xF6m-Ki^F$n&;uUa|4J`%J zRq*1y#$IsXp~WZu_5qYs{^d;KN(+G2J>X>SSPeQFK&+J-l$FAYuwIRmuad||B=IY& z3sY6UAw9=}OT>YcZ}ftP`lZKL(I^;O%Qq{m;DiKZu`EA~tsBj3N?1 zCAX;nu4u_*tmkzG-Zf|4TKw_BxAKPXveEWOu(R=2;8giY4esTX2M8c_c_59MMMh7=ZjMHKJ3C%QynWt8muv{NV-fgmAu z^cer1U%V3j@xTq-!pANv3xP5CR`F&b6KRvfKFdz5Q(0e$g)r4GV_$h$1L;1Jx_)qZ z9#YLuZC?<8g{)Cf?v=Z*6d)YWaP>HD4jMl&+u{J`-#d@i-FgcQSr(&JQ?A4r?T!yv;J9K{YpWjKU2o9w$A zuKmR%xZ>he1#P1hDAt%_vy4pFYP*>oKKd=%=3{#lx>kb=2SOD3d?Mw?pHo#h_bOL` zlH|YG*P|o^vt`Ll4E@5J0j~PwwYhnJdOv{v##MaFI{dgVv)Yy)#u{6*vmrwW4hA3| zwm!eXL9{dA2 z(okS(r=v>hS8gQ$2w)>tUN>hs^!3*WDX6b<;Ua{_x?AASa&z4I~%kNu+SZx@h8TCk&(ne1); z!7tLzCx2O1+Z4sHP*4L)41DtA$Y1P4OD?QQ+yZL$b%J97v<@>ye7f^jj~8z^?F$_B zU0rq2m_0`<{dM=+TVnl#=AtkVf{b6MiYHHy*5{XwOZC_r-a=Ao-ed@s_i12GuxM6( z*6^!$&u2HZusovi!T5^3gmb$kFTbdsy|14<-+*0(6C^3dfFLcS1O$jIliyM3aD~A8 z^Vc8Gops<^(Q|j5N5=2Ue&qM_!z~W(LjPhbo@WH*6=505J$*n)-MVKc>rKL7J$zbjmG>Si4ET|Iu^A)X_8iw|FHyJs@FqM|gJr&1QR z$|q^o`-tNH<@s3WR1^ZlSR;dw=WqTu02%MH_3_txm4*NzKXUEWMw1f9VddM7Gol>` zfVK=}IW*n9D*MbGfjjd(rwm>FgH6R1Us9meJKz7K=fa(jon#04tf-7(L0Owy!Vjk(Z7BIrG+}jLE=$dDQY@a+f;9M z)Lag87D|~agR_Z~Z`f`Hv7O(-Bxu%`RMf?g5J+jp;A?QN-SyYxqLDq{GGhE006TyP zJnx#bX~`Ra7jX4MIf(d>u)vbhHeKS!KBO__bZd_lC&$y03q z(hy1;X(shc++3t51*RvL@C~p7{Ky~cCl;D#R_HaFpK;l=1q3db|YWf#gknj2GrF#cm{?!L6#yxHr zUmpMnq`i!|Fyw(nLw^54VERNAzgUV_ab-X_3QgBUKl92%%3l1%pA{d!?ppu`p?$!G z$ZUD=sr>%a-d!B(E09t_l_i$$9|+VCc`Qk2xDP}GM0#}_P3Juuj=dZd1Kp4-(@fAwr$N=IuDlTMi^^L!hIpOT@gwxlvu%Vx(dkRN@ z@(L!K4a{xlzEb?lSN60+3!kFcY#_K$ycwJ9u8%$Y%$zG06eAGQsMWO^R$UE)s{C_^ zZ@rAo?tI*@!e?%~2*QQ2d8r+ojx3v#&|zq30JzIt!kb_K1@%>i?qdSuBM< zqf`WjSAG@-(7=SLMR{eS;Z4u7U%$b%jV}UG2CDxh*q5&VaXhB+mHxnZF@Q2hSJ|x( z`w@=&)hR>W(-?(8rAR}VAQH05^jbf^B*33Ob7B6L3qFd3>q*R+MfqK8c7FJ0H!SU* zT%e(WK(wjxBG*j35fOe_RTUD5CQvTNvgrZ7nKpM{`{t9Ejk`p8t5Hq(WTtI=fZ5>_ zzfqgNZ)Pz&4K6*cG;Y|RAaZxFA$kU9ea2W3Nx&Z#Eof8LiX zPP={Zk&6yMse-S06lkoU)vUJFiM-j-(`;cV1e6b^(s;}Brm@7Yent#S{HexSc7Upi zYa8?rf+z+UP+BW`;if^CU--g!EdSLAW6uB*wS_ z8w#msil3c-haX+|*%QJS?>`?g3yf7Br9dL4Ff^7<5#>8UjKy??c!%{z=}}$OGvZ}G96~; zZ~Zvb-4-ryKswP(q$U3uk&WM^-S84WzFLwfgmPg>#`E3NSDXnTAHSpVH$}(R?XBWP z7*)_ar{maP++W<~*sf@>KQxYfr#%pd0O`;KfloJ|_n`gjsRy~jz{}Nr#0B2w|BwJfE5fF&LOdTSv&pf)-~)d58C&QJLB2nnY8}V_)4y6X2!&cLw7iDFh#(Fm z8VVWf^*^GDm+U>2ZEl5SrjDHkkPyE&W%lmD$IibgH+N^2fhAVK^R4y(9$@(0g=}GI zhg@*Vg@s%G@DTwxy)H(q0T7u^@8IuVkoUSmasxh-fsPb}l5Lwx_F8ic_W{yG4FLhf zP*Q}7V8Y%t*zESPKe@}kG9B_Z2nkfH`r1s|;79)UCmip#I&zZjoB=5eg_2upK-wqL zs87_XMpG$=WV*6|$rOSh2-LIJ<-+T}@ge*AgO`=YvLat;vO`?OH<=3#yEnZRTKQNXIfE~=bI~)^y;)08Y zrmh!q-=I<2Gav!E0joMEnase!H|ZQ7$W5(35BP1@S~IX0Ej?NKrB`To$Q4R16=>zYYTV$ z^8F0gU%p^P@>5ElohEhcar%{m-D`%F8wgmLq`0hk+V5y2ejMVfl=!U`>dHD55bQe~ZIT*C+k%YlFdr0&;_a)^S-QGlyR16yH2>`-sz~*3r1NKv=3EbgSL?BO@>x=zLFsj7>XH=C1h=&JmsUO zS7o+6;%D}%({~f1;RNE!i&WxI-RMJRo8!N#w|M{DLhsrr=5kepFQuk6;nT{lY6-ta zEmm>Lw$)u(<2`)?ir0PX2>1E}mtck3`ZY-CC)o1Dzvt(^-ypZp5;_60^>kGsP)3r- zQ+N3&Bz2u|0-!iz7fdqr#J^tPrBD82nU*(O4lD-)C~>K5cGnZmy6dsLT{D2( zP>7*7jDqirDsOugufwP6yxNkK@idO-qiiTMTgr686c-p={hiD6cc1+Qk;2o-pxz?l z*MK;f={ub0b~$4YSLhFkI((-E+6jcJ_m6I+YF9e>O1S}PWMBl+p-SS{oD(8AG1f@l zw@>`(uK%w}KjBx}x1NBEscO_=P3zUBKHWXjI{g>mG20aW$ z&j88}iq7EB(;nnQzOsHam=B>O001}#1VJIl^#^?3M=vbQyS!}g4=WMk2j{}jA{MJa z4)G!qX{^*iF(p2l%9o^|(r^_eQW`Z!2X3_=}bod7F)zCud=k9_$a`_+a6F2-d z|Hc!y3NTRaP9+BMO04_W*!X76Z9k}YIO+S?=%AT}>{M`JFhp*#Ay5F)0BjtaZxMH| z53k0Atl$9>LBxSbh15C4_m@0o%*W*FO-7Hojs5@DL+AG3;3IVi2Bv^hcuS?z{la?EPAZ0Homh zUw^@S`c~H4e!#fCMIoucC$h<^Dn0}bm7k5+#ecD|Kr9ro=s;1B@fAV&wg@bF1?I(j z2Hg|a|B#=${i@=^r~W5E*&Lxn!$?YAS58!@hB4t!^48gTcfI{dKjsbhpIr#1Kn?X- z3@uhf#*bs&@gX;yvwV@V^e3q_nnkkrUF|MLG+0E;M zS};$A&tCa$04pT(uNEU`0I7`*boob}^+2@WDeFi5gBHs83Pp%DOkI=Ne|cU|{M84x zu?wELf7ym4MSICtxArHk^B}Lgy>Fu^bj%=IU=fR?>Lgy=PbonA2FgH@GFsrn=0W|h zz4^_a#ZXGE01_T!9A!>eX}J^Cf?~6Tc?wy&FZp z7gAUns=-ADq>WJWNhQr&uXQ|COiUS$_|pbOIAcQXcl%v0-iE?0KRP3P=0D#=X^V@u zQtFpMfah;|+$CrY*cR~~;j zdgF;(c<9ZS02He`RVc@KS5|^gP%|M&`BS|O5BiC>-L;+NLlp1#{+I6V25FFZ0}|3H ztaJ-VNOy;Hry!Cdog%e#!_pEG5(_L{qSR7Lv-{o8?|=BsgLyU&&fGKS%$fT-*US6o zihrQ0`a3mAQt}iM{p5jdd@Bu6m>=)2+@Mxojn998Fu82Qjk%!rllzvUeut>3_08uV zidCY%EJCMB)+fKBJQSmNs`vKucj|h6lq;nZPZCWr-Pzk^&PxuS1kBo6#^XQJPKn}q z0T<0;QE(h)k+_K7Kpec>&Azh22wt@T01(4qaB%D6FQ$k3E)Q#Op4WRZw07)j%PMC< zC-kV9#Q%C$*LTfxbMI=obB~r}7pi`oEg75So%y4!8b)1-%X?8A<_Kn?QXU6AIoiYA zUn0KuYzHQ2i=_RRuT4br`(bu1rx#Xf4rhtD$-IoM=y$AGHGCg_2Dl&c* zOT^<4yg9>RavQmS`2Ch_Gjyye;2#o!tDrCR2#gYQ^-J9VDreO6C+ra`2laMU>|tj3o6mVy8wugz zbqp5tO7>yZxQ2FWfhK?DZ$OQA$9o-nACSngUtgMD@_o@yDs0Cm3r+r}QPPqraoZue zYv>ko@?Sukz(CHsA;q+%r%PgSIG+=oTWiWcU%*8Jy%F0FQ}`XVtvIm|3 z=9LMX4g|d9x$Vp7T^76YgEM{pFTo;I?{_g}GeH=A)TAX1mg4L86={(SWiRsTs4%Jv z6TZ>SDONwQ+@5aTZe*2Le(ukP`3JEmDE(RuWV)va*`G~Ijek(qAETifq-1*QDfCEU zMHLH{^YA6cDWYHSo1Mhd`UlI>l5pYXZ^8i4WjOLBTxR`yZ<_{QdJ{fi&f>2w_A#uJ z2Y-4_Kk?|iR1|*~IJ-WMVDt_Pe*&$HlH#R!8{1di<&Dl_g&vxKrZ*yfn&C4b?isY?{+cNy{aqo0;^uYYOP*xY?qIjuJ(GK7lp~o(L*q3h zdyLD1Q{lkw8*P7H{{+HejU?i;xU3t+T8p^bR}wNRN?!L^8J>Xjkr~gwK|tM}RW(iy z@A%4)ZADrR@|u#Lw*=3+Bt?M{7c*O-L&UvaEa~wcCgf#0UuLFWZTf$b+ZUptnjVQJ zCO~t=h*lO2v?}k6a62Rsx^R6JpHnT$_90b_SVwxmFMd_RL;QYbFxJ zT16b?!k?2bQvXN}3F$M-%T6QPBMB50L8o6Mz8629zJZR&(;Gqte%2geMidrqIkrph z(5zT*HGKF<+4+G@#Vn&%HXS$C-8m!+inq!W2nG(tuw~dG^?H@arQ*}^c^B}eAts-Z zaL&NvcUu$Si*u~%%QKMO2+7C@c*c^u_IVchu9}N-{0};W{$ECU8?Ad-IHyZxP;eOU zh`uf6h#Xo?x21QlNt^*8ZbE--e;@47ib7EEldssOSp+aff;uAT^wursd5DVS0J7FJ z(1m981vDb1JEu3^dy$?L?sMLAGH-I`N`b`Hy-Ld>t_t?Oea=|Di52R6*TZ^^;YAxi z`G;`aM2Y@~GH6Hqkcw7&mz`IaY{iZc+Y92d)u=yqTMWh;U5oI}2)mJ|VI%s|khRrf z$DS#Fd%ghQM8aILmfl6~M}Lvr(EaGfK`!sX2u4EFEnPlj8{=yffGU-j2+{0HEdZk* z+gFnc!j3?3L-3zkIoTY0t!fzRU2<9{cLSe2C*2!c4*J=wcy*dg2T1T&`z4~!JAR(n z=Jxw+nY)AQ)>*Tt{nc@S{@TTR?!n_E*ASLqtJa71@g$pA4C?zzwc$IZ_W%o2RcKk# zrwYhg3eMuHl$nXCdDBdVQGHet@9(-&`nJI;=JY)Of* z97X?$0*ouTh+hF;RREJnDqyy;a>XcL4G?w4=7x zx{iNwxP%EZbz4QHvTQW z69F2fTzq7iit{nm3~cC+0nZ8{_8_v2+>^Hnn5P7IyY;(W*N91z8gdFoJS`z`rjT08 z5X%Q_O~=yzNsJ=yqw;+hZ_g(wwNqb3#JH4kV29q8YAq4iRq*y} zxOI)Xyh%L?jpDN!{3Yxbwy~Gr@0j{@TV^u!C@2FV?(?Al2$CTt|88UO(K&z5>pBeV z?VEj4@}93MDIGO-o;{q8W+pd(`3jbtdAlse1V(3P4^{fsXTpEM0M`?jjSK=~;$g%p zUd$}a?pDKgvQ^i8VKm`u#JxV!`X*Pv=TqR1Al7+n2xb6yUDT_8|TU8biY{E!AH6h9Bw2}Gn^nHJNPkJ>6j%! z@b#eOBKkZlDhLBJkwKt&o97qcdpK=1E^#E<0ZsGsRm@Q37s3qJkZH+g9`!*Q4zRqHEDzWTwMi2#{CZ$>z`6@kYeus*B2gt#BuwW z)op}7z4gvck`QMDP%AO>Xz1*a#lrc>bQEydl5a)rQoo)nCnj>r55GPS^Zea15JR)6 zUvL}oy>l>^MXi3sYJ%%MUgda9j6Om$;_CCJ)>^Sl`Pagk24P$*s*zgCT08kS4axA& zt#b12(&W3}@#FPdI=nN5-46Q5MYY9LR)r%mZ*CS4j15k}kx4#;`4eV>NE;LQM z)ZBTYJb7296$ktVJ;+%3Xa7^@D*KbJT=Cz=I?PEjO!{bab)MK|BArZ_Kq#gUk_D8^ zj98N>Uq2=(Gu{QY#ulf2Mzc=+RTq|4&ZI3n-OyCqJX(rAWI+mhJCHP4?4%>W$HY)- zZ>|CMcR5_{@HCX3!!4+&;Wx29)e#Lk|9Lf>3zA71zfy4%+f@W9&;slNBrg{~R3-e^ zsp1SslQ~u`wd#FZdy~Avc`+%&|Id#8?c&-E&@p&fkAXR;V)Zg{p=pK~SJZ%QbTFMD z48MXX3ILqSit6UoH``aF4=!-^y?uMgX?#CH7O6`0Bi5t3Sn z3)3-5ji}^&!k-P4k=rV6dj5@o6u*|RKlZPO2iz*njb(C=X>}3T|e!s9JB5 zo=jyu=UwT+EP|fk2HAA=3NcDrG~m0tg;bai-_^+y&##_4k~nVAU0UaE?ZF$CCd=NR-Pq5?>aDu@PEtvFgB_Egc`>^YnNU*QBl1|!=lwCrIiWHO||?g zLE@csIw{tZLt->4IWb~5==aL?k?EQX_Q?F_44jnTDOfPwl9HU``>aMb`nb>OeF%I7 zhSYPAdj=LUd$>pBCy_tW zWk0TD)7Eem=55!v!-<3YF#1zgAKg#6+#+Q%{Rrr-7Ed@qgia&5GCifn)m9= zr0Ojn?9a%)r6=QiCdJpKhB`~))&KhnfS(H$@y{TqycI-jjD2WdJ-&aWE-htMV^o?i z5}I8PF6|sD>rh<*O*ROH?Q5W9pCsqva_7Fy7vbhTbmlKan6|xmRBV!7{Ei>LY6HAz zP^n0DdlJI{`Ne;G`$Rc?e1!*OG$pH>q+jUa3h^abN!Sbut}(y+JDL$RvSYu`nqu79lU7NCoCYO#isSG&F4WKl)W+*#f0rvpJOomqMVvu0^eK$p{s7(?&jd zOdfH1=Gg9m{avbpTFWuq8aTO{pu!I*=$$K&PV+RdTtw9AXy5e?T3rSXTK$MK1uY)i zoLauS@9FR0j~uEglVp%}55RFFQ^7`$r^e}hc#7@_ww{WsnS1|_-A)sBglg4q?%Sbv z=@cJNnMnOLqY0E#cv^V`Z?Z+?N?7qpZASP>B}`~LGqLev5_MOu zW8~Ze_P@M7lAk}0xYBGn_f`SPhsyOK?({9M@3b0VOVSmcSy<~N%%$aOue1dkl&BVR zldZCm@#w!Wm;U=PrMYg+uetR{rEkm_XS}t3U_*3A6A-IXlfvqI(9kh2j zH;ORe0n>KF=*}~8t%l1Sg%r04h@XYPHol&lY3pU&Qj;xSWRbCnxr(`ZPJZ+XB#IbZ z0Q=zY)icfv$1|@2bCO&!G9^}Ybf8Y`5;pFv(pYY<$&&1olu{Z8D2eHK>=Rqj&Do;D+MMz-pn0u8&6Jgyu{kq!WB+|ji%sRJqzL7G^Jup>8# z>)j3hrEf{>k$R{csb$5I_;P^S__5@Sw-$g=t=F_W;suzi2^s5lxM^S!_Ps@B)U_Cp z=$JB)(Ul030zDL>#!1Oa=0A*!C5agint!~hMR-IR>w^)|KK=mp7()?t8lkb>SY;ga zZBhSvo1x9sx#KxPtlM{q3aYAKy+{pDV)zMJ4NL(Ox5Lyqa)(2M5=N3s%^*I{lNxtatp#xN7OT0(Sx%mmY1K98KU;8M|JH^K~hUmcRoK1rxq!gq;~KwWX+c zYm0G3dq(3T!|te=SOI1Izk;KX+ky+bv)@*yEX5YD3c}l1J%@g!_6cp>IQ82mbJEbo zN9)tAt=<}%$~^vMi37avcZ#eUBS}=0k%iT>N%dH+h+E}zP>;GVvb$ZtOu_xf=i zNBihYnnpY!az)DMNnlaDfnR*miXT50ZCLffeSNVmp*6rK1sNaEUZRx@#p%UAx^}P* zuNF{6IoiWdDDU&%OW3+@Cv5f&e#L8%62nnZlaC3tA)qH5quy$p0zFJW{QxMTsQryc z>jM|V(JKk*0!NPyah)P8%uE|RQU)_B>cJ*_w?spY*ZkfSenuMM0jgX2k4^c~_dp#J zDy}m8w8)%TD+yWGhwsT2exx-44eoayH3be!fljn%^u}un%}we`Ps5T+lg#H;T6_f| z**FwA2CrqFWX*cee$>Ui5?=iVImFBDE2ddQADZA8tC+=*gUZT`OAXs zQO(HDjtUbk@+_}O?`DyKg`q%F`pu|P!aSh^mJ<^_N~Ca+kG-)G@ysOeKb+lwtW3Z% z6`{W6Dxs;0F+8l8aG38-u$}BSaC#`{YJJGTQ8D;4;HMRP6}+vkSoT4^VPLla9R)t1 z9Kjh;9C?TPUl!Uu9s3u1;e0=;b;EHyOpZ28{kIC$5Bd7odM|Vd*ZR-cT*XezD*al? z!TnlVQf{2SHQOEb#j00_?y+iwO-G?xRz@(2Xk!MKG|0r8;Vfi(4diVa>Ow>~KymN~ z+RZN)NXQK=9P#kEju*AlV*e7xBmg`{v-@)bCwuLI zJ&ep{i4XZV=(^v=%{)ABQhyq8v5ZTgyz6Em%p>^ESdo{=iR8T5!0<4u&Hu1f*8OLe zg3i|&8JpCN=ZTh%wBTg`Tr(Mx2@lDn$w5|r!V3YJ55J3| zE>ay+N)RTBAQ@aUUsuZ4jX&FU=2t}$pDw(|G)3?jQjTJ~qkHe$2gz!{; zav-2O7>GklPX(9*h8PpwHSM?vg6JAAStl{>-=)(ilwuv$tbA~;E3PC?XShG;w(t6vy96}%tZ#s*_rb>jqP?ZZ(!IYJ zy~Us}wx2Seq`?4cH-N8pN?l;obNab5gxw)r);^(33YZx*c7aQGW)*0y8Fy{Z%W~7| zL&4KykRX{UepG!YIgC+MSuR+uh+t7{%RdJi_1;M?=RrxS3S zuyQ>PyT*wKv2UGeM@_?$3K}M>D$_V5xi3?6xoyvZF6lUHcm-Eoh^E(~Dh-vR8;wu+kEojk zx_BsORiwzEIEv2sBBCqcOi>o28VozPe0F$N(&y?8B?k+B`Cj(;^*6}PrVo0M^Bl+6hD)l(l%B+mhu^&;7oF>wByHcokB*m(W?5(9KPfF1< z$oye`L#9;>M+H95q|VVR#(if~^18}{i80_T?DzWfwoi(ZB80nhg>#_%2ZW(-B-f|B zYMCTrtI7Ye(rk6y{T#}WcKV`VF;=l^DHuMxeAK|sVkH8IFol{z^rSgAe2YJk|6w zB%ry*B6v8$%f;(CgHF|GoY#B2O+i)V@#4NzW-np7G>}>*M}nl{G>i#dbG~g`j*F?~ zPlme~kqT01{6s!jiOcq)J0EO45$if9DaeNxK(*1e%)2PE&|;srHNsD0f8(+qFo7+h zGL``!Af$72CwE=^POycWao;zJU7S%opq;(e%jVp@BLNpQzUmUbu7*Viza$x>Fk2KV z25Y#+6MRB0-!*amjB%i|F%s+aO?_!#jHQ0$d-5!KemAClVfKt=_Pe{tWDmp%$;r?o zIITl;$;4WWIpg8&kFpd8S&eD=v6ON1Qa4D@DxBnwn(4CE6P5kVCIqnQY%%fuXMkd? z3W`P|vtPL{#k!Eqz^5NZ< zYO}vIF!jvR?su@6aM+`3%k*Tdg&V89&^dnm={wMgZVsAqP>`E_yb#^NkcXn}Qa2=X z_D#hE(qgEmtKg1LHBg`UL$d>VGCKm=w~|u`d1B1Cglig^`2=?xvZECi+) zAhq+2^m7_5jh6pRQyMLe*2pC9bu)uC{ke3mE|zMZre0j|PrqeLx-u82!w>!>A;xk8 zwvro`MHa(wN&7N6B9a5SFaUhRh0v{Yj4j9`ayx&tAus1DOtT`C!}71q@l|cIMMg^- zm)}NgO^&rK-vyd30e5k-bZz4=wLAv-p#aWLu5Fh5>TUUXSQUm&PoH#zEOJ|r&rf6? z+xPr?nVz1GR-(PWbb!do&kfx5EqaZxS!ROeEfzg*Vayhic}{fA%)-cK4p&2Wxq|6U zzPjb=tsHW2Gi?6>J1NPzeBZ5YQa;C~ujT^a&lgb}vLq7uoAWYD5FKrsqlE;pUB?L~ z=sy;{+|Iii`UqavVts~*WrL|Cb4}n`wDr<*NB;71dt}9getIZf@jGm3lT2UN)3N zM0Cb>7m(U?#R?{j(Ob_`8-69u^mJC&5BCBUIPvGjjW}2Za%?(Ji9H z4^CS>8e#cYC>No=*dvJ2q#AyBhJIix$w~Nd4_mq0V!= zTlh7^*uRPLLCO*+qvwuCMUcP+-^+fVisIXK3q#M{=?~6KL@S`e4uwFGvZkz`EoH;O z`iO|^B#tHl9WIWDGT5yw<~s7~3jYfE^lG4A532P;0A7p2E^8@fKiKDjZv?l5rNa(V zJRr<`&4O9N-_?0DgK&0%<7ze?^eBKPLZD$5)E8dS^*CKvu;i3AZjjcFXBV5KHj&NB zCLE{B2pfNyT*|0iLNM+}5AGb;PP+2IW4S+-kV5`(zp!^V2h%|bOQ;gl&%)kRghXo* zBc%fFPA+y@>jza$BAJAB#{l?Bj3~^u8gvfNG{bu8UA@V1MXml07Olp5ui;fBHbt4w zv{DIMCPU!}Xfq>){;T2i&k2AO4@2bm*xUY$*h!L40i~D4{giQU!tXgd{(dPgp#-?C zJq(vl1Jsi7dCQ)PUiD3L}3ThK^rZ2NQ^>wqL2o@l2I_XTcZmpngMfLeSkrxSam*){0fyA)({>b4zfI_P^lE-D=Spux`yGt)Xh5nLO};a@R$XT=D+rd_m!9=dLFC{q&8y;M zwqJ-Nwz-=|Q6O4ghQVa?hnZx0!{in@#VA+zNUC?mLc^eE7K!9M^ItD(V3KZU{fkfUUF09u!#?2PiYklt zMz#@$W=5a|A(G$RPavg5gRi2d(5^=%+$a#2{}0gqN|gf>{};5UpiV;e0jGIJF5Oy7 z#lpHdhME{3NLQgzQ%-c8ic&gc3$Y1U@?kA+mOX?ju%0O3Dys?cZxM*$pHn}RT%K%s z;1(l(e%}~K(P8sH8$DL@y# zMyUmoL|N-`BP&AKjAKc68!fdoBVuj(?CWR0e;X+} zY{@%VIE&6#0#_01ToNOvJV~*-PztLvg9T`$a8lZBm)>%v_}0ULG*^$2+T=aqF)%y3#RArepcHhVC^nl?R|pZJI7}N zhF)S4I2rHLUA)fHFUvYb+1q1W|H0Tc@sLZg>VbO(Z0G&NqU8fu?XN=T!{boFZIbJj z&3At*U;M%SD_o+=vrlw-CDc*Wzvpx#79MF$UPsJ&^0FYVu`OMFv`0|!OEh|kf`&IS z=WVqDFN;Qe?j4mk4&VTeF=>>XG?Jj8LQBdZgae{A<9|_ZEuuC!7>iLdXrlPGI5On9 zf#6~+1_V_lLFNosrU3S9JZm!)+bf@wRr1k#52|lzD!6g0Oaei2Mmj6|VZG2QhGXY^ z#$gHwhQDUQUFN{+NCm}Mwi#&9l)vjEz<|4(3!%{ChT7vVUNQv6LQg}GZzAZQ=j=!qyK7ZfPbpTyuX zGHOpk2%t(^ouT?th?qeXclu1mQ9*anzdT=Bt~&RMz&{os$J4M*-yHA4+Nfv3Ry+m1 z$QTySnPjo`v0vWC`uE`%(?AK`XrE^cz$l>?CMR0~Ke{+cwiTh!?dzcPrO^iDzo{Mj%a!KE`fMP{%QQu#q&6DD3GFBYJ^oO1ej0EgvguNfggR*y$w`rKr&P%a| z#tVMMwd3YJ4WvB`gehAtGpcUtSN3uy;r=-qRRjcJMP8M7{q?5ADQ>oruWqERxpUY& zpqb4aDnkOjc}OI1qpmTWWEzdlJKCD`QLAn&W$1*5IpTs;7gFJlME^$S{z{ED@TD}z z6rHMjw3QL6M0qk)XhzX9Gh>zWC|3pq9}FgQJd2lh{8nRdZ~gyvdP#zizsy#gSyP>z Q1E3BKRc)09CF|(_1KZV3$p8QV literal 0 HcmV?d00001 diff --git a/desktop/OmniNX.DesktopInstaller/Assets/logo-128.png b/desktop/OmniNX.DesktopInstaller/Assets/logo-128.png new file mode 100644 index 0000000000000000000000000000000000000000..a14362b1ef5c9c198c760174c11abcadc96efe8b GIT binary patch literal 13498 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 +// Generated by tools/generate_deletion_lists.py — do not edit manually. + +namespace OmniNX.DesktopInstaller.Data; + +public static partial class DeletionLists +{ + public static class Clean + { + public static readonly string[] clean_atmosphere_dirs_to_delete = + { + @"atmosphere", + }; + + public static readonly string[] clean_atmosphere_contents_dirs_to_delete = + { + }; + + public static readonly string[] clean_atmosphere_files_to_delete = + { + }; + + public static readonly string[] clean_bootloader_dirs_to_delete = + { + @"bootloader\boot", + @"bootloader\bootlogo", + @"bootloader\ini2", + @"bootloader\payloads", + @"bootloader\reboot", + @"bootloader\res", + @"bootloader\sys", + }; + + public static readonly string[] clean_bootloader_files_to_delete = + { + @"bootloader\ArgonNX.bin", + @"bootloader\bootlogo.bmp", + @"bootloader\hekate_ipl.ini", + @"bootloader\patches.ini", + @"bootloader\update.bin", + @"bootloader\ini\EmuMMC ohne Mods.ini", + }; + + public static readonly string[] clean_config_dirs_to_delete = + { + @"config\aio-switch-updater", + @"config\blue_pack_updater", + @"config\kefir-updater", + @"config\nx-hbmenu", + @"config\sys-con", + @"config\sys-patch", + @"config\uberhand", + @"config\ultrahand", + }; + + public static readonly string[] clean_switch_dirs_to_delete = + { + @"switch", + }; + + public static readonly string[] clean_switch_files_to_delete = + { + }; + + public static readonly string[] clean_root_files_to_delete = + { + @"boot.dat", + @"boot.ini", + @"exosphere.bin", + @"exosphere.ini", + @"hbmenu.nro", + @"install.bat", + @"license", + @"loader.bin", + @"mc-mitm.log", + @"payload.bin", + @"update.bin", + @"version", + }; + + public static readonly string[] clean_misc_dirs_to_delete = + { + @"argon", + @"games", + @"NSPs (Tools)", + @"Patched Apps", + @"SaltySD\flags", + @"SaltySD\patches", + @"scripts", + @"TegraExplorer", + @"themes\systemData", + @"tools", + @"warmboot_mariko", + }; + + public static readonly string[] clean_misc_files_to_delete = + { + @"fusee-primary.bin", + @"fusee.bin", + @"SaltySD\exceptions.txt", + @"SaltySD\saltysd_bootstrap.elf", + @"SaltySD\saltysd_bootstrap32_3k.elf", + @"SaltySD\saltysd_bootstrap32_5k.elf", + @"SaltySD\saltysd_core.elf", + @"SaltySD\saltysd_core32.elf", + }; + + public static readonly string[] clean_post_restore_dirs_to_delete = + { + @"switch\tinfoil\db", + }; + + public static readonly string[] old_version_files_to_delete = + { + @"1.4.0-pre", + @"1.4.0-pre-c", + @"1.4.0-pre-d", + @"1.4.1", + @"1.5.0", + @"1.6.0", + @"1.6.1", + @"1.6.2", + }; + + } + + public static class Update + { + public static readonly string[] atmosphere_dirs_to_delete = + { + @"atmosphere\config", + @"atmosphere\crash_reports", + @"atmosphere\erpt_reports", + @"atmosphere\exefs_patches\audio_mastervolume", + @"atmosphere\exefs_patches\CrunchPatch", + @"atmosphere\exefs_patches\SaltyNX_Fixes", + @"atmosphere\exefs_patches\Crunchyroll Patch 1.10.0", + @"atmosphere\exefs_patches\bluetooth_patches", + @"atmosphere\exefs_patches\bootlogo", + @"atmosphere\exefs_patches\btm_patches", + @"atmosphere\exefs_patches\es_patches", + @"atmosphere\exefs_patches\hid_patches", + @"atmosphere\exefs_patches\logo", + @"atmosphere\exefs_patches\nfim_ctest", + @"atmosphere\exefs_patches\nim_ctest", + @"atmosphere\exefs_patches\nvnflinger_cmu", + @"atmosphere\extrazz", + @"atmosphere\fatal_errors", + @"atmosphere\fatal_reports", + @"atmosphere\flags", + @"atmosphere\hbl_html", + @"atmosphere\hosts", + @"atmosphere\kips", + @"atmosphere\kip1", + @"atmosphere\kip_patches", + @"atmosphere\logs", + }; + + public static readonly string[] atmosphere_contents_dirs_to_delete = + { + @"atmosphere\contents\0000000000534C56", + @"atmosphere\contents\00FF0000B378D640", + @"atmosphere\contents\00FF0000636C6BFF", + @"atmosphere\contents\00FF0000A53BB665", + @"atmosphere\contents\0100000000000008", + @"atmosphere\contents\010000000000000D", + @"atmosphere\contents\010000000000002B", + @"atmosphere\contents\0100000000000032", + @"atmosphere\contents\0100000000000034", + @"atmosphere\contents\0100000000000036", + @"atmosphere\contents\0100000000000037", + @"atmosphere\contents\010000000000003C", + @"atmosphere\contents\0100000000000042", + @"atmosphere\contents\0100000000000895", + @"atmosphere\contents\0100000000000F12", + @"atmosphere\contents\0100000000001000", + @"atmosphere\contents\0100000000001007", + @"atmosphere\contents\0100000000001013", + @"atmosphere\contents\010000000000DA7A", + @"atmosphere\contents\010000000000bd00", + @"atmosphere\contents\01006a800016e000", + @"atmosphere\contents\01009D901BC56000", + @"atmosphere\contents\0100A3900C3E2000", + @"atmosphere\contents\0100F43008C44000", + @"atmosphere\contents\050000BADDAD0000", + @"atmosphere\contents\4200000000000000", + @"atmosphere\contents\420000000000000B", + @"atmosphere\contents\420000000000000E", + @"atmosphere\contents\4200000000000010", + @"atmosphere\contents\4200000000000FFF", + @"atmosphere\contents\420000000007E51A", + @"atmosphere\contents\420000000007E51B", + @"atmosphere\contents\690000000000000D", + }; + + public static readonly string[] atmosphere_files_to_delete = + { + @"atmosphere\config\exosphere.ini", + @"atmosphere\config\stratosphere.ini", + @"atmosphere\hbl.nsp", + @"atmosphere\package3", + @"atmosphere\reboot_payload.bin", + @"atmosphere\stratosphere.romfs", + @"atmosphere\kips\loader.kip", + @"atmosphere\kips\hoc.kip", + }; + + public static readonly string[] bootloader_dirs_to_delete = + { + @"bootloader\boot", + @"bootloader\bootlogo", + @"bootloader\ini2", + @"bootloader\payloads", + @"bootloader\reboot", + @"bootloader\res", + @"bootloader\sys", + }; + + public static readonly string[] bootloader_files_to_delete = + { + @"bootloader\ArgonNX.bin", + @"bootloader\bootlogo.bmp", + @"bootloader\hekate_ipl.ini", + @"bootloader\nyx.ini", + @"bootloader\patches.ini", + @"bootloader\update.bin", + @"bootloader\ini\EmuMMC ohne Mods.ini", + }; + + public static readonly string[] config_dirs_to_delete = + { + @"config\aio-switch-updater", + @"config\blue_pack_updater", + @"config\kefir-updater", + @"config\nx-hbmenu", + @"config\quickntp", + @"config\sys-con", + @"config\sys-patch", + @"config\uberhand", + @"config\ultrahand", + }; + + public static readonly string[] switch_dirs_to_delete = + { + @"switch\.packages", + @"switch\.overlays", + @"switch\90DNS_tester", + @"switch\aio-switch-updater", + @"switch\amsPLUS-downloader", + @"switch\appstore", + @"switch\AtmoXL-Titel-Installer", + @"switch\Benchmark-Toolbox", + @"switch\breeze", + @"switch\cheats-updater", + @"switch\chiaki", + @"switch\ChoiDujourNX", + @"switch\crash_ams", + @"switch\Daybreak", + @"switch\DNS-Block_Tester", + @"switch\DNS_mitm Tester", + @"switch\EdiZon", + @"switch\Fizeau", + @"switch\FTPD", + @"switch\Furmark-NX", + @"switch\fw-downloader", + @"switch\gamecard_installer", + @"switch\Goldleaf", + @"switch\haze", + @"switch\JKSV", + @"switch\kefir-updater", + @"switch\ldnmitm_config", + @"switch\Linkalho", + @"switch\Moonlight-Switch", + @"switch\Neumann", + @"switch\NX-Activity-Log", + @"switch\NX-Save-Sync", + @"switch\NX-Shell", + @"switch\NX-Update-Checker ", + @"switch\NXGallery", + @"switch\NXRemoteLauncher", + @"switch\NXThemesInstaller", + @"switch\nxdumptool", + @"switch\nxmtp", + @"switch\Payload_launcher", + @"switch\Reboot", + @"switch\reboot_to_argonNX", + @"switch\reboot_to_hekate", + @"switch\Shutdown_System", + @"switch\SimpleModAlchemist", + @"switch\SimpleModDownloader", + @"switch\SimpleModManager", + @"switch\sphaira", + @"switch\studious-pancake", + @"switch\Switch-Time", + @"switch\SwitchIdent", + @"switch\Switch_themes_Installer", + @"switch\swr-ini-tool", + @"switch\Sys-Clk Manager", + @"switch\Sys-Con", + @"switch\sys-clk-manager", + @"switch\themezer-nx", + @"switch\ThemezerNX", + @"switch\themezernx", + @"switch\tinwoo", + @"switch\Ultrahand-Reload", + }; + + public static readonly string[] switch_files_to_delete = + { + @"switch\90DNS_tester\90DNS_tester.nro", + @"switch\Benchmark-Toolbox\Benchmark-Toolbox.nro", + @"switch\breeze.nro", + @"switch\cheats-updater.nro", + @"switch\chiaki.nro", + @"switch\ChoiDujourNX.nro", + @"switch\daybreak.nro", + @"switch\DBI.nro", + @"switch\DBI\DBI.nro", + @"switch\DBI\DBI_810_DE.nro", + @"switch\DBI\DBI_810_EN.nro", + @"switch\DBI\DBI_845_DE.nro", + @"switch\DBI\DBI_845_EN.nro", + @"switch\DBI\DBI_849_DE.nro", + @"switch\DBI\DBI_849_EN.nro", + @"switch\DBI_810_DE\DBI_810.nro", + @"switch\DBI_810_DE\DBI_810_DE.nro", + @"switch\DBI_810_EN\DBI_810_EN.nro", + @"switch\DBI_RU\DBI_RU.nro", + @"switch\DBI\DBI_EN.nro", + @"switch\DBI_DE\DBI_DE.nro", + @"switch\DNS-Block_Tester\dns_tester.nro", + @"switch\DNS_mitm Tester.nro", + @"switch\EdiZon.nro", + @"switch\Fizeau.nro", + @"switch\Furmark-NX\Furmark-NX.nro", + @"switch\Goldleaf.nro", + @"switch\haze.nro", + @"switch\JKSV.nro", + @"switch\ldnmitm_config.nro", + @"switch\linkalho.nro", + @"switch\Moonlight-Switch.nro", + @"switch\Neumann.nro", + @"switch\NX-Shell.nro", + @"switch\NXGallery.nro", + @"switch\NXThemesInstaller.nro", + @"switch\nxdumptool.nro", + @"switch\nxtc.bin", + @"switch\reboot_to_payload.nro", + @"switch\SimpleModAlchemist\Simple_Mod_Alchemist.nro", + @"switch\SimpleModDownloader.nro", + @"switch\SimpleModManager.nro", + @"switch\sphaira.nro", + @"switch\swr-ini-tool\swr-ini-tool.nro", + @"switch\SwitchIdent.nro", + @"switch\Switch_themes_Installer\NXThemesInstaller.nro", + @"switch\Sys-Clk Manager\sys-clk-manager.nro", + @"switch\Sys-Con.nro", + @"switch\sys-clk-manager.nro", + @"switch\ThemezerNX\themezer-nx.nro", + @"switch\tinfoil.nro", + @"switch\tinfoil\tinfoil.nro", + @"switch\tinwoo.nro", + @"switch\tinwoo\tinwoo.nro", + @"switch\Ultrahand-Reload\Ultrahand-Reload.nro", + }; + + public static readonly string[] root_files_to_delete = + { + @"boot.dat", + @"boot.ini", + @"exosphere.bin", + @"exosphere.ini", + @"hbmenu.nro", + @"install.bat", + @"license", + @"loader.bin", + @"mc-mitm.log", + @"payload.bin", + @"update.bin", + @"version", + }; + + public static readonly string[] misc_dirs_to_delete = + { + @"argon", + @"games", + @"NSPs (Tools)", + @"Patched Apps", + @"SaltySD\flags", + @"scripts", + @"switch\tinfoil\db", + @"themes\systemData", + @"tools", + @"warmboot_mariko", + }; + + public static readonly string[] misc_files_to_delete = + { + @"fusee-primary.bin", + @"fusee.bin", + @"SaltySD\exceptions.txt", + @"SaltySD\saltysd_bootstrap.elf", + @"SaltySD\saltysd_bootstrap32_3k.elf", + @"SaltySD\saltysd_bootstrap32_5k.elf", + @"SaltySD\saltysd_core.elf", + @"SaltySD\saltysd_core32.elf", + }; + + } +} diff --git a/desktop/OmniNX.DesktopInstaller/MainForm.cs b/desktop/OmniNX.DesktopInstaller/MainForm.cs new file mode 100644 index 0000000..191b5bf --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/MainForm.cs @@ -0,0 +1,581 @@ +using OmniNX.DesktopInstaller.Models; +using OmniNX.DesktopInstaller.Services; +using System.Runtime.InteropServices; + +namespace OmniNX.DesktopInstaller; + +public sealed class MainForm : Form +{ + private readonly GiteaClient _gitea = new(); + private readonly VersionDetectService _versionDetect = new(); + private readonly InstallEngine _installEngine = new(); + + private ComboBox _driveCombo = null!; + private Button _refreshDrivesButton = null!; + private Label _statusLabel = null!; + private Label _releaseLabel = null!; + private Button _checkReleaseButton = null!; + private RadioButton _variantLight = null!; + private RadioButton _variantStandard = null!; + private RadioButton _variantOc = null!; + private RadioButton _ram4Gb = null!; + private RadioButton _ram8Gb = null!; + private RadioButton _hekateStandard = null!; + private RadioButton _hekatePro = null!; + private CheckBox _forceCleanCheck = null!; + private Button _installButton = null!; + private ProgressBar _progressBar = null!; + private TextBox _logBox = null!; + + private ReleaseInfo? _latestRelease; + private CancellationTokenSource? _operationCts; + + public MainForm() + { + Text = "OmniNX Desktop Installer"; + MinimumSize = new Size(820, 640); + Size = new Size(900, 700); + StartPosition = FormStartPosition.CenterScreen; + Font = new Font("Segoe UI", 9F); + Icon = LoadFormIcon() ?? Icon.ExtractAssociatedIcon(Application.ExecutablePath); + + BuildLayout(); + RefreshDrives(); + _ = LoadLatestReleaseAsync(); + } + + private void BuildLayout() + { + var root = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 8, + Padding = new Padding(12), + }; + root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + root.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + + root.Controls.Add(BuildHeaderPanel(), 0, 0); + root.Controls.Add(BuildDrivePanel(), 0, 1); + root.Controls.Add(BuildVariantPanel(), 0, 2); + root.Controls.Add(BuildRamPanel(), 0, 3); + root.Controls.Add(BuildHekatePanel(), 0, 4); + root.Controls.Add(BuildOptionsPanel(), 0, 5); + root.Controls.Add(BuildActionPanel(), 0, 6); + root.Controls.Add(BuildLogPanel(), 0, 7); + + Controls.Add(root); + } + + private Control BuildHeaderPanel() + { + var panel = new TableLayoutPanel + { + AutoSize = true, + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 2, + }; + panel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + panel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + + var title = new Label + { + Text = "OmniNX Desktop Installer", + Font = new Font(Font.FontFamily, 14F, FontStyle.Bold), + AutoSize = true, + Anchor = AnchorStyles.Left, + Margin = new Padding(0, 0, 16, 4), + }; + + _releaseLabel = new Label + { + AutoSize = true, + Text = "Release: checking...", + Margin = new Padding(0, 0, 8, 8), + Anchor = AnchorStyles.Left, + }; + _checkReleaseButton = new Button { Text = "Refresh release", AutoSize = true, Margin = new Padding(0, 0, 0, 8) }; + _checkReleaseButton.Click += async (_, _) => await LoadLatestReleaseAsync(); + + var releaseRow = new FlowLayoutPanel + { + AutoSize = true, + WrapContents = false, + Dock = DockStyle.Fill, + Margin = new Padding(0), + }; + releaseRow.Controls.Add(_releaseLabel); + releaseRow.Controls.Add(_checkReleaseButton); + + panel.Controls.Add(title, 0, 0); + panel.Controls.Add(releaseRow, 0, 1); + return panel; + } + + private static Image? LoadEmbeddedImage(string resourceName) + { + var stream = typeof(MainForm).Assembly.GetManifestResourceStream(resourceName); + return stream is null ? null : Image.FromStream(stream); + } + + private static Icon? LoadFormIcon() + { + using var image = LoadEmbeddedImage("OmniNX.DesktopInstaller.Assets.logo-128.png"); + if (image is null) + return null; + + using var bitmap = new Bitmap(image, new Size(32, 32)); + var handle = bitmap.GetHicon(); + try + { + using var temp = Icon.FromHandle(handle); + return (Icon)temp.Clone(); + } + finally + { + DestroyIcon(handle); + } + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool DestroyIcon(IntPtr hIcon); + + private Control BuildDrivePanel() + { + var group = new GroupBox { Text = "SD Card / Target Drive", Dock = DockStyle.Fill, AutoSize = true, Padding = new Padding(10) }; + var layout = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 3, AutoSize = true }; + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90)); + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); + layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + + _driveCombo = new ComboBox { Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDownList }; + _driveCombo.SelectedIndexChanged += (_, _) => UpdateInstallationStatus(); + + _refreshDrivesButton = new Button { Text = "Refresh", AutoSize = true }; + _refreshDrivesButton.Click += (_, _) => RefreshDrives(); + + _statusLabel = new Label + { + Dock = DockStyle.Fill, + AutoSize = true, + ForeColor = Color.DimGray, + Text = "Select a removable drive (SD card reader).", + }; + + layout.Controls.Add(new Label { Text = "Drive:", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 0); + layout.Controls.Add(_driveCombo, 1, 0); + layout.Controls.Add(_refreshDrivesButton, 2, 0); + layout.SetColumnSpan(_statusLabel, 3); + layout.Controls.Add(_statusLabel, 0, 1); + + group.Controls.Add(layout); + return group; + } + + private Control BuildVariantPanel() + { + var group = new GroupBox { Text = "OmniNX Pack Variant", Dock = DockStyle.Fill, AutoSize = true, Padding = new Padding(10) }; + var flow = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true }; + + _variantLight = new RadioButton { Text = "Light", AutoSize = true, Margin = new Padding(0, 0, 24, 0) }; + _variantStandard = new RadioButton { Text = "Standard", AutoSize = true, Checked = true, Margin = new Padding(0, 0, 24, 0) }; + _variantOc = new RadioButton { Text = "OC", AutoSize = true }; + + flow.Controls.AddRange([_variantLight, _variantStandard, _variantOc]); + group.Controls.Add(flow); + return group; + } + + private Control BuildRamPanel() + { + var group = new GroupBox { Text = "RAM / Hekate Payload", Dock = DockStyle.Fill, AutoSize = true, Padding = new Padding(10) }; + var layout = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 1 }; + + _ram4Gb = new RadioButton + { + Text = "4 GB console (default)", + AutoSize = true, + Checked = true, + }; + _ram8Gb = new RadioButton + { + Text = "8 GB console (RAM mod) — copies 8GB hekate payload to payload.bin and bootloader/update.bin", + AutoSize = true, + }; + + var warn = new Label + { + Text = "Warning: Only select 8 GB if the console has an 8 GB RAM mod. Wrong selection can cause boot issues.", + AutoSize = true, + ForeColor = Color.DarkOrange, + MaximumSize = new Size(820, 0), + Margin = new Padding(0, 6, 0, 0), + }; + + layout.Controls.Add(_ram4Gb); + layout.Controls.Add(_ram8Gb); + layout.Controls.Add(warn); + group.Controls.Add(layout); + return group; + } + + private Control BuildHekatePanel() + { + var group = new GroupBox { Text = "Hekate / Atmosphere", Dock = DockStyle.Fill, AutoSize = true, Padding = new Padding(10) }; + var layout = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 1 }; + + _hekateStandard = new RadioButton + { + Text = "Standard Hekate / Atmosphere", + AutoSize = true, + Checked = true, + }; + _hekatePro = new RadioButton + { + Text = "Hekate-Pro / Atmosphere-Pro (only for emuMMC on eMMC)", + AutoSize = true, + }; + + var hint = new Label + { + Text = "Pro variant replaces bootloader/atmosphere files from extras/hekate-ams-pro/.", + AutoSize = true, + ForeColor = Color.DimGray, + Margin = new Padding(0, 6, 0, 0), + }; + + layout.Controls.Add(_hekateStandard); + layout.Controls.Add(_hekatePro); + layout.Controls.Add(hint); + group.Controls.Add(layout); + return group; + } + + private Control BuildOptionsPanel() + { + _forceCleanCheck = new CheckBox + { + Text = "Force clean install (full wipe with backup/restore) even if OmniNX is already installed", + AutoSize = true, + MaximumSize = new Size(820, 0), + }; + + return _forceCleanCheck; + } + + private Control BuildActionPanel() + { + var panel = new FlowLayoutPanel { AutoSize = true, Dock = DockStyle.Fill, WrapContents = false }; + _installButton = new Button + { + Text = "Download && Install", + AutoSize = true, + Height = 36, + Font = new Font(Font.FontFamily, 10F, FontStyle.Bold), + }; + _installButton.Click += async (_, _) => await RunInstallAsync(); + + _progressBar = new ProgressBar { Width = 500, Height = 24, Margin = new Padding(16, 6, 0, 0) }; + + panel.Controls.Add(_installButton); + panel.Controls.Add(_progressBar); + return panel; + } + + private Control BuildLogPanel() + { + var group = new GroupBox { Text = "Log", Dock = DockStyle.Fill, Padding = new Padding(10) }; + _logBox = new TextBox + { + Dock = DockStyle.Fill, + Multiline = true, + ScrollBars = ScrollBars.Vertical, + ReadOnly = true, + Font = new Font("Consolas", 9F), + BackColor = Color.FromArgb(24, 24, 24), + ForeColor = Color.Gainsboro, + }; + group.Controls.Add(_logBox); + return group; + } + + private void RefreshDrives() + { + DriveService.DriveItem? selected = _driveCombo.SelectedItem as DriveService.DriveItem; + _driveCombo.Items.Clear(); + + foreach (var drive in DriveService.GetCandidateDrives()) + _driveCombo.Items.Add(drive); + + if (selected is not null) + { + for (var i = 0; i < _driveCombo.Items.Count; i++) + { + if (_driveCombo.Items[i] is DriveService.DriveItem d && + d.Drive.Name.Equals(selected.Drive.Name, StringComparison.OrdinalIgnoreCase)) + { + _driveCombo.SelectedIndex = i; + return; + } + } + } + + if (_driveCombo.Items.Count > 0) + _driveCombo.SelectedIndex = 0; + else + UpdateInstallationStatus(); + } + + private DriveInfo? SelectedDrive => + (_driveCombo.SelectedItem as DriveService.DriveItem)?.Drive; + + private OmniNxVariant SelectedVariant + { + get + { + if (_variantOc.Checked) return OmniNxVariant.Oc; + if (_variantLight.Checked) return OmniNxVariant.Light; + return OmniNxVariant.Standard; + } + } + + private void UpdateInstallationStatus() + { + var drive = SelectedDrive; + if (drive is null) + { + _statusLabel.Text = "No drive selected."; + _statusLabel.ForeColor = Color.DimGray; + return; + } + + try + { + var status = _versionDetect.DetectInstallation(drive.RootDirectory.FullName); + if (status.IsInstalled) + { + _statusLabel.Text = + $"OmniNX detected: {VariantHelper.GetDisplayName(status.Variant)} v{status.Version} — " + + (_forceCleanCheck.Checked ? "will clean install" : "will update"); + _statusLabel.ForeColor = Color.ForestGreen; + } + else + { + _statusLabel.Text = "No OmniNX installation detected — will perform clean install."; + _statusLabel.ForeColor = Color.SteelBlue; + } + } + catch (Exception ex) + { + _statusLabel.Text = $"Could not read drive: {ex.Message}"; + _statusLabel.ForeColor = Color.Firebrick; + } + } + + private async Task LoadLatestReleaseAsync() + { + try + { + SetUiEnabled(false); + _releaseLabel.Text = "Release: fetching..."; + _latestRelease = await _gitea.GetLatestReleaseAsync(); + _releaseLabel.Text = $"Latest release: {_latestRelease.TagName} ({_latestRelease.Name})"; + Log($"Latest release: {_latestRelease.TagName}"); + } + catch (Exception ex) + { + _releaseLabel.Text = "Release: fetch failed"; + Log($"Failed to fetch release: {ex.Message}", error: true); + } + finally + { + SetUiEnabled(true); + } + } + + private async Task RunInstallAsync() + { + var drive = SelectedDrive; + if (drive is null) + { + MessageBox.Show(this, "Please select a target drive.", "OmniNX Installer", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + if (_latestRelease is null) + { + MessageBox.Show(this, "Release information is not loaded yet. Click 'Refresh release' first.", + "OmniNX Installer", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + var variant = SelectedVariant; + var asset = _gitea.FindPackAsset(_latestRelease, variant); + if (asset is null) + { + MessageBox.Show(this, + $"No zip asset found for {VariantHelper.GetDisplayName(variant)} in release {_latestRelease.TagName}.", + "OmniNX Installer", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + var driveRoot = drive.RootDirectory.FullName; + var status = _versionDetect.DetectInstallation(driveRoot); + var mode = _versionDetect.DetermineMode(status, _forceCleanCheck.Checked); + + var modeText = mode == InstallMode.Update ? "UPDATE" : "CLEAN INSTALL"; + var confirm = MessageBox.Show(this, + $"Target drive: {drive.Name}\n" + + $"Variant: {VariantHelper.GetDisplayName(variant)}\n" + + $"Mode: {modeText}\n" + + $"Release: {_latestRelease.TagName}\n" + + $"RAM: {(_ram8Gb.Checked ? "8 GB" : "4 GB")}\n" + + $"Hekate/AMS: {(_hekatePro.Checked ? "Pro" : "Standard")}\n\n" + + "All data on paths targeted by the installer may be modified or deleted.\n" + + "Continue?", + "Confirm installation", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2); + + if (confirm != DialogResult.Yes) + return; + + _operationCts?.Cancel(); + _operationCts = new CancellationTokenSource(); + var token = _operationCts.Token; + + try + { + SetUiEnabled(false); + _progressBar.Value = 0; + _logBox.Clear(); + + var cacheDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "OmniNX-Desktop-Installer", + "cache"); + PathUtils.EnsureDirectory(cacheDir); + + var zipPath = Path.Combine(cacheDir, asset.Name); + if (!File.Exists(zipPath)) + { + Log($"Downloading {asset.Name} ({asset.Size / (1024 * 1024)} MB)..."); + await _gitea.DownloadAssetAsync(asset, zipPath, new Progress<(long, long)>(p => + { + var (downloaded, total) = p; + if (total > 0) + { + var pct = (int)(downloaded * 100 / total); + _progressBar.Value = Math.Clamp(pct, 0, 100); + } + }), token); + Log("Download complete."); + } + else + { + Log($"Using cached pack: {zipPath}"); + } + + var extractRoot = Path.Combine(cacheDir, "extract", _latestRelease.TagName); + var staging = ZipExtractor.ExtractPack(zipPath, extractRoot, new Progress(msg => Log(msg))); + Log($"Pack staging: {staging}"); + + var options = new InstallOptions + { + DriveRoot = driveRoot, + Variant = variant, + Mode = mode, + Use8GbRam = _ram8Gb.Checked, + UseHekateAmsPro = _hekatePro.Checked, + ForceCleanInstall = _forceCleanCheck.Checked, + StagingPath = staging, + }; + + var progress = new Progress(report => + { + Log(report.Message, report.IsError); + if (report.Percent is int pct) + _progressBar.Value = Math.Clamp(pct, 0, 100); + }); + + await _installEngine.RunAsync(options, progress, token); + _progressBar.Value = 100; + + MessageBox.Show(this, + mode == InstallMode.Update + ? "OmniNX update completed successfully." + : "OmniNX installation completed successfully.", + "OmniNX Installer", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + UpdateInstallationStatus(); + } + catch (OperationCanceledException) + { + Log("Operation cancelled.", error: true); + } + catch (Exception ex) + { + Log($"Installation failed: {ex.Message}", error: true); + MessageBox.Show(this, ex.Message, "Installation failed", + MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetUiEnabled(true); + } + } + + private void Log(string message, bool error = false) + { + if (InvokeRequired) + { + BeginInvoke(() => Log(message, error)); + return; + } + + var prefix = $"[{DateTime.Now:HH:mm:ss}] "; + _logBox.AppendText(prefix + message + Environment.NewLine); + if (error) + _statusLabel.ForeColor = Color.Firebrick; + } + + private void SetUiEnabled(bool enabled) + { + if (InvokeRequired) + { + BeginInvoke(() => SetUiEnabled(enabled)); + return; + } + + _installButton.Enabled = enabled; + _checkReleaseButton.Enabled = enabled; + _refreshDrivesButton.Enabled = enabled; + _driveCombo.Enabled = enabled; + _variantLight.Enabled = enabled; + _variantStandard.Enabled = enabled; + _variantOc.Enabled = enabled; + _ram4Gb.Enabled = enabled; + _ram8Gb.Enabled = enabled; + _hekateStandard.Enabled = enabled; + _hekatePro.Enabled = enabled; + _forceCleanCheck.Enabled = enabled; + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + _forceCleanCheck.CheckedChanged += (_, _) => UpdateInstallationStatus(); + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Models/InstallModels.cs b/desktop/OmniNX.DesktopInstaller/Models/InstallModels.cs new file mode 100644 index 0000000..6800780 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Models/InstallModels.cs @@ -0,0 +1,54 @@ +namespace OmniNX.DesktopInstaller.Models; + +public enum OmniNxVariant +{ + None = 0, + Light, + Standard, + Oc, +} + +public enum InstallMode +{ + Clean, + Update, +} + +public sealed class InstallOptions +{ + public required string DriveRoot { get; init; } + public required OmniNxVariant Variant { get; init; } + public InstallMode Mode { get; init; } + public bool Use8GbRam { get; init; } + public bool UseHekateAmsPro { get; init; } + public bool ForceCleanInstall { get; init; } + public string? StagingPath { get; init; } +} + +public sealed class OmniNxStatus +{ + public bool IsInstalled { get; init; } + public OmniNxVariant Variant { get; init; } + public string Version { get; init; } = string.Empty; +} + +public sealed class ReleaseInfo +{ + public required string TagName { get; init; } + public required string Name { get; init; } + public required IReadOnlyList Assets { get; init; } +} + +public sealed class ReleaseAsset +{ + public required string Name { get; init; } + public required string DownloadUrl { get; init; } + public long Size { get; init; } +} + +public sealed class InstallProgressReport +{ + public required string Message { get; init; } + public int? Percent { get; init; } + public bool IsError { get; init; } +} diff --git a/desktop/OmniNX.DesktopInstaller/OmniNX.DesktopInstaller.csproj b/desktop/OmniNX.DesktopInstaller/OmniNX.DesktopInstaller.csproj new file mode 100644 index 0000000..0ca6f32 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/OmniNX.DesktopInstaller.csproj @@ -0,0 +1,25 @@ + + + + WinExe + net8.0-windows + true + enable + enable + app.manifest + Assets\app.ico + OmniNX-Desktop-Installer + OmniNX.DesktopInstaller + 1.0.0 + OmniNX + PC-based OmniNX installer for mass deployments and modchip installers + win-x64;win-arm64 + true + false + + + + + + + diff --git a/desktop/OmniNX.DesktopInstaller/Program.cs b/desktop/OmniNX.DesktopInstaller/Program.cs new file mode 100644 index 0000000..c101f6e --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Program.cs @@ -0,0 +1,11 @@ +namespace OmniNX.DesktopInstaller; + +static class Program +{ + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/BackupService.cs b/desktop/OmniNX.DesktopInstaller/Services/BackupService.cs new file mode 100644 index 0000000..f390f2d --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/BackupService.cs @@ -0,0 +1,118 @@ +namespace OmniNX.DesktopInstaller.Services; + +public sealed class BackupService +{ + private const string TempBackup = @"temp_backup"; + private const string DbiConfig = @"switch\DBI\dbi.config"; + private const string ProdKeys = @"switch\prod.keys"; + private const string CyberFoilDir = @"switch\CyberFoil"; + private const string CheckpointDir = @"switch\Checkpoint"; + + private const string HorizonOcConfig = @"config\horizon-oc\config.ini"; + private const string HorizonOcBackupDir = @".omninx_oc_update"; + + public void BackupUserData(string driveRoot, IProgress? progress = null) + { + var backupRoot = PathUtils.ToDrivePath(driveRoot, TempBackup); + PathUtils.EnsureDirectory(backupRoot); + progress?.Report("Backing up CyberFoil, Checkpoint, DBI (dbi.config), prod.keys..."); + + CopyIfExists(PathUtils.ToDrivePath(driveRoot, DbiConfig), + Path.Combine(backupRoot, "dbi.config")); + + CopyIfExists(PathUtils.ToDrivePath(driveRoot, ProdKeys), + Path.Combine(backupRoot, "prod.keys")); + + var cyberfoil = PathUtils.ToDrivePath(driveRoot, CyberFoilDir); + if (Directory.Exists(cyberfoil)) + { + var dst = Path.Combine(backupRoot, "CyberFoil"); + PathUtils.EnsureDirectory(dst); + CopyIfExists(Path.Combine(cyberfoil, "cyberfoil.nro"), Path.Combine(dst, "cyberfoil.nro")); + CopyIfExists(Path.Combine(cyberfoil, "config.json"), Path.Combine(dst, "config.json")); + var remotes = Path.Combine(cyberfoil, "remotes"); + if (Directory.Exists(remotes)) + PathUtils.CopyDirectory(remotes, Path.Combine(dst, "remotes")); + } + + var checkpoint = PathUtils.ToDrivePath(driveRoot, CheckpointDir); + if (Directory.Exists(checkpoint)) + PathUtils.CopyDirectory(checkpoint, Path.Combine(backupRoot, "Checkpoint")); + } + + public void RestoreUserData(string driveRoot, IProgress? progress = null) + { + var backupRoot = PathUtils.ToDrivePath(driveRoot, TempBackup); + if (!Directory.Exists(backupRoot)) + return; + + progress?.Report("Restoring CyberFoil, Checkpoint, DBI (dbi.config), prod.keys..."); + PathUtils.EnsureDirectory(PathUtils.ToDrivePath(driveRoot, @"switch")); + + CopyIfExists(Path.Combine(backupRoot, "dbi.config"), + PathUtils.ToDrivePath(driveRoot, DbiConfig)); + + CopyIfExists(Path.Combine(backupRoot, "prod.keys"), + PathUtils.ToDrivePath(driveRoot, ProdKeys)); + + var cyberBackup = Path.Combine(backupRoot, "CyberFoil"); + if (Directory.Exists(cyberBackup)) + { + var dst = PathUtils.ToDrivePath(driveRoot, CyberFoilDir); + PathUtils.EnsureDirectory(dst); + CopyIfExists(Path.Combine(cyberBackup, "cyberfoil.nro"), Path.Combine(dst, "cyberfoil.nro")); + CopyIfExists(Path.Combine(cyberBackup, "config.json"), Path.Combine(dst, "config.json")); + var remotes = Path.Combine(cyberBackup, "remotes"); + if (Directory.Exists(remotes)) + PathUtils.CopyDirectory(remotes, Path.Combine(dst, "remotes")); + } + + var checkpointBackup = Path.Combine(backupRoot, "Checkpoint"); + if (Directory.Exists(checkpointBackup)) + PathUtils.CopyDirectory(checkpointBackup, PathUtils.ToDrivePath(driveRoot, @"switch")); + } + + public void CleanupBackup(string driveRoot) + { + var backupRoot = PathUtils.ToDrivePath(driveRoot, TempBackup); + if (Directory.Exists(backupRoot)) + PathUtils.DeletePath(backupRoot); + } + + public void BackupHorizonOcConfig(string driveRoot, IProgress? progress = null) + { + var src = PathUtils.ToDrivePath(driveRoot, HorizonOcConfig); + if (!File.Exists(src)) + return; + + progress?.Report("HorizonOC: backing up config.ini..."); + var dstDir = PathUtils.ToDrivePath(driveRoot, HorizonOcBackupDir); + PathUtils.EnsureDirectory(dstDir); + PathUtils.CopyFile(src, Path.Combine(dstDir, "config.ini")); + } + + public void RestoreHorizonOcConfig(string driveRoot, IProgress? progress = null) + { + var src = Path.Combine(PathUtils.ToDrivePath(driveRoot, HorizonOcBackupDir), "config.ini"); + if (!File.Exists(src)) + return; + + progress?.Report("HorizonOC: restoring config.ini..."); + PathUtils.EnsureDirectory(PathUtils.ToDrivePath(driveRoot, @"config\horizon-oc")); + PathUtils.CopyFile(src, PathUtils.ToDrivePath(driveRoot, HorizonOcConfig)); + } + + public void CleanupHorizonOcBackup(string driveRoot) + { + var dir = PathUtils.ToDrivePath(driveRoot, HorizonOcBackupDir); + if (Directory.Exists(dir)) + PathUtils.DeletePath(dir); + } + + private static void CopyIfExists(string src, string dst) + { + if (!File.Exists(src)) + return; + PathUtils.CopyFile(src, dst); + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/ConfigIniService.cs b/desktop/OmniNX.DesktopInstaller/Services/ConfigIniService.cs new file mode 100644 index 0000000..c821765 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/ConfigIniService.cs @@ -0,0 +1,132 @@ +namespace OmniNX.DesktopInstaller.Services; + +public sealed class ConfigIniService +{ + private const string ConfigIniRelative = @"config\omninx\config.ini"; + private const string LegacyRamConfigRelative = @"config\omninx\ram_config.ini"; + + public bool TryReadBool(string driveRoot, string section, string key, out bool value) + { + value = false; + var path = PathUtils.ToDrivePath(driveRoot, ConfigIniRelative); + if (!File.Exists(path)) + return false; + + var inSection = false; + foreach (var rawLine in File.ReadLines(path)) + { + var line = rawLine.Trim(); + if (line.Length == 0) + continue; + + if (line.StartsWith('[') && line.EndsWith(']')) + { + inSection = line[1..^1].Equals(section, StringComparison.Ordinal); + continue; + } + + if (!inSection) + continue; + + var eq = line.IndexOf('='); + if (eq <= 0) + continue; + + var k = line[..eq].Trim(); + if (!k.Equals(key, StringComparison.Ordinal)) + continue; + + var val = line[(eq + 1)..].Trim(); + if (val == "1") + { + value = true; + return true; + } + + if (val == "0") + { + value = false; + return true; + } + + return false; + } + + return false; + } + + public void MigrateLegacyRamConfig(string driveRoot) + { + var legacy = PathUtils.ToDrivePath(driveRoot, LegacyRamConfigRelative); + if (!File.Exists(legacy)) + return; + + if (TryReadLegacyBool(legacy, "Ram", "8gb", out var use8Gb)) + { + var havePro = TryReadBool(driveRoot, "HekateAms", "pro", out var usePro); + WriteExtras(driveRoot, needRam: true, use8Gb, needHekateAms: havePro, havePro ? usePro : false); + } + + PathUtils.DeletePath(legacy); + } + + public void WriteExtras(string driveRoot, bool needRam, bool use8Gb, bool needHekateAms, bool usePro) + { + var path = PathUtils.ToDrivePath(driveRoot, ConfigIniRelative); + PathUtils.EnsureDirectory(Path.GetDirectoryName(path)!); + + using var writer = new StreamWriter(path, append: false); + if (needRam) + { + writer.WriteLine("[Ram]"); + writer.WriteLine($"8gb={(use8Gb ? 1 : 0)}"); + writer.WriteLine(); + } + + if (needHekateAms) + { + writer.WriteLine("[HekateAms]"); + writer.WriteLine($"pro={(usePro ? 1 : 0)}"); + } + } + + private static bool TryReadLegacyBool(string path, string section, string key, out bool value) + { + value = false; + var inSection = false; + foreach (var rawLine in File.ReadLines(path)) + { + var line = rawLine.Trim(); + if (line.StartsWith('[') && line.EndsWith(']')) + { + inSection = line[1..^1].Equals(section, StringComparison.Ordinal); + continue; + } + + if (!inSection) + continue; + + var eq = line.IndexOf('='); + if (eq <= 0) + continue; + + if (!line[..eq].Trim().Equals(key, StringComparison.Ordinal)) + continue; + + var val = line[(eq + 1)..].Trim(); + if (val == "1") + { + value = true; + return true; + } + + if (val == "0") + { + value = false; + return true; + } + } + + return false; + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/DeletionService.cs b/desktop/OmniNX.DesktopInstaller/Services/DeletionService.cs new file mode 100644 index 0000000..5ea76f4 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/DeletionService.cs @@ -0,0 +1,187 @@ +using CleanLists = OmniNX.DesktopInstaller.Data.DeletionLists.Clean; +using UpdateLists = OmniNX.DesktopInstaller.Data.DeletionLists.Update; + +namespace OmniNX.DesktopInstaller.Services; + +public sealed class DeletionService +{ + private readonly PreserveService _preserve = new(); + + public void RunCleanDeletion(string driveRoot, IProgress? progress = null) + { + DeleteGroup(driveRoot, "atmosphere", CleanLists.clean_atmosphere_dirs_to_delete, progress); + DeleteGroup(driveRoot, "bootloader", CleanLists.clean_bootloader_dirs_to_delete, progress); + DeleteFiles(driveRoot, CleanLists.clean_bootloader_files_to_delete, progress); + DeleteGroup(driveRoot, "config", CleanLists.clean_config_dirs_to_delete, progress); + DeleteGroup(driveRoot, "switch", CleanLists.clean_switch_dirs_to_delete, progress); + DeleteFiles(driveRoot, CleanLists.clean_root_files_to_delete, progress); + DeleteGroup(driveRoot, "misc", CleanLists.clean_misc_dirs_to_delete, progress); + DeleteFiles(driveRoot, CleanLists.clean_misc_files_to_delete, progress); + DeleteFiles(driveRoot, CleanLists.old_version_files_to_delete, progress); + PathUtils.EnsureDirectory(PathUtils.ToDrivePath(driveRoot, @"switch")); + } + + public void RunUpdateDeletion(string driveRoot, IProgress? progress = null) + { + _preserve.Load(driveRoot, progress); + + DeleteGroup(driveRoot, "atmosphere", UpdateLists.atmosphere_dirs_to_delete, progress, _preserve); + DeleteGroup(driveRoot, "atmosphere/contents", UpdateLists.atmosphere_contents_dirs_to_delete, progress, _preserve); + DeleteFiles(driveRoot, UpdateLists.atmosphere_files_to_delete, progress, _preserve); + DeleteGroup(driveRoot, "bootloader", UpdateLists.bootloader_dirs_to_delete, progress, _preserve); + DeleteFiles(driveRoot, UpdateLists.bootloader_files_to_delete, progress, _preserve); + DeleteGroup(driveRoot, "config", UpdateLists.config_dirs_to_delete, progress, _preserve); + DeleteGroup(driveRoot, "switch", UpdateLists.switch_dirs_to_delete, progress, _preserve); + DeleteFiles(driveRoot, UpdateLists.switch_files_to_delete, progress, _preserve); + DeleteFiles(driveRoot, UpdateLists.root_files_to_delete, progress, _preserve); + DeleteGroup(driveRoot, "misc", UpdateLists.misc_dirs_to_delete, progress, _preserve); + DeleteFiles(driveRoot, UpdateLists.misc_files_to_delete, progress, _preserve); + + _preserve.Clear(); + } + + public void RunPostRestoreCleanup(string driveRoot) + { + DeleteGroup(driveRoot, "switch (post-restore)", CleanLists.clean_post_restore_dirs_to_delete, null); + } + + private static void DeleteGroup( + string driveRoot, + string label, + string[] paths, + IProgress? progress, + PreserveService? preserve = null) + { + if (paths.Length == 0) + return; + + progress?.Report($"Deleting {label}..."); + foreach (var rel in paths) + { + var full = PathUtils.ToDrivePath(driveRoot, rel); + if (!PathUtils.PathExists(full)) + continue; + if (preserve?.IsBlocked(full, driveRoot) == true) + { + progress?.Report($" Skipped (preserve): {rel}"); + continue; + } + + progress?.Report($" Delete: {rel}"); + PathUtils.DeletePath(full); + } + } + + private static void DeleteFiles( + string driveRoot, + string[] paths, + IProgress? progress, + PreserveService? preserve = null) + { + foreach (var rel in paths) + { + var full = PathUtils.ToDrivePath(driveRoot, rel); + if (!File.Exists(full)) + continue; + if (preserve?.IsBlocked(full, driveRoot) == true) + { + progress?.Report($" Skipped (preserve): {rel}"); + continue; + } + + progress?.Report($" Delete: {rel}"); + PathUtils.DeletePath(full); + } + } +} + +public sealed class PreserveService +{ + private readonly List _paths = new(); + + public void Load(string driveRoot, IProgress? progress = null) + { + _paths.Clear(); + var iniPath = PathUtils.ToDrivePath(driveRoot, @"config\omninx\preserve.ini"); + if (!File.Exists(iniPath)) + return; + + var inSection = false; + foreach (var rawLine in File.ReadLines(iniPath)) + { + var line = rawLine.Trim(); + if (line.StartsWith('[') && line.EndsWith(']')) + { + inSection = line[1..^1].Equals("Preserve", StringComparison.Ordinal); + continue; + } + + if (!inSection || !line.Contains('=')) + continue; + + var eq = line.IndexOf('='); + var key = line[..eq].Trim().Trim('\'', '"'); + var val = line[(eq + 1)..].Trim(); + if (!IsTruthy(val)) + continue; + + var normalized = NormalizePreservePath(key, driveRoot); + if (normalized is null) + continue; + + if (!_paths.Contains(normalized, StringComparer.OrdinalIgnoreCase)) + _paths.Add(normalized); + } + + if (_paths.Count > 0) + progress?.Report($"preserve.ini: {_paths.Count} path(s) excluded from update cleanup."); + } + + public bool IsBlocked(string deleteFullPath, string driveRoot) + { + if (_paths.Count == 0) + return false; + + var normalizedDelete = Path.GetFullPath(deleteFullPath).TrimEnd(Path.DirectorySeparatorChar); + foreach (var preserve in _paths) + { + var normalizedPreserve = Path.GetFullPath(preserve).TrimEnd(Path.DirectorySeparatorChar); + if (normalizedDelete.Equals(normalizedPreserve, StringComparison.OrdinalIgnoreCase)) + return true; + if (IsPrefix(normalizedPreserve, normalizedDelete)) + return true; + if (IsPrefix(normalizedDelete, normalizedPreserve)) + return true; + } + + return false; + } + + public void Clear() => _paths.Clear(); + + private static string? NormalizePreservePath(string key, string driveRoot) + { + var path = key.Replace("sd:/", string.Empty).Replace("sd:\\", string.Empty) + .Replace('/', Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar); + if (path.Length < 2) + return null; + + return PathUtils.ToDrivePath(driveRoot, path); + } + + private static bool IsPrefix(string prefix, string path) + { + if (!path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + return false; + return path.Length == prefix.Length || + path[prefix.Length] == Path.DirectorySeparatorChar; + } + + private static bool IsTruthy(string val) + { + if (string.IsNullOrEmpty(val)) + return false; + var c = char.ToUpperInvariant(val[0]); + return c is '1' or 'T' or 'Y'; + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/ExtrasService.cs b/desktop/OmniNX.DesktopInstaller/Services/ExtrasService.cs new file mode 100644 index 0000000..cdd8841 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/ExtrasService.cs @@ -0,0 +1,97 @@ +namespace OmniNX.DesktopInstaller.Services; + +public sealed class ExtrasService +{ + private readonly ConfigIniService _config = new(); + + public (bool needRam, bool needHekateAms) DetectExtras(string driveRoot) + { + var extras = PathUtils.ToDrivePath(driveRoot, "extras"); + var needRam = File.Exists(Path.Combine(extras, "hekate_8gb.bin")) || + File.Exists(Path.Combine(extras, "hekate-pro_8gb.bin")); + var needHekateAms = Directory.Exists(Path.Combine(extras, "hekate-ams-pro")); + return (needRam, needHekateAms); + } + + public void ApplyExtras( + string driveRoot, + bool use8GbRam, + bool useHekateAmsPro, + IProgress? progress = null) + { + _config.MigrateLegacyRamConfig(driveRoot); + + var extras = PathUtils.ToDrivePath(driveRoot, "extras"); + if (!Directory.Exists(extras)) + return; + + var (needRam, needHekateAms) = DetectExtras(driveRoot); + if (!needRam && !needHekateAms) + return; + + progress?.Report("Saving RAM / Hekate-AMS selection to config.ini..."); + _config.WriteExtras(driveRoot, needRam, use8GbRam, needHekateAms, useHekateAmsPro); + + if (useHekateAmsPro && needHekateAms) + { + progress?.Report("Installing Hekate-Pro / Atmosphere-Pro..."); + ApplyHekateAmsPro(extras, driveRoot, progress); + } + else if (needHekateAms) + { + progress?.Report("Keeping standard Hekate / Atmosphere."); + } + + if (needRam) + { + progress?.Report("Applying RAM payload setting..."); + Apply8GbPayload(extras, driveRoot, use8GbRam, useHekateAmsPro); + } + + progress?.Report("Removing extras/..."); + PathUtils.DeletePath(extras); + } + + private static void ApplyHekateAmsPro(string extrasDir, string driveRoot, IProgress? progress) + { + var src = Path.Combine(extrasDir, "hekate-ams-pro"); + if (!Directory.Exists(src)) + return; + + foreach (var entry in Directory.GetFileSystemEntries(src)) + { + var name = Path.GetFileName(entry); + var dst = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), name); + if (Directory.Exists(entry)) + PathUtils.CopyDirectory(entry, dst); + else + PathUtils.CopyFile(entry, dst); + progress?.Report($" Copied: {name}"); + } + } + + private static void Apply8GbPayload(string extrasDir, string driveRoot, bool use8Gb, bool usePro) + { + var hekate8 = Path.Combine(extrasDir, "hekate_8gb.bin"); + var hekatePro8 = Path.Combine(extrasDir, "hekate-pro_8gb.bin"); + + if (!use8Gb) + { + if (File.Exists(hekate8)) File.Delete(hekate8); + if (File.Exists(hekatePro8)) File.Delete(hekatePro8); + return; + } + + var src = usePro ? hekatePro8 : hekate8; + if (!File.Exists(src)) + return; + + var payloadDst = PathUtils.ToDrivePath(driveRoot, "payload.bin"); + var updateDst = PathUtils.ToDrivePath(driveRoot, @"bootloader\update.bin"); + PathUtils.CopyFile(src, payloadDst); + PathUtils.CopyFile(src, updateDst); + + if (File.Exists(hekate8)) File.Delete(hekate8); + if (File.Exists(hekatePro8)) File.Delete(hekatePro8); + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/GiteaClient.cs b/desktop/OmniNX.DesktopInstaller/Services/GiteaClient.cs new file mode 100644 index 0000000..4d9f147 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/GiteaClient.cs @@ -0,0 +1,100 @@ +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using OmniNX.DesktopInstaller.Models; + +namespace OmniNX.DesktopInstaller.Services; + +public sealed class GiteaClient +{ + public const string DefaultBaseUrl = "https://git.niklascfw.de"; + private readonly HttpClient _http; + private readonly string _baseUrl; + + public GiteaClient(HttpClient? http = null, string? baseUrl = null) + { + _http = http ?? new HttpClient(); + _http.DefaultRequestHeaders.UserAgent.ParseAdd("OmniNX-Desktop-Installer/1.0"); + _baseUrl = (baseUrl ?? DefaultBaseUrl).TrimEnd('/'); + } + + public async Task GetLatestReleaseAsync(CancellationToken cancellationToken = default) + { + var url = $"{_baseUrl}/api/v1/repos/OmniNX/OmniNX/releases/latest"; + var response = await _http.GetFromJsonAsync(url, cancellationToken) + ?? throw new InvalidOperationException("Empty release response from Gitea."); + + return MapRelease(response); + } + + public async Task DownloadAssetAsync( + ReleaseAsset asset, + string destinationPath, + IProgress<(long downloaded, long total)>? progress = null, + CancellationToken cancellationToken = default) + { + PathUtils.EnsureDirectory(Path.GetDirectoryName(destinationPath)!); + + using var response = await _http.GetAsync(asset.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + response.EnsureSuccessStatusCode(); + + var total = response.Content.Headers.ContentLength ?? asset.Size; + await using var input = await response.Content.ReadAsStreamAsync(cancellationToken); + await using var output = File.Create(destinationPath); + + var buffer = new byte[81920]; + long downloaded = 0; + int read; + while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0) + { + await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken); + downloaded += read; + progress?.Report((downloaded, total)); + } + + return destinationPath; + } + + public ReleaseAsset? FindPackAsset(ReleaseInfo release, OmniNxVariant variant) + { + var prefix = VariantHelper.GetZipAssetPrefix(variant); + return release.Assets.FirstOrDefault(a => + a.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && + a.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)); + } + + private static ReleaseInfo MapRelease(GiteaReleaseDto dto) => new() + { + TagName = dto.TagName ?? string.Empty, + Name = dto.Name ?? dto.TagName ?? string.Empty, + Assets = dto.Assets?.Select(a => new ReleaseAsset + { + Name = a.Name ?? string.Empty, + DownloadUrl = a.BrowserDownloadUrl ?? string.Empty, + Size = a.Size, + }).ToList() ?? [], + }; + + private sealed class GiteaReleaseDto + { + [JsonPropertyName("tag_name")] + public string? TagName { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("assets")] + public List? Assets { get; set; } + } + + private sealed class GiteaAssetDto + { + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("browser_download_url")] + public string? BrowserDownloadUrl { get; set; } + + [JsonPropertyName("size")] + public long Size { get; set; } + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/InstallEngine.cs b/desktop/OmniNX.DesktopInstaller/Services/InstallEngine.cs new file mode 100644 index 0000000..175a037 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/InstallEngine.cs @@ -0,0 +1,186 @@ +using OmniNX.DesktopInstaller.Models; + +namespace OmniNX.DesktopInstaller.Services; + +public sealed class InstallEngine +{ + private readonly DeletionService _deletion = new(); + private readonly BackupService _backup = new(); + private readonly ExtrasService _extras = new(); + + public async Task RunAsync( + InstallOptions options, + IProgress progress, + CancellationToken cancellationToken = default) + { + var driveRoot = PathUtils.NormalizeDriveRoot(options.DriveRoot); + var staging = options.StagingPath + ?? throw new InvalidOperationException("Staging path is required."); + + if (!Directory.Exists(staging)) + throw new DirectoryNotFoundException($"Staging path not found: {staging}"); + + Report(progress, $"Install mode: {options.Mode}"); + Report(progress, $"Pack variant: {VariantHelper.GetDisplayName(options.Variant)}"); + + if (options.Mode == InstallMode.Update) + { + await RunUpdateAsync(driveRoot, staging, options, progress, cancellationToken); + } + else + { + await RunCleanAsync(driveRoot, staging, options, progress, cancellationToken); + } + + Report(progress, options.Mode == InstallMode.Update + ? "Update completed successfully." + : "Installation completed successfully."); + } + + private async Task RunUpdateAsync( + string driveRoot, + string staging, + InstallOptions options, + IProgress progress, + CancellationToken cancellationToken) + { + if (options.Variant == OmniNxVariant.Oc) + { + Report(progress, "HorizonOC: backing up config.ini..."); + _backup.BackupHorizonOcConfig(driveRoot, StringProgress(progress)); + } + + Report(progress, "Step 1: Cleanup..."); + await Task.Run(() => _deletion.RunUpdateDeletion(driveRoot, StringProgress(progress)), cancellationToken); + + Report(progress, "Step 2: Copying files..."); + await Task.Run(() => CopyPackFiles(staging, driveRoot, options.Variant, progress), cancellationToken); + + Report(progress, "Applying RAM / Hekate-AMS extras..."); + await Task.Run(() => _extras.ApplyExtras(driveRoot, options.Use8GbRam, options.UseHekateAmsPro, + StringProgress(progress)), cancellationToken); + + if (options.Variant == OmniNxVariant.Oc) + { + Report(progress, "HorizonOC: restoring config.ini..."); + await Task.Run(() => + { + _backup.RestoreHorizonOcConfig(driveRoot, StringProgress(progress)); + _backup.CleanupHorizonOcBackup(driveRoot); + }, cancellationToken); + } + } + + private async Task RunCleanAsync( + string driveRoot, + string staging, + InstallOptions options, + IProgress progress, + CancellationToken cancellationToken) + { + Report(progress, "Step 1: Backing up user data..."); + await Task.Run(() => _backup.BackupUserData(driveRoot, StringProgress(progress)), cancellationToken); + + Report(progress, "Step 2: Cleaning old installation..."); + await Task.Run(() => _deletion.RunCleanDeletion(driveRoot, StringProgress(progress)), cancellationToken); + + Report(progress, "Step 3: Restoring user data..."); + await Task.Run(() => + { + _backup.RestoreUserData(driveRoot, StringProgress(progress)); + _deletion.RunPostRestoreCleanup(driveRoot); + _backup.CleanupBackup(driveRoot); + }, cancellationToken); + + Report(progress, "Step 4: Copying files..."); + await Task.Run(() => CopyPackFiles(staging, driveRoot, options.Variant, progress), cancellationToken); + + Report(progress, "Applying RAM / Hekate-AMS extras..."); + await Task.Run(() => _extras.ApplyExtras(driveRoot, options.Use8GbRam, options.UseHekateAmsPro, + StringProgress(progress)), cancellationToken); + } + + private static void CopyPackFiles( + string staging, + string driveRoot, + OmniNxVariant variant, + IProgress progress) + { + CopyFolderIfExists(staging, driveRoot, "atmosphere", progress); + CopyFolderIfExists(staging, driveRoot, "bootloader", progress); + CopyFolderIfExists(staging, driveRoot, "config", progress); + CopyFolderIfExists(staging, driveRoot, "switch", progress); + CopyFolderIfExists(staging, driveRoot, "themes", progress); + CopyFolderIfExists(staging, driveRoot, "warmboot_mariko", progress); + + if (variant == OmniNxVariant.Oc) + CopyFolderIfExists(staging, driveRoot, "SaltySD", progress); + + CopyFolderIfExists(staging, driveRoot, "extras", progress); + CopyRootFiles(staging, driveRoot, progress); + } + + private static void CopyFolderIfExists( + string staging, + string driveRoot, + string folder, + IProgress progress) + { + var src = Path.Combine(staging, folder); + if (!Directory.Exists(src)) + return; + + var dst = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), folder); + Report(progress, $"Copying {folder}/..."); + PathUtils.CopyDirectory(src, dst, new Progress<(int current, int total, string file)>(copyProgress => + { + var (current, total, file) = copyProgress; + var pct = total > 0 ? current * 100 / total : 0; + progress.Report(new InstallProgressReport + { + Message = $" {folder}/ {file}", + Percent = pct, + }); + })); + } + + private static void CopyRootFiles( + string staging, + string driveRoot, + IProgress progress) + { + Report(progress, "Copying root files..."); + var skipDirs = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "atmosphere", "bootloader", "config", "switch", "themes", + "warmboot_mariko", "SaltySD", "extras", + }; + + foreach (var file in Directory.GetFiles(staging)) + { + var name = Path.GetFileName(file); + if (name.StartsWith('.')) + continue; + + var dst = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), name); + PathUtils.CopyFile(file, dst); + Report(progress, $" {name}"); + } + + foreach (var dir in Directory.GetDirectories(staging)) + { + var name = Path.GetFileName(dir); + if (skipDirs.Contains(name)) + continue; + Report(progress, $"Skipping unexpected directory at pack root: {name}"); + } + } + + private static void Report(IProgress progress, string message, bool isError = false) + { + progress.Report(new InstallProgressReport { Message = message, IsError = isError }); + } + + private static IProgress StringProgress(IProgress progress) => + new Progress(msg => Report(progress, msg)); +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/PathUtils.cs b/desktop/OmniNX.DesktopInstaller/Services/PathUtils.cs new file mode 100644 index 0000000..bf992fe --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/PathUtils.cs @@ -0,0 +1,133 @@ +using OmniNX.DesktopInstaller.Models; + +namespace OmniNX.DesktopInstaller.Services; + +public static class VariantHelper +{ + public static string GetDisplayName(OmniNxVariant variant) => variant switch + { + OmniNxVariant.Light => "Light", + OmniNxVariant.Standard => "Standard", + OmniNxVariant.Oc => "OC", + _ => "None", + }; + + public static string GetStagingFolderName(OmniNxVariant variant) => variant switch + { + OmniNxVariant.Light => "OmniNX Light", + OmniNxVariant.Standard => "OmniNX Standard", + OmniNxVariant.Oc => "OmniNX OC", + _ => string.Empty, + }; + + public static string GetZipAssetPrefix(OmniNxVariant variant) => variant switch + { + OmniNxVariant.Light => "OmniNX-Light", + OmniNxVariant.Standard => "OmniNX-Standard", + OmniNxVariant.Oc => "OmniNX-OC", + _ => string.Empty, + }; + + public static OmniNxVariant ParsePackName(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return OmniNxVariant.None; + + return value.Trim() switch + { + "Light" => OmniNxVariant.Light, + "Standard" => OmniNxVariant.Standard, + "OC" => OmniNxVariant.Oc, + _ => OmniNxVariant.None, + }; + } +} + +public static class PathUtils +{ + public static string NormalizeDriveRoot(string driveRoot) + { + var root = driveRoot.Trim(); + if (!root.EndsWith(Path.DirectorySeparatorChar)) + root += Path.DirectorySeparatorChar; + return root; + } + + public static string ToDrivePath(string driveRoot, string sdRelative) + { + var rel = sdRelative.Replace("sd:/", string.Empty).Replace("sd:\\", string.Empty) + .Replace('/', Path.DirectorySeparatorChar); + return Path.Combine(NormalizeDriveRoot(driveRoot), rel); + } + + public static string RelativeFromDrive(string driveRoot, string fullPath) + { + var root = NormalizeDriveRoot(driveRoot); + if (fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + return fullPath[root.Length..]; + return fullPath; + } + + public static bool PathExists(string path) + { + return Directory.Exists(path) || File.Exists(path); + } + + public static void EnsureDirectory(string path) + { + Directory.CreateDirectory(path); + } + + public static void CopyFile(string src, string dst, bool overwrite = true) + { + var dir = Path.GetDirectoryName(dst); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + File.Copy(src, dst, overwrite); + } + + public static void CopyDirectory(string src, string dst, IProgress<(int current, int total, string file)>? progress = null) + { + if (!Directory.Exists(src)) + return; + + var files = Directory.GetFiles(src, "*", SearchOption.AllDirectories); + var total = files.Length; + var current = 0; + + foreach (var file in files) + { + var rel = Path.GetRelativePath(src, file); + var target = Path.Combine(dst, rel); + CopyFile(file, target); + current++; + progress?.Report((current, total, rel)); + } + } + + public static void DeletePath(string path) + { + if (File.Exists(path)) + { + File.SetAttributes(path, FileAttributes.Normal); + File.Delete(path); + return; + } + + if (Directory.Exists(path)) + Directory.Delete(path, recursive: true); + } + + public static int CountItemsRecursive(string path) + { + if (File.Exists(path)) + return 1; + if (!Directory.Exists(path)) + return 0; + + var count = 1; + foreach (var entry in Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories)) + count++; + return count; + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/VersionDetectService.cs b/desktop/OmniNX.DesktopInstaller/Services/VersionDetectService.cs new file mode 100644 index 0000000..760ec04 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/VersionDetectService.cs @@ -0,0 +1,79 @@ +using OmniNX.DesktopInstaller.Models; + +namespace OmniNX.DesktopInstaller.Services; + +public sealed class VersionDetectService +{ + private const string ManifestRelative = @"config\omninx\manifest.ini"; + + public OmniNxStatus DetectInstallation(string driveRoot) + { + var manifest = PathUtils.ToDrivePath(driveRoot, ManifestRelative); + if (!File.Exists(manifest)) + return new OmniNxStatus { IsInstalled = false, Variant = OmniNxVariant.None }; + + var variant = OmniNxVariant.None; + var version = string.Empty; + var inSection = false; + + foreach (var rawLine in File.ReadLines(manifest)) + { + var line = rawLine.Trim(); + if (line.StartsWith('[') && line.EndsWith(']')) + { + inSection = line[1..^1].Equals("OmniNX", StringComparison.Ordinal); + continue; + } + + if (!inSection || !line.Contains('=')) + continue; + + var eq = line.IndexOf('='); + var key = line[..eq].Trim(); + var val = line[(eq + 1)..].Trim(); + + if (key.Equals("current_pack", StringComparison.Ordinal)) + variant = VariantHelper.ParsePackName(val); + else if (key.Equals("version", StringComparison.Ordinal)) + version = val; + } + + return new OmniNxStatus + { + IsInstalled = variant != OmniNxVariant.None, + Variant = variant, + Version = version, + }; + } + + public InstallMode DetermineMode(OmniNxStatus status, bool forceClean) + { + if (forceClean) + return InstallMode.Clean; + return status.IsInstalled ? InstallMode.Update : InstallMode.Clean; + } +} + +public static class DriveService +{ + public sealed record DriveItem(DriveInfo Drive) + { + public override string ToString() => FormatDriveLabel(Drive); + } + + public static IReadOnlyList GetCandidateDrives() + { + return DriveInfo.GetDrives() + .Where(d => d.IsReady && d.DriveType == DriveType.Removable) + .OrderBy(d => d.Name) + .Select(d => new DriveItem(d)) + .ToList(); + } + + public static string FormatDriveLabel(DriveInfo drive) + { + var label = string.IsNullOrWhiteSpace(drive.VolumeLabel) ? "No Label" : drive.VolumeLabel; + var sizeGb = drive.TotalSize / (1024.0 * 1024 * 1024); + return $"{drive.Name} [{label}] ({sizeGb:F1} GB, {drive.DriveFormat})"; + } +} diff --git a/desktop/OmniNX.DesktopInstaller/Services/ZipExtractor.cs b/desktop/OmniNX.DesktopInstaller/Services/ZipExtractor.cs new file mode 100644 index 0000000..184ba3d --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/Services/ZipExtractor.cs @@ -0,0 +1,40 @@ +using System.IO.Compression; +using OmniNX.DesktopInstaller.Models; + +namespace OmniNX.DesktopInstaller.Services; + +public static class ZipExtractor +{ + public static string ExtractPack(string zipPath, string extractRoot, IProgress? progress = null) + { + PathUtils.EnsureDirectory(extractRoot); + var targetDir = Path.Combine(extractRoot, Path.GetFileNameWithoutExtension(zipPath)); + if (Directory.Exists(targetDir)) + Directory.Delete(targetDir, recursive: true); + + progress?.Report($"Extracting {Path.GetFileName(zipPath)}..."); + ZipFile.ExtractToDirectory(zipPath, targetDir, overwriteFiles: true); + return ResolveStagingPath(targetDir); + } + + public static string ResolveStagingPath(string extractRoot) + { + foreach (var variant in new[] { OmniNxVariant.Standard, OmniNxVariant.Light, OmniNxVariant.Oc }) + { + var candidate = Path.Combine(extractRoot, VariantHelper.GetStagingFolderName(variant)); + if (Directory.Exists(candidate)) + return candidate; + } + + if (Directory.Exists(Path.Combine(extractRoot, "atmosphere")) || + Directory.Exists(Path.Combine(extractRoot, "bootloader"))) + return extractRoot; + + var subdirs = Directory.GetDirectories(extractRoot); + if (subdirs.Length == 1) + return ResolveStagingPath(subdirs[0]); + + throw new InvalidOperationException( + "Could not locate OmniNX pack root in extracted archive. Expected 'OmniNX Light', 'OmniNX Standard', or 'OmniNX OC' folder."); + } +} diff --git a/desktop/OmniNX.DesktopInstaller/app.manifest b/desktop/OmniNX.DesktopInstaller/app.manifest new file mode 100644 index 0000000..f280441 --- /dev/null +++ b/desktop/OmniNX.DesktopInstaller/app.manifest @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/tools/generate_deletion_lists.py b/tools/generate_deletion_lists.py new file mode 100644 index 0000000..20eddae --- /dev/null +++ b/tools/generate_deletion_lists.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Generate C# deletion list arrays from payload header files.""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "source" +OUT = ROOT / "desktop" / "OmniNX.DesktopInstaller" / "Data" / "DeletionLists.Generated.cs" + +HEADERS = { + "Clean": SOURCE / "deletion_lists_clean.h", + "Update": SOURCE / "deletion_lists_update.h", +} + + +def parse_arrays(text: str) -> dict[str, list[str]]: + arrays: dict[str, list[str]] = {} + pattern = re.compile( + r"static const char\*\s+(\w+)\[\]\s*=\s*\{([^}]*)\};", + re.MULTILINE | re.DOTALL, + ) + for match in pattern.finditer(text): + name = match.group(1) + body = match.group(2) + paths = [] + for line in body.splitlines(): + line = line.strip() + if not line or line.startswith("//"): + continue + m = re.match(r'"([^"]+)"', line) + if m: + paths.append(m.group(1)) + arrays[name] = paths + return arrays + + +def emit_cs(mode: str, arrays: dict[str, list[str]]) -> str: + lines = [f" public static class {mode}"] + lines.append(" {") + for name, paths in arrays.items(): + lines.append(f" public static readonly string[] {name} =") + lines.append(" {") + for p in paths: + rel = p.replace("sd:/", "").replace("sd:\\", "").replace("/", "\\") + lines.append(f' @"{rel}",') + lines.append(" };") + lines.append("") + lines.append(" }") + return "\n".join(lines) + + +def main() -> None: + sections = [] + for mode, header in HEADERS.items(): + text = header.read_text(encoding="utf-8") + arrays = parse_arrays(text) + sections.append(emit_cs(mode, arrays)) + + OUT.parent.mkdir(parents=True, exist_ok=True) + content = """// +// Generated by tools/generate_deletion_lists.py — do not edit manually. + +namespace OmniNX.DesktopInstaller.Data; + +public static partial class DeletionLists +{ +""" + content += "\n\n".join(sections) + content += "\n}\n" + OUT.write_text(content, encoding="utf-8") + print(f"Wrote {OUT}") + + +if __name__ == "__main__": + main()