PrismLauncher-10.0.5/0000755000175100017510000000000015144136757013774 5ustar runnerrunnerPrismLauncher-10.0.5/.git-blame-ignore-revs0000644000175100017510000000043615144136756020076 0ustar runnerrunner# .git-blame-ignore-revs # tabs -> spaces bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 # (nix) alejandra -> nixfmt 4c81d8c53d09196426568c4a31a4e752ed05397a # reformat codebase 1d468ac35ad88d8c77cc83f25e3704d9bd7df01b # format a part of codebase 5c8481a118c8fefbfe901001d7828eaf6866eac4 PrismLauncher-10.0.5/program_info/0000755000175100017510000000000015144136757016456 5ustar runnerrunnerPrismLauncher-10.0.5/program_info/instance_icons.svg0000644000175100017510000034431715144136757022212 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.logo.svg0000644000175100017510000001615415144136757026362 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.Source.svg0000644000175100017510000001743615144136757026666 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.svg0000644000175100017510000000662615144136757025426 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.logo.source.svg0000644000175100017510000003246715144136757027666 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.logo-darkmode.svg0000644000175100017510000002352615144136757030147 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.bigsur.svg0000644000175100017510000003052515144136757026713 0ustar runnerrunner PrismLauncher-10.0.5/program_info/README.md0000644000175100017510000000031615144136757017735 0ustar runnerrunner# Prism Launcher Program Info This is Prism Launcher's program info which contains information about: - Application name and logo (and branding in general) - Various URLs and API endpoints - Desktop file PrismLauncher-10.0.5/program_info/prismlauncher.icns0000644000175100017510000057302115144136757022220 0ustar runnerrunnericnsöic04ARGB‘ ƒ '¼îóôø÷ôóî½(€¼‡ÿ¾î‡ÿïñ‡ÿññ‡ÿññ‡ÿññ‡ÿññ‡ÿññ‡ÿñí‡ÿﻇÿ½€&ºìƒñì»'ƒƒ‘ÿÿ÷ÿÿƒ øþþýþüûýýþÿù€ÿü€ÿ÷ÕíüþÿÿûÿèûÿüÖ¤™Öæûüþûôíûù¶–“®àïüúýúííüé}ŠÌüÿûøõûúííüåzƒÛóõåæçøùííûä{†ÕØàÞƒæûííúèt^ÐÙ‘\sèúíí÷ø‘FCa}SBøøíÜõúù¿hGe‚¿ùúôÞªïÿùÿò¶Àôÿùÿ婢 Ðìòðñ÷õñðòíу­»³»­‘‘ÿÿ÷ÿÿƒ øþþýýûÿþýþÿù€ÿü€ÿüâ¾ò€ÿûÿèúÿýêÔÀ`xÒûþúôíú÷ÏÏÌÐu‹¼üúííüâb–åÿùȳÔ÷ùííûÞVaÕñöèÚØôøííúÝWeÏÕß༷íøííùáTHƒÌÖ©’¬ï÷íäôõ…85SZ\x®÷õíÜóø÷¹\6=hÌöøòÞªìÿ÷ýï®°ïû÷ÿ쪀 Éêðîïôôïîðë˃­ƒ³­‘‘ÿÿ÷ÿÿƒ øþþýþÿÿþýþÿù€ÿü€ÿó¸¿õ€ÿûÿèúÿüÀtYl†Ãûÿúôíúö\Y}Žga™øûííüÞ?O±úûµikæûííüØ:K×òöã|pâûííûØ;LÎÕßÜ—çùííùÝ:5}ÊÚË«˜éøíäõô},'M‘£½ÎõõíÜóùøµQ0~¢áöøòÞªìÿ÷ýî­Çòø÷ÿ쪀 Éêðîïõòîîðë˃­ƒ³­‘ic11ljPNG  IHDR szzô†IDATxÚí”+ýÅ{…϶mÛ¶kÛ¶mÛ¶mÛXÉ^$™L8øŸ×3µ¹[¹•Y<ëT¨õ«î¥ÿëßU®`Ìn¡ëzoööÀb²p(> ¸¦¥ªªŸ¸’*".úHq ?0@Ê;ü=&÷€Åï–:L 6+5׫úý•µöD8 –¦i­ëœ*8“»¶í„â 5B·¬3¤(ÊÉÂÂBës¸Ýîw˜ºUø‚n.X™§šaÒÎÊHªÝ yÍzñb· Í^g½eY~Ÿ¬ä•åú`iªfh0ßÏžÛ¹§Ú`Ì¡?1îH5LÛ] )Ú »ZdVj¼¶ý8²‡rˆÐ‡ÅKÛ@°ø ÅÖ(rg`eJŒ=TãWÅ„#50þh€íY½=lMšCR êâ!Pm™PÍ^Z¨_  YI’¤n&€¢°¹HSpüÜZL>VÇnyì‘*˜¼«*’ë¶@V•ÖȮվþ ¬bTc¨û×—ôâwC²$u'+Á€À™#;Ðoü—¸&ž¨iæÏ|‚JXt¶ R/솼÷0ò;ÇùV<|D¨#`{x]×BÐq ª*Ê (Á m]2­¾~=Z|„KÃøcÕ0ãDcœ,ÚU"$T Þ mBK¨Cë 8»¼‡AN¯ ÿ…õ PÒSªÀºÙcÐúû—Ðá§7Ñá—70tÐo°KY°’bË…gW[Èi !g4†œZ¾¢eP”Š0´rêP´a€Ž¿½…v?½Š­k"-)çìöKÏvè–ÓéDv^YáK¯oz#xÓêÀo_ÀÂè :Œ@2„ÐW] Ç&Æ“.4bÊJO'Éå"¯×K6›€„¦“WI §G.¶S‰'¿e6DØ +€"ÿnEG¯” óò¨¨¨ˆE¡¨¨(Šm°ßJã3ï§IY÷Ó¸ŒhÏÅùwA‘Z„‚11áóubx8ËfCF(AAÃŽ6-(&´Ó²Äe'ˆK IQ& %§gQòÙ“Ÿ`l¡¤ 'ÄÇEÓÙ|%eÄQ”ˆaPA†â¹.ì¬Öf'”œtÅUW›¥.¿N™2욉®ë´eýJÞ¿ ­_µ”v¯8Eszo¦C«N“âß—HéIwSnÚýtË•· Ês&¸´UQè‘g^¦Z­zÐ7ÝLüãÞæ \N­]²˜¶-:BnWn–\t•ì!ÁqEÓéÆkã¨ö{¯Ò#7ßIª®—ï€ÆÃ…}ú 5…>þâ[@U•KOB\|<;†¢¢£øî‚®Íʧ(þñ ·Ñ šÏÐÇÏßL‚.Û²n  (Š3tÿ?ÝzÛíÔ¢cêÚo=ñôóœ$M+aÕy¨¢ zòÞk©ç¯R«ï¥[¯M €¢‡õ"¶®i²Ò™3gÞx½ÂïñÀÁš¢ÂëvcýÊ%hXõg|õæ+¨ÿaô«; »×å˜ -èXëg|>$%%½GVêÞ½{ây»ý t>YŽh£‘Î ’Û6âà–cTÅoÄ,댞çl¶£<ãJ*M[¶ly×ír9¹À$÷rq$15„ô—™gôâžÒöÍ›? òhÕ²eïÛòó÷{$IU4ÍhR1sQëq»…­ àЪU«>¢ êªyóæ}´kûö¦‡èvøàÁž±Q³g×®¦K,ø˜ˆ®¦ÿëßUK“ñw\CyÍIEND®B`‚ic05gARGBã Dq„‰ŠŠƒ‹ŠŠ‰„rF ˆCÏÿÑF†?ô‘ÿõC„ΓÿÒ ƒ=•ÿ@ƒh•ÿlƒy•ÿ}ƒ•ÿ€ƒ€•ÿ€ƒ€•ÿ€ƒ€•ÿ€ƒ€•ÿ€ƒ€•ÿ€ƒ€•ÿ€ƒ€•ÿ€ƒ€•ÿ€ƒ•ÿ€ƒ•ÿ€ƒ~•ÿ€ƒy•ÿ}ƒh•ÿkƒ<þ”ÿ?ƒÊ“ÿÎ „>ï‘ÿñA†@ÈþÿþËCˆ Cn‰‚nD ãã€ÿŒýûÿˆûþÿû†ûýÿ„ýþÿþ…ýÿþû„ÿûÿƒüýÿóÕìúý„üÿúæƒòþý‚üÿöÌ¢›×ãîúƒüýþóƒõÿü ÿüÖ¦•—ŸÙàÞãøƒüÿóƒòÿ€û ÿé²–—™˜ŸÙàÞç„ûÿóƒñÿ€ûМ–€™–œØßàöüƒûÿóƒóÿûþÕ‡˜š™˜•«Úóäëü€ûøùûûÿóƒñÿúÿ¾s‘˜¡Òûÿÿþüüûùõó÷ûúÿñƒ ñÿúÿ·tyyŽèƒÿúô€óöúúÿñƒñÿúÿ²uzw‘Ûêüÿÿýïàîöõõ÷úúÿñƒñÿùÿ°uzw•ØÔÛðôäÞÞãååäîúùÿñƒñÿùþ°uzw•ØÖÕÖÝÞßâœ}€{³þùÿñƒñÿùþ²uzw”ÛÖÖ×Þßßä•vyt±þùÿñƒïÿøþ¶t|wl³ÜØ×Þáåºlw|t·þøÿïƒïÿøý¿tlUIOŠËÝä×MGSkt¿ýøÿïƒíÿ÷üÔXILMLG[•¨|[IKJGVÔü÷ÿïƒíÿ÷÷ù»[FLMMKKgnoTIDYºù÷÷ÿïƒìÿ€÷þäŠKHMMPlopbJˆäþ€÷ÿíƒæÿö úûÉqGIPlljŒÌûúöÿêƒÕø÷‚ö üõ¸jKh‡Äõü‚ö÷ø×ƒ€ëÿƒö÷ýñÂÊóû÷ƒöÿꀄ½òÿöƒõöûúöƒõöÿòÀ†³çôþ‹ÿþô糉§ÃˉÌËÅ­äã€ÿŒýûÿˆûþÿû†ûýÿ…ý€ÿ„ýÿþ÷„ÿûÿƒüýþùâ¿îÿþƒüÿúæƒòþƒü þùåÓÂdo±òÿƒüþóƒóÿû üûêÓËÍÄk_[vÍüþûÿóƒòÿ€ûýòÙËÌÍÎÄka`m¬çÿ€ûÿóƒñÿúúüêÏËÍÃf]cˆ’“È€úÿñƒñÿúþ͘ÌÑÍÌËÖìÆ|s‘‘¬îûúÿñƒñÿùÿ­Lq®ÏÑéýÿÿùÌ™Ž¤ËÜìûùÿñƒ ñÿùÿ¤PSU‹ñƒÿ çÃÛÝÚéúùÿñƒñÿøÿžPWSt×èüÿÿýïãßÜÜÛèúøÿñƒïÿøÿ›PWS|ÕÐØïôäÞßÙÖÖÕäùøÿïƒïÿ÷þ›PWS{ÕÒÑÒÝÞßàÁ´µ²Ñú÷ÿïƒïÿ÷þžPWSx×ÒÒÓÞßßâÀ²³°Ñú÷ÿïƒíÿ÷þ£PXUT­ØÔÓÞá䯰µ°Óú÷ÿïƒïÿöý®POC9N–VW}|{x‹âøöÿíƒëÿõõø¶N7>??=:AFHp}w†Êöõõÿíƒêÿ€õüá€=:??@GHFOy¨èú€õÿëƒäþô øúÄf9;@GDBmÏøöôþçƒÕõƒô úò²^:Bfµòùƒôö׃€èýƒóôúïúôƒóý瀄½ðýôƒóôùùôƒóôý𼆯äñûü‰ýüûò峉§ÀɉÊÉéäã€ÿŒýûÿˆûþÿû†ûýÿ„ý€ÿþ„ýÿþ÷„ÿûÿƒüþÿîºÂñÿýƒüÿúæƒòþƒü ÿò±o\o„»óÿƒüþóƒóÿû ÿûÁuZ_cuuqˆÅûÿûÿóƒòÿûûüÿ݈\^aacuwxpcÞÿüûûÿóƒñÿúûú¶e\€a]^qtwkhcl¸úûúÿñƒñÿúÿÁJ_ba_[}ÄÌkehhfcÏþúÿñƒñÿùÿ 1CW_l¹ùÿÿú¼reikf¸ÿùÿñƒ ñÿùÿ•3:9W߃ÿ âvjlf¯ÿùÿñƒñÿøÿŽ3;6bÚèüÿÿýïåŠhke©þøÿñƒïÿøÿ‹3;6hÖÐØïôäÞâ‘mpkªþøÿïƒïÿ÷ÿ‹3;6gÖÒÑÒÝÞßá§Œ¼ü÷ÿïƒïÿ÷ÿŽ3;6cØÒÒÓÞßßá¥’Ž¿û÷ÿïƒíÿ÷ÿ•3;9BªÙÔÓÞàáת“ŽÂû÷ÿïƒïÿöþ¡28505zÅÚâ×Ƚ½µ ÈûöÿíƒíÿöûÄ1/332,Bˆµ“œ½¼½¼­Ýøöÿíƒëÿõõø²C+23304‚ŒŒ±½ºÁã÷õõÿíƒêÿ€õýày1-32:…Š•ºÑî÷€õÿëƒäþô øúÀ],-:…Šˆ£à÷õôþçƒÕõƒô ûò­T5ŸÎó÷ƒôö׃€èýƒóôûîºÏñ÷ôƒóý瀄½ðýôƒóôù÷ôƒóôý𼆯äñûü‰ýüûò峉§ÀɉÊÉéäic12 ¯‰PNG  IHDR@@ªiqÞ nIDATxÚíš|£[Æ?Û¶mcmÛn—ÕJ•Vºj×¶m©ZtšIÛ¤ASKI^>ßäüš{7½I·A÷Sç·ÏÖçÌóŸ9sÞâÑÑÑ®€~*Â@Q}$IZH !E¶(ŠÝ¢n*š‰­ÅI %-¤=} "ýœô±×iüû”À*Y–Óè¥ ÿæ <̤LÊeÅbùqGÿÏóKh³ ¼´ñ¿S­aT[;$))é#^5¯×ë?I-wÔÓ¯åzÀ§½YùÃŒ0Ûè?^6§¼Ò t®üß­º(Bl‘Äd÷ºƒ;[–d€ý$Y¶_ÏCÙ Pî‹=2_^^þuJÈØBÔ+’%0×Í\=ÔeÏP«H_× É^Û2[«Ò`0|ÇmMMMK¼eÞVñ:S9žêÏâhÖì0ùóC¿x=ªÏÞW^Å@ˆ²w@XÃÜÔî–ùU«V}ˆZè§$‘Gƒ¹ Ot§q(=;^LÁÎJž eP0Š}‚¡žºÚ •¨:}\UM ÉcœÙüììÙ³v€B¡ø&ÏqÕ¬•Áe±$ÀÌ5"¥ä*Žd,`Æw+|°'e&v§Î|ÀÌPhf‘|CÀ@,\ƒš«ñà›fÈdÊx}NNÎ÷\ V«ÿF‹H’›C{™_ù§²B팓œ°É—uô¡o¡ññs@ëÊp§L䥻˴Zí0àý› ö¯3Ù^—D‰UÝX¯ÂU囨©˜†]Š6Óí`“zF0ê£ ^ˆ…`(€ÈºAj½¿³·™¬AÏ1c]PRR2É@xµ˜ñfK’µÇ±7u6v¾˜f3ëµoJ/ƒ°ÕBìlz!é4„¦z„öæEQj0øº À ÕúàÕ ²U=Çɬ`°vOñe&=P<;¦7ƒ nõ‡°…´™@ÄÌ‚p(‚RAQjº ý]`Ôëç0<ïT²(Ãd©G’f?ku'íî*VýÊÅ·ù1óvÚ4LwP74@ä6s´†Ñht€Ñè׉*_mÖãl^«ú'Uw¦)Ó[ž–@3}Ù{†Î/\lV}G²vÃLGWC(Õ³NèþÎàÍ>¸›oÃÞÌÙØ“Ú>ã;ÓY§\V®‡ªò§ tÃN¨}–ÓÐ[ƪ_½ÐV}ÇÚLÚè~×,˜ï€ùŽÀÛ‹]-ÕåF¬šÖÓºâí­#°ë‰áÌ8™¦N™Š y« ®U@–¥—¿EsJJVm…Áo!øóœšgÆwøÃ|3yAhÔ,oª¤¾ ÂÇü+ü{aã±q¬öfÌzɼ/;"Ç2#»"¢ÌÃYÈÖÎR܇¸?„ <Öêïž}VyXΠ)Œ«ç£±^,פ!Ã’£\; Ê´§÷DLìÊ>Ž@Œí‚5Áý±õÚd‚0›UÞ÷Š ‰¯A»£±RÜ2<*n•?¸#h~J†‹æƒ© Eª@Xês€ìyñ2™2æ¯661“ºbýúÁØÿpÔu/àv¨RÀ^ S|%³jfÚ^*ꊺ”× ÀФý{6MèŠàáÆéëÑPÛI’ÜùYê-¨R§Šû1³¥òƒ¹æÉk`±Ø-úäÎEðg‡H¬;®Ù‰üœ<)•¨¯«C{£¹¹Ùúð‚"µ¥EWÐTЀy0W?`(G¯`‹r­däg<Ê9€Ð±í3‡ ÌËGNF“N£Ùd‚³à(á²²2¨Õj0iô0ªoÿ¶T%2­óäIn0´àœxpõdÛÆýw/‡27¹™™LÙééÈËÊBiI »¢lÁ®Õêj3ãEjs¡?š ƒÊT0–ª8ª¶ì4WCG¸åD›Â@ÜÅ“/°¡ÊÍEMU ÓélÆ[IMaг¡8g©Ci²æ£¦,áõ=ÀDG‚½,**rlž¤+ÖáAæSD\Û„èë[*ìJ,ª^@Ä`6¿O²ÜŽ#0öï¸sþ˜3Ltœ™gÒkuHÊx‚«›~m«C-¿¼T ȼè(׎|ãì+‡àÍS ÊË÷@|z2B®l¢.ØêPÁàQA*:€…i-Yž²kð/m_ƒ‡·¥Gn¦&½ÀÓ¢tÀA®Ðéœ$™=…8É:à¯Øµv9 ”*ägg» @[¬A‰N‡½ñ7zu"é¼;@p6"E“ ‰s€¼¸@÷2“ÉN¢(!óI"kóÖæ£è1xù¸.˜>ðoÑóÏxkU82RRP@G¡½ úb¨ Õˆ9­À°è8LÛ|ËÏïBÔ͈¸n ”º#S—‘ã[çéƒVë€-ÔZ¢ B™þ aãÿig<Œç ý;Fõú †öø3†õøþó7˜;y$®œ=EÇ!Êœ\§t:-ôz-n%ç`nìô‹ÃÈx ŽHĈ•w1{÷I„]ÞÎ@Ø„_Ý•Q ÑÂ9ÊÕ}ZÆ)žhë r5¥µ{ hä?0®O‹qªüð—4¸ëïŒèeAx‡Â|%òsrÞ ÕÃ`Ô#ýIÞÚB†£00<C£âmzĘu7xø0ë„Hu};ô•%ì4Þ0Ï v¿Ö”—bÝìÁX4êï˜<à¯Æ*Î ;­~‹±ºà5‘x|ÿûAoÐ!+5Gc¯"xÄ&̼ ÃÃncht‚Í|+ ‘€Iï\ÁÂS{±îÎÔÔ×°‰ï]jõ\È2{vo%¶°Àqؾ> ýÿú ªúŸ˜vâìXLÜë#C°wíY„ŽÙŠÀ>obáÀw°dÀÛðñ9‚Á+™agLú„ÞÆöÉ,,'‡¹z ¹Ù‘ñÚêjVÍATÙ!ÝþÐn D÷?`h—¿À¯÷j,ƒEƒÞÓÂÁ±½ì†8é‚Aq4îâíÓ™¨­mGFäé>€bµz¶ €sÖ 7/Ãäa½1à¿vŸhXvÁüëÉt¬€ÅÔs&ís _è]L\7kXå933ï}…J¥$©ÍÅI¬õdI‚N]ˆuáK0¸ËïØÐóƒ@ÇaÂü Òrh(2­9– ­±2Ogžåж¬E´t—äååM–E‘-Ò±NàI‰·¯SõF ÿß…¡Ýÿ耀Ñ;0$ü.úRÕgÅ$#A¡o6±Ê·7/kU*•¯Ë²ÒÒ†ò4íÙBMMí}®DЪ+*ppçfŒЕ º\°p` ‚znÀ¬€3ØwK‰êšzHœ™öhr)‘ç‘••5Æe knh­mf¢Å\;¢0?¢‚mW`»õy‹ÝÖ\‚V©‡ÄÚÝå<€æÆF999¹›;!ò­úÚÚr궘;â-;Š'É[0‡fÃï0¨Ëoêûæ÷{ [–Dö3xª8g1¹»?»©ÈCõö˜˜ïÀø°±¤$’ÄóDϳjlHüç¯1²ç?4`=X¿ØEÇ’”ÍÚœçÍžî˺°Ôh|@^>Jr=ž>~D‹°Å<•™$òLx”‡Ë1a`,ñ&vFœCêýö1Áfœ^·[ÃÅ·Il>útÙÜiÓ¦}³¬´´Pây¶7Ä@pp ˆrý,©CâCßùÎw~zìÈ‘¥Ù™™wÊŒF]mU•©±®N¦dع{ bÆiOÔUU™ËFCnNNÜé“'ƒö³Ÿýš#©£ƒmò¥Ÿüä'¿žãëÛŸš&mЉ™³{ÇŽÀ=;wu¤¬{ÄÆÆÎ]½zõdšð~ñ‹_ü–rù2ÉÅ?‡íŒÎèŒÎèŒÎøÞ¥ÊdJÃIEND®B`‚ic07Ó‰PNG  IHDR€€Ã>aË’IDATxÚìÀ€ ý©©fç, ÚJÞ6þwww×u³ºu¡JÝKÝ€ºKi¡F©P/Ý.uwݺàîϽÉóÍûegÙ9Ù–\býwÎyÎM˜esgžß¼ï;sSœÙ|—é·L¯0}ÈÔA–eýØuÓ0¦á\#?§Q\£ü¿Øë1\c¤1$ÿ<Ò(Òîm8×0>_¦®’$uЄéU¦ß3}ïÑpx‹Å²Öjµ^dJg¯+™Ìì5^ôFcdc•˜ª™TìýUv g€ 2 }‘Wy_6ØãLö&†OÎ ->N{pÔ0c0 ­¨¨øÑ‹`ü7(²AÅuÍÎä¼Ô`H5›ÍþyyyßõJóÙÍ¿KáM™é/a` è>K -½Íü‰ΔÿR<$I `SûU6þðáÃ_gæ‡:gÕ¿Œlnwø¶'›Á‰}iœŠGAtFF†çA`2™V¿4ßù¢f4·zT:0hµCë¶q²l»Y~­'Y~v¿øÚþÏ<¿ßþ˜D sc¯ŸC0Å#Ì/++û—,Ëå¸Á6Év^Ûù™¢~åÿåÿÙ…ý<ÊjªªªÞv«ùK–,ù#ñß®x•,²V ` [„~W###¿é6ØiUgF¢•Ék&Îj±VVB•¦†âbÈ’ÁbµzÀ<TWWp‹ù¾¾¾_7 Wx‰ñ_c(ÅÓ¢38’º{ïL@ú´ .Ùˆª3×`*­ H^Ì&Óƒ 6¸~W V«›ð‡ž=IÜÑR­7Ô»°+Ö›ÆæÇCu{Ò&ÎFöÐÙÈ29þ‹Q¶+†ì<¯o­………\€F£ àÙ+ž©D£ÂeU¶?2~ë£áˆ|<[ŸŒÄ®Û£‘6iÔ#=ŠiÄ\dùÍ€zlJ"@¯Êñx¨iµÚÝ.5?$$äû¬Iౡ¾T“ 2~Û“ÑÌø!ØúxO² @F3¡Ã@ØzÀ£#5“É”³cÇŽŸ¹ €øøøwÙ‡›=)ü[xqW©+ÀõìlÅŒNDÆÎCéÎÃ0Û ð bÑb“5%%¥µË`(ÇÖ­~Iª/Y¶ÿ^–ŸÕO×õÓvŽ\Ñ*p'÷¢žN P/ß0D¨FPO\ˆòC§`ª¨âÛG‹½{eÿ¿Sü;ÔŠŠŠf» €òòò èÃÝ([g4ë[t{ã¦s㇠f+@!wúrT½Ig€EX®µÊÊÊ=®òÿ«5ÕÕgÜ d€V~fù}Nš õã•`_#怊Åü¡¨½ I²@¶Â­hjko2o¾át÷ÿð‡?|—U]€î‹k3q:}-3}(¶0qS]WÖ0¶}d[ÈÚ¨-°© ó´à z}jûöí슠ŸéõzÕ— Ùûùsö‹?“yž¯Äíœý|K7D0Õå°ßË›8¦16úCº ©¶²“Ò‚D²@ÑŠ+~ït‚ƒƒÿÀ(@7ãtYehÉ%×±/~†PเÚeS o)Œií(H; Åß´Ý¿ÕEsd±Ðsê]»výÓélݺõŸ&£±ŠžH9sPòçÂýÉ´5,Ôû±?L0Ò]dDÑ´Y6L€þ9­ )t4¤˜PH*H$ÙâÔ¹¢í¸ÉdÒÅÄļáteÿa´ÕZ-ÎdÀ`ÒànÞ' p¿å¡Ÿ` »P€n?d€ ‚€ÒÂÍ#ôÁyÈ2=0Ÿq‰ñe8p£p*ýc¤”\GÍÍ{(ÙŽìÑôè×.ê1s¡¶}Ž+Œiݘ7‚ááz€—Dú%Ðö±87 ¶Â‚-Ù [ŒÀ¶¸QÎ3Ÿ¿íñh\ÊÚ‚"M:¾ØŒj”F@ö˜ BýÕ_8]ýkÓÆ 0œ˜m¢?4YÓaÖB2ËÞ€DÊo€_ëÉl“ÐO-ñþ ôýó|›"¨OS,™Òa‡ûcÛÓQØÛ¸ Ð!­ü ™P¢ÍijšQ•‹’{l~‡Î²mû&Ìi°ík¸ÖÛdŠžÝc2~24™Lé`¬¡§ŠVa¾ÌÂüŠý’;R Á¢vóäAÌîõ.æ hRP?ƒaÅüNØx~°--‚…û “ IcfgÕ_}I ðåóggÎÉ ?p ´ T@tØ"Ìéý™.ª?¡O,Ó ¡;û òÑÈç­˜ñ~Øñd,îå†AÒ ÑšAËí£6M¦3þç÷”2ŒÇXžOàÆg’áv(ÜÅŒ´:0§^€­ܹbæöyŸ ·+J –No‡ð£¨6°[ÐW½É|ªìKuÙpV³–ä@>ºÞÁúq_žç÷³<ÿÐß®ñõ]Þ&˜Í²Ã'žÏY[æ¡"ÐŽùbZ ¨°rIglºÄoÔl¶= Þžž*`ûòHKIñ¢nUÀÉp,=Òª2›™væÒÄ%̹«0<Ÿ¨Z-/.À²Qä«ìqðÆÀ1HMLr;9êl¾ͶraŠX~>5•0;0¯Þ€ÑúBÈʱݨú6ÑÇþ}‘g{àã6HjB.íD «.lGUm5ÌOàôéÎbrTúJØš‰½þJØ|®}›átÌ!¨ÒÒÝ@a~.ÝKÀŒè˜& óO‡;@‚/E¡VSCaÝ¡y5¸ €sçÎ5wF7})4tÚúZWƒÍ_0åߦéó!ºµ|¾[à“½QÈHIeé Ñe䨳‘Ÿ§Æ±k‰è·ì|\ÄðM‡1÷è,` 8@è•=Ðé´_€^ïè9tC  ŽÁƒ s‡³<þaƒŒêß ã»„ÞmÞC×–ï¢{«wѵÅ[Locõ¹xúààtÈø”t>>ø]æ_F—y—Ðu]¯¢×Òó·c?ËéÏ N„aãõƒ0pƒ˜Wpê” n”׈\4‘Uò<;Ü´ij¯&èßþ}ñÝZÙÌçb‘àtjòFùvÅÑèýHONAZRR£›£F~®§o%aTÈ t¼f|×ÏÉgþýVŸÆ”ýQ”ž+-Ð"òö§¶ü¯÷pΞ=ÛŒ­bÙH²$aOð\Ìíýž}ãùªŸÕ·)ü:}À&Ãí˧ٛ h°xÎܹq¾H+@­f¹>/O3°t÷}ЪïÄW½=ùÌ»‚a𺣘y8 x}`€uØ{ï$d£‰æÈÑÈê"Nœh¦W€ÅbEÌ–U˜Óë]»á>€åùQ,Ï÷lÍÂü—o' ði…Ȱ$‡…ü+Ç?òñQè:ý:/¸B¦6\óm ô^~¢ö’áB}0›p1é3ïàøñãMY@¶àþ¥uÿ<|>7Zï&ØAÈóŠÔ…¥ŸæoaÖøá8wüÕLÉvÈe¡žôÙ•GX7s¦tøSÚ®ÁèÁ;à³Ð„úà úŸÄ´´mäõTq^€Vë0ô@(#þèAîg÷kŠa…<ߨêÜôuÐuÞô ¸xú‚Œ”Š Ìø,P¨'ãï]‚MóaZçµðoL×:ùN=ŸEWA@"èêv³c¶ò´ŽÌ"5$ƒÑó8zôhÀð…1ûé÷ªËK±lL7ŒêüzµVîVPZèÑú}N‹‡£‘ð”Õ陸qî>6ÄtŸPø·eÆwâÆsMï‚ }· ÛüK Ì냋Y}qËÎìBumLzƒ0oan…~×pòÈ‘X0s’E’³':|ø*åkÁ,gƒ@ÎÆ è…EÃ6ajGÛŠŸZg¼¨é?Æàñ‡ÐEˆJ ¸Œvs/!æf*,fšO]ý¹Òñ÷öÄû®à€Sçˆ\ÉdÆ¡ÝÛÉÚ‘9.Ö;èÞâCLj¿œ Ñt1 øwGÀó誰 uf[IV F_É€Ù@éT§d>] €N£á(»i«lÁ­+á׳=:~ôª«`é§)&wZ)`O3X>|âZ }ÀE ^u7cóa5sóÎ%[”®ûká dµX›……3'1^£”౦v EŸ™§Úta5™??êr +É|>^À¡C‡ÞgHâÛŸ#v÷sstb x*T Œ¸½Át¼ˆ¾Ë®áÓë™0Ò˜<‡7&§O;€¨¨¨wz僠h™šŒù3&´‡÷Äz ÿä#B*°—ëÉü ‘‘Sn'ä+Ÿ;€Ô9€-[¶¼¡­©Ñr]üIáä§1¤G;td;*= €‰½7£Û—<ðá¹~ÐÊë8~+ aÕ72|úôé¦N`íÚµ¯hjjjœ òÕÙ^Š]š¾áfÄT0dìAa[Hêt ´òWŒE^Q¥°ê1_ÚÚZÓ>p:Ë—/ÿ' ÂÌp¦èÑ1E„;×/ø/:òs·@bQ`r·0ô 8Gõ@½"obøÜŽË­x³Aïôy¢ÅÈ<Ñ„……½át† ò‡êÊÊ<Éd‚A«%Õ»þ^èú…>û¿OG5••8°s+tiI[FJ î€o G Ý. ¯‚Œï·üö]LCMµSݱ7O$‡úé5_(FÔVWW,Y²ä_NàÿøÇ/+ÊÊÒd³™nÀaéÐO¯ù¹ԙ頴ÀvÝ iRûÑgêq¬ŠŽCv~¬Ìx#7ÉU¢ÅÈe®¯¯ïï¿â‚öƒ’ââ;VY&cqÃHNé'ÚiÛøà³›˜5~=ÿ§“D—0™=)œÔv B§ìEìƒLHÌx³A½‚ñì·˜Í(/)‰ÿñüSWðÍœ¬¬X­ôán“l–¨òÅ™c‡1º7¡>pS:†`b›ÕX2,7O<݃d6pcÜ#X,(ÈË»À¼ùŽ+øj|lìn€ÄëY&ú±oûf êÖÚ¶m@PÀÔN6ãûmÄ©]×QYR Y2Ò=¸}¨¥&'oaÞ|Éùí7ý$£‘Sï~Q}@ äegaSÈ ôíÐT(€¿³z„áPØ9ª‹ Ëôܞ_½|yÒW\Õ¦L™ò6+:jL¶#Hm‡hÇ‘’„¥óЫíöŸÄŒŸÑ5»WŸ„:5’ddŸáQc&øi hX±bEsWùÿU¦ŸåçäÜâ… ‡‰Šf3’ãžbÍâô¬ám{+~†O(¢VGf¼f“ÞÃd‘$<ýå/ùîKÚ·XÈY‹…nÂSeû;º&!ËæRC‡_¡“EúþßÄÖ«1»G­xf|6Èx37ÞSEÅø;wB˜'ßfrYû:;z¿ª¢¢‚®pcM=)î'‰ýÂk¡ŸÄ#[¨ÒR°}ÃZŒèÛÙö±ËGï`\ë%ðo÷1 ‰À‘-—“–鋯Ûÿ|±ßþ{û÷klvûyø§ KÉ­˜'ßp%j~üèÁƒ]D ݈×Ê‹‹qõüil ^…}kãÁ¥xT–T0H(Çë¼f<4÷ qqG˜?ç;—¶oûùùµfQ Òl0Ð yè¤N6™A(´‡§oÐi½j &Ûê×Μ:ÕGØÿ»* yçΞ ,ÓM½”‹Wÿ­7¶2~ɽpKûîo~ó›WT÷¨ Ô5p:EýÊ¥óBãuL´àrsr^yå•·É&·µ¯2ýdèС>奥E,¤Ò Ö¿iAJûõÿý¢øƒŸÊéS¦ôcsÿSžûÝÚ¾ÉôëåK—Žf9©F6™èF_Ê b»C>ÍùoùÜ{D£"ä!!!“kªªª…HðRŠÅ–.2""ÍõŸyè÷¨F7ô§AA£ËŠ‹sÁ·‡º—R,ª¯ªÊËK‚ƒƒ§²9þ+Óxúõ¸ö=‚ K—.Ý““’®µôÐHW[ ®z~®¬_¸6 Ÿ$ö‹}.ë'ÑöÚ*IP¥§ß+™ÆNïµ55Riaa~Ü“'ç#""þç?ÿéÄæì¦ß3}Ÿoõ¼¶}â×Lgzóç?ÿy»Q#FŒ`ƒ]|îÌ™îß?™”p›åº8uVVjA^žº0??¿¤   ¸°°°ÄKULWK!SnVV;4‹ONH¸óèáÃ3ÏŸÚ¹lüøñ£ÿøíЃ aágÛ¶m×I7E\ôg'ù Ø­×-¾Y£‡2bð˜< "èaŒ%v8ã‹Å>ÅbñW©Tþ&( ÿd2ùeÛgì°Ä}4QBAx`tnx@q¤ACå‘E D„n(¥”RJ)¥”RJ)¥”RNg%­žÛ'¶âIEND®B`‚ic1396‰PNG  IHDR\r¨f8õIDATxÚìÀ!À0 ý#Ÿ˜m>T{옌õXÇïÚ¶m[Š6Úp¥h­¯mEÚ ×F¸vüÙ¶ðFwÞ½=çÌ÷ïÞÜ×aûæü’_ýh{Nw‡{½àAð(x<<Ù9w:ÆgÀ³0}vŸ˜O=7Oïýù÷‹ù à…/º!bÙŃŲabù%a໇˜óûá°ÿyAº2†öÛ99ûù x:<ž†‡À½ážp¸‹Q”¼ânoo?ã ˆèzf~(I’W0ý±ˆ|‹ñoÌ<Ó‹1^WÁõp+l%"‹q:èa’‘¦HÆ®ÁŠHÔ²!"Q™9«äH“œýì`ZØkp#\WÀ¥p6ýÀôgðmçG1»÷þÒÞ¦±«Qš›ÖÖÖpà¯bæGá'"2 nÄ´ŒÁ‰¯Nž£=viCŸGDßâüxã묵G¥ú¤·êDt/üZDV†NfV§žÁæÁÌ›±îgŒŸì}ôØÎ(Õ V«í眻ˆ~bæÖ¢Å®ª™s¦ÿbæû1{¸QÊIggçÑÌü´ˆ,ÎtsU“»L¯gæÒF)ÖÚ#™ù•ôàŒcÑ«êàFÐÁÌbò £LkÖ¬ÙÝ{ÿ°ˆ¬™¤ÂWµ´0ókmmmeBo÷¯‘ÿKRøª6‚…Dt³QÆ—•+Wîæœ{ADêe)|U탈>Z·nÝÁF{êõúIDôk™¯úª¾,Äô,kíåFÓâ¿–™W–½ðU5EDZ½÷weLž÷ï‘ŽŠ¿ªM@œsOc¶ƒÊ¯üa_RU‹_ÕGïýÛŸþùF)\ü h‚âWµ ¼ÿÌ3Ïlo”Æhkk»/­ûf)~U›.h¯¥¡â¿Y˜}³¿ªMÀZû¸QÂlÚ´ébÙ.~H”ŽcŽo^óš'*Ü0Np»Þ(ÃY²dÉ!œ$sºõØñ‘e±mÃÛŶ gâù>£yXö|hÏ3W&Cùâ ëA:Þ°víÚ3Œ2@ú‚Ä9÷iœl¡¦5Þzl8ãyæÂy*yŽ8£yMž¹TyØp>%ñþŸß~ûmO£ôP«Õî\ü9`¤€ ç©ây®RžyÌòpLó4ßOäaJ{{ûKÆ(fáÂ…'0óú.@D}F‹ƒ†/ï1š+ž§qÎSó\ Ï%ËS‘ê…-·ú ið(dôSÈTä.\òK·a/. pT`Dû÷ïÿn£IRSS›Ë³¿‹Íþ†Ô– äô4ëçU&ãzÆvìz2ëöÂúG½±1¦?6ÆÀŽ;ƒ6¦‘1`ÒûŽEzï10˜…¢í‡aI΀Óíò€Àø®Tåçi QÀ§H555‡ÕŸý ¹¼Ž/I¤—ÆâLòRlŽrüúaSÌ&k©ÿx2MBÞÒ-¨zôN‡ÃA=‰9"¾ûÎ;ï|Q÷QÀåË—/IR¹›å™†Ts|»dEbÑmK˜ úbýÃÞ²³÷gN//Š úÈQA¿qÈ™½·B²X ¨,·§AHzû–ÞgZú­òìo8¾Ã‚¸‚k87B|Y}8‡÷ ¼ÒûŽÁ {ÚrT\½Éb1@ ¢è())Y©ç4àÓùË_>o±X®ªÃñ­ˆ+¼ŽCqSA³ýÎñÕ‚1Èž² ×î©bA‰¡¡¡ßÒmpæÌ™?I’Ti„ÿuõ o°Ÿs|õÀ+½_­ˆàz4$«‚:6‘œ=j¢»(€}“É4пÙß9¾Sr ¹ø.ŽÄOç?Àà#‚é+PyçQ­b¡ÓøÞ$Éç(  /o–ðiRYié.ß`¬ãÓÌŸ^ƒ“‰ó(¿ç?8àA@ˈ9óÖ¢*æ9k*2¾CPUUuEö•/¿è òÇ7åå¿ç„tKÿöÔ®KÊ.§å<ªêÓ¬ÏXk`ò®ŒGޒͨ‰KÖ@C‘VÇÿoÑa·ÙòæÍ›÷ =àS$ùÖȳÛíÕ”ÿsO’¸ß¹Ÿ•Îb{NÜ¿#¶‹·÷ëß æò˜Â*.¦­%g¥s\ €ó×î†%#‹µ+}¢ïœý¬{å¿“ÜwïÞ}ùnðäÉ“ŽÄTü7õìÅI5{ óÉK*,…¸eÞ­±C©‡­ã78°>j(2 Š¢ÝÇa/(f…B%§ð÷ú‰íy;MÚÓ‘––6ŠEÎzÉÿ©8@]œŠh{ú¹^í]Þ<ßb¯DLN$XËîFÎñµ1Ì#f£ôäE8*«XZà˜}Äœè(ÈÏß$»Ìg¼jð}ˆÏí GÖ¬àò´í&ÞÄþ縖]€o1î5Y£âÆ}HlÅ€]C £¢¢â’ì3_Ò >+ë+•••WãË&¹´¦d•¿ ÝyÞ%½¾Ì9u ¾‡ wþ:T?O‚nVhìbÏHzï½÷~àõO5ôüÿ³o¾ùæä•ÜØÀòü’ê,\NßÀøô¾PHéAÁ†}°ÒýÚÉØè`µZ 'ý[Ý`Ĉ¿±Z,åù÷³Ž`ûãP®À§sˆëçÃv-ΚJ]³Æ9I¸\pØíöcÇŽ½N)€.°{÷î—åÏdq±ê¯@’Žþ'@gÚ¥wà…7ÏáòüF RzÿI(ó!¤å íœéÅÏõs7¾ ƒÝ#àÂ… -¼øtC_ølTTTS§$¹YQNâ…û]ðšØ¥ ÚKµÂý¼ŠœNZB| y¾ó °. ƒsõ0H+†@Š éX¤ì8Ù²¡`¼^Ø‹%ùú~Å×Ä¢#::ºùŽð9™fmXâÅ©-?¡ ùü%ûn/ ìY¸_i-ÁMÓnl‰¬ç`³É”Qp® ´jØGZ>Òš0HWöAª(ô±eCIp:¿è\Ïö$Iáýt$ÄÅñà3 >i6€ÀÑ9ñ¯ûh/ØÞíèL{ó÷<-‡û=¡¼žo ]þïf…އ}…Çé9­”¡°l ¤­!=½Z6”ÜP#N¾S‘½D{:’““§34ø€¸¸¸± >9"{M« àÂýdœJZD3þ' ÷ TÌÁf±´`¤#Ë@i@r¹Tøþ´iOGJJÊ|Ùw>OþÓÐð¹ÄÄÄé#·Žä ÷«­e¸mÞçC¸o ½ÿDäKþÉáM ®€TUæäÔݸ¢#33s í lèø,Q,))i¶Þà"ß—\H*ºƒ}ÏÆùîÀ ‰¨Y8\0û+¤ËBÚ>Rü]HN$·[wÈËÍÝIÝ€º@rbâ<½€ùJª³q>e…ú>†û¨ðW8~´ÈùEi¬Á"×A*ÊöDN§nPPPp„:h)‚nèøBzjê2=Àíô܇ïqîYìxFkú¾:žù¿e:¶eaV ],Š6|éÁ9Hv+ u€¢ÂÂS²ï|Mø¢ÉdZ£ 8þÏúN ·" 'æ 6íÍþeÓ¸e¿ºiåP.‚”•Ì¢ÆIpìé(,,ŒÒèþËf³yvqØYpñ¸÷ˆìé5_/>³Q´§Yßf¯AtÖ*ò îÊc@TõÏ>Ž•þ;»¸H8Ò­ã¬5ÊÑ€òØ#q¯Òž ¸¸ø¼^ðÕL³yv8 œ‘{IèÌ<4¸÷)ýûlÖÏ*ÇѸ*ìÑ7@áåÜp8רîü|‘pß\H¦xœN¥1#WâÉ¥nc“·g¸(ûÎ×u€¬¬¬½”%˜¡~çÿæ·=ÍúV{5îfÀæ˜A܃6 øúç'Ëûë[ÔN¼:ÒõÃ,ÕјGßí%É{Rm\Ò ¾–“}@Ëb›w²Ëp$^ÍY߀iÐDX øh`ï\H™‰ŸY,õíIt”è òšæ‚ \¹%7;fŸP9×7@³ñÄ…¿zލ6pï$» ’˭˜Q ú@A~þ -Gp‚Öõ©WåYßåý™!`_*Xö P4pt9¤¢–BFP˜ŸZ‹p:¯.¯"‚Y#ÞÅÊÃ](% ú@ƒ©ê“ãï|Ž9ÇPã(‡Ú‡³¢ e'.Ò#¾ ´jÀßæk»Í—öò|ûPÔÄ|Ìñ9ÉpȘ‡­’ àKv»` ^÷Ù^ÿ`†§#ÿ3gÃ_D±=ݶ;?+3û¼‹É]_#‡W’§> Ã`Þ´fX{¾§œÈÐp}€Bý­±CpÓ¼ ¶BÔ÷!• xÏ ˜†N¥Ô€{º´Zcáþ¶PXn÷8}st%ÑûÆÀañiì¤<Ùí~ÛÓQÜ@_á…Û»Ý@ʳ‡^ççÀ‹Õz½EËZcý­¾-Õ¼ë‡ó©«PX“@6S6 ÖìBºœ¤õ‡Röt­,ëmë…0T%²<ŸI {e"’‹ܸËw{]GôÁy± Ã_ å¿ í™Ø}|ó¼œÿÿbççë3¿ƒeÛ:`Ó£à×6x×ò%Ì‚©<Á>jžÄ£pÑJØ— ¢þú ‡ûØŽ‡¡ú™B¸/­”=€C‚òØâÅ98'åñKj¼¸‹$‚ƒï¢ãFä^Z$ÇöY“½õÙ£ßêãݨ6@ Jž¿ûéH<-8Ée‡f§®ÇW m™@]uÔgpÇ'Ù÷†¢æ-ë _«<\%€`lJâ1Iv¾Úëþ;³ßötœÝ½ãØ Ÿ"PT0Vs¬½Ü+`ˆô´¡Í1qüUöbhö¨,…ëÊ>Ï­¸".Ïß ËõáäÀ¢<_,Z ,<Î éu/@p.( ©¯ãwÀ§¯aZŸ7±xU[l¸Û¯Þê´¬GÎ"q.r*ÐPwv2œ‡—xöÞ¯Zyþºa°F…¡:Açû €¼ÝnõAž6à ÇŠj~Õf†4AÄîNä°ªÖ(ϧe½gátKhp‡S‚ëñUH›ÇQZ j¸OgÛÑPT?äù~¦5Ùáp¸ô# &  \*ú¨­xüX}ª{ÛŠÙ£Æ.¥­C¹- ýp—µ…"YCêÞ¾»;5Ѭ}W-Çç›I0@…›¥«ÆõÁÄN/«éü|} ûX0¿%Ö]ííW}€fý½ÏÆ"¥4z;ÜÉéžÞh Ä÷<Ó0X®†ÉÊ9¾ºJEµyv«,Gp f€ Õ•åX6¢ &u~¥>œŸo+î'·¯o‡Ñý("øÄ³þՌͨv”A·Gu9œwR$@údí»k‡Áz: Õq‚<_MdÌ‚ÃV¥ $T”aaH[Ö\ÿêâ)ÎÞ+öwaÛŽgý]OG"¹äËáN¼ióx* ·éÖÄ ò|Õ%Ã&}*Ö28ìRC€ÚPR‹9šar×W¾­xîä¦XÅo;&ç§[‘—[óÐØwYœÇ"(%àóü²ãßáÚw¤á¨N»¥X0àt"?“Û8±¶âžo`áb¶íx ¨•÷–ykèiœ‡ä€óê~zp‡¬Ð6]ë%n›n@ÅöØkrõ´˜™i=ßøúÀôocñ¦Ö¸k>ãð®ÛGa;6Õ/¹p?8{µ ‡³áÀ€iq±^'dž&uzs´Dâ³g¨¬¨@c?j,Väåe EfF¬ Ûa·ÛQXXˆ ùó§§›Q’ºÕ)ÃÈ 5[e<5éâÞb=Au#î[5É ‰ˆ—£€¸'Oðü9òrr@= z?¨&SZZ “É$;~ºÙ(LÛ- DH…­â™½àÙÝ+äxš@äÎu``z!ƒ 9>ž:Ààv¹ôWõw»QUU…¬¬,0Ç'1ä¥EjÃ`+‹…C¥ýŠŠ‚1×£4jFºt|’âÀœŸ‰RAzJ ª*+¡—Ãjµ"77—Ÿs~²“~Ev¾P€î p¿þ`³±³"ÈgôþÃÎ^_$%{!M£/ž`;5¡›ç"‘Ï 6èœe6Ãfµ¢¡´£¨¨ä䵟@&²Òï’óiÖÒ[(O&îuö³àu%{ ¶ø Ag%ùlÏpûì!M€V!h92úÚ%$¾ˆS@í´ ñÅ äå:ÊAK¯eee0›ÍÌñ2#3=Vv¾Q²†kÅ׸1&{JŽÍ‹·o à×þÑëÜïvþâÁ®dÏË{7 }šõ!Ìêû>ݾÙ±y§WAJBÊJJ(—Öv»u5²³³9ÇÀœñ •)c5€Ë B¶‹܇¿7Ÿsvå¿)¼Nbóêñ]2þtÐRäÜA-ð8ú.ž‹À§´b‘šŠêª*hí°ÙlÈÏÏWÈóE2ɈGEÊD`(¬Ej×j)±ì$ß&ºÆ^vN'ÿ„öt\>¼=(¨¢ÍH BÚá飴ôçøú@Nf&è3û$‰*Ö‚<_ SF*R§Òfm 0 »›sh&»è¬îuÎN·PpRîg|³§ãÂMšÀ¢°Nxñ8ÖðiAR\Š hm=(ËzÈÌÌäß?¤¢m;–$€oßå¶é’ €í ï ½àø¦ÅšÀúi2â ~Ûqiq1k+æ¶éGfT¤EÀ‘6ö´°à*u0ìùûa·»ti¡ÁßVÌåùÁ’IÀ­Ømˆº;çîÍ ªÎÞ™‚ç©‘p: èF ¬´´4rÀ Ël2cݵÃ}rÆE® ªF_‰£¯Â-9 Ô ¬Vîg%²7jbQM€f~À„×a⩘rzuP5þdN>1¼ÀjUí[Â*€€ ×jãdD>5X­$AtÀÙíé̹u™Æû ÐêÃúkd¬ÔN=½·Ã©0öüž¤èõF«UYü…âÏb[§¶-×z' € Ö^ݯ,Çég×É©<öèwñøóÉ^ÿ°Z-”Ï`p³V` íˆ7@â´úê^ à:@iì)I<.ůéêK €Ó b7 €—wkgŸß`œ ¨/@Ô­ßÀ@zF:–]Ú‰I ¬œ{q nGðP¤+nàÂ~-ßÈ€IVjz–\Ü®\Œ¿£"Œ ¨¸tx«†î Ø–»' ’ÓR°ðÂV«4€+ ÷àvH«¶Ú((P FpõØN-ß؀Ʉ„”$Ì;· “5€k‰÷ëF `«+-}À“{5ü\Ôô"9³¢6`òimàfò#@ÆŸ­.ÐSPT 6…‹cüLgì].àÖ™ƒš~23ž$>Çô3kÉ5€Û)±KLll‘xgW›þØÓQ¬GØÍ$ø›Oö.—ÑŽc‚¦ž x²Ö³ dʸKΧ MˆŒÀý´§p9$ßÇ'ÿ:ÿÁßô/’ø‚ùnOxtí¬–ŸlÀœ‰Ï¢i@#XS\vÉ·qHD{Ý à¢;Ý<½sY3 ÿÈëœhÀ«,Q1×Éñ4€g™‰2bKl¯wäçNâܤ>|Í`ßÊÙ>€ÃÑQšE"ñÙ)p2¨&—Sr"1ö-Á‘jcKð´aF àc5€M7kb+0‰V"’rÓ ¨€Â‚‚‹Á@êóV…×ÀR૘?¤ž>¼O+´˜œ–Œ6ÉŽ§ÀÔÓk–o†Ón7 Rà,…¿ùn/9$˜“^€–ß&ÓºË)@—×ÐçƒcÉì)ôð/-Ì& ÿMˆ8ô½VÂøcë1íìÊ ÏþÓÏ®EfQ$›U ¾ƒ‹å°;gJÃÌ>ï‚äø4ó¿Ž6/£C“ÿ¢Õ[ÿF‹×ÿ¹“ÇàùãÇt—ÞFrüÔ´t,ÜûÍ&_B‹ÉWÑnö Ù²“O­! 3£Ö#¿´ÖY¿În±4 |MÀJš½^[>:¾²==#¯8/s4£ð; Ž?µ»G#Ú¿Š®ïÿ­ßúZ¿ý´ñªù«ÃÈ=pëÊ%¤&&5äd™ðði2>\{M']B«i—Aj9õ ZL¹‚Î Î |ÏL=³ŠPP+òœs›P\^‡ÕƯZ¿‹Î$e{åG·àœ”ûðìg|±§{”`aH[ÚŒÇ÷Îúc:½ŠÞÍ^"§'ç'§çÔüµ¿¡{«w°wëFzÞ?I·È4›%ëðåçè6ïšNfÎÏôH=—ŸÀ˜C›(h3Ò‹ÛPVY;€o‚ñǽÇ{ ?7— à¢/±²¬KGt¡&œ€åù[¾Œvïü­8ÇçÕêÍ¢åÿÀŒqá¸ûEºÍúÏâS1{ç}4Ÿr‰Ä^Q-¦\E›ÑÝ!L8±Ž@%ÀeWv¡ªºv朓N#€¼¼óÁ€–ª*¬Û›zê5ܧsh›WÐñÝÿ2Ç÷I”ôn÷>öïØìâ<hÖ'½ò½^Ç,ä÷A‚ösÎ!dÛnL‘ëSÏÖ_401r%V]Û KMZ0—›¼· Û0m(&tzI}Ç÷:ÿȯ¢Û\žï«äHàŸ M‚[W/S4@ŽÚà`2yfýGÏ’1m[´`Ö‹Õº,:‘û¶Õ[}€š‘6Þ:¬²ó8LHv,K]xª‡ûã:ÉËzÍ_B.ÏI tmñ6¯Z†O#%!¡Á ;Ó„´ô ì8ó]ç^e³¾*j)C€Î½WÃØ#U¯vEGÒ `€ätºphÍlÚ¬šãO’—·âò|EµŠ„ÃûuÁ™ãGœ@©f@¡>9ÿå{ _}Täk1Õog×f^ÄÀ 0ñ¤zõÚ |8æuPQÁí<³sÝL•@©A·%‘µ« -àFÒ¸Œ@=äfgŸ v s=ʯ9þøÎ¯¡_‹—ÐömÂ}Òª„ôꀣûvS+1u™Yf˜3͸vê.f؃æãÏ£9sü ‰Š„­åsŸUG1îèOZpÆ÷@¬)N›5#€3Á€$IHz|ßÛ ,ˇöÝ!­_Fû&lYO;¢Vbê˜0| .E¦h€êõrzšõïß|ŒÕ÷cDÓ¥ñî"ô´-g\ ’óóõ¶³.`ð¦}˜ImžEɹFPUdgŸ&¨87#µÖ~qûn¸œçwáò|퉊„íß} ó¦ŒÁíkW©>@Qê0S/'Ïbâ°mÁqŒiáï.ƨæË@ o½íÇG¡åô+€[6¼ŠNóÏbø® °ú€p@^I>*.ÈÉ9¥k/–õv–aQh{Ö¬˜çîøz5ý×¾«e±ú@׿obåÂYxp÷6A`ò:~üóDì_};®F˜<ãl¶äøL£›.Á€ÞÛ5ðmÅ=–Ä胛Y}@± pñ¥í¨ 6`KŸ¯+D* pP°ZY3bûnÊóUXÖ f}€@ЫÍ{X±1÷£|süÜ,$Æ'ãèæ rT´NvüÅÙ´¶ãóêüa¤V¢¾>0ýú­9Œ ÇÙ¶c…& r~õÆ €ììl€_Ë*xMôk­BàÑõ ØRàGy¾|‘Ûw;py~ÃÕ¨PØ·CSlZ¹±÷ï3ÀB}ãßv 3zo…ú#ŽOúPކvÞÈ9Ÿ¶@@ÛŽÏc(ÛvìM Ø À±ØK>­jÀíÖr²²NðÅÑyGöá¢Z@·§¥@Êñ)ägÛtù<_' xƒ Ö-[€w(5HD­ÈÎÏÀªúêÇ=MÄ‘ç1½—×ñ?XÂ9ºÝCk- ñÛŽžÁˆ=ÛÁÚŠ Ñ©Oü$ãûø¿ÆÎz@Vfæ b~L55tV~¯ö$*æ¤'cfï&Ó‘¶é Úwu‚f2z´n‚E3&árÔP–”Œ,Ùé©‘çÁ­Çص4R.’®Eçø>@® „µ[6“/ Õt­9?ßVL= =#Žcô¡˜µ¹Åù´€›D˜¸×œY`OR°×gpç ÜÏþIlÏ–ív¬›7Í_ù³Ž_\#h×ä%„÷ï†5K`ÿšSX¾£[-G˜ Ô÷% è;`h9-ø`Ê9¬;w ’ýcN[SÃUñØõÝÞVëw=Fð‚Iéb*ý³åí¹÷x–³³@½õM_ù‹Ž]¼j@íÅ-_û/BšÌFøûKåª>9¯zÑ2íÇeAMëý‰¶êr *á°YùqÇŸk‹·gÙëÙ™™Ç(|pŸ.ý²gr:$ä˜M5¸'š¾ÜX!@ú7Ú¿ó›͗v¹ªÎÏ¢€A=·ÊQ€¶ðžìü#ÖÞCv~œv+¬5ÜØ;»xì}r è´À  Š ò1{ÂHhF4°¬^Ôeä MB€ršùgîŠAqI%9¿Ʀ^#€¬¬£Z‰ÒKu5¶­‹åÆ-åÖZêŠ ‚!Ö£õÔKšr~º1I‹)—±ål,4lÌùµ€]Ef³Ö@ò>:̉k¢Ð«í{T 3P©@σš)~0é"z,¸k±™pÙ­°s!»ê2m6aТÜ.Ìi)˜2j()"0 –ä(`x›Uh;鸇‘ƒzx¢AmÀ€xU kø1Jê}y/\žõcò…>Mà¢n’”´Y GNT–•açÆÕèðÞ+ÔSo í°­§\¬· [9×ß•ˆ Á¬¯uг4(}Ö’×5T°h€jÏcaì°¾höÊ_iƒ?SÞC …ŠQ@‹)—ð¾ìü£7ÜÇÓ”¸ÂY_ûÈÎŽÒ ¾òâÅ‹ YNIBue%ŽìÙî-ߦ%C~, †·^‰vΩR¤p¿ËÜk8t5ÕUÕ\G_C@NvöiÝàÙ“' õíÚ¢¾Œ”$ºO?Ýœ“dÀÇ( Ÿuª4›| ¤Ù»c‘žUÌUø:²åæ9½àËcbæ3èE´µØa³áʹÓص•wÉð_>©Z,G§Ñ§~ùú/½…KͰ[-déilÑ‘ž–¶‡Òg=àK2fé $›,ª æåbãŠÅr‘ðe®Ø€r0¤ë&¿Š|ëN¾@aq9Ü,ܯ®ö÷;Ô€=/:’7蟕õÅû÷îM„Ë¥Öb\3¢§Ó’á‹'1˜Û[êÙžàуsõ€/8p ³gYÆ{ÄRrLfÏ^óÕžoç—=û™Š„”äçdƒnÄÙ^1-0Àž'Ðg {ªî¯9þye,Ü}Wüß|ÿ™$œ |·;>ƒEËÑwïN"ßÑvîÜÙVþ`nÅ &~Í×÷ÒÏþÙóv~ÙSšˆ^<ŽÁÔÑÃÐ⵿s«d5[†­>zªUö©wÒV ÷óá²[`·søpýIuùþø1 –r¤á³=E’ÏŸìùPƒ=>CbñâÅMjªª,°¬ XLÉÞâ§½EE{&k-Ñ—i«±àòÙH éÑŽÒ¶ÝØ@­( mxOžõ.» ÷Mž•›…s‹Ò÷¯ðš²=Éw{«:ö$Þ¾Vqy÷îÝõ€¯têÔéïåå…4+rŽ¢ð»ÀÑ|°çTOö$Þž…uT$,), –bz„5Q} Ñ€nFÖdÆw\ƒ='Ÿ¢¨´n»…9+ï¸âï·¯©Ñº=¥ÔlæX½zuSò†€OÅþô§?ý®¤¨(Ýi·+|¡œ”.¤ºöJ_T=ÙÓ—K HONÄ’Y“ÈiÅ Ñ`ø{‹AçíóN"39Û»q‡›¥•¯±àoþÚ“ÄötVßžJZQVVܽ{÷yð™Y?ÊÍξ §Sà°Â×êÇž@{Š„¨‘èQômz¼7m7¦ú@£@øûK@Îñá^°!¶å˜êû·o¢›“²ûê#i½¿ÉBLé²gwÝDYa)$É>Ï7D“¤¼{vMš4yꟗõ#–lÆ—®$V ‡–lˆXˆN¼Æ … ¬À7¶Í \uyæ|8%lž<ßBpóúõ4iz'ÏO5t|Ž0nܸw«+*jh +^Cž;I’âžcáôñä¬ÞF¢7(Г…é¼uö ¤Ç™A¡¾Íj|÷ÿ±%@÷æ º{ðy€†Þ ôÿøÇ(,(Hö潆rØìdÅDßÁô1¡ôÌ*jTÜ£' ¯xq÷“a·YdYŸ×[(/--n×®ÝË^|NøŒwWÓROøV4$9 ¨éÎõË7¬šSG¡,­ üýÅ ç_>rboÄÁj¡§/Žï‹ÜN'²33£e_ù¥¬¯s= |)ð»çΞLÒ—‹bˆuJ»)q%êF èF÷&¤'ü´¤Gáþ¢a;}þ ý?z*û5þ|V£øðÁƒµ²¯ü€[làu€/ÂÃÛU•—[êR0V $ÐMJÏ< zÌys‚€€9þ",² ·NÇ ª¼Â¨ì×í»%¹×®]Û‡|EÖ—¸ð¿¿ý…/|á÷rˆóˆqÝ/š‚вRD8‚°¾©PH ¨wã‡ÉŽ?ð6ÜŒ|ˆÊRÃñÕ5†˜ÿþ÷¿ÿ›–¹ ê”ÓüøÚ•+KÕ­=ô“3ÇQD@…Bª¨ÊïI4ãß¿¢´¼q:~UU½Ø“O<{útŸì#?£ —ÿë¤ðÖr@Ë*(5¸xæ$>Ò -åŽBªP¿ u|ÊïÃåªþÒð]¸‹JÎñuá”ZXþs­X¾¼¿·ð«úÈÿù† Êm~—š”t.—ï_Iüßµ=PÄöô·:Û³Ô€n]~ûÚ%LJ6ã: E –ÝÐ&‹ða‹åX;ñb®¿ý›’ƒ_ÍÿÞ^s9Ôþ÷ýïÿTTjÒC?¥?ݼyójx±òƒ‰{û™Åöœ8¨4l{î}Vïò!]cz˜ÉÊ…³X‹1¥p v] ñ©sor—µØ»ô ’§ƒºö$ºàú+þ?Šÿÿµi¯l«Š}m[ºÍ¼¼B¶@ö_Èú–NÖÿ•wÊMAÿ&âù>á—&xŸªöºÛyHKˆ¹™fDÞÚ}ØñƒWÑL†A‹Wþ¡ïÌB˜æm»+ÇìÃÕ£ÑÈÏ,€ä`;ôªÔ¾V"§õ]êÛÓϾÚúeOß¼U>¿É›o¾-ûÆO¸å?¦TáüÕ¡¦±®À }ÑJTs¡ô€Ò„ÔÄxœ9~k—,ľˆS¸qò!ÌIY ðþÿ±wÀvÄP†kÛ¶mÛ¶mÛ¶mjÛ¶mÛz¨Ýÿ©vf;M{۽͛ùžO±'Ú$›Ë0_z}‰1\PÖ˜ü“½ÿ㨱þ5zÇì Œ%J”t7®^=©F†+¨ç éé?íÓ7•ÞÕ^}êýóð&ÃÝŽÛq&OœØ–!Ð[uiÿÈ‘{ÿ¡Ô…xˆ ñø¯ã&ƒ"†_¿~Sñ ([Yø¯or;vãÚµÓ±cÇÎL]ˆ…à“Ž› ôpˆ× nÝ ÞžžÞœü_$Þ0>½Èì«Aƒ5¥Ä×èý?€Ä+–-ÉCBê=ÕçÖßS4âõsØÇkz¢÷D#Öµñ®ÏLüíÙ¹se?9bª{ÞßÉ+ñ‚ š‘G…wÊ­À‹ i“hËØÇ ›xÛBîúxå‰ÐרlöñöySŸ;<þéO–[úߺ~ý\ʤIeâ/>Â"€Fïïì A$$*UªTIfDoq*ªÅ…Vì.´urtâ5*–m¯¤oß(þåø'ÎŽWž@7^V_žxy=îжmù"ªî¶_·8. 1‘t@ß¾ÍYxÆ|€uÅ·O¢oO%^'Î¿Ž§º¿óßÅ[ÿyÏ>žøóvæôé²ã/â ¤ÆSn5!ñröÌ™ƒh)åuå†ãÉÞ YòÛ¼qãTÊxZ$D8¡¿[Þ D@"¤]»fÍv J«éèÄÆSÈÜÖ}û–ûñãG–ü’ ’Úò«Ñû»Ó­€Ad$EÆ-›7Ï`ÂÄ4†ã+?Ïù¯ "DŽO³þÑLcÍß­ç‚!’#³ ™ä¤”çævÀpuÆÿ‘ƒW‡>·ÜÞ"¦ºï7CËùèHLË—-Ǥàk&P‘xÃù+yÆe÷®] ‚ ’“rœ ± þ®ò›F R Ô)SzsŠ·\Tu¡½½¿ýhÍâw~!ÿl¼5kâšx¸e<϶HðjÅòå(»Y‘q~mÉόҷnÙ²(Î:IH%Ë2yò¹}’Ï÷þåx™é÷¸ÿî¨#:É<R#6B#à¯W~Ó$Gú)R—ÙTæäQÊo“Å:©V¿goýs—ÅÃññ|®ß/ô㕟ÎZ8wæÌ®òeÊT–²Š”ˆ‰Pz=¿i‚# #=²M?¾ûý»w¯Éh€ÆÀ®¢ÙdnoèSëû^‹,Å2_îOe4¢!„þ=¿iü"(" >Ò CÚ´iKíØºu¶¼à(IåB—$Ý0dyïíÇ%ê×G^[¡B…ª2atHˆHö{gûÍ>@ƒ˜H† ÈR§NÚÇ^CbžÓ¨aünTøÏÿ-Ãý=½zôhEÌŽŒH‰8‡À¿}ß4jÛppDB|¤FFdçeÇØ¿Ã1OidŽà©îÙ€éíe Zn7ÓÉœ>qb[Ÿ>}ÚPær#Ò"!¢"$üù~fÃP „Ft$FZdB¶"EŠT–%˜ë—/Ÿ!y¯¥1à ß½E0 u^Ÿ”YÓ¿{óæÕm›7ϬËe,'2!=’"&Â"üºfÈoßð ‡ß5Yx‚mÛ¶mºqݺ©W/_>ñÈÃã‰4Ÿ™¹5#CÊ€Ü6~.2Äg^é%KÎ8´caÿþýÛG‹­è§5ýÌ_Uü؈€`·×7s\|ÕDGB¤BFdAVä/]ºtõ)“&õÞµ}û‚‹çϹwûö=I´ ï$ñ2Ä“äËf#iýù¾4R8dô Üãd!œš£IåKr'9’SÉ­äXò-ä{”7ïÝ{H'qòà¾}+fΘ1´víÚõƒ VXF”È‚LHƒÄˆ‰€ðóOôú¦!P#‚0ˆ‚8H‚Ôª1²!o‚ JÕ«W¯þAƒº,˜?ÜÆ f9thã™S§ö^½téìí7nÜ»sç®6w<ööðxÎnÄט·P½‡*p†Ë©†äD¼ÅkrõÂãÁƒ'ïß÷ ‡÷îܺuóÚ•+8uêÀ‰cǶmß²eá’E‹&Ž9²W³÷ìÙ®$Q@ágÛ¶mÛ¶Ù¶½ü9•üc…Í[É·‚S×N§sffæA[$°‰5ñõ|?´£(F>r’kÕWO†¹(@)ªÐ€vôaSu«XûÁ¶°Ëñᤥ¥å²··÷zjjêþøøøãõéÉl5›—Ëðz=¡@À …|3üš˜wah ´~¿ßëñxÜv‹Åùþþn9??×ñVü800pÝÞÞ~YRR¢­è{Øþa ¯ÿ2à0Qô£¨F) ‘—_½äI°RT¢Mµƒz3˜Ã"–±‚Uñãò«U%)¬ýǪ4]–Æó˜ÁÆ0„>t¢õ¨BŠŸúg|5䣥¨@5êЈ´£=èC?1ŒŒ*)gXHÓ^t¡­hB=jP‰2£yAõ¥Ådðë„P€"‰^‚2”£RT¡µ¨SRN ªD¥´-C©4/Bò!>ã¶÷jR9"y?ÈWRZ„ p¡V÷/íÀ!€ ÿ¯}a`6F zŠMšKIEND®B`‚ic0896‰PNG  IHDR\r¨f8õIDATxÚìÀ!À0 ý#Ÿ˜m>T{옌õXÇïÚ¶m[Š6Úp¥h­¯mEÚ ×F¸vüÙ¶ðFwÞ½=çÌ÷ïÞÜ×aûæü’_ýh{Nw‡{½àAð(x<<Ù9w:ÆgÀ³0}vŸ˜O=7Oïýù÷‹ù à…/º!bÙŃŲabù%a໇˜óûá°ÿyAº2†öÛ99ûù x:<ž†‡À½ážp¸‹Q”¼ânoo?ã ˆèzf~(I’W0ý±ˆ|‹ñoÌ<Ó‹1^WÁõp+l%"‹q:èa’‘¦HÆ®ÁŠHÔ²!"Q™9«äH“œýì`ZØkp#\WÀ¥p6ýÀôgðmçG1»÷þÒÞ¦±«Qš›ÖÖÖpà¯bæGá'"2 nÄ´ŒÁ‰¯Nž£=viCŸGDßâüxã묵G¥ú¤·êDt/üZDV†NfV§žÁæÁÌ›±îgŒŸì}ôØÎ(Õ V«í眻ˆ~bæÖ¢Å®ª™s¦ÿbæû1{¸QÊIggçÑÌü´ˆ,ÎtsU“»L¯gæÒF)ÖÚ#™ù•ôàŒcÑ«êàFÐÁÌbò £LkÖ¬ÙÝ{ÿ°ˆ¬™¤ÂWµ´0ókmmmeBo÷¯‘ÿKRøª6‚…Dt³QÆ—•+Wîæœ{ADêe)|U탈>Z·nÝÁF{êõúIDôk™¯úª¾,Äô,kíåFÓâ¿–™W–½ðU5EDZ½÷weLž÷ï‘ŽŠ¿ªM@œsOc¶ƒÊ¯üa_RU‹_ÕGïýÛŸþùF)\ü h‚âWµ ¼ÿÌ3Ïlo”Æhkk»/­ûf)~U›.h¯¥¡â¿Y˜}³¿ªMÀZû¸QÂlÚ´ébÙ.~H”ŽcŽo^óš'*Ü0Np»Þ(ÃY²dÉ!œ$sºõØñ‘e±mÃÛŶ gâù>£yXö|hÏ3W&Cùâ ëA:Þ°víÚ3Œ2@ú‚Ä9÷iœl¡¦5Þzl8ãyæÂy*yŽ8£yMž¹TyØp>%ñþŸß~ûmO£ôP«Õî\ü9`¤€ ç©ây®RžyÌòpLó4ßOäaJ{{ûKÆ(fáÂ…'0óú.@D}F‹ƒ†/ï1š+ž§qÎSó\ Ï%ËS‘ê…-·ú ið(dôSÈTä.\òK·a/. pT`Dû÷ïÿn£IRSS›Ë³¿‹Íþ†Ô– äô4ëçU&ãzÆvìz2ëöÂúG½±1¦?6ÆÀŽ;ƒ6¦‘1`ÒûŽEzï10˜…¢í‡aI΀Óíò€Àø®Tåçi QÀ§H555‡ÕŸý ¹¼Ž/I¤—ÆâLòRlŽrüúaSÌ&k©ÿx2MBÞÒ-¨zôN‡ÃA=‰9"¾ûÎ;ï|Q÷QÀåË—/IR¹›å™†Ts|»dEbÑmK˜ úbýÃÞ²³÷gN//Š úÈQA¿qÈ™½·B²X ¨,·§AHzû–ÞgZú­òìo8¾Ã‚¸‚k87B|Y}8‡÷ ¼ÒûŽÁ {ÚrT\½Éb1@ ¢è())Y©ç4àÓùË_>o±X®ªÃñ­ˆ+¼ŽCqSA³ýÎñÕ‚1Èž² ×î©bA‰¡¡¡ßÒmpæÌ™?I’Ti„ÿuõ o°Ÿs|õÀ+½_­ˆàz4$«‚:6‘œ=j¢»(€}“É4пÙß9¾Sr ¹ø.ŽÄOç?Àà#‚é+PyçQ­b¡ÓøÞ$Éç(  /o–ðiRYié.ß`¬ãÓÌŸ^ƒ“‰ó(¿ç?8àA@ˈ9óÖ¢*æ9k*2¾CPUUuEö•/¿è òÇ7åå¿ç„tKÿöÔ®KÊ.§å<ªêÓ¬ÏXk`ò®ŒGޒͨ‰KÖ@C‘VÇÿoÑa·ÙòæÍ›÷ =àS$ùÖȳÛíÕ”ÿsO’¸ß¹Ÿ•Îb{NÜ¿#¶‹·÷ëß æò˜Â*.¦­%g¥s\ €ó×î†%#‹µ+}¢ïœý¬{å¿“ÜwïÞ}ùnðäÉ“ŽÄTü7õìÅI5{ óÉK*,…¸eÞ­±C©‡­ã78°>j(2 Š¢ÝÇa/(f…B%§ð÷ú‰íy;MÚÓ‘––6ŠEÎzÉÿ©8@]œŠh{ú¹^í]Þ<ßb¯DLN$XËîFÎñµ1Ì#f£ôäE8*«XZà˜}Äœè(ÈÏß$»Ìg¼jð}ˆÏí GÖ¬àò´í&ÞÄþ縖]€o1î5Y£âÆ}HlÅ€]C £¢¢â’ì3_Ò >+ë+•••WãË&¹´¦d•¿ ÝyÞ%½¾Ì9u ¾‡ wþ:T?O‚nVhìbÏHzï½÷~àõO5ôüÿ³o¾ùæä•ÜØÀòü’ê,\NßÀøô¾PHéAÁ†}°ÒýÚÉØè`µZ 'ý[Ý`Ĉ¿±Z,åù÷³Ž`ûãP®À§sˆëçÃv-ΚJ]³Æ9I¸\pØíöcÇŽ½N)€.°{÷î—åÏdq±ê¯@’Žþ'@gÚ¥wà…7ÏáòüF RzÿI(ó!¤å íœéÅÏõs7¾ ƒÝ#àÂ… -¼øtC_ølTTTS§$¹YQNâ…û]ðšØ¥ ÚKµÂý¼ŠœNZB| y¾ó °. ƒsõ0H+†@Š éX¤ì8Ù²¡`¼^Ø‹%ùú~Å×Ä¢#::ºùŽð9™fmXâÅ©-?¡ ùü%ûn/ ìY¸_i-ÁMÓnl‰¬ç`³É”Qp® ´jØGZ>Òš0HWöAª(ô±eCIp:¿è\Ïö$Iáýt$ÄÅñà3 >i6€ÀÑ9ñ¯ûh/ØÞíèL{ó÷<-‡û=¡¼žo ]þïf…އ}…Çé9­”¡°l ¤­!=½Z6”ÜP#N¾S‘½D{:’““§34ø€¸¸¸± >9"{M« àÂýdœJZD3þ' ÷ TÌÁf±´`¤#Ë@i@r¹Tøþ´iOGJJÊ|Ùw>OþÓÐð¹ÄÄÄé#·Žä ÷«­e¸mÞçC¸o ½ÿDäKþÉáM ®€TUæäÔݸ¢#33s í lèø,Q,))i¶Þà"ß—\H*ºƒ}ÏÆùîÀ ‰¨Y8\0û+¤ËBÚ>Rü]HN$·[wÈËÍÝIÝ€º@rbâ<½€ùJª³q>e…ú>†û¨ðW8~´ÈùEi¬Á"×A*ÊöDN§nPPPp„:h)‚nèøBzjê2=Àíô܇ïqîYìxFkú¾:žù¿e:¶eaV ],Š6|éÁ9Hv+ u€¢ÂÂS²ï|Mø¢ÉdZ£ 8þÏúN ·" 'æ 6íÍþeÓ¸e¿ºiåP.‚”•Ì¢ÆIpìé(,,ŒÒèþËf³yvqØYpñ¸÷ˆìé5_/>³Q´§Yßf¯AtÖ*ò îÊc@TõÏ>Ž•þ;»¸H8Ò­ã¬5ÊÑ€òØ#q¯Òž ¸¸ø¼^ðÕL³yv8 œ‘{IèÌ<4¸÷)ýûlÖÏ*ÇѸ*ìÑ7@áåÜp8רîü|‘pß\H¦xœN¥1#WâÉ¥nc“·g¸(ûÎ×u€¬¬¬½”%˜¡~çÿæ·=ÍúV{5îfÀæ˜A܃6 øúç'Ëûë[ÔN¼:ÒõÃ,ÕјGßí%É{Rm\Ò ¾–“}@Ëb›w²Ëp$^ÍY߀iÐDX øh`ï\H™‰ŸY,õíIt”è òšæ‚ \¹%7;fŸP9×7@³ñÄ…¿zލ6pï$» ’˭˜Q ú@A~þ -Gp‚Öõ©WåYßåý™!`_*Xö P4pt9¤¢–BFP˜ŸZ‹p:¯.¯"‚Y#ÞÅÊÃ](% ú@ƒ©ê“ãï|Ž9ÇPã(‡Ú‡³¢ e'.Ò#¾ ´jÀßæk»Í—öò|ûPÔÄ|Ìñ9ÉpȘ‡­’ àKv»` ^÷Ù^ÿ`†§#ÿ3gÃ_D±=ݶ;?+3û¼‹É]_#‡W’§> Ã`Þ´fX{¾§œÈÐp}€Bý­±CpÓ¼ ¶BÔ÷!• xÏ ˜†N¥Ô€{º´Zcáþ¶PXn÷8}st%ÑûÆÀañiì¤<Ùí~ÛÓQÜ@_á…Û»Ý@ʳ‡^ççÀ‹Õz½EËZcý­¾-Õ¼ë‡ó©«PX“@6S6 ÖìBºœ¤õ‡Röt­,ëmë…0T%²<ŸI {e"’‹ܸËw{]GôÁy± Ã_ å¿ í™Ø}|ó¼œÿÿbççë3¿ƒeÛ:`Ó£à×6x×ò%Ì‚©<Á>jžÄ£pÑJØ— ¢þú ‡ûØŽ‡¡ú™B¸/­”=€C‚òØâÅ98'åñKj¼¸‹$‚ƒï¢ãFä^Z$ÇöY“½õÙ£ßêãݨ6@ Jž¿ûéH<-8Ée‡f§®ÇW m™@]uÔgpÇ'Ù÷†¢æ-ë _«<\%€`lJâ1Iv¾Úëþ;³ßötœÝ½ãØ Ÿ"PT0Vs¬½Ü+`ˆô´¡Í1qüUöbhö¨,…ëÊ>Ï­¸".Ïß ËõáäÀ¢<_,Z ,<Î éu/@p.( ©¯ãwÀ§¯aZŸ7±xU[l¸Û¯Þê´¬GÎ"q.r*ÐPwv2œ‡—xöÞ¯Zyþºa°F…¡:Açû €¼ÝnõAž6à ÇŠj~Õf†4AÄîNä°ªÖ(ϧe½gátKhp‡S‚ëñUH›ÇQZ j¸OgÛÑPT?äù~¦5Ùáp¸ô# &  \*ú¨­xüX}ª{ÛŠÙ£Æ.¥­C¹- ýp—µ…"YCêÞ¾»;5Ѭ}W-Çç›I0@…›¥«ÆõÁÄN/«éü|} ûX0¿%Ö]ííW}€fý½ÏÆ"¥4z;ÜÉéžÞh Ä÷<Ó0X®†ÉÊ9¾ºJEµyv«,Gp f€ Õ•åX6¢ &u~¥>œŸo+î'·¯o‡Ñý("øÄ³þՌͨv”A·Gu9œwR$@údí»k‡Áz: Õq‚<_MdÌ‚ÃV¥ $T”aaH[Ö\ÿêâ)ÎÞ+öwaÛŽgý]OG"¹äËáN¼ióx* ·éÖÄ ò|Õ%Ã&}*Ö28ìRC€ÚPR‹9šar×W¾­xîä¦XÅo;&ç§[‘—[óÐØwYœÇ"(%àóü²ãßáÚw¤á¨N»¥X0àt"?“Û8±¶âžo`áb¶íx ¨•÷–ykèiœ‡ä€óê~zp‡¬Ð6]ë%n›n@ÅöØkrõ´˜™i=ßøúÀôocñ¦Ö¸k>ãð®ÛGa;6Õ/¹p?8{µ ‡³áÀ€iq±^'dž&uzs´Dâ³g¨¬¨@c?j,Väåe EfF¬ Ûa·ÛQXXˆ ùó§§›Q’ºÕ)ÃÈ 5[e<5éâÞb=Au#î[5É ‰ˆ—£€¸'Oðü9òrr@= z?¨&SZZ “É$;~ºÙ(LÛ- DH…­â™½àÙÝ+äxš@äÎu``z!ƒ 9>ž:Ààv¹ôWõw»QUU…¬¬,0Ç'1ä¥EjÃ`+‹…C¥ýŠŠ‚1×£4jFºt|’âÀœŸ‰RAzJ ª*+¡—Ãjµ"77—Ÿs~²“~Ev¾P€î p¿þ`³±³"ÈgôþÃÎ^_$%{!M£/ž`;5¡›ç"‘Ï 6èœe6Ãfµ¢¡´£¨¨ä䵟@&²Òï’óiÖÒ[(O&îuö³àu%{ ¶ø Ag%ùlÏpûì!M€V!h92úÚ%$¾ˆS@í´ ñÅ äå:ÊAK¯eee0›ÍÌñ2#3=Vv¾Q²†kÅ׸1&{JŽÍ‹·o à×þÑëÜïvþâÁ®dÏË{7 }šõ!Ìêû>ݾÙ±y§WAJBÊJJ(—Öv»u5²³³9ÇÀœñ •)c5€Ë B¶‹܇¿7Ÿsvå¿)¼Nbóêñ]2þtÐRäÜA-ð8ú.ž‹À§´b‘šŠêª*hí°ÙlÈÏÏWÈóE2ɈGEÊD`(¬Ej×j)±ì$ß&ºÆ^vN'ÿ„öt\>¼=(¨¢ÍH BÚá飴ôçøú@Nf&è3û$‰*Ö‚<_ SF*R§Òfm 0 »›sh&»è¬îuÎN·PpRîg|³§ãÂMšÀ¢°Nxñ8ÖðiAR\Š hm=(ËzÈÌÌäß?¤¢m;–$€oßå¶é’ €í ï ½àø¦ÅšÀúi2â ~Ûqiq1k+æ¶éGfT¤EÀ‘6ö´°à*u0ìùûa·»ti¡ÁßVÌåùÁ’IÀ­Ømˆº;çîÍ ªÎÞ™‚ç©‘p: èF ¬´´4rÀ Ël2cݵÃ}rÆE® ªF_‰£¯Â-9 Ô ¬Vîg%²7jbQM€f~À„×a⩘rzuP5þdN>1¼ÀjUí[Â*€€ ×jãdD>5X­$AtÀÙíé̹u™Æû ÐêÃúkd¬ÔN=½·Ã©0öüž¤èõF«UYü…âÏb[§¶-×z' € Ö^ݯ,Çég×É©<öèwñøóÉ^ÿ°Z-”Ï`p³V` íˆ7@â´úê^ à:@iì)I<.ůéêK €Ó b7 €—wkgŸß`œ ¨/@Ô­ßÀ@zF:–]Ú‰I ¬œ{q nGðP¤+nàÂ~-ßÈ€IVjz–\Ü®\Œ¿£"Œ ¨¸tx«†î Ø–»' ’ÓR°ðÂV«4€+ ÷àvH«¶Ú((P FpõØN-ß؀Ʉ„”$Ì;· “5€k‰÷ëF `«+-}À“{5ü\Ôô"9³¢6`òimàfò#@ÆŸ­.ÐSPT 6…‹cüLgì].àÖ™ƒš~23ž$>Çô3kÉ5€Û)±KLll‘xgW›þØÓQ¬GØÍ$ø›Oö.—ÑŽc‚¦ž x²Ö³ dʸKΧ MˆŒÀý´§p9$ßÇ'ÿ:ÿÁßô/’ø‚ùnOxtí¬–ŸlÀœ‰Ï¢i@#XS\vÉ·qHD{Ý à¢;Ý<½sY3 ÿÈëœhÀ«,Q1×Éñ4€g™‰2bKl¯wäçNâܤ>|Í`ßÊÙ>€ÃÑQšE"ñÙ)p2¨&—Sr"1ö-Á‘jcKð´aF àc5€M7kb+0‰V"’rÓ ¨€Â‚‚‹Á@êóV…×ÀR૘?¤ž>¼O+´˜œ–Œ6ÉŽ§ÀÔÓk–o†Ón7 Rà,…¿ùn/9$˜“^€–ß&ÓºË)@—×ÐçƒcÉì)ôð/-Ì& ÿMˆ8ô½VÂøcë1íìÊ ÏþÓÏ®EfQ$›U ¾ƒ‹å°;gJÃÌ>ï‚äø4ó¿Ž6/£C“ÿ¢Õ[ÿF‹×ÿ¹“ÇàùãÇt—ÞFrüÔ´t,ÜûÍ&_B‹ÉWÑnö Ù²“O­! 3£Ö#¿´ÖY¿În±4 |MÀJš½^[>:¾²==#¯8/s4£ð; Ž?µ»G#Ú¿Š®ïÿ­ßúZ¿ý´ñªù«ÃÈ=pëÊ%¤&&5äd™ðði2>\{M']B«i—Aj9õ ZL¹‚Î Î |ÏL=³ŠPP+òœs›P\^‡ÕƯZ¿‹Î$e{åG·àœ”ûðìg|±§{”`aH[ÚŒÇ÷Îúc:½ŠÞÍ^"§'ç'§çÔüµ¿¡{«w°wëFzÞ?I·È4›%ëðåçè6ïšNfÎÏôH=—ŸÀ˜C›(h3Ò‹ÛPVY;€o‚ñǽÇ{ ?7— à¢/±²¬KGt¡&œ€åù[¾Œvïü­8ÇçÕêÍ¢åÿÀŒqá¸ûEºÍúÏâS1{ç}4Ÿr‰Ä^Q-¦\E›ÑÝ!L8±Ž@%ÀeWv¡ªºv朓N#€¼¼óÁ€–ª*¬Û›zê5ܧsh›WÐñÝÿ2Ç÷I”ôn÷>öïØìâ<hÖ'½ò½^Ç,ä÷A‚ösÎ!dÛnL‘ëSÏÖ_401r%V]Û KMZ0—›¼· Û0m(&tzI}Ç÷:ÿȯ¢Û\žï«äHàŸ M‚[W/S4@ŽÚà`2yfýGÏ’1m[´`Ö‹Õº,:‘û¶Õ[}€š‘6Þ:¬²ó8LHv,K]xª‡ûã:ÉËzÍ_B.ÏI tmñ6¯Z†O#%!¡Á ;Ó„´ô ì8ó]ç^e³¾*j)C€Î½WÃØ#U¯vEGÒ `€ätºphÍlÚ¬šãO’—·âò|EµŠ„ÃûuÁ™ãGœ@©f@¡>9ÿå{ _}Täk1Õog×f^ÄÀ 0ñ¤zõÚ |8æuPQÁí<³sÝL•@©A·%‘µ« -àFÒ¸Œ@=äfgŸ v s=ʯ9þøÎ¯¡_‹—ÐömÂ}Òª„ôꀣûvS+1u™Yf˜3͸vê.f؃æãÏ£9sü ‰Š„­åsŸUG1îèOZpÆ÷@¬)N›5#€3Á€$IHz|ßÛ ,ˇöÝ!­_Fû&lYO;¢Vbê˜0| .E¦h€êõrzšõïß|ŒÕ÷cDÓ¥ñî"ô´-g\ ’óóõ¶³.`ð¦}˜ImžEɹFPUdgŸ&¨87#µÖ~qûn¸œçwáò|퉊„íß} ó¦ŒÁíkW©>@Qê0S/'Ïbâ°mÁqŒiáï.ƨæË@ o½íÇG¡åô+€[6¼ŠNóÏbø® °ú€p@^I>*.ÈÉ9¥k/–õv–aQh{Ö¬˜çîøz5ý×¾«e±ú@׿obåÂYxp÷6A`ò:~üóDì_};®F˜<ãl¶äøL£›.Á€ÞÛ5ðmÅ=–Ä胛Y}@± pñ¥í¨ 6`KŸ¯+D* pP°ZY3bûnÊóUXÖ f}€@ЫÍ{X±1÷£|süÜ,$Æ'ãèæ rT´NvüÅÙ´¶ãóêüa¤V¢¾>0ýú­9Œ ÇÙ¶c…& r~õÆ €ììl€_Ë*xMôk­BàÑõ ØRàGy¾|‘Ûw;py~ÃÕ¨PØ·CSlZ¹±÷ï3ÀB}ãßv 3zo…ú#ŽOúPކvÞÈ9Ÿ¶@@ÛŽÏc(ÛvìM Ø À±ØK>­jÀíÖr²²NðÅÑyGöá¢Z@·§¥@Êñ)ägÛtù<_' xƒ Ö-[€w(5HD­ÈÎÏÀªúêÇ=MÄ‘ç1½—×ñ?XÂ9ºÝCk- ñÛŽžÁˆ=ÛÁÚŠ Ñ©Oü$ãûø¿ÆÎz@Vfæ b~L55tV~¯ö$*æ¤'cfï&Ó‘¶é Úwu‚f2z´n‚E3&árÔP–”Œ,Ùé©‘çÁ­Çص4R.’®Eçø>@® „µ[6“/ Õt­9?ßVL= =#Žcô¡˜µ¹Åù´€›D˜¸×œY`OR°×gpç ÜÏþIlÏ–ív¬›7Í_ù³Ž_\#h×ä%„÷ï†5K`ÿšSX¾£[-G˜ Ô÷% è;`h9-ø`Ê9¬;w ’ýcN[SÃUñØõÝÞVëw=Fð‚Iéb*ý³åí¹÷x–³³@½õM_ù‹Ž]¼j@íÅ-_û/BšÌFøûKåª>9¯zÑ2íÇeAMëý‰¶êr *á°YùqÇŸk‹·gÙëÙ™™Ç(|pŸ.ý²gr:$ä˜M5¸'š¾ÜX!@ú7Ú¿ó›͗v¹ªÎÏ¢€A=·ÊQ€¶ðžìü#ÖÞCv~œv+¬5ÜØ;»xì}r è´À  Š ò1{ÂHhF4°¬^Ôeä MB€ršùgîŠAqI%9¿Ʀ^#€¬¬£Z‰ÒKu5¶­‹åÆ-åÖZêŠ ‚!Ö£õÔKšr~º1I‹)—±ål,4lÌùµ€]Ef³Ö@ò>:̉k¢Ð«í{T 3P©@σš)~0é"z,¸k±™pÙ­°s!»ê2m6aТÜ.Ìi)˜2j()"0 –ä(`x›Uh;鸇‘ƒzx¢AmÀ€xU kø1Jê}y/\žõcò…>Mà¢n’”´Y GNT–•açÆÕèðÞ+ÔSo í°­§\¬· [9×ß•ˆ Á¬¯uг4(}Ö’×5T°h€jÏcaì°¾höÊ_iƒ?SÞC …ŠQ@‹)—ð¾ìü£7ÜÇÓ”¸ÂY_ûÈÎŽÒ ¾òâÅ‹ YNIBue%ŽìÙî-ߦ%C~, †·^‰vΩR¤p¿ËÜk8t5ÕUÕ\G_C@NvöiÝàÙ“' õíÚ¢¾Œ”$ºO?Ýœ“dÀÇ( Ÿuª4›| ¤Ù»c‘žUÌUø:²åæ9½àËcbæ3èE´µØa³áʹÓص•wÉð_>©Z,G§Ñ§~ùú/½…KͰ[-déilÑ‘ž–¶‡Òg=àK2fé $›,ª æåbãŠÅr‘ðe®Ø€r0¤ë&¿Š|ëN¾@aq9Ü,ܯ®ö÷;Ô€=/:’7蟕õÅû÷îM„Ë¥Öb\3¢§Ó’á‹'1˜Û[êÙžàуsõ€/8p ³gYÆ{ÄRrLfÏ^óÕžoç—=û™Š„”äçdƒnÄÙ^1-0Àž'Ðg {ªî¯9þye,Ü}Wüß|ÿ™$œ |·;>ƒEËÑwïN"ßÑvîÜÙVþ`nÅ &~Í×÷ÒÏþÙóv~ÙSšˆ^<ŽÁÔÑÃÐ⵿s«d5[†­>zªUö©wÒV ÷óá²[`·søpýIuùþø1 –r¤á³=E’ÏŸìùPƒ=>CbñâÅMjªª,°¬ XLÉÞâ§½EE{&k-Ñ—i«±àòÙH éÑŽÒ¶ÝØ@­( mxOžõ.» ÷Mž•›…s‹Ò÷¯ðš²=Éw{«:ö$Þ¾Vqy÷îÝõ€¯têÔéïåå…4+rŽ¢ð»ÀÑ|°çTOö$Þž…uT$,), –bz„5Q} Ñ€nFÖdÆw\ƒ='Ÿ¢¨´n»…9+ï¸âï·¯©Ñº=¥ÔlæX½zuSò†€OÅþô§?ý®¤¨(Ýi·+|¡œ”.¤ºöJ_T=ÙÓ—K HONÄ’Y“ÈiÅ Ñ`ø{‹AçíóN"39Û»q‡›¥•¯±àoþÚ“ÄötVßžJZQVVܽ{÷yð™Y?ÊÍξ §Sà°Â×êÇž@{Š„¨‘èQômz¼7m7¦ú@£@øûK@Îñá^°!¶å˜êû·o¢›“²ûê#i½¿ÉBLé²gwÝDYa)$É>Ï7D“¤¼{vMš4yꟗõ#–lÆ—®$V ‡–lˆXˆN¼Æ … ¬À7¶Í \uyæ|8%lž<ßBpóúõ4iz'ÏO5t|Ž0nܸw«+*jh +^Cž;I’âžcáôñä¬ÞF¢7(Г…é¼uö ¤Ç™A¡¾Íj|÷ÿ±%@÷æ º{ðy€†Þ ôÿøÇ(,(Hö潆rØìdÅDßÁô1¡ôÌ*jTÜ£' ¯xq÷“a·YdYŸ×[(/--n×®ÝË^|NøŒwWÓROøV4$9 ¨éÎõË7¬šSG¡,­ üýÅ ç_>rboÄÁj¡§/Žï‹ÜN'²33£e_ù¥¬¯s= |)ð»çΞLÒ—‹bˆuJ»)q%êF èF÷&¤'ü´¤Gáþ¢a;}þ ý?z*û5þ|V£øðÁƒµ²¯ü€[làu€/ÂÃÛU•—[êR0V $ÐMJÏ< zÌys‚€€9þ",² ·NÇ ª¼Â¨ì×í»%¹×®]Û‡|EÖ—¸ð¿¿ý…/|á÷rˆóˆqÝ/š‚вRD8‚°¾©PH ¨wã‡ÉŽ?ð6ÜŒ|ˆÊRÃñÕ5†˜ÿþ÷¿ÿ›–¹ ê”ÓüøÚ•+KÕ­=ô“3ÇQD@…Bª¨ÊïI4ãß¿¢´¼q:~UU½Ø“O<{útŸì#?£ —ÿë¤ðÖr@Ë*(5¸xæ$>Ò -åŽBªP¿ u|ÊïÃåªþÒð]¸‹JÎñuá”ZXþs­X¾¼¿·ð«úÈÿù† Êm~—š”t.—ï_Iüßµ=PÄöô·:Û³Ô€n]~ûÚ%LJ6ã: E –ÝÐ&‹ða‹åX;ñb®¿ý›’ƒ_ÍÿÞ^s9Ôþ÷ýïÿTTjÒC?¥?ݼyójx±òƒ‰{û™Åöœ8¨4l{î}Vïò!]cz˜ÉÊ…³X‹1¥p v] ñ©sor—µØ»ô ’§ƒºö$ºàú+þ?Šÿÿµi¯l«Š}m[ºÍ¼¼B¶@ö_Èú–NÖÿ•wÊMAÿ&âù>á—&xŸªöºÛyHKˆ¹™fDÞÚ}ØñƒWÑL†A‹Wþ¡ïÌB˜æm»+ÇìÃÕ£ÑÈÏ,€ä`;ôªÔ¾V"§õ]êÛÓϾÚúeOß¼U>¿É›o¾-ûÆO¸å?¦TáüÕ¡¦±®À }ÑJTs¡ô€Ò„ÔÄxœ9~k—,ľˆS¸qò!ÌIY ðþÿ±wÀvÄP†kÛ¶mÛ¶mÛ¶mjÛ¶mÛz¨Ýÿ©vf;M{۽͛ùžO±'Ú$›Ë0_z}‰1\PÖ˜ü“½ÿ㨱þ5zÇì Œ%J”t7®^=©F†+¨ç éé?íÓ7•ÞÕ^}êýóð&ÃÝŽÛq&OœØ–!Ð[uiÿÈ‘{ÿ¡Ô…xˆ ñø¯ã&ƒ"†_¿~Sñ ([Yø¯or;vãÚµÓ±cÇÎL]ˆ…à“Ž› ôpˆ× nÝ ÞžžÞœü_$Þ0>½Èì«Aƒ5¥Ä×èý?€Ä+–-ÉCBê=ÕçÖßS4âõsØÇkz¢÷D#Öµñ®ÏLüíÙ¹se?9bª{ÞßÉ+ñ‚ š‘G…wÊ­À‹ i“hËØÇ ›xÛBîúxå‰ÐרlöñöySŸ;<þéO–[úߺ~ý\ʤIeâ/>Â"€Fïïì A$$*UªTIfDoq*ªÅ…Vì.´urtâ5*–m¯¤oß(þåø'ÎŽWž@7^V_žxy=îжmù"ªî¶_·8. 1‘t@ß¾ÍYxÆ|€uÅ·O¢oO%^'Î¿Ž§º¿óßÅ[ÿyÏ>žøóvæôé²ã/â ¤ÆSn5!ñröÌ™ƒh)åuå†ãÉÞ YòÛ¼qãTÊxZ$D8¡¿[Þ D@"¤]»fÍv J«éèÄÆSÈÜÖ}û–ûñãG–ü’ ’Úò«Ñû»Ó­€Ad$EÆ-›7Ï`ÂÄ4†ã+?Ïù¯ "DŽO³þÑLcÍß­ç‚!’#³ ™ä¤”çævÀpuÆÿ‘ƒW‡>·ÜÞ"¦ºï7CËùèHLË—-Ǥàk&P‘xÃù+yÆe÷®] ‚ ’“rœ ± þ®ò›F R Ô)SzsŠ·\Tu¡½½¿ýhÍâw~!ÿl¼5kâšx¸e<϶HðjÅòå(»Y‘q~mÉόҷnÙ²(Î:IH%Ë2yò¹}’Ï÷þåx™é÷¸ÿî¨#:É<R#6B#à¯W~Ó$Gú)R—ÙTæäQÊo“Å:©V¿goýs—ÅÃññ|®ß/ô㕟ÎZ8wæÌ®òeÊT–²Š”ˆ‰Pz=¿i‚# #=²M?¾ûý»w¯Éh€ÆÀ®¢ÙdnoèSëû^‹,Å2_îOe4¢!„þ=¿iü"(" >Ò CÚ´iKíØºu¶¼à(IåB—$Ý0dyïíÇ%ê×G^[¡B…ª2atHˆHö{gûÍ>@ƒ˜H† ÈR§NÚÇ^CbžÓ¨aünTøÏÿ-Ãý=½zôhEÌŽŒH‰8‡À¿}ß4jÛppDB|¤FFdçeÇØ¿Ã1OidŽà©îÙ€éíe Zn7ÓÉœ>qb[Ÿ>}ÚPær#Ò"!¢"$üù~fÃP „Ft$FZdB¶"EŠT–%˜ë—/Ÿ!y¯¥1à ß½E0 u^Ÿ”YÓ¿{óæÕm›7ϬËe,'2!=’"&Â"üºfÈoßð ‡ß5Yx‚mÛ¶mºqݺ©W/_>ñÈÃã‰4Ÿ™¹5#CÊ€Ü6~.2Äg^é%KÎ8´caÿþýÛG‹­è§5ýÌ_Uü؈€`·×7s\|ÕDGB¤BFdAVä/]ºtõ)“&õÞµ}û‚‹çϹwûö=I´ ï$ñ2Ä“äËf#iýù¾4R8dô Üãd!œš£IåKr'9’SÉ­äXò-ä{”7ïÝ{H'qòà¾}+fΘ1´víÚõƒ VXF”È‚LHƒÄˆ‰€ðóOôú¦!P#‚0ˆ‚8H‚Ôª1²!o‚ JÕ«W¯þAƒº,˜?ÜÆ f9thã™S§ö^½téìí7nÜ»sç®6w<ööðxÎnÄט·P½‡*p†Ë©†äD¼ÅkrõÂãÁƒ'ïß÷ ‡÷îܺuóÚ•+8uêÀ‰cǶmß²eá’E‹&Ž9²W³÷ìÙ®$Q@ágÛ¶mÛ¶Ù¶½ü9•üc…Í[É·‚S×N§sffæA[$°‰5ñõ|?´£(F>r’kÕWO†¹(@)ªÐ€vôaSu«XûÁ¶°Ëñᤥ¥å²··÷zjjêþøøøãõéÉl5›—Ëðz=¡@À …|3üš˜wah ´~¿ßëñxÜv‹Åùþþn9??×ñVü800pÝÞÞ~YRR¢­è{Øþa ¯ÿ2à0Qô£¨F) ‘—_½äI°RT¢Mµƒz3˜Ã"–±‚Uñãò«U%)¬ýǪ4]–Æó˜ÁÆ0„>t¢õ¨BŠŸúg|5䣥¨@5êЈ´£=èC?1ŒŒ*)gXHÓ^t¡­hB=jP‰2£yAõ¥Ådðë„P€"‰^‚2”£RT¡µ¨SRN ªD¥´-C©4/Bò!>ã¶÷jR9"y?ÈWRZ„ p¡V÷/íÀ!€ ÿ¯}a`6F zŠMšKIEND®B`‚ic14›K‰PNG  IHDRôxÔú› IDATxÚìÀ10 Ø?$!0…ß6Àƒ$ÛöØ» Y– ÃýlÛ¶mÛ¶mÛ¶mÛ¶mëÚÒ\ÎnwUåÉû²'¦×6Îñí(8ªlÄ΢ù|~ïýú!„-̶"²ƒÙÉìlv·ÇްûÏ4¸ÊÜ`î0÷‰È#æIÏÚå³é¥ª¾b^­Ç›"ònc¨ê‡æãŒˆd—ÙõÔgæ )H¯g·³Kó•ù¦ßšïªRÕŠ·¿7?4À]È QÏó–Ýþ¶¾×AU¿*ËUy}?7WyoÔtû½F¾ßÞ1¯‰H­ïcyͼà™Âûß.Í£"ò0€»Ì-æZs‰9ÏïœÛ/I’]ŠŸ±CÛ™­ÍÆÞû5í±¥'Mš4Ï‹/¾8UÄcÉôù½÷놶I¿hìú)îJfùÌ|àgÓà 5ã›Ìk·`©jÀ(Ó/„𻪦ÃÔ׿]ó€Ëœsû¦CzÇ›&I²ìÏ?ÿ]äEä@s·ª~d—?ÚåOndªZ€ˆÚª4&Q£Uõâð<€Óì¡JKK×Ïår³FŒ±ÎŸ}˜.î&<[Ußð«ÉÕ·  ¢®§Î€SÕæs·9çö²»×íÝ»÷tc¬ãïÆO’d;7…®‘QmCAbþ2/8çöÞ¯µÏ>ûð<Æ:BÞûÕœ ªØR‹=QÕTµÀ/æÆôäßÁƒÏ1ÆÚ|Ñ?§xfô¤vZð‰ˆAo»¸?zôè1KÄkùJJJðÞ%"ŸW]ôµëùþ3×zï׉c-²µ¿.€Uu`G_ô‰ˆÃ€ªÆ"ò‘sîÀ±cÇÎ1ÆžW›Á9·§ª¾câβèULU{8?ŽãE#ÆXí1b¦ôL[ù©³oíef®O’d™ˆ1Vy‹ß{¸ªþÞÕNæ#"ÊRÕÑnŒãxñˆ±nÚ&²ÏÔ"²§ª~×¶ö‰ˆƒÀðÂ¥éïDŒuóú×TÕ×»ñ?qè)"‡ÿóÏ?Ó–o1ÖE³‰wÞÂ-ª:‘ ?pø(„°AÄXWÜÝŸæœÛÀÜÝODdŒªfC@IáºñãÇÏÁ½¬Ëd¿²µ0€§Uu.üDDÜðK>Ÿß¶ÊFcr«}êßÝODDiªš„®±_"œµÓ Œ‹ú°BwØ{94|«Ÿˆˆ²B_ÚÞ€U:ÃÀ¸ð›Â‰~+©ê—M?ÖODDi ŒŠãøÎpH€q—ÿF6á'"¢BªpÎÝ4lذ;ÜÀ¸ø?ðÀÓ8ç.PUîò'"jAY"òîèÑ£è0CãâŸËåfõÞ?Ùz»ü‰ˆ( Àï£FZ£Ê¡WÆÚþx¿½ç‘Ûì,""ƒ“$ٮ݆ÆÅèСËø¹l«_„P"¢ÿÙ; Wg® Š×«bu3¬nö´8uÁêÅj8ý/ª8Åk¸Ô‡ÖÝÝûü%}ïÌGoÊT–üÃZ6û2'ÎýÁÍdäîÝÍbNÜ5Noö¸|ÀBŠ€´Lþ~šß¹~U¿ú<|±üTúߨùÉ÷"àÏRܱ°" -“ÿŽ;.ö‹ûä;ùÉO 4ÿÉ÷"`£ü¹Úý½HËä¿sçÎKÌìûâwË÷£’Ÿü•J:ÉO¾¼è|% -“ÿóüñÇyÞù÷úƒ ÆNþi þøç蕟üä»m”‹°ÝÕy–¹Å9föyëÓü€¶Ÿ?XQ~ò]Ãò±ò“¯¬—UÚÛ;+Ò2ùþùçÇmll¼÷ÿäalòäxE5ùâýä·ÿþ“ï1zÏ®]»®m^¤eòwýðÃG•ä?©œêW€œîu×Ecùc¥•ã蔃øEð1ðëiæ· ú£á¨Çï#+ùJšï±ú×ßÿý²ÆE@Z&ÿ·ß~{~yßgÜ¡ê몟TÕóºüä=ðµ0F~ñ€˜/ ×*ò[Õó-Çg1ßÿIð³O?ýôäFE@Znú+»Jõ£Š‹;%ÄD  ª8¨ÄGÅ÷Ï ÇsmÕ5£zïœîù@{¾±Dà¦9 üo>ÚóiþC> øºcŽùÉâ¸`Fq`nëë믖Fîë®´4‘üËF’[Šÿ l*‘A§¨úš .Ù©†è,ˆïžoÖŒtÇ׉Wò!–.ñk||øç¨ð ÿÖi½>·Ùlö„Çõ"Q¤eòÿùçŸÏðGñîÄâîƒ28òÒšw‡~O|×ð|³F|Cð+0+äÛøøz_†.Xâùw6꞉Pg¾§øt_ïã‹ Ç×þO* Ü\ûÊéw«Ci™ü÷ã7Ž)ËF¯Óõý(@Å%¨`AI,²hÃgvòýy>³˜ïÇÃ碊Þó_i’øøøü8~î"¾?è›_e > ˆ6n/ÿÝreT¤å¦¿ç÷³éôqw˜pI â*:Ô°¢EÜÝpå]“eã‹å»ˆ_D|@óÍb~Ñ |ßμZ…ØÿÁlžÿxLâÛ"ù€æKàþÁýVó‹b~òMñ«1÷ÿûÞ›L&ÇV¤ËîßÿÚ÷†¿ûs·@ǹŠ"ÇŸ{ Úóæ=ßjòQåo~¼Ûï!øÁø.J¦ËÌWÇìÃïߥ ’*?JþMøÀð|€ø4vÀGÄW§kF|ÌFLJâûkÒÿý~nec÷c÷ü·ùKË¥ÿ>øàT߉–UãîÞ…úU$‡»gÕàƒ^ÿ|ôÄ·ùk¾ó\X*¾?œgÌðÃU±Í6’„͊Иñ ‘þþ·Šï÷Ìýß÷ 4z×å¡€´’ÿE­­­=[r¿î¬çÛ´h˜+Ëû¨kCÀ˜o}ñÍB>–œoÄæ³/иËØo¢Ø MLGû°¬|. ¾ò?ÿë›ïjÍ·øðoD¬ÊXã¿~}29!dp ÿ·ÿ-¥24•€ŠâŠ5èòÉii< ò-øÉw5âû8ÄŸ—£™×’ñÃÂÌt±¥:Fd|ÔàW>Ì¿ä›õÊGC¾-9®àtUúüܦÓéÓž\Õ" »ÿ¢mÛ¶à›âò˜1¹‹ª+Ùº|ç›ø&øü Ã®ÍÇð|¾æŸUM¼‚O×Ð|´äó}{>ºâ«û1ñýu’ä3ǯí²^N÷¾¦² mÅ–þ‹•óC+¹_99Wäâqð>9µ -ùÉ·|tÌÇxø1ƒÔÏ÷¯“æøù¶ ~òcùYï¼øâ‹G®è¡€\úÿæ›o®6³½¥"N#’Ѝ’E÷Ëï¯ ßêðÍ">+à£ßZò­#>ºáëBBÏ£àëyÔŸG•×3ßêðÍZñQÿûO>Є3ü¬€;v<âyá *²ûàŸÍf/бYPg†æõÂO¾ Žâ£ߺà«îGñÅb,1¯-mý±†ç·HjׂøÉ}Çcÿoåâogy^XCyÁŸƒæO~ýõ×›÷óÊVW“ÚÙtÇKÒ ¤>?ùó“=,ÁçN1b4äcyùñ…§šÏòkÉÌן…kn»ÿbï* £H³õ[ßq×çîîîz|]w7w3d,ƒqÛa±QlÛ!h€˜‘&B̺þ¿ê{U§ïy9¼>ÙËåo:Uáæœïtzûc§î_÷»Z…P@£ÿ—^zéÊt:½€8•§¹ŸTå'äƒß&š?B×5ƒüü÷ç‘ÿë/à÷óȯüG~*ÿv8pàÏÉGh`È:ŠþS©Ô$Z÷ËGcãt3jyŠ‹áˆ7¿òûyå÷é÷>à2ð~|W~w§Âó+¿òçÐÕÕõ&9ÿ!œÐÆ¿/?ùä“צûúàÓ]’š¤›Á»+_þ)¿Cº‘Áàò3|9üèµ'Ýúö£(K}Тзpê½uhût;ºƒ×Ú ‚@òßÛÝþ$•ß]å—ÃýÞdÁžƒþùŠ¡¸&X£ªýOÌŠþ9¸; ÷›NNù•ß:ð³ þLx„8ÝÓˆ£ÍÛðñ‰¼V6+öŽÅ³Å?ÀŠm· |Ì,Tþ Ę[Q3ç!4>½íŸ!ÝØ? 1ø®Q›ûõW~ågàr6  ƒœÿW†^@£ÿ¯,Z´èê0ú/ÀW¾À; ʯüÙÑ~׉ãÍEøèø³xyßTß‚K‹‡cÙž‘X¶w^Ü1Ç&ÍGÕ¨¨5•#ç¡bØD¨ž~RϬDÇ®xm”lÄ ƒ„óçßþ”ŸÿÞ Sþê,-.þKrþCmC FÿµÕշШX8{åwÿœ^'ªë@ôšê¬ÀŽš×ñzÙ<,+‚Ý· z-Ü3ê d €Ñg _ Ÿ‹Úy ùÍuè9^•á`}H;c?áüÊ/ttt¬ Ÿ¡Y€!ýõOÿôO/îíéÙ àü¦˜òÿÊ 3߀èõºq¬y6[„ç÷ŽÇs»EúYNŸ¢bD(n™ªñ·£qáóèÜUÓÝêPûK¾ý)?eŒ1§Ö®]û[4‘,€Fÿ_Ü¿ÿ¿ù¾Ÿ|_ò'åã;z›±ïäxëàí‘ÃÏDû{F‚œ¼Tð5QV 2uw,Äéõ›‘>ÕLBj{‰çW~hjjº‹ü‡f†Bô‰€0µ³Š¢ÿDBºòƒšúšºj°­úU¬* ªë³_.¸¬ÀÜP ÌFõŒûÑüÚjôVÖ‚&ôÚ'–_ù š»í¶Û®#ß‘ä½ýGS:¿n<¯‘–>(‰Íí#zmè(ÇÆÊåx±dR”æÏ®í» 9FRy`âH-{݇Ãú>Ó' Pľï#Ž9ò­¤÷¨ ‹ŽþÍHbô¯PÇoŒAÕé}ØP¾(ßË8þ=Çï,$åÙ¨{¿€®’C°ž—8! PÐHà»”øZr³šþÿÚ 7ÜpIOOÏŽÄ…:þ0ÆCEK1Ö},rØL}?߀ïˆ^.@çOöÁ¦Ó*‰56¯^½ú÷)ˆLèv@þ¿¼mÛ¶¿³ÆtzóQ$ âO{½(oÚ÷<ˆeÅ#QPÌ8þÁ¼ ‘óQÿ3èØ¾¦·/B@¡€ššš)äü“Ú  Í©TêqôoÌà ò_pŽß3}8Ö¼?:|¦£¿x ÇÀ#üîh—@49PßÓ¡(æ„€ÚŸòÇBôtum }ÈÅT ‘@ýIJóß×í×~íò0ý_Ûô¿B#~9þ]xÿðQS9þQ â(xDB šˆ„@»f15Œ·}üñÇFÙä$5jú?ÂÖÿÁZKéÿ¨TåW~ªñ{ÆÃñ柄ŽÿArüLÄŸ<À ûŸE´jØP€Ú_œø•ªªª&“?IR@›ÿ¢‹V[[;@² ÐåçïÎoL„<òSsŸIãDËn¬>ò0ÕøÇŸpÀ y¨ðÿ ? ö§üäýüÓjàB_rQˆ¯'¨PÓÿ!.îìèØ àÜŘ|;UîσͯürNçó9þGŽŸ‰ø“.ä=seB!ÐYT ›öà²ëhL¾¯?÷Ù„ó+?¤ÓéúÂÂÂß"Ÿ’2€¦ÿ¿¶jÕªßóúú¹t¬‘Ñ«'‰&‹ß˜!ÊO˜ŸøXTŸ.ÅÚòÇÇ?´£æ¡á‘¥è*9kì¥<\ gþù•Ÿ{þìç”––~+aeMÿ—••}ôä?Æ\”%w@ƒÃOð+?=Ù[àÚËñáñ§±<šã/¾å ǯ€ ߟ\übf³`tއV<ó¶@¿K¿òó÷8ÕØøeúËú³Ÿ3Óÿ¥R©ç0†Å“›‚äïLœêÔ„¤òX‘~rüˆÉ»±¢+öŽcø¨à U» ©e¯¡çD5| =åN³}·¨’ÿ»½`ùåó@ww÷îЧ\â☗4ýOéòð¢`ŒÒÑhå7)?÷9)wRùÝÒ{òÿ­ˆ?°à-ÝõØZõ^(™€çǯ@¶b¸jÂ8õÂÛè­=™AàñGàìÓ!åíȯüçz¿Í¹Ø@ÀK§[Þxã?£àRË1_þsÑòåËÿ¨¯¯/&ZàRHôçÞ3Eesß™õgáA?!Žügþ§Y~´÷6agí[xißÚÕ/pü*DB zòÝh~} ú›@B€»ærÁÉŸA„œc~å'ˆøEgûÌ3nEEEߥòrœŸ  õÿ»wïþVð™h–1VÞñóõh\ëZ§Çˆ”„ó[ëÄÏ_ >"Çî¾6ìiXƒWJgŠžÎ§Àýéƒ53@ëû!ÝÚÆmdl‹ 5ëÜó[;”ù‘/8Çüÿ·³P__ÿe—/Šé8 ŽÿÑŹ¸ººúnLºŠ1RÇÔióÆÇ+åóG~Nİ7Aоþ©xýÀ„Ž©ƒãWà&jç=‚¶O¶Ãôô 41à"@gÄŠ}¹ý+?ÅóAþïx›ÈÞðõ\ó>­ÿ·µµ}@¢>Rù"#¢W‰ã’öÀFž~þfG~yÙˆ^O4á½C÷Eã|ÑÎ~7G¬À#æ"Z(TwïS™'—¿ø(ÓÁþ‘ߘ$ñóœÙpÌúñß===åÿñÿñó$bÚ õÿKo¼ñÆëºººöÖ%i‰’•üŽS¿\Īü|Ô!à¤Î~ ÔµƆò…Q}ŸÙׯ`°¶ FO<ùÄrt,§ÑÁàlìs RÛ•Úßùä'$ŸÏ*€ ä—ïÃx^ïG}ôï”eŽi€Öÿ/^¹rå_§ÓéVàU£<`Bœµ²Í†¤á…ç·Vȯü|ù€½ 4uUcSÅr¬Ø;–™åW·ÑÁÞê:jô¹sÎg§†sf|¹)¿üÖ&‘?ÃOÈ#àIĵ@çÿ·oßþ-ß÷q*RGÅFþÝÌçø•_òo°³ÿÅ’I(`:ûUÄPÜ2Õ“îÊL ¤šA‚œýñöÅÿYb÷ áW~¨¬¬¼7Ê2Çt€Öÿ#ìß¿îÂ2ÏâÜêS|ê‚åw¯ù¹óÃzú:Prr^Ù?‹Fú˜?ñŸ˜ùZ×o‚×ÙÐŒH¬æ>ˆ0!âɯüpºµumèc®ˆ_€ €¯ÒE¹¬±±q €Üƒo`“Bž¥P~`\… 0ÆCyÓ¼}ðŽh‰Óà§ iB * ÔݱÛ‹a¼4|@b—¼#‘G³ÎüFùapá€ÎÎÎâÐÇ\ù™öh`ˆ«Âq-rœ"–«Mw~ÞÉå0âçù | °@}ûQ¬/2ç ‘ì?üã‡O>Qˆ®CÇ2‚!ç\`Îö¯üÖ:óœùiàÄ-·Üòk$è¹qÚxyˆk €&à “Ïï°6ÇüÊï›L¿µû$¶T¾íì4ø©H82‚coéo¡·¾ÙýÙögÓþ•ßîüAÀó¼®wÞyç_)ؼ(^ÒÀËæÌ™ó;¡J«qîÆ™w~£üà¿¥ ~í(®ÿ1V–Ns[Ý« ù‚ÓîEë>Bº­=«?@dù¿/(?çw¾µ|Û¶mû•›cÖ¨/Y½zõzž×PZÏÈ @®t™Ïºóœù•?°€ç¥q¤és¼yà¶è)}‚Õ½*†vÀlÔÞö8Ú?/‚I÷÷±ó÷#8#ÊÁÁ‘;óg7íÜ9’@Ì6êÀ%[·ný®ïûŒ¯ÄÉSøƒÂˆþ×ù•Ÿæù#ԵĚ£FÍ}LƒŸ € #2ý .C×£0~lеSÆþÍ9Ÿ å· ¾Ê¶pôèÑ{¨Ü—Im¤‹qi˜žÆU!ä|%Iªüs¼hQ~šçGKW6ž±Èg”BÀOë_oCÛëoÀ¶4Àfõ0Á‚Ðþ àâün„øûå¼N™½5›Ço@'./++›àœRWF} ãüEêšá¼5¿=Ÿü4Ïß›îÄžú5xyßTQ_€¢jÌ|t?8¶p6ÌÎ5°Ý°ÔÀÞ ²Þ‹Ê]¼“;ü6¦ü†ãd æäù íôéMQ³yäob"t€ÔØ•uuu˸¥äug÷z7¯¤y§ÉòÆŸß2ü†á,`űæ"¼}ðNq_€¢bä­hZ0öé‰0‹ÇÃ,óÊ}0‡‹`=€¹gä¡ôxó¿ bÅt´·ï}Íu´èë1šÐÀ¦¦¦5œ0¼ã£ÔÈxUÊóKÅEBø­+?¥û ¤:*ðá±§Ïí=*£ fÂ<ô-œ €§‹ÇeÄÀêga*`ÀZŸºLés‚|–ñÂáwÈ&Úò@8f^úšC\9Ø£€*hÔØµíííŸà/®# £åÆ%ˆòÃÒX_Wo+¢½ý/”LˆööŸÓ<¿ EŨ[qú®é°Ïã?`yn:ÌÖ·aÚ[ak¹s‘ý^ ¹>#Câü<'Z˜HŸãÝÝÝU?üá‡@Fu €ëC°£F‘:¹ºÖÖ9‡7Dù ?~f¬ïPj ^/›§½ý*Ôù/@ýÔ¹ð–L"‡?–L€Y8æ¥;`J·Â¤û`ö¼ðŽÍµŸ'Žü ˆx~‡DÒ½½-………G>G@Lv\↮ÎÎ}øô>§Dƒ“;:^ˆIglrùåâ!»»ŸÆúaíÑÇr6Ö§@ÑùàTØg&DŽž•…yçI˜êC°Œ ‡ûûJ76Ç“_þïõ5¹ó¾/îyóÍ7ÿûŒQÀAºàª_ Bpaò†+0æ3Œ«kyZ+†üVÌߟîoë9…h}ïrëS³Æ¿HÍžMu!"ðÌd˜OVÁ¶¦`¨,À• |¶Rº4ÉÅ–Ÿ çãAÎo¾µ0žç¯Y³æ›ñÚ àÊiÓ¦ýqOwwFñ‰ˆW‘Ìw±†'1ZáÈI<ÿ¿›2é-Ö±>¹CõØèyl ,#زÀó `J6æû`Hœ©ãyú“gÐÿ  «‹‹‹çpLc“qfgræxí€Nu`~[~ž¢~t÷µagÍ[x¾dü 4ù)TT„h¿O<ö—Ç&Á©™&ÁÎ6˜nN/ûŒg—üxg› ~þ^žý^ÎÍÍÍ›è‘À×ÄC裀¯.--½ÀOUš–¢ˆ™qŠ~ÆðrÈïyyä÷àÖXœhÞ·Ü®M~*uìïäŒ9qpöüwÃÙ c©IΚÔ1’pgî?ìû$ò3[O=± ‘ÃYZËTÐÖÚº+ô97^ @¾Hà"êļîÈ‘#OȾp¼âä.:ãøÅ*S¼r~Þ)Ÿ_~‚õ @£}F7ù©ˆMã_Íøyè{rRœ¿IpMLs=L.Jeœ™Ðáf;Éøñ{ž” ¿gù ­­­hP€þô? ˜ÀõåååO 4ÈG»|J]õËùé•ϧâóÊïÛž‰×»«JgDQ¿Žö©ˆÍ¾ÿÓwÎH‚óçG—Ï…Ù·ÆKS6€u¦;Qþ¾ÇÕÆÉ/¿gg €ö¶¶âøW‡¸!\ÏX€sZ£f Š p òÔáÊ7ÔßÜY‹Ž-Žêüõ«ˆUã_ÝÔ¹ð;ýÅkd0ʼÿLª´N˜;çÌùg¢kÞQ:?½çÇGý ? €öö½1*è"ÜXQQ±‚稙´·ÀYòb‚áa žãÏæÉ¿o€´×‡ýcå¾i((Ö¨_@üÐñ³ï?‰ˆ²ËfÁìù&ÝGÙÖ!÷CþîÞ“~ž‹‹úe%ƒlNP $f@@eeåËYM€2GÉGÒ¼¢”ñgƒçç•óyå·Æ"°@ª£ëŽ>ÍôëB±ŒþS³f'ÝÙó „Þ]s² &8c ó)¸—ðÁÔùåçãø~>“W  ¶ºúU|ôË+GyÇ?¯@…:Ï/?<îü¾ àyiDk|_Ú79¶ }*ªÆÎGÏ£‚±¿$g–ÎD´N¸¿7@Ø4ìy²ˆžwºùâg„BÖgœù ££c_Ì€ €ºšš7Ϙ` ùA¡;Dö ?!ü4×Oµþ Të_¦Q¿ €ïûo¾u& íûòXBÙ€¨7 ©.kR@|þùßËJ¡îü x~‚+L€ €›êêÞ wþr…èþwîÑxÞø}ã#úóÁÔf¬,¥Z¿:Pqû›8} 'ÁÈ£ÿäg ç”nÍœaë»;uþþÃà¼ñ;CÎO ­­4n@@}ýj.'w’ÙïsÉïh´îüÚ{šð鉥:ׯ 9cwOwmüKþ¤À†0§›²²lÔÌCº3ÅŸE~²Ùq'Ož\Ç€<¨FQŸAžT³œßz¾*ZöàõýóðœÎõ«HHã_}4ö·„Æþ.XÐÁ—î€)ß›é °vÐî?W~GAà\ Žs@@*•ú@bÀ;ëüóèMwc{õëX¾gŒîðW(t>øcŠÅcÃ×I0[Þ†é@>î?‰MMMŸ:¥üOuTbõ‘‡ð\ñ-ºÃ_@¢öý§æÌ¦®EÖ3Þz ¦±š/ ä?‰?ü€ €æææÍç&ÖXD8”Ú‚—÷MÕñ>‰kü«7½OV0E ‚s2²~VI@Á"Î%-MMŸË€"0zûºðYÕÊh´O—ú¨Hdã_Ëí35õÏ7fF?}5»$ Ht  €––R Îhê¬Ñ”¿ €D7þÕNš‡ô¢³ûÓ’À˜·‡IÕ0%E2€ €›[[Zv­Pdêý'š‹éé}šòO.T´ß+ûSD"`Å|˜ÿeï,œÛHš6þ?½|÷233æÀvŽ/ä0333sÎaf‡ qŒ2³c"Ù–vWÒóyä¨ÊŸZs½µ"­ÝSõ”liW}åÌMÿ¶§§»$ V($`¼É÷xÌýþœ†ÓØ”ý‘œíWpíÞÃßÂZþƒœ HÕ X•ëþÙ'™ò’à^xÀ€$ûùý}¸^µ=ìø×gÉ~¿‹%0jzæŽFÀYÉ_ÙP pi,¿T¹º:;³ô R%}ûüÝ8WºJ ûˆ\êØ_óدX9d¡É ÈX«§[ À• =T`&x’™qpþÞ¾vd/À©å/r;¨cŸŒƒo‘ûR©zª½pw»‚W­’($ìßëïBFÉBqþ"—íö×>éK ýÇ Ž,S‘É pO@@µhŒ¸:J Úø–gËV†ÃþâøD­Ëz›n¾ÏÇß ê½±®Êú¯K£ŽýÅÓÊ “`ùü*90ù×?‰¨êLnSó³Sû¡ð 8#\Ùoc·#Üé‰T‘§îɃ3…‹Q¿|#jÇÌp®oƒ IÝ3?ç±?Ѳ0óÏGŠ‘õÆLÀš7ÔöMÍûà ÌD8yNÿW @GËc,üâ%Ìšög¬>÷6>쇀ì‘åôDï+ WyÜûh,®UmEMWü„‚!{zÑ—_‚Ö]ÇP7a!*ß‹Šw¾Eåã’.ñ¯ñ«oãåøDËœ¿¹9}WRÐS6¦¿–i‘õ‡]»â°þ%Þ¾@Ò€Ú—0dÎÞ´K±Naƒ|c“±¯ÆýË'1î…Ÿb‹Ïaò»¿Æ‚ÿƺ[ï Œ­ò´¯j=dx ´-3ìô¹ìõÁ›™¦Å›QõÉÄ~ø¦’ªFCïüÑ’ø)Ç¿6¾Óiè)ü Þò~yRàï¸Ë‚íõÇ¾óµ¿æZÎíÓkÝ &3±â@–Îí³×*ñöƒÁ ö.›€‰¯ý_ý%Ò_zSS~‡¥;_êw`cîpÝǯš:]­ÚŠF¯¡P¶G(E Z·FõçӶޛЧÿ–q_‹óƒãW%”ý‡RÑ“3zÀñ—õ«TÀ§èkÜË '‘M'Î;oÛçÅÛOþ$@Ë®ÖN&çIíó0ÁÛç¿Oeþww´aaÚ‹˜ðò/ÂQúËÏ#ý•ç1s쟱òÄkØÐ’à~©ÂNª¤óμ/‘Y»þ& Õ0·¢ýðÔ|3Gm 0yñ9öWóéXø§ a¸_ÉØ™ŠÞÌÑN¿\9þAò¤¢§rL·Ú°^ç2o‚·fMwFìOJ*Ó0l;mþ}ûöùèU(”dcò›¿Eú«Ï+ÇO”þÒó˜ôƯ0wÎ?°æÒÛ²-àrǯö÷³N ÇlG¼F ³'¯ vì|q?ö7åKqþCåø—¦Àܘ‚¾K£á-äø‰|Ã[ª¢üÈ“®èŸ Ãþ^~<`Ã}9’hÚíÓ‰qÂ&}’'ïÅ ø—Þ¾™g`ìÿ~¬œ½^¯€À”~ƒ…kÿ‹u™ïõƒÀ(9@qü{}‹ìÆï೺ñ¬FÐÛ‹Î3WQ;n¸û=æ29ö7$áþÕ)ðg¤¡'ÿ3x+…ûuò|û X`j¢“·Aõë(•ýõT¿~ó'ˆ\6¾](ˆˆ¹—ö¹{ƒÁެŸ‡±/üD9z^¯<iŸýËö¾¢Œä$ïÿ ÇŸ>« ̈/œˆSŽ€wÖgrìÏ¡ãW¯þ©èÍM¿V‘<€¦}°Ì ƒ­Ur=󾃽~fVâmêí«¡| @’Ùýt§ûü6H’¼Ú·£%?[X7ùŒégà€æÌšøW¬<õ6&Y~€$÷½]y_!«á;ô™]H–èò¢#ãj¾£’Õ1Â!Kükúú‡PŽõÛSÑwK³ÏÏÉ“‚ÞšejMáCÿ†acý}Ê(*Ô¾Þñ;€Ó  Ÿ 6ÂêlHÈ0”þÿ~½zwàêZ~RÒß#÷RûÑûe–N\öB$‘&?à­_aþÂbíÕw$? ZŸõ~8«[îhdÖí‡×hC²«£+œ,XýÅŒø`œ£Ä¿ªÆ¡OŽýýð}þõ©ð'ûü¬h"àTDÍXùQ†¡Ý»7é¾¾}Ç®µþq@íêíë#Ô¾^€¤&DD*›hj&Ö ‡ÏOl%zŸúNJ¿Ä† û†ÿ×”bú{Fú+ÏqΞÏøè·X¼ñ¬¿û¾gèüÄñ«Pÿ–œ\«Ú†v_Ü2̦´î<†êÔ)¨xG|â_넯¤Þ¿]-}²Ï< =ø}~^ið–Ñ[­¶tëu̺u•[éšKï¥k&Æ>½ž·ïš^RH÷”­Sñûíä _ãÜûuút+1öuÿ©Qpÿ:&¼ò\ä€#¥?ɘþå±üЫ* ùqV¸tsÖ(\¨X‹–Þ*¸uÕõhÞ°UOT9¶žþkSÆÂX’ KžþŸ~Ÿ_ëÛ›ŠÞQçùitXþ®£k6µIí“«Æ©S0 ö]Ð=õ+iÂ>dRF¤z§ˆ@†&êÀÛ§ßÍÛªpz÷jŒ}9è`[`ò;¿Æ‚eÿƺRVxÈŽôõ;ÿÃ…SQÖ~ÁPÃnƒè½Ÿ‡úé+¨£ƒÚÄ¿úÏÇÀ\Î:?‘Úç_— ß™4x‹Çï|Í'`Z!ºþP Ž—þ®DÖ6‹>PQ{öíG^é}º\1½}7€ i'¥ãÄ FD Bë¨#²oŸ’r 4P±gÉ$Œ{1\8>Š”þäwX²õE¬¿'ùNŽôíÎû«Îk~ ÷2Lt]¸…Úqócö¨êWÏl9ö÷Tå{¦¡ç¡“}~G¶Á2$IºÆájÃíúýyÝZª^õö#bŠªi@{èJú$@9h$4NÈ“\CÞÓG èDc¿ÇŠ¢Zr&aû„ ÉçJk'}lã ³üµ50ã›?aűװ!GòžN ~[sSp»v/zŒvŒ´¡ª ¶8…ê´©‘DÁpâßão¿‘¬®|ï®TôÞUûüqxêgŽÆÈÈWÒl œ&šÄ¬_kuYÔùk¾Ÿ\«³gß%9’ÑÓ8~}ØŸîÁÓëÉD³øï²cŸËQ àëíÁâ/^Å„—OçOó^ÿæÌüV_¶Ãß#êïýçÊW‘¿‘8ŒÚF4¯ß‹ÊQPýÁ7ð-LÐíóoRmzGÃ뉻ã§G«fÂ4|äA‡ä&©Wýú§_5ë¶6bJï%Æ>‘Ö~D.« 9:§©ƒú¹îZ~OK7)!óö©í˜öU Ö¦:Ìýäߘð S ^ùªíðÊÿHÛa²Ïÿ^Øù+ž…êÎÜ(7(£ï‘]ë–ÁZöQ¿>ìåXßšøN¦Á[ð$Ü_ú¬•†žŠ‰0}­0#Qæ)[ã€ukš>ÁYÿÄmAÐûˆ]Ûö“ ƒ Oé+½>z¢P‡Î‹‹è·-ô°©PJjÄC|YáÔ߇Ûoám‡ÕyþH?µÏ™¡AÁ¼k°¶¤ÃZ2 ÖòO¥|ïAR¾71P>fOµÊ ë“&:òYÿ”D `ÄÚwÐ#ܤ҉sì4+•·O¡·¯¿žœ(ɹƒI¯ÿŠ©7ѶÃãÿŒ•ß½Ž9#¯¬°Úçßœý1®Wï@·ÑO7BÞv®î‡µæsXKGÌò½;RÑ{›ìó'HªŒð0º‹ŠQ'O¢–ìš©‡ÍõÌiGü+µï2à'ö'Æ>CÈÄÑ3¶4öÕxxë"Æ‘ÀÄ‚€j;ÿ¶TôÝ$mz“SžOá{|f8€D, 8ØQ §: S+ÝÞØ»l2©˜Hñm‡ù‹þ‰µ×H~€+ºõíÎû…-WŸuù^ÙxtÖÖ‰d[À5mzÏ¥Á[œÜŽŸVÜÓ Äw=ã¯M}~H ¡ö-Ó–Y_$ ÐyYápÛáÍîh;¬ÎóoÌú—+7Iv¢O \Ø ke¬¥%¸U üÇRÑ“—”ûü|5Àº5jIÚõO".ŸŒÎ·Vã_ú¹K€¶žñÕ±üpr¶^Ÿ5P»ÿPáTuæ@FrŒPå#öÎÈ XþIrîóïIEïýdÞçç«öÖ,bÖ7—€€DÜ/U¸7\xüËnÚvxδ¿aõ¹·’¦í°rü[rRp·þü^ÈH²aø¼}Öš/I4 ¡å{·6½î-삵P@`„ɲ,t¶µ`~ê ˜ðò/\ ´¬°j;¼6m‡Õy~•á¼d𼥑Ü#ÔT‰ÀáÅ T4 mzSÚô’ò½î”' =•S`ú;a–€€@2) ¥¾s>þç³îÿ¶ÃŸþK¶?û¶ÃûR‘ÕpVЀ —Œ€…àý³°Ö}­@àÙ·é=œŠžÜx–ïMT?€t}ÍÉAÔWz0óƒ¿&¨@üÛÏó'¬8þºj;Ìt¾×¯ý2JàqO9d¸s„šk8ºŒDâÚ¦÷S¾×Åà-£·Nw@@†@¢ QUœ‡iïü鯺ø¶Ãsgý«/¾—mµ÷ßÁ–œOÃOýÁ.Á‚÷ÏÀZËä8mÓ{™iÓëz)°ùFO…€€@²) ¡ôá=L~ã7¹(?àýß`áêÿbÝí÷89פh†Ôï†#ÔPŽÀþù°–QÝåøU›Þ Ò¦wø@Ù—0¼%ª@r€€Pxÿ†rþÃhÛá´ßcéî—±ži;ÌUóÛ˜= ·k÷ÂôAÆ0¦Á‡a­Håªòmz¨6½äXß°õêïz$ Œ3÷æÅUL|ÛáYþŒ•¯Û.+¼öI¯þòöû12F¨,Ö– ªŠ ýò½ÛSÑw‹”ïò¨Ž€Y0Í@²€€ÀýKó¿„uL|Yá7…yóÿ‰5Wž&?àýðñ¾ Ï´ûê!cdP{Ó ÁOŸªM¯ïÂèD·éM|GÀŽL˜fÒ­³7OíÇgÝŸðâs˜üáo°hýÿ°þNäØ ÍòWaÿ5»äxßH–àÕ}j; rJ€†ûW§Àw<=HùÞ ¾¶«É€‹‡¶Æ£°kË OÿüX~à•HYáˆó×ñÏiÌ€ jœ…µ2ˆìó{SÑû€ìólh9+| Ð9â†pz窴vAÛáÉŪ3obCîØ”3 E­× CÆàÊ¿…@¸©Ð§mzoÄ*ß+Ð÷øx2& H@ ݰã4m‡…¹‹ÿŽ{•G CF¬aÝ? ßéOumzEª%pÓþd¬ ¥€÷¯˜¦9 Rýæ~ôdg¡­¥5ü÷’!C UC£ÛëE}]9:+&Ã[–¦s‚ ;`šR x$€€ß¯û,aöV»MÐh|ÿßeë¼q(.(DAîC”y<èê섌‘=úúúÐÐÐ€ŠŠJTTÖ ¥|zJSb9@‘'}õ›™^®[$Àý²L Ûæ|Ó?£PntbÇxŠŠQ˜—‡‚‡ꮨ@_o/dŒ¬aš&ZZZPYY‰ŠŠŠ×ÊZ4–gÀ[*ôÖ­KÂf@èèx&`p×$ȾešØ8- ã_ˆ¥Ioü×ÏžPaE@ èÑ#4ÖׇÿŽ2†÷P=3:::P]]qüƒTÚŠ{ð–~¡u‚+afâÖ?æé_@¶.òû•®Z“>J€*\yæ‡Cö[(z”¯?QAn.<……hkm ; ÃoŸßëõ¢®®ÇOU…êÊBt•Ž‹”¾E@͒Ĭ}üwÅÛ¾‹@¶øÉà÷Ç—RyûÌõü}+ƼÓ?u‚’¸ä«7ð(ûzÚ ƒ·*JKáíá1ü~?ššš”ã8-TU–¢£l6¼¥©§OK÷VÏcÖ)ö)] ê¾ø8oöÝ€ý‰cÿz³&ŸÇxõßïïëÃ’/_Ãø—¢¥¢"k&¥ 8z±A@½ÖVWÃ××î–e¡µµUUUÄñë@½¶—-•D@ ôTÍÔ¬ŽÖ8;ßéÀ¾s? †Únvz'ë•ÊÁ“|íbUûÿ½Þn,þüeL ÷ä@IQ1qöççãqCCøo,Ã=ûü]]]¨©©!ŽŸ“J8 ði,'(P9¦¿—¬oäÁ$ÑWþ‰=þÑ]5:]S~D?è©Ýà'?y©}%Û‰€èjoÅÂÑ/†ÃÝÔ J`ÿêyð—(Çn[ù¹¹(-*BG[[x/YFòŽÞÞ^Ô××kÃý<Ôáqùm@`*L_Id@€sÞô>çâíÓ÷] Ì)†NuŽÞ9]òöµ×Oy u¶77b~Ê´G×Úˆèó*ËÊÐãõBFr Å?f?£ÈQÀŠ1ŠÒÐS1F_›€Èú£Y+y00è{v>§ö•4ÑR}úßMí» üþˆô”§Óà Ÿ‘‰D¿+rm”åòöÉgÆ kt‘‹€e¡¥¾s?ùW¿³ zõyœ;´%…E6¿>? ¾¦&œ\&#ñûümmmdŸß ÔW\О˜£¯YZ(ˆülôËæúG×>º³QYÞ>•Aí¸* P"Ô)ë÷Òi˜=ò™’ÌW[ö)tDÙúŠNà'U«+0{Ôßûà¹((šôú¯põäQRÀ)” ¹©) `2žù±>Í>¿SÕ ®â¼¥ŸÇr‚å`ô6"R ÈÐ<‘5Vs.Êà#œ} iìsÛÂFÒ€$’°ŽÍp“zÕ=ÍGsß !¥Èûzû”–#ߣ?m@퇠®¼3?ø Ò¢ô<&¿ñܹ|ÅùÄ‘;…•PVR‚~}æùR¾—†û«5•YÚb@ãà懲ȚR$Cã€É“9‰$°"ö#ÒÙ§QYòþ°ÉˆrƆ’>̹O?‘Ë0È÷’Ïõöé6CäUc? PÈ÷ªN€5¥˜þÞŸ @ÃÿSßù²nß@Q>)äT¤¬poOdÄoŸ¿¹¹ypùÞ8¨Õ•ùè.ý†lˆFÃ[>~o # _·èÏtK•®ƒ4b [ó¸+jŸ®«tk•·€$>(§h†>„>˜ùÉAE#¼}:é©ýȵ¬M¥` ˆªâŸÌ>¼ÀƒŽ²™1@à+ø»=4NS÷3ýu±ŸÈu'ìÛWbí›®ˆÐä}"_ì“Ì}ÄÁk¾Ã¦ýØÿQ ³ †àɽ‡ÉoþF€ÀϱìÛw‘Ÿ“Ã@\ój*+¥íðkÓK(C{Ù<ôH?Pøþîbú5ÏÎÓ¸.9O/½F"âíoß9ú: äZ–R¹§|f¿‹œï¥[öƒ! 8;3|ÜM€V\þñÀùýg ú¶Ã¦ ¤|¯ ­>“}~¾@ÚÊ PŸŽðw* )ú„eêàéúG:“@ì“{‡Ø¾ ¶ôÿˆê=~²PÐO: íj¾›Ø7È5Äk_½†B@მa‡'@`ýÔÑOöÿ9ˆ¿òµƒ€ ¦Mob  ­lEÌrÀŸÃß•ÓÂÐ=”Ð-LzÞ¡ó €úÏiÄÔ}r­ "Lè‰ Oqׂ$ÆáóNZÞ~òï]8(@mœùS(1Û•LÛaiÓ›(Õ µ|€ð«¿3O€Î¹òQK&«_ã™5•ïø™ïuI€D9˜4\¡ öµ÷Ñ£3 *x”yåI'@€hØ2w,<¤ p‚EÛK›Þĉv,[³# @ü9 ˜"i6Ö_M.s“5^¿¦2ö]0¹ps‘î,ªCúäa‚‚‚€‡·.ªp7q€?Á¶…é¤`ÂEÛ«²ÂÊ9çc}jÿ”-ß›x¨GKù&MG@_G–€x­|$–DM™ï¦²oßE Í€4Ο}"wN›Œ}Þ.}åá! çÆ9Œ{á' NP"»–LMV m‡‹‹#m‡¥Mo ¹|›@,yRàë¸Ó1ëã”í¬wvႿϾ=W$ 8˜ qã ªÄF@ÖÕÓó¿G9@Ñø~Ø»rV²Í(/'m‡]ܦ—ëKn¨Ããò]±rDžOák¿^6’¬‰˜õφm%˜®‰ÈóTîlÏßà•û!÷.e`ÌD œø9¬]OQ±€¶Ve…ý~·¶é%ÇúÜûbÉó |í™â¼þ9wèNì»$À?qsDjoO>NöõŠœ¸{á8ÆüO V! Ã—¸ hYáÇUYaWìówww“ò½n€¦òC±êˆ<Ã×~[€&|ïpý‹Ã“½û.†ââN’ ±¯àÎùc8ºy¹€–.)Qe…ÝÔ¦×Åp^€ØÐvK“À®Îw p?H .€0À¹£ šðÊ/plÛ*”¸ÜÚvØ0 'å{“ò`cù1m/€› 8\÷’÷~÷€€ó šxQ¸}NìX‹’¤«༬pC]LÃpA›^@Å -Ü€ArܱžJ@ ñ“Óùž€3‡4í€3vmp1ðe…[[ZT-ýg¹ÏÏ”ïu?4”ŸÔ€$öµ]Wu’~5G(pGNâ­øÛ§pú ä Uú«ÏáÔÞ͸>? ÜãAwW— Úôº*NGJߊ\ƒÁ@×_‰$~¢™Ïx àÖ©TáæHgöo¦@ój«ªT2^ÂÛôºÎê@޶]e q므ÀˆS0Ü<¹_@¶g ùùùhjhPN;AmzÝõçt Ðz…€K% à~…‚!ÜÈØ+ €³wŒ ùEEhwÐv¸§§G•ï%ŽdÀy-\Hƒ!\ÿnÆ P< e…Ëʘ¶Ãl›Þ5ß— ” X¸vb·@L=s‡v¢¤°p8}¾í°ßçãÊ÷’6½#.h À%˜#B!€ä€«Çv htþÈî ´íð㨶ÃêX_WWiÓ;²à"zJ?EK@Ë…øD|>g÷;·ïN¸rt‡€Fb¶îlo·é¥å{jË/¡µä Q”ZŠRàm¾ÓÈ€€@‚‰U@ÀѶuü¢šªjÜx”‰™§—‹¢4%cnzî"hZI÷Ä.[î™x>Ÿ“{i@¶ìmˆT9aqþ¸úðÒ3VŠ¢ôíñ¥¸^ò@À° ÷ ¸jBÒ$À]C–( ‰‰+EQÓ7g“ldž;* €cÛV¡¤¨ø©£€À©Wú`y,'(P™§€Û*e¶2Ù{ù÷éÚoû®9À‡×é5|øÈÉÏœ}]Þ¾€;çŽA€jÂË?Ç‘ÍËà°ß=¸$ €ûaÐ9d;á|"æaˆ»†ØZûîý„qÞ×Ù°g_oÖý`¸{þ¸€m\"` NÜ¿ˆñ± ò‘:ȹã×8õêüýµÜw1r'ð²S…Ûë²o“ß—²e?î]úTã_ú9¬] ` ŽÞ;/ ©˜U™OÀéúG€ÕPÚZ¹ÿ /¿£ûS§ßæÄVuå= Âøн+gÁS\òTNQ@TW]‹ƒwÎ`܉eQP4îÄRdW¨-ç!|ú¹õ/ö XŒ$ðûÞ“û ²¯Å¸~åEãú`×’©¶@`oæIP?± ¹ÕE‘vÀ#M.Øf@N°“Ð~Bì«RÀ¹7/`ü‹?#Pà'ض0]ÀìºuB€*œ™WS¬Àù“7½ÎY”Ö¹}ÙøþIáh¨¶ ØïâïQwû2&¼ü Õþ6Ê J`ËܱðH 9;n  GʯõD@»fù®yüýÎìkïã Ã][þDä8pêüï4÷j¸ê]ú«ѰqæW(.(x*§( ª®ªÆÖëG¢éŽXXWvmòÇoýK€}€d†êlüãÿ0Šƒ}Þf0DáýÊá S?Ãú©£Ãή0ïëDU•UØxí Æ DÀ L:¹ Å å&¿Ö9s¾ü{NìSñöÝ•(àWrJ–öBO ±Qœ}“^ÿ(¬Nÿ¢ð‘âü+QQQ‰µWöAŽR˜|r5< l8mîw x[ÎåJ$À!ÿøM>4ÅÛç¿3À“{“ßø h! eß¾‹üœñyú/«(ÇÊK»!@`Ê©5(m¬ €áà‰Üƽqsòöí ¸1“ˆûÌÁDôÛ¶Ï_€²¼˜òÖïü‹¿x yYž JË˰äÂvÐRÀ’8õÔZ”7UG ÉwüÏsüvîs]€ÿônã©ÜÏýÌOTþ}­}>b ^Vå9˜úöïUÿû('(í€ç¥¼€ÜûwQô(ŸuŠ%e¥Xpn ¤ €i§×¢òq-†Á¬gÌ6¨sçoÓ¾}° ×¸9ÀO"šµç`òqö–…Ê¢7ɸWÝgDºIÊßc™êÊŠ0óƒ¿ =NŽÌ¼|ÅùR €©P…œâ¹*ÊŠÔ–Ȭ³PßÖ¤àéžÂí%ôÙûNn}䲈˜µ×%€ˆìž¯w>‘YûœÛ·LUe˜=êï˜ @¤ŽG^=yÅ…ßëD5UÕÈÌ€I1 ÀœsÑÔÞ‰Øuîô•wúüv¬ƒmvývï)Ùð3”ç(D¤¹ÏŽ}¿Cû™†‰æºjÌýä_ýð‹((R'#Î܉) ®<¼-ûÿ˜w~3Z:[aúýìšÆ¯ö3ïøyû†‚Kê´µ=D0È„‹8Ú£2ì‡äùÿ1¸í›öMÃ@[Sæ¥üW€*ü79±c-JŠŠY§( }Ng]Õ”F@ .lAGwf­c½žØ°a;Jëš$@‰8qþ<9ÚøŒ™Øv@…·á7ÐÙÖ‚i/„ϽS'(Õ÷¯ž'Ÿ ¤àá»g¥€]ÜoO7ÿm#×ÊF2•‡ïؾ@2ÌE(doRòâï·Kµöíñvü~ôtubÑç/‡+ßQ'( ¶Î'€§éx\@‹/í@__¯gÌDSù,¯8ÚG0(¬@¶xGê`ËÀNx‹·o8$W_o–|ùÆ ÄŒ¬™”¢œžôЀ¨êÉëú«¤ 0Uøo²ìò.6LoØ_ÿèõÎ3ýéuÎíK i·p 0u J]1æ~g'³ðWoàQ¶”f@ú”—aéÅ 9¢ñ'–cÕµ½6²ðŠù®DØG0(I€IšH¶‡ñûú`8É pnŸ\£ßbðcMú¨ðÓ.u‚r `æCö[ß[ P@Š••`Ö™ r €*Üy݃Î-w]ÿ%j?É·‚AÍ„³?É §{óÎíG>§×hò6NKÐÖø5®Ÿ9.µ¾¤Ѓ¢\LÊ"@:Ø|ëˆzØàÖ?þ×­sOÞ7âNí»& [9é?´Rµå€É„o2bÄNäÕôØ:çkŒÐ<¾} <’=HàÙìkš&@"•¹ãÎ ˜>?Y‡ }Ô”Ïr¾þi䨾‹"ÈA(Ä;lçŽ×08ˆ$0öMÃÀÎ…ãÃïÔŠÆ÷ÿ]¶Ì'Å€¾vÊ €ï€=÷N‘ÌúÇDGÙ§uÇë¹á,7KÙOö€@»€`Йc§?Ó‰M?'ïÛ·ÉÛ§ŸÓj€ûWL…€¾-ðü”ÿâáý;Ò(&Hø¿¤Üƒùç7!ýÿØ;ëö¨²%Ü‹+ŸaÜ‘ñAãÁÝ]‚ÇÝÝu®I HB€à ÷NBp·NGæ½·ëé3ër×45›ÝIïîÔï³#;]G«~«ªVÕ)tûnœ 6ÀÌ~fÞi3Ÿíñhoß@€'N~Á›°cÚ> :´~&‡«9?Éÿ·DT—–Ðþ[¬\0Ó•ÛJw+ʱ.!ýÖîDdÂ*Ä%KàÿפÄ%Hȃ_ð¼}ýg µµuHÞ¾“ ?=12´:·ú?}_¿ÅÖ5+P˜›×Ô¯ På*+lØqü6‚ãÓÐ.& ç$cĶmäôb ü Kp<û<êªæW¼梣ްoé€À“'<˜Ï/éÃC…û:,èö©’µ®®§ömƤ°oÄñ»œÿ¸-УÍwþåýüëù=WÌŸI0¿É¦ÊmXwø&ãN¡}ì)zĦѳۢŒÝ½‘ €@@gò¯¢®ºÆhê”Çó§y̼¯Ê|, ·n0Î’q®îá…ÃÛç-OÁêûúºzœ?²›8Äuk‰É[ À÷ýõ[º¿’¾F@Ë/0#jn߸‚Ü\€&WðgCaq)æî¸†v1© çÿ¾bÒ4%}Wîäƒk—¼ 1G›.LLXŒKE7Qï¨ùðþÇïƒÜAŠƒþ{%#á~Ö¾u@"7Ô-6¼¯Ä,PÔ;ñI¦åúú¿píT&†}ÝdÃýÑ]ZbhP3tø]wüº¾Fû–Ÿctÿn8“rEMæz ÀÝJ®db̪‹hŠrøîÕ>& a3N`ðƈJ\…ØäåM6Q–EÀîê{ÎIóР?½fß àŠLß¹_<ù?éóö9ʦÀ­ó)t  ‰9~RDXstmõ…úƒ”£çDÐ=è7ìÚº‘"þ›(·ÑɇNg¡Çœ³äüÉɳ¢´©Óœ£õçVŠPZ ‰Í¸]‘¯€Kƒêâ9 àTŒ˜ÃóŽu@ CKðÿ‡óΙŸ£G‹•!]÷ÔK5ÙWÓmoIM ÏOáþñ›£WÛï¢òü†øã—ôœ?7¯^Ea^ž€‰ ýò K°h÷uÅ¢°¿æèY©ú€‹cÜÞõt[  €Àß­‘sÀpDÕ¸sW¿çÿŽÈâ?WÀªðDã|M"_ Â¼Ë‚ñh„€Ú:d^E\Ÿøyž@Àý ÷³RuíZ|Ž!=Âpäà>º*Hs|¾Êÿn… iWò0léy´‰Vù~3jsÁSSÐoõL>´†êü’–#>y5Šî—úð)Sõdö_É?Øñõ>‘xüXë0™ß;…£O>ìô1´K}Js31µ÷ïˆîÒܯ¯õ i†Ž|ûQ'~>%ðÂÿh†åó¦#ëæMææ øð©¿¸„ªü65 m™¿aÅR}ÂgÃÐÍÿAtÒJªðW˜v|-l*ßõ4vç ²[ì&³ö¬ OŸ<¹>@Ỏ·ïÁ‚õóZG îçaFÿ¶ÔõίõE„7G·ÖßQŽŸÉó›Pm@Ä€î8‘”€"ŸŠЩ¿Ò†s×ó1fÕ´IE{Ó§~¾> Ë¼$DìØì×i>ÂÌPõô>µþˆýÐHÊ”ßÿxGÎÍïÛ>”€qêcŽ ™²Ï *Ñ,€‡•e˜=8ßøSžb§æèÓî{ÿªòü ‚€Ž­[`ù¼¸‘A̓,ê§ë}kßD‡é§=}êçëâSÑkéALطΟêæœÜ„GÏÓ0 v_3îd Þ¾2o_‰·o}¸¦]ôÌ1Rò¼}ž~iðóÇ1ox¢:5óö½[bPàSyþF5"Þ§’ï§AëÝ(wåúO_ÍÃÈÈñ7Ü©Ÿ¯™vÖîFd‚\ŒJZ†ù©[ðâÕ 8˜T¨éýÏøžëÉè­ÏFô"@´hŽÍÛ7^{÷úEtAd§|>Ï?"´:QžŸ ÷7b4 C«æX4#W.»Š³¼,[9~ä–`ùþ›Æäú/@„:Ì:†á[·#&‰® úôÀŧ·Ãþî­±ðzÃï ÿ7Ö€Ç Ì/0ó‹Ò6I+#û#²ã>›çÛ¡9º·~¯}¯eDµ->Ç€.ØÿŸm (ÈÉð’*Êm¤#éÙ´øœVáo©¶Â 1f×&Ÿ­ˆL\ŠÕçöÀñ-°ß2¿ó;¸ÊÔ0òyp 4À晘Üñ{Ÿ»Ö7©S ôkÿ=BTžß² øñ ÿü â'ŒÄÅ3i(ʧh€@#ùÝÌ*ÂŒíWŠ9õ[‚âSÑ{ùLÜOm… |iðÖË ¨Q{T”€•SWðw0 BÀK㨠/éô½ç7ß7à3ôþ›W/CÖ­[T$(ÐðE~%¥¥øóØmôœ{Vµòµ¼T}@èô´~¢|§>€`OÆqUh) H@€Ê<ÔÖÔ aãB5ØâyþQ¡ÍѹշêZŸ*ð§¯h°ÐèÝœpÐÕN8GÀÃ*/·¡ÒUä7vÕE´‹q:S+Ÿúùú€Ž³}fì0Í8rûŒê`! üïG]VÐ$EÝS÷n´ìHàXµïE϶VËó›/ ÿã̉›„«ΡH ˜ ÷WUÚp'·ØÙÆ—ú¨þ¾,UÐÝ9vx¥Ç¤å]A€€%àáæ4èÒñ4вczÛÿ éõ©´@ï°Öؾ~²33™N‚\¸¿´´ »NÞAŸùg™"?Ÿu ž’‚¾«œc‡©­°%G_-¹­YL—š|à¿©°…µÆô7CG5¦×¯øÓ—øñKŒÔɇÐMüœÕý•åîÇ„5îWÃ{üTjìðq Ù¸Q‰ÔVØR}î”çKÀ² p’@þˈíþ ²JûÞ®&Ú÷úzZ ì÷ï13j.¥ŸQi€†ûoeaÞÎkUýû›ˆT[áÎóŽ`Ô¶X!-@sâ’W¡ð^©ÔX>xÐ(`÷ò´3E€¶‚,LëÛŠæx³}ïט^ äù-s[`ݲ…¸}ãŠòòþ_pÎé¯,GAI¶ͤê~? ÷;¼äÆï]Xïµ&˜q|*Wq·ü~ÿý˲ p¡©GÕ<®ªÀœ!A4ÀáþÈÎ-10à„©H À9&Ð4Òv;ÖÄFdÇïÃñ“ƨ1½*Ü/bÒ4ió¦Dâò¹tW7Ál¿›Í†Êª ädæaû¢DL [†‘ÌGDÛEÛfôÜ"ÀžŸ„ˆ6v˜Ú¯?¿_þ-âðí^°/`ÕÀýûþšâ?ßáì¸8–º6ü˜ÞèÓNòüfëz†üõË"óúuU(èW@~äø ó‹qpS âz¬9þ6 1¦Ý¢÷Ô9â0¦œþG'(¢hµ?îµì&ì_çj+ܰ]wg£6ÀVÝ_% ®Àú ÏÞ`…€µHþsÓ Ðü˜^jß+y~Ö î‚›×9ûPj —@À÷ ¢²œìœØw³m¨V 0ºµrüJÀÐNk+uÿvìðÀuαë—¼¼ÁšÈ:ºêjµÿøzÔÔ¿@àÁýûg-Qèåp˜jôMƒäùG6Ô˜^õü¿ŠÐ {wQJ€ }ÈñS®ÿ܉kX4öOçiŸœ?9ûhlÛ…è>t¯DŒŒžŒáÛ¶7D[a€+Å™¨¯®ñdÝÿûÞ?íLÁîS pÆx @ÇnnzÜ>É`/€¬+gé 'zĺ4Î9¦· µïÇßýB~ýÑc†"õØääR3!_€òŠrrþWÏÝÄêØ=°£þ˜åäyº!‘Ç¥ÀȵÁXçØáŒÝí¹¶ÂÑGè3SYÄ7bö2f¿ÓÄî}ž¯à ®NÀ’pï€ÝÃÎÛNj¬…Æ©y¯ÆQƒò‚lLëÛš À·ÇôJ¡`‡VÍ1#j,ÒSOª€ò ge9n^ÍÂæ9‡0!d©«ÀqúnR}úý)Qãõ4v¸ÏŠý˜x€Æ#樹Ó=QØM8tF&÷¿†—ÝÒ pÚ$}aò ŽÄ,>öƒ ÕÎEÞ¾%ñdL?£JÝOaþˆpDujfjLï fè`‰1½R(Ø¥ÝOtcà™4j$”Ÿm I}w+pçF¶-LÄäðå.ǯòü†Õv!F,Aø„#øÈú€0çØá ;•ÈŒfz,HÝ‚—¯_ÀÁZt™pøÚþg RËú ᲯROŸ X îWU¥9Àt¸‰Y¸vŽJyûêi ôo7’²°Û±6nˆá«€±®ç¨°fèb©1½¢ Ÿ¾"èð+–Íþ÷ÄA¯€rüÙ·ó°{E2¢»¬Ô*û±Q€Ý7™t†RÐiÎQŒÜ¾UÕ½xa?í)LîœÙ™½‹ßû \˜ö*ðL"ŒTU"0ÎçóyŒ}–FuûŠBíÜ{®4ÀÁµs´›\ß~ÓÛÆòczåÆ@‹ÏÑ#øœ?¯@ Q@9þÜ;ùØ»ú8⺯&Ç?Z9~ªË¨ƒ’ ð@}@÷Ň1nÏCõ“—àð­S¨Q7LÔÑÓÈ{jÿ㢯¼}@ûVNÜ«ªJ%àBîFBOú{|xËp€ÄÛWa(Vu®›<ã§1½œcz¿‘p¿ï€Õô i…ógâÚÅóÔL(?;Ç$ðŽ?çN>ö­;øžk0Ê´ãç ‡uXƒà˜O8Ci+<%ýVïäCk\×Y uŽmÿ3eœµr¸Õîö?ξ脱oWö $`MHQE€HP˯,VÑdÈ>-,»þµ»¼¿º¨@ñ Ä÷üUÝp3¦w˜_ŒéèÖËæNÃų§©Xn ˜UÕO9þ›¹Ø½êøÉñ·f¿ÇR=‡ìFÀ©ðèØáM;í~ì0NI^’ûå®@µGi{¦×œ³¶Ï©ÏÒíûL=¼}K€ÀIPÄæ–õ÷”túäŽz°¯;zeŸô±ö]…€ Fv B@wcz»ùј^/©F kÀ/T,˜~*…¹¹N‘£7 6Ýã'ݼr‡ÚöFw^Å„ú. 02x9B'“‚ÀÆ;L€ OmÅËW/´0¼›½”‹ j?{ï=µ1¡þŒàòÑƾ€•àêë•ã¤'[d÷þ»: ¨…­ÞQ¿×瞈u'Ïäý?Ú¾«À]Kã©%ðûcz[ ·ŒéõóˆÀgèÜîGLŸ<ɇ çömU'À€Íæw~}éô ¬ŸáL%-gŠû' ЯÏ6€;Üs)F\²ɉK°'ã8þµ\¼zªŸ¹¾góòêïÕóC§yµ¿ý“}’fŸäqû®8NÀŸ°Ý“ Wï1Q—]µhõP¾ò× ü>d_‹6¼ÔèÚ©$LîøÓ(cz›ÎõA„üúÆ ìA-†¯_¾D ]×*++%§ï:í#/»ÇvŸÅ¢±Û1.p±vÏ›Šh¿Ç%I* ¡Ú ÿwìðaçØáet £4‹¹;h¸¾Ö#žúþ§º”øÃ{ûnÿó耢ï©.©¿Óí¿÷9Vܽ{Œ@-¾$<—ô•¾8•ÜÜIÕHØs&éÿXô…çžnÕûJê3õ pØ«ñôAæÃÐÀo¥}oSÕÏ_;ç P›á^¡­0=r ìúÓ (-)%§öØlœyñ=Ö`të Î}LŸÆ,Äà®È Š\c‡;Ì:†!›¶aÞÉíxöòïìLŽ_ÛÿÜdt§®¥þaÿãKú»JÌAÊ®ÄÚ§k€–·a!z29×ÓÜç‡Ôgj ¡ý½þEýŒIUüè¯TW[‹ÄÝÛжù§Î†2N‡ ’ô­…ž¡`âðX4v+b»­&k"¿ß¨õ]Ghàk’h•Š#— Pï¨Ö­ëÉD@?ìˆõŸk0`wnöwξû=UÙdì XîVV¸qâî íôð‘rÐ$¨wxúÔ‰Òm„ Z'V÷O÷ÿ0©+à›W¯1;n"Ú4ûä=g ’¢ÁÀ–ßbijцq¼‹ _…àè“”z€Q«ÈÌÙ•‰·¯ßÁ¡öýТžÜ!J?$éûŸzº‰²²û-WÕÿ¾t_ÀÛ·p €@ò{·”4ºÓNÒJš³Ö¤ÿN¿ŽÂÐ¥öH?éT­‡Ë>`_ý®¦ÚgO!nÜ0ˆ”¾Fè¯Í1ºÝ\Œi·ØW@] ¸K¢ äüã·ÞÀ³ç¯PSmל?[£¤ï•\´ö´3QzºK-0Žž~¯‰ÙÕß[< à6DDÒ€û»¥LÆíëTLÔ¬çùÕ;¼}zÖ:xüྠ>õeÇ% 9£‚–!l’\ ô´óÙœ'O_¢–œ¿î4«Ý9Y®2_ßÿô¿Ó÷\ÍžÚÿØÜ½þ½›¿5`ßÂE€G¡@säÕŒcvª×©W—²cξ¾õ"@=zÁÙÿ^<}‚¹ñ“)à›5"èßk‹€‡ŠÿZG¥`î®L<þšNþZõ¼›}¥Zß·ôš&÷'oþ´î~¯&iöݤGÝH½ËÛ÷™€€î0Ý/bþ4­ýG—ÆíÓ“³ï xû¤Úêj¼~ùk–ÌE@Ë/¨(Ì÷—H@] ì4&AR&Ô>6íbR±:!o^¿EÝ@”Ó]øÿŸBòLšÀøª}%ƾQY? à.Œ¯é_;^~‘)5†}%£öéz £º‡vmGx«ftE̘H€n é¼±©áüDm£S:5 ÒKÈñ;˜}Šä¦ºžÝ˜=#>jê ݆í“(€1Qȿ׼s¸x§ õû‡£’ÀÀÄþëÆ¾0`Ä~o_ÀW€Ä/†0Yqö‹)úcå¾A@iQ!"G ôÙ^"€¡+uB®þÛ|d &®¿†ÒÊ'¨¯±ÃþŽ9ÄÝÔ; ˜Ž†š{‡·ï+ PY^®€/2á!¿¨=‘³2Ò7BåÿÚ~mM ^>†5‹çRM@À’ð½‚ÀÞýwH€‘3×›Š5‰9xñâ5j«Ý8fökz=¸0…}žjO€õ‹*ÊË“ÐòU&eÄ›+@1^`Ä>5 ª©®FJrµ‹mÛ\® ø–F.Eø„£r+ÀÚD¥ Ûœ³8y­œ¿Ê÷›ŠlšÙEÜð¡(À€›´*åû'®¿Š¢ò'”ï—ýϸ¬Hd"¼(%ðêù lZµ¡¿}‡ö->g)à3êq’ PUþÁñiØ”œ‡—/_£Îa—ýÏðø±€?€H¥êjjpåÜ é&‚>S8´ÓŤ"Pû`ࢠÎ*rüu¿_ä‡}l¶ÃM|x¯ §Ç àG)+ŽSÀòÐcÈŠ4ÕB¿ö1§0ÏmÜøuŽjÿß«$ Pn³ð¸œÅÔ7àxâôk-ÑËK`dÈ „DoRõ®»ý=æ¤ãØeøkäÔ/ðTÀrð¿ÊJK÷4„TÏ[qfDAû–ŸK4@ÀÒ}ûmo2Q€ö1©”ýJ+Ÿ¢ŽûȾՀE€™€’¢¢ *(ôîÍ$íß…îA¿® J4@À‚°Æ¡(€¿Ÿú»Ì:‹ƒéÅx÷æ­ºÛ/jÈ€€Õ ¨ `»@ãE s³1eâH´o!Ñk¦uÛè×¹þ6Ñ©ˆÝœ"ÛcÔË©¿1à–Å@ ?7w“@cFjðöõk$îß…Á¿£m3© °˜Ú.B—Q)ào§þ®³éÔ·oÞÐì~Ù“=°äæä¬Å_yaQH4 ´0³bÆÓdA‰X§`!†uXà˜¿9õ·qæúo¢Drý^€ÇÝ´dee­ðZ4€žÇö£wX‰Xª °çàݘ’æë÷ú©Â?éB)Þ½õf®_àÉ£G€ÌÌÌÅÞ”—cþÔHþø%ZÊ`!/«íBŒ ^†°ÉǨ Ð7ïõ§bÖŽ[°U=“\¿5àšµ@àÞÌȘ'à}Õ8p¼³#õh"úw h€€%¢ýûl#ðµ±½½çŸÃñ«6rú5rê·F àÁƒ+€Œk×fX+PUnâ™±ùå™) àUE´_ŒŽcU*ÀÚ=üé¹hïÜ}ðtê—}Å2ððÁƒKÎæs€K—.Å X®‹ éâÙSÙ·3Ee €—®î²ÞÚ }\“û†-¿ˆs·*QcÍn~÷¤§§G Xw¦ÀÓG±uírtjÛíš&XÀ+õÝFì§kVœ×>- ›“óðäéK5¶Wd9¸ïÞk€Àÿ8‘œ<€…L¬¯­Evæ ÄŒ‚vÒ@HÀ Q€áá«}ñijè“‚Ië¯áváÔUÛá°t‘ŸÀ½ªª€#‰‰Ã Þ¼1ó{%±Oc†_¿z‰Ä½;h¸ 4vA`ï;) `…«}Ýç¤ãPz1^¿zƒ:)ò³¼P_ÊŠŠ‹€ÀѤ¤!L'@£NË ŸåÿöU‘`-l%ÅX0-Š ½\h¤î€£ƒ–"lb2Ý ðR‘ÀÜ]™(»ûÔüÕ¾7o<ö7þoß<”ïq^=·üö®º#ˆ–™™™™™ëB’’ËÌ\ÛefHÃÌÌœ:åÆv˜™™cfŒu–œvº#OAo»o²½³tr&ïýwâŸäænþÀΤpè÷ßÿüµµ¾t€ô>÷[Næ±Ïù©IK“3ÆÂË'J“ €¨ež}¬/•¢ßä÷B»i0aA&4ÐÇþÚCxuÿiHüôØžŸ°M`ͪU}|&D¤¦¦>‹éåXD¶Æxì:þùé9@ Ü$X”Ÿ ½;·…ûo½Zí2x®”DÔ+RÔ¿ùþ׿§R@tæ÷'~1zÿ² ‹ËÝ7ù¹¿þ╟wàöüV@Mí{ `…=D0jÔ¨'1šÄô2LF 2í*šçùé±ðž»­¡ZX¾x¡Úe0 ¥,  ^_¼¿;ÜñqF½§û±ÑïÃ>saÉZlò Ô5ùm1\7^]æ#Aø­9HÌŸ?ÿ\y& vv$°‡Â ‡ 2äá@UÕ(\(¾úR² ÏöÂÏ«mÜW PµÒ/á㋬…ÙðkÆK¿¥ñkßÑŒ_ã§cüò£˜ÃŒÀâùspv–dƒ!ž7>ùÌ O²˜êÇ”ÿ{´¦#þ €€ÁšjèÝQi¿Aß1,‘¿üVB~ßôýmâGð‘¿>`̘1/Æ^ˆØù_à°gžyæòʲ²ìÚ`]Z§— ¡ÁiÁoº(t~³Ê¦£/ùnøéym([*+á—1#áÙšÔͲ€ܸ-$¾ý³¶,ÐfM‚ŠúŸl1~˜¶^ÙjÐv½šýŽ“ýóןÉëü„¸à×… ÕkßÝV~„™_[¹4jøðgü!Dì®°ŸÂáGuÔé%EE«ÕJídÓ‰ä † )D³‘Gr˜Œm[øé1ïEËZB"1åxÁ¯Ž5TÈËʄέ¾Ä„+àvY- À£,Àsô¡2€ýß»ÔCR—…7îÙJé~Íiñ÷s„ªß—LQ°Y´ÿsŒO~zn¼/iå‹Èçöüz6û˪UŸÙÀ%p?€€#NPû4/Qf´è3 ñÎ>ò=ƒ¡œ=½ÏòóÎVw’o€q¤„¸å×3ÿ”ÍSe7^’Õ"<ÃÉ©Z)€ëî¯ç˜¿*jìîG[Ö gÿzÚ\ûsÍàk#?}Þ–Ÿ^‹1¿.8è}ŸÂ4þ>0ÀL¤Ó¹sçDÒJ´]| D,$ƒc!hNÝp‘rÍ%ºa+hüF„ÒofgI0òÒX¾à'þ€™EªqY- À³/Ýו–2Ã|°»¿õ4øuæFØRá« Þ€©‡H¿Çè™5‚©гp ‹?òÈ ŸÀ9½‹`à9T¨>³/¾øâVÌ:ûHH @Ḽœœ™8 PS‚zºavÎCŠsîFþ€öŒ Ѳ šâåùéè7~Çž_ÿÿ¦²@An.ôïÖ¼ýZµZàÿï- @DÀc/Ç,€qvÿ}_N„^?¯€üÂòöéߢÝ´Æ`ͱDÚ¿öXK©ë¯E\3 ?"~øõÏF~†ãàÐþMÚQû7ø±¿¬¼´4óÞ{ï½”‚Îý©mòIò'`_JÇ—¹iӯإ©tÝp˜ÔœnLôXûnD¤k0:.NŸ1½áøŠŸ^wÏ1Dhõ²%ðÍGo¢ƒÃÝÅÉ‹°_xW¸ûýß°!P›Ýÿyÿù°b}!íØglnÕíÖ”ö'ôkQû§f(O„:¿ÆÉô;hü˜].ÎÏ_®|ÍÉ GÆ^ˆØí_à˜•Ë—÷S“š4cД«)=d0Ã’®^¦EÚÚgMê˜3Vž_/ôOù1à§Ïèü¤ÐkLÉÏ<„ÙÙrX€uCàSO g}T7»ÿ•3`Ò‚L⃵~½kÜ`«FÑK zúmFÖùLZ=žøé¨Li@û]ß ý3ú¿@A~þB,7S`? BwŽ°Â¡¨È¦Nú9jˆpЦT6g |íÞ¬d¹îyžß "޶üˆ¨ñ¢Æ¯ŸcÌ”ÁÈ}à±»hËaé°mHnÔîL};†d¬’’rìîÿoGgÎöD­õýGçmøüºÒùÍ‚Bãg²¾Û˜!ÀòrNvö4åkŽ÷°·Â!xB~HM}3†ÈÝä|ôÈÕÖX Žå×?««O†,ÀöËOÇÚdhÓº5Ðæ›O!ñæËeÙ v«à×nno5j ½:O€õÙ%ØàW·U/oüõæþ£EÎöü„øãüM¦Ï2˲™{Šö:¨òò†µkG+_s¬Âá±"hG@£®4Ð(`ÆÓkLŠñ;&õ«wÿ럱ágê↛Mñëœîù#_ÇÁˆ…sfÁÇjÙ`“ð²A鉤„–|KKèðöPX>{M¸Ær´€@?î ŒýëG“ý ÿ¶9o~>ÛˆÐø1¸œ;{v ,7SÙyߨ {‘8²S§N÷©uš5ýñ5¤9ãÖ ŠinaÒÜ®ø „?P?üÚ²Áq¿ü¯>y?î-°=÷ˆ $ßÚ ^½©9|ólo˜ö˨Æ)~AÇT7EšÌg´×¸²–güÎvÀïXò;&~„Yø–—øá‡$j<”ÊÏ»Å^ˆ8Há¨|ðê²’’l3^|ÇÎQéåî1c„LSœk~á×/ölä ÕBqa ëß¿;a;  å6åøol>Ð~î?J J 6TcÈQZ\œÙ¸qãË1ØÔ6#@;RZæ„Õ+WÃnM65ìE$jáˆY¿üÂ_·ƒdLJÿ5zРAχj"šyX(\ë÷üÏ/üz Üv¸Oç¶ðpãëQ`y@@œ5ø½—ؾë6 2 ¨ÎïêžÀ¿ç=„ßýwÜóÓÀysætÀ!@Ú`¾™x¬Ú©é®ÊòòÊ ãÔ§éݧ±4bæï"ü¶|´¿@0ëW­„öß~Žƒ„üÛ((€ëpªÿMÕà7°ùO°iU6„B5Xç÷>cd‘6wר&üÞ_ÿö½I(0zäÈ$Jÿût  L<îðÃ?§¨°pÕ•²SüŽK~á× -ž7¾|?€Úhè\>vö§¨”×GйkÃN?XCŽß§öGGá¯_ðü|Vgˆ;vìøˆ¿‡É0 cNÍܼy ­ðx%í;~á§FÁðqÚ„ xûå§Â» 6ºú|>˜à‡Î¿UÒ@˜;~)¨ÁOsH1„[û~„ ~¼öKŠ‹7^rÉ%—Q ÀÇC€dÀ‰sfÍj5:‚8„ã?~j AEY)ü2f$¼ôè=±˜((€:û±Áï«§zÂÄÔ9PQZN ~ž±á§¹ÙÙs”o9Má8Ï¥€Ç 4è¼Q[4 6+hoð<Ú·<™x+­¸@@”:û?y¸+üÔoçkü/Áä¢ z+ßr²¿—ÊRÀÃ1E“’’Ò¸¢¬¬k·bÀ.W doÚýºµ‡Gïº n­—¥ƒ"°¾ýÜß Ætù› pf?ÓÙ/¸ƒC€¡ƒ'㜟/”•t’ÎÊÏÍ ,Ô hÅ@m(Þz¸Kë¦ðÀí×bF€V ˆp»¤#þwïnCÛýYks „ŽŸ:û‚úDÈq@“•ï¿ÿþÝä[ŽˆXè«è_VLM§._ºt8M¢'‚AX½| ´þú#HL¸B[:(ÀÆñ×Íìï÷Í÷°~é&Ç/ˆ"p5™ &—ï¶Ûnçb™ÿWÈ®€Çà‰R}¯K@ „j‚/üêƒ7àî/…Û·IˆH¡ÍzÞhÔº:VÌ[ 5Nµ¶¤O ˆ0ˆ\²xñPj<Á×»J#à?›½öòËw„ûh @m`6›ÓæÎ˜ Ÿ¾õ öà " Ž#ÿNï‡ÅÓWÕmÁ”ëWÛú?“4øØøØHö8Yáìœììù±ÍÕáþ€êªJ˜2. ÞOzš(@B@9~ìîoÿæ˜?q]Ç/ˆ90‹W^ZZòüóÏ7&Ÿr™Ô(À¯€'*œ6eòävÿk@UUlOø^4A3¶TV(!0…€\F þïø[BÛש!>K º²Jâ#ö/ü±ÂïuÍÄ`Rá¿–>j¤a §4oÞüIu"ƒ5²FXà;!Pï¾úŒVhЀ’äÓF9þ9‹aKE%Ô†â#ø¸«lÚo¿5£úÿ‰ñ5P&žvä‘G^’—›»zk0è*(…„@- Š ˜”þ›O“8„@¼ Þñ·NÞ—¿ª¼‚µö;{ð??}¦AòbÎ P¤>ý裇qUYÄÀ¸©ÿKÀ™sgÎìMeÏ ñš+#Ô‹Ãöů¿ÎóóœÌ÷\ñÓcžŸ2PUQÓ~†·^z_}¾–ð¹àSý9þ4rüLÄoi3Ñv8 ‹§­ü{9Ÿã6Ì—áܦ³™ÇÌ9ã_sË/üôÜ^ Øg p+ù ëÖMU¾ã|q¶ôì«p)·3_xá…D5¨"è8¬A󑄇Q ‚7JÛ”V|Äoý‹0bÆÏsºçwh –°Ï›­¿új|= Ú†8¶ ™6éy/± hþ#NîÃHŸ¿é:âÅ=öÞy»çgxØïs…Ÿn>Ãrª¦qÜüçC iùß‘Tî?Ià`šÜt¦Ây«W­‡c™H»ñÛ9"óÍOï€Îøá§cÔøéqøéXü4˜±~ÕJèѾ<™x›* œ®>ŸÊQÔØ—”Ð2œêÿä‘®0ºKl\™žÓªq¸,ŠÕõEÏù£}–ÎŽß}6³ Û+?}ÎK~^Ìjkj ¤¨(ë²Ë.»‰ÖÿŸßóÿ¥ p *¹nݺ¥çwL­ZD´:xcâ¿Ãß :?=Ž.¿}ý’Jüw½çÖԄ˹Y™:l $=ý f°O€)°€o죎þo_ì iæ㶼íGÌê78^ŒÚ;g³Þøõ×ýÇ/üœ ²ßÅRñ¬éÓû)Ÿq.ÇÄÿø_)œ}ôÑG_™•™¹|›š¼¡ÙE¼|ºË>’´¿ ½ä§côø .ø­¢3?‹ M n,+.†Ii¿Àï¥À·_·†Ë”ð@ ÃÇ4ÿ;wµƒ.Œï)/)ÃÆ>ÜŸþ®ŒÈdþ¬}óïGŸŸ/Ùòó 3o¼ó»ï!Òaubó_UEE¥ÚGæAå3ÎQ8EÛþW@|®ÀšššÚ<˜èÒÂ]ßÜÍüüßɆŸ‰Œ…Ÿ`Ïϧ)뙟VC(Â[÷êØ^xø.Ì`‰@/°TÛ¿©npÏ—Oõ„Ônã`ÝÒMÿÔ÷«™HϪLfï èèc~ÏO𞟟ü¼èâ`}ýaô¿rÙ²±ÊW\€þâ_ÝÿûÆg÷¿”öÿ×P s/½ôÒ„âÂÂLlªªfU¢Ås>RaxlSÉÂO°åç½`àÄ@Tø«é;¸û%ÚwIaLÎ ß~òÊéÏøuänÌÃ}øõhŸ?·¼ƒ´?ÿ~倧ç_ø9>7üÐ'¬[³—þ]Jà$…Ãö‹ïô¿”T8ŠfœÍ5×Ü^\T”‹ÝžVj˜žW›ëW¶ªÕ5?½ç~ÃÏ: Qá§^Àe°Eù°hîløqÔPèÛ¥#Œé‘“RçÀò9k¡(·‚a§Oµý-~×MÍÿ–÷üÂOˆk~‚=¿>| ¯‘­íZ·NÆ Qá̈ÙÿqŸþ—½S8‘ê:çg¤§w¢,€K'Á£ZÁÂ(ã’¿::ü¶Ì cûäw¶(_Äž<* –ºøjsÔUíÆþÜ;…zæçGµöÏŸÿèð ¿ý¯];}Ï=÷¼‚†ÿ˜ÖþÿÉÞ9Y’aøl ³§µmmÛ¶mÛ¶mÛ¶ç¼¶gg§zg…ʽœˆŒé»ù£&·ç5²#2Þ _U•¬l32eÄ‹[zéÔµk×ÍÇ=„>ÔËcà ²< Rßå€Ïç%ÀgÑò±dŸ•¥îõ¦˜Ï×C|Åg ßøzá{ÄL¿ä¢‹Žââ¿6œ2^5_{ÿ­puÎë´÷Òùùçž»’·j`ÊÕø˜]r¾äº†ãÎW¾Ó¦* ÊçÇóï´|®ü/ÿå—·½NèÁ@sy翜ÿY1 è ØÊK—¦M›n4‚ú̘QO++'òZ+Ù58ßøQaùÌ ¯“O¢àˆžÏÂûþ§üñû‘NÿäÿòÕú×:6åB.7Þxã)ÕÎÍœ8]²aÚäÅøÆ×,üÆOüû7~òB­á?ýðÃG¼.èæ¥³—ù¿ñm \‡«<»zéåÃ?ïP(ÒäM“›x2ª&”ñ]Qù걭绌òñSþýc¡ú¯±£GïÓ§Ï6l´óÒXzÿù/þ³(@·ýöÛoO§À‰Ô@Ecéºà¿a†ð|Ä7¾|D‹[‘øzBÿý§kþáϰÀçøÆñ ¿—©ÿ6ÌšýÐC]LŽ`ÞŽ‹ÿ, Њó>Ý_zá…iŸ4 ŒØ€!IÀ{r ‹O„Òó¥¸óñßõü(Ý|lt ¾öw¿¿ð«AùQÆù‘žoù[^þá"‹,Ò—œ@ûÏ©`;óŽ€î«¬²Ê†ƒ ø–öƒº: >)dÅär,àZ,ùäG ¾Ke™/EñÙ¢ëD%à;œøŠë•€¥„i?¯:øéõß ûî»ï^´ö‹Êÿ2/+æ?÷oQ€¥e_ÿt?öØcªœ<¹‚Rðñ¿a/Ü¡ÿãÐ7ö¤ ÂW–˜%Äw ðYôüŒqðð¾Bù,Á|0>áÏç—„¥”¯Å‚ÆSUåþg?óÔS×ð¶¿n^ZË}ÿÅðþ- °œˆ´eK°ÇsÏR΀J¯tü(¾Sðcç_œ£ãóÝþ>Xb‰%úûõ¾'×5áÆ­P¬}ÿÖpE/jî@b™%–Xÿòòi Xë`Âx)A!2Ìg1¾6Mø.»||]ðºñóXø.|ÀN€`¬€¯˜ûŽ«þ'Œ7b›m¶Ù•ޝÍ=b–‘=ÿYìÈ[@Ü#`YYX³-p‹-¶ØÅ·  ÚCOÊáɂÔèÜò£à; +¬4Ñ瑾¢X¾'ÄÈ>)Ìਠ~®ñ¢ÕQDáÿ7ÝtÓY¬ü»sä·1G‚—/ZÏ»S`¼Ep;Î õºòÊ+Oöƒ¨š:EÁÅ+7ìÍjŒ ð{æùàgÄsá|}Ø:+| Ü‚]ÍNpP:Iðã¼P¾¢ª(IpÝòÇ¡|œí»½¾÷î»øµ½·¾á·üµmV(S<@z¿òÊ+·Ò-ƒik ¨rÅ1È8ÎJÄw€Ï¢âG|5Ÿ%|QÁ|…¦ÿ˜_‚ùÈ‹’%œ¥— 3Cæwûûã÷ß?+++Û” €.^Zqäw•þYA H¬Z«7@¿Gtƒï¾þúe@¡+ìý‹X0ßå)øQ\ò]ø8„ŠŸçB¢_ÙæëS ~”¾K’¾£À]tŸ—QÇèׯßN´¦×ú/p៥–ä wt£Ó¦M›­ð#8Ï œœB)æ…eðýG!ÞH†øæãz)àGá|¬€YœÎ83¾²æ…¢·'N8î¸ãcåß‹«þ›y)¡ÿ‚þ™ SKñ€(ãÒž‹Eúl¹å–»5jå‘êѬ$.á–´½·m|b."WÉÿ€‘,Ÿ“ü÷o|ÙÂ2íß¶îÕ7ß|ó¹¬üI:ÕTýËпþÉÃR+sƒ \ЛÏGqßB2ŠÚ+ª°Áßà$¢ð~¯R”Ùçc# ³às²Ï’ãK1~=ǵsdÌzò±Ç®cÅß×KÑð‡Ûý‚ª; Û hU(­¼t®D_xቾ…d…–à¤Ì ïCÉ7¾Ó 9âGÊ¿GŠèBfø€Y4¾K!ªsóBÿ¯¾òÊÝ~Íîǽþã j…þí°A±z€6^ºò@ê{Ýu×UYQQ5Œ€ÊÊKW1™4á0|¾ñž²BJÀ×(S=#Ê"œ“4ßG³ÄþføÞ-¾÷ÞÃ~­ÞÀKÞÖÝ^æýyç×buUýÛaõ+ŠÛ·ãêÑ~$wÜvÛS&OžŠŒ€Hoˆë£ ƒù.9¾ñ‘d€¯öæ*+ÁøÏ'?J–o|$žKžÿ,Ÿžýèßò»·6"å/Zý6§´nM·?÷·ÃR|Û`î Š{#àBŠP¸IéÑa !õ|~NNùøºàcž~ü%Ï·ùOÊŸº¶~òÑGOùÿ´×}/½DÑï÷ÏûÛaE«°õØœ­Él]ö½á†Îñ5“©0X±`â€ÿãóKÌ7~$þ^¾Ê‹Ë>þÉó_Í9ÿ÷ÞyçaïùoÌÊ¿7×nµEËßèÇ+ M‚Öþ# ßå—_~ê¤ñãÇPà ”$õ™DüÜXø—&å|’ÒóIŠË/Ü`,'2þÀõŸÐ÷ŸY>µi§jÿ7^{í¿oÄyÿ>¢â=Ñìg©¢?;¬@6 ZN-¤@rìQG>vôèAÔ,ÈáAüŠE>ÔàÈ0_ÆL=Ÿÿ–1>ÿ¬}ªï‚O1_?'ô|ã“×_UY9ý©'Ÿ¼–ÿ†µ”cQñ¿T}‹þì°¢@i¬Æù¤ÚFÀú[o½õ^Cþ…Œ€¨ª X¶á“)â|=ã“$ÎgÉ&?nœ>4/œïÒÇùüç#ßøm4aÂÄ[n¹åB^¥òoã¥1p¯Ø`ÿv˜о}û~øî»·h+Jµs  L<ùþïgdUgž/óY$/^XÙ`…š>P¤Šï_H,ÍS>I"|üž|ÅøÏ:_rùyä`9rÀñÇ${þ@ù7LÅ¿f¬OƒqñÅßüÍ×_¿ßïINaªØ`ÆÖqüoØ‚/?~Í´ðëbÕ!á|~,_ú×…äñøkH>=êùú?2.æ+Ÿ3ÉçG¹Íîê÷ùF믿{Íz+îî×Z(ÿ•æëv?;¬(0fü_a £ÜxãçŽ3fÔlJ Ô1°…„NŠÐB­„ù’— >ºNøÿ°Ã’2>öøðµ±ÂÇœÒóñµ1?Ä`Kšqøœï¯vnÆ;¾Ò5ÖØŠÿ¢Ú_äüMùÛ‘P$íE³ yFÀŽ;î¸ß€¿þúš,×ê(ÒxÉxba y>ö¤°w<_—ÃÄ¿#…ƒ¹¥çcþÞŸƒòI´|íøK޹©äË?9R·Ýrˬøe“ŸN¼Õo=Vþ+6¨ò·ÃŒÑ' w ìÆ7ŸØdUWÝš¶©L©¨ˆÈÜþ‚x\“¤øü·Ôñù1ôüâðñØQÔ%£%ûüØsô|~Ôócœ,ðIèn~¾Ó*…ü?Ý~›möe#v°zpÔµ%ïó_ÝË ó·ÃŒ€%„ЈÛ·åœTo´{ÙðŒÓN;aØ!åó¢Î‡Òøÿš_>üÚŒ™,É|ÿøyùáã×VH~ä…Ò¦“&N¬xöé§oôÍ}6gÅ¿¡—¾ì`µç¨ë:¢ÉWû›ò·£qá2ÎEµæðTO/ýkŒ€Æë¬³ÝÛo¾ùÀ”I“Üì3j¶ ¢P^ÔñsÁϹâ# XÅŒÔñ“ýü±1RD~]ŒBóÉãŸî=ÿ?ÊË?Ýoï½a¥¿±È÷wa«E]E{ß%K¹ÕÏk´¸¸wÀ–jÉaªî"%°1=íþüókª à,õ¨Ç碿å‘_ßEòñyz>f«ù$!çcH¶ùŠóRÇgvºø%¯ß7SñÈC]í½þ-Øë߸V¾¿5ßÒ·Œ®eÙ[´TÊß3ä½–æ\Ôj\جVJ`}ÔùA¾å÷Ýw…ßÓ:” ]¨­¥Ãa¹0…Þ,ÿÏø, >‹–Ï’#¾ñéö½¤ø}}”ûäãŸ^ýõ÷`Giòï.òýëÉJÿ´);ÌX¬Vq`#¶X[yéÈÅ+}å ïÖ³ç®oû¾ÆŽ?› /|½ðóOy~_ E3//ÿèðÃ?Š=þM„×ßK†üÙ¡Z­Žb?;ìHA¯® )u¸h¥—®4¸Em ú¶ÝvÛ}>ûôÓg'û⪆€6·¬,òÏ/)ŸÅøÆ—?)~zœýן~uá…žî×¾ÍÄ:¸ðú;²ãÔØK#™ïOw±ŸVO ¬‹pmúMX6Þs·Ýüü“Ožš8~ü$J PQ O2í¤Õ\#Ï|ã[„ÅÞ_bÂ9þ3ÈãŸñ×|uå•Wží׺-Ùëß”#¢ý¼ô¬ñúE•ÿêìHe6ßo‡¥âÑ® àAß'Á¦5†À.»ìrÀÛo½õà˜#†Ïô÷ ! Z±”ba0¾ñsðäc9/äÄTUTTÍeï€$‰bhñlÛ¶mÛ¶mÛ¶mÛ¶Í^sxk[JêrSYõÙIÕÛ;ÉËK~sE¹;kÆŒ‰Ûš²Ä_Ÿ”Ðꤌò^N*œÒrÉÿ¯ÛÎWLZL HËÔ€üôc/ÍÛqˆ@ý’%Kv<°oß­FcÛ_†#“VQàwVû(ócÁ‚SýNîß?=þ¡Ó°ÿuhŸÊl¿ -ïËHϪþ$}¿_LÔ6ž¤­Üô£/Nì·R|"`ê‘5Ÿ6mÚøçÏžÇrÁåƒèh4+ ¿¨LbÂÇjß×Û;ÀÞÆæå®;–•+W®3Æ.Bãy&÷ç%…4#J)IÿEÉ_LÔ€ô#ÏH»WåaD ì'"ÀZ ù°LåÊ•»nÚ°a¡¹™Ù]/77wt¾HR‚D?¡“>Ä_ß`££å¹3g¶ 8«ý&¬Ú'©?^â/(BrRFÓ±å}t`Ÿ¨ê5€¯`mLl> S*ÒŒ “frZãÆ{m\¿~Áë—/¯À¼€™8:§@ð£¤}T£HÞ‡!e/G[ÛwgNÚ {óÄ¡>6Íߨ´–ŸzüLê/I‰?<¹ŸMøÿëU¿˜¨¼-ŠØ/'iÅ@i6,Xƒ˜4­™M äÍÛaÁ‚SΟ=»ÝÞÖö;종΋,–¶ÕR T%},0fà̶}<= :ÝËgÏ®¬[³f~Û¶mû³D+±å|ÕHÍ, (AR>–ø3Ò¨Èý1±X à«âÜ´t°(IfåˆIWcª#ÆÂË—/ßmáüùSO?¾¸ñÔ¨×Û{{xøÓt.:öH ú:&úPJô”ìxd¾p\}äho¯Ü»}û$&üAƒáÁzZð‚ƒ'}Ví×T¡¢¥4©™…¨ÇŸƒ%þÔ’øÅÔMˆ@F@VbÌù‰A'Ç*Od :Í Ôeó"èÀ}úôºbÙ²YÇ^mƒ›{{sg£Qãåîîà다ÀD"10Â!PàÀa0…€€?,’Ô)±#¡ÇäŽäÞäÃèÓ˜àéÐã8°‚“ú:ÆÚBQž]»|ùÀHöS'MšP¸páŽÔËoÄbHVõ×ÔeI¿* "Uû%©h)@jfvêñ§W©øEîã&D€Ídd!‡«Éjeˆmse ¶I`l=žCÓeéR§nÓ£G!sæÌ™ºvíÚ¹û÷î]}ïîÝÊÛ·÷ Ï‡ÂÉhÔÁኽ¼==ƒü||"0Ð`p¡€‚ÿMr!D|„«œÆÇDD˜ r^å25„«œþŸ?þsÿ¦çø—žŸ¯ÚóÓo>þÿDÀ|ËäoÑšùÁ¤$>Ê;ôè}Ýœ z½ô`_ý×/_¼¸yñüùÝ83´|éÒ™£GŽS¡lÙ®â%{žðY•_‡ žôË‘BY Pˆ­ãÏJELºD*þÏTýbbB’‘ã¤fí¬ä`y9`Ê@E’áªsBÀv$ä*wþ†üzv]“,Y²´kÛªUÿ!ƒœ2eÊDh-L_¹|ù쵫WÏÛºeËâcGŽl¾réÒ¾»·oŸxòøñÅ7 .(ïß?°47nmiùÊÎÆæ­½Í{Í@u°À °Brñ ÆF¯ÕÚÔ ÓÙ 4Õß²®ç¿ô ð K;;s€>óÎ7øÏKsEyüöÕ«»ÏŸ>½zÿÞ½3ׯ^=töÔ©íûöìY¹~íÚy«W®œ»tñâ™`SF5¶[·nƒcع Kƒ Àãîîîîîz…½ÿI6@BQãf |øÃÿMº{™üÏyRæò‡¢§kºL ÿva Ö“Ò‘Ó~´A34>râÇqµP/·mЩc@nfùQ.ÃZrCpGÜ\ë8 ;qC×tCþ»Ü½™¥îRZÔù·x|‹÷éoõw¼Î—²?†C؇)üUX‚y˜¥ß'§ýhÒ?âóúÓ¾ãxÄ[zhJÆ@ ÂLè ‚õ0 öÃ08á88Ó@W"ûÇ&º3³ws«âW‚'yà›})y)úØ“²ß€5XEþ LÂ( Àœô; 5”~í;¿ãxè­ÕBÞ È3AW¹A0“0FÁŠ ƒ Øâ8Ø áŽè˜t0èh(.ÌìÝœg¥ÐéXI¹ï³àwùmoÁ¦ý*,‡²Ÿ‚‰¤ð»¡ÚÂI¿>–þç¿ãx èÍ@4%ƒ ze Ã(Œ‡a0Çq°È° +°ë´›ÄÁ bÏÌÞÕ®Ø)…®¥NëRî+Rð ,ùÙPôã0"eß=Iáë›~Ý÷+}Çñ¨…l´@GA't—§C0 ¼5>'ˆ˜•Á0¯£ÁÌ>Üb(ô¹Rê4 “Rîc0*%?XŠ^Nö]Ð íÐ ÍÐôHá¿Òw|„QИ ƒvê„.½Ð'Caã` Q3³1ªeNC¥Ô©_Ê'yà›=µ±è[BÙ7¼ ðÁqœo>â(ȇŽÔ mÔ.: ‹ºÍìÃuA§h§¶BËšbÉS],û´ð•ã8?qä£ T'êEƒh¤&3ûTÔPh±‹ZQ“ý.{Çñ0 ªDµP5A­™}ªQ¨ª2å8ŽƒT¾@•™}™Êøßúÿº'M‘ʼn7IEND®B`‚ic09›K‰PNG  IHDRôxÔú› IDATxÚìÀ10 Ø?$!0…ß6Àƒ$ÛöØ» Y– ÃýlÛ¶mÛ¶mÛ¶mÛ¶mëÚÒ\ÎnwUåÉû²'¦×6Îñí(8ªlÄ΢ù|~ïýú!„-̶"²ƒÙÉìlv·ÇްûÏ4¸ÊÜ`î0÷‰È#æIÏÚå³é¥ª¾b^­Ç›"ònc¨ê‡æãŒˆd—ÙõÔgæ )H¯g·³Kó•ù¦ßšïªRÕŠ·¿7?4À]È QÏó–Ýþ¶¾×AU¿*ËUy}?7WyoÔtû½F¾ßÞ1¯‰H­ïcyͼà™Âûß.Í£"ò0€»Ì-æZs‰9ÏïœÛ/I’]ŠŸ±CÛ™­ÍÆÞû5í±¥'Mš4Ï‹/¾8UÄcÉôù½÷놶I¿hìú)îJfùÌ|àgÓà 5ã›Ìk·`©jÀ(Ó/„𻪦ÃÔ׿]ó€Ëœsû¦CzÇ›&I²ìÏ?ÿ]äEä@s·ª~d—?ÚåOndªZ€ˆÚª4&Q£Uõâð<€Óì¡JKK×Ïår³FŒ±ÎŸ}˜.î&<[Ußð«ÉÕ·  ¢®§Î€SÕæs·9çö²»×íÝ»÷tc¬ãïÆO’d;7…®‘QmCAbþ2/8çöÞ¯µÏ>ûð<Æ:BÞûÕœ ªØR‹=QÕTµÀ/æÆôäßÁƒÏ1ÆÚ|Ñ?§xfô¤vZð‰ˆAo»¸?zôè1KÄkùJJJðÞ%"ŸW]ôµëùþ3×zï׉c-²µ¿.€Uu`G_ô‰ˆÃ€ªÆ"ò‘sîÀ±cÇÎ1ÆžW›Á9·§ª¾câβèULU{8?ŽãE#ÆXí1b¦ôL[ù©³oíef®O’d™ˆ1Vy‹ß{¸ªþÞÕNæ#"ÊRÕÑnŒãxñˆ±nÚ&²ÏÔ"²§ª~×¶ö‰ˆƒÀðÂ¥éïDŒuóú×TÕ×»ñ?qè)"‡ÿóÏ?Ó–o1ÖE³‰wÞÂ-ª:‘ ?pø(„°AÄXWÜÝŸæœÛÀÜÝODdŒªfC@IáºñãÇÏÁ½¬Ëd¿²µ0€§Uu.üDDÜðK>Ÿß¶ÊFcr«}êßÝODDiªš„®±_"œµÓ Œ‹ú°BwØ{94|«Ÿˆˆ²B_ÚÞ€U:ÃÀ¸ð›Â‰~+©ê—M?ÖODDi ŒŠãøÎpH€q—ÿF6á'"¢BªpÎÝ4lذ;ÜÀ¸ø?ðÀÓ8ç.PUîò'"jAY"òîèÑ£è0CãâŸËåfõÞ?Ùz»ü‰ˆ( Àï£FZ£Ê¡WÆÚþx¿½ç‘Ûì,""ƒ“$ٮ݆ÆÅèСËø¹l«_„P"¢ÿÙ; Wg® Š×«bu3¬nö´8uÁêÅj8ý/ª8Åk¸Ô‡ÖÝÝûü%}ïÌGoÊT–üÃZ6û2'ÎýÁÍdäîÝÍbNÜ5Noö¸|ÀBŠ€´Lþ~šß¹~U¿ú<|±üTúߨùÉ÷"àÏRܱ°" -“ÿŽ;.ö‹ûä;ùÉO 4ÿÉ÷"`£ü¹Úý½HËä¿sçÎKÌìûâwË÷£’Ÿü•J:ÉO¾¼è|% -“ÿóüñÇyÞù÷úƒ ÆNþi þøç蕟üä»m”‹°ÝÕy–¹Å9föyëÓü€¶Ÿ?XQ~ò]Ãò±ò“¯¬—UÚÛ;+Ò2ùþùçÇmll¼÷ÿäalòäxE5ùâýä·ÿþ“ï1zÏ®]»®m^¤eòwýðÃG•ä?©œêW€œîu×Ecùc¥•ã蔃øEð1ðëiæ· ú£á¨Çï#+ùJšï±ú×ßÿý²ÆE@Z&ÿ·ß~{~yßgÜ¡ê몟TÕóºüä=ðµ0F~ñ€˜/ ×*ò[Õó-Çg1ßÿIð³O?ýôäFE@Znú+»Jõ£Š‹;%ÄD  ª8¨ÄGÅ÷Ï ÇsmÕ5£zïœîù@{¾±Dà¦9 üo>ÚóiþC> øºcŽùÉâ¸`Fq`nëë믖Fîë®´4‘üËF’[Šÿ l*‘A§¨úš .Ù©†è,ˆïžoÖŒtÇ׉Wò!–.ñk||øç¨ð ÿÖi½>·Ùlö„Çõ"Q¤eòÿùçŸÏðGñîÄâîƒ28òÒšw‡~O|×ð|³F|Cð+0+äÛøøz_†.Xâùw6꞉Pg¾§øt_ïã‹ Ç×þO* Ü\ûÊéw«Ci™ü÷ã7Ž)ËF¯Óõý(@Å%¨`AI,²hÃgvòýy>³˜ïÇÃ碊Þó_i’øøøü8~î"¾?è›_e > ˆ6n/ÿÝreT¤å¦¿ç÷³éôqw˜pI â*:Ô°¢EÜÝpå]“eã‹å»ˆ_D|@óÍb~Ñ |ßμZ…ØÿÁlžÿxLâÛ"ù€æKàþÁýVó‹b~òMñ«1÷ÿûÞ›L&ÇV¤ËîßÿÚ÷†¿ûs·@ǹŠ"ÇŸ{ Úóæ=ßjòQåo~¼Ûï!øÁø.J¦ËÌWÇìÃïߥ ’*?JþMøÀð|€ø4vÀGÄW§kF|ÌFLJâûkÒÿý~nec÷c÷ü·ùKË¥ÿ>øàT߉–UãîÞ…úU$‡»gÕàƒ^ÿ|ôÄ·ùk¾ó\X*¾?œgÌðÃU±Í6’„͊Иñ ‘þþ·Šï÷Ìýß÷ 4z×å¡€´’ÿE­­­=[r¿î¬çÛ´h˜+Ëû¨kCÀ˜o}ñÍB>–œoÄæ³/иËØo¢Ø MLGû°¬|. ¾ò?ÿë›ïjÍ·øðoD¬ÊXã¿~}29!dp ÿ·ÿ-¥24•€ŠâŠ5èòÉii< ò-øÉw5âû8ÄŸ—£™×’ñÃÂÌt±¥:Fd|ÔàW>Ì¿ä›õÊGC¾-9®àtUúüܦÓéÓž\Õ" »ÿ¢mÛ¶à›âò˜1¹‹ª+Ùº|ç›ø&øü Ã®ÍÇð|¾æŸUM¼‚O×Ð|´äó}{>ºâ«û1ñýu’ä3ǯí²^N÷¾¦² mÅ–þ‹•óC+¹_99Wäâqð>9µ -ùÉ·|tÌÇxø1ƒÔÏ÷¯“æøù¶ ~òcùYï¼øâ‹G®è¡€\úÿæ›o®6³½¥"N#’Ѝ’E÷Ëï¯ ßêðÍ">+à£ßZò­#>ºáëBBÏ£àëyÔŸG•×3ßêðÍZñQÿûO>Є3ü¬€;v<âyá *²ûàŸÍf/бYPg†æõÂO¾ Žâ£ߺà«îGñÅb,1¯-mý±†ç·HjׂøÉ}Çcÿoåâogy^XCyÁŸƒæO~ýõ×›÷óÊVW“ÚÙtÇKÒ ¤>?ùó“=,ÁçN1b4äcyùñ…§šÏòkÉÌן…kn»ÿbï* £H³õ[ßq×çîîîz|]w7w3d,ƒqÛa±QlÛ!h€˜‘&B̺þ¿ê{U§ïy9¼>ÙËåo:Uáæœïtzûc§î_÷»Z…P@£ÿ—^zéÊt:½€8•§¹ŸTå'äƒß&š?B×5ƒüü÷ç‘ÿë/à÷óȯüG~*ÿv8pàÏÉGh`È:ŠþS©Ô$Z÷ËGcãt3jyŠ‹áˆ7¿òûyå÷é÷>à2ð~|W~w§Âó+¿òçÐÕÕõ&9ÿ!œÐÆ¿/?ùä“צûúàÓ]’š¤›Á»+_þ)¿Cº‘Áàò3|9üèµ'Ýúö£(K}Тзpê½uhût;ºƒ×Ú ‚@òßÛÝþ$•ß]å—ÃýÞdÁžƒþùŠ¡¸&X£ªýOÌŠþ9¸; ÷›NNù•ß:ð³ þLx„8ÝÓˆ£ÍÛðñ‰¼V6+öŽÅ³Å?ÀŠm· |Ì,Tþ Ę[Q3ç!4>½íŸ!ÝØ? 1ø®Q›ûõW~ågàr6  ƒœÿW†^@£ÿ¯,Z´èê0ú/ÀW¾À; ʯüÙÑ~׉ãÍEøèø³xyßTß‚K‹‡cÙž‘X¶w^Ü1Ç&ÍGÕ¨¨5•#ç¡bØD¨ž~RϬDÇ®xm”lÄ ƒ„óçßþ”ŸÿÞ Sþê,-.þKrþCmC FÿµÕշШX8{åwÿœ^'ªë@ôšê¬ÀŽš×ñzÙ<,+‚Ý· z-Ü3ê d €Ñg _ Ÿ‹Úy ùÍuè9^•á`}H;c?áüÊ/ttt¬ Ÿ¡Y€!ýõOÿôO/îíéÙ àü¦˜òÿÊ 3߀èõºq¬y6[„ç÷ŽÇs»EúYNŸ¢bD(n™ªñ·£qáóèÜUÓÝêPûK¾ý)?eŒ1§Ö®]û[4‘,€Fÿ_Ü¿ÿ¿ù¾Ÿ|_ò'åã;z›±ïäxëàí‘ÃÏDû{F‚œ¼Tð5QV 2uw,Äéõ›‘>ÕLBj{‰çW~hjjº‹ü‡f†Bô‰€0µ³Š¢ÿDBºòƒšúšºj°­úU¬* ªë³_.¸¬ÀÜP ÌFõŒûÑüÚjôVÖ‚&ôÚ'–_ù š»í¶Û®#ß‘ä½ýGS:¿n<¯‘–>(‰Íí#zmè(ÇÆÊåx±dR”æÏ®í» 9FRy`âH-{݇Ãú>Ó' Pľï#Ž9ò­¤÷¨ ‹ŽþÍHbô¯PÇoŒAÕé}ØP¾(ßË8þ=Çï,$åÙ¨{¿€®’C°ž—8! PÐHà»”øZr³šþÿÚ 7ÜpIOOÏŽÄ…:þ0ÆCEK1Ö},rØL}?߀ïˆ^.@çOöÁ¦Ó*‰56¯^½ú÷)ˆLèv@þ¿¼mÛ¶¿³ÆtzóQ$ âO{½(oÚ÷<ˆeÅ#QPÌ8þÁ¼ ‘óQÿ3èØ¾¦·/B@¡€ššš)äü“Ú  Í©TêqôoÌà ò_pŽß3}8Ö¼?:|¦£¿x ÇÀ#üîh—@49PßÓ¡(æ„€ÚŸòÇBôtum }ÈÅT ‘@ýIJóß×í×~íò0ý_Ûô¿B#~9þ]xÿðQS9þQ â(xDB šˆ„@»f15Œ·}üñÇFÙä$5jú?ÂÖÿÁZKéÿ¨TåW~ªñ{ÆÃñ柄ŽÿArüLÄŸ<À ûŸE´jØP€Ú_œø•ªªª&“?IR@›ÿ¢‹V[[;@² ÐåçïÎoL„<òSsŸIãDËn¬>ò0ÕøÇŸpÀ y¨ðÿ ? ö§üäýüÓjàB_rQˆ¯'¨PÓÿ!.îìèØ àÜŘ|;UîσͯürNçó9þGŽŸ‰ø“.ä=seB!ÐYT ›öà²ëhL¾¯?÷Ù„ó+?¤ÓéúÂÂÂß"Ÿ’2€¦ÿ¿¶jÕªßóúú¹t¬‘Ñ«'‰&‹ß˜!ÊO˜ŸøXTŸ.ÅÚòÇÇ?´£æ¡á‘¥è*9kì¥<\ gþù•Ÿ{þìç”––~+aeMÿ—••}ôä?Æ\”%w@ƒÃOð+?=Ù[àÚËñáñ§±<šã/¾å ǯ€ ߟ\übf³`tއV<ó¶@¿K¿òó÷8ÕØøeúËú³Ÿ3Óÿ¥R©ç0†Å“›‚äïLœêÔ„¤òX‘~rüˆÉ»±¢+öŽcø¨à U» ©e¯¡çD5| =åN³}·¨’ÿ»½`ùåó@ww÷îЧ\â☗4ýOéòð¢`ŒÒÑhå7)?÷9)wRùÝÒ{òÿ­ˆ?°à-ÝõØZõ^(™€çǯ@¶b¸jÂ8õÂÛè­=™AàñGàìÓ!åíȯüçz¿Í¹Ø@ÀK§[Þxã?£àRË1_þsÑòåËÿ¨¯¯/&ZàRHôçÞ3Eesß™õgáA?!Žügþ§Y~´÷6agí[xißÚÕ/pü*DB zòÝh~} ú›@B€»ærÁÉŸA„œc~å'ˆøEgûÌ3nEEEߥòrœŸ  õÿ»wïþVð™h–1VÞñóõh\ëZ§Çˆ”„ó[ëÄÏ_ >"Çî¾6ìiXƒWJgŠžÎ§Àýéƒ53@ëû!ÝÚÆmdl‹ 5ëÜó[;”ù‘/8Çüÿ·³P__ÿe—/Šé8 ŽÿÑŹ¸ººúnLºŠ1RÇÔióÆÇ+åóG~Nİ7Aоþ©xýÀ„Ž©ƒãWà&jç=‚¶O¶Ãôô 41à"@gÄŠ}¹ý+?ÅóAþïx›ÈÞðõ\ó>­ÿ·µµ}@¢>Rù"#¢W‰ã’öÀFž~þfG~yÙˆ^O4á½C÷Eã|ÑÎ~7G¬À#æ"Z(TwïS™'—¿ø(ÓÁþ‘ߘ$ñóœÙpÌúñß===åÿñÿñó$bÚ õÿKo¼ñÆëºººöÖ%i‰’•üŽS¿\Īü|Ô!à¤Î~ ÔµƆò…Q}ŸÙׯ`°¶ FO<ùÄrt,§ÑÁàlìs RÛ•Úßùä'$ŸÏ*€ ä—ïÃx^ïG}ôï”eŽi€Öÿ/^¹rå_§ÓéVàU£<`Bœµ²Í†¤á…ç·Vȯü|ù€½ 4uUcSÅr¬Ø;–™åW·ÑÁÞê:jô¹sÎg§†sf|¹)¿üÖ&‘?ÃOÈ#àIĵ@çÿ·oßþ-ß÷q*RGÅFþÝÌçø•_òo°³ÿÅ’I(`:ûUÄPÜ2Õ“îÊL ¤šA‚œýñöÅÿYb÷ áW~¨¬¬¼7Ê2Çt€Öÿ#ìß¿îÂ2ÏâÜêS|ê‚åw¯ù¹óÃzú:Prr^Ù?‹Fú˜?ñŸ˜ùZ×o‚×ÙÐŒH¬æ>ˆ0!âɯüpºµumèc®ˆ_€ €¯ÒE¹¬±±q €Üƒo`“Bž¥P~`\… 0ÆCyÓ¼}ðŽh‰Óà§ iB * ÔݱÛ‹a¼4|@b—¼#‘G³ÎüFùapá€ÎÎÎâÐÇ\ù™öh`ˆ«Âq-rœ"–«Mw~ÞÉå0âçù | °@}ûQ¬/2ç ‘ì?üã‡O>Qˆ®CÇ2‚!ç\`Îö¯üÖ:óœùiàÄ-·Üòk$è¹qÚxyˆk €&à “Ïï°6ÇüÊï›L¿µû$¶T¾íì4ø©H82‚coéo¡·¾ÙýÙögÓþ•ßîüAÀó¼®wÞyç_)ؼ(^ÒÀËæÌ™ó;¡J«qîÆ™w~£üà¿¥ ~í(®ÿ1V–Ns[Ý« ù‚ÓîEë>Bº­=«?@dù¿/(?çw¾µ|Û¶mû•›cÖ¨/Y½zõzž×PZÏÈ @®t™Ïºóœù•?°€ç¥q¤és¼yà¶è)}‚Õ½*†vÀlÔÞö8Ú?/‚I÷÷±ó÷#8#ÊÁÁ‘;óg7íÜ9’@Ì6êÀ%[·ný®ïûŒ¯ÄÉSøƒÂˆþ×ù•Ÿæù#ԵĚ£FÍ}LƒŸ € #2ý .C×£0~lеSÆþÍ9Ÿ å· ¾Ê¶pôèÑ{¨Ü—Im¤‹qi˜žÆU!ä|%Iªüs¼hQ~šçGKW6ž±Èg”BÀOë_oCÛëoÀ¶4Àfõ0Á‚Ðþ àâün„øûå¼N™½5›Ço@'./++›àœRWF} ãüEêšá¼5¿=Ÿü4Ïß›îÄžú5xyßTQ_€¢jÌ|t?8¶p6ÌÎ5°Ý°ÔÀÞ ²Þ‹Ê]¼“;ü6¦ü†ãd æäù íôéMQ³yäob"t€ÔØ•uuu˸¥äug÷z7¯¤y§ÉòÆŸß2ü†á,`űæ"¼}ðNq_€¢bä­hZ0öé‰0‹ÇÃ,óÊ}0‡‹`=€¹gä¡ôxó¿ bÅt´·ï}Íu´èë1šÐÀ¦¦¦5œ0¼ã£ÔÈxUÊóKÅEBø­+?¥û ¤:*ðá±§Ïí=*£ fÂ<ô-œ €§‹ÇeÄÀêga*`ÀZŸºLés‚|–ñÂáwÈ&Úò@8f^úšC\9Ø£€*hÔØµíííŸà/®# £åÆ%ˆòÃÒX_Wo+¢½ý/”LˆööŸÓ<¿ EŨ[qú®é°Ïã?`yn:ÌÖ·aÚ[ak¹s‘ý^ ¹>#Câü<'Z˜HŸãÝÝÝU?üá‡@Fu €ëC°£F‘:¹ºÖÖ9‡7Dù ?~f¬ïPj ^/›§½ý*Ôù/@ýÔ¹ð–L"‡?–L€Y8æ¥;`J·Â¤û`ö¼ðŽÍµŸ'Žü ˆx~‡DÒ½½-………G>G@Lv\↮ÎÎ}øô>§Dƒ“;:^ˆIglrùåâ!»»ŸÆúaíÑÇr6Ö§@ÑùàTØg&DŽž•…yçI˜êC°Œ ‡ûûJ76Ç“_þïõ5¹ó¾/îyóÍ7ÿûŒQÀAºàª_ Bpaò†+0æ3Œ«kyZ+†üVÌߟîoë9…h}ïrëS³Æ¿HÍžMu!"ðÌd˜OVÁ¶¦`¨,À• |¶Rº4ÉÅ–Ÿ çãAÎo¾µ0žç¯Y³æ›ñÚ àÊiÓ¦ýqOwwFñ‰ˆW‘Ìw±†'1ZáÈI<ÿ¿›2é-Ö±>¹CõØèyl ,#زÀó `J6æû`Hœ©ãyú“gÐÿ  «‹‹‹çpLc“qfgræxí€Nu`~[~ž¢~t÷µagÍ[x¾dü 4ù)TT„h¿O<ö—Ç&Á©™&ÁÎ6˜nN/ûŒg—üxg› ~þ^žý^ÎÍÍÍ›è‘À×ÄC裀¯.--½ÀOUš–¢ˆ™qŠ~ÆðrÈïyyä÷àÖXœhÞ·Ü®M~*uìïäŒ9qpöüwÃÙ c©IΚÔ1’pgî?ìû$ò3[O=± ‘ÃYZËTÐÖÚº+ô97^ @¾Hà"êļîÈ‘#OȾp¼âä.:ãøÅ*S¼r~Þ)Ÿ_~‚õ @£}F7ù©ˆMã_Íøyè{rRœ¿IpMLs=L.Jeœ™Ðáf;Éøñ{ž” ¿gù ­­­hP€þô? ˜ÀõåååO 4ÈG»|J]õËùé•ϧâóÊïÛž‰×»«JgDQ¿Žö©ˆÍ¾ÿÓwÎH‚óçG—Ï…Ù·ÆKS6€u¦;Qþ¾ÇÕÆÉ/¿gg €ö¶¶âøW‡¸!\ÏX€sZ£f Š p òÔáÊ7ÔßÜY‹Ž-Žêüõ«ˆUã_ÝÔ¹ð;ýÅkd0ʼÿLª´N˜;çÌùg¢kÞQ:?½çÇGý ? €öö½1*è"ÜXQQ±‚稙´·ÀYòb‚áa žãÏæÉ¿o€´×‡ýcå¾i((Ö¨_@üÐñ³ï?‰ˆ²ËfÁìù&ÝGÙÖ!÷CþîÞ“~ž‹‹úe%ƒlNP $f@@eeåËYM€2GÉGÒ¼¢”ñgƒçç•óyå·Æ"°@ª£ëŽ>ÍôëB±ŒþS³f'ÝÙó „Þ]s² &8c ó)¸—ðÁÔùåçãø~>“W  ¶ºúU|ôË+GyÇ?¯@…:Ï/?<îü¾ àyiDk|_Ú79¶ }*ªÆÎGÏ£‚±¿$g–ÎD´N¸¿7@Ø4ìy²ˆžwºùâg„BÖgœù ££c_Ì€ €ºšš7Ϙ` ùA¡;Dö ?!ü4×Oµþ Të_¦Q¿ €ïûo¾u& íûòXBÙ€¨7 ©.kR@|þùßËJ¡îü x~‚+L€ €›êêÞ wþr…èþwîÑxÞø}ã#úóÁÔf¬,¥Z¿:Pqû›8} 'ÁÈ£ÿäg ç”nÍœaë»;uþþÃà¼ñ;CÎO ­­4n@@}ýj.'w’ÙïsÉïh´îüÚ{šð鉥:ׯ 9cwOwmüKþ¤À†0§›²²lÔÌCº3ÅŸE~²Ùq'Ož\Ç€<¨FQŸAžT³œßz¾*ZöàõýóðœÎõ«HHã_}4ö·„Æþ.XÐÁ—î€)ß›é °vÐî?W~GAà\ Žs@@*•ú@bÀ;ëüóèMwc{õëX¾gŒîðW(t>øcŠÅcÃ×I0[Þ†é@>î?‰MMMŸ:¥üOuTbõ‘‡ð\ñ-ºÃ_@¢öý§æÌ¦®EÖ3Þz ¦±š/ ä?‰?ü€ €æææÍç&ÖXD8”Ú‚—÷MÕñ>‰kü«7½OV0E ‚s2²~VI@Á"Î%-MMŸË€"0zûºðYÕÊh´O—ú¨Hdã_Ëí35õÏ7fF?}5»$ Ht  €––R Îhê¬Ñ”¿ €D7þÕNš‡ô¢³ûÓ’À˜·‡IÕ0%E2€ €›[[Zv­Pdêý'š‹éé}šòO.T´ß+ûSD"`Å|˜ÿeï,œÛHš6þ?½|÷233æÀvŽ/ä0333sÎaf‡ qŒ2³c"Ù–vWÒóyä¨ÊŸZs½µ"­ÝSõ”liW}åÌMÿ¶§§»$ V($`¼É÷xÌýþœ†ÓØ”ý‘œíWpíÞÃßÂZþƒœ HÕ X•ëþÙ'™ò’à^xÀ€$ûùý}¸^µ=ìø×gÉ~¿‹%0jzæŽFÀYÉ_ÙP pi,¿T¹º:;³ô R%}ûüÝ8WºJ ûˆ\êØ_óدX9d¡É ÈX«§[ À• =T`&x’™qpþÞ¾vd/À©å/r;¨cŸŒƒo‘ûR©zª½pw»‚W­’($ìßëïBFÉBqþ"—íö×>éK ýÇ Ž,S‘É pO@@µhŒ¸:J Úø–gËV†ÃþâøD­Ëz›n¾ÏÇß ê½±®Êú¯K£ŽýÅÓÊ “`ùü*90ù×?‰¨êLnSó³Sû¡ð 8#\Ùoc·#Üé‰T‘§îɃ3…‹Q¿|#jÇÌp®oƒ IÝ3?ç±?Ѳ0óÏGŠ‘õÆLÀš7ÔöMÍûà ÌD8yNÿW @GËc,üâ%Ìšög¬>÷6>쇀ì‘åôDï+ WyÜûh,®UmEMWü„‚!{zÑ—_‚Ö]ÇP7a!*ß‹Šw¾Eåã’.ñ¯ñ«oãåøDËœ¿¹9}WRÐS6¦¿–i‘õ‡]»â°þ%Þ¾@Ò€Ú—0dÎÞ´K±Naƒ|c“±¯ÆýË'1î…Ÿb‹Ïaò»¿Æ‚ÿƺ[ï Œ­ò´¯j=dx ´-3ìô¹ìõÁ›™¦Å›QõÉÄ~ø¦’ªFCïüÑ’ø)Ç¿6¾Óiè)ü Þò~yRàï¸Ë‚íõÇ¾óµ¿æZÎíÓkÝ &3±â@–Îí³×*ñöƒÁ ö.›€‰¯ý_ý%Ò_zSS~‡¥;_êw`cîpÝǯš:]­ÚŠF¯¡P¶G(E Z·FõçӶޛЧÿ–q_‹óƒãW%”ý‡RÑ“3zÀñ—õ«TÀ§èkÜË '‘M'Î;oÛçÅÛOþ$@Ë®ÖN&çIíó0ÁÛç¿Oeþww´aaÚ‹˜ðò/ÂQúËÏ#ý•ç1s쟱òÄkØÐ’à~©ÂNª¤óμ/‘Y»þ& Õ0·¢ýðÔ|3Gm 0yñ9öWóéXø§ a¸_ÉØ™ŠÞÌÑN¿\9þAò¤¢§rL·Ú°^ç2o‚·fMwFìOJ*Ó0l;mþ}ûöùèU(”dcò›¿Eú«Ï+ÇO”þÒó˜ôƯ0wÎ?°æÒÛ²-àrǯö÷³N ÇlG¼F ³'¯ vì|q?ö7åKqþCåø—¦Àܘ‚¾K£á-äø‰|Ã[ª¢üÈ“®èŸ Ãþ^~<`Ã}9’hÚíÓ‰qÂ&}’'ïÅ ø—Þ¾™g`ìÿ~¬œ½^¯€À”~ƒ…kÿ‹u™ïõƒÀ(9@qü{}‹ìÆï೺ñ¬FÐÛ‹Î3WQ;n¸û=æ29ö7$áþÕ)ðg¤¡'ÿ3x+…ûuò|û X`j¢“·Aõë(•ýõT¿~ó'ˆ\6¾](ˆˆ¹—ö¹{ƒÁެŸ‡±/üD9z^¯<iŸýËö¾¢Œä$ïÿ ÇŸ>« ̈/œˆSŽ€wÖgrìÏ¡ãW¯þ©èÍM¿V‘<€¦}°Ì ƒ­Ur=󾃽~fVâmêí«¡| @’Ùýt§ûü6H’¼Ú·£%?[X7ùŒégà€æÌšøW¬<õ6&Y~€$÷½]y_!«á;ô™]H–èò¢#ãj¾£’Õ1Â!Kükúú‡PŽõÛSÑwK³ÏÏÉ“‚ÞšejMáCÿ†acý}Ê(*Ô¾Þñ;€Ó  Ÿ 6ÂêlHÈ0”þÿ~½zwàêZ~RÒß#÷RûÑûe–N\öB$‘&?à­_aþÂbíÕw$? ZŸõ~8«[îhdÖí‡×hC²«£+œ,XýÅŒø`œ£Ä¿ªÆ¡OŽýýð}þõ©ð'ûü¬h"àTDÍXùQ†¡Ý»7é¾¾}Ç®µþq@íêíë#Ô¾^€¤&DD*›hj&Ö ‡ÏOl%zŸúNJ¿Ä† û†ÿ×”bú{Fú+ÏqΞÏøè·X¼ñ¬¿û¾gèüÄñ«Pÿ–œ\«Ú†v_Ü2̦´î<†êÔ)¨xG|â_넯¤Þ¿]-}²Ï< =ø}~^ið–Ñ[­¶tëu̺u•[éšKï¥k&Æ>½ž·ïš^RH÷”­Sñûíä _ãÜûuút+1öuÿ©Qpÿ:&¼ò\ä€#¥?ɘþå±üЫ* ùqV¸tsÖ(\¨X‹–Þ*¸uÕõhÞ°UOT9¶žþkSÆÂX’ KžþŸ~Ÿ_ëÛ›ŠÞQçùitXþ®£k6µIí“«Æ©S0 ö]Ð=õ+iÂ>dRF¤z§ˆ@†&êÀÛ§ßÍÛªpz÷jŒ}9è`[`ò;¿Æ‚eÿƺRVxÈŽôõ;ÿÃ…SQÖ~ÁPÃnƒè½Ÿ‡úé+¨£ƒÚÄ¿úÏÇÀ\Î:?‘Úç_— ß™4x‹Çï|Í'`Z!ºþP Ž—þ®DÖ6‹>PQ{öíG^é}º\1½}7€ i'¥ãÄ FD Bë¨#²oŸ’r 4P±gÉ$Œ{1\8>Š”þäwX²õE¬¿'ùNŽôíÎû«Îk~ ÷2Lt]¸…Úqócö¨êWÏl9ö÷Tå{¦¡ç¡“}~G¶Á2$IºÆájÃíúýyÝZª^õö#bŠªi@{èJú$@9h$4NÈ“\CÞÓG èDc¿ÇŠ¢Zr&aû„ ÉçJk'}lã ³üµ50ã›?aűװ!GòžN ~[sSp»v/zŒvŒ´¡ª ¶8…ê´©‘DÁpâßão¿‘¬®|ï®TôÞUûüqxêgŽÆÈÈWÒl œ&šÄ¬_kuYÔùk¾Ÿ\«³gß%9’ÑÓ8~}ØŸîÁÓëÉD³øï²cŸËQ àëíÁâ/^Å„—OçOó^ÿæÌüV_¶Ãß#êïýçÊW‘¿‘8ŒÚF4¯ß‹ÊQPýÁ7ð-LÐíóoRmzGÃ뉻ã§G«fÂ4|äA‡ä&©Wýú§_5ë¶6bJï%Æ>‘Ö~D.« 9:§©ƒú¹îZ~OK7)!óö©í˜öU Ö¦:Ìýäߘð S ^ùªíðÊÿHÛa²Ïÿ^Øù+ž…êÎÜ(7(£ï‘]ë–ÁZöQ¿>ìåXßšøN¦Á[ð$Ü_ú¬•†žŠ‰0}­0#Qæ)[ã€ukš>ÁYÿÄmAÐûˆ]Ûö“ ƒ Oé+½>z¢P‡Î‹‹è·-ô°©PJjÄC|YáÔ߇Ûoám‡ÕyþH?µÏ™¡AÁ¼k°¶¤ÃZ2 ÖòO¥|ïAR¾71P>fOµÊ ë“&:òYÿ”D `ÄÚwÐ#ܤ҉sì4+•·O¡·¯¿žœ(ɹƒI¯ÿŠ©7ѶÃãÿŒ•ß½Ž9#¯¬°Úçßœý1®Wï@·ÑO7BÞv®î‡µæsXKGÌò½;RÑ{›ìó'HªŒð0º‹ŠQ'O¢–ìš©‡ÍõÌiGü+µï2à'ö'Æ>CÈÄÑ3¶4öÕxxë"Æ‘ÀÄ‚€j;ÿ¶TôÝ$mz“SžOá{|f8€D, 8ØQ §: S+ÝÞØ»l2©˜Hñm‡ù‹þ‰µ×H~€+ºõíÎû…-WŸuù^ÙxtÖÖ‰d[À5mzÏ¥Á[œÜŽŸVÜÓ Äw=ã¯M}~H ¡ö-Ó–Y_$ ÐyYápÛáÍîh;¬ÎóoÌú—+7Iv¢O \Ø ke¬¥%¸U üÇRÑ“—”ûü|5Àº5jIÚõO".ŸŒÎ·Vã_ú¹K€¶žñÕ±üpr¶^Ÿ5P»ÿPáTuæ@FrŒPå#öÎÈ XþIrîóïIEïýdÞçç«öÖ,bÖ7—€€DÜ/U¸7\xüËnÚvxδ¿aõ¹·’¦í°rü[rRp·þü^ÈH²aø¼}Öš/I4 ¡å{·6½î-삵P@`„ɲ,t¶µ`~ê ˜ðò/\ ´¬°j;¼6m‡Õy~•á¼d𼥑Ü#ÔT‰ÀáÅ T4 mzSÚô’ò½î”' =•S`ú;a–€€@2) ¥¾s>þç³îÿ¶ÃŸþK¶?û¶ÃûR‘ÕpVЀ —Œ€…àý³°Ö}­@àÙ·é=œŠžÜx–ïMT?€t}ÍÉAÔWz0óƒ¿&¨@üÛÏó'¬8þºj;Ìt¾×¯ý2JàqO9d¸s„šk8ºŒDâÚ¦÷S¾×Åà-£·Nw@@†@¢ QUœ‡iïü鯺ø¶Ãsgý«/¾—mµ÷ßÁ–œOÃOýÁ.Á‚÷ÏÀZËä8mÓ{™iÓëz)°ùFO…€€@²) ¡ôá=L~ã7¹(?àýß`áêÿbÝí÷89פh†Ôï†#ÔPŽÀþù°–QÝåøU›Þ Ò¦wø@Ù—0¼%ª@r€€Pxÿ†rþÃhÛá´ßcéî—±ži;ÌUóÛ˜= ·k÷ÂôAÆ0¦Á‡a­Håªòmz¨6½äXß°õêïz$ Œ3÷æÅUL|ÛáYþŒ•¯Û.+¼öI¯þòöû12F¨,Ö– ªŠ ýò½ÛSÑw‹”ïò¨Ž€Y0Í@²€€ÀýKó¿„uL|Yá7…yóÿ‰5Wž&?àýðñ¾ Ï´ûê!cdP{Ó ÁOŸªM¯ïÂèD·éM|GÀŽL˜fÒ­³7OíÇgÝŸðâs˜üáo°hýÿ°þNäØ ÍòWaÿ5»äxßH–àÕ}j; rJ€†ûW§Àw<=HùÞ ¾¶«É€‹‡¶Æ£°kË OÿüX~à•HYáˆó×ñÏiÌ€ jœ…µ2ˆìó{SÑû€ìólh9+| Ð9â†pz窴vAÛáÉŪ3obCîØ”3 E­× CÆàÊ¿…@¸©Ð§mzoÄ*ß+Ð÷øx2& H@ ݰã4m‡…¹‹ÿŽ{•G CF¬aÝ? ßéOumzEª%pÓþd¬ ¥€÷¯˜¦9 Rýæ~ôdg¡­¥5ü÷’!C UC£ÛëE}]9:+&Ã[–¦s‚ ;`šR x$€€ß¯û,aöV»MÐh|ÿßeë¼q(.(DAîC”y<èê섌‘=úúúÐÐÐ€ŠŠJTTÖ ¥|zJSb9@‘'}õ›™^®[$Àý²L Ûæ|Ó?£PntbÇxŠŠQ˜—‡‚‡ꮨ@_o/dŒ¬aš&ZZZPYY‰ŠŠŠ×ÊZ4–gÀ[*ôÖ­KÂf@èèx&`p×$ȾešØ8- ã_ˆ¥Ioü×ÏžPaE@ èÑ#4ÖׇÿŽ2†÷P=3:::P]]qüƒTÚŠ{ð–~¡u‚+afâÖ?æé_@¶.òû•®Z“>J€*\yæ‡Cö[(z”¯?QAn.<……hkm ; ÃoŸßëõ¢®®ÇOU…êÊBt•Ž‹”¾E@͒Ĭ}üwÅÛ¾‹@¶øÉà÷Ç—RyûÌõü}+ƼÓ?u‚’¸ä«7ð(ûzÚ ƒ·*JKáíá1ü~?ššš”ã8-TU–¢£l6¼¥©§OK÷VÏcÖ)ö)] ê¾ø8oöÝ€ý‰cÿz³&ŸÇxõßïïëÃ’/_Ãø—¢¥¢"k&¥ 8z±A@½ÖVWÃ××î–e¡µµUUUÄñë@½¶—-•D@ ôTÍÔ¬ŽÖ8;ßéÀ¾s? †Únvz'ë•ÊÁ“|íbUûÿ½Þn,þüeL ÷ä@IQ1qöççãqCCøo,Ã=ûü]]]¨©©!ŽŸ“J8 ði,'(P9¦¿—¬oäÁ$ÑWþ‰=þÑ]5:]S~D?è©Ýà'?y©}%Û‰€èjoÅÂÑ/†ÃÝÔ J`ÿêyð—(Çn[ù¹¹(-*BG[[x/YFòŽÞÞ^Ô××kÃý<Ôáqùm@`*L_Id@€sÞô>çâíÓ÷] Ì)†NuŽÞ9]òöµ×Oy u¶77b~Ê´G×Úˆèó*ËÊÐãõBFr Å?f?£ÈQÀŠ1ŠÒÐS1F_›€Èú£Y+y00è{v>§ö•4ÑR}úßMí» üþˆô”§Óà Ÿ‘‰D¿+rm”åòöÉgÆ kt‘‹€e¡¥¾s?ùW¿³ zõyœ;´%…E6¿>? ¾¦&œ\&#ñûümmmdŸß ÔW\О˜£¯YZ(ˆülôËæúG×>º³QYÞ>•Aí¸* P"Ô)ë÷Òi˜=ò™’ÌW[ö)tDÙúŠNà'U«+0{Ôßûà¹((šôú¯põäQRÀ)” ¹©) `2žù±>Í>¿SÕ ®â¼¥ŸÇr‚å`ô6"R ÈÐ<‘5Vs.Êà#œ} iìsÛÂFÒ€$’°ŽÍp“zÕ=ÍGsß !¥Èûzû”–#ߣ?m@퇠®¼3?ø Ò¢ô<&¿ñܹ|ÅùÄ‘;…•PVR‚~}æùR¾—†û«5•YÚb@ãà懲ȚR$Cã€É“9‰$°"ö#ÒÙ§QYòþ°ÉˆrƆ’>̹O?‘Ë0È÷’Ïõöé6CäUc? PÈ÷ªN€5¥˜þÞŸ @ÃÿSßù²nß@Q>)äT¤¬poOdÄoŸ¿¹¹ypùÞ8¨Õ•ùè.ý†lˆFÃ[>~o # _·èÏtK•®ƒ4b [ó¸+jŸ®«tk•·€$>(§h†>„>˜ùÉAE#¼}:é©ýȵ¬M¥` ˆªâŸÌ>¼ÀƒŽ²™1@à+ø»=4NS÷3ýu±ŸÈu'ìÛWbí›®ˆÐä}"_ì“Ì}ÄÁk¾Ã¦ýØÿQ ³ †àɽ‡ÉoþF€ÀϱìÛw‘Ÿ“Ã@\ój*+¥íðkÓK(C{Ù<ôH?Pøþîbú5ÏÎÓ¸.9O/½F"âíoß9ú: äZ–R¹§|f¿‹œï¥[öƒ! 8;3|ÜM€V\þñÀùýg ú¶Ã¦ ¤|¯ ­>“}~¾@ÚÊ PŸŽðw* )ú„eêàéúG:“@ì“{‡Ø¾ ¶ôÿˆê=~²PÐO: íj¾›Ø7È5Äk_½†B@მa‡'@`ýÔÑOöÿ9ˆ¿òµƒ€ ¦Mob  ­lEÌrÀŸÃß•ÓÂÐ=”Ð-LzÞ¡ó €úÏiÄÔ}r­ "Lè‰ Oqׂ$ÆáóNZÞ~òï]8(@mœùS(1Û•LÛaiÓ›(Õ µ|€ð«¿3O€Î¹òQK&«_ã™5•ïø™ïuI€D9˜4\¡ öµ÷Ñ£3 *x”yåI'@€hØ2w,<¤ p‚EÛK›Þĉv,[³# @ü9 ˜"i6Ö_M.s“5^¿¦2ö]0¹ps‘î,ªCúäa‚‚‚€‡·.ªp7q€?Á¶…é¤`ÂEÛ«²ÂÊ9çc}jÿ”-ß›x¨GKù&MG@_G–€x­|$–DM™ï¦²oßE Í€4Ο}"wN›Œ}Þ.}åá! çÆ9Œ{á' NP"»–LMV m‡‹‹#m‡¥Mo ¹|›@,yRàë¸Ó1ëã”í¬wvႿϾ=W$ 8˜ qã ªÄF@ÖÕÓó¿G9@Ñø~Ø»rV²Í(/'m‡]ܦ—ëKn¨Ããò]±rDžOák¿^6’¬‰˜õφm%˜®‰ÈóTîlÏßà•û!÷.e`ÌD œø9¬]OQ±€¶Ve…ý~·¶é%ÇúÜûbÉó |í™â¼þ9wèNì»$À?qsDjoO>NöõŠœ¸{á8ÆüO V! Ã—¸ hYáÇUYaWìówww“ò½n€¦òC±êˆ<Ã×~[€&|ïpý‹Ã“½û.†ââN’ ±¯àÎùc8ºy¹€–.)Qe…ÝÔ¦×Åp^€ØÐvK“À®Îw p?H .€0À¹£ šðÊ/plÛ*”¸ÜÚvØ0 'å{“ò`cù1m/€› 8\÷’÷~÷€€ó šxQ¸}NìX‹’¤«༬pC]LÃpA›^@Å -Ü€ArܱžJ@ ñ“Óùž€3‡4í€3vmp1ðe…[[ZT-ýg¹ÏÏ”ïu?4”ŸÔ€$öµ]Wu’~5G(pGNâ­øÛ§pú ä Uú«ÏáÔÞ͸>? ÜãAwW— Úôº*NGJߊ\ƒÁ@×_‰$~¢™Ïx àÖ©TáæHgöo¦@ój«ªT2^ÂÛôºÎê@޶]e q므ÀˆS0Ü<¹_@¶g ùùùhjhPN;AmzÝõçt Ðz…€K% à~…‚!ÜÈØ+ €³wŒ ùEEhwÐv¸§§G•ï%ŽdÀy-\Hƒ!\ÿnÆ P< e…Ëʘ¶Ãl›Þ5ß— ” X¸vb·@L=s‡v¢¤°p8}¾í°ßçãÊ÷’6½#.h À%˜#B!€ä€«Çv htþÈî ´íð㨶ÃêX_WWiÓ;²à"zJ?EK@Ë…øD|>g÷;·ïN¸rt‡€Fb¶îlo·é¥å{jË/¡µä Q”ZŠRàm¾ÓÈ€€@‚‰U@ÀѶuü¢šªjÜx”‰™§—‹¢4%cnzî"hZI÷Ä.[î™x>Ÿ“{i@¶ìmˆT9aqþ¸úðÒ3VŠ¢ôíñ¥¸^ò@À° ÷ ¸jBÒ$À]C–( ‰‰+EQÓ7g“ldž;* €cÛV¡¤¨ø©£€À©Wú`y,'(P™§€Û*e¶2Ù{ù÷éÚoû®9À‡×é5|øÈÉÏœ}]Þ¾€;çŽA€jÂË?Ç‘ÍËà°ß=¸$ €ûaÐ9d;á|"æaˆ»†ØZûîý„qÞ×Ù°g_oÖý`¸{þ¸€m\"` NÜ¿ˆñ± ò‘:ȹã×8õêüýµÜw1r'ð²S…Ûë²o“ß—²e?î]úTã_ú9¬] ` ŽÞ;/ ©˜U™OÀéúG€ÕPÚZ¹ÿ /¿£ûS§ßæÄVuå= Âøн+gÁS\òTNQ@TW]‹ƒwÎ`܉eQP4îÄRdW¨-ç!|ú¹õ/ö XŒ$ðûÞ“û ²¯Å¸~åEãú`×’©¶@`oæIP?± ¹ÕE‘vÀ#M.Øf@N°“Ð~Bì«RÀ¹7/`ü‹?#Pà'ض0]ÀìºuB€*œ™WS¬Àù“7½ÎY”Ö¹}ÙøþIáh¨¶ ØïâïQwû2&¼ü Õþ6Ê J`ËܱðH 9;n  GʯõD@»fù®yüýÎìkïã Ã][þDä8pêüï4÷j¸ê]ú«ѰqæW(.(x*§( ª®ªÆÖëG¢éŽXXWvmòÇoýK€}€d†êlüãÿ0Šƒ}Þf0DáýÊá S?Ãú©£Ãή0ïëDU•UØxí Æ DÀ L:¹ Å å&¿Ö9s¾ü{NìSñöÝ•(àWrJ–öBO ±Qœ}“^ÿ(¬Nÿ¢ð‘âü+QQQ‰µWöAŽR˜|r5< l8mîw x[ÎåJ$À!ÿøM>4ÅÛç¿3À“{“ßø h! eß¾‹üœñyú/«(ÇÊK»!@`Ê©5(m¬ €áà‰Üƽqsòöí ¸1“ˆûÌÁDôÛ¶Ï_€²¼˜òÖïü‹¿x yYž JË˰äÂvÐRÀ’8õÔZ”7UG ÉwüÏsüvîs]€ÿônã©ÜÏýÌOTþ}­}>b ^Vå9˜úöïUÿû('(í€ç¥¼€ÜûwQô(ŸuŠ%e¥Xpn ¤ €i§×¢òq-†Á¬gÌ6¨sçoÓ¾}° ×¸9ÀO"šµç`òqö–…Ê¢7ɸWÝgDºIÊßc™êÊŠ0óƒ¿ =NŽÌ¼|ÅùR €©P…œâ¹*ÊŠÔ–Ȭ³PßÖ¤àéžÂí%ôÙûNn}䲈˜µ×%€ˆìž¯w>‘YûœÛ·LUe˜=êï˜ @¤ŽG^=yÅ…ßëD5UÕÈÌ€I1 ÀœsÑÔÞ‰Øuîô•wúüv¬ƒmvývï)Ùð3”ç(D¤¹ÏŽ}¿Cû™†‰æºjÌýä_ýð‹((R'#Î܉) ®<¼-ûÿ˜w~3Z:[aúýìšÆ¯ö3ïøyû†‚Kê´µ=D0È„‹8Ú£2ì‡äùÿ1¸í›öMÃ@[Sæ¥üW€*ü79±c-JŠŠY§( }Ng]Õ”F@ .lAGwf­c½žØ°a;Jëš$@‰8qþ<9ÚøŒ™Øv@…·á7ÐÙÖ‚i/„ϽS'(Õ÷¯ž'Ÿ ¤àá»g¥€]ÜoO7ÿm#×ÊF2•‡ïؾ@2ÌE(doRòâï·Kµöíñvü~ôtubÑç/‡+ßQ'( ¶Î'€§éx\@‹/í@__¯gÌDSù,¯8ÚG0(¬@¶xGê`ËÀNx‹·o8$W_o–|ùÆ ÄŒ¬™”¢œžôЀ¨êÉëú«¤ 0Uøo²ìò.6LoØ_ÿèõÎ3ýéuÎíK i·p 0u J]1æ~g'³ðWoàQ¶”f@ú”—aéÅ 9¢ñ'–cÕµ½6²ðŠù®DØG0(I€IšH¶‡ñûú`8É pnŸ\£ßbðcMú¨ðÓ.u‚r `æCö[ß[ P@Š••`Ö™ r €*Üy݃Î-w]ÿ%j?É·‚AÍ„³?É §{óÎíG>§×hò6NKÐÖø5®Ÿ9.µ¾¤Ѓ¢\LÊ"@:Ø|ëˆzØàÖ?þ×­sOÞ7âNí»& [9é?´Rµå€É„o2bÄNäÕôØ:çkŒÐ<¾} <’=HàÙìkš&@"•¹ãÎ ˜>?Y‡ }Ô”Ïr¾þi䨾‹"ÈA(Ä;lçŽ×08ˆ$0öMÃÀÎ…ãÃïÔŠÆ÷ÿ]¶Ì'Å€¾vÊ €ï€=÷N‘ÌúÇDGÙ§uÇë¹á,7KÙOö€@»€`Йc§?Ó‰M?'ïÛ·ÉÛ§ŸÓj€ûWL…€¾-ðü”ÿâáý;Ò(&Hø¿¤Üƒùç7!ýÿØ;ëö¨²%Ü‹+ŸaÜ‘ñAãÁÝ]‚ÇÝÝu®I HB€à ÷NBp·NGæ½·ëé3ër×45›ÝIïîÔï³#;]G«~«ªVÕ)tûnœ 6ÀÌ~fÞi3Ÿíñhoß@€'N~Á›°cÚ> :´~&‡«9?Éÿ·DT—–Ðþ[¬\0Ó•ÛJw+ʱ.!ýÖîDdÂ*Ä%KàÿפÄ%Hȃ_ð¼}ýg µµuHÞ¾“ ?=12´:·ú?}_¿ÅÖ5+P˜›×Ô¯ På*+lØqü6‚ãÓÐ.& ç$cĶmäôb ü Kp<û<êªæW¼梣ްoé€À“'<˜Ï/éÃC…û:,èö©’µ®®§ömƤ°oÄñ»œÿ¸-УÍwþåýüëù=WÌŸI0¿É¦ÊmXwø&ãN¡}ì)zĦѳۢŒÝ½‘ €@@gò¯¢®ºÆhê”Çó§y̼¯Ê|, ·n0Î’q®îá…ÃÛç-OÁêûúºzœ?²›8Äuk‰É[ À÷ýõ[º¿’¾F@Ë/0#jn߸‚Ü\€&WðgCaq)æî¸†v1© çÿ¾bÒ4%}Wîäƒk—¼ 1G›.LLXŒKE7Qï¨ùðþÇïƒÜAŠƒþ{%#á~Ö¾u@"7Ô-6¼¯Ä,PÔ;ñI¦åúú¿píT&†}ÝdÃýÑ]ZbhP3tø]wüº¾Fû–Ÿctÿn8“rEMæz ÀÝJ®db̪‹hŠrøîÕ>& a3N`ðƈJ\…ØäåM6Q–EÀîê{ÎIóР?½fß àŠLß¹_<ù?éóö9ʦÀ­ó)t  ‰9~RDXstmõ…úƒ”£çDÐ=è7ìÚº‘"þ›(·ÑɇNg¡Çœ³äüÉɳ¢´©Óœ£õçVŠPZ ‰Í¸]‘¯€Kƒêâ9 àTŒ˜ÃóŽu@ CKðÿ‡óΙŸ£G‹•!]÷ÔK5ÙWÓmoIM ÏOáþñ›£WÛï¢òü†øã—ôœ?7¯^Ea^ž€‰ ýò K°h÷uÅ¢°¿æèY©ú€‹cÜÞõt[  €Àß­‘sÀpDÕ¸sW¿çÿŽÈâ?WÀªðDã|M"_ Â¼Ë‚ñh„€Ú:d^E\Ÿøyž@Àý ÷³RuíZ|Ž!=Âpäà>º*Hs|¾Êÿn… iWò0léy´‰Vù~3jsÁSSÐoõL>´†êü’–#>y5Šî—úð)Sõdö_É?Øñõ>‘xüXë0™ß;…£O>ìô1´K}Js31µ÷ïˆîÒܯ¯õ i†Ž|ûQ'~>%ðÂÿh†åó¦#ëæMææ øð©¿¸„ªü65 m™¿aÅR}ÂgÃÐÍÿAtÒJªðW˜v|-l*ßõ4vç ²[ì&³ö¬ OŸ<¹>@Ỏ·ïÁ‚õóZG îçaFÿ¶ÔõίõE„7G·ÖßQŽŸÉó›Pm@Ä€î8‘”€"ŸŠЩ¿Ò†s×ó1fÕ´IE{Ó§~¾> Ë¼$DìØì×i>ÂÌPõô>µþˆýÐHÊ”ßÿxGÎÍïÛ>”€qêcŽ ™²Ï *Ñ,€‡•e˜=8ßøSžb§æèÓî{ÿªòü ‚€Ž­[`ù¼¸‘A̓,ê§ë}kßD‡é§=}êçëâSÑkéALطΟêæœÜ„GÏÓ0 v_3îd Þ¾2o_‰·o}¸¦]ôÌ1Rò¼}ž~iðóÇ1ox¢:5óö½[bPàSyþF5"Þ§’ï§AëÝ(wåúO_ÍÃÈÈñ7Ü©Ÿ¯™vÖîFd‚\ŒJZ†ù©[ðâÕ 8˜T¨éýÏøžëÉè­ÏFô"@´hŽÍÛ7^{÷úEtAd§|>Ï?"´:QžŸ ÷7b4 C«æX4#W.»Š³¼,[9~ä–`ùþ›Æäú/@„:Ì:†á[·#&‰® úôÀŧ·Ãþî­±ðzÃï ÿ7Ö€Ç Ì/0ó‹Ò6I+#û#²ã>›çÛ¡9º·~¯}¯eDµ->Ç€.ØÿŸm (ÈÉð’*Êm¤#éÙ´øœVáo©¶Â 1f×&Ÿ­ˆL\ŠÕçöÀñ-°ß2¿ó;¸ÊÔ0òyp 4À晘Üñ{Ÿ»Ö7©S ôkÿ=BTžß² øñ ÿü â'ŒÄÅ3i(ʧh€@#ùÝÌ*ÂŒíWŠ9õ[‚âSÑ{ùLÜOm… |iðÖË ¨Q{T”€•SWðw0 BÀK㨠/éô½ç7ß7à3ôþ›W/CÖ­[T$(ÐðE~%¥¥øóØmôœ{Vµòµ¼T}@èô´~¢|§>€`OÆqUh) H@€Ê<ÔÖÔ aãB5ØâyþQ¡ÍѹշêZŸ*ð§¯h°ÐèÝœpÐÕN8GÀÃ*/·¡ÒUä7vÕE´‹q:S+Ÿúùú€Ž³}fì0Í8rûŒê`! üïG]VÐ$EÝS÷n´ìHàXµïE϶VËó›/ ÿã̉›„«ΡH ˜ ÷WUÚp'·ØÙÆ—ú¨þ¾,UÐÝ9vx¥Ç¤å]A€€%àáæ4èÒñ4вczÛÿ éõ©´@ï°Öؾ~²33™N‚\¸¿´´ »NÞAŸùg™"?Ÿu ž’‚¾«œc‡©­°%G_-¹­YL—š|à¿©°…µÆô7CG5¦×¯øÓ—øñKŒÔɇÐMüœÕý•åîÇ„5îWÃ{üTjìðq Ù¸Q‰ÔVØR}î”çKÀ² p’@þˈíþ ²JûÞ®&Ú÷úzZ ì÷ï13j.¥ŸQi€†ûoeaÞÎkUýû›ˆT[áÎóŽ`Ô¶X!-@sâ’W¡ð^©ÔX>xÐ(`÷ò´3E€¶‚,LëÛŠæx³}ïט^ äù-s[`ݲ…¸}ãŠòòþ_pÎé¯,GAI¶ͤê~? ÷;¼äÆï]Xïµ&˜q|*Wq·ü~ÿý˲ p¡©GÕ<®ªÀœ!A4ÀáþÈÎ-10à„©H À9&Ð4Òv;ÖÄFdÇïÃñ“ƨ1½*Ü/bÒ4ió¦Dâò¹tW7Ál¿›Í†Êª ädæaû¢DL [†‘ÌGDÛEÛfôÜ"ÀžŸ„ˆ6v˜Ú¯?¿_þ-âðí^°/`ÕÀýûþšâ?ßáì¸8–º6ü˜ÞèÓNòüfëz†üõË"óúuU(èW@~äø ó‹qpS âz¬9þ6 1¦Ý¢÷Ô9â0¦œþG'(¢hµ?îµì&ì_çj+ܰ]wg£6ÀVÝ_% ®Àú ÏÞ`…€µHþsÓ Ðü˜^jß+y~Ö î‚›×9ûPj —@À÷ ¢²œìœØw³m¨V 0ºµrüJÀÐNk+uÿvìðÀuαë—¼¼ÁšÈ:ºêjµÿøzÔÔ¿@àÁýûg-Qèåp˜jôMƒäùG6Ô˜^õü¿ŠÐ {wQJ€ }ÈñS®ÿ܉kX4öOçiŸœ?9ûhlÛ…è>t¯DŒŒžŒáÛ¶7D[a€+Å™¨¯®ñdÝÿûÞ?íLÁîS pÆx @ÇnnzÜ>É`/€¬+gé 'zĺ4Î9¦· µïÇßýB~ýÑc†"õØääR3!_€òŠrrþWÏÝÄêØ=°£þ˜åäyº!‘Ç¥ÀȵÁXçØáŒÝí¹¶ÂÑGè3SYÄ7bö2f¿ÓÄî}ž¯à ®NÀ’pï€ÝÃÎÛNj¬…Æ©y¯ÆQƒò‚lLëÛš À·ÇôJ¡`‡VÍ1#j,ÒSOª€ò ge9n^ÍÂæ9‡0!d©«ÀqúnR}úý)Qãõ4v¸ÏŠý˜x€Æ#樹Ó=QØM8tF&÷¿†—ÝÒ pÚ$}aò ŽÄ,>öƒ ÕÎEÞ¾%ñdL?£JÝOaþˆpDujfjLï fè`‰1½R(Ø¥ÝOtcà™4j$”Ÿm I}w+pçF¶-LÄäðå.ǯòü†Õv!F,Aø„#øÈú€0çØá ;•ÈŒfz,HÝ‚—¯_ÀÁZt™pøÚþg RËú ᲯROŸ X îWU¥9Àt¸‰Y¸vŽJyûêi ôo7’²°Û±6nˆá«€±®ç¨°fèb©1½¢ Ÿ¾"èð+–Íþ÷ÄA¯€rüÙ·ó°{E2¢»¬Ô*û±Q€Ý7™t†RÐiÎQŒÜ¾UÕ½xa?í)LîœÙ™½‹ßû \˜ö*ðL"ŒTU"0ÎçóyŒ}–FuûŠBíÜ{®4ÀÁµs´›\ß~ÓÛÆòczåÆ@‹ÏÑ#øœ?¯@ Q@9þÜ;ùØ»ú8⺯&Ç?Z9~ªË¨ƒ’ ð@}@÷Ň1nÏCõ“—àð­S¨Q7LÔÑÓÈ{jÿ㢯¼}@ûVNÜ«ªJ%àBîFBOú{|xËp€ÄÛWa(Vu®›<ã§1½œcz¿‘p¿ï€Õô i…ógâÚÅóÔL(?;Ç$ðŽ?çN>ö­;øžk0Ê´ãç ‡uXƒà˜O8Ci+<%ýVïäCk\×Y uŽmÿ3eœµr¸Õîö?ξ脱oWö $`MHQE€HP˯,VÑdÈ>-,»þµ»¼¿º¨@ñ Ä÷üUÝp3¦w˜_ŒéèÖËæNÃų§©Xn ˜UÕO9þ›¹Ø½êøÉñ·f¿ÇR=‡ìFÀ©ðèØáM;í~ì0NI^’ûå®@µGi{¦×œ³¶Ï©ÏÒíûL=¼}K€ÀIPÄæ–õ÷”túäŽz°¯;zeŸô±ö]…€ Fv B@wcz»ùј^/©F kÀ/T,˜~*…¹¹N‘£7 6Ýã'ݼr‡ÚöFw^Å„ú. 02x9B'“‚ÀÆ;L€ OmÅËW/´0¼›½”‹ j?{ï=µ1¡þŒàòÑƾ€•àêë•ã¤'[d÷þ»: ¨…­ÞQ¿×瞈u'Ïäý?Ú¾«À]Kã©%ðûcz[ ·ŒéõóˆÀgèÜîGLŸ<ɇ çömU'À€Íæw~}éô ¬ŸáL%-gŠû' ЯÏ6€;Üs)F\²ɉK°'ã8þµ\¼zªŸ¹¾góòêïÕóC§yµ¿ý“}’fŸäqû®8NÀŸ°Ý“ Wï1Q—]µhõP¾ò× ü>d_‹6¼ÔèÚ©$LîøÓ(cz›ÎõA„üúÆ ìA-†¯_¾D ]×*++%§ï:í#/»ÇvŸÅ¢±Û1.p±vÏ›Šh¿Ç%I* ¡Ú ÿwìðaçØáet £4‹¹;h¸¾Ö#žúþ§º”øÃ{ûnÿó耢ï©.©¿Óí¿÷9Vܽ{Œ@-¾$<—ô•¾8•ÜÜIÕHØs&éÿXô…çžnÕûJê3õ pØ«ñôAæÃÐÀo¥}oSÕÏ_;ç P›á^¡­0=r ìúÓ (-)%§öØlœyñ=Ö`të Î}LŸÆ,Äà®È Š\c‡;Ì:†!›¶aÞÉíxöòïìLŽ_ÛÿÜdt§®¥þaÿãKú»JÌAÊ®ÄÚ§k€–·a!z29×ÓÜç‡Ôgj ¡ý½þEýŒIUüè¯TW[‹ÄÝÛжù§Î†2N‡ ’ô­…ž¡`âðX4v+b»­&k"¿ß¨õ]Ghàk’h•Š#— Pï¨Ö­ëÉD@?ìˆõŸk0`wnöwξû=UÙdì XîVV¸qâî íôð‘rÐ$¨wxúÔ‰Òm„ Z'V÷O÷ÿ0©+à›W¯1;n"Ú4ûä=g ’¢ÁÀ–ßbijцq¼‹ _…àè“”z€Q«ÈÌÙ•‰·¯ßÁ¡öýТžÜ!J?$éûŸzº‰²²û-WÕÿ¾t_ÀÛ·p €@ò{·”4ºÓNÒJš³Ö¤ÿN¿ŽÂÐ¥öH?éT­‡Ë>`_ý®¦ÚgO!nÜ0ˆ”¾Fè¯Í1ºÝ\Œi·ØW@] ¸K¢ äüã·ÞÀ³ç¯PSmל?[£¤ï•\´ö´3QzºK-0Žž~¯‰ÙÕß[< à6DDÒ€û»¥LÆíëTLÔ¬çùÕ;¼}zÖ:xüྠ>õeÇ% 9£‚–!l’\ ô´óÙœ'O_¢–œ¿î4«Ý9Y®2_ßÿô¿Ó÷\ÍžÚÿØÜ½þ½›¿5`ßÂE€G¡@säÕŒcvª×©W—²cξ¾õ"@=zÁÙÿ^<}‚¹ñ“)à›5"èßk‹€‡ŠÿZG¥`î®L<þšNþZõ¼›}¥Zß·ôš&÷'oþ´î~¯&iöݤGÝH½ËÛ÷™€€î0Ý/bþ4­ýG—ÆíÓ“³ï xû¤Úêj¼~ùk–ÌE@Ë/¨(Ì÷—H@] ì4&AR&Ô>6íbR±:!o^¿EÝ@”Ó]øÿŸBòLšÀøª}%ƾQY? à.Œ¯é_;^~‘)5†}%£öéz £º‡vmGx«ftE̘H€n é¼±©áüDm£S:5 ÒKÈñ;˜}Šä¦ºžÝ˜=#>jê ݆í“(€1Qȿ׼s¸x§ õû‡£’ÀÀÄþëÆ¾0`Ä~o_ÀW€Ä/†0Yqö‹)úcå¾A@iQ!"G ôÙ^"€¡+uB®þÛ|d &®¿†ÒÊ'¨¯±ÃþŽ9ÄÝÔ; ˜Ž†š{‡·ï+ PY^®€/2á!¿¨=‘³2Ò7BåÿÚ~mM ^>†5‹çRM@À’ð½‚ÀÞýwH€‘3×›Š5‰9xñâ5j«Ý8fökz=¸0…}žjO€õ‹*ÊË“ÐòU&eÄ›+@1^`Ä>5 ª©®FJrµ‹mÛ\® ø–F.Eø„£r+ÀÚD¥ Ûœ³8y­œ¿Ê÷›ŠlšÙEÜð¡(À€›´*åû'®¿Š¢ò'”ï—ýϸ¬Hd"¼(%ðêù lZµ¡¿}‡ö->g)à3êq’ PUþÁñiØ”œ‡—/_£Îa—ýÏðø±€?€H¥êjjpåÜ é&‚>S8´ÓŤ"Pû`ࢠÎ*rüu¿_ä‡}l¶ÃM|x¯ §Ç àG)+ŽSÀòÐcÈŠ4ÕB¿ö1§0ÏmÜøuŽjÿß«$ Pn³ð¸œÅÔ7àxâôk-ÑËK`dÈ „DoRõ®»ý=æ¤ãØeøkäÔ/ðTÀrð¿ÊJK÷4„TÏ[qfDAû–ŸK4@ÀÒ}ûmo2Q€ö1©”ýJ+Ÿ¢ŽûȾՀE€™€’¢¢ *(ôîÍ$íß…îA¿® J4@À‚°Æ¡(€¿Ÿú»Ì:‹ƒéÅx÷æ­ºÛ/jÈ€€Õ ¨ `»@ãE s³1eâH´o!Ñk¦uÛè×¹þ6Ñ©ˆÝœ"ÛcÔË©¿1à–Å@ ?7w“@cFjðöõk$îß…Á¿£m3© °˜Ú.B—Q)ào§þ®³éÔ·oÞÐì~Ù“=°äæä¬Å_yaQH4 ´0³bÆÓdA‰X§`!†uXà˜¿9õ·qæúo¢Drý^€ÇÝ´dee­ðZ4€žÇö£wX‰Xª °çàݘ’æë÷ú©Â?éB)Þ½õf®_àÉ£G€ÌÌÌÅÞ”—cþÔHþø%ZÊ`!/«íBŒ ^†°ÉǨ Ð7ïõ§bÖŽ[°U=“\¿5àšµ@àÞÌȘ'à}Õ8p¼³#õh"úw h€€%¢ýûl#ðµ±½½çŸÃñ«6rú5rê·F àÁƒ+€Œk×fX+PUnâ™±ùå™) àUE´_ŒŽcU*ÀÚ=üé¹hïÜ}ðtê—}Å2ððÁƒKÎæs€K—.Å X®‹ éâÙSÙ·3Ee €—®î²ÞÚ }\“û†-¿ˆs·*QcÍn~÷¤§§G Xw¦ÀÓG±uírtjÛíš&XÀ+õÝFì§kVœ×>- ›“óðäéK5¶Wd9¸ïÞk€Àÿ8‘œ<€…L¬¯­Evæ ÄŒ‚vÒ@HÀ Q€áá«}ñijè“‚Ië¯áváÔUÛá°t‘ŸÀ½ªª€#‰‰Ã Þ¼1ó{%±Oc†_¿z‰Ä½;h¸ 4vA`ï;) `…«}Ýç¤ãPz1^¿zƒ:)ò³¼P_ÊŠŠ‹€ÀѤ¤!L'@£NË ŸåÿöU‘`-l%ÅX0-Š ½\h¤î€£ƒ–"lb2Ý ðR‘ÀÜ]™(»ûÔüÕ¾7o<ö7þoß<”ïq^=·üö®º#ˆ–™™™™™ëB’’ËÌ\ÛefHÃÌÌœ:åÆv˜™™cfŒu–œvº#OAo»o²½³tr&ïýwâŸäænþÀΤpè÷ßÿüµµ¾t€ô>÷[Næ±Ïù©IK“3ÆÂË'J“ €¨ež}¬/•¢ßä÷B»i0aA&4ÐÇþÚCxuÿiHüôØžŸ°M`ͪU}|&D¤¦¦>‹éåXD¶Æxì:þùé9@ Ü$X”Ÿ ½;·…ûo½Zí2x®”DÔ+RÔ¿ùþ׿§R@tæ÷'~1zÿ² ‹ËÝ7ù¹¿þ╟wàöüV@Mí{ `…=D0jÔ¨'1šÄô2LF 2í*šçùé±ðž»­¡ZX¾x¡Úe0 ¥,  ^_¼¿;ÜñqF½§û±ÑïÃ>saÉZlò Ô5ùm1\7^]æ#Aø­9HÌŸ?ÿ\y& vv$°‡Â ‡ 2äá@UÕ(\(¾úR² ÏöÂÏ«mÜW PµÒ/á㋬…ÙðkÆK¿¥ñkßÑŒ_ã§cüò£˜ÃŒÀâùspv–dƒ!ž7>ùÌ O²˜êÇ”ÿ{´¦#þ €€ÁšjèÝQi¿Aß1,‘¿üVB~ßôýmâGð‘¿>`̘1/Æ^ˆØù_à°gžyæòʲ²ìÚ`]Z§— ¡ÁiÁoº(t~³Ê¦£/ùnøéym([*+á—1#áÙšÔͲ€ܸ-$¾ý³¶,ÐfM‚ŠúŸl1~˜¶^ÙjÐv½šýŽ“ýóןÉëü„¸à×… ÕkßÝV~„™_[¹4jøðgü!Dì®°ŸÂáGuÔé%EE«ÕJídÓ‰ä † )D³‘Gr˜Œm[øé1ïEËZB"1åxÁ¯Ž5TÈËʄέ¾Ä„+àvY- À£,Àsô¡2€ýß»ÔCR—…7îÙJé~Íiñ÷s„ªß—LQ°Y´ÿsŒO~zn¼/iå‹Èçöüz6û˪UŸÙÀ%p?€€#NPû4/Qf´è3 ñÎ>ò=ƒ¡œ=½ÏòóÎVw’o€q¤„¸å×3ÿ”ÍSe7^’Õ"<ÃÉ©Z)€ëî¯ç˜¿*jìîG[Ö gÿzÚ\ûsÍàk#?}Þ–Ÿ^‹1¿.8è}ŸÂ4þ>0ÀL¤Ó¹sçDÒJ´]| D,$ƒc!hNÝp‘rÍ%ºa+hüF„ÒofgI0òÒX¾à'þ€™EªqY- À³/Ýו–2Ã|°»¿õ4øuæFØRá« Þ€©‡H¿Çè™5‚©гp ‹?òÈ ŸÀ9½‹`à9T¨>³/¾øâVÌ:ûHH @Ḽœœ™8 PS‚zºavÎCŠsîFþ€öŒ Ѳ šâåùéè7~Çž_ÿÿ¦²@An.ôïÖ¼ýZµZàÿï- @DÀc/Ç,€qvÿ}_N„^?¯€üÂòöéߢÝ´Æ`ͱDÚ¿öXK©ë¯E\3 ?"~øõÏF~†ãàÐþMÚQû7ø±¿¬¼´4óÞ{ï½”‚Îý©mòIò'`_JÇ—¹iӯإ©tÝp˜ÔœnLôXûnD¤k0:.NŸ1½áøŠŸ^wÏ1Dhõ²%ðÍGo¢ƒÃÝÅÉ‹°_xW¸ûýß°!P›Ýÿyÿù°b}!íØglnÕíÖ”ö'ôkQû§f(O„:¿ÆÉô;hü˜].ÎÏ_®|ÍÉ GÆ^ˆØí_à˜•Ë—÷S“š4cД«)=d0Ã’®^¦EÚÚgMê˜3Vž_/ôOù1à§Ïèü¤ÐkLÉÏ<„ÙÙrX€uCàSO g}T7»ÿ•3`Ò‚L⃵~½kÜ`«FÑK zúmFÖùLZ=žøé¨Li@û]ß ý3ú¿@A~þB,7S`? BwŽ°Â¡¨È¦Nú9jˆpЦT6g |íÞ¬d¹îyžß "޶üˆ¨ñ¢Æ¯ŸcÌ”ÁÈ}à±»hËaé°mHnÔîL};†d¬’’rìîÿoGgÎöD­õýGçmøüºÒùÍ‚Bãg²¾Û˜!ÀòrNvö4åkŽ÷°·Â!xB~HM}3†ÈÝä|ôÈÕÖX Žå×?««O†,ÀöËOÇÚdhÓº5Ðæ›O!ñæËeÙ v«à×nno5j ½:O€õÙ%ØàW·U/oüõæþ£EÎöü„øãüM¦Ï2˲™{Šö:¨òò†µkG+_s¬Âá±"hG@£®4Ð(`ÆÓkLŠñ;&õ«wÿ럱ágê↛Mñëœîù#_ÇÁˆ…sfÁÇjÙ`“ð²A鉤„–|KKèðöPX>{M¸Ær´€@?î ŒýëG“ý ÿ¶9o~>ÛˆÐø1¸œ;{v ,7SÙyߨ {‘8²S§N÷©uš5ýñ5¤9ãÖ ŠinaÒÜ®ø „?P?üÚ²Áq¿ü¯>y?î-°=÷ˆ $ßÚ ^½©9|ólo˜ö˨Æ)~AÇT7EšÌg´×¸²–güÎvÀïXò;&~„Yø–—øá‡$j<”ÊÏ»Å^ˆ8Há¨|ðê²’’l3^|ÇÎQéåî1c„LSœk~á×/ölä ÕBqa ëß¿;a;  å6åøol>Ð~î?J J 6TcÈQZ\œÙ¸qãË1ØÔ6#@;RZæ„Õ+WÃnM65ìE$jáˆY¿üÂ_·ƒdLJÿ5zРAχj"šyX(\ë÷üÏ/üz Üv¸Oç¶ðpãëQ`y@@œ5ø½—ؾë6 2 ¨ÎïêžÀ¿ç=„ßýwÜóÓÀysætÀ!@Ú`¾™x¬Ú©é®ÊòòÊ ãÔ§éݧ±4bæï"ü¶|´¿@0ëW­„öß~Žƒ„üÛ((€ëpªÿMÕà7°ùO°iU6„B5Xç÷>cd‘6wר&üÞ_ÿö½I(0zäÈ$Jÿût  L<îðÃ?§¨°pÕ•²SüŽK~á× -ž7¾|?€Úhè\>vö§¨”×GйkÃN?XCŽß§öGGá¯_ðü|Vgˆ;vìøˆ¿‡É0 cNÍܼy ­ðx%í;~á§FÁðqÚ„ xûå§Â» 6ºú|>˜à‡Î¿UÒ@˜;~)¨ÁOsH1„[û~„ ~¼öKŠ‹7^rÉ%—Q ÀÇC€dÀ‰sfÍj5:‚8„ã?~j AEY)ü2f$¼ôè=±˜((€:û±Áï«§zÂÄÔ9PQZN ~ž±á§¹ÙÙs”o9Má8Ï¥€Ç 4è¼Q[4 6+hoð<Ú·<™x+­¸@@”:û?y¸+üÔoçkü/Áä¢ z+ßr²¿—ÊRÀÃ1E“’’Ò¸¢¬¬k·bÀ.W doÚýºµ‡Gïº n­—¥ƒ"°¾ýÜß Ætù› pf?ÓÙ/¸ƒC€¡ƒ'㜟/”•t’ÎÊÏÍ ,Ô hÅ@m(Þz¸Kë¦ðÀí×bF€V ˆp»¤#þwïnCÛýYks „ŽŸ:û‚úDÈq@“•ï¿ÿþÝä[ŽˆXè«è_VLM§._ºt8M¢'‚AX½| ´þú#HL¸B[:(ÀÆñ×Íìï÷Í÷°~é&Ç/ˆ"p5™ &—ï¶Ûnçb™ÿWÈ®€Çà‰R}¯K@ „j‚/üêƒ7àî/…Û·IˆH¡ÍzÞhÔº:VÌ[ 5Nµ¶¤O ˆ0ˆ\²xñPj<Á×»J#à?›½öòËw„ûh @m`6›ÓæÎ˜ Ÿ¾õ öà " Ž#ÿNï‡ÅÓWÕmÁ”ëWÛú?“4øØøØHö8Yáìœììù±ÍÕáþ€êªJ˜2. ÞOzš(@B@9~ìîoÿæ˜?q]Ç/ˆ90‹W^ZZòüóÏ7&Ÿr™Ô(À¯€'*œ6eòävÿk@UUlOø^4A3¶TV(!0…€\F þïø[BÛש!>K º²Jâ#ö/ü±ÂïuÍÄ`Rá¿–>j¤a §4oÞüIu"ƒ5²FXà;!Pï¾úŒVhЀ’äÓF9þ9‹aKE%Ô†â#ø¸«lÚo¿5£úÿ‰ñ5P&žvä‘G^’—›»zk0è*(…„@- Š ˜”þ›O“8„@¼ Þñ·NÞ—¿ª¼‚µö;{ð??}¦AòbÎ P¤>ý裇qUYÄÀ¸©ÿKÀ™sgÎìMeÏ ñš+#Ô‹Ãöů¿ÎóóœÌ÷\ñÓcžŸ2PUQÓ~†·^z_}¾–ð¹àSý9þ4rüLÄoi3Ñv8 ‹§­ü{9Ÿã6Ì—áܦ³™ÇÌ9ã_sË/üôÜ^ Øg p+ù ëÖMU¾ã|q¶ôì«p)·3_xá…D5¨"è8¬A󑄇Q ‚7JÛ”V|Äoý‹0bÆÏsºçwh –°Ï›­¿új|= Ú†8¶ ™6éy/± hþ#NîÃHŸ¿é:âÅ=öÞy»çgxØïs…Ÿn>Ãrª¦qÜüçC iùß‘Tî?Ià`šÜt¦Ây«W­‡c™H»ñÛ9"óÍOï€Îøá§cÔøéqøéXü4˜±~ÕJèѾ<™x›* œ®>ŸÊQÔØ—”Ð2œêÿä‘®0ºKl\™žÓªq¸,ŠÕõEÏù£}–ÎŽß}6³ Û+?}ÎK~^Ìjkj ¤¨(ë²Ë.»‰ÖÿŸßóÿ¥ p *¹nݺ¥çwL­ZD´:xcâ¿Ãß :?=Ž.¿}ý’Jüw½çÖԄ˹Y™:l $=ý f°O€)°€o죎þo_ì iæ㶼íGÌê78^ŒÚ;g³Þøõ×ýÇ/üœ ²ßÅRñ¬éÓû)Ÿq.ÇÄÿø_)œ}ôÑG_™•™¹|›š¼¡ÙE¼|ºË>’´¿ ½ä§côø .ø­¢3?‹ M n,+.†Ii¿Àï¥À·_·†Ë”ð@ ÃÇ4ÿ;wµƒ.Œï)/)ÃÆ>ÜŸþ®ŒÈdþ¬}óïGŸŸ/Ùòó 3o¼ó»ï!Òaubó_UEE¥ÚGæAå3ÎQ8EÛþW@|®ÀšššÚ<˜èÒÂ]ßÜÍüüßɆŸ‰Œ…Ÿ`Ïϧ)뙟VC(Â[÷êØ^xø.Ì`‰@/°TÛ¿©npÏ—Oõ„Ônã`ÝÒMÿÔ÷«™HϪLfï èèc~ÏO𞟟ü¼èâ`}ýaô¿rÙ²±ÊW\€þâ_ÝÿûÆg÷¿”öÿ×P s/½ôÒ„âÂÂLlªªfU¢Ås>RaxlSÉÂO°åç½`àÄ@Tø«é;¸û%ÚwIaLÎ ß~òÊéÏøuänÌÃ}øõhŸ?·¼ƒ´?ÿ~倧ç_ø9>7üÐ'¬[³—þ]Jà$…Ãö‹ïô¿”T8ŠfœÍ5×Ü^\T”‹ÝžVj˜žW›ëW¶ªÕ5?½ç~ÃÏ: Qá§^Àe°Eù°hîløqÔPèÛ¥#Œé‘“RçÀò9k¡(·‚a§Oµý-~×MÍÿ–÷üÂOˆk~‚=¿>| ¯‘­íZ·NÆ Qá̈ÙÿqŸþ—½S8‘ê:çg¤§w¢,€K'Á£ZÁÂ(ã’¿::ü¶Ì cûäw¶(_Äž<* –ºøjsÔUíÆþÜ;…zæçGµöÏŸÿèð ¿ý¯];}Ï=÷¼‚†ÿ˜ÖþÿÉÞ9Y’aøl ³§µmmÛ¶mÛ¶mÛ¶ç¼¶gg§zg…ʽœˆŒé»ù£&·ç5²#2Þ _U•¬l32eÄ‹[zéÔµk×ÍÇ=„>ÔËcà ²< Rßå€Ïç%ÀgÑò±dŸ•¥îõ¦˜Ï×C|Åg ßøzá{ÄL¿ä¢‹Žââ¿6œ2^5_{ÿ­puÎë´÷Òùùçž»’·j`ÊÕø˜]r¾äº†ãÎW¾Ó¦* ÊçÇóï´|®ü/ÿå—·½NèÁ@sy翜ÿY1 è ØÊK—¦M›n4‚ú̘QO++'òZ+Ù58ßøQaùÌ ¯“O¢àˆžÏÂûþ§üñû‘NÿäÿòÕú×:6åB.7Þxã)ÕÎÍœ8]²aÚäÅøÆ×,üÆOüû7~òB­á?ýðÃG¼.èæ¥³—ù¿ñm \‡«<»zéåÃ?ïP(ÒäM“›x2ª&”ñ]Qù걭绌òñSþýc¡ú¯±£GïÓ§Ï6l´óÒXzÿù/þ³(@·ýöÛoO§À‰Ô@Ecéºà¿a†ð|Ä7¾|D‹[‘øzBÿý§kþáϰÀçøÆñ ¿—©ÿ6ÌšýÐC]LŽ`ÞŽ‹ÿ, Њó>Ý_zá…iŸ4 ŒØ€!IÀ{r ‹O„Òó¥¸óñßõü(Ý|lt ¾öw¿¿ð«AùQÆù‘žoù[^þá"‹,Ò—œ@ûÏ©`;óŽ€î«¬²Ê†ƒ ø–öƒº: >)dÅär,àZ,ùäG ¾Ke™/EñÙ¢ëD%à;œøŠë•€¥„i?¯:øéõß ûî»ï^´ö‹Êÿ2/+æ?÷oQ€¥e_ÿt?öØcªœ<¹‚Rðñ¿a/Ü¡ÿãÐ7ö¤ ÂW–˜%Äw ðYôüŒqðð¾Bù,Á|0>áÏç—„¥”¯Å‚ÆSUåþg?óÔS×ð¶¿n^ZË}ÿÅðþ- °œˆ´eK°ÇsÏR΀J¯tü(¾Sðcç_œ£ãóÝþ>Xb‰%úûõ¾'×5áÆ­P¬}ÿÖpE/jî@b™%–Xÿòòi Xë`Âx)A!2Ìg1¾6Mø.»||]ðºñóXø.|ÀN€`¬€¯˜ûŽ«þ'Œ7b›m¶Ù•ޝÍ=b–‘=ÿYìÈ[@Ü#`YYX³-p‹-¶ØÅ·  ÚCOÊáɂÔèÜò£à; +¬4Ñ瑾¢X¾'ÄÈ>)Ìਠ~®ñ¢ÕQDáÿ7ÝtÓY¬ü»sä·1G‚—/ZÏ»S`¼Ep;Î õºòÊ+Oöƒ¨š:EÁÅ+7ìÍjŒ ð{æùàgÄsá|}Ø:+| Ü‚]ÍNpP:Iðã¼P¾¢ª(IpÝòÇ¡|œí»½¾÷î»øµ½·¾á·üµmV(S<@z¿òÊ+·Ò-ƒik ¨rÅ1È8ÎJÄw€Ï¢âG|5Ÿ%|QÁ|…¦ÿ˜_‚ùÈ‹’%œ¥— 3Cæwûûã÷ß?+++Û” €.^Zqäw•þYA H¬Z«7@¿Gtƒï¾þúe@¡+ìý‹X0ßå)øQ\ò]ø8„ŠŸçB¢_ÙæëS ~”¾K’¾£À]tŸ—QÇèׯßN´¦×ú/p៥–ä wt£Ó¦M›­ð#8Ï œœB)æ…eðýG!ÞH†øæãz)àGá|¬€YœÎ83¾²æ…¢·'N8î¸ãcåß‹«þ›y)¡ÿ‚þ™ SKñ€(ãÒž‹Eúl¹å–»5jå‘êѬ$.á–´½·m|b."WÉÿ€‘,Ÿ“ü÷o|ÙÂ2íß¶îÕ7ß|ó¹¬üI:ÕTýËпþÉÃR+sƒ \ЛÏGqßB2ŠÚ+ª°Áßà$¢ð~¯R”Ùçc# ³às²Ï’ãK1~=ǵsdÌzò±Ç®cÅß×KÑð‡Ûý‚ª; Û hU(­¼t®D_xቾ…d…–à¤Ì ïCÉ7¾Ó 9âGÊ¿GŠèBfø€Y4¾K!ªsóBÿ¯¾òÊÝ~Íîǽþã j…þí°A±z€6^ºò@ê{Ýu×UYQQ5Œ€ÊÊKW1™4á0|¾ñž²BJÀ×(S=#Ê"œ“4ßG³ÄþføÞ-¾÷ÞÃ~­ÞÀKÞÖÝ^æýyç×buUýÛaõ+ŠÛ·ãêÑ~$wÜvÛS&OžŠŒ€Hoˆë£ ƒù.9¾ñ‘d€¯öæ*+ÁøÏ'?J–o|$žKžÿ,Ÿžýèßò»·6"å/Zý6§´nM·?÷·ÃR|Û`î Š{#àBŠP¸IéÑa !õ|~NNùøºàcž~ü%Ï·ùOÊŸº¶~òÑGOùÿ´×}/½DÑï÷ÏûÛaE«°õØœ­Él]ö½á†Îñ5“©0X±`â€ÿãóKÌ7~$þ^¾Ê‹Ë>þÉó_Í9ÿ÷ÞyçaïùoÌÊ¿7×nµEËßèÇ+ M‚Öþ# ßå—_~ê¤ñãÇPà ”$õ™DüÜXø—&å|’ÒóIŠË/Ü`,'2þÀõŸÐ÷ŸY>µi§jÿ7^{í¿oÄyÿ>¢â=Ñìg©¢?;¬@6 ZN-¤@rìQG>vôèAÔ,ÈáAüŠE>ÔàÈ0_ÆL=Ÿÿ–1>ÿ¬}ªï‚O1_?'ô|ã“×_UY9ý©'Ÿ¼–ÿ†µ”cQñ¿T}‹þì°¢@i¬Æù¤ÚFÀú[o½õ^Cþ…Œ€¨ª X¶á“)â|=ã“$ÎgÉ&?nœ>4/œïÒÇùüç#ßøm4aÂÄ[n¹åB^¥òoã¥1p¯Ø`ÿv˜о}û~øî»·h+Jµs  L<ùþïgdUgž/óY$/^XÙ`…š>P¤Šï_H,ÍS>I"|üž|ÅøÏ:_rùyä`9rÀñÇ${þ@ù7LÅ¿f¬OƒqñÅßüÍ×_¿ßïINaªØ`ÆÖqüoØ‚/?~Í´ðëbÕ!á|~,_ú×…äñøkH>=êùú?2.æ+Ÿ3ÉçG¹Íîê÷ùF믿{Íz+îî×Z(ÿ•æëv?;¬(0fü_a £ÜxãçŽ3fÔlJ Ô1°…„NŠÐB­„ù’— >ºNøÿ°Ã’2>öøðµ±ÂÇœÒóñµ1?Ä`Kšqøœï¯vnÆ;¾Ò5ÖØŠÿ¢Ú_äüMùÛ‘P$íE³ yFÀŽ;î¸ß€¿þúš,×ê(ÒxÉxba y>ö¤°w<_—ÃÄ¿#…ƒ¹¥çcþÞŸƒòI´|íøK޹©äË?9R·Ýrˬøe“ŸN¼Õo=Vþ+6¨ò·ÃŒÑ' w ìÆ7ŸØdUWÝš¶©L©¨ˆÈÜþ‚x\“¤øü·Ôñù1ôüâðñØQÔ%£%ûüØsô|~Ôócœ,ðIèn~¾Ó*…ü?Ý~›möe#v°zpÔµ%ïó_ÝË ó·ÃŒ€%„ЈÛ·åœTo´{ÙðŒÓN;aØ!åó¢Î‡Òøÿš_>üÚŒ™,É|ÿøyùáã×VH~ä…Ò¦“&N¬xöé§oôÍ}6gÅ¿¡—¾ì`µç¨ë:¢ÉWû›ò·£qá2ÎEµæðTO/ýkŒ€Æë¬³ÝÛo¾ùÀ”I“Üì3j¶ ¢P^ÔñsÁϹâ# XÅŒÔñ“ýü±1RD~]ŒBóÉãŸî=ÿ?ÊË?Ýoï½a¥¿±È÷wa«E]E{ß%K¹ÕÏk´¸¸wÀ–jÉaªî"%°1=íþüókª à,õ¨Ç碿å‘_ßEòñyz>f«ù$!çcH¶ùŠóRÇgvºø%¯ß7SñÈC]í½þ-Øë߸V¾¿5ßÒ·Œ®eÙ[´TÊß3ä½–æ\Ôj\جVJ`}ÔùA¾å÷Ýw…ßÓ:” ]¨­¥Ãa¹0…Þ,ÿÏø, >‹–Ï’#¾ñéö½¤ø}}”ûäãŸ^ýõ÷`Giòï.òýëÉJÿ´);ÌX¬Vq`#¶X[yéÈÅ+}å ïÖ³ç®oû¾ÆŽ?› /|½ðóOy~_ E3//ÿèðÃ?Š=þM„×ßK†üÙ¡Z­Žb?;ìHA¯® )u¸h¥—®4¸Em ú¶ÝvÛ}>ûôÓg'û⪆€6·¬,òÏ/)ŸÅøÆ—?)~zœýן~uá…žî×¾ÍÄ:¸ðú;²ãÔØK#™ïOw±ŸVO ¬‹pmúMX6Þs·Ýüü“Ožš8~ü$J PQ O2í¤Õ\#Ï|ã[„ÅÞ_bÂ9þ3ÈãŸñ×|uå•Wží׺-Ùëß”#¢ý¼ô¬ñúE•ÿêìHe6ßo‡¥âÑ® àAß'Á¦5†À.»ìrÀÛo½õà˜#†Ïô÷ ! Z±”ba0¾ñsðäc9/äÄTUTTÍeï€$‰bhñlÛ¶mÛ¶mÛ¶mÛ¶Í^sxk[JêrSYõÙIÕÛ;ÉËK~sE¹;kÆŒ‰Ûš²Ä_Ÿ”Ðꤌò^N*œÒrÉÿ¯ÛÎWLZL HËÔ€üôc/ÍÛqˆ@ý’%Kv<°oß­FcÛ_†#“VQàwVû(ócÁ‚SýNîß?=þ¡Ó°ÿuhŸÊl¿ -ïËHϪþ$}¿_LÔ6ž¤­Üô£/Nì·R|"`ê‘5Ÿ6mÚøçÏžÇrÁåƒèh4+ ¿¨LbÂÇjß×Û;ÀÞÆæå®;–•+W®3Æ.Bãy&÷ç%…4#J)IÿEÉ_LÔ€ô#ÏH»WåaD ì'"ÀZ ù°LåÊ•»nÚ°a¡¹™Ù]/77wt¾HR‚D?¡“>Ä_ß`££å¹3g¶ 8«ý&¬Ú'©?^â/(BrRFÓ±å}t`Ÿ¨ê5€¯`mLl> S*ÒŒ “frZãÆ{m\¿~Áë—/¯À¼€™8:§@ð£¤}T£HÞ‡!e/G[ÛwgNÚ {óÄ¡>6Íߨ´–ŸzüLê/I‰?<¹ŸMøÿëU¿˜¨¼-ŠØ/'iÅ@i6,Xƒ˜4­™M äÍÛaÁ‚SΟ=»ÝÞÖö;종΋,–¶ÕR T%},0fà̶}<= :ÝËgÏ®¬[³f~Û¶mû³D+±å|ÕHÍ, (AR>–ø3Ò¨Èý1±X à«âÜ´t°(IfåˆIWcª#ÆÂË—/ßmáüùSO?¾¸ñÔ¨×Û{{xøÓt.:öH ú:&úPJô”ìxd¾p\}äho¯Ü»}û$&üAƒáÁzZð‚ƒ'}Ví×T¡¢¥4©™…¨ÇŸƒ%þÔ’øÅÔMˆ@F@VbÌù‰A'Ç*Od :Í Ôeó"èÀ}úôºbÙ²YÇ^mƒ›{{sg£Qãåîîà다ÀD"10Â!PàÀa0…€€?,’Ô)±#¡ÇäŽäÞäÃèÓ˜àéÐã8°‚“ú:ÆÚBQž]»|ùÀHöS'MšP¸páŽÔËoÄbHVõ×ÔeI¿* "Uû%©h)@jfvêñ§W©øEîã&D€Ídd!‡«Éjeˆmse ¶I`l=žCÓeéR§nÓ£G!sæÌ™ºvíÚ¹û÷î]}ïîÝÊÛ·÷ Ï‡ÂÉhÔÁኽ¼==ƒü||"0Ð`p¡€‚ÿMr!D|„«œÆÇDD˜ r^å25„«œþŸ?þsÿ¦çø—žŸ¯ÚóÓo>þÿDÀ|ËäoÑšùÁ¤$>Ê;ôè}Ýœ z½ô`_ý×/_¼¸yñüùÝ83´|éÒ™£GŽS¡lÙ®â%{žðY•_‡ žôË‘BY Pˆ­ãÏJELºD*þÏTýbbB’‘ã¤fí¬ä`y9`Ê@E’áªsBÀv$ä*wþ†üzv]“,Y²´kÛªUÿ!ƒœ2eÊDh-L_¹|ù쵫WÏÛºeËâcGŽl¾réÒ¾»·oŸxòøñÅ7 .(ïß?°47nmiùÊÎÆæ­½Í{Í@u°À °Brñ ÆF¯ÕÚÔ ÓÙ 4Õß²®ç¿ô ð K;;s€>óÎ7øÏKsEyüöÕ«»ÏŸ>½zÿÞ½3ׯ^=töÔ©íûöìY¹~íÚy«W®œ»tñâ™`SF5¶[·nƒcع Kƒ Àãîîîîîz…½ÿI6@BQãf |øÃÿMº{™üÏyRæò‡¢§kºL ÿva Ö“Ò‘Ó~´A34>râÇqµP/·mЩc@nfùQ.ÃZrCpGÜ\ë8 ;qC×tCþ»Ü½™¥îRZÔù·x|‹÷éoõw¼Î—²?†C؇)üUX‚y˜¥ß'§ýhÒ?âóúÓ¾ãxÄ[zhJÆ@ ÂLè ‚õ0 öÃ08á88Ó@W"ûÇ&º3³ws«âW‚'yà›})y)úØ“²ß€5XEþ LÂ( Àœô; 5”~í;¿ãxè­ÕBÞ È3AW¹A0“0FÁŠ ƒ Øâ8Ø áŽè˜t0èh(.ÌìÝœg¥ÐéXI¹ï³àwùmoÁ¦ý*,‡²Ÿ‚‰¤ð»¡ÚÂI¿>–þç¿ãx èÍ@4%ƒ ze Ã(Œ‡a0Çq°È° +°ë´›ÄÁ bÏÌÞÕ®Ø)…®¥NëRî+Rð ,ùÙPôã0"eß=Iáë›~Ý÷+}Çñ¨…l´@GA't—§C0 ¼5>'ˆ˜•Á0¯£ÁÌ>Üb(ô¹Rê4 “Rîc0*%?XŠ^Nö]Ð íÐ ÍÐôHá¿Òw|„QИ ƒvê„.½Ð'Caã` Q3³1ªeNC¥Ô©_Ê'yà›=µ±è[BÙ7¼ ðÁqœo>â(ȇŽÔ mÔ.: ‹ºÍìÃuA§h§¶BËšbÉS],û´ð•ã8?qä£ T'êEƒh¤&3ûTÔPh±‹ZQ“ý.{Çñ0 ªDµP5A­™}ªQ¨ª2å8ŽƒT¾@•™}™Êøßúÿº'M‘ʼn7IEND®B`‚ic10‰PNG  IHDR+ƒÃIDATxÚìØ À°ýÿtwƘ Щ€çD0€+`€€€€`````€€€€````€€€€````€€€€````€€€€````€€€€€Ë^YeQ¸ $’À% $8íÀb@#@"Àîÿ ww™í•׋Mõ¿;=U_½q³g†a†aÿ€WU6 Ã@(ùBJï9??¯<>>®:==­¿ººj!¢ç\kE]aÐÚíe0ßA"ò’8Ž™iÄgô‘©H¿Œ½ Úæ*i=_o¿Œy¯ù Oª_v¬¿ =€fpüN‹2_eþ!ôzá+ÐÓº: ”§õ|±᩸FbüXb7œ¿{æþIÕÊoÞqÿe½#í-Êw(ÕT¾ÿâ?D©#ó¡…ûÿˆh^üÅosžÊûÿY1Ö(xð Œ5ɼñî; œs÷ÞZÙ®fÚÜÜ,ó}뫼2eï€$[²0¯mÛ¶\Û¶íКϡµmÛ¶í7vÝ(Ý<“µçnT¨Gj᫈oÌÆ=yþÔ)Õs@0“ {;êt:wì÷ûÏÓ‹òÏKD¼/}©m¶K)ίW¥±ˆØ; N¥î´Æm‹LM¥ÖÚŒBœ©QP³;¿½%­…?Ìúøù6pHohšæyY;’¡÷íÖ®]{¹Ó €8Õ¦þ¯ý륺ÝîÝ›¦yQ\ÎKŸ¯µþ<¿þO~½-¿Íxµ¯!púj­‘úùíiK­õŸé§ñ‰üþÙMÓ<;ÝñŒ3θÈñÂáàý µÎ?ÿü+EÄ£r0qnþþ¯×Zÿ>Zê9‘" €­¤ñˆØ”þ”¾”ßC¿ß¿ÖùËL –p0@³¡_üâ—h¥”sk­?Lkr`ppY4ø@IûÒŠZë7K)¯oÏ0˜ ,”P':£ßétî\J9st¸Ý¦üº{„@M“µÖÕñRÊ+ó`ÃN f;À‰4ûíÉø¥”·ÖZ›|÷ ìÃhj­[#âÇ9&yãäää­¦…§àD㋈G¥Ïfa^ŸJ̶ˆ8TkkÏ j¯Düò—¿|± høÛk€š¦yEDü*\@ƒ€švEÄwƒÁG·œh @Óßëõî=šá[Dwáü…@D|6Ç3÷m’0 é¿PkçÎWkïÖ¯µ®^Bû÷ñ§¦i^ÒÞF4-X:a€ãÜw‹ˆøð2™å¨£ÕÞ»wïµ—tÐôïß¿ÿ6ñ¥,€ãi°\ÕZ·¶·]iI„ Mÿ®?šéßUkVüÝ,pvn¸Ì¢ @šþv[)åÌ,hÛÓ€Ò¾þ«Fæ!pĦ¤ÛíÞ½”òÃZk9õÂ@­µSJùÆøøøMĪ€ÆÿüóÏ¿LÓ4﮵î›ÙÂ@D´_¯išæùó¹*`oÿ7² E0ë:MÓ|¤€™õ `™§Óyp­õŸóVø8Tkýe{­òŒohüK)¯ˆý §ðPJY955u¿™ØÐø¿¦Ö:¹ ‹cNçQ'€4þ_þò—/–E䜈è,ªÂÀ®^¯÷lA3ÿgœq‘öDÿˆ,êÂÀÎÁ`ðDA1Ký³P˜ñXZÆúýþýC ñˆçdap¸ÀÒ¶²ÓéÜiZ°”BÀ±®óËB°= XjDüz÷îÝ×XAÐø·üˆøÕ²-|DD|ä/xÁE—à¶`¹û€ošæí_Ñ "¦Òsr¬x᥀ûü/Üëõž–ù …€éj­ë÷ïßÛÅ¿-XîÿG… €ã¨é³íªÑE€³þƒÁàìˆÅ €ÇÇǼ𷀳þžššºC­u½À©*¥üðË_þò%§ó€ëÂñéš/E €ÓÁ`ð„´˜õßµk×íò!½3 `&•R~õ¡}èRó¼˜õÏTö-qHq`MäVÓûÍÃj`Öݺu×ÍñÊ4€9P#âs9½È­fý›¦yADE€¹c[¶l¹ñ,† À¬ÿãÿø‹EÄ7æSDD¿ßÁ(˜É ¸×Æ ×χíÖ4\P"–wü{wãHr…q<°»Caf¢ã„™™™AfæŒ(Ì¢0ã1…™ytÍÜ2x¼ž–}îzn¥Gò)Ö–>?×­}k·ÿ'}ºjû'x³ýmU5~üøñãÇ?Ã~üøÍìôm „€ð¯þ›i·Û/í_òof£jn½¯¥Ç,é5-áû'Ï?~üøñãÇßBØ)¶$„Pnþ·„NÙ€7Å/ hÉ'É?~üøñãÇ¿(ŠN£ÑxöÕX @áæÿ?øÁÊaz9ËܦÄ?~üøñãÇ¿Èóüã %!„õ_]]=® 2©ËÉœíþ|jœö³¢‘V??}~üøñãÇ?~üföËåååy§ „€póŸeÙó‹¢£Ýû•þóâõGüz:ÓëÇ?~üøñãÇ_Å+++·á)D„€póŸçùgŽÐþ°ôå_Þ2þë‹kVÄ?~üøñãÇ¿U¯×Ä–rH(7ÿêùþ–:hÍÔò,ÿ:#l¬õûÑ¿H*çÇ?~üøñãÇßͲìÅ”¤ áÆÿcûØR9ÿ Qµ§*áó£j§Ó—ƒ¥§²~üøñãÇ?~üE»ÝþÀL—„€póÉ%—ÜÔÌ®ó ®#k}͹–ø³ûýÕ÷ãÇ?~üøñã/óòïÀ[g® „›ÿÿþ÷QEQLXÂ}nd­þ™±7ܳãÇ?~üøñãÇof?ê•Ñj€J–„€póàÀ”C°s؇¨ÁŸ>ôõ^5}íÃ^¶6{~üøñãÇ?~ü!„?>þñŸ«ü–B@¸ù___XQyB몆´Žs8̸ŸkC¼ÏÙõãÇ?~üøñã7³ÿlž…UÕ€Pnþ·Öëõ'•ÏÜ¡š>HÇ¿ôË;ÅÖÿåÕÿ>ñãÇ?~üøñã_ýò—¿|ýª”„€póÝÞ@ÛÚl6Ÿ^…¹5}VúÏ‹¡?àúþ×Òƒ?~üøñãÇÿî̓±+S ÂÍÿÁƒ_bf…h[õ@ÕiúÀׯ™ískñãÇ?~üøñãßW–·¬B @(ÿòÿ²r¨Îr©ôª»ÿ5¿YÃ92¤´ÃøñãÇ?~üøñ«ì;ûì³o6µ $„›ÿF£ñœ¢üo¨×,úsR[œþ±¾ÖšáÄ?~üøñãÇ?þ]_ýêWo4‘%¡ Dôß;ðï‰fփϴÑ@ê€ÿµÒ½™¥´Âú½ú~üøñãÇ?~üøw,//ß Wl™ø€PnþËçü?ÆÌÌÎþŸÓO‡û¬œ×Ò^¿¿‰N÷ãÇ?~üøñãÇÿ¯—¿üå‹}%Àu'² „eÿkkk)‡Vpö@©a©Û”!œþÌØ¤ƒjtË›îÇ?~üøñãÇ?Ïó¿?ìa›ŸØ€PnþkµÚ±EQt†:ñ´7 ÅÕûÁôÇ mlò~2ùžük¥ûñãÇ?~üøñã!ü¬ü;ö¶‰Û@(7ÿ?øÁnefÍ®7 Ó—uéëyû±³?Ðý÷;*?~üøñãÇ?~üNçÔ^ 0 ÂÍÿÛßþöë‡vŠS ¸a??ìÉ­ÎÐCZ~œþ aÔ~üøñãÇ?~üø¯¼òÊOLD @(7ÿe¶…þ \9ØõÐ,ãy½/,þ³þ¿ÌΠ—?~üøñãÇ?þ×€PnþËôÝü›Ùdk©‡¬ìz`úÃÔÌß'&®“ð¨a›?~üøñãÇ?þb÷îÝOé•[¢`L¡ Üü_÷ª›ÿV«õIg–: 5å4WgYT”„ŸwÛß„ý^cõãÇ?~üøñãÇþñœp—„€póŸeÙ‹AåW=<õ×ãåRz ë×Ò¿(|“pÝ?~üøñãÇÿøÃ7Œè—„È/"nþ·”Ù¶¶¶v¯¢(ÌÝ—å?ÒEéôæ×¤Êˆ÷u]Ó~üøñãÇ?~üøC,ÿN>•£>€PnþÏ<óÌ[šÙF1`ÅÀJjPýƒR,nSù’vè‹Yd9R~üøñãÇ?~üøÛíöwÆ] ‰ÿsyž_î Õ”CSÒž«ê´¿bù—¿$KpñGÖ?~üøñãdzÙ|Kß“¶ŒôÉ„€póßétÎtJјŠCNœákÞçÚTy}1°ÕП?~üøñãÇ?~üÝZ­v?ñxÀ«] ÂãþæêõúË\ñr¨´åUÃîÒŸóš]õq<´£a.>ž?~üøñãÇ?~üeËËË7Jz2¡ dЉÿ+++GuÍÂ0ígµ|ÉŠqÃ)¾G ýÔÇ»è.†í¤ùñãÇ?~üøñãït:¿(ÿÎ>Ø%¡ úw›ÛÜf±.»†Ü?5ü4Ó׿ŽÒêôÕô_âó“æÇ?~üøñãÇ¿Õj}ôp$„}ÿÛÊÌçy~QbS:øð=ìÄ×äðtZÑèbèúmò„ûñãÇ?~üøñã/VWWÙ+Ò$„Cÿ²,{ƒzЉÏ÷ *o8>üÄyÔ‹‰Ï‹Ö·?z`[|ÍiðãÇ?~üøñãÇ_f£w‡’8dС\pÁݺf!0q£ª>§‡²ßœêv6~þ2)ÿºò4×iñãÇ?~üøñãÇßn·Üw€_ ¡eæÍìßò±%NãÙ‹jâçÕá'úuã×Ð_׭뀖?~üøñãÇÿ´ùñ7êõWö•ú<B@8ô¯Ì|–eŸˆ†wà‰ß|ú‡™¨A§ÿï]Ç;qU"3~üøñãÇ?~üø‹¢ÈÏ?ÿü;q( q ¾ÿZ­vÿBá4©ºQõŸ‰ `qJk4¬Å@Ž×”Á)\ÓêÇ?~üøñãÇ¿Óéü½ü»ýB¯`+‰ ¾ÿOþþØ4#~üøñãÇ?~üø›GuÔõÅV€*„€¥ÿzÔ£nBÈäž!ý±ZÚ$]B *~N¼N|}gï—ž3áÇ?~üøñãÇccãó›«€gk+¡àÔÿÅV«õ]gß’n"å°‰Óæ§bŸ“¼ê5¼æ66Î?~üøñãÇ?þ2vÖYgÝy†¶ Ný?ûì³ïnf]Ùúm¤×ŒFCiÈëêÏ÷¢—ség«*ç¬ùñãÇ?~üøñão·Û¿ê­`+@B@-ý/³”çùŸ½ÖM;dD R±lI OwÈêA&¾__VýøñãÇ?~üøñ¯­­=µúO ,ý_رcÇsÅi§ƒ†Ý0{¦ô²$ñuñŒTÝpŠA&‡ž0̲?~üøñãÇ?þžÍT«¦· ü·µÿ_ÿC{e»˜0HÜýSñÓ-ª~?z;ÃÜ9áu¦ýøñãÇ?~üøñïß¿ÿ]}[¶Vg+¡à™ÿóe÷íÛ÷f3ÓˇôÀñ—9ÏVõZKsÐÔýXúûñãÇ?~üøñãÇBÈîv·»Ý o+@U$<ó¿ÌõBè1&zhøû‹ÍPK˜tS©÷h zòµñãÇ?~üøñãÇ¿¾¾þI±`š BÀÁzý£Î3QÅIx~©:…ÔÛ;¥¾®‡«þX\/ráÇ?~üøñãÇ¿óò—¿üf:PðÌÿ=èA7!´»~9øðµ*áš]¿Ô¯å|] j1èñãÇ?~üøñãÇŸeÙ·*´ €Pð¯ÿÍfó+QÃ(˜{z©þºÿxu­øÀ’ä¶µë7ºøñãÇ?~üøñã¯aùÈÿØ; I–5 ?ãÚVðÙÂzfèÚ¶mÛ6Ö¶m #ô½;mÕÔæëšÛWõ¶âÔÖ4g¾ˆ8£Ö|Q•'"Oþ™ÿƒÇ9 ‡T _ýÿûßÿ~¸SÚ# ÅÛìDi«×‰ýSææerÞŸ->o8óÃ?üð'2ÓÝn¶õ,4Z§š¾Y‹LtÑ*“ÚÑhrÑטóÃ?üð'“ÉQΙa…­_qæUØ ¯¶…Á»$yÑeªE‰ÿÒ;‘ÔåUÊõ{¹ÓSÏÏnüðÃ?ü±tØÔ…—›Å­ï˜1[ï0/®9+¯3ôÒê3MÓY×F¡ë4=/}`¢s—™L¨ƒë?üøá‡~øû/¹ä’ãj±- "`õ¿Ðöï”SN9Ķ팇©+còLµ!ÚˆDïSÉàm˜CŸ~øá‡?œl5«ÚÇ™[nr&úr ]÷  ¿;Á$·7Û²¸þÀ~øá‡?‹½åTTq[@DÀäß‘WÛ¿h4ú„­SÀ@æb “ô™œúëcªÿOž¡Ê?üðÃoõ[¦-²Ù,ly˼³ñ* ¿ ´B×ÜgÂïŒ7É­u¦ß²¸þC•~øá‡ß¶sþóŸ¨ÁÿåueYI[˜I€¾¤:E žBz›–×cBC~øá‡?œhXéoÓµb²/€ ]}ŸÙ5rŠÉ´wqý‡ ?üðÃ<{¡««ÿápøa,Þ¥JÊ Ä{y|ž÷cþ?ßûý„ÙÕ2?üðÃ:—0Ûz™I;úÌ~þÒZmwyŽëwÛý·ÁöHu½§­ŒÌ²„™U?üðÃ(²ÅL«{¼$Zø ¡@ëìL×So˜äÖ:îÿêä‡~øá·~ò“Ÿá,4º«* Vÿó:8?xûøàƒˆ$Ïc°‹r A¶!±UBê™rê2(­âóÃ?üð÷ĉ¿håGPÎ UßÌý_~øá‡þT*µ¡pà•k ˆhýwH<}_$‘ÚT¼ ÂýZm0:æ#>C$¤Åæ‡~øáon7Së7/Љ?@eÔ~ï³&¹i;÷ ùá‡~ø-˲ÿøÇ?S™–€ˆ€Õÿý ƒï°\.×çJµ¸×Ï©§0*ñ? Ãt±‰ç‰~øá‡?Ùb&l¿OL¨ ªE¼`’[ë¸ÿKÄ?üðÃßÑÑqs["ÿ›1cƯ…©øJmÑ»Ôã»~l™"?[ö)-6?üðÿ³â?iÇCb"MPÕ›wpÿ™~øá‡?“Nï,ÿa€ˆ€Ãÿö+ ºC£ÑèDŸ­<¼ Åýw½?Èûun“Ñ©¤þY˜^±øá‡~ø; ¥þbMPS[vpÿ~øá‡Ïi§vRÙDþW(ÿÏf㟸ŽtË-i ú¹þMJ¿¯6®àüðÃ?ü±bâOPËêp‚€-;¹ÿ‹À?üðÃßÛÛû`Dþ7uêÔßCñ6qú¨è+ª B?O>ÇöÕ^%8?üðÃGTLü †^EÀÖ:îÿAðÃ?üðg2™ÎB¤8 €½ü?!O#U¥JÊ0³åGÒ¤¼ž£MTšX~øá‡þÎØN1ñ'Êê|ø%“ªkfüà‡~øáßcÛöw¾óc Û(Ã6DÀáynYVϾì!²ƒ—+is‰¢ûqm6ÅßC?üðÃß“h63žý¤A@}3ãùá‡~ø›šš.‡3@”ÿß}÷Ýß°\{~ľ ÷ÏÚØÜé2!-÷ûè(‘„j~øá‡þÞx´0þ}òÃ?üð'“É΂¤Ø0èPþ_8ü¯§§çI×`ÕF¤%Nÿ”©eP³ñø»x\ðÃ?üð÷&Z˜øï[@ÐÐÂø×üðÃ?ü™üœäˆ¼),P~½„ÛåÿéTªQ‚jñámLÅ4¯ eL޼QexðÃ?üLü¤ €ñïÉ?üðÿtéÒ¿•l"`õÿ“òÿ£Ž:êhË²ì½ h[ hß&äß<Ôs}>6ø}WðÃ?üáD‹™^ÿÔà'þèìL×3o™t¨ƒñ¿~øá‡þh4:~/Û¾Rämˆ€òÿÍ›7Ÿ.ÓFÝ3T¥“þ’MýÞêïú3Eò ?üðÃIu›ͯ›—Öž]ôÉ2A@÷³o›LgãÿS‚~øáÏe2]ù¹É‘EØ€˜ü;ò*ÿÅbÓ<÷–êù?¥T•*©×IP=à‡~øé>³¼u„yeÝy%0 æón2½oŒ6Ù]}Œø?üðÿç¿øÅ %Ü€8ýßIÙr¹\¯ÏQï÷ñÿ·`¦¢ÍÅwêh¹þ>|ùá‡þt.iÖtL2¯¯¿¨¬gÔ|á­f×È)æìutW¶¯/…È!Ça¢ÇDÿ>ff¦¬ñx&f¢€æ8Ì ‘sÇÇ×›žíXf˲¸Õ\u:9¯ßéœÑÖ.µe©»ëËZ¿¥(ª®îﳫϗª½ÃdŠõ?üðÇžß¾}³Õi)ÿöÿk®¹æ²‚1?¶Hnýã£Þ®¤[Aýo‘ ’Ñ•O~øá‡? sv{ÇjûjÝL6ò€1MÓô[m­ÉæXÿ±å‡~øÓéôWL(3åöÿÆÆÆÕ©/b©(y¯~—_/K¡H•ùìTüøá‡þЄvW×7ömó+j# -sﲉÕk­ CÖìøá‡þ ú‹{” #ôA0þÏ¿ý?•J­ñŠI¹·ûèE'ºAT‹„þY¢?'~øá‡ßØ}]kí[Û¯¯è 5€´^¿íÿzƒ·XÿµÏ?üð/Z´èÏÜ©@ý«:~øá‡¿ø¨²½æškþœx´}€çÿs¹\§·ðõÛtts'¾ÖÈ‹^*tRÁñSöûT?üðßȲ«êŸ-¤A01€úWUüðÃqdùôòúÏÿŸXúü1çŠÿ±a‡nð¼×Š ÛhÅC)rÒŒP¹` ŸÏc©p~øá‡?“KØuÍoÓÙ¤‚`b@o‚úWüðÃ_Oϲ£ï@<ÿÎsÏ=÷·Å¦Å( Õûùv ßJç»›Å8Ç6V6?üðÆ9[×þ©}iËÔܨišv‹íýpµ5¹<õ¯¢ùá‡þ\.·GèpÜ0úÏÿ755Ýí6©Ð¨‹Ü#'#ÛÀ¨ÏÉ3MõgˆDƒY©üðÃh÷u¯³olw 7¬@ZæÝe«×ÚBRÿ*’~øá7a˜)î].(f|I€éà€žÿwäùÿD_ßJϸ‰‹Ï/@F°ŠJÑÒ;”j·ù'ZÑG’T?üðÃßÜ·Õ¾·ó¦Qܸi½å›Ù¶‡úWYüðÿû÷Ë/¿üR×>‚ àñ^ÀŸžÿÏårõÞâ•-ž‹ŠŒ¸}Ó§œAMâ EL¶ ÑŸiHåðÃ?üÝéûù'ÆpK¤ýþgm¶¡…úWQüðÃÿçŸþï‡×€ hxRIÀsй ‚ŒòŒ0fD†D:Þã~øá‡ß5ø{gÇ¢*ÜÜi½q‰MoÞNýu~øá‡?‚I3¡ŒF€@ÀD"ñÙ E@˜Ã©ý‹ƒ<®Ä…K)2Ò3JRTÆ‘Œ?üðÃßnµï{¨V6º@£À¦6êßhòÃ?ü?÷0•Ù h˜ÍfwK£:\ÄY F)D‚]”Œž¾èýQ JññXÄ&(Çž~øáOçv]óÛö¹Í“Ø ×P¤qÒ ¶ë¥wmЛ þ ?üðÃÿ /ü}×ð,F €€N\A¯·èäžÂâׯü\’Q¬¢` µŽ¦Â¬SáØcÆ?üð‡aÞnïXm_©›ÎƸ†ƒ MÓn±½®¶&PÿŽ)?üðÿsçÎ%“N]4<ÍÙ² Å\dŒ1ÂB–ž1RŠb%uû¨&ñó(ãNŽç‡~ø›ÛìÛÛodC£ HËu÷Ùäú-ÔÿcÆ?üð···¿êŽ/ixâÏ"€½ çý¥¿ô—®Tu´Y¢ÊmGÊœQ¯€ˆç¬¦:FDu2ÒüðÃwºÅ~¼ïA6Â1€¼ïY›kl¡þ8?üðÃßßß¿Æ5d€ óþ4`åÊ•ÿ¥ ;ÝúÇxsAÅÅ«Ýæå"Ù:Ê·> ï;RüðÃ:Ÿ°ß4-µÏnžÈ&Ø@b‰×ÙÎgßrý¨ÿ#Ã?üðçr¹&&”8pW3†r§Q¹ ø–Ï;F´Š²ÔÍ¥ôïCµ£ç‡~øƒ0oëÚ?µ/m™bÙüñÓ4y±íýãÏ­Éå¨ÿGÍ?üð‡a˜b@A0 »³s™×0DY˜Baˆ:ŠDø½ä|BŠ<ÇT¶†.FxMyüðÃ}ï&ûæ¶lz €¨iYx¯M®ûžúTüðÆ?÷2Ë“~L˜PÌ…ÉdrƒÐ©s¸ÏùEDy®H*ògQ;‰õ\ò±Ñùá‡þŽä»|÷Ýlv € ;ï}Æf뛩ÿeóÃ?üÿäŸü“?Ç$€èA0àÜb.Êf³¥éÆ0oýr‘¹¨ø¯Sº›F6£r±ò~øáïÏvÛ/^ä92"€Ð ßÝKý6?üðÿ|ùòÿ0ðHs1ã=p<à·A Æ•€‹ƒ H¸Et$òÌM}œG”ù¢²õ¬à0:– ç—o{H4~øá‡?s<çOdÄÓèú„Ùõ?2?üðÿ}ûöEnà“&Œ;2 ˜KŒ1¡gÿ$C'ÿÖ©)óKÅB&ÿôÏ+ØHås(üðÃh÷v­±¯o›ÇÆ– È1KËü»mfÓfê$~øá‡¿µµõå#£½IÇÓ` ˜PÌ¥a=’Ãÿm,‰ÐMTZØ.BaÒÞ×Qn‡ò™d~øá‡¿#Uo—ヒ -AQIðø|kÞ]b ÍÔøá’þ¾ÞÞÕn@É(@& baÀ‰þ€%K–ü±û§T¼ \oò!¿Vf½Ï+¼N²—ðÿ›çÿ-óü €Œbºζæ‘kçÑÉÖ|ú¢-$û¨ÿƒrÀ?üétz'££ÀÀ ×­Yó+y<‡o å[… R¡ŸÙ‘÷‹‡ô»67Tf‘Ç“À?üÅ„vWç×ö•ºlb €ŒZš&-°Á“(É3s­Ù´Òš0 þÿ,ðÃ.›m/x6£• ¸oß¾»”g…4kW}¨r¼2ÿSkÍ‚Š¼ðÃkb§}wÇb6¯@F=‰Å3ܦ_Èë·ÛBËk¨ÿðÿKÉžfŒÔƒ`à„b.jooU¼•GX|Ê(Ïì)Çû·þÈÇê¯þ¦N¿…þd¶Ë®ª¶¸iûW‚ £ž–)ólè6új–?nMo‡¥þÃ?üA„=Í1Fvww¯T¶\ÜïÊs}Ñú{¸þÕ(?üð:thùÀ£Í®ÇÙÙÅŒ+x\1+€`@‰8Í=3Á €Ë£6îPŽ‹Þ$êí<š CÿXù5Êç¯%~øáÂüOcý^Ú2¥27…@ÈÄ…6}—û7Zyz¶5>±&ŸãúWcüðÃß×Ûûµ'N@¬F"è €‹þ×ÿú_#â³7zqˆ^ô[}¢[Bý½”¢WKüðÃßҷ;µýúŠÞ!sÜØ¿±ÈÒ[¬iÚÅõ¯†øá‡¿?‘ø~ ·™ëq6ÞÇÇU ';p–{>æ¢W^yå_ Uÿ9¼çs\#ž£+8¦Œ1#ÂgQ8ª‹~øû³ÝvUý³ön÷'.iüåB›[âÆþe–?nM_'׿à‡þT*µÏg~ô€@Œ+¹jÕ$·p¼EZL…Å+0ÑŽòÞ~QÌa¤×ºT3?üðaÀíþ¤ª!=7Ìt›ð ÈS³­Ù´Òš Ïõ¯Šùá‡?—˵ ô6sMÎÏÀÆY çsñ–-[E0wÒb‹nÿüø Þ¥àG{èfRa¨>~øá?”Üo—í¼µj6‚@H˯çÛð!7ö¯’òÚíÓ¸þU)?üðçóù^'.ôÀI±€³_§8p¶—Ôïßÿˆ°À”â" £Ýú£? õç„”&…±òùá‡?“KØoèîOªPÒÛô Øð+Ó’}\ÿªŒ~øÃ È 47/gsZ‰8..€ø#Oœ^*<¸4‚ÕSððoå1J‘0ÒíIBÁQÞÓ{ðš ç‡~cB»«ókûJÝôªÛ!mÓçZó°ÛlWržžsø±€0àúW%üðÃAXÜÛ\áÀOœPÓ@J6ÿ’ïd\ÒÕÕõ‘‰V$ü…í'Š£»m¨ «ŸSÿYùüðÃß‘¬·ïﺽj7@Hæî©n“]%yãNkZ÷qý«~øáÿ¡PøÁ €‹œ8Ë €“ã(€ãpioOÏw‘‹FôcÃçý7¯ˆ¨¶Ð+HêùóXÉüðßÉ÷¾ÝóD6“@ª6óg»u•åÑØT‚ë_…óÉ8× €qqÀ©®!Æx×!óÒd2¹uÈ9ÂBížd ýc${)[Iå½½¢!œG(Ž•Ç?üîvÿWëfVõæiš´Àæ—¸ÆÕšgæZ³åK[0!׿ å‡þÿý¿ÿ÷ßhrg€@üa‰8©DœãÀe33=û6tCù áu%•Šƒ·ØÛèá=”¢SaüðÃß•j°¿Ù}gMlþA¾Enì_-äû­9ÔÈõ¯ùá‡ÿÉ'Ÿüç€ß p¦sy:•jžÝÑlžl…âàýŒÜÄH…J/8þç>seðÃ.HÛÿ×ú¾}nó¤šØø‚ -×η¡küW3ylŠ5«ß´…LŠë_ñÃÿ²eËþ«çÅY Ç•€Óœ˜pDär¹vµxÈ‹]1¢ŸÛÿ]#âýw¥Ø¨·IU?üð×÷l´¯o›[S?B$uû4·q®Á<¿Ðšmßrý«~øá_¹rå¤&çNœíÀ)Åœ7€@œ €+r¹\·jî¤B"§¡0N3½™‰Zxä.¢cÉ?ü=éVûáÞ%5·é#@ÚgÎu›åÏ{XÓÑÌõoŒùá‡ݺus=pº'þ(N€8Ù €³œ¸¨˜+‚ èž©QŸR^§Œñ¹R|„Û‘üÏPÖíGcÁ?ü¹ c×¶¼Ííþ¤&ƒ ÚÜ}Sb \ŸfÍ·ïÛB.ËõoŒøá‡¿®®îæH ˜¸ì €qNœû§ ŸO 6M2qúí;úb÷>÷SxŸ!‹‰—‚WTF›~øz¿·olW³>B¤ká,·9ŽY^¼Ñšu\ÿÆ€~øwíÚµd`Ê™w>Þǃ@ LæKÌ™dû§Þ&$[=mV©ð7ñ¼å¼¿ËhñÃ*ÛcWÕ?[ó>BŒý tcÿ⚟¶&ÙËõoùᇿ¡¡á)€àâb® Ã0TG|£Œëк ‹u¿Iï¡Ün$.½s~øáw3ý_©›^ó›=B$qÓOcÿÈÓ³­Ù´ÒÂëÿ(ðÃKKËÒ1çnÜùx7þüTOüa¼·hüç„üEîÿ®éP¼`=)XFÍúÇ(3O?üð÷dÚìŠ=÷Äb£G€´Lžoßo„É;÷[ÓÙÂõÿóÃ{{û{%àœpÀ€(ó£ðlŽúß\tÛè"4õPž,¥`+Vº9y~øáÏÙŸfú?O“?£ HêÎAÇþ‘ǦZóõ{Öä³\ÿ?üðwtt|20漘 8p8Ày%w‹O0xÞq‚!o*h‹¹ôÁPúþRó‰£äõ#Í?ü­‰ö­í×Çj“G€tÌ™£l„Iá¥EÖ4lçú øá‡¿««ëË0Á €ÓJÀq±À¸qã®6ʘbôE*,X¡‘‡n)õ‚"·Á>÷¶p$ùá‡?í³_6¼XÜôü"~=BŒý»¿dìQ›’}\ÿG~ø}}ëšœ à„¸ à’ÿðþÃßwøÅC½á½Vøa®§ÃHñÃÿÞ®µö•º±Üà‚ Ý×{ìyzŽ5[¾´& ¸þ?üð÷õõmqà"'Îràä¸ àÄbNqàì#àÖ[oýûÞm@Rq­díd¨*ÿœÒù¤¢2”ÕÎ_>?üð÷¦Ûì{î‹íæŽiþ5cÿŽ*ï.±¦«•ëÿQòÃ*•ÚU"Îu`À€xä‘Gþ™hêtçýM^äîoê裙=ùó(EI6‘åòÃæí¦ƒØç¿ÿU¬7w„ Hâ–éG» &O·fÍrkò9®ÿeòÃÿþ¸ à<pz©xùå—ÿ°àû¦Üº#<“#Ì!õÎ'4öЇ`¥ç’DZ?üð·%vÙ··ßû!Ò6mž5ÔF˜^^lMãN®ÿeðÃ&“ihrîš#ü ¯¿þúö‘Ô‰Óý³ÿS¾ÕGÿ»f0ýÿ®Ý†¤ÌÎ?ü™|¿ý¶i©}–&–i˜¸ÐfîfìßÈg²5Ÿ¼`M*ÁõüðßN¥šc)Ämþ%0¾˜ó‹¹dÙ²eÿÃkð!Z=wœlý×G‡ÈfQþ]/Z‘óމÊ?ü{:¿µ¯ÖÍt›B€tÎ=¦cÿÈs ¬ÙµžëD~øáÏf³m‹°€ãpéŠ+®‘¡[€J‡N¡aˆWŒ”& Ú˜½‰ô»:×Tã‡þþL—ýtß#%B€4ýrÍ/•Æä7ÙB_'×…~ø3™Ì¡pÀ",à '.(æÒU«VMÒŠƒûwÝØ¹´sÉÏ$ÉÅLŸª|vyŠÌ?üÝÙùµ}¹nš·¡!„ Hï3GsLžšýÓÈÀ‚ ¹þ üðßÍdºâ,€?ôÀ©NœãÀeE0Í[´Z‡N­Ë¨T,ü¿I?ýóë#K\ôŽ¥òy|~øáïÍ´3Ú!Ò|í|>Äÿý“¼ÿ°5½\ÿá‡þ|>ß‹8à$O\XÌeß}÷ÝoaI?墢Ìì¶\\„÷öÍŸn?ƒ~¹°À`·w¬¶/~­°™!„ Hò¶1ûGž˜aÍú­ ®ÿ%¿Ã>—K ~àÌpùÚµkd (c4s'/hqŒ‡÷»z¼l)åb%ÛLøáïÍ´+öÜ3ÄF†‚ gÌ«”0yû>k:[¹þ»ŸðÃäóɸ à¸bNpà4'&6lXä-ÅÀ ‹RéªÜZ¤ÞS™ñ"øá‚¼­kÿÔ¾Àÿõ'@Ô±Ù{¦VÒ&˜<1Ýš ŸØBpý5?üN¤ƒ€¸¨˜+6nÜx›ÚÐÃ7†ò¢ÔǃÈç×n?’L¥^õüþgŒ7?ü]©Fûþ®[#lb!Ò¹`v¥n„É[÷XÓÑÌõ?Öüð‡AEp²g• €ººº;ó'ÍýŒÒ…Tk"åœb±ó3TçP—xòÄÝtðûüæI60„iš´Àæ¤ñ_E籩Ö|ýž5ù×ÿXòÆaðÛ ãÜ"8÷ˆØ¶mÛ’!ÍŸ<»³4þ¢ö“-£´¸µYŸúóEº±t‰?üÉzûÞΛ‡±!„ HßbÆþUM^»Íàú;~øƒâ?€A‚@¸EqåÎ;V¬\Ô/¿Î/ òß$s¨YB¥1ˆÒ$%6üðaÞnj[aŸãÿú2¬ HË”y6|¸ª6Áä±)‡ïÈeùþ~øÃ0 C€Ø³gÏã‚©óÞÐ ^>N;j´q&r!“:žzQlc,øáoëßcßÚ~}›B€¤nŸV­a²ôkZöòý'üðù<`¨ {÷î}Æ[ÒüN½á‡ÜT­f,õy¢bA(H\.µÍ>ÈØµÍoÛg7O,cãBAC³æVû&˜<:ÙV¾jM&Å÷Ÿšæ‡? Cƒ"À^ôÌ];èÿ”½\¢ùðqQÞOùÌ¥©]~ø[ûvØ7¶-(sÓBAƉ lî¾)µ²&//vwðý§6ùáƒ0H€óŽ€úúú¥ÂB–°èõg‰”¢½#©~»pŽZã‡?dù¿þ„Œ@¤{á¬Üs7€Yý¦ë À÷Ÿšã‡¿€"@ccã‚uÓ ƒŒ>GT?‡nùÊ1žÊç­%~ø;S öÝ‹rÃBAæ_/°cÿj»7@Û¾ÿÔ?ü-@KKË;LÞUT^èz‘‹Œx|¡ “)5/© ~øƒ ?0ןÿ„ŒP$qóŒ¸l†™Ïñý§&øáƒàÀÐA Þ—;ºh¿ë3;£šDÍj¶S(Jªz~ø»ÒÍö½·ŒÐf…‚ ­Sç[ÃØ¿øäÍ»¬éjåûO-ðÃ*@[[Û ÅØi _?Fÿ›ð¾ògÒgôÏY½üð‡&´u?±Ïóý Ñ HúΩqÛ“ǧ[³þcw½åûOuòÃÁ€!‚@´··2Œ…Õ&j–Oú]-~ ‘?ƒl&«“þÞL»ýÍî;Ù\2ÂAŽÙsØ Ç9ï=`Mo'ߪ‘~€àÐÁƒŸ ³qˆ^ô‚¢-vyö¨l/Ëf¨>~ø÷v­µ/o™ÆÆŠ€4þr¡ÍÝÏØ¿Øç©ÙÖl_Ã÷Ÿªã‡¿P( † pèÐ*Áä•ý¼N¨ño¡R4”‚ r„JÁª~ø3ù¤]uài6T„£ HÏ 3Ùü’ßæÃ§­I%øþSMüð#† ÐÙÑñu}qé¿ë‹×Ë5&Ñ›‘ˆ?á¯þæÞmöõ­sØLrÌ‚`ìß|>ÄØ?âå…ë­iÚÅ÷Ÿjà‡ €èééY¹Û¦bÅ$ÿn£êyÕ÷«l~øƒ g7´¾oŸÝô 6R„à Hÿ­ÓÙì’ÁóèdkV¿éÆòý§RùáGèA º»7¨‹J/êBÕ^ÈÂ2 ”òù+”þ®T“}wÇMl 9ÆA¶isûGô¼áÆòý§2ùáGhA úz{7›0Ôž’¾`½Å+–P)R‘ •Bú¿{ç«L~øwv~m_øþZ6O„ŒB$s7cÿHÄ<1ÝšM+ùþS‘üð‡Å †H$¶ê.ºÙ •† ‘M¤ŽèÖR(bÊ9*†þL®ßþI=þ­ HÇ<Æþ‘2¦ûùþSQüð‡A€*@"±³ŒÛy¤E]Îk¢Ž÷(÷6$•¥røáoïßgߨ¶€ !£iú囀±¤Ì¼¼Øšö¾ÿT ?ü-@2™Ü£,,Åð©¯óoבL¥ºÈýׇBÓ ¤ÿúJà‡?°u?±ÏmžÄf‰Q €ôÞÈØ?r”y|š5?·& øþ3Öüð#´ ©dr¿°8½ÿVÞp8ÆN}Iÿ ¡ð7%cÈ*Ûc?Úû›$BF9Ò*–è,oæ À‚á»zŠù_ô£Ÿ@…€  6*Kˆôƒ=ôRo†Çm©Ÿ!ÍT‰òùéÖþ¨œº·Ežú0Ë& iÂ,˜@¯Ä|!±˜Åü/}úÑO C@‰Dš-ã€Wš´~꨻½;zƒ°cúYŽ–'¥O?úÿl»V‘ žkÿ Û|W-±P€ù_úô£?#x%áðs§D}‹&¤¾Gy­¥¦œzR™ýèo=–/.MÊàŸ:(û`m™Ä1ÿK½~ô³@‡€ ‰ø ûq”Æál¯‘¥\7biûœ_3¢'††ÇS¯ý÷š/˪sCsÄ|<>"w À’1k¸Æü/åúÑO C@´ eð9ðÊr u°»o†öï’Býèo=ìo ÷û@Îp»±„gø0[ÌÜz8 ó¿éG?€€†ÔA¬7ý ‡É¢eJõ%M®ÓÈÔéGÿù;d‡ý@Nñtì0Ì&ä&>‰ÙÇü/eúÑO CÀ €ˆ¥4 ¥Á¸û¿ÛDPy¯›ïýèo=飹ÄÝ‹%:;ǯýn8RÃü/%úÑO CÀ €¨²Ïƈå®!éד(»ÛƒdÞëÔ1ýè·¢Ùk9&r–‰\ûyžu‹F™ÿuH?ú t¸Ðr4€M¯q™ZÊ×Nuž,êߣ#úÑ•½·–bp€ i8 S ùÅöå³¢Ìÿ\ëG?€+bÊÕ±ýÿ)Àr³·Éå{ÜëGÿá;ë07@‹ô#i1”ìÝÀüÏ~ôèÄ,+ÞÞ=9–›&dÔúgG£î„ÔÑ~ô­ß„±€åÑ®ýƒ<æÈæèw¥Ÿ@‡€ ¡$k®RFKÀîqü}Üúkï©€åö€b ÏäÚ?Èsjw3ÿ3¼ýlp @<Kت¥ xu ›±”tÏUCˆFõäQyܹ~ôßn<+‹jûç¨Ñx6nhþ›?€ÏJìö%æíÓ~ÎHºOóô½C–a°Z.–ò:KѧëG Ô,kÎÏQ“Pÿñh±æȵKÆHÌßÂüϹ~ô¨° éêžÍLÜçikWê[ÎKR’’²J&%tîŠ<œ³LnõÛó GÏkãÙà•À™/L¿À­IÌÿœêG?€€½)‚-ýÓ–ÿèÏë§‘Ú_ï|©‘ýó”QÓþÛuZ ¿™)ÅïJåölz`ùŸ¸³BY⟚Š=z*›·ÊÁ“ó wKd×þA~b-ñJððKÆß€¬gþçD?ú :¶ÏFÙSdxÞq3ÐÓ̘’JêúÑ¿³uù¿J÷"ùôÓ_É‚C} ÈôÀÆ‹ŸÈùG;%–ˆH¦+iÅÄä”Ü›0Ç`€¡±”kÿ ÿ°ª<Þ>Püu­æ_!üdó?åyô¨¨ûz §Šª÷*é¢Òl´TÐáý¥ºgúÑ?ÿ“þ/}Ò¾¯ËÌÅ]¥údþøP¶^›-÷ž_–œ¨dR—®ÉÃÙK_2¿p×;Z,þƒ|Ûçÿ¥Om&_!X?‡ùŸ¢ý:N7-ù3 Ló€W–ø( Cùy¶&elTº~ô7>~(%ݺ´aôu&úÞ–¹_ü5ÏLûû·]Ÿ'ƒ7%W+z÷þ®¼5à“Nk‘GBû4ã¯i©eþ§èG?€[´å4JÚ§4ç Co8æ÷èMÂðM?ú·,›­|g”ô(’éå¿–ê£}sÈôÀ²Óžÿ\ã°%_+ÞâÿÏ5‚wNìT@è\û¹Ué•ðVŸøësï𣯙ÿ¡ŸC;€)yÓOužð¹I#Mi Ö´ÇlÏ£_×_5n€ÁÔ»cBÿ7dÖ²?Ê“ý²hz`ÅÙArê~Db)”Jøƒ/‚€A“ Þü?)–ÓÆ ²É+s3ï’`ý\æª~ôèpà Ì  ý5jÚ×ú:ó#­ÉØÞgµõÿ6~žò i¢I?úÃÁ Lèõ¦bêÝ1iè;R±¥{†M,;ã•ã ›%J¡V"‘æ­û 6¸óQ±DgysÒôX+=<¡,÷wIàÆ±¢æfýègÀ«! Pží9%TÞc)Ÿ¯4 =•4|¾I?úo^<­ùŽQÒí5™:ᙿ·w†ÌÆ?H~A@óø!9gú¬jeŸа·™ÿµúYÐ^¸ÐÔ,Ô};­¯QRGó~ KÙ'dJíÿ¶”¦ö/lŸ§5Dôøv½bâSCI¯.R^ñ;©>Á¶€4ìñ/ ãOPï%V.?€J„¿÷IàŠbþSDøÙ>æýèç Û@5ìÁq´ŒGK4)S’gÚ¤âè0³~ôo®šª˜÷Ô2~À2kÕŸSg~0þ/íñ§`ÞèŸÊµ;DÖû$pN1þ)&ôpó¿WêG?€s¸ ­fЊ’Ƶ·Ø«a) Œ9Z椧‘FýèWL/“G½+ŸÿÐÃ¥ñÀøŸ¸÷¥„c~¡Ú®¸? _ü ·½ãóÊü?>"'L€µÜ+Áãi3þúA€ÌÿÌúÑOà¶Ø—òœyÙr¨áµæÖΦf™RDS“A¿±¹Nî÷¾bÖÓH·"™VöKYp°#ÓÀ=þäÀ´š…jG°y«Üþ{IΛÿÛý‹%\î˪é°ª<ÚåÿUŨ§‘ÀͱÌÿLúÑOàÛ SSCóžœWŸêüsŸljHm)¦±©˜õ£ÿɃzŤg†Ò¾]dFUW©>Îùm±èÔ‡²óf•´D å®bÏšäéÊ/åÖ€Or6x:–kÿ ‹Tx$Rã“ÀeÅ gˆh¸‘ùŸI?ú ´h¸zÃ|—§~ª§~}ˆò½ô{LõýAÊÏAÿ…sžY&zß”9›þú’ñ€ï®Î§¡»B¥¦¬{äqÕ¹ÕlN™ÿ»-ÑÙ\ûÙ!ºÆ+ZÅ”g˜èóKÌÿLúÑO BÀ5€ö† /ÝQ¨šÐ驤²¤ÉÖ˜Lé§1Qü ³~ôïýz•bʳÔ1ïIåöžÚô|sešÜ÷× •žŠÜ¸+f,Êkÿ& θé°–x$t8;ûüõ›ö0ÿ3éG?€·hɽ¡˜^«$ƒ†mÄJb©íWrܬÌúÑ¿y¾r@)é^$ÓË- Žp>t.6_*•'„ÊL….^“{“+²jþŽÊ¼ùöùï¨ìóÏ.¡‡ë™ÿ™ô£Ÿ@€-êÒŸ˜y ©wˆ¶bhNŽÒ@Óý¥Ê ê'Êa'è_8Á«ñìSÚïu™¹¸«TŸ,lÓ°þ˜VãŸ*ÕLJàøY©[žù ÿ |ʵÁ}þ_ú$pQ3àÙ'X_ÁüÏ ý ¬0¤m­¼bŽ)ÍÓ@ýóõFezÎN›‡œX/ýmÖþÏ>þbÀs‡Iƒß‘ŠoþVp¦`åÙÁrúá÷OÆ„Ên%ãqy¾ï¸ÜV–±àñH®ýƒLîóW–ûç[“˜ÿ™ô£ŸÀ¬Pó0¦xÊ)œÊò$ci}N?uTI"Õ½LmêG0à—’nEŠñÎ=ÊJÞ—ù»{å½éXzúc9Þ°Y¢ñP¹U‰HTšjvÊmOiz¯ýP,á™\ûéÅZä‘Ð>Åøç"ׇˆ 3ÿkK?ú X`Nþ”»;Ûx½iùŽ’ö©›îú|™W^ƒbÐixý÷o]UÌvîRÒ³H¦Ïþ,8Ú7oÍp¥ßóè¡r»bÍi½:ðÙ¸¡é3~• oõ‰¿N1Ú9ŒzÈüý\è®Ôöø˜Ó¶VÔAìØôœ³½@m73sÂhlPmëGÿùãû£ûLèÿ†ÌZõg΀<»ÒïŽPùwuà£y+Rjþë?-Ö®ýƒôÙä•À9í·þ¹O¤åó¿¶ô£Ÿ =p  eYú Ô“:Ó 77sJéd‰¶H¹¦Ä®ýû·¬U vþ0yØ;R±¥[Κ>€MKäNó¡ àÆ€IóR´LæÚ?H=Ö ¯O(Æ?ˆ4îgþ÷èç W°À0¨Í)œùêËÐTŒ?KÛÿ£ß'jºäUÍÊðÐÿÍ¢rÅXç%Ý^“©>ª}½sÆô¬97Bêž’¤$…*œü”»#§¹6ÿ÷”ØÜÔ™>«Ú+¡Ýʵ~yHøñWÌÿÚÔ~¶h Ÿi°kMÅðúÖç•Æ`{¹ÉËÿbjhÆïƒþŸRLu~RÚ«HÊ+~'Õ'úeÝüüYñ°P…Yɨ%Í[÷Éß„v¡éScü>÷Hø{Ÿ®(f:O Þ[Ìüý6ý¬P! P–9ºWÔr¶—Èð^%´¦9T³ë4/ƒBÿ¼Q½3ߌðâ|€Œ?€ÖþüѧBuŽŠ7¶¼8(°ÿXg×þžãYï“ÀYÅDç9Á»3™ÿµ©ý©€€†×(ûŒ´S@ i¡ñçÛ_cn.ö¿•ÏCYÿ_*&º0˜2ú=ù|[´?€¯¯L‘‡kBuΊܪ—Ó«_iþï|T,‘™üÃZê‘àeŸ¡p³”ù_[úÑO @@à´ihé¢r刣TÒôœ½Qè ¢]m~ôŸ·(ƹÀèV$ÓÊ~)Uû¤Åøûü¯>;,"I¡¨à™KR_ü6€¦R®ý÷XU^ íò)ûü !³"ÌÿÐÏ5€® à@ýªŽV-ë1ê¡èaH•ûG û‡ŒÉä+¯,Aÿû7Ó\˜”öí"3ªºr>¤hŸ¿GNÞÿFb‰¨PÔË•ŒÅ¥eç!¹3ðçÔ{F‹5—ßþƒ *<ùÚ+‹ŠY.P¬ðæ?ûôÇc1ó@7B=ÍSAõOã{Zqú™ÊÒ'»~ô×9¦˜åÂf‚÷-™»ù¯®LÀ¢SÊÞ[‹%h5 E½ªâþ€<[·En øDüeƒÚmü¢k½¨U–û8‘çW™ÿ¡ŸkÛ€1ñ³=ÖŽ½Dæç W“®ò04"%IÔššùg£¿õï÷|§˜äÎÁ”1ïIåöžŽ@MÝgò$xG(ª=eÕ7H|óLǦÀZâ•ÐÅøw"M?2ÿ³ëG?€CØ ÿm€íÜŸcÙ1¤•Ú’ ˜ú>ý{™—¡׿¥­&JºÉôò_Ë‚#œfVŸÎ>ªÃ•¼yNbËÆM€Ué‘ðVŸøë~2Àyº“ùúí¯#P 0¥hZºçô}ú%í\d^Ú¤7=ÉDÿ׋ËmFJû½.3w•ê“ÿ3}‹kÈáúuÜçO¥®bQIûNb•ƒmæØçïkcŸ?„~ÁüÏþ:ô(îþTÓ<½ù^«6CÃhó=úÒ#åäS»~ô¯üG±Áä!o˼šn‚ù…ï®Î¦ð}¡¨tT²ù±Ä·.Ì/DWz%ø#ËýMï/cþ‡þ—ÿ&P! P’<ÃãNR=SÓ1œêéâ´Q%Í´œ$”výè—ª’# e%ïËü=½:¡ñ… ÆÊæ³BQ™¨äÝË_3¹_°z%´ã¯¬¯`þ‡~»~V(ƒÊ˜Ð9]b¤½Ö]#sÑ, ßßö:ô—ú“ %=‹dúìßHõѾÈs­_<ŠÊh%â’8³Gb †uã • ë•À'·§2ÿC+î¶€y/‘ùDQËr2`•ÆbØÊ2 µÁ™—8){˜Ð?±Ï;í0Â0~À2kÕŸ9 PáZ?*W*Ø"‰Ýk$Vá-Pó ‘õ> œk†ÀͱÌÿìúÑO @À5€z§'{Ú‰¡ú÷é¡ãï­gôZš]a˜4ì]©ø¶{_øòòDy¸.•K•¼Câë¦ñk…W‚ÇYîïŽÁbEBÌÿÐÿòkTX`Ǽ‡§íDQo"zŠh~½)É´'†Ž÷7™¯CAÿû7;`‚¡¤Ûk2­ì—Ru€kó™åg|rþÑNI&BQ9Yɤ$Δ؂áyl|ÁªöJh÷@ñ_툆hè)ó¿VÐoq€CX -÷qzX‡ÓdPýü¡-Gj ª¾ý×ÏL†Ò^ER^ñ;©>Þ/ÏÌ/l»>OV£PT¾l ˆïX!±yl È»kýj|¸œ  Vàó?ôs €SÔý5ú’§×yØ0$ŒÊkõÇÍMCo8è?shG 0Lô¼%sÖý%/Œ/§û"õ-„¢ò±’ W%¾fJ¾`öùŸMårˆ´œeþ÷è'p [̃Ïýá#úgéÏ«i`ëczCÒ¿'úåÐÖMi0Â0eô{R¹­GN_N÷ÿø?§û'8ÝŸÊ÷J$¸- ‡±–x%xãŸ"‡˜ÿµþý¨C@àô®M穜Þ´ï¼>[inÆÇÑ¿cât™`è^$Ÿ~ú+©:Èù¹´Üß}*UPhnÝàÀªòHhW:÷ùCèÉ6æèw} ° õo%]S8'êMIÁYã°éµýý_/.O³†Ò¾¯ËÌÅ]e᜽åþcYîO|%o_”øÊ Ù4¿ìóÿÚ+‹é6À~üó?ôs € Ø 5 ýßZš¨`·ë%ôïgýkg—fÈÃDï[2wó_3h|aqí9Þ°Yâ K(ªST".‰“Û%V9(ƒæ¢k¼¨ÍÔr=XÅüýØNØà¼9è G_ä<µTRI-©ÔSÅŸëGÿ’Ƀ2l„¡¬ä}™¿»WšÍ/ÔÔ}*á{BQ±’M$þÕÜ4_°–x$tãŸi‚ ÕÌÿÐOàà³7C:§6 }€ë¯×ÓO)uØ Ð_QÜ7 &JºÉôò_KõѾ)7¾Üé?ðÅþ’ŠêÜ•”äåc[4*ÅÆ¬J¯„·úÄ_—  Á»³˜ÿ¡Ÿ-N! hÇ@t¹HOöœp÷ Í™NôO÷vÍ¢†ñ¾!³–ýQžä|€T°óf•„b-BQÔKláÀùÒ' Ù4À¸=…ùú¹ ]°ÀE*¨à´©¸ÿlcÓЛ‚ù³Ð?±Ï;9`„aòз¥¢¦›Kã ë/K}Ëy¡(Ê\Éú:‰¯šèÒø‚µÒ#Á,÷Ï7Ç2ÿC?[\BÀ5½S^ïöúý¾Qý;)Ïk£? ä †’n¯ÉÔ Èü½œàü¿þr¸~X‰ˆPå b–$Ž}Ç!íÀZè‘Ð>Œn1D¬h„ùú B@ x=™KMÒ?Gm î÷#¡ÿÙÃ{9h„¡¤W)¯øTg[À«øúÊyªŠ¢Ú_É' ßðÙ«Œ/Tz$ü­WWrÑC4ÔÄüýN! PЂ²7Çõf\ü,ý9ô×_¿œÃF& xSf­ú³TŸ|ÙøÂ’Ó—Ó¿—d2!Eu ’IIœ?(±ª¡6ó ‘õ> œã·þ¹L4xùúÝŸÜ Bý5îqõœ'¡ÐõÌñ<0Â0yØ;Rñ]wÌÿ¿øæÊ4i ߊ¢RWÉæÇÿr6Æÿ_X+¼ìóÏ"Ïë˜ÿ¡¿C+€€-zê§¿Çý5 .‚úôŸ;²;_L0t+’ie¿”ªƒ½;¥ñ_zúc~ëOQi­ÖÕ †uNã_å‘Ð.Ÿø¯æ‹†HËYæèOalp: uÜ7­4§‘è?±«&ÏŒ0”ôî|çÔÔ}&Í‘‡BQTú+ÙòTâ_Ïë<æ¿Â#‘Ÿ.盆HÓqæèOiî÷ ¥ËJa“Bÿo×穆 ž7eκ¿´ñ_~Æ'ç픤$…¢¨ÌVòê)‰-YÐæ?ºÆ+Ó,÷ÏWÂÏö3ÿûè·T,+3{€,Ëí£j"èßµiIža˜2æ=©ÜÞ£àÌÿÖk³%`5 EQY,£Äk* o¹ÿ¯„Ž`üóðÓíÌÿП–@À-úói|Þ¤Ü'—èÿnż0ÁPÒ½H>ýôW²àPŸBØëÿŸßú‹$…¢¨Ü¨äåcÊMù³Ï?¼} øë ÁüCøq ó?ô8€ ËtÜŸZª“ú«MÐ/_VV@FJû¾.3wÍÛk¿¾R&Í‘BQT.ž ðDâ›gæï>ÿ/}¸XHB72ÿC?€”´0cébö›#ú×Î.-@# }oËÜ/þš7Æqm9Þ°YɸP•ÕLHâäv‰Uʯ}þ§ ÑCèÁJæè'p @N‘µûGÑ¿|ê°6ÂPVò¾ÌßÓ+§ÍÿÆ‹ãäIð–P•?•|Ú ñµe¹½Ü‘GBûXê_Èï-dþ‡~ç~£ÑAÿ‚Ò¿¸ †’E2½üײàhß3ÿÊ;+$–ˆ EQyXqK‡¾’X…7·Œ¥WÂ[}â¯+t Á»ó˜ÿ¡Ÿ =Ìѳ“a˜Ðÿ ™µì²ðd¿¬›ÿÕç†IÃó‹BQTþWòîe‰-.Î óÙä•À9~ëßYÜ)g.í€`º§k'3Â0iè;R±¥[ÖÌÿ–ºé´š…¢¨ªÐs‰=/kÆßZá•à Œg#pkr!ÎÍ€€¢‘Hš>ý“û½ß M0”t{M¦Nø@æïíÉ%ÿÿ9è/™LEQXÉä‹+|™3þÕ í(þ«ÑCàÆ'ÌÿÐOà €Âo6º&ôG#a)éÖ¥a(éÕEÊ+~'Õ'Ò»-`åÙÁr·åœPUø•¬¯Kÿ–€Ï½©ñIàr§þ­?\ÎüýN PºÞ ò¾¡ ?"Ï›žýÇŒð†ÌZõç4-ùÿL‚V“PÕɶ|57=ûü×ûØçÿÅŠ„˜ÿ¡?£° Éè€*?Ïùw@ÿÓ ?3“G½+ŸÿÐ#eæÿhýI°äŸ¢:g%’8øEê–û/õHðÆ~N4ÔÈüý® PšI^î2¾ý 7®ü¿ èV$ÓÊ~) öqmüÕö— w EQTâÂ!‰}>нñ¯òJh—}þÐ&Ñà}æè'Ð p<Èô×éI óÿg¸™ ÿúùSFPÚ·‹Ì¨êÚîó–öÈæ3BQõS%ï\’Ø‚aí3ÿžûü/™Í@Ô“ùú;[Ü5„ÌÓ¡Ÿþ‹?TM ÀDï›2wó_Þï?\žo EQ”½’O$¶lœ#ó]ã•@­nþ"-—˜ÿ¡¿Ýp`$Ò¾A®'úkÍï×Bûs._þÚý?86SƼ'•Û{ÍÿÆ‹ã$`5 EQ”±üM_=ɼ܉GB‡ÙçÎ 7Õ2ÿC¦ à@ý=z£Ê<è?ºíËv@€’îE2½üײàÈÏÏX{~”ø£O…¢(ÊQ°¢Ô¶Ïß#áíÅ_×>ói<Âüý*îºö¼òo'{””äÒy*Šþ½_¯reJû½.3w•ê“/öü7GEQ”ÓJ6=|q&@…G"_ú$pÁÝoýÂO÷0ÿC¿Û8Ðø¸ò|–&1,Ò@ÿöu :d& ~Gjë¶ý“½³înäG¢è[ffff¦3333333ApÆŒqb渓íN/ÍöFó"…œô­sn ín»þP½w¤’!‚páÌÓ¦÷&ÂÖÆ`áæáÿä ÁÀptîû¢×ý ŽÏ²!éF’ÿ=—Ÿ±&ñpÂÏ>g¦ßyÇ$fgÍp00A*–––LµZ5™LÚ´“‡¯Iü çïdþGþ^+€-z ú‡uë4:òø>ö"Bþ·_t""ÖÄÍœd¦ß}÷?ä³Y³¼¼l‚ ¢±wï^Ól6M6›5™L&d>uÓšÄÀ r ó?ò_³lП[¡p}¾KqøwQ@þ7}äšÄÀ > ÿý™™œ4s¥’!‚øwôû}S(BÑ¿?…ô+kƒòuÌÿÈŸ- ÿ£DFz°ïç»»€ò»Øó ÿkO=È[ø÷ãOš©wÞ EÿJìž™1­fÓßX\\4åry?Ñ!Üp°·øè—¯bþGþ¬``èA©]=½·ÇáÚ:½îä‚’ÿ•ÇÿÁ[ü\xø/B¡¯HíÞmô ˆ8E¸¨^¯‡"_QKžã-þú¥K˜ÿ‘?€ 2€t§Ï}èB£±{1 ‰|§âcÿ›ü/9êWÞâà¦óNˆˆ};3a€={ö‚ vö>ÿv»mr¹\DèÛ™Oßæ-þz…ó™ÿ‘ÿZ À ÀH4;‘Aë½ôÇÕÔùD ÿ ù±·øxä¶k#B_3;9i*CD\öùkŠéç¼Å@¿pó?òÇP``D©­©†KwN—½_72ÃîÚ»›ŽÈßœõ§o{‹?€WŸ{ÊIüGût;CÄö L¥R±|M.3í-þúùS ó?òhè+Ä •KŒ´»§±??úÛZPÜKò?í7_ñ~G}ÿ#–€n¤‰°IAÛ.Â-=–}þδó½ìñÌÿÈŸ áºéÂ"¶x¶í5ûuýYîî"ù›“~ö/ñpÎß~ Ä½S€R±¸cûûü5ä©^â —9Šùù{€À1€z°îC8Ò-´wÏ·–ÿÙ§äì?î%þ®<éoBØ»3;5e굚!b|c8šR©$½;Õä%^â —>„ùùc(00V]8ô@ÖƒW¿Çî Š"' ‘pÉÿˆo½ßKüÜráÉBÐû“˜5½^Ï1^ûü„÷g>u³—øè¥þjæäO'08Чk§Þdoþár–©ú;òüÖ<Éq0ð÷ßp™£°w'›J™¥ 0Alírÿf³i²Ù¬ƒ wg.ý€ŸHþžùù³@€pÀ®œ«]f$¢8—T½?úá\ºòï6Yðæéûïpôþý …}*ı¹ÑívM>ŸwòþÓÏ#dÁ›`8`þGþ €ÈàÑ­aˆj:âr4‰8¢D|'k3òoT+Yðæµçžöôþìšž6ÍFñážÌQ.—=„¼?ùÌ»Yðf4ì0ÿ# O0hèRXVã6:,}üHôoÿȾ˜EÈ‚‡}ó½fêí·…h߸þƒ~ß±þ±¼¼lªÕªëEÊt¿CÌ‚£Aùùc8€À1€®.Ÿí¾èß•Tî£^zâÕð„üK©]ˆYðâÄŸ}Nõ fbÂä2³´´d‚XŸ}þ­VKë·ñ´‡"fÁ‹`P1ÌÿÈ@ƒ@@áðY÷©e=bËb Aí ÷‘ü³³ˆYð✿ý@ˆôÍavrÒ,T*† ïOÜ( BœoäɈYðbÔË3ÿ‹yþKA€ ÀÀ° p1håÀ¶GbÄö"%–99ì…Š\'ÿÄ»¯!fÁ‹ËŽýƒç›ËîéiÓnµ A«Ñhdæææ„(ß\j©ó³àŨ›bþGþ ¶88‚r¹ßÙ ú;èf%ö{Eñ ÿé7^@Ì‚מ~¸å[C:‘›—a={ö˜z½.ÄøÖ°º1 ^,vf˜ÿ‘?€€ÑAíêΩ}EQWpå#@œ ”ÝŒæ¢_'ÿw^x1 ^ÜzÑ)[(ôõ±ùl69Aüï>ÿv»-öùo-ó©›³àÅbë]æäp 00"NšÝ9Ô˃lN û²¡È Ï. xù›7Ÿ~1 ^ÜsÍ…BˆI€ùyc‚0ƒÁÀ‹E!À·ž¹ô=ž0Þ`þGþ«€zÙ—ÝŒ\Gñ¹‘ÿmA:„ºó)ù¿ôè]ˆYðâ‘Û®#±¯ ìv:† âA˜J¥"„÷øPJ?Š˜/†—˜ÿ‘?€€¶¦GP8rzß‘rý¢¿íK„Dѱ¸È5òþ[³àÅ3Ü%„÷ø‘N&Í(Fýöù7 “Íf…è/Š™ç³àŰþó?òÇ`p À Ë"ׄƒ¨pˆíµ¯H.+‰ïb[Î4 !ÿ'ï¸1 ^¼üÔcBpo€R¡@bGG·Û5ù|^ˆíñ$Ÿ~ÓSÀ“ÌÿÈ@€À 1°äÙ›Ú•Ku¬û’lû”Ä¥‘ׄIþÝrb¼xû•—„Øof§¦L½V3±“b8šR©$Döx“ÍÌ fÁ‹aõaæäà€¤£.žhPqE!÷E¾oô>òøÆ‹œ…Àaß|¯™zçm!²·M€°9AlçXZZ2ÕjUˆëíBÚt¿sƒ…˜ÿ‘?€€Sl4õÑûcoþaíîbß·}ÝVLÄÒ"á‚’¿yàÚóœÅÀ±?ú¤ÖÛ\&Š(‚`ŸÿÖÓNä!à>æä Á`€m0þóºpÙ"ÿÛº|êsAW¿/Èî:7; ÈßÜ{åYÎâàä_EêíÉÌädØÀìÝkbÜ£×ë™B¡ Äôö¤•<ÊCÀÝÌÿÈ@€€ ‹C´0h7NŸjìÑ"bw&E1ˆ8Ž–åDä÷e§9‹?€3ÿøm!¦·7»¦§M³Ñ01ޱ¸¸hÊå²ÑÛ›fògñ0œ¿ƒùùc(08 D-’Ρ}_ø ['Ñ(zÿ^ִ³Èÿ΋OrçòÓMåô ˆååeS«Õ„xÞÔ“§;‹?€~åVæä À`@Ôу^b½H/?Z]ñpûmù~äÛùÇ9‹?€‹þíÎúúØ@ú[{÷î5ívÛär¹PÇZò\gñ0˜»‰ùùc ‹+hsÅu±Iq$ÈJŸ·rQ{‘X¡±ùß|îÑÎâàÊ“þ ñíP)— AlfôûýÈ>ÿxPM^â,þåë™ÿ‘?€€&€ºy‡e‹žz¹ŽÍt;NDž½Y ù›Î<ÌYü\wæQ¡(Ž#»gfL»Õ2±‘1ÌÜÜ\(†ãÈBêj×0ÿ# @t©=<ª9‡åýÖAuòìG»‰ºkh´) ù›kO=ÈYüÜ|þ‰¡Ž3éD"lÆFë}¬_½^Epœ™OÝè,þú¥+˜ÿ‘?€€-!–Á«nôÿha‰<Û:¨WåRöBcÿ–ÿÿ ù_}âŸÅÀ—Ÿ-r|úä³ÙP´ûü×Jêv—3ÿ#'0Ø`°NKr,÷”û}¶>®$DËkÿ„ü¯8î÷Îâà¾ë.ÞOÃìÔ”Y˜Ÿ7ááIÅbq? åÔ}Îâ _¼„ùùc8€@ÀèZyÐ[DË^"QTìK}Fâ¹–gEò±•Bþ—ýkgñðð­× ü-Çv;C«‰ Ìüüü ÊéGÅÀ xó?òÇ8 ¬ˆ ÑeS›;}ž^ê3¹îg² û^¦@þþ3gñðä=·@C:™ ÇAØöù7 “Íf-Jé§Å@¿p®aþGþ V¨¥>‘,ܹÈ5Ëó\ Žn6²êcE¢÷“ÿ‡þÄYü<û𽡯 ,—Jû6w‚øwt:“Ïç…†BúEgñÐËŸÅüü1ôX¹»¦KAVº&É@<ßRT,Ÿeq¥³HþçþíûÎâàÅÇZ¥†]SS¦^«"Þ1M©TZ¥†BæUgñÐÏÎüü1‘%ŠCwNq¤ˆ}ŸOÄ´‹«i)>ÖûÉÿ¬?}ÛYü¼òôãb˜þÃÁÀñŠ¥¥%S­V0ä3o9‹?€~þTæä À` €ÅQ“îœ:¶CK*oQ€ÔÒ#ûY¦äÆï¿î,þ^ñ9O! ÙT*…ÄÎ?Ö¯Ùl²Ïß“\ú]gñÐËÄüü1$.,âÎpêÞi{]/U²»ÈõÈïó#ÿSýegñðö«/¯QÓ`®T2ÄÎŒ^¯g …Â0dÓ3Îâ —=žùùc00lKä>›1Ø#ÏÓÏ…Ëú]-Dïa"ÿ“~ñygñ0ñæ›ë „a×ô´i·Z†Ø±¸¸hÊåò:`ÈfÎâ —9šùù;€@Ýt•%Ľqˆv £ÿëf#ò7'üäSNÂà°o¼×L½óÎ: aHíÞmôض±¼¼ljµÚ:‹`è&~ë(àæäp 00DñÐ{yô’!QÄçGþ—K¸ ú<ò7ÇþèãNâàÈï|pD0ÌLL˜|6žOlŸ}þívÛär¹ ÀÐMþÑM¤cþGþ VX\BË`Tƒ×¡qˆì^jßÏäRD~äô÷>ì$þŽùÁÇ6PÃì䤩”ˆïè÷ûìóß`Ú‰¿:‰?€^ú`æä À @tÐD\Bé®ÏàMA¬ØŸ¢‹#ùñ­÷;‰?€~öÙM°{fÆt:CŒWA`*•Ê&`h%q½Ô_™ÿ‘?€À²äG,rs m÷©&V§0ˆ\@Ä=ÿÅáÐö÷8‰?€“õ¥MÂN$ÂærÄ–F¸5£^¯o¢†VòGbþGþ w§Pü¶Þ¯­þ< =ádÆ;ÿA¿ç,þNÿÝ7¶@Ó ˜ÏoMöù‡+1òùü& `h&u€Éß3ÿ# €U‚À 9X½‡Hdýšê*„ˆ"ï‰{þ½NÛYüœõçïm‘†]SS¦^«bS"<™¡X,n™Æ8ÉQü–ùùcH0X £("vwOÿ¨F"¾ IœÎ8çßm5ÅÀ9ûá aHÌΚ^·kˆÛç¿°°°ÅÉSœ ÀhqÈü/®ùch00¬{iT‡PuˆÃ’#Çå=ÊqtÚ÷ü»Í†³ø8ÿŸŽ‰†l*e–‚Àë·Ï¿Ùlšl6;êÉÓ| `0ÿ‹mþ í¨é¥=nS‡ÏDÅ{ãž§Qsö‹1Âô( û6«Â?ºÝ.ûüÇŒFêLgñ Ìÿbž?€€-z_xç^]´|>ËÁ=Œ{þ­zÕYü\tä¯ÇPîéiÓl4 áá åry 0Ô“ç8‹?€Ñ°Ïü/Öùch0Ø yˆAï³ÏGßå@ŽË‘âž»¶à,þ.9æwc.†é0 „ì%–––LµZc µäy0ºÌÿâ™ÿº€@@=ðô ÍBÔÑ"º ¹ϸçßÄ.=îã.‚abÂä2™Päÿ¬_«Õ2¹\nÌ0ÔRx@Àè0ÿ‹kþ owN8‡Ò!Ôƒ:úºvÿ<Þ÷ü›Õгø¸âÄ¿n! 3““¦R.âŸÑï÷M¡PØ6àbgñ0t˜ÿÅ9 +\št(§Qü¶>W=Ï¥8Џç_ŸŸsW|Ð6°{zÚ´[-×Ffnnn› `¨&/u‹ƒ6ó¿8çïl[üht1p}æp(Š’¸óüëóegñpÍi‡mS! éD"lz—X^^6µZm› `XH^î! Éü/æùch0X ]B‡A+EÿãGô5G4îù׿ŠÎâàº3ŽÜæB˜cóÙlxæýNÞçßn·Ùç¿ÍYH]å! Áü/Þùch0è ÑΛ~=øçõU/1 Äs½Fñ=â”­\p7œ}ÌÂ0;öýØçp­ æ±Î@ƒÀË "×ê¶YîstøÜ¯E ‡ Nfó_(åÅÀM翃„0ìž™1ÝNÇl÷‚ÀT*•$€a>u‡ €:ó¿øæ Á`€{¹" ÇÈâFîïóÛ›÷üŠYgñpó'í@! ™T*ÑÛ,­ ìóß©ÀMÎâ`4¨1ÿ‹sþ VX¡ïYœQ'QV'P£?;îùW gñpË…§ì`!L€b>Šêí°Ï¿Óé˜|>¿c0ÀÍ0ªÌÿ✿£ôÐŽ›ºî_¬¢ÏN`d‰’øŽ–gÄ<ÿJ.å,þn½øÔ.„avjÊÔk53®1M©TÚá*àÁ¨¿Àü/æùch0è »‰‡îËĽÁÚŸ%œK]lâžÿxpëEq!1;kz½ÞXíó_XXˆÆ¸ÅYüýyæñÎ@ƒ€ Ý·‘[Q‰ÝÁS?ß֜ĩÛiôï¸ç_Î$œÅÀm—œ3! ÙTÊ,Á–.÷o6›&›ÍÆJcÜê,þFý ó¿8ç ÀÀ°tï ,î›\‚ãZ0Š‘úÑâèûMÜó/¥fÅÀí—žS!L€R¡°O›ÍŒn·Ë>ÿØ·9‹?€ ?Çü/Îùc00ÔÀÓM¿×V("ÿ;¹•ª0EÅ)¶ùS»œ„ìšž6ÍFÃlt,î[¥T.; FÀXì•™ÿÅ9 08@ LG·NÝã¾OI/)ŠÞ'_{þ…䌳ø¸ã²3C!ôôûf½cyyÙT«Uƒ†Jêv%æ±Î@ƒ€ ‰:j {‰<Ëz-úÛ¡XÞëüó‰igñpçågÿGÛr™ŒYZZZ—}þ­VËär¹PüTRwx@À(2ÿ‹oþ`p €Í tíÈ©œGËR"á$jÄ'K!‹yþ¹]“ÎâàÎ+Îù039i*å²ñ~¿o …Âÿˆ?€JúNæ1Ï@ƒÀQ ,ׄK'–©‚a+\ÑÏ ‘K‹,Ï{þÙ]Îâà. °°{fÆ´[-³ÚFfnnÎ"à.gñ°ØÍ3ÿ‹kþ _·-ði¢„ÈgëbáPÀâfæ]gñp×UçP¤‰°‰Ÿ-öìÙcêõúÅÀ\ ÜYìd™ÿÅ; ÇútítrúÜ‹’¾'°\WD£÷Æ=ÿôôÛÎâàî«Î_•úä³ÙPìï¿Ï¿Ýn³ÏViÜí,þ»YæñÎÀ VØ–Y^â¾ H|¾KAñ*6qÏ?…܃ÌNNš…ùùpŸ±X\µø˜Kßë,þ;)æ1Ï@ƒ€àÚ„#ºTGñ|Ñ ÄÍt*VqÏ?=õÖfFÀp½ù´9ô¾sœ(ÔJÌÿâš?+4ˆbàɈgˆçê÷EJÇÆ&qÏ?à Ó-€ðààc”™ÿÅ<0X`wæô}ºðèg¨kÂqÔÇÄ4ÿ±o4 €^ÊYü«eæñÎ@€@@‡Ï}Y÷@ׯëýFú¹±ÎŒŽ €ÞÀwÊõ ó¿ç¿4a00Vïâ¹¹€!+þ­«×"ÿû·¸çŸÛ5é,þîÄø;{çø%ɲÅÑìÙ¶mû cÛ¶m÷ôضm£]“—}YsÖ©_èʧÜö :³²Ï‡8+~{EFÀ¿A¬>¸™@ )€ù_±ëG@p @G…{E[Åø’£¸Ñ,zý×/œ‡?€¥“‡ç°  Üytù_ëGh¬1lçtƒ°Ÿy÷ÉçeæºØÿ‹^ÿ $°dÒÐÜÃ" VØwî3ÿ+vý€A§ž¹K~ô Ït“I2—™ø®¢ÖóÒ¹pøXoù“­¥¸õß¿u=þæê{XÀ¢½ë ˜ÿ»~€€°ÍÁ°ÊðÌýözo@›!–#™ʈ¦Tèúܾs†÷È=,`Áž5áðÐÐÒÄü¯¨õ³@ƒ@dCÍ5ÊØÙÁì?ؽô Dl=~3*xýHbÖ®¹‡E@ÌÛ³:þ[š˜ÿº~€À Ý(ô¡o“ÙÁþæ{Ìuns;“ŠßUXÒâÖÿèÞípø˜1¨Sîa³w­L €€`þWìú+ü%8¶aTþÜvkþTSÒÆÞ¯ÿv gÁëG@Óú¿{XÀÌ+Âá ¹µ™ù_ÁëGhl¨Þ­)£®ñŽípÞÍ3 4.s½}¶1•E¯¿áÁÝpø˜Ò÷¹ÜÃ" fì\-­ÍÌÿ ^?@ƒ@˜¤–õdºÞoö3m0íßsH˹þ†‡÷Ãá`b¯¿æ0uÇÒpøhmkeþWÔúY A Ìà´›rØAk7¨ýûí 7Ÿ›ï6×Ûküæâ4›‚×߈€&ôøSîa“·/‡?€¶Ró¿"×Ð XÙAï ºÀn¡Æ"Z»§wôô7 q®q–7¹ ¬èõ7>z@ …0ãºþŽ@ ȉÛh!L©ÔÆü¯Àõ#4VøÞR{6‹Þ½zGOoSñ]¦¯©”)pýMH`L§ßh¹3~ë-„yRjcþWðú{¸–-SGˆØc¶¿‹h¥w‰ì³íuÕÚJwó“¢×ßÜð˜@ aF½ø - wÆl™K …0Y©Äü¯Øõ#$Vèc8œjÉœjL¤þ[¿Ïd-¤÷3÷w)zý- Z3âùŸh¹3jó-„É ?ÿ£~€À u\‡ßì ÷›„¸Öüílú!ž¥!1·þÖæ&-„öÌ ´€€Ü±i6BtzæÔP ØÐô¾-ô‹µþR$³Qˆ8ªÄ®ß@*þE¯¿ÔÖJ …0ƒÿô- w†l˜A¨…]VfþGý€J ®áÓÆÐl½q‡7¸Õî¡Öz¿‹h>v9õwúε¢ÿï¾F ä΀ú©„ZÑ}í8æÔ¨©¥?þ Œb°ýNóÝÂúU2Ö€ºÿ¦þ.ßû0¡BôùÕ ´€€ÜéS7‘P !z®ÏüúU‚`€ ¢Idb)‘±|ÆŠMH„õ³ÍN[Lû<êïþ£j!DŸ|Š@ ÈkÇj!Dïõ™ÿQ?@‚`;ðlÓטAèï ª6q¸nþ½Yó]Ôßó§Ÿ"ÔBˆ®?ø(;]׌!ÔBˆ~õS˜ÿQ?@€`€;èí ¶ï ™û…¹sµ×Ôñ%¾ù´×8g”Rï_|žP !:}çƒí§Ž#ÔrãòÛtZ=ŠP !nœÆüú+¬UsÞ¯qvñ4÷‡6ñ¯«¶ yËžþÛi6Ôßï7_&ÔB˜‡j¹qñò%-„²ió?êG@°Àìš©e6Ö¼ÙFâ/2Ïvº>:¤ª÷Œláþ¿ÿ:ÂÝ¿—P ȳÏh!̰-³˜ÿQ? jè 4çœMÏš¥9Á @*-ïñ-`•MæÓœ¨ÈŸ¿M …0wm'Ôrãä…3Z3rËæÔ¨+Ô1!¶aøƒXÞŸ‰ç¸–O/ ò‹{õûÛ÷ ´fßÖ„Z@@n;w’@ aÆlŸÏüúAhónNùz±œG6 ÿý"¿y¸ÿÎlïDQÿ¨çB …0»6®Ë-(àà™cáð0nÇæÔ¨ {”Q‚è÷süw‹¼ Cl3ñ– ùÏQLJ¸Ç’Pÿ˜—~Û×­È-(`ß©Ãáð0qçbæÔ j©OùsƒŒ¾ô£yŽ}¾¶…öù¾m¤þñ›V,È-(`ljýáð0e÷RæÔ 8°²)´èã:ìà_lX¢›Šþ½ÜgRÿÄ?€º…3r Š€Ø|lw8üLÛ³œùõ#$^×Ù]T-÷±×«eIæûô’$ñûë H¨¿}J¯?‡ÃÀª™ãs Š€Xwh[8üÌÜ»’ùõ#€w.¨ùÌ1ú=çû2µ©}޽¶Œ×„¤9¤þéýž ‡?€%“‡å°òÀ¦pø˜³5ó?êGT€0ƒÌû·wú¹X’ä÷¬]݈ôw–‘¿¯øÛ6 ê_8ºW8üŒïöÇÜ‚" &m[ËŽnbþGý€J Òî9…˜k¹¶†ê¹öKùYâ=(ßDRÿÒñýÂá`Ô‹¿È-(`ì–yáð°òØfæÔ¨ðê+¯xÎÚ·Ø{=ú}#±¨´€â#Q‡yFQë_6iP8ü ýë÷s Š€¾iV8ü¬9±ùõ#$VØèhaC¢Œ2‚ÆðÉëmMî»CÔ¿rê°pøð»¯ç0°~Z8üÔÜÁüú@Y¿2ÚÆé¿C†OEÝ ô½Ô¿fÆÈpøèñÓÏäÐ{Ý„pøXjó?êGT€(•Ìàr½>ÃSRqm`ÇÐÀ'¢Y¼þº9cÃá ów>Ð~êØÑš‡D@\~›N«F…ÃÀ¦3{˜ÿQ?@‚à€ãfî 4qmÜ,ê]BýåKÔ_¿`RR8²wwÍC" Î\8—þ6ŸÝÇüú+Bf-K/(>uƒ°ÖÐ ŸSøú7/‘þön^_ó€#gO$…?€ç}þGýY†¨ð¤TúÇÀ*•ôqq3¨¶ð¡]J Ò(¾þ«æ%…?€­«—Ô<$`Ï©ƒIá`北Ìÿ¨ @° à›ÒÒY´™‹.r𑼾L`‡Ñ|÷{½þ½ë—%…?€º…3j°ùèî¤ðpèêIæÔ ØàÍÀ¦¶h;g›ƒèÒê¥Iâ>a5 ]ÿ¡-k“ÂÀŠi£k°öÐÖ¤ðpüÚæÔ xàM϶e΀ÿVP/²Ï~HÃ)•œïµ½þc»6&…?€cû×<$`éþú¤ðpúÆæÔ¨ +”KŒLɜש‚iVn³Ï0 ÎRôúOØ‘þf î\ó€9»V%…?€ w®0ÿ£~€À €§±{ö§ŒúÝŸ§±›ZÓY–°Œ¦Î2¯ÿ±Iá`b?×<$`òöÅIáàÊý›Ìÿ¨ @°ÀX¹JV¯ü™X¶cuä½%óo÷R±1ˆ°¯ÿò™ãIá`øs?©yHÀÈM³“ÂÀ­‡·™ÿQ? *œPÝ{=ž±ÓƒWB¿á˜û£†²üo¯Î¢×ëÒÙ¤ðÐçW_ªyH@ߺIIáà^ÃæÔ¨€WÚÚÞpßPÆO5{½¿ÈÜò™ÂP–q,eùïB×÷Æ•¤ðÐé»l?uôhÍ" .]¾ÔÞiÕ¨¤ðð¸¹ùõ#^xÃÙDà \Ó<ÌuÆÜiSè7sl2j§S§A½þÇ÷ï$@€;¶Ô, àÄùÓÉá ¹¥™ùõ#€3 |ûgŠx—H`ÝHüfdžç4ñÜâÖßôøQrøزjIÍ" vŸ<˜þž”JÌÿ¨ @ Ì@7U¼4„ÖèéÏ2mU“sŸUôúK­­Éá`õœI5 ˆ€¨?²#)øt]3†ùõ³@‚`ÀRé 3p­å;qŠ#Gìwø×yÏ‘Q—ÿ=ÔßåûI óÇô«Y@ÀÒýõIá çºñÌÿ¨ @ Jïmhº\þÀvnpSólçßæÙÎûCn“±ŸQÏŸ~:)üLé÷\Í" fîZžþúÕOngþGý¯f@€àÀ×AæX?óqXô4v0[‹¨6)1ŸyM§ õ÷ýÕ“ÂÀðç~R³€€Ñ›ç&…?€A§3ÿ£~ެ¯¼î=ƒ<îCìøéìüiMdwSßrÚfR¾ÆšJêô‡o$…?€n?úDû©cÇjðö2î¤ð0|ËlæÔ¨¯¼žùg…š¿Å2Ç$Ú{2k*íF$Ά%sL«Üé”ú‡?óƒä°Û¦‡C@œ½x.9üŒÙ6ùõs €€xR^àtmòì ÷Þ »úMÆ/H›Oݰ¨Ì‹?Ku §w8`×ÉÉá`ÂÎEÌÿ¨ŸM%@[ÛkÆ¢©s<í€ó—ÙÝ=íf$Úäëçz½‰mVÔ?¾ëo“ÃÀܽ:°üÀ†äð0uÏ2æÔ¨+^«°é†ÿ.‘3H]cè¿÷ð†xïǽÇÚHw÷RêŸÚû/Éá`ôË¿êp8ÀÄm‹’ÃÀ¬}+™ÿQ? ¬öϺ§0FÏo&úÝ#ßð™Ï»h¾ÇXBó{PÿÌÏ'‡?€?ý B‡Àå·é½nBrø˜w`-ó?êg@ €M˯hógf®h,îÙö~ýΑoÅ!vƒêŸ;¼k‡ ÀŽõ«’ƒ! œ>Ú¡ð°øðæÔÏ&€ àíòª0p¾U,•*7 q¨¹×>ßY"d¯ñï³Î.¢þÅcût(üÌÝ79`ñ¾º…?€G71ÿ£~VT€=^5Ñ.rµ±uªQØF ®WÍCkb›¹‡úWLÒ¡ðÐûŸo?yäp8àÒåKí}ê&v(ü¬=±ùõ³@À.Éñwù¬ôßÚÏœeC¦¹x÷; α—®1¤þº9ã:þÞbï¬ÞâHƒ¶ÿG­`k1#0ƒY²Ä–@”xÐø’7Ä Ù@ww⎻e=!.°óæ¨>–åÚí—Jõ430rü®™ ÝÍÔÁSyz¶z9Ñ¥¤3F‹B€ŒšFÚ|íü”ĵ]÷°ÿCü0T3Æ[ÄäÀ-Våüt2‘Q&e‚ï•K”ÏAüõù—4‰>Â|œiÅ"rÿî3òšçDõUª!€ÀÆòÚ_G‹w×Ð÷'óhgn‚&ñÀ­¾ÇØÿ!~*€€S>N²°¹„! ß`A&I(~ÎLeŽ0aÜFÑ1œäˆÿNyŽQ¢€H? qs$YŸ‘û·ÿŸ@ý\ºU_'ŠA€¦ö>ZÛ@‹wÕü‹ûÞ* JL§ˆÂ³F‰?>iÃþñc  Tüõ×G¦\‡_М3È»vÊkåÄ%%ÆÁ¾3ã@"þÇ eª…¡Žä=çóE¿’•>®0À' µõÑŠÃÄ¿´.õ¢jñ@ë¯=Øÿ!~ `xø?ò%>Übgú}„ÅÎ"8‘üS.éñ½CŒc‰ø;ÜEÛ¼ié¼/ÆD¾„¿ë·TVKJ`Ðð ›Ö } ÿ#…´-LïÓAìÿ?ZTÿË,&!¡°®·à¹„ *aG>q)®™ôsÅ÷AüýmXÑ@˜¯ ­tuPˆ|¯¹Ž”žvþ_`PØÐNžûêB_&ðTíÌgÅ¿<ÿû?Ä ÀÀÀ1à#ï°qçq2‹Rpþ&[äÒÔRî™rY’à*Žƒøé÷þ.RŠ>"GYëî@ž³¸rÝwŸÓéØ ~AjI3¹í®eD¾Œn_%­N¾F‘E˜þ/C¯Ÿcÿ‡øQ 0 RÙÎGÎc?aÑs‰b²ÏYñ£p¾¨¬Ÿ#þçüúáÀ&OGòeúüµp0zµ56Ú¡ð0@ÿÀ:|õ¡ ðÕãSJ¡—Sÿ#þxóþ-öˆ€ £ Å0 — ¤Eª¶ÜHqì>~Ô0}”M2ã×ñq"þw¯^Œ‰>¶{;Qà‚/A¯uË|èf]­ _“þCÏÞD½6ŽÒ¶¬ä1ñÀÈÈ0öˆ§¨*þšäŒN©œˆ/ ’ƒð÷rî$ŸŒ˜äÅÜÃÆ‰ø £¯Ñ_Û±ð~δØtß õŸ"¾ ¾¢ÌKì@ð øzùý( û›"n{ªie\…çÇÙ±ø{˰ÿñãuÀ€ tçøò)9®#û3Î ”ú™¸#öH¾4 ñï_9ß…/ðs¡õ:GòœeJ¡/ψݖÀfKþ)>¯Qc¿¿öù!)W(ªÈ0øŸÊìÿ? j€02<ü³P…³DÅWù¬P!áˆÃFd·’O†Êg!~Š]çagâlõr"¿¹Â±~fdÝrºU_gcBÀ@Éÿ&¡äßœø*¡Í×ÎÛ™Çë.aÿ‡ø1@%0P0¸kF%„œ[('!þ^>È?ãž0n#â?µ=ÐN„/ØéãBË }þÓ„¿ë·”sí’ \ %ÿÓÉ’c´#7ÉN0ˆ¿žŽýßø+⇠€ÒU//0y¡*?W":ƒ¥!>¡—0?%F‡Ø¼ðEŸ¿ /v ýwRŸÿô³og(5=x`…ÂÀ=}t4ý!¹íž^‘/Ϩ¢U Qˆù¶NÊí\ìÿÆ_?Z$` `„uöx'oöáÌ9¤B¢PYºÄõ ßñ_8¸Õ†Å/Ø w$ïÙ‚ðŸa–{. Ò¼l+µ¨»ßE+\ÄøÌâq œÖ¥^Ä|æò½"ìÿþñ£@0` 3Ž ØK4ã ÏR –i<ßTùÊÿñ_=mƒÂlõv¢¡Ïß’ÐÍú­ôç¾GôgAøÆÓæôÀ ëqöˆ3ÔÀðû÷ @u’˜ˆÚI£ò³™D"\Ëö+©j‚øsÚða>δb‘ƒ ¸-—à%zª-/±@! `€»M=´îÔMAp[(»k(ðT…å%Ø…-u„ýâÿˆST3>°å5r²¯á¯îÝBMIƒâ/N=e#âÇú…¸9’‡p¬Ÿ5à5בŽÇRGs³X >¡+-乯NÚ–ûÞ* JL§È³6 €AeÇ-ìÿ?ZT->( ÆYã’‚Ü“Ã;“|’]AyZ(ç2ÏCüU™)V.~A¨‡Ðço¥l Äq3 𨵗¶%Ü„µõáùc­OK³r ê{`ÿ‡ø1P 0`†‡ß nãÊ‹÷£””øë4¹U\Ï'5ģ誕 _°ÝÛ™–Îúüm¤ ½¥eFÄ+€€¿ú ½þVNÀ‘BÚš™l¥ÜéoÂþïo? ÀÀ €÷Ì"V×»cšÅ+õòpˆÏUï"þûÕ…V&|A¸¯3­tu žm‰+ý©¡ªb+€´ôÒ–xá¯þ6„ÛîjZ~&›Âòâ­LƒÇOÚ±ÿCü¨P LÉ´Ð$ÇP^ä²;(M!•“Úćø©ùVµ•_9ÊZwòœ%”ûÛp5ÀéØÔÞÔd&± ``ÂjI3yízýmݾJ I¹B‘E˜`-´ÿÞ‹ýâG€J`Àx'œ³)/Rù:á3 Ï‘‰qï?õ4Þ…¸¶6y8‘ïAøÛ !K=©º´âÚ„Àw… ÿv„×Á2Úxù¶0ðôgìÿ? •À€ðVƒ›È_#/ä©Lü”Ž Ñ^~„øé×Þl f‡3.øÂ_~ö—t†Èž0@ÿÀ:WØDú½Ê^°äXmÏN‚ж`ž¾z†ýâG €`ÀøðîÝ[aÑiqê´»“ò¹ŸšŽ ‘cDü/Ÿþ¡mDø9SÐâ¿ûüñWÿOWxPeq!„¶`€†Ý|üAìó¸í©¦•qY^Ám¼}ÿû?Ä@0Pðáã\AõŽžp¶òƒ8´;“ˆäÃ{n c½Îш> ›õÅDm§Æû÷!¸Ut÷Щ¬Gä¾õW‹Ç ZóÓeŠ*‚è¶v•Ä‘û?Ä•À@À¨À-PÃ(F:|Ú“œPä÷²ó(üø÷­˜ ámlñr&ÿ¹ZõKusèÊù$‚èæŠotPࡈzø*¦Í×ÎC€[1•ç°ÿ›xâ‡ðI`Àxÿþ°Ðùd",T6ùÈŽ äDjr&åD‡ø¯÷‚ŸAvú:Óò…èó7a‚èNC=Ä÷`€æŽ>Š>"Þ쮡ïOæÑŽœDñädýeìÿ?ZŒ*^«uå¤ë ’ÛÈß«iq+Ÿo`®œÈ ïÿÙ°â3@¤Ÿ /v ýw(÷75^óœ(áx,µ·´@„ðkéJE y›üh?ྦྷšV%dPD!æÌI7³°ÿCü0Œ†¾\8Ù)Ô>LD¾—_ðrbaâ?`ù4êáH^³!üÍÍšï=© CaØ)·÷ІÓ8ÚÏüóÊi]êE̘f.Ý+Âþoˆ€ ´¼Êt¤òŸñWþã×JÿVÜ/|aá ×3åKˆÿêÉ]åÓÄV/' ˜‡>ÿéD?ë ŠÝAM@ð zú(>¯‘t{ Χߨ"Ú’ŽùÓEöãJìÿÿÄëaˆÀ€À—ùA¥ãg˜Ê$PÙ)“Á$F~ÊQˆs3îçB+] Ègåž (ûJD9 ›¦°¡Cþf˜ÀS9–—‘nfJ[¯cÿ‡øÑ 0Þ¿{÷Ò ²ŒÇ 8‰r2`œÀIE>ñp½HÆ\ø+Ó“Í)~Ñçïæ@³ À-…­Á˨¡ºâ€Mñ°µ—ÂÏaÈŸą̊¢ ÄtŠ,< ±n&êºîaÿ7 âG €Z` à…XÊ#¹‹Ì‚]C¡GIÛ9§rrៅøo•fB¬›MžNä3åþ–ˆ×\G:{€Z?†H‡`Õô Ò¹Â&òØ‹3ý-¯˜2ZŸ–Án ¶bÿ7á=⇠»ðä….þ\¹Ð ÆÂ?—CJfüwBüÔ|«‚Ý„l÷q¦¥ó!ü­Þ‹(/ý „: «¤âv'ýpì„¶p´¶e&C¸›Žßz±ÿCü0ŒZ†Ø&—ãÈCAÄ+;€á9Üw4ȨÄß×úÂÝ„û:Ó‹H÷„µµ¾!ˆn7ÔA°Ã° šÚûhÚ}k+Ãmw5-?“Maù˜` ~~þ;öˆÂ+ 8pH¹¸ärõýDa é' ƒ0µTtùž&Äÿôç~ø©àçBëuŽä‰cý¬¯yNcm­h °TåþO(µ¤™¼ö£ÜßšÑ﫤”+86pмzû û?Ä?ñ= €OÀ»·o‡ J}ØÏø^ Éᓟ)ÁÉåJryâeߊ¹ôF°ÓÇ™–/ü‚yZ@[ ”û»í†`¶U–+ 9IöFp°2û?Å{Ä@fÀʸ, /ˆƒÀWÁéú+Øÿ)Aü0` à©î›| ˆpf¨&Gw/åû€‚ø)!:_`ƒÞ‘¼Ðço—êçÒ•óIô0¦…â´üðuc;D¿¿‚Öüt"_àü\ìÿ?ZŒ €wïþ0Ê=”ûs4—„d$9cžÍ^‹ø/ ‡ÈgØêåLþó üÝm ¤ºÊr{fá~s/…%Ý!aà{¨„6§§@ì3d<*ÇþoÂ+⇠-oÞüÁ,BÑÉ3¨YèÌ«š3BeäÄ$Ç£ü9âÏ?wb_A˜3­Xä@î(÷ÐÏú‚b÷FPãƒûø0LBOß Åç5’nÊýÁv×Ð÷'óhgn"D¿‚ÒÖÂþoÄ@ 0`¼yýúƒ—»Vº^~¾œdäÞ%1™Xgñ×d_€è'ÒÏ…‚Ñçü]¿¥”3Ç©½¥B€&ŸPfu¬‡Ø,î{ª)(1" ÏBüs£÷!öŠû? ¨øÝX§Í µ¤G»£Ç9‰ü=°Äÿ°¦â”PGòFŸ?0‚uË}¨¢(ß(Q ܺÞDëNÝT-ð8PAëR/Â¥ù—Nìÿ?f oß¼ùYdª‹A{òP½¨ •úß71~ÄßÝx×¾ûü½)`ÎóÚÐ}÷9EmY‹cE´5uÒù˜\Ú²è8ùmÌ5Zई¶ež³k`àÙ/Øÿ!~Ì0 0^¿þÍ a±j(+2Kb3huÙ{ÿOúìRø‡û¹ÐJW“ A€cÇì¦Æû˜ð_ôöôQvríП¤Í ±^–Ü¢«5‰@OåÐÎüx»4^¾}‰ýŸâ=⇠À¯‡ìrŸËŽ¢œ@4&"¹„ñ¿{KÑ_Ùø÷s¡7Gôù³ð½n¥&žÁ|€1}þU¹·h×Ò„1ѯ$põ5ÍÝÞJZ|"‹ìg>Àî’82ŒŒ`ÿ§¼ñÀ€!€¿ügñŒŒ‰Bx/åÁý.9ÁÈï5—$)¿âYíjâ“§ùÎAŸ?0?k½¨4/€r·þ1Å®Oú¡®'I^1%€×Á2Úxù‚]‡«/`ÿ‡øQ ˜ð³x§1}AZ’“Æ>7S2Ä?ñ+ޟݹ¦…ÿvogZ:ÿËi„o¢[u50ì„¶ÆNJÚ›E[)?êÀT“ˆ@ŽÒ¶¬d›6ofaÿ7 ˆ€ O„E©€O(ùZõI@Hü{9™Èî&âO;´ÝFûüiÎó3Œ~ö—»7‚܇`£ôv÷Sz\mwïó7¯­E&€¸í©¦•qYžg“ÀÕ%Øÿ‚ø1ÐX` à‰°ðeWO(í®“ÝGyáËÉEý÷Dü£¤µ¹>ÿõ:Gòı~À‚Xâ6‹.$œ¦öæf6Â@ÿ ¤ÖP„ïYFàˬñI4©@·¯’BR®PT‘m¥m×±ÿSÄøa¨*™)¸|Ú<ƒPÞ#8—r“xâ¿^xÕÆúüq¬°\~ðu¥Ì‹?Q‡U „p£ü>øáœ ðÕá¿>Ûä"Ÿÿ)¡MWÏÛŒp§¿ û?Žˆ-"0`¼~õjPmr‘¤ëÔ& )0nŸVgR¾ñ·Ü­·zá¿ÓÇ…–/DŸ?°Ö/÷¥ÒüVƽúF:²1óÚØ ;CîQUf,9V@;r­Þèú½Ÿ°ÿS¾Gü Ÿ €—/û™.,ZÙÁ30ï‹Wp…R%á>mÓCÿoý]V+ü#ü\(h±ŽõV=(ðfm5 §åq‡0àoj,ûáŠE À|€*Z•AñVk<{5„ýßd÷!~00 _èÇ‘yðîœ\ªcÖ ¢ÁÁäËŸÿð»·ðµÕ‰ÿ zGòBŸ?°‘A1QÛéáí[0,Œ®öº|¢˜¶¹DüÔØ´èéÃÊÌ*Ðﯠu©­n>Àî’8™°—ÁþñãõÀ@@³ xNvçø¡\¤B¿óû䄤¾”‰¹ñZ«·á¿Õˉüç¢ÏØÞó]ètìjyüÀŒø ü ÕæyZï¦#héOÓ"ð9TL[ÒS¬Æ8V{û¿ÉïCü0` àÕ«^õG{¨èaàŸ)÷÷(îQ,zÁ5dž!? ñ+îIÚ³Îâ…˜3­À±~ÀXæ9Ÿ.§$Rû´ „000H%Wë)zI¼ ØÍƒÏ–‚é쮡ÀS9–—`ñÀ…ÛyØÿ!~eü0ÔÀË—/{ÅÏ$¡ôGq{­ìäÉîŸöɧlÂCüYñ?ZtŸ°Ûß}þ(÷öE¿»µž`µ“ý JDºyYë?­"÷½U”˜N…g-ÖÈoªÁþñO? 0^½|Ù£X˜ŠRÅB\=-ÃC Â"WéB ɉ/Cš<~Ä_ŸwÉ"Ũ‡#yÏðö~b€ådÀ0·kÒaa²ÿt²dmæ´‹@<ŒÏ°@àVïcìÿFAü¨Ð ]±Æ;ˆŠäaDßïð)žÉ& æ~ÁœÄßþà†E ÿíÞδd>Žõ`"[C–QUI! q¯¡‘Žm¹$òégƒû)ÒEVΈÀÿH!mË€eøæÝkìÿ¸û? 8 “=““ê1ŽwyOHÊÏGßK‰ˆ¿O™(¸ÄƒøÙ׫æÏ˜ðe­»yÎB¹?jÐÍú‚¢¶¬¥›uµ0TÒÖØIçcri‹ëq ùÒ±€ÇÉsgéŒ @tû*iuò5Š,š¹ù+S°ÿããGü0D`  C(¹‘7áf«ë[â{…ä)¢üçŠ{ùøBtðŒˆÿMžŽä‹>4á5בb÷FÐý›7`0´5uRjl¾p–¿å±Ú?Å"D Þ1¥z9uF €”Û¹Øÿñq"~0Ð04Ô¡ºWH¢øLm’‘Ž áïWº€Š¸D¢Œ…ñgÇÇLsŸ¿.@Ÿ?¦ÀcŽÅDm§{ÿ0:Ûzèò‰bÚî~RÛ–‹oh¾Åˆ@ŽÒ¶¬äi5 šk±ÿããGü0D``@»bÑð}@ü{¡çGpêø¥{Èß«ø.ìs'¹‡ñß(º6mÇú­ú»Ïçù`r<ç8ŽBE€M]íÿÿºS‚À¶|Öyœ%·èj‹€¸í©¦•qYž7-Àݾ&ìÿ˜ø?*D`Àx>4ÔfPÛ³£xe1¿˜ù>".iÈÏ–Ï%ÜC>~Ä?ÐÑd^ñïçBëuŽä9ËÜ"à=ÏùŸÖ€[7mÈ…z\íô„¿•ñ}HºE A€ù!)W(ªÈ¼À¯/þÄþ‹ñÀàÅóç-‚»Ç½2åF¬K'=‡y®Â™Î ^—Tùñ¿G{—Í6‹øßêåD~s¿˜!1Œ€·oÙ¬ÐÙÖ-ë&tñ)ÒETX¤ÀçP m¾vÞ,ây cÿÇÄøq   CÏžµ°%8\i÷¹Ô‡$L Uù¼O/z.áG¤ðñ#þÄ]!&þ;}\hùBôù0Óø,p¡1{#Àº €ö–.º|¢ˆvè¥ëgåò4‹,9V@;r“L=û?)~Ä@Zš…2 !¡¨›øiÜCqèˆü;—Sñ<&Ùðñ#þâÔ“&ëó^ì@úï0ÝK›°og(]¯©¶Z åQ¥)†ûÙžÛ‹-\̨¢U Qhšùåm7°ÿ“âGü0`  ‰YdJgNùsÞ dÜEÁ}ä+‘'‹5ÄDɧãGü åSÿ¡Žä=ÂKF7ë ßDU¥EVc<ºÝBI{³„süm—`ßd«x(§u©§< õ×ìÿ¤ø? ¨hÊvøó8…Œ³§®GIy-s"ñH®$û|>~Äÿzè)íZòµæ>ÿôù`ul YFåykÜ®yH§v^¥-‹$‘lûøm̱€ol1mNOÑ$þw—ÄÑÛ÷o±ÿ“âGü0`àÀÇÜ¢ç'xÊ fü•så’"áßÞÅäú—ؤ'Çø¢ƒþa>δb‘ƒ•‹ À¦ ¥”—~™:š›gܤÚÂ;txCê ÖëÏ[T•õˆ@v×Pà© ËK0Ê8w+û?)~Ä@†>}úH±à™-”ì°É‚w jøàÊ›ää¢x.“ôÄøufŠêcýBÜÉÇú`S¬ð^D ÇcéÑÝ»ÓnôtõQÑå:Ú¿2™Á põ5«¸ï­¢ ÄtŠ,<«Êh躇ýŸ?âÇ)20ÐðìÙCÅ‚—ŽÖà§zrî 0Ìà ôñ0÷r‰EÚ‚„‰DloÒÌO~ÃßΘèÀ‚Ð-qqX~K<`ý×ÜüäG˜@”””\ðq˜†tR§qÊÆPž˜„ÉÁÏÕTóirRhóò“¿¡‘• g9°èdvÝ…À’ý¿ÿ¬ÿšŸü  ‰æÎ—AÔ8ÍSc€ “‚³½Oò?7bM~òïÙ²^Å}ð€€^1sUìðãŽ.ü€ÿ¶g?gýgA~ò#d€ââó¡ Æ/ N?­H&fP¸Ôt°¾{@{‡¬ËOþã÷rE 8L@Û¶ëYøÄÍV'¯°þ³.?ù2€Ð–#˜?ÁÔ™ü»IAšL ¦`…ŒÖå'ÿùœ“*ù³·Rxú|9[%:ä¨Â yBŽº|¿„õŸ¥ùÉA°༩lD˜4ÚÄûA˜Y@ )´*YŸüîßUíS¿rHñ蜾Â1…@›égÕƒ¬ÿ¬ÍO~€ PZRrN0j¿ÿ7ñôP½ý>&í>š¿Cþa2´'?ùËŠ Uÿ.­R€Òzï yá0`ÙUVábýgO~òó @À€sûz$›hrŸ¨ØªäïyÒïì ¼¯È¾üäw»jÕ¬ #P€z$,T1#O†¬ð˜ºõ¶r»½¬ÿlËOþ¯ € àlÀ-D¡qò¨ÙϽԊdø~rûó“÷æu*îÃWC\„ZvÞô¢ vL–ÚšõŒõŸíùÉßÀgýföP¸jÄÄŠ÷‰ ŸÑ0”ÆÏ 9‚“ŸüçrNªô¯Þaèýõ7ôhÐ ?€ôɧÕÅ{ŬÿìÎO~:4@ ƒH´sQ0„€÷aö$Ó¨÷ùF‚—Ÿü…¹/U¿Î-CXˆÚµ^” ÷¢K*¿¸šõ_0ó“`P\XxÖǤ OFä;G£'<Ïß÷O9•¿Cpó“¿®¶VÍ›:.„Å ©ÿ[ ?€Ûï¨:—‡õOó“Ÿ ¸øŒp ˆÆ "rÛôLa1 q(I°ó“ÿø=*åó·CPŒº¦,µ¥èH—­]Ìeý’üäGÈ è8ãËú}kž?¦îW||Æf߯ñïÒ¤$þÿÐå'ÿ£{wT§Œ¸$€€ôž»,-ü:͹ çU±þ U~ò#DœPTtZ°lºvP÷dPaO’ð\Ù|І4´ùÉ_UQ®&ä‚Ð3n¾Šq²â¸â¯ºÖÍú'„ùÉA Nû`ÒµÂçM¬ôÿåßiüïÆÏkµ-9#?ù±% è  uÇÍÍ*úÇg«]§Ÿ³þqB~ò#$€¢ÂÂà jŽõ“'!-Ã'N~¾ü]“ŸüÏ=P=Û¦©(ôùjŽŠ|$ Â Û¼_Zþ+Yÿ8)?ù¦ EE9Â@– 0pÅ»E­J’=Ô„Äç:+?ùëjjÔÒ9SƒT˜:´\£]øÌØv[¹\nÖ?ŽÊOþ†úz€€3²…A§óßM¹&Òd ©l•Ÿü§OQé_½„âÜo“Š>€Ô 9êÔµ|Ö?ŽÌO~¶È Ø-ì¯Ñm¿±k‘€`.œ™ŸüyÏŸª]ÛØ\œº%/ ?€Ë¯ªü’Ö?ŽÍOþï¾ùæg€9¶dÙ5©èfý‰CþY¹ÉÙùÉïu»Õ¶u+UÂǯÛX 2ºïôYôÄÉRë=Võ/ë§ç'?@@@–i+Æ€ÛäVýÏÈ–R$<ò“ÿÞÍëªK‹› @@¯Øy*v8×Â?i?뼺ý¤ŒõOä'?2@AÁI]“'£ß"ô­í?œ.êðüäwUWs@ €  Mû†8èÏÃú'œò“`PŸB{’¥“J -Eá–ŸügNQ™1Z\¤ú|9[% :å…/dN9£ÎÜ*dývùÉßàõ"Ì@ òrsj hù*[Í¡ü³úß9<󓿤 _ì×ÕâBÐ1se¿0zíuUVábý®ùÉ0€ÈÏÍ=`aKŽð9‹ ¢ cxå'ƒ×«îÚªR¿x×ÂbÚ{o”¾4>Gí>óB5xYÿ„o~ò×Ó`ûâÅž ´òÈ'|Z{¸‰Æ÷ ÿüäÏñ\ éÕÑ¢bÐ=q‘Šy2JŠ_°ìŠzYXÅú'ò“ÿ'€ À‹gÏvØ4àÄ|¢Køç'ƒÇ£vo^§’>yÓ‚‚Тëö/|!q\¶ÚšõL¸ÞõOøä'½×‹0€xúäÉVñ°ý}<¡˜€ôOýÈÊOþ§¨¾3›Y°zÅÌU±ÃŽEhñ =\TóªXÿDZ~òÿˆ0€xòøñÆPšCû[•d"/?ù½n·Ú¶n¥Jøøõf-€€vm×GXá qc²ÔÊC•ÇíaýqùÉßàõ"Ì@ ?z´Æ gÿ3íÿ.៟ü÷n^WÝ[%5£p×&<!Å/t›wA=xQÁú'bó“¿¡¾þ€ Àýû÷WØ:f/圑–ŸüuµµjÊE*ñ“7(\]Ò–EHÌŸú»ë<¬":?ùë½^€À[·;hâ±á÷Fn~ò?¾Wõál€€@@Z¯=a\³×ÿÁËJÖ?Q‘ŸüõÏ÷€7n̳aðEÓ$ëüüä7ž ÀMš  GüB® 3âÇ {ý#Ö?äoðzf ׯ]› “€ñ¦€þ]Zi0€€–·„Iñ ý—^QOó«¢îýàu»¿C˜€@\¾|yj4N :¸k«Jýü%0€€Þ_ÍUñC¹ÐÉ$Žîõˆp¼Ï·3€‹'„y› ϦüäÏ{þT ëÝIÉE  ]ëµ-~aà²+êya5ïÿ¨ÎOþz§`pöìÙÑÿ<^¯EƒR¨òçìÿýä'ãÿÿµ í º$<à ƒ _HŸóûŸú{¢þýO~ò×»Ý^€ @VVÖ0aÐEˆy”³Ÿü/Ÿ«‘ýºš2€€®)KRüÂÈÕ×UAI ïÿß!?ù=uun€ À‰cÇÒÂôOÈOþœc‡TfÌ~Š@@zÏ]!,|!mbŽ:pá¥jðòþÿ;ä'¿Çív!L@ <Ø×–AlÅÀözC6i’Ÿü¥…jâðþ>Š@@ϸ*vĉ¿0fÝ UTVËûßä'¿ÛåªA˜€@ìݵ«{〘𠌟5Ï”ÿ¿üïä'¿± EìG†‚к㦠¾6é×?õçýÿ;ä'¿P…0€Ø±cGG[l þ€·âs¶XRò“¿¼¸HÍ?üo  Ï—sTü#A(~aÒæ[ª¼ÒÅûŸüB~ò×¹\å€uëÖµ1´Üw‘·c¾ùÉöÔ1Õ.ù‹ß‹@@‡–«m,|¡íŒsêÜí"ÞÿM„ü䯫­-A˜€@,[¶,]c€ú3|²}” aà–Sû÷~5 ùÉ类V«ÍVñ¾JQÐRúð…˜ÑYjÞ®{ª¦ÖÃû_#?ùÉ節-D˜€@Ì;7É0 ŒÞžöaŸ‘„úùÉOþGwo«~[F}a€€nI‹-,~¡çÂKêîÓ2Þÿd ?ùkkjr& “&Mе¡•ÇÒIC6~ò³å Fò“ßëv«Ý›×©Ô/Þêâݶ7³ð…¤ñ9jkÖ3Uïñòþ0?ùÉ_[Uõ`0dÈO5®õ0 Bí6 “Ÿ ø:ùÙZmGúùÉOþâ‚<® ˆj½b穸áÇ›Qsµ_AI ïÿfæ'?ù++*ž L@ >ûì³×MZ€¬l’?ÓHÀ¶²WˆX•Ÿüä?—}‚C¢V@›v5 _h9õŒ:v9—÷¿EùÉOþªŠŠ»€W^yåÿl:ÁS0…â3Íl¤Ÿ‰K°ž~&+ó“ŸüÕåj霩*ŽC¢Np-àl•8øp“ _ù;¥fl»­*«ëxÿ[˜Ÿüä/+-½Šø7Àý!ùïzßf¥¦oìäŸ Üp ÏL¦¥ùÉOþÛW/«žmR¢ªÈ@@§Œ•Bñ Ýç_Twž–ñþ·!?ùÉ_RRr €@4–ŸÅ&|ýA/ïU’ö6 “š!“`­ÎO~ò{ëê~;$ðów¢¦Ð@@jï½> _HŸ­Ö{¬:>å›üUT†øû׆üðÿzõêÿ`G³:{ÀZD ±©Qø¸–³sKÞÖã‡þ¥ æªÛ¯ê™´>Bäºk?MÉÍÿ³ÕÂ5%üýk~øá_±|ù`ÀoA ¶5`W[„kjj|Þ jZgPûYù¾ý¬|¤I~_HÛñÃ8X­>{¸êÕõ¬¤Üô‚ =RMþ>›˜­jBaþþµ?üðÏ;÷@ì ûè¨sD(¬j”ÑŸÿÿÞ$tõÝÐÄÇÏè}¶=ùᇿ`]ŽzæÁ»’nÓG€ÜÜç½”Øü?óɵ¾(Àß¿væ‡þéÓ§?–ê€è ÓÑ€Œèä‡×„Be.ÛfÞ—ŽÞØEB^èvÜE§!+èwŠÓJÆ?üðÏš:QÝÐ÷’¤Úø‚ }ïú!i7þ×¼6S›_Àß¿8á‡þÑ£G@ü{€]<àꫪ }Œño" XLJt¹xÉÇ¢â…~økªªÔÈÃTÏ ÎLŠ!rGú¤ ˜þÌ5üçU* ó÷/Žøá‡ÿóÏ?¿ð[€m,°÷ÿ••yY  <#%Ç{ÈMCäÆ#¾ŠšýZñÆ?üù9YêÉûnOŠÍ!rÕM_$Íæÿ=Ó?{}%ÿâ~øØ3u c €íH3`C+ÊËWz¢Ÿ#?ã2}ò×IsC}Ñq¾V¼òÃÿ¬)Ôõ}º%ôæÐm J$±Ç^õêL3ÓŸ¿ñÊ?ü×_ýŸÿ`g#öú]ͳ´<&ĵ@å…)??ÏH?S£ïÅ3?üð«õ×z\pI‚ ›®ú8!7þ—==E½­ûWÃüý‹s~øáïØ±ã1…P€­ ‹Î¡ Æ[ ̱øœvÏ~Æ.>R!ñgø£èù¸ûëlÆà‡þ¼œ,õø=·$ì& éu߯ µùðÝæ¸?ÿâ™~øM¶è½Í‘°G* à?»#ÖE第¬¯âÃæ9Ç‹Hoågìçä‚#6oK~øáF"jü¯?ª«Ò/H¸M Arkïwbãå+3ê»ûoªåï_¢ðÃ4þ A V,[6ܳˆlÃæ²n ßr/NùŽù¦>ŠMƒ…-Ñøá‡?PQ®ÞÎxY¥Ÿ{jBm €þ··ÿKžš¢^ÿz¹ª Ôð÷/Áøá‡¿6Þ„@ ô/¸%v²À¬3žuÜéqÙCÉú)P Y¿ý¸ •<+Ô~?Qùá‡ÍŠeêÛ®M˜ Ar祃եOŒ»Íÿ€aóÔÊœ2þþ%(?üð‡C¡ A ~ýõ×;q´ÆZ˜:ò¢”ïù(>,4ÿœe~øíkW^v~Bl €«¯ÿ®œ¿IÄ?üåå™1À®Fl‹0A tŽÚãØ›Ãü¹m¢Ÿû>îçܶÏuTH¶œî¯³9“„~øëÖô¸_~PW2-€Ä± ¤×=¿´éƿߋÓÍqÿZþþ%?üðÎ5à@}ÿ`8R/œ FѺ³#Îu¿–{‘×¥i3JÝãL|ÜWJ>~øá¯,+­ŸÐý¼ÓânSH„ÜÖcx›v÷/¯ ñ÷/Iùᇿ ?œÞ×îŒØÁ€ŽfB€¨Dþn”Û¸¹ß:îùÄ|_:Ò#]rÝM²_Ëb‘@%3?üðçf¯UOÞw[Üm €+ný¦U7ÿX¤²ò+øû—äüðÿfÍš/õ¾æ0<`@Œ ÑH$*ZAÿÇ}ä†#î;EvñpÙ?×½"ÑLÚ¯™ üðÃ?kÊuCßKâhsH„±€ƒÔeMhñÿÕ¯ÍTã¬WÑõ?øá‡éÒ¥C=`oKlƒP…Ø×4É82 U7¸ØäEj¡à–R°Œæ­pDH0‹Ök¦ ?üð5rÄ0ÕëÂ3ãbsH„\{íÈÛø§?3U½ýËjU SÿSˆ~øg̘ñ¬Þת³¿Î^:iFlo €ÿJu€@Q]UUØ@÷ÏØãEäÅì*:~Šƒ\„EOê|js¥?üðä«7ž{,6ˆ@ ˜¡z<8ºÙ›ÿ§?Y¢ ŠÔÿä‡þ‘|] °sª `G#:{@yYÙJ½ˆ\P« xã^°Òk:^ǹ(ÙÏ8>žªüðÿpÎLuÇ5½ÛuƒH„ÜÔD“7þ7¿5[ÍYQDýOa~øáïß¿ÿŒèbÀî–Ø*Õ°­»°Ïï pãÆéR·Õsý9iD‰Ó왹~ž†‹R#Ìa*óÃmMúá‹OTß‹þØn›D‚ ¤Ï€Ÿ|mü{=?M}:>KEÂê óÃ4©Û­sˆ{ à?“}?ˆ@tt€uÏÎÊú&ÆÌM—´©l…»BÂØ§ñ³ƒÁÍaÿLðÃýØÀwôØÀËÎ=µÍ7‰@Èí݇ªKž˜Ôˆ±~“ëÇú•–õ~øá¯ ‡ÿ¡÷4GÅ;¥¢@ :xÀFtò €… ´˜ë­Š´¹©Ç~ÜEÌþùÄ»LöÏß|~øá‡?RSSßàÊô ”½$Bí=TÓ÷ükC5ÔÿfóÃ?üÑpx“‡êìo@šCüW* `G#:{À_|q{ƒ6O¸ä¸û#5$qY?±#©\€d j?ÓrüðÃeY©ú`X†êyÁ™ŠM0€/¹ÿ’·ÔcU 4@ýo1~øá‡?T]]Z×ÐÜ#ö2`gíS] Û°«û˜»2GœtÒIg4`å\fOÇWCÿ÷‚L¯ëË0ÚÅi>[ž~øáÏËÉR/=ù ›`Iñ{þë×Rÿ[œ~øá/Ñ}ÌŒ8D§‹és¶»%¶J5€@tÐéhÀFtò€ÃuŽF"ÿtCG‰Ø‹ÞŸ!” ¦³A‰< E.$­Ã?üð/š3SÝu]¿ÚøyéÆÔ²™k¨ÿ­Æ?üð¯ËÉù¥n¢Y °“CügR À.Fì­s€išqLM0XÓˆã7®Eí÷’]Äb²Iþ^ι¥›}“j ~øá‡_'Vã~ùA]E€¤€<Ög˜šôÍ<ÔRÿZ•~øáŸ9sæku ÍMcóýLŸ³ÝŒØNgkk`’ @Ç#¶ñ€4#öÿ]TUT8L›û¿Ý‹ÞQLj„¯ŽÿÈÔ~Öñó´>?üðà Tª‘#†©^Ò ¹‚àžÿ@õíЉ*XQMýk~øá‡ðàÁWpξ+€9 ³³{p¨ÎÑ7l˜ÝÐB–Gw˜¸‹4nÄý5«h.×Çœ¯o?ÓvüðÃaA¾zã¹ÇÔezƒ˜Ø›\‚ ï>ù*Î+¡þµ?üðÃDTZZÚIuýÌ< ³;¦º@ ¶²ÀîæŽLsgæèË—à0mÂÑaaK¯áÃ*ZÓ÷]§Í³mÏ?üðg.Y¨¸íÚÜÜyíŽOÕšEë¨mÎ?üð׆ÃÕ{˜ãt7}Íö1`W#¶MU€  -vŠ!ŽùñÇ·9l›´pZèò±žÆÞ/òQhk(p´5?üðíëðówêº^ÿ“›Z‚ O_ù®š5z‰ŠF"Ô¿vá‡~ø«’ºFæ–èd®;ï`€ˆH]À(ÀíŒØÍÜ‘ÙÏ4Í8²K—.'ëÅ$ÙÚŸ—¹‰ø²Aô5Õ.0íË?üðõõ'﫾ý17³@î>Húhº W‡¨íÊ?üð—-¨ëc¦s˜il¾·%¶±@ªnþ€=`_ƒÌÝ™ãêŽÒ8¬Ÿ°ð}A¡ƒ©`)I¹(µ??üðÃ_YZ¢>–¡z^@£Àø€o©/2ƪʢJê_\ðÃ?ü+–-û´î³ég¶¿éo–f®;oo €ÿJU€@lkÀ®:ÍQ™U••%²môo%Í–l¥P‹)5ûkÏÄ?üðߟ“¥^zâ¶cäÝà¯p]1õ/®øá‡þo¾ùænèb®7ïn €­RS f€;ÐÉ€ÌÝ™c 7nœïXì‚—îôÈÙ, Á^º~™Áz&þøá‡þÌ%‹ÔÃwÞІW‚ oøÌ4ø£þÅ?üðÃÜqÇQ×ÇLçKìd €”¸ÿ °»°·‡é³dÑ¢üÞÚ,üçÊñW›a2ã—~øá_0{†ºãê^­¸a%òÂõï«…“2U4Bý‹O~øá‡?ÿMï]Ž×9Òô3ÛÏ\oÞÍFê ØÑ˜fîÊìoŽÎ=dÈkÄ®¢ò"õÿqGQ“#GêxÍøæ‡~ø#¡õÝ—êªô Zp£Jä±>ÃÔ¸Ïg©H°†ú×üðಲЌ<Âô3Û×#vD ÿwàαFêœD¶X N´ˆ:òq¤æ_MGšß%þùá‡þê@¥úêãªÏ_ÎiÆ• ÈCéƒÕïNQÁŠjê_BðÃ?üyyScŒì¬³«Û"bÀ(À=½£uŽVW‡|¡°ø¸{ÔL‹èß`º‡~øá/ÚP ½úœJ?÷TS‚ ÷^”¡>xîGU\PJýK(~øá‡Ú´i¯Ö ÏÀ}¬€ÛZ#€càñz¦f¦µà¤†-ygIž*˜IùsrJ<~øá‡?gíêú‰ò¦” ÈЇ¿Vy«6Pÿ’~øáïׯ_׺þež€{ë¤yÀ61›+F®sܪÌÌ/}›=aûZðíÙżM\~øá‡ÁœjÀ ýclF €¼tó‡jÙŒÕÔ¿„å‡~øk#‘¿ë=ˉÖÀ½tÒ¬€h‚` 5 `ï$€GŒ¸M,r1‘¿Fz¾Å‘øï8šØüðÃ4QSÇV7ö»„ ¸€<}å»júO Um8BýKh~øá‡¿*(©ë[ÆÀ¦À(À4½½“¶Ûn»SôâÚ"—HD6‚ci¾¾Iq³õ½›\¬„ï™øüðÃ$T?|ñ‰ê×íO)¹ùF‡»R£>š®jªCÔ¿¤à‡~øóóòf´Ü@‚`ÀÑ:'ÔƒUfÊM9ì÷¥ÑòãEì‚⿨yYÍÛäá‡~ø+JKÕÃ2T¯®g¥Ä¦@î¿d ú"c¬ª,®¤þ%?üðÃ?aܸgì€Ö€k°“Îî±&lX¿~®hó$“(<×âMCüS!IÆ?ü𛉃õÄ€îç–¤nÿ·ê7þeËYÿIÇ?üðG5ÓÉ'Ÿ|NŒ€tvµ&4R“Ž›¥Ñ{•“bLèÔô À$€cÏ?ÿüsµa³³E,[Dw±²‹›\ÐäÂá:¢$ÙФæ‡~ø³W¯dt`RFúåf°þ“ž~øá/*,\Ñ2€I{{'èœ …¢~ïý˜ø/`²Å”Ÿ‰DÜ…ÎñŒ4ã45øá‡þå ç«Gî¼!¡6Ö’q÷gjåüÖÊðÃ?ü çÏÏnÈ9v´¦éìå sbiIÉÇÁÊ‹W^°Š¾#ü M+¤©Å?üð/˜5] ¸¾_\o¨äå›>T &e²þSŽ~øá¿ûÎ;Óí€þ&“vŠ1 à(æÏŸ?B<"$,z…B:®dáû´PÇÔÔã‡~ø£á°š:~´º¹ÿeqµ‘Fg¯~Ï3ËŸõŸZüðø¦æ¯zr²ÝК°mÓAÐðønݺ]húø8$XH¹x¹ „\¤±$.ó)®”ã‡~ø#¡õÝ—êê]Ûu ÷¦Æ}>K…ƒ5¬ÿ”å‡~øKŠ‹Wy¦s€· 5 ƒO@4Ô99T]]ë0}æ­ûèãk|GÉrº›Œ¸¹¬×Je~øá‡?¨¯>¡úuûS›nœäÑ^CÕ¨¦«` ˜òë~øá‡Μ9Ã[¹ AаhãÆÅâè¹ShLó¨ÓÐ=$cFc([Gñµà‡~ø+JKÕÈÃTßÿ9§U7ÌòPú`õã»STUyë~øá‡ß\ÏëÚµë…­Ü h8a„W|D×b÷kú¤™¢þ‹¡ËrZÏIÅ~øá‡¿¬¸H}0,Cõ¼àÌÝ(#Èý—¼¥¾È«Ê6–³þá‡~ø=« ÃzorJë4$lðÁŸ­­Ý²Y^œòÝ"Á,ê·®Ž¤ò‘&ÿÅÉ×s6#üðÃþºlõú³ªËÎ=µYd¹¯[†úô•_UQ^ ëþlðÃA~þ|sÿßnØ©…a;¥º²²Â±è;è»XÉEG.Tþ÷(ñøüðÃÎÚÕêçS鈟€Ü{Q†úà¹Õ†œ"ÖüðÃ?üßÿýÓÖýÿýÍõå4OÀmšß hxÒêU«~ ¯‚´Yh\âx+ÜQTþÞö³®À?üð7W Øøg±þ~øá‡þh$ò¯m·Ýöôº1å:G™±å]<÷ÿw¶îÿûiH4´ûÜzë­½­¢ ŽòXÏøw"#’­£ü¾›Gä‡~øáÏY³ àlüßTï<ñ­Ê[µžõßH~øá‡þò²²|ø]bÑŽ*ù1~ò3~›‹4çõ>~øá‡?åD`èÃ_«u+ øýo?üðÃ@7#¯;ÜÈûÿ[èþ?AÐ@çôÚpøïMºC$WjvAÜì¾[$ßg²ŸŒfsøá‡~ø³V®Hb€0~ÿ[€~øá‡_7#ÿ¹‘÷ÿ·iáãÿ@€Âõë— ¦0V!‘ßÊϸžcŠLÃG ÜßK(rMæ‡~øáO&€0ÿœåùüþ·?üðÃT¿½¢_¿ôº©d-ÿŸ ¸`÷Ø×Óà8SFŒq¿Pü¤ùÇ•äb#=ÿwZš~øá‡?sÉBõÌC‰¼ñ_–Ïï óÃ?üðWWUUê=Èi­sÿŸ Ûzútòô8LçcÞNÔÔüU°€Bjaû)¿ï·›©XðZƒ~øá‡ù¢ùêÉûnC$H†?öÊZšÇï+ñÃ?üð/^¼øKsÿÿø–¿ÿOôØÆÓ Mg/«À :§åçæÎó¹ ¥â }Þ׳þ?çÿÞUkòÃ?üð¯Y±Ìˆˆ·®þÙËòøýoE~øá‡þh8¼åücW3þï8Ïýÿ}íûÿþÿ}¶òôØÉÓ`?ƒuŽü}àK/½tSc,cT.BÖ³âq ùûÈÇœdËêÃ|¶&?üðÃöê•õ" ûy§!Ú}ãŸñÛÿÌ~ÿÛ€~øá‡¿²¢¢¸îô±9…|Œ9•|€9¥ÜÉsÿÛæ ‚`àn®q€:g„ªª¢Öâ–Š‰?Khb"»¨èË·Au2Øi+~øá‡þõ¹ëÔÛ/«^ž‰hãÜÉ@õé+¿ª Ù…üþ·!?üðÃÿì™3ß7÷ÿOðÜÿïbN)§éìlN/oÓº÷ÿ €q€§­]»v¼µˆ}ŠJÏù/*Bq¬¤•¨`?Ûš~øá‡¿¤p£ú`蛪÷_ÎF´r¸t ú"c¬*]_ÆïóÃ?üðëÑãÿ:üðÃÏõŒÿ;ÒœJÞÏsÿ'Ïýÿ­„ûÿ€ð3кpêÅúzanÌžkÁ Ía1ý|N6¥VQŒªƒ§=øá‡~øK‹ Շê~Ýþ„há<Ú{¨úåƒiª²¨‚ßÿvâ‡~øá/.,\Uwê¸íÇÿã;ÿ>оPYV¶Ñ*$’m´yƒ ?*Ì'5Ÿw?ûcþ»§ŠE­=ùá‡~ø«•ê‡/>Q×õ¾ÐÌ<Ùÿm5êÃi*XQÍï;óÃ?üð:ôž6ÿGÇ5€ìk£Fz½‰ã=LÄ1$ƒ(|­T ìçe+*|}ûòÃ?üðGB!5îçïÕ×ôFøÌË·|¤f^¢"¡0¿ÿqÀ?üðê®Žè½Æ™ÂñÿæŽÿ#€ßk:gEÂá¿Å,&1L ]H„ÿö9^D(’öÏ >¹Ãi<ñÃ?üð/]8O=óЀ·îù\-¾JE#üþÇ?üðÃÿòeË~2ÇÿOvÿß­™Çÿ €q€Â5€=\×òrrf9ì lùüÛD÷œP¿Oeój3çkÇ#?üðÃÿŠÅ ÔËO>è!ˆ¸ÿ’·ÔG/ü¬r–åóûŸüðÃ?ü[.<÷Ün~Žÿ·˜ ®ÈÓnºé¦~Σ>.è.(vQqEë5EÃu/ó¾Ûš:î]Å+?üðÃÉÆ jä{CÕå—ü9eÀÃ=‡¨o†LP%ù%üþÇ1?üðÃIQQ®ãøéø+ ‚à€Y˜g–—•mpw#wÓù>”#â½#éõ›l5ã™~øá‡ßÜ©¬ïpûÕ½RF<Ý5é›y*òûŸüðÃ?üï þ€Ðý7Zêø?Ap @žp çÀI:g|øá‡n¬cÌE/ØÆF1ÆëZiäQ¦¨ã}¡[©Ó¼& ?üðÃmMš3m’züž[ÔezcžlàÞ‹ÞTÃùF-ŸµF³óïŸ(üðÃ?üU••Uzoqvûÿ'®tlì5³««ªjû(Ï#•燺¾6æ‘)óV6 vasÛJ¡yJbñÃ?üðçåd©†¾©¯œ›ØÀóÿâÍ1jýšüû' ?üðÃÿ䉇 ÝÿÛáø?Ap `¿X×fÍœù‘ÐÑ3–á´M¢ûëäç书¶AmìŒUÙÆÖ'Qùá‡~øk‚ÕjêøÑu§N¼~ç§júO U¨*È¿‚òÃ?üð×êÉb:tø£ÞWœnNãøÿ®­ßýŸ h¸Yh»š…·çÀ1fž¾Í6ÛüQ/Ü¿7²8È#IÜMQšÓP¥1Ç™¤¯ŒjbóÃ?üð¯ZºH |ùiÕ§ëÙq+î>X}þÆ•›YÀ¿ðÃ?üð¯ÊÌ[7bÜŒ?Aç(CL²½tÒbÿïк€ ¸fàþ:‡˜…y‚Y¨g­Y½zrLó貎®"á¸ç$Ü9’Ž@¹ ©l^]…7Æ÷L~øá‡þP  ¦Ž¥žyh€J?÷Ôv÷^”Q7»¿þÿíBüû' ?üðÃ$ÞrÖYg]ì™ýœÎ:™d{üÞüÏsü«:þO“õ Ë\ð4ÜÃ,ăÌÂ<Î,Ô3N:餿DÃáyŠEcm t4Ij^b¿¾X…Êq¯I.ÄQÁh&?üðÃaA¾úêãê–ËÓÛ\¼xÃjÔGÓUa^ ÿþIÈ?üðß—“³ Fó¿Ãt0§;µßñ‚à@'}Ì‚<Ìn˜•5ÇÑLÄ]¤‚ç[â6¤–šxŸ•Æ ˆ7)ùá‡~øÍÇ—/š¯F ~CÝÔÿÒfy|ßïLR9Ëòù÷OR~øá‡~óþ–¾}ûöfÿ§9šÿµ 𦙅Ø%V3À®]»^ÕÇxÍ]ølË(qj¸±ˆûûɶÕþ˜»«ªó{¦?üðÃÿÚ•ËÕ'ïQw\Û§ÙàÕÛ>Q£>ž®òWoàß?Eøá‡~ø òó—ë=Ä9Vó?çìÿ¶;þO\ØÎs ÀÙ PçœüÜÜEž¢e§˜EË=#T~¿!‹ÚØ®¤Î׊ …V(r)Ã?üðÃ_ZT¨fM™ >–¡î¹ñ uÙ¹§:À}Ý2ÔK7}¨¾È«fYªŠóKø÷OA~øá‡þÛn»íJ»ùŸgöÿ^öìÿ6:þO\°švò4<Ônx饗öÒ z‹0o4VQ’ï 3*}²Ÿ³¿—ôó9‹¦mjS™~øá‡?PQ¡V-[¬¦M£~üê·æ}‹&gªÿÇÞYÀ¶™-QøñrÊéR™™(eL33-õ1333333–™9Ð8´Ü}×ÒXú5òÙÉß6/±óI:òÖþ ŸVçž;÷Ìöÿì¾çDC3ÿÿá‡~øû8ÿ¡ýû77Eøß0þ×íÿ€c> pX¹0À¤%Åvž°ˆdÿ\Û! œ+bÁÌÑ 0¬SÐÒ?üðÃ?üðÃ?üðûÏwÅW<§Løß8Ë»¸÷…ÿ! Â/¶/è8X__ÿØb¨GÇQ&=bD'=Ê$x?z"ž#f*§U:³ðÃ?üðÃ?üðÃÿ¾½{7¤5ÃÒ¤:Ë›ž ÿf™cƒzOø p}1‡e§Û¸.iišðW@T¡ÑÅM'†ú"$Î,ç¨Ä¹§øýôh“åºÂ?üðÃ?üðÃ?ü'Ÿüä'?ÅíþOµ c*üï,þwÿ‰0À³T }a§f»¦L™òöÖÖ»œÛ¨G–˜T‘4év"]¬ôëºûE‘ôÿÖ³Ue‘…~øá‡~øá‡~øwlÝú'Ûý÷£ÿÆ& ·¬±Á.üïA•Ùþ*€cra€ƒí :wH¿˜Áê ±+šðÃ?üð÷u~øá‡~øÿûïÿ(­–Ùfá<Û<œœ4¶òFÿ! F޵/ðŒ¤yöÅ^öûßýîs® å/$ºUJŸ­ŠUõç‘'Aû“*Ò}”~øá‡~øá‡~øm÷ÿŽAƒ­±Ñ¥Ýÿi¶‰8:éÒÊý‡0 xiÒhûOËv$-onllqE@á>ºÇÜs¢âª_Câø¹ª5K°ôU~øá‡~øá‡~øáÿõ¯~õ Ûý_d›…3“&ÛD±Ñè¿Þþ‡0 8¾À“í =ϾàË>ðÜtkG‡r%uš©.jÚ•Ìá¼Ê"© otžêÞŠaŸã‡~øá‡~øá‡þãG6¦5ÁŠ2»ÿ“FWàè?„ÀHÀLÀÄr]÷ïß&ÛŒtЈs“5|­8`%vT•ó$©ö%~øá‡~øá‡~øáOSÁîyÇ[ßzõéîþ÷Œ€0 ˜¿ À²úЇ>!€»]Rn£~,(|Ÿµ׈³Nq¡,ÕÎ?üðÃ?üðÃ?ü»wíZ_Ü ¬ÚÝ„@€ÎøÏ¿þõ+ª EÁ"ñù#ý:º`éçè‘-þUPg_à‡~øá‡~øá‡þö4öoÕªUuÉÿ9vÿ{vôÂÀÈß0#i®}á—¦±€«O45uÊ]hô­lmŠ‹l‡ºFŸ©º÷y©Q4U;?üðÃ?üðÃ?üðÿùÏþ†íþ/öÉÿìþW‡0èð¦ú.€}ä#¯cGD‘‘®b”¤›è÷Óï§ŸŠ"šQóÃ?üðÃ?üðÃcCÉô·ÿʤ¥¶87i»ÿÕ+ ºÆº.€:++Šg‚°“®¥”gªD»UPt¥ûš·õI;®IUÇ?üðÃ?üðÃ?üü·nݺ—evÿçÛ¦àT?÷ŸÝÿÊ]5I3]£2]³’æ—ºf̘ñˆ¶––;¤ë)Æ£äL,õÏ×é¥Ù÷EM¹›ú½u[WIÕÆ?üðÃ?üðÃ?ü›6løý[šT—ÙýŸ”4–äÿÊ]çØ·¿}‘/Jž4Ö¾èÓ“æXX’´â›_ÿú;níèP§,dúÖBŸ–vTópÅb×T ?üðÃ?üðÃ?üð·47w¦Ü¯5™±óm3pjÒø¤Q™ÝÿI5ìþW¾0è¨Mº$i”}ѧØž‚eI+:´GšDn§¾/(¶a›V޳T±û·jU>?üðÃ?üðÃ?üð'¥Öÿðƒël÷IRmN·MÁ1IÃm³p°m^t»ÿ•) ºÎ·/ò`Kôn_ô‰¥.€l àÃüà'µ w‰â£ˆç‰Â§Ÿ§þ¶C™â³XKeóÃ?üðÃ?üðÃÿÎíÛÿiÁ¥ÝÿÒØ¿)¶)8Ò6 kÙý¯^aÐ0Ò’>§X˜W ˜´òÛßþö{Âv%ULõ5îþØ­ ?ƒ»UÒ¯¥?g%óÃ?üðÃ?üðÃqÔwmmíZ1öo¢ûWÚý?ŸÝÿJ€IvØX@û‚ËŒœæÇ&­Ú»gÏæQHe‘Š ¦¾_´ÏÁ)s«?#üðÃ?üÇ?üðÿ¥þ¿ýío¿ÖZÿ£±ÙÝÿsÙý¯LapàfœŒœT ˜ =zôà ÍÍ·vF…4‘b ׉v(ᚊ÷”×Å •Æ?üðÃ?üðÃ?üëÿýïŸ7õ¬õq&øÏý»°4ö/³ûÿ ·û_ €^( $º2G¬ ÀÆF€ïï{o³HóÍT f¥ '3*lº8F!/q:«»¯2øá‡~øá‡~øá‡¿±¡¡å¬³Îª¿—࿱™à?5öï´þW®0è8+é\?Ж; °iÓ¦?tæ(ž§t´=åxéØjG8n¿ª~øá‡~øá‡~øáo-N¾ô¥/}ÿ•Zÿ}ðߨ¤K‚±U¾û€œ\: \Qt›Žo g¡æ,¨Á뮨øoý¾ñëïÙÛùá‡~øá‡~øá‡ÿç?ýé'­õ?wð_µîþ# Æê@À©v`~ö(Àcûا·¶´Ü•«p™âBeŠ‹o\P½+Õ\‰¯½•~øá‡~øá‡~øwïÜùßô·ûj×ú/‚ÿªwì  ž œž4ÇøÂ¾ðÖŽÖÖ¨úÛøz§àÚ8ŒEÙ¸&¾(µµ×ñÃ?üðÃ?üðÃÿ‰ÆÆ¶>ÄÏüÏÿYë¿þ«¾±€.q`¨; 0%i¦? ´zÆ ¿=MçRëœãSâŸäŠzðÚúÖ©·ðÃ?üðÃ?üðÃùw÷å—_þbÑú?ÝZÿÇDÁÕ·û0ôGú' îêQ€¤5Ç=bE5l›²¬ÛŸìñ¼3Síy§x–ʦØ}õ¼½…~øá‡~øá‡~øò£}¬ ­ÿ#mÓ¯¶oÿ! ÏMªÉ{`íÚµONy· ç4,®éñÀ•Oiu]ž`QôUáîi~øá‡~øá‡~øáß²eËŸ‹›t¢õ?;óXßnýG8Ç\¿~™£j*@]Ò’ÒQ€×¬[wE*H'eqÕ-Rñã‘û)Ö 8ê×q¯•')¶§øá‡~øá‡~øá‡ÿØ‘#Glñ/RÿÃÖÿ³ûVë?Âà(À¹]< 0/“°2iõw¾õ­„a+‘kªŠ¹wRóܪV)ýž§<³µ'øá‡~øá‡~øá‡¿pâÄm³gÏ~\iñŸiýŸ“§õ¿ wÿ@Σ™K8Æ\Ãiæ"Î7Wq™š5Ö¯ÿ­+ ª˜•-ˆâ¹ê VPå{…E5~=íØþßøá‡~øá‡~øá‡ßBÿ®ºêª—gÎý/µÍºyæ=E¥þ3óù;SjÅQ€9IuÙ<€¤úCîuM;§yVƒTWW`Åss‡Âè‚o|ÝÎ?üðÃ?üðÃ?üíI_úÒ—ÞQæÜÿü\­ÿÿa ¦˜+8È\ÂKÍ5Ÿ èóFõÈ¦ÆÆVQèt‹Ú©r¸¿²xûÝúeŠ_ßnc÷ñÃ?üðÃ?üðÃÿ_þô§ïØâ_ü›d¡Þ#,ä›Ö$ ÄQ€ ÜQ€.å¤ÉOiMçR±ÅÎM{L·Kå(°Aû”pŽezkžóaŽ£»øá‡~øá‡~øá‡ó¦M)vàfÿÑÈ¿¡´þ#/ñâ(€Ôy‹’–™ °æ²Ë.{IjIº+(ÒyÃV‚")]_QìMAcýYÏ4?üðÃ?üðÃ?üðÜ·o¯-þ£sÿÙ‘ƒ“úG©ÿˆ£j4à$k-š“´Àå¬yï{ß»®­P8©gžê‚m ÎwÉÛ8ðEuïÆêןÙóœ)~øá‡~øá‡~øáojhh2dÈÃlñ¿ÒÿnÞ¿<÷ï[ÿ@ë?â(€ (òfd󲡀_üüçßÕ j1…i§&ÖâŠxþÂÜáå¯ÉHc:]~øá‡~øá‡~øáonllŸ1uêãDè_WÏýŸÃ¹ä ÄQ€y èòV˜+Yÿ«_þò«ÒÁÕ.¨vqË¯Š¯ËŸÊª~t±FÀäç‡~øá‡~øá‡~›õûžð„g‰Ð¿9yÎýŸvë?Â@ädByà¿ÿý7»Ð‚å ®.â¦ümVúÇÁߪݶ„åç‡~øá‡~øá‡þöBáÎW¾ò•/ BÿܼÿÓ;÷0yçeòjÍU‘4¶” B“Öþýoû±(¶a›”ݧ ­~~ üðÃ?üðÃ?üðûÏuò]ïz׫J‹ú7-i¢ýë—t^0ï?6"@…f&ÌÌNð&ÀÖ-[þé ¹Ióè±(Ü%v‚µ;{Js_ã11åøá‡~øá‡~øá‡¿­¥åäûßÿþ×ÿnv‰ÿú—IüïÐ?„€¼(ixf2ÀÔÈXÿŸÿüº³½]bé’*W¸¼û«Š~äújGV½Oܪ¼?üðÃ?üðÃ?üð§Úw¿ï=ïy­_üËÄÿ8ôïôÏý# D€3jËL˜Zš  L€ÿüë_¿-X¾*oŒ*¼þ~迎»Np„Ÿ ~øá‡~øá‡~øÓÎÿ]éÌÿõ¶øwãþdâÿ°Ìâ€[üwù„€t“„ °°œ ð§?ýé{¢ ÇU¦«*çV»¬"HFù2¯…Ñxñ£Ö§ùá‡~øá‡~øáOsþï¼êª«.s‹7î/Hü?S¡q@…úÉn<àD 'Q&@}ÒÚ?üþ÷ßTp’.–ÊÖ®¬ÿ·.ȦN=“Õ½Oø£¤ÈöE~øá‡~øá‡~øÓâÿŽ¿ð…/Q‹ÿ̸¿©™Å¿HüïîÐ?„€0l2Àé˜ßÿÞ÷>ž áIï˜Ú¿£³bêºøñ¼c]ôH÷˜¿ÏäßOpT??üðÃ?üðÃ?ü'š›;Ÿýìg?_-þÕ¸?—øßý¡ ¨ÆfL€‘yL€|èC¯/Î; Ú·wVò¨è›DKYðüØýíÔŸ[\_ÍüðÃ?üðÃ?üð7;Ö4wîÜ'ˆÅ¿šõ?ÂþήíÆÄ„€0âÉθðTL€[n¹åêdÜ!‹hô㡊´xÎs]c§ØŠ{ä»ÇÅg¨R~øá‡~øá‡~øìß¿»¦¦æaÑâßÏúãþº?ña L+0‘ piƘÔàñ}ì³›ÛË^õã•J{õ²(êÁŒp£Õ{tjÿ:¢µ­šøá‡~øá‡~øáß¹mÛúô7ïCNgñß#ãþb2€0j’Ä&€žPl‡:väHƒpj½ƒg‰GËÄ?2Ñy0ýyôçÛߪƒ~øá‡~øá‡þöBáž4ëû§°ø™Yü»Yÿ:ñ¿Û „€x&M€³Î:ëa[·lù»(êÚ}Õ­[bô‹˜ßª_Ïÿx(Ç7vš…û}­Êå‡~øá‡~øá‡ßnïþÒ—¾ôîÓXü_èÿçUÝâa L€¤‡üü§?ýbrLOf ¬»Õ ¯úúø‡"žU;Å:<&<篭<~øá‡~øá‡~ø ÍÍ·Ýpà W¦¿k¤ýç^üWå¸?„€0ŠZ …»\±6yWפäptLgðœcËÕæV†I¦ÌV?üðÃ?üðÃ?üG9^WW÷$[ü×Ûß·+Oañ? ©F,þ+&ña L€‰]5žùÌg¾ÈÂU+WR0’%ŠÉ(J™ ÜgÝÚæ>¿¾O¼Vïç‡~øá‡~øá‡ëÖ­³–ÿÒâµ-þ—%-sþÃÅûC9àL™uÂxðàÁƒQLN W“p…ƒq­¾FÿhÅ®²vŃÇÞÈ?üðÃ?üðÃÒÉþàŸîÂânÒL·øQ­‹„€0. L€Åf¬´¢YoEô!ßûÞ÷>QÌp7J™•Îm’>c¦_¿ÜãºÝ,L Õ?.г÷ðÃ?üðÃ?üðÃßÜÔÔ~Ùe—½2öWoZ+’–Úß· Ê,þÇ$ðiÿÕ±øGˆãÚ˜jÅpnÆXVθòÊ+/OG::‚10Q"m|fÌ®ZÄü­v¢ÕcžÁ¿¸íi~øá‡~øá‡~øìß¿kذaÉ&ý»Åÿ"[üÏIš‘4%i½,þ/¨ÖÅ?Â@˜#„ ° c¬(™¥\€‘#G>fÏîÝÛ]qw®t†Ma«—x,¾Þ¿oàfGmp=Ã?üðÃ?üðÃ[êJýñøùLË¿ó7?i¶[üNžYüêÊâßT1‹„€0.°â68ihÆcÅpŠÇ9f,2çt…Lzè¾ÿýO¦â{wpŽË»Óº­+ûïà¿Å{*Ç9>C;áḚîç‡~øá‡~øá‡¿éøñÖLË¿?ïïgüÏNšž49i|fñqR­Xü?¨Úÿa L€ÙIóËMÈ xèCú¬#‡ÕŽmÐê%~ÄsU*­úS¯¯Þ_üHÆŸ¯{ùá‡~øá‡~øáoO·Û¶ný{MMÍÃ3-ÿQÒÿ4ƒ=.iTÒ0·øïgŸÛWFý! „ p±9¡£ÍlNéìì˜@˜ôÐßÿö·ß*¦¯Z÷=˜×¶„¯)ìÂ÷°k¼´›­Þ۸ΠÐÌHthFFÆù¥ÅÅùfŽÝºfÍ–Ù¯·‡˜C5êwŸ“ùá‡~øá‡~øýœÿ_¹99#7Þ`ƒDáoßúË”ßò¯ì÷÷aÑóþa r”‘»@Ï80ìp:ìæo¼¦¦ººR9Ô,'Úr c(ëgÅ|Q,s?üðÃ?üðÃÚ©Ä]øì³Ï>§'Ï~JÈŸuë¿…Ó&VËÿô›÷G1°šÑ fìÑY7€4233rA“”ÃpjËØ9º(·Ýþ>ÛaO‹Ï϶üðÃ?üðÃ?ü>௢¼¼èä“OîÖ¥vŸð/nýeПOù×[þâß.üÂ@˜rK€Ý àÝÚ탱€Ýö ºT#àå—^z¤¾¶¶Q¸ÛæÜY:~nNÈ~M:îµÆjhvâ‡~øá‡~øá÷…ÒÙyÕå—_å «Ýßïõ·oý•”ÿé·ß! Ä–»@Û †Ê|o<÷ì³8# ÎvÂõƒ-­|®«vz*çõÒŒö~\ñ³g=~øá‡~øá‡~É\Y^^rá…^nþ¢Ý?ØëïþŬÿ:2èof´ü#„€´»6 º¶ ºÄX€ÈFÀã?~O]mm½Ñ¦f|.âÀ´¿×>¨ãwíšá9³?üðÃ?üðÿ»ñÿ«0?ÜQGu®(üÙî¿ypë¿®r뿌¼õŸ–-ÿa L§ÀÝKkÝÞµÝÈ»¸[Š@? ·èFÀ…çŸyqAÁøä ‰ Ö™ÊpœøÝºöϰ›eæðÃ?üðÃ?üð'ëü&ŒŸ½Ç{œ& 9çßÅvÿMýsãzA¿që?«µü#„€èX7è!b[@€-¶Øâ¤aC†ôŸÔÚú[:ú€žúCU½8Ĺ¹˜sFòÃ?üðÃ?üð·67ÿøÅ AY /¼ð‘Fá/æü}ºçíþ;m´û¯æ´²>ë?ã[þÂ@˜~] ’ °’wm×pZ[ ÜNäDÉÁÓ¯OŸw›Û”QiW‹ù5c^Nù#`ǘϓJG¶N~øá‡~øá‡~ìWYQQù¤ËôE\áïçüe»¿ògßú{Í?+ÿa 6ècÒØQ¬ ôA¶èÒK/½"/7wd{[ÛÂu—‡XŒÕ2'•6"|Á¢kZðÃ?üðÃ?üð·55ý2fÔ¨¯vß}÷ ¥ð?H)ü}À_8ço·û{ýgé[„0&€¾)`ye,Ào ó¦ÞXrÉ%ìÝ«×ëu55uέ¶=Û7öôš­r1¯±ÖûÄ®Šå‡~øá‡~øáÿ«¦ªªòùçŸÐ=[ýAª¿RøË€?}Î}Ñî/Cþô[ÿ_ü#„€0"º–îÂXÀ&¦ ˜‡fÀá'Ÿ|r·1£GÓÜØ˜VÚß”9=ËÙ×~ÿÙŽ½þû؇´òs”ïÓüðÃ?üðÿoñw«—S_~ùe÷7ÞøD­Í_¦úËYø+sþëZíþ ÿ3³øGÑ àt!b,@16׌€N2ö“F€ì ð:ü²Ë.»2IªufÀO]qÌõý½ö®\)9oõóãÛÿº´Ã~øá‡~øá‡¿9•jO‚•÷Ûo¿3üm¿Uøï/Sý»Xøo¨¤ûG´ûÏb·þa ºì@=À6DXàžÂØ_3¤pÏ=÷Ü’›“3²¥©é'ãеe'ó¦Ëo¬ 2wù?ná‡~øá‡~ø;ÜMS*Õ2bøð/Ž;î¸nAÑo·ùû=þ…ÿ&²ðéþJ»ÿluë¢À Pò¦ÖØ'0Ì®€pLà›¯¿þ,É HfÞ 'ÞtØÓòkö,_c'G<¼¤•ßKc˜³øá‡~øá‡~×ÞÿWòœóÝ7ß|¶÷Þ{Ÿ¡ýV›ÿ>Á|°Î/÷Ó ÿ5­Âßh÷Ÿ5oýÂ@˜öX€Ì°Œ™ Öîä´‹Ì ]]56Ûl³“’µ‚eeÅm--¿s{âóSß–ç?ŽXEdþÌ(ÉäàÙŒ~øá‡~øá÷óünäñÇ‚üü±o½ñÆSI@²YôÛ·ý{8í& ÿm'‡û)­þ"à/˜ó7Úýç¤[„0c> Úð[‚õÛJ#@é ²º6&(##㢤; Ú%áº÷íݼöAm»þöƒæcØãïSý}¸ŸQø‹9#ݶ.üÂ@tL+# X¸©?x¶òÑöb<@tˆ3ÀëÒ‹.º"1\‡@i«Û‹ûcD[¡áü¯±û6¾Ç¿5?7ãùá‡~øá‡~שøKEyyiRðŸuÖYûg•آ߷øÛ·ýÁ|ÿVN›ëü|ª8ãoþ´û#Ka„[ÖõÎFAN€2à³¢ÍÛHö¾½{¿]XP0±±¡¡Íëü}è3ŠÖ ŸôT´'¿«q;a?øL=?üðÃ?üðÃßÞÖöGC]]ãÄ F|ðÁÏo±Å'Y¿Qô‹?Û¯Ýöû61ß¿þäu~AªÿÊ…ÿ\×îb, ÞXÙ0køg]§õÜ€ +`›3@ ”ݪ!àu饗^Ñ¿ÿ,g ŒKÕÕµ´»–<€Ç¶þ黆§"ThjnÒæï ï$¶ùá‡~øá‡þI--¿»}ü ®ØÞó³Ï^=â°ÃΑÏ²à—·üúL¿^ô[·ýb¾ß¹º(ü—›–…?Ba,'Œ€ÕÖôÑzZW€°Ì€}îµC@3öÚ}÷Œ7_ý‰ÁÙÙK‹‹ \·@k` LÕƒI:âá%* 9þA*¾q.ä‡~øá‡þ$¤¯µ¥å7Wè7æåMøîÛo{<óÌ3÷¯½öÚÇëž~ÃoÜòïYôËÛþ‚Û~9ßï÷ø‡©þvá?·¶û#„€ ˆ7–òÌrþÀYIŒˆ®ŸcøA±M Ê0M¯ 7Üðøï¿ÿöþýú½;fôèÊKJŠ9ÐÒæ ’•„Æœ¡õÀdÏ:ƵLZéÄöëuKãìÁ?üðÃ?ü~õ^2§ïZ÷›JŠ‹óGŽñÕgŸ|òêÍ7ß|]ÊoûñPôïíµ§RôËÿ-ÅmÿÁmÿZÜö//×ù…¿…?Bñ/B#` a,¯t¬Ý3@Œ øÁ`›€è0 (SÀëpM;í°Ã©Î ¸5++ëÅìï¿ï3~üg¹ƒú–¦¦´Küý½£­ÍXSù dßÄß ØŸ·e¼fºóÃ?üðÃ0“ïÎáŸRδ¯*/¯ÈÏÍ7tðà®eÿµGz莭·Þúdq¦K[웿¿åéý»3ýÁM¿ZôoØÉmÿêÁm¿œï ÿEãoü)þBvXࢠÆÔ®¿A@7ÂÌ€`›À.–! FLS@èPÃHt¸¥½÷ÞûŒÇ}ô®îÝ»¿ôù AÝÝCÈ€ñcÆd'9e®« ¶ªª¶±®®¹1•J»Ûˆ_“§¿"¸ì}ÅÖ×ânVìôeãõÖûs)?üðÃ?üþ†>ÇKnéÝÎüvg®7¹5zÕîÜ,tý˜1£F}?$;»oÿ¾}ßïÝwŸ{øÁoßÝôuå\VÎóCõBß.öEK¿Uðïù‰ö~»è—·ýF›ÿŒ)üÂ@# ³ñ€04P1ÖQÍ€0@Ð †ÀV—€4¤1`˜†A`¶Ž:ê¨s/»ì²«o½õÖ›^~ùåG233ŸèñÙgo¸žw¾þ꫃ø¡¿k[ü:é>ÈÉÉY\X˜ëZ ’]ɪœÜCS*I'voÝmHkò ÕœJµ»Û‘Žd]b›Sò€å:ýÑîäMˆ?’ýçþLL‰Èœ’·þmWä_+Æ&"ntäû3è¶,=ýnÆâùá‡~øõúÿÿÖvˆ¿»^þo·þñ¤äï¼üûï?nõçÄä·n®#9K%£qÉ“¨®¦¦¡ºªê¿ó§¢¢Òåêä;³;'ÏOÎøœœWÙ?üÐ'9¿úôêõFÒrÿjfæãO?ýôwÝ~ûMÝÎ=÷Òƒ>øÌØ» ÔHÂ8ˆâëîîîîî.÷¿Ï. –šµCÇfò€ßB½ï?ò·±û2ðûÈ÷¡ïc¿¿î¿)ƒá´_ò»Fÿðûõ%ÿm¿>ê×Îü—qø€ЯÂO,ØÏôÁ[á:`Ê  (~> 0â€ü~ÖXÐý`ø%ÕÏàGð=|ó%œñkìO9øÃ+¿>äg/ý6úíÄ?¿ö¯Êð! ^è[ü2À¿0ü7]Ä ðÐ~2ð¬Dÿ RA ÝÏâ3îgñ£(þ|éC¿ŒýgvÒÿ0þ…W~}ÈÏÏûóè׿ðó×þÁ6ÿ°ßªŒ~€p06è‚“×ú÷‚‹{~%P¢@òÁA‰ Ü·ÜæÔ÷2ä5æ³/eÜÛÀ¼oC¿Œ}Ý¿ÿµ¿®„Wþá7ýeôëÄÖ^ûÀUÀÔ1@<î×úv@ºÐ7BÐ¥€…¿P°@P"…îKˆÍ7Ö¨¯&ù†¼z÷±{øùþ¢ïC_/ûaìë7üé…_ÿ®Ï_ùë7ýcF¿^ûWaø€0ÝU@ŠþA»ˆAÀ~2 oX°Kûù€.<„@ HB¼>ÄxÐ}`údúˆ—÷Á»>ìõŠïßF¾½è†—}ûú ¿ô§Áï¯üí7ýaôÛ‰yígø«B@ƒt /˜ƒÀ»ð(à—·üZÀãÀ@q@ D Š îMŒò€÷®Œx yôòRÃ>Œ{ø |ùþªË_ömìûëþ™<øõå~{寣ßOüçþµ Ä€üBÀþ»ÀI‹éRàŠ] LÄ ºX$x(P,°` Ïk<È^°F½Šòˆ×—§æ‰ûÅǽ^ñmàOŒ|{Õ¿â/ûaìŸügðµþ:øGŽþÙþ LñS&¯rP¸â@“‘@¡À® äX8ðx O˜Oâˆ÷!/÷mÔÛ°Ÿ÷ià‡‘AC?ýÉ×ý>øÃ+þM?£ Ä¿P% ä0ÐãÀà’E‚ DÁ@nKîkÜ}ÓG¼Ü zIÃ^ã^¿ü<ôÛØ/üüöÊÏè"@ èA ÿd G !L†¹,ŠZéÑ8@ èA@<ä(Â@Šìz`"BÇ9ÂA‰̤>â}ÈËYѨ·a¯×{ƽü4òÓÐ/c?œô—ÁÏè Œ- ä0àq@— Š1Èq»*°pÐ㳯x yór¬ {{ |½äÛÈOC¿ýñƒ_ýÀ¨ P¢€…Z ðH  ,„pãœ`Æo#Þ‡¼ù4êEã¾ ü2òm臱¿Òƒ Ȧ`óˆ00dW‰  <ˆÂ`Nñ>ä}ÔËž8îegùÓ }Ùl\ÝÑ€úµ€lu̳Óì‹-È>æÄÞ6â㘗f‡Ù^¾ÛR^õgfì ÙTl6-x$pÛK0¨ñ€¹ÓG¼ÛF½Û:õÀ—MÅÆÙüQ@a Ûl ¶%0—úˆï¶›ËÀo6®Ç±dÓ”6 ÙVæÜ–:ä»MSÚØ1ôÂ@·i¤Í¬S›FÚhVmè ôp6ŽÇ¸ €?íØ±À ëAìíH"ø @ @€@€ € @ @€@€ @€ @€ @ @€@€ w†K˯éñIEND®B`‚info>bplist00Ô X$versionY$archiverT$topX$objects† _NSKeyedArchiverÑ Troot€§ U$nullÓ WNS.keysZNS.objectsV$class¢€€¢€€€Tname_assetcatalog-referenceTiconÓ   €Ò !"Z$classnameX$classes\NSDictionary¢!#XNSObject$)27ILQS[ahp{‚…‡‰ŒŽ’—°µ¼½¾ÀÅÐÙæé$òPrismLauncher-10.0.5/program_info/LICENSE0000644000175100017510000004735215144136757017476 0ustar runnerrunnerAttribution-ShareAlike 4.0 International This license only applies to the logos and branding in this folder. ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More_considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution-ShareAlike 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. Additional offer from the Licensor -- Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply. c. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. b. ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.†The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. PrismLauncher-10.0.5/program_info/App.entitlements0000644000175100017510000000047115144136757021635 0ustar runnerrunner com.apple.security.device.audio-input com.apple.security.device.camera PrismLauncher-10.0.5/program_info/CMakeLists.txt0000644000175100017510000000746615144136757021233 0ustar runnerrunnerif(UNIX) find_package(PkgConfig) if(PkgConfig_FOUND) pkg_search_module(SCDOC scdoc) if(SCDOC_FOUND) pkg_get_variable(SCDOC_SCDOC scdoc scdoc) endif() endif() endif() set(Launcher_CommonName "PrismLauncher") set(Launcher_DisplayName "Prism Launcher") set(Launcher_Name "${Launcher_CommonName}" PARENT_SCOPE) set(Launcher_DisplayName "${Launcher_DisplayName}" PARENT_SCOPE) set(Launcher_AppID "org.prismlauncher.PrismLauncher") set(Launcher_SVGFileName "${Launcher_AppID}.svg") set(Launcher_Copyright "© 2022-2026 Prism Launcher Contributors\\n© 2021-2022 PolyMC Contributors\\n© 2012-2021 MultiMC Contributors") set(Launcher_Copyright_Mac "© 2022-2026 Prism Launcher Contributors, © 2021-2022 PolyMC Contributors and © 2012-2021 MultiMC Contributors" PARENT_SCOPE) set(Launcher_Copyright "${Launcher_Copyright}" PARENT_SCOPE) set(Launcher_Domain "prismlauncher.org" PARENT_SCOPE) set(Launcher_UserAgent "${Launcher_CommonName}/${Launcher_VERSION_NAME}" PARENT_SCOPE) set(Launcher_ConfigFile "prismlauncher.cfg" PARENT_SCOPE) set(Launcher_Git "https://github.com/PrismLauncher/PrismLauncher" PARENT_SCOPE) set(Launcher_AppID "${Launcher_AppID}" PARENT_SCOPE) set(Launcher_SVGFileName "${Launcher_SVGFileName}" PARENT_SCOPE) set(Launcher_Desktop "program_info/${Launcher_AppID}.desktop" PARENT_SCOPE) set(Launcher_mrpack_MIMEInfo "program_info/modrinth-mrpack-mime.xml" PARENT_SCOPE) set(Launcher_MetaInfo "program_info/${Launcher_AppID}.metainfo.xml" PARENT_SCOPE) set(Launcher_PNG_256 "program_info/${Launcher_AppID}_256.png" PARENT_SCOPE) set(Launcher_SVG "program_info/${Launcher_SVGFileName}" PARENT_SCOPE) set(Launcher_Branding_ICNS "program_info/prismlauncher.icns" PARENT_SCOPE) set(Launcher_Branding_MAC_ICON "program_info/PrismLauncher.icon" PARENT_SCOPE) set(Launcher_Branding_ICO "program_info/prismlauncher.ico") set(Launcher_Branding_ICO "${Launcher_Branding_ICO}" PARENT_SCOPE) set(Launcher_Branding_WindowsRC "program_info/prismlauncher.rc" PARENT_SCOPE) set(Launcher_Branding_LogoQRC "program_info/prismlauncher.qrc" PARENT_SCOPE) set(Launcher_Portable_File "program_info/portable.txt" PARENT_SCOPE) configure_file(${Launcher_AppID}.desktop.in ${Launcher_AppID}.desktop) configure_file(${Launcher_AppID}.metainfo.xml.in ${Launcher_AppID}.metainfo.xml) configure_file(prismlauncher.rc.in prismlauncher.rc @ONLY) configure_file(prismlauncher.qrc.in prismlauncher.qrc @ONLY) configure_file(prismlauncher.manifest.in prismlauncher.manifest @ONLY) configure_file(prismlauncher.ico prismlauncher.ico COPYONLY) configure_file(${Launcher_SVGFileName} ${Launcher_SVGFileName} COPYONLY) if(MSVC) set(Launcher_MSVC_Redist_NSIS_Section [=[ !ifdef haveNScurl Section "Visual Studio Runtime" Var /GLOBAL vc_redist_exe ${If} ${IsNativeARM64} StrCpy $vc_redist_exe "vc_redist.arm64.exe" ${Else} StrCpy $vc_redist_exe "vc_redist.x64.exe" ${EndIf} DetailPrint 'Downloading Microsoft Visual C++ Redistributable...' NScurl::http GET "https://aka.ms/vs/17/release/$vc_redist_exe" "$INSTDIR\vc_redist\$vc_redist_exe" /INSIST /CANCEL /Zone.Identifier /END Pop $0 ${If} $0 == "OK" DetailPrint "Download successful" ExecWait "$INSTDIR\vc_redist\$vc_redist_exe /install /passive /norestart" ${Else} DetailPrint "Download failed with error $0" ${EndIf} SectionEnd !endif ]=]) endif() configure_file(win_install.nsi.in win_install.nsi @ONLY) if(SCDOC_FOUND) set(in_scd "${CMAKE_CURRENT_SOURCE_DIR}/prismlauncher.6.scd") set(out_man "${CMAKE_CURRENT_BINARY_DIR}/prismlauncher.6") add_custom_command( DEPENDS "${in_scd}" OUTPUT "${out_man}" COMMAND ${SCDOC_SCDOC} < "${in_scd}" > "${out_man}" ) add_custom_target(man ALL DEPENDS ${out_man}) set(Launcher_ManPage "program_info/prismlauncher.6" PARENT_SCOPE) endif() PrismLauncher-10.0.5/program_info/prismlauncher.rc.in0000644000175100017510000000170015144136757022263 0ustar runnerrunner#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include IDI_ICON1 ICON DISCARDABLE "prismlauncher.ico" 1 RT_MANIFEST "prismlauncher.manifest" VS_VERSION_INFO VERSIONINFO FILEVERSION @Launcher_VERSION_NAME4_COMMA@ FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_APP BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "000004b0" BEGIN VALUE "CompanyName", "MultiMC & Prism Launcher Contributors" VALUE "FileDescription", "@Launcher_DisplayName@" VALUE "FileVersion", "@Launcher_VERSION_NAME4@" VALUE "ProductName", "@Launcher_DisplayName@" VALUE "ProductVersion", "@Launcher_VERSION_NAME4@" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x0000, 0x04b0 // Unicode END END PrismLauncher-10.0.5/program_info/prismlauncher.manifest.in0000644000175100017510000000252315144136757023471 0ustar runnerrunner true Custom Minecraft launcher for managing multiple installs. PrismLauncher-10.0.5/program_info/prismlauncher.qrc.in0000644000175100017510000000020015144136757022436 0ustar runnerrunner @Launcher_AppID@.svg PrismLauncher-10.0.5/program_info/prismlauncher.ico0000644000175100017510000132745615144136757022050 0ustar runnerrunner ( v€€ (ž @@ (BÆ(00 ¨%îj  ¨– ˆ >¡ hƪ( 3BP#8?LMŒHoGKq"UUU5@OD2?L3?Mç3?Mÿ3?MþŒHoÿŒHoÿŒHoæHoœHnCªUU3AO73?M’3?Mç3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoçHo‘ŽGq6++U3?NU2?LÅ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿHoÍŒInfŽUq 3=R4@N€3?Mê3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ‹HoêGoˆŒJk5@J04@M¨3?Mû3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoûŒHo§ŠJp03@M<3?M³3?Mþ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿHoÄ‹IqM5@OD2?LÅ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿHoÍHoUÿ€2>LW3?MÒ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÒIoW€€€2?LM3?MÎ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÕIo^ªUU2?LM3?MÍ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÔIo^€€€5@OD3?NÌ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒIoÓHoUÿ2@NH3?MÍ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒIoÌŽGnH3AO73?M¾3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÅŽIm?1=O*3?Mª3?Mþ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHn»Im13No3?Mð3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoô‹Hp€ŽUq 3?NU3?MÞ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoãŒInf€€€3AO72?LÅ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒIoÌŽIm?0>L%3?Mª3?Mþ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHn»ŽGq+-Nƒ3?M÷3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ‘Njä¼}K…³€M 2?LM3?Mß3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¯mUÿ¼|Kÿ¼|Kä»}L^0>L%3?Mµ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ›Ycÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼{K¾¾}M+33M 4@N€3?Mø3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿInÿµtPÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kú¼}L‘¶€I2?LM3?Mà3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ£a^ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kæ»}L^ÿ6N¸3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿMlÿºyMÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ½|JÀ»}J-.FF 4@N€3?Mù3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ«iXÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kû¼}L‘»wD2?LM3?Mâ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ–Shÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kç»}L^ÿ3@M(2?L»3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ±pSÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Jʼ}M50@P3?M‘3?Mû3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿž[bÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Ký»|L¢¿€M2@N\3?Mé3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŽJnÿ¶uOÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Lé½{J]5N¸3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¥c]ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ½|JÀ¹yM(@@@3>No3?Mô3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ’OkÿºzLÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|K÷»|L€¶mI5>O:3?MÓ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ­kWÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼}KÙ¼~KA3DU3?M‘3?Mý3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ—Tgÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kþ»|L¢¼yQ2>NR3?Mæ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ³rQÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kæ½{LQ.:Q2?M¢3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ ^aÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|L³º€N3?NU3?Mê3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿKmÿ¹xNÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kî¼}Kf5@J3?N®3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¦f[ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ»|Kµ¹{O4>Mc3?Mï3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ“Pjÿ»{Lÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kï»}Kbÿ.:Q2?L±3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ®mVÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼{K¸º€N2>NR3?Më3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿšWdÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ½|Kê½{LQ77I4?M™3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿInÿ´tQÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼}KªÃxK5>O:3?MÜ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¢`_ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kà»|L@@@@4?M…3?Mý3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿLlÿ¹yMÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Ký½}LƒªUU.:Q2?L»3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ©gYÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|KÁº€N3?NA3?Mç3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ•Qhÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ½|Kê¼|JHUUU3>Nƒ3?Mý3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ±oSÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Ký¼}KªUU.FF 2?L±3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿœZcÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|K·ÄvN 3=R4?MË3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŽJnÿ¶uPÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kп€I4@K,3?Mà3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ£b^ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|KäºzJ03>O-3?Mí3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ‘MkÿºzMÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|KïºzJ03=R3?Mç3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ«jXÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kå¼zN9Nhª4BQÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ>JWÿnv€ÿ¡¤«ÿê¸ÿª˜ÿ’RwÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ—Tgÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¹~Nø¤œf£>U|!;Wzý;Vxý6Jbÿ3@Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ^grÿ¢¥­ÿÎÎÓÿÒÒÖÿÒÒÖÿßßßÿßßßÿÝÜÝÿƲ¾ÿ£qŽÿŒIpÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ²qSÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ»~Lÿª”`ÿ•°wÿ’³zü‘³w;W{p;Wzÿ;Wzÿ;Wzÿ9Ssÿ6G\ÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ6BOÿhpzÿ±³ºÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿÑÅÌÿ­„œÿMsÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿž\aÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ±‹Xÿ™ªrÿ’³zÿ’³zÿ’³zÿ’³{l:W{³;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ8Pnÿ4CTÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿJWÿˆ–ÿÎÎÓÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿÞÞÞÿÁ¨¶ÿ“VyÿŒHoÿŒHoÿ¨fZÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¸‚Pÿ£gÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³z÷ÿÿ>X{;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Vxÿ7Kcÿ3@Oÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ7CPÿxˆÿÈÉÎÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿÛØÚÿ¶”§ÿ–Umÿ»{Lÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ»}Lÿª”`ÿ”°wÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ±zZ|%;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ:Rqÿ5DWÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿNXcÿ´·½ÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿÙÏÈÿÃgÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ´‡Tÿœ¦oÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ“²|!;X{4;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ8Njÿ4AQÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4@Nÿv}‡ÿÍÍÑÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿÞÜÚÿ˧‹ÿ¼}Lÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¸€Oÿ¤›fÿ’²zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³w/:Y|B;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ:Vxÿ7Kcÿ3@Oÿ3?Mÿ3?Mÿ3?Mÿ5AOÿ’—ŸÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿÔ¾®ÿ½Pÿ¼|Kÿ¼|Kÿ¼|Kÿ»~Lÿª”`ÿ”°wÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿµ{>Z|%;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;WzÿCpÿX²’ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿ{Õªÿâöíÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿçîþÿ…¦üÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿi úÿkÉõÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿiÚð"7Yz;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ?c~ÿT¢ÿa̘ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿfÏœÿ»éÓÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈ×þÿo–ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿj§øÿlÒôÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿfÙò@`€;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ<[{ÿLŽˆÿ_Ç—ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿÛµÿðúõÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿöùÿÿœ·üÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh”ûÿi³÷ÿlØôÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿ€Õÿ;Wzù;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;WzÿF{ƒÿ\½•ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿpÒ£ÿËïÝÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÐÜþÿwœûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿi™úÿkÀöÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóö;Vyé;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;WzÿCm€ÿW°’ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿÜ·ÿíùóÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿñõÿÿ ºüÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿi£ùÿkÍôÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóç`|ÿP›Œÿa̘ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿjПÿ»éÓÿþÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿÈ×þÿs™ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh’ûÿj­øÿkÔôÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÓ;WzÁ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;WzÿX{;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ=\{ÿN’Šÿ`ʘÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿeΛÿ¦ãÅÿöüùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿùøþÿ»±ïÿyoäÿhúÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿi“ûÿj±øÿl×ôÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿjßôUUU;Wzú;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;X{ÿG~„ÿ]¿•ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿmÑ¡ÿµèÏÿûþýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿÍÆóÿ„qâÿwbßÿwbßÿn~ïÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh˜úÿk½öÿlÚóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóøÿÿ;WzÞ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;WzÿCo€ÿY²’ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿyÕ©ÿÊîÝÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÛÕöÿ~åÿwbßÿwbßÿwbßÿwbßÿveáÿiŽøÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿi úÿkÊõÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÚ;Wz¾;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ?c~ÿRžÿ`Ì™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿŠÚ³ÿáöìÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿéæúÿ ‘éÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿquëÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿiªøÿkÒôÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÜó»;WzŸ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ_|ÿP›Œÿa̘ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿhÏžÿ«äÉÿöüùÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿùøþÿÀ¶ðÿ€láÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿuhâÿhúÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh’ûÿj­øÿlÕôÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿoÞô;W{ç;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;WzÿU|!;Wzý=]{ÿN’Šÿ`ɘÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿbÍ™ÿŒÛµÿÏðàÿûþýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿüüþÿÖÐõÿŽèÿxcßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿk‡õÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh’ûÿj²øÿlØôýlÛóýp×ï K‡†¤_Å—øaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿlÑ ÿœà¿ÿ¼êÔÿƽòÿ«žëÿnáÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿtmåÿhûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh—úÿj½ö«fÌ™aΙèaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿm€ñÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûçj•ÿ`Ì™-aΙíaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿugâÿiúÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿg‘ûïh’ú1cË—,aÍ™áaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿozíÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûåh’ú1fÌ™bÍ™ÌaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwdàÿj‹÷ÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÑjÿjÕ• a͘³aÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿqréÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh’û¸m’ÿUªªaΘ†aÍ™þaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿl†ôÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûýh‘û„Uªÿ_ΘCaΙèaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿukäÿhûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûëi“üIdÑ›aÍ™ÃaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿn~ðÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÉh÷ €¿€aÍ™‰aÍ™þaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿvgâÿiŽùÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûýh‘û‡€€ÿ_ΘCaÍ™âaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿpwìÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘úáh“ûB[È’aÍ™™aÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿvcàÿjŠ÷ÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh’ûªcŽÿaΘTaÍ™ëaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿspèÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿg‘ûïh’ü[dÈ›aÍ™²aÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿm„óÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿg‘û¹hŽÿÿÿ`ÍšeaÍ™ðaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿtjãÿhúÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûði‘údÿÿfÌ™bÍ™¯aÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿo}ïÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿhû¶jÿ`Ì™UaÍ™ëaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿveáÿiŽøÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿg‘ûïi‘úfdÈ›aÌš«aÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿqvêÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘û³hŽÿbÍšVaÍ™éaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwcßÿkˆöÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûèg’üT`ÏŸaΙ‘aÍ™ýaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿtnæÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûþh‘ú¢fŒÿbÌ™N_;?O¡Io“Io^‡Ki3>MF2?L±3?Mù3?Mÿ3?MÿŒHoÿŒHoÿŒHoùŒHo±ŒFoEÿ++U3?NU2?MÊ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÑŒInf€@`77I3>No3?Mâ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒIoè‹Hp€ŽGq6CQ2?L‰3?Mó3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoóŒIp‰†Ck+@U 3>Mw3?Mï3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoóGoˆPp33M 3>No3?Mé3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoí‹Hp€’Im++U2?Nf3?Mæ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoë‹IowŽUq €2@M`3?Mâ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoâŒHn_€€€2>MB3?MÎ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÕHnJ6No3?Mð3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoó‹Hp€ŽUq 4AMO3?MÞ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ­lWл{JO2@N$3>M´3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ›Xdÿ¼|Kÿ¼|Kÿ½{K¼»|K)33M 4@N€3?M÷3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿInÿµuQÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kú¼}L‘ÄvN 2?LM3?Mß3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¢`_ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kä»}L^0>L%4?M·3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿMkÿ¹yNÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼{K¾¾}M+@@@2@M„3?Mú3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿªhYÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kù½}Lƒ¶mI4AN;3?MÖ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ•Rhÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ»{KÛ½|IB3DU3?M‘3?Mý3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ°oTÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kþ»|L¢¸€G2@ML3?Mâ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿZbÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kæ»{JS5@J3?N«3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŽInÿ¶vOÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|K«¿€J2AKG3?Mç3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¤b]ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kë¾|KN@@@3?M‘3?Mþ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ’NkÿºzMÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ»|L¢³€M 1=O*3?MÑ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¬kWÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|KÖ¾}L/3?Mm3?M÷3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ—Tgÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|K÷¼|Jkÿ++U2?M¢3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ²qRÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|K ª€U99U 3?M¾3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŸ]`ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ½{K¼¿€@6DU¢3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ4@NÿYbnÿŸj‰ÿŒIpÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿJmÿ¸wOÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ³‰U¨JWÿ’›ÿÑÑÔÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßÞÞÿÓ»©ÿ¿„Xÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ·‚Pÿ¡Ÿiÿ’²zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ‘´z,;WzI;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Vxÿ7Keÿ4@Oÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ5ANÿt{…ÿÇÇÌÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿÜØÔÿʤ‡ÿ½}Lÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿº~Lÿ¨–aÿ•°xÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ”±zEY|ÿÐÑÕÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßÞÿ”´|ÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ“³zÈ;WzÙ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;WzÿLe„ÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿ›¸†ÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³yÔ;Wzá;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ[qÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿ£¼ÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ“³zÝ;Wzå;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿdx“ÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿ§¿–ÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zá;Vyé;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿk~—ÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿ«Á›ÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ“³zä;Xzì;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿr„œÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿ®ÃŸÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zè`|ÿP›Œÿa̘ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿbÍ™ÿ–Þ»ÿëùòÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿñïüÿ­¡ìÿsyëÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿj­øÿlÔôÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóùÿÿMw3?Mñ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoôGoˆ‰Nv 3?NU3?Mß3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoäŒInfªUU4=L64?MË3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿJnõ·xMǾ}J799G3?M‘3?Mü3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¨fZÿ¼|Kÿ¼|Kþ»|L¢¹€F€3?Ni3?Mî3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ”Piÿ»{Lÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kí½}Lh€€€2@N$3?M¾3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ®nTÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|KżyI*33f3>Mw3?Mø3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿœXdÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kú¼|Kˆ¶mI4AN'3?MÎ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿInÿµtPÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ½|KÓ¹zK,4>L^3?Mó3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ£a^ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kö¼|Lo€3?Lš3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿMlÿºyMÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ»}K‹8Mjq5CTÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿKUbÿ›Ÿ§ÿ¿£³ÿ˜^ÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿªiXÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ·ƒPñŸ¢lh_|ÿP›Œÿ`Ë™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿrÒ¤ÿÀëÖÿýþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþÿÿÒËôÿ‰wãÿwcßÿl†ôÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh‘ûÿh’ûÿj°øÿlÖôÿlÛóÿlÛóÿlÛóçM‹8?OåŒHoÛŒHoŠŒJk2>K)4@M¨3?Mû3?Mÿ3?MÿŒHoÿŒHoÿŒHoûŒHo§ŒFl(/BL4?M™3?Mû3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoýŒHp¢‰El+@U 3>Mw3?Mò3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿHoõGoˆPp$II3>Ns3?Mí3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoìImsª€U1>N>3?MÕ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¡__ñ¼|KÚ¼~JE99G4?M™3?Mþ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿMlÿºyMÿ¼|Kÿ¼|Kÿ¼}Kª¹€F4>L^3?Më3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿªiXÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kï¼|Lo€€€4AN'4?MÆ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ–Shÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|KżyJ&4>L^3?Mó3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ±pSÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kö¼|Lo3>Nƒ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ[bÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|LŠ=Yz.8Pnë4CSÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ9ERÿy€ŠÿÈÉÍÿÕÌÑÿ¯‡žÿNtÿŒHoÿŒHoÿŽJnÿ¶uOÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¸‚Pÿ¡Ÿiÿ“µ}-:Wyi;Wzÿ:Wyÿ7Leÿ4@Oÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ5ANÿmu€ÿÂÃÈÿÒÒÖÿÒÒÖÿßßßÿßßßÿÖÏÓÿ­„œÿJpÿ¥c]ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|KÿºMÿ§—cÿ“±xÿ’³zÿ‘´{f;Xz†;Wzÿ;Wzÿ;Wzÿ:Tvÿ7I_ÿ3@Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ_gtÿ¹»ÀÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿÓÈÍÿÄ–uÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼}Kÿ®[ÿ—­uÿ’³zÿ’³zÿ’³zÿ‘³z‚;X{ ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ9Sqÿ5DWÿ3?Mÿ3?Mÿ3?Mÿ3?MÿBMZÿ˜œ¤ÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿÕ³ÿÀ‡[ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ´‡Uÿœ¦oÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³z;Vz±;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ8Okÿ4BSÿ3?MÿgozÿÉÊÎÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿÝÙÖÿÇœ{ÿ¼|Kÿ¸Oÿ£gÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ“³z®aÍ™ÕaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿozíôh’üÛj’ûFmÛ’aÌštaΙíaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßíweàtm’ÿjÕ• bÍ™€aÍ™öaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßÿwbßùxbß‘p`ß^ЗaÍ™™aÍ™ûaÍ™ÿaÍ™ÿaÍ™ÿaÍ™ÿwbßÿwbßÿwbßÿwbßÿwbßýwbàªx`ß dÍ›)a͘©aÍ™üaÍ™ÿaÍ™ÿwbßÿwbßÿwbßüvcߨvdà)dÑ›!bÌšcÉœæwcßÜwbÞtdà!ÿÿÿÿÿÿÿÿÿÿÿÿÿÿü?ÿÿÿÿðÿÿÿÿÀÿÿÿÿ€ÿÿÿþÿÿøÿÿàÿÿÀÿÿÿþøøøððððððððððððððððððøøøþÿÿÿÀÿÿàÿÿøÿÿþÿÿÿÿÿÿÿÀÿÿÿÿðÿÿÿÿü?ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ( @ 4AN'=?P–Hn†‰Ho'5AM+3?M¯3?Mý3?MÿŒHoÿŒHoýHp°ŽGq+7@I4?M™3?Mû3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoýŒHoª‹Fl!+@U 2?L‰3?M÷3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŒHo÷[`Š¿€U 2?LM3?Mà3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿŽJnü¶uOÿ¼|Kå»}L^/BL3?N«3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿŒHoÿ¥c]ÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|L³¿€H 3?NU3?Mï3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿ‘NkÿºzLÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kò¼}Kf3EU3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿS^iÿ›bƒÿŒHoÿŒHoÿŒHoÿ¬kWÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ°ŒY‡:Uyì8Ojÿ4ARÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿPZfÿ£§®ÿÒÒÖÿßßßÿDz¾ÿ›cƒÿ—Tfÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ¸Oÿ£gÿ’³zå9Uq ;Wzÿ;Wzÿ;Vxÿ7Kcÿ3@Oÿ3?Mÿ3?Mÿ3?Mÿ?JXÿ•™¢ÿÐÐÕÿÒÒÖÿÒÒÖÿßßßÿßßßÿÞÞÞÿÍ´©ÿ¾ƒVÿ¼|Kÿ¼|Kÿ¼|Kÿ»}Lÿª”`ÿ•°xÿ’³zÿ’³zþŸ¿€:Z{;Wzÿ;Wzÿ;Wzÿ;Wzÿ:Tuÿ5G\ÿ3?Mÿiq|ÿÆÇÌÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿÜ×ÔÿÈž}ÿ¼|Kÿ°ŒYÿ˜¬tÿ’³zÿ’³zÿ’³zÿ’³zÿ•°{9Yy(;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿk|ÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿßßßÿßßßÿßßßÿßßßÿßßßÿßßßÿ²·‘ÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ’³zÿ‘³|%;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ˜¢³ÿÒÒÖÿÒÒÖÿÒÒÖÿÒÒÖÿÕÕØÿêêëÿöööÿäääÿßßßÿßßßÿßßßÿßßßÿ²ÝæÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿjÝò<9U{6;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ;Wzÿ” °ÿÒÒÖÿÒÒÖÿÓÓ×ÿààâÿýýýÿÿÿÿÿÿÿÿÿþþþÿñññÿáááÿßßßÿßßßÿ¯ÞèÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÛóÿlÝõ4L^3?Må3?Mÿ3?MÿŒHoÿŒHoÿŒHoäŒInf€@€2@NH3?M×3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿ‘Nlλ~KG5@J2?M¢3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿŒHoÿ¬kWÿ¼|Kÿ¼|L³¹{O4@LT3?Mñ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŒHoÿ—Tfÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kð»{JS9Kct3@Oþ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ3?Mÿ5ANÿu}†ÿª~—ÿIpÿŒHoÿ²qRÿ¼|Kÿ¼|Kÿ¼|Kÿ¼|Kÿ»}Kû¥œeoM´3?MþŒHoþŒHo´‹Ho.3@M2?L3?Mý3?Mÿ3?MÿŒHoÿŒHoÿŒIoû²sQ›¿€M4@NX3?Mì3?Mÿ3?Mÿ3?Mÿ3?MÿŒHoÿŒHoÿŸ]`ÿ¼|Kÿ¼|Kì¼{LW8Nj[4CVþ3?Mÿ3?Mÿ3?Mÿ8DRÿ‘—ÿ¸–©ÿ‘Pqÿ·wNÿ¼|Kÿ¼|Kÿ¶ƒQïž§mT;W{‡:Wzþ7Mhÿ4AQÿbjuÿÂÃÈÿÒÒÖÿßßßÿÙÒÑÿÇšwÿ¹€Oÿ¤›fÿ’²zÿ’²{… PrismLauncher-10.0.5/program_info/prismlauncher.6.scd0000644000175100017510000000355515144136757022201 0ustar runnerrunnerprismlauncher(6) # NAME prismlauncher - a launcher and instance manager for Minecraft. # SYNOPSIS *prismlauncher* [OPTIONS...] # DESCRIPTION Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. It also allows you to easily install and remove mods by simply dragging and dropping. Here are the current features of Prism Launcher. # OPTIONS *-d, --dir*=DIRECTORY Use DIRECTORY as the Prism Launcher root. *-l, --launch*=INSTANCE_ID Launch the instance specified by INSTANCE_ID. *--show*=INSTANCE_ID Show the configuration window of the instance specified by INSTANCE_ID. *--alive* Write a small 'live.check' file after Prism Launcher starts. *-h, --help* Display help text and exit. *-v, --version* Display program version and exit. *-a, --profile*=PROFILE Use the account specified by PROFILE (only valid in combination with --launch). # ENVIRONMENT The behavior of the launcher can be customized by the following environment variables, besides other common Qt variables: *QT_LOGGING_RULES* Specifies which logging categories are shown in the logs. One can enable/disable multiple categories by separating them with a semicolon (;). The specific syntax, and alternatives to this setting, can be found at https://doc.qt.io/qt-6/qloggingcategory.html#configuring-categories. *QT_MESSAGE_PATTERN* Specifies the format in which the console output will be shown. Available options, as well as syntax, can be viewed at https://doc.qt.io/qt-6/qtglobal.html#qSetMessagePattern. # EXIT STATUS *0* Success *1* Failure (syntax or usage error; configuration error; unexpected error). # BUGS https://github.com/PrismLauncher/PrismLauncher/issues # RESOURCES GitHub: https://github.com/PrismLauncher/PrismLauncher Main website: https://prismlauncher.org # AUTHORS Prism Launcher Contributors PrismLauncher-10.0.5/program_info/portable.txt0000644000175100017510000000045115144136757021027 0ustar runnerrunnerThis file enables the portable mode for the launcher. If this file is present in the root directory of the launcher, it will store all data here. Otherwise it will store your data in your appdata directory. You can safely delete this file, if you don't want the launcher to store your data here. PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in0000644000175100017510000000763015144136757027631 0ustar runnerrunner @Launcher_AppID@ Prism Launcher Custom Minecraft Launcher to easily manage multiple Minecraft installations at once Prism Launcher Contributors CC0-1.0 GPL-3.0-only

Prism Launcher is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity.

Features:

  • Easily install game modifications, such as Fabric, Forge and Quilt
  • Easily install and update modpacks from the Launcher
  • Control your Java settings, and enable Mangohud or Gamemode with a toggle
  • Manage worlds and resource packs from the launcher
  • See logs and other details easily through a dashboard
  • Kill Minecraft in case of a crash/freeze
  • Isolate Minecraft instances to keep everything clean
  • Install and update mods directly from the launcher
  • Customize the launcher with themes, and more
  • And cat :3
The main Prism Launcher window https://prismlauncher.org/img/screenshots/LauncherDark.png Modpack installation https://prismlauncher.org/img/screenshots/ModpackInstallDark.png Modpack updating https://prismlauncher.org/img/screenshots/ModpackUpdateDark.png Mod installation https://prismlauncher.org/img/screenshots/ModInstallDark.png Mod updating https://prismlauncher.org/img/screenshots/ModUpdateDark.png Instance management https://prismlauncher.org/img/screenshots/PropertiesDark.png Cat :3 https://prismlauncher.org/img/screenshots/LauncherCatDark.png Customization https://prismlauncher.org/img/screenshots/CustomizeDark.png https://prismlauncher.org/ https://github.com/PrismLauncher/PrismLauncher/issues https://prismlauncher.org/wiki/overview/faq/ https://prismlauncher.org/wiki/ https://opencollective.com/prismlauncher https://hosted.weblate.org/projects/prismlauncher/launcher https://prismlauncher.org/discord https://github.com/PrismLauncher/PrismLauncher https://github.com/PrismLauncher/PrismLauncher/blob/develop/CONTRIBUTING.md moderate intense @Launcher_AppID@.desktop
PrismLauncher-10.0.5/program_info/genicons.sh0000644000175100017510000000504215144136757020620 0ustar runnerrunner#!/bin/bash LAUNCHER_APPID="org.prismlauncher.PrismLauncher" svg2png() { input_file="$1" output_file="$2" width="$3" height="$4" inkscape -w "$width" -h "$height" -o "$output_file" "$input_file" } if command -v "inkscape" && command -v "icotool" && command -v "oxipng"; then # Windows ICO d=$(mktemp -d) svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_16.png" 16 16 svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_24.png" 24 24 svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_32.png" 32 32 svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_48.png" 48 48 svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_64.png" 64 64 svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_128.png" 128 128 svg2png ${LAUNCHER_APPID}.svg "$d/prismlauncher_256.png" 256 256 oxipng --opt max --strip all --alpha --interlace 0 "$d/prismlauncher_"*".png" rm prismlauncher.ico && icotool -o prismlauncher.ico -c \ "$d/prismlauncher_256.png" \ "$d/prismlauncher_128.png" \ "$d/prismlauncher_64.png" \ "$d/prismlauncher_48.png" \ "$d/prismlauncher_32.png" \ "$d/prismlauncher_24.png" \ "$d/prismlauncher_16.png" else echo "ERROR: Windows icons were NOT generated!" >&2 echo "ERROR: requires inkscape, icotool and oxipng in PATH" fi if command -v "inkscape" && command -v "iconutil" && command -v "oxipng"; then # macOS ICNS d=$(mktemp -d) d="$d/prismlauncher.iconset" mkdir -p "$d" svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_16x16.png" 16 16 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_16x16@2x.png" 32 32 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_32x32.png" 32 32 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_32x32@2x.png" 64 64 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_128x128.png" 128 128 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_128x128@2x.png" 256 256 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_256x256.png" 256 256 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_256x256@2x.png" 512 512 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_512x512.png" 512 512 svg2png ${LAUNCHER_APPID}.bigsur.svg "$d/icon_512x512@2x.png" 1024 1024 oxipng --opt max --strip all --alpha --interlace 0 "$d/icon_"*".png" iconutil -c icns "$d" cp -v "$d/prismlauncher.icns" . else echo "ERROR: macOS icons were NOT generated!" >&2 echo "ERROR: requires inkscape, iconutil and oxipng in PATH" fi # replace icon in themes cp -v ${LAUNCHER_APPID}.svg "../launcher/resources/multimc/scalable/launcher.svg" PrismLauncher-10.0.5/program_info/win_install.nsi.in0000644000175100017510000004356315144136757022134 0ustar runnerrunner!include "FileFunc.nsh" !include "LogicLib.nsh" !include "MUI2.nsh" !include "x64.nsh" Unicode true Name "@Launcher_DisplayName@" InstallDir "$LOCALAPPDATA\Programs\@Launcher_CommonName@" InstallDirRegKey HKCU "Software\@Launcher_CommonName@" "InstallDir" RequestExecutionLevel user OutFile "../@Launcher_CommonName@-Setup.exe" !define MUI_ICON "../@Launcher_Branding_ICO@" !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_CommonName@" ;-------------------------------- ; Pages !insertmacro MUI_PAGE_WELCOME !define MUI_COMPONENTSPAGE_NODESC !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !define MUI_FINISHPAGE_RUN "$InstDir\@Launcher_APP_BINARY_NAME@.exe" !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES ;-------------------------------- ; Languages !insertmacro MUI_LANGUAGE "English" !insertmacro MUI_LANGUAGE "French" !insertmacro MUI_LANGUAGE "German" !insertmacro MUI_LANGUAGE "Spanish" !insertmacro MUI_LANGUAGE "SpanishInternational" !insertmacro MUI_LANGUAGE "SimpChinese" !insertmacro MUI_LANGUAGE "TradChinese" !insertmacro MUI_LANGUAGE "Japanese" !insertmacro MUI_LANGUAGE "Korean" !insertmacro MUI_LANGUAGE "Italian" !insertmacro MUI_LANGUAGE "Dutch" !insertmacro MUI_LANGUAGE "Danish" !insertmacro MUI_LANGUAGE "Swedish" !insertmacro MUI_LANGUAGE "Norwegian" !insertmacro MUI_LANGUAGE "NorwegianNynorsk" !insertmacro MUI_LANGUAGE "Finnish" !insertmacro MUI_LANGUAGE "Greek" !insertmacro MUI_LANGUAGE "Russian" !insertmacro MUI_LANGUAGE "Portuguese" !insertmacro MUI_LANGUAGE "PortugueseBR" !insertmacro MUI_LANGUAGE "Polish" !insertmacro MUI_LANGUAGE "Ukrainian" !insertmacro MUI_LANGUAGE "Czech" !insertmacro MUI_LANGUAGE "Slovak" !insertmacro MUI_LANGUAGE "Croatian" !insertmacro MUI_LANGUAGE "Bulgarian" !insertmacro MUI_LANGUAGE "Hungarian" !insertmacro MUI_LANGUAGE "Thai" !insertmacro MUI_LANGUAGE "Romanian" !insertmacro MUI_LANGUAGE "Latvian" !insertmacro MUI_LANGUAGE "Macedonian" !insertmacro MUI_LANGUAGE "Estonian" !insertmacro MUI_LANGUAGE "Turkish" !insertmacro MUI_LANGUAGE "Lithuanian" !insertmacro MUI_LANGUAGE "Slovenian" !insertmacro MUI_LANGUAGE "Serbian" !insertmacro MUI_LANGUAGE "SerbianLatin" !insertmacro MUI_LANGUAGE "Arabic" !insertmacro MUI_LANGUAGE "Farsi" !insertmacro MUI_LANGUAGE "Hebrew" !insertmacro MUI_LANGUAGE "Indonesian" !insertmacro MUI_LANGUAGE "Mongolian" !insertmacro MUI_LANGUAGE "Luxembourgish" !insertmacro MUI_LANGUAGE "Albanian" !insertmacro MUI_LANGUAGE "Breton" !insertmacro MUI_LANGUAGE "Belarusian" !insertmacro MUI_LANGUAGE "Icelandic" !insertmacro MUI_LANGUAGE "Malay" !insertmacro MUI_LANGUAGE "Bosnian" !insertmacro MUI_LANGUAGE "Kurdish" !insertmacro MUI_LANGUAGE "Irish" !insertmacro MUI_LANGUAGE "Uzbek" !insertmacro MUI_LANGUAGE "Galician" !insertmacro MUI_LANGUAGE "Afrikaans" !insertmacro MUI_LANGUAGE "Catalan" !insertmacro MUI_LANGUAGE "Esperanto" !insertmacro MUI_LANGUAGE "Asturian" !insertmacro MUI_LANGUAGE "Basque" !insertmacro MUI_LANGUAGE "Pashto" !insertmacro MUI_LANGUAGE "ScotsGaelic" !insertmacro MUI_LANGUAGE "Georgian" !insertmacro MUI_LANGUAGE "Vietnamese" !insertmacro MUI_LANGUAGE "Welsh" !insertmacro MUI_LANGUAGE "Armenian" !insertmacro MUI_LANGUAGE "Corsican" !insertmacro MUI_LANGUAGE "Tatar" !insertmacro MUI_LANGUAGE "Hindi" ;-------------------------------- ; Version info VIProductVersion "@Launcher_VERSION_NAME4@" VIFileVersion "@Launcher_VERSION_NAME4@" VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductName" "@Launcher_DisplayName@" VIAddVersionKey /LANG=${LANG_ENGLISH} "FileDescription" "@Launcher_DisplayName@ Installer" VIAddVersionKey /LANG=${LANG_ENGLISH} "LegalCopyright" "@Launcher_Copyright@" VIAddVersionKey /LANG=${LANG_ENGLISH} "FileVersion" "@Launcher_VERSION_NAME4@" VIAddVersionKey /LANG=${LANG_ENGLISH} "ProductVersion" "@Launcher_VERSION_NAME4@" ;-------------------------------- ; Conditional comp with file exist !macro CompileTimeIfFileExist path define !tempfile tmpinc !system 'IF EXIST "${path}" echo !define ${define} > "${tmpinc}"' !include "${tmpinc}" !delfile "${tmpinc}" !undef tmpinc !macroend ;-------------------------------- ; Shell Associate Macros !macro APP_SETUP_Def DESCRIPTION ICON APP_ID APP_NAME APP_EXE COMMANDTEXT COMMAND ; setup APP_ID WriteRegStr ShCtx "Software\Classes\${APP_ID}" "" `${DESCRIPTION}` WriteRegStr ShCtx "Software\Classes\${APP_ID}\DefaultIcon" "" `${ICON}` ; default open verb WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell" "" "open" WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\open" "" `${COMMANDTEXT}` WriteRegStr ShCtx "Software\Classes\${APP_ID}\shell\open\command" "" `${COMMAND}` WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\shell\open\command" "" `${COMMAND}` WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}" "FriendlyAppName" `${APP_NAME}` ; [Optional] !macroend !macro APP_SETUP DESCRIPTION ICON APP_ID APP_NAME APP_EXE COMMANDTEXT COMMAND !insertmacro APP_SETUP_Def `${DESCRIPTION}` `${ICON}` `${APP_ID}` `${APP_NAME}` `${APP_EXE}` `${COMMANDTEXT}` `${COMMAND}` !macroend !macro APP_SETUP_DEFAULT DESCRIPTION ICON APP_ID APP_NAME APP_EXE COMMANDTEXT COMMAND !insertmacro APP_SETUP_Def `${DESCRIPTION}` `${ICON}` `${APP_ID}` `${APP_NAME}` `${APP_EXE}` `${COMMANDTEXT}` `${COMMAND}` # Register "Default Programs" WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities" "ApplicationDescription" `${DESCRIPTION}` WriteRegStr ShCtx "Software\RegisteredApplications" `${APP_NAME}` "Software\Classes\Applications\${APP_EXE}\Capabilities" !macroend !macro APP_ASSOCIATE_Def EXT APP_ID APP_EXE OVERWIRTE ; Backup the previously associated file class ${If} ${OVERWIRTE} == true ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" "" WriteRegStr ShCtx "Software\Classes\${EXT}" "${APP_ID}_backup" "$R0" WriteRegStr ShCtx "Software\Classes\${EXT}" "" "${APP_ID}" ${EndIf} WriteRegNone ShCtx "Software\Classes\${EXT}\OpenWithList" "${APP_EXE}" ; Win2000+ WriteRegNone ShCtx "Software\Classes\${EXT}\OpenWithProgids" "${APP_ID}" ; WinXP+ !macroend !macro APP_ASSOCIATE EXT APP_ID APP_EXE OVERWIRTE !insertmacro APP_ASSOCIATE_Def `${EXT}` `${APP_ID}` `${APP_EXE}` `${OVERWIRTE}` !macroend !macro APP_ASSOCIATE_DEFAULT EXT APP_ID APP_EXE OVERWIRTE !insertmacro APP_ASSOCIATE_Def `${EXT}` `${APP_ID}` `${APP_EXE}` `${OVERWIRTE}` # Register "Default Programs" WriteRegStr ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities\FileAssociations" "${EXT}" "${APP_ID}" !macroend !macro APP_UNASSOCIATE EXT APP_ID APP_EXE # Unregister file type ClearErrors ; restore backup ReadRegStr $R1 ShCtx "Software\Classes\${EXT}" "" ${If} $R1 == "${APP_ID}" ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" `${APP_ID}_backup` WriteRegStr ShCtx "Software\Classes\${EXT}" "" "$R0" ${Else} ReadRegStr $R0 ShCtx "Software\Classes\${EXT}" "" ${EndIf} DeleteRegKey /IfEmpty ShCtx "Software\Classes\${APP_ID}" ${IfNot} ${Errors} ${AndIf} $R0 == "${APP_ID}" DeleteRegValue ShCtx "Software\Classes\${EXT}" "" DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}" ${EndIf} DeleteRegValue ShCtx "Software\Classes\${EXT}\OpenWithList" "${APP_EXE}" DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}\OpenWithList" DeleteRegValue ShCtx "Software\Classes\${EXT}\OpenWithProgids" "${APP_ID}" DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}\OpenWithProgids" DeleteRegKey /IfEmpty ShCtx "Software\Classes\${EXT}" # Attempt to clean up junk left behind by the Windows shell DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\ApplicationAssociationToasts" "${APP_ID}_${EXT}" DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\ApplicationAssociationToasts" "Applications\${APP_EXE}_${EXT}" DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithProgids" "${APP_ID}" DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithProgids" DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}\OpenWithList" DeleteRegKey /IfEmpty HKCU "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\${EXT}" !macroend !macro APP_TEARDOWN_Def APP_ID APP_NAME APP_EXE # Unregister file type ClearErrors DeleteRegKey /IfEmpty ShCtx "Software\Classes\${APP_ID}\shell" ${IfNot} ${Errors} DeleteRegKey ShCtx "Software\Classes\${APP_ID}\DefaultIcon" ${EndIf} # Unregister "Open With" DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}" DeleteRegKey ShCtx `Software\Classes\${APP_ID}` DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}" # Attempt to clean up junk left behind by the Windows shell DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Search\JumplistData" "$INSTDIR\${APP_EXE}" DeleteRegValue HKCU "Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache" "$INSTDIR\${APP_EXE}.FriendlyAppName" DeleteRegValue HKCU "Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\MuiCache" "$INSTDIR\${APP_EXE}.ApplicationCompany" DeleteRegValue HKCU "Software\Microsoft\Windows\ShellNoRoam\MUICache" "$INSTDIR\${APP_EXE}" ; WinXP DeleteRegValue HKCU "Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Compatibility Assistant\Store" "$INSTDIR\${APP_EXE}" !macroend !macro APP_TEARDOWN APP_ID APP_NAME APP_EXE !insertmacro APP_TEARDOWN_Def `${APP_ID}` `${APP_NAME}` `${APP_EXE}` !macroend !macro APP_TEARDOWN_DEFAULT APP_ID APP_NAME APP_EXE !insertmacro APP_TEARDOWN_Def `${APP_ID}` `${APP_NAME}` `${APP_EXE}` # Unregister "Default Programs" DeleteRegValue ShCtx "Software\RegisteredApplications" `${APP_NAME}` DeleteRegKey ShCtx "Software\Classes\Applications\${APP_EXE}\Capabilities" DeleteRegKey /IfEmpty ShCtx "Software\Classes\Applications\${APP_EXE}" !macroend ; !defines for use with SHChangeNotify !ifdef SHCNE_ASSOCCHANGED !undef SHCNE_ASSOCCHANGED !endif !define SHCNE_ASSOCCHANGED 0x08000000 !ifdef SHCNF_FLUSH !undef SHCNF_FLUSH !endif !define SHCNF_FLUSH 0x1000 # ensure this is called at the end of any section that changes shell keys !macro NotifyShell_AssocChanged ; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we ; can update the shell. System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)" !macroend ;------------------------------------------ ; Uninstall Previous install !macro RunUninstall exitcode uninstcommand Push `${uninstcommand}` Call RunUninstall Pop ${exitcode} !macroend ; Checks that the uninstaller in the provided command exists and runs it. Function RunUninstall Exch $1 ; input uninstcommand Push $2 ; Uninstaller Push $3 ; Len Push $4 ; uninstcommand StrCpy $4 $1 ; make a copy of the command for later StrCpy $3 "" StrCpy $2 $1 1 ; take first char of string StrCmp $2 '"' quoteloop stringloop stringloop: ; get string length StrCpy $2 $1 1 $3 ; get next char IntOp $3 $3 + 1 ; index += 1 StrCmp $2 "" +2 stringloop ; if empty exit loop IntOp $3 $3 - 1 ; index -= 1 Goto run quoteloop: ; get string length with quotes removed StrCmp $3 "" 0 +2 ; if index is set skip quote removal StrCpy $1 $1 "" 1 ; Remove initial quote IntOp $3 $3 + 1 ; index += 1 StrCpy $2 $1 1 $3 ; get next char StrCmp $2 "" +2 ; if empty exit loop StrCmp $2 '"' 0 quoteloop ; if ending quote exit loop, else loop run: StrCpy $2 $1 $3 ; Path to uninstaller ; (copy string up to ending quote - if it exists) StrCpy $1 161 ; ERROR_BAD_PATHNAME ; set exit code (it get's overwritten with uninstaller exit code if ExecWait call doesn't error) GetFullPathName $3 "$2\.." ; $InstDir IfFileExists "$2" 0 +4 ExecWait $4 $1 ; The file exists, call the saved command IntCmp $1 0 "" +2 +2 ; Don't delete the installer if it was aborted ; Delete "$2" ; Delete the uninstaller RMDir "$3" ; Try to delete $InstDir Pop $4 Pop $3 Pop $2 Exch $1 ; exitcode FunctionEnd ; The "" makes the section hidden. Section "" UninstallPrevious ReadRegStr $0 HKCU "${UNINST_KEY}" "QuietUninstallString" ${If} $0 == "" ReadRegStr $0 HKCU "${UNINST_KEY}" "UninstallString" ${EndIf} ${If} $0 != "" !insertmacro RunUninstall $0 $0 ${If} $0 <> 0 MessageBox MB_YESNO|MB_ICONSTOP "Failed to uninstall, continue anyway?" /SD IDYES IDYES +2 Abort ${EndIf} ${EndIf} SectionEnd ;------------------------------------ ; include nice plugins ; NScurl - curl in NSIS ; used for MSVS redist download ; extract to ../NSISPlugins/NScurl ; https://github.com/negrutiu/nsis-nscurl/releases/latest/download/NScurl.zip !insertmacro CompileTimeIfFileExist "../NSISPlugins/NScurl/Plugins/" haveNScurl !ifdef haveNScurl !AddPluginDir /x86-unicode "../NSISPlugins/NScurl/Plugins/x86-unicode" !AddPluginDir /x86-ansi "../NSISPlugins/NScurl/Plugins/x86-ansi" !AddPluginDir /amd64-unicode "../NSISPlugins/NScurl/Plugins/amd64-unicode" !endif ;------------------------------------ ; The stuff to install Section "@Launcher_DisplayName@" SectionIn RO nsExec::Exec /TIMEOUT=2000 'TaskKill /IM @Launcher_APP_BINARY_NAME@.exe /F' SetOutPath $INSTDIR File "@Launcher_APP_BINARY_NAME@.exe" File "@Launcher_APP_BINARY_NAME@_filelink.exe" File "@Launcher_APP_BINARY_NAME@_updater.exe" File "qt.conf" File "qtlogging.ini" File *.dll File /r "iconengines" File /r "imageformats" File /r "jars" File /r "platforms" File /r "styles" File /nonfatal /r "tls" ; Write the installation path into the registry WriteRegStr HKCU Software\@Launcher_CommonName@ "InstallDir" "$INSTDIR" ; Write the URL Handler into registry for curseforge WriteRegStr HKCU Software\Classes\curseforge "URL Protocol" "" WriteRegStr HKCU Software\Classes\curseforge\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' ; Write the URL Handler into registry for prismlauncher WriteRegStr HKCU Software\Classes\@Launcher_APP_BINARY_NAME@ "URL Protocol" "" WriteRegStr HKCU Software\Classes\@Launcher_APP_BINARY_NAME@\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' ; Write the uninstall keys for Windows ; https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key ${GetParameters} $R0 ${GetOptions} $R0 "/NoUninstaller" $R1 ${If} ${Errors} WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "@Launcher_DisplayName@" WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe" _?=$INSTDIR' WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S _?=$INSTDIR' WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "@Launcher_DisplayName@ Contributors" WriteRegStr HKCU "${UNINST_KEY}" "Version" "@Launcher_VERSION_NAME4@" WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "@Launcher_VERSION_NAME@" WriteRegStr HKCU "${UNINST_KEY}" "VersionMajor" "@Launcher_VERSION_MAJOR@" WriteRegStr HKCU "${UNINST_KEY}" "VersionMinor" "@Launcher_VERSION_MINOR@" ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 IntFmt $0 "0x%08X" $0 WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" WriteRegDWORD HKCU "${UNINST_KEY}" "NoModify" 1 WriteRegDWORD HKCU "${UNINST_KEY}" "NoRepair" 1 WriteUninstaller "$INSTDIR\uninstall.exe" ${EndIf} SectionEnd @Launcher_MSVC_Redist_NSIS_Section@ Section "Start Menu Shortcut" SM_SHORTCUTS CreateShortcut "$SMPROGRAMS\@Launcher_DisplayName@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 SectionEnd Section /o "Desktop Shortcut" DESKTOP_SHORTCUTS CreateShortcut "$DESKTOP\@Launcher_DisplayName@.lnk" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "" "$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" 0 SectionEnd !define APPID "@Launcher_CommonName@.App" !define APPEXE "@Launcher_APP_BINARY_NAME@.exe" !define APPICON "$INSTDIR\${APPEXE},0" !define APPDESCRIPTION "@Launcher_DisplayName@" !define APPNAME "@Launcher_DisplayName@" !define APPCMDTEXT "@Launcher_DisplayName@" Section /o "Shell Association (Open-With dialog)" SHELL_ASSOC !insertmacro APP_SETUP `${APPDESCRIPTION}` `${APPICON}` `${APPID}` `${APPCMDTEXT}` `${APPEXE}` `${APPCMDTEXT}` '$INSTDIR\${APPEXE} -I "%1"' !insertmacro APP_ASSOCIATE_DEFAULT ".mrpack" `${APPID}` `${APPEXE}` true !insertmacro APP_ASSOCIATE ".zip" `${APPID}` `${APPEXE}` false !insertmacro NotifyShell_AssocChanged SectionEnd ;-------------------------------- ; Uninstaller Section "Uninstall" nsExec::Exec /TIMEOUT=2000 'TaskKill /IM @Launcher_APP_BINARY_NAME@.exe /F' DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\@Launcher_CommonName@" DeleteRegKey HKCU SOFTWARE\@Launcher_CommonName@ Delete $INSTDIR\@Launcher_APP_BINARY_NAME@.exe Delete $INSTDIR\@Launcher_APP_BINARY_NAME@_filelink.exe Delete $INSTDIR\@Launcher_APP_BINARY_NAME@_updater.exe Delete $INSTDIR\qt.conf Delete $INSTDIR\*.dll Delete $INSTDIR\uninstall.exe RMDir /r $INSTDIR\iconengines RMDir /r $INSTDIR\imageformats RMDir /r $INSTDIR\jars RMDir /r $INSTDIR\platforms RMDir /r $INSTDIR\styles RMDir /r $INSTDIR\tls Delete "$SMPROGRAMS\@Launcher_DisplayName@.lnk" Delete "$DESKTOP\@Launcher_DisplayName@.lnk" RMDir "$INSTDIR" SectionEnd Section -un.ShellAssoc !insertmacro APP_TEARDOWN_DEFAULT `${APPID}` `${APPNAME}` `${APPEXE}` !insertmacro APP_UNASSOCIATE ".zip" `${APPID}` `${APPEXE}` !insertmacro APP_UNASSOCIATE ".mrpack" `${APPID}` `${APPEXE}` !insertmacro NotifyShell_AssocChanged SectionEnd ;-------------------------------- ; Extra command line parameters Function .onInit ${GetParameters} $R0 ${GetOptions} $R0 "/NoShortcuts" $R1 ${IfNot} ${Errors} !insertmacro UnselectSection ${SM_SHORTCUTS} !insertmacro UnselectSection ${DESKTOP_SHORTCUTS} ${EndIf} FunctionEnd PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.desktop.in0000644000175100017510000000074515144136757026701 0ustar runnerrunner[Desktop Entry] Version=1.0 Name=@Launcher_DisplayName@ Comment=Discover, manage, and play Minecraft instances Type=Application Terminal=false Exec=@Launcher_APP_BINARY_NAME@ %U StartupNotify=true Icon=@Launcher_AppID@ Categories=Game;ActionGame;AdventureGame;Simulation;PackageManager; Keywords=game;minecraft;mc; StartupWMClass=@Launcher_CommonName@ MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge;x-scheme-handler/@Launcher_APP_BINARY_NAME@; PrismLauncher-10.0.5/program_info/PrismLauncher.icon/0000755000175100017510000000000015144136757022161 5ustar runnerrunnerPrismLauncher-10.0.5/program_info/PrismLauncher.icon/icon.json0000644000175100017510000000256315144136757024012 0ustar runnerrunner{ "color-space-for-untagged-svg-colors" : "display-p3", "fill" : { "solid" : "extended-gray:1.00000,1.00000" }, "groups" : [ { "layers" : [ { "image-name" : "block.svg", "name" : "block", "position" : { "scale" : 19.28, "translation-in-points" : [ 0, 0 ] } } ], "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } }, { "blur-material-specializations" : [ { "value" : 0.5 }, { "appearance" : "dark", "value" : null } ], "layers" : [ { "blend-mode" : "normal", "fill" : "automatic", "glass" : true, "hidden" : false, "image-name" : "rainbow.svg", "name" : "rainbow", "position" : { "scale" : 19.28, "translation-in-points" : [ 0, 0 ] } } ], "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : false, "value" : 0.5 } } ], "supported-platforms" : { "squares" : [ "macOS" ] } } PrismLauncher-10.0.5/program_info/PrismLauncher.icon/Assets/0000755000175100017510000000000015144136757023423 5ustar runnerrunnerPrismLauncher-10.0.5/program_info/PrismLauncher.icon/Assets/block.svg0000644000175100017510000000575015144136757025245 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-10.0.5/program_info/PrismLauncher.icon/Assets/rainbow.svg0000644000175100017510000000640615144136757025613 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-10.0.5/program_info/modrinth-mrpack-mime.xml0000644000175100017510000000056615144136757023233 0ustar runnerrunner Modrinth Modpack File PrismLauncher-10.0.5/program_info/AdhocSignedApp.entitlements0000644000175100017510000000060515144136757023725 0ustar runnerrunner com.apple.security.cs.disable-library-validation com.apple.security.device.audio-input com.apple.security.device.camera PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher_256.png0000755000175100017510000002617315144136757026011 0ustar runnerrunner‰PNG  IHDR\r¨fbKGDÿÿÿ ½§“ IDATxœíw€UÕÕöŸ}νwzïÞAA‰Št(–øŠÆ˜7bùÔ$¯&Á$M¾Œ1K,` ˜DóÅT1É+b7R ˆ”z‘©ÓçÎÜ2sË)ëûcft†iwŸv÷ï?eîÞë–ýœµ×^km@ @ @ @ @ }af 0‡ßm½u Ѹ¹/å)š®W ÿÃ̶I`ôOÔšmL¼ ÀžÚ°0)91ånFì~éfÛÓ @'­=×–˜üËñ/”Í6ÆéàØ‹[] Æ0Äl{N‡ “ûÅÀÁß‹£C~àÄo·.úŠöb¯Ï @'Ø=C÷è³ q"Bb̲ÊE#I›Ùï 6í'LÆ÷ÿö±ÝfÛâ$„Ĉßm¸5[÷ЉÑ÷$˜mOo±‹t ô²¦è?ñʯëÍ6Æ  Ë·ÜîSoèaô² •°™tÒÆ…–¸dèÊÃfcg$³ °3˶.ºLÙ ÐrØpñÛ˜L= ©íÓC·Üû-²~#>¸~ðb墩ºÎždÀEfÛ2PlêœÌ&¸gøKýÇlC솀>ðü¦›Ë\2{ oÂ!ŸCÀÀV1à{ƒ_zôÙ¶ØGüˆy³|ËíÉ`Êýîh¶=±ÄIî$-R0:ø¤+ÒúûÑ+­fÛcuD  :öù»<‡-~'’Qvƒp¿êIÙ¯>sǷ̶Çê –U.Éti)@—šm Oœä$g*È|òÝ%´Z×\w%,~^äœ!'Ñîî«?è>Øè<¿¿8E Æàòœ¶©Âo\’ûgìîgýFÛfeÄ  ]Žõ~Ž8XüN"5?ÒÝâ7îVIÙ§>sÇ·ˆÄƒ¯ñAxaó-£eI_ °K̶ÅhœàÈnBÁè$™zû’uº¤ÿOÂ]/îäi—ˆkX¾åö õ'Äèö ˆžrIžŸ³»Ÿå®8Fã¸-Àò-·LS¶1bB,~G’Y6jñ€ŒÝ«’²SyöÎ9FMjŽñ^®\”%ö ÿ /ìæ$¤jÈnZ·p"àON :b¡¼¸õ–ë¢:ÛßäsÄ{œdÇ4êßg :‚„»Ô¥w\k¦!±ÂÖÀo7ÞV@.í9bpÄ—avòRr£È*1UNf•KÃlñ²fÒ_lû´\¾eÑBÝ­í‹?>dBz¡åú.PeÚ]òíÛÍ6¤¿ØÎX¾åö"0õ€®6Û'` ³$ŒÔ\ á o»4évÏ ÇÌ6¥/ØÆè¸fëÿt”ëŠÅG¸5¤äXxñÃ|Õ¥ïŒ.ùömvJ ²…¡¿Ýx[îÖ~ à ³mqvðr‡µ!1­_?&Aï¹4v‹b–÷–UÜ|­îÖvA,þ¸$)]µÙâv‰*Ó.ué7šmIOXÖXºñÆô·ç ¶ °Ø+{Œ FáJè¶ÓõaXáR•;Ùâß{Í6åtXÒXVqÓÜ·gÄâkRs£ö^ü@X¨ÈîJ«fZÊ(_]î*J?ò0€Áb¶9«zýhóeut»š ÆÊËcÒÀ XÆøíöÛJ‹Ò¬ðcˆÅ÷¤„´øöµvŸšU»>ôôCÌ6¦KÀ²Š›æêš¶Àt³m˜;IGJvÔl3xqžKÂfõÙoÏ7ÛÀä'í«¯.”½#Rb„û̶%^±â oD+R,ã%óBð+WQÓÏÙu+4³Œ0Íx~÷wR½ÃSÿ—~±ø$gFãañíkïµ6çMZzWº™FÎò-·¹Âá5 X`ÆükÂ$ ½ÈⱆðU•”èÉï”™1½áð›Í·NS64Åè¹Ö&-?|¦Û}œÌÙªKß]zç9FOl¨,ß²è,IÖ×dä¼ë#»u¤å96ð׊чF‹€a°|ËíEØû d5§À>dÚæË¢°LFx‹žÿN¡Q3&MïŸxBé™FÍ'èžW Få\ˆ ËnDÎW!uú¹R“M³Ç“¢!¹·û8ò 04Ï5Ÿ!Ñ÷Ÿ.œ1T—´*摤Œ©YHŸ– æca(M‡ñysQ–>s}é_IÓÚõ)~ŒÐîíÝï "dždÓN¬´!ÔQ’N2 ª®áYcŸ9Ì{ZWÏ2pHR¿0‰¢:ZÖ7!¸Ç¬yHjÄôqÄ\‘uÎ.¸ÙI¥Ýþ“e$O‹ä‰c¡Ô5"ðáǬۊò}2'gGã~ñë9 Ê8” Ã+wËêU–ðžÛ iØOµYAÃëÕHœ„ì¹ùpç&aF\á‘“0*g:&\ŠwVŸ^ë.ÈEö W ãòÙ¬Ùÿ¿7@¶ÅÜFI"dÄÛ±_(…A'AÏ;7Ìh. Cü𮿰†€Ó6$†Ô³Ò‘5#R’¸µk $ºR1±`>ÆåÍ[аR$ŠÀšOà{{ ´@kLÆ »ÝÇrІËP‡°3EáN¤|®{—-Fp€ûo˜VÝUÛÓßI‰22§g#õœ,0è3n)ãóæbRáåðÈI\æ Hþ?†ï­5ÐÛÖWöè(ˆ¯ïšZ‰uŒêÅ…ôL– S‡-©ãiÿ-€îšÜ«? kð~Ѐ@¥Ysò4Ô¼¨´pI ˜7 ç#ANá:Kð cþL¤Íþ «7Â÷æjè¡þuêÍ,ÇÕâoßçK ´>¼iMŸà=nFÁ °‰ ½(+MÔ¯8ޤ)Èž›W†›£uöEb.ŒÎ¹SŠ®F²;ÃØ¹څࢩð½³þoèS°0!MERF|ûQƒ:V‚VØwµÓ‰&ÁîÀ@£ûóºPU+ª?;Œ´Iȼ(Ìc‰ÊeÓa`•3ç_Ýçà^¬‘R“‘uí|¤Ï»-¯¿À¶z©¼ ÈŒýµÞÖCÔaÔáR¿³m$†Q±5êTŒ8Üß’FðomAÛþ 2.ÌAêÙéˆ+¿ñ$ŠÓÆâüÒë‘“d­Lj9397ýÒ/¹Í+ÞFÛöîoÓNÍŽÂäìc?­ˆµïó“ö[%ÖÿµÓ[Œ€!@ ªhz·í>dÏÍGBI|]ú›™Xˆs‹¾†aYSÍ6匸‹ò‘÷Mí©BóßßDôxÍ—þ]’ i~úSFDZ^VŒRÄ_¸>N.\(”ªCb¶‘g H•ЬÙùp¥’Æ`‰®Tœ]p)Îοä”Ì=ËC„àÆJ4¯xš/È, !5Çy?”¨#ehe,Ö+*’:â¹$Ö— Zá*|}F‘v”ÇØÌ-!ã¼,¤%LvÖ¶@b.ŒË›s‹®æv¤g‰Â÷î:´~ø!ò‡6;k'1hƒÔQˆ“>3—^”:ô…Ñû ×ÇŠm¯ï›”ö´âÀ2gä!u|§™ŒeHÆ98¿ì¤yrÍ6%&°2¯œ‡Œ™“¡¯ TUi¶I1AË— Ž“@œO«5UÀž żKJ´€Š¦7kÜÑ‚ì¹ùðäÛ3­8=!”~ƒ2Î6Û.°ŒlÈ—Ý:þ)ôµ5Õôü" Bé€2V†žcŒ+#•ðŸ«Q¾Qíþ"ÇB¨ùG‘2> Ù³ò %Û#­Ø%y0±`>&\Yr~Î+ù† ï\ýã•@ÔAAr3¨£´A’Ñ,óxÎ7²D,ÏЋ­»üU‘1-içfZ:>08c.,»©ž³M1I†4q6¤“¡mø_ÐÞO~q®Á­ŒA%÷*}7öó×¶Í|@Bžß«ÖѼ¶Á-Èœäá|SdûJFB!.(»eég™mй¤d@¾ø&и  ¯ù;¨ÉZ—éê¹ ÊØ>¦ïÆfg€¯ûÒŠWAÃ?N qH2²çäÃk†„,¹1¹ð L,˜‰Ùc‹b¬d$ä~½òßÐ?Y¨æ¦ SJ{d_+²„÷hcàl|o nCÍŽ íœ dLÏ”`üâ+L‰ƒ!3±Èð¹m$Cšr ؈ÉÐ?ü3èØ>ÃM И¾Ë; ˳ÊÞŽôö´âàž2/4®ìØ#'áÜ¢¯a|þ\0qÿI°Œ\È_»´o´u+€pìút?iG™îhJ°ÜwdßåX-ñCµ—w=7 eüreLÄô²ÿFªG4Bî lÌ4¸…¶þ_ ½¹Í¤gw”é¦[ì‡ú\B¸ c°làh}µí(;ž“WfìŽà’ݸ ìF Ë<7fcÆ%ÉéíAÂa“ ­ù+Ðê‹ÙÐ”È Ž– •Xváwµ֛ۻ/_4+1RÖ6Æ ˜‹!mJ&2ÎÏ4À²ãÒô ˜5øVÃkôO4íÿ€>Ý2°qd@Ì ”A6‰Ã¦FsØør.EÜ<€h$”aPÏÑC*ÁÿI3Úv13)ãÒú%7¦/Ä„ü¹wrÀ“ùÒ[AC&@[ýW@é{/A-_‚:^Ù¬¼"àªNÐÈcln+Tb é:ìU÷­;ÒŠ+š‘5' %½û¥d%cîÐ;dÊýŽq3 rÁ`èï¼j8Ö«×PFûy¾žmOaÖ™l? ]³­© £î/Ç4*Ùsò!§u÷11LÈ›‹i% ã"×*°¬BÈ×ýú†ÿ…^ù!º;i¢u¤Ä£L×PÜÌÅm-ñèV:Lí+D@Ûþ ‡Z‘~^ö)eÇsaÆàE•}‰VÆ1² ÒEׂ ƒöÞ¾œ<Äm°ÄµL×Ht רÜ>9,’0t…N);–%7.þ=”¤3Û¼¸‡˜ 9) ÚëÏj´£L—’müÈ? qó¸=¢Û=çÐYv\÷·ã˜šùu±ø-+ }Þ ˆN•¡œ+9jñ€ÄÑà&Œ˜#/þ“š=prðYU"‘ø»ÕÆj躎ææfÔxò¡æ[6íd@ÛZâèÀf‡-½£dÄ áPŸ8€ê' ÷Ô [sˆ@LJÏçAB” 5Û,.0Ưï¿ Q²³Àµ“]øEKn"‚ÏëEÀçCn~>rrÑÆËê„Ãax½^D£_ÎQ×´yÓ Æ¸=LyÆHédäŸòÿtMC}M Z¼^•–"9Y\kÆUUáõzÑÖvú›ŠU¾ióæ¡ó[K¥$ŽÝŒM#%½ûÛx¢‘Ž<ˆä”””•Áå¹±€ˆàóùÚ]}êþ7EÌaO†ì·à¹o1 ÆSzȶµ¶¢jÿ~¤gf¢¸¤$®o3(­­­hnn†ªª=þ­FÉhk9+&Ø-= ðb$ëáIL†ìêÝGFDð57# ¿°™YæÞãg7"‘¼^oßNZ˜I`@!²£àÀ@rZß™4UEÍñãh¬¯GIY’D|àŒhš†––~½^g)ÈYÀs-ñB29ÌHJín“âð¡CHIMEqi)\½ô$â"‚ßï‡Ïçб*±T€êch™ù0;ƈÁž7tœ¤~x_‚­ªöïGVv6 ŠD@hkkƒ×ëíÕ>¿'4J‰ÝE”tÆí6\ž!Ç=â“c“Eºoc#ü> ŠŠžá¨¬é^FáõzÇîrb)ViC;¿Ö%ü)ƒËi_„Ë[§FUœ8zMII(.+CB‚㜦Ӣë:ZZZà÷ûc?¶óž;ñ»šã§Åd§Iq¬ “δ┴4””•A’ì[F}&:Ów[ZZø¥O3sï~à‚-=Ò]¶îÂp\n~Oh"BÐïǽ{‘•›‹ü‚ns™A(‚×ë…¢ð½ôC'§E²£ÀÀdg=ÿ·‡[,æst]GS}=ü--(,.Fjš½³ÛEAsss·é»1ÇcüÚšpX\ÎzþóÛœ%űÇ‘˜œŒ’ÒRxlÐu>Ÿ~¿ÿŒé»±†w€øÝ#Ç3È&M—{ËmüÓ%ÜÖ†C =3…ÅŶˆƒA477CÓŒo Kpž;zœÇ6·@W:ÓŠ~?ò cÍëÄ»+Ó5rÞÏΦA@!1G×4ÔUWÃÛØˆâ²2Ë”«ªŠ––ƒA³M90`Óc@çm$—5~\J4Š#"%-ÍÔ´b"úü<ßÈ}þ™qÜsÄñMq<„Ëa§€`²µöß­ªöíCfv6 ‹Š -;îK™®‘Yë;Š<ãi<Ëe‡­HÌz?."BsSü>Ÿ!eÇÑhMMMÖmˆêÌÞ öósž/få|gÙ±·±E¥¥HJŠmé@Ët‰ìè81˜YÐ8™H8üE| ¤¤× LºÃôÝXbƒï¨ØÏ€#.e: {]ù<­8PÙq,Ët‚œÖ„ϵ$Nú€$Ùë-õ·ìXQx½^„B!ÎÆ;xi}dž™€œÇ6fÓSoËŽy–é9íè©áX»y'Ó]Ù1! ¢¥¥Å”ôÝØ"¡½ ÕQñ'{yåå¢{(Å6õºòy|`ß>dçæ"5-Í2]#!8­2ãpцãÜtAïÐ5 uuh5ªL×@ß? ºe2cÃ##øÈ—ˆIy9t8Ì8ìGåT¬“–340ÆåMñ ™Ú}3y ºî¸·ä8t">yø­%ž`ŸÃã^âÀ'‹ãpäò縖„Є`}t²A¶bß±£0Çy°C*¬À‰Øq @Ž{\’3Ÿ.ŽBx}ƒ›9/`‹b˜8Ç¡aû s` €„XŽüŽl¸`Â#=ŽkIœô]5¯Û­ w¨Î =·Ó" (Q‹¶Á|Ž¢9îgØr @`*9îg²c¸P±°:ŠíË™OE²£ð4Ú,TáX'Æx>L9zÎû&”hØl=uf À~˜ä¸oBU„`uœ€='¦‹-€õqè)€ =¦ °>ª¨èâ°„[­}+ŽhSœ§±e÷¸ }f› è긟$7Uãw Àà¸n“mB,O«=Ƹ­%ž‰@ö»V¦„`}ZUÇýìÀˆq{Sü8ª–YDC­ÐTçôÏw:Íyßìçèºó<"B¨Õ¾×f96%ìÔÆ­öÝqmþ³MtƒO€qÜNó<t¤´4œ0ÛA74…ééÌŽàÀ4×3ÛA74…¤•ì¹p¦p¢j—ãîsàx°Ál3¸À3žÆ1€‚¼Æ6“úF/vlÝh¶‚“Øv؇êz{_ßÞ·µÄív`Éç¤kš¢¡6 ÂÖpü//#5=#F3Û,€5!üqmµ i­(\‹Ä$çdê$qÛÛðÛ0Ý2uA#ð…ÛS²EÁ¬_c®qT~ÖŠÞ­¢¶?l¾|ºkªBÓxƸC’UnÑMn³}H¶%¤¡& BÕOõdt]Ç¿þþGÔVÇeW_·Ûm‚…ñ‹¢^ßÜ„µ{|§´'¡¡6Íé((iDn`æØ TŸÀo É>²i¶B¨ö+hSÎ\ZJDذîߨ:°߸év• 2ÈÂø¦®%Š—×ÔãxÓ™Ý|UuáÄ‘Bx3Q2¸)iö<˜rs|˜òˤˆí<E'÷)¨jŠô¸ø»R_sÏ?ùK¬_ó¾S3Ñ,æª _y¢ÇÅß•Pk"ªö Ágû!õp´Ž.b?À“䋆쑗M4¶ª¨oUqo¿W(Š‚•ÿø öíÙë¾yÒÒ3bkdœŠêøëúTê@Üß’ŠàöáÈ-lBAI#$ÉÍCªsS¸5¢àº3ºÿú ÃxÎ1P Õ~Q-vOî´ô \uí8뜩1“Iiif›Ð#•ŸñÚÆ&øÚb×cÆíQPTV¬ŸÕãm\qw ¯Á¹y@cÈç9G +„ꀂÖh쟿zéŒ?×\ÿ-ddeÇ|ŽxÀÒðê†l;Ü󱕨G– ±.Ńk‘’jÙ¼5®§i\ÏIC#ÏñûƒF@u@Ŧ—Åß•}»·ã©_ý´=6 .í5Dí{ý‡ÿqŒËâïJ[0 U»‡âèÁb¨ ×ça¿`@=Ïñù¾cF ‹øW4¶i¨ *ýÞç÷‡p(„•ÿø ¶Wnµ__„ü¢ã&·! ~[߈ýÕÆFì›3áó¦#¿¨ ùÅ`’5‚¹:À5¿™¯èR˜ùd0ª£Æ¯ ¬šgË‘CUXòøƒ˜óÕ˜5ï2È.ë=mÌDÓ÷w4ãÝmÍPbé º.¡öDš›2PXZÌ d1; gã{"¢j k¸ßªªà½7ÿ…ŠMpåµ7bô¸³Ì6ɨ áÕQÓl»#aŽT•¢©¾%ƒk‘˜l^Z1#²ï€ÌÐr ­[ÕS²Ä¬@cC^úÍS7a®\x#²²sÍ6É|m*^ßìŦ*k¶[úÛÓŠ³óZPXÖ—Ë„N÷dg€óþåh k¨ ªPMr#ûž]Ûp`ÿnÌœwf]|yܤk:ðÑ^ÞØÚ„ˆbí¡©> >o: JSÐ fà¶–Iv†£ [£:ª*Â}Èೊ¢àƒ·_GÅæqÕµßÀ˜ñÍ6‰+û«Cxm£uÜýÞ¢ª2N)Dc}6JÕ"-Ó˜jwŒëI_ÐØ pަ*Z{µ^sÈžuxëñò²g0ö¬I¸âšo '7Ïl“bJ³?Š×6{±ó±o"!í„ôÌ J†Ô“ÀWÈ$ÂqžãóY:ÂëŽP€†VHßµ"{wnç{wá+Ógã’Ë®FbR²Ù& %¢bë°aía|6j$À,r,<@ü-©lŽÜ‚f”ÖC–ùxž¤ëG¸ Ü×ocáÂ…òH©: ¦›[_¸½L׬ã"£HNIÁ̹—á¢Ù—p;6ä• LDØ·é>úç´ùÛ£è ƒJáËw^ÀÓíQÛãùͱ:üÈ‚»’y¸öPÚ³g]4aÐ"Y±/¤è8ê‹¢±UsÔS¿;EAÕþ=ØQ¹ ié(àDäNˆ}©Æ±}õX¹ìcìüè3(‘/<À¤`üyÙ É::Ñ5 þ–4ø[R‘˜Ç³Ó‚Cþʳ±ìt‘rÀ°  j„ºV Þ uë5 õuøÓK/`ø¨±X𵯣¸Ôš}šjüøÏ?wâ³]µ§ýwIS‘U]‹ÆA¥[f ¡Ö$TíŠÌl?ŠÊêàIX5,ÇÆ²îá. ìp{M@EóB $IDAT&¤ïZ•ƒŸîÅÒ'Ää©çã«—_c™"£`K¿±{>>Òc?„ÌúFøórMJ2È:ƒ! ¥)þæ4ä5"¯¸Rÿá\÷ÿ€ Ÿö'Ðà‹è¨õ+1-Óu¤ëØúÉzlÛú Î6Ï¿ i™¦ØnbË{ŸbÛê*¨J¹G«Q=z8GËÌG×jOä¡©!³ßeÇàcÝð÷H«ëýž/¬jý œ+õ쎦ªødýTlÞ€ó.˜‰9—,@jZº!s‡[£Ø¶æ *>øÑpß÷»ÉRZ|hÍt~ӔβcoCVGZq_®/£ n†uÀ]< ¬2Ú‹£RUê[U4µšnic”hë׼̈́ fÌÁì‹/çvt¨D4l_[…ÍïìGd€Ýžr@[zšã‚Ýô'cÿÎaÈÊmAÑà:¸]={L(ÛyÛeÈ¡ìý×_x@ñiÿ‘oHE]P;m÷]AßHNIÅ̹óqÁŒ9ð$$öø÷½9T"*v¬;„ÍïîG¸5v‰/M¥Åh.´d¿®È²†üâ&ä6©ìøè#WÜ=˜·-†Ô¤2P"Áˆ†ê€Šˆ‰eºN£­5ˆ·W®Àê÷WaÊ´é˜9çÒ~ Ûüìøè¶¯9ˆP0öqÙ5µðggAóÄG D'š&£æX>¼™(T‹ôÓ¥3Ta‹AEéìßtþWDÕQÔ>¿hC{¡Ö¯yô!ÆŸ=³æÎGéࡽzmSµÛVWaï'GûÜë+LÓ‘S]ƒú!Ö<ÖäM$äÁgûö6#FøÀŒñtùu’´'uꃊÔЦÅåy¾蚆•›±³r3FŽ gÌè±NÉ,Ô5GöÔaÛêƒ8º¯Î°2êôF/üy¹§Ø;åy tÞf”›ßŒ‚ÒȲ¦© •FÌmXböWžÿj]PYh‡2]§“œ’‚±&aüÙSæ†Ã»kðÙÎZ„Û̩Р§¦àø˜‘¦Ìm5dI‹jXñâmßüo#æ3L®¹hJ‘"¡@¡Qs z‚aXÒ×Í6P7l0Ù1É·;Õ²ŽÉ+™WgÄd†Áüó£­5á^£æ؇œcÕDçäÒõËZü€¯¯ÛºSgl& HqØ —¢ «Ö°ß½9Aº>û­G.Ùf䤆ga¼¹fË.·Žó¶Åè¹Ö&³¶:ňíºKþŠÑ‹0A€öí@$!:‹ Št ìÓu䜨6Û £yGŽ&^ôvùl®ºÃ´<Ì÷ÞÛÑš”?üFø%€¸ßü ÚIõ¶ 1`ï¶a½DÃ/’÷{¬||ºim‘-ÑŸéÊ™Sfðˆƒ±Î)@W"II86~´Ùfp„5è[oýrÞ;f[b‰JŒ•k·®v)ÊD’ý$°6 ¡ÒšÌ6ƒëd—6É ‹°ˆÀ¿6쨲ôù{bK÷䜨†¤9*U\gŒý2y¿wÎÊòK,è°Äàd®š1õéà쮦cÍ-@'-…ùh,=}©Í8Â@‹Výòâ5fr2–ñºòúºÍÔ°k2À^4ÛydÔ5ÀîK °jÂ9V\ü€E=€®\9cò5ÄØrÎë'm:Öö 5#5#ÔSÖ$X#ܱêá¹ÿ2Û’3aI ++×UüÓ¥(ãEÎ@|’âó#Ågkºûá]Ù¥M²úâlàt]1ãÜ[Àè×Ìé‚é8¬ï€’˜€£ãÇ€¬«P ÝóæCó^ ¼AtXÞè½±nËï5&e€å•U;Üá2Œ½hºïÐ[.ÒÎ~ó¡‹_²Ëâìå|‰+gNYHÀóœu‹¦¡ØÃ]–qä¬qÐ\\/³êÍ`¸ï͇æÙ2`m'àK¬\»u…KQ&ôŠÙ¶ø#irªkÌ6ãK0`•ìÒ'Øuñ6öºÒqRð€"³m±öñ:9:n4¢É¦ß*TÈ}×A¾ž°­Е•ë*þ™èÆXK!²MþQSŠæ:!zÅ£á,',~À!@W:²—˜`¶-ÖÇ~ÔŠ`–Á· 1| †;ßüżկNÌGx]y}Ýæ –v#|@\ԕƹÇO€×>,Ì@ê-ѳ¶øz]¹|úÔa²¤¿@ _5ÛkbO¼%EðpƒÀÖÊšzç¿úê>®™ˆ£ “+gMþ&{@üÝCuFì+$I82~ ÔOÌÇf@½Nì‡o=<×ñ'LŽÛœŽ•k*þ$3m4Úƒ„âöQÀt9Õµ±Vg W˜K‹ˆ +WÌš2†ˆže`ó̶Å|ìëtr|ÌH„SS<›e†ï¾ñмÍ10Ë6Ät²`æä+ØóÊ̶Å<ì/‘”d;j CÔ2F÷®zhÞ+vJáq±8«ÖV¼IPÆ2Æ—½¨@BkÒššûóR•1,(4fÕCÿ1?Ç@W.›~Þ(—¤-¿Óû{ ¹]82at¹wÏ3[ ¦ßõÖCïälšåÐ…ŽmÁ³›m‹18C ¹¨M%=f‚×0F÷Å«»:âv p:V­­xAŒëØDz|À2dÖÖÃîö+SÃRÝkwÿt ̘4’1ù—™m ?œã@0+µÃ‡~ùVK’t×ÍÙmŽUÖF@tl –b¶-±ÇYÕ#‡£-# N0F÷·?ñÝ!¶=б-ßq…™Í[Ô:Ÿ¼#GO€áÉ%Ï ÌŸ5­ÔEÚϺ ŽO‡y «$IþÞÓoþàÙ¦Ø!ýàÊYçœK$? Ð ³mÎ61àž%ïüð?fÛb7„ €ŽøÀSF˜mKÿ°¹0vÀÏ–¼½Xëõ¸±æ±jmÅEAŒc ;X½m­“h£û|¡Àè%oß#Žõ€ðbÄåÓÏÊ’\ž{ÑÞˆ$Ál{z‡í<…/»túÙ¯ßûQ½ÙÆ8!1¦#àa×ÂòŸ¯}€€d’¾ÿô»‹Åy~ ±øÔ¾\5{Ê4M§§ØfÛÒ=6†­:¤{ž}{ñZ³Mq""À‰×WoýdÕÚŠé ¸ÀgfÛcCŽÃYÓ‚ç‰ÅÏáÀÂñã=‘œÄoà ng{&,é´2ЯYšüØÓ+‡Ì6Æé0¯]pv¾êvÿÀÿà6Û‹ @ÀïÜ:=(|Æ!À®˜>edö@·0ñ²;K€àI÷?ýîâ*³‰7„˜ÈU3N—¤r˜vb`®ð1öãgß¾§Ò4#â!àªÙS¦é:~`¶±3›&t&Ý/‚{æ#ÀB\9kò¥Dìa“™Ñ``ØJºôÓ¥ï.~ǸIgB€¹jÖÔyDú£Lá;“AÀ° Ä~±äů‰´]k!ò,Èëk6°ríÖ©º@…Ùö €Ý`즬iÁ‰KÞ¹g…XüÖCxÖ‡-˜9y{À91š—°Œ=VZöç+®ÓxL ˆ BlB9 m™1y!cìgÅfÔ˜ ÀN0öpÖ´ÀŠòòrîïô!6äªYSçi¤=8ð:ƒX «ð¨ØãÛ!6æŠYS¦ƒp/€Ëѯïr ÀÖKLìé·ôƘˆpÙ¬©“dÐ} ú/®Þ¿²_ xIúcϼõãm}}±ÀZp×\4¥H•ÙDôy=¿¢/@ é%U¦ß<ÿÖ ÄNuà@Žï‰ä&^¥ƒ¾æ8Ao€U£årªôЍÎsBNGšñ(üò¿v+µ{U"öç§ß]¼‰¿•³',\¸PŽ6ž­‘v» íb@Ã’nèü Ôì-þZ“V¶FœßÇB┫çL®jîqÓ®J²|@\¦!@ @ @ @ @`7þ?^›ìÐ×WIEND®B`‚PrismLauncher-10.0.5/program_info/org.prismlauncher.PrismLauncher.Social.svg0000644000175100017510000000701115144136757026624 0ustar runnerrunner Prism Launcher Logo Prism Launcher Logo 19/10/2022 Prism Launcher AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke https://github.com/PrismLauncher/PrismLauncher Prism Launcher PrismLauncher-10.0.5/.clang-format0000644000175100017510000000065015144136756016347 0ustar runnerrunner--- BasedOnStyle: Chromium IndentWidth: 4 AllowShortIfStatementsOnASingleLine: false ColumnLimit: 140 --- Language: Cpp AccessModifierOffset: -1 AlignConsecutiveMacros: None AlignConsecutiveAssignments: None BraceWrapping: AfterFunction: true SplitEmptyFunction: false SplitEmptyRecord: false SplitEmptyNamespace: false BreakBeforeBraces: Custom BreakConstructorInitializers: BeforeComma Cpp11BracedListStyle: false PrismLauncher-10.0.5/CMakePresets.json0000644000175100017510000001413415144136756017217 0ustar runnerrunner{ "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", "version": 8, "cmakeMinimumRequired": { "major": 3, "minor": 28 }, "configurePresets": [ { "name": "base", "hidden": true, "binaryDir": "build", "installDir": "install", "generator": "Ninja Multi-Config", "cacheVariables": { "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", "Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}", "Launcher_ENABLE_JAVA_DOWNLOADER": "ON", "ENABLE_LTO": "ON" } }, { "name": "linux", "displayName": "Linux", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" } }, { "name": "macos", "displayName": "macOS", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }, "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" } }, { "name": "macos_universal", "displayName": "macOS (Universal Binary)", "inherits": [ "macos" ], "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", "VCPKG_TARGET_TRIPLET": "universal-osx" } }, { "name": "windows_mingw", "displayName": "Windows (MinGW)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" } }, { "name": "windows_msvc", "displayName": "Windows (MSVC)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "$penv{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" } } ], "buildPresets": [ { "name": "linux", "displayName": "Linux", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" }, "configurePreset": "linux" }, { "name": "macos", "displayName": "macOS", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }, "configurePreset": "macos" }, { "name": "macos_universal", "displayName": "macOS (Universal Binary)", "inherits": [ "macos" ], "configurePreset": "macos_universal" }, { "name": "windows_mingw", "displayName": "Windows (MinGW)", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "configurePreset": "windows_mingw" }, { "name": "windows_msvc", "displayName": "Windows (MSVC)", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "configurePreset": "windows_msvc" } ], "testPresets": [ { "name": "base", "hidden": true, "output": { "outputOnFailure": true }, "execution": { "noTestsAction": "error" }, "filter": { "exclude": { "name": "^example64|example$" } } }, { "name": "linux", "displayName": "Linux", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Linux" }, "configurePreset": "linux" }, { "name": "macos", "displayName": "macOS", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }, "configurePreset": "macos" }, { "name": "macos_universal", "displayName": "macOS (Universal Binary)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Darwin" }, "configurePreset": "macos_universal" }, { "name": "windows_mingw", "displayName": "Windows (MinGW)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "configurePreset": "windows_mingw" }, { "name": "windows_msvc", "displayName": "Windows (MSVC)", "inherits": [ "base" ], "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "configurePreset": "windows_msvc" } ] } PrismLauncher-10.0.5/shell.nix0000644000175100017510000000034215144136757015622 0ustar runnerrunner(import (fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz"; sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU="; }) { src = ./.; }).shellNix PrismLauncher-10.0.5/default.nix0000644000175100017510000000034415144136756016140 0ustar runnerrunner(import (fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz"; sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU="; }) { src = ./.; }).defaultNix PrismLauncher-10.0.5/README.md0000644000175100017510000001547415144136756015265 0ustar runnerrunner

Prism Launcher

Prism Launcher is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.

This is a fork of the MultiMC Launcher and is not endorsed by it.

## Installation Packaging status - All downloads and instructions for Prism Launcher can be found on our [Website](https://prismlauncher.org/download). - Last build status can be found in the [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions) tab (this also includes the pull requests status). ### Development Builds Please understand that these builds are not intended for most users. There may be bugs, and other instabilities. You have been warned. There are development builds available through: - [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions) (includes builds from pull requests opened by contributors) - [nightly.link](https://prismlauncher.org/nightly) (this will always point only to the latest version of develop) These have debug information in the binaries, so their file sizes are relatively larger. Prebuilt Development builds are provided for **Linux**, **Windows** and **macOS**. On Linux, we also offer our own [Flatpak nightly repository](https://github.com/PrismLauncher/flatpak). Most software centers are able to install it by opening [this link](https://flatpak.prismlauncher.org/prismlauncher-nightly.flatpakref). ## Community & Support Feel free to create a GitHub issue if you find a bug or want to suggest a new feature. We have multiple community spaces where other community members can help you: - **Our Discord server:** [![Prism Launcher Discord server](https://discordapp.com/api/guilds/1031648380885147709/widget.png?style=banner3)](https://prismlauncher.org/discord) - **Our Matrix space:** [![Prism Launcher Space](https://img.shields.io/matrix/prismlauncher:matrix.org?style=for-the-badge&label=Matrix%20Space&logo=matrix&color=purple)](https://prismlauncher.org/matrix) - **Our Subreddit:** [![r/PrismLauncher](https://img.shields.io/reddit/subreddit-subscribers/prismlauncher?style=for-the-badge&logo=reddit)](https://prismlauncher.org/reddit) ## Translations The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at . ## Building If you want to build Prism Launcher yourself, check the [build instructions](https://prismlauncher.org/wiki/development/build-instructions). ## Sponsors & Partners We thank all the wonderful backers over at Open Collective! Support Prism Launcher by [becoming a backer](https://opencollective.com/prismlauncher). [![OpenCollective Backers](https://opencollective.com/prismlauncher/backers.svg?width=890&limit=1000)](https://opencollective.com/prismlauncher#backers) Thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/). JetBrains logo Thanks to Weblate for hosting our translation efforts. Translation status Thanks to Netlify for providing us their excellent web services, as part of their [Open Source program](https://www.netlify.com/open-source/). Deploys by Netlify Thanks to the awesome people over at [MacStadium](https://www.macstadium.com/), for providing M1-Macs for development purposes! Powered by MacStadium ## Forking/Redistributing/Custom builds policy You are free to fork, redistribute and provide custom builds as long as you follow the terms of the [license](LICENSE) (this is a legal responsibility), and if you made code changes rather than just packaging a custom build, please do the following as a basic courtesy: - Make it clear that your fork is not Prism Launcher and is not endorsed by or affiliated with the Prism Launcher project (). - Go through [CMakeLists.txt](CMakeLists.txt) and change Prism Launcher's API keys to your own or set them to empty strings (`""`) to disable them (this way the program will still compile but the functionality requiring those keys will be disabled). If you have any questions or want any clarification on the above conditions please make an issue and ask us. If you are just building Prism Launcher for your distribution, please make sure to set the `Launcher_BUILD_PLATFORM` to a slug representing your distribution. Examples are `archlinux`, `fedora` and `nixpkgs`. Note that if you build this software without removing the provided API keys in [CMakeLists.txt](CMakeLists.txt) you are accepting the following terms and conditions: - [Microsoft Identity Platform Terms of Use](https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) - [CurseForge 3rd Party API Terms and Conditions](https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions) If you do not agree with these terms and conditions, then remove the associated API keys from the [CMakeLists.txt](CMakeLists.txt) file by setting them to an empty string (`""`). ## License [![https://github.com/PrismLauncher/PrismLauncher/blob/develop/LICENSE](https://img.shields.io/github/license/PrismLauncher/PrismLauncher?label=License&logo=gnu&color=C4282D)](LICENSE) All launcher code is available under the GPL-3.0-only license. The logo and related assets are under the CC BY-SA 4.0 license. PrismLauncher-10.0.5/COPYING.md0000644000175100017510000004754615144136756015445 0ustar runnerrunner## Prism Launcher Prism Launcher - Minecraft Launcher Copyright (C) 2022-2026 Prism Launcher Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . This file incorporates work covered by the following copyright and permission notice: Copyright 2013-2021 MultiMC Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ## PolyMC PolyMC - Minecraft Launcher Copyright (C) 2021-2022 PolyMC Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . This file incorporates work covered by the following copyright and permission notice: Copyright 2013-2021 MultiMC Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ## MinGW-w64 runtime (Windows) Copyright (c) 2009, 2010, 2011, 2012, 2013 by the mingw-w64 project This license has been certified as open source. It has also been designated as GPL compatible by the Free Software Foundation (FSF). Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. The right to distribute this software or to use it for any purpose does not give you the right to use Servicemarks (sm) or Trademarks (tm) of the copyright holders. Use of them is covered by separate agreement with the copyright holders. 5. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt. ## Qt 6 Copyright (C) 2022 The Qt Company Ltd and other contributors. Contact: https://www.qt.io/licensing Licensed under LGPL v3 ## libnbt++ libnbt++ - A library for the Minecraft Named Binary Tag format. Copyright (C) 2013, 2015 ljfa-ag libnbt++ is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. libnbt++ is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with libnbt++. If not, see . ## rainbow (KGuiAddons) Copyright (C) 2007 Matthew Woehlke Copyright (C) 2007 Olaf Schmidt Copyright (C) 2007 Thomas Zander Copyright (C) 2007 Zack Rusin Copyright (C) 2015 Petr Mrazek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ## cmark Copyright (c) 2014, John MacFarlane All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Batch icon set You are free to use Batch (the "icon set") or any part thereof (the "icons") in any personal, open-source or commercial work without obligation of payment (monetary or otherwise) or attribution. Do not sell the icon set, host the icon set or rent the icon set (either in existing or modified form). While attribution is optional, it is always appreciated. Intellectual property rights are not transferred with the download of the icons. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL ADAM WHITCROFT BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THE USE OF THE ICONS, EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ## Material Design Icons Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/), with Reserved Font Name Material Design Icons. Copyright (c) 2014, Google (http://www.google.com/design/) uses the license at https://github.com/google/material-design-icons/blob/master/LICENSE This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ## launcher (`libraries/launcher`) PolyMC - Minecraft Launcher Copyright (C) 2021-2022 PolyMC Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. You should have received a copy of the GNU General Public License along with this program. If not, see . This file incorporates work covered by the following copyright and permission notice: Copyright 2013-2021 MultiMC Contributors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ## lionshead Code has been taken from https://github.com/natefoo/lionshead and loosely translated to C++ laced with Qt. MIT License Copyright (c) 2017 Nate Coraor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## tomlplusplus MIT License Copyright (c) Mark Gillard Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Gamemode Copyright (c) 2017-2022, Feral Interactive All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Feral Interactive nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## Breeze icons Copyright (C) 2014 Uri Herrera and others This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . ## Oxygen Icons The Oxygen Icon Theme Copyright (C) 2007 Nuno Pinheiro Copyright (C) 2007 David Vignoni Copyright (C) 2007 David Miller Copyright (C) 2007 Johann Ollivier Lapeyre Copyright (C) 2007 Kenneth Wimer Copyright (C) 2007 Riccardo Iaconelli and others This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . ## libqrencode (`fukuchi/libqrencode`) Copyright (C) 2020 libqrencode Authors This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA ## vcpkg (`cmake/vcpkg-ports`) MIT License Copyright (c) Microsoft Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PrismLauncher-10.0.5/tests/0000755000175100017510000000000015144136757015136 5ustar runnerrunnerPrismLauncher-10.0.5/tests/ResourcePackParse_test.cpp0000644000175100017510000000534615144136757022272 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "minecraft/mod/tasks/LocalDataPackParseTask.h" #include #include class ResourcePackParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/ResourcePackParse"); QString zip_rp = FS::PathCombine(source, "test_resource_pack_idk.zip"); ResourcePack pack{ QFileInfo(zip_rp) }; bool valid = DataPackUtils::processZIP(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 3); QVERIFY(pack.description() == "um dois, feijão com arroz, três quatro, feijão no prato, cinco seis, café inglês, sete oito, comer biscoito, nove dez " "comer pastéis!!"); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/ResourcePackParse"); QString folder_rp = FS::PathCombine(source, "test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; bool valid = DataPackUtils::processFolder(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 1); QVERIFY(pack.description() == "Some resource pack maybe"); QVERIFY(valid == true); } void test_parseFolder2() { QString source = QFINDTESTDATA("testdata/ResourcePackParse"); QString folder_rp = FS::PathCombine(source, "another_test_folder"); ResourcePack pack{ QFileInfo(folder_rp) }; bool valid = DataPackUtils::process(&pack, DataPackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); QVERIFY(valid == true); // no assets dir but it is still valid based on https://minecraft.wiki/w/Resource_pack } }; QTEST_GUILESS_MAIN(ResourcePackParseTest) #include "ResourcePackParse_test.moc" PrismLauncher-10.0.5/tests/DataPackParse_test.cpp0000644000175100017510000000461615144136757021353 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include class DataPackParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/DataPackParse"); QString zip_dp = FS::PathCombine(source, "test_data_pack_boogaloo.zip"); DataPack pack{ QFileInfo(zip_dp) }; bool valid = DataPackUtils::processZIP(&pack); QVERIFY(pack.packFormat() == 4); QVERIFY(pack.description() == "Some data pack 2 boobgaloo"); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/DataPackParse"); QString folder_dp = FS::PathCombine(source, "test_folder"); DataPack pack{ QFileInfo(folder_dp) }; bool valid = DataPackUtils::processFolder(&pack); QVERIFY(pack.packFormat() == 10); QVERIFY(pack.description() == "Some data pack, maybe"); QVERIFY(valid == true); } void test_parseFolder2() { QString source = QFINDTESTDATA("testdata/DataPackParse"); QString folder_dp = FS::PathCombine(source, "another_test_folder"); DataPack pack{ QFileInfo(folder_dp) }; bool valid = DataPackUtils::process(&pack); QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "Some data pack three, leaves on the tree"); QVERIFY(valid == true); } }; QTEST_GUILESS_MAIN(DataPackParseTest) #include "DataPackParse_test.moc" PrismLauncher-10.0.5/tests/Library_test.cpp0000644000175100017510000003636615144136757020323 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include class LibraryTest : public QObject { Q_OBJECT private: LibraryPtr readMojangJson(const QString path) { QFile jsonFile(path); if (!jsonFile.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file" << jsonFile.fileName() << "for reading!"; return LibraryPtr(); } auto data = jsonFile.readAll(); jsonFile.close(); ProblemContainer problems; return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), path); } // get absolute path to expected storage, assuming default cache prefix QStringList getStorage(QString relative) { return { FS::PathCombine(cache->getBasePath("libraries"), relative) }; } RuntimeContext dummyContext(QString system = "linux", QString arch = "64", QString realArch = "amd64") { RuntimeContext r; r.javaArchitecture = arch; r.javaRealArchitecture = realArch; r.system = system; return r; } private slots: void initTestCase() { cache.reset(new HttpMetaCache()); cache->addBase("libraries", QDir("libraries").absolutePath()); dataDir = QDir(QFINDTESTDATA("testdata/Library")).absolutePath(); } void test_legacy() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); QCOMPARE(test.artifactPrefix(), QString("test.package:testname")); QCOMPARE(test.isNative(), false); QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, getStorage("test/package/testname/testversion/testname-testversion.jar")); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } void test_legacy_url() { RuntimeContext r = dummyContext(); QStringList failedFiles; Library test("test.package:testname:testversion"); test.setRepositoryURL("file://foo/bar"); auto downloads = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(downloads.size(), 1); QCOMPARE(failedFiles, {}); Net::NetRequest::Ptr dl = downloads[0]; QCOMPARE(dl->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion.jar")); } void test_legacy_url_local_broken() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); QCOMPARE(test.isNative(), false); QStringList failedFiles; test.setHint("local"); auto downloads = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(downloads.size(), 0); QCOMPARE(failedFiles, { "testname-testversion.jar" }); } void test_legacy_url_local_override() { RuntimeContext r = dummyContext(); Library test("com.paulscode:codecwav:20101023"); QCOMPARE(test.isNative(), false); QStringList failedFiles; test.setHint("local"); auto downloads = test.getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); QCOMPARE(downloads.size(), 0); qDebug() << failedFiles; QCOMPARE(failedFiles.size(), 0); QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } void test_legacy_native() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); test.m_nativeClassifiers["linux"] = "linux"; QCOMPARE(test.isNative(), true); test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, getStorage("test/package/testname/testversion/testname-testversion-linux.jar")); QCOMPARE(native32, {}); QCOMPARE(native64, {}); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); auto dl = dls[0]; QCOMPARE(dl->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux.jar")); } } void test_legacy_native_arch() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); test.m_nativeClassifiers["linux"] = "linux-${arch}"; test.m_nativeClassifiers["osx"] = "osx-${arch}"; test.m_nativeClassifiers["windows"] = "windows-${arch}"; QCOMPARE(test.isNative(), true); test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-linux-32.jar")); QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar")); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-32.jar")); QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-64.jar")); } r.system = "windows"; { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-windows-32.jar")); QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-windows-64.jar")); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-32.jar")); QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-64.jar")); } r.system = "osx"; { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-osx-32.jar")); QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-osx-64.jar")); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-32.jar")); QCOMPARE(dls[1]->url(), QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-64.jar")); } } void test_legacy_native_arch_local_override() { RuntimeContext r = dummyContext(); Library test("test.package:testname:testversion"); test.m_nativeClassifiers["linux"] = "linux-${arch}"; test.setHint("local"); QCOMPARE(test.isNative(), true); test.setRepositoryURL("file://foo/bar"); { QStringList jar, native, native32, native64; test.getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, { QFileInfo(QFINDTESTDATA("testdata/Library/testname-testversion-linux-32.jar")).absoluteFilePath() }); QCOMPARE(native64, { QFileInfo(QFINDTESTDATA("testdata/Library") + "/testname-testversion-linux-64.jar").absoluteFilePath() }); QStringList failedFiles; auto dls = test.getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, { QFileInfo(QFINDTESTDATA("testdata/Library") + "/testname-testversion-linux-64.jar").absoluteFilePath() }); } } void test_onenine() { RuntimeContext r = dummyContext("osx"); auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-simple.json")); { QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, getStorage("com/paulscode/codecwav/20101023/codecwav-20101023.jar")); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } r.system = "linux"; { QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar")); } r.system = "osx"; test->setHint("local"); { QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } r.system = "linux"; { QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } } void test_onenine_local_override() { RuntimeContext r = dummyContext("osx"); auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-simple.json")); test->setHint("local"); { QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QFINDTESTDATA("testdata/Library")); QCOMPARE(jar, { QFileInfo(QFINDTESTDATA("testdata/Library/codecwav-20101023.jar")).absoluteFilePath() }); QCOMPARE(native, {}); QCOMPARE(native32, {}); QCOMPARE(native64, {}); } r.system = "linux"; { QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QFINDTESTDATA("testdata/Library")); QCOMPARE(dls.size(), 0); QCOMPARE(failedFiles, {}); } } void test_onenine_native() { RuntimeContext r = dummyContext("osx"); auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-native.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, QStringList()); QCOMPARE(native, getStorage("org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar")); QCOMPARE(native32, {}); QCOMPARE(native64, {}); QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 1); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/" "lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar")); } void test_onenine_native_arch() { RuntimeContext r = dummyContext("windows"); auto test = readMojangJson(QFINDTESTDATA("testdata/Library/lib-native-arch.json")); QStringList jar, native, native32, native64; test->getApplicableFiles(r, jar, native, native32, native64, QString()); QCOMPARE(jar, {}); QCOMPARE(native, {}); QCOMPARE(native32, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar")); QCOMPARE(native64, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar")); QStringList failedFiles; auto dls = test->getDownloads(r, cache.get(), failedFiles, QString()); QCOMPARE(dls.size(), 2); QCOMPARE(failedFiles, {}); QCOMPARE(dls[0]->url(), QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar")); QCOMPARE(dls[1]->url(), QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar")); } private: std::unique_ptr cache; QString dataDir; }; QTEST_GUILESS_MAIN(LibraryTest) #include "Library_test.moc" PrismLauncher-10.0.5/tests/Index_test.cpp0000644000175100017510000000327615144136757017760 0ustar runnerrunner#include #include #include class IndexTest : public QObject { Q_OBJECT private slots: void test_hasUid_and_getList() { Meta::Index windex({ std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3") }); QVERIFY(windex.hasUid("list1")); QVERIFY(!windex.hasUid("asdf")); QVERIFY(windex.get("list2") != nullptr); QCOMPARE(windex.get("list2")->uid(), QString("list2")); QVERIFY(windex.get("adsf") != nullptr); } void test_merge() { Meta::Index windex({ std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3") }); QCOMPARE(windex.lists().size(), 3); windex.merge(std::shared_ptr( new Meta::Index({ std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3") }))); QCOMPARE(windex.lists().size(), 3); windex.merge(std::shared_ptr( new Meta::Index({ std::make_shared("list4"), std::make_shared("list2"), std::make_shared("list5") }))); QCOMPARE(windex.lists().size(), 5); windex.merge(std::shared_ptr(new Meta::Index({ std::make_shared("list6") }))); QCOMPARE(windex.lists().size(), 6); } }; QTEST_GUILESS_MAIN(IndexTest) #include "Index_test.moc" PrismLauncher-10.0.5/tests/Packwiz_test.cpp0000644000175100017510000000642515144136757020320 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "modplatform/ModIndex.h" #include class PackwizTest : public QObject { Q_OBJECT private slots: // Files taken from https://github.com/packwiz/packwiz-example-pack void loadFromFile_Modrinth() { QString source = QFINDTESTDATA("testdata/Packwiz"); QDir index_dir(source); QString slug_mod("borderless-mining"); QString file_name = slug_mod + ".pw.toml"; QVERIFY(index_dir.entryList().contains(file_name)); auto metadata = Packwiz::V1::getIndexForMod(index_dir, slug_mod); QVERIFY(metadata.isValid()); QCOMPARE(metadata.name, "Borderless Mining"); QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); QCOMPARE(metadata.side, ModPlatform::Side::ClientSide); QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar")); QCOMPARE(metadata.hash_format, "sha512"); QCOMPARE(metadata.hash, "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba6362306449" "9b3188d"); QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::MODRINTH); QCOMPARE(metadata.version(), "ug2qKTPR"); QCOMPARE(metadata.mod_id(), "kYq5qkSL"); } void loadFromFile_Curseforge() { QString source = QFINDTESTDATA("testdata/Packwiz"); QDir index_dir(source); QString name_mod("screenshot-to-clipboard-fabric.pw.toml"); QVERIFY(index_dir.entryList().contains(name_mod)); // Try without the .pw.toml at the end name_mod.chop(8); auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod); QVERIFY(metadata.isValid()); QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); QCOMPARE(metadata.side, ModPlatform::Side::UniversalSide); QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); QCOMPARE(metadata.hash_format, "murmur2"); QCOMPARE(metadata.hash, "1781245820"); QCOMPARE(metadata.provider, ModPlatform::ResourceProvider::FLAME); QCOMPARE(metadata.file_id, 3509043); QCOMPARE(metadata.project_id, 327154); } }; QTEST_GUILESS_MAIN(PackwizTest) #include "Packwiz_test.moc" PrismLauncher-10.0.5/tests/XmlLogs_test.cpp0000644000175100017510000001344115144136757020271 0ustar runnerrunner// SPDX-FileCopyrightText: 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include class XmlLogParseTest : public QObject { Q_OBJECT private slots: void parseXml_data() { QString source = QFINDTESTDATA("testdata/TestLogs"); QString shortXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.xml.log"))); QString shortText = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.text.log"))); QStringList shortTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5-levels.txt"))) .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); QList shortTextLevels; shortTextLevels.reserve(24); std::transform(shortTextLevels_s.cbegin(), shortTextLevels_s.cend(), std::back_inserter(shortTextLevels), [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); QString longXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.xml.log"))); QString longText = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.text.log"))); QStringList longTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-levels.txt"))) .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); QStringList longTextLevelsXml_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-xml-levels.txt"))) .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); QList longTextLevelsPlain; longTextLevelsPlain.reserve(974); std::transform(longTextLevels_s.cbegin(), longTextLevels_s.cend(), std::back_inserter(longTextLevelsPlain), [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); QList longTextLevelsXml; longTextLevelsXml.reserve(896); std::transform(longTextLevelsXml_s.cbegin(), longTextLevelsXml_s.cend(), std::back_inserter(longTextLevelsXml), [](const QString& line) { return MessageLevel::fromName(line.trimmed()); }); QTest::addColumn("log"); QTest::addColumn("num_entries"); QTest::addColumn>("entry_levels"); QTest::newRow("short-vanilla-plain") << shortText << 25 << shortTextLevels; QTest::newRow("short-vanilla-xml") << shortXml << 25 << shortTextLevels; QTest::newRow("long-forge-plain") << longText << 945 << longTextLevelsPlain; QTest::newRow("long-forge-xml") << longXml << 869 << longTextLevelsXml; } void parseXml() { QFETCH(QString, log); QFETCH(int, num_entries); QFETCH(QList, entry_levels); QList> entries = {}; QBENCHMARK { entries = parseLines(log.split(QRegularExpression("\n|\r\n|\r"))); } QCOMPARE(entries.length(), num_entries); QList levels = {}; std::transform(entries.cbegin(), entries.cend(), std::back_inserter(levels), [](std::pair entry) { return entry.first; }); QCOMPARE(levels, entry_levels); } private: LogParser m_parser; QList> parseLines(const QStringList& lines) { QList> out; MessageLevel last = MessageLevel::Unknown; for (const auto& line : lines) { m_parser.appendLine(line); auto items = m_parser.parseAvailable(); for (const auto& item : items) { if (std::holds_alternative(item)) { auto entry = std::get(item); auto msg = QString("[%1] [%2/%3] [%4]: %5") .arg(entry.timestamp.toString("HH:mm:ss")) .arg(entry.thread) .arg(entry.levelText) .arg(entry.logger) .arg(entry.message); out.append(std::make_pair(entry.level, msg)); last = entry.level; } else if (std::holds_alternative(item)) { auto msg = std::get(item).message; auto level = LogParser::guessLevel(msg, last); out.append(std::make_pair(level, msg)); last = level; } } } return out; } }; QTEST_GUILESS_MAIN(XmlLogParseTest) #include "XmlLogs_test.moc" PrismLauncher-10.0.5/tests/ParseUtils_test.cpp0000644000175100017510000000177715144136757021010 0ustar runnerrunner#include #include class ParseUtilsTest : public QObject { Q_OBJECT private slots: void test_Through_data() { QTest::addColumn("timestamp"); const char* timestamps[] = { "2016-02-29T13:49:54+01:00", "2016-02-26T15:21:11+00:01", "2016-02-24T15:52:36+01:13", "2016-02-18T17:41:00+00:00", "2016-02-17T15:23:19+00:00", "2016-02-16T15:22:39+09:22", "2016-02-10T15:06:41+00:00", "2016-02-04T15:28:02-05:33" }; for (unsigned i = 0; i < (sizeof(timestamps) / sizeof(const char*)); i++) { QTest::newRow(timestamps[i]) << QString(timestamps[i]); } } void test_Through() { QFETCH(QString, timestamp); auto time_parsed = timeFromS3Time(timestamp); auto time_serialized = timeToS3Time(time_parsed); QCOMPARE(time_serialized, timestamp); } }; QTEST_GUILESS_MAIN(ParseUtilsTest) #include "ParseUtils_test.moc" PrismLauncher-10.0.5/tests/GZip_test.cpp0000644000175100017510000000240415144136757017552 0ustar runnerrunner#include #include #include void fib(int& prev, int& cur) { auto ret = prev + cur; prev = cur; cur = ret; } class GZipTest : public QObject { Q_OBJECT private slots: void test_Through() { // test up to 10 MB static const int size = 10 * 1024 * 1024; QByteArray random; QByteArray compressed; QByteArray decompressed; std::default_random_engine eng((std::random_device())()); std::uniform_int_distribution idis(0, std::numeric_limits::max()); // initialize random buffer for (int i = 0; i < size; i++) { random.append(static_cast(idis(eng))); } // initialize fibonacci int prev = 1; int cur = 1; // test if fibonacci long random buffers pass through GZip do { QByteArray copy = random; copy.resize(cur); compressed.clear(); decompressed.clear(); QVERIFY(GZip::zip(copy, compressed)); QVERIFY(GZip::unzip(compressed, decompressed)); QCOMPARE(decompressed, copy); fib(prev, cur); } while (cur < size); } }; QTEST_GUILESS_MAIN(GZipTest) #include "GZip_test.moc" PrismLauncher-10.0.5/tests/TexturePackParse_test.cpp0000644000175100017510000000453115144136757022136 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "FileSystem.h" #include "minecraft/mod/TexturePack.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" class TexturePackParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/TexturePackParse"); QString zip_rp = FS::PathCombine(source, "test_texture_pack_idk.zip"); TexturePack pack{ QFileInfo(zip_rp) }; bool valid = TexturePackUtils::processZIP(pack); QVERIFY(pack.description() == "joe biden, wake up"); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/TexturePackParse"); QString folder_rp = FS::PathCombine(source, "test_texturefolder"); TexturePack pack{ QFileInfo(folder_rp) }; bool valid = TexturePackUtils::processFolder(pack, TexturePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.description() == "Some texture pack surely"); QVERIFY(valid == true); } void test_parseFolder2() { QString source = QFINDTESTDATA("testdata/TexturePackParse"); QString folder_rp = FS::PathCombine(source, "another_test_texturefolder"); TexturePack pack{ QFileInfo(folder_rp) }; bool valid = TexturePackUtils::process(pack, TexturePackUtils::ProcessingLevel::BasicInfoOnly); QVERIFY(pack.description() == "quieres\nfor real"); QVERIFY(valid == true); } }; QTEST_GUILESS_MAIN(TexturePackParseTest) #include "TexturePackParse_test.moc" PrismLauncher-10.0.5/tests/Version_test.cpp0000644000175100017510000001603415144136757020332 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include class VersionTest : public QObject { Q_OBJECT QStringList m_flex_test_names = {}; void addDataColumns() { QTest::addColumn("first"); QTest::addColumn("second"); QTest::addColumn("lessThan"); QTest::addColumn("equal"); } void setupVersions() { addDataColumns(); QTest::newRow("equal, explicit") << "1.2.0" << "1.2.0" << false << true; QTest::newRow("equal, two-digit") << "1.42" << "1.42" << false << true; QTest::newRow("lessThan, explicit 1") << "1.2.0" << "1.2.1" << true << false; QTest::newRow("lessThan, explicit 2") << "1.2.0" << "1.3.0" << true << false; QTest::newRow("lessThan, explicit 3") << "1.2.0" << "2.2.0" << true << false; QTest::newRow("lessThan, implicit 1") << "1.2" << "1.2.0" << true << false; QTest::newRow("lessThan, implicit 2") << "1.2" << "1.2.1" << true << false; QTest::newRow("lessThan, implicit 3") << "1.2" << "1.3.0" << true << false; QTest::newRow("lessThan, implicit 4") << "1.2" << "2.2.0" << true << false; QTest::newRow("lessThan, two-digit") << "1.41" << "1.42" << true << false; QTest::newRow("lessThan, snapshot") << "1.20.0-rc2" << "1.20.1" << true << false; QTest::newRow("greaterThan, explicit 1") << "1.2.1" << "1.2.0" << false << false; QTest::newRow("greaterThan, explicit 2") << "1.3.0" << "1.2.0" << false << false; QTest::newRow("greaterThan, explicit 3") << "2.2.0" << "1.2.0" << false << false; QTest::newRow("greaterThan, implicit 1") << "1.2.0" << "1.2" << false << false; QTest::newRow("greaterThan, implicit 2") << "1.2.1" << "1.2" << false << false; QTest::newRow("greaterThan, implicit 3") << "1.3.0" << "1.2" << false << false; QTest::newRow("greaterThan, implicit 4") << "2.2.0" << "1.2" << false << false; QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false; QTest::newRow("greaterThan, snapshot") << "1.20.2-rc2" << "1.20.1" << false << false; } private slots: void test_versionCompare_data() { setupVersions(); } void test_versionCompare() { QFETCH(QString, first); QFETCH(QString, second); QFETCH(bool, lessThan); QFETCH(bool, equal); const auto v1 = Version(first); const auto v2 = Version(second); qDebug() << v1 << "vs" << v2; QCOMPARE(v1 < v2, lessThan); QCOMPARE(v1 > v2, !lessThan && !equal); QCOMPARE(v1 == v2, equal); } void test_flexVerTestVector_data() { addDataColumns(); QDir test_vector_dir(QFINDTESTDATA("testdata/Version")); QFile vector_file{ test_vector_dir.absoluteFilePath("test_vectors.txt") }; if (!vector_file.open(QFile::OpenModeFlag::ReadOnly)) { qCritical() << "Failed to open file '" << vector_file.fileName() << "' for reading!"; return; } int test_number = 0; const QString test_name_template{ "FlexVer test #%1 (%2)" }; for (auto line = vector_file.readLine(); !vector_file.atEnd(); line = vector_file.readLine()) { line = line.simplified(); if (line.startsWith('#') || line.isEmpty()) continue; test_number += 1; auto split_line = line.split('<'); if (split_line.size() == 2) { QString first{ split_line.first().simplified() }; QString second{ split_line.last().simplified() }; auto new_test_name = test_name_template.arg(QString::number(test_number), "lessThan"); m_flex_test_names.append(new_test_name); QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << true << false; continue; } split_line = line.split('='); if (split_line.size() == 2) { QString first{ split_line.first().simplified() }; QString second{ split_line.last().simplified() }; auto new_test_name = test_name_template.arg(QString::number(test_number), "equals"); m_flex_test_names.append(new_test_name); QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << false << true; continue; } split_line = line.split('>'); if (split_line.size() == 2) { QString first{ split_line.first().simplified() }; QString second{ split_line.last().simplified() }; auto new_test_name = test_name_template.arg(QString::number(test_number), "greaterThan"); m_flex_test_names.append(new_test_name); QTest::newRow(m_flex_test_names.last().toLatin1().data()) << first << second << false << false; continue; } qCritical() << "Unexpected separator in the test vector:"; qCritical() << line; QVERIFY(0 != 0); } vector_file.close(); } void test_flexVerTestVector() { QFETCH(QString, first); QFETCH(QString, second); QFETCH(bool, lessThan); QFETCH(bool, equal); const auto v1 = Version(first); const auto v2 = Version(second); qDebug() << v1 << "vs" << v2; QCOMPARE(v1 < v2, lessThan); QCOMPARE(v1 > v2, !lessThan && !equal); QCOMPARE(v1 == v2, equal); } }; QTEST_GUILESS_MAIN(VersionTest) #include "Version_test.moc" PrismLauncher-10.0.5/tests/GradleSpecifier_test.cpp0000644000175100017510000000416615144136757021740 0ustar runnerrunner#include #include class GradleSpecifierTest : public QObject { Q_OBJECT private slots: void initTestCase() {} void cleanupTestCase() {} void test_Positive_data() { QTest::addColumn("through"); QTest::newRow("3 parter") << "org.gradle.test.classifiers:service:1.0"; QTest::newRow("classifier") << "org.gradle.test.classifiers:service:1.0:jdk15"; QTest::newRow("jarextension") << "org.gradle.test.classifiers:service:1.0@jar"; QTest::newRow("jarboth") << "org.gradle.test.classifiers:service:1.0:jdk15@jar"; QTest::newRow("packxz") << "org.gradle.test.classifiers:service:1.0:jdk15@jar.pack.xz"; } void test_Positive() { QFETCH(QString, through); QString converted = GradleSpecifier(through).serialize(); QCOMPARE(converted, through); } void test_Path_data() { QTest::addColumn("spec"); QTest::addColumn("expected"); QTest::newRow("3 parter") << "group.id:artifact:1.0" << "group/id/artifact/1.0/artifact-1.0.jar"; QTest::newRow("doom") << "id.software:doom:1.666:demons@wad" << "id/software/doom/1.666/doom-1.666-demons.wad"; } void test_Path() { QFETCH(QString, spec); QFETCH(QString, expected); QString converted = GradleSpecifier(spec).toPath(); QCOMPARE(converted, expected); } void test_Negative_data() { QTest::addColumn("input"); QTest::newRow("too many :") << "org:gradle.test:class:::ifiers:service:1.0::"; QTest::newRow("nonsense") << "I like turtles"; QTest::newRow("empty string") << ""; QTest::newRow("missing version") << "herp.derp:artifact"; } void test_Negative() { QFETCH(QString, input); GradleSpecifier spec(input); QVERIFY(!spec.valid()); QCOMPARE(spec.serialize(), input); QCOMPARE(spec.toPath(), QString()); } }; QTEST_GUILESS_MAIN(GradleSpecifierTest) #include "GradleSpecifier_test.moc" PrismLauncher-10.0.5/tests/INIFile_test.cpp0000644000175100017510000001466615144136757020135 0ustar runnerrunner#include #include #include #include #include #include #include "FileSystem.h" #include class IniFileTest : public QObject { Q_OBJECT private slots: void initTestCase() {} void cleanupTestCase() {} void test_Escape_data() { QTest::addColumn("through"); QTest::newRow("unix path") << "/abc/def/ghi/jkl"; QTest::newRow("windows path") << "C:\\Program files\\terrible\\name\\of something\\"; QTest::newRow("Plain text") << "Lorem ipsum dolor sit amet."; QTest::newRow("Escape sequences") << "Lorem\n\t\n\\n\\tAAZ\nipsum dolor\n\nsit amet."; QTest::newRow("Escape sequences 2") << "\"\n\n\""; QTest::newRow("Hashtags") << "some data#something"; } void test_SaveLoad() { QString a = "a"; QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\#thisIsNotAComment"; QString filename = "test_SaveLoad.ini"; // save INIFile f; f.set("a", a); f.set("b", b); f.saveFile(filename); // load INIFile f2; f2.loadFile(filename); QCOMPARE(f2.get("a", "NOT SET").toString(), a); QCOMPARE(f2.get("b", "NOT SET").toString(), b); } void test_SaveLoadLists() { QString slist_strings = "(\"a\",\"b\",\"c\")"; QStringList list_strings = { "a", "b", "c" }; QString slist_numbers = "(1,2,3,10)"; QList list_numbers = { 1, 2, 3, 10 }; QString filename = "test_SaveLoadLists.ini"; INIFile f; f.set("list_strings", list_strings); f.set("list_numbers", QVariantUtils::fromList(list_numbers)); f.saveFile(filename); // load INIFile f2; f2.loadFile(filename); QStringList out_list_strings = f2.get("list_strings", QStringList()).toStringList(); qDebug() << "OutStringList" << out_list_strings; QList out_list_numbers = QVariantUtils::toList(f2.get("list_numbers", QVariantUtils::fromList(QList()))); qDebug() << "OutNumbersList" << out_list_numbers; QCOMPARE(out_list_strings, list_strings); QCOMPARE(out_list_numbers, list_numbers); } void test_SaveAlreadyExistingFile() { QString fileContent = R"(InstanceType=OneSix iconKey=vanillia_icon name=Minecraft Vanillia OverrideCommands=true PreLaunchCommand="$INST_JAVA" -jar packwiz-installer-bootstrap.jar link Wrapperommand=)"; fileContent += "\""; fileContent += +R"(\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link =)"; fileContent += "\"\n"; #if defined(Q_OS_WIN) QString fileName = "test_SaveAlreadyExistingFile.ini"; QFile file(fileName); QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true); #else QTemporaryFile file; QCOMPARE(file.open(), true); QCOMPARE(file.fileName().isEmpty(), false); QString fileName = file.fileName(); #endif QTextStream stream(&file); stream << fileContent; file.close(); // load INIFile f1; f1.loadFile(fileName); QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); QCOMPARE(f1.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); f1.saveFile(fileName); INIFile f2; f2.loadFile(fileName); QCOMPARE(f2.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); QCOMPARE(f2.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif } void test_SaveAlreadyExistingFileWithSpecialChars() { #if defined(Q_OS_WIN) QString fileName = "test_SaveAlreadyExistingFileWithSpecialChars.ini"; #else QTemporaryFile file; QCOMPARE(file.open(), true); QCOMPARE(file.fileName().isEmpty(), false); QString fileName = file.fileName(); file.close(); #endif QSettings settings{ fileName, QSettings::Format::IniFormat }; settings.setFallbacksEnabled(false); settings.setValue("simple", "value1"); settings.setValue("withQuotes", R"("value2" with quotes)"); settings.setValue("withSpecialCharacters", "env mesa=true"); settings.setValue("withSpecialCharacters2", "1,2,3,4"); settings.setValue("withSpecialCharacters2", "1;2;3;4"); settings.setValue("withAll", "val=\"$INST_JAVA\" -jar; ls "); settings.sync(); QCOMPARE(settings.status(), QSettings::Status::NoError); // load INIFile f1; f1.loadFile(fileName); for (auto key : settings.allKeys()) QCOMPARE(f1.get(key, "NOT SET").toString(), settings.value(key).toString()); f1.saveFile(fileName); INIFile f2; f2.loadFile(fileName); for (auto key : settings.allKeys()) QCOMPARE(f2.get(key, "NOT SET").toString(), settings.value(key).toString()); QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif } void test_SaveAlreadyExistingFileWithSpecialCharsV1() { QString fileContent = R"(InstanceType=OneSix ConfigVersion=1.1 iconKey=vanillia_icon name=Minecraft Vanillia OverrideCommands=true PreLaunchCommand=)"; fileContent += "\"\\\"env mesa=true\\\"\"\n"; #if defined(Q_OS_WIN) QString fileName = "test_SaveAlreadyExistingFileWithSpecialCharsV1.ini"; QFile file(fileName); QCOMPARE(file.open(QFile::WriteOnly | QFile::Text), true); #else QTemporaryFile file; QCOMPARE(file.open(), true); QCOMPARE(file.fileName().isEmpty(), false); QString fileName = file.fileName(); #endif QTextStream stream(&file); stream << fileContent; file.close(); // load INIFile f1; f1.loadFile(fileName); QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "env mesa=true"); QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif } }; QTEST_GUILESS_MAIN(IniFileTest) #include "INIFile_test.moc" PrismLauncher-10.0.5/tests/CMakeLists.txt0000644000175100017510000000475215144136757017706 0ustar runnerrunnerproject(tests) ecm_add_test(FileSystem_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME FileSystem) ecm_add_test(GZip_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME GZip) ecm_add_test(GradleSpecifier_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME GradleSpecifier) ecm_add_test(MojangVersionFormat_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME MojangVersionFormat) ecm_add_test(Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Library) ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ResourceFolderModel) ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ResourcePackParse) ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME TexturePackParse) ecm_add_test(DataPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME DataPackParse) ecm_add_test(ShaderPackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ShaderPackParse) ecm_add_test(WorldSaveParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME WorldSaveParse) ecm_add_test(ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ParseUtils) ecm_add_test(Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Task) ecm_add_test(INIFile_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME INIFile) ecm_add_test(JavaVersion_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME JavaVersion) ecm_add_test(Packwiz_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Packwiz) ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Index) ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Version) ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME MetaComponentParse) ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME CatPack) ecm_add_test(XmlLogs_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME XmlLogs) PrismLauncher-10.0.5/tests/ShaderPackParse_test.cpp0000644000175100017510000000440115144136757021700 0ustar runnerrunner // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include class ShaderPackParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/ShaderPackParse"); QString zip_sp = FS::PathCombine(source, "shaderpack1.zip"); ShaderPack pack{ QFileInfo(zip_sp) }; bool valid = ShaderPackUtils::processZIP(pack); QVERIFY(pack.packFormat() == ShaderPackFormat::VALID); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/ShaderPackParse"); QString folder_sp = FS::PathCombine(source, "shaderpack2"); ShaderPack pack{ QFileInfo(folder_sp) }; bool valid = ShaderPackUtils::processFolder(pack); QVERIFY(pack.packFormat() == ShaderPackFormat::VALID); QVERIFY(valid == true); } void test_parseZIP2() { QString source = QFINDTESTDATA("testdata/ShaderPackParse"); QString folder_sp = FS::PathCombine(source, "shaderpack3.zip"); ShaderPack pack{ QFileInfo(folder_sp) }; bool valid = ShaderPackUtils::process(pack); QVERIFY(pack.packFormat() == ShaderPackFormat::INVALID); QVERIFY(valid == false); } }; QTEST_GUILESS_MAIN(ShaderPackParseTest) #include "ShaderPackParse_test.moc" PrismLauncher-10.0.5/tests/FileSystem_test.cpp0000644000175100017510000006310715144136757020774 0ustar runnerrunner#include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; class LinkTask : public Task { Q_OBJECT friend class FileSystemTest; LinkTask(QString src, QString dst) { m_lnk = new FS::create_link(src, dst, this); m_lnk->debug(true); } ~LinkTask() { delete m_lnk; } void matcher(Filter filter) { m_lnk->matcher(filter); } void linkRecursively(bool recursive) { m_lnk->linkRecursively(recursive); m_linkRecursive = recursive; } void whitelist(bool b) { m_lnk->whitelist(b); } void setMaxDepth(int depth) { m_lnk->setMaxDepth(depth); } private: void executeTask() override { if (!(*m_lnk)()) { #if defined Q_OS_WIN32 if (!m_useHard) { qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "atempting to run with privelage"; connect(m_lnk, &FS::create_link::finishedPrivileged, this, [this](bool gotResults) { if (gotResults) { emitSucceeded(); } else { qDebug() << "Privileged run exited without results!"; emitFailed(); } }); m_lnk->runPrivileged(); } else { qDebug() << "Link Failed!" << m_lnk->getOSError().value() << m_lnk->getOSError().message().c_str(); } #else qDebug() << "Link Failed!" << m_lnk->getOSError().value() << m_lnk->getOSError().message().c_str(); #endif } else { emitSucceeded(); } } FS::create_link* m_lnk; #if defined Q_OS_WIN32 bool m_useHard = false; #endif bool m_linkRecursive = true; }; class FileSystemTest : public QObject { Q_OBJECT const QString bothSlash = "/foo/"; const QString trailingSlash = "foo/"; const QString leadingSlash = "/foo"; private slots: void test_pathCombine() { QCOMPARE(QString("/foo/foo"), FS::PathCombine(bothSlash, bothSlash)); QCOMPARE(QString("foo/foo"), FS::PathCombine(trailingSlash, trailingSlash)); QCOMPARE(QString("/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash)); QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(bothSlash, bothSlash, bothSlash)); QCOMPARE(QString("foo/foo/foo"), FS::PathCombine(trailingSlash, trailingSlash, trailingSlash)); QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash, leadingSlash)); } void test_PathCombine1_data() { QTest::addColumn("result"); QTest::addColumn("path1"); QTest::addColumn("path2"); QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl"; QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl"; #if defined(Q_OS_WIN) QTest::newRow("win native, from C:") << "C:/abc" << "C:" << "abc"; QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def" << "ghi\\jkl"; QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def\\" << "ghi\\jkl"; #endif } void test_PathCombine1() { QFETCH(QString, result); QFETCH(QString, path1); QFETCH(QString, path2); QCOMPARE(FS::PathCombine(path1, path2), result); } void test_PathCombine2_data() { QTest::addColumn("result"); QTest::addColumn("path1"); QTest::addColumn("path2"); QTest::addColumn("path3"); QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl"; QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl"; QTest::newRow("qt 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl"; QTest::newRow("qt 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl"; #if defined(Q_OS_WIN) QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def" << "ghi\\jkl"; QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def\\" << "ghi\\jkl"; QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; #endif } void test_PathCombine2() { QFETCH(QString, result); QFETCH(QString, path1); QFETCH(QString, path2); QFETCH(QString, path3); QCOMPARE(FS::PathCombine(path1, path2, path3), result); } void test_copy() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); c(); for (auto entry : target_dir.entryList()) { qDebug() << entry; } QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_copy_with_blacklist() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); c.matcher(re); c(); for (auto entry : target_dir.entryList()) { qDebug() << entry; } QVERIFY(!target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_copy_with_whitelist() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); c.matcher(re); c.whitelist(true); c(); for (auto entry : target_dir.entryList()) { qDebug() << entry; } QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(!target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_copy_with_dot_hidden() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); c(); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; } QVERIFY(target_dir.entryList(filter).contains(".secret_folder")); target_dir.cd(".secret_folder"); QVERIFY(target_dir.entryList(filter).contains(".secret_file.txt")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_copy_single_file() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); { QString file = QFINDTESTDATA("testdata/FileSystem/test_folder/pack.mcmeta"); qDebug() << "From:" << file << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "pack.mcmeta")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(file, target_dir.filePath("pack.mcmeta")); c(); auto filter = QDir::Filter::Files; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; } QVERIFY(target_dir.entryList(filter).contains("pack.mcmeta")); } } void test_getDesktop() { QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); } void test_link() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(false); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); if (!entry_lnk_info.isDir()) QVERIFY(!entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(lnk_info.isSymLink()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_hard_link() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { // use working dir to prevent makeing a hard link to a tmpfs or across devices QTemporaryDir tempDir("./tmp"); tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::create_link lnk(folder, target_dir.path()); lnk.useHardLinks(true); lnk.debug(true); if (!lnk()) { qDebug() << "Link Failed!" << lnk.getOSError().value() << lnk.getOSError().message().c_str(); } for (auto entry : target_dir.entryList()) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); QVERIFY(!entry_lnk_info.isSymLink()); QFileInfo entry_orig_info(QDir(folder).filePath(entry)); if (!entry_lnk_info.isDir()) { qDebug() << "hard link equivalency?" << entry_lnk_info.absoluteFilePath() << "vs" << entry_orig_info.absoluteFilePath(); QVERIFY(fs::equivalent(fs::path(StringUtils::toStdString(entry_lnk_info.absoluteFilePath())), fs::path(StringUtils::toStdString(entry_orig_info.absoluteFilePath())))); } } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(!lnk_info.isSymLink()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_with_blacklist() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); if (!entry_lnk_info.isDir()) QVERIFY(entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(!target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_with_whitelist() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); lnk_tsk.whitelist(true); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); for (auto entry : target_dir.entryList()) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); if (!entry_lnk_info.isDir()) QVERIFY(entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(!target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_with_dot_hidden() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; QFileInfo entry_lnk_info(target_dir.filePath(entry)); if (!entry_lnk_info.isDir()) QVERIFY(entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(target_dir.entryList(filter).contains(".secret_folder")); target_dir.cd(".secret_folder"); QVERIFY(target_dir.entryList(filter).contains(".secret_file.txt")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_single_file() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); { QString file = QFINDTESTDATA("testdata/FileSystem/test_folder/pack.mcmeta"); qDebug() << "From:" << file << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "pack.mcmeta")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(file, target_dir.filePath("pack.mcmeta")); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); auto filter = QDir::Filter::Files; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; } QFileInfo lnk_info(target_dir.filePath("pack.mcmeta")); QVERIFY(lnk_info.exists()); QVERIFY(lnk_info.isSymLink()); QVERIFY(target_dir.entryList(filter).contains("pack.mcmeta")); } } void test_link_with_max_depth() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(0); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); QVERIFY(!QFileInfo(target_dir.path()).isSymLink()); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : target_dir.entryList(filter)) { qDebug() << entry; if (entry == "." || entry == "..") continue; QFileInfo entry_lnk_info(target_dir.filePath(entry)); QVERIFY(entry_lnk_info.isSymLink()); } QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(!lnk_info.isSymLink()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_link_with_no_max_depth() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); qDebug() << tempDir.path(); qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(-1); connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); std::function verify_check = [&verify_check](QString check_path) { QDir check_dir(check_path); auto filter = QDir::Filter::Files | QDir::Filter::Dirs | QDir::Filter::Hidden; for (auto entry : check_dir.entryList(filter)) { QFileInfo entry_lnk_info(check_dir.filePath(entry)); qDebug() << entry << check_dir.filePath(entry); if (!entry_lnk_info.isDir()) { QVERIFY(entry_lnk_info.isSymLink()); } else if (entry != "." && entry != "..") { qDebug() << "Decending tree to verify symlinks:" << check_dir.filePath(entry); verify_check(entry_lnk_info.filePath()); } } }; verify_check(target_dir.path()); QFileInfo lnk_info(target_dir.path()); QVERIFY(lnk_info.exists()); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // first try variant without trailing / QVERIFY(!folder.endsWith('/')); f(); // then variant with trailing / folder.append('/'); QVERIFY(folder.endsWith('/')); f(); } void test_path_depth() { QCOMPARE(FS::pathDepth(""), 0); QCOMPARE(FS::pathDepth("."), 0); QCOMPARE(FS::pathDepth("foo.txt"), 0); QCOMPARE(FS::pathDepth("./foo.txt"), 0); QCOMPARE(FS::pathDepth("./bar/foo.txt"), 1); QCOMPARE(FS::pathDepth("../bar/foo.txt"), 0); QCOMPARE(FS::pathDepth("/bar/foo.txt"), 1); QCOMPARE(FS::pathDepth("baz/bar/foo.txt"), 2); QCOMPARE(FS::pathDepth("/baz/bar/foo.txt"), 2); QCOMPARE(FS::pathDepth("./baz/bar/foo.txt"), 2); QCOMPARE(FS::pathDepth("/baz/../bar/foo.txt"), 1); } void test_path_trunc() { QCOMPARE(FS::pathTruncate("", 0), QDir::toNativeSeparators("")); QCOMPARE(FS::pathTruncate("foo.txt", 0), QDir::toNativeSeparators("")); QCOMPARE(FS::pathTruncate("foo.txt", 1), QDir::toNativeSeparators("")); QCOMPARE(FS::pathTruncate("./bar/foo.txt", 0), QDir::toNativeSeparators("./bar")); QCOMPARE(FS::pathTruncate("./bar/foo.txt", 1), QDir::toNativeSeparators("./bar")); QCOMPARE(FS::pathTruncate("/bar/foo.txt", 1), QDir::toNativeSeparators("/bar")); QCOMPARE(FS::pathTruncate("bar/foo.txt", 1), QDir::toNativeSeparators("bar")); QCOMPARE(FS::pathTruncate("baz/bar/foo.txt", 2), QDir::toNativeSeparators("baz/bar")); #if defined(Q_OS_WIN) QCOMPARE(FS::pathTruncate("C:\\bar\\foo.txt", 1), QDir::toNativeSeparators("C:\\bar")); #endif } }; QTEST_GUILESS_MAIN(FileSystemTest) #include "FileSystem_test.moc" PrismLauncher-10.0.5/tests/testdata/0000755000175100017510000000000015144136757016747 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ResourceFolderModel0000777000175100017510000000000015144136757026077 2ResourcePackParseustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/Library0000777000175100017510000000000015144136757024227 2MojangVersionFormat/ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/TexturePackParse/0000755000175100017510000000000015144136757022201 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/TexturePackParse/test_texturefolder/0000755000175100017510000000000015144136757026134 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/TexturePackParse/test_texturefolder/assets/0000755000175100017510000000000015144136757027436 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/0000755000175100017510000000000015144136757031406 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/textures/0000755000175100017510000000000015144136757033271 5ustar runnerrunner././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootPrismLauncher-10.0.5/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/textures/blah.txtPrismLauncher-10.0.5/tests/testdata/TexturePackParse/test_texturefolder/assets/minecraft/textures/bl0000644000175100017510000000000215144136757033601 0ustar runnerrunner PrismLauncher-10.0.5/tests/testdata/TexturePackParse/test_texturefolder/pack.txt0000644000175100017510000000003015144136757027604 0ustar runnerrunnerSome texture pack surelyPrismLauncher-10.0.5/tests/testdata/TexturePackParse/test_texture_pack_idk.zip0000644000175100017510000000027015144136757027310 0ustar runnerrunnerPK ·t$UÐ bpack.txtUT œc!œcux èèjoe biden, wake upPK ·t$UÐ b¤pack.txtUTœcux èèPKNTPrismLauncher-10.0.5/tests/testdata/TexturePackParse/another_test_texturefolder/0000755000175100017510000000000015144136757027654 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/TexturePackParse/another_test_texturefolder/pack.txt0000644000175100017510000000002015144136757031323 0ustar runnerrunnerquieres for realPrismLauncher-10.0.5/tests/testdata/MojangVersionFormat/0000755000175100017510000000000015144136757022701 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/MojangVersionFormat/1.9-simple.json0000644000175100017510000001256615144136757025404 0ustar runnerrunner{ "assets": "1.9", "id": "1.9", "libraries": [ { "name": "oshi-project:oshi-core:1.1" }, { "name": "net.java.dev.jna:jna:3.4.0" }, { "name": "net.java.dev.jna:platform:3.4.0" }, { "name": "com.ibm.icu:icu4j-core-mojang:51.2" }, { "name": "net.sf.jopt-simple:jopt-simple:4.6" }, { "name": "com.paulscode:codecjorbis:20101023" }, { "name": "com.paulscode:codecwav:20101023" }, { "name": "com.paulscode:libraryjavasound:20101123" }, { "name": "com.paulscode:librarylwjglopenal:20100824" }, { "name": "com.paulscode:soundsystem:20120107" }, { "name": "io.netty:netty-all:4.0.23.Final" }, { "name": "com.google.guava:guava:17.0" }, { "name": "org.apache.commons:commons-lang3:3.3.2" }, { "name": "commons-io:commons-io:2.4" }, { "name": "commons-codec:commons-codec:1.9" }, { "name": "net.java.jinput:jinput:2.0.5" }, { "name": "net.java.jutils:jutils:1.0.0" }, { "name": "com.google.code.gson:gson:2.2.4" }, { "name": "com.mojang:authlib:1.5.22" }, { "name": "com.mojang:realms:1.8.4" }, { "name": "org.apache.commons:commons-compress:1.8.1" }, { "name": "org.apache.httpcomponents:httpclient:4.3.3" }, { "name": "commons-logging:commons-logging:1.1.3" }, { "name": "org.apache.httpcomponents:httpcore:4.3.2" }, { "name": "org.apache.logging.log4j:log4j-api:2.0-beta9" }, { "name": "org.apache.logging.log4j:log4j-core:2.0-beta9" }, { "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "extract": { "exclude": [ "META-INF/" ] }, "name": "net.java.jinput:jinput-platform:2.0.5", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" } } ], "mainClass": "net.minecraft.client.main.Main", "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}", "minimumLauncherVersion": 18, "releaseTime": "2016-02-29T13:49:54+00:00", "time": "2016-03-01T13:14:53+00:00", "type": "release" } PrismLauncher-10.0.5/tests/testdata/MojangVersionFormat/testname-testversion-linux-32.jar0000644000175100017510000000002115144136757031152 0ustar runnerrunnerdummy test file. PrismLauncher-10.0.5/tests/testdata/MojangVersionFormat/codecwav-20101023.jar0000644000175100017510000000002115144136757026051 0ustar runnerrunnerdummy test file. PrismLauncher-10.0.5/tests/testdata/MojangVersionFormat/1.9.json0000644000175100017510000005674115144136757024120 0ustar runnerrunner{ "assetIndex": { "id": "1.9", "sha1": "cde65b47a43f638653ab1da3848b53f8a7477b16", "size": 136916, "totalSize": 119917473, "url": "https://launchermeta.mojang.com/mc-staging/assets/1.9/cde65b47a43f638653ab1da3848b53f8a7477b16/1.9.json" }, "assets": "1.9", "downloads": { "client": { "sha1": "2f67dfe8953299440d1902f9124f0f2c3a2c940f", "size": 8697592, "url": "https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar" }, "server": { "sha1": "b4d449cf2918e0f3bd8aa18954b916a4d1880f0d", "size": 8848015, "url": "https://launcher.mojang.com/mc/game/1.9/server/b4d449cf2918e0f3bd8aa18954b916a4d1880f0d/server.jar" } }, "id": "1.9", "libraries": [ { "downloads": { "artifact": { "path": "oshi-project/oshi-core/1.1/oshi-core-1.1.jar", "sha1": "9ddf7b048a8d701be231c0f4f95fd986198fd2d8", "size": 30973, "url": "https://libraries.minecraft.net/oshi-project/oshi-core/1.1/oshi-core-1.1.jar" } }, "name": "oshi-project:oshi-core:1.1" }, { "downloads": { "artifact": { "path": "net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar", "sha1": "803ff252fedbd395baffd43b37341dc4a150a554", "size": 1008730, "url": "https://libraries.minecraft.net/net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar" } }, "name": "net.java.dev.jna:jna:3.4.0" }, { "downloads": { "artifact": { "path": "net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar", "sha1": "e3f70017be8100d3d6923f50b3d2ee17714e9c13", "size": 913436, "url": "https://libraries.minecraft.net/net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar" } }, "name": "net.java.dev.jna:platform:3.4.0" }, { "downloads": { "artifact": { "path": "com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar", "sha1": "63d216a9311cca6be337c1e458e587f99d382b84", "size": 1634692, "url": "https://libraries.minecraft.net/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar" } }, "name": "com.ibm.icu:icu4j-core-mojang:51.2" }, { "downloads": { "artifact": { "path": "net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar", "sha1": "306816fb57cf94f108a43c95731b08934dcae15c", "size": 62477, "url": "https://libraries.minecraft.net/net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar" } }, "name": "net.sf.jopt-simple:jopt-simple:4.6" }, { "downloads": { "artifact": { "path": "com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar", "sha1": "c73b5636faf089d9f00e8732a829577de25237ee", "size": 103871, "url": "https://libraries.minecraft.net/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar" } }, "name": "com.paulscode:codecjorbis:20101023" }, { "downloads": { "artifact": { "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar", "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da", "size": 5618, "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar" } }, "name": "com.paulscode:codecwav:20101023" }, { "downloads": { "artifact": { "path": "com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar", "sha1": "5c5e304366f75f9eaa2e8cca546a1fb6109348b3", "size": 21679, "url": "https://libraries.minecraft.net/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar" } }, "name": "com.paulscode:libraryjavasound:20101123" }, { "downloads": { "artifact": { "path": "com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar", "sha1": "73e80d0794c39665aec3f62eee88ca91676674ef", "size": 18981, "url": "https://libraries.minecraft.net/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar" } }, "name": "com.paulscode:librarylwjglopenal:20100824" }, { "downloads": { "artifact": { "path": "com/paulscode/soundsystem/20120107/soundsystem-20120107.jar", "sha1": "419c05fe9be71f792b2d76cfc9b67f1ed0fec7f6", "size": 65020, "url": "https://libraries.minecraft.net/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar" } }, "name": "com.paulscode:soundsystem:20120107" }, { "downloads": { "artifact": { "path": "io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar", "sha1": "0294104aaf1781d6a56a07d561e792c5d0c95f45", "size": 1779991, "url": "https://libraries.minecraft.net/io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar" } }, "name": "io.netty:netty-all:4.0.23.Final" }, { "downloads": { "artifact": { "path": "com/google/guava/guava/17.0/guava-17.0.jar", "sha1": "9c6ef172e8de35fd8d4d8783e4821e57cdef7445", "size": 2243036, "url": "https://libraries.minecraft.net/com/google/guava/guava/17.0/guava-17.0.jar" } }, "name": "com.google.guava:guava:17.0" }, { "downloads": { "artifact": { "path": "org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar", "sha1": "90a3822c38ec8c996e84c16a3477ef632cbc87a3", "size": 412739, "url": "https://libraries.minecraft.net/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar" } }, "name": "org.apache.commons:commons-lang3:3.3.2" }, { "downloads": { "artifact": { "path": "commons-io/commons-io/2.4/commons-io-2.4.jar", "sha1": "b1b6ea3b7e4aa4f492509a4952029cd8e48019ad", "size": 185140, "url": "https://libraries.minecraft.net/commons-io/commons-io/2.4/commons-io-2.4.jar" } }, "name": "commons-io:commons-io:2.4" }, { "downloads": { "artifact": { "path": "commons-codec/commons-codec/1.9/commons-codec-1.9.jar", "sha1": "9ce04e34240f674bc72680f8b843b1457383161a", "size": 263965, "url": "https://libraries.minecraft.net/commons-codec/commons-codec/1.9/commons-codec-1.9.jar" } }, "name": "commons-codec:commons-codec:1.9" }, { "downloads": { "artifact": { "path": "net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar", "sha1": "39c7796b469a600f72380316f6b1f11db6c2c7c4", "size": 208338, "url": "https://libraries.minecraft.net/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar" } }, "name": "net.java.jinput:jinput:2.0.5" }, { "downloads": { "artifact": { "path": "net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar", "sha1": "e12fe1fda814bd348c1579329c86943d2cd3c6a6", "size": 7508, "url": "https://libraries.minecraft.net/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar" } }, "name": "net.java.jutils:jutils:1.0.0" }, { "downloads": { "artifact": { "path": "com/google/code/gson/gson/2.2.4/gson-2.2.4.jar", "sha1": "a60a5e993c98c864010053cb901b7eab25306568", "size": 190432, "url": "https://libraries.minecraft.net/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar" } }, "name": "com.google.code.gson:gson:2.2.4" }, { "downloads": { "artifact": { "path": "com/mojang/authlib/1.5.22/authlib-1.5.22.jar", "sha1": "afaa8f6df976fcb5520e76ef1d5798c9e6b5c0b2", "size": 64539, "url": "https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar" } }, "name": "com.mojang:authlib:1.5.22" }, { "downloads": { "artifact": { "path": "com/mojang/realms/1.8.4/realms-1.8.4.jar", "sha1": "15f8dc326c97a96dee6e65392e145ad6d1cb46cb", "size": 1131574, "url": "https://libraries.minecraft.net/com/mojang/realms/1.8.4/realms-1.8.4.jar" } }, "name": "com.mojang:realms:1.8.4" }, { "downloads": { "artifact": { "path": "org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar", "sha1": "a698750c16740fd5b3871425f4cb3bbaa87f529d", "size": 365552, "url": "https://libraries.minecraft.net/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar" } }, "name": "org.apache.commons:commons-compress:1.8.1" }, { "downloads": { "artifact": { "path": "org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar", "sha1": "18f4247ff4572a074444572cee34647c43e7c9c7", "size": 589512, "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar" } }, "name": "org.apache.httpcomponents:httpclient:4.3.3" }, { "downloads": { "artifact": { "path": "commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar", "sha1": "f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f", "size": 62050, "url": "https://libraries.minecraft.net/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar" } }, "name": "commons-logging:commons-logging:1.1.3" }, { "downloads": { "artifact": { "path": "org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar", "sha1": "31fbbff1ddbf98f3aa7377c94d33b0447c646b6e", "size": 282269, "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar" } }, "name": "org.apache.httpcomponents:httpcore:4.3.2" }, { "downloads": { "artifact": { "path": "org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar", "sha1": "1dd66e68cccd907880229f9e2de1314bd13ff785", "size": 108161, "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar" } }, "name": "org.apache.logging.log4j:log4j-api:2.0-beta9" }, { "downloads": { "artifact": { "path": "org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar", "sha1": "678861ba1b2e1fccb594bb0ca03114bb05da9695", "size": 681134, "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar" } }, "name": "org.apache.logging.log4j:log4j-core:2.0-beta9" }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar", "sha1": "697517568c68e78ae0b4544145af031c81082dfe", "size": 1047168, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar" } }, "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar", "sha1": "d51a7c040a721d13efdfbd34f8b257b2df882ad0", "size": 173887, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar" } }, "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar", "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33", "size": 22, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar" }, "classifiers": { "natives-linux": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar", "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b", "size": 578680, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar" }, "natives-osx": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar", "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed", "size": 426822, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" }, "natives-windows": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar", "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0", "size": 613748, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar", "sha1": "7707204c9ffa5d91662de95f0a224e2f721b22af", "size": 1045632, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar" } }, "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar", "sha1": "f0e612c840a7639c1f77f68d72a28dae2f0c8490", "size": 173887, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar" } }, "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "downloads": { "classifiers": { "natives-linux": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar", "sha1": "d898a33b5d0a6ef3fed3a4ead506566dce6720a5", "size": 578539, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar" }, "natives-osx": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar", "sha1": "79f5ce2fea02e77fe47a3c745219167a542121d7", "size": 468116, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar" }, "natives-windows": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar", "sha1": "78b2a55ce4dc29c6b3ec4df8ca165eba05f9b341", "size": 613680, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow", "os": { "name": "osx" } } ] }, { "downloads": { "classifiers": { "natives-linux": { "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar", "sha1": "7ff832a6eb9ab6a767f1ade2b548092d0fa64795", "size": 10362, "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar" }, "natives-osx": { "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar", "sha1": "53f9c919f34d2ca9de8c51fc4e1e8282029a9232", "size": 12186, "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar" }, "natives-windows": { "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar", "sha1": "385ee093e01f587f30ee1c8a2ee7d408fd732e16", "size": 155179, "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "net.java.jinput:jinput-platform:2.0.5", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" } } ], "mainClass": "net.minecraft.client.main.Main", "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}", "minimumLauncherVersion": 18, "releaseTime": "2016-02-29T13:49:54+00:00", "time": "2016-03-01T13:14:53+00:00", "type": "release" } PrismLauncher-10.0.5/tests/testdata/MojangVersionFormat/lib-native.json0000644000175100017510000000444615144136757025636 0ustar runnerrunner{ "downloads": { "artifact": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar", "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33", "size": 22, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar" }, "classifiers": { "natives-linux": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar", "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b", "size": 578680, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar" }, "natives-osx": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar", "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed", "size": 426822, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" }, "natives-windows": { "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar", "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0", "size": 613748, "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows" }, "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] } PrismLauncher-10.0.5/tests/testdata/MojangVersionFormat/lib-native-arch.json0000644000175100017510000000316015144136757026541 0ustar runnerrunner{ "downloads": { "classifiers": { "natives-osx": { "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar", "sha1": "62503ee712766cf77f97252e5902786fd834b8c5", "size": 418331, "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar" }, "natives-windows-32": { "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar", "sha1": "7c6affe439099806a4f552da14c42f9d643d8b23", "size": 386792, "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar" }, "natives-windows-64": { "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar", "sha1": "39d0c3d363735b4785598e0e7fbf8297c706a9f9", "size": 463390, "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar" } } }, "extract": { "exclude": [ "META-INF/" ] }, "name": "tv.twitch:twitch-platform:5.16", "natives": { "linux": "natives-linux", "osx": "natives-osx", "windows": "natives-windows-${arch}" }, "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "linux" } } ] } PrismLauncher-10.0.5/tests/testdata/MojangVersionFormat/lib-simple.json0000644000175100017510000000057615144136757025641 0ustar runnerrunner{ "downloads": { "artifact": { "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar", "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da", "size": 5618, "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar" } }, "name": "com.paulscode:codecwav:20101023" } PrismLauncher-10.0.5/tests/testdata/Packwiz/0000755000175100017510000000000015144136757020357 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/Packwiz/screenshot-to-clipboard-fabric.pw.toml0000644000175100017510000000050315144136757027655 0ustar runnerrunnername = "Screenshot to Clipboard (Fabric)" filename = "screenshot-to-clipboard-1.0.7-fabric.jar" side = "both" [download] url = "https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar" hash-format = "murmur2" hash = "1781245820" [update] [update.curseforge] file-id = 3509043 project-id = 327154 PrismLauncher-10.0.5/tests/testdata/Packwiz/borderless-mining.pw.toml0000644000175100017510000000065715144136757025334 0ustar runnerrunnername = "Borderless Mining" filename = "borderless-mining-1.1.1+1.18.jar" side = "client" [download] url = "https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar" hash-format = "sha512" hash = "c8fe6e15ddea32668822dddb26e1851e5f03834be4bcb2eff9c0da7fdc086a9b6cead78e31a44d3bc66335cba11144ee0337c6d5346f1ba63623064499b3188d" [update] [update.modrinth] mod-id = "kYq5qkSL" version = "ug2qKTPR" PrismLauncher-10.0.5/tests/testdata/FileSystem/0000755000175100017510000000000015144136757021033 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/FileSystem/FileSystem-test_createShortcut-unix0000755000175100017510000000013615144136757030062 0ustar runnerrunner[Desktop Entry] Type=Application TryExec=asdfDest Exec=asdfDest 'arg1' 'arg2' Name=asdf Icon= PrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/0000755000175100017510000000000015144136757023345 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/pack.nfo0000644000175100017510000000000215144136757024757 0ustar runnerrunner PrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/pack.mcmeta0000644000175100017510000000013015144136757025445 0ustar runnerrunner{ "pack": { "pack_format": 1, "description": "Some resource pack maybe" } } PrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/assets/0000755000175100017510000000000015144136757024647 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/assets/minecraft/0000755000175100017510000000000015144136757026617 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/assets/minecraft/textures/0000755000175100017510000000000015144136757030502 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/assets/minecraft/textures/blah.txt0000644000175100017510000000000215144136757032141 0ustar runnerrunner PrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/.secret_folder/0000755000175100017510000000000015144136757026243 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/FileSystem/test_folder/.secret_folder/.secret_file.txt0000644000175100017510000000003515144136757031344 0ustar runnerrunneroooooo spooky easter egg :oo PrismLauncher-10.0.5/tests/testdata/Version/0000755000175100017510000000000015144136757020374 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/Version/test_vectors.txt0000644000175100017510000000311015144136757023654 0ustar runnerrunner# Test vector from: # https://github.com/unascribed/FlexVer/blob/704e12759b6e59220ff888f8bf2ec15b8f8fd969/test/test_vectors.txt # # This test file is formatted as " ", seperated by the space character # Implementations should ignore lines starting with "#" and lines that have a length of 0 # Basic numeric ordering (lexical string sort fails these) 10 > 2 100 > 10 # Trivial common numerics 1.0 < 1.1 1.0 < 1.0.1 1.1 > 1.0.1 # SemVer compatibility 1.5 > 1.5-pre1 1.5 = 1.5+foobar # SemVer incompatibility 1.5 < 1.5-2 1.5-pre10 > 1.5-pre2 # Empty strings = 1 > < 1 # Check boundary between textual and prerelease a-a < a # Check boundary between textual and appendix a+a = a # Dash is included in prerelease comparison (if stripped it will be a smaller component) # Note that a-a < a=a regardless since the prerelease splits the component creating a smaller first component; 0 is added to force splitting regardless a0-a < a0=a # Pre-releases must contain only non-digit 1.16.5-10 > 1.16.5 # Pre-releases can have multiple dashes (should not be split) # Reasoning for test data: "p-a!" > "p-a-" (correct); "p-a!" < "p-a t-" (what happens if every dash creates a new component) -a- > -a! # Misc b1.7.3 > a1.2.6 b1.2.6 > a1.7.3 a1.1.2 < a1.1.2_01 1.16.5-0.00.5 > 1.14.2-1.3.7 1.0.0 < 1.0.0_01 1.0.1 > 1.0.0_01 1.0.0_01 < 1.0.1 0.17.1-beta.1 < 0.17.1 0.17.1-beta.1 < 0.17.1-beta.2 1.4.5_01 = 1.4.5_01+fabric-1.17 1.4.5_01 = 1.4.5_01+fabric-1.17+ohgod 14w16a < 18w40b 18w40a < 18w40b 1.4.5_01+fabric-1.17 < 18w40b 13w02a < c0.3.0_01 0.6.0-1.18.x < 0.9.beta-1.18.x PrismLauncher-10.0.5/tests/testdata/TestLogs/0000755000175100017510000000000015144136757020513 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/TestLogs/vanilla-1.21.5.xml.log0000644000175100017510000001273415144136757024174 0ustar runnerrunner PrismLauncher-10.0.5/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt0000644000175100017510000001045315144136757027430 0ustar runnerrunnerUNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN WARN WARN INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR INFO ERROR ERROR ERROR INFO INFO INFO WARN INFO INFO INFO INFO WARN INFO INFO INFO WARN WARN INFO WARN WARN WARN WARN WARN INFO WARN WARN INFO INFO WARN WARN WARN INFO WARN INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN INFO INFO WARN INFO WARN WARN INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN INFO WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO WARN INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO INFO INFO INFO UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO INFO WARN WARN INFO INFO INFO INFO INFO INFO UNKNOWN INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO WARN WARN INFO INFO INFO INFO ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN WARN WARN UNKNOWN INFO UNKNOWN INFO ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN WARN WARN WARN WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR ERROR ERROR ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO PrismLauncher-10.0.5/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt0000644000175100017510000001144315144136757026632 0ustar runnerrunnerUNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO WARN WARN WARN WARN WARN INFO ERROR INFO ERROR ERROR ERROR INFO INFO INFO WARN INFO INFO INFO INFO WARN INFO INFO INFO WARN WARN INFO WARN WARN WARN WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO INFO WARN WARN WARN INFO WARN INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN WARN WARN INFO INFO WARN INFO WARN WARN INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN INFO WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN INFO INFO WARN WARN WARN INFO INFO INFO INFO INFO INFO UNKNOWN INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR INFO ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO WARN ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR INFO INFO INFO WARN ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR WARN ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN WARN WARN WARN WARN WARN WARN WARN UNKNOWN INFO UNKNOWN INFO ERROR INFO INFO INFO INFO UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO UNKNOWN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO WARN WARN WARN WARN WARN WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO ERROR ERROR ERROR ERROR INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO WARN PrismLauncher-10.0.5/tests/testdata/TestLogs/vanilla-1.21.5.text.log0000644000175100017510000000462515144136757024360 0ustar runnerrunner[12:50:56] [Datafixer Bootstrap/INFO]: 263 Datafixer optimizations took 908 milliseconds [12:50:58] [Render thread/INFO]: Environment: Environment[sessionHost=https://sessionserver.mojang.com, servicesHost=https://api.minecraftservices.com, name=PROD] [12:50:58] [Render thread/INFO]: Setting user: Ryexandrite [12:50:58] [Render thread/INFO]: Backend library: LWJGL version 3.3.3+5 [12:50:58] [Render thread/INFO]: Using optional rendering extensions: GL_KHR_debug, GL_ARB_vertex_attrib_binding, GL_ARB_direct_state_access [12:50:58] [Render thread/INFO]: Reloading ResourceManager: vanilla [12:50:59] [Worker-Main-6/INFO]: Found unifont_all_no_pua-16.0.01.hex, loading [12:50:59] [Worker-Main-7/INFO]: Found unifont_jp_patch-16.0.01.hex, loading [12:50:59] [Render thread/WARN]: minecraft:pipeline/entity_translucent_emissive shader program does not use sampler Sampler2 defined in the pipeline. This might be a bug. [12:50:59] [Render thread/INFO]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo [12:50:59] [Render thread/INFO]: Sound engine started [12:50:59] [Render thread/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas [12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas [12:50:59] [Render thread/INFO]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas [12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas [12:50:59] [Render thread/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas [12:50:59] [Render thread/INFO]: Created: 64x64x0 minecraft:textures/atlas/map_decorations.png-atlas [12:50:59] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/particles.png-atlas [12:51:00] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/paintings.png-atlas [12:51:00] [Render thread/INFO]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas [12:51:00] [Render thread/INFO]: Created: 1024x512x0 minecraft:textures/atlas/gui.png-atlas PrismLauncher-10.0.5/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log0000644000175100017510000031134215144136757027370 0ustar runnerrunnerChecking: MC_SLIM Checking: MERGED_MAPPINGS Checking: MAPPINGS Checking: MC_EXTRA Checking: MOJMAPS Checking: PATCHED Checking: MC_SRG 2025-04-18 12:47:23,932 main WARN Advanced terminal features are not available in this environment [12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher running: args [--username, Ryexandrite, --version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --assetIndex, 5, --uuid, , --accessToken, â„â„â„â„â„â„â„â„, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480] [12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher 10.0.9+10.0.9+main.dcd20f30 starting: java version 17.0.8 by Microsoft; OS Linux arch amd64 version 6.6.85 [12:47:24] [main/INFO] [ne.mi.fm.lo.ImmediateWindowHandler/]: Loading ImmediateWindowProvider fmlearlywindow [12:47:24] [main/INFO] [EARLYDISPLAY/]: Trying GL version 4.6 [12:47:24] [main/INFO] [EARLYDISPLAY/]: Requested GL version 4.6 got version 4.6 [12:47:24] [main/INFO] [mixin/]: SpongePowered MIXIN Subsystem Version=0.8.5 Source=union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/org/spongepowered/mixin/0.8.5/mixin-0.8.5.jar%23140!/ Service=ModLauncher Env=CLIENT [12:47:24] [pool-2-thread-1/INFO] [EARLYDISPLAY/]: GL info: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) GL version 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f), AMD [12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/fmlcore/1.20.1-47.2.6/fmlcore-1.20.1-47.2.6.jar is missing mods.toml file [12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/javafmllanguage/1.20.1-47.2.6/javafmllanguage-1.20.1-47.2.6.jar is missing mods.toml file [12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/lowcodelanguage/1.20.1-47.2.6/lowcodelanguage-1.20.1-47.2.6.jar is missing mods.toml file [12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/mclanguage/1.20.1-47.2.6/mclanguage-1.20.1-47.2.6.jar is missing mods.toml file [12:47:25] [main/WARN] [ne.mi.ja.se.JarSelector/]: Attempted to select two dependency jars from JarJar which have the same identification: Mod File: and Mod File: . Using Mod File: [12:47:25] [main/INFO] [ne.mi.fm.lo.mo.JarInJarDependencyLocator/]: Found 28 dependencies adding them to mods collection [12:47:28] [main/ERROR] [mixin/]: Mixin config dynamiclightsreforged.mixins.json does not specify "minVersion" property [12:47:28] [main/INFO] [mixin/]: Compatibility level set to JAVA_17 [12:47:28] [main/ERROR] [mixin/]: Mixin config mixins.satin.client.json does not specify "minVersion" property [12:47:28] [main/ERROR] [mixin/]: Mixin config firstperson.mixins.json does not specify "minVersion" property [12:47:28] [main/ERROR] [mixin/]: Mixin config yacl.mixins.json does not specify "minVersion" property [12:47:28] [main/INFO] [cp.mo.mo.LaunchServiceHandler/MODLAUNCHER]: Launching target 'forgeclient' with arguments [--version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --uuid, , --username, Ryexandrite, --assetIndex, 5, --accessToken, â„â„â„â„â„â„â„â„, --userType, msa, --versionType, release, --width, 854, --height, 480] [12:47:28] [main/INFO] [co.ab.sa.co.Saturn/]: Loaded Saturn config file with 4 configurable options [12:47:28] [main/INFO] [ModernFix/]: Loaded configuration file for ModernFix 5.18.1+mc1.20.1: 83 options available, 1 override(s) found [12:47:28] [main/WARN] [ModernFix/]: Option 'mixin.perf.thread_priorities' overriden (by mods [smoothboot]) to 'false' [12:47:28] [main/INFO] [ModernFix/]: Applying Nashorn fix [12:47:28] [main/INFO] [ModernFix/]: Applied Forge config corruption patch [12:47:28] [main/INFO] [fpsreducer/]: OptiFine was NOT detected. [12:47:28] [main/INFO] [fpsreducer/]: OptiFabric was NOT detected. [12:47:28] [main/WARN] [EmbeddiumConfig/]: Mod 'tfc' attempted to override option 'mixin.features.fast_biome_colors', which doesn't exist, ignoring [12:47:28] [main/INFO] [Embeddium/]: Loaded configuration file for Embeddium: 205 options available, 3 override(s) found [12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Searching for graphics cards... [12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Found graphics card: GraphicsAdapterInfo[vendor=AMD, name=Navi 10 [Radeon RX 5600 OEM/5600 XT / 5700/5700 XT], version=unknown] [12:47:28] [main/WARN] [Embeddium-Workarounds/]: Sodium has applied one or more workarounds to prevent crashes or other issues on your system: [NO_ERROR_CONTEXT_UNSUPPORTED] [12:47:28] [main/WARN] [Embeddium-Workarounds/]: This is not necessarily an issue, but it may result in certain features or optimizations being disabled. You can sometimes fix these issues by upgrading your graphics driver. [12:47:28] [main/INFO] [Radium Config/]: Loaded configuration file for Radium: 125 options available, 7 override(s) found [12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-common-refmap.json' for carpeted-common.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-forge-refmap.json' for carpeted.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'emi-forge-refmap.json' for emi-forge.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-common-refmap.json' for ftbfiltersystem-common.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-forge-refmap.json' for ftbfiltersystem.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/INFO] [Puzzles Lib/]: Loading 160 mods: - additionalplacements 1.8.0 - ae2 15.2.13 - ae2insertexportcard 1.20.1-1.3.0 - ae2netanalyser 1.20-1.0.6-forge - ae2wtlib 15.2.3-forge - aiimprovements 0.5.2 - ambientsounds 6.1.1 - architectury 9.2.14 - astikorcarts 1.1.8 - attributefix 21.0.4 - balm 7.3.9 \-- kuma_api 20.1.8 - barrels_2012 2.1 - betterf3 7.0.2 - betterfoliage 5.0.2 - betterpingdisplay 1.1 - betterthirdperson 1.9.0 - blur 3.1.1 \-- satin 1.20.1+1.15.0-SNAPSHOT - carpeted 1.20-1.4 - carryon 2.1.2.7 \-- mixinextras 0.2.0-beta.6 - catalogue 1.8.0 - chat_heads 0.13.9 - cherishedworlds 6.1.7+1.20.1 - clienttweaks 11.1.0 - cloth_config 11.1.136 - clumps 12.0.0.4 - computercraft 1.113.1 - controlling 12.0.2 - coralstfc 1.0.0 - corpse 1.20.1-1.0.19 - cosmeticarmorreworked 1.20.1-v1a - craftingtweaks 18.2.5 - craftpresence 2.5.0 - create 0.5.1.f \-- flywheel 0.6.10-7 - create_connected 0.8.2-mc1.20.1 - createaddition 1.20.1-1.2.4c - creativecore 2.12.15 - cucumber 7.0.12 - cupboard 1.20.1-2.7 - curios 5.10.0+1.20.1 - defaultoptions 18.0.1 - do_a_barrel_roll 3.5.6+1.20.1 - drippyloadingscreen 3.0.1 - dynamiclightsreforged 1.20.1_v1.6.0 - embeddium 0.3.19+mc1.20.1 \-- rubidium 0.7.1 - embeddiumplus 1.2.12 - emi 1.1.7+1.20.1+forge - enhancedvisuals 1.8.1 - etched 3.0.2 - everycomp 1.20-2.7.12 - expatternprovider 1.20-1.1.14-forge - exposure 1.7.7 - fallingtrees 0.12.7 - fancymenu 3.2.3 - ferritecore 6.0.1 - firmaciv 0.2.10-alpha-1.20.1 - firmalife 2.1.15 - firstperson 2.4.5 - flickerfix 4.0.1 - forge 47.2.6 - fpsreducer 1.20-2.5 - framedblocks 9.3.1 - ftbbackups2 1.0.23 - ftbessentials 2001.2.2 - ftbfiltersystem 1.0.2 - ftblibrary 2001.2.4 - ftbquests 2001.4.8 - ftbranks 2001.1.3 - ftbteams 2001.3.0 - ftbxmodcompat 2.1.1 - gcyr 0.1.8 - getittogetherdrops 1.3 - glodium 1.20-1.5-forge - gtceu 1.2.3.a |-- configuration 2.2.0 \-- ldlib 1.0.25.j - hangglider 8.0.1 - immediatelyfast 1.2.18+1.20.4 - inventoryhud 3.4.26 - invtweaks 1.1.0 - itemphysiclite 1.6.5 - jade 11.9.4+forge - jadeaddons 5.2.2 - jei 15.3.0.8 - konkrete 1.8.0 - ksyxis 1.3.2 - kubejs 2001.6.5-build.14 - kubejs_create 2001.2.5-build.2 - kubejs_tfc 1.20.1-1.1.3 - letmedespawn 1.3.2b - lootjs 1.20.1-2.12.0 - megacells 2.4.4-1.20.1 - melody 1.0.2 - memoryleakfix 1.1.5 - merequester 1.20.1-1.1.5 - minecraft 1.20.1 - modelfix 1.15 - modernfix 5.18.1+mc1.20.1 - moonlight 1.20-2.13.51 - morered 4.0.0.4 |-- jumbofurnace 4.0.0.5 \-- useitemonblockevent 1.0.0.2 - mousetweaks 2.25.1 - myserveriscompatible 1.0 - nanhealthfixer 1.20.1-0.0.1 - nerb 0.4.1 - noisium 2.3.0+mc1.20-1.20.1 - noreportbutton 1.5.0 - notenoughanimations 1.7.6 - octolib 0.4.2 - oculus 1.7.0 - openpartiesandclaims 0.23.2 - packetfixer 1.4.2 - pandalib 0.4.2 - patchouli 1.20.1-84-FORGE - pickupnotifier 8.0.0 - placebo 8.6.2 - playerrevive 2.0.27 - polylib 2000.0.3-build.143 - puzzleslib 8.1.23 \-- puzzlesaccessapi 8.0.7 - radium 0.12.3+git.50c5c33 - railways 1.6.4+forge-mc1.20.1 - recipeessentials 1.20.1-3.6 - rhino 2001.2.2-build.18 - saturn 0.1.3 - searchables 1.0.3 - shimmer 1.20.1-0.2.4 - showcaseitem 1.20.1-1.2 - simplylight 1.20.1-1.4.6-build.50 - smoothboot 0.0.4 - sophisticatedbackpacks 3.20.5.1044 - sophisticatedcore 0.6.22.611 - supermartijn642configlib 1.1.8 - supermartijn642corelib 1.1.17 - tfc 3.2.12 - tfc_tumbleweed 1.2.2 - tfcagedalcohol 2.1 - tfcambiental 1.20.1-3.3.0 - tfcastikorcarts 1.1.8.2 - tfcchannelcasting 0.2.3-beta - tfcea 0.0.2 - tfcgroomer 1.20.1-0.1.2 - tfchotornot 1.0.4 - tfcvesseltooltip 1.1 - tfg 0.5.9 - toofast 0.4.3.5 - toolbelt 1.20.01 - treetap 1.20.1-0.4.0 - tumbleweed 0.5.5 - unilib 1.0.2 - uteamcore 5.1.4.312 - waterflasks 3.0.3 - xaerominimap 24.4.0 - xaeroworldmap 1.39.0 - yeetusexperimentus 2.3.1-build.6+mc1.20.1 - yet_another_config_lib_v3 3.5.0+1.20.1-forge [12:47:28] [main/WARN] [mixin/]: Reference map 'packetfixer-forge-forge-refmap.json' for packetfixer-forge.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:28] [main/WARN] [mixin/]: Reference map 'tfchotornot.refmap.json' for tfchotornot.mixins.json could not be read. If this is a development environment you can ignore this message [12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:29] [main/WARN] [mixin/]: Error loading class: mezz/modnametooltip/TooltipEventHandler (java.lang.ClassNotFoundException: mezz.modnametooltip.TooltipEventHandler) [12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/ClientHelperImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.ClientHelperImpl) [12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.gui.ScreenOverlayImpl) [12:47:29] [main/INFO] [co.cu.Cupboard/]: Loaded config for: recipeessentials.json [12:47:30] [main/WARN] [mixin/]: Error loading class: loaderCommon/forge/com/seibel/distanthorizons/common/wrappers/worldGeneration/mimicObject/ChunkLoader (java.lang.ClassNotFoundException: loaderCommon.forge.com.seibel.distanthorizons.common.wrappers.worldGeneration.mimicObject.ChunkLoader) [12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.RenderSystemMixin will be applied. [12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.WindowMixin will NOT be applied because OptiFine was NOT detected. [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin2 false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatListenerMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ClientPacketListenerMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.CommandSuggestionSuggestionsListMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ConnectionMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.DownloadedPackSourceMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.FontStringRenderOutputMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageLineMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.HttpTextureMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.PlayerChatMessageMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.SkinManagerMixin false false [12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.compat.EmojifulMixin true false [12:47:30] [main/WARN] [mixin/]: Error loading class: dan200/computercraft/shared/integration/jei/JEIComputerCraft (java.lang.ClassNotFoundException: dan200.computercraft.shared.integration.jei.JEIComputerCraft) [12:47:30] [main/WARN] [mixin/]: @Mixin target dan200.computercraft.shared.integration.jei.JEIComputerCraft was not found tfg.mixins.json:common.cc.JEIComputerCraftMixin [12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/slab/CopycatSlabBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock) [12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin [12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/board/CopycatBoardBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock) [12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin [12:47:30] [main/WARN] [mixin/]: Error loading class: me/jellysquid/mods/lithium/common/ai/pathing/PathNodeDefaults (java.lang.ClassNotFoundException: me.jellysquid.mods.lithium.common.ai.pathing.PathNodeDefaults) [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'alloc.blockstate.StateMixin' as option 'mixin.alloc.blockstate' (added by mods [ferritecore]) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.fluid.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.intersection.WorldMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.movement.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.AbstractMinecartEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.BoatEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityPredicatesMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityTrackingSectionMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.LivingEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children [12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'world.player_chunk_tick.ThreadedAnvilChunkStorageMixin' as option 'mixin.world.player_chunk_tick' (added by user configuration) disables it and children [12:47:30] [main/WARN] [mixin/]: Error loading class: org/cyclops/integrateddynamics/block/BlockCable (java.lang.ClassNotFoundException: org.cyclops.integrateddynamics.block.BlockCable) [12:47:30] [main/WARN] [mixin/]: @Mixin target org.cyclops.integrateddynamics.block.BlockCable was not found mixins.epp.json:MixinBlockCable [12:47:30] [main/WARN] [mixin/]: Error loading class: blusunrize/immersiveengineering/api/wires/GlobalWireNetwork (java.lang.ClassNotFoundException: blusunrize.immersiveengineering.api.wires.GlobalWireNetwork) [12:47:30] [main/WARN] [mixin/]: @Mixin target blusunrize.immersiveengineering.api.wires.GlobalWireNetwork was not found mixins.epp.json:MixinGlobalWireNetwork [12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) [12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:TornadoHelperMixin [12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) [12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:client.TornadoHelperMixin [12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Will be applying 3 memory leak fixes! [12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Currently enabled memory leak fixes: [targetEntityLeak, biomeTemperatureLeak, hugeScreenshotLeak] [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.WorldRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.ClientWorldMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.BackgroundRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.GlyphRendererMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.FontSetMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.shadows.EntityRenderDispatcherMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.ModelPartMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.CuboidMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.cull.EntityRendererMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children [12:47:31] [main/WARN] [mixin/]: Error loading class: org/jetbrains/annotations/ApiStatus$Internal (java.lang.ClassNotFoundException: org.jetbrains.annotations.ApiStatus$Internal) [12:47:31] [main/INFO] [MixinExtras|Service/]: Initializing MixinExtras via com.llamalad7.mixinextras.service.MixinExtrasServiceImpl(version=0.4.1). [12:47:31] [main/INFO] [Smooth Boot (Reloaded)/]: Smooth Boot (Reloaded) config initialized [12:47:31] [main/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_216202_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagOrElementLocationMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. [12:47:32] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: ModernFix reached bootstrap stage (9.773 s after launch) [12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByName:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final [12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByValue:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final [12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getNeighborPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin [12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin [12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getAllFlags from me.jellysquid.mods.lithium.mixin.util.block_tracking.AbstractBlockStateMixin [12:47:32] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_6104_ in embeddium.mixins.json:features.options.render_layers.LeavesBlockMixin, previously written by me.srrapero720.embeddiumplus.mixins.impl.leaves_culling.LeavesBlockMixin. Skipping method. [12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel [12:47:33] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: Vanilla bootstrap took 779 milliseconds [12:47:34] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_47505_ in lithium.mixins.json:world.temperature_cache.BiomeMixin, previously written by org.embeddedt.modernfix.common.mixin.perf.remove_biome_temperature_cache.BiomeMixin. Skipping method. [12:47:34] [pool-4-thread-1/INFO] [co.al.me.MERequester/]: Registering content [12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/assets/.mcassetsroot' uses unexpected schema [12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/data/.mcassetsroot' uses unexpected schema [12:47:35] [Render thread/INFO] [mojang/YggdrasilAuthenticationService]: Environment: authHost='https://authserver.mojang.com', accountsHost='https://api.mojang.com', sessionHost='https://sessionserver.mojang.com', servicesHost='https://api.minecraftservices.com', name='PROD' [12:47:35] [Render thread/INFO] [minecraft/Minecraft]: Setting user: Ryexandrite [12:47:35] [Render thread/INFO] [ModernFix/]: Bypassed Mojang DFU [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant is searching for constants in method with descriptor (Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/GuiMessageTag;)V [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = , stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 0 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\r, stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 1 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\r [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = , stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 2 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\n, stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 3 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\n [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found CLASS constant: value = Ljava/lang/String;, typeValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [{}] [CHAT] {}, stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 4 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [{}] [CHAT] {} [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [CHAT] {}, stringValue = null [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 5 [12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [CHAT] {} [12:47:35] [Render thread/INFO] [defaultoptions/]: Loaded default options for extra-folder [12:47:35] [Render thread/INFO] [ModernFix/]: Instantiating Mojang DFU [12:47:36] [Render thread/INFO] [minecraft/Minecraft]: Backend library: LWJGL version 3.3.1 build 7 [12:47:36] [Render thread/INFO] [KubeJS/]: Loaded client.properties [12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Vendor: AMD [12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Renderer: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) [12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Version: 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) [12:47:36] [Render thread/WARN] [Embeddium++/Config]: Loading Embeddium++Config [12:47:36] [Render thread/INFO] [Embeddium++/Config]: Updating config cache [12:47:36] [Render thread/INFO] [Embeddium++/Config]: Cache updated successfully [12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Initializing ImmediatelyFast 1.2.18+1.20.4 on AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (AMD) with OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) [12:47:36] [Render thread/INFO] [ImmediatelyFast/]: AMD GPU detected. Enabling coherent buffer mapping [12:47:36] [Datafixer Bootstrap/INFO] [mojang/DataFixerBuilder]: 188 Datafixer optimizations took 85 milliseconds [12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Found Iris/Oculus 1.7.0. Enabling compatibility. [12:47:36] [Render thread/INFO] [Oculus/]: Debug functionality is disabled. [12:47:36] [Render thread/INFO] [Oculus/]: OpenGL 4.5 detected, enabling DSA. [12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled because no valid shaderpack is selected [12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled [12:47:36] [modloading-worker-0/INFO] [dynamiclightsreforged/]: [LambDynLights] Initializing Dynamic Lights Reforged... [12:47:36] [modloading-worker-0/INFO] [LowDragLib/]: LowDragLib is initializing on platform: Forge [12:47:36] [modloading-worker-0/INFO] [in.u_.u_.ut.ve.JarSignVerifier/]: Mod uteamcore is signed with a valid certificate. [12:47:36] [modloading-worker-0/INFO] [de.ke.me.Melody/]: [MELODY] Loading Melody background audio library.. [12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for pickupnotifier:main [12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_109501_ in embeddium.mixins.json:core.render.world.WorldRendererMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. [12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Successfully initialized! [12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Server-side libs ready to use! [12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for pickupnotifier:main [12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_215924_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagEntryMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. [12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: Attempting to manually load Additional Placements config early. [12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: manual config load successful. [12:47:36] [modloading-worker-0/WARN] [Additional Placements/]: During block registration you may recieve several reports of "Potentially Dangerous alternative prefix `additionalplacements`". Ignore these, they are intended. [12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for hangglider:main [12:47:36] [modloading-worker-0/INFO] [noisium/]: Loading Noisium. [12:47:36] [modloading-worker-0/INFO] [co.cu.Cupboard/]: Loaded config for: cupboard.json [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id architectury:sync_ids [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id architectury:sync_ids [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id pandalib:config_sync [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id pandalib:config_sync [12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_client' [12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_client' [12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for hangglider:main [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:container_to_client [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:tile_to_client [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:container_packet_server [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_data_server [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_packet_server [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_nbt [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftblibrary:edit_nbt_response [12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_common' [12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_common' [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:sync_known_server_registries [12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_config [12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "craftpresence" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json" [12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "unilib" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json" [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbessentials:update_tab_name [12:47:37] [modloading-worker-0/INFO] [invtweaks/]: Registered 2 network packets [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded common.properties [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded dev.properties [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Looking for KubeJS plugins... [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_teams [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_message_history [12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: GregTechCEu is initializing on platform: Forge [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:open_gui [12:47:37] [CraftPresence/INFO] [craftpresence/]: Configuration settings have been saved and reloaded successfully! [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:open_my_team_gui [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:update_settings [12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.latvian.mods.kubejs.integration.forge.gamestages.GameStagesIntegration does not have required mod gamestages loaded, skipping [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ldlib [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source exposure [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source tfg [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_settings_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:send_message [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ftbxmodcompat [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:send_message_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_presence [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:create_party [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:player_gui_operation [12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.ftb.mods.ftbxmodcompat.ftbchunks.kubejs.FTBChunksKubeJSPlugin does not have required mod ftbchunks loaded, skipping [12:47:37] [modloading-worker-0/INFO] [de.ke.dr.DrippyLoadingScreen/]: [DRIPPY LOADING SCREEN] Loading v3.0.1 in client-side mode on FORGE! [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source lootjs [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source cucumber [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gtceu [12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Initializing TerraFirmaCraft [12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Options: Assertions Enabled = false, Boostrap = false, Test = false, Debug Logging = true [12:47:37] [CraftPresence/INFO] [craftpresence/]: Checking Discord for available assets with Client Id: 1182610212121743470 [12:47:37] [CraftPresence/INFO] [craftpresence/]: Originally coded by paulhobbel - https://github.com/paulhobbel [12:47:37] [modloading-worker-0/INFO] [ne.mi.co.ForgeMod/FORGEMOD]: Forge mod loading, version 47.2.6, for MC 1.20.1 with MCP 20230612.114412 [12:47:37] [modloading-worker-0/INFO] [ne.mi.co.MinecraftForge/FORGE]: MinecraftForge v47.2.6 Initialized [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gcyr [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_tfc [12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.precpros.PrecProsPlugin does not have required mod precisionprospecting loaded, skipping [12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.afc.AFCPlugin does not have required mod afc loaded, skipping [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_create [Mouse Tweaks] Main.initialize() [Mouse Tweaks] Initialized. [12:47:37] [modloading-worker-0/INFO] [Every Compat/]: Loaded EveryCompat Create Module [12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function [12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function [12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gcyr config for auto-sync function [12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: High-Tier is Disabled. [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Using default implementation for ThreadExecutor [12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Done in 309.0 ms [12:47:37] [CraftPresence/INFO] [craftpresence/]: 3 total assets detected! [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_quests [12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Booting... (platform: Forge, manual: false) [12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Found Mixin library. (version: 0.8.5) [12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Ready. As always, this mod will speed up your world loading and might or might not break it. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_team_data [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.SchedulerSignalerImpl/]: Initialized Scheduler Signaller of type: class net.creeperhost.ftbbackups.repack.org.quartz.core.SchedulerSignalerImpl [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Quartz Scheduler v.2.0.2 created. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_task_progress [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.si.RAMJobStore/]: RAMJobStore initialized. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:submit_task [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler meta-data: Quartz Scheduler (v2.0.2) 'ftbbackups2' with instanceId 'NON_CLUSTERED' Scheduler class: 'net.creeperhost.ftbbackups.repack.org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.SimpleThreadPool' - with 1 threads. Using job-store 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler 'ftbbackups2' initialized from an externally provided properties instance. [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler version: 2.0.2 [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_reward [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:claim_reward_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editing_mode [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:get_emergency_items [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_other_team_data [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_all_rewards [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_choice_reward [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_completion_toast [12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler ftbbackups2_$_NON_CLUSTERED started. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_reward_toast [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_item_reward_toast [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_pinned [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_pinned_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_chapter_pinned [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_chapter_pinned_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_editing_mode [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:force_save [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_team_data [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:set_custom_image [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started_reset [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed_reset [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_lock [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:reset_reward [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:team_data_changed [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:task_screen_config_req [12:47:37] [modloading-worker-0/INFO] [co.jo.fl.ba.Backend/]: Oculus detected. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:task_screen_config_resp [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_progress [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_object [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_object_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_task_at [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:delete_object [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:delete_object_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:edit_object [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:edit_object_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_quest [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_quest_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_chapter_group [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:change_chapter_group_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter_group [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_group_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_reward_blocking [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_quest [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_chapter_image [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:sync_structures_request [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_structures_response [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:request_team_data [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editor_permission [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:open_quest_book [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:clear_display_cache [12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager... [12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager took 11.32 ms [12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised items. [12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbfiltersystem:sync_filter [12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised blocks. [12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised block entities. [12:47:37] [modloading-worker-0/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Loading v3.2.3 in client-side mode on FORGE! [12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loading config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml [12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Built config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml [12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loaded config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml [12:47:37] [modloading-worker-0/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled KubeJS integration [12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Starting... [12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Loading... [12:47:37] [modloading-worker-0/INFO] [de.to.pa.PacketFixer/]: Packet Fixer has been initialized successfully [12:47:37] [modloading-worker-0/INFO] [YetAnotherConfigLib/]: Deserializing YACLConfig from '/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/yacl.json5' [12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for puzzleslib:main [12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] All done! [12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for puzzleslib:main [12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading Rhino Minecraft remapper... [12:47:37] [modloading-worker-0/INFO] [de.la.mo.rh.mo.ut.RhinoProperties/]: Rhino properties loaded. [12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading mappings for 1.20.1 [12:47:37] [modloading-worker-0/WARN] [mixin/]: @Inject(@At("INVOKE")) Shift.BY=2 on create_connected.mixins.json:sequencedgearshift.SequencedGearshiftScreenMixin::handler$cfa000$updateParamsOfRow exceeds the maximum allowed value: 0. Increase the value of maxShiftBy to suppress this warning. [12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Done in 0.090 s [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registered bogey styles from railways [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering data fixers [12:47:38] [modloading-worker-0/WARN] [Railways/]: Skipping Datafixer Registration due to it being disabled in the config. [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Hex Casting [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Oh The Biomes You'll Go [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Blue Skies [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Twilight Forest [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Biomes O' Plenty [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Nature's Spirit [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Dreams and Desires [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Quark [12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for TerraFirmaCraft [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:main_startup_script.js in 0.058 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfc/constants.js in 0.024 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:horornot/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:minecraft/constants.js in 0.004 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:railways/constants.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/machines.js in 0.008 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/material_info.js in 0.002 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/recipe_types.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/blocks.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/items.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:framedblocks/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmalife/constants.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:sophisticated_backpacks/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:more_red/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:mega_cells/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create/constants.js in 0.002 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ae2/constants.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create_additions/constants.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:chisel_and_bits/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:extended_ae2/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:asticor_carts/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/blocks.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ftb_quests/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/fluids.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/materials.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/blocks.js in 0.001 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/items.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:computer_craft/constants.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded 30/30 KubeJS startup scripts in 0.721 s with 0 errors and 0 warnings [12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: example.js#3: TerraFirmaGreg the best modpack in the world :) [12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:example.js in 0.0 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:tooltips.js in 0.003 s [12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded 2/2 KubeJS client scripts in 0.022 s with 0 errors and 0 warnings [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#48: Loaded Java class 'net.minecraft.world.level.block.AmethystClusterBlock' [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#49: Loaded Java class 'net.minecraft.world.level.block.Blocks' [12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#50: Loaded Java class 'net.minecraft.world.level.block.state.BlockBehaviour$Properties' [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:send_data_from_client [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:send_data_from_server [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:paint [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:add_stage [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:remove_stage [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:sync_stages [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:first_click [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:toast [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:reload_startup_scripts [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_server_errors [12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_client_errors [12:47:38] [Render thread/INFO] [GregTechCEu/]: GTCEu common proxy init! [12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering material registries [12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering GTCEu Materials [12:47:38] [CraftPresence/INFO] [craftpresence/]: Attempting to connect to Discord (1/10)... [12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering addon Materials [12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:water [12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:lava [12:47:39] [CraftPresence/INFO] [craftpresence/]: Loaded display data with Client Id: 1182610212121743470 (Logged in as RyRy) [12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering KeyBinds [12:47:39] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'gtceu' took 1.043 s to run a deferred task. [12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@67141ef8 has been registered twice for the same name ae2:export_card. [12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@f4864e9 has been registered twice for the same name ae2:insert_card. [12:47:42] [Render thread/INFO] [Moonlight/]: Initialized block sets in 21ms [12:47:42] [Render thread/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ae2wtlib:cycle_terminal [12:47:43] [Render thread/INFO] [Every Compat/]: Registering Compat WoodType Blocks [12:47:43] [Render thread/INFO] [Every Compat/]: EveryCompat Create Module: registered 42 WoodType blocks [12:47:43] [Render thread/INFO] [tf.TFCTumbleweed/]: Injecting TFC Tumbleweed override pack [12:47:43] [Render thread/INFO] [co.ee.fi.FirmaLife/]: Injecting firmalife override pack [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) [12:47:43] [Render thread/INFO] [Oculus/]: Hardware information: [12:47:43] [Render thread/INFO] [Oculus/]: CPU: 16x AMD Ryzen 7 3700X 8-Core Processor [12:47:43] [Render thread/INFO] [Oculus/]: GPU: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (Supports OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f)) [12:47:43] [Render thread/INFO] [Oculus/]: OS: Linux (6.6.85) [12:47:44] [Render thread/WARN] [mixin/]: Method overwrite conflict for isHidden in mixins.oculus.compat.sodium.json:copyEntity.ModelPartMixin, previously written by dev.tr7zw.firstperson.mixins.ModelPartMixin. Skipping method. [12:47:44] [Render thread/INFO] [minecraft/Minecraft]: [FANCYMENU] Registering resource reload listener.. [12:47:44] [Render thread/INFO] [de.ke.fa.cu.ScreenCustomization/]: [FANCYMENU] Initializing screen customization engine! Addons should NOT REGISTER TO REGISTRIES anymore now! [12:47:44] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: STARTING [12:47:44] [Render thread/INFO] [ModernFix/]: Invalidating pack caches [12:47:44] [Render thread/INFO] [minecraft/ReloadableResourceManager]: Reloading ResourceManager: Additional Placements blockstate redirection pack, vanilla, mod_resources, gtceu:dynamic_assets, Moonlight Mods Dynamic Assets, Firmalife-1.20.1-2.1.15.jar:overload, TFCTumbleweed-1.20.1-1.2.2.jar:overload, KubeJS Resource Pack [assets], ldlib [12:47:44] [Finalizer/WARN] [ModernFix/]: One or more BufferBuilders have been leaked, ModernFix will attempt to correct this. [12:47:45] [Render thread/INFO] [Every Compat/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (everycomp) in: 597 ms [12:47:45] [Render thread/INFO] [Moonlight/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (moonlight) in: 0 ms [12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for pickupnotifier [12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for hangglider [12:47:45] [Worker-ResourceReload-4/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading [12:47:45] [Worker-ResourceReload-3/INFO] [xa.pa.OpenPartiesAndClaims/]: Loading Open Parties and Claims! [12:47:45] [Worker-ResourceReload-1/INFO] [co.re.RecipeEssentials/]: recipeessentials mod initialized [12:47:45] [Worker-ResourceReload-10/INFO] [ne.dr.tf.TerraFirmaCraft/]: TFC Common Setup [12:47:45] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [saturn] Starting version check at https://github.com/AbdElAziz333/Saturn/raw/mc1.20.1/dev/updates.json [12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting game stages provider implementation to: KubeJS Stages [12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [KubeJS Stages] as the active game stages implementation [12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting permissions provider implementation to: FTB Ranks [12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [FTB Ranks] as the active permissions implementation [12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] recipe helper provider is [JEI] [12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled FTB Filter System integration [12:47:45] [Render thread/INFO] [GregTechCEu/]: GregTech Model loading took 520ms [12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Initializing fonts for text rendering.. [12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Calculating animation sizes for FancyMenu.. [12:47:46] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: drippy_loading_overlay [12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Preloading animations! This could cause the loading screen to freeze for a while.. [12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Finished preloading animations! [12:47:46] [Render thread/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Starting late client initialization phase.. [12:47:46] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} ... 6 more [12:47:46] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Starting version check at https://updates.blamejared.com/get?n=controlling&gv=1.20.1 [12:47:46] [Worker-ResourceReload-6/ERROR] [minecraft/SimpleJsonResourceReloadListener]: Couldn't parse data file tfc:field_guide/ru_ru/entries/tfg_ores/surface_copper from tfc:patchouli_books/field_guide/ru_ru/entries/tfg_ores/surface_copper.json com.google.gson.JsonParseException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:526) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} at net.minecraft.util.GsonHelper.m_263475_(GsonHelper.java:531) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} at net.minecraft.util.GsonHelper.m_13776_(GsonHelper.java:581) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_278771_(SimpleJsonResourceReloadListener.java:41) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:29) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:17) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} at net.minecraft.server.packs.resources.SimplePreparableReloadListener.m_10786_(SimplePreparableReloadListener.java:11) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,re:classloading,pl:accesstransformer:B,pl:mixin:APP:moonlight.mixins.json:ConditionHackMixin,pl:mixin:A} at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[?:?] {} at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1760) ~[?:?] {} at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1657) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1463) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:569) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:422) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:779) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:725) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.TypeAdapters$34$1.read(TypeAdapters.java:1007) ~[gson-2.10.jar%2388!/:?] {} at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:524) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} ... 13 more [12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! [12:47:46] [Render thread/INFO] [KubeJS Client/]: Client resource reload complete! [12:47:46] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings [12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! [12:47:46] [Worker-Main-6/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Found status: BETA Current: 12.0.2 Target: 12.0.2 [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Starting version check at https://api.u-team.info/update/uteamcore.json [12:47:47] [FTB Backups Config Watcher 0/INFO] [ne.cr.ft.FTBBackups/]: Config at /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/ftbbackups2.json has changed, reloaded! [12:47:47] [Worker-ResourceReload-14/WARN] [minecraft/SpriteLoader]: Texture create_connected:block/fluid_container_window_debug with size 40x32 limits mip level from 4 to 3 [12:47:47] [UniLib/INFO] [unilib/]: Received update status for "unilib" -> Outdated (Target version: "v1.0.5") [12:47:47] [UniLib/INFO] [unilib/]: Received update status for "craftpresence" -> Outdated (Target version: "v2.5.4") [12:47:47] [Render thread/INFO] [Every Compat/]: Registered 42 compat blocks making up 0.31% of total blocks registered [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Found status: OUTDATED Current: 5.1.4.312 Target: 5.1.4.346 [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/pickupnotifier.json [12:47:47] [Render thread/INFO] [Moonlight/]: Initialized color sets in 104ms [12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: KubeJS TFC configuration: [12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: Debug mode enabled: false [12:47:47] [Render thread/INFO] [MEGA Cells/]: Initialised AE2WT integration. [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Found status: UP_TO_DATE Current: 8.0.0 Target: null [12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Starting version check at https://update.maxhenkel.de/forge/corpse [12:47:47] [Worker-ResourceReload-0/INFO] [FirstPersonModel/]: Loading FirstPerson Mod [12:47:47] [Worker-ResourceReload-4/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 1/2 [12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loading patreon trails data... [12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loading patreon wing data... [12:47:47] [Worker-ResourceReload-13/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Client-side libs ready to use! [12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loaded 45 patreon trails. [12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loaded 21 patreon wings. [12:47:47] [Worker-ResourceReload-7/INFO] [EMI/]: [EMI] Discovered Sodium [12:47:47] [Worker-ResourceReload-14/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 1/2 [12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_wut [12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_restock [12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:restock_amounts [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Found status: OUTDATED Current: 1.20.1-1.0.19 Target: 1.20.1-1.0.20 [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Starting version check at https://raw.githubusercontent.com/hlysine/create_connected/main/update.json [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Found status: OUTDATED Current: 0.8.2-mc1.20.1 Target: 1.0.1-mc1.20.1 [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Starting version check at https://api.modrinth.com/updates/rubidium-extra/forge_updates.json [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Found status: AHEAD Current: 3.1.1 Target: null [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/hangglider.json [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Found status: UP_TO_DATE Current: 8.0.1 Target: null [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Starting version check at https://updates.blamejared.com/get?n=searchables&gv=1.20.1 [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Found status: BETA Current: 1.0.3 Target: 1.0.3 [12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Starting version check at https://api.modrinth.com/updates/cc-tweaked/forge_updates.json [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Found status: OUTDATED Current: 1.113.1 Target: 1.115.1 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Found status: AHEAD Current: 1.0.2 Target: null [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Found status: AHEAD Current: 2.5.0 Target: null [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Starting version check at https://api.modrinth.com/updates/radium/forge_updates.json [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Found status: OUTDATED Current: 0.12.3+git.50c5c33 Target: 0.12.4 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Starting version check at https://updates.blamejared.com/get?n=attributefix&gv=1.20.1 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Found status: BETA Current: 21.0.4 Target: 21.0.4 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Starting version check at https://updates.blamejared.com/get?n=clumps&gv=1.20.1 [12:47:49] [Worker-ResourceReload-4/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking patreon: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Found status: BETA Current: 12.0.0.4 Target: 12.0.0.4 [12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [catalogue] Starting version check at https://mrcrayfish.com/modupdatejson?id=catalogue [12:47:50] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} ... 6 more [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzlesaccessapi.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Found status: BETA Current: 8.0.7 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Starting version check at https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json [12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: tfcambiental:snowshoes#inventory java.io.FileNotFoundException: tfcambiental:models/item/snowshoes.json at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:266) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119306_(ModelBakery.java:384) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:150) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} [12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: carpeted:block/label java.io.FileNotFoundException: carpeted:models/block/label.json at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:262) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:159) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Found status: OUTDATED Current: 47.2.6 Target: 47.4.0 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Starting version check at https://raw.githubusercontent.com/MehVahdJukaar/Moonlight/multi-loader/forge/update.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Found status: BETA Current: 1.20-2.13.51 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Starting version check at https://raw.githubusercontent.com/Toma1O6/UpdateSchemas/master/configuration-forge.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Found status: OUTDATED Current: 2.2.0 Target: 2.2.1 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Starting version check at https://github.com/AbdElAziz333/SmoothBoot-Reloaded/raw/mc1.20.1/dev/updates.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Found status: UP_TO_DATE Current: 0.0.4 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Starting version check at https://raw.githubusercontent.com/VidTu/Ksyxis/main/updater_ksyxis_forge.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Found status: OUTDATED Current: 1.3.2 Target: 1.3.3 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Starting version check at https://api.modrinth.com/updates/flywheel/forge_updates.json [12:47:50] [Worker-ResourceReload-4/ERROR] [xa.ma.WorldMap/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Found status: BETA Current: 0.6.10-7 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Starting version check at https://raw.githubusercontent.com/DmitryLovin/pluginUpdate/master/invupdate.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Found status: UP_TO_DATE Current: 3.4.26 Target: null [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzleslib.json [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Found status: OUTDATED Current: 8.1.23 Target: 8.1.32 [12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Starting version check at https://api.modrinth.com/updates/betterf3/forge_updates.json [12:47:50] [Render thread/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 2/2 [12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Found status: UP_TO_DATE Current: 7.0.2 Target: null [12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Starting version check at https://api.modrinth.com/updates/packet-fixer/forge_updates.json [12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Found status: OUTDATED Current: 1.4.2 Target: 2.0.0 [12:47:51] [Render thread/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: minimap_synced [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: World Map found! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: openpartiesandclaims [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Open Parties And Claims found! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: No Optifine! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: No Vivecraft! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Framed Blocks found! [12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Iris found! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 2/2 [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: New world map region cache hash code: -815523079 [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: map_synced [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Xaero's minimap found! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: minimap_synced [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: openpartiesandclaims [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Open Parties And Claims found! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: No Optifine! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: No Vivecraft! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Framed Blocks found! [12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Iris found! [12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model firmaciv:firmaciv_compass#inventory: minecraft:textures/atlas/blocks.png:minecraft:item/compass [12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:tin_double_ingot#inventory: minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay [12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model createaddition:small_light_connector#facing=west,mode=push,powered=true,rotation=x_clockwise_180,variant=default: minecraft:textures/atlas/blocks.png:create:block/chute_block [12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:copper_double_ingot#inventory: minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay Reloading Dynamic Lights [12:47:57] [Render thread/INFO] [co.jo.fl.ba.Backend/]: Loaded all shader sources. Create Crafts & Additions Initialized! [12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded values for 19 compatible attributes. [12:47:57] [Worker-ResourceReload-2/ERROR] [AttributeFix/]: Attribute ID 'minecolonies:mc_mob_damage' does not belong to a known attribute. This entry will be ignored. [12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded 20 values from config. [12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Saving config file. 20 entries. [12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Applying changes for 20 attributes. [12:47:57] [Worker-ResourceReload-11/INFO] [de.me.as.AstikorCarts/]: Automatic pull animal configuration: pull_animals = [ "minecraft:camel", "minecraft:donkey", "minecraft:horse", "minecraft:mule", "minecraft:skeleton_horse", "minecraft:zombie_horse", "minecraft:player", "tfc:donkey", "tfc:mule", "tfc:horse" ] [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at io.github.mortuusars.exposure.integration.jade.ExposureJadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at de.maxhenkel.corpse.integration.waila.PluginCorpse [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at xfacthd.framedblocks.common.compat.jade.FramedJadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.general.GeneralPlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.create.CreatePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at me.pandamods.fallingtrees.compat.JadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.gregtechceu.gtceu.integration.jade.GTJadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at net.dries007.tfc.compat.jade.JadeIntegration [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.vanilla.VanillaPlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.universal.UniversalPlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.core.CorePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at appeng.integration.modules.jade.JadeModule [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.glodblock.github.extendedae.xmod.jade.JadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at cy.jdkdigital.treetap.compat.jade.JadePlugin [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.eerussianguy.firmalife.compat.tooltip.JadeIntegration [12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.ljuangbminecraft.tfcchannelcasting.compat.JadeIntegration [12:47:59] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'create_connected' took 1.342 s to run a deferred task. [12:47:59] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) [12:47:59] [Render thread/INFO] [mojang/Library]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo [12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: Sound engine started [12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: [FANCYMENU] Reloading AudioResourceHandler after Minecraft SoundEngine reload.. [12:47:59] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 4096x2048x4 minecraft:textures/atlas/blocks.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x512x4 minecraft:textures/atlas/signs.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x1024x4 minecraft:textures/atlas/chest.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh particle. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_solid. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_solid. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout_mipped. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout_mipped. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_translucent. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_armor_cutout_no_cull. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_solid. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull_z_offset. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent_cull. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_smooth_cutout. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_decal. [12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_no_outline. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find sampler named Sampler2 in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named IViewRotMat in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named FogShape in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find sampler named Sampler2 in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light0_Direction in the specified shader program. [12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light1_Direction in the specified shader program. [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x0 minecraft:textures/atlas/particles.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas [12:48:00] [Render thread/INFO] [xa.ma.WorldMap/]: Successfully reloaded the world map shaders! [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Loading exposure filters: [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_blue_pane, exposure:shaders/light_blue_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:orange_pane, exposure:shaders/orange_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:red_pane, exposure:shaders/red_filter.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:purple_pane, exposure:shaders/purple_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:blue_pane, exposure:shaders/blue_filter.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_gray_pane, exposure:shaders/light_gray_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:magenta_pane, exposure:shaders/magenta_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:gray_pane, exposure:shaders/gray_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:lime_pane, exposure:shaders/lime_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:green_pane, exposure:shaders/green_filter.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:pink_pane, exposure:shaders/pink_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:yellow_pane, exposure:shaders/yellow_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:white_pane, exposure:shaders/white_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:brown_pane, exposure:shaders/brown_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:glass_pane, exposure:shaders/crisp.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:interplanar_projector, exposure:shaders/invert.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:black_pane, exposure:shaders/black_tint.json] added. [12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:cyan_pane, exposure:shaders/cyan_tint.json] added. [12:48:00] [Render thread/INFO] [patchouli/]: BookContentResourceListenerLoader preloaded 1073 jsons [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x128x0 computercraft:textures/atlas/gui.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x0 polylib:textures/atlas/gui.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 jei:textures/atlas/gui.png-atlas [12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 moonlight:textures/atlas/map_markers.png-atlas [12:48:00] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Successfully reloaded the minimap shaders! [12:48:00] [Render thread/INFO] [Shimmer/]: buildIn shimmer configuration is enabled, this can be disabled by config file [12:48:00] [Render thread/INFO] [Shimmer/]: mod jar and resource pack discovery: file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] [12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.ResourceHandlers/]: [FANCYMENU] Reloading resources.. [12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.pr.ResourcePreLoader/]: [FANCYMENU] Pre-loading resources.. [12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Updating animation sizes.. [12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: FINISHED [12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: title_screen [12:48:00] [Render thread/INFO] [Oculus/]: Creating pipeline for dimension NamespacedId{namespace='minecraft', name='overworld'} [12:48:01] [Render thread/INFO] [ambientsounds/]: Loaded AmbientEngine 'basic' v3.1.0. 11 dimension(s), 11 features, 11 blockgroups, 2 sound collections, 37 regions, 58 sounds, 11 sound categories, 5 solids and 2 biome types [12:48:01] [Render thread/INFO] [FirstPersonModel/]: PlayerAnimator not found! [12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Vanilla Hands items: [] [12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Auto Disable items: [camera] [12:48:02] [Render thread/WARN] [ModernFix/]: Game took 40.304 seconds to start PrismLauncher-10.0.5/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log0000644000175100017510000077213615144136757027220 0ustar runnerrunnerChecking: MC_SLIM Checking: MERGED_MAPPINGS Checking: MAPPINGS Checking: MC_EXTRA Checking: MOJMAPS Checking: PATCHED Checking: MC_SRG , --accessToken, â„â„â„â„â„â„â„â„, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480]]]> , --username, Ryexandrite, --assetIndex, 5, --accessToken, â„â„â„â„â„â„â„â„, --userType, msa, --versionType, release, --width, 854, --height, 480]]]> Outdated (Target version: "v2.5.4")]]> [Mouse Tweaks] Main.initialize() [Mouse Tweaks] Initialized. Outdated (Target version: "v1.0.5")]]> [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) (ModelBakery.java:150) at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ]]> (ModelBakery.java:159) at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ]]> Reloading Dynamic Lights Create Crafts & Additions Initialized! [ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) PrismLauncher-10.0.5/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt0000644000175100017510000000017515144136757024717 0ustar runnerrunnerINFO INFO INFO INFO INFO INFO INFO INFO WARN INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO INFO PrismLauncher-10.0.5/tests/testdata/CatPacks/0000755000175100017510000000000015144136757020440 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/CatPacks/index.json0000644000175100017510000000205415144136757022443 0ustar runnerrunner{ "name": "My Cute Cat", "default": "maxwell.png", "variants": [ { "startTime": { "day": 12, "month": 4 }, "endTime": { "day": 12, "month": 4 }, "path": "oneDay.png" }, { "startTime": { "day": 20, "month": 12 }, "endTime": { "day": 28, "month": 12 }, "path": "christmas.png" }, { "startTime": { "day": 30, "month": 12 }, "endTime": { "day": 1, "month": 1 }, "path": "newyear2.png" }, { "startTime": { "day": 28, "month": 12 }, "endTime": { "day": 3, "month": 1 }, "path": "newyear.png" } ] } PrismLauncher-10.0.5/tests/testdata/PackageManifest/0000755000175100017510000000000015144136757021771 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect/0000755000175100017510000000000015144136757023436 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect/a/0000755000175100017510000000000015144136757023656 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect/a/b/0000755000175100017510000000000015144136757024077 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect/a/b/b.txt0000777000175100017510000000000015144136757026253 2../b.txtustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect/a/b.txt0000755000175100017510000000000015144136757024631 0ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect_win/0000755000175100017510000000000015144136757024313 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect_win/a/0000755000175100017510000000000015144136757024533 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect_win/a/b/0000755000175100017510000000000015144136757024754 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect_win/a/b/b.txt0000644000175100017510000000000015144136757025724 0ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/inspect_win/a/b.txt0000644000175100017510000000000015144136757025503 0ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/PackageManifest/1.8.0_202-x64.json0000644000175100017510000060172315144136757024343 0ustar runnerrunner{ "files": { "COPYRIGHT": { "downloads": { "lzma": { "sha1": "dd860e040807f7e53ae89da5f28dd73d57ac605d", "size": 1431, "url": "https://launcher.mojang.com/v1/objects/dd860e040807f7e53ae89da5f28dd73d57ac605d/COPYRIGHT" }, "raw": { "sha1": "c725183c757011e7ba96c83c1e86ee7e8b516a2b", "size": 3244, "url": "https://launcher.mojang.com/v1/objects/c725183c757011e7ba96c83c1e86ee7e8b516a2b/COPYRIGHT" } }, "executable": false, "type": "file" }, "LICENSE": { "downloads": { "raw": { "sha1": "3e86865deec0814c958bcf7fb87f790bccc0e8bd", "size": 40, "url": "https://launcher.mojang.com/v1/objects/3e86865deec0814c958bcf7fb87f790bccc0e8bd/LICENSE" } }, "executable": false, "type": "file" }, "README": { "downloads": { "raw": { "sha1": "f90331df1e5badeadc501d8dd70714c62a920204", "size": 46, "url": "https://launcher.mojang.com/v1/objects/f90331df1e5badeadc501d8dd70714c62a920204/README" } }, "executable": false, "type": "file" }, "THIRDPARTYLICENSEREADME-JAVAFX.txt": { "downloads": { "lzma": { "sha1": "4fee85109d7ff04b982d0576dabd15397f599125", "size": 15455, "url": "https://launcher.mojang.com/v1/objects/4fee85109d7ff04b982d0576dabd15397f599125/THIRDPARTYLICENSEREADME-JAVAFX.txt" }, "raw": { "sha1": "56ff42f87607b997b52ae0ef8bf315e36932e870", "size": 112724, "url": "https://launcher.mojang.com/v1/objects/56ff42f87607b997b52ae0ef8bf315e36932e870/THIRDPARTYLICENSEREADME-JAVAFX.txt" } }, "executable": false, "type": "file" }, "THIRDPARTYLICENSEREADME.txt": { "downloads": { "lzma": { "sha1": "419c1414ba46ae9dbfd38cf4e0601fff61644429", "size": 32266, "url": "https://launcher.mojang.com/v1/objects/419c1414ba46ae9dbfd38cf4e0601fff61644429/THIRDPARTYLICENSEREADME.txt" }, "raw": { "sha1": "b83c3f32261de3e48ccd20614a11e066b1ec9027", "size": 153824, "url": "https://launcher.mojang.com/v1/objects/b83c3f32261de3e48ccd20614a11e066b1ec9027/THIRDPARTYLICENSEREADME.txt" } }, "executable": false, "type": "file" }, "Welcome.html": { "downloads": { "lzma": { "sha1": "01c21a74b4aafb7cbe0388233c43cbdf77dcaaea", "size": 528, "url": "https://launcher.mojang.com/v1/objects/01c21a74b4aafb7cbe0388233c43cbdf77dcaaea/Welcome.html" }, "raw": { "sha1": "d98ae54f03dac87419abc19b97e315830c2da55f", "size": 955, "url": "https://launcher.mojang.com/v1/objects/d98ae54f03dac87419abc19b97e315830c2da55f/Welcome.html" } }, "executable": false, "type": "file" }, "bin": { "type": "directory" }, "bin/ControlPanel": { "target": "jcontrol", "type": "link" }, "bin/java": { "downloads": { "lzma": { "sha1": "3857eea1d59e1bc545c67a753ed2768254807b8a", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/3857eea1d59e1bc545c67a753ed2768254807b8a/java" }, "raw": { "sha1": "3d20560fb5d1a49cb689c2226972e92e06d27ba6", "size": 8464, "url": "https://launcher.mojang.com/v1/objects/3d20560fb5d1a49cb689c2226972e92e06d27ba6/java" } }, "executable": true, "type": "file" }, "bin/javaws": { "downloads": { "lzma": { "sha1": "a6bec5c049e76c4488294a256a2084ea23ddb440", "size": 38173, "url": "https://launcher.mojang.com/v1/objects/a6bec5c049e76c4488294a256a2084ea23ddb440/javaws" }, "raw": { "sha1": "955c0f0066e2f893b0c2b3ccd83e223722e4ab74", "size": 140296, "url": "https://launcher.mojang.com/v1/objects/955c0f0066e2f893b0c2b3ccd83e223722e4ab74/javaws" } }, "executable": true, "type": "file" }, "bin/jcontrol": { "downloads": { "lzma": { "sha1": "40c5e33748f252e1d950b579a4185ab2c23fc908", "size": 2166, "url": "https://launcher.mojang.com/v1/objects/40c5e33748f252e1d950b579a4185ab2c23fc908/jcontrol" }, "raw": { "sha1": "ed541733c8b51e34349c1f8010b277e58ad73f1e", "size": 6264, "url": "https://launcher.mojang.com/v1/objects/ed541733c8b51e34349c1f8010b277e58ad73f1e/jcontrol" } }, "executable": true, "type": "file" }, "bin/jjs": { "downloads": { "lzma": { "sha1": "d44d1ac421979f7671921986214812095a5b0e3b", "size": 2168, "url": "https://launcher.mojang.com/v1/objects/d44d1ac421979f7671921986214812095a5b0e3b/jjs" }, "raw": { "sha1": "f00f944c3dbe556793b5dc686aaeee3e5722e99b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/f00f944c3dbe556793b5dc686aaeee3e5722e99b/jjs" } }, "executable": true, "type": "file" }, "bin/keytool": { "downloads": { "lzma": { "sha1": "93c607dce450976667c382f609a367167bdec05c", "size": 2175, "url": "https://launcher.mojang.com/v1/objects/93c607dce450976667c382f609a367167bdec05c/keytool" }, "raw": { "sha1": "7114b561546270e441e9ed1bcc24e5188c068a42", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/7114b561546270e441e9ed1bcc24e5188c068a42/keytool" } }, "executable": true, "type": "file" }, "bin/orbd": { "downloads": { "lzma": { "sha1": "b27dfded5e2b2f6f02c555971c94e46ca14ac81b", "size": 2254, "url": "https://launcher.mojang.com/v1/objects/b27dfded5e2b2f6f02c555971c94e46ca14ac81b/orbd" }, "raw": { "sha1": "7f31217fecb3dbbd89f1dd3783fca58793a66fd2", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/7f31217fecb3dbbd89f1dd3783fca58793a66fd2/orbd" } }, "executable": true, "type": "file" }, "bin/pack200": { "downloads": { "lzma": { "sha1": "b52da4497b49b1508b6225a5740857ddb8f52e97", "size": 2183, "url": "https://launcher.mojang.com/v1/objects/b52da4497b49b1508b6225a5740857ddb8f52e97/pack200" }, "raw": { "sha1": "16ef3e801efb57e50bc6477a27a9d95d02d0775b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/16ef3e801efb57e50bc6477a27a9d95d02d0775b/pack200" } }, "executable": true, "type": "file" }, "bin/policytool": { "downloads": { "lzma": { "sha1": "87da4c07da45f3d1a1a9d732af197cd39bf69d10", "size": 2182, "url": "https://launcher.mojang.com/v1/objects/87da4c07da45f3d1a1a9d732af197cd39bf69d10/policytool" }, "raw": { "sha1": "a52a29424470cb9b8db5c2fb1751d0b697a7ec8e", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/a52a29424470cb9b8db5c2fb1751d0b697a7ec8e/policytool" } }, "executable": true, "type": "file" }, "bin/rmid": { "downloads": { "lzma": { "sha1": "1494c1174fde0c0a93ea117bc7edf7eb936c0512", "size": 2172, "url": "https://launcher.mojang.com/v1/objects/1494c1174fde0c0a93ea117bc7edf7eb936c0512/rmid" }, "raw": { "sha1": "5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8/rmid" } }, "executable": true, "type": "file" }, "bin/rmiregistry": { "downloads": { "lzma": { "sha1": "7070cf2ec5a5e520a880bae699431edf02083e7e", "size": 2174, "url": "https://launcher.mojang.com/v1/objects/7070cf2ec5a5e520a880bae699431edf02083e7e/rmiregistry" }, "raw": { "sha1": "5f518daa7050028d5d9d849634c73136f2b23a54", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/5f518daa7050028d5d9d849634c73136f2b23a54/rmiregistry" } }, "executable": true, "type": "file" }, "bin/servertool": { "downloads": { "lzma": { "sha1": "1db683a11cc9b7313426c84412f4d95be2fa7ccd", "size": 2185, "url": "https://launcher.mojang.com/v1/objects/1db683a11cc9b7313426c84412f4d95be2fa7ccd/servertool" }, "raw": { "sha1": "49d0ebfeb265ce5a8733e1014541ea2525674a60", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/49d0ebfeb265ce5a8733e1014541ea2525674a60/servertool" } }, "executable": true, "type": "file" }, "bin/tnameserv": { "downloads": { "lzma": { "sha1": "36da9c9a2c5a8b662a3f8d52ca67339bce1c2714", "size": 2291, "url": "https://launcher.mojang.com/v1/objects/36da9c9a2c5a8b662a3f8d52ca67339bce1c2714/tnameserv" }, "raw": { "sha1": "09d998f8efcb6f55d0d87f59e08f8b89662796d9", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/09d998f8efcb6f55d0d87f59e08f8b89662796d9/tnameserv" } }, "executable": true, "type": "file" }, "bin/unpack200": { "downloads": { "lzma": { "sha1": "344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2", "size": 79721, "url": "https://launcher.mojang.com/v1/objects/344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2/unpack200" }, "raw": { "sha1": "5dd933132f1b202e19e0c8e093f7113711cfdfc1", "size": 182616, "url": "https://launcher.mojang.com/v1/objects/5dd933132f1b202e19e0c8e093f7113711cfdfc1/unpack200" } }, "executable": true, "type": "file" }, "lib": { "type": "directory" }, "lib/amd64": { "type": "directory" }, "lib/amd64/jli": { "type": "directory" }, "lib/amd64/jli/libjli.so": { "downloads": { "lzma": { "sha1": "372331ee8e375888f798a2e88180a94493e141b0", "size": 48327, "url": "https://launcher.mojang.com/v1/objects/372331ee8e375888f798a2e88180a94493e141b0/libjli.so" }, "raw": { "sha1": "73b0cf8b7415686bc40c561ff77ff2740ccf7a44", "size": 108616, "url": "https://launcher.mojang.com/v1/objects/73b0cf8b7415686bc40c561ff77ff2740ccf7a44/libjli.so" } }, "executable": true, "type": "file" }, "lib/amd64/jvm.cfg": { "downloads": { "lzma": { "sha1": "86bcfebec37b38415525ffd77d3eaf70d0b1b4ca", "size": 435, "url": "https://launcher.mojang.com/v1/objects/86bcfebec37b38415525ffd77d3eaf70d0b1b4ca/jvm.cfg" }, "raw": { "sha1": "84b38bdc745de446ba0ca0232ea3aaf2efd721da", "size": 627, "url": "https://launcher.mojang.com/v1/objects/84b38bdc745de446ba0ca0232ea3aaf2efd721da/jvm.cfg" } }, "executable": false, "type": "file" }, "lib/amd64/libavplugin-53.so": { "downloads": { "lzma": { "sha1": "a332366762d9efc7b845a682b7edce62db44618c", "size": 14747, "url": "https://launcher.mojang.com/v1/objects/a332366762d9efc7b845a682b7edce62db44618c/libavplugin-53.so" }, "raw": { "sha1": "9bd1473dd8a0dc7950c7af1cc69a45548df26eb5", "size": 51720, "url": "https://launcher.mojang.com/v1/objects/9bd1473dd8a0dc7950c7af1cc69a45548df26eb5/libavplugin-53.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-54.so": { "downloads": { "lzma": { "sha1": "2c615852a0720a275163e00597c1f711f11341da", "size": 15153, "url": "https://launcher.mojang.com/v1/objects/2c615852a0720a275163e00597c1f711f11341da/libavplugin-54.so" }, "raw": { "sha1": "8808050c5949c4800b42d1b19b1f8b0d120bcacb", "size": 51768, "url": "https://launcher.mojang.com/v1/objects/8808050c5949c4800b42d1b19b1f8b0d120bcacb/libavplugin-54.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-55.so": { "downloads": { "lzma": { "sha1": "39ee8e7fe14f0010c78973962800f539c3e4c16b", "size": 15168, "url": "https://launcher.mojang.com/v1/objects/39ee8e7fe14f0010c78973962800f539c3e4c16b/libavplugin-55.so" }, "raw": { "sha1": "f10ea4ea3489e96d8d161a96790133c417ec44e1", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/f10ea4ea3489e96d8d161a96790133c417ec44e1/libavplugin-55.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-56.so": { "downloads": { "lzma": { "sha1": "abe7feced5a559f1bdc868526dc69484e0e591a0", "size": 15169, "url": "https://launcher.mojang.com/v1/objects/abe7feced5a559f1bdc868526dc69484e0e591a0/libavplugin-56.so" }, "raw": { "sha1": "e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed/libavplugin-56.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-57.so": { "downloads": { "lzma": { "sha1": "4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9", "size": 15174, "url": "https://launcher.mojang.com/v1/objects/4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9/libavplugin-57.so" }, "raw": { "sha1": "2949e7ff9b0ac90e8943c211cff141ab12eec3f8", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/2949e7ff9b0ac90e8943c211cff141ab12eec3f8/libavplugin-57.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-ffmpeg-56.so": { "downloads": { "lzma": { "sha1": "c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6", "size": 15231, "url": "https://launcher.mojang.com/v1/objects/c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6/libavplugin-ffmpeg-56.so" }, "raw": { "sha1": "0d36c971a9ad99fc2292092fdec3a4179b1021b9", "size": 51920, "url": "https://launcher.mojang.com/v1/objects/0d36c971a9ad99fc2292092fdec3a4179b1021b9/libavplugin-ffmpeg-56.so" } }, "executable": true, "type": "file" }, "lib/amd64/libavplugin-ffmpeg-57.so": { "downloads": { "lzma": { "sha1": "087426bdbffebcfa372a438e863785f4ffbe9a6b", "size": 15180, "url": "https://launcher.mojang.com/v1/objects/087426bdbffebcfa372a438e863785f4ffbe9a6b/libavplugin-ffmpeg-57.so" }, "raw": { "sha1": "5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462/libavplugin-ffmpeg-57.so" } }, "executable": true, "type": "file" }, "lib/amd64/libawt.so": { "downloads": { "lzma": { "sha1": "018be58b205b73c842a55df811b70d0e8237216e", "size": 195720, "url": "https://launcher.mojang.com/v1/objects/018be58b205b73c842a55df811b70d0e8237216e/libawt.so" }, "raw": { "sha1": "02632cd326e3161c00a7e784599dd7b9ee053dce", "size": 759184, "url": "https://launcher.mojang.com/v1/objects/02632cd326e3161c00a7e784599dd7b9ee053dce/libawt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libawt_headless.so": { "downloads": { "lzma": { "sha1": "7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa", "size": 11211, "url": "https://launcher.mojang.com/v1/objects/7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa/libawt_headless.so" }, "raw": { "sha1": "862157ec957008d0911c5daedc004b3a202623a4", "size": 39176, "url": "https://launcher.mojang.com/v1/objects/862157ec957008d0911c5daedc004b3a202623a4/libawt_headless.so" } }, "executable": true, "type": "file" }, "lib/amd64/libawt_xawt.so": { "downloads": { "lzma": { "sha1": "d536a96af27dfe35de6bb2c8759d51c488cdd8d4", "size": 149598, "url": "https://launcher.mojang.com/v1/objects/d536a96af27dfe35de6bb2c8759d51c488cdd8d4/libawt_xawt.so" }, "raw": { "sha1": "28232b3e01b6f11bfe098bfc6eafc3a513dcebf1", "size": 470232, "url": "https://launcher.mojang.com/v1/objects/28232b3e01b6f11bfe098bfc6eafc3a513dcebf1/libawt_xawt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libbci.so": { "downloads": { "lzma": { "sha1": "c36fad091d11e64c815d5ca17c0ef7a55b0776b1", "size": 3509, "url": "https://launcher.mojang.com/v1/objects/c36fad091d11e64c815d5ca17c0ef7a55b0776b1/libbci.so" }, "raw": { "sha1": "33824051db1ccb6332e22c2b63231055240d0af0", "size": 12760, "url": "https://launcher.mojang.com/v1/objects/33824051db1ccb6332e22c2b63231055240d0af0/libbci.so" } }, "executable": true, "type": "file" }, "lib/amd64/libdcpr.so": { "downloads": { "lzma": { "sha1": "70c6b0933a37f2b1124d6e7c131039241fe796ee", "size": 75969, "url": "https://launcher.mojang.com/v1/objects/70c6b0933a37f2b1124d6e7c131039241fe796ee/libdcpr.so" }, "raw": { "sha1": "fa7001bc5d80579e2716590f3eee8027da0beae7", "size": 204456, "url": "https://launcher.mojang.com/v1/objects/fa7001bc5d80579e2716590f3eee8027da0beae7/libdcpr.so" } }, "executable": true, "type": "file" }, "lib/amd64/libdecora_sse.so": { "downloads": { "lzma": { "sha1": "514acc017dfb6cefaf8cc6d18006ce55781cc9bc", "size": 24397, "url": "https://launcher.mojang.com/v1/objects/514acc017dfb6cefaf8cc6d18006ce55781cc9bc/libdecora_sse.so" }, "raw": { "sha1": "d0c84233504c916e548e29f513e25f6a7479abfc", "size": 74912, "url": "https://launcher.mojang.com/v1/objects/d0c84233504c916e548e29f513e25f6a7479abfc/libdecora_sse.so" } }, "executable": true, "type": "file" }, "lib/amd64/libdeploy.so": { "downloads": { "lzma": { "sha1": "6cf31fd98301c749ac0d2c7825f6d925a4409760", "size": 168999, "url": "https://launcher.mojang.com/v1/objects/6cf31fd98301c749ac0d2c7825f6d925a4409760/libdeploy.so" }, "raw": { "sha1": "b3832e97ed8ca794884b56a591b83d02a2c0c06f", "size": 642368, "url": "https://launcher.mojang.com/v1/objects/b3832e97ed8ca794884b56a591b83d02a2c0c06f/libdeploy.so" } }, "executable": true, "type": "file" }, "lib/amd64/libdt_socket.so": { "downloads": { "lzma": { "sha1": "4cc5c880dbb6fa180436d12d60f0abec8ebb59dc", "size": 7784, "url": "https://launcher.mojang.com/v1/objects/4cc5c880dbb6fa180436d12d60f0abec8ebb59dc/libdt_socket.so" }, "raw": { "sha1": "91ce96f252b8139fc12f0f224ed5b1a041767ab7", "size": 24616, "url": "https://launcher.mojang.com/v1/objects/91ce96f252b8139fc12f0f224ed5b1a041767ab7/libdt_socket.so" } }, "executable": true, "type": "file" }, "lib/amd64/libfontmanager.so": { "downloads": { "lzma": { "sha1": "f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d", "size": 146951, "url": "https://launcher.mojang.com/v1/objects/f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d/libfontmanager.so" }, "raw": { "sha1": "2428e805f2c53d1283a033dfd11a86fbb7bd7159", "size": 490672, "url": "https://launcher.mojang.com/v1/objects/2428e805f2c53d1283a033dfd11a86fbb7bd7159/libfontmanager.so" } }, "executable": true, "type": "file" }, "lib/amd64/libfxplugins.so": { "downloads": { "lzma": { "sha1": "a640143365d382a5ad743a784bc2f3706d9d6d67", "size": 50048, "url": "https://launcher.mojang.com/v1/objects/a640143365d382a5ad743a784bc2f3706d9d6d67/libfxplugins.so" }, "raw": { "sha1": "0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee", "size": 151448, "url": "https://launcher.mojang.com/v1/objects/0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee/libfxplugins.so" } }, "executable": true, "type": "file" }, "lib/amd64/libglass.so": { "downloads": { "lzma": { "sha1": "f1ff517714fa5f2c861f33b32db823fe851541f1", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/f1ff517714fa5f2c861f33b32db823fe851541f1/libglass.so" }, "raw": { "sha1": "e7f4fece30ac727be8148d33b8256abd3a41cef9", "size": 13072, "url": "https://launcher.mojang.com/v1/objects/e7f4fece30ac727be8148d33b8256abd3a41cef9/libglass.so" } }, "executable": true, "type": "file" }, "lib/amd64/libglassgtk2.so": { "downloads": { "lzma": { "sha1": "15b90f7a2baacd15e80aa9785d87cf1e4258376d", "size": 220476, "url": "https://launcher.mojang.com/v1/objects/15b90f7a2baacd15e80aa9785d87cf1e4258376d/libglassgtk2.so" }, "raw": { "sha1": "e30a634c2ff2143bdee512360553d6e0304f33b2", "size": 844984, "url": "https://launcher.mojang.com/v1/objects/e30a634c2ff2143bdee512360553d6e0304f33b2/libglassgtk2.so" } }, "executable": true, "type": "file" }, "lib/amd64/libglassgtk3.so": { "downloads": { "lzma": { "sha1": "868c231165f8c9043b7f0e7de208ec023f06a6e7", "size": 220560, "url": "https://launcher.mojang.com/v1/objects/868c231165f8c9043b7f0e7de208ec023f06a6e7/libglassgtk3.so" }, "raw": { "sha1": "762a11a2b376b7b5a2a7cad780715524fdd176d5", "size": 845304, "url": "https://launcher.mojang.com/v1/objects/762a11a2b376b7b5a2a7cad780715524fdd176d5/libglassgtk3.so" } }, "executable": true, "type": "file" }, "lib/amd64/libglib-lite.so": { "downloads": { "lzma": { "sha1": "61b8871242febe1be262de167dc20ae94bf964b4", "size": 457046, "url": "https://launcher.mojang.com/v1/objects/61b8871242febe1be262de167dc20ae94bf964b4/libglib-lite.so" }, "raw": { "sha1": "63afa060fc3f120af76128e51d32603fc4336fa8", "size": 1538352, "url": "https://launcher.mojang.com/v1/objects/63afa060fc3f120af76128e51d32603fc4336fa8/libglib-lite.so" } }, "executable": true, "type": "file" }, "lib/amd64/libgstreamer-lite.so": { "downloads": { "lzma": { "sha1": "2447dc368406ba1b989a29937d41924620e01988", "size": 673056, "url": "https://launcher.mojang.com/v1/objects/2447dc368406ba1b989a29937d41924620e01988/libgstreamer-lite.so" }, "raw": { "sha1": "5505e7ca592ac64371d3db8fe53bcb602e9723d3", "size": 2263872, "url": "https://launcher.mojang.com/v1/objects/5505e7ca592ac64371d3db8fe53bcb602e9723d3/libgstreamer-lite.so" } }, "executable": true, "type": "file" }, "lib/amd64/libhprof.so": { "downloads": { "lzma": { "sha1": "94a5589c818db1fb1cf1881e24e217c309fce2e4", "size": 64471, "url": "https://launcher.mojang.com/v1/objects/94a5589c818db1fb1cf1881e24e217c309fce2e4/libhprof.so" }, "raw": { "sha1": "4bb9bdeef6133b6dd558d52d691b077c03e9b0ee", "size": 175504, "url": "https://launcher.mojang.com/v1/objects/4bb9bdeef6133b6dd558d52d691b077c03e9b0ee/libhprof.so" } }, "executable": true, "type": "file" }, "lib/amd64/libinstrument.so": { "downloads": { "lzma": { "sha1": "84ffea356caf725b42c86a8ebc9587f477ddde29", "size": 18603, "url": "https://launcher.mojang.com/v1/objects/84ffea356caf725b42c86a8ebc9587f477ddde29/libinstrument.so" }, "raw": { "sha1": "cb8009769601e3fecd7ea2b36c344f737b1a9da7", "size": 51560, "url": "https://launcher.mojang.com/v1/objects/cb8009769601e3fecd7ea2b36c344f737b1a9da7/libinstrument.so" } }, "executable": true, "type": "file" }, "lib/amd64/libj2gss.so": { "downloads": { "lzma": { "sha1": "4b2aa699506b126098b585a9617ce1c05707fa29", "size": 14132, "url": "https://launcher.mojang.com/v1/objects/4b2aa699506b126098b585a9617ce1c05707fa29/libj2gss.so" }, "raw": { "sha1": "cbce4a302b255d4d1924ef7606f038af766c5e86", "size": 47688, "url": "https://launcher.mojang.com/v1/objects/cbce4a302b255d4d1924ef7606f038af766c5e86/libj2gss.so" } }, "executable": true, "type": "file" }, "lib/amd64/libj2pcsc.so": { "downloads": { "lzma": { "sha1": "2361d3b2e3da48593c391b29b0d2b5409e4c55e5", "size": 5074, "url": "https://launcher.mojang.com/v1/objects/2361d3b2e3da48593c391b29b0d2b5409e4c55e5/libj2pcsc.so" }, "raw": { "sha1": "1274178492e7a3e997e12f67794616f7c3d8d0b9", "size": 18296, "url": "https://launcher.mojang.com/v1/objects/1274178492e7a3e997e12f67794616f7c3d8d0b9/libj2pcsc.so" } }, "executable": true, "type": "file" }, "lib/amd64/libj2pkcs11.so": { "downloads": { "lzma": { "sha1": "ef927e2790ba05931d0f0bdd63da3d275a834946", "size": 21573, "url": "https://launcher.mojang.com/v1/objects/ef927e2790ba05931d0f0bdd63da3d275a834946/libj2pkcs11.so" }, "raw": { "sha1": "bd4f2af9bfdc6168633d1920c1a1415de06bb45a", "size": 79472, "url": "https://launcher.mojang.com/v1/objects/bd4f2af9bfdc6168633d1920c1a1415de06bb45a/libj2pkcs11.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjaas_unix.so": { "downloads": { "lzma": { "sha1": "7f7e843544ee1eb1454a5826bdd4218685b79430", "size": 2404, "url": "https://launcher.mojang.com/v1/objects/7f7e843544ee1eb1454a5826bdd4218685b79430/libjaas_unix.so" }, "raw": { "sha1": "4c517925c7d464a5b719898eb0bea1b04df31f1f", "size": 8192, "url": "https://launcher.mojang.com/v1/objects/4c517925c7d464a5b719898eb0bea1b04df31f1f/libjaas_unix.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjava.so": { "downloads": { "lzma": { "sha1": "5eee7a42600a44a8bb8d6d7f510fd96a29637ac0", "size": 63113, "url": "https://launcher.mojang.com/v1/objects/5eee7a42600a44a8bb8d6d7f510fd96a29637ac0/libjava.so" }, "raw": { "sha1": "e280aeddf3fc0ec664aef7efc0e0e197a54aaf02", "size": 227672, "url": "https://launcher.mojang.com/v1/objects/e280aeddf3fc0ec664aef7efc0e0e197a54aaf02/libjava.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjava_crw_demo.so": { "downloads": { "lzma": { "sha1": "b197cf23ae3556eb0b45c663f0a8cb62408b961e", "size": 10412, "url": "https://launcher.mojang.com/v1/objects/b197cf23ae3556eb0b45c663f0a8cb62408b961e/libjava_crw_demo.so" }, "raw": { "sha1": "18f20f906977c90d0090b41dbda8dd5cfead5a4c", "size": 26144, "url": "https://launcher.mojang.com/v1/objects/18f20f906977c90d0090b41dbda8dd5cfead5a4c/libjava_crw_demo.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_font.so": { "downloads": { "lzma": { "sha1": "ffbba0e5022f829412b86063d8a90f95f16709b1", "size": 5608, "url": "https://launcher.mojang.com/v1/objects/ffbba0e5022f829412b86063d8a90f95f16709b1/libjavafx_font.so" }, "raw": { "sha1": "8634a0aca612fc40420a4a7cc8af4cc46cfc6725", "size": 17104, "url": "https://launcher.mojang.com/v1/objects/8634a0aca612fc40420a4a7cc8af4cc46cfc6725/libjavafx_font.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_font_freetype.so": { "downloads": { "lzma": { "sha1": "310271eda8a2ac264ffc3640a9d847b49438d0bd", "size": 6942, "url": "https://launcher.mojang.com/v1/objects/310271eda8a2ac264ffc3640a9d847b49438d0bd/libjavafx_font_freetype.so" }, "raw": { "sha1": "3e7572d047c12ba2bc43acec7f98a67c20af8042", "size": 27616, "url": "https://launcher.mojang.com/v1/objects/3e7572d047c12ba2bc43acec7f98a67c20af8042/libjavafx_font_freetype.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_font_pango.so": { "downloads": { "lzma": { "sha1": "a7bcf0669e70b0f43099a99c81e6b6440cb40ac0", "size": 5820, "url": "https://launcher.mojang.com/v1/objects/a7bcf0669e70b0f43099a99c81e6b6440cb40ac0/libjavafx_font_pango.so" }, "raw": { "sha1": "f0b775cc9a514c7ee8b4d6fb300653ce548caf10", "size": 25560, "url": "https://launcher.mojang.com/v1/objects/f0b775cc9a514c7ee8b4d6fb300653ce548caf10/libjavafx_font_pango.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_font_t2k.so": { "downloads": { "lzma": { "sha1": "551c29dc7c7fc83223aa36a6187f7e0c5d650538", "size": 431450, "url": "https://launcher.mojang.com/v1/objects/551c29dc7c7fc83223aa36a6187f7e0c5d650538/libjavafx_font_t2k.so" }, "raw": { "sha1": "91e5813057c3b852d411540160f8ad05fb9f1ed3", "size": 1486128, "url": "https://launcher.mojang.com/v1/objects/91e5813057c3b852d411540160f8ad05fb9f1ed3/libjavafx_font_t2k.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjavafx_iio.so": { "downloads": { "lzma": { "sha1": "c832998fd5e06ed6dcd6428816194c350785420c", "size": 101479, "url": "https://launcher.mojang.com/v1/objects/c832998fd5e06ed6dcd6428816194c350785420c/libjavafx_iio.so" }, "raw": { "sha1": "dcdf68cb25677b76c1cf0bb94294e6e9880a6678", "size": 256336, "url": "https://launcher.mojang.com/v1/objects/dcdf68cb25677b76c1cf0bb94294e6e9880a6678/libjavafx_iio.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjawt.so": { "downloads": { "lzma": { "sha1": "c1ced6aad5c69ff444dc67d0fd7e333558953831", "size": 1872, "url": "https://launcher.mojang.com/v1/objects/c1ced6aad5c69ff444dc67d0fd7e333558953831/libjawt.so" }, "raw": { "sha1": "c5032f2c6fa40bea24e56605cf76b26a27e87b67", "size": 8048, "url": "https://launcher.mojang.com/v1/objects/c5032f2c6fa40bea24e56605cf76b26a27e87b67/libjawt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjdwp.so": { "downloads": { "lzma": { "sha1": "c1aabbb3f5a624b9ad10ed871a1d83510a99b646", "size": 94884, "url": "https://launcher.mojang.com/v1/objects/c1aabbb3f5a624b9ad10ed871a1d83510a99b646/libjdwp.so" }, "raw": { "sha1": "a043e97be47937f6f552e94cf79c76c1c57f9594", "size": 272248, "url": "https://launcher.mojang.com/v1/objects/a043e97be47937f6f552e94cf79c76c1c57f9594/libjdwp.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjfr.so": { "downloads": { "lzma": { "sha1": "11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7", "size": 8780, "url": "https://launcher.mojang.com/v1/objects/11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7/libjfr.so" }, "raw": { "sha1": "312392dd186b11c418183e818f1928e8685a07e5", "size": 28384, "url": "https://launcher.mojang.com/v1/objects/312392dd186b11c418183e818f1928e8685a07e5/libjfr.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjfxmedia.so": { "downloads": { "lzma": { "sha1": "a4e7a126eb648ce6e5e6dc151831da37d8334139", "size": 391897, "url": "https://launcher.mojang.com/v1/objects/a4e7a126eb648ce6e5e6dc151831da37d8334139/libjfxmedia.so" }, "raw": { "sha1": "5fa54944327a6012c3d34cb5c1c4432762178dc8", "size": 1636376, "url": "https://launcher.mojang.com/v1/objects/5fa54944327a6012c3d34cb5c1c4432762178dc8/libjfxmedia.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjfxwebkit.so": { "downloads": { "lzma": { "sha1": "b274debd222cdcc2ee84160ebb95144b3880bc97", "size": 20492825, "url": "https://launcher.mojang.com/v1/objects/b274debd222cdcc2ee84160ebb95144b3880bc97/libjfxwebkit.so" }, "raw": { "sha1": "ecee564c3b2f645131b35bb3004abd4caeabd291", "size": 91014584, "url": "https://launcher.mojang.com/v1/objects/ecee564c3b2f645131b35bb3004abd4caeabd291/libjfxwebkit.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjpeg.so": { "downloads": { "lzma": { "sha1": "9ad55e370c5eaaa73c3158339db3c368b1aaf0cb", "size": 113072, "url": "https://launcher.mojang.com/v1/objects/9ad55e370c5eaaa73c3158339db3c368b1aaf0cb/libjpeg.so" }, "raw": { "sha1": "651e6d53ae67db1f0efbf7f104447a9b49b7e333", "size": 292520, "url": "https://launcher.mojang.com/v1/objects/651e6d53ae67db1f0efbf7f104447a9b49b7e333/libjpeg.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjsdt.so": { "downloads": { "lzma": { "sha1": "04b6d1361a34c496b5f652b2477784d69b8b6baf", "size": 3964, "url": "https://launcher.mojang.com/v1/objects/04b6d1361a34c496b5f652b2477784d69b8b6baf/libjsdt.so" }, "raw": { "sha1": "82b48a82bf6183d34cf00a0f81661b45c616f31b", "size": 12904, "url": "https://launcher.mojang.com/v1/objects/82b48a82bf6183d34cf00a0f81661b45c616f31b/libjsdt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjsig.so": { "downloads": { "lzma": { "sha1": "37d3b89abde397216cc4ecb1339d8543d99b8428", "size": 3536, "url": "https://launcher.mojang.com/v1/objects/37d3b89abde397216cc4ecb1339d8543d99b8428/libjsig.so" }, "raw": { "sha1": "42e52ba1bcbe0362ab24bcf65c93797354db6fb9", "size": 13336, "url": "https://launcher.mojang.com/v1/objects/42e52ba1bcbe0362ab24bcf65c93797354db6fb9/libjsig.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjsound.so": { "downloads": { "lzma": { "sha1": "7e3c565d74d8ffae716f32b05544fa4a6f108adc", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7e3c565d74d8ffae716f32b05544fa4a6f108adc/libjsound.so" }, "raw": { "sha1": "0c0fc63b92d7b83c9960fa80d45c80553ea20254", "size": 8232, "url": "https://launcher.mojang.com/v1/objects/0c0fc63b92d7b83c9960fa80d45c80553ea20254/libjsound.so" } }, "executable": true, "type": "file" }, "lib/amd64/libjsoundalsa.so": { "downloads": { "lzma": { "sha1": "b06c51858a25ff776519495f1b9b3d9f604b089f", "size": 23097, "url": "https://launcher.mojang.com/v1/objects/b06c51858a25ff776519495f1b9b3d9f604b089f/libjsoundalsa.so" }, "raw": { "sha1": "281d37f0326d4a12dc7ea316ead09c198ff7bdf7", "size": 83256, "url": "https://launcher.mojang.com/v1/objects/281d37f0326d4a12dc7ea316ead09c198ff7bdf7/libjsoundalsa.so" } }, "executable": true, "type": "file" }, "lib/amd64/liblcms.so": { "downloads": { "lzma": { "sha1": "7a239baba2086cae49114b382b74b971da02f08e", "size": 176175, "url": "https://launcher.mojang.com/v1/objects/7a239baba2086cae49114b382b74b971da02f08e/liblcms.so" }, "raw": { "sha1": "c8895cc3c3d023d9e059225969ab67954772c0a1", "size": 526872, "url": "https://launcher.mojang.com/v1/objects/c8895cc3c3d023d9e059225969ab67954772c0a1/liblcms.so" } }, "executable": true, "type": "file" }, "lib/amd64/libmanagement.so": { "downloads": { "lzma": { "sha1": "aed3fdbcefd1716abfc6a306687c8b741cbb318e", "size": 12838, "url": "https://launcher.mojang.com/v1/objects/aed3fdbcefd1716abfc6a306687c8b741cbb318e/libmanagement.so" }, "raw": { "sha1": "eba35f61e0d50e30874b7c7b335edf2d52662423", "size": 51808, "url": "https://launcher.mojang.com/v1/objects/eba35f61e0d50e30874b7c7b335edf2d52662423/libmanagement.so" } }, "executable": true, "type": "file" }, "lib/amd64/libmlib_image.so": { "downloads": { "lzma": { "sha1": "1bb181f079492d55c7a458e96488cd17fe0a7b86", "size": 310272, "url": "https://launcher.mojang.com/v1/objects/1bb181f079492d55c7a458e96488cd17fe0a7b86/libmlib_image.so" }, "raw": { "sha1": "c973c450d33873675945d4694be484e3427f58f1", "size": 1048136, "url": "https://launcher.mojang.com/v1/objects/c973c450d33873675945d4694be484e3427f58f1/libmlib_image.so" } }, "executable": true, "type": "file" }, "lib/amd64/libnet.so": { "downloads": { "lzma": { "sha1": "9dd79703b6deb86e0321afe01c6ac508263c8312", "size": 38123, "url": "https://launcher.mojang.com/v1/objects/9dd79703b6deb86e0321afe01c6ac508263c8312/libnet.so" }, "raw": { "sha1": "b3a17b7d53fcdf1e689e1ec29ce851eee6022ead", "size": 116920, "url": "https://launcher.mojang.com/v1/objects/b3a17b7d53fcdf1e689e1ec29ce851eee6022ead/libnet.so" } }, "executable": true, "type": "file" }, "lib/amd64/libnio.so": { "downloads": { "lzma": { "sha1": "5697c89d5d5d9b74f2e1555fcbba79dd4049e287", "size": 24445, "url": "https://launcher.mojang.com/v1/objects/5697c89d5d5d9b74f2e1555fcbba79dd4049e287/libnio.so" }, "raw": { "sha1": "573bf8f64dbcc397f8abd3e1da28f90ab0679f5b", "size": 93872, "url": "https://launcher.mojang.com/v1/objects/573bf8f64dbcc397f8abd3e1da28f90ab0679f5b/libnio.so" } }, "executable": true, "type": "file" }, "lib/amd64/libnpjp2.so": { "downloads": { "lzma": { "sha1": "6fe53b5951ff740e7f2ef7ffe5975af26da06718", "size": 57892, "url": "https://launcher.mojang.com/v1/objects/6fe53b5951ff740e7f2ef7ffe5975af26da06718/libnpjp2.so" }, "raw": { "sha1": "2bb13c53a4280379253475e51216b97eed1d4ce3", "size": 216592, "url": "https://launcher.mojang.com/v1/objects/2bb13c53a4280379253475e51216b97eed1d4ce3/libnpjp2.so" } }, "executable": true, "type": "file" }, "lib/amd64/libnpt.so": { "downloads": { "lzma": { "sha1": "1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876", "size": 5070, "url": "https://launcher.mojang.com/v1/objects/1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876/libnpt.so" }, "raw": { "sha1": "6b1ff6b9b4624f3cc7801f221c82b8046fb76364", "size": 17504, "url": "https://launcher.mojang.com/v1/objects/6b1ff6b9b4624f3cc7801f221c82b8046fb76364/libnpt.so" } }, "executable": true, "type": "file" }, "lib/amd64/libprism_common.so": { "downloads": { "lzma": { "sha1": "f4aca04c90bc7505851c074a08b2c31cae1f94fa", "size": 23315, "url": "https://launcher.mojang.com/v1/objects/f4aca04c90bc7505851c074a08b2c31cae1f94fa/libprism_common.so" }, "raw": { "sha1": "b00866b6ed8646a29a334d46e297267552f27de8", "size": 59008, "url": "https://launcher.mojang.com/v1/objects/b00866b6ed8646a29a334d46e297267552f27de8/libprism_common.so" } }, "executable": true, "type": "file" }, "lib/amd64/libprism_es2.so": { "downloads": { "lzma": { "sha1": "7ff4173c338c7a6f370f231670055737e032da3e", "size": 18416, "url": "https://launcher.mojang.com/v1/objects/7ff4173c338c7a6f370f231670055737e032da3e/libprism_es2.so" }, "raw": { "sha1": "1390a1dc14345e5a948148e59195d62f3a83863f", "size": 63808, "url": "https://launcher.mojang.com/v1/objects/1390a1dc14345e5a948148e59195d62f3a83863f/libprism_es2.so" } }, "executable": true, "type": "file" }, "lib/amd64/libprism_sw.so": { "downloads": { "lzma": { "sha1": "6728e8bf7b214067d715be6fe0325910d63c2468", "size": 29457, "url": "https://launcher.mojang.com/v1/objects/6728e8bf7b214067d715be6fe0325910d63c2468/libprism_sw.so" }, "raw": { "sha1": "7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4", "size": 71608, "url": "https://launcher.mojang.com/v1/objects/7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4/libprism_sw.so" } }, "executable": true, "type": "file" }, "lib/amd64/libresource.so": { "downloads": { "lzma": { "sha1": "1e35e63f1e74915fba620f1febf420b919d49bc5", "size": 2633, "url": "https://launcher.mojang.com/v1/objects/1e35e63f1e74915fba620f1febf420b919d49bc5/libresource.so" }, "raw": { "sha1": "57490353ad0d83ab1930355213dea269795434fe", "size": 13456, "url": "https://launcher.mojang.com/v1/objects/57490353ad0d83ab1930355213dea269795434fe/libresource.so" } }, "executable": true, "type": "file" }, "lib/amd64/libsctp.so": { "downloads": { "lzma": { "sha1": "4340132ed250d7849a016e071be773eaedd33aa8", "size": 9332, "url": "https://launcher.mojang.com/v1/objects/4340132ed250d7849a016e071be773eaedd33aa8/libsctp.so" }, "raw": { "sha1": "4a80e743750f127c0d5a359f5cd60b97e7ee12ae", "size": 29552, "url": "https://launcher.mojang.com/v1/objects/4a80e743750f127c0d5a359f5cd60b97e7ee12ae/libsctp.so" } }, "executable": true, "type": "file" }, "lib/amd64/libsplashscreen.so": { "downloads": { "lzma": { "sha1": "b226c8dbd73a548fc2b042ee6db6cc80e727597c", "size": 190305, "url": "https://launcher.mojang.com/v1/objects/b226c8dbd73a548fc2b042ee6db6cc80e727597c/libsplashscreen.so" }, "raw": { "sha1": "87d6a491f5ba7e6c4d972264a0c9063afea567a2", "size": 441376, "url": "https://launcher.mojang.com/v1/objects/87d6a491f5ba7e6c4d972264a0c9063afea567a2/libsplashscreen.so" } }, "executable": true, "type": "file" }, "lib/amd64/libsunec.so": { "downloads": { "lzma": { "sha1": "6ebba98fab1e3d872d1363235b76497f6f9babdc", "size": 88829, "url": "https://launcher.mojang.com/v1/objects/6ebba98fab1e3d872d1363235b76497f6f9babdc/libsunec.so" }, "raw": { "sha1": "3b262a0a530f6e4e539aed2cd27b4de1d0ed8859", "size": 283368, "url": "https://launcher.mojang.com/v1/objects/3b262a0a530f6e4e539aed2cd27b4de1d0ed8859/libsunec.so" } }, "executable": true, "type": "file" }, "lib/amd64/libt2k.so": { "downloads": { "lzma": { "sha1": "602cb812ef0b350ccf56ce209a260ddbe3743d92", "size": 190720, "url": "https://launcher.mojang.com/v1/objects/602cb812ef0b350ccf56ce209a260ddbe3743d92/libt2k.so" }, "raw": { "sha1": "b072c56df997f61e15e6b5a43b8907b0d25c2043", "size": 504840, "url": "https://launcher.mojang.com/v1/objects/b072c56df997f61e15e6b5a43b8907b0d25c2043/libt2k.so" } }, "executable": true, "type": "file" }, "lib/amd64/libunpack.so": { "downloads": { "lzma": { "sha1": "7107b615e941074f0b14c31c88fb67200aacd37f", "size": 70308, "url": "https://launcher.mojang.com/v1/objects/7107b615e941074f0b14c31c88fb67200aacd37f/libunpack.so" }, "raw": { "sha1": "b05ff862ed87928ed91e80e5604673c5ea710a53", "size": 197712, "url": "https://launcher.mojang.com/v1/objects/b05ff862ed87928ed91e80e5604673c5ea710a53/libunpack.so" } }, "executable": true, "type": "file" }, "lib/amd64/libverify.so": { "downloads": { "lzma": { "sha1": "ecd98efb8c7da441a8c3580e8f5598f3cb4165b1", "size": 21885, "url": "https://launcher.mojang.com/v1/objects/ecd98efb8c7da441a8c3580e8f5598f3cb4165b1/libverify.so" }, "raw": { "sha1": "e2c8d92531c45ab9be69ffb72c87fa12e9e59827", "size": 66112, "url": "https://launcher.mojang.com/v1/objects/e2c8d92531c45ab9be69ffb72c87fa12e9e59827/libverify.so" } }, "executable": true, "type": "file" }, "lib/amd64/libzip.so": { "downloads": { "lzma": { "sha1": "7c562342e3f7b138dc978495447e3e6a96c2cf45", "size": 54876, "url": "https://launcher.mojang.com/v1/objects/7c562342e3f7b138dc978495447e3e6a96c2cf45/libzip.so" }, "raw": { "sha1": "5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f", "size": 127016, "url": "https://launcher.mojang.com/v1/objects/5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f/libzip.so" } }, "executable": true, "type": "file" }, "lib/amd64/server": { "type": "directory" }, "lib/amd64/server/Xusage.txt": { "downloads": { "lzma": { "sha1": "acb2da24a4c765887df83985e4c26d6be302a0a3", "size": 629, "url": "https://launcher.mojang.com/v1/objects/acb2da24a4c765887df83985e4c26d6be302a0a3/Xusage.txt" }, "raw": { "sha1": "6983727eafe140f9dd793c78aa6f3e007438243a", "size": 1423, "url": "https://launcher.mojang.com/v1/objects/6983727eafe140f9dd793c78aa6f3e007438243a/Xusage.txt" } }, "executable": false, "type": "file" }, "lib/amd64/server/libjsig.so": { "target": "../libjsig.so", "type": "link" }, "lib/amd64/server/libjvm.so": { "downloads": { "lzma": { "sha1": "d5c6f3338aaa6712f79d680ac8c3e31beebaa886", "size": 4154311, "url": "https://launcher.mojang.com/v1/objects/d5c6f3338aaa6712f79d680ac8c3e31beebaa886/libjvm.so" }, "raw": { "sha1": "23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca", "size": 17045016, "url": "https://launcher.mojang.com/v1/objects/23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca/libjvm.so" } }, "executable": true, "type": "file" }, "lib/applet": { "type": "directory" }, "lib/calendars.properties": { "downloads": { "lzma": { "sha1": "4a757c23f2942bd802a4f80235131146d9267750", "size": 558, "url": "https://launcher.mojang.com/v1/objects/4a757c23f2942bd802a4f80235131146d9267750/calendars.properties" }, "raw": { "sha1": "42ebb0988124433b8f2a6e5d9a74ed41240bcfc6", "size": 1378, "url": "https://launcher.mojang.com/v1/objects/42ebb0988124433b8f2a6e5d9a74ed41240bcfc6/calendars.properties" } }, "executable": false, "type": "file" }, "lib/charsets.jar": { "downloads": { "lzma": { "sha1": "2bf44143b2ad9d7d55045a4de4a562330c194dc0", "size": 412367, "url": "https://launcher.mojang.com/v1/objects/2bf44143b2ad9d7d55045a4de4a562330c194dc0/charsets.jar" }, "raw": { "sha1": "d73ab9f8de255a7e112ddd13622bf7f6b18c8447", "size": 3135615, "url": "https://launcher.mojang.com/v1/objects/d73ab9f8de255a7e112ddd13622bf7f6b18c8447/charsets.jar" } }, "executable": false, "type": "file" }, "lib/classlist": { "downloads": { "lzma": { "sha1": "14e7c73d21b8513b0aff8d86e5cb34c52021fbca", "size": 15024, "url": "https://launcher.mojang.com/v1/objects/14e7c73d21b8513b0aff8d86e5cb34c52021fbca/classlist" }, "raw": { "sha1": "9c0404b63c87e2fed35e3a6cd137d6cf876c42bd", "size": 84311, "url": "https://launcher.mojang.com/v1/objects/9c0404b63c87e2fed35e3a6cd137d6cf876c42bd/classlist" } }, "executable": false, "type": "file" }, "lib/cmm": { "type": "directory" }, "lib/cmm/CIEXYZ.pf": { "downloads": { "lzma": { "sha1": "fcc5ca2fd3f45cac3434b480fa3ce00007e96529", "size": 8964, "url": "https://launcher.mojang.com/v1/objects/fcc5ca2fd3f45cac3434b480fa3ce00007e96529/CIEXYZ.pf" }, "raw": { "sha1": "b7779924c70554647b87c2a86159ca7781e929f8", "size": 51236, "url": "https://launcher.mojang.com/v1/objects/b7779924c70554647b87c2a86159ca7781e929f8/CIEXYZ.pf" } }, "executable": false, "type": "file" }, "lib/cmm/GRAY.pf": { "downloads": { "lzma": { "sha1": "5388ccfe67d3131d6d02143d8e8895003ab14ff6", "size": 299, "url": "https://launcher.mojang.com/v1/objects/5388ccfe67d3131d6d02143d8e8895003ab14ff6/GRAY.pf" }, "raw": { "sha1": "27f93961d66b8230d0cdb8b166bc8b4153d5bc2d", "size": 632, "url": "https://launcher.mojang.com/v1/objects/27f93961d66b8230d0cdb8b166bc8b4153d5bc2d/GRAY.pf" } }, "executable": false, "type": "file" }, "lib/cmm/LINEAR_RGB.pf": { "downloads": { "lzma": { "sha1": "2bd90f09c8deb64b1729d6b8173c78f9e9cab27b", "size": 678, "url": "https://launcher.mojang.com/v1/objects/2bd90f09c8deb64b1729d6b8173c78f9e9cab27b/LINEAR_RGB.pf" }, "raw": { "sha1": "7913274c2f73bafcf888f09ff60990b100214ede", "size": 1044, "url": "https://launcher.mojang.com/v1/objects/7913274c2f73bafcf888f09ff60990b100214ede/LINEAR_RGB.pf" } }, "executable": false, "type": "file" }, "lib/cmm/PYCC.pf": { "downloads": { "lzma": { "sha1": "dbb2197ecff3fcdd142e9006490c8cb5c8d19af8", "size": 171521, "url": "https://launcher.mojang.com/v1/objects/dbb2197ecff3fcdd142e9006490c8cb5c8d19af8/PYCC.pf" }, "raw": { "sha1": "4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144", "size": 274474, "url": "https://launcher.mojang.com/v1/objects/4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144/PYCC.pf" } }, "executable": false, "type": "file" }, "lib/cmm/sRGB.pf": { "downloads": { "raw": { "sha1": "9eaea0911d89d63e39e95f2e2116eaec7e0bb91e", "size": 3144, "url": "https://launcher.mojang.com/v1/objects/9eaea0911d89d63e39e95f2e2116eaec7e0bb91e/sRGB.pf" } }, "executable": false, "type": "file" }, "lib/content-types.properties": { "downloads": { "lzma": { "sha1": "43a23d9a6c637c128b14cfa3feced93cbcf85b1a", "size": 1617, "url": "https://launcher.mojang.com/v1/objects/43a23d9a6c637c128b14cfa3feced93cbcf85b1a/content-types.properties" }, "raw": { "sha1": "b21698017c4a2866b5fabe59681b7592e72c83b1", "size": 5916, "url": "https://launcher.mojang.com/v1/objects/b21698017c4a2866b5fabe59681b7592e72c83b1/content-types.properties" } }, "executable": false, "type": "file" }, "lib/currency.data": { "downloads": { "lzma": { "sha1": "451b3f166ea34ef2aefbb01606ea5adcc0d65b42", "size": 1184, "url": "https://launcher.mojang.com/v1/objects/451b3f166ea34ef2aefbb01606ea5adcc0d65b42/currency.data" }, "raw": { "sha1": "bf524381a7a9b9d5bbab48069c583d2936e367a1", "size": 4134, "url": "https://launcher.mojang.com/v1/objects/bf524381a7a9b9d5bbab48069c583d2936e367a1/currency.data" } }, "executable": false, "type": "file" }, "lib/deploy": { "type": "directory" }, "lib/deploy.jar": { "downloads": { "raw": { "sha1": "fbe1de8fcd9a3d482c59414dce9311e4194766c9", "size": 2255881, "url": "https://launcher.mojang.com/v1/objects/fbe1de8fcd9a3d482c59414dce9311e4194766c9/deploy.jar" } }, "executable": false, "type": "file" }, "lib/deploy/MixedCodeMainDialog.ui": { "downloads": { "lzma": { "sha1": "7d812964343d1e978442f5c847c709784fc18fc0", "size": 683, "url": "https://launcher.mojang.com/v1/objects/7d812964343d1e978442f5c847c709784fc18fc0/MixedCodeMainDialog.ui" }, "raw": { "sha1": "c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79", "size": 4507, "url": "https://launcher.mojang.com/v1/objects/c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79/MixedCodeMainDialog.ui" } }, "executable": false, "type": "file" }, "lib/deploy/MixedCodeMainDialogJs.ui": { "downloads": { "lzma": { "sha1": "54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2", "size": 792, "url": "https://launcher.mojang.com/v1/objects/54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2/MixedCodeMainDialogJs.ui" }, "raw": { "sha1": "ad6337fb6d46750e14c12b439a5856f4b6864d0d", "size": 6110, "url": "https://launcher.mojang.com/v1/objects/ad6337fb6d46750e14c12b439a5856f4b6864d0d/MixedCodeMainDialogJs.ui" } }, "executable": false, "type": "file" }, "lib/deploy/cautionshield.icns": { "downloads": { "lzma": { "sha1": "7cea751dc168605054ec38ce8bfa71812be405c1", "size": 2333, "url": "https://launcher.mojang.com/v1/objects/7cea751dc168605054ec38ce8bfa71812be405c1/cautionshield.icns" }, "raw": { "sha1": "1de7ed5d5fc75aa1bcede088c655bee3bde64c38", "size": 3588, "url": "https://launcher.mojang.com/v1/objects/1de7ed5d5fc75aa1bcede088c655bee3bde64c38/cautionshield.icns" } }, "executable": false, "type": "file" }, "lib/deploy/ffjcext.zip": { "downloads": { "lzma": { "sha1": "80bcb9b3794f69d87dba93e90230f288e651e798", "size": 1809, "url": "https://launcher.mojang.com/v1/objects/80bcb9b3794f69d87dba93e90230f288e651e798/ffjcext.zip" }, "raw": { "sha1": "76d051ca7d3666ff25ea8eb9957a05574a45287f", "size": 13454, "url": "https://launcher.mojang.com/v1/objects/76d051ca7d3666ff25ea8eb9957a05574a45287f/ffjcext.zip" } }, "executable": false, "type": "file" }, "lib/deploy/java-icon.ico": { "downloads": { "lzma": { "sha1": "2a24f0207d7ab5976a8b0d92b4b381d49e895c9d", "size": 8468, "url": "https://launcher.mojang.com/v1/objects/2a24f0207d7ab5976a8b0d92b4b381d49e895c9d/java-icon.ico" }, "raw": { "sha1": "2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914", "size": 29926, "url": "https://launcher.mojang.com/v1/objects/2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914/java-icon.ico" } }, "executable": false, "type": "file" }, "lib/deploy/messages.properties": { "downloads": { "lzma": { "sha1": "c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4", "size": 1225, "url": "https://launcher.mojang.com/v1/objects/c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4/messages.properties" }, "raw": { "sha1": "dc52841c708e3c1eb2a044088a43396d1291bb5e", "size": 2860, "url": "https://launcher.mojang.com/v1/objects/dc52841c708e3c1eb2a044088a43396d1291bb5e/messages.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_de.properties": { "downloads": { "lzma": { "sha1": "42b42e6e1d2cb2d781f2226bde612ce519b29bc8", "size": 1394, "url": "https://launcher.mojang.com/v1/objects/42b42e6e1d2cb2d781f2226bde612ce519b29bc8/messages_de.properties" }, "raw": { "sha1": "d989fe1b8f7904888d5102294ebefd28d932ecdb", "size": 3306, "url": "https://launcher.mojang.com/v1/objects/d989fe1b8f7904888d5102294ebefd28d932ecdb/messages_de.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_es.properties": { "downloads": { "lzma": { "sha1": "c4a653e9802ca982e892b45d88c1e259c09e8c8e", "size": 1404, "url": "https://launcher.mojang.com/v1/objects/c4a653e9802ca982e892b45d88c1e259c09e8c8e/messages_es.properties" }, "raw": { "sha1": "1b0334b79db481c3a59be6915d5118d760c97baa", "size": 3600, "url": "https://launcher.mojang.com/v1/objects/1b0334b79db481c3a59be6915d5118d760c97baa/messages_es.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_fr.properties": { "downloads": { "lzma": { "sha1": "2d8dee07e3f5aab7318a22e169810b216ac44f97", "size": 1401, "url": "https://launcher.mojang.com/v1/objects/2d8dee07e3f5aab7318a22e169810b216ac44f97/messages_fr.properties" }, "raw": { "sha1": "69bd2d03c2064f8679de5b4e430ea61b567c69c5", "size": 3409, "url": "https://launcher.mojang.com/v1/objects/69bd2d03c2064f8679de5b4e430ea61b567c69c5/messages_fr.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_it.properties": { "downloads": { "lzma": { "sha1": "7c28cdd8d9e34355ba0fc03004c4f64749cae57e", "size": 1375, "url": "https://launcher.mojang.com/v1/objects/7c28cdd8d9e34355ba0fc03004c4f64749cae57e/messages_it.properties" }, "raw": { "sha1": "dbe49949308f28540a42ae6cd2ad58afbf615592", "size": 3223, "url": "https://launcher.mojang.com/v1/objects/dbe49949308f28540a42ae6cd2ad58afbf615592/messages_it.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_ja.properties": { "downloads": { "lzma": { "sha1": "9a6a4c086e48b9e615b72b6bbebb3c724d178ff4", "size": 1680, "url": "https://launcher.mojang.com/v1/objects/9a6a4c086e48b9e615b72b6bbebb3c724d178ff4/messages_ja.properties" }, "raw": { "sha1": "751170a7cdefcb1226604ac3f8196e06a04fd7ac", "size": 6349, "url": "https://launcher.mojang.com/v1/objects/751170a7cdefcb1226604ac3f8196e06a04fd7ac/messages_ja.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_ko.properties": { "downloads": { "lzma": { "sha1": "0c57c2ebfa0830f816657a384898487fc492efac", "size": 1645, "url": "https://launcher.mojang.com/v1/objects/0c57c2ebfa0830f816657a384898487fc492efac/messages_ko.properties" }, "raw": { "sha1": "bf9e055b5ab138ad6d49769e2b7630b7938848d6", "size": 5712, "url": "https://launcher.mojang.com/v1/objects/bf9e055b5ab138ad6d49769e2b7630b7938848d6/messages_ko.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_pt_BR.properties": { "downloads": { "lzma": { "sha1": "f8364dba0aa0a7e445a1a8d0e7ad66b996f70063", "size": 1388, "url": "https://launcher.mojang.com/v1/objects/f8364dba0aa0a7e445a1a8d0e7ad66b996f70063/messages_pt_BR.properties" }, "raw": { "sha1": "24e4951743521ab9a11381c77bd0cdb1ed30f5b5", "size": 3285, "url": "https://launcher.mojang.com/v1/objects/24e4951743521ab9a11381c77bd0cdb1ed30f5b5/messages_pt_BR.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_sv.properties": { "downloads": { "lzma": { "sha1": "65e5897d552258141aacf02f087c7c9c33ad0727", "size": 1355, "url": "https://launcher.mojang.com/v1/objects/65e5897d552258141aacf02f087c7c9c33ad0727/messages_sv.properties" }, "raw": { "sha1": "bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb", "size": 3384, "url": "https://launcher.mojang.com/v1/objects/bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb/messages_sv.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_zh_CN.properties": { "downloads": { "lzma": { "sha1": "de7d39a6e6748e9f47e842c9da90f515921c222c", "size": 1506, "url": "https://launcher.mojang.com/v1/objects/de7d39a6e6748e9f47e842c9da90f515921c222c/messages_zh_CN.properties" }, "raw": { "sha1": "1c2b96673dddd3596890ef4fc22017d484a1f652", "size": 4072, "url": "https://launcher.mojang.com/v1/objects/1c2b96673dddd3596890ef4fc22017d484a1f652/messages_zh_CN.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_zh_HK.properties": { "downloads": { "lzma": { "sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties" }, "raw": { "sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties" } }, "executable": false, "type": "file" }, "lib/deploy/messages_zh_TW.properties": { "downloads": { "lzma": { "sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties" }, "raw": { "sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties" } }, "executable": false, "type": "file" }, "lib/deploy/mixcode_s.png": { "downloads": { "raw": { "sha1": "4604e9f265eec97bccd0151c3a81afa9e69499e5", "size": 3115, "url": "https://launcher.mojang.com/v1/objects/4604e9f265eec97bccd0151c3a81afa9e69499e5/mixcode_s.png" } }, "executable": false, "type": "file" }, "lib/deploy/splash.gif": { "downloads": { "raw": { "sha1": "20e7aec75f6d036d504277542e507eb7dc24aae8", "size": 8590, "url": "https://launcher.mojang.com/v1/objects/20e7aec75f6d036d504277542e507eb7dc24aae8/splash.gif" } }, "executable": false, "type": "file" }, "lib/deploy/splash@2x.gif": { "downloads": { "raw": { "sha1": "0ae4a5bda2a6d628fac51462390b503c99509fdc", "size": 15276, "url": "https://launcher.mojang.com/v1/objects/0ae4a5bda2a6d628fac51462390b503c99509fdc/splash2x.gif" } }, "executable": false, "type": "file" }, "lib/deploy/splash_11-lic.gif": { "downloads": { "raw": { "sha1": "8def364e07f40142822df84b5bb4f50846cb5e4e", "size": 7805, "url": "https://launcher.mojang.com/v1/objects/8def364e07f40142822df84b5bb4f50846cb5e4e/splash_11-lic.gif" } }, "executable": false, "type": "file" }, "lib/deploy/splash_11@2x-lic.gif": { "downloads": { "raw": { "sha1": "d2bff9bbf7920ca743b81a0ee23b0719b4d057ca", "size": 12250, "url": "https://launcher.mojang.com/v1/objects/d2bff9bbf7920ca743b81a0ee23b0719b4d057ca/splash_11%402x-lic.gif" } }, "executable": false, "type": "file" }, "lib/desktop": { "type": "directory" }, "lib/desktop/applications": { "type": "directory" }, "lib/desktop/applications/sun-java.desktop": { "downloads": { "lzma": { "sha1": "109d1cdf165f38da92da70b403ca940192a7a9a8", "size": 536, "url": "https://launcher.mojang.com/v1/objects/109d1cdf165f38da92da70b403ca940192a7a9a8/sun-java.desktop" }, "raw": { "sha1": "d346dfe90505603ce5aff5a3c6c2e4a23d5bd990", "size": 777, "url": "https://launcher.mojang.com/v1/objects/d346dfe90505603ce5aff5a3c6c2e4a23d5bd990/sun-java.desktop" } }, "executable": false, "type": "file" }, "lib/desktop/applications/sun-javaws.desktop": { "downloads": { "lzma": { "sha1": "5e1815e7f83515881e6998584dc6bb02c5bef09a", "size": 451, "url": "https://launcher.mojang.com/v1/objects/5e1815e7f83515881e6998584dc6bb02c5bef09a/sun-javaws.desktop" }, "raw": { "sha1": "50ce8e519b836e0f53d58ce1a359d98b6cafdda6", "size": 619, "url": "https://launcher.mojang.com/v1/objects/50ce8e519b836e0f53d58ce1a359d98b6cafdda6/sun-javaws.desktop" } }, "executable": false, "type": "file" }, "lib/desktop/applications/sun_java.desktop": { "downloads": { "lzma": { "sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop" }, "raw": { "sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop" } }, "executable": false, "type": "file" }, "lib/desktop/icons": { "type": "directory" }, "lib/desktop/icons/HighContrast": { "type": "directory" }, "lib/desktop/icons/HighContrast/16x16": { "type": "directory" }, "lib/desktop/icons/HighContrast/16x16/apps": { "type": "directory" }, "lib/desktop/icons/HighContrast/16x16/apps/sun-java.png": { "downloads": { "raw": { "sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/mimetypes": { "type": "directory" }, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48": { "type": "directory" }, "lib/desktop/icons/HighContrast/48x48/apps": { "type": "directory" }, "lib/desktop/icons/HighContrast/48x48/apps/sun-java.png": { "downloads": { "raw": { "sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/mimetypes": { "type": "directory" }, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/16x16": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/16x16/apps": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-java.png": { "downloads": { "raw": { "sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/48x48/apps": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-java.png": { "downloads": { "raw": { "sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes": { "type": "directory" }, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast": { "type": "directory" }, "lib/desktop/icons/LowContrast/16x16": { "type": "directory" }, "lib/desktop/icons/LowContrast/16x16/apps": { "type": "directory" }, "lib/desktop/icons/LowContrast/16x16/apps/sun-java.png": { "downloads": { "raw": { "sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/mimetypes": { "type": "directory" }, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48": { "type": "directory" }, "lib/desktop/icons/LowContrast/48x48/apps": { "type": "directory" }, "lib/desktop/icons/LowContrast/48x48/apps/sun-java.png": { "downloads": { "raw": { "sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/mimetypes": { "type": "directory" }, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor": { "type": "directory" }, "lib/desktop/icons/hicolor/16x16": { "type": "directory" }, "lib/desktop/icons/hicolor/16x16/apps": { "type": "directory" }, "lib/desktop/icons/hicolor/16x16/apps/sun-java.png": { "downloads": { "raw": { "sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/mimetypes": { "type": "directory" }, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48": { "type": "directory" }, "lib/desktop/icons/hicolor/48x48/apps": { "type": "directory" }, "lib/desktop/icons/hicolor/48x48/apps/sun-java.png": { "downloads": { "raw": { "sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/apps/sun-javaws.png": { "downloads": { "raw": { "sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/apps/sun-jcontrol.png": { "downloads": { "raw": { "sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/mimetypes": { "type": "directory" }, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-archive.png": { "downloads": { "raw": { "sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": { "downloads": { "raw": { "sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-text-x-java.png": { "downloads": { "raw": { "sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png" } }, "executable": false, "type": "file" }, "lib/desktop/mime": { "type": "directory" }, "lib/desktop/mime/packages": { "type": "directory" }, "lib/desktop/mime/packages/x-java-archive.xml": { "downloads": { "lzma": { "sha1": "841230729f0a59de2a1071d155d96358232b2ba1", "size": 591, "url": "https://launcher.mojang.com/v1/objects/841230729f0a59de2a1071d155d96358232b2ba1/x-java-archive.xml" }, "raw": { "sha1": "b6297fd36efa799312961f95ebf0c85c920d5037", "size": 1822, "url": "https://launcher.mojang.com/v1/objects/b6297fd36efa799312961f95ebf0c85c920d5037/x-java-archive.xml" } }, "executable": false, "type": "file" }, "lib/desktop/mime/packages/x-java-jnlp-file.xml": { "downloads": { "lzma": { "sha1": "abf9acbe7c18027c4f036c4e42bb2cf1115525fa", "size": 302, "url": "https://launcher.mojang.com/v1/objects/abf9acbe7c18027c4f036c4e42bb2cf1115525fa/x-java-jnlp-file.xml" }, "raw": { "sha1": "72f03da83ddb76c9105f619fcfa4dbdad70e6b30", "size": 412, "url": "https://launcher.mojang.com/v1/objects/72f03da83ddb76c9105f619fcfa4dbdad70e6b30/x-java-jnlp-file.xml" } }, "executable": false, "type": "file" }, "lib/ext": { "type": "directory" }, "lib/ext/cldrdata.jar": { "downloads": { "raw": { "sha1": "6cacc961942d3f02a88907aa8f2eaae8e20c95a0", "size": 3860502, "url": "https://launcher.mojang.com/v1/objects/6cacc961942d3f02a88907aa8f2eaae8e20c95a0/cldrdata.jar" } }, "executable": false, "type": "file" }, "lib/ext/dnsns.jar": { "downloads": { "raw": { "sha1": "93bebdd7514e53ae31d60c5daba673878c8822ec", "size": 8286, "url": "https://launcher.mojang.com/v1/objects/93bebdd7514e53ae31d60c5daba673878c8822ec/dnsns.jar" } }, "executable": false, "type": "file" }, "lib/ext/jaccess.jar": { "downloads": { "raw": { "sha1": "2f54879df7c29ec67c40d40cfc95c0d4a968bea1", "size": 44516, "url": "https://launcher.mojang.com/v1/objects/2f54879df7c29ec67c40d40cfc95c0d4a968bea1/jaccess.jar" } }, "executable": false, "type": "file" }, "lib/ext/jfxrt.jar": { "downloads": { "lzma": { "sha1": "a6c5b6a782ba360ada6651f5322dcab88c75711c", "size": 3374270, "url": "https://launcher.mojang.com/v1/objects/a6c5b6a782ba360ada6651f5322dcab88c75711c/jfxrt.jar" }, "raw": { "sha1": "1ad7a876f045399c23ee4ab7dee380a04ca2ac08", "size": 18508094, "url": "https://launcher.mojang.com/v1/objects/1ad7a876f045399c23ee4ab7dee380a04ca2ac08/jfxrt.jar" } }, "executable": false, "type": "file" }, "lib/ext/localedata.jar": { "downloads": { "raw": { "sha1": "0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70", "size": 1178926, "url": "https://launcher.mojang.com/v1/objects/0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70/localedata.jar" } }, "executable": false, "type": "file" }, "lib/ext/meta-index": { "downloads": { "lzma": { "sha1": "1359457529f42bacf495afcb68149ae036442dd9", "size": 594, "url": "https://launcher.mojang.com/v1/objects/1359457529f42bacf495afcb68149ae036442dd9/meta-index" }, "raw": { "sha1": "334649c6e2d5d7248211f30855e97cfcb4558851", "size": 1269, "url": "https://launcher.mojang.com/v1/objects/334649c6e2d5d7248211f30855e97cfcb4558851/meta-index" } }, "executable": false, "type": "file" }, "lib/ext/nashorn.jar": { "downloads": { "raw": { "sha1": "dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5", "size": 2023869, "url": "https://launcher.mojang.com/v1/objects/dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5/nashorn.jar" } }, "executable": false, "type": "file" }, "lib/ext/sunec.jar": { "downloads": { "raw": { "sha1": "bf1c817820341a246f7130fe046e8310b03d04f6", "size": 41672, "url": "https://launcher.mojang.com/v1/objects/bf1c817820341a246f7130fe046e8310b03d04f6/sunec.jar" } }, "executable": false, "type": "file" }, "lib/ext/sunjce_provider.jar": { "downloads": { "raw": { "sha1": "bb3494e4b5cb3c3e60da767207731f18b267cb34", "size": 279326, "url": "https://launcher.mojang.com/v1/objects/bb3494e4b5cb3c3e60da767207731f18b267cb34/sunjce_provider.jar" } }, "executable": false, "type": "file" }, "lib/ext/sunpkcs11.jar": { "downloads": { "raw": { "sha1": "5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4", "size": 250218, "url": "https://launcher.mojang.com/v1/objects/5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4/sunpkcs11.jar" } }, "executable": false, "type": "file" }, "lib/ext/zipfs.jar": { "downloads": { "raw": { "sha1": "37b338f0e8e60d6396f51275130e8110816d7b30", "size": 68964, "url": "https://launcher.mojang.com/v1/objects/37b338f0e8e60d6396f51275130e8110816d7b30/zipfs.jar" } }, "executable": false, "type": "file" }, "lib/flavormap.properties": { "downloads": { "lzma": { "sha1": "2d5ef19ee77ccfc95c9413eea155cde59a48fadd", "size": 1541, "url": "https://launcher.mojang.com/v1/objects/2d5ef19ee77ccfc95c9413eea155cde59a48fadd/flavormap.properties" }, "raw": { "sha1": "4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2", "size": 3901, "url": "https://launcher.mojang.com/v1/objects/4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2/flavormap.properties" } }, "executable": false, "type": "file" }, "lib/fontconfig.RedHat.5.bfc": { "downloads": { "lzma": { "sha1": "5197f6e387f16458b7408134e38b06f20f625e4c", "size": 795, "url": "https://launcher.mojang.com/v1/objects/5197f6e387f16458b7408134e38b06f20f625e4c/fontconfig.RedHat.5.bfc" }, "raw": { "sha1": "fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2", "size": 4532, "url": "https://launcher.mojang.com/v1/objects/fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2/fontconfig.RedHat.5.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.RedHat.5.properties.src": { "downloads": { "lzma": { "sha1": "3897ae198e96e5a687c9c9b218ff5df60c868e0d", "size": 1089, "url": "https://launcher.mojang.com/v1/objects/3897ae198e96e5a687c9c9b218ff5df60c868e0d/fontconfig.RedHat.5.properties.src" }, "raw": { "sha1": "c67d1a06cb37b66e69560c9f5e4be7cf08af0493", "size": 8841, "url": "https://launcher.mojang.com/v1/objects/c67d1a06cb37b66e69560c9f5e4be7cf08af0493/fontconfig.RedHat.5.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.RedHat.6.bfc": { "downloads": { "lzma": { "sha1": "ef2f5d1f8d620be9927db45d3a28bd75777245cb", "size": 818, "url": "https://launcher.mojang.com/v1/objects/ef2f5d1f8d620be9927db45d3a28bd75777245cb/fontconfig.RedHat.6.bfc" }, "raw": { "sha1": "9ba3b3e2c621c31d0ef1b2053c80f77419a19965", "size": 4250, "url": "https://launcher.mojang.com/v1/objects/9ba3b3e2c621c31d0ef1b2053c80f77419a19965/fontconfig.RedHat.6.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.RedHat.6.properties.src": { "downloads": { "lzma": { "sha1": "74f4148f9d7ec3d67bbd724834d478a72cfdb0db", "size": 1111, "url": "https://launcher.mojang.com/v1/objects/74f4148f9d7ec3d67bbd724834d478a72cfdb0db/fontconfig.RedHat.6.properties.src" }, "raw": { "sha1": "768e58d4d314621c38daf9fde6d67119f370acd9", "size": 8735, "url": "https://launcher.mojang.com/v1/objects/768e58d4d314621c38daf9fde6d67119f370acd9/fontconfig.RedHat.6.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.SuSE.10.bfc": { "downloads": { "lzma": { "sha1": "d8fe9b1d8d02368dcd452de93024c6f60670eb87", "size": 1083, "url": "https://launcher.mojang.com/v1/objects/d8fe9b1d8d02368dcd452de93024c6f60670eb87/fontconfig.SuSE.10.bfc" }, "raw": { "sha1": "ffd0dfbd1553e15b11649a73a0b3f48318138e0d", "size": 6702, "url": "https://launcher.mojang.com/v1/objects/ffd0dfbd1553e15b11649a73a0b3f48318138e0d/fontconfig.SuSE.10.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.SuSE.10.properties.src": { "downloads": { "lzma": { "sha1": "2c382bd741a9e23114be3da82dee8290ebfca8a9", "size": 1555, "url": "https://launcher.mojang.com/v1/objects/2c382bd741a9e23114be3da82dee8290ebfca8a9/fontconfig.SuSE.10.properties.src" }, "raw": { "sha1": "a38dbdbbc514567b8281e1aea726865f37e97894", "size": 16772, "url": "https://launcher.mojang.com/v1/objects/a38dbdbbc514567b8281e1aea726865f37e97894/fontconfig.SuSE.10.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.SuSE.11.bfc": { "downloads": { "lzma": { "sha1": "2b78cbf11289c9858951fea7180696ba3b7176d6", "size": 1092, "url": "https://launcher.mojang.com/v1/objects/2b78cbf11289c9858951fea7180696ba3b7176d6/fontconfig.SuSE.11.bfc" }, "raw": { "sha1": "a4d8500fcb47f6327460a95851b1368660da8302", "size": 7032, "url": "https://launcher.mojang.com/v1/objects/a4d8500fcb47f6327460a95851b1368660da8302/fontconfig.SuSE.11.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.SuSE.11.properties.src": { "downloads": { "lzma": { "sha1": "5c1635803906e2c59d36492dec724dd7ae49a5ab", "size": 1589, "url": "https://launcher.mojang.com/v1/objects/5c1635803906e2c59d36492dec724dd7ae49a5ab/fontconfig.SuSE.11.properties.src" }, "raw": { "sha1": "c4b69589e41a7279a71866a9134213be19cdf88d", "size": 16781, "url": "https://launcher.mojang.com/v1/objects/c4b69589e41a7279a71866a9134213be19cdf88d/fontconfig.SuSE.11.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.Turbo.bfc": { "downloads": { "lzma": { "sha1": "1c771325d9ee4af209a3db92294451d58962c7a4", "size": 822, "url": "https://launcher.mojang.com/v1/objects/1c771325d9ee4af209a3db92294451d58962c7a4/fontconfig.Turbo.bfc" }, "raw": { "sha1": "f24368deeb85cc9d0781083bc56e773518d72219", "size": 4668, "url": "https://launcher.mojang.com/v1/objects/f24368deeb85cc9d0781083bc56e773518d72219/fontconfig.Turbo.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.Turbo.properties.src": { "downloads": { "lzma": { "sha1": "7748ffa17e2c8a34754138efa963ba39bd1cbbb3", "size": 1113, "url": "https://launcher.mojang.com/v1/objects/7748ffa17e2c8a34754138efa963ba39bd1cbbb3/fontconfig.Turbo.properties.src" }, "raw": { "sha1": "2bb7258bed7ccd4f117e4e5f892c9b13424b0c82", "size": 9192, "url": "https://launcher.mojang.com/v1/objects/2bb7258bed7ccd4f117e4e5f892c9b13424b0c82/fontconfig.Turbo.properties.src" } }, "executable": false, "type": "file" }, "lib/fontconfig.bfc": { "downloads": { "lzma": { "sha1": "be6d49ee8c64f458c4f0e64254963fec48d25150", "size": 286, "url": "https://launcher.mojang.com/v1/objects/be6d49ee8c64f458c4f0e64254963fec48d25150/fontconfig.bfc" }, "raw": { "sha1": "de39b0e19637f58d92a0188122514aa7247ebb5b", "size": 1678, "url": "https://launcher.mojang.com/v1/objects/de39b0e19637f58d92a0188122514aa7247ebb5b/fontconfig.bfc" } }, "executable": false, "type": "file" }, "lib/fontconfig.properties.src": { "downloads": { "lzma": { "sha1": "9498d5e00e5401200667687e826e28c60fa60ba4", "size": 417, "url": "https://launcher.mojang.com/v1/objects/9498d5e00e5401200667687e826e28c60fa60ba4/fontconfig.properties.src" }, "raw": { "sha1": "3617ff1424fd204415242565541facf862b16eb4", "size": 1938, "url": "https://launcher.mojang.com/v1/objects/3617ff1424fd204415242565541facf862b16eb4/fontconfig.properties.src" } }, "executable": false, "type": "file" }, "lib/fonts": { "type": "directory" }, "lib/fonts/LucidaBrightDemiBold.ttf": { "downloads": { "lzma": { "sha1": "4f748750831a7719440dff5457f4d207d0f24d21", "size": 42347, "url": "https://launcher.mojang.com/v1/objects/4f748750831a7719440dff5457f4d207d0f24d21/LucidaBrightDemiBold.ttf" }, "raw": { "sha1": "b5c97f985639e19a3b712193ee48b55dda581fd1", "size": 75144, "url": "https://launcher.mojang.com/v1/objects/b5c97f985639e19a3b712193ee48b55dda581fd1/LucidaBrightDemiBold.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaBrightDemiItalic.ttf": { "downloads": { "lzma": { "sha1": "f82e9a688553c100ecb98412b985807ed56dff5d", "size": 43119, "url": "https://launcher.mojang.com/v1/objects/f82e9a688553c100ecb98412b985807ed56dff5d/LucidaBrightDemiItalic.ttf" }, "raw": { "sha1": "1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7", "size": 75124, "url": "https://launcher.mojang.com/v1/objects/1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7/LucidaBrightDemiItalic.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaBrightItalic.ttf": { "downloads": { "lzma": { "sha1": "6d630df719271319c3d53f90a3d425118b908266", "size": 46206, "url": "https://launcher.mojang.com/v1/objects/6d630df719271319c3d53f90a3d425118b908266/LucidaBrightItalic.ttf" }, "raw": { "sha1": "aa5c037865c563726ecd63d61ca26443589be425", "size": 80856, "url": "https://launcher.mojang.com/v1/objects/aa5c037865c563726ecd63d61ca26443589be425/LucidaBrightItalic.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaBrightRegular.ttf": { "downloads": { "lzma": { "sha1": "4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe", "size": 181085, "url": "https://launcher.mojang.com/v1/objects/4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe/LucidaBrightRegular.ttf" }, "raw": { "sha1": "5d7ed564791c900a8786936930ba99385653139c", "size": 344908, "url": "https://launcher.mojang.com/v1/objects/5d7ed564791c900a8786936930ba99385653139c/LucidaBrightRegular.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaSansDemiBold.ttf": { "downloads": { "lzma": { "sha1": "079b16dc3c4918ab1f4f760b6dc5e6586c219042", "size": 173229, "url": "https://launcher.mojang.com/v1/objects/079b16dc3c4918ab1f4f760b6dc5e6586c219042/LucidaSansDemiBold.ttf" }, "raw": { "sha1": "92b79fefc35e96190250c602a8fed85276b32a95", "size": 317896, "url": "https://launcher.mojang.com/v1/objects/92b79fefc35e96190250c602a8fed85276b32a95/LucidaSansDemiBold.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaSansRegular.ttf": { "downloads": { "lzma": { "sha1": "64a65d7b94d7153d20957ef6d06bebb4dd0f48e4", "size": 326062, "url": "https://launcher.mojang.com/v1/objects/64a65d7b94d7153d20957ef6d06bebb4dd0f48e4/LucidaSansRegular.ttf" }, "raw": { "sha1": "39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add", "size": 698236, "url": "https://launcher.mojang.com/v1/objects/39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add/LucidaSansRegular.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaTypewriterBold.ttf": { "downloads": { "lzma": { "sha1": "cdb017f7c34bea0802bc5ea5583aef721ed99c49", "size": 130412, "url": "https://launcher.mojang.com/v1/objects/cdb017f7c34bea0802bc5ea5583aef721ed99c49/LucidaTypewriterBold.ttf" }, "raw": { "sha1": "a5da2eb49448f461470387c939f0e69119310e0b", "size": 234068, "url": "https://launcher.mojang.com/v1/objects/a5da2eb49448f461470387c939f0e69119310e0b/LucidaTypewriterBold.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/LucidaTypewriterRegular.ttf": { "downloads": { "lzma": { "sha1": "aeda4a09a53783b0dc97de8e20071bea874cbfe5", "size": 135184, "url": "https://launcher.mojang.com/v1/objects/aeda4a09a53783b0dc97de8e20071bea874cbfe5/LucidaTypewriterRegular.ttf" }, "raw": { "sha1": "c144dcafe4faf2e79cfd74d8134a631f30234db1", "size": 242700, "url": "https://launcher.mojang.com/v1/objects/c144dcafe4faf2e79cfd74d8134a631f30234db1/LucidaTypewriterRegular.ttf" } }, "executable": false, "type": "file" }, "lib/fonts/fonts.dir": { "downloads": { "lzma": { "sha1": "68f2dd93b215ec8b8d9409d2b9c825632c6b907d", "size": 273, "url": "https://launcher.mojang.com/v1/objects/68f2dd93b215ec8b8d9409d2b9c825632c6b907d/fonts.dir" }, "raw": { "sha1": "97f40cca185c954adf5cc582345a7cb8e4c50578", "size": 4041, "url": "https://launcher.mojang.com/v1/objects/97f40cca185c954adf5cc582345a7cb8e4c50578/fonts.dir" } }, "executable": false, "type": "file" }, "lib/hijrah-config-umalqura.properties": { "downloads": { "lzma": { "sha1": "02e8d296e3b18a450f1ed1547cbf2b7275664c9a", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/02e8d296e3b18a450f1ed1547cbf2b7275664c9a/hijrah-config-umalqura.properties" }, "raw": { "sha1": "84aa425100740722e91f4725caf849e7863d12ba", "size": 13962, "url": "https://launcher.mojang.com/v1/objects/84aa425100740722e91f4725caf849e7863d12ba/hijrah-config-umalqura.properties" } }, "executable": false, "type": "file" }, "lib/images": { "type": "directory" }, "lib/images/cursors": { "type": "directory" }, "lib/images/cursors/cursors.properties": { "downloads": { "lzma": { "sha1": "612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f", "size": 385, "url": "https://launcher.mojang.com/v1/objects/612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f/cursors.properties" }, "raw": { "sha1": "f2b9a22ddd0a77869497a64f28f07e89a7d41f48", "size": 1274, "url": "https://launcher.mojang.com/v1/objects/f2b9a22ddd0a77869497a64f28f07e89a7d41f48/cursors.properties" } }, "executable": false, "type": "file" }, "lib/images/cursors/invalid32x32.gif": { "downloads": { "raw": { "sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_CopyDrop32x32.gif": { "downloads": { "raw": { "sha1": "eb7620fae702172aa663a19d170a0b929d3b11d1", "size": 158, "url": "https://launcher.mojang.com/v1/objects/eb7620fae702172aa663a19d170a0b929d3b11d1/motif_CopyDrop32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_CopyNoDrop32x32.gif": { "downloads": { "raw": { "sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_LinkDrop32x32.gif": { "downloads": { "raw": { "sha1": "9699137f990c240e714481563181069c8f6c17bb", "size": 162, "url": "https://launcher.mojang.com/v1/objects/9699137f990c240e714481563181069c8f6c17bb/motif_LinkDrop32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_LinkNoDrop32x32.gif": { "downloads": { "raw": { "sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_MoveDrop32x32.gif": { "downloads": { "raw": { "sha1": "03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b", "size": 141, "url": "https://launcher.mojang.com/v1/objects/03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b/motif_MoveDrop32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/cursors/motif_MoveNoDrop32x32.gif": { "downloads": { "raw": { "sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif" } }, "executable": false, "type": "file" }, "lib/images/icons": { "type": "directory" }, "lib/images/icons/sun-java.png": { "downloads": { "raw": { "sha1": "d101b693aa054f51097eebdfeed8b8a6ca7b55b8", "size": 4707, "url": "https://launcher.mojang.com/v1/objects/d101b693aa054f51097eebdfeed8b8a6ca7b55b8/sun-java.png" } }, "executable": false, "type": "file" }, "lib/images/icons/sun-java_HighContrast.png": { "downloads": { "raw": { "sha1": "a6b1e418d6b5d03719b96f61f0c5236a60970151", "size": 3729, "url": "https://launcher.mojang.com/v1/objects/a6b1e418d6b5d03719b96f61f0c5236a60970151/sun-java_HighContrast.png" } }, "executable": false, "type": "file" }, "lib/images/icons/sun-java_HighContrastInverse.png": { "downloads": { "raw": { "sha1": "2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887", "size": 3777, "url": "https://launcher.mojang.com/v1/objects/2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887/sun-java_HighContrastInverse.png" } }, "executable": false, "type": "file" }, "lib/images/icons/sun-java_LowContrast.png": { "downloads": { "raw": { "sha1": "7714cc4e894c3626c8da6fe742ed22b2829122d9", "size": 4012, "url": "https://launcher.mojang.com/v1/objects/7714cc4e894c3626c8da6fe742ed22b2829122d9/sun-java_LowContrast.png" } }, "executable": false, "type": "file" }, "lib/javafx.properties": { "downloads": { "raw": { "sha1": "49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d", "size": 56, "url": "https://launcher.mojang.com/v1/objects/49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d/javafx.properties" } }, "executable": false, "type": "file" }, "lib/javaws.jar": { "downloads": { "raw": { "sha1": "04fa5ae04ead65b91be5dee575497e49ffd49fe9", "size": 488118, "url": "https://launcher.mojang.com/v1/objects/04fa5ae04ead65b91be5dee575497e49ffd49fe9/javaws.jar" } }, "executable": false, "type": "file" }, "lib/jce.jar": { "downloads": { "raw": { "sha1": "5460adee09cc5fc8829c0acfc46c34670a7d70a0", "size": 115646, "url": "https://launcher.mojang.com/v1/objects/5460adee09cc5fc8829c0acfc46c34670a7d70a0/jce.jar" } }, "executable": false, "type": "file" }, "lib/jexec": { "downloads": { "lzma": { "sha1": "2d4323d4e060f8126d026ca6c03b8972aedd2fab", "size": 3311, "url": "https://launcher.mojang.com/v1/objects/2d4323d4e060f8126d026ca6c03b8972aedd2fab/jexec" }, "raw": { "sha1": "6aa01f1d8d103974164bcfaea03c04eeeefd7d41", "size": 13376, "url": "https://launcher.mojang.com/v1/objects/6aa01f1d8d103974164bcfaea03c04eeeefd7d41/jexec" } }, "executable": true, "type": "file" }, "lib/jfr": { "type": "directory" }, "lib/jfr.jar": { "downloads": { "lzma": { "sha1": "5b9d615c91c72f4fe356d9b4105946679452d1e1", "size": 137982, "url": "https://launcher.mojang.com/v1/objects/5b9d615c91c72f4fe356d9b4105946679452d1e1/jfr.jar" }, "raw": { "sha1": "0f3fd66a336703d935bdc22ad8082bc51d34e534", "size": 560713, "url": "https://launcher.mojang.com/v1/objects/0f3fd66a336703d935bdc22ad8082bc51d34e534/jfr.jar" } }, "executable": false, "type": "file" }, "lib/jfr/default.jfc": { "downloads": { "lzma": { "sha1": "373ddd878146dd8ce8991c2c5115a05a82859bdb", "size": 2207, "url": "https://launcher.mojang.com/v1/objects/373ddd878146dd8ce8991c2c5115a05a82859bdb/default.jfc" }, "raw": { "sha1": "1a64b68d0e7d43f8149faba94440be54f4f24527", "size": 20109, "url": "https://launcher.mojang.com/v1/objects/1a64b68d0e7d43f8149faba94440be54f4f24527/default.jfc" } }, "executable": false, "type": "file" }, "lib/jfr/profile.jfc": { "downloads": { "lzma": { "sha1": "3dcdc5feee3ccfb66bc8726b666944cd4bdadae3", "size": 2199, "url": "https://launcher.mojang.com/v1/objects/3dcdc5feee3ccfb66bc8726b666944cd4bdadae3/profile.jfc" }, "raw": { "sha1": "5d7d08a595f76322c51ae43ea966fbba6b69eebe", "size": 20065, "url": "https://launcher.mojang.com/v1/objects/5d7d08a595f76322c51ae43ea966fbba6b69eebe/profile.jfc" } }, "executable": false, "type": "file" }, "lib/jfxswt.jar": { "downloads": { "raw": { "sha1": "99d9a264c898d84c01e1c42565e7fe1a89dcd72d", "size": 33932, "url": "https://launcher.mojang.com/v1/objects/99d9a264c898d84c01e1c42565e7fe1a89dcd72d/jfxswt.jar" } }, "executable": false, "type": "file" }, "lib/jsse.jar": { "downloads": { "lzma": { "sha1": "94a17dfbc2e76cd12c33970a15341424f875a9ce", "size": 187549, "url": "https://launcher.mojang.com/v1/objects/94a17dfbc2e76cd12c33970a15341424f875a9ce/jsse.jar" }, "raw": { "sha1": "92c5c626e8a2d16f41272c0e404d4f992dd8310a", "size": 675599, "url": "https://launcher.mojang.com/v1/objects/92c5c626e8a2d16f41272c0e404d4f992dd8310a/jsse.jar" } }, "executable": false, "type": "file" }, "lib/jvm.hprof.txt": { "downloads": { "lzma": { "sha1": "eccdb240a815b2a83a502749339b27bb8669965b", "size": 1863, "url": "https://launcher.mojang.com/v1/objects/eccdb240a815b2a83a502749339b27bb8669965b/jvm.hprof.txt" }, "raw": { "sha1": "fbd61d52534cdd0c15df332114d469c65d001e33", "size": 4226, "url": "https://launcher.mojang.com/v1/objects/fbd61d52534cdd0c15df332114d469c65d001e33/jvm.hprof.txt" } }, "executable": false, "type": "file" }, "lib/locale": { "type": "directory" }, "lib/locale/de": { "type": "directory" }, "lib/locale/de/LC_MESSAGES": { "type": "directory" }, "lib/locale/de/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "3061d922907cc557208109088fc6ab81d577ff6f", "size": 970, "url": "https://launcher.mojang.com/v1/objects/3061d922907cc557208109088fc6ab81d577ff6f/sunw_java_plugin.mo" }, "raw": { "sha1": "5b223a3d723ac1cfce63623fb109f2868d47d1b7", "size": 2483, "url": "https://launcher.mojang.com/v1/objects/5b223a3d723ac1cfce63623fb109f2868d47d1b7/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/es": { "type": "directory" }, "lib/locale/es/LC_MESSAGES": { "type": "directory" }, "lib/locale/es/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "24338049a89b323e17182b3a3006b50565d4fa0f", "size": 979, "url": "https://launcher.mojang.com/v1/objects/24338049a89b323e17182b3a3006b50565d4fa0f/sunw_java_plugin.mo" }, "raw": { "sha1": "6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1", "size": 2477, "url": "https://launcher.mojang.com/v1/objects/6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/fr": { "type": "directory" }, "lib/locale/fr/LC_MESSAGES": { "type": "directory" }, "lib/locale/fr/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "22796a48ef39f57d2d6fa70f41308e493d7f05c1", "size": 1033, "url": "https://launcher.mojang.com/v1/objects/22796a48ef39f57d2d6fa70f41308e493d7f05c1/sunw_java_plugin.mo" }, "raw": { "sha1": "d9d5b458db6e83fdf85c3526aeee3f57c4929840", "size": 2746, "url": "https://launcher.mojang.com/v1/objects/d9d5b458db6e83fdf85c3526aeee3f57c4929840/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/it": { "type": "directory" }, "lib/locale/it/LC_MESSAGES": { "type": "directory" }, "lib/locale/it/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "59a4cae38bfb8927745674d0efc2f284bc277987", "size": 958, "url": "https://launcher.mojang.com/v1/objects/59a4cae38bfb8927745674d0efc2f284bc277987/sunw_java_plugin.mo" }, "raw": { "sha1": "f6e72e3b2141ccc3dffab10ae14a754e494577ba", "size": 2434, "url": "https://launcher.mojang.com/v1/objects/f6e72e3b2141ccc3dffab10ae14a754e494577ba/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/ja": { "type": "directory" }, "lib/locale/ja/LC_MESSAGES": { "type": "directory" }, "lib/locale/ja/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "7d6aeed563e1cefcf0224cf522048468088884a9", "size": 1036, "url": "https://launcher.mojang.com/v1/objects/7d6aeed563e1cefcf0224cf522048468088884a9/sunw_java_plugin.mo" }, "raw": { "sha1": "378881a8cb8dd2aebb43eacd0c68519be4f258b1", "size": 2415, "url": "https://launcher.mojang.com/v1/objects/378881a8cb8dd2aebb43eacd0c68519be4f258b1/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/ko": { "type": "directory" }, "lib/locale/ko.UTF-8": { "type": "directory" }, "lib/locale/ko.UTF-8/LC_MESSAGES": { "type": "directory" }, "lib/locale/ko.UTF-8/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "12ee3b21511e8497d95ea0ba9d6fe519227d0b16", "size": 1069, "url": "https://launcher.mojang.com/v1/objects/12ee3b21511e8497d95ea0ba9d6fe519227d0b16/sunw_java_plugin.mo" }, "raw": { "sha1": "cb19df01c59662dbe2f4050b1290d374b82fe1fa", "size": 2753, "url": "https://launcher.mojang.com/v1/objects/cb19df01c59662dbe2f4050b1290d374b82fe1fa/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/ko/LC_MESSAGES": { "type": "directory" }, "lib/locale/ko/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "6e2e47c64c360517fd436bc79c823b5679a1efe6", "size": 996, "url": "https://launcher.mojang.com/v1/objects/6e2e47c64c360517fd436bc79c823b5679a1efe6/sunw_java_plugin.mo" }, "raw": { "sha1": "12c8a118d150c78f719314df6dec49a967af71e9", "size": 2399, "url": "https://launcher.mojang.com/v1/objects/12c8a118d150c78f719314df6dec49a967af71e9/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/pt_BR": { "type": "directory" }, "lib/locale/pt_BR/LC_MESSAGES": { "type": "directory" }, "lib/locale/pt_BR/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "bcaa7e7916493f071f1bf64bf58c6b038e3569c9", "size": 940, "url": "https://launcher.mojang.com/v1/objects/bcaa7e7916493f071f1bf64bf58c6b038e3569c9/sunw_java_plugin.mo" }, "raw": { "sha1": "a3bc0c43994c53c59bba94982cf95f6d36283dd0", "size": 2420, "url": "https://launcher.mojang.com/v1/objects/a3bc0c43994c53c59bba94982cf95f6d36283dd0/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/sv": { "type": "directory" }, "lib/locale/sv/LC_MESSAGES": { "type": "directory" }, "lib/locale/sv/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "76017835d6261fe2eedbcbe5eb08a7484c3080c5", "size": 946, "url": "https://launcher.mojang.com/v1/objects/76017835d6261fe2eedbcbe5eb08a7484c3080c5/sunw_java_plugin.mo" }, "raw": { "sha1": "09a47686edec4bbb34e82fbd08559f8bb6266544", "size": 2359, "url": "https://launcher.mojang.com/v1/objects/09a47686edec4bbb34e82fbd08559f8bb6266544/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh": { "type": "directory" }, "lib/locale/zh.GBK": { "type": "directory" }, "lib/locale/zh.GBK/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh.GBK/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo" }, "raw": { "sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo" }, "raw": { "sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh_HK.BIG5HK": { "type": "directory" }, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo" }, "raw": { "sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh_TW": { "type": "directory" }, "lib/locale/zh_TW.BIG5": { "type": "directory" }, "lib/locale/zh_TW.BIG5/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh_TW.BIG5/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo" }, "raw": { "sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/locale/zh_TW/LC_MESSAGES": { "type": "directory" }, "lib/locale/zh_TW/LC_MESSAGES/sunw_java_plugin.mo": { "downloads": { "lzma": { "sha1": "c05e610e75182f0c4e77f3e7a4d9670ed62bf63c", "size": 897, "url": "https://launcher.mojang.com/v1/objects/c05e610e75182f0c4e77f3e7a4d9670ed62bf63c/sunw_java_plugin.mo" }, "raw": { "sha1": "f9b972dd059eae3cd337dfcef6a178e8ed8a7db6", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/f9b972dd059eae3cd337dfcef6a178e8ed8a7db6/sunw_java_plugin.mo" } }, "executable": false, "type": "file" }, "lib/logging.properties": { "downloads": { "lzma": { "sha1": "642202a58e5216d086ad37c0b5a633be802edc78", "size": 896, "url": "https://launcher.mojang.com/v1/objects/642202a58e5216d086ad37c0b5a633be802edc78/logging.properties" }, "raw": { "sha1": "89da8094484891f9ec1fa40c6c8b61f94c5869d0", "size": 2455, "url": "https://launcher.mojang.com/v1/objects/89da8094484891f9ec1fa40c6c8b61f94c5869d0/logging.properties" } }, "executable": false, "type": "file" }, "lib/management": { "type": "directory" }, "lib/management-agent.jar": { "downloads": { "lzma": { "sha1": "3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd", "size": 243, "url": "https://launcher.mojang.com/v1/objects/3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd/management-agent.jar" }, "raw": { "sha1": "9fbed36522aa3a80bac08a328942cbc5ef39ca8e", "size": 381, "url": "https://launcher.mojang.com/v1/objects/9fbed36522aa3a80bac08a328942cbc5ef39ca8e/management-agent.jar" } }, "executable": false, "type": "file" }, "lib/management/jmxremote.access": { "downloads": { "lzma": { "sha1": "69042ff1b14165db19c9d728614639dec16d6a31", "size": 1419, "url": "https://launcher.mojang.com/v1/objects/69042ff1b14165db19c9d728614639dec16d6a31/jmxremote.access" }, "raw": { "sha1": "21200eaad898ba4a2a8834a032efb6616fabb930", "size": 3998, "url": "https://launcher.mojang.com/v1/objects/21200eaad898ba4a2a8834a032efb6616fabb930/jmxremote.access" } }, "executable": false, "type": "file" }, "lib/management/jmxremote.password.template": { "downloads": { "lzma": { "sha1": "556c64b1e920766f8867be3964de6e49f5b81a60", "size": 1129, "url": "https://launcher.mojang.com/v1/objects/556c64b1e920766f8867be3964de6e49f5b81a60/jmxremote.password.template" }, "raw": { "sha1": "c1e0f01408bf20fbbb8b4810520c725f70050db5", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/c1e0f01408bf20fbbb8b4810520c725f70050db5/jmxremote.password.template" } }, "executable": false, "type": "file" }, "lib/management/management.properties": { "downloads": { "lzma": { "sha1": "3e52f9baa6394ca6956845424c607e5cde5d3c67", "size": 3176, "url": "https://launcher.mojang.com/v1/objects/3e52f9baa6394ca6956845424c607e5cde5d3c67/management.properties" }, "raw": { "sha1": "e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1", "size": 14630, "url": "https://launcher.mojang.com/v1/objects/e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1/management.properties" } }, "executable": false, "type": "file" }, "lib/management/snmp.acl.template": { "downloads": { "lzma": { "sha1": "9a4aa6396c3b488b0663bed5e5ecb762985669c9", "size": 1121, "url": "https://launcher.mojang.com/v1/objects/9a4aa6396c3b488b0663bed5e5ecb762985669c9/snmp.acl.template" }, "raw": { "sha1": "2e9f9ac287274532eb1f0d1afcefd7f3e97cc794", "size": 3376, "url": "https://launcher.mojang.com/v1/objects/2e9f9ac287274532eb1f0d1afcefd7f3e97cc794/snmp.acl.template" } }, "executable": false, "type": "file" }, "lib/meta-index": { "downloads": { "lzma": { "sha1": "1ac60b31362fda4725c665b591c5fbe384cbc8c1", "size": 788, "url": "https://launcher.mojang.com/v1/objects/1ac60b31362fda4725c665b591c5fbe384cbc8c1/meta-index" }, "raw": { "sha1": "bf204f09242203e713c31785158a0792f9edb600", "size": 2034, "url": "https://launcher.mojang.com/v1/objects/bf204f09242203e713c31785158a0792f9edb600/meta-index" } }, "executable": false, "type": "file" }, "lib/net.properties": { "downloads": { "lzma": { "sha1": "e9ec3981a0797bf55bb87b24d9eb651ce7e6916b", "size": 1830, "url": "https://launcher.mojang.com/v1/objects/e9ec3981a0797bf55bb87b24d9eb651ce7e6916b/net.properties" }, "raw": { "sha1": "fd9471742eb759f4478bb1de9a0dc0527265b6ea", "size": 5352, "url": "https://launcher.mojang.com/v1/objects/fd9471742eb759f4478bb1de9a0dc0527265b6ea/net.properties" } }, "executable": false, "type": "file" }, "lib/oblique-fonts": { "type": "directory" }, "lib/oblique-fonts/LucidaSansDemiOblique.ttf": { "downloads": { "lzma": { "sha1": "49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2", "size": 38580, "url": "https://launcher.mojang.com/v1/objects/49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2/LucidaSansDemiOblique.ttf" }, "raw": { "sha1": "53e4e12a675ac222469341c3dbc102464a1be4c7", "size": 91352, "url": "https://launcher.mojang.com/v1/objects/53e4e12a675ac222469341c3dbc102464a1be4c7/LucidaSansDemiOblique.ttf" } }, "executable": false, "type": "file" }, "lib/oblique-fonts/LucidaSansOblique.ttf": { "downloads": { "lzma": { "sha1": "553123c0edcd08035dede4ffd92b5b81c9a7538a", "size": 116575, "url": "https://launcher.mojang.com/v1/objects/553123c0edcd08035dede4ffd92b5b81c9a7538a/LucidaSansOblique.ttf" }, "raw": { "sha1": "95a195ad4fc520b3e395c85b747fc3024d118dd9", "size": 253724, "url": "https://launcher.mojang.com/v1/objects/95a195ad4fc520b3e395c85b747fc3024d118dd9/LucidaSansOblique.ttf" } }, "executable": false, "type": "file" }, "lib/oblique-fonts/LucidaTypewriterBoldOblique.ttf": { "downloads": { "lzma": { "sha1": "2475b08151556ad4d89bb1d2b6494c6bee9abd82", "size": 29954, "url": "https://launcher.mojang.com/v1/objects/2475b08151556ad4d89bb1d2b6494c6bee9abd82/LucidaTypewriterBoldOblique.ttf" }, "raw": { "sha1": "f331fc8b0cc494702bc46b690f2b8eed36469a02", "size": 63168, "url": "https://launcher.mojang.com/v1/objects/f331fc8b0cc494702bc46b690f2b8eed36469a02/LucidaTypewriterBoldOblique.ttf" } }, "executable": false, "type": "file" }, "lib/oblique-fonts/LucidaTypewriterOblique.ttf": { "downloads": { "lzma": { "sha1": "5b970bc3b7abb21dce1aa28ff7f03459d351e552", "size": 60133, "url": "https://launcher.mojang.com/v1/objects/5b970bc3b7abb21dce1aa28ff7f03459d351e552/LucidaTypewriterOblique.ttf" }, "raw": { "sha1": "f8ea00db73f8a89a27674d050edc37c2280930e1", "size": 137484, "url": "https://launcher.mojang.com/v1/objects/f8ea00db73f8a89a27674d050edc37c2280930e1/LucidaTypewriterOblique.ttf" } }, "executable": false, "type": "file" }, "lib/oblique-fonts/fonts.dir": { "downloads": { "lzma": { "sha1": "067528c789bd713c7c3f34e779aa6e2e8253dcf6", "size": 188, "url": "https://launcher.mojang.com/v1/objects/067528c789bd713c7c3f34e779aa6e2e8253dcf6/fonts.dir" }, "raw": { "sha1": "5aee54ffba9e33de56fd84ef64fa496b898585bb", "size": 2115, "url": "https://launcher.mojang.com/v1/objects/5aee54ffba9e33de56fd84ef64fa496b898585bb/fonts.dir" } }, "executable": false, "type": "file" }, "lib/plugin.jar": { "downloads": { "raw": { "sha1": "3f250842c79112bae5369e372025b166990820e8", "size": 950772, "url": "https://launcher.mojang.com/v1/objects/3f250842c79112bae5369e372025b166990820e8/plugin.jar" } }, "executable": false, "type": "file" }, "lib/psfont.properties.ja": { "downloads": { "lzma": { "sha1": "7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4", "size": 701, "url": "https://launcher.mojang.com/v1/objects/7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4/psfont.properties.ja" }, "raw": { "sha1": "56ed1c661eeede17b4fae8c9de7b5edbad387abc", "size": 2796, "url": "https://launcher.mojang.com/v1/objects/56ed1c661eeede17b4fae8c9de7b5edbad387abc/psfont.properties.ja" } }, "executable": false, "type": "file" }, "lib/psfontj2d.properties": { "downloads": { "lzma": { "sha1": "4252fa01af8739a3545e2b705e3383892e22ab40", "size": 2278, "url": "https://launcher.mojang.com/v1/objects/4252fa01af8739a3545e2b705e3383892e22ab40/psfontj2d.properties" }, "raw": { "sha1": "aa327a22a49967f4d74afeee6726f505f209692f", "size": 10393, "url": "https://launcher.mojang.com/v1/objects/aa327a22a49967f4d74afeee6726f505f209692f/psfontj2d.properties" } }, "executable": false, "type": "file" }, "lib/resources.jar": { "downloads": { "lzma": { "sha1": "1b0e08441750dc17efe4b527aa146da6cc14e8a6", "size": 579294, "url": "https://launcher.mojang.com/v1/objects/1b0e08441750dc17efe4b527aa146da6cc14e8a6/resources.jar" }, "raw": { "sha1": "daa021906e4648d4c37e798c11733dc2047f2da1", "size": 3505206, "url": "https://launcher.mojang.com/v1/objects/daa021906e4648d4c37e798c11733dc2047f2da1/resources.jar" } }, "executable": false, "type": "file" }, "lib/rt.jar": { "downloads": { "lzma": { "sha1": "fc4a8681aeda29c2a2a3fd11bad7729543283f3d", "size": 14378994, "url": "https://launcher.mojang.com/v1/objects/fc4a8681aeda29c2a2a3fd11bad7729543283f3d/rt.jar" }, "raw": { "sha1": "5396b0954a20f3210f1f4f1886ead30880d6ebfe", "size": 66334986, "url": "https://launcher.mojang.com/v1/objects/5396b0954a20f3210f1f4f1886ead30880d6ebfe/rt.jar" } }, "executable": false, "type": "file" }, "lib/security": { "type": "directory" }, "lib/security/blacklist": { "downloads": { "lzma": { "sha1": "8206fce6c1d91a39fdf78e8e79e953913994a1cd", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/8206fce6c1d91a39fdf78e8e79e953913994a1cd/blacklist" }, "raw": { "sha1": "d4ffb3857eab403955ce9d156e46d056061e6a5a", "size": 4054, "url": "https://launcher.mojang.com/v1/objects/d4ffb3857eab403955ce9d156e46d056061e6a5a/blacklist" } }, "executable": false, "type": "file" }, "lib/security/blacklisted.certs": { "downloads": { "lzma": { "sha1": "8311bead054caf6cfe678d4b7998de4caaabfa53", "size": 806, "url": "https://launcher.mojang.com/v1/objects/8311bead054caf6cfe678d4b7998de4caaabfa53/blacklisted.certs" }, "raw": { "sha1": "c5c005c29a80493f5c31cd7eb629ac1b9c752404", "size": 1273, "url": "https://launcher.mojang.com/v1/objects/c5c005c29a80493f5c31cd7eb629ac1b9c752404/blacklisted.certs" } }, "executable": false, "type": "file" }, "lib/security/cacerts": { "downloads": { "lzma": { "sha1": "654dd94809655d5b28385cbb5eba8d6ad9f2c1aa", "size": 67802, "url": "https://launcher.mojang.com/v1/objects/654dd94809655d5b28385cbb5eba8d6ad9f2c1aa/cacerts" }, "raw": { "sha1": "2917859c443c68e19f93abcd1315c3c2904cbef9", "size": 104430, "url": "https://launcher.mojang.com/v1/objects/2917859c443c68e19f93abcd1315c3c2904cbef9/cacerts" } }, "executable": false, "type": "file" }, "lib/security/java.policy": { "downloads": { "lzma": { "sha1": "b601c420d02ef3dbd8595453d08fdef91134e8b5", "size": 647, "url": "https://launcher.mojang.com/v1/objects/b601c420d02ef3dbd8595453d08fdef91134e8b5/java.policy" }, "raw": { "sha1": "c0112209a567b3b523cfed7041709f9440227968", "size": 2466, "url": "https://launcher.mojang.com/v1/objects/c0112209a567b3b523cfed7041709f9440227968/java.policy" } }, "executable": false, "type": "file" }, "lib/security/java.security": { "downloads": { "lzma": { "sha1": "531620e82ca0365ce8dc97096bb0ac5a7ace5952", "size": 10959, "url": "https://launcher.mojang.com/v1/objects/531620e82ca0365ce8dc97096bb0ac5a7ace5952/java.security" }, "raw": { "sha1": "5dcc17a168c53d0b366784e520bd4d55aa61ac18", "size": 41528, "url": "https://launcher.mojang.com/v1/objects/5dcc17a168c53d0b366784e520bd4d55aa61ac18/java.security" } }, "executable": false, "type": "file" }, "lib/security/javaws.policy": { "downloads": { "raw": { "sha1": "4384ca5e4d32f7dd86d8baddd1e690730d74e694", "size": 98, "url": "https://launcher.mojang.com/v1/objects/4384ca5e4d32f7dd86d8baddd1e690730d74e694/javaws.policy" } }, "executable": false, "type": "file" }, "lib/security/policy": { "type": "directory" }, "lib/security/policy/limited": { "type": "directory" }, "lib/security/policy/limited/US_export_policy.jar": { "downloads": { "raw": { "sha1": "7d69ea3b385bc067738520f1b5c549e1084be285", "size": 3026, "url": "https://launcher.mojang.com/v1/objects/7d69ea3b385bc067738520f1b5c549e1084be285/US_export_policy.jar" } }, "executable": false, "type": "file" }, "lib/security/policy/limited/local_policy.jar": { "downloads": { "raw": { "sha1": "238b8826e110f58acb2e1959773b0a577cd4d569", "size": 3527, "url": "https://launcher.mojang.com/v1/objects/238b8826e110f58acb2e1959773b0a577cd4d569/local_policy.jar" } }, "executable": false, "type": "file" }, "lib/security/policy/unlimited": { "type": "directory" }, "lib/security/policy/unlimited/US_export_policy.jar": { "downloads": { "raw": { "sha1": "f6fb2af1e87fc622cda194a7d6b5f5f069653ff1", "size": 3023, "url": "https://launcher.mojang.com/v1/objects/f6fb2af1e87fc622cda194a7d6b5f5f069653ff1/US_export_policy.jar" } }, "executable": false, "type": "file" }, "lib/security/policy/unlimited/local_policy.jar": { "downloads": { "raw": { "sha1": "517368ab2cbaf6b42ea0b963f98eeedd996e83e3", "size": 3035, "url": "https://launcher.mojang.com/v1/objects/517368ab2cbaf6b42ea0b963f98eeedd996e83e3/local_policy.jar" } }, "executable": false, "type": "file" }, "lib/security/trusted.libraries": { "downloads": { "raw": { "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "size": 0, "url": "https://launcher.mojang.com/v1/objects/da39a3ee5e6b4b0d3255bfef95601890afd80709/trusted.libraries" } }, "executable": false, "type": "file" }, "lib/sound.properties": { "downloads": { "lzma": { "sha1": "3b5f7e4ec437d79048af35094290577f483b3fe1", "size": 473, "url": "https://launcher.mojang.com/v1/objects/3b5f7e4ec437d79048af35094290577f483b3fe1/sound.properties" }, "raw": { "sha1": "9afceb218059d981d0fa9f07aad3c5097cf41b0c", "size": 1210, "url": "https://launcher.mojang.com/v1/objects/9afceb218059d981d0fa9f07aad3c5097cf41b0c/sound.properties" } }, "executable": false, "type": "file" }, "lib/tzdb.dat": { "downloads": { "lzma": { "sha1": "39c69339965484afe89c14111baeeb862fdefd97", "size": 32547, "url": "https://launcher.mojang.com/v1/objects/39c69339965484afe89c14111baeeb862fdefd97/tzdb.dat" }, "raw": { "sha1": "b59c07e3619271a3b9861e999f4b138e971baf69", "size": 105734, "url": "https://launcher.mojang.com/v1/objects/b59c07e3619271a3b9861e999f4b138e971baf69/tzdb.dat" } }, "executable": false, "type": "file" }, "man": { "type": "directory" }, "man/ja": { "target": "ja_JP.UTF-8", "type": "link" }, "man/ja_JP.UTF-8": { "type": "directory" }, "man/ja_JP.UTF-8/man1": { "type": "directory" }, "man/ja_JP.UTF-8/man1/java.1": { "downloads": { "lzma": { "sha1": "f9da09710b6c6df23c256e324a0c4df00a0d6ded", "size": 25461, "url": "https://launcher.mojang.com/v1/objects/f9da09710b6c6df23c256e324a0c4df00a0d6ded/java.1" }, "raw": { "sha1": "b0b12a0bb66e6171771ca4b1dfca32fb759bcaec", "size": 148688, "url": "https://launcher.mojang.com/v1/objects/b0b12a0bb66e6171771ca4b1dfca32fb759bcaec/java.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/javaws.1": { "downloads": { "lzma": { "sha1": "6188fae453ca09ccb19be5c9f4d2059926b36267", "size": 2154, "url": "https://launcher.mojang.com/v1/objects/6188fae453ca09ccb19be5c9f4d2059926b36267/javaws.1" }, "raw": { "sha1": "8f39d928870268ace07bedfebd18db1e1d07fc37", "size": 6641, "url": "https://launcher.mojang.com/v1/objects/8f39d928870268ace07bedfebd18db1e1d07fc37/javaws.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/jjs.1": { "downloads": { "lzma": { "sha1": "6e42b989d28b185dc1aab50c0389834e649a37d4", "size": 3452, "url": "https://launcher.mojang.com/v1/objects/6e42b989d28b185dc1aab50c0389834e649a37d4/jjs.1" }, "raw": { "sha1": "e023322a2013912315a2bd1034e6f829a27c76e0", "size": 11365, "url": "https://launcher.mojang.com/v1/objects/e023322a2013912315a2bd1034e6f829a27c76e0/jjs.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/keytool.1": { "downloads": { "lzma": { "sha1": "a78134a4bddd53d684a70aa677e51a215db1c9cb", "size": 20698, "url": "https://launcher.mojang.com/v1/objects/a78134a4bddd53d684a70aa677e51a215db1c9cb/keytool.1" }, "raw": { "sha1": "148583c837eaaf6333ccfd8c9e8df08574e14b0c", "size": 111033, "url": "https://launcher.mojang.com/v1/objects/148583c837eaaf6333ccfd8c9e8df08574e14b0c/keytool.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/orbd.1": { "downloads": { "lzma": { "sha1": "326af0dcbff173ef8aee29163dbe146d7389cc3e", "size": 4225, "url": "https://launcher.mojang.com/v1/objects/326af0dcbff173ef8aee29163dbe146d7389cc3e/orbd.1" }, "raw": { "sha1": "95651622d33c08286858ec337edd3ea72acd93dc", "size": 16092, "url": "https://launcher.mojang.com/v1/objects/95651622d33c08286858ec337edd3ea72acd93dc/orbd.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/pack200.1": { "downloads": { "lzma": { "sha1": "e0eedafa748c61a44e5be4355fe9d44b05048e80", "size": 4293, "url": "https://launcher.mojang.com/v1/objects/e0eedafa748c61a44e5be4355fe9d44b05048e80/pack200.1" }, "raw": { "sha1": "aa21a0ab75707f7fc66e83c7a392e69b37ddf80e", "size": 14482, "url": "https://launcher.mojang.com/v1/objects/aa21a0ab75707f7fc66e83c7a392e69b37ddf80e/pack200.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/policytool.1": { "downloads": { "lzma": { "sha1": "3c766ed12dab58166169d35680c392a6be1814a1", "size": 1380, "url": "https://launcher.mojang.com/v1/objects/3c766ed12dab58166169d35680c392a6be1814a1/policytool.1" }, "raw": { "sha1": "80879c74e072a98fad6f32b3283331aaf9bd002f", "size": 4020, "url": "https://launcher.mojang.com/v1/objects/80879c74e072a98fad6f32b3283331aaf9bd002f/policytool.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/rmid.1": { "downloads": { "lzma": { "sha1": "1e20779d990beacc32a48237777d670fcc47ca14", "size": 4836, "url": "https://launcher.mojang.com/v1/objects/1e20779d990beacc32a48237777d670fcc47ca14/rmid.1" }, "raw": { "sha1": "7e40cb8003d098d6e36f45640b26f979ac94b5c5", "size": 19715, "url": "https://launcher.mojang.com/v1/objects/7e40cb8003d098d6e36f45640b26f979ac94b5c5/rmid.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/rmiregistry.1": { "downloads": { "lzma": { "sha1": "aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9", "size": 1627, "url": "https://launcher.mojang.com/v1/objects/aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9/rmiregistry.1" }, "raw": { "sha1": "c53c52f3ae7a011c135894c9fc51b741e729c33d", "size": 4557, "url": "https://launcher.mojang.com/v1/objects/c53c52f3ae7a011c135894c9fc51b741e729c33d/rmiregistry.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/servertool.1": { "downloads": { "lzma": { "sha1": "3b9e624e9d1cf2959b438a35061162e2100ddecd", "size": 2626, "url": "https://launcher.mojang.com/v1/objects/3b9e624e9d1cf2959b438a35061162e2100ddecd/servertool.1" }, "raw": { "sha1": "50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e", "size": 9081, "url": "https://launcher.mojang.com/v1/objects/50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e/servertool.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/tnameserv.1": { "downloads": { "lzma": { "sha1": "bb3106ff74c60a76de3d20659b9c2128c70f3bf2", "size": 4478, "url": "https://launcher.mojang.com/v1/objects/bb3106ff74c60a76de3d20659b9c2128c70f3bf2/tnameserv.1" }, "raw": { "sha1": "01e714671ecd1167edcb5310b16a9c59c33c3eaa", "size": 17722, "url": "https://launcher.mojang.com/v1/objects/01e714671ecd1167edcb5310b16a9c59c33c3eaa/tnameserv.1" } }, "executable": false, "type": "file" }, "man/ja_JP.UTF-8/man1/unpack200.1": { "downloads": { "lzma": { "sha1": "c115a881cf800b08df294df55d9f250ae944e33c", "size": 1973, "url": "https://launcher.mojang.com/v1/objects/c115a881cf800b08df294df55d9f250ae944e33c/unpack200.1" }, "raw": { "sha1": "7c882bba0067367a41ad84868d18793b8a7397a3", "size": 5382, "url": "https://launcher.mojang.com/v1/objects/7c882bba0067367a41ad84868d18793b8a7397a3/unpack200.1" } }, "executable": false, "type": "file" }, "man/man1": { "type": "directory" }, "man/man1/java.1": { "downloads": { "lzma": { "sha1": "06a6b0275c202bf698d73ca71f95618d56d81c15", "size": 25796, "url": "https://launcher.mojang.com/v1/objects/06a6b0275c202bf698d73ca71f95618d56d81c15/java.1" }, "raw": { "sha1": "69fec7a341aa91f18dbdcdb95952dede7e1b689a", "size": 124796, "url": "https://launcher.mojang.com/v1/objects/69fec7a341aa91f18dbdcdb95952dede7e1b689a/java.1" } }, "executable": false, "type": "file" }, "man/man1/javaws.1": { "downloads": { "lzma": { "sha1": "4bae251c6dfb5420f56928815cf80d0b6d517a1f", "size": 1759, "url": "https://launcher.mojang.com/v1/objects/4bae251c6dfb5420f56928815cf80d0b6d517a1f/javaws.1" }, "raw": { "sha1": "e61e44e101b1bc119c2d2d4b10320f38b36a8036", "size": 4897, "url": "https://launcher.mojang.com/v1/objects/e61e44e101b1bc119c2d2d4b10320f38b36a8036/javaws.1" } }, "executable": false, "type": "file" }, "man/man1/jjs.1": { "downloads": { "lzma": { "sha1": "29683cf2bd47015c9461b688749ddffd95f6671d", "size": 1881, "url": "https://launcher.mojang.com/v1/objects/29683cf2bd47015c9461b688749ddffd95f6671d/jjs.1" }, "raw": { "sha1": "78d419bd3a7f3e0802d5220e690429194b5d1beb", "size": 4932, "url": "https://launcher.mojang.com/v1/objects/78d419bd3a7f3e0802d5220e690429194b5d1beb/jjs.1" } }, "executable": false, "type": "file" }, "man/man1/keytool.1": { "downloads": { "lzma": { "sha1": "b67e5126d43713ee3675706724b34061578b42db", "size": 19690, "url": "https://launcher.mojang.com/v1/objects/b67e5126d43713ee3675706724b34061578b42db/keytool.1" }, "raw": { "sha1": "4c976f86057ab779763fcfb98f5702ebef47f629", "size": 86925, "url": "https://launcher.mojang.com/v1/objects/4c976f86057ab779763fcfb98f5702ebef47f629/keytool.1" } }, "executable": false, "type": "file" }, "man/man1/orbd.1": { "downloads": { "lzma": { "sha1": "147064d6f7e027002e296bb246ae572d0ce0495b", "size": 3708, "url": "https://launcher.mojang.com/v1/objects/147064d6f7e027002e296bb246ae572d0ce0495b/orbd.1" }, "raw": { "sha1": "64201e1846fcf1dcc45c786ffeab89426d1c7742", "size": 12180, "url": "https://launcher.mojang.com/v1/objects/64201e1846fcf1dcc45c786ffeab89426d1c7742/orbd.1" } }, "executable": false, "type": "file" }, "man/man1/pack200.1": { "downloads": { "lzma": { "sha1": "fe17486bbe9c58cf4182fa056b9cd124e8295607", "size": 3724, "url": "https://launcher.mojang.com/v1/objects/fe17486bbe9c58cf4182fa056b9cd124e8295607/pack200.1" }, "raw": { "sha1": "26826cf52b89924f2d2a60d6cda798891875eae6", "size": 11623, "url": "https://launcher.mojang.com/v1/objects/26826cf52b89924f2d2a60d6cda798891875eae6/pack200.1" } }, "executable": false, "type": "file" }, "man/man1/policytool.1": { "downloads": { "lzma": { "sha1": "bd154e7c39aca71d15b2098c588866f8d95bc743", "size": 1122, "url": "https://launcher.mojang.com/v1/objects/bd154e7c39aca71d15b2098c588866f8d95bc743/policytool.1" }, "raw": { "sha1": "ab296625155d9a2b25ecc2b4feff2f741b3ad136", "size": 3235, "url": "https://launcher.mojang.com/v1/objects/ab296625155d9a2b25ecc2b4feff2f741b3ad136/policytool.1" } }, "executable": false, "type": "file" }, "man/man1/rmid.1": { "downloads": { "lzma": { "sha1": "6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa", "size": 4255, "url": "https://launcher.mojang.com/v1/objects/6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa/rmid.1" }, "raw": { "sha1": "6f10e214d7950a6a8460524e41dc700f112f89e5", "size": 15979, "url": "https://launcher.mojang.com/v1/objects/6f10e214d7950a6a8460524e41dc700f112f89e5/rmid.1" } }, "executable": false, "type": "file" }, "man/man1/rmiregistry.1": { "downloads": { "lzma": { "sha1": "f40dd17e3a734600ad1828b0c42d3a1685c4c520", "size": 1301, "url": "https://launcher.mojang.com/v1/objects/f40dd17e3a734600ad1828b0c42d3a1685c4c520/rmiregistry.1" }, "raw": { "sha1": "d9a3d23fab689df5bb9a792b88f462f939b49f70", "size": 3449, "url": "https://launcher.mojang.com/v1/objects/d9a3d23fab689df5bb9a792b88f462f939b49f70/rmiregistry.1" } }, "executable": false, "type": "file" }, "man/man1/servertool.1": { "downloads": { "lzma": { "sha1": "74f1e10712202cd3ca0ff5833de05b7ee67092e1", "size": 2307, "url": "https://launcher.mojang.com/v1/objects/74f1e10712202cd3ca0ff5833de05b7ee67092e1/servertool.1" }, "raw": { "sha1": "e6c7b510740ac8681a9bfb5f4ee1f0306125b728", "size": 7237, "url": "https://launcher.mojang.com/v1/objects/e6c7b510740ac8681a9bfb5f4ee1f0306125b728/servertool.1" } }, "executable": false, "type": "file" }, "man/man1/tnameserv.1": { "downloads": { "lzma": { "sha1": "4bec7f4e070d023f124f9352a8971d7acd249a15", "size": 3955, "url": "https://launcher.mojang.com/v1/objects/4bec7f4e070d023f124f9352a8971d7acd249a15/tnameserv.1" }, "raw": { "sha1": "a31dbbe800d49cb371fab9a4b73d22c3bf8799ad", "size": 15747, "url": "https://launcher.mojang.com/v1/objects/a31dbbe800d49cb371fab9a4b73d22c3bf8799ad/tnameserv.1" } }, "executable": false, "type": "file" }, "man/man1/unpack200.1": { "downloads": { "lzma": { "sha1": "f8e73863187929debf2ea6dadefb2995ec7917e7", "size": 1672, "url": "https://launcher.mojang.com/v1/objects/f8e73863187929debf2ea6dadefb2995ec7917e7/unpack200.1" }, "raw": { "sha1": "437f7233d738cb9b822e99003127049005663e0f", "size": 4244, "url": "https://launcher.mojang.com/v1/objects/437f7233d738cb9b822e99003127049005663e0f/unpack200.1" } }, "executable": false, "type": "file" }, "plugin": { "type": "directory" }, "plugin/desktop": { "type": "directory" }, "plugin/desktop/sun_java.desktop": { "downloads": { "lzma": { "sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop" }, "raw": { "sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop" } }, "executable": false, "type": "file" }, "plugin/desktop/sun_java.png": { "downloads": { "raw": { "sha1": "699c41e97a35414e72a80327a54d6e14e874e951", "size": 4351, "url": "https://launcher.mojang.com/v1/objects/699c41e97a35414e72a80327a54d6e14e874e951/sun_java.png" } }, "executable": false, "type": "file" }, "release": { "downloads": { "raw": { "sha1": "cb462682644c0275d94a45b759108815f3112064", "size": 424, "url": "https://launcher.mojang.com/v1/objects/cb462682644c0275d94a45b759108815f3112064/release" } }, "executable": false, "type": "file" } } }PrismLauncher-10.0.5/tests/testdata/ShaderPackParse/0000755000175100017510000000000015144136757021747 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ShaderPackParse/shaderpack3.zip0000644000175100017510000000020015144136757024653 0ustar runnerrunnerPKl‹˜Uno_shaders_herePK?l‹˜U¤no_shaders_herePK=-PrismLauncher-10.0.5/tests/testdata/ShaderPackParse/shaderpack1.zip0000644000175100017510000000036215144136757024662 0ustar runnerrunnerPKu‹˜Ushaders/PKG‹˜Ushaders/shaders.propertiesPK?u‹˜UíAshaders/PK?G‹˜U¤&shaders/shaders.propertiesPK~^PrismLauncher-10.0.5/tests/testdata/ShaderPackParse/shaderpack2/0000755000175100017510000000000015144136757024136 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ShaderPackParse/shaderpack2/shaders/0000755000175100017510000000000015144136757025567 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ShaderPackParse/shaderpack2/shaders/shaders.properties0000644000175100017510000000000015144136757031324 0ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/0000755000175100017510000000000015144136757021412 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/0000755000175100017510000000000015144136757023724 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/pack.mcmeta0000644000175100017510000000012615144136757026031 0ustar runnerrunner{ "pack": { "pack_format": 10, "description": "Some data pack, maybe" } } PrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/data/0000755000175100017510000000000015144136757024635 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/data/dummy/0000755000175100017510000000000015144136757025770 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/data/dummy/tags/0000755000175100017510000000000015144136757026726 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/0000755000175100017510000000000015144136757027664 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/0000755000175100017510000000000015144136757031654 5ustar runnerrunner././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootPrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.jsonPrismLauncher-10.0.5/tests/testdata/DataPackParse/test_folder/data/dummy/tags/item/foo_proof/bar.jso0000644000175100017510000000000215144136757033125 0ustar runnerrunner{}PrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/0000755000175100017510000000000015144136757025444 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/pack.mcmeta0000644000175100017510000000015015144136757027546 0ustar runnerrunner{ "pack": { "pack_format": 6, "description": "Some data pack three, leaves on the tree" } } PrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/data/0000755000175100017510000000000015144136757026355 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/data/dummy/0000755000175100017510000000000015144136757027510 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/0000755000175100017510000000000015144136757030446 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/0000755000175100017510000000000015144136757031404 5ustar runnerrunner././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootPrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/PrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof0000755000175100017510000000000015144136757033315 5ustar runnerrunner././@LongLink0000644000000000000000000000015600000000000011605 Lustar rootrootPrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof/bar.jsonPrismLauncher-10.0.5/tests/testdata/DataPackParse/another_test_folder/data/dummy/tags/item/foo_proof0000644000175100017510000000000215144136757033307 0ustar runnerrunner{}PrismLauncher-10.0.5/tests/testdata/DataPackParse/test_data_pack_boogaloo.zip0000644000175100017510000000160215144136757026764 0ustar runnerrunnerPKm ‰Udata/PKm ‰U data/dummy/PKm ‰Udata/dummy/tags/PKm ‰Udata/dummy/tags/item/PKm ‰Udata/dummy/tags/item/foo_proof/PKÓ‰U'data/dummy/tags/item/foo_proof/bar.jsonPK0Ÿ‰U˜6\PLZ pack.mcmeta«æRPP*HLÎV²R¨²¡¼ø´ü¢ÜÄ  ‰D4%µ8¹(³ $3?(ªœŸ›ª’X’¨R®`¤”ŸŸ”ž˜“Ÿ¯T_ËUËPK?m ‰UíAdata/PK?m ‰U íA#data/dummy/PK?m ‰UíALdata/dummy/tags/PK?m ‰UíAzdata/dummy/tags/item/PK?m ‰UíA­data/dummy/tags/item/foo_proof/PK?Ó‰U'¤êdata/dummy/tags/item/foo_proof/bar.jsonPK?0Ÿ‰U˜6\PLZ ¤/pack.mcmetaPKȤPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/0000755000175100017510000000000015144136757021650 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_3/0000755000175100017510000000000015144136757025060 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/0000755000175100017510000000000015144136757026431 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_3/world_3/level.dat0000644000175100017510000000000015144136757030220 0ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_1.zip0000644000175100017510000000027015144136757025601 0ustar runnerrunnerPK ©€˜Uworld_1/level.datUT ®…§c®…§cux èèPK ©€˜U¤world_1/level.datUT®…§cux èèPKWKPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_4/0000755000175100017510000000000015144136757025061 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_4/saves/0000755000175100017510000000000015144136757026202 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/0000755000175100017510000000000015144136757027554 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_4/saves/world_4/level.dat0000644000175100017510000000000015144136757031343 0ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/WorldSaveParse/minecraft_save_2.zip0000644000175100017510000000054015144136757025602 0ustar runnerrunnerPK €˜Usaves/world_2/UT Ü…§c鉧cux èèPK €˜Usaves/world_2/level.datUT Ü…§cÜ…§cux èèPK €˜UíAsaves/world_2/UTÜ…§cux èèPK €˜U¤Hsaves/world_2/level.datUTÜ…§cux èèPK±™PrismLauncher-10.0.5/tests/testdata/ResourcePackParse/0000755000175100017510000000000015144136757022330 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ResourcePackParse/test_folder/0000755000175100017510000000000015144136757024642 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ResourcePackParse/test_folder/pack.nfo0000644000175100017510000000000215144136757026254 0ustar runnerrunner PrismLauncher-10.0.5/tests/testdata/ResourcePackParse/test_folder/pack.mcmeta0000644000175100017510000000013015144136757026742 0ustar runnerrunner{ "pack": { "pack_format": 1, "description": "Some resource pack maybe" } } PrismLauncher-10.0.5/tests/testdata/ResourcePackParse/test_folder/assets/0000755000175100017510000000000015144136757026144 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ResourcePackParse/test_folder/assets/minecraft/0000755000175100017510000000000015144136757030114 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ResourcePackParse/test_folder/assets/minecraft/textures/0000755000175100017510000000000015144136757031777 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ResourcePackParse/test_folder/assets/minecraft/textures/blah.txt0000644000175100017510000000000215144136757033436 0ustar runnerrunner PrismLauncher-10.0.5/tests/testdata/ResourcePackParse/supercoolmod.jar0000644000175100017510000000001615144136757025536 0ustar runnerrunnerthe best mod. PrismLauncher-10.0.5/tests/testdata/ResourcePackParse/another_test_folder/0000755000175100017510000000000015144136757026362 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/ResourcePackParse/another_test_folder/pack.mcmeta0000644000175100017510000000022715144136757030471 0ustar runnerrunner{ "pack": { "pack_format": 6, "description": "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional" } } PrismLauncher-10.0.5/tests/testdata/ResourcePackParse/test_resource_pack_idk.zip0000644000175100017510000000144415144136757027572 0ustar runnerrunnerPK3vUß G –Ë pack.mcmetaUT ±Mc±Mcux ééEŽ1Â0 Eg*õnæll\…4E;ehÅi˜Ê5r1L‹„ô‡ÿŸÿ—<·ÍÎdç¯æ³ú-â芲½]aÄ3æ‚”š1BO(†€—ú"ðÁ1Ód¡p} ÜGW˜þ…DÙ%“'ð{7Ô0oº± KµE10œPü–=ôaú²“R”®3úß³mTPK4˜Uassets/PK4˜Uassets/minecraft/PK4˜Uassets/minecraft/textures/PKÖAVUC߈4"assets/minecraft/textures/blah.txtSàPK3vUß G –Ë ¤pack.mcmetaUT±Mcux ééPK?4˜UíAÛassets/PK?4˜UíAassets/minecraft/PK?4˜UíA3assets/minecraft/textures/PK?ÖAVUC߈4"¤massets/minecraft/textures/blah.txtPK]±PrismLauncher-10.0.5/tests/testdata/MetaComponentParse/0000755000175100017510000000000015144136757022513 5ustar runnerrunnerPrismLauncher-10.0.5/tests/testdata/MetaComponentParse/component_with_format.json0000644000175100017510000000060115144136757030010 0ustar runnerrunner{ "description": [ { "text": "Hello, Component!", "color": "blue", "bold": true, "italic": true, "underlined": true, "strikethrough": true } ], "expected_output": "Hello, Component!" }PrismLauncher-10.0.5/tests/testdata/MetaComponentParse/component_with_extra.json0000644000175100017510000000114015144136757027642 0ustar runnerrunner{ "description": [ { "text": "Hello, ", "color": "red", "bold": true, "italic": true, "extra": [ { "extra": [ "Component!" ], "bold": false, "italic": false } ] } ], "expected_output": "Hello, Component!" }PrismLauncher-10.0.5/tests/testdata/MetaComponentParse/component_with_mixed.json0000644000175100017510000000315415144136757027634 0ustar runnerrunner{ "description": [ { "text": "The quick ", "color": "blue", "italic": true }, { "text": "brown fox ", "color": "#873600", "bold": true, "underlined": true, "extra": [ { "text": "jumped over ", "color": "blue", "bold": false, "underlined": false, "italic": true, "strikethrough": true } ] }, { "text": "the lazy dog's back. ", "color": "green", "bold": true, "italic": true, "underlined": true, "strikethrough": true, "extra": [ { "text": "1234567890 ", "color": "black", "strikethrough": false, "extra": [ "How vexingly quick daft zebras jump!" ] } ] } ], "expected_output": "The quick brown fox jumped over the lazy dog's back. 1234567890 How vexingly quick daft zebras jump!" } PrismLauncher-10.0.5/tests/testdata/MetaComponentParse/component_with_link.json0000644000175100017510000000046515144136757027465 0ustar runnerrunner{ "description": [ { "text": "Hello, Component!", "clickEvent": { "action": "open_url", "value": "https://google.com" } } ], "expected_output": "Hello, Component!" } PrismLauncher-10.0.5/tests/testdata/MetaComponentParse/component_basic.json0000644000175100017510000000021715144136757026551 0ustar runnerrunner{ "description": [ { "text": "Hello, Component!" } ], "expected_output": "Hello, Component!" } PrismLauncher-10.0.5/tests/CatPack_test.cpp0000644000175100017510000000353215144136757020212 0ustar runnerrunner#include #include #include #include #include #include "FileSystem.h" #include "ui/themes/CatPack.h" class CatPackTest : public QObject { Q_OBJECT private slots: void test_catPack() { auto dataDir = QDir(QFINDTESTDATA("testdata/CatPacks")).absolutePath(); auto fileName = FS::PathCombine(dataDir, "index.json"); auto fileinfo = QFileInfo(fileName); try { auto cat = JsonCatPack(fileinfo); QCOMPARE(cat.path(QDate(2023, 4, 12)), FS::PathCombine(fileinfo.path(), "oneDay.png")); QCOMPARE(cat.path(QDate(2023, 4, 11)), FS::PathCombine(fileinfo.path(), "maxwell.png")); QCOMPARE(cat.path(QDate(2023, 4, 13)), FS::PathCombine(fileinfo.path(), "maxwell.png")); QCOMPARE(cat.path(QDate(2023, 12, 21)), FS::PathCombine(fileinfo.path(), "christmas.png")); QCOMPARE(cat.path(QDate(2023, 12, 28)), FS::PathCombine(fileinfo.path(), "christmas.png")); QCOMPARE(cat.path(QDate(2023, 12, 29)), FS::PathCombine(fileinfo.path(), "newyear.png")); QCOMPARE(cat.path(QDate(2023, 12, 30)), FS::PathCombine(fileinfo.path(), "newyear2.png")); QCOMPARE(cat.path(QDate(2023, 12, 31)), FS::PathCombine(fileinfo.path(), "newyear2.png")); QCOMPARE(cat.path(QDate(2024, 1, 1)), FS::PathCombine(fileinfo.path(), "newyear2.png")); QCOMPARE(cat.path(QDate(2024, 1, 2)), FS::PathCombine(fileinfo.path(), "newyear.png")); QCOMPARE(cat.path(QDate(2024, 1, 3)), FS::PathCombine(fileinfo.path(), "newyear.png")); QCOMPARE(cat.path(QDate(2024, 1, 4)), FS::PathCombine(fileinfo.path(), "maxwell.png")); } catch (const Exception& e) { QFAIL(e.cause().toLatin1()); } } }; QTEST_GUILESS_MAIN(CatPackTest) #include "CatPack_test.moc" PrismLauncher-10.0.5/tests/Task_test.cpp0000644000175100017510000001677015144136757017616 0ustar runnerrunner#include #include #include #include #include #include #include #include /* Does nothing. Only used for testing. */ class BasicTask : public Task { Q_OBJECT friend class TaskTest; public: BasicTask(bool show_debug_log = true) : Task(show_debug_log) {} private: void executeTask() override { emitSucceeded(); } }; /* Does nothing. Only used for testing. */ class BasicTask_MultiStep : public Task { Q_OBJECT friend class TaskTest; private: auto isMultiStep() const -> bool override { return true; } void executeTask() override {} }; class BigConcurrentTask : public ConcurrentTask { Q_OBJECT void executeNextSubTask() override { // This is here only to help fill the stack a bit more quickly (if there's an issue, of course :^)) // Each tasks thus adds 1024 * 4 bytes to the stack, at the very least. [[maybe_unused]] volatile std::array some_data_on_the_stack{}; ConcurrentTask::executeNextSubTask(); } }; class BigConcurrentTaskThread : public QThread { Q_OBJECT QTimer m_deadline; void run() override { BigConcurrentTask big_task; m_deadline.setInterval(10000); // NOTE: Arbitrary value that manages to trigger a problem when there is one. // Considering each tasks, in a problematic state, adds 1024 * 4 bytes to the stack, // this number is enough to fill up 16 MiB of stack, more than enough to cause a problem. static const unsigned s_num_tasks = 1 << 12; for (unsigned i = 0; i < s_num_tasks; i++) { auto sub_task = makeShared(false); big_task.addTask(sub_task); } connect(&big_task, &Task::finished, this, &QThread::quit); connect(&m_deadline, &QTimer::timeout, this, [this] { passed_the_deadline = true; quit(); }); if (thread() != QThread::currentThread()) { QMetaObject::invokeMethod(this, &BigConcurrentTaskThread::start_timer, Qt::QueuedConnection); } big_task.run(); exec(); } void start_timer() { m_deadline.start(); } public: bool passed_the_deadline = false; }; class TaskTest : public QObject { Q_OBJECT private slots: void test_SetStatus_NoMultiStep() { BasicTask t; QString status{ "test status" }; t.setStatus(status); QCOMPARE(t.getStatus(), status); QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty()); } void test_SetStatus_MultiStep() { BasicTask_MultiStep t; QString status{ "test status" }; t.setStatus(status); QCOMPARE(t.getStatus(), status); // Even though it is multi step, it does not override the getStepStatus method, // so it should remain the same. QCOMPARE(t.getStepProgress().isEmpty(), TaskStepProgressList{}.isEmpty()); } void test_SetProgress() { BasicTask t; int current = 42; int total = 207; t.setProgress(current, total); QCOMPARE(t.getProgress(), current); QCOMPARE(t.getTotalProgress(), total); } void test_basicRun() { BasicTask t; connect(&t, &Task::finished, [&t] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicConcurrentRun() { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); ConcurrentTask t; t.addTask(t1); t.addTask(t2); t.addTask(t3); connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); QVERIFY(t3->wasSuccessful()); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } // Tests if starting new tasks after the 6 initial ones is working void test_moreConcurrentRun() { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); auto t4 = makeShared(); auto t5 = makeShared(); auto t6 = makeShared(); auto t7 = makeShared(); auto t8 = makeShared(); auto t9 = makeShared(); ConcurrentTask t; t.addTask(t1); t.addTask(t2); t.addTask(t3); t.addTask(t4); t.addTask(t5); t.addTask(t6); t.addTask(t7); t.addTask(t8); t.addTask(t9); connect(&t, &Task::finished, [&t, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); QVERIFY(t3->wasSuccessful()); QVERIFY(t4->wasSuccessful()); QVERIFY(t5->wasSuccessful()); QVERIFY(t6->wasSuccessful()); QVERIFY(t7->wasSuccessful()); QVERIFY(t8->wasSuccessful()); QVERIFY(t9->wasSuccessful()); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicSequentialRun() { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); SequentialTask t; t.addTask(t1); t.addTask(t2); t.addTask(t3); connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); QVERIFY(t3->wasSuccessful()); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_basicMultipleOptionsRun() { auto t1 = makeShared(); auto t2 = makeShared(); auto t3 = makeShared(); MultipleOptionsTask t; t.addTask(t1); t.addTask(t2); t.addTask(t3); connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(!t2->wasSuccessful()); QVERIFY(!t3->wasSuccessful()); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); } void test_stackOverflowInConcurrentTask() { QEventLoop loop; BigConcurrentTaskThread thread; connect(&thread, &BigConcurrentTaskThread::finished, &loop, &QEventLoop::quit); thread.start(); loop.exec(); QVERIFY(!thread.passed_the_deadline); } }; QTEST_GUILESS_MAIN(TaskTest) #include "Task_test.moc" PrismLauncher-10.0.5/tests/WorldSaveParse_test.cpp0000644000175100017510000000547715144136757021617 0ustar runnerrunner // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include class WorldSaveParseTest : public QObject { Q_OBJECT private slots: void test_parseZIP() { QString source = QFINDTESTDATA("testdata/WorldSaveParse"); QString zip_ws = FS::PathCombine(source, "minecraft_save_1.zip"); WorldSave save{ QFileInfo(zip_ws) }; bool valid = WorldSaveUtils::processZIP(save); QVERIFY(save.saveFormat() == WorldSaveFormat::SINGLE); QVERIFY(save.saveDirName() == "world_1"); QVERIFY(valid == true); } void test_parse_ZIP2() { QString source = QFINDTESTDATA("testdata/WorldSaveParse"); QString zip_ws = FS::PathCombine(source, "minecraft_save_2.zip"); WorldSave save{ QFileInfo(zip_ws) }; bool valid = WorldSaveUtils::processZIP(save); QVERIFY(save.saveFormat() == WorldSaveFormat::MULTI); QVERIFY(save.saveDirName() == "world_2"); QVERIFY(valid == true); } void test_parseFolder() { QString source = QFINDTESTDATA("testdata/WorldSaveParse"); QString folder_ws = FS::PathCombine(source, "minecraft_save_3"); WorldSave save{ QFileInfo(folder_ws) }; bool valid = WorldSaveUtils::processFolder(save); QVERIFY(save.saveFormat() == WorldSaveFormat::SINGLE); QVERIFY(save.saveDirName() == "world_3"); QVERIFY(valid == true); } void test_parseFolder2() { QString source = QFINDTESTDATA("testdata/WorldSaveParse"); QString folder_ws = FS::PathCombine(source, "minecraft_save_4"); WorldSave save{ QFileInfo(folder_ws) }; bool valid = WorldSaveUtils::process(save); QVERIFY(save.saveFormat() == WorldSaveFormat::MULTI); QVERIFY(save.saveDirName() == "world_4"); QVERIFY(valid == true); } }; QTEST_GUILESS_MAIN(WorldSaveParseTest) #include "WorldSaveParse_test.moc" PrismLauncher-10.0.5/tests/ResourceFolderModel_test.cpp0000644000175100017510000002147115144136757022612 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include "BaseInstance.h" #include #include #include #define EXEC_UPDATE_TASK(EXEC, VERIFY) \ QEventLoop loop; \ \ connect(&model, &ResourceFolderModel::updateFinished, &loop, &QEventLoop::quit); \ \ QTimer expire_timer; \ expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \ expire_timer.setSingleShot(true); \ expire_timer.start(4000); \ \ VERIFY(EXEC); \ loop.exec(); \ \ QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \ expire_timer.stop(); \ \ disconnect(&model, nullptr, &loop, nullptr); class ResourceFolderModelTest : public QObject { Q_OBJECT private slots: // test for GH-1178 - install a folder with files to a mod list void test_1178() { // source QString source = QFINDTESTDATA("testdata/ResourceFolderModel/test_folder"); // sanity check QVERIFY(!source.endsWith('/')); auto verify = [](QString path) { QDir target_dir(FS::PathCombine(path, "test_folder")); QVERIFY(target_dir.entryList().contains("pack.mcmeta")); QVERIFY(target_dir.entryList().contains("assets")); }; // 1. test with no trailing / { QString folder = source; QTemporaryDir tempDir; QEventLoop loop; ModFolderModel m(tempDir.path(), nullptr, true, true); connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); QTimer expire_timer; expire_timer.callOnTimeout(&loop, &QEventLoop::quit); expire_timer.setSingleShot(true); expire_timer.start(4000); m.installResource(folder); loop.exec(); QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); expire_timer.stop(); verify(tempDir.path()); } // 2. test with trailing / { QString folder = source + '/'; QTemporaryDir tempDir; QEventLoop loop; ModFolderModel m(tempDir.path(), nullptr, true, true); connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit); QTimer expire_timer; expire_timer.callOnTimeout(&loop, &QEventLoop::quit); expire_timer.setSingleShot(true); expire_timer.start(4000); m.installResource(folder); loop.exec(); QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); expire_timer.stop(); verify(tempDir.path()); } } void test_addFromWatch() { QString source = QFINDTESTDATA("testdata/ResourceFolderModel"); ModFolderModel model(source, nullptr, false, true); QCOMPARE(model.size(), 0); EXEC_UPDATE_TASK(model.startWatching(), ) for (auto mod : model.allMods()) qDebug() << mod->name(); QCOMPARE(model.size(), 4); model.stopWatching(); } void test_removeResource() { QString folder_resource = QFINDTESTDATA("testdata/ResourceFolderModel/test_folder"); QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar"); QTemporaryDir tmp; ResourceFolderModel model(QDir(tmp.path()), nullptr, false, false); QCOMPARE(model.size(), 0); { EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) } QCOMPARE(model.size(), 1); qDebug() << "Added first mod."; { EXEC_UPDATE_TASK(model.startWatching(), ) } QCOMPARE(model.size(), 1); qDebug() << "Started watching the temp folder."; { EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) } QCOMPARE(model.size(), 2); qDebug() << "Added second mod."; { EXEC_UPDATE_TASK(model.uninstallResource("supercoolmod.jar"), QVERIFY); } QCOMPARE(model.size(), 1); qDebug() << "Removed first mod."; QString mod_file_name{ model.at(0).fileinfo().fileName() }; QVERIFY(!mod_file_name.isEmpty()); { EXEC_UPDATE_TASK(model.uninstallResource(mod_file_name), QVERIFY); } QCOMPARE(model.size(), 0); qDebug() << "Removed second mod."; model.stopWatching(); } void test_enable_disable() { QString folder_resource = QFINDTESTDATA("testdata/ResourceFolderModel/test_folder"); QString file_mod = QFINDTESTDATA("testdata/ResourceFolderModel/supercoolmod.jar"); QTemporaryDir tmp; ResourceFolderModel model(tmp.path(), nullptr, false, false); QCOMPARE(model.size(), 0); { EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY) } { EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY) } for (auto res : model.allResources()) qDebug() << res->name(); QCOMPARE(model.size(), 2); auto& res_1 = model.at(0).type() != ResourceType::FOLDER ? model.at(0) : model.at(1); auto& res_2 = model.at(0).type() == ResourceType::FOLDER ? model.at(0) : model.at(1); auto id_1 = res_1.internal_id(); auto id_2 = res_2.internal_id(); bool initial_enabled_res_2 = res_2.enabled(); bool initial_enabled_res_1 = res_1.enabled(); QVERIFY(res_1.type() != ResourceType::FOLDER && res_1.type() != ResourceType::UNKNOWN); qDebug() << "res_1 is of the correct type."; QVERIFY(res_1.enabled()); qDebug() << "res_1 is initially enabled."; QVERIFY(res_1.enable(EnableAction::TOGGLE)); QVERIFY(res_1.enabled() == !initial_enabled_res_1); qDebug() << "res_1 got successfully toggled."; QVERIFY(res_1.enable(EnableAction::TOGGLE)); qDebug() << "res_1 got successfully toggled again."; QVERIFY(res_1.enabled() == initial_enabled_res_1); QVERIFY(res_1.internal_id() == id_1); qDebug() << "res_1 got back to its initial state."; QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE)); QVERIFY(res_2.enabled() == initial_enabled_res_2); QVERIFY(res_2.internal_id() == id_2); } }; QTEST_GUILESS_MAIN(ResourceFolderModelTest) #include "ResourceFolderModel_test.moc" PrismLauncher-10.0.5/tests/MojangVersionFormat_test.cpp0000644000175100017510000000353415144136757022640 0ustar runnerrunner#include #include #include class MojangVersionFormatTest : public QObject { Q_OBJECT static QJsonDocument readJson(const QString path) { QFile jsonFile(path); if (!jsonFile.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file '" << jsonFile.fileName() << "' for reading!"; return QJsonDocument(); } auto data = jsonFile.readAll(); jsonFile.close(); return QJsonDocument::fromJson(data); } static void writeJson(const char* file, QJsonDocument doc) { QFile jsonFile(file); if (!jsonFile.open(QIODevice::WriteOnly | QIODevice::Text)) { qCritical() << "Failed to open file '" << jsonFile.fileName() << "' for writing!"; return; } auto data = doc.toJson(QJsonDocument::Indented); qDebug() << QString::fromUtf8(data); jsonFile.write(data); jsonFile.close(); } private slots: void test_Through_Simple() { QJsonDocument doc = readJson(QFINDTESTDATA("testdata/MojangVersionFormat/1.9-simple.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-simple-passthorugh.json", doc2); QCOMPARE(doc.toJson(), doc2.toJson()); } void test_Through() { QJsonDocument doc = readJson(QFINDTESTDATA("testdata/MojangVersionFormat/1.9.json")); auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json"); auto doc2 = MojangVersionFormat::versionFileToJson(vfile); writeJson("1.9-passthorugh.json", doc2); QCOMPARE(doc.toJson(), doc2.toJson()); } }; QTEST_GUILESS_MAIN(MojangVersionFormatTest) #include "MojangVersionFormat_test.moc" PrismLauncher-10.0.5/tests/MetaComponentParse_test.cpp0000644000175100017510000000604415144136757022451 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include class MetaComponentParseTest : public QObject { Q_OBJECT void doTest(QString name) { QString source = QFINDTESTDATA("testdata/MetaComponentParse"); QString comp_rp = FS::PathCombine(source, name); QFile file; file.setFileName(comp_rp); QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text)); QString data = file.readAll(); file.close(); QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8()); QJsonObject obj = doc.object(); QJsonValue description_json = obj.value("description"); QJsonValue expected_json = obj.value("expected_output"); QVERIFY(!description_json.isUndefined()); QVERIFY(expected_json.isString()); QString expected = expected_json.toString(); QString processed = DataPackUtils::processComponent(description_json); QCOMPARE(processed, expected); } private slots: void test_parseComponentBasic() { doTest("component_basic.json"); } void test_parseComponentWithFormat() { doTest("component_with_format.json"); } void test_parseComponentWithExtra() { doTest("component_with_extra.json"); } void test_parseComponentWithLink() { doTest("component_with_link.json"); } void test_parseComponentWithMixed() { doTest("component_with_mixed.json"); } }; QTEST_GUILESS_MAIN(MetaComponentParseTest) #include "MetaComponentParse_test.moc" PrismLauncher-10.0.5/tests/JavaVersion_test.cpp0000644000175100017510000001300515144136757021127 0ustar runnerrunner#include #include class JavaVersionTest : public QObject { Q_OBJECT private slots: void test_Parse_data() { QTest::addColumn("string"); QTest::addColumn("major"); QTest::addColumn("minor"); QTest::addColumn("security"); QTest::addColumn("prerelease"); QTest::newRow("old format") << "1.6.0_33" << 6 << 0 << 33 << QString(); QTest::newRow("old format prerelease") << "1.9.0_1-ea" << 9 << 0 << 1 << "ea"; QTest::newRow("new format major") << "9" << 9 << 0 << 0 << QString(); QTest::newRow("new format minor") << "9.1" << 9 << 1 << 0 << QString(); QTest::newRow("new format security") << "9.0.1" << 9 << 0 << 1 << QString(); QTest::newRow("new format prerelease") << "9-ea" << 9 << 0 << 0 << "ea"; QTest::newRow("new format long prerelease") << "9.0.1-ea" << 9 << 0 << 1 << "ea"; } void test_Parse() { QFETCH(QString, string); QFETCH(int, major); QFETCH(int, minor); QFETCH(int, security); QFETCH(QString, prerelease); JavaVersion test(string); QCOMPARE(test.m_string, string); QCOMPARE(test.toString(), string); QCOMPARE(test.m_major, major); QCOMPARE(test.m_minor, minor); QCOMPARE(test.m_security, security); QCOMPARE(test.m_prerelease, prerelease); } void test_Sort_data() { QTest::addColumn("lhs"); QTest::addColumn("rhs"); QTest::addColumn("smaller"); QTest::addColumn("equal"); QTest::addColumn("bigger"); // old format and new format equivalence QTest::newRow("1.6.0_33 == 6.0.33") << "1.6.0_33" << "6.0.33" << false << true << false; // old format major version QTest::newRow("1.5.0_33 < 1.6.0_33") << "1.5.0_33" << "1.6.0_33" << true << false << false; // new format - first release vs first security patch QTest::newRow("9 < 9.0.1") << "9" << "9.0.1" << true << false << false; QTest::newRow("9.0.1 > 9") << "9.0.1" << "9" << false << false << true; // new format - first minor vs first release/security patch QTest::newRow("9.1 > 9.0.1") << "9.1" << "9.0.1" << false << false << true; QTest::newRow("9.0.1 < 9.1") << "9.0.1" << "9.1" << true << false << false; QTest::newRow("9.1 > 9") << "9.1" << "9" << false << false << true; QTest::newRow("9 > 9.1") << "9" << "9.1" << true << false << false; // new format - omitted numbers QTest::newRow("9 == 9.0") << "9" << "9.0" << false << true << false; QTest::newRow("9 == 9.0.0") << "9" << "9.0.0" << false << true << false; QTest::newRow("9.0 == 9.0.0") << "9.0" << "9.0.0" << false << true << false; // early access and prereleases compared to final release QTest::newRow("9-ea < 9") << "9-ea" << "9" << true << false << false; QTest::newRow("9 < 9.0.1-ea") << "9" << "9.0.1-ea" << true << false << false; QTest::newRow("9.0.1-ea > 9") << "9.0.1-ea" << "9" << false << false << true; // prerelease difference only testing QTest::newRow("9-1 == 9-1") << "9-1" << "9-1" << false << true << false; QTest::newRow("9-1 < 9-2") << "9-1" << "9-2" << true << false << false; QTest::newRow("9-5 < 9-20") << "9-5" << "9-20" << true << false << false; QTest::newRow("9-rc1 < 9-rc2") << "9-rc1" << "9-rc2" << true << false << false; QTest::newRow("9-rc5 < 9-rc20") << "9-rc5" << "9-rc20" << true << false << false; QTest::newRow("9-rc < 9-rc2") << "9-rc" << "9-rc2" << true << false << false; QTest::newRow("9-ea < 9-rc") << "9-ea" << "9-rc" << true << false << false; } void test_Sort() { QFETCH(QString, lhs); QFETCH(QString, rhs); QFETCH(bool, smaller); QFETCH(bool, equal); QFETCH(bool, bigger); JavaVersion lver(lhs); JavaVersion rver(rhs); QCOMPARE(lver < rver, smaller); QCOMPARE(lver == rver, equal); QCOMPARE(lver > rver, bigger); } void test_PermGen_data() { QTest::addColumn("version"); QTest::addColumn("needs_permgen"); QTest::newRow("1.6.0_33") << "1.6.0_33" << true; QTest::newRow("1.7.0_60") << "1.7.0_60" << true; QTest::newRow("1.8.0_22") << "1.8.0_22" << false; QTest::newRow("9-ea") << "9-ea" << false; QTest::newRow("9.2.4") << "9.2.4" << false; } void test_PermGen() { QFETCH(QString, version); QFETCH(bool, needs_permgen); JavaVersion v(version); QCOMPARE(needs_permgen, v.requiresPermGen()); } }; QTEST_GUILESS_MAIN(JavaVersionTest) #include "JavaVersion_test.moc" PrismLauncher-10.0.5/vcpkg.json0000644000175100017510000000066515144136757016010 0ustar runnerrunner{ "dependencies": [ { "name": "ecm", "host": true }, { "name": "libqrencode", "default-features": false }, { "name": "pkgconf", "host": true }, "cmark", { "name": "libarchive", "default-features": false, "features": [ "bzip2", "lz4", "lzma", "lzo", "zstd" ] }, "tomlplusplus", "zlib" ] } PrismLauncher-10.0.5/nix/0000755000175100017510000000000015144136757014572 5ustar runnerrunnerPrismLauncher-10.0.5/nix/unwrapped.nix0000644000175100017510000000476215144136757017330 0ustar runnerrunner{ lib, stdenv, cmake, cmark, extra-cmake-modules, gamemode, jdk17, kdePackages, libnbtplusplus, ninja, qrencode, self, stripJavaArchivesHook, tomlplusplus, zlib, msaClientID ? null, libarchive, }: let date = let # YYYYMMDD date' = lib.substring 0 8 self.lastModifiedDate; year = lib.substring 0 4 date'; month = lib.substring 4 2 date'; date = lib.substring 6 2 date'; in if (self ? "lastModifiedDate") then lib.concatStringsSep "-" [ year month date ] else "unknown"; in stdenv.mkDerivation { pname = "prismlauncher-unwrapped"; version = "10.0-unstable-${date}"; src = lib.fileset.toSource { root = ../.; fileset = lib.fileset.unions [ ../CMakeLists.txt ../COPYING.md ../buildconfig ../cmake ../launcher ../libraries ../program_info ../tests ]; }; postUnpack = '' rm -rf source/libraries/libnbtplusplus ln -s ${libnbtplusplus} source/libraries/libnbtplusplus ''; nativeBuildInputs = [ cmake ninja extra-cmake-modules jdk17 stripJavaArchivesHook ]; buildInputs = [ cmark kdePackages.qtbase kdePackages.qtnetworkauth qrencode libarchive tomlplusplus zlib ] ++ lib.optional stdenv.hostPlatform.isLinux gamemode; cmakeFlags = [ # downstream branding (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") ] ++ lib.optionals (msaClientID != null) [ (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ # we wrap our binary manually (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") # disable built-in updater (lib.cmakeFeature "MACOSX_SPARKLE_UPDATE_FEED_URL" "''") (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") ]; doCheck = true; dontWrapQtApps = true; meta = { description = "Free, open source launcher for Minecraft"; longDescription = '' Allows you to have multiple, separate instances of Minecraft (each with their own mods, texture packs, saves, etc) and helps you manage them and their associated options with a simple interface. ''; homepage = "https://prismlauncher.org/"; license = lib.licenses.gpl3Only; maintainers = with lib.maintainers; [ Scrumplex getchoo ]; mainProgram = "prismlauncher"; platforms = lib.platforms.linux ++ lib.platforms.darwin; }; } PrismLauncher-10.0.5/nix/README.md0000644000175100017510000001422115144136757016051 0ustar runnerrunner# Prism Launcher Nix Packaging ## Installing a stable release (nixpkgs) Prism Launcher is packaged in [nixpkgs](https://github.com/NixOS/nixpkgs/) since 22.11. Check the [NixOS Wiki](https://wiki.nixos.org/wiki/Prism_Launcher) for up-to-date instructions. ## Installing a development release (flake) We use [cachix](https://cachix.org/) to cache our development and release builds. If you want to avoid rebuilds you may add the Cachix bucket to your substitutors, or use `--accept-flake-config` to temporarily enable it when using `nix` commands. Example (NixOS): ```nix { nix.settings = { trusted-substituters = [ "https://prismlauncher.cachix.org" ]; trusted-public-keys = [ "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; } ``` ### Installing the package directly After adding `github:PrismLauncher/PrismLauncher` to your flake inputs, you can access the flake's `packages` output. Example: ```nix { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; prismlauncher = { url = "github:PrismLauncher/PrismLauncher"; # Optional: Override the nixpkgs input of prismlauncher to use the same revision as the rest of your flake # Note that this may break the reproducibility mentioned above, and you might not be able to access the binary cache # # inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { nixpkgs, prismlauncher, ... }: { nixosConfigurations.foo = nixpkgs.lib.nixosSystem { modules = [ ./configuration.nix ( { pkgs, ... }: { environment.systemPackages = [ prismlauncher.packages.${pkgs.system}.prismlauncher ]; } ) ]; }; }; } ``` ### Using the overlay Alternatively, if you don't want to use our `packages` output, you can add our overlay to your nixpkgs instance. This will ensure Prism is built with your system's packages. > [!WARNING] > Depending on what revision of nixpkgs your system uses, this may result in binaries that differ from the above `packages` output > If this is the case, you will not be able to use the binary cache Example: ```nix { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; prismlauncher = { url = "github:PrismLauncher/PrismLauncher"; # Optional: Override the nixpkgs input of prismlauncher to use the same revision as the rest of your flake # Note that this may break the reproducibility mentioned above, and you might not be able to access the binary cache # # inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { nixpkgs, prismlauncher, ... }: { nixosConfigurations.foo = nixpkgs.lib.nixosSystem { modules = [ ./configuration.nix ( { pkgs, ... }: { nixpkgs.overlays = [ prismlauncher.overlays.default ]; environment.systemPackages = [ pkgs.prismlauncher ]; } ) ]; }; }; } ``` ### Installing the package ad-hoc (`nix shell`, `nix run`, etc.) You can simply call the default package of this flake. Example: ```shell nix run github:PrismLauncher/PrismLauncher nix shell github:PrismLauncher/PrismLauncher nix profile install github:PrismLauncher/PrismLauncher ``` ## Installing a development release (without flakes) We use [Cachix](https://cachix.org/) to cache our development and release builds. If you want to avoid rebuilds you may add the Cachix bucket to your substitutors. Example (NixOS): ```nix { nix.settings = { trusted-substituters = [ "https://prismlauncher.cachix.org" ]; trusted-public-keys = [ "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; } ``` ### Installing the package directly (`fetchTarball`) We use flake-compat to allow using this Flake on a system that doesn't use flakes. Example: ```nix { pkgs, ... }: { environment.systemPackages = [ (import ( builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz" )).packages.${pkgs.system}.prismlauncher ]; } ``` ### Using the overlay (`fetchTarball`) Alternatively, if you don't want to use our `packages` output, you can add our overlay to your instance of nixpkgs. This results in Prism using your system's libraries Example: ```nix { pkgs, ... }: { nixpkgs.overlays = [ (import ( builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz" )).overlays.default ]; environment.systemPackages = [ pkgs.prismlauncher ]; } ``` ### Installing the package ad-hoc (`nix-env`) You can add this repository as a channel and install its packages that way. Example: ```shell nix-channel --add https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz prismlauncher nix-channel --update prismlauncher nix-env -iA prismlauncher.prismlauncher ``` ## Package variants Both Nixpkgs and this repository offer the following packages: - `prismlauncher` - The preferred build, wrapped with everything necessary to run the launcher and Minecraft - `prismlauncher-unwrapped` - A minimal build that allows for advanced customization of the launcher's runtime environment ### Customizing wrapped packages The wrapped package (`prismlauncher`) offers some build parameters to further customize the launcher's environment. The following parameters can be overridden: - `additionalLibs` (default: `[ ]`) Additional libraries that will be added to `LD_LIBRARY_PATH` - `additionalPrograms` (default: `[ ]`) Additional libraries that will be added to `PATH` - `controllerSupport` (default: `isLinux`) Turn on/off support for controllers on Linux (macOS will always have this) - `gamemodeSupport` (default: `isLinux`) Turn on/off support for [Feral GameMode](https://github.com/FeralInteractive/gamemode) on Linux - `jdks` (default: `[ jdk21 jdk17 jdk8 ]`) Java runtimes added to `PRISMLAUNCHER_JAVA_PATHS` variable - `msaClientID` (default: `null`, requires full rebuild!) Client ID used for Microsoft Authentication - `textToSpeechSupport` (default: `isLinux`) Turn on/off support for text-to-speech on Linux (macOS will always have this) PrismLauncher-10.0.5/nix/wrapper.nix0000644000175100017510000000524515144136757017000 0ustar runnerrunner{ addDriverRunpath, alsa-lib, flite, gamemode, glfw3-minecraft, jdk17, jdk21, jdk8, kdePackages, lib, libGL, libX11, libXcursor, libXext, libXrandr, libXxf86vm, libjack2, libpulseaudio, libusb1, mesa-demos, openal, pciutils, pipewire, prismlauncher-unwrapped, stdenv, symlinkJoin, udev, vulkan-loader, xrandr, additionalLibs ? [ ], additionalPrograms ? [ ], controllerSupport ? stdenv.hostPlatform.isLinux, gamemodeSupport ? stdenv.hostPlatform.isLinux, jdks ? [ jdk21 jdk17 jdk8 ], msaClientID ? null, textToSpeechSupport ? stdenv.hostPlatform.isLinux, }: assert lib.assertMsg ( controllerSupport -> stdenv.hostPlatform.isLinux ) "controllerSupport only has an effect on Linux."; assert lib.assertMsg ( textToSpeechSupport -> stdenv.hostPlatform.isLinux ) "textToSpeechSupport only has an effect on Linux."; let prismlauncher' = prismlauncher-unwrapped.override { inherit msaClientID; }; in symlinkJoin { name = "prismlauncher-${prismlauncher'.version}"; paths = [ prismlauncher' ]; nativeBuildInputs = [ kdePackages.wrapQtAppsHook ]; buildInputs = [ kdePackages.qtbase kdePackages.qtimageformats kdePackages.qtsvg ] ++ lib.optional ( lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.hostPlatform.isLinux ) kdePackages.qtwayland; postBuild = '' wrapQtAppsHook ''; qtWrapperArgs = let runtimeLibs = [ (lib.getLib stdenv.cc.cc) ## native versions glfw3-minecraft openal ## openal alsa-lib libjack2 libpulseaudio pipewire ## glfw libGL libX11 libXcursor libXext libXrandr libXxf86vm udev # oshi vulkan-loader # VulkanMod's lwjgl ] ++ lib.optional textToSpeechSupport flite ++ lib.optional gamemodeSupport gamemode.lib ++ lib.optional controllerSupport libusb1 ++ additionalLibs; runtimePrograms = [ mesa-demos pciutils # need lspci xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 ] ++ additionalPrograms; in [ "--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}" ] ++ lib.optionals stdenv.hostPlatform.isLinux [ "--set LD_LIBRARY_PATH ${addDriverRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" "--prefix PATH : ${lib.makeBinPath runtimePrograms}" ]; meta = { inherit (prismlauncher'.meta) description longDescription homepage changelog license maintainers mainProgram platforms ; }; } PrismLauncher-10.0.5/.markdownlintignore0000644000175100017510000000002615144136756017707 0ustar runnerrunnerlibraries/nbtplusplus PrismLauncher-10.0.5/LICENSE0000644000175100017510000010451515144136756015006 0ustar runnerrunner GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . PrismLauncher-10.0.5/.editorconfig0000644000175100017510000000072615144136756016455 0ustar runnerrunner# EditorConfig specs and documentation: https://EditorConfig.org # top-most EditorConfig file root = true [*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true [*.{yml,nix}] indent_size = 2 # C++ Code Style settings [*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] cpp_generate_documentation_comments = doxygen_slash_star [CMakeLists.txt] ij_continuation_indent_size = 4 PrismLauncher-10.0.5/libraries/0000755000175100017510000000000015144136757015750 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/README.md0000644000175100017510000000606015144136757017231 0ustar runnerrunner# Third-party libraries This folder has third-party or otherwise external libraries needed for other parts to work. ## javacheck Simple Java tool that prints the JVM details - version and platform bitness. Do what you want with it. It is so trivial that noone cares. ## launcher Java launcher part for Minecraft. It does the following: - Waits for a launch script on stdin. - Consumes the launch script you feed it. - Proceeds with launch when it gets the `launcher` command. If "abort" is sent, the process will exit. This means the process is essentially idle until the final command is sent. You can, for example, attach a profiler before you send it. The `standard` and `legacy` launchers are available. - `standard` can handle launching any Minecraft version, at the cost of some extra features `legacy` enables (custom window icon and title). - `legacy` is intended for use with Minecraft versions < 1.6 and is deprecated. Example (some parts have been censored): ```text mod legacyjavafixer-1.0 mainClass net.minecraft.launchwrapper.Launch param --username param CENSORED param --version param Prism Launcher param --gameDir param /home/peterix/minecraft/FTB/17ForgeTest/minecraft param --assetsDir param /home/peterix/minecraft/mmc5/assets param --assetIndex param 1.7.10 param --uuid param CENSORED param --accessToken param CENSORED param --userProperties param {} param --userType param mojang param --tweakClass param cpw.mods.fml.common.launcher.FMLTweaker windowTitle Prism Launcher: 172ForgeTest windowParams 854x480 userName CENSORED sessionId token:CENSORED:CENSORED launcher standard ``` Available under `GPL-3.0-only` (with classpath exception), sublicensed from its original `Apache-2.0` codebase ## libnbtplusplus libnbt++ is a free C++ library for Minecraft's file format Named Binary Tag (NBT). It can read and write compressed and uncompressed NBT files and provides a code interface for working with NBT data. See [github repo](https://github.com/ljfa-ag/libnbtplusplus). Available either under LGPL version 3 or later. ## LocalPeer Library for making only one instance of the application run at all times. BSD licensed, derived from [QtSingleApplication](https://github.com/qtproject/qt-solutions/tree/master/qtsingleapplication). Changes are made to make the code more generic and useful in less usual conditions. ## murmur2 Canonical implementation of the murmur2 hash, taken from [SMHasher](https://github.com/aappleby/smhasher). Public domain (the author disclaimed the copyright). ## rainbow Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiaddons.html). Used for adaptive text coloring. Available either under LGPL version 2.1 or later. ## systeminfo A Prism Launcher-specific library for probing system information. Apache 2.0 ## qdcss A quick and dirty css parser, used by NilLoader to store mod metadata. Translated (and heavily trimmed down) from [the original Java code](https://github.com/unascribed/NilLoader/blob/trunk/src/main/java/nilloader/api/lib/qdcss/QDCSS.java) from NilLoader Licensed under LGPL version 3. PrismLauncher-10.0.5/libraries/qdcss/0000755000175100017510000000000015144136757017065 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/qdcss/LICENSE0000644000175100017510000001640415144136757020077 0ustar runnerrunnerGNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. PrismLauncher-10.0.5/libraries/qdcss/include/0000755000175100017510000000000015144136757020510 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/qdcss/include/qdcss.h0000644000175100017510000000121415144136757021774 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 kumquat-ir 66188216+kumquat-ir@users.noreply.github.com // // SPDX-License-Identifier: LGPL-3.0-only #ifndef QDCSS_H #define QDCSS_H #include #include #include #include class QDCSS { // these are all we need to parse a couple string values out of a css string // lots more in the original code, yet to be ported // https://github.com/unascribed/NilLoader/blob/trunk/src/main/java/nilloader/api/lib/qdcss/QDCSS.java public: QDCSS(QString); std::optional* get(QString); private: QMap m_data; }; #endif // QDCSS_H PrismLauncher-10.0.5/libraries/qdcss/CMakeLists.txt0000644000175100017510000000061515144136757021627 0ustar runnerrunnercmake_minimum_required(VERSION 3.15) project(qdcss) if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core REQUIRED) endif() set(QDCSS_SOURCES include/qdcss.h src/qdcss.cpp ) add_library(qdcss STATIC ${QDCSS_SOURCES}) target_include_directories(qdcss PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) target_link_libraries(qdcss Qt${QT_VERSION_MAJOR}::Core ${qdcss_LIBS}) PrismLauncher-10.0.5/libraries/qdcss/src/0000755000175100017510000000000015144136757017654 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/qdcss/src/qdcss.cpp0000644000175100017510000000357415144136757021506 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 kumquat-ir 66188216+kumquat-ir@users.noreply.github.com // // SPDX-License-Identifier: LGPL-3.0-only #include "qdcss.h" #include #include #include static const QRegularExpression s_rulesetRe(R"([#.]?(@?\w+?)\s*\{(.*?)\})", QRegularExpression::DotMatchesEverythingOption); static const QRegularExpression s_ruleRe(R"((\S+?)\s*:\s*(?:\"(.*?)(?append(value); } } } std::optional* QDCSS::get(QString key) { auto found = m_data.find(key); if (found == m_data.end() || found->empty()) { return new std::optional; } return new std::optional(found->back()); } PrismLauncher-10.0.5/libraries/.clang-tidy0000644000175100017510000000007415144136757020005 0ustar runnerrunner# We don't care about linting third-party code. Checks: -* PrismLauncher-10.0.5/libraries/systeminfo/0000755000175100017510000000000015144136757020150 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/systeminfo/include/0000755000175100017510000000000015144136757021573 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/systeminfo/include/sys.h0000644000175100017510000000211715144136757022563 0ustar runnerrunner#pragma once #include namespace Sys { const uint64_t mebibyte = 1024ull * 1024ull; enum class KernelType { Undetermined, Windows, Darwin, Linux }; struct KernelInfo { QString kernelName; QString kernelVersion; KernelType kernelType = KernelType::Undetermined; int kernelMajor = 0; int kernelMinor = 0; int kernelPatch = 0; bool isCursed = false; }; KernelInfo getKernelInfo(); struct DistributionInfo { DistributionInfo operator+(const DistributionInfo& rhs) const { DistributionInfo out; if (!distributionName.isEmpty()) { out.distributionName = distributionName; } else { out.distributionName = rhs.distributionName; } if (!distributionVersion.isEmpty()) { out.distributionVersion = distributionVersion; } else { out.distributionVersion = rhs.distributionVersion; } return out; } QString distributionName; QString distributionVersion; }; DistributionInfo getDistributionInfo(); uint64_t getSystemRam(); } // namespace Sys PrismLauncher-10.0.5/libraries/systeminfo/include/distroutils.h0000644000175100017510000000105415144136757024331 0ustar runnerrunner#include #include "sys.h" namespace Sys { struct LsbInfo { QString distributor; QString version; QString description; QString codename; }; bool main_lsb_info(LsbInfo& out); bool fallback_lsb_info(Sys::LsbInfo& out); void lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out); Sys::DistributionInfo read_lsb_release(); QString _extract_distribution(const QString& x); QString _extract_version(const QString& x); Sys::DistributionInfo read_legacy_release(); Sys::DistributionInfo read_os_release(); } // namespace Sys PrismLauncher-10.0.5/libraries/systeminfo/CMakeLists.txt0000644000175100017510000000141715144136757022713 0ustar runnerrunnerproject(systeminfo) if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core REQUIRED) endif() set(systeminfo_SOURCES include/sys.h include/distroutils.h src/distroutils.cpp ) if (WIN32) list(APPEND systeminfo_SOURCES src/sys_win32.cpp) elseif (UNIX) if(APPLE) list(APPEND systeminfo_SOURCES src/sys_apple.cpp) else() list(APPEND systeminfo_SOURCES src/sys_unix.cpp) endif() endif() add_library(systeminfo STATIC ${systeminfo_SOURCES}) target_link_libraries(systeminfo Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Network ${systeminfo_LIBS}) target_include_directories(systeminfo PUBLIC include) ecm_add_test(src/sys_test.cpp LINK_LIBRARIES systeminfo Qt${QT_VERSION_MAJOR}::Test TEST_NAME sys) PrismLauncher-10.0.5/libraries/systeminfo/src/0000755000175100017510000000000015144136757020737 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/systeminfo/src/sys_test.cpp0000644000175100017510000000125215144136757023320 0ustar runnerrunner#include #include class SysTest : public QObject { Q_OBJECT private slots: void test_kernelNotNull() { auto kinfo = Sys::getKernelInfo(); QVERIFY(!kinfo.kernelName.isEmpty()); QVERIFY(kinfo.kernelVersion != "0.0"); } /* void test_systemDistroNotNull() { auto kinfo = Sys::getDistributionInfo(); QVERIFY(!kinfo.distributionName.isEmpty()); QVERIFY(!kinfo.distributionVersion.isEmpty()); qDebug() << "Distro: " << kinfo.distributionName << "version" << kinfo.distributionVersion; } */ }; QTEST_GUILESS_MAIN(SysTest) #include "sys_test.moc" PrismLauncher-10.0.5/libraries/systeminfo/src/sys_apple.cpp0000644000175100017510000000273515144136757023451 0ustar runnerrunner#include "sys.h" #include #include #include #include Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; struct utsname buf; uname(&buf); out.kernelType = KernelType::Darwin; out.kernelName = buf.sysname; QString release = out.kernelVersion = buf.release; // TODO: figure out how to detect cursed-ness (macOS emulated on linux via mad hacks and so on) out.isCursed = false; out.kernelMajor = 0; out.kernelMinor = 0; out.kernelPatch = 0; auto sections = release.split('-'); if (sections.size() >= 1) { auto versionParts = sections[0].split('.'); if (versionParts.size() >= 3) { out.kernelMajor = versionParts[0].toInt(); out.kernelMinor = versionParts[1].toInt(); out.kernelPatch = versionParts[2].toInt(); } else { qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); } } else { qWarning() << "Not enough '-' sections in " << release << " found " << sections.size(); } return out; } #include uint64_t Sys::getSystemRam() { uint64_t memsize; size_t memsizesize = sizeof(memsize); if (!sysctlbyname("hw.memsize", &memsize, &memsizesize, NULL, 0)) { return memsize; } else { return 0; } } Sys::DistributionInfo Sys::getDistributionInfo() { DistributionInfo result; return result; } PrismLauncher-10.0.5/libraries/systeminfo/src/sys_win32.cpp0000644000175100017510000000153415144136757023306 0ustar runnerrunner#include "sys.h" #include Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; out.kernelType = KernelType::Windows; out.kernelName = "Windows"; OSVERSIONINFOW osvi; ZeroMemory(&osvi, sizeof(OSVERSIONINFOW)); osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOW); GetVersionExW(&osvi); out.kernelVersion = QString("%1.%2").arg(osvi.dwMajorVersion).arg(osvi.dwMinorVersion); out.kernelMajor = osvi.dwMajorVersion; out.kernelMinor = osvi.dwMinorVersion; out.kernelPatch = osvi.dwBuildNumber; return out; } uint64_t Sys::getSystemRam() { MEMORYSTATUSEX status; status.dwLength = sizeof(status); GlobalMemoryStatusEx(&status); // bytes return (uint64_t)status.ullTotalPhys; } Sys::DistributionInfo Sys::getDistributionInfo() { DistributionInfo result; return result; } PrismLauncher-10.0.5/libraries/systeminfo/src/sys_unix.cpp0000644000175100017510000000532015144136757023324 0ustar runnerrunner#include "sys.h" #include "distroutils.h" #include #include #include #include #include #include Sys::KernelInfo Sys::getKernelInfo() { Sys::KernelInfo out; struct utsname buf; uname(&buf); // NOTE: we assume linux here. this needs further elaboration out.kernelType = KernelType::Linux; out.kernelName = buf.sysname; QString release = out.kernelVersion = buf.release; // linux binary running on WSL is cursed. out.isCursed = release.contains("WSL", Qt::CaseInsensitive) || release.contains("Microsoft", Qt::CaseInsensitive); out.kernelMajor = 0; out.kernelMinor = 0; out.kernelPatch = 0; auto sections = release.split('-'); if (sections.size() >= 1) { auto versionParts = sections[0].split('.'); if (versionParts.size() >= 3) { out.kernelMajor = versionParts[0].toInt(); out.kernelMinor = versionParts[1].toInt(); out.kernelPatch = versionParts[2].toInt(); } else { qWarning() << "Not enough version numbers in " << sections[0] << " found " << versionParts.size(); } } else { qWarning() << "Not enough '-' sections in " << release << " found " << sections.size(); } return out; } uint64_t Sys::getSystemRam() { std::string token; #ifdef Q_OS_LINUX std::ifstream file("/proc/meminfo"); while (file >> token) { if (token == "MemTotal:") { uint64_t mem; if (file >> mem) { return mem * 1024ull; } else { return 0; } } // ignore rest of the line file.ignore(std::numeric_limits::max(), '\n'); } #elif defined(Q_OS_FREEBSD) char buff[512]; FILE* fp = popen("sysctl hw.physmem", "r"); if (fp != NULL) { while (fgets(buff, 512, fp) != NULL) { std::string str(buff); uint64_t mem = std::stoull(str.substr(12, std::string::npos)); return mem * 1024ull; } } #endif return 0; // nothing found } Sys::DistributionInfo Sys::getDistributionInfo() { DistributionInfo systemd_info = read_os_release(); DistributionInfo lsb_info = read_lsb_release(); DistributionInfo legacy_info = read_legacy_release(); DistributionInfo result = systemd_info + lsb_info + legacy_info; if (result.distributionName.isNull()) { result.distributionName = "unknown"; } if (result.distributionVersion.isNull()) { if (result.distributionName == "arch") { result.distributionVersion = "rolling"; } else { result.distributionVersion = "unknown"; } } return result; } PrismLauncher-10.0.5/libraries/systeminfo/src/distroutils.cpp0000644000175100017510000001766015144136757024042 0ustar runnerrunner/* Code has been taken from https://github.com/natefoo/lionshead and loosely translated to C++ laced with Qt. MIT License Copyright (c) 2017 Nate Coraor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "distroutils.h" #include #include #include #include #include #include #include #include #include static const QRegularExpression s_distoSplitRegex("\\s+"); Sys::DistributionInfo Sys::read_os_release() { Sys::DistributionInfo out; QStringList files = { "/etc/os-release", "/usr/lib/os-release" }; QString name; QString version; for (auto& file : files) { if (!QFile::exists(file)) { continue; } QSettings settings(file, QSettings::IniFormat); if (settings.contains("ID")) { name = settings.value("ID").toString().toLower(); } else if (settings.contains("NAME")) { name = settings.value("NAME").toString().toLower(); } else { continue; } if (settings.contains("VERSION_ID")) { version = settings.value("VERSION_ID").toString().toLower(); } else if (settings.contains("VERSION")) { version = settings.value("VERSION").toString().toLower(); } break; } if (name.isEmpty()) { return out; } out.distributionName = name; out.distributionVersion = version; return out; } bool Sys::main_lsb_info(Sys::LsbInfo& out) { int status = 0; QProcess lsbProcess; QStringList arguments; arguments << "-a"; lsbProcess.start("lsb_release", arguments); lsbProcess.waitForFinished(); status = lsbProcess.exitStatus(); QString output = lsbProcess.readAllStandardOutput(); qDebug() << output; lsbProcess.close(); if (status == 0) { auto lines = output.split('\n'); for (auto line : lines) { int index = line.indexOf(':'); auto key = line.left(index).trimmed(); auto value = line.mid(index + 1).toLower().trimmed(); if (key == "Distributor ID") out.distributor = value; else if (key == "Release") out.version = value; else if (key == "Description") out.description = value; else if (key == "Codename") out.codename = value; } return !out.distributor.isEmpty(); } return false; } bool Sys::fallback_lsb_info(Sys::LsbInfo& out) { // running lsb_release failed, try to read the file instead // /etc/lsb-release format, if the file even exists, is non-standard. // Only the `lsb_release` command is specified by LSB. Nonetheless, some // distributions install an /etc/lsb-release as part of the base // distribution, but `lsb_release` remains optional. QString file = "/etc/lsb-release"; if (QFile::exists(file)) { QSettings settings(file, QSettings::IniFormat); if (settings.contains("DISTRIB_ID")) { out.distributor = settings.value("DISTRIB_ID").toString().toLower(); } if (settings.contains("DISTRIB_RELEASE")) { out.version = settings.value("DISTRIB_RELEASE").toString().toLower(); } return !out.distributor.isEmpty(); } return false; } void Sys::lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out) { QString dist = lsb.distributor; QString vers = lsb.version; if (dist.startsWith("redhatenterprise")) { dist = "rhel"; } else if (dist == "archlinux") { dist = "arch"; } else if (dist.startsWith("suse")) { if (lsb.description.startsWith("opensuse")) { dist = "opensuse"; } else if (lsb.description.startsWith("suse linux enterprise")) { dist = "sles"; } } else if (dist == "debian" and vers == "testing") { vers = lsb.codename; } else { // ubuntu, debian, gentoo, scientific, slackware, ... ? auto parts = dist.split(s_distoSplitRegex, Qt::SkipEmptyParts); if (parts.size()) { dist = parts[0]; } } if (!dist.isEmpty()) { out.distributionName = dist; out.distributionVersion = vers; } } Sys::DistributionInfo Sys::read_lsb_release() { LsbInfo lsb; if (!main_lsb_info(lsb)) { if (!fallback_lsb_info(lsb)) { return Sys::DistributionInfo(); } } Sys::DistributionInfo out; lsb_postprocess(lsb, out); return out; } QString Sys::_extract_distribution(const QString& x) { QString release = x.toLower(); if (release.startsWith("red hat enterprise")) { return "rhel"; } if (release.startsWith("suse linux enterprise")) { return "sles"; } QStringList list = release.split(s_distoSplitRegex, Qt::SkipEmptyParts); if (list.size()) { return list[0]; } return QString(); } QString Sys::_extract_version(const QString& x) { static const QRegularExpression s_versionishString(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); QStringList list = x.split(s_distoSplitRegex, Qt::SkipEmptyParts); for (int i = list.size() - 1; i >= 0; --i) { QString chunk = list[i]; if (s_versionishString.match(chunk).hasMatch()) { return chunk; } } return QString(); } Sys::DistributionInfo Sys::read_legacy_release() { struct checkEntry { QString file; std::function extract_distro; std::function extract_version; }; QList checks = { { "/etc/arch-release", [](const QString&) { return "arch"; }, [](const QString&) { return "rolling"; } }, { "/etc/slackware-version", &Sys::_extract_distribution, &Sys::_extract_version }, { QString(), &Sys::_extract_distribution, &Sys::_extract_version }, { "/etc/debian_version", [](const QString&) { return "debian"; }, [](const QString& x) { return x; } }, }; for (auto& check : checks) { QStringList files; if (check.file.isNull()) { QDir etcDir("/etc"); etcDir.setNameFilters({ "*-release" }); etcDir.setFilter(QDir::Files | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden); files = etcDir.entryList(); } else { files.append(check.file); } for (auto file : files) { QFile relfile(file); if (!relfile.open(QIODevice::ReadOnly | QIODevice::Text)) continue; QString contents = QString::fromUtf8(relfile.readLine()).trimmed(); QString dist = check.extract_distro(contents); QString vers = check.extract_version(contents); if (!dist.isEmpty()) { Sys::DistributionInfo out; out.distributionName = dist; out.distributionVersion = vers; return out; } } } return Sys::DistributionInfo(); } PrismLauncher-10.0.5/libraries/LocalPeer/0000755000175100017510000000000015144136757017616 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/LocalPeer/include/0000755000175100017510000000000015144136757021241 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/LocalPeer/include/LocalPeer.h0000644000175100017510000000650315144136757023264 0ustar runnerrunner/**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Solutions component. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #pragma once #include #include #include class QLocalServer; class LockedFile; class ApplicationId { public: /* methods */ // traditional app = installed system wide and used in a multi-user environment static ApplicationId fromTraditionalApp(); // ID based on a path with all the application data (no two instances with the same data path should run) static ApplicationId fromPathAndVersion(const QString& dataPath, const QString& version); // custom ID static ApplicationId fromCustomId(const QString& id); // custom ID, based on a raw string previously acquired from 'toString' static ApplicationId fromRawString(const QString& id); QString toString() { return m_id; } private: /* methods */ ApplicationId(const QString& value) { m_id = value; } private: /* data */ QString m_id; }; class LocalPeer : public QObject { Q_OBJECT public: LocalPeer(QObject* parent, const ApplicationId& appId); ~LocalPeer(); bool isClient(); bool sendMessage(const QByteArray& message, int timeout); ApplicationId applicationId() const; Q_SIGNALS: void messageReceived(const QByteArray& message); protected Q_SLOTS: void receiveConnection(); protected: ApplicationId id; QString socketName; std::unique_ptr server; std::unique_ptr lockFile; }; PrismLauncher-10.0.5/libraries/LocalPeer/CMakeLists.txt0000644000175100017510000000120115144136757022350 0ustar runnerrunnercmake_minimum_required(VERSION 3.15) project(LocalPeer) if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Network REQUIRED) endif() set(SINGLE_SOURCES src/LocalPeer.cpp src/LockedFile.cpp src/LockedFile.h include/LocalPeer.h ) if(UNIX) list(APPEND SINGLE_SOURCES src/LockedFile_unix.cpp ) endif() if(WIN32) list(APPEND SINGLE_SOURCES src/LockedFile_win.cpp ) endif() add_library(LocalPeer STATIC ${SINGLE_SOURCES}) target_include_directories(LocalPeer PUBLIC include) target_link_libraries(LocalPeer Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network ${LocalPeer_LIBS}) PrismLauncher-10.0.5/libraries/LocalPeer/src/0000755000175100017510000000000015144136757020405 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/LocalPeer/src/LockedFile_win.cpp0000644000175100017510000001407515144136757023776 0ustar runnerrunner/**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Solutions component. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include "LockedFile.h" #define MUTEX_PREFIX "QtLockedFile mutex " // Maximum number of concurrent read locks. Must not be greater than MAXIMUM_WAIT_OBJECTS #define MAX_READERS MAXIMUM_WAIT_OBJECTS Qt::HANDLE LockedFile::getMutexHandle(int idx, bool doCreate) { if (mutexname.isEmpty()) { QFileInfo fi(*this); mutexname = QString::fromLatin1(MUTEX_PREFIX) + fi.absoluteFilePath().toLower(); } QString mname(mutexname); if (idx >= 0) mname += QString::number(idx); Qt::HANDLE mutex; if (doCreate) { mutex = CreateMutexW(NULL, FALSE, (LPCWSTR)mname.utf16()); if (!mutex) { qErrnoWarning("QtLockedFile::lock(): CreateMutex failed"); return 0; } } else { mutex = OpenMutexW(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE, (LPCWSTR)mname.utf16()); if (!mutex) { if (GetLastError() != ERROR_FILE_NOT_FOUND) qErrnoWarning("QtLockedFile::lock(): OpenMutex failed"); return 0; } } return mutex; } bool LockedFile::waitMutex(Qt::HANDLE mutex, bool doBlock) { Q_ASSERT(mutex); DWORD res = WaitForSingleObject(mutex, doBlock ? INFINITE : 0); switch (res) { case WAIT_OBJECT_0: case WAIT_ABANDONED: return true; break; case WAIT_TIMEOUT: break; default: qErrnoWarning("QtLockedFile::lock(): WaitForSingleObject failed"); } return false; } bool LockedFile::lock(LockMode mode, bool block) { if (!isOpen()) { qWarning("QtLockedFile::lock(): file is not opened"); return false; } if (mode == NoLock) return unlock(); if (mode == m_lock_mode) return true; if (m_lock_mode != NoLock) unlock(); if (!wmutex && !(wmutex = getMutexHandle(-1, true))) return false; if (!waitMutex(wmutex, block)) return false; if (mode == ReadLock) { int idx = 0; for (; idx < MAX_READERS; idx++) { rmutex = getMutexHandle(idx, false); if (!rmutex || waitMutex(rmutex, false)) break; CloseHandle(rmutex); } bool ok = true; if (idx >= MAX_READERS) { qWarning("QtLockedFile::lock(): too many readers"); rmutex = 0; ok = false; } else if (!rmutex) { rmutex = getMutexHandle(idx, true); if (!rmutex || !waitMutex(rmutex, false)) ok = false; } if (!ok && rmutex) { CloseHandle(rmutex); rmutex = 0; } ReleaseMutex(wmutex); if (!ok) return false; } else { Q_ASSERT(rmutexes.isEmpty()); for (int i = 0; i < MAX_READERS; i++) { Qt::HANDLE mutex = getMutexHandle(i, false); if (mutex) rmutexes.append(mutex); } if (rmutexes.size()) { DWORD res = WaitForMultipleObjects(rmutexes.size(), rmutexes.constData(), TRUE, block ? INFINITE : 0); if (res != WAIT_OBJECT_0 && res != WAIT_ABANDONED) { if (res != WAIT_TIMEOUT) qErrnoWarning("QtLockedFile::lock(): WaitForMultipleObjects failed"); m_lock_mode = WriteLock; // trick unlock() to clean up - semiyucky unlock(); return false; } } } m_lock_mode = mode; return true; } bool LockedFile::unlock() { if (!isOpen()) { qWarning("QtLockedFile::unlock(): file is not opened"); return false; } if (!isLocked()) return true; if (m_lock_mode == ReadLock) { ReleaseMutex(rmutex); CloseHandle(rmutex); rmutex = 0; } else { foreach (Qt::HANDLE mutex, rmutexes) { ReleaseMutex(mutex); CloseHandle(mutex); } rmutexes.clear(); ReleaseMutex(wmutex); } m_lock_mode = LockedFile::NoLock; return true; } LockedFile::~LockedFile() { if (isOpen()) unlock(); if (wmutex) CloseHandle(wmutex); } PrismLauncher-10.0.5/libraries/LocalPeer/src/LockedFile.h0000644000175100017510000000515715144136757022567 0ustar runnerrunner/**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Solutions component. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #pragma once #include #ifdef Q_OS_WIN #include #endif class LockedFile : public QFile { public: enum LockMode { NoLock = 0, ReadLock, WriteLock }; LockedFile(); LockedFile(const QString& name); ~LockedFile(); bool open(OpenMode mode); bool lock(LockMode mode, bool block = true); bool unlock(); bool isLocked() const; LockMode lockMode() const; private: #ifdef Q_OS_WIN Qt::HANDLE wmutex; Qt::HANDLE rmutex; QList rmutexes; QString mutexname; Qt::HANDLE getMutexHandle(int idx, bool doCreate); bool waitMutex(Qt::HANDLE mutex, bool doBlock); #endif LockMode m_lock_mode; }; PrismLauncher-10.0.5/libraries/LocalPeer/src/LocalPeer.cpp0000644000175100017510000001627415144136757022771 0ustar runnerrunner/**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Solutions component. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "LocalPeer.h" #include #include #include #include #include #include #include #include "LockedFile.h" #if defined(Q_OS_WIN) #include #include typedef BOOL(WINAPI* PProcessIdToSessionId)(DWORD, DWORD*); static PProcessIdToSessionId pProcessIdToSessionId = 0; #endif #if defined(Q_OS_UNIX) #include #include #endif #include #include #include static const char* ack = "ack"; ApplicationId ApplicationId::fromTraditionalApp() { QString protoId = QCoreApplication::applicationFilePath(); #if defined(Q_OS_WIN) protoId = protoId.toLower(); #endif auto prefix = protoId.section(QLatin1Char('/'), -1); static const QRegularExpression s_removeChars("[^a-zA-Z]"); prefix.remove(s_removeChars); prefix.truncate(6); QByteArray idc = protoId.toUtf8(); quint16 idNum = qChecksum(idc); auto socketName = QLatin1String("pl") + prefix + QLatin1Char('-') + QString::number(idNum, 16).left(12); #if defined(Q_OS_WIN) if (!pProcessIdToSessionId) { QLibrary lib("kernel32"); pProcessIdToSessionId = (PProcessIdToSessionId)lib.resolve("ProcessIdToSessionId"); } if (pProcessIdToSessionId) { DWORD sessionId = 0; pProcessIdToSessionId(GetCurrentProcessId(), &sessionId); socketName += QLatin1Char('-') + QString::number(sessionId, 16); } #else socketName += QLatin1Char('-') + QString::number(::getuid(), 16); #endif return ApplicationId(socketName); } ApplicationId ApplicationId::fromPathAndVersion(const QString& dataPath, const QString& version) { QCryptographicHash shasum(QCryptographicHash::Algorithm::Sha1); QString result = dataPath + QLatin1Char('-') + version; shasum.addData(result.toUtf8()); return ApplicationId(QLatin1String("pl") + QString::fromLatin1(shasum.result().toHex()).left(12)); } ApplicationId ApplicationId::fromCustomId(const QString& id) { return ApplicationId(QLatin1String("pl") + id); } ApplicationId ApplicationId::fromRawString(const QString& id) { return ApplicationId(id); } LocalPeer::LocalPeer(QObject* parent, const ApplicationId& appId) : QObject(parent), id(appId) { socketName = id.toString(); server.reset(new QLocalServer()); QString lockName = QDir(QDir::tempPath()).absolutePath() + QLatin1Char('/') + socketName + QLatin1String("-lockfile"); lockFile.reset(new LockedFile(lockName)); lockFile->open(QIODevice::ReadWrite); } LocalPeer::~LocalPeer() {} ApplicationId LocalPeer::applicationId() const { return id; } bool LocalPeer::isClient() { if (lockFile->isLocked()) return false; if (!lockFile->lock(LockedFile::WriteLock, false)) return true; bool res = server->listen(socketName); #if defined(Q_OS_UNIX) // ### Workaround if (!res && server->serverError() == QAbstractSocket::AddressInUseError) { QLocalServer::removeServer(socketName); res = server->listen(socketName); } #endif if (!res) qWarning("QtSingleCoreApplication: listen on local socket failed, %s", qPrintable(server->errorString())); connect(server.get(), &QLocalServer::newConnection, this, &LocalPeer::receiveConnection); return false; } bool LocalPeer::sendMessage(const QByteArray& message, int timeout) { if (!isClient()) return false; QLocalSocket socket; bool connOk = false; int tries = 2; for (int i = 0; i < tries; i++) { // Try twice, in case the other instance is just starting up socket.connectToServer(socketName); connOk = socket.waitForConnected(timeout / 2); if (!connOk && i < (tries - 1)) { std::this_thread::sleep_for(std::chrono::milliseconds(250)); } } if (!connOk) { return false; } QByteArray uMsg(message); QDataStream ds(&socket); ds.writeBytes(uMsg.constData(), uMsg.size()); if (!socket.waitForBytesWritten(timeout)) { return false; } // wait for 'ack' if (!socket.waitForReadyRead(timeout)) { return false; } // make sure we got 'ack' if (!(socket.read(qstrlen(ack)) == ack)) { return false; } return true; } void LocalPeer::receiveConnection() { QLocalSocket* socket = server->nextPendingConnection(); if (!socket) { return; } while (socket->bytesAvailable() < static_cast(sizeof(quint32))) { socket->waitForReadyRead(); } QDataStream ds(socket); QByteArray uMsg; quint32 remaining; ds >> remaining; uMsg.resize(remaining); int got = 0; char* uMsgBuf = uMsg.data(); do { got = ds.readRawData(uMsgBuf, remaining); remaining -= got; uMsgBuf += got; } while (remaining && got >= 0 && socket->waitForReadyRead(2000)); if (got < 0) { qWarning("QtLocalPeer: Message reception failed %s", socket->errorString().toLatin1().constData()); delete socket; return; } socket->write(ack, qstrlen(ack)); socket->waitForBytesWritten(1000); socket->waitForDisconnected(1000); // make sure client reads ack delete socket; emit messageReceived(uMsg); // ### (might take a long time to return) } PrismLauncher-10.0.5/libraries/LocalPeer/src/LockedFile_unix.cpp0000644000175100017510000000654315144136757024165 0ustar runnerrunner/**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Solutions component. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include "LockedFile.h" bool LockedFile::lock(LockMode mode, bool block) { if (!isOpen()) { qWarning("QtLockedFile::lock(): file is not opened"); return false; } if (mode == NoLock) return unlock(); if (mode == m_lock_mode) return true; if (m_lock_mode != NoLock) unlock(); struct flock fl; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0; fl.l_type = (mode == ReadLock) ? F_RDLCK : F_WRLCK; int cmd = block ? F_SETLKW : F_SETLK; int ret = fcntl(handle(), cmd, &fl); if (ret == -1) { if (errno != EINTR && errno != EAGAIN) qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); return false; } m_lock_mode = mode; return true; } bool LockedFile::unlock() { if (!isOpen()) { qWarning("QtLockedFile::unlock(): file is not opened"); return false; } if (!isLocked()) return true; struct flock fl; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0; fl.l_type = F_UNLCK; int ret = fcntl(handle(), F_SETLKW, &fl); if (ret == -1) { qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); return false; } m_lock_mode = NoLock; return true; } LockedFile::~LockedFile() { if (isOpen()) unlock(); } PrismLauncher-10.0.5/libraries/LocalPeer/src/LockedFile.cpp0000644000175100017510000001367015144136757023121 0ustar runnerrunner/**************************************************************************** ** ** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the Qt Solutions component. ** ** $QT_BEGIN_LICENSE:BSD$ ** You may use this file under the terms of the BSD license as follows: ** ** "Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are ** met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in ** the documentation and/or other materials provided with the ** distribution. ** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names ** of its contributors may be used to endorse or promote products derived ** from this software without specific prior written permission. ** ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "LockedFile.h" /*! \class QtLockedFile \brief The QtLockedFile class extends QFile with advisory locking functions. A file may be locked in read or write mode. Multiple instances of \e QtLockedFile, created in multiple processes running on the same machine, may have a file locked in read mode. Exactly one instance may have it locked in write mode. A read and a write lock cannot exist simultaneously on the same file. The file locks are advisory. This means that nothing prevents another process from manipulating a locked file using QFile or file system functions offered by the OS. Serialization is only guaranteed if all processes that access the file use QLockedFile. Also, while holding a lock on a file, a process must not open the same file again (through any API), or locks can be unexpectedly lost. The lock provided by an instance of \e QtLockedFile is released whenever the program terminates. This is true even when the program crashes and no destructors are called. */ /*! \enum QtLockedFile::LockMode This enum describes the available lock modes. \value ReadLock A read lock. \value WriteLock A write lock. \value NoLock Neither a read lock nor a write lock. */ /*! Constructs an unlocked \e QtLockedFile object. This constructor behaves in the same way as \e QFile::QFile(). \sa QFile::QFile() */ LockedFile::LockedFile() : QFile() { #ifdef Q_OS_WIN wmutex = 0; rmutex = 0; #endif m_lock_mode = NoLock; } /*! Constructs an unlocked QtLockedFile object with file \a name. This constructor behaves in the same way as \e QFile::QFile(const QString&). \sa QFile::QFile() */ LockedFile::LockedFile(const QString& name) : QFile(name) { #ifdef Q_OS_WIN wmutex = 0; rmutex = 0; #endif m_lock_mode = NoLock; } /*! Opens the file in OpenMode \a mode. This is identical to QFile::open(), with the one exception that the Truncate mode flag is disallowed. Truncation would conflict with the advisory file locking, since the file would be modified before the write lock is obtained. If truncation is required, use resize(0) after obtaining the write lock. Returns true if successful; otherwise false. \sa QFile::open(), QFile::resize() */ bool LockedFile::open(OpenMode mode) { if (mode & QIODevice::Truncate) { qWarning("QtLockedFile::open(): Truncate mode not allowed."); return false; } return QFile::open(mode); } /*! Returns \e true if this object has a in read or write lock; otherwise returns \e false. \sa lockMode() */ bool LockedFile::isLocked() const { return m_lock_mode != NoLock; } /*! Returns the type of lock currently held by this object, or \e QtLockedFile::NoLock. \sa isLocked() */ LockedFile::LockMode LockedFile::lockMode() const { return m_lock_mode; } /*! \fn bool QtLockedFile::lock(LockMode mode, bool block = true) Obtains a lock of type \a mode. The file must be opened before it can be locked. If \a block is true, this function will block until the lock is aquired. If \a block is false, this function returns \e false immediately if the lock cannot be aquired. If this object already has a lock of type \a mode, this function returns \e true immediately. If this object has a lock of a different type than \a mode, the lock is first released and then a new lock is obtained. This function returns \e true if, after it executes, the file is locked by this object, and \e false otherwise. \sa unlock(), isLocked(), lockMode() */ /*! \fn bool QtLockedFile::unlock() Releases a lock. If the object has no lock, this function returns immediately. This function returns \e true if, after it executes, the file is not locked by this object, and \e false otherwise. \sa lock(), isLocked(), lockMode() */ /*! \fn QtLockedFile::~QtLockedFile() Destroys the \e QtLockedFile object. If any locks were held, they are released. */ PrismLauncher-10.0.5/libraries/launcher/0000755000175100017510000000000015144136757017551 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/LICENSE0000777000175100017510000000000015144136757022206 2../../LICENSEustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/CMakeLists.txt0000644000175100017510000000354315144136757022316 0ustar runnerrunnercmake_minimum_required(VERSION 3.15) project(launcher Java) find_package(Java 1.7 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT org.prismlauncher.EntryPoint) set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7) set(SRC org/prismlauncher/EntryPoint.java org/prismlauncher/launcher/Launcher.java org/prismlauncher/launcher/impl/AbstractLauncher.java org/prismlauncher/launcher/impl/StandardLauncher.java org/prismlauncher/exception/ParameterNotFoundException.java org/prismlauncher/exception/ParseException.java org/prismlauncher/utils/Parameters.java org/prismlauncher/utils/ReflectionUtils.java org/prismlauncher/utils/logging/Level.java org/prismlauncher/utils/logging/Log.java org/prismlauncher/legacy/LegacyProxy.java ) set(LEGACY_SRC legacy/org/prismlauncher/legacy/LegacyFrame.java legacy/org/prismlauncher/legacy/LegacyLauncher.java legacy/org/prismlauncher/legacy/fix/online/Handler.java legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java legacy/org/prismlauncher/legacy/fix/online/OnlineModeFix.java legacy/org/prismlauncher/legacy/fix/online/SkinFix.java legacy/org/prismlauncher/legacy/utils/Base64.java legacy/org/prismlauncher/legacy/utils/api/MojangApi.java legacy/org/prismlauncher/legacy/utils/api/Texture.java legacy/org/prismlauncher/legacy/utils/json/JsonParseException.java legacy/org/prismlauncher/legacy/utils/json/JsonParser.java legacy/org/prismlauncher/legacy/utils/url/ByteArrayUrlConnection.java legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java legacy/net/minecraft/Launcher.java legacy/org/prismlauncher/legacy/LegacyProxy.java ) add_jar(NewLaunch ${SRC}) add_jar(NewLaunchLegacy ${LEGACY_SRC} INCLUDE_JARS NewLaunch) install_jar(NewLaunch "${JARS_DEST_DIR}") install_jar(NewLaunchLegacy "${JARS_DEST_DIR}") PrismLauncher-10.0.5/libraries/launcher/legacy/0000755000175100017510000000000015144136757021015 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/net/0000755000175100017510000000000015144136757021603 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/net/minecraft/0000755000175100017510000000000015144136757023553 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/net/minecraft/Launcher.java0000644000175100017510000001427015144136757026163 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 solonovamax * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.minecraft; import java.applet.Applet; import java.applet.AppletStub; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Graphics; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; /** * WARNING: This class is reflectively accessed by legacy Forge versions. *

* Changing field and method declarations without further testing is not * recommended. */ public final class Launcher extends Applet implements AppletStub { private static final long serialVersionUID = 1L; private final Map params = new HashMap<>(); private Applet wrappedApplet; private final URL documentBase; private boolean active = false; public Launcher(Applet applet) { this(applet, null); } public Launcher(Applet applet, URL documentBase) { setLayout(new BorderLayout()); add(applet, "Center"); wrappedApplet = applet; try { if (documentBase == null) { if (applet.getClass().getPackage().getName().startsWith("com.mojang")) // Special case only for Classic versions documentBase = new URL("http://www.minecraft.net:80/game/"); else documentBase = new URL("http://www.minecraft.net/game/"); } } catch (MalformedURLException e) { throw new AssertionError(e); } this.documentBase = documentBase; } public void replace(Applet applet) { wrappedApplet = applet; applet.setStub(this); applet.setSize(getWidth(), getHeight()); setLayout(new BorderLayout()); add(applet, "Center"); applet.init(); active = true; applet.start(); validate(); } @Override public boolean isActive() { return active; } @Override public URL getDocumentBase() { return documentBase; } @Override public URL getCodeBase() { try { return new URL("http://www.minecraft.net/game/"); } catch (MalformedURLException e) { throw new AssertionError(e); } } @Override public String getParameter(String key) { String param = params.get(key); if (param != null) return param; try { return super.getParameter(key); } catch (Throwable ignored) { } return null; } @Override public void resize(int width, int height) { wrappedApplet.resize(width, height); } @Override public void resize(Dimension size) { wrappedApplet.resize(size); } @Override public void init() { if (wrappedApplet != null) wrappedApplet.init(); } @Override public void start() { wrappedApplet.start(); active = true; } @Override public void stop() { wrappedApplet.stop(); active = false; } @Override public void destroy() { wrappedApplet.destroy(); } @Override public void appletResize(int width, int height) { wrappedApplet.resize(width, height); } @Override public void setVisible(boolean visible) { super.setVisible(visible); wrappedApplet.setVisible(visible); } @Override public void paint(Graphics graphics) {} @Override public void update(Graphics graphics) {} public void setParameter(String key, String value) { params.put(key, value); } public void setParameter(String key, boolean value) { setParameter(key, value ? "true" : "false"); } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/0000755000175100017510000000000015144136757021604 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/0000755000175100017510000000000015144136757024460 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/0000755000175100017510000000000015144136757025724 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyLauncher.java0000644000175100017510000001372015144136757031460 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 flow * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 solonovamax * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.legacy; import org.prismlauncher.launcher.impl.AbstractLauncher; import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.ReflectionUtils; import org.prismlauncher.utils.logging.Log; import java.applet.Applet; import java.io.File; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Collections; import java.util.List; /** * Used to launch old versions which support applets. */ final class LegacyLauncher extends AbstractLauncher { private final String user, session; private final String title; private final String appletClass; private final boolean useApplet; private final String gameDir; public LegacyLauncher(Parameters params) { super(params); user = params.getString("userName"); session = params.getString("sessionId"); title = params.getString("windowTitle", "Minecraft"); appletClass = params.getString("appletClass", "net.minecraft.client.MinecraftApplet"); List traits = params.getList("traits", Collections.emptyList()); useApplet = !traits.contains("noapplet"); gameDir = System.getProperty("user.dir"); } @Override public void launch() throws Throwable { Class main = ClassLoader.getSystemClassLoader().loadClass(mainClassName); Field gameDirField = findMinecraftGameDirField(main); if (gameDirField != null) { gameDirField.setAccessible(true); gameDirField.set(null, new File(gameDir)); } if (useApplet) { System.setProperty("minecraft.applet.TargetDirectory", gameDir); try { LegacyFrame window = new LegacyFrame(title, createAppletClass(appletClass)); window.start(user, session, width, height, maximize, serverAddress, serverPort, gameArgs.contains("--demo")); return; } catch (Throwable e) { Log.error("Running applet wrapper failed with exception; falling back to main class", e); } } // find and invoke the main method, this time without size parameters - in all // versions that support applets, these are ignored MethodHandle method = ReflectionUtils.findMainMethod(main); method.invokeExact(gameArgs.toArray(new String[0])); } private static Applet createAppletClass(String clazz) throws Throwable { Class appletClass = ClassLoader.getSystemClassLoader().loadClass(clazz); MethodHandle appletConstructor = MethodHandles.lookup().findConstructor(appletClass, MethodType.methodType(void.class)); return (Applet) appletConstructor.invoke(); } private static Field findMinecraftGameDirField(Class clazz) { // search for private static File for (Field field : clazz.getDeclaredFields()) { if (field.getType() != File.class) continue; int fieldModifiers = field.getModifiers(); if (!Modifier.isStatic(fieldModifiers)) continue; if (!Modifier.isPrivate(fieldModifiers)) continue; if (Modifier.isFinal(fieldModifiers)) continue; return field; } return null; } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/fix/0000755000175100017510000000000015144136757026512 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/0000755000175100017510000000000015144136757027776 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/SkinFix.java0000644000175100017510000001547215144136757032225 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.legacy.fix.online; import org.prismlauncher.legacy.utils.api.MojangApi; import org.prismlauncher.legacy.utils.api.Texture; import org.prismlauncher.legacy.utils.url.ByteArrayUrlConnection; import org.prismlauncher.legacy.utils.url.UrlUtils; import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; import javax.imageio.ImageIO; final class SkinFix { static URLConnection openConnection(URL address, Proxy proxy) throws IOException { String skinOwner = findSkinOwner(address); if (skinOwner != null) // we need to correct the skin return getSkinConnection(skinOwner, proxy); String capeOwner = findCapeOwner(address); if (capeOwner != null) { // since we do not need to process the image, open a direct connection bypassing // Handler Texture texture = MojangApi.getTexture(MojangApi.getUuid(capeOwner), "CAPE"); if (texture == null) return null; return UrlUtils.openConnection(texture.getUrl(), proxy); } return null; } private static URLConnection getSkinConnection(String owner, Proxy proxy) throws IOException { Texture texture = MojangApi.getTexture(MojangApi.getUuid(owner), "SKIN"); if (texture == null) return null; URLConnection connection = UrlUtils.openConnection(texture.getUrl(), proxy); try (InputStream in = connection.getInputStream()) { // thank you ahnewark! // this is heavily based on // https://github.com/ahnewark/MineOnline/blob/4f4f86f9d051e0a6fd7ff0b95b2a05f7437683d7/src/main/java/gg/codie/mineonline/gui/textures/TextureHelper.java#L17 BufferedImage image = ImageIO.read(in); Graphics2D graphics = image.createGraphics(); graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); BufferedImage subimage; if (image.getHeight() > 32) { // flatten second layers subimage = image.getSubimage(0, 32, 56, 16); graphics.drawImage(subimage, 0, 16, null); } if (texture.isSlim()) { // convert slim to classic subimage = image.getSubimage(45, 16, 9, 16); graphics.drawImage(subimage, 46, 16, null); subimage = image.getSubimage(49, 16, 2, 4); graphics.drawImage(subimage, 50, 16, null); subimage = image.getSubimage(53, 20, 2, 12); graphics.drawImage(subimage, 54, 20, null); } graphics.dispose(); // crop the image - old versions disregard all secondary layers besides the hat ByteArrayOutputStream out = new ByteArrayOutputStream(); image = image.getSubimage(0, 0, 64, 32); ImageIO.write(image, "png", out); return new ByteArrayUrlConnection(out.toByteArray()); } } private static String findSkinOwner(URL address) { switch (address.getHost()) { case "www.minecraft.net": return stripIfPrefixed(address.getPath(), "/skin/"); case "s3.amazonaws.com": case "skins.minecraft.net": return stripIfPrefixed(address.getPath(), "/MinecraftSkins/"); } return null; } private static String findCapeOwner(URL address) { switch (address.getHost()) { case "www.minecraft.net": if (!address.getPath().equals("/cloak/get.jsp")) return null; return stripIfPrefixed(address.getQuery(), "user="); case "s3.amazonaws.com": case "skins.minecraft.net": return stripIfPrefixed(address.getPath(), "/MinecraftCloaks/"); } return null; } private static String stripIfPrefixed(String string, String prefix) { if (string != null && string.startsWith(prefix)) { string = string.substring(prefix.length()); if (string.endsWith(".png")) string = string.substring(0, string.lastIndexOf('.')); return string; } return null; } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/Handler.java0000644000175100017510000000516115144136757032221 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.fix.online; import org.prismlauncher.legacy.utils.url.UrlUtils; import java.io.IOException; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; final class Handler extends URLStreamHandler { @Override protected URLConnection openConnection(URL address) throws IOException { return openConnection(address, null); } @Override protected URLConnection openConnection(URL address, Proxy proxy) throws IOException { URLConnection result; // try various fixes... result = SkinFix.openConnection(address, proxy); if (result != null) return result; result = OnlineModeFix.openConnection(address, proxy); if (result != null) return result; // ...then give up and make the request directly return UrlUtils.openConnection(address, proxy); } } ././@LongLink0000644000000000000000000000014600000000000011604 Lustar rootrootPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineModeFix.javaPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineModeFix.jav0000644000175100017510000000575215144136757033211 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.fix.online; import org.prismlauncher.legacy.utils.url.UrlUtils; import java.io.IOException; import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; public final class OnlineModeFix { public static URLConnection openConnection(URL address, Proxy proxy) throws IOException { // we start with "http://www.minecraft.net/game/joinserver.jsp?user=..." if (!(address.getHost().equals("www.minecraft.net") && address.getPath().equals("/game/joinserver.jsp"))) return null; // change it to "https://session.minecraft.net/game/joinserver.jsp?user=..." // this seems to be the modern version of the same endpoint... // maybe Mojang planned to patch old versions of the game to use it // if it ever disappears this should be changed to use sessionserver.mojang.com/session/minecraft/join // which of course has a different usage requiring JSON serialisation... URL url; try { url = new URL("https", "session.minecraft.net", address.getPort(), address.getFile()); } catch (MalformedURLException e) { throw new AssertionError("url should be valid", e); } return UrlUtils.openConnection(url, proxy); } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java0000644000175100017510000000635415144136757033074 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.fix.online; import org.prismlauncher.legacy.utils.Base64; import org.prismlauncher.legacy.utils.url.UrlUtils; import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.logging.Log; import java.net.URL; import java.net.URLStreamHandler; import java.net.URLStreamHandlerFactory; /** * Fixes skins by redirecting to other URLs. * Thanks to MineOnline for the implementation from which this was inspired! * See https://github.com/ahnewark/MineOnline/tree/main/src/main/java/gg/codie/mineonline/protocol. * * @see {@link Handler} * @see {@link UrlUtils} */ public final class OnlineFixes implements URLStreamHandlerFactory { public static void apply(Parameters params) { if (!"true".equals(params.getString("onlineFixes", null))) return; if (!UrlUtils.isSupported() || !Base64.isSupported()) { Log.warning("Cannot access the necessary Java internals for skin fix"); Log.warning("Turning off online fixes in the settings will silence the warnings"); return; } try { URL.setURLStreamHandlerFactory(new OnlineFixes()); } catch (Error e) { Log.warning("Cannot apply skin fix: URLStreamHandlerFactory is already set"); Log.warning("Turning off online fixes in the settings will silence the warnings"); } } @Override public URLStreamHandler createURLStreamHandler(String protocol) { if ("http".equals(protocol)) return new Handler(); return null; } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyFrame.java0000644000175100017510000001513415144136757030752 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 flow * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.legacy; import org.prismlauncher.utils.logging.Log; import java.applet.Applet; import java.awt.Dimension; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.List; import javax.imageio.ImageIO; import javax.swing.JFrame; import net.minecraft.Launcher; final class LegacyFrame extends JFrame { private static final long serialVersionUID = 1L; private final Launcher launcher; public LegacyFrame(String title, Applet applet) { super(title); launcher = new Launcher(applet); applet.setStub(launcher); try { setIconImage(ImageIO.read(new File("icon.png"))); } catch (IOException e) { Log.error("Failed to read window icon", e); } addWindowListener(new ForceExitHandler()); } public void start( String user, String session, int width, int height, boolean maximize, String serverAddress, String serverPort, boolean demo) { // Implements support for launching in to multiplayer on classic servers using a // mpticket file generated by an external program and stored in the instance's // root folder. Path instanceFolder = Paths.get(".."); Path mpticket = instanceFolder.resolve("mpticket"); Path mpticketCorrupt = instanceFolder.resolve("mpticket.corrupt"); if (Files.exists(mpticket)) { try { List lines = Files.readAllLines(mpticket, StandardCharsets.UTF_8); if (lines.size() < 3) { Files.move(mpticket, mpticketCorrupt, StandardCopyOption.REPLACE_EXISTING); Log.warning("mpticket file is corrupted"); } else { // Assumes parameters are valid and in the correct order launcher.setParameter("server", lines.get(0)); launcher.setParameter("port", lines.get(1)); launcher.setParameter("mppass", lines.get(2)); } } catch (IOException e) { Log.error("Failed to read mpticket file", e); } } if (serverAddress != null) { launcher.setParameter("server", serverAddress); launcher.setParameter("port", serverPort); } launcher.setParameter("username", user); launcher.setParameter("sessionid", session); launcher.setParameter("stand-alone", true); // Show the quit button. This often doesn't seem to work. launcher.setParameter("haspaid", true); // Some old versions need this for world saves to work. launcher.setParameter("demo", demo); launcher.setParameter("fullscreen", false); add(launcher); launcher.setPreferredSize(new Dimension(width, height)); pack(); setLocationRelativeTo(null); setResizable(true); if (maximize) setExtendedState(MAXIMIZED_BOTH); validate(); launcher.init(); launcher.start(); setVisible(true); } private final class ForceExitHandler extends WindowAdapter { @Override public void windowClosing(WindowEvent event) { // FIXME better solution new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(30000L); } catch (InterruptedException e) { Log.error("Thread interrupted", e); } Log.warning("Forcing exit"); System.exit(0); } }).start(); if (launcher != null) { launcher.stop(); launcher.destroy(); } // old minecraft versions can hang without this >_< System.exit(0); } } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/0000755000175100017510000000000015144136757027064 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/0000755000175100017510000000000015144136757027635 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java0000644000175100017510000000755715144136757032363 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.utils.api; import org.prismlauncher.legacy.utils.Base64; import org.prismlauncher.legacy.utils.json.JsonParser; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.Map; /** * Basic wrapper for Mojang's Minecraft API. */ @SuppressWarnings("unchecked") public final class MojangApi { public static String getUuid(String username) throws IOException { try (InputStream in = new URL("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + username).openStream()) { Map map = (Map) JsonParser.parse(in); return (String) map.get("id"); } } public static Texture getTexture(String player, String id) throws IOException { Map map = getTextures(player); if (map != null) { map = (Map) map.get(id); if (map == null) return null; URL url = new URL((String) map.get("url")); boolean slim = false; if (id.equals("SKIN")) { map = (Map) map.get("metadata"); if (map != null && "slim".equals(map.get("model"))) slim = true; } return new Texture(url, slim); } return null; } public static Map getTextures(String player) throws IOException { try (InputStream profileIn = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + player).openStream()) { Map profile = (Map) JsonParser.parse(profileIn); for (Map property : (Iterable>) profile.get("properties")) { if (property.get("name").equals("textures")) { Map result = (Map) JsonParser.parse(new String(Base64.decode((String) property.get("value")))); result = (Map) result.get("textures"); return result; } } return null; } } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/Texture.java0000644000175100017510000000414315144136757032142 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.utils.api; import java.net.URL; /** * Represents a texture from the Mojang API. */ public final class Texture { private final URL url; private final boolean slim; public Texture(URL url, boolean slim) { this.url = url; this.slim = slim; } public URL getUrl() { return url; } public boolean isSlim() { return slim; } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/Base64.java0000644000175100017510000000672115144136757030761 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.utils; import org.prismlauncher.utils.logging.Log; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.nio.charset.StandardCharsets; /** * Uses Base64 with Java 8 or later, otherwise DatatypeConverter. In the latter * case, reflection is used to allow using newer compilers. */ public final class Base64 { private static boolean supported = true; private static MethodHandle legacy; static { try { Class.forName("java.util.Base64"); } catch (ClassNotFoundException e) { try { Class datatypeConverter = Class.forName("javax.xml.bind.DatatypeConverter"); legacy = MethodHandles.lookup().findStatic( datatypeConverter, "parseBase64Binary", MethodType.methodType(byte[].class, String.class)); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e1) { Log.error("Base64 not supported", e1); supported = false; } } } /** * Determines whether base64 is supported. * * @return true if base64 can be parsed */ public static boolean isSupported() { return supported; } public static byte[] decode(String input) { if (!isSupported()) throw new UnsupportedOperationException(); if (legacy == null) return java.util.Base64.getDecoder().decode(input.getBytes(StandardCharsets.UTF_8)); try { return (byte[]) legacy.invokeExact(input); } catch (Error | RuntimeException e) { throw e; } catch (Throwable e) { throw new Error(e); } } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/0000755000175100017510000000000015144136757030035 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParser.java0000644000175100017510000003013115144136757032764 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.utils.json; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A lightweight portable JSON parser used instead of GSON since it is not * available in a lot of versions. */ public final class JsonParser { private final Reader in; private char[] buffer; private int pos, length; public static Object parse(String in) throws IOException { return parse(new StringReader(in)); } public static Object parse(InputStream in) throws IOException { return parse(new InputStreamReader(in, StandardCharsets.UTF_8)); } public static Object parse(Reader in) throws IOException { return new JsonParser(in).readSingleValue(); } private JsonParser(Reader in) throws IOException { this.in = in; pos = length = 0; read(); } private int character() { if (length == -1) return -1; return buffer[pos]; } private int read() throws IOException { if (length == -1) return -1; if (buffer == null || pos++ == length - 1) { pos = 0; buffer = new char[8192]; length = in.read(buffer); } return character(); } private void assertCharacter(char character) throws JsonParseException { if (character() != character) throw new JsonParseException( "Expected '" + character + "' but got " + (character() != -1 ? ("'" + (char) character() + "'") : "EOF")); } private void assertNoEOF(String expected) throws JsonParseException { if (character() == -1) throw new JsonParseException("Expected " + expected + " but got EOF"); } private void skipWhitespace() throws IOException { while (isWhitespace()) read(); } private boolean isWhitespace() { return character() == ' ' || character() == '\n' || character() == '\r' || character() == '\t'; } private Object readSingleValue() throws IOException { skipWhitespace(); Object result = readValue(); if (!(result instanceof Double)) read(); skipWhitespace(); if (character() != -1) throw new JsonParseException("Found trailing non-whitespace characters"); return result; } private Object readValue() throws IOException { assertNoEOF("a value"); int character = character(); switch (character) { case '{': return readObject(); case '[': return readArray(); case '"': return readString(); case 't': case 'f': // probably boolean Boolean bool = readBoolean(); if (bool != null) return bool; break; case 'n': // probably null if (readNull()) return null; break; } if (character == '-' || isDigit()) // probably a number return readNumber(); throw new JsonParseException("Expected a JSON value but got '" + (char) character + "'"); } private Map readObject() throws IOException { assertCharacter('{'); Map obj = new HashMap<>(); boolean comma = false; read(); skipWhitespace(); while (character() != '}') { if (comma) { assertCharacter(','); read(); skipWhitespace(); } String key = readString(); read(); skipWhitespace(); assertCharacter(':'); read(); skipWhitespace(); Object value = readValue(); obj.put(key, value); if (!(value instanceof Double)) read(); skipWhitespace(); comma = true; } return obj; } private List readArray() throws IOException { assertCharacter('['); List array = new ArrayList<>(); boolean comma = false; read(); skipWhitespace(); while (character() != ']') { if (comma) { assertCharacter(','); read(); skipWhitespace(); } Object value = readValue(); array.add(value); if (!(value instanceof Double)) read(); skipWhitespace(); comma = true; } return array; } private String readString() throws IOException { assertCharacter('"'); StringBuilder result = new StringBuilder(); while (read() != '"') { int character = character(); if (character >= '\u0000' && character <= '\u001F') throw new JsonParseException("Found unescaped control character within string"); switch (character) { case -1: throw new JsonParseException("Expected '\"' but got EOF"); case 0x7F: if (read() == '"') { return result.toString(); } continue; case '\\': int seq = read(); switch (seq) { case -1: throw new JsonParseException("Expected an escape sequence but got EOF"); case '\\': break; case '/': case '\"': character = seq; break; case 'b': character = '\b'; break; case 'f': character = '\f'; break; case 'n': character = '\n'; break; case 'r': character = '\r'; break; case 't': character = '\t'; break; case 'u': // char array to allow allocation in advance. char[] digits = new char[4]; for (int index = 0; index < digits.length; index++) { character = read(); if (index == 0 && character() == '-') { throw new JsonParseException("Hex sequence may not be negative"); } else if (character() == -1) { throw new JsonParseException("Expected a hex sequence but got EOF"); } digits[index] = (char) character; } String digitsString = new String(digits); try { character = Integer.parseInt(digitsString, 16); } catch (NumberFormatException e) { throw new JsonParseException("Could not parse hex sequence \"" + digitsString + "\""); } break; default: throw new JsonParseException("Invalid escape sequence: \\" + (char) seq); } break; } result.append((char) character); } return result.toString(); } private boolean isDigit() { return character() >= '0' && character() <= '9'; } private Double readNumber() throws IOException { StringBuilder result = new StringBuilder(); if (character() == '-') { result.append((char) character()); read(); } if (character() == '0') { result.append((char) character()); read(); if (isDigit()) throw new JsonParseException("Found superfluous leading zero"); } else if (!isDigit()) throw new JsonParseException("Expected digits"); while (character() != -1 && isDigit()) { result.append((char) character()); read(); } if (character() == '.') { result.append('.'); read(); assertNoEOF("digits"); if (!isDigit()) throw new JsonParseException("Expected digits after decimal point"); while (character() != -1 && isDigit()) { result.append((char) character()); read(); } } if (character() == 'e' || character() == 'E') { result.append('E'); read(); assertNoEOF("digits"); if (character() == '+' || character() == '-') { result.append((char) character()); read(); } if (!(character() == '+' || character() == '-' || isDigit())) throw new JsonParseException("Expected exponent digits"); while (character() != -1 && isDigit()) { result.append((char) character()); read(); } } String resultStr = result.toString(); try { return Double.parseDouble(resultStr); } catch (NumberFormatException e) { throw new JsonParseException("Failed to parse number '" + resultStr + "'"); } } private Boolean readBoolean() throws IOException { if (character() == 't') { if (read() == 'r' && read() == 'u' && read() == 'e') { return true; } } else if (character() == 'f' && read() == 'a' && read() == 'l' && read() == 's' && read() == 'e') { return false; } return null; } private boolean readNull() throws IOException { return character() == 'n' && read() == 'u' && read() == 'l' && read() == 'l'; } } ././@LongLink0000644000000000000000000000015300000000000011602 Lustar rootrootPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParseException.javaPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParseExceptio0000644000175100017510000000371215144136757033370 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.utils.json; import java.io.IOException; public final class JsonParseException extends IOException { private static final long serialVersionUID = 1L; public JsonParseException(String message) { super(message); } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/0000755000175100017510000000000015144136757027666 5ustar runnerrunner././@LongLink0000644000000000000000000000015600000000000011605 Lustar rootrootPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/ByteArrayUrlConnection.javaPrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/ByteArrayUrlConnec0000644000175100017510000000462515144136757033333 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.utils.url; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; public final class ByteArrayUrlConnection extends HttpURLConnection { private final InputStream in; public ByteArrayUrlConnection(byte[] data) { super(null); this.in = new ByteArrayInputStream(data); } @Override public void connect() throws IOException { responseCode = 200; } @Override public void disconnect() {} @Override public InputStream getInputStream() throws IOException { return in; } @Override public boolean usingProxy() { return false; } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java0000644000175100017510000001052515144136757032317 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.legacy.utils.url; import org.prismlauncher.utils.logging.Log; import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.net.Proxy; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; /** * A utility class for URLs which uses reflection to access constructors for * internal classes. */ public final class UrlUtils { private static URLStreamHandler http; private static MethodHandle openConnection; static { try { // we first obtain the stock URLStreamHandler for http as we overwrite it later Method getURLStreamHandler = URL.class.getDeclaredMethod("getURLStreamHandler", String.class); getURLStreamHandler.setAccessible(true); http = (URLStreamHandler) getURLStreamHandler.invoke(null, "http"); // we next find the openConnection method Method openConnectionReflect = URLStreamHandler.class.getDeclaredMethod("openConnection", URL.class, Proxy.class); openConnectionReflect.setAccessible(true); openConnection = MethodHandles.lookup().unreflect(openConnectionReflect); } catch (Throwable e) { Log.error("URL reflection failed - some features may not work", e); } } /** * Determines whether all the features of this class are available. * * @return true if all features can be used */ public static boolean isSupported() { return http != null && openConnection != null; } public static URLConnection openConnection(URL url, Proxy proxy) throws IOException { if (http == null) throw new UnsupportedOperationException(); if (url.getProtocol().equals("http")) return openConnection(http, url, proxy); // fall back to Java's default method // at this point, this should not cause a StackOverflowError unless we've missed // a protocol out from the if statements return url.openConnection(); } public static URLConnection openConnection(URLStreamHandler handler, URL url, Proxy proxy) throws IOException { if (openConnection == null) throw new UnsupportedOperationException(); try { return (URLConnection) openConnection.invokeExact(handler, url, proxy); } catch (IOException | Error | RuntimeException e) { throw e; // rethrow if possible } catch (Throwable e) { throw new AssertionError("openConnection should not throw", e); // oh dear! this isn't meant to happen } } } PrismLauncher-10.0.5/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyProxy.java0000644000175100017510000000560115144136757031037 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.legacy; import org.prismlauncher.launcher.Launcher; import org.prismlauncher.legacy.fix.online.OnlineFixes; import org.prismlauncher.utils.Parameters; // implementation of LegacyProxy public final class LegacyProxy { public static Launcher createLauncher(Parameters params) { return new LegacyLauncher(params); } public static void applyOnlineFixes(Parameters parameters) { OnlineFixes.apply(parameters); } } PrismLauncher-10.0.5/libraries/launcher/org/0000755000175100017510000000000015144136757020340 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/0000755000175100017510000000000015144136757023214 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/exception/0000755000175100017510000000000015144136757025212 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/exception/ParameterNotFoundException.java0000644000175100017510000000415615144136757033337 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 solonovamax * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.exception; public final class ParameterNotFoundException extends IllegalArgumentException { private static final long serialVersionUID = 1L; public ParameterNotFoundException(String key) { super(String.format("Required parameter '%s' was not found", key)); } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/exception/ParseException.java0000644000175100017510000000415615144136757031014 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 solonovamax * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.exception; public final class ParseException extends IllegalArgumentException { private static final long serialVersionUID = 1L; public ParseException(String input, String format) { super(String.format("For input '%s' - should match '%s'", input, format)); } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/SystemProperties.java0000644000175100017510000000326715144136757027430 0ustar runnerrunnerpackage org.prismlauncher; import org.prismlauncher.utils.Parameters; public final class SystemProperties { public static void apply(Parameters params) { String launcherBrand = params.getString("launcherBrand", null); String launcherVersion = params.getString("launcherVersion", null); String name = params.getString("instanceName", null); String iconId = params.getString("instanceIconKey", null); String iconPath = params.getString("instanceIconPath", null); String windowTitle = params.getString("windowTitle", null); String windowDimensions = params.getString("windowParams", null); if (launcherBrand != null) System.setProperty("minecraft.launcher.brand", launcherBrand); if (launcherVersion != null) System.setProperty("minecraft.launcher.version", launcherVersion); // set useful properties for mods if (name != null) System.setProperty("org.prismlauncher.instance.name", name); if (iconId != null) System.setProperty("org.prismlauncher.instance.icon.id", iconId); if (iconPath != null) System.setProperty("org.prismlauncher.instance.icon.path", iconPath); if (windowTitle != null) System.setProperty("org.prismlauncher.window.title", windowTitle); if (windowDimensions != null) System.setProperty("org.prismlauncher.window.dimensions", windowDimensions); // set multimc properties for compatibility if (name != null) System.setProperty("multimc.instance.title", name); if (iconId != null) System.setProperty("multimc.instance.icon", iconId); } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/legacy/0000755000175100017510000000000015144136757024460 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/legacy/LegacyProxy.java0000644000175100017510000000557215144136757027602 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.legacy; import org.prismlauncher.launcher.Launcher; import org.prismlauncher.utils.Parameters; // used as a fallback if NewLaunchLegacy is not on the classpath // if it is, this class will be replaced public final class LegacyProxy { public static Launcher createLauncher(Parameters params) { throw new AssertionError("NewLaunchLegacy is not loaded"); } public static void applyOnlineFixes(Parameters params) {} } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/utils/0000755000175100017510000000000015144136757024354 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java0000644000175100017510000000754415144136757030344 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 solonovamax * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.utils; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public final class ReflectionUtils { private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static final ClassLoader LOADER = ClassLoader.getSystemClassLoader(); /** * Gets the main method within a class. * * @param clazz The class * @return A method matching the descriptor of a main method * @throws ClassNotFoundException * @throws NoSuchMethodException * @throws IllegalAccessException */ public static MethodHandle findMainMethod(Class clazz) throws NoSuchMethodException, IllegalAccessException { return LOOKUP.findStatic(clazz, "main", MethodType.methodType(void.class, String[].class)); } /** * Gets the main method within a class by its name. * * @param clazz The class name * @return A method matching the descriptor of a main method * @throws ClassNotFoundException * @throws NoSuchMethodException * @throws IllegalAccessException */ public static MethodHandle findMainMethod(String clazz) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException { return findMainMethod(LOADER.loadClass(clazz)); } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/utils/Parameters.java0000644000175100017510000000770715144136757027335 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 solonovamax * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.utils; import org.prismlauncher.exception.ParameterNotFoundException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public final class Parameters { private final Map> map = new HashMap<>(); public void add(String key, String value) { List params = map.get(key); if (params == null) { params = new ArrayList<>(); map.put(key, params); } params.add(value); } public List getList(String key) throws ParameterNotFoundException { List params = map.get(key); if (params == null) throw new ParameterNotFoundException(key); return params; } public List getList(String key, List def) { List params = map.get(key); if (params == null || params.isEmpty()) return def; return params; } public String getString(String key) throws ParameterNotFoundException { List list = getList(key); if (list.isEmpty()) throw new ParameterNotFoundException(key); return list.get(0); } public String getString(String key, String def) { List params = map.get(key); if (params == null || params.isEmpty()) return def; return params.get(0); } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/utils/logging/0000755000175100017510000000000015144136757026002 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/utils/logging/Log.java0000644000175100017510000000703515144136757027373 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.utils.logging; import java.io.PrintStream; /** * Used to print messages with different levels used to colourise the output. * Used instead of a logging framework, as the launcher knows how to parse these * messages. */ public final class Log { // original before possibly overridden by MC private static final PrintStream OUT = new PrintStream(System.out), ERR = new PrintStream(System.err); private static final boolean DEBUG = Boolean.getBoolean("org.prismlauncher.debug"); public static void launcher(String message) { log(message, Level.LAUNCHER); } public static void error(String message) { log(message, Level.ERROR); } public static void debug(String message) { log(message, Level.DEBUG); } public static void warning(String message) { log(message, Level.WARNING); } public static void error(String message, Throwable e) { error(message); e.printStackTrace(ERR); } public static void fatal(String message) { log(message, Level.FATAL); } public static void fatal(String message, Throwable e) { fatal(message); e.printStackTrace(ERR); } /** * Logs a message with the prefix !![LEVEL]!. This is picked up by * the log viewer to give it nice colours. * * @param message The message * @param level The level */ public static void log(String message, Level level) { if (!DEBUG && level == Level.DEBUG) return; String prefix = "!![" + level.name + "]!"; // prefix first line message = prefix + message; // prefix subsequent lines message = message.replace("\n", "\n" + prefix); if (level.stderr) ERR.println(message); else OUT.println(message); } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/utils/logging/Level.java0000644000175100017510000000415215144136757027716 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.utils.logging; public enum Level { LAUNCHER("Launcher"), DEBUG("Debug"), INFO("Info"), MESSAGE("Message"), WARNING("Warning"), ERROR("Error", true), FATAL("Fatal", true); String name; boolean stderr; Level(String name) { this(name, false); } Level(String name, boolean stderr) { this.name = name; this.stderr = stderr; } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/EntryPoint.java0000644000175100017510000001402715144136757026176 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 solonovamax * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher; import org.prismlauncher.exception.ParseException; import org.prismlauncher.launcher.Launcher; import org.prismlauncher.launcher.impl.StandardLauncher; import org.prismlauncher.legacy.LegacyProxy; import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.logging.Log; import java.io.BufferedReader; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; public final class EntryPoint { public static void main(String[] args) { ExitCode code = listen(); if (code != ExitCode.NORMAL) { Log.fatal("Exiting with " + code); System.exit(code.numeric); } } private static ExitCode listen() { Parameters params = new Parameters(); PreLaunchAction action = PreLaunchAction.PROCEED; try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) { while (action == PreLaunchAction.PROCEED) { String line = reader.readLine(); if (line != null) action = parseLine(line, params); else action = PreLaunchAction.ABORT; } } catch (IllegalArgumentException e) { Log.fatal("Aborting due to wrong argument", e); return ExitCode.ILLEGAL_ARGUMENT; } catch (Throwable e) { Log.fatal("Aborting due to exception", e); return ExitCode.ABORT; } if (action == PreLaunchAction.ABORT) { Log.fatal("Launch aborted by the launcher"); return ExitCode.ABORT; } SystemProperties.apply(params); String launcherType = params.getString("launcher"); try { LegacyProxy.applyOnlineFixes(params); Launcher launcher; switch (launcherType) { case "standard": launcher = new StandardLauncher(params); break; case "legacy": launcher = LegacyProxy.createLauncher(params); break; default: throw new IllegalArgumentException("Invalid launcher type: " + launcherType); } launcher.launch(); return ExitCode.NORMAL; } catch (IllegalArgumentException e) { Log.fatal("Illegal argument", e); return ExitCode.ILLEGAL_ARGUMENT; } catch (Throwable e) { Log.fatal("Exception caught from launcher", e); return ExitCode.ERROR; } } private static PreLaunchAction parseLine(String input, Parameters params) throws ParseException { switch (input) { case "": return PreLaunchAction.PROCEED; case "launch": return PreLaunchAction.LAUNCH; case "abort": return PreLaunchAction.ABORT; default: String[] pair = input.split(" ", 2); if (pair.length != 2) throw new ParseException(input, "[key] [value]"); params.add(pair[0], pair[1]); return PreLaunchAction.PROCEED; } } private enum PreLaunchAction { PROCEED, LAUNCH, ABORT } private enum ExitCode { NORMAL(0), ABORT(1), ERROR(2), ILLEGAL_ARGUMENT(65); private final int numeric; ExitCode(int numeric) { this.numeric = numeric; } } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/launcher/0000755000175100017510000000000015144136757025015 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/launcher/Launcher.java0000644000175100017510000000364015144136757027424 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 solonovamax * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.prismlauncher.launcher; public interface Launcher { void launch() throws Throwable; } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/launcher/impl/0000755000175100017510000000000015144136757025756 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java0000644000175100017510000001170715144136757032051 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 solonovamax * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.launcher.impl; import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.ReflectionUtils; import java.lang.invoke.MethodHandle; import java.util.Collections; import java.util.List; public final class StandardLauncher extends AbstractLauncher { private final boolean quickPlayMultiplayerSupported; private final boolean quickPlaySingleplayerSupported; public StandardLauncher(Parameters params) { super(params); List traits = params.getList("traits", Collections.emptyList()); quickPlayMultiplayerSupported = traits.contains("feature:is_quick_play_multiplayer"); quickPlaySingleplayerSupported = traits.contains("feature:is_quick_play_singleplayer"); } @Override public void launch() throws Throwable { // window size, title and state gameArgs.add("--width"); gameArgs.add(Integer.toString(width)); gameArgs.add("--height"); gameArgs.add(Integer.toString(height)); if (serverAddress != null) { if (quickPlayMultiplayerSupported) { // as of 23w14a gameArgs.add("--quickPlayMultiplayer"); gameArgs.add(serverAddress + ':' + serverPort); } else { gameArgs.add("--server"); gameArgs.add(serverAddress); gameArgs.add("--port"); gameArgs.add(serverPort); } } else if (worldName != null && quickPlaySingleplayerSupported) { gameArgs.add("--quickPlaySingleplayer"); gameArgs.add(worldName); } StringBuilder joinedGameArgs = new StringBuilder(); for (String gameArg : gameArgs) { if (joinedGameArgs.length() > 0) { joinedGameArgs.append('\u001F'); // unit separator, designed for this purpose } joinedGameArgs.append(gameArg); } // pass the real main class and game arguments in so mods can access them System.setProperty("org.prismlauncher.launch.mainclass", mainClassName); // unit separator ('\u001F') delimited list of game args System.setProperty("org.prismlauncher.launch.gameargs", joinedGameArgs.toString()); // find and invoke the main method MethodHandle method = ReflectionUtils.findMainMethod(mainClassName); method.invokeExact(gameArgs.toArray(new String[0])); } } PrismLauncher-10.0.5/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java0000644000175100017510000001054315144136757032051 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 icelimetea * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 solonovamax * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Linking this library statically or dynamically with other modules is * making a combined work based on this library. Thus, the terms and * conditions of the GNU General Public License cover the whole * combination. * * As a special exception, the copyright holders of this library give * you permission to link this library with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also meet, * for each linked independent module, the terms and conditions of the * license of that module. An independent module is a module which is * not derived from or based on this library. If you modify this * library, you may extend this exception to your version of the * library, but you are not obliged to do so. If you do not wish to do * so, delete this exception statement from your version. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.prismlauncher.launcher.impl; import org.prismlauncher.exception.ParseException; import org.prismlauncher.launcher.Launcher; import org.prismlauncher.utils.Parameters; import java.util.ArrayList; import java.util.List; public abstract class AbstractLauncher implements Launcher { private static final int DEFAULT_WINDOW_WIDTH = 854, DEFAULT_WINDOW_HEIGHT = 480; // parameters, separated from ParamBucket protected final List gameArgs; // secondary parameters protected final int width, height; protected final boolean maximize; protected final String serverAddress, serverPort, worldName; protected final String mainClassName; protected AbstractLauncher(Parameters params) { gameArgs = params.getList("param", new ArrayList()); mainClassName = params.getString("mainClass", "net.minecraft.client.Minecraft"); serverAddress = params.getString("serverAddress", null); serverPort = params.getString("serverPort", null); worldName = params.getString("worldName", null); String windowParams = params.getString("windowParams", null); if ("maximized".equals(windowParams) || windowParams == null) { maximize = windowParams != null; width = DEFAULT_WINDOW_WIDTH; height = DEFAULT_WINDOW_HEIGHT; } else { maximize = false; String[] sizePair = windowParams.split("x", 2); if (sizePair.length == 2) { try { width = Integer.parseInt(sizePair[0]); height = Integer.parseInt(sizePair[1]); return; } catch (NumberFormatException ignored) { } } throw new ParseException(windowParams, "[width]x[height]"); } } } PrismLauncher-10.0.5/libraries/launcher/.gitignore0000644000175100017510000000005715144136757021543 0ustar runnerrunner.idea *.iml out .classpath .idea .project bin/ PrismLauncher-10.0.5/libraries/javacheck/0000755000175100017510000000000015144136757017667 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/javacheck/JavaCheck.java0000644000175100017510000000102715144136757022351 0ustar runnerrunnerpublic final class JavaCheck { private static final String[] CHECKED_PROPERTIES = new String[] {"os.arch", "java.version", "java.vendor"}; public static void main(String[] args) { int returnCode = 0; for (String key : CHECKED_PROPERTIES) { String property = System.getProperty(key); if (property != null) { System.out.println(key + "=" + property); } else { returnCode = 1; } } System.exit(returnCode); } } PrismLauncher-10.0.5/libraries/javacheck/CMakeLists.txt0000644000175100017510000000055015144136757022427 0ustar runnerrunnercmake_minimum_required(VERSION 3.15) project(launcher Java) find_package(Java 1.7 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT JavaCheck) set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) set(SRC JavaCheck.java ) add_jar(JavaCheck ${SRC}) install_jar(JavaCheck "${JARS_DEST_DIR}") PrismLauncher-10.0.5/libraries/javacheck/.gitignore0000644000175100017510000000005215144136757021654 0ustar runnerrunner.idea *.iml out .classpath .idea .project PrismLauncher-10.0.5/libraries/murmur2/0000755000175100017510000000000015144136757017361 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/murmur2/CMakeLists.txt0000644000175100017510000000046315144136757022124 0ustar runnerrunnercmake_minimum_required(VERSION 3.15) project(murmur2) set(MURMUR_SOURCES src/MurmurHash2.h src/MurmurHash2.cpp ) add_library(Launcher_murmur2 STATIC ${MURMUR_SOURCES}) target_include_directories(Launcher_murmur2 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "src" ) generate_export_header(Launcher_murmur2) PrismLauncher-10.0.5/libraries/murmur2/src/0000755000175100017510000000000015144136757020150 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/murmur2/src/MurmurHash2.cpp0000644000175100017510000000566315144136757023043 0ustar runnerrunner//----------------------------------------------------------------------------- // MurmurHash2 was written by Austin Appleby, and is placed in the public // domain. The author hereby disclaims copyright to this source code. // // This was modified as to possibilitate it's usage incrementally. // Those modifications are also placed in the public domain, and the author of // such modifications hereby disclaims copyright to this source code. #include "MurmurHash2.h" namespace Murmur2 { // 'm' and 'r' are mixing constants generated offline. // They're not really 'magic', they just happen to work well. const uint32_t m = 0x5bd1e995; const int r = 24; uint32_t hash(Reader* file_stream, std::size_t buffer_size, std::function filter_out) { auto* buffer = new char[buffer_size]; char data[4]; int read = 0; uint32_t size = 0; // We need the size without the filtered out characters before actually calculating the hash, // to setup the initial value for the hash. do { read = file_stream->read(buffer, buffer_size); for (int i = 0; i < read; i++) { if (!filter_out(buffer[i])) size += 1; } } while (!file_stream->eof()); file_stream->goToBeginning(); int index = 0; // This forces a seed of 1. IncrementalHashInfo info{ (uint32_t)1 ^ size, (uint32_t)size }; do { read = file_stream->read(buffer, buffer_size); for (int i = 0; i < read; i++) { char c = buffer[i]; if (filter_out(c)) continue; data[index] = c; index = (index + 1) % 4; // Mix 4 bytes at a time into the hash if (index == 0) FourBytes_MurmurHash2(reinterpret_cast(&data), info); } } while (!file_stream->eof()); // Do one last bit shuffle in the hash FourBytes_MurmurHash2(reinterpret_cast(&data), info); delete[] buffer; return info.h; } void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev) { if (prev.len >= 4) { // Not the final mix uint32_t k = *reinterpret_cast(data); k *= m; k ^= k >> r; k *= m; prev.h *= m; prev.h ^= k; prev.len -= 4; } else { // The final mix // Handle the last few bytes of the input array switch (prev.len) { case 3: prev.h ^= data[2] << 16; /* fall through */ case 2: prev.h ^= data[1] << 8; /* fall through */ case 1: prev.h ^= data[0]; prev.h *= m; }; // Do a few final mixes of the hash to ensure the last few // bytes are well-incorporated. prev.h ^= prev.h >> 13; prev.h *= m; prev.h ^= prev.h >> 15; prev.len = 0; } } } // namespace Murmur2PrismLauncher-10.0.5/libraries/murmur2/src/MurmurHash2.h0000644000175100017510000000204215144136757022474 0ustar runnerrunner//----------------------------------------------------------------------------- // The original MurmurHash2 was written by Austin Appleby, and is placed in the // public domain. The author hereby disclaims copyright to this source code. // // This was modified as to possibilitate it's usage incrementally. // Those modifications are also placed in the public domain, and the author of // such modifications hereby disclaims copyright to this source code. #pragma once #include #include namespace Murmur2 { #define KiB 1024 #define MiB 1024 * KiB class Reader { public: virtual ~Reader() = default; virtual int read(char* s, int n) = 0; virtual bool eof() = 0; virtual void goToBeginning() = 0; }; uint32_t hash(Reader* file_stream, std::size_t buffer_size = 4 * MiB, std::function filter_out = [](char) { return false; }); struct IncrementalHashInfo { uint32_t h; uint32_t len; }; void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev); } // namespace Murmur2 PrismLauncher-10.0.5/libraries/libnbtplusplus/0000755000175100017510000000000015144136757021032 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/COPYING0000644000175100017510000010451315144136757022071 0ustar runnerrunner GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . PrismLauncher-10.0.5/libraries/libnbtplusplus/README.md0000644000175100017510000000072315144136757022313 0ustar runnerrunner# libnbt++ 2 libnbt++ is a free C++ library for Minecraft's file format Named Binary Tag (NBT). It can read and write compressed and uncompressed NBT files and provides a code interface for working with NBT data. ---------- libnbt++2 is a remake of the old libnbt++ library with the goal of making it more easily usable and fixing some problems. The old libnbt++ especially suffered from a very convoluted syntax and boilerplate code needed to work with NBT data. PrismLauncher-10.0.5/libraries/libnbtplusplus/COPYING.LESSER0000644000175100017510000001674315144136757023074 0ustar runnerrunner GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. PrismLauncher-10.0.5/libraries/libnbtplusplus/include/0000755000175100017510000000000015144136757022455 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/include/nbt_tags.h0000644000175100017510000000164015144136757024430 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "tag_primitive.h" #include "tag_string.h" #include "tag_array.h" #include "tag_list.h" #include "tag_compound.h" PrismLauncher-10.0.5/libraries/libnbtplusplus/include/tag_array.h0000644000175100017510000001634315144136757024606 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef TAG_ARRAY_H_INCLUDED #define TAG_ARRAY_H_INCLUDED #include "crtp_tag.h" #include "io/stream_reader.h" #include "io/stream_writer.h" #include #include #include namespace nbt { ///@cond namespace detail { ///Meta-struct that holds the tag_type value for a specific array type template struct get_array_type { static_assert(sizeof(T) != sizeof(T), "Invalid type paramter for tag_array, can only use byte or int"); }; template<> struct get_array_type : public std::integral_constant {}; template<> struct get_array_type : public std::integral_constant {}; template<> struct get_array_type : public std::integral_constant {}; } ///@cond /** * @brief Tag that contains an array of byte or int values * * Common class for tag_byte_array, tag_int_array and tag_long_array. */ template class tag_array final : public detail::crtp_tag> { public: //Iterator types typedef typename std::vector::iterator iterator; typedef typename std::vector::const_iterator const_iterator; ///The type of the contained values typedef T value_type; ///The type of the tag static constexpr tag_type type = detail::get_array_type::value; ///Constructs an empty array tag_array() {} ///Constructs an array with the given values tag_array(std::initializer_list init): data(init) {} tag_array(std::vector&& vec) noexcept: data(std::move(vec)) {} ///Returns a reference to the vector that contains the values std::vector& get() { return data; } const std::vector& get() const { return data; } /** * @brief Accesses a value by index with bounds checking * @throw std::out_of_range if the index is out of range */ T& at(size_t i) { return data.at(i); } T at(size_t i) const { return data.at(i); } /** * @brief Accesses a value by index * * No bounds checking is performed. */ T& operator[](size_t i) { return data[i]; } T operator[](size_t i) const { return data[i]; } ///Appends a value at the end of the array void push_back(T val) { data.push_back(val); } ///Removes the last element from the array void pop_back() { data.pop_back(); } ///Returns the number of values in the array size_t size() const { return data.size(); } ///Erases all values from the array. void clear() { data.clear(); } //Iterators iterator begin() { return data.begin(); } iterator end() { return data.end(); } const_iterator begin() const { return data.begin(); } const_iterator end() const { return data.end(); } const_iterator cbegin() const { return data.cbegin(); } const_iterator cend() const { return data.cend(); } void read_payload(io::stream_reader& reader) override; /** * @inheritdoc * @throw std::length_error if the array is too large for NBT */ void write_payload(io::stream_writer& writer) const override; private: std::vector data; }; template bool operator==(const tag_array& lhs, const tag_array& rhs) { return lhs.get() == rhs.get(); } template bool operator!=(const tag_array& lhs, const tag_array& rhs) { return !(lhs == rhs); } //Slightly different between byte_array and int_array //Reading template<> inline void tag_array::read_payload(io::stream_reader& reader) { int32_t length; reader.read_num(length); if(length < 0) reader.get_istr().setstate(std::ios::failbit); if(!reader.get_istr()) throw io::input_error("Error reading length of tag_byte_array"); data.resize(length); reader.get_istr().read(reinterpret_cast(data.data()), length); if(!reader.get_istr()) throw io::input_error("Error reading contents of tag_byte_array"); } template inline void tag_array::read_payload(io::stream_reader& reader) { int32_t length; reader.read_num(length); if(length < 0) reader.get_istr().setstate(std::ios::failbit); if(!reader.get_istr()) throw io::input_error("Error reading length of generic array tag"); data.clear(); data.reserve(length); for(T i = 0; i < length; ++i) { T val; reader.read_num(val); data.push_back(val); } if(!reader.get_istr()) throw io::input_error("Error reading contents of generic array tag"); } template<> inline void tag_array::read_payload(io::stream_reader& reader) { int32_t length; reader.read_num(length); if(length < 0) reader.get_istr().setstate(std::ios::failbit); if(!reader.get_istr()) throw io::input_error("Error reading length of tag_long_array"); data.clear(); data.reserve(length); for(int32_t i = 0; i < length; ++i) { int64_t val; reader.read_num(val); data.push_back(val); } if(!reader.get_istr()) throw io::input_error("Error reading contents of tag_long_array"); } //Writing template<> inline void tag_array::write_payload(io::stream_writer& writer) const { if(size() > io::stream_writer::max_array_len) { writer.get_ostr().setstate(std::ios::failbit); throw std::length_error("Byte array is too large for NBT"); } writer.write_num(static_cast(size())); writer.get_ostr().write(reinterpret_cast(data.data()), data.size()); } template inline void tag_array::write_payload(io::stream_writer& writer) const { if(size() > io::stream_writer::max_array_len) { writer.get_ostr().setstate(std::ios::failbit); throw std::length_error("Generic array is too large for NBT"); } writer.write_num(static_cast(size())); for(T i: data) writer.write_num(i); } template<> inline void tag_array::write_payload(io::stream_writer& writer) const { if(size() > io::stream_writer::max_array_len) { writer.get_ostr().setstate(std::ios::failbit); throw std::length_error("Long array is too large for NBT"); } writer.write_num(static_cast(size())); for(int64_t i: data) writer.write_num(i); } //Typedefs that should be used instead of the template tag_array. typedef tag_array tag_byte_array; typedef tag_array tag_int_array; typedef tag_array tag_long_array; } #endif // TAG_ARRAY_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/primitive_detail.h0000644000175100017510000000361415144136757026164 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef PRIMITIVE_DETAIL_H_INCLUDED #define PRIMITIVE_DETAIL_H_INCLUDED #include ///@cond namespace nbt { namespace detail { ///Meta-struct that holds the tag_type value for a specific primitive type template struct get_primitive_type { static_assert(sizeof(T) != sizeof(T), "Invalid type paramter for tag_primitive, can only use types that NBT uses"); }; template<> struct get_primitive_type : public std::integral_constant {}; template<> struct get_primitive_type : public std::integral_constant {}; template<> struct get_primitive_type : public std::integral_constant {}; template<> struct get_primitive_type : public std::integral_constant {}; template<> struct get_primitive_type : public std::integral_constant {}; template<> struct get_primitive_type : public std::integral_constant {}; } } ///@endcond #endif // PRIMITIVE_DETAIL_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/tag_primitive.h0000644000175100017510000000631515144136757025476 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef TAG_PRIMITIVE_H_INCLUDED #define TAG_PRIMITIVE_H_INCLUDED #include "crtp_tag.h" #include "primitive_detail.h" #include "io/stream_reader.h" #include "io/stream_writer.h" #include #include namespace nbt { /** * @brief Tag that contains an integral or floating-point value * * Common class for tag_byte, tag_short, tag_int, tag_long, tag_float and tag_double. */ template class tag_primitive final : public detail::crtp_tag> { public: ///The type of the value typedef T value_type; ///The type of the tag static constexpr tag_type type = detail::get_primitive_type::value; //Constructor constexpr tag_primitive(T val = 0) noexcept: value(val) {} //Getters operator T&() { return value; } constexpr operator T() const { return value; } constexpr T get() const { return value; } //Setters tag_primitive& operator=(T val) { value = val; return *this; } void set(T val) { value = val; } void read_payload(io::stream_reader& reader) override; void write_payload(io::stream_writer& writer) const override; private: T value; }; template bool operator==(const tag_primitive& lhs, const tag_primitive& rhs) { return lhs.get() == rhs.get(); } template bool operator!=(const tag_primitive& lhs, const tag_primitive& rhs) { return !(lhs == rhs); } //Typedefs that should be used instead of the template tag_primitive. typedef tag_primitive tag_byte; typedef tag_primitive tag_short; typedef tag_primitive tag_int; typedef tag_primitive tag_long; typedef tag_primitive tag_float; typedef tag_primitive tag_double; //Explicit instantiations template class NBT_EXPORT tag_primitive; template class NBT_EXPORT tag_primitive; template class NBT_EXPORT tag_primitive; template class NBT_EXPORT tag_primitive; template class NBT_EXPORT tag_primitive; template class NBT_EXPORT tag_primitive; template void tag_primitive::read_payload(io::stream_reader& reader) { reader.read_num(value); if(!reader.get_istr()) { std::ostringstream str; str << "Error reading tag_" << type; throw io::input_error(str.str()); } } template void tag_primitive::write_payload(io::stream_writer& writer) const { writer.write_num(value); } } #endif // TAG_PRIMITIVE_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/text/0000755000175100017510000000000015144136757023441 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/include/text/json_formatter.h0000644000175100017510000000241115144136757026644 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef JSON_FORMATTER_H_INCLUDED #define JSON_FORMATTER_H_INCLUDED #include "tagfwd.h" #include #include "nbt_export.h" namespace nbt { namespace text { /** * @brief Prints tags in a JSON-like syntax into a stream * * @todo Make it configurable and able to produce actual standard-conformant JSON */ class NBT_EXPORT json_formatter { public: json_formatter() {} void print(std::ostream& os, const tag& t) const; }; } } #endif // JSON_FORMATTER_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/make_unique.h0000644000175100017510000000221315144136757025127 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef MAKE_UNIQUE_H_INCLUDED #define MAKE_UNIQUE_H_INCLUDED #include namespace nbt { ///Creates a new object of type T and returns a std::unique_ptr to it template std::unique_ptr make_unique(Args&&... args) { return std::unique_ptr(new T(std::forward(args)...)); } } #endif // MAKE_UNIQUE_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/value_initializer.h0000644000175100017510000000462315144136757026352 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef VALUE_INITIALIZER_H_INCLUDED #define VALUE_INITIALIZER_H_INCLUDED #include "value.h" namespace nbt { /** * @brief Helper class for implicitly constructing value objects * * This type is a subclass of @ref value. However the only difference to value * is that this class has additional constructors which allow implicit * conversion of various types to value objects. These constructors are not * part of the value class itself because implicit conversions like this * (especially from @c tag&& to @c value) can cause problems and ambiguities * in some cases. * * value_initializer is especially useful as function parameter type, it will * allow convenient conversion of various values to tags on function call. * * As value_initializer objects are in no way different than value objects, * they can just be converted to value after construction. */ class NBT_EXPORT value_initializer : public value { public: value_initializer(std::unique_ptr&& t) noexcept: value(std::move(t)) {} value_initializer(std::nullptr_t) noexcept : value(nullptr) {} value_initializer(value&& val) noexcept : value(std::move(val)) {} value_initializer(tag&& t) : value(std::move(t)) {} value_initializer(int8_t val); value_initializer(int16_t val); value_initializer(int32_t val); value_initializer(int64_t val); value_initializer(float val); value_initializer(double val); value_initializer(const std::string& str); value_initializer(std::string&& str); value_initializer(const char* str); }; } #endif // VALUE_INITIALIZER_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/tagfwd.h0000644000175100017510000000275415144136757024112 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ /** @file * @brief Provides forward declarations for all tag classes */ #ifndef TAGFWD_H_INCLUDED #define TAGFWD_H_INCLUDED #include namespace nbt { class tag; template class tag_primitive; typedef tag_primitive tag_byte; typedef tag_primitive tag_short; typedef tag_primitive tag_int; typedef tag_primitive tag_long; typedef tag_primitive tag_float; typedef tag_primitive tag_double; class tag_string; template class tag_array; typedef tag_array tag_byte_array; typedef tag_array tag_int_array; typedef tag_array tag_long_array; class tag_list; class tag_compound; } #endif // TAGFWD_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/tag_compound.h0000644000175100017510000001144415144136757025311 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef TAG_COMPOUND_H_INCLUDED #define TAG_COMPOUND_H_INCLUDED #include "crtp_tag.h" #include "value_initializer.h" #include #include namespace nbt { ///Tag that contains multiple unordered named tags of arbitrary types class NBT_EXPORT tag_compound final : public detail::crtp_tag { typedef std::map map_t_; public: //Iterator types typedef map_t_::iterator iterator; typedef map_t_::const_iterator const_iterator; ///The type of the tag static constexpr tag_type type = tag_type::Compound; ///Constructs an empty compound tag_compound() {} ///Constructs a compound with the given key-value pairs tag_compound(std::initializer_list> init); /** * @brief Accesses a tag by key with bounds checking * * Returns a value to the tag with the specified key, or throws an * exception if it does not exist. * @throw std::out_of_range if given key does not exist */ value& at(const std::string& key); const value& at(const std::string& key) const; /** * @brief Accesses a tag by key * * Returns a value to the tag with the specified key. If it does not exist, * creates a new uninitialized entry under the key. */ value& operator[](const std::string& key) { return tags[key]; } /** * @brief Inserts or assigns a tag * * If the given key already exists, assigns the tag to it. * Otherwise, it is inserted under the given key. * @return a pair of the iterator to the value and a bool indicating * whether the key did not exist */ std::pair put(const std::string& key, value_initializer&& val); /** * @brief Inserts a tag if the key does not exist * @return a pair of the iterator to the value with the key and a bool * indicating whether the value was actually inserted */ std::pair insert(const std::string& key, value_initializer&& val); /** * @brief Constructs and assigns or inserts a tag into the compound * * Constructs a new tag of type @c T with the given args and inserts * or assigns it to the given key. * @note Unlike std::map::emplace, this will overwrite existing values * @return a pair of the iterator to the value and a bool indicating * whether the key did not exist */ template std::pair emplace(const std::string& key, Args&&... args); /** * @brief Erases a tag from the compound * @return true if a tag was erased */ bool erase(const std::string& key); ///Returns true if the given key exists in the compound bool has_key(const std::string& key) const; ///Returns true if the given key exists and the tag has the given type bool has_key(const std::string& key, tag_type type) const; ///Returns the number of tags in the compound size_t size() const { return tags.size(); } ///Erases all tags from the compound void clear() { tags.clear(); } //Iterators iterator begin() { return tags.begin(); } iterator end() { return tags.end(); } const_iterator begin() const { return tags.begin(); } const_iterator end() const { return tags.end(); } const_iterator cbegin() const { return tags.cbegin(); } const_iterator cend() const { return tags.cend(); } void read_payload(io::stream_reader& reader) override; void write_payload(io::stream_writer& writer) const override; friend bool operator==(const tag_compound& lhs, const tag_compound& rhs) { return lhs.tags == rhs.tags; } friend bool operator!=(const tag_compound& lhs, const tag_compound& rhs) { return !(lhs == rhs); } private: map_t_ tags; }; template std::pair tag_compound::emplace(const std::string& key, Args&&... args) { return put(key, value(make_unique(std::forward(args)...))); } } #endif // TAG_COMPOUND_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/io/0000755000175100017510000000000015144136757023064 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/include/io/stream_reader.h0000644000175100017510000001002515144136757026050 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef STREAM_READER_H_INCLUDED #define STREAM_READER_H_INCLUDED #include "endian_str.h" #include "tag.h" #include "tag_compound.h" #include #include #include #include namespace nbt { namespace io { ///Exception that gets thrown when reading is not successful class NBT_EXPORT input_error : public std::runtime_error { using std::runtime_error::runtime_error; }; /** * @brief Reads a named tag from the stream, making sure that it is a compound * @param is the stream to read from * @param e the byte order of the source data. The Java edition * of Minecraft uses Big Endian, the Pocket edition uses Little Endian * @throw input_error on failure, or if the tag in the stream is not a compound */ NBT_EXPORT std::pair> read_compound(std::istream& is, endian::endian e = endian::big); /** * @brief Reads a named tag from the stream * @param is the stream to read from * @param e the byte order of the source data. The Java edition * of Minecraft uses Big Endian, the Pocket edition uses Little Endian * @throw input_error on failure */ NBT_EXPORT std::pair> read_tag(std::istream& is, endian::endian e = endian::big); /** * @brief Helper class for reading NBT tags from input streams * * Can be reused to read multiple tags */ class NBT_EXPORT stream_reader { public: /** * @param is the stream to read from * @param e the byte order of the source data. The Java edition * of Minecraft uses Big Endian, the Pocket edition uses Little Endian */ explicit stream_reader(std::istream& is, endian::endian e = endian::big) noexcept; ///Returns the stream std::istream& get_istr() const; ///Returns the byte order endian::endian get_endian() const; /** * @brief Reads a named tag from the stream, making sure that it is a compound * @throw input_error on failure, or if the tag in the stream is not a compound */ std::pair> read_compound(); /** * @brief Reads a named tag from the stream * @throw input_error on failure */ std::pair> read_tag(); /** * @brief Reads a tag of the given type without name from the stream * @throw input_error on failure */ std::unique_ptr read_payload(tag_type type); /** * @brief Reads a tag type from the stream * @param allow_end whether to consider tag_type::End valid * @throw input_error on failure */ tag_type read_type(bool allow_end = false); /** * @brief Reads a binary number from the stream * * On failure, will set the failbit on the stream. */ template void read_num(T& x); /** * @brief Reads an NBT string from the stream * * An NBT string consists of two bytes indicating the length, followed by * the characters encoded in modified UTF-8. * @throw input_error on failure */ std::string read_string(); private: std::istream& is; int depth = 0; const endian::endian endian; }; template void stream_reader::read_num(T& x) { endian::read(is, x, endian); } } } #endif // STREAM_READER_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/io/izlibstream.h0000644000175100017510000000566315144136757025574 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef IZLIBSTREAM_H_INCLUDED #define IZLIBSTREAM_H_INCLUDED #include "io/zlib_streambuf.h" #include #include namespace zlib { /** * @brief Stream buffer used by zlib::izlibstream * @sa izlibstream */ class NBT_EXPORT inflate_streambuf : public zlib_streambuf { public: /** * @param input the istream to wrap * @param bufsize the size of the internal buffers * @param window_bits the base two logarithm of the maximum window size that * zlib will use. * This parameter also determines which type of input to expect. * The default argument will autodetect between zlib and gzip data. * Refer to the zlib documentation of inflateInit2 for more details. * * @throw zlib_error if zlib encounters a problem during initialization */ explicit inflate_streambuf(std::istream& input, size_t bufsize = 32768, int window_bits = 32 + 15); ~inflate_streambuf() noexcept; ///@return the wrapped istream std::istream& get_istr() const { return is; } private: std::istream& is; bool stream_end; int_type underflow() override; }; /** * @brief An istream adapter that decompresses data using zlib * * This istream wraps another istream. The izlibstream will read compressed * data from the wrapped istream and inflate (decompress) it with zlib. * * @note If you want to read more data from the wrapped istream after the end * of the compressed data, then it must allow seeking. It is unavoidable for * the izlibstream to consume more data after the compressed data. * It will automatically attempt to seek the wrapped istream back to the point * after the end of the compressed data. * @sa inflate_streambuf */ class NBT_EXPORT izlibstream : public std::istream { public: /** * @param input the istream to wrap * @param bufsize the size of the internal buffers */ explicit izlibstream(std::istream& input, size_t bufsize = 32768): std::istream(&buf), buf(input, bufsize) {} ///@return the wrapped istream std::istream& get_istr() const { return buf.get_istr(); } private: inflate_streambuf buf; }; } #endif // IZLIBSTREAM_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/io/ozlibstream.h0000644000175100017510000000601315144136757025570 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef OZLIBSTREAM_H_INCLUDED #define OZLIBSTREAM_H_INCLUDED #include "io/zlib_streambuf.h" #include #include namespace zlib { /** * @brief Stream buffer used by zlib::ozlibstream * @sa ozlibstream */ class NBT_EXPORT deflate_streambuf : public zlib_streambuf { public: /** * @param output the ostream to wrap * @param bufsize the size of the internal buffers * @param level the compression level, ranges from 0 to 9, or -1 for default * * Refer to the zlib documentation of deflateInit2 for details about the arguments. * * @throw zlib_error if zlib encounters a problem during initialization */ explicit deflate_streambuf(std::ostream& output, size_t bufsize = 32768, int level = Z_DEFAULT_COMPRESSION, int window_bits = 15, int mem_level = 8, int strategy = Z_DEFAULT_STRATEGY); ~deflate_streambuf() noexcept; ///@return the wrapped ostream std::ostream& get_ostr() const { return os; } ///Finishes compression and writes all pending data to the output void close(); private: std::ostream& os; void deflate_chunk(int flush = Z_NO_FLUSH); int_type overflow(int_type ch) override; int sync() override; }; /** * @brief An ostream adapter that compresses data using zlib * * This ostream wraps another ostream. Data written to an ozlibstream will be * deflated (compressed) with zlib and written to the wrapped ostream. * * @sa deflate_streambuf */ class NBT_EXPORT ozlibstream : public std::ostream { public: /** * @param output the ostream to wrap * @param level the compression level, ranges from 0 to 9, or -1 for default * @param gzip if true, the output will be in gzip format rather than zlib * @param bufsize the size of the internal buffers */ explicit ozlibstream(std::ostream& output, int level = Z_DEFAULT_COMPRESSION, bool gzip = false, size_t bufsize = 32768): std::ostream(&buf), buf(output, bufsize, level, 15 + (gzip ? 16 : 0)) {} ///@return the wrapped ostream std::ostream& get_ostr() const { return buf.get_ostr(); } ///Finishes compression and writes all pending data to the output void close(); private: deflate_streambuf buf; }; } #endif // OZLIBSTREAM_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/io/zlib_streambuf.h0000644000175100017510000000171515144136757026251 0ustar runnerrunner#ifndef ZLIB_STREAMBUF_H_INCLUDED #define ZLIB_STREAMBUF_H_INCLUDED #include #include #include #include #include "nbt_export.h" namespace zlib { ///Exception thrown in case zlib encounters a problem class NBT_EXPORT zlib_error : public std::runtime_error { public: const int errcode; zlib_error(const char* msg, int errcode): std::runtime_error(msg ? std::string(zError(errcode)) + ": " + msg : zError(errcode)), errcode(errcode) {} }; ///Base class for deflate_streambuf and inflate_streambuf class zlib_streambuf : public std::streambuf { protected: std::vector in; std::vector out; z_stream zstr; explicit zlib_streambuf(size_t bufsize): in(bufsize), out(bufsize) { zstr.zalloc = Z_NULL; zstr.zfree = Z_NULL; zstr.opaque = Z_NULL; } }; } #endif // ZLIB_STREAMBUF_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/io/stream_writer.h0000644000175100017510000000670615144136757026135 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef STREAM_WRITER_H_INCLUDED #define STREAM_WRITER_H_INCLUDED #include "tag.h" #include "endian_str.h" #include #include namespace nbt { namespace io { /* Not sure if that is even needed ///Exception that gets thrown when writing is not successful class output_error : public std::runtime_error { using std::runtime_error::runtime_error; };*/ /** * @brief Writes a named tag into the stream, including the tag type * @param key the name of the tag * @param t the tag * @param os the stream to write to * @param e the byte order of the written data. The Java edition * of Minecraft uses Big Endian, the Pocket edition uses Little Endian */ NBT_EXPORT void write_tag(const std::string& key, const tag& t, std::ostream& os, endian::endian e = endian::big); /** * @brief Helper class for writing NBT tags to output streams * * Can be reused to write multiple tags */ class NBT_EXPORT stream_writer { public: ///Maximum length of an NBT string (16 bit unsigned) static constexpr size_t max_string_len = UINT16_MAX; ///Maximum length of an NBT list or array (32 bit signed) static constexpr uint32_t max_array_len = INT32_MAX; /** * @param os the stream to write to * @param e the byte order of the written data. The Java edition * of Minecraft uses Big Endian, the Pocket edition uses Little Endian */ explicit stream_writer(std::ostream& os, endian::endian e = endian::big) noexcept: os(os), endian(e) {} ///Returns the stream std::ostream& get_ostr() const { return os; } ///Returns the byte order endian::endian get_endian() const { return endian; } /** * @brief Writes a named tag into the stream, including the tag type */ void write_tag(const std::string& key, const tag& t); /** * @brief Writes the given tag's payload into the stream */ void write_payload(const tag& t) { t.write_payload(*this); } /** * @brief Writes a tag type to the stream */ void write_type(tag_type tt) { write_num(static_cast(tt)); } /** * @brief Writes a binary number to the stream */ template void write_num(T x); /** * @brief Writes an NBT string to the stream * * An NBT string consists of two bytes indicating the length, followed by * the characters encoded in modified UTF-8. * @throw std::length_error if the string is too long for NBT */ void write_string(const std::string& str); private: std::ostream& os; const endian::endian endian; }; template void stream_writer::write_num(T x) { endian::write(os, x, endian); } } } #endif // STREAM_WRITER_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/value.h0000644000175100017510000001675215144136757023755 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef TAG_REF_PROXY_H_INCLUDED #define TAG_REF_PROXY_H_INCLUDED #include "tag.h" #include #include namespace nbt { /** * @brief Contains an NBT value of fixed type * * This class is a convenience wrapper for @c std::unique_ptr. * A value can contain any kind of tag or no tag (nullptr) and provides * operations for handling tags of which the type is not known at compile time. * Assignment or the set method on a value with no tag will fill in the value. * * The rationale for the existance of this class is to provide a type-erasured * means of storing tags, especially when they are contained in tag_compound * or tag_list. The alternative would be directly using @c std::unique_ptr * and @c tag&, which is how it was done in libnbt++1. The main drawback is that * it becomes very cumbersome to deal with tags of unknown type. * * For example, in this case it would not be possible to allow a syntax like * compound["foo"] = 42. If the key "foo" does not exist beforehand, * the left hand side could not have any sensible value if it was of type * @c tag&. * Firstly, the compound tag would have to create a new tag_int there, but it * cannot know that the new tag is going to be assigned an integer. * Also, if the type was @c tag& and it allowed assignment of integers, that * would mean the tag base class has assignments and conversions like this. * Which means that all other tag classes would inherit them from the base * class, even though it does not make any sense to allow converting a * tag_compound into an integer. Attempts like this should be caught at * compile time. * * This is why all the syntactic sugar for tags is contained in the value class * while the tag class only contains common operations for all tag types. */ class NBT_EXPORT value { public: //Constructors value() noexcept {} explicit value(std::unique_ptr&& t) noexcept: tag_(std::move(t)) {} explicit value(tag&& t); //Moving value(value&&) noexcept = default; value& operator=(value&&) noexcept = default; //Copying explicit value(const value& rhs); value& operator=(const value& rhs); /** * @brief Assigns the given value to the tag if the type matches * @throw std::bad_cast if the type of @c t is not the same as the type * of this value */ value& operator=(tag&& t); void set(tag&& t); //Conversion to tag /** * @brief Returns the contained tag * * If the value is uninitialized, the behavior is undefined. */ operator tag&() { return get(); } operator const tag&() const { return get(); } tag& get() { return *tag_; } const tag& get() const { return *tag_; } /** * @brief Returns a reference to the contained tag as an instance of T * @throw std::bad_cast if the tag is not of type T */ template T& as(); template const T& as() const; //Assignment of primitives and string /** * @brief Assigns the given value to the tag if the type is compatible * @throw std::bad_cast if the value is not convertible to the tag type * via a widening conversion */ value& operator=(int8_t val); value& operator=(int16_t val); value& operator=(int32_t val); value& operator=(int64_t val); value& operator=(float val); value& operator=(double val); /** * @brief Assigns the given string to the tag if it is a tag_string * @throw std::bad_cast if the contained tag is not a tag_string */ value& operator=(const std::string& str); value& operator=(std::string&& str); //Conversions to primitives and string /** * @brief Returns the contained value if the type is compatible * @throw std::bad_cast if the tag type is not convertible to the desired * type via a widening conversion */ explicit operator int8_t() const; explicit operator int16_t() const; explicit operator int32_t() const; explicit operator int64_t() const; explicit operator float() const; explicit operator double() const; /** * @brief Returns the contained string if the type is tag_string * * If the value is uninitialized, the behavior is undefined. * @throw std::bad_cast if the tag type is not tag_string */ explicit operator const std::string&() const; ///Returns true if the value is not uninitialized explicit operator bool() const { return tag_ != nullptr; } /** * @brief In case of a tag_compound, accesses a tag by key with bounds checking * * If the value is uninitialized, the behavior is undefined. * @throw std::bad_cast if the tag type is not tag_compound * @throw std::out_of_range if given key does not exist * @sa tag_compound::at */ value& at(const std::string& key); const value& at(const std::string& key) const; /** * @brief In case of a tag_compound, accesses a tag by key * * If the value is uninitialized, the behavior is undefined. * @throw std::bad_cast if the tag type is not tag_compound * @sa tag_compound::operator[] */ value& operator[](const std::string& key); value& operator[](const char* key); //need this overload because of conflict with built-in operator[] /** * @brief In case of a tag_list, accesses a tag by index with bounds checking * * If the value is uninitialized, the behavior is undefined. * @throw std::bad_cast if the tag type is not tag_list * @throw std::out_of_range if the index is out of range * @sa tag_list::at */ value& at(size_t i); const value& at(size_t i) const; /** * @brief In case of a tag_list, accesses a tag by index * * No bounds checking is performed. If the value is uninitialized, the * behavior is undefined. * @throw std::bad_cast if the tag type is not tag_list * @sa tag_list::operator[] */ value& operator[](size_t i); const value& operator[](size_t i) const; ///Returns a reference to the underlying std::unique_ptr std::unique_ptr& get_ptr() { return tag_; } const std::unique_ptr& get_ptr() const { return tag_; } ///Resets the underlying std::unique_ptr to a different value void set_ptr(std::unique_ptr&& t) { tag_ = std::move(t); } ///@sa tag::get_type tag_type get_type() const; friend NBT_EXPORT bool operator==(const value& lhs, const value& rhs); friend NBT_EXPORT bool operator!=(const value& lhs, const value& rhs); private: std::unique_ptr tag_; }; template T& value::as() { return tag_->as(); } template const T& value::as() const { return tag_->as(); } } #endif // TAG_REF_PROXY_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/tag_list.h0000644000175100017510000001647315144136757024447 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef TAG_LIST_H_INCLUDED #define TAG_LIST_H_INCLUDED #include "crtp_tag.h" #include "tagfwd.h" #include "value_initializer.h" #include #include namespace nbt { /** * @brief Tag that contains multiple unnamed tags of the same type * * All the tags contained in the list have the same type, which can be queried * with el_type(). The types of the values contained in the list should not * be changed to mismatch the element type. * * If the list is empty, the type can be undetermined, in which case el_type() * will return tag_type::Null. The type will then be set when the first tag * is added to the list. */ class NBT_EXPORT tag_list final : public detail::crtp_tag { public: //Iterator types typedef std::vector::iterator iterator; typedef std::vector::const_iterator const_iterator; ///The type of the tag static constexpr tag_type type = tag_type::List; /** * @brief Constructs a list of type T with the given values * * Example: @code tag_list::of({3, 4, 5}) @endcode * @param init list of values from which the elements are constructed */ template static tag_list of(std::initializer_list init); /** * @brief Constructs an empty list * * The content type is determined when the first tag is added. */ tag_list(): tag_list(tag_type::Null) {} ///Constructs an empty list with the given content type explicit tag_list(tag_type type): el_type_(type) {} ///Constructs a list with the given contents tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); tag_list(std::initializer_list init); /** * @brief Constructs a list with the given contents * @throw std::invalid_argument if the tags are not all of the same type */ tag_list(std::initializer_list init); /** * @brief Accesses a tag by index with bounds checking * * Returns a value to the tag at the specified index, or throws an * exception if it is out of range. * @throw std::out_of_range if the index is out of range */ value& at(size_t i); const value& at(size_t i) const; /** * @brief Accesses a tag by index * * Returns a value to the tag at the specified index. No bounds checking * is performed. */ value& operator[](size_t i) { return tags[i]; } const value& operator[](size_t i) const { return tags[i]; } /** * @brief Assigns a value at the given index * @throw std::invalid_argument if the type of the value does not match the list's * content type * @throw std::out_of_range if the index is out of range */ void set(size_t i, value&& val); /** * @brief Appends the tag to the end of the list * @throw std::invalid_argument if the type of the tag does not match the list's * content type */ void push_back(value_initializer&& val); /** * @brief Constructs and appends a tag to the end of the list * @throw std::invalid_argument if the type of the tag does not match the list's * content type */ template void emplace_back(Args&&... args); ///Removes the last element of the list void pop_back() { tags.pop_back(); } ///Returns the content type of the list, or tag_type::Null if undetermined tag_type el_type() const { return el_type_; } ///Returns the number of tags in the list size_t size() const { return tags.size(); } ///Erases all tags from the list. Preserves the content type. void clear() { tags.clear(); } /** * @brief Erases all tags from the list and changes the content type. * @param type the new content type. Can be tag_type::Null to leave it undetermined. */ void reset(tag_type type = tag_type::Null); //Iterators iterator begin() { return tags.begin(); } iterator end() { return tags.end(); } const_iterator begin() const { return tags.begin(); } const_iterator end() const { return tags.end(); } const_iterator cbegin() const { return tags.cbegin(); } const_iterator cend() const { return tags.cend(); } /** * @inheritdoc * In case of a list of tag_end, the content type will be undetermined. */ void read_payload(io::stream_reader& reader) override; /** * @inheritdoc * In case of a list of undetermined content type, the written type will be tag_end. * @throw std::length_error if the list is too long for NBT */ void write_payload(io::stream_writer& writer) const override; /** * @brief Equality comparison for lists * * Lists are considered equal if their content types and the contained tags * are equal. */ friend NBT_EXPORT bool operator==(const tag_list& lhs, const tag_list& rhs); friend NBT_EXPORT bool operator!=(const tag_list& lhs, const tag_list& rhs); private: std::vector tags; tag_type el_type_; /** * Internally used initialization function that initializes the list with * tags of type T, with the constructor arguments of each T given by il. * @param il list of values that are, one by one, given to a constructor of T */ template void init(std::initializer_list il); }; template tag_list tag_list::of(std::initializer_list il) { tag_list result; result.init(il); return result; } template void tag_list::emplace_back(Args&&... args) { if(el_type_ == tag_type::Null) //set content type if undetermined el_type_ = T::type; else if(el_type_ != T::type) throw std::invalid_argument("The tag type does not match the list's content type"); tags.emplace_back(make_unique(std::forward(args)...)); } template void tag_list::init(std::initializer_list init) { el_type_ = T::type; tags.reserve(init.size()); for(const Arg& arg: init) tags.emplace_back(nbt::make_unique(arg)); } } #endif // TAG_LIST_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/crtp_tag.h0000644000175100017510000000407115144136757024433 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef CRTP_TAG_H_INCLUDED #define CRTP_TAG_H_INCLUDED #include "tag.h" #include "nbt_visitor.h" #include "make_unique.h" namespace nbt { namespace detail { template class crtp_tag : public tag { public: //Pure virtual destructor to make the class abstract virtual ~crtp_tag() noexcept = 0; tag_type get_type() const noexcept override final { return Sub::type; }; std::unique_ptr clone() const& override final { return make_unique(sub_this()); } std::unique_ptr move_clone() && override final { return make_unique(std::move(sub_this())); } tag& assign(tag&& rhs) override final { return sub_this() = dynamic_cast(rhs); } void accept(nbt_visitor& visitor) override final { visitor.visit(sub_this()); } void accept(const_nbt_visitor& visitor) const override final { visitor.visit(sub_this()); } private: bool equals(const tag& rhs) const override final { return sub_this() == static_cast(rhs); } Sub& sub_this() { return static_cast(*this); } const Sub& sub_this() const { return static_cast(*this); } }; template crtp_tag::~crtp_tag() noexcept {} } } #endif // CRTP_TAG_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/tag_string.h0000644000175100017510000000456415144136757025000 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef TAG_STRING_H_INCLUDED #define TAG_STRING_H_INCLUDED #include "crtp_tag.h" #include namespace nbt { ///Tag that contains a UTF-8 string class NBT_EXPORT tag_string final : public detail::crtp_tag { public: ///The type of the tag static constexpr tag_type type = tag_type::String; //Constructors tag_string() {} tag_string(const std::string& str): value(str) {} tag_string(std::string&& str) noexcept: value(std::move(str)) {} tag_string(const char* str): value(str) {} //Getters operator std::string&() { return value; } operator const std::string&() const { return value; } const std::string& get() const { return value; } //Setters tag_string& operator=(const std::string& str) { value = str; return *this; } tag_string& operator=(std::string&& str) { value = std::move(str); return *this; } tag_string& operator=(const char* str) { value = str; return *this; } void set(const std::string& str) { value = str; } void set(std::string&& str) { value = std::move(str); } void read_payload(io::stream_reader& reader) override; /** * @inheritdoc * @throw std::length_error if the string is too long for NBT */ void write_payload(io::stream_writer& writer) const override; private: std::string value; }; inline bool operator==(const tag_string& lhs, const tag_string& rhs) { return lhs.get() == rhs.get(); } inline bool operator!=(const tag_string& lhs, const tag_string& rhs) { return !(lhs == rhs); } } #endif // TAG_STRING_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/nbt_visitor.h0000644000175100017510000000461415144136757025175 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef NBT_VISITOR_H_INCLUDED #define NBT_VISITOR_H_INCLUDED #include "tagfwd.h" namespace nbt { /** * @brief Base class for visitors of tags * * Implementing the Visitor pattern */ class nbt_visitor { public: virtual ~nbt_visitor() noexcept = 0; //Abstract class virtual void visit(tag_byte&) {} virtual void visit(tag_short&) {} virtual void visit(tag_int&) {} virtual void visit(tag_long&) {} virtual void visit(tag_float&) {} virtual void visit(tag_double&) {} virtual void visit(tag_byte_array&) {} virtual void visit(tag_string&) {} virtual void visit(tag_list&) {} virtual void visit(tag_compound&) {} virtual void visit(tag_int_array&) {} virtual void visit(tag_long_array&) {} }; /** * @brief Base class for visitors of constant tags * * Implementing the Visitor pattern */ class const_nbt_visitor { public: virtual ~const_nbt_visitor() noexcept = 0; //Abstract class virtual void visit(const tag_byte&) {} virtual void visit(const tag_short&) {} virtual void visit(const tag_int&) {} virtual void visit(const tag_long&) {} virtual void visit(const tag_float&) {} virtual void visit(const tag_double&) {} virtual void visit(const tag_byte_array&) {} virtual void visit(const tag_string&) {} virtual void visit(const tag_list&) {} virtual void visit(const tag_compound&) {} virtual void visit(const tag_int_array&) {} virtual void visit(const tag_long_array&) {} }; inline nbt_visitor::~nbt_visitor() noexcept {} inline const_nbt_visitor::~const_nbt_visitor() noexcept {} } #endif // NBT_VISITOR_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/endian_str.h0000644000175100017510000001003315144136757024751 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef ENDIAN_STR_H_INCLUDED #define ENDIAN_STR_H_INCLUDED #include #include #include "nbt_export.h" /** * @brief Reading and writing numbers from and to streams * in binary format with different byte orders. */ namespace endian { enum endian { little, big }; ///Reads number from stream in specified endian template void read(std::istream& is, T& x, endian e); ///Reads number from stream in little endian NBT_EXPORT void read_little(std::istream& is, uint8_t& x); NBT_EXPORT void read_little(std::istream& is, uint16_t& x); NBT_EXPORT void read_little(std::istream& is, uint32_t& x); NBT_EXPORT void read_little(std::istream& is, uint64_t& x); NBT_EXPORT void read_little(std::istream& is, int8_t& x); NBT_EXPORT void read_little(std::istream& is, int16_t& x); NBT_EXPORT void read_little(std::istream& is, int32_t& x); NBT_EXPORT void read_little(std::istream& is, int64_t& x); NBT_EXPORT void read_little(std::istream& is, float& x); NBT_EXPORT void read_little(std::istream& is, double& x); ///Reads number from stream in big endian NBT_EXPORT void read_big(std::istream& is, uint8_t& x); NBT_EXPORT void read_big(std::istream& is, uint16_t& x); NBT_EXPORT void read_big(std::istream& is, uint32_t& x); NBT_EXPORT void read_big(std::istream& is, uint64_t& x); NBT_EXPORT void read_big(std::istream& is, int8_t& x); NBT_EXPORT void read_big(std::istream& is, int16_t& x); NBT_EXPORT void read_big(std::istream& is, int32_t& x); NBT_EXPORT void read_big(std::istream& is, int64_t& x); NBT_EXPORT void read_big(std::istream& is, float& x); NBT_EXPORT void read_big(std::istream& is, double& x); ///Writes number to stream in specified endian template void write(std::ostream& os, T x, endian e); ///Writes number to stream in little endian NBT_EXPORT void write_little(std::ostream& os, uint8_t x); NBT_EXPORT void write_little(std::ostream& os, uint16_t x); NBT_EXPORT void write_little(std::ostream& os, uint32_t x); NBT_EXPORT void write_little(std::ostream& os, uint64_t x); NBT_EXPORT void write_little(std::ostream& os, int8_t x); NBT_EXPORT void write_little(std::ostream& os, int16_t x); NBT_EXPORT void write_little(std::ostream& os, int32_t x); NBT_EXPORT void write_little(std::ostream& os, int64_t x); NBT_EXPORT void write_little(std::ostream& os, float x); NBT_EXPORT void write_little(std::ostream& os, double x); ///Writes number to stream in big endian NBT_EXPORT void write_big(std::ostream& os, uint8_t x); NBT_EXPORT void write_big(std::ostream& os, uint16_t x); NBT_EXPORT void write_big(std::ostream& os, uint32_t x); NBT_EXPORT void write_big(std::ostream& os, uint64_t x); NBT_EXPORT void write_big(std::ostream& os, int8_t x); NBT_EXPORT void write_big(std::ostream& os, int16_t x); NBT_EXPORT void write_big(std::ostream& os, int32_t x); NBT_EXPORT void write_big(std::ostream& os, int64_t x); NBT_EXPORT void write_big(std::ostream& os, float x); NBT_EXPORT void write_big(std::ostream& os, double x); template void read(std::istream& is, T& x, endian e) { if(e == little) read_little(is, x); else read_big(is, x); } template void write(std::ostream& os, T x, endian e) { if(e == little) write_little(os, x); else write_big(os, x); } } #endif // ENDIAN_STR_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/include/tag.h0000644000175100017510000001023315144136757023400 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #ifndef TAG_H_INCLUDED #define TAG_H_INCLUDED #include #include #include #include "nbt_export.h" namespace nbt { ///Tag type values used in the binary format enum class tag_type : int8_t { End = 0, Byte = 1, Short = 2, Int = 3, Long = 4, Float = 5, Double = 6, Byte_Array = 7, String = 8, List = 9, Compound = 10, Int_Array = 11, Long_Array = 12, Null = -1 ///< Used to denote empty @ref value s }; /** * @brief Returns whether the given number falls within the range of valid tag types * @param allow_end whether to consider tag_type::End (0) valid */ NBT_EXPORT bool is_valid_type(int type, bool allow_end = false); //Forward declarations class nbt_visitor; class const_nbt_visitor; namespace io { class stream_reader; class stream_writer; } ///Base class for all NBT tag classes class NBT_EXPORT tag { public: //Virtual destructor virtual ~tag() noexcept {} ///Returns the type of the tag virtual tag_type get_type() const noexcept = 0; //Polymorphic clone methods virtual std::unique_ptr clone() const& = 0; virtual std::unique_ptr move_clone() && = 0; std::unique_ptr clone() &&; /** * @brief Returns a reference to the tag as an instance of T * @throw std::bad_cast if the tag is not of type T */ template T& as(); template const T& as() const; /** * @brief Move-assigns the given tag if the class is the same * @throw std::bad_cast if @c rhs is not the same type as @c *this */ virtual tag& assign(tag&& rhs) = 0; /** * @brief Calls the appropriate overload of @c visit() on the visitor with * @c *this as argument * * Implementing the Visitor pattern */ virtual void accept(nbt_visitor& visitor) = 0; virtual void accept(const_nbt_visitor& visitor) const = 0; /** * @brief Reads the tag's payload from the stream * @throw io::stream_reader::input_error on failure */ virtual void read_payload(io::stream_reader& reader) = 0; /** * @brief Writes the tag's payload into the stream */ virtual void write_payload(io::stream_writer& writer) const = 0; /** * @brief Default-constructs a new tag of the given type * @throw std::invalid_argument if the type is not valid (e.g. End or Null) */ static std::unique_ptr create(tag_type type); friend NBT_EXPORT bool operator==(const tag& lhs, const tag& rhs); friend NBT_EXPORT bool operator!=(const tag& lhs, const tag& rhs); private: /** * @brief Checks for equality to a tag of the same type * @param rhs an instance of the same class as @c *this */ virtual bool equals(const tag& rhs) const = 0; }; ///Output operator for tag types NBT_EXPORT std::ostream& operator<<(std::ostream& os, tag_type tt); /** * @brief Output operator for tags * * Uses @ref text::json_formatter * @relates tag */ NBT_EXPORT std::ostream& operator<<(std::ostream& os, const tag& t); template T& tag::as() { static_assert(std::is_base_of::value, "T must be a subclass of tag"); return dynamic_cast(*this); } template const T& tag::as() const { static_assert(std::is_base_of::value, "T must be a subclass of tag"); return dynamic_cast(*this); } } #endif // TAG_H_INCLUDED PrismLauncher-10.0.5/libraries/libnbtplusplus/CMakeLists.txt0000644000175100017510000000334715144136757023601 0ustar runnerrunnercmake_minimum_required(VERSION 3.15) project(libnbt++ VERSION 2.3) # supported configure options option(NBT_BUILD_SHARED "Build shared libraries" OFF) option(NBT_USE_ZLIB "Build additional zlib stream functionality" ON) option(NBT_BUILD_TESTS "Build the unit tests. Requires CxxTest." ON) if(NBT_NAME) message("Using override nbt++ name: ${NBT_NAME}") else() set(NBT_NAME nbt++) endif() # hide this from includers. set(BUILD_SHARED_LIBS ${NBT_BUILD_SHARED}) include(GenerateExportHeader) set(NBT_SOURCES src/endian_str.cpp src/tag.cpp src/tag_compound.cpp src/tag_list.cpp src/tag_string.cpp src/value.cpp src/value_initializer.cpp src/io/stream_reader.cpp src/io/stream_writer.cpp src/text/json_formatter.cpp) set(NBT_SOURCES_Z src/io/izlibstream.cpp src/io/ozlibstream.cpp) if(NBT_USE_ZLIB) find_package(ZLIB REQUIRED) list(APPEND NBT_SOURCES ${NBT_SOURCES_Z}) add_definitions("-DNBT_HAVE_ZLIB") endif() add_library(${NBT_NAME} ${NBT_SOURCES}) target_include_directories(${NBT_NAME} PUBLIC include ${CMAKE_CURRENT_BINARY_DIR}) # Install it if(DEFINED NBT_DEST_DIR) install( TARGETS ${NBT_NAME} ARCHIVE DESTINATION ${LIBRARY_DEST_DIR} RUNTIME DESTINATION ${LIBRARY_DEST_DIR} LIBRARY DESTINATION ${LIBRARY_DEST_DIR} ) endif() if(NBT_USE_ZLIB) target_link_libraries(${NBT_NAME} ZLIB::ZLIB) endif() set_property(TARGET ${NBT_NAME} PROPERTY CXX_STANDARD 11) generate_export_header(${NBT_NAME} BASE_NAME nbt) if(${BUILD_SHARED_LIBS}) set_target_properties(${NBT_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1) endif() if(NBT_BUILD_TESTS) enable_testing() add_subdirectory(test) endif() PrismLauncher-10.0.5/libraries/libnbtplusplus/.gitattributes0000644000175100017510000000010215144136757023716 0ustar runnerrunner# Auto detect text files and perform LF normalization * text=auto PrismLauncher-10.0.5/libraries/libnbtplusplus/test/0000755000175100017510000000000015144136757022011 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/test/nbttest.h0000644000175100017510000004707415144136757023661 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include #include "nbt_tags.h" #include "nbt_visitor.h" #include #include #include using namespace nbt; class nbttest : public CxxTest::TestSuite { public: void test_tag() { TS_ASSERT(!is_valid_type(-1)); TS_ASSERT(!is_valid_type(0)); TS_ASSERT(is_valid_type(0, true)); TS_ASSERT(is_valid_type(1)); TS_ASSERT(is_valid_type(5, false)); TS_ASSERT(is_valid_type(7, true)); TS_ASSERT(is_valid_type(12)); TS_ASSERT(!is_valid_type(13)); //looks like TS_ASSERT_EQUALS can't handle abstract classes... TS_ASSERT(*tag::create(tag_type::Byte) == tag_byte()); TS_ASSERT_THROWS(tag::create(tag_type::Null), std::invalid_argument); TS_ASSERT_THROWS(tag::create(tag_type::End), std::invalid_argument); tag_string tstr("foo"); auto cl = tstr.clone(); TS_ASSERT_EQUALS(tstr.get(), "foo"); TS_ASSERT(tstr == *cl); cl = std::move(tstr).clone(); TS_ASSERT(*cl == tag_string("foo")); TS_ASSERT(*cl != tag_string("bar")); cl = std::move(*cl).move_clone(); TS_ASSERT(*cl == tag_string("foo")); tstr.assign(tag_string("bar")); TS_ASSERT_THROWS(tstr.assign(tag_int(6)), std::bad_cast); TS_ASSERT_EQUALS(tstr.get(), "bar"); TS_ASSERT_EQUALS(&tstr.as(), &tstr); TS_ASSERT_THROWS(tstr.as(), std::bad_cast); } void test_get_type() { TS_ASSERT_EQUALS(tag_byte().get_type() , tag_type::Byte); TS_ASSERT_EQUALS(tag_short().get_type() , tag_type::Short); TS_ASSERT_EQUALS(tag_int().get_type() , tag_type::Int); TS_ASSERT_EQUALS(tag_long().get_type() , tag_type::Long); TS_ASSERT_EQUALS(tag_float().get_type() , tag_type::Float); TS_ASSERT_EQUALS(tag_double().get_type() , tag_type::Double); TS_ASSERT_EQUALS(tag_byte_array().get_type(), tag_type::Byte_Array); TS_ASSERT_EQUALS(tag_string().get_type() , tag_type::String); TS_ASSERT_EQUALS(tag_list().get_type() , tag_type::List); TS_ASSERT_EQUALS(tag_compound().get_type() , tag_type::Compound); TS_ASSERT_EQUALS(tag_int_array().get_type() , tag_type::Int_Array); TS_ASSERT_EQUALS(tag_long_array().get_type(), tag_type::Long_Array); } void test_tag_primitive() { tag_int tag(6); TS_ASSERT_EQUALS(tag.get(), 6); int& ref = tag; ref = 12; TS_ASSERT(tag == 12); TS_ASSERT(tag != 6); tag.set(24); TS_ASSERT_EQUALS(ref, 24); tag = 7; TS_ASSERT_EQUALS(static_cast(tag), 7); TS_ASSERT_EQUALS(tag, tag_int(7)); TS_ASSERT_DIFFERS(tag_float(2.5), tag_float(-2.5)); TS_ASSERT_DIFFERS(tag_float(2.5), tag_double(2.5)); TS_ASSERT(tag_double() == 0.0); TS_ASSERT_EQUALS(tag_byte(INT8_MAX).get(), INT8_MAX); TS_ASSERT_EQUALS(tag_byte(INT8_MIN).get(), INT8_MIN); TS_ASSERT_EQUALS(tag_short(INT16_MAX).get(), INT16_MAX); TS_ASSERT_EQUALS(tag_short(INT16_MIN).get(), INT16_MIN); TS_ASSERT_EQUALS(tag_int(INT32_MAX).get(), INT32_MAX); TS_ASSERT_EQUALS(tag_int(INT32_MIN).get(), INT32_MIN); TS_ASSERT_EQUALS(tag_long(INT64_MAX).get(), INT64_MAX); TS_ASSERT_EQUALS(tag_long(INT64_MIN).get(), INT64_MIN); } void test_tag_string() { tag_string tag("foo"); TS_ASSERT_EQUALS(tag.get(), "foo"); std::string& ref = tag; ref = "bar"; TS_ASSERT_EQUALS(tag.get(), "bar"); TS_ASSERT_DIFFERS(tag.get(), "foo"); tag.set("baz"); TS_ASSERT_EQUALS(ref, "baz"); tag = "quux"; TS_ASSERT_EQUALS("quux", static_cast(tag)); std::string str("foo"); tag = str; TS_ASSERT_EQUALS(tag.get(),str); TS_ASSERT_EQUALS(tag_string(str).get(), "foo"); TS_ASSERT_EQUALS(tag_string().get(), ""); } void test_tag_compound() { tag_compound comp{ {"foo", int16_t(12)}, {"bar", "baz"}, {"baz", -2.0}, {"list", tag_list{16, 17}} }; //Test assignments and conversions, and exceptions on bad conversions TS_ASSERT_EQUALS(comp["foo"].get_type(), tag_type::Short); TS_ASSERT_EQUALS(static_cast(comp["foo"]), 12); TS_ASSERT_EQUALS(static_cast(comp.at("foo")), int16_t(12)); TS_ASSERT(comp["foo"] == tag_short(12)); TS_ASSERT_THROWS(static_cast(comp["foo"]), std::bad_cast); TS_ASSERT_THROWS(static_cast(comp["foo"]), std::bad_cast); TS_ASSERT_THROWS(comp["foo"] = 32, std::bad_cast); comp["foo"] = int8_t(32); TS_ASSERT_EQUALS(static_cast(comp["foo"]), 32); TS_ASSERT_EQUALS(comp["bar"].get_type(), tag_type::String); TS_ASSERT_EQUALS(static_cast(comp["bar"]), "baz"); TS_ASSERT_THROWS(static_cast(comp["bar"]), std::bad_cast); TS_ASSERT_THROWS(comp["bar"] = -128, std::bad_cast); comp["bar"] = "barbaz"; TS_ASSERT_EQUALS(static_cast(comp["bar"]), "barbaz"); TS_ASSERT_EQUALS(comp["baz"].get_type(), tag_type::Double); TS_ASSERT_EQUALS(static_cast(comp["baz"]), -2.0); TS_ASSERT_THROWS(static_cast(comp["baz"]), std::bad_cast); //Test nested access comp["quux"] = tag_compound{{"Hello", "World"}, {"zero", 0}}; TS_ASSERT_EQUALS(comp.at("quux").get_type(), tag_type::Compound); TS_ASSERT_EQUALS(static_cast(comp["quux"].at("Hello")), "World"); TS_ASSERT_EQUALS(static_cast(comp["quux"]["Hello"]), "World"); TS_ASSERT(comp["list"][1] == tag_int(17)); TS_ASSERT_THROWS(comp.at("nothing"), std::out_of_range); //Test equality comparisons tag_compound comp2{ {"foo", int16_t(32)}, {"bar", "barbaz"}, {"baz", -2.0}, {"quux", tag_compound{{"Hello", "World"}, {"zero", 0}}}, {"list", tag_list{16, 17}} }; TS_ASSERT(comp == comp2); TS_ASSERT(comp != dynamic_cast(comp2["quux"].get())); TS_ASSERT(comp != comp2["quux"]); TS_ASSERT(dynamic_cast(comp["quux"].get()) == comp2["quux"]); //Test whether begin() through end() goes through all the keys and their //values. The order of iteration is irrelevant there. std::set keys{"bar", "baz", "foo", "list", "quux"}; TS_ASSERT_EQUALS(comp2.size(), keys.size()); unsigned int i = 0; for(const std::pair& val: comp2) { TS_ASSERT_LESS_THAN(i, comp2.size()); TS_ASSERT(keys.count(val.first)); TS_ASSERT(val.second == comp2[val.first]); ++i; } TS_ASSERT_EQUALS(i, comp2.size()); //Test erasing and has_key TS_ASSERT_EQUALS(comp.erase("nothing"), false); TS_ASSERT(comp.has_key("quux")); TS_ASSERT(comp.has_key("quux", tag_type::Compound)); TS_ASSERT(!comp.has_key("quux", tag_type::List)); TS_ASSERT(!comp.has_key("quux", tag_type::Null)); TS_ASSERT_EQUALS(comp.erase("quux"), true); TS_ASSERT(!comp.has_key("quux")); TS_ASSERT(!comp.has_key("quux", tag_type::Compound)); TS_ASSERT(!comp.has_key("quux", tag_type::Null)); comp.clear(); TS_ASSERT(comp == tag_compound{}); //Test inserting values TS_ASSERT_EQUALS(comp.put("abc", tag_double(6.0)).second, true); TS_ASSERT_EQUALS(comp.put("abc", tag_long(-28)).second, false); TS_ASSERT_EQUALS(comp.insert("ghi", tag_string("world")).second, true); TS_ASSERT_EQUALS(comp.insert("abc", tag_string("hello")).second, false); TS_ASSERT_EQUALS(comp.emplace("def", "ghi").second, true); TS_ASSERT_EQUALS(comp.emplace("def", 4).second, false); TS_ASSERT((comp == tag_compound{ {"abc", tag_long(-28)}, {"def", tag_byte(4)}, {"ghi", tag_string("world")} })); } void test_value() { value val1; value val2(make_unique(42)); value val3(tag_int(42)); TS_ASSERT(!val1 && val2 && val3); TS_ASSERT(val1 == val1); TS_ASSERT(val1 != val2); TS_ASSERT(val2 == val3); TS_ASSERT(val3 == val3); value valstr(tag_string("foo")); TS_ASSERT_EQUALS(static_cast(valstr), "foo"); valstr = "bar"; TS_ASSERT_THROWS(valstr = 5, std::bad_cast); TS_ASSERT_EQUALS(static_cast(valstr), "bar"); TS_ASSERT(valstr.as() == "bar"); TS_ASSERT_EQUALS(&valstr.as(), &valstr.get()); TS_ASSERT_THROWS(valstr.as(), std::bad_cast); val1 = int64_t(42); TS_ASSERT(val2 != val1); TS_ASSERT_THROWS(val2 = int64_t(12), std::bad_cast); TS_ASSERT_EQUALS(static_cast(val2), 42); tag_int* ptr = dynamic_cast(val2.get_ptr().get()); TS_ASSERT(*ptr == 42); val2 = 52; TS_ASSERT_EQUALS(static_cast(val2), 52); TS_ASSERT(*ptr == 52); TS_ASSERT_THROWS(val1["foo"], std::bad_cast); TS_ASSERT_THROWS(val1.at("foo"), std::bad_cast); val3 = 52; TS_ASSERT(val2 == val3); TS_ASSERT(val2.get_ptr() != val3.get_ptr()); val3 = std::move(val2); TS_ASSERT(val3 == tag_int(52)); TS_ASSERT(!val2); tag_int& tag = dynamic_cast(val3.get()); TS_ASSERT(tag == tag_int(52)); tag = 21; TS_ASSERT_EQUALS(static_cast(val3), 21); val1.set_ptr(std::move(val3.get_ptr())); TS_ASSERT(val1.as() == 21); TS_ASSERT_EQUALS(val1.get_type(), tag_type::Int); TS_ASSERT_EQUALS(val2.get_type(), tag_type::Null); TS_ASSERT_EQUALS(val3.get_type(), tag_type::Null); val2 = val1; val1 = val3; TS_ASSERT(!val1 && val2 && !val3); TS_ASSERT(val1.get_ptr() == nullptr); TS_ASSERT(val2.get() == tag_int(21)); TS_ASSERT(value(val1) == val1); TS_ASSERT(value(val2) == val2); val1 = val1; val2 = val2; TS_ASSERT(!val1); TS_ASSERT(val1 == value_initializer(nullptr)); TS_ASSERT(val2 == tag_int(21)); val3 = tag_short(2); TS_ASSERT_THROWS(val3 = tag_string("foo"), std::bad_cast); TS_ASSERT(val3.get() == tag_short(2)); val2.set_ptr(make_unique("foo")); TS_ASSERT(val2 == tag_string("foo")); } void test_tag_list() { tag_list list; TS_ASSERT_EQUALS(list.el_type(), tag_type::Null); TS_ASSERT_THROWS(list.push_back(value(nullptr)), std::invalid_argument); list.emplace_back("foo"); TS_ASSERT_EQUALS(list.el_type(), tag_type::String); list.push_back(tag_string("bar")); TS_ASSERT_THROWS(list.push_back(tag_int(42)), std::invalid_argument); TS_ASSERT_THROWS(list.emplace_back(), std::invalid_argument); TS_ASSERT((list == tag_list{"foo", "bar"})); TS_ASSERT(list[0] == tag_string("foo")); TS_ASSERT_EQUALS(static_cast(list.at(1)), "bar"); TS_ASSERT_EQUALS(list.size(), 2u); TS_ASSERT_THROWS(list.at(2), std::out_of_range); TS_ASSERT_THROWS(list.at(-1), std::out_of_range); list.set(1, value(tag_string("baz"))); TS_ASSERT_THROWS(list.set(1, value(nullptr)), std::invalid_argument); TS_ASSERT_THROWS(list.set(1, value(tag_int(-42))), std::invalid_argument); TS_ASSERT_EQUALS(static_cast(list[1]), "baz"); TS_ASSERT_EQUALS(list.size(), 2u); tag_string values[] = {"foo", "baz"}; TS_ASSERT_EQUALS(list.end() - list.begin(), int(list.size())); TS_ASSERT(std::equal(list.begin(), list.end(), values)); list.pop_back(); TS_ASSERT(list == tag_list{"foo"}); TS_ASSERT(list == tag_list::of({"foo"})); TS_ASSERT(tag_list::of({"foo"}) == tag_list{"foo"}); TS_ASSERT((list != tag_list{2, 3, 5, 7})); list.clear(); TS_ASSERT_EQUALS(list.size(), 0u); TS_ASSERT_EQUALS(list.el_type(), tag_type::String) TS_ASSERT_THROWS(list.push_back(tag_short(25)), std::invalid_argument); TS_ASSERT_THROWS(list.push_back(value(nullptr)), std::invalid_argument); list.reset(); TS_ASSERT_EQUALS(list.el_type(), tag_type::Null); list.emplace_back(17); TS_ASSERT_EQUALS(list.el_type(), tag_type::Int); list.reset(tag_type::Float); TS_ASSERT_EQUALS(list.el_type(), tag_type::Float); list.emplace_back(17.0f); TS_ASSERT(list == tag_list({17.0f})); TS_ASSERT(tag_list() != tag_list(tag_type::Int)); TS_ASSERT(tag_list() == tag_list()); TS_ASSERT(tag_list(tag_type::Short) != tag_list(tag_type::Int)); TS_ASSERT(tag_list(tag_type::Short) == tag_list(tag_type::Short)); tag_list short_list = tag_list::of({25, 36}); TS_ASSERT_EQUALS(short_list.el_type(), tag_type::Short); TS_ASSERT((short_list == tag_list{int16_t(25), int16_t(36)})); TS_ASSERT((short_list != tag_list{25, 36})); TS_ASSERT((short_list == tag_list{value(tag_short(25)), value(tag_short(36))})); TS_ASSERT_THROWS((tag_list{value(tag_byte(4)), value(tag_int(5))}), std::invalid_argument); TS_ASSERT_THROWS((tag_list{value(nullptr), value(tag_int(6))}), std::invalid_argument); TS_ASSERT_THROWS((tag_list{value(tag_int(7)), value(tag_int(8)), value(nullptr)}), std::invalid_argument); TS_ASSERT_EQUALS((tag_list(std::initializer_list{})).el_type(), tag_type::Null); TS_ASSERT_EQUALS((tag_list{2, 3, 5, 7}).el_type(), tag_type::Int); } void test_tag_byte_array() { std::vector vec{1, 2, 127, -128}; tag_byte_array arr{1, 2, 127, -128}; TS_ASSERT_EQUALS(arr.size(), 4u); TS_ASSERT(arr.at(0) == 1 && arr[1] == 2 && arr[2] == 127 && arr.at(3) == -128); TS_ASSERT_THROWS(arr.at(-1), std::out_of_range); TS_ASSERT_THROWS(arr.at(4), std::out_of_range); TS_ASSERT(arr.get() == vec); TS_ASSERT(arr == tag_byte_array(std::vector(vec))); arr.push_back(42); vec.push_back(42); TS_ASSERT_EQUALS(arr.size(), 5u); TS_ASSERT_EQUALS(arr.end() - arr.begin(), int(arr.size())); TS_ASSERT(std::equal(arr.begin(), arr.end(), vec.begin())); arr.pop_back(); arr.pop_back(); TS_ASSERT_EQUALS(arr.size(), 3u); TS_ASSERT((arr == tag_byte_array{1, 2, 127})); TS_ASSERT((arr != tag_int_array{1, 2, 127})); TS_ASSERT((arr != tag_long_array{1, 2, 127})); TS_ASSERT((arr != tag_byte_array{1, 2, -1})); arr.clear(); TS_ASSERT(arr == tag_byte_array()); } void test_tag_int_array() { std::vector vec{100, 200, INT32_MAX, INT32_MIN}; tag_int_array arr{100, 200, INT32_MAX, INT32_MIN}; TS_ASSERT_EQUALS(arr.size(), 4u); TS_ASSERT(arr.at(0) == 100 && arr[1] == 200 && arr[2] == INT32_MAX && arr.at(3) == INT32_MIN); TS_ASSERT_THROWS(arr.at(-1), std::out_of_range); TS_ASSERT_THROWS(arr.at(4), std::out_of_range); TS_ASSERT(arr.get() == vec); TS_ASSERT(arr == tag_int_array(std::vector(vec))); arr.push_back(42); vec.push_back(42); TS_ASSERT_EQUALS(arr.size(), 5u); TS_ASSERT_EQUALS(arr.end() - arr.begin(), int(arr.size())); TS_ASSERT(std::equal(arr.begin(), arr.end(), vec.begin())); arr.pop_back(); arr.pop_back(); TS_ASSERT_EQUALS(arr.size(), 3u); TS_ASSERT((arr == tag_int_array{100, 200, INT32_MAX})); TS_ASSERT((arr != tag_int_array{100, -56, -1})); arr.clear(); TS_ASSERT(arr == tag_int_array()); } void test_tag_long_array() { std::vector vec{100, 200, INT64_MAX, INT64_MIN}; tag_long_array arr{100, 200, INT64_MAX, INT64_MIN}; TS_ASSERT_EQUALS(arr.size(), 4u); TS_ASSERT(arr.at(0) == 100 && arr[1] == 200 && arr[2] == INT64_MAX && arr.at(3) == INT64_MIN); TS_ASSERT_THROWS(arr.at(-1), std::out_of_range); TS_ASSERT_THROWS(arr.at(4), std::out_of_range); TS_ASSERT(arr.get() == vec); TS_ASSERT(arr == tag_long_array(std::vector(vec))); arr.push_back(42); vec.push_back(42); TS_ASSERT_EQUALS(arr.size(), 5u); TS_ASSERT_EQUALS(arr.end() - arr.begin(), int(arr.size())); TS_ASSERT(std::equal(arr.begin(), arr.end(), vec.begin())); arr.pop_back(); arr.pop_back(); TS_ASSERT_EQUALS(arr.size(), 3u); TS_ASSERT((arr == tag_long_array{100, 200, INT64_MAX})); TS_ASSERT((arr != tag_long_array{100, -56, -1})); arr.clear(); TS_ASSERT(arr == tag_long_array()); } void test_visitor() { struct : public nbt_visitor { tag* visited = nullptr; void visit(tag_byte& tag) { visited = &tag; } void visit(tag_short& tag) { visited = &tag; } void visit(tag_int& tag) { visited = &tag; } void visit(tag_long& tag) { visited = &tag; } void visit(tag_float& tag) { visited = &tag; } void visit(tag_double& tag) { visited = &tag; } void visit(tag_byte_array& tag) { visited = &tag; } void visit(tag_string& tag) { visited = &tag; } void visit(tag_list& tag) { visited = &tag; } void visit(tag_compound& tag) { visited = &tag; } void visit(tag_int_array& tag) { visited = &tag; } void visit(tag_long_array& tag) { visited = &tag; } } v; tag_byte b; b.accept(v); TS_ASSERT_EQUALS(v.visited, &b); tag_short s; s.accept(v); TS_ASSERT_EQUALS(v.visited, &s); tag_int i; i.accept(v); TS_ASSERT_EQUALS(v.visited, &i); tag_long l; l.accept(v); TS_ASSERT_EQUALS(v.visited, &l); tag_float f; f.accept(v); TS_ASSERT_EQUALS(v.visited, &f); tag_double d; d.accept(v); TS_ASSERT_EQUALS(v.visited, &d); tag_byte_array ba; ba.accept(v); TS_ASSERT_EQUALS(v.visited, &ba); tag_string st; st.accept(v); TS_ASSERT_EQUALS(v.visited, &st); tag_list ls; ls.accept(v); TS_ASSERT_EQUALS(v.visited, &ls); tag_compound c; c.accept(v); TS_ASSERT_EQUALS(v.visited, &c); tag_int_array ia; ia.accept(v); TS_ASSERT_EQUALS(v.visited, &ia); tag_long_array la; la.accept(v); TS_ASSERT_EQUALS(v.visited, &la); } }; PrismLauncher-10.0.5/libraries/libnbtplusplus/test/testfiles/0000755000175100017510000000000015144136757024013 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/test/testfiles/errortest_eof10000644000175100017510000000005315144136757026677 0ustar runnerrunner Test (unexpected EOF) some double?ßk»PrismLauncher-10.0.5/libraries/libnbtplusplus/test/testfiles/bigtest_eof.nbt0000644000175100017510000000065215144136757027015 0ustar runnerrunner‹ÎÂUbigtest_uncompressedí”ÏOAÇŸ° Ë‚`,1Äój-º% E$Æ"Z°h6h`c1ÄE†…vÙ5»ƒ§^ÚcÓ[ÿ™ùzîµÿ–^Ús/¼LòÞ¼ùÎ|ÞÌ$O^!÷Äà@0,SW‰C?&æ€Óµl:É :ÔîM$¬Vå ßÕ•2ªÕ“²qˆj¥¡bC­ŸÔÞâðËðóðûSÃÒÜSŠ£XÖ þžIg¢& Io­þ50ÛHÙ\oWë À™ZŸ€¯ªõïü½f ÈA ˜‚èúLá¯èz‹Øt.± bô—…‰ñí’Ëò0±àÔ‡¦~yêÃS À“Çí³ò’"[ò̸‘7óª5ŸÉˆ·6ÑØ}^Z&ÀR¼¾ûó+üCù«šU~ÄFt«Y¡õ@‰ûn~ ãøÐ¶µ‡‰Œv vz6 3²,£ûZL˜)3•ÝÙya¦v“ÏÙ¢„ÕlÊþ?öhÍ×2&X6Ÿ•ðUNÂL^‚„ét:ÉÀÞßPÜZÄ5IY=Fî¢PrismLauncher-10.0.5/libraries/libnbtplusplus/test/testfiles/bigtest_corrupt.nbt0000644000175100017510000000106115144136757027735 0ustar runnerrunner‹ÎÂUbigtest_uncompressedí”ÏOAÇŸ° Ë‚`,1Äój-º% E$Æ"Z°h6h`c1ÄE†…vÙ5»ƒ§^ÚcÓ[ÿ™ùzîµÿ–^Ús/¼LòÞ¼ùÎ|ÞÌ$O^!÷Äà@0,SW‰C?&æ€Óµl:É :ÔîM$¬Vå ßÕ•2ªÕ“²qˆj¥¡bC­ŸÔÞâðËðóðûSÃÒÜSŠ£XÖ þžIgâ& Io­þ50ÛHÙ\oWë À™ZŸ€¯ªõïü½f ÈA ˜‚èúLá¯èz‹Øt.± bô—…‰ñí’Ëò0±àÔ‡¦~yêÃS À“Çí³ò’"[ò̸‘7óª5ŸÉˆ·6ÑØ}^Z&ÀR¼¾ûó+üCù«šU~ÄFt«Y¡õ@‰ûn~ ãøÐ¶µ‡‰Œv vz6 3²,£ûZL˜)3•ÝÙya¦v“ÏÙ¢„ÕlÊþ?öhÍ×2&X6Ÿ•ðUNÂL^‚„ét:ÉÀÞßPÜZÄ5IY=Fî¢v~N_&<§9[TWöĪ´¾ÅüÅÆy*|syå‰ZÙz´·_m®—£õüöÆfªt¿ÊÅ|-m ûkM¥|\ç¶k›×¥t<‘;õÉ Æ‚±`ü_†Ķ5hn‡9øõíÃÑûfB¬WÏû kC,y¼ïó 1ZGà>}‘$APrismLauncher-10.0.5/libraries/libnbtplusplus/test/testfiles/trailing_data.zlib0000644000175100017510000000002415144136757027473 0ustar runnerrunnerxœKËÏOJ,«zbarbazPrismLauncher-10.0.5/libraries/libnbtplusplus/test/testfiles/bigtest.zlib0000644000175100017510000000102015144136757026327 0ustar runnerrunnerxœí”ÏOAǰ » ‚±ÄcÌ«µè–,ˆHŒE´`ÑlÐÀÆcˆ‹ vÙ5»‹§^ÚcÓ[ÿ™ùzîµÿ–^Ús/¼LòÞ¼ùÎ|ÞÌ$OV&Dg€ÓMCSˆí|MÌ ¼Ý5-g’ã@°«7‘@²Z‘å3|V—˨TOHÇ!*•†‚ ¥~R{‡Ã/ÃÏÃïÏYà;º©º§G±¬=Ùqˆ4$m¼5û÷æÀh£CçøºjŸÆPûüUµ?°Y`T}@J@DÓfŠ@EÓZÄræ½g»,LŒo—dh–…‰§>4õKSžúÏž¶ÏÊK tÉ;ãFÞΫV5|!1 ÜZD¥÷ymžx}÷çWø‡:ów5á',¡D·p­G‡¸ï2Ž-K}œÈœ.ÁNÏ¢aF’$tßÀF³ƒ #e¤²;;¯ŒÔnò%]ÑvTË¡ÿˆ{N7&h6Ÿq;'b&/bAÄt:¤`ßo(n.sª(¯#sQ;¿§/ÞÓ‚”-*Ë{BU\[‰bþbý<¾¹¼òÆ ­lH9ÚÛ¯6×ÊÑz~k}#Uº‰_åbþ„6¸ýÕ¦\>®3[µëR:žÈú¥cÁX0þ/ÃBÛ´t·ÃüúöáÇ讄íÕó>CÛ¯aýŽ‚¡¥pþÔ¬bçPrismLauncher-10.0.5/libraries/libnbtplusplus/test/testfiles/bigtest.nbt0000644000175100017510000000106115144136757026157 0ustar runnerrunner‹ÎÂUbigtest_uncompressedí”ÏOAÇŸ° Ë‚`,1Äój-º% E$Æ"Z°h6h`c1ÄE†…vÙ5»ƒ§^ÚcÓ[ÿ™ùzîµÿ–^Ús/¼LòÞ¼ùÎ|ÞÌ$O^!÷Äà@0,SW‰C?&æ€Óµl:É :ÔîM$¬Vå ßÕ•2ªÕ“²qˆj¥¡bC­ŸÔÞâðËðóðûSÃÒÜSŠ£XÖ þžIg¢& Io­þ50ÛHÙ\oWë À™ZŸ€¯ªõïü½f ÈA ˜‚èúLá¯èz‹Øt.± bô—…‰ñí’Ëò0±àÔ‡¦~yêÃS À“Çí³ò’"[ò̸‘7óª5ŸÉˆ·6ÑØ}^Z&ÀR¼¾ûó+üCù«šU~ÄFt«Y¡õ@‰ûn~ ãøÐ¶µ‡‰Œv vz6 3²,£ûZL˜)3•ÝÙya¦v“ÏÙ¢„ÕlÊþ?öhÍ×2&X6Ÿ•ðUNÂL^‚„ét:ÉÀÞßPÜZÄ5IY=Fî¢v~N_&<§9[TWöĪ´¾ÅüÅÆy*|syå‰ZÙz´·_m®—£õüöÆfªt¿ÊÅ|-m ûkM¥|\ç¶k›×¥t<‘;õÉ Æ‚±`ü_†Ķ5hn‡9øõíÃÑûfB¬WÏû kC,y¼ïó 1ZGà>}‘$APrismLauncher-10.0.5/libraries/libnbtplusplus/test/testfiles/bigtest_uncompr0000644000175100017510000000310115144136757027135 0ustar runnerrunner LevellongTestÿÿÿÿÿÿÿ shortTestÿ stringTest)HELLO WORLD THIS IS A TEST STRING ÅÄÖ! floatTest>ÿ2intTestÿÿÿ nested compound test hamnameHampusvalue?@ eggnameEggbertvalue? listTest (long)  listTest (compound) nameCompound tag #0 created-on&R7ÕnameCompound tag #1 created-on&R7Õ listTest (end)byteTestebyteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))è>" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:intTestÿÿÿ nested compound test hamnameHampusvalue@? eggnameEggbertvalue? listTest (long)  listTest (compound) nameCompound tag #0 created-onÕ7R&nameCompound tag #1 created-onÕ7R& listTest (end)byteTestebyteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))è>" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:" ,LF VNP\.X(J802>T: H, 6VP*`XZ8b2 TB:. */ #include #include "io/stream_reader.h" #ifdef NBT_HAVE_ZLIB #include "io/izlibstream.h" #endif #include "nbt_tags.h" #include #include #include using namespace nbt; #include "data.h" class read_test : public CxxTest::TestSuite { public: void test_stream_reader_big() { std::string input{ 1, //tag_type::Byte 0, //tag_type::End 11, //tag_type::Int_Array 0x0a, 0x0b, 0x0c, 0x0d, //0x0a0b0c0d in Big Endian 0x00, 0x06, //String length in Big Endian 'f', 'o', 'o', 'b', 'a', 'r', 0 //tag_type::End (invalid with allow_end = false) }; std::istringstream is(input); nbt::io::stream_reader reader(is); TS_ASSERT_EQUALS(&reader.get_istr(), &is); TS_ASSERT_EQUALS(reader.get_endian(), endian::big); TS_ASSERT_EQUALS(reader.read_type(), tag_type::Byte); TS_ASSERT_EQUALS(reader.read_type(true), tag_type::End); TS_ASSERT_EQUALS(reader.read_type(false), tag_type::Int_Array); int32_t i; reader.read_num(i); TS_ASSERT_EQUALS(i, 0x0a0b0c0d); TS_ASSERT_EQUALS(reader.read_string(), "foobar"); TS_ASSERT_THROWS(reader.read_type(false), io::input_error); TS_ASSERT(!is); is.clear(); //Test for invalid tag type 13 is.str("\x0d"); TS_ASSERT_THROWS(reader.read_type(), io::input_error); TS_ASSERT(!is); is.clear(); //Test for unexpcted EOF on numbers (input too short for int32_t) is.str("\x03\x04"); reader.read_num(i); TS_ASSERT(!is); } void test_stream_reader_little() { std::string input{ 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, //0x0d0c0b0a09080706 in Little Endian 0x06, 0x00, //String length in Little Endian 'f', 'o', 'o', 'b', 'a', 'r', 0x10, 0x00, //String length (intentionally too large) 'a', 'b', 'c', 'd' //unexpected EOF }; std::istringstream is(input); nbt::io::stream_reader reader(is, endian::little); TS_ASSERT_EQUALS(reader.get_endian(), endian::little); int64_t i; reader.read_num(i); TS_ASSERT_EQUALS(i, 0x0d0c0b0a09080706); TS_ASSERT_EQUALS(reader.read_string(), "foobar"); TS_ASSERT_THROWS(reader.read_string(), io::input_error); TS_ASSERT(!is); } //Tests if comp equals an extended variant of Notch's bigtest NBT void verify_bigtest_structure(const tag_compound& comp) { TS_ASSERT_EQUALS(comp.size(), 13u); TS_ASSERT(comp.at("byteTest") == tag_byte(127)); TS_ASSERT(comp.at("shortTest") == tag_short(32767)); TS_ASSERT(comp.at("intTest") == tag_int(2147483647)); TS_ASSERT(comp.at("longTest") == tag_long(9223372036854775807)); TS_ASSERT(comp.at("floatTest") == tag_float(std::stof("0xff1832p-25"))); //0.4982315 TS_ASSERT(comp.at("doubleTest") == tag_double(std::stod("0x1f8f6bbbff6a5ep-54"))); //0.493128713218231 //From bigtest.nbt: "the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...)" tag_byte_array byteArrayTest; for(int n = 0; n < 1000; ++n) byteArrayTest.push_back((n*n*255 + n*7) % 100); TS_ASSERT(comp.at("byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))") == byteArrayTest); TS_ASSERT(comp.at("stringTest") == tag_string("HELLO WORLD THIS IS A TEST STRING \u00C5\u00C4\u00D6!")); TS_ASSERT(comp.at("listTest (compound)") == tag_list::of({ {{"created-on", tag_long(1264099775885)}, {"name", "Compound tag #0"}}, {{"created-on", tag_long(1264099775885)}, {"name", "Compound tag #1"}} })); TS_ASSERT(comp.at("listTest (long)") == tag_list::of({11, 12, 13, 14, 15})); TS_ASSERT(comp.at("listTest (end)") == tag_list()); TS_ASSERT((comp.at("nested compound test") == tag_compound{ {"egg", tag_compound{{"value", 0.5f}, {"name", "Eggbert"}}}, {"ham", tag_compound{{"value", 0.75f}, {"name", "Hampus"}}} })); TS_ASSERT(comp.at("intArrayTest") == tag_int_array( {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f})); } void test_read_bigtest() { //Uses an extended variant of Notch's original bigtest file std::string input(__binary_bigtest_uncompr_start, __binary_bigtest_uncompr_end); std::istringstream file(input, std::ios::binary); auto pair = nbt::io::read_compound(file); TS_ASSERT_EQUALS(pair.first, "Level"); verify_bigtest_structure(*pair.second); } void test_read_littletest() { //Same as bigtest, but little endian std::string input(__binary_littletest_uncompr_start, __binary_littletest_uncompr_end); std::istringstream file(input, std::ios::binary); auto pair = nbt::io::read_compound(file, endian::little); TS_ASSERT_EQUALS(pair.first, "Level"); TS_ASSERT_EQUALS(pair.second->get_type(), tag_type::Compound); verify_bigtest_structure(*pair.second); } void test_read_eof1() { std::string input(__binary_errortest_eof1_start, __binary_errortest_eof1_end); std::istringstream file(input, std::ios::binary); nbt::io::stream_reader reader(file); //EOF within a tag_double payload TS_ASSERT(file); TS_ASSERT_THROWS(reader.read_tag(), io::input_error); TS_ASSERT(!file); } void test_read_eof2() { std::string input(__binary_errortest_eof2_start, __binary_errortest_eof2_end); std::istringstream file(input, std::ios::binary); nbt::io::stream_reader reader(file); //EOF within a key in a compound TS_ASSERT(file); TS_ASSERT_THROWS(reader.read_tag(), io::input_error); TS_ASSERT(!file); } void test_read_errortest_noend() { std::string input(__binary_errortest_noend_start, __binary_errortest_noend_end); std::istringstream file(input, std::ios::binary); nbt::io::stream_reader reader(file); //Missing tag_end TS_ASSERT(file); TS_ASSERT_THROWS(reader.read_tag(), io::input_error); TS_ASSERT(!file); } void test_read_errortest_neg_length() { std::string input(__binary_errortest_neg_length_start, __binary_errortest_neg_length_end); std::istringstream file(input, std::ios::binary); nbt::io::stream_reader reader(file); //Negative list length TS_ASSERT(file); TS_ASSERT_THROWS(reader.read_tag(), io::input_error); TS_ASSERT(!file); } void test_read_misc() { std::string input(__binary_toplevel_string_start, __binary_toplevel_string_end); std::istringstream file(input, std::ios::binary); nbt::io::stream_reader reader(file); //Toplevel tag other than compound TS_ASSERT(file); TS_ASSERT_THROWS(reader.read_compound(), io::input_error); TS_ASSERT(!file); //Rewind and try again with read_tag file.clear(); TS_ASSERT(file.seekg(0)); auto pair = reader.read_tag(); TS_ASSERT_EQUALS(pair.first, "Test (toplevel tag_string)"); TS_ASSERT(*pair.second == tag_string( "Even though unprovided for by NBT, the library should also handle " "the case where the file consists of something else than tag_compound")); } void test_read_gzip() { #ifdef NBT_HAVE_ZLIB std::string input(__binary_bigtest_nbt_start, __binary_bigtest_nbt_end); std::istringstream file(input, std::ios::binary); zlib::izlibstream igzs(file); TS_ASSERT(file && igzs); auto pair = nbt::io::read_compound(igzs); TS_ASSERT(igzs); TS_ASSERT_EQUALS(pair.first, "Level"); verify_bigtest_structure(*pair.second); #endif } }; PrismLauncher-10.0.5/libraries/libnbtplusplus/test/CMakeLists.txt0000644000175100017510000000667315144136757024565 0ustar runnerrunnerif(CMAKE_SYSTEM_NAME STREQUAL "Linux") if(CMAKE_SYSTEM_PROCESSOR STREQUAL x86_64 OR CMAKE_SYSTEM_PROCESSOR STREQUAL amd64) set(OBJCOPY_TARGET "elf64-x86-64") set(OBJCOPY_ARCH "x86_64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL i686) set(OBJCOPY_TARGET "elf32-i386") set(OBJCOPY_ARCH "i386") else() message(AUTHOR_WARNING "This is not a platform that would support testing nbt++") return() endif() else() message(AUTHOR_WARNING "This is not a platform that would support testing nbt++") return() endif() enable_testing() find_package(CxxTest REQUIRED) include_directories(${libnbt++_SOURCE_DIR}/include) include_directories(${CXXTEST_INCLUDE_DIR}) function(build_data out_var) set(result) foreach(in_f ${ARGN}) set(out_f "${CMAKE_CURRENT_BINARY_DIR}/testfiles/${in_f}.obj") add_custom_command( COMMAND mkdir -p "${CMAKE_CURRENT_BINARY_DIR}/testfiles" COMMAND ${CMAKE_OBJCOPY} --prefix-symbol=_ --input-target=binary --output-target=${OBJCOPY_TARGET} "${in_f}" "${out_f}" DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/testfiles/${in_f} OUTPUT ${out_f} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/testfiles/ VERBATIM ) SET_SOURCE_FILES_PROPERTIES( ${out_f} PROPERTIES EXTERNAL_OBJECT true GENERATED true ) list(APPEND result ${out_f}) endforeach() set(${out_var} "${result}" PARENT_SCOPE) endfunction() build_data(DATA_OBJECTS bigtest.nbt bigtest.zlib bigtest_corrupt.nbt bigtest_eof.nbt bigtest_uncompr errortest_eof1 errortest_eof2 errortest_neg_length errortest_noend littletest_uncompr toplevel_string trailing_data.zlib ) add_library(NbtTestData STATIC ${DATA_OBJECTS}) #Specifies that the directory containing the testfiles get copied when the target is built function(use_testfiles target) add_custom_command(TARGET ${target} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/testfiles ${CMAKE_CURRENT_BINARY_DIR}) endfunction() function(stop_warnings target) target_compile_options(${target} PRIVATE -Wno-unused-value -Wno-self-assign-overloaded ) endfunction() if(NBT_USE_ZLIB) set(EXTRA_TEST_LIBS ${ZLIB_LIBRARY}) endif() CXXTEST_ADD_TEST(nbttest nbttest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nbttest.h) target_link_libraries(nbttest ${NBT_NAME}) stop_warnings(nbttest) CXXTEST_ADD_TEST(endian_str_test endian_str_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/endian_str_test.h) target_link_libraries(endian_str_test ${NBT_NAME}) stop_warnings(endian_str_test) CXXTEST_ADD_TEST(read_test read_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/read_test.h) target_link_libraries(read_test ${NBT_NAME} ${EXTRA_TEST_LIBS} NbtTestData) stop_warnings(read_test) CXXTEST_ADD_TEST(write_test write_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/write_test.h) target_link_libraries(write_test ${NBT_NAME} ${EXTRA_TEST_LIBS} NbtTestData) stop_warnings(write_test) if(NBT_USE_ZLIB) CXXTEST_ADD_TEST(zlibstream_test zlibstream_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/zlibstream_test.h) target_link_libraries(zlibstream_test ${NBT_NAME} ${EXTRA_TEST_LIBS} NbtTestData) stop_warnings(zlibstream_test) endif() add_executable(format_test format_test.cpp) target_link_libraries(format_test ${NBT_NAME}) add_test(format_test format_test) stop_warnings(format_test) PrismLauncher-10.0.5/libraries/libnbtplusplus/test/data.h0000644000175100017510000000243115144136757023073 0ustar runnerrunner#pragma once #include extern "C" uint8_t __binary_bigtest_uncompr_start[]; extern "C" uint8_t __binary_bigtest_uncompr_end[]; extern "C" uint8_t __binary_littletest_uncompr_start[]; extern "C" uint8_t __binary_littletest_uncompr_end[]; extern "C" uint8_t __binary_errortest_eof1_start[]; extern "C" uint8_t __binary_errortest_eof1_end[]; extern "C" uint8_t __binary_errortest_eof2_start[]; extern "C" uint8_t __binary_errortest_eof2_end[]; extern "C" uint8_t __binary_errortest_noend_start[]; extern "C" uint8_t __binary_errortest_noend_end[]; extern "C" uint8_t __binary_errortest_neg_length_start[]; extern "C" uint8_t __binary_errortest_neg_length_end[]; extern "C" uint8_t __binary_toplevel_string_start[]; extern "C" uint8_t __binary_toplevel_string_end[]; extern "C" uint8_t __binary_bigtest_nbt_start[]; extern "C" uint8_t __binary_bigtest_nbt_end[]; extern "C" uint8_t __binary_bigtest_zlib_start[]; extern "C" uint8_t __binary_bigtest_zlib_end[]; extern "C" uint8_t __binary_bigtest_corrupt_nbt_start[]; extern "C" uint8_t __binary_bigtest_corrupt_nbt_end[]; extern "C" uint8_t __binary_bigtest_eof_nbt_start[]; extern "C" uint8_t __binary_bigtest_eof_nbt_end[]; extern "C" uint8_t __binary_trailing_data_zlib_start[]; extern "C" uint8_t __binary_trailing_data_zlib_end[]; PrismLauncher-10.0.5/libraries/libnbtplusplus/test/endian_str_test.h0000644000175100017510000001247615144136757025361 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include #include "endian_str.h" #include #include #include using namespace endian; class endian_str_test : public CxxTest::TestSuite { public: void test_uint() { std::stringstream str(std::ios::in | std::ios::out | std::ios::binary); write_little(str, uint8_t (0x01)); write_little(str, uint16_t(0x0102)); write (str, uint32_t(0x01020304), little); write_little(str, uint64_t(0x0102030405060708)); write_big (str, uint8_t (0x09)); write_big (str, uint16_t(0x090A)); write_big (str, uint32_t(0x090A0B0C)); write (str, uint64_t(0x090A0B0C0D0E0F10), big); std::string expected{ 1, 2, 1, 4, 3, 2, 1, 8, 7, 6, 5, 4, 3, 2, 1, 9, 9, 10, 9, 10, 11, 12, 9, 10, 11, 12, 13, 14, 15, 16 }; TS_ASSERT_EQUALS(str.str(), expected); uint8_t u8; uint16_t u16; uint32_t u32; uint64_t u64; read_little(str, u8); TS_ASSERT_EQUALS(u8, 0x01); read_little(str, u16); TS_ASSERT_EQUALS(u16, 0x0102); read_little(str, u32); TS_ASSERT_EQUALS(u32, 0x01020304u); read(str, u64, little); TS_ASSERT_EQUALS(u64, 0x0102030405060708u); read_big(str, u8); TS_ASSERT_EQUALS(u8, 0x09); read_big(str, u16); TS_ASSERT_EQUALS(u16, 0x090A); read(str, u32, big); TS_ASSERT_EQUALS(u32, 0x090A0B0Cu); read_big(str, u64); TS_ASSERT_EQUALS(u64, 0x090A0B0C0D0E0F10u); TS_ASSERT(str); //Check if stream has failed } void test_sint() { std::stringstream str(std::ios::in | std::ios::out | std::ios::binary); write_little(str, int8_t (-0x01)); write_little(str, int16_t(-0x0102)); write_little(str, int32_t(-0x01020304)); write (str, int64_t(-0x0102030405060708), little); write_big (str, int8_t (-0x09)); write_big (str, int16_t(-0x090A)); write (str, int32_t(-0x090A0B0C), big); write_big (str, int64_t(-0x090A0B0C0D0E0F10)); std::string expected{ //meh, stupid narrowing conversions '\xFF', '\xFE', '\xFE', '\xFC', '\xFC', '\xFD', '\xFE', '\xF8', '\xF8', '\xF9', '\xFA', '\xFB', '\xFC', '\xFD', '\xFE', '\xF7', '\xF6', '\xF6', '\xF6', '\xF5', '\xF4', '\xF4', '\xF6', '\xF5', '\xF4', '\xF3', '\xF2', '\xF1', '\xF0', '\xF0' }; TS_ASSERT_EQUALS(str.str(), expected); int8_t i8; int16_t i16; int32_t i32; int64_t i64; read_little(str, i8); TS_ASSERT_EQUALS(i8, -0x01); read_little(str, i16); TS_ASSERT_EQUALS(i16, -0x0102); read(str, i32, little); TS_ASSERT_EQUALS(i32, -0x01020304); read_little(str, i64); TS_ASSERT_EQUALS(i64, -0x0102030405060708); read_big(str, i8); TS_ASSERT_EQUALS(i8, -0x09); read_big(str, i16); TS_ASSERT_EQUALS(i16, -0x090A); read_big(str, i32); TS_ASSERT_EQUALS(i32, -0x090A0B0C); read(str, i64, big); TS_ASSERT_EQUALS(i64, -0x090A0B0C0D0E0F10); TS_ASSERT(str); //Check if stream has failed } void test_float() { std::stringstream str(std::ios::in | std::ios::out | std::ios::binary); //C99 has hexadecimal floating point literals, C++ doesn't... const float fconst = std::stof("-0xCDEF01p-63"); //-1.46325e-012 const double dconst = std::stod("-0x1DEF0102030405p-375"); //-1.09484e-097 //We will be assuming IEEE 754 here write_little(str, fconst); write_little(str, dconst); write_big (str, fconst); write_big (str, dconst); std::string expected{ '\x01', '\xEF', '\xCD', '\xAB', '\x05', '\x04', '\x03', '\x02', '\x01', '\xEF', '\xCD', '\xAB', '\xAB', '\xCD', '\xEF', '\x01', '\xAB', '\xCD', '\xEF', '\x01', '\x02', '\x03', '\x04', '\x05' }; TS_ASSERT_EQUALS(str.str(), expected); float f; double d; read_little(str, f); TS_ASSERT_EQUALS(f, fconst); read_little(str, d); TS_ASSERT_EQUALS(d, dconst); read_big(str, f); TS_ASSERT_EQUALS(f, fconst); read_big(str, d); TS_ASSERT_EQUALS(d, dconst); TS_ASSERT(str); //Check if stream has failed } }; PrismLauncher-10.0.5/libraries/libnbtplusplus/test/write_test.h0000644000175100017510000002200215144136757024347 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include #include "io/stream_writer.h" #include "io/stream_reader.h" #ifdef NBT_HAVE_ZLIB #include "io/ozlibstream.h" #include "io/izlibstream.h" #endif #include "nbt_tags.h" #include #include #include #include "data.h" using namespace nbt; class read_test : public CxxTest::TestSuite { public: void test_stream_writer_big() { std::ostringstream os; nbt::io::stream_writer writer(os); TS_ASSERT_EQUALS(&writer.get_ostr(), &os); TS_ASSERT_EQUALS(writer.get_endian(), endian::big); writer.write_type(tag_type::End); writer.write_type(tag_type::Long); writer.write_type(tag_type::Int_Array); writer.write_num(int64_t(0x0102030405060708)); writer.write_string("foobar"); TS_ASSERT(os); std::string expected{ 0, //tag_type::End 4, //tag_type::Long 11, //tag_type::Int_Array 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, //0x0102030405060708 in Big Endian 0x00, 0x06, //string length in Big Endian 'f', 'o', 'o', 'b', 'a', 'r' }; TS_ASSERT_EQUALS(os.str(), expected); //too long for NBT TS_ASSERT_THROWS(writer.write_string(std::string(65536, '.')), std::length_error); TS_ASSERT(!os); } void test_stream_writer_little() { std::ostringstream os; nbt::io::stream_writer writer(os, endian::little); TS_ASSERT_EQUALS(writer.get_endian(), endian::little); writer.write_num(int32_t(0x0a0b0c0d)); writer.write_string("foobar"); TS_ASSERT(os); std::string expected{ 0x0d, 0x0c, 0x0b, 0x0a, //0x0a0b0c0d in Little Endian 0x06, 0x00, //string length in Little Endian 'f', 'o', 'o', 'b', 'a', 'r' }; TS_ASSERT_EQUALS(os.str(), expected); TS_ASSERT_THROWS(writer.write_string(std::string(65536, '.')), std::length_error); TS_ASSERT(!os); } void test_write_payload_big() { std::ostringstream os; nbt::io::stream_writer writer(os); //tag_primitive writer.write_payload(tag_byte(127)); writer.write_payload(tag_short(32767)); writer.write_payload(tag_int(2147483647)); writer.write_payload(tag_long(9223372036854775807)); //Same values as in endian_str_test writer.write_payload(tag_float(std::stof("-0xCDEF01p-63"))); writer.write_payload(tag_double(std::stod("-0x1DEF0102030405p-375"))); TS_ASSERT_EQUALS(os.str(), (std::string{ '\x7F', '\x7F', '\xFF', '\x7F', '\xFF', '\xFF', '\xFF', '\x7F', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xAB', '\xCD', '\xEF', '\x01', '\xAB', '\xCD', '\xEF', '\x01', '\x02', '\x03', '\x04', '\x05' })); os.str(""); //clear and reuse the stream //tag_string writer.write_payload(tag_string("barbaz")); TS_ASSERT_EQUALS(os.str(), (std::string{ 0x00, 0x06, //string length in Big Endian 'b', 'a', 'r', 'b', 'a', 'z' })); TS_ASSERT_THROWS(writer.write_payload(tag_string(std::string(65536, '.'))), std::length_error); TS_ASSERT(!os); os.clear(); //tag_byte_array os.str(""); writer.write_payload(tag_byte_array{0, 1, 127, -128, -127}); TS_ASSERT_EQUALS(os.str(), (std::string{ 0x00, 0x00, 0x00, 0x05, //length in Big Endian 0, 1, 127, -128, -127 })); os.str(""); //tag_int_array writer.write_payload(tag_int_array{0x01020304, 0x05060708, 0x090a0b0c}); TS_ASSERT_EQUALS(os.str(), (std::string{ 0x00, 0x00, 0x00, 0x03, //length in Big Endian 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c })); os.str(""); //tag_list writer.write_payload(tag_list()); //empty list with undetermined type, should be written as list of tag_end writer.write_payload(tag_list(tag_type::Int)); //empty list of tag_int writer.write_payload(tag_list{ //nested list tag_list::of({0x3456, 0x789a}), tag_list::of({0x0a, 0x0b, 0x0c, 0x0d}) }); TS_ASSERT_EQUALS(os.str(), (std::string{ 0, //tag_type::End 0x00, 0x00, 0x00, 0x00, //length 3, //tag_type::Int 0x00, 0x00, 0x00, 0x00, //length 9, //tag_type::List 0x00, 0x00, 0x00, 0x02, //length //list 0 2, //tag_type::Short 0x00, 0x00, 0x00, 0x02, //length '\x34', '\x56', '\x78', '\x9a', //list 1 1, //tag_type::Byte 0x00, 0x00, 0x00, 0x04, //length 0x0a, 0x0b, 0x0c, 0x0d })); os.str(""); //tag_compound /* Testing if writing compounds works properly is problematic because the order of the tags is not guaranteed. However with only two tags in a compound we only have two possible orderings. See below for a more thorough test that uses writing and re-reading. */ writer.write_payload(tag_compound{}); writer.write_payload(tag_compound{ {"foo", "quux"}, {"bar", tag_int(0x789abcde)} }); std::string endtag{0x00}; std::string subtag1{ 8, //tag_type::String 0x00, 0x03, //key length 'f', 'o', 'o', 0x00, 0x04, //string length 'q', 'u', 'u', 'x' }; std::string subtag2{ 3, //tag_type::Int 0x00, 0x03, //key length 'b', 'a', 'r', '\x78', '\x9A', '\xBC', '\xDE' }; TS_ASSERT(os.str() == endtag + subtag1 + subtag2 + endtag || os.str() == endtag + subtag2 + subtag1 + endtag); //Now for write_tag: os.str(""); writer.write_tag("foo", tag_string("quux")); TS_ASSERT_EQUALS(os.str(), subtag1); TS_ASSERT(os); //too long key for NBT TS_ASSERT_THROWS(writer.write_tag(std::string(65536, '.'), tag_long(-1)), std::length_error); TS_ASSERT(!os); } void test_write_bigtest() { /* Like already stated above, because no order is guaranteed for tag_compound, we cannot simply test it by writing into a stream and directly comparing the output to a reference value. Instead, we assume that reading already works correctly and re-read the written tag. Smaller-grained tests are already done above. */ std::string input(__binary_bigtest_uncompr_start, __binary_bigtest_uncompr_end); std::istringstream file(input, std::ios::binary); const auto orig_pair = io::read_compound(file); std::stringstream sstr; //Write into stream in Big Endian io::write_tag(orig_pair.first, *orig_pair.second, sstr); TS_ASSERT(sstr); //Read from stream in Big Endian and compare auto written_pair = io::read_compound(sstr); TS_ASSERT_EQUALS(orig_pair.first, written_pair.first); TS_ASSERT(*orig_pair.second == *written_pair.second); sstr.str(""); //Reset and reuse stream //Write into stream in Little Endian io::write_tag(orig_pair.first, *orig_pair.second, sstr, endian::little); TS_ASSERT(sstr); //Read from stream in Little Endian and compare written_pair = io::read_compound(sstr, endian::little); TS_ASSERT_EQUALS(orig_pair.first, written_pair.first); TS_ASSERT(*orig_pair.second == *written_pair.second); #ifdef NBT_HAVE_ZLIB //Now with gzip compression sstr.str(""); zlib::ozlibstream ogzs(sstr, -1, true); io::write_tag(orig_pair.first, *orig_pair.second, ogzs); ogzs.close(); TS_ASSERT(ogzs); TS_ASSERT(sstr); //Read and compare zlib::izlibstream igzs(sstr); written_pair = io::read_compound(igzs); TS_ASSERT(igzs); TS_ASSERT_EQUALS(orig_pair.first, written_pair.first); TS_ASSERT(*orig_pair.second == *written_pair.second); #endif } }; PrismLauncher-10.0.5/libraries/libnbtplusplus/test/format_test.cpp0000644000175100017510000000565115144136757025053 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ //#include "text/json_formatter.h" //#include "io/stream_reader.h" #include #include #include #include "nbt_tags.h" using namespace nbt; int main() { //TODO: Write that into a file tag_compound comp{ {"byte", tag_byte(-128)}, {"short", tag_short(-32768)}, {"int", tag_int(-2147483648)}, {"long", tag_long(-9223372036854775808U)}, {"float 1", 1.618034f}, {"float 2", 6.626070e-34f}, {"float 3", 2.273737e+29f}, {"float 4", -std::numeric_limits::infinity()}, {"float 5", std::numeric_limits::quiet_NaN()}, {"double 1", 3.141592653589793}, {"double 2", 1.749899444387479e-193}, {"double 3", 2.850825855152578e+175}, {"double 4", -std::numeric_limits::infinity()}, {"double 5", std::numeric_limits::quiet_NaN()}, {"string 1", "Hello World! \u00E4\u00F6\u00FC\u00DF"}, {"string 2", "String with\nline breaks\tand tabs"}, {"byte array", tag_byte_array{12, 13, 14, 15, 16}}, {"int array", tag_int_array{0x0badc0de, -0x0dedbeef, 0x1badbabe}}, {"long array", tag_long_array{0x0badc0de0badc0de, -0x0dedbeef0dedbeef, 0x1badbabe1badbabe}}, {"list (empty)", tag_list::of({})}, {"list (float)", tag_list{2.0f, 1.0f, 0.5f, 0.25f}}, {"list (list)", tag_list::of({ {}, {4, 5, 6}, {tag_compound{{"egg", "ham"}}, tag_compound{{"foo", "bar"}}} })}, {"list (compound)", tag_list::of({ {{"created-on", 42}, {"names", tag_list{"Compound", "tag", "#0"}}}, {{"created-on", 45}, {"names", tag_list{"Compound", "tag", "#1"}}} })}, {"compound (empty)", tag_compound()}, {"compound (nested)", tag_compound{ {"key", "value"}, {"key with \u00E4\u00F6\u00FC", tag_byte(-1)}, {"key with\nnewline and\ttab", tag_compound{}} }}, {"null", nullptr} }; std::cout << "----- default operator<<:\n"; std::cout << comp; std::cout << "\n-----" << std::endl; } PrismLauncher-10.0.5/libraries/libnbtplusplus/test/zlibstream_test.h0000644000175100017510000002162115144136757025377 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include #include "io/izlibstream.h" #include "io/ozlibstream.h" #include #include #include "data.h" using namespace zlib; class zlibstream_test : public CxxTest::TestSuite { private: std::string bigtest; public: zlibstream_test() { std::string input(__binary_bigtest_uncompr_start, __binary_bigtest_uncompr_end); std::istringstream bigtest_f(input, std::ios::binary); std::stringbuf bigtest_b; bigtest_f >> &bigtest_b; bigtest = bigtest_b.str(); if(!bigtest_f || bigtest.size() == 0) throw std::runtime_error("Could not read bigtest_uncompr file"); } void test_inflate_gzip() { std::string input(__binary_bigtest_nbt_start, __binary_bigtest_nbt_end); std::istringstream gzip_in(input, std::ios::binary); TS_ASSERT(gzip_in); std::stringbuf data; //Small buffer so not all fits at once (the compressed file is 561 bytes) { izlibstream igzs(gzip_in, 256); igzs.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT(igzs.good()); TS_ASSERT_THROWS_NOTHING(igzs >> &data); TS_ASSERT(igzs); TS_ASSERT(igzs.eof()); TS_ASSERT_EQUALS(data.str(), bigtest); } //Clear and reuse buffers data.str(""); gzip_in.clear(); gzip_in.seekg(0); //Now try the same with larger buffer (but not large enough for all output, uncompressed size 1561 bytes) { izlibstream igzs(gzip_in, 1000); igzs.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT(igzs.good()); TS_ASSERT_THROWS_NOTHING(igzs >> &data); TS_ASSERT(igzs); TS_ASSERT(igzs.eof()); TS_ASSERT_EQUALS(data.str(), bigtest); } data.str(""); gzip_in.clear(); gzip_in.seekg(0); //Now with large buffer { izlibstream igzs(gzip_in, 4000); igzs.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT(igzs.good()); TS_ASSERT_THROWS_NOTHING(igzs >> &data); TS_ASSERT(igzs); TS_ASSERT(igzs.eof()); TS_ASSERT_EQUALS(data.str(), bigtest); } } void test_inflate_zlib() { std::string input(__binary_bigtest_zlib_start, __binary_bigtest_zlib_end); std::istringstream zlib_in(input, std::ios::binary); TS_ASSERT(zlib_in); std::stringbuf data; izlibstream izls(zlib_in, 256); izls.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT(izls.good()); TS_ASSERT_THROWS_NOTHING(izls >> &data); TS_ASSERT(izls); TS_ASSERT(izls.eof()); TS_ASSERT_EQUALS(data.str(), bigtest); } void test_inflate_corrupt() { std::string input(__binary_bigtest_corrupt_nbt_start, __binary_bigtest_corrupt_nbt_end); std::istringstream gzip_in(input, std::ios::binary); TS_ASSERT(gzip_in); std::vector buf(bigtest.size()); izlibstream igzs(gzip_in); igzs.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT_THROWS(igzs.read(buf.data(), buf.size()), zlib_error); TS_ASSERT(igzs.bad()); } void test_inflate_eof() { std::string input(__binary_bigtest_eof_nbt_start, __binary_bigtest_eof_nbt_end); std::istringstream gzip_in(input, std::ios::binary); TS_ASSERT(gzip_in); std::vector buf(bigtest.size()); izlibstream igzs(gzip_in); igzs.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT_THROWS(igzs.read(buf.data(), buf.size()), zlib_error); TS_ASSERT(igzs.bad()); } void test_inflate_trailing() { //This file contains additional uncompressed data after the zlib-compressed data std::string input(__binary_trailing_data_zlib_start, __binary_trailing_data_zlib_end); std::istringstream file(input, std::ios::binary); izlibstream izls(file, 32); TS_ASSERT(file && izls); std::string str; izls >> str; TS_ASSERT(izls); TS_ASSERT(izls.eof()); TS_ASSERT_EQUALS(str, "foobar"); //Now read the uncompressed data TS_ASSERT(file); TS_ASSERT(!file.eof()); file >> str; TS_ASSERT(!file.bad()); TS_ASSERT_EQUALS(str, "barbaz"); } void test_deflate_zlib() { //Here we assume that inflating works and has already been tested std::stringstream str; std::stringbuf output; //Small buffer { ozlibstream ozls(str, -1, false, 256); ozls.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT_THROWS_NOTHING(ozls << bigtest); TS_ASSERT(ozls.good()); TS_ASSERT_THROWS_NOTHING(ozls.close()); TS_ASSERT(ozls.good()); } TS_ASSERT(str.good()); { izlibstream izls(str); TS_ASSERT_THROWS_NOTHING(izls >> &output); TS_ASSERT(izls); } TS_ASSERT_EQUALS(output.str(), bigtest); str.clear(); str.str(""); output.str(""); //Medium sized buffer //Write first half, then flush and write second half { ozlibstream ozls(str, 9, false, 512); ozls.exceptions(std::ios::failbit | std::ios::badbit); std::string half1 = bigtest.substr(0, bigtest.size()/2); std::string half2 = bigtest.substr(bigtest.size()/2); TS_ASSERT_THROWS_NOTHING(ozls << half1 << std::flush << half2); TS_ASSERT(ozls.good()); TS_ASSERT_THROWS_NOTHING(ozls.close()); TS_ASSERT(ozls.good()); } TS_ASSERT(str.good()); { izlibstream izls(str); izls >> &output; TS_ASSERT(izls); } TS_ASSERT_EQUALS(output.str(), bigtest); str.clear(); str.str(""); output.str(""); //Large buffer { ozlibstream ozls(str, 1, false, 4000); ozls.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT_THROWS_NOTHING(ozls << bigtest); TS_ASSERT(ozls.good()); TS_ASSERT_THROWS_NOTHING(ozls.close()); TS_ASSERT_THROWS_NOTHING(ozls.close()); //closing twice shouldn't be a problem TS_ASSERT(ozls.good()); } TS_ASSERT(str.good()); { izlibstream izls(str); izls >> &output; TS_ASSERT(izls); } TS_ASSERT_EQUALS(output.str(), bigtest); } void test_deflate_gzip() { std::stringstream str; std::stringbuf output; { ozlibstream ozls(str, -1, true); ozls.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT_THROWS_NOTHING(ozls << bigtest); TS_ASSERT(ozls.good()); TS_ASSERT_THROWS_NOTHING(ozls.close()); TS_ASSERT(ozls.good()); } TS_ASSERT(str.good()); { izlibstream izls(str); izls >> &output; TS_ASSERT(izls); } TS_ASSERT_EQUALS(output.str(), bigtest); } void test_deflate_closed() { std::stringstream str; { ozlibstream ozls(str); ozls.exceptions(std::ios::failbit | std::ios::badbit); TS_ASSERT_THROWS_NOTHING(ozls << bigtest); TS_ASSERT_THROWS_NOTHING(ozls.close()); TS_ASSERT_THROWS_NOTHING(ozls << "foo"); TS_ASSERT_THROWS_ANYTHING(ozls.close()); TS_ASSERT(ozls.bad()); TS_ASSERT(!str); } str.clear(); str.seekp(0); { ozlibstream ozls(str); //this time without exceptions TS_ASSERT_THROWS_NOTHING(ozls << bigtest); TS_ASSERT_THROWS_NOTHING(ozls.close()); TS_ASSERT_THROWS_NOTHING(ozls << "foo" << std::flush); TS_ASSERT(ozls.bad()); TS_ASSERT_THROWS_NOTHING(ozls.close()); TS_ASSERT(ozls.bad()); TS_ASSERT(!str); } } }; PrismLauncher-10.0.5/libraries/libnbtplusplus/src/0000755000175100017510000000000015144136757021621 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/src/value.cpp0000644000175100017510000002022115144136757023436 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "value.h" #include "nbt_tags.h" #include namespace nbt { value::value(tag&& t): tag_(std::move(t).move_clone()) {} value::value(const value& rhs): tag_(rhs.tag_ ? rhs.tag_->clone() : nullptr) {} value& value::operator=(const value& rhs) { if(this != &rhs) { tag_ = rhs.tag_ ? rhs.tag_->clone() : nullptr; } return *this; } value& value::operator=(tag&& t) { set(std::move(t)); return *this; } void value::set(tag&& t) { if(tag_) tag_->assign(std::move(t)); else tag_ = std::move(t).move_clone(); } //Primitive assignment //FIXME: Make this less copypaste! value& value::operator=(int8_t val) { if(!tag_) set(tag_byte(val)); else switch(tag_->get_type()) { case tag_type::Byte: static_cast(*tag_).set(val); break; case tag_type::Short: static_cast(*tag_).set(val); break; case tag_type::Int: static_cast(*tag_).set(val); break; case tag_type::Long: static_cast(*tag_).set(val); break; case tag_type::Float: static_cast(*tag_).set(val); break; case tag_type::Double: static_cast(*tag_).set(val); break; default: throw std::bad_cast(); } return *this; } value& value::operator=(int16_t val) { if(!tag_) set(tag_short(val)); else switch(tag_->get_type()) { case tag_type::Short: static_cast(*tag_).set(val); break; case tag_type::Int: static_cast(*tag_).set(val); break; case tag_type::Long: static_cast(*tag_).set(val); break; case tag_type::Float: static_cast(*tag_).set(val); break; case tag_type::Double: static_cast(*tag_).set(val); break; default: throw std::bad_cast(); } return *this; } value& value::operator=(int32_t val) { if(!tag_) set(tag_int(val)); else switch(tag_->get_type()) { case tag_type::Int: static_cast(*tag_).set(val); break; case tag_type::Long: static_cast(*tag_).set(val); break; case tag_type::Float: static_cast(*tag_).set(val); break; case tag_type::Double: static_cast(*tag_).set(val); break; default: throw std::bad_cast(); } return *this; } value& value::operator=(int64_t val) { if(!tag_) set(tag_long(val)); else switch(tag_->get_type()) { case tag_type::Long: static_cast(*tag_).set(val); break; case tag_type::Float: static_cast(*tag_).set(val); break; case tag_type::Double: static_cast(*tag_).set(val); break; default: throw std::bad_cast(); } return *this; } value& value::operator=(float val) { if(!tag_) set(tag_float(val)); else switch(tag_->get_type()) { case tag_type::Float: static_cast(*tag_).set(val); break; case tag_type::Double: static_cast(*tag_).set(val); break; default: throw std::bad_cast(); } return *this; } value& value::operator=(double val) { if(!tag_) set(tag_double(val)); else switch(tag_->get_type()) { case tag_type::Double: static_cast(*tag_).set(val); break; default: throw std::bad_cast(); } return *this; } //Primitive conversion value::operator int8_t() const { switch(tag_->get_type()) { case tag_type::Byte: return static_cast(*tag_).get(); default: throw std::bad_cast(); } } value::operator int16_t() const { switch(tag_->get_type()) { case tag_type::Byte: return static_cast(*tag_).get(); case tag_type::Short: return static_cast(*tag_).get(); default: throw std::bad_cast(); } } value::operator int32_t() const { switch(tag_->get_type()) { case tag_type::Byte: return static_cast(*tag_).get(); case tag_type::Short: return static_cast(*tag_).get(); case tag_type::Int: return static_cast(*tag_).get(); default: throw std::bad_cast(); } } value::operator int64_t() const { switch(tag_->get_type()) { case tag_type::Byte: return static_cast(*tag_).get(); case tag_type::Short: return static_cast(*tag_).get(); case tag_type::Int: return static_cast(*tag_).get(); case tag_type::Long: return static_cast(*tag_).get(); default: throw std::bad_cast(); } } value::operator float() const { switch(tag_->get_type()) { case tag_type::Byte: return static_cast(*tag_).get(); case tag_type::Short: return static_cast(*tag_).get(); case tag_type::Int: return static_cast(*tag_).get(); case tag_type::Long: return static_cast(*tag_).get(); case tag_type::Float: return static_cast(*tag_).get(); default: throw std::bad_cast(); } } value::operator double() const { switch(tag_->get_type()) { case tag_type::Byte: return static_cast(*tag_).get(); case tag_type::Short: return static_cast(*tag_).get(); case tag_type::Int: return static_cast(*tag_).get(); case tag_type::Long: return static_cast(*tag_).get(); case tag_type::Float: return static_cast(*tag_).get(); case tag_type::Double: return static_cast(*tag_).get(); default: throw std::bad_cast(); } } value& value::operator=(std::string&& str) { if(!tag_) set(tag_string(std::move(str))); else dynamic_cast(*tag_).set(std::move(str)); return *this; } value::operator const std::string&() const { return dynamic_cast(*tag_).get(); } value& value::at(const std::string& key) { return dynamic_cast(*tag_).at(key); } const value& value::at(const std::string& key) const { return dynamic_cast(*tag_).at(key); } value& value::operator[](const std::string& key) { return dynamic_cast(*tag_)[key]; } value& value::operator[](const char* key) { return (*this)[std::string(key)]; } value& value::at(size_t i) { return dynamic_cast(*tag_).at(i); } const value& value::at(size_t i) const { return dynamic_cast(*tag_).at(i); } value& value::operator[](size_t i) { return dynamic_cast(*tag_)[i]; } const value& value::operator[](size_t i) const { return dynamic_cast(*tag_)[i]; } tag_type value::get_type() const { return tag_ ? tag_->get_type() : tag_type::Null; } bool operator==(const value& lhs, const value& rhs) { if(lhs.tag_ != nullptr && rhs.tag_ != nullptr) return *lhs.tag_ == *rhs.tag_; else return lhs.tag_ == nullptr && rhs.tag_ == nullptr; } bool operator!=(const value& lhs, const value& rhs) { return !(lhs == rhs); } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/endian_str.cpp0000644000175100017510000001701615144136757024460 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "endian_str.h" #include #include #include static_assert(CHAR_BIT == 8, "Assuming that a byte has 8 bits"); static_assert(sizeof(float) == 4, "Assuming that a float is 4 byte long"); static_assert(sizeof(double) == 8, "Assuming that a double is 8 byte long"); namespace endian { namespace //anonymous { void pun_int_to_float(float& f, uint32_t i) { //Yes we need to do it this way to avoid undefined behavior memcpy(&f, &i, 4); } uint32_t pun_float_to_int(float f) { uint32_t ret; memcpy(&ret, &f, 4); return ret; } void pun_int_to_double(double& d, uint64_t i) { memcpy(&d, &i, 8); } uint64_t pun_double_to_int(double f) { uint64_t ret; memcpy(&ret, &f, 8); return ret; } } //------------------------------------------------------------------------------ void read_little(std::istream& is, uint8_t& x) { is.get(reinterpret_cast(x)); } void read_little(std::istream& is, uint16_t& x) { uint8_t tmp[2]; is.read(reinterpret_cast(tmp), 2); x = uint16_t(tmp[0]) | (uint16_t(tmp[1]) << 8); } void read_little(std::istream& is, uint32_t& x) { uint8_t tmp[4]; is.read(reinterpret_cast(tmp), 4); x = uint32_t(tmp[0]) | (uint32_t(tmp[1]) << 8) | (uint32_t(tmp[2]) << 16) | (uint32_t(tmp[3]) << 24); } void read_little(std::istream& is, uint64_t& x) { uint8_t tmp[8]; is.read(reinterpret_cast(tmp), 8); x = uint64_t(tmp[0]) | (uint64_t(tmp[1]) << 8) | (uint64_t(tmp[2]) << 16) | (uint64_t(tmp[3]) << 24) | (uint64_t(tmp[4]) << 32) | (uint64_t(tmp[5]) << 40) | (uint64_t(tmp[6]) << 48) | (uint64_t(tmp[7]) << 56); } void read_little(std::istream& is, int8_t & x) { read_little(is, reinterpret_cast(x)); } void read_little(std::istream& is, int16_t& x) { read_little(is, reinterpret_cast(x)); } void read_little(std::istream& is, int32_t& x) { read_little(is, reinterpret_cast(x)); } void read_little(std::istream& is, int64_t& x) { read_little(is, reinterpret_cast(x)); } void read_little(std::istream& is, float& x) { uint32_t tmp; read_little(is, tmp); pun_int_to_float(x, tmp); } void read_little(std::istream& is, double& x) { uint64_t tmp; read_little(is, tmp); pun_int_to_double(x, tmp); } //------------------------------------------------------------------------------ void read_big(std::istream& is, uint8_t& x) { is.read(reinterpret_cast(&x), 1); } void read_big(std::istream& is, uint16_t& x) { uint8_t tmp[2]; is.read(reinterpret_cast(tmp), 2); x = uint16_t(tmp[1]) | (uint16_t(tmp[0]) << 8); } void read_big(std::istream& is, uint32_t& x) { uint8_t tmp[4]; is.read(reinterpret_cast(tmp), 4); x = uint32_t(tmp[3]) | (uint32_t(tmp[2]) << 8) | (uint32_t(tmp[1]) << 16) | (uint32_t(tmp[0]) << 24); } void read_big(std::istream& is, uint64_t& x) { uint8_t tmp[8]; is.read(reinterpret_cast(tmp), 8); x = uint64_t(tmp[7]) | (uint64_t(tmp[6]) << 8) | (uint64_t(tmp[5]) << 16) | (uint64_t(tmp[4]) << 24) | (uint64_t(tmp[3]) << 32) | (uint64_t(tmp[2]) << 40) | (uint64_t(tmp[1]) << 48) | (uint64_t(tmp[0]) << 56); } void read_big(std::istream& is, int8_t & x) { read_big(is, reinterpret_cast(x)); } void read_big(std::istream& is, int16_t& x) { read_big(is, reinterpret_cast(x)); } void read_big(std::istream& is, int32_t& x) { read_big(is, reinterpret_cast(x)); } void read_big(std::istream& is, int64_t& x) { read_big(is, reinterpret_cast(x)); } void read_big(std::istream& is, float& x) { uint32_t tmp; read_big(is, tmp); pun_int_to_float(x, tmp); } void read_big(std::istream& is, double& x) { uint64_t tmp; read_big(is, tmp); pun_int_to_double(x, tmp); } //------------------------------------------------------------------------------ void write_little(std::ostream& os, uint8_t x) { os.put(x); } void write_little(std::ostream& os, uint16_t x) { uint8_t tmp[2] { uint8_t(x), uint8_t(x >> 8)}; os.write(reinterpret_cast(tmp), 2); } void write_little(std::ostream& os, uint32_t x) { uint8_t tmp[4] { uint8_t(x), uint8_t(x >> 8), uint8_t(x >> 16), uint8_t(x >> 24)}; os.write(reinterpret_cast(tmp), 4); } void write_little(std::ostream& os, uint64_t x) { uint8_t tmp[8] { uint8_t(x), uint8_t(x >> 8), uint8_t(x >> 16), uint8_t(x >> 24), uint8_t(x >> 32), uint8_t(x >> 40), uint8_t(x >> 48), uint8_t(x >> 56)}; os.write(reinterpret_cast(tmp), 8); } void write_little(std::ostream& os, int8_t x) { write_little(os, static_cast(x)); } void write_little(std::ostream& os, int16_t x) { write_little(os, static_cast(x)); } void write_little(std::ostream& os, int32_t x) { write_little(os, static_cast(x)); } void write_little(std::ostream& os, int64_t x) { write_little(os, static_cast(x)); } void write_little(std::ostream& os, float x) { write_little(os, pun_float_to_int(x)); } void write_little(std::ostream& os, double x) { write_little(os, pun_double_to_int(x)); } //------------------------------------------------------------------------------ void write_big(std::ostream& os, uint8_t x) { os.put(x); } void write_big(std::ostream& os, uint16_t x) { uint8_t tmp[2] { uint8_t(x >> 8), uint8_t(x)}; os.write(reinterpret_cast(tmp), 2); } void write_big(std::ostream& os, uint32_t x) { uint8_t tmp[4] { uint8_t(x >> 24), uint8_t(x >> 16), uint8_t(x >> 8), uint8_t(x)}; os.write(reinterpret_cast(tmp), 4); } void write_big(std::ostream& os, uint64_t x) { uint8_t tmp[8] { uint8_t(x >> 56), uint8_t(x >> 48), uint8_t(x >> 40), uint8_t(x >> 32), uint8_t(x >> 24), uint8_t(x >> 16), uint8_t(x >> 8), uint8_t(x)}; os.write(reinterpret_cast(tmp), 8); } void write_big(std::ostream& os, int8_t x) { write_big(os, static_cast(x)); } void write_big(std::ostream& os, int16_t x) { write_big(os, static_cast(x)); } void write_big(std::ostream& os, int32_t x) { write_big(os, static_cast(x)); } void write_big(std::ostream& os, int64_t x) { write_big(os, static_cast(x)); } void write_big(std::ostream& os, float x) { write_big(os, pun_float_to_int(x)); } void write_big(std::ostream& os, double x) { write_big(os, pun_double_to_int(x)); } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/tag.cpp0000644000175100017510000000674515144136757023114 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "tag.h" #include "nbt_tags.h" #include "text/json_formatter.h" #include #include #include #include namespace nbt { static_assert(std::numeric_limits::is_iec559 && std::numeric_limits::is_iec559, "The floating point values for NBT must conform to IEC 559/IEEE 754"); bool is_valid_type(int type, bool allow_end) { return (allow_end ? 0 : 1) <= type && type <= 12; } std::unique_ptr tag::clone() && { return std::move(*this).move_clone(); } std::unique_ptr tag::create(tag_type type) { switch(type) { case tag_type::Byte: return make_unique(); case tag_type::Short: return make_unique(); case tag_type::Int: return make_unique(); case tag_type::Long: return make_unique(); case tag_type::Float: return make_unique(); case tag_type::Double: return make_unique(); case tag_type::Byte_Array: return make_unique(); case tag_type::String: return make_unique(); case tag_type::List: return make_unique(); case tag_type::Compound: return make_unique(); case tag_type::Int_Array: return make_unique(); case tag_type::Long_Array: return make_unique(); default: throw std::invalid_argument("Invalid tag type"); } } bool operator==(const tag& lhs, const tag& rhs) { if(typeid(lhs) != typeid(rhs)) return false; return lhs.equals(rhs); } bool operator!=(const tag& lhs, const tag& rhs) { return !(lhs == rhs); } std::ostream& operator<<(std::ostream& os, tag_type tt) { switch(tt) { case tag_type::End: return os << "end"; case tag_type::Byte: return os << "byte"; case tag_type::Short: return os << "short"; case tag_type::Int: return os << "int"; case tag_type::Long: return os << "long"; case tag_type::Float: return os << "float"; case tag_type::Double: return os << "double"; case tag_type::Byte_Array: return os << "byte_array"; case tag_type::String: return os << "string"; case tag_type::List: return os << "list"; case tag_type::Compound: return os << "compound"; case tag_type::Int_Array: return os << "int_array"; case tag_type::Long_Array: return os << "long_array"; case tag_type::Null: return os << "null"; default: return os << "invalid"; } } std::ostream& operator<<(std::ostream& os, const tag& t) { static const text::json_formatter formatter; formatter.print(os, t); return os; } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/tag_compound.cpp0000644000175100017510000000553415144136757025013 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "tag_compound.h" #include "io/stream_reader.h" #include "io/stream_writer.h" #include #include namespace nbt { tag_compound::tag_compound(std::initializer_list> init) { for(const auto& pair: init) tags.emplace(std::move(pair.first), std::move(pair.second)); } value& tag_compound::at(const std::string& key) { return tags.at(key); } const value& tag_compound::at(const std::string& key) const { return tags.at(key); } std::pair tag_compound::put(const std::string& key, value_initializer&& val) { auto it = tags.find(key); if(it != tags.end()) { it->second = std::move(val); return {it, false}; } else { return tags.emplace(key, std::move(val)); } } std::pair tag_compound::insert(const std::string& key, value_initializer&& val) { return tags.emplace(key, std::move(val)); } bool tag_compound::erase(const std::string& key) { return tags.erase(key) != 0; } bool tag_compound::has_key(const std::string& key) const { return tags.find(key) != tags.end(); } bool tag_compound::has_key(const std::string& key, tag_type type) const { auto it = tags.find(key); return it != tags.end() && it->second.get_type() == type; } void tag_compound::read_payload(io::stream_reader& reader) { clear(); tag_type tt; while((tt = reader.read_type(true)) != tag_type::End) { std::string key; try { key = reader.read_string(); } catch(io::input_error& ex) { std::ostringstream str; str << "Error reading key of tag_" << tt; throw io::input_error(str.str()); } auto tptr = reader.read_payload(tt); tags.emplace(std::move(key), value(std::move(tptr))); } } void tag_compound::write_payload(io::stream_writer& writer) const { for(const auto& pair: tags) writer.write_tag(pair.first, pair.second); writer.write_type(tag_type::End); } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/text/0000755000175100017510000000000015144136757022605 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/src/text/json_formatter.cpp0000644000175100017510000001276415144136757026357 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "text/json_formatter.h" #include "nbt_tags.h" #include "nbt_visitor.h" #include #include #include namespace nbt { namespace text { namespace //anonymous { ///Helper class which uses the Visitor pattern to pretty-print tags class json_fmt_visitor : public const_nbt_visitor { public: json_fmt_visitor(std::ostream& os): os(os) {} void visit(const tag_byte& b) override { os << static_cast(b.get()) << "b"; } //We don't want to print a character void visit(const tag_short& s) override { os << s.get() << "s"; } void visit(const tag_int& i) override { os << i.get(); } void visit(const tag_long& l) override { os << l.get() << "l"; } void visit(const tag_float& f) override { write_float(f.get()); os << "f"; } void visit(const tag_double& d) override { write_float(d.get()); os << "d"; } void visit(const tag_byte_array& ba) override { os << "[" << ba.size() << " bytes]"; } void visit(const tag_string& s) override { os << '"' << s.get() << '"'; } //TODO: escape special characters void visit(const tag_list& l) override { //Wrap lines for lists of lists or compounds. //Lists of other types can usually be on one line without problem. const bool break_lines = l.size() > 0 && (l.el_type() == tag_type::List || l.el_type() == tag_type::Compound); os << "["; if(break_lines) { os << "\n"; ++indent_lvl; for(unsigned int i = 0; i < l.size(); ++i) { indent(); if(l[i]) l[i].get().accept(*this); else write_null(); if(i != l.size()-1) os << ","; os << "\n"; } --indent_lvl; indent(); } else { for(unsigned int i = 0; i < l.size(); ++i) { if(l[i]) l[i].get().accept(*this); else write_null(); if(i != l.size()-1) os << ", "; } } os << "]"; } void visit(const tag_compound& c) override { if(c.size() == 0) //No line breaks inside empty compounds please { os << "{}"; return; } os << "{\n"; ++indent_lvl; unsigned int i = 0; for(const auto& kv: c) { indent(); os << kv.first << ": "; if(kv.second) kv.second.get().accept(*this); else write_null(); if(i != c.size()-1) os << ","; os << "\n"; ++i; } --indent_lvl; indent(); os << "}"; } void visit(const tag_int_array& ia) override { os << "["; for(unsigned int i = 0; i < ia.size(); ++i) { os << ia[i]; if(i != ia.size()-1) os << ", "; } os << "]"; } void visit(const tag_long_array& la) override { os << "["; for(unsigned int i = 0; i < la.size(); ++i) { os << la[i]; if(i != la.size()-1) os << ", "; } os << "]"; } private: const std::string indent_str = " "; std::ostream& os; int indent_lvl = 0; void indent() { for(int i = 0; i < indent_lvl; ++i) os << indent_str; } template void write_float(T val, int precision = std::numeric_limits::max_digits10) { if(std::isfinite(val)) os << std::setprecision(precision) << val; else if(std::isinf(val)) { if(std::signbit(val)) os << "-"; os << "Infinity"; } else os << "NaN"; } void write_null() { os << "null"; } }; } void json_formatter::print(std::ostream& os, const tag& t) const { json_fmt_visitor v(os); t.accept(v); } } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/value_initializer.cpp0000644000175100017510000000321215144136757026042 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "value_initializer.h" #include "nbt_tags.h" namespace nbt { value_initializer::value_initializer(int8_t val) : value(tag_byte(val)) {} value_initializer::value_initializer(int16_t val) : value(tag_short(val)) {} value_initializer::value_initializer(int32_t val) : value(tag_int(val)) {} value_initializer::value_initializer(int64_t val) : value(tag_long(val)) {} value_initializer::value_initializer(float val) : value(tag_float(val)) {} value_initializer::value_initializer(double val) : value(tag_double(val)) {} value_initializer::value_initializer(const std::string& str): value(tag_string(str)) {} value_initializer::value_initializer(std::string&& str) : value(tag_string(std::move(str))) {} value_initializer::value_initializer(const char* str) : value(tag_string(str)) {} } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/tag_string.cpp0000644000175100017510000000232115144136757024464 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "tag_string.h" #include "io/stream_reader.h" #include "io/stream_writer.h" namespace nbt { void tag_string::read_payload(io::stream_reader& reader) { try { value = reader.read_string(); } catch(io::input_error& ex) { throw io::input_error("Error reading tag_string"); } } void tag_string::write_payload(io::stream_writer& writer) const { writer.write_string(value); } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/io/0000755000175100017510000000000015144136757022230 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/libnbtplusplus/src/io/stream_writer.cpp0000644000175100017510000000306615144136757025630 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "io/stream_writer.h" #include namespace nbt { namespace io { void write_tag(const std::string& key, const tag& t, std::ostream& os, endian::endian e) { stream_writer(os, e).write_tag(key, t); } void stream_writer::write_tag(const std::string& key, const tag& t) { write_type(t.get_type()); write_string(key); write_payload(t); } void stream_writer::write_string(const std::string& str) { if(str.size() > max_string_len) { os.setstate(std::ios::failbit); std::ostringstream sstr; sstr << "String is too long for NBT (" << str.size() << " > " << max_string_len << ")"; throw std::length_error(sstr.str()); } write_num(static_cast(str.size())); os.write(str.data(), str.size()); } } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/io/stream_reader.cpp0000644000175100017510000000577115144136757025563 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "io/stream_reader.h" #include "make_unique.h" #include "tag_compound.h" #include namespace nbt { namespace io { static constexpr int MAX_DEPTH = 1024; std::pair> read_compound(std::istream& is, endian::endian e) { return stream_reader(is, e).read_compound(); } std::pair> read_tag(std::istream& is, endian::endian e) { return stream_reader(is, e).read_tag(); } stream_reader::stream_reader(std::istream& is, endian::endian e) noexcept: is(is), endian(e) {} std::istream& stream_reader::get_istr() const { return is; } endian::endian stream_reader::get_endian() const { return endian; } std::pair> stream_reader::read_compound() { if(read_type() != tag_type::Compound) { is.setstate(std::ios::failbit); throw input_error("Tag is not a compound"); } std::string key = read_string(); auto comp = make_unique(); comp->read_payload(*this); return {std::move(key), std::move(comp)}; } std::pair> stream_reader::read_tag() { tag_type type = read_type(); std::string key = read_string(); std::unique_ptr t = read_payload(type); return {std::move(key), std::move(t)}; } std::unique_ptr stream_reader::read_payload(tag_type type) { if (++depth > MAX_DEPTH) throw input_error("Too deeply nested"); std::unique_ptr t = tag::create(type); t->read_payload(*this); --depth; return t; } tag_type stream_reader::read_type(bool allow_end) { int type = is.get(); if(!is) throw input_error("Error reading tag type"); if(!is_valid_type(type, allow_end)) { is.setstate(std::ios::failbit); throw input_error("Invalid tag type: " + std::to_string(type)); } return static_cast(type); } std::string stream_reader::read_string() { uint16_t len; read_num(len); if(!is) throw input_error("Error reading string"); std::string ret(len, '\0'); is.read(&ret[0], len); //C++11 allows us to do this if(!is) throw input_error("Error reading string"); return ret; } } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/io/izlibstream.cpp0000644000175100017510000000555515144136757025273 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "io/izlibstream.h" #include "io/zlib_streambuf.h" namespace zlib { inflate_streambuf::inflate_streambuf(std::istream& input, size_t bufsize, int window_bits): zlib_streambuf(bufsize), is(input), stream_end(false) { zstr.next_in = Z_NULL; zstr.avail_in = 0; int ret = inflateInit2(&zstr, window_bits); if(ret != Z_OK) throw zlib_error(zstr.msg, ret); char* end = out.data() + out.size(); setg(end, end, end); } inflate_streambuf::~inflate_streambuf() noexcept { inflateEnd(&zstr); } inflate_streambuf::int_type inflate_streambuf::underflow() { if(gptr() < egptr()) return traits_type::to_int_type(*gptr()); size_t have; do { //Read if input buffer is empty if(zstr.avail_in <= 0) { is.read(in.data(), in.size()); if(is.bad()) throw std::ios_base::failure("Input stream is bad"); size_t count = is.gcount(); if(count == 0 && !stream_end) throw zlib_error("Unexpected end of stream", Z_DATA_ERROR); zstr.next_in = reinterpret_cast(in.data()); zstr.avail_in = count; } zstr.next_out = reinterpret_cast(out.data()); zstr.avail_out = out.size(); int ret = inflate(&zstr, Z_NO_FLUSH); have = out.size() - zstr.avail_out; switch(ret) { case Z_NEED_DICT: case Z_DATA_ERROR: throw zlib_error(zstr.msg, ret); case Z_MEM_ERROR: throw std::bad_alloc(); case Z_STREAM_END: if(!stream_end) { stream_end = true; //In case we consumed too much, we have to rewind the input stream is.clear(); is.seekg(-static_cast(zstr.avail_in), std::ios_base::cur); } if(have == 0) return traits_type::eof(); break; } } while(have == 0); setg(out.data(), out.data(), out.data() + have); return traits_type::to_int_type(*gptr()); } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/io/ozlibstream.cpp0000644000175100017510000000540415144136757025272 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "io/ozlibstream.h" #include "io/zlib_streambuf.h" namespace zlib { deflate_streambuf::deflate_streambuf(std::ostream& output, size_t bufsize, int level, int window_bits, int mem_level, int strategy): zlib_streambuf(bufsize), os(output) { int ret = deflateInit2(&zstr, level, Z_DEFLATED, window_bits, mem_level, strategy); if(ret != Z_OK) throw zlib_error(zstr.msg, ret); setp(in.data(), in.data() + in.size()); } deflate_streambuf::~deflate_streambuf() noexcept { try { close(); } catch(...) { //ignore as we can't do anything about it } deflateEnd(&zstr); } void deflate_streambuf::close() { deflate_chunk(Z_FINISH); } void deflate_streambuf::deflate_chunk(int flush) { zstr.next_in = reinterpret_cast(pbase()); zstr.avail_in = pptr() - pbase(); do { zstr.next_out = reinterpret_cast(out.data()); zstr.avail_out = out.size(); int ret = deflate(&zstr, flush); if(ret != Z_OK && ret != Z_STREAM_END) { os.setstate(std::ios_base::failbit); throw zlib_error(zstr.msg, ret); } int have = out.size() - zstr.avail_out; if(!os.write(out.data(), have)) throw std::ios_base::failure("Could not write to the output stream"); } while(zstr.avail_out == 0); setp(in.data(), in.data() + in.size()); } deflate_streambuf::int_type deflate_streambuf::overflow(int_type ch) { deflate_chunk(); if(ch != traits_type::eof()) { *pptr() = ch; pbump(1); } return ch; } int deflate_streambuf::sync() { deflate_chunk(); return 0; } void ozlibstream::close() { try { buf.close(); } catch(...) { setstate(badbit); //FIXME: This will throw the wrong type of exception //but there's no good way of setting the badbit //without causing an exception when exceptions is set } } } PrismLauncher-10.0.5/libraries/libnbtplusplus/src/tag_list.cpp0000644000175100017510000001142615144136757024137 0ustar runnerrunner/* * libnbt++ - A library for the Minecraft Named Binary Tag format. * Copyright (C) 2013, 2015 ljfa-ag * * This file is part of libnbt++. * * libnbt++ is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * libnbt++ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with libnbt++. If not, see . */ #include "tag_list.h" #include "nbt_tags.h" #include "io/stream_reader.h" #include "io/stream_writer.h" #include namespace nbt { tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list il) { init(il); } tag_list::tag_list(std::initializer_list init) { if(init.size() == 0) el_type_ = tag_type::Null; else { el_type_ = init.begin()->get_type(); for(const value& val: init) { if(!val || val.get_type() != el_type_) throw std::invalid_argument("The values are not all the same type"); } tags.assign(init.begin(), init.end()); } } value& tag_list::at(size_t i) { return tags.at(i); } const value& tag_list::at(size_t i) const { return tags.at(i); } void tag_list::set(size_t i, value&& val) { if(val.get_type() != el_type_) throw std::invalid_argument("The tag type does not match the list's content type"); tags.at(i) = std::move(val); } void tag_list::push_back(value_initializer&& val) { if(!val) //don't allow null values throw std::invalid_argument("The value must not be null"); if(el_type_ == tag_type::Null) //set content type if undetermined el_type_ = val.get_type(); else if(el_type_ != val.get_type()) throw std::invalid_argument("The tag type does not match the list's content type"); tags.push_back(std::move(val)); } void tag_list::reset(tag_type type) { clear(); el_type_ = type; } void tag_list::read_payload(io::stream_reader& reader) { tag_type lt = reader.read_type(true); int32_t length; reader.read_num(length); if(length < 0) reader.get_istr().setstate(std::ios::failbit); if(!reader.get_istr()) throw io::input_error("Error reading length of tag_list"); if(lt != tag_type::End) { reset(lt); tags.reserve(length); for(int32_t i = 0; i < length; ++i) tags.emplace_back(reader.read_payload(lt)); } else { //In case of tag_end, ignore the length and leave the type undetermined reset(tag_type::Null); } } void tag_list::write_payload(io::stream_writer& writer) const { if(size() > io::stream_writer::max_array_len) { writer.get_ostr().setstate(std::ios::failbit); throw std::length_error("List is too large for NBT"); } writer.write_type(el_type_ != tag_type::Null ? el_type_ : tag_type::End); writer.write_num(static_cast(size())); for(const auto& val: tags) { //check if the value is of the correct type if(val.get_type() != el_type_) { writer.get_ostr().setstate(std::ios::failbit); throw std::logic_error("The tags in the list do not all match the content type"); } writer.write_payload(val); } } bool operator==(const tag_list& lhs, const tag_list& rhs) { return lhs.el_type_ == rhs.el_type_ && lhs.tags == rhs.tags; } bool operator!=(const tag_list& lhs, const tag_list& rhs) { return !(lhs == rhs); } } PrismLauncher-10.0.5/libraries/libnbtplusplus/.gitignore0000644000175100017510000000011415144136757023016 0ustar runnerrunner*.cbp *.depend *.layout *.cbtemp *.bak *.swp /bin /lib /obj /build /doxygen PrismLauncher-10.0.5/libraries/rainbow/0000755000175100017510000000000015144136757017411 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/rainbow/COPYING.LIB0000644000175100017510000000000015144136757021037 0ustar runnerrunnerPrismLauncher-10.0.5/libraries/rainbow/include/0000755000175100017510000000000015144136757021034 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/rainbow/include/rainbow.h0000644000175100017510000001344615144136757022656 0ustar runnerrunner/* This was part of the KDE project - see KGuiAddons * Copyright (C) 2007 Matthew Woehlke * Copyright (C) 2007 Olaf Schmidt * Copyright (C) 2007 Thomas Zander * Copyright (C) 2007 Zack Rusin * Copyright (C) 2015 Petr Mrazek * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #pragma once #include class QColor; /** * A set of methods used to work with colors. */ namespace Rainbow { /** * Calculate the luma of a color. Luma is weighted sum of gamma-adjusted * R'G'B' components of a color. The result is similar to qGray. The range * is from 0.0 (black) to 1.0 (white). * * Rainbow::darken(), Rainbow::lighten() and Rainbow::shade() * operate on the luma of a color. * * @see http://en.wikipedia.org/wiki/Luma_(video) */ qreal luma(const QColor&); /** * Calculate hue, chroma and luma of a color in one call. * @since 5.0 */ void getHcy(const QColor&, qreal* hue, qreal* chroma, qreal* luma, qreal* alpha = 0); /** * Calculate the contrast ratio between two colors, according to the * W3C/WCAG2.0 algorithm, (Lmax + 0.05)/(Lmin + 0.05), where Lmax and Lmin * are the luma values of the lighter color and the darker color, * respectively. * * A contrast ration of 5:1 (result == 5.0) is the minimum for "normal" * text to be considered readable (large text can go as low as 3:1). The * ratio ranges from 1:1 (result == 1.0) to 21:1 (result == 21.0). * * @see Rainbow::luma */ qreal contrastRatio(const QColor&, const QColor&); /** * Adjust the luma of a color by changing its distance from white. * * @li amount == 1.0 gives white * @li amount == 0.5 results in a color whose luma is halfway between 1.0 * and that of the original color * @li amount == 0.0 gives the original color * @li amount == -1.0 gives a color that is 'twice as far from white' as * the original color, that is luma(result) == 1.0 - 2*(1.0 - luma(color)) * * @param amount factor by which to adjust the luma component of the color * @param chromaInverseGain (optional) factor by which to adjust the chroma * component of the color; 1.0 means no change, 0.0 maximizes chroma * @see Rainbow::shade */ QColor lighten(const QColor&, qreal amount = 0.5, qreal chromaInverseGain = 1.0); /** * Adjust the luma of a color by changing its distance from black. * * @li amount == 1.0 gives black * @li amount == 0.5 results in a color whose luma is halfway between 0.0 * and that of the original color * @li amount == 0.0 gives the original color * @li amount == -1.0 gives a color that is 'twice as far from black' as * the original color, that is luma(result) == 2*luma(color) * * @param amount factor by which to adjust the luma component of the color * @param chromaGain (optional) factor by which to adjust the chroma * component of the color; 1.0 means no change, 0.0 minimizes chroma * @see Rainbow::shade */ QColor darken(const QColor&, qreal amount = 0.5, qreal chromaGain = 1.0); /** * Adjust the luma and chroma components of a color. The amount is added * to the corresponding component. * * @param lumaAmount amount by which to adjust the luma component of the * color; 0.0 results in no change, -1.0 turns anything black, 1.0 turns * anything white * @param chromaAmount (optional) amount by which to adjust the chroma * component of the color; 0.0 results in no change, -1.0 minimizes chroma, * 1.0 maximizes chroma * @see Rainbow::luma */ QColor shade(const QColor&, qreal lumaAmount, qreal chromaAmount = 0.0); /** * Create a new color by tinting one color with another. This function is * meant for creating additional colors withings the same class (background, * foreground) from colors in a different class. Therefore when @p amount * is low, the luma of @p base is mostly preserved, while the hue and * chroma of @p color is mostly inherited. * * @param base color to be tinted * @param color color with which to tint * @param amount how strongly to tint the base; 0.0 gives @p base, * 1.0 gives @p color */ QColor tint(const QColor& base, const QColor& color, qreal amount = 0.3); /** * Blend two colors into a new color by linear combination. * @code QColor lighter = Rainbow::mix(myColor, Qt::white) * @endcode * @param c1 first color. * @param c2 second color. * @param bias weight to be used for the mix. @p bias <= 0 gives @p c1, * @p bias >= 1 gives @p c2. @p bias == 0.5 gives a 50% blend of @p c1 * and @p c2. */ QColor mix(const QColor& c1, const QColor& c2, qreal bias = 0.5); /** * Blend two colors into a new color by painting the second color over the * first using the specified composition mode. * @code QColor white(Qt::white); white.setAlphaF(0.5); QColor lighter = Rainbow::overlayColors(myColor, white); @endcode * @param base the base color (alpha channel is ignored). * @param paint the color to be overlayed onto the base color. * @param comp the CompositionMode used to do the blending. */ QColor overlayColors(const QColor& base, const QColor& paint, QPainter::CompositionMode comp = QPainter::CompositionMode_SourceOver); } // namespace Rainbow PrismLauncher-10.0.5/libraries/rainbow/CMakeLists.txt0000644000175100017510000000066215144136757022155 0ustar runnerrunnercmake_minimum_required(VERSION 3.15) project(rainbow) if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Gui REQUIRED) endif() set(RAINBOW_SOURCES src/rainbow.cpp ) add_library(Launcher_rainbow STATIC ${RAINBOW_SOURCES}) target_include_directories(Launcher_rainbow PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") target_link_libraries(Launcher_rainbow Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui) PrismLauncher-10.0.5/libraries/rainbow/src/0000755000175100017510000000000015144136757020200 5ustar runnerrunnerPrismLauncher-10.0.5/libraries/rainbow/src/rainbow.cpp0000644000175100017510000002072215144136757022350 0ustar runnerrunner/* This was part of the KDE project - see KGuiAddons * Copyright (C) 2007 Matthew Woehlke * Copyright (C) 2007 Olaf Schmidt * Copyright (C) 2007 Thomas Zander * Copyright (C) 2007 Zack Rusin * Copyright (C) 2015 Petr Mrazek * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "../include/rainbow.h" #include #include #include // qIsNaN #include // BEGIN internal helper functions static inline qreal wrap(qreal a, qreal d = 1.0) { qreal r = fmod(a, d); return (r < 0.0 ? d + r : (r > 0.0 ? r : 0.0)); } // normalize: like qBound(a, 0.0, 1.0) but without needing the args and with // "safer" behavior on NaN (isnan(a) -> return 0.0) static inline qreal normalize(qreal a) { return (a < 1.0 ? (a > 0.0 ? a : 0.0) : 1.0); } /////////////////////////////////////////////////////////////////////////////// // HCY color space #define HCY_REC 709 // use 709 for now #if HCY_REC == 601 static const qreal yc[3] = { 0.299, 0.587, 0.114 }; #elif HCY_REC == 709 static const qreal yc[3] = { 0.2126, 0.7152, 0.0722 }; #else // use Qt values static const qreal yc[3] = { 0.34375, 0.5, 0.15625 }; #endif class KHCY { public: explicit KHCY(const QColor& color) { qreal r = gamma(color.redF()); qreal g = gamma(color.greenF()); qreal b = gamma(color.blueF()); a = color.alphaF(); // luma component y = lumag(r, g, b); // hue component qreal p = qMax(qMax(r, g), b); qreal n = qMin(qMin(r, g), b); qreal d = 6.0 * (p - n); if (n == p) { h = 0.0; } else if (r == p) { h = ((g - b) / d); } else if (g == p) { h = ((b - r) / d) + (1.0 / 3.0); } else { h = ((r - g) / d) + (2.0 / 3.0); } // chroma component if (r == g && g == b) { c = 0.0; } else { c = qMax((y - n) / y, (p - y) / (1 - y)); } } explicit KHCY(qreal h_, qreal c_, qreal y_, qreal a_ = 1.0) { h = h_; c = c_; y = y_; a = a_; } QColor qColor() const { // start with sane component values qreal _h = wrap(h); qreal _c = normalize(c); qreal _y = normalize(y); // calculate some needed variables qreal _hs = _h * 6.0, th, tm; if (_hs < 1.0) { th = _hs; tm = yc[0] + yc[1] * th; } else if (_hs < 2.0) { th = 2.0 - _hs; tm = yc[1] + yc[0] * th; } else if (_hs < 3.0) { th = _hs - 2.0; tm = yc[1] + yc[2] * th; } else if (_hs < 4.0) { th = 4.0 - _hs; tm = yc[2] + yc[1] * th; } else if (_hs < 5.0) { th = _hs - 4.0; tm = yc[2] + yc[0] * th; } else { th = 6.0 - _hs; tm = yc[0] + yc[2] * th; } // calculate RGB channels in sorted order qreal tn, to, tp; if (tm >= _y) { tp = _y + _y * _c * (1.0 - tm) / tm; to = _y + _y * _c * (th - tm) / tm; tn = _y - (_y * _c); } else { tp = _y + (1.0 - _y) * _c; to = _y + (1.0 - _y) * _c * (th - tm) / (1.0 - tm); tn = _y - (1.0 - _y) * _c * tm / (1.0 - tm); } // return RGB channels in appropriate order if (_hs < 1.0) { return QColor::fromRgbF(igamma(tp), igamma(to), igamma(tn), a); } else if (_hs < 2.0) { return QColor::fromRgbF(igamma(to), igamma(tp), igamma(tn), a); } else if (_hs < 3.0) { return QColor::fromRgbF(igamma(tn), igamma(tp), igamma(to), a); } else if (_hs < 4.0) { return QColor::fromRgbF(igamma(tn), igamma(to), igamma(tp), a); } else if (_hs < 5.0) { return QColor::fromRgbF(igamma(to), igamma(tn), igamma(tp), a); } else { return QColor::fromRgbF(igamma(tp), igamma(tn), igamma(to), a); } } qreal h, c, y, a; static qreal luma(const QColor& color) { return lumag(gamma(color.redF()), gamma(color.greenF()), gamma(color.blueF())); } private: static qreal gamma(qreal n) { return pow(normalize(n), 2.2); } static qreal igamma(qreal n) { return pow(normalize(n), 1.0 / 2.2); } static qreal lumag(qreal r, qreal g, qreal b) { return r * yc[0] + g * yc[1] + b * yc[2]; } }; static inline qreal mixQreal(qreal a, qreal b, qreal bias) { return a + (b - a) * bias; } // END internal helper functions qreal Rainbow::luma(const QColor& color) { return KHCY::luma(color); } void Rainbow::getHcy(const QColor& color, qreal* h, qreal* c, qreal* y, qreal* a) { if (!c || !h || !y) { return; } KHCY khcy(color); *c = khcy.c; *h = khcy.h; *y = khcy.y; if (a) { *a = khcy.a; } } static qreal contrastRatioForLuma(qreal y1, qreal y2) { if (y1 > y2) { return (y1 + 0.05) / (y2 + 0.05); } else { return (y2 + 0.05) / (y1 + 0.05); } } qreal Rainbow::contrastRatio(const QColor& c1, const QColor& c2) { return contrastRatioForLuma(luma(c1), luma(c2)); } QColor Rainbow::lighten(const QColor& color, qreal ky, qreal kc) { KHCY c(color); c.y = 1.0 - normalize((1.0 - c.y) * (1.0 - ky)); c.c = 1.0 - normalize((1.0 - c.c) * kc); return c.qColor(); } QColor Rainbow::darken(const QColor& color, qreal ky, qreal kc) { KHCY c(color); c.y = normalize(c.y * (1.0 - ky)); c.c = normalize(c.c * kc); return c.qColor(); } QColor Rainbow::shade(const QColor& color, qreal ky, qreal kc) { KHCY c(color); c.y = normalize(c.y + ky); c.c = normalize(c.c + kc); return c.qColor(); } static QColor tintHelper(const QColor& base, qreal baseLuma, const QColor& color, qreal amount) { KHCY result(Rainbow::mix(base, color, pow(amount, 0.3))); result.y = mixQreal(baseLuma, result.y, amount); return result.qColor(); } QColor Rainbow::tint(const QColor& base, const QColor& color, qreal amount) { if (amount <= 0.0) { return base; } if (amount >= 1.0) { return color; } if (qIsNaN(amount)) { return base; } qreal baseLuma = luma(base); // cache value because luma call is expensive double ri = contrastRatioForLuma(baseLuma, luma(color)); double rg = 1.0 + ((ri + 1.0) * amount * amount * amount); double u = 1.0, l = 0.0; QColor result; for (int i = 12; i; --i) { double a = 0.5 * (l + u); result = tintHelper(base, baseLuma, color, a); double ra = contrastRatioForLuma(baseLuma, luma(result)); if (ra > rg) { u = a; } else { l = a; } } return result; } QColor Rainbow::mix(const QColor& c1, const QColor& c2, qreal bias) { if (bias <= 0.0) { return c1; } if (bias >= 1.0) { return c2; } if (qIsNaN(bias)) { return c1; } qreal r = mixQreal(c1.redF(), c2.redF(), bias); qreal g = mixQreal(c1.greenF(), c2.greenF(), bias); qreal b = mixQreal(c1.blueF(), c2.blueF(), bias); qreal a = mixQreal(c1.alphaF(), c2.alphaF(), bias); return QColor::fromRgbF(r, g, b, a); } QColor Rainbow::overlayColors(const QColor& base, const QColor& paint, QPainter::CompositionMode comp) { // This isn't the fastest way, but should be "fast enough". // It's also the only safe way to use QPainter::CompositionMode QImage img(1, 1, QImage::Format_ARGB32_Premultiplied); QPainter p(&img); QColor start = base; start.setAlpha(255); // opaque p.fillRect(0, 0, 1, 1, start); p.setCompositionMode(comp); p.fillRect(0, 0, 1, 1, paint); p.end(); return img.pixel(0, 0); } PrismLauncher-10.0.5/.clang-tidy0000644000175100017510000000274215144136756016034 0ustar runnerrunnerChecks: - modernize-use-using - readability-avoid-const-params-in-decls - misc-unused-parameters, - readability-identifier-naming # ^ Without unused-parameters the readability-identifier-naming check doesn't cause any warnings. CheckOptions: - { key: readability-identifier-naming.ClassCase, value: PascalCase } - { key: readability-identifier-naming.EnumCase, value: PascalCase } - { key: readability-identifier-naming.FunctionCase, value: camelCase } - { key: readability-identifier-naming.GlobalVariableCase, value: camelCase } - { key: readability-identifier-naming.GlobalFunctionCase, value: camelCase } - { key: readability-identifier-naming.GlobalConstantCase, value: SCREAMING_SNAKE_CASE } - { key: readability-identifier-naming.MacroDefinitionCase, value: SCREAMING_SNAKE_CASE } - { key: readability-identifier-naming.ClassMemberCase, value: camelCase } - { key: readability-identifier-naming.PrivateMemberPrefix, value: m_ } - { key: readability-identifier-naming.ProtectedMemberPrefix, value: m_ } - { key: readability-identifier-naming.PrivateStaticMemberPrefix, value: s_ } - { key: readability-identifier-naming.ProtectedStaticMemberPrefix, value: s_ } - { key: readability-identifier-naming.PublicStaticConstantCase, value: SCREAMING_SNAKE_CASE } - { key: readability-identifier-naming.EnumConstantCase, value: PascalCase } PrismLauncher-10.0.5/CONTRIBUTING.md0000644000175100017510000001241515144136756016227 0ustar runnerrunner# Contributions Guidelines ## Code style All files are formatted with `clang-format` using the configuration in `.clang-format`. Ensure it is run on changed files before committing! Please also follow the project's conventions for C++: - Class and type names should be formatted as `PascalCase`: `MyClass`. - Private or protected class data members should be formatted as `camelCase` prefixed with `m_`: `m_myCounter`. - Private or protected `static` class data members should be formatted as `camelCase` prefixed with `s_`: `s_instance`. - Public class data members should be formatted as `camelCase` without the prefix: `dateOfBirth`. - Public, private or protected `static const` class data members should be formatted as `SCREAMING_SNAKE_CASE`: `MAX_VALUE`. - Class function members should be formatted as `camelCase` without a prefix: `incrementCounter`. - Global functions and non-`const` global variables should be formatted as `camelCase` without a prefix: `globalData`. - `const` global variables and macros should be formatted as `SCREAMING_SNAKE_CASE`: `LIGHT_GRAY`. - enum constants should be formatted as `PascalCase`: `CamelusBactrianus` - Avoid inventing acronyms or abbreviations especially for a name of multiple words - like `tp` for `texturePack`. - Avoid using `[[nodiscard]]` unless ignoring the return value is likely to cause a bug in cases such as: - A function allocates memory or another resource and the caller needs to clean it up. - A function has side effects and an error status is returned. - A function is likely be mistaken for having side effects. - A plain getter is unlikely to cause confusion and adding `[[nodiscard]]` can create clutter and inconsistency. Most of these rules are included in the `.clang-tidy` file, so you can run `clang-tidy` to check for any violations. Here is what these conventions with the formatting configuration look like: ```c++ #define AWESOMENESS 10 constexpr double PI = 3.14159; enum class PizzaToppings { HamAndPineapple, OreoAndKetchup }; struct Person { QString name; QDateTime dateOfBirth; long daysOld() const { return dateOfBirth.daysTo(QDateTime::currentDateTime()); } }; class ImportantClass { public: void incrementCounter() { if (m_counter + 1 > MAX_COUNTER_VALUE) throw std::runtime_error("Counter has reached limit!"); ++m_counter; } int counter() const { return m_counter; } private: static constexpr int MAX_COUNTER_VALUE = 100; int m_counter; }; ImportantClass importantClassInstance; ``` If you see any names which do not follow these conventions, it is preferred that you leave them be - renames increase the number of changes therefore make reviewing harder and make your PR more prone to conflicts. However, if you're refactoring a whole class anyway, it's fine. ## Signing your work In an effort to ensure that the code you contribute is actually compatible with the licenses in this codebase, we require you to sign-off all your contributions. This can be done by appending `-s` to your `git commit` call, or by manually appending the following text to your commit message: ```text Signed-off-by: Author name ``` By signing off your work, you agree to the terms below: ```text Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. ``` These terms will be enforced once you create a pull request, and you will be informed automatically if any of your commits aren't signed-off by you. As a bonus, you can also [cryptographically sign your commits][gh-signing-commits] and enable [vigilant mode][gh-vigilant-mode] on GitHub. [gh-signing-commits]: https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits [gh-vigilant-mode]: https://docs.github.com/en/authentication/managing-commit-signature-verification/displaying-verification-statuses-for-all-of-your-commits ## Backporting to Release Branches We use [automated backports](https://github.com/PrismLauncher/PrismLauncher/blob/develop/.github/workflows/backport.yml) to merge specific contributions from develop into `release` branches. This is done when pull requests are merged and have labels such as `backport release-7.x` - which should be added along with the milestone for the release. PrismLauncher-10.0.5/CMakeLists.txt0000644000175100017510000005555615144136756016553 0ustar runnerrunnercmake_minimum_required(VERSION 3.22) # minimum version required by Qt project(Launcher LANGUAGES C CXX) if(APPLE) enable_language(OBJC OBJCXX) endif() string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) if(IS_IN_SOURCE_BUILD) message(FATAL_ERROR "You are building the Launcher in-source. Please separate the build tree from the source tree.") endif() ##################################### Set CMake options ##################################### set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") # Output all executables and shared libs in the main build folder, not in subfolders. set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) if(UNIX) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) endif() set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) ######## Set compiler flags ######## set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) add_compile_definitions($<$>:QT_NO_DEBUG>) if(MSVC) # /GS Adds buffer security checks, default on but incuded anyway to mirror gcc's fstack-protector flag # /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20 # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs set(CMAKE_CXX_FLAGS "/GS /permissive- /W4 ${CMAKE_CXX_FLAGS}") # /EHs Enables stack unwind semantics for standard C++ exceptions to ensure stackframes are unwound # and object deconstructors are called when an exception is caught. # without it memory leaks and a warning is printed # /EHc tells the compiler to assume that functions declared as extern "C" never throw a C++ exception # This appears to not always be a defualt compiler option in CMAKE set(CMAKE_CXX_FLAGS "/EHsc ${CMAKE_CXX_FLAGS}") # LINK accepts /SUBSYSTEM whics sets if we are a WINDOWS (gui) or a CONSOLE programs # This implicitly selects an entrypoint specific to the subsystem selected # qtmain/QtEntryPointLib provides the correct entrypoint (wWinMain) for gui programs # Additinaly LINK autodetects we use a GUI so we can omit /SUBSYSTEM # This allows tests to still use have console without using seperate linker flags # /LTCG allows for linking wholy optimizated programs # /MANIFEST:NO disables generating a manifest file, we instead provide our own # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB set(CMAKE_EXE_LINKER_FLAGS "/LTCG /MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}") # /GL enables whole program optimizations # /Gw helps reduce binary size # /Gy allows the compiler to package individual functions # /guard:cf enables control flow guard foreach(lang C CXX) set("CMAKE_${lang}_FLAGS_RELEASE" "/GL /Gw /Gy /guard:cf") endforeach() # See https://github.com/ccache/ccache/issues/1040 # Note, CMake 3.25 replaces this with CMAKE_MSVC_DEBUG_INFORMATION_FORMAT # See https://cmake.org/cmake/help/v3.25/variable/CMAKE_MSVC_DEBUG_INFORMATION_FORMAT.html foreach(config DEBUG RELWITHDEBINFO) foreach(lang C CXX) set(flags_var "CMAKE_${lang}_FLAGS_${config}") string(REGEX REPLACE "/Z[Ii]" "/Z7" ${flags_var} "${${flags_var}}") endforeach() endforeach() if(CMAKE_MSVC_RUNTIME_LIBRARY STREQUAL "MultiThreadedDLL") set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release "") set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release "") endif() else() set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") # ATL's pack list needs more than the default 1 Mib stack on windows if(WIN32) set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}") # -ffunction-sections and -fdata-sections help reduce binary size # -mguard=cf enables Control Flow Guard # TODO: Look into -gc-sections to further reduce binary size foreach(lang C CXX) set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections -mguard=cf") endforeach() endif() endif() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060400") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060400") # set CXXFLAGS for build targets set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") # Export compile commands for debug builds if we can (useful in LSPs like clangd) # https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR MATCHES "^Ninja") set(CMAKE_EXPORT_COMPILE_COMMANDS ON) endif() option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser if (DEBUG_ADDRESS_SANITIZER) message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") set(USE_ASAN_COMPILE_OPTIONS $,$>) if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") message(STATUS "Using Address Sanitizer compile options for MSVC frontend") add_compile_options( $<${USE_ASAN_COMPILE_OPTIONS}:/fsanitize=address> $<${USE_ASAN_COMPILE_OPTIONS}:/Oy-> ) elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") message(STATUS "Using Address Sanitizer compile options for GCC/Clang") add_compile_options( $<${USE_ASAN_COMPILE_OPTIONS}:-fsanitize=address,undefined> $<${USE_ASAN_COMPILE_OPTIONS}:-fno-omit-frame-pointer> $<${USE_ASAN_COMPILE_OPTIONS}:-fno-sanitize-recover=null> ) link_libraries("asan" "ubsan") else() message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}") endif() endif() option(ENABLE_LTO "Enable Link Time Optimization" off) if(ENABLE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT ipo_supported OUTPUT ipo_error) if(ipo_supported) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL TRUE) if(CMAKE_BUILD_TYPE) if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel") message(STATUS "IPO / LTO enabled") else() message(STATUS "Not enabling IPO / LTO on debug builds") endif() else() message(STATUS "IPO / LTO will only be enabled for release builds") endif() else() message(STATUS "IPO / LTO not supported: <${ipo_error}>") endif() endif() option(BUILD_TESTING "Build the testing tree." ON) find_package(ECM NO_MODULE REQUIRED) set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") include(CTest) include(ECMAddTests) if(BUILD_TESTING) enable_testing() endif() ##################################### Set Application options ##################################### ######## Set URLs ######## set(Launcher_NEWS_RSS_URL "https://prismlauncher.org/feed/feed.xml" CACHE STRING "URL to fetch Prism Launcher's news RSS feed from.") set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CACHE STRING "URL that gets opened when the user successfully logins.") set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.") ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 10) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_PATCH 5) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}") set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0") set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0") # Build platform. set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") # Github repo URL with releases for updater set(Launcher_UPDATER_GITHUB_REPO "https://github.com/PrismLauncher/PrismLauncher" CACHE STRING "Base github URL for the updater.") # Name to help updater identify valid artifacts set(Launcher_BUILD_ARTIFACT "" CACHE STRING "Artifact name to help the updater identify valid artifacts.") # The metadata server set(Launcher_META_URL "https://meta.prismlauncher.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.") # Imgur API Client ID set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can get from Imgur when you register an application") # Bug tracker URL set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/issues" CACHE STRING "URL for the bug tracker.") # Translations Platform URL set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.") set(Launcher_TRANSLATION_FILES_URL "https://i18n.prismlauncher.org/" CACHE STRING "URL for the translations files.") # Matrix Space set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space") # Discord URL set(Launcher_DISCORD_URL "https://prismlauncher.org/discord" CACHE STRING "URL for the Discord guild.") # Subreddit URL set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL for the subreddit.") # Builds set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") option(Launcher_USE_PCH "Use precompiled headers where possible" ON) # Java downloader set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) # Although we recommend enabling this, we cannot guarantee binary compatibility on # differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this # feature if they know it will work with their distribution. if(UNIX AND NOT APPLE) set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) endif() # Java downloader option(Launcher_ENABLE_JAVA_DOWNLOADER "Build the java downloader feature" ${Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT}) # Native libraries if(UNIX AND APPLE) set(Launcher_GLFW_LIBRARY_NAME "libglfw.dylib" CACHE STRING "Name of native glfw library") set(Launcher_OPENAL_LIBRARY_NAME "libopenal.dylib" CACHE STRING "Name of native openal library") elseif(UNIX) set(Launcher_GLFW_LIBRARY_NAME "libglfw.so" CACHE STRING "Name of native glfw library") set(Launcher_OPENAL_LIBRARY_NAME "libopenal.so" CACHE STRING "Name of native openal library") elseif(WIN32) set(Launcher_GLFW_LIBRARY_NAME "glfw.dll" CACHE STRING "Name of native glfw library") set(Launcher_OPENAL_LIBRARY_NAME "OpenAL.dll" CACHE STRING "Name of native openal library") endif() # API Keys # NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service # of these platforms, please change these API keys beforehand. # Be aware that if you were to use these API keys for malicious purposes they might get revoked, which might cause # breakage to thousands of users. # If you don't plan to use these features of this software, you can just remove these values. # By using this key in your builds you accept the terms of use laid down in # https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use set(Launcher_MSA_CLIENT_ID "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") # By using this key in your builds you accept the terms and conditions laid down in # https://support.curseforge.com/en/support/solutions/articles/9000207405-curse-forge-3rd-party-api-terms-and-conditions # NOTE: CurseForge requires you to change this if you make any kind of derivative work. # This key was issued specifically for Prism Launcher set(Launcher_CURSEFORGE_API_KEY "$2a$10$wuAJuNZuted3NORVmpgUC.m8sI.pv1tOPKZyBgLFGjxFp/br0lZCC" CACHE STRING "API key for the CurseForge platform") set(Launcher_COMPILER_NAME ${CMAKE_CXX_COMPILER_ID}) set(Launcher_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION}) set(Launcher_COMPILER_TARGET_SYSTEM ${CMAKE_SYSTEM_NAME}) set(Launcher_COMPILER_TARGET_SYSTEM_VERSION ${CMAKE_SYSTEM_VERSION}) set(Launcher_COMPILER_TARGET_PROCESSOR ${CMAKE_SYSTEM_PROCESSOR}) #### Check the current Git commit and branch include(GetGitRevisionDescription) git_get_exact_tag(Launcher_GIT_TAG) get_git_head_revision(Launcher_GIT_REFSPEC Launcher_GIT_COMMIT) message(STATUS "Git commit: ${Launcher_GIT_COMMIT}") message(STATUS "Git tag: ${Launcher_GIT_TAG}") message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}") string(TIMESTAMP TODAY "%Y-%m-%d") set(Launcher_BUILD_TIMESTAMP "${TODAY}") ################################ 3rd Party Libs ################################ # Find the required Qt parts if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) find_package(Qt6 6.4 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL) find_package(Qt6 COMPONENTS DBus) list(APPEND Launcher_QT_DBUS Qt6::DBus) else() message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") endif() if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS}) set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS}) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) endif() find_package(cmark REQUIRED) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") find_package(PkgConfig REQUIRED) pkg_check_modules(gamemode REQUIRED IMPORTED_TARGET gamemode) endif() # Find libqrencode ## NOTE(@getchoo): Never use pkg-config with MSVC since the vcpkg port makes our install bundle fail to find the dll if(MSVC) find_path(LIBQRENCODE_INCLUDE_DIR qrencode.h REQUIRED) find_library(LIBQRENCODE_LIBRARY_RELEASE qrencode REQUIRED) find_library(LIBQRENCODE_LIBRARY_DEBUG qrencoded) set(LIBQRENCODE_LIBRARIES optimized ${LIBQRENCODE_LIBRARY_RELEASE} debug ${LIBQRENCODE_LIBRARY_DEBUG}) else() find_package(PkgConfig REQUIRED) pkg_check_modules(libqrencode REQUIRED IMPORTED_TARGET libqrencode) endif() # Find libarchive through CMake packages, mainly for vcpkg find_package(LibArchive) # CMake packages aren't available in most distributions of libarchive, so fallback to pkg-config if(NOT LibArchive_FOUND) find_package(PkgConfig REQUIRED) pkg_check_modules(libarchive REQUIRED IMPORTED_TARGET libarchive) endif() find_package(tomlplusplus 3.2.0) # fallback to pkgconfig, important especially as many distros package toml++ built with meson if(NOT tomlplusplus_FOUND) find_package(PkgConfig REQUIRED) pkg_check_modules(tomlplusplus REQUIRED IMPORTED_TARGET tomlplusplus>=3.2.0) endif() find_package(ZLIB REQUIRED) include(ECMQtDeclareLoggingCategory) ####################################### Program Info ####################################### set(Launcher_APP_BINARY_NAME "prismlauncher" CACHE STRING "Name of the Launcher binary") add_subdirectory(program_info) ####################################### Install layout ####################################### set(Launcher_ENABLE_UPDATER NO) set(Launcher_BUILD_UPDATER NO) if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL "")) set(Launcher_BUILD_UPDATER YES) endif() if(NOT (UNIX AND APPLE)) # Install "portable.txt" if selected component is "portable" install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Portable_File}" DESTINATION "." COMPONENT portable EXCLUDE_FROM_ALL) endif() if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(LIBRARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(PLUGIN_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") set(FRAMEWORK_DEST_DIR "${Launcher_Name}.app/Contents/Frameworks") set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") # Mac bundle settings set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_DisplayName}") set(MACOSX_BUNDLE_INFO_STRING "${Launcher_DisplayName}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") set(MACOSX_BUNDLE_GUI_IDENTIFIER "${Launcher_AppID}") set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_NAME}") set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) set(MACOSX_BUNDLE_COPYRIGHT "${Launcher_Copyright_Mac}") set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed") set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed") set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0/Sparkle-2.8.0.tar.xz" CACHE STRING "URL to Sparkle release archive") set(MACOSX_SPARKLE_SHA256 "fd5681ee92bf238aaac2d08214ceaf0cc8976e452d7f882d80bac1e61581f3b1" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "") set(Launcher_ENABLE_UPDATER YES) endif() # Add the icon install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) find_program(ACTOOL_EXE actool DOC "Path to the apple asset catalog compiler") if(ACTOOL_EXE) execute_process( COMMAND xcodebuild -version OUTPUT_VARIABLE XCODE_VERSION_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ) string(REGEX MATCH "Xcode ([0-9]+\.[0-9]+)" XCODE_VERSION_MATCH "${XCODE_VERSION_OUTPUT}") if(XCODE_VERSION_MATCH) set(XCODE_VERSION ${CMAKE_MATCH_1}) else() set(XCODE_VERSION 0.0) endif() if(XCODE_VERSION VERSION_GREATER_EQUAL 26.0) set(ASSETS_OUT_DIR "${CMAKE_BINARY_DIR}/program_info") set(GENERATED_ASSETS_CAR "${ASSETS_OUT_DIR}/Assets.car") set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Branding_MAC_ICON}") add_custom_command( OUTPUT "${GENERATED_ASSETS_CAR}" COMMAND ${ACTOOL_EXE} "${ICON_SOURCE}" --compile "${ASSETS_OUT_DIR}" --output-partial-info-plist /dev/null --app-icon PrismLauncher --enable-on-demand-resources NO --target-device mac --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} --platform macosx DEPENDS "${ICON_SOURCE}" COMMENT "Compiling asset catalog (${ICON_SOURCE})" VERBATIM ) add_custom_target(compile_assets ALL DEPENDS "${GENERATED_ASSETS_CAR}") install(FILES "${GENERATED_ASSETS_CAR}" DESTINATION "${RESOURCES_DEST_DIR}") else() message(WARNING "Xcode ${XCODE_VERSION} is too old. Minimum required version is 26.0. Not compiling liquid glass icons.") endif() else() message(WARNING "actool not found. Cannot compile macOS app icons.\n" "Install Xcode command line tools: 'xcode-select --install'") endif() elseif(UNIX) include(KDEInstallDirs) set(BINARY_DEST_DIR "bin") set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") set(JARS_DEST_DIR "share/${Launcher_Name}") # Set RPATH SET(Launcher_BINARY_RPATH "$ORIGIN/") install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_Desktop} DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_MetaInfo} DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_SVG} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/scalable/apps") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_PNG_256} DESTINATION "${KDE_INSTALL_ICONDIR}/hicolor/256x256/apps" RENAME "${Launcher_AppID}.png") install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_mrpack_MIMEInfo} DESTINATION ${KDE_INSTALL_MIMEDIR}) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") set(PLUGIN_DEST_DIR "plugins") set(BUNDLE_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") if(Launcher_ManPage) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") endif() # Install basic runner script if component "portable" is selected configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION "." RENAME ${Launcher_Name} COMPONENT portable EXCLUDE_FROM_ALL) elseif(WIN32) set(BINARY_DEST_DIR ".") set(LIBRARY_DEST_DIR ".") set(PLUGIN_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") set(JARS_DEST_DIR "jars") else() message(FATAL_ERROR "Platform not supported") endif() ################################ Included Libs ################################ include(ExternalProject) set_directory_properties(PROPERTIES EP_BASE External) option(NBT_BUILD_SHARED "Build NBT shared library" OFF) option(NBT_USE_ZLIB "Build NBT library with zlib support" OFF) option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests. add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker add_subdirectory(libraries/rainbow) # Qt extension for colors add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/qdcss) # css parser ############################### Built Artifacts ############################### add_subdirectory(buildconfig) if(BUILD_TESTING) add_subdirectory(tests) endif() # NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. add_subdirectory(launcher) PrismLauncher-10.0.5/.github/0000755000175100017510000000000015144136756015333 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/0000755000175100017510000000000015144136756016773 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/setup-dependencies/0000755000175100017510000000000015144136756022557 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/setup-dependencies/linux/0000755000175100017510000000000015144136756023716 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/setup-dependencies/linux/action.yml0000644000175100017510000000334715144136756025725 0ustar runnerrunnername: Setup Linux dependencies description: Install and setup dependencies for building Prism Launcher runs: using: composite steps: - name: Install host dependencies shell: bash run: | sudo apt-get -y update sudo apt-get -y install \ dpkg-dev \ ninja-build extra-cmake-modules pkg-config scdoc \ cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \ libxcb-cursor-dev libtomlplusplus-dev - name: Setup AppImage tooling shell: bash env: GH_TOKEN: ${{ github.token }} run: | # Determinate AppImage architecture to use dpkg_arch="$(dpkg-architecture -q DEB_HOST_ARCH_CPU)" case "$dpkg_arch" in "amd64") APPIMAGE_ARCH="x86_64" ;; "arm64") APPIMAGE_ARCH="aarch64" ;; *) echo "# 🚨 The Debian architecture \"$deb_arch\" is not recognized!" >> "$GITHUB_STEP_SUMMARY" exit 1 ;; esac gh release download \ --repo VHSgunzo/sharun \ --pattern "sharun-$APPIMAGE_ARCH-aio" \ --output ~/bin/sharun # FIXME!: revert this to probonopd/go-appimage once https://github.com/probonopd/go-appimage/pull/377 is merged! gh release download continuous \ --repo DioEgizio/go-appimage \ --pattern "mkappimage-*-$APPIMAGE_ARCH.AppImage" \ --output ~/bin/mkappimage gh release download \ --repo AppImageCommunity/AppImageUpdate \ --pattern "AppImageUpdate-$APPIMAGE_ARCH.AppImage" \ --output ~/bin/AppImageUpdate.AppImage chmod +x ~/bin/* echo "$HOME/bin" >> "$GITHUB_PATH" PrismLauncher-10.0.5/.github/actions/setup-dependencies/action.yml0000644000175100017510000000467215144136756024570 0ustar runnerrunnername: Setup Dependencies description: Install and setup dependencies for building Prism Launcher inputs: build-type: description: Type for the build required: true default: Debug artifact-name: description: Name of the uploaded artifact required: true msystem: description: MSYS2 subsystem to use required: false vcvars-arch: description: Visual Studio architecture to use required: false qt-architecture: description: Qt architecture required: false qt-version: description: Version of Qt to use required: true outputs: build-type: description: Type of build used value: ${{ inputs.build-type }} qt-version: description: Version of Qt used value: ${{ inputs.qt-version }} runs: using: composite steps: - name: Setup Linux dependencies if: ${{ runner.os == 'Linux' }} uses: ./.github/actions/setup-dependencies/linux - name: Setup macOS dependencies if: ${{ runner.os == 'macOS' }} uses: ./.github/actions/setup-dependencies/macos with: build-type: ${{ inputs.build-type }} - name: Setup Windows dependencies if: ${{ runner.os == 'Windows' }} uses: ./.github/actions/setup-dependencies/windows with: build-type: ${{ inputs.build-type }} msystem: ${{ inputs.msystem }} vcvars-arch: ${{ inputs.vcvars-arch }} # TODO(@getchoo): Get this working on MSYS2! - name: Setup ccache if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} uses: hendrikmuhs/ccache-action@v1.2.20 with: variant: sccache create-symlink: ${{ runner.os != 'Windows' }} key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.artifact-name }}-sccache - name: Use ccache on debug builds if: ${{ inputs.build-type == 'Debug' }} shell: bash env: # Only use ccache on MSYS2 CCACHE_VARIANT: ${{ (runner.os == 'Windows' && inputs.msystem != '') && 'ccache' || 'sccache' }} run: | echo "CMAKE_C_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" echo "CMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" - name: Install Qt if: ${{ inputs.msystem == '' }} uses: jurplel/install-qt-action@v4 with: aqtversion: "==3.1.*" version: ${{ inputs.qt-version }} modules: qtimageformats qtnetworkauth cache: ${{ inputs.build-type == 'Debug' }} PrismLauncher-10.0.5/.github/actions/setup-dependencies/macos/0000755000175100017510000000000015144136756023661 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/setup-dependencies/macos/action.yml0000644000175100017510000000245215144136756025664 0ustar runnerrunnername: Setup macOS dependencies inputs: build-type: description: Type for the build required: true default: Debug runs: using: composite steps: - name: Install dependencies shell: bash run: | brew update brew install ninja extra-cmake-modules temurin@17 mono - name: Set JAVA_HOME shell: bash run: | echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV" - name: Setup vcpkg cache if: ${{ inputs.build-type == 'Debug' }} shell: bash env: USERNAME: ${{ github.repository_owner }} GITHUB_TOKEN: ${{ github.token }} FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json run: | mono `vcpkg fetch nuget | tail -n 1` \ sources add \ -Source "$FEED_URL" \ -StorePasswordInClearText \ -Name GitHubPackages \ -UserName "$USERNAME" \ -Password "$GITHUB_TOKEN" mono `vcpkg fetch nuget | tail -n 1` \ setapikey "$GITHUB_TOKEN" \ -Source "$FEED_URL" echo "VCPKG_BINARY_SOURCES=clear;nuget,$FEED_URL,readwrite" >> "$GITHUB_ENV" - name: Setup vcpkg environment shell: bash run: | echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" PrismLauncher-10.0.5/.github/actions/setup-dependencies/windows/0000755000175100017510000000000015144136756024251 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/setup-dependencies/windows/action.yml0000644000175100017510000000616715144136756026263 0ustar runnerrunnername: Setup Windows Dependencies description: Install and setup dependencies for building Prism Launcher inputs: build-type: description: Type for the build required: true default: Debug msystem: description: MSYS2 subsystem to use required: false vcvars-arch: description: Visual Studio architecture to use required: true default: amd64 runs: using: composite steps: # NOTE: Installed on MinGW as well for SignTool - name: Enter VS Developer shell if: ${{ runner.os == 'Windows' }} uses: ilammy/msvc-dev-cmd@v1 with: arch: ${{ inputs.vcvars-arch }} vsversion: 2022 - name: Setup Java (MSVC) uses: actions/setup-java@v5 with: # NOTE(@getchoo): We should probably stay on Zulu. # Temurin doesn't have Java 17 builds for WoA distribution: zulu java-version: 17 - name: Setup vcpkg cache (MSVC) if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} shell: pwsh env: USERNAME: ${{ github.repository_owner }} GITHUB_TOKEN: ${{ github.token }} FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json run: | .$(vcpkg fetch nuget) ` sources add ` -Source "$env:FEED_URL" ` -StorePasswordInClearText ` -Name GitHubPackages ` -UserName "$env:USERNAME" ` -Password "$env:GITHUB_TOKEN" .$(vcpkg fetch nuget) ` setapikey "$env:GITHUB_TOKEN" ` -Source "$env:FEED_URL" "VCPKG_BINARY_SOURCES=clear;nuget,$env:FEED_URL,readwrite" | Out-File -Append $env:GITHUB_ENV - name: Setup vcpkg environment (MSVC) if: ${{ inputs.msystem == '' }} shell: bash run: | echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" - name: Setup MSYS2 (MinGW) if: ${{ inputs.msystem != '' }} uses: msys2/setup-msys2@v2 with: msystem: ${{ inputs.msystem }} update: true install: >- git pacboy: >- toolchain:p ccache:p cmake:p extra-cmake-modules:p ninja:p qt6-base:p qt6-svg:p qt6-imageformats:p qt6-networkauth:p cmark:p qrencode:p tomlplusplus:p libarchive:p - name: List pacman packages (MinGW) if: ${{ inputs.msystem != '' }} shell: msys2 {0} run: | pacman -Qe - name: Retrieve ccache cache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} uses: actions/cache@v5.0.1 with: path: '${{ github.workspace }}\.ccache' key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} restore-keys: | ${{ runner.os }}-mingw-w64-ccache - name: Setup ccache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} shell: msys2 {0} run: | ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' ccache --set-config=max_size='500M' ccache --set-config=compression=true ccache -p # Show config PrismLauncher-10.0.5/.github/actions/package/0000755000175100017510000000000015144136756020366 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/package/linux/0000755000175100017510000000000015144136756021525 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/package/linux/action.yml0000644000175100017510000001360515144136756023532 0ustar runnerrunnername: Package for Linux description: Create Linux packages for Prism Launcher inputs: version: description: Launcher version required: true build-type: description: Type for the build required: true default: Debug artifact-name: description: Name of the uploaded artifact required: true default: Linux qt-version: description: Version of Qt to use required: true gpg-private-key: description: Private key for AppImage signing required: false gpg-private-key-id: description: ID for the gpg-private-key, to select the signing key required: false runs: using: composite steps: - name: Cleanup Qt installation on Linux shell: bash run: | rm -rf "$QT_PLUGIN_PATH"/printsupport rm -rf "$QT_PLUGIN_PATH"/sqldrivers rm -rf "$QT_PLUGIN_PATH"/help rm -rf "$QT_PLUGIN_PATH"/designer rm -rf "$QT_PLUGIN_PATH"/qmltooling rm -rf "$QT_PLUGIN_PATH"/qmlls rm -rf "$QT_PLUGIN_PATH"/qmllint rm -rf "$QT_PLUGIN_PATH"/platformthemes/libqgtk3.so - name: Setup build variables shell: bash run: | # Fixup architecture naming for AppImages dpkg_arch="$(dpkg-architecture -q DEB_HOST_ARCH_CPU)" case "$dpkg_arch" in "amd64") APPIMAGE_ARCH="x86_64" ;; "arm64") APPIMAGE_ARCH="aarch64" ;; *) echo "# 🚨 The Debian architecture \"$deb_arch\" is not recognized!" >> "$GITHUB_STEP_SUMMARY" exit 1 ;; esac echo "APPIMAGE_ARCH=$APPIMAGE_ARCH" >> "$GITHUB_ENV" # Used for the file paths of libraries echo "DEB_HOST_MULTIARCH=$(dpkg-architecture -q DEB_HOST_MULTIARCH)" >> "$GITHUB_ENV" - name: Package AppImage shell: bash env: VERSION: ${{ github.ref_type == 'tag' && github.ref_name || inputs.version }} BUILD_DIR: build INSTALL_APPIMAGE_DIR: install-appdir GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }} run: | cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }} if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then echo "$GPG_PRIVATE_KEY" > privkey.asc gpg --import privkey.asc gpg --export --armor ${{ inputs.gpg-private-key-id }} > pubkey.asc else echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY fi sharun lib4bin \ --hard-links \ --with-hooks \ --dst-dir "$INSTALL_APPIMAGE_DIR" \ "$INSTALL_APPIMAGE_DIR"/bin/* "$QT_PLUGIN_PATH"/*/*.so cp ~/bin/AppImageUpdate.AppImage "$INSTALL_APPIMAGE_DIR"/bin/ # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! find "$INSTALL_APPIMAGE_DIR" -name '*gamemode*' -exec rm {} + #makes the launcher use portals for file picking echo "QT_QPA_PLATFORMTHEME=xdgdesktopportal" > "$INSTALL_APPIMAGE_DIR"/.env ln -s org.prismlauncher.PrismLauncher.metainfo.xml "$INSTALL_APPIMAGE_DIR"/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml ln -s share/applications/org.prismlauncher.PrismLauncher.desktop "$INSTALL_APPIMAGE_DIR" ln -s share/icons/hicolor/256x256/apps/org.prismlauncher.PrismLauncher.png "$INSTALL_APPIMAGE_DIR" mv "$INSTALL_APPIMAGE_DIR"/{sharun,AppRun} ls -la "$INSTALL_APPIMAGE_DIR" if [[ "${{ github.ref_type }}" == "tag" ]]; then APPIMAGE_DEST="PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage" else APPIMAGE_DEST="PrismLauncher-Linux-$VERSION-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage" fi mkappimage \ --updateinformation "gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage.zsync" \ "$INSTALL_APPIMAGE_DIR" \ "$APPIMAGE_DEST" - name: Package portable tarball shell: bash env: BUILD_DIR: build INSTALL_PORTABLE_DIR: install-portable run: | cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable sharun lib4bin \ --with-hooks \ --hard-links \ --dst-dir "$INSTALL_PORTABLE_DIR" \ "$INSTALL_PORTABLE_DIR"/bin/* "$QT_PLUGIN_PATH"/*/*.so # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! find "$INSTALL_PORTABLE_DIR" -name '*gamemode*' -exec rm {} + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f -o -type l); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt cd ${{ env.INSTALL_PORTABLE_DIR }} tar -czf ../PrismLauncher-portable.tar.gz * - name: Upload binary tarball uses: actions/upload-artifact@v6 with: name: PrismLauncher-${{ inputs.artifact-name }}-Qt6-Portable-${{ inputs.version }}-${{ inputs.build-type }} path: PrismLauncher-portable.tar.gz - name: Upload AppImage uses: actions/upload-artifact@v6 with: name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage path: PrismLauncher-${{ runner.os }}-*${{ env.APPIMAGE_ARCH }}.AppImage - name: Upload AppImage Zsync uses: actions/upload-artifact@v6 with: name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync path: PrismLauncher-${{ runner.os }}-*${{ env.APPIMAGE_ARCH }}.AppImage.zsync PrismLauncher-10.0.5/.github/actions/package/macos/0000755000175100017510000000000015144136756021470 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/package/macos/action.yml0000644000175100017510000001105615144136756023473 0ustar runnerrunnername: Package for macOS description: Create a macOS package for Prism Launcher inputs: version: description: Launcher version required: true build-type: description: Type for the build required: true default: Debug artifact-name: description: Name of the uploaded artifact required: true default: macOS apple-codesign-cert: description: Certificate for signing macOS builds required: false apple-codesign-password: description: Password for signing macOS builds required: false apple-codesign-id: description: Certificate ID for signing macOS builds required: false apple-notarize-apple-id: description: Apple ID used for notarizing macOS builds required: false apple-notarize-team-id: description: Team ID used for notarizing macOS builds required: false apple-notarize-password: description: Password used for notarizing macOS builds required: false sparkle-ed25519-key: description: Private key for signing Sparkle updates required: false runs: using: composite steps: - name: Fetch codesign certificate shell: bash run: | echo '${{ inputs.apple-codesign-cert }}' | base64 --decode > codesign.p12 if [ -n '${{ inputs.apple-codesign-id }}' ]; then security create-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain security default-keychain -s build.keychain security unlock-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain security import codesign.p12 -k build.keychain -P '${{ inputs.apple-codesign-password }}' -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ inputs.apple-codesign-password }}' build.keychain else echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY fi - name: Package shell: bash env: BUILD_DIR: build INSTALL_DIR: install run: | cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} cd ${{ env.INSTALL_DIR }} chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" if [ -n '${{ inputs.apple-codesign-id }}' ]; then APPLE_CODESIGN_ID='${{ inputs.apple-codesign-id }}' ENTITLEMENTS_FILE='../program_info/App.entitlements' else APPLE_CODESIGN_ID='-' ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements' fi sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" mv "PrismLauncher.app" "Prism Launcher.app" - name: Notarize shell: bash env: INSTALL_DIR: install run: | cd ${{ env.INSTALL_DIR }} if [ -n '${{ inputs.apple-notarize-password }}' ]; then ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip xcrun notarytool submit ../PrismLauncher.zip \ --wait --progress \ --apple-id '${{ inputs.apple-notarize-apple-id }}' \ --team-id '${{ inputs.apple-notarize-team-id }}' \ --password '${{ inputs.apple-notarize-password }}' xcrun stapler staple "Prism Launcher.app" else echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY fi ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip - name: Make Sparkle signature shell: bash run: | if [ '${{ inputs.sparkle-ed25519-key }}' != '' ]; then echo '${{ inputs.sparkle-ed25519-key }}' > ed25519-priv.pem signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) rm ed25519-priv.pem cat >> $GITHUB_STEP_SUMMARY << EOF ### Artifact Information :information_source: - :memo: Sparkle Signature (ed25519): \`$signature\` EOF else cat >> $GITHUB_STEP_SUMMARY << EOF ### Artifact Information :information_source: - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork) EOF fi - name: Upload binary tarball uses: actions/upload-artifact@v6 with: name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} path: PrismLauncher.zip PrismLauncher-10.0.5/.github/actions/package/windows/0000755000175100017510000000000015144136756022060 5ustar runnerrunnerPrismLauncher-10.0.5/.github/actions/package/windows/action.yml0000644000175100017510000001752315144136756024070 0ustar runnerrunnername: Package for Windows description: Create a Windows package for Prism Launcher inputs: version: description: Launcher version required: true build-type: description: Type for the build required: true default: Debug artifact-name: description: Name of the uploaded artifact required: true msystem: description: MSYS2 subsystem to use required: false azure-client-id: description: Client ID for the Azure Signer Application required: true azure-tenant-id: description: Tenant ID for the Azure Signer Application required: true azure-subscription-id: description: Subscription ID for the Azure Signer Application required: true runs: using: composite steps: - name: Package (MinGW) if: ${{ inputs.msystem != '' }} shell: msys2 {0} env: BUILD_DIR: build INSTALL_DIR: install run: | cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} touch ${{ env.INSTALL_DIR }}/manifest.txt for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt - name: Package (MSVC) if: ${{ inputs.msystem == '' }} shell: pwsh env: BUILD_DIR: build INSTALL_DIR: install run: | cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} cd ${{ github.workspace }} Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - name: Emit warning for unsigned builds if: ${{ env.CI_HAS_ACCESS_TO_AZURE == '' || inputs.azure-client-id == '' }} shell: pwsh run: | ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - name: Login to Azure if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} uses: azure/login@v2 with: client-id: ${{ inputs.azure-client-id }} tenant-id: ${{ inputs.azure-tenant-id }} subscription-id: ${{ inputs.azure-subscription-id }} - name: Sign executables if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} uses: azure/artifact-signing-action@v1 with: endpoint: https://eus.codesigning.azure.net/ trusted-signing-account-name: PrismLauncher certificate-profile-name: PrismLauncher files-folder: ${{ github.workspace }}\install\ files-folder-filter: dll,exe files-folder-recurse: true files-folder-depth: 2 # recommended in https://github.com/Azure/artifact-signing-action#timestamping-1 timestamp-rfc3161: "http://timestamp.acs.microsoft.com" timestamp-digest: "SHA256" # TODO(@getchoo): Is this all really needed??? # https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md exclude-environment-credential: true exclude-workload-identity-credential: true exclude-managed-identity-credential: true exclude-shared-token-cache-credential: true exclude-visual-studio-credential: true exclude-visual-studio-code-credential: true exclude-azure-cli-credential: false exclude-azure-powershell-credential: true exclude-azure-developer-cli-credential: true exclude-interactive-browser-credential: true - name: Package (MinGW, portable) if: ${{ inputs.msystem != '' }} shell: msys2 {0} env: BUILD_DIR: build INSTALL_DIR: install INSTALL_PORTABLE_DIR: install-portable run: | cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt - name: Package (MSVC, portable) if: ${{ inputs.msystem == '' }} shell: pwsh env: BUILD_DIR: build INSTALL_DIR: install INSTALL_PORTABLE_DIR: install-portable run: | cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - name: Package (installer) shell: pwsh env: BUILD_DIR: build INSTALL_DIR: install NSCURL_VERSION: "v24.9.26.122" NSCURL_SHA256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" run: | New-Item -Name NSISPlugins -ItemType Directory Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/"${{ env.NSCURL_VERSION }}"/NScurl.zip -OutFile NSISPlugins\NScurl.zip $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash if ( $nscurl_hash -ne "${{ env.nscurl_sha256 }}") { echo "::error:: NSCurl.zip sha256 mismatch" exit 1 } Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl cd ${{ env.INSTALL_DIR }} makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" - name: Sign installer if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} uses: azure/artifact-signing-action@v1 with: endpoint: https://eus.codesigning.azure.net/ trusted-signing-account-name: PrismLauncher certificate-profile-name: PrismLauncher files: | ${{ github.workspace }}\PrismLauncher-Setup.exe # recommended in https://github.com/Azure/artifact-signing-action#timestamping-1 timestamp-rfc3161: "http://timestamp.acs.microsoft.com" timestamp-digest: "SHA256" # TODO(@getchoo): Is this all really needed??? # https://github.com/Azure/trusted-signing-action/blob/fc390cf8ed0f14e248a542af1d838388a47c7a7c/docs/OIDC.md exclude-environment-credential: true exclude-workload-identity-credential: true exclude-managed-identity-credential: true exclude-shared-token-cache-credential: true exclude-visual-studio-credential: true exclude-visual-studio-code-credential: true exclude-azure-cli-credential: false exclude-azure-powershell-credential: true exclude-azure-developer-cli-credential: true exclude-interactive-browser-credential: true - name: Upload binary zip uses: actions/upload-artifact@v6 with: name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} path: install/** - name: Upload portable zip uses: actions/upload-artifact@v6 with: name: PrismLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }} path: install-portable/** - name: Upload installer uses: actions/upload-artifact@v6 with: name: PrismLauncher-${{ inputs.artifact-name }}-Setup-${{ inputs.version }}-${{ inputs.build-type }} path: PrismLauncher-Setup.exe PrismLauncher-10.0.5/.github/workflows/0000755000175100017510000000000015144136756017370 5ustar runnerrunnerPrismLauncher-10.0.5/.github/workflows/stale.yml0000644000175100017510000000116015144136756021221 0ustar runnerrunnername: Stale on: schedule: # run weekly on sunday - cron: "0 0 * * 0" workflow_dispatch: jobs: label: name: Label issues and PRs runs-on: ubuntu-latest permissions: issues: write pull-requests: write steps: - uses: actions/stale@v10 with: days-before-stale: 60 days-before-close: -1 # Don't close anything exempt-issue-labels: rfc,nostale,help wanted exempt-all-milestones: true exempt-all-assignees: true operations-per-run: 1000 stale-issue-label: inactive stale-pr-label: inactive PrismLauncher-10.0.5/.github/workflows/publish.yml0000644000175100017510000000076215144136756021566 0ustar runnerrunnername: Publish on: release: types: [ released ] permissions: contents: read jobs: winget: name: Winget runs-on: windows-latest steps: - name: Publish on Winget uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: PrismLauncher.PrismLauncher version: ${{ github.event.release.tag_name }} installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$' token: ${{ secrets.WINGET_TOKEN }} PrismLauncher-10.0.5/.github/workflows/blocked-prs.yml0000644000175100017510000002420315144136756022321 0ustar runnerrunnername: Blocked/Stacked Pull Requests Automation on: pull_request_target: types: - opened - reopened - edited - synchronize workflow_dispatch: inputs: pr_id: description: Local Pull Request number to work on required: true type: number jobs: blocked_status: name: Check Blocked Status runs-on: ubuntu-latest steps: - name: Generate token id: generate-token uses: actions/create-github-app-token@v2 with: app-id: ${{ vars.PULL_REQUEST_APP_ID }} private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} - name: Setup From Dispatch Event if: github.event_name == 'workflow_dispatch' id: dispatch_event_setup env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} PR_NUMBER: ${{ inputs.pr_id }} run: | # setup env for the rest of the workflow OWNER=$(dirname "${{ github.repository }}") REPO=$(basename "${{ github.repository }}") PR_JSON=$( gh api \ -H "Accept: application/vnd.github.raw+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "/repos/$OWNER/$REPO/pulls/$PR_NUMBER" ) echo "PR_JSON=$PR_JSON" >> "$GITHUB_ENV" - name: Setup Environment id: env_setup env: EVENT_PR_JSON: ${{ toJSON(github.event.pull_request) }} run: | # setup env for the rest of the workflow PR_JSON=${PR_JSON:-"$EVENT_PR_JSON"} { echo "REPO=$(jq -r '.base.repo.name' <<< "$PR_JSON")" echo "OWNER=$(jq -r '.base.repo.owner.login' <<< "$PR_JSON")" echo "PR_NUMBER=$(jq -r '.number' <<< "$PR_JSON")" echo "JOB_DATA=$(jq -c ' { "repo": .base.repo.name, "owner": .base.repo.owner.login, "repoUrl": .base.repo.html_url, "prNumber": .number, "prHeadSha": .head.sha, "prHeadLabel": .head.label, "prBody": (.body // ""), "prLabels": (reduce .labels[].name as $l ([]; . + [$l])) } ' <<< "$PR_JSON")" } >> "$GITHUB_ENV" - name: Find Blocked/Stacked PRs in body id: pr_ids run: | prs=$( jq -c ' .prBody as $body | ( $body | reduce ( . | scan("[Bb]locked (?:[Bb]y|[Oo]n):? #([0-9]+)") | map({ "type": "Blocked on", "number": ( . | tonumber ) }) ) as $i ([]; . + [$i[]]) ) as $bprs | ( $body | reduce ( . | scan("[Ss]tacked [Oo]n:? #([0-9]+)") | map({ "type": "Stacked on", "number": ( . | tonumber ) }) ) as $i ([]; . + [$i[]]) ) as $sprs | ($bprs + $sprs) as $prs | { "blocking": $prs, "numBlocking": ( $prs | length), } ' <<< "$JOB_DATA" ) echo "prs=$prs" >> "$GITHUB_OUTPUT" - name: Collect Blocked PR Data id: blocking_data if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} BLOCKING_PRS: ${{ steps.pr_ids.outputs.prs }} run: | blocked_pr_data=$( while read -r pr_data ; do gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "/repos/$OWNER/$REPO/pulls/$(jq -r '.number' <<< "$pr_data")" \ | jq -c --arg type "$(jq -r '.type' <<< "$pr_data")" \ ' . | { "type": $type, "number": .number, "merged": .merged, "state": (if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end), "labels": (reduce .labels[].name as $l ([]; . + [$l])), "basePrUrl": .html_url, "baseRepoName": .head.repo.name, "baseRepoOwner": .head.repo.owner.login, "baseRepoUrl": .head.repo.html_url, "baseSha": .head.sha, "baseRefName": .head.ref, } ' done < <(jq -c '.blocking[]' <<< "$BLOCKING_PRS") | jq -c -s ) { echo "data=$blocked_pr_data"; echo "all_merged=$(jq -r 'all(.[] | (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")); .)' <<< "$blocked_pr_data")"; echo "current_blocking=$(jq -c 'map( select( (.type == "Stacked on" and (.merged | not)) or (.type == "Blocked on" and (.state == "Open")) ) | .number )' <<< "$blocked_pr_data" )"; } >> "$GITHUB_OUTPUT" - name: Add 'blocked' Label if Missing id: label_blocked if: (fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'blocked') && !fromJSON(steps.blocking_data.outputs.all_merged) continue-on-error: true env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} run: | gh -R ${{ github.repository }} issue edit --add-label 'blocked' "$PR_NUMBER" - name: Remove 'blocked' Label if All Dependencies Are Merged id: unlabel_blocked if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 && fromJSON(steps.blocking_data.outputs.all_merged) continue-on-error: true env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} run: | gh -R ${{ github.repository }} issue edit --remove-label 'blocked' "$PR_NUMBER" - name: Apply 'blocking' Label to Unmerged Dependencies id: label_blocking if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 continue-on-error: true env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} BLOCKING_ISSUES: ${{ steps.blocking_data.outputs.current_blocking }} run: | while read -r pr ; do gh -R ${{ github.repository }} issue edit --add-label 'blocking' "$pr" || true done < <(jq -c '.[]' <<< "$BLOCKING_ISSUES") - name: Apply Blocking PR Status Check id: blocked_check if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 continue-on-error: true env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }} run: | pr_head_sha=$(jq -r '.prHeadSha' <<< "$JOB_DATA") # create commit Status, overwrites previous identical context while read -r pr_data ; do DESC=$( jq -r 'if .type == "Stacked on" then "Stacked PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged" else "Blocking PR #" + (.number | tostring) + " is " + (if .state == "Open" then "" else "not yet " end) + "merged or closed" end ' <<< "$pr_data" ) gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \ -f "state=$(jq -r 'if (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")) then "success" else "failure" end' <<< "$pr_data")" \ -f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \ -f "description=$DESC" \ -f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")" done < <(jq -c '.[]' <<< "$BLOCKING_DATA") - name: Context Comment id: generate-comment if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 continue-on-error: true env: BLOCKING_DATA: ${{ steps.blocking_data.outputs.data }} run: | COMMENT_PATH="$(pwd)/temp_comment_file.txt" echo '

PR Dependencies :pushpin:

' > "$COMMENT_PATH" echo >> "$COMMENT_PATH" pr_head_label=$(jq -r '.prHeadLabel' <<< "$JOB_DATA") while read -r pr_data ; do base_pr=$(jq -r '.number' <<< "$pr_data") base_ref_name=$(jq -r '.baseRefName' <<< "$pr_data") base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data") base_repo_name=$(jq -r '.baseRepoName' <<< "$pr_data") compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label" status=$(jq -r ' if .type == "Stacked on" then if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged (" + .state + ")" end else if .state != "Open" then ":white_check_mark: " + .state else ":x: Open" end end ' <<< "$pr_data") type=$(jq -r '.type' <<< "$pr_data") echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH" done < <(jq -c '.[]' <<< "$BLOCKING_DATA") { echo 'body<> "$GITHUB_OUTPUT" - name: 💬 PR Comment if: fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0 continue-on-error: true env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} COMMENT_BODY: ${{ steps.generate-comment.outputs.body }} run: | gh -R ${{ github.repository }} issue comment "$PR_NUMBER" \ --body "$COMMENT_BODY" \ --create-if-none \ --edit-last PrismLauncher-10.0.5/.github/workflows/nix.yml0000644000175100017510000000554015144136756020715 0ustar runnerrunnername: Nix concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: push: branches: - "develop" - "release-*" tags: - "*" paths: # File types - "**.cpp" - "**.h" - "**.java" - "**.ui" # Build files - "**.nix" - "nix/**" - "flake.lock" # Directories - "buildconfig/**" - "cmake/**" - "launcher/**" - "libraries/**" - "program_info/**" - "tests/**" # Files - "CMakeLists.txt" - "COPYING.md" # Workflows - ".github/workflows/nix.yml" pull_request: paths: # File types - "**.cpp" - "**.h" - "**.java" - "**.ui" # Build files - "**.nix" - "nix/**" - "flake.lock" # Directories - "buildconfig/**" - "cmake/**" - "launcher/**" - "libraries/**" - "program_info/**" - "tests/**" # Files - "CMakeLists.txt" - "COPYING.md" # Workflows - ".github/workflows/nix.yml" workflow_dispatch: permissions: contents: read env: DEBUG: ${{ github.ref_type != 'tag' }} jobs: build: name: Build (${{ matrix.system }}) strategy: fail-fast: false matrix: include: - os: ubuntu-22.04 system: x86_64-linux - os: ubuntu-22.04-arm system: aarch64-linux - os: macos-14 system: aarch64-darwin runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v6 - name: Install Nix uses: cachix/install-nix-action@v31 # For PRs - name: Setup Nix Magic Cache if: ${{ github.event_name == 'pull_request' }} uses: DeterminateSystems/magic-nix-cache-action@v13 with: diagnostic-endpoint: "" use-flakehub: false # For in-tree builds - name: Setup Cachix if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} uses: cachix/cachix-action@v16 with: name: prismlauncher authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Run Flake checks run: | nix flake check --print-build-logs --show-trace - name: Build debug package if: ${{ env.DEBUG == 'true' }} run: | nix build \ --no-link --print-build-logs --print-out-paths \ .#prismlauncher-debug >> "$GITHUB_STEP_SUMMARY" - name: Build release package if: ${{ env.DEBUG == 'false' }} env: TAG: ${{ github.ref_name }} SYSTEM: ${{ matrix.system }} run: | nix build --no-link --print-out-paths .#prismlauncher \ | tee -a "$GITHUB_STEP_SUMMARY" \ | xargs cachix pin prismlauncher "$TAG"-"$SYSTEM" PrismLauncher-10.0.5/.github/workflows/merge-blocking-pr.yml0000644000175100017510000000420015144136756023413 0ustar runnerrunnername: Merged Blocking Pull Request Automation on: pull_request_target: types: - closed workflow_dispatch: inputs: pr_id: description: Local Pull Request number to work on required: true type: number jobs: update-blocked-status: name: Update Blocked Status runs-on: ubuntu-latest # a pr that was a `blocking:` label was merged. # find the open pr's it was blocked by and trigger a refresh of their state if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') }} steps: - name: Generate token id: generate-token uses: actions/create-github-app-token@v2 with: app-id: ${{ vars.PULL_REQUEST_APP_ID }} private-key: ${{ secrets.PULL_REQUEST_APP_PRIVATE_KEY }} - name: Gather Dependent PRs id: gather_deps env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }} run: | blocked_prs=$( gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \ | jq -c --argjson pr "$PR_NUMBER" ' reduce ( .[] | select( .body | scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") | map(tonumber) | any(.[]; . == $pr) )) as $i ([]; . + [$i]) ' ) { echo "deps=$blocked_prs" echo "numdeps=$(jq -r '. | length' <<< "$blocked_prs")" } >> "$GITHUB_OUTPUT" - name: Trigger Blocked PR Workflows for Dependants if: fromJSON(steps.gather_deps.outputs.numdeps) > 0 env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} DEPS: ${{ steps.gather_deps.outputs.deps }} run: | while read -r pr ; do gh -R ${{ github.repository }} workflow run 'blocked-prs.yml' -r "${{ github.ref_name }}" -f pr_id="$pr" done < <(jq -c '.[].number' <<< "$DEPS") PrismLauncher-10.0.5/.github/workflows/backport.yml0000644000175100017510000000234215144136756021721 0ustar runnerrunnername: Backport on: pull_request_target: types: [closed, labeled] # WARNING: # When extending this action, be aware that $GITHUB_TOKEN allows write access to # the GitHub repository. This means that it should not evaluate user input in a # way that allows code injection. permissions: contents: read jobs: backport: permissions: contents: write # for korthout/backport-action to create branch pull-requests: write # for korthout/backport-action to create PR to backport actions: write # for korthout/backport-action to create PR with workflow changes name: Backport Pull Request if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs uses: korthout/backport-action@v4.0.0 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- Bot-based backport to `${target_branch}`, triggered by a label in #${pull_number}. PrismLauncher-10.0.5/.github/workflows/update-flake.yml0000644000175100017510000000127615144136756022463 0ustar runnerrunnername: Update Flake Lockfile on: schedule: # run weekly on sunday - cron: "0 0 * * 0" workflow_dispatch: permissions: contents: write pull-requests: write jobs: update-flake: if: github.repository == 'PrismLauncher/PrismLauncher' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31 - uses: DeterminateSystems/update-flake-lock@v28 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" pr-labels: | Linux packaging simple change changelog:omit PrismLauncher-10.0.5/.github/workflows/release.yml0000644000175100017510000001241715144136756021540 0ustar runnerrunnername: Build Application and Make Release on: push: tags: - "*" jobs: build_release: name: Build Release uses: ./.github/workflows/build.yml with: build-type: Release environment: Release secrets: inherit create_release: needs: build_release runs-on: ubuntu-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout uses: actions/checkout@v6 with: submodules: "true" path: "PrismLauncher-source" - name: Download artifacts uses: actions/download-artifact@v7 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") echo "VERSION=$tag_name" >> $GITHUB_ENV - name: Package artifacts properly run: | mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-aarch64-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-aarch64-Qt6-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-*.AppImage/PrismLauncher-*-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*-x86_64.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-*.AppImage/PrismLauncher-*-aarch64.AppImage PrismLauncher-Linux-aarch64.AppImage mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*-aarch64.AppImage.zsync PrismLauncher-Linux-aarch64.AppImage.zsync mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} for d in PrismLauncher-Windows-MSVC*; do cd "${d}" || continue LEGACY="$(echo -n ${d} | grep -o Legacy || true)" ARM64="$(echo -n ${d} | grep -o arm64 || true)" INST="$(echo -n ${d} | grep -o Setup || true)" PORT="$(echo -n ${d} | grep -o Portable || true)" NAME="PrismLauncher-Windows-MSVC" test -z "${LEGACY}" || NAME="${NAME}-Legacy" test -z "${ARM64}" || NAME="${NAME}-arm64" test -z "${PORT}" || NAME="${NAME}-Portable" test -z "${INST}" || mv PrismLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * cd .. done for d in PrismLauncher-Windows-MinGW-w64*; do cd "${d}" || continue INST="$(echo -n ${d} | grep -o Setup || true)" PORT="$(echo -n ${d} | grep -o Portable || true)" NAME="PrismLauncher-Windows-MinGW-w64" test -z "${PORT}" || NAME="${NAME}-Portable" test -z "${INST}" || mv PrismLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * cd .. done for d in PrismLauncher-Windows-MinGW-arm64*; do cd "${d}" || continue INST="$(echo -n ${d} | grep -o Setup || true)" PORT="$(echo -n ${d} | grep -o Portable || true)" NAME="PrismLauncher-Windows-MinGW-arm64" test -z "${PORT}" || NAME="${NAME}-Portable" test -z "${INST}" || mv PrismLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * cd .. done - name: Create release id: create_release uses: softprops/action-gh-release@v2 with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ github.ref }} name: Prism Launcher ${{ env.VERSION }} draft: true prerelease: false fail_on_unmatched_files: true files: | PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage.zsync PrismLauncher-Linux-aarch64.AppImage PrismLauncher-Linux-aarch64.AppImage.zsync PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-aarch64-Qt6-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MinGW-arm64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-arm64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-Setup-${{ env.VERSION }}.exe PrismLauncher-macOS-${{ env.VERSION }}.zip PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-10.0.5/.github/workflows/codeql.yml0000644000175100017510000000303515144136756021363 0ustar runnerrunnername: "CodeQL Code Scanning" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: merge_group: types: [checks_requested] pull_request: paths: # File types - "**.cpp" - "**.h" - "**.java" - "**.ui" # Directories - "buildconfig/**" - "cmake/**" - "launcher/**" - "libraries/**" - "program_info/**" - "tests/**" # Files - "CMakeLists.txt" - "COPYING.md" # Workflows - ".github/codeql/**" - ".github/workflows/codeql.yml" - ".github/actions/setup-dependencies/**" workflow_dispatch: jobs: CodeQL: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 with: submodules: "true" - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: config-file: ./.github/codeql/codeql-config.yml queries: security-and-quality languages: cpp, java - name: Setup dependencies uses: ./.github/actions/setup-dependencies with: build-type: Debug qt-version: 6.4.3 - name: Configure and Build run: | cmake --preset linux -DLauncher_USE_PCH=OFF cmake --build --preset linux --config Debug - name: Run tests run: | ctest --preset linux --build-config Debug --extra-verbose --output-on-failure - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 PrismLauncher-10.0.5/.github/workflows/build.yml0000644000175100017510000001363115144136756021216 0ustar runnerrunnername: Build concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true on: merge_group: types: [checks_requested] pull_request: paths: # File types - "**.cpp" - "**.h" - "**.java" - "**.ui" # Directories - "buildconfig/**" - "cmake/**" - "launcher/**" - "libraries/**" - "program_info/**" - "tests/**" # Files - "CMakeLists.txt" - "COPYING.md" # Workflows - ".github/workflows/build.yml" - ".github/actions/package/**" - ".github/actions/setup-dependencies/**" workflow_call: inputs: build-type: description: Type of build (Debug or Release) type: string default: Debug environment: description: Deployment environment to run under type: string workflow_dispatch: inputs: build-type: description: Type of build (Debug or Release) type: string default: Debug jobs: build: name: Build (${{ matrix.artifact-name }}) environment: ${{ inputs.environment || '' }} permissions: # Required for Azure Trusted Signing id-token: write # Required for vcpkg binary cache packages: write strategy: fail-fast: false matrix: include: - os: ubuntu-24.04 artifact-name: Linux cmake-preset: linux qt-version: 6.10.2 - os: ubuntu-24.04-arm artifact-name: Linux-aarch64 cmake-preset: linux qt-version: 6.10.2 - os: windows-2022 artifact-name: Windows-MinGW-w64 cmake-preset: windows_mingw msystem: CLANG64 vcvars-arch: amd64_x86 - os: windows-11-arm artifact-name: Windows-MinGW-arm64 cmake-preset: windows_mingw msystem: CLANGARM64 vcvars-arch: arm64 - os: windows-2022 artifact-name: Windows-MSVC cmake-preset: windows_msvc # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?! vcvars-arch: amd64 qt-version: 6.10.2 - os: windows-11-arm artifact-name: Windows-MSVC-arm64 cmake-preset: windows_msvc vcvars-arch: arm64 qt-version: 6.10.2 - os: macos-26 artifact-name: macOS cmake-preset: macos_universal macosx-deployment-target: 12.0 qt-version: 6.9.3 runs-on: ${{ matrix.os }} defaults: run: shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }} env: ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6 BUILD_PLATFORM: official BUILD_TYPE: ${{ inputs.build-type || 'Debug' }} CMAKE_PRESET: ${{ matrix.cmake-preset }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }} steps: ## # SETUP ## - name: Checkout uses: actions/checkout@v6 with: submodules: true - name: Setup dependencies id: setup-dependencies uses: ./.github/actions/setup-dependencies with: build-type: ${{ env.BUILD_TYPE }} artifact-name: ${{ matrix.artifact-name }} msystem: ${{ matrix.msystem }} vcvars-arch: ${{ matrix.vcvars-arch }} qt-version: ${{ matrix.qt-version }} ## # BUILD ## - name: Configure project run: | cmake --preset "$CMAKE_PRESET" - name: Run build run: | cmake --build --preset "$CMAKE_PRESET" --config "$BUILD_TYPE" - name: Run tests run: | ctest --preset "$CMAKE_PRESET" --build-config "$BUILD_TYPE" --extra-verbose --output-on-failure ## # PACKAGE ## - name: Get short version id: short-version shell: bash run: | echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - name: Package (Linux) if: ${{ runner.os == 'Linux' }} uses: ./.github/actions/package/linux with: version: ${{ steps.short-version.outputs.version }} build-type: ${{ steps.setup-dependencies.outputs.build-type }} artifact-name: ${{ matrix.artifact-name }} qt-version: ${{ steps.setup-dependencies.outputs.qt-version }} gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-private-key-id: ${{ secrets.GPG_PRIVATE_KEY_ID }} - name: Package (macOS) if: ${{ runner.os == 'macOS' }} uses: ./.github/actions/package/macos with: version: ${{ steps.short-version.outputs.version }} build-type: ${{ steps.setup-dependencies.outputs.build-type }} artifact-name: ${{ matrix.artifact-name }} apple-codesign-cert: ${{ secrets.APPLE_CODESIGN_CERT }} apple-codesign-password: ${{ secrets.APPLE_CODESIGN_PASSWORD }} apple-codesign-id: ${{ secrets.APPLE_CODESIGN_ID }} apple-notarize-apple-id: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} apple-notarize-team-id: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} apple-notarize-password: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} sparkle-ed25519-key: ${{ secrets.SPARKLE_ED25519_KEY }} - name: Package (Windows) if: ${{ runner.os == 'Windows' }} uses: ./.github/actions/package/windows env: CI_HAS_ACCESS_TO_AZURE: ${{ vars.CI_HAS_ACCESS_TO_AZURE || '' }} with: version: ${{ steps.short-version.outputs.version }} build-type: ${{ steps.setup-dependencies.outputs.build-type }} artifact-name: ${{ matrix.artifact-name }} msystem: ${{ matrix.msystem }} azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} PrismLauncher-10.0.5/.github/pull_request_template.md0000644000175100017510000000101415144136756022270 0ustar runnerrunner PrismLauncher-10.0.5/.github/codeql/0000755000175100017510000000000015144136756016602 5ustar runnerrunnerPrismLauncher-10.0.5/.github/codeql/codeql-config.yml0000644000175100017510000000007015144136756022034 0ustar runnerrunnerquery-filters: - exclude: id: cpp/fixme-comment PrismLauncher-10.0.5/.github/ISSUE_TEMPLATE/0000755000175100017510000000000015144136756017516 5ustar runnerrunnerPrismLauncher-10.0.5/.github/ISSUE_TEMPLATE/suggestion.yml0000644000175100017510000000213615144136756022432 0ustar runnerrunnername: Suggestion description: Make a suggestion labels: [enhancement] body: - type: markdown attributes: value: | ### Use this form to suggest a feature for Prism Launcher. - type: input attributes: label: Role description: In what way do you use Prism Launcher that needs this feature? placeholder: I play modded Minecraft. validations: required: true - type: input attributes: label: Suggestion description: What do you want Prism Launcher to do? placeholder: I want the cat button to meow. validations: required: true - type: input attributes: label: Benefit description: Why do you need Prism Launcher to do this? placeholder: so that I can always hear a cat when I need to. validations: required: true - type: checkboxes attributes: label: This suggestion is unique options: - label: I have searched the issue tracker and did not find an issue describing my suggestion, especially not one that has been rejected. required: true - type: textarea attributes: label: You may use the editor below to elaborate further. PrismLauncher-10.0.5/.github/ISSUE_TEMPLATE/bug_report.yml0000644000175100017510000000442515144136756022416 0ustar runnerrunnername: Bug Report description: File a bug report labels: [bug] body: - type: markdown attributes: value: | If you need help with running Minecraft, please visit us on our Discord before making a bug report. Before submitting a bug report, please make sure you have read this *entire* form, and that: * You have read the [Prism Launcher wiki](https://prismlauncher.org/wiki/) and it has not answered your question. * Your bug is not caused by Minecraft or any mods you have installed. * Your issue has not been reported before, [make sure to use the search function!](https://github.com/PrismLauncher/PrismLauncher/issues) **Do not forget to give your issue a descriptive title.** "Bug in the instance screen" makes it hard to distinguish issues at a glance. - type: dropdown attributes: label: Operating System description: If you know this bug occurs on multiple operating systems, select all you have tested. multiple: true options: - Windows - macOS - Linux - Other - type: textarea attributes: label: Version of Prism Launcher description: The version of Prism Launcher used in the bug report. placeholder: Prism Launcher 5.0 validations: required: true - type: textarea attributes: label: Version of Qt description: The version of Qt used in the bug report. You can find it in Help -> About Prism Launcher -> About Qt. placeholder: Qt 6.3.0 validations: required: true - type: textarea attributes: label: Description of bug description: What did you expect to happen, what happened, and why is it incorrect? placeholder: The cat button should show a cat, but it showed a dog instead! validations: required: true - type: textarea attributes: label: Steps to reproduce description: A bulleted list, or an exported instance if relevant. placeholder: "* Press the cat button" validations: required: true - type: textarea attributes: label: Suspected cause description: If you know what could be causing this bug, describe it here. validations: required: false - type: checkboxes attributes: label: This issue is unique options: - label: I have searched the issue tracker and did not find an issue describing my bug. required: true PrismLauncher-10.0.5/.github/ISSUE_TEMPLATE/config.yml0000644000175100017510000000031615144136756021506 0ustar runnerrunnerblank_issues_enabled: true contact_links: - name: Prism Launcher Matrix Support Room url: https://matrix.to/#/#prism-support:matrix.org about: Please ask for support here before opening an issue. PrismLauncher-10.0.5/.github/ISSUE_TEMPLATE/rfc.yml0000644000175100017510000000550715144136756021022 0ustar runnerrunner# Template based on https://gitlab.archlinux.org/archlinux/rfcs/-/blob/0ba3b61e987e197f8d1901709409b8564958f78a/rfcs/0000-template.rst name: Request for Comment (RFC) description: Propose a larger change and start a discussion. labels: [rfc] body: - type: markdown attributes: value: | ### Use this form to suggest a larger change for Prism Launcher. - type: textarea attributes: label: Goal description: Short description, 1-2 sentences. placeholder: Remove the cat from the launcher. validations: required: true - type: textarea attributes: label: Motivation description: | Introduce the topic. If this is a not-well-known section of Prism Launcher, a detailed explanation of the background is recommended. Some example points of discussion: - What specific problems are you facing right now that you're trying to address? - Are there any previous discussions? Link to them and summarize them (don't force your readers to read them though!). - Is there any precedent set by other software? If so, link to resources. placeholder: I don't like cats. I think many users also don't like cats. validations: required: true - type: textarea attributes: label: Specification description: A concrete, thorough explanation of what is being planned. placeholder: Remove the cat button and all references to the cat from the codebase. Including resource files. validations: required: true - type: textarea attributes: label: Drawbacks description: Carefully consider every possible objection and issue with your proposal. This section should be updated as feedback comes in from discussion. placeholder: Some users might like cats. validations: required: true - type: textarea attributes: label: Unresolved Questions description: | Are there any portions of your proposal which need to be discussed with the community before the RFC can proceed? Be careful here -- an RFC with a lot of remaining questions is likely to be stalled. If your RFC is mostly unresolved questions and not too much substance, it may not be ready. placeholder: Do a lot of users care about the cat? validations: required: true - type: textarea attributes: label: Alternatives Considered description: A list of alternatives, that have been considered and offer equal or similar features to the proposed change. placeholder: Maybe the cat could be replaced with an axolotl? validations: required: true - type: checkboxes attributes: label: This suggestion is unique options: - label: I have searched the issue tracker and did not find an issue describing my suggestion, especially not one that has been rejected. required: true - type: textarea attributes: label: You may use the editor below to elaborate further. PrismLauncher-10.0.5/.github/FUNDING.yml0000644000175100017510000000003715144136756017150 0ustar runnerrunneropen_collective: prismlauncher PrismLauncher-10.0.5/.github/dco.yml0000644000175100017510000000005415144136756016622 0ustar runnerrunnerallowRemediationCommits: individual: true PrismLauncher-10.0.5/vcpkg-configuration.json0000644000175100017510000000071215144136757020646 0ustar runnerrunner{ "default-registry": { "kind": "git", "baseline": "2d6a6cf3ac9a7cc93942c3d289a2f9c661a6f4a7", "repository": "https://github.com/microsoft/vcpkg" }, "registries": [ { "kind": "artifact", "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", "name": "microsoft" } ], "overlay-ports": [ "./cmake/vcpkg-ports" ], "overlay-triplets": [ "./cmake/vcpkg-triplets" ] } PrismLauncher-10.0.5/.gitattributes0000644000175100017510000000004715144136756016667 0ustar runnerrunner*.pem -crlf **/testdata/** -text -diff PrismLauncher-10.0.5/buildconfig/0000755000175100017510000000000015144136756016260 5ustar runnerrunnerPrismLauncher-10.0.5/buildconfig/CMakeLists.txt0000644000175100017510000000063415144136756021023 0ustar runnerrunner######## Configure the file with build properties ######## configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp") add_library(BuildConfig STATIC BuildConfig.h ${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp ) target_link_libraries(BuildConfig Qt${QT_VERSION_MAJOR}::Core) target_include_directories(BuildConfig PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") PrismLauncher-10.0.5/buildconfig/BuildConfig.h0000644000175100017510000001467215144136756020630 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include /** * \brief The Config class holds all the build-time information passed from the build system. */ class Config { public: Config(); QString LAUNCHER_NAME; QString LAUNCHER_APP_BINARY_NAME; QString LAUNCHER_DISPLAYNAME; QString LAUNCHER_COPYRIGHT; QString LAUNCHER_DOMAIN; QString LAUNCHER_CONFIGFILE; QString LAUNCHER_GIT; QString LAUNCHER_APPID; QString LAUNCHER_SVGFILENAME; /// The major version number. int VERSION_MAJOR; /// The minor version number. int VERSION_MINOR; /// The patch version number. int VERSION_PATCH; /** * The version channel * This is used by the updater to determine what channel the current version came from. */ QString VERSION_CHANNEL; bool UPDATER_ENABLED = false; bool JAVA_DOWNLOADER_ENABLED = false; /// A short string identifying this build's platform or distribution. QString BUILD_PLATFORM; /// A short string identifying this build's valid artifacts int he updater. For example, "lin64" or "win32". QString BUILD_ARTIFACT; /// A string containing the build timestamp QString BUILD_DATE; /// A string identifying the compiler use to build QString COMPILER_NAME; /// A string identifying the compiler version used to build QString COMPILER_VERSION; /// A string identifying the compiler target system os QString COMPILER_TARGET_SYSTEM; /// A String identifying the compiler target system version QString COMPILER_TARGET_SYSTEM_VERSION; /// A String identifying the compiler target processor QString COMPILER_TARGET_SYSTEM_PROCESSOR; /// URL for the updater's channel QString UPDATER_GITHUB_REPO; /// The public key used to sign releases for the Sparkle updater appcast QString MAC_SPARKLE_PUB_KEY; /// URL for the Sparkle updater's appcast QString MAC_SPARKLE_APPCAST_URL; /// User-Agent to use. QString USER_AGENT; /// The git commit hash of this build QString GIT_COMMIT; /// The git tag of this build QString GIT_TAG; /// The git refspec of this build QString GIT_REFSPEC; /** * This is used to fetch the news RSS feed. * It defaults in CMakeLists.txt to "https://multimc.org/rss.xml" */ QString NEWS_RSS_URL; /** * URL that gets opened when the user clicks "More News" */ QString NEWS_OPEN_URL; /** * URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help */ QString HELP_URL; /** * URL that gets opened when the user succesfully logins. */ QString LOGIN_CALLBACK_URL; /** * Client ID you can get from Imgur when you register an application */ QString IMGUR_CLIENT_ID; /** * Client ID you can get from Microsoft Identity Platform when you register an application */ QString MSA_CLIENT_ID; /** * Client API key for CurseForge */ QString FLAME_API_KEY; /** * Metadata repository URL prefix */ QString META_URL; QString GLFW_LIBRARY_NAME; QString OPENAL_LIBRARY_NAME; QString BUG_TRACKER_URL; QString TRANSLATIONS_URL; QString MATRIX_URL; QString DISCORD_URL; QString SUBREDDIT_URL; QString DEFAULT_RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; QString FMLLIBS_BASE_URL; QString TRANSLATION_FILES_URL; QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; QString ATL_API_BASE_URL = "https://api.atlauncher.com/v1/"; QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; /** * The build that is reported to the Technic API. */ QString TECHNIC_API_BUILD = "multimc"; QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; QStringList MODRINTH_MRPACK_HOSTS{ "cdn.modrinth.com", "github.com", "raw.githubusercontent.com", "gitlab.com" }; QString FLAME_BASE_URL = "https://api.curseforge.com/v1"; QString versionString() const; /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build). */ QString printableVersionString() const; /** * \brief Compiler ID String * \return a string of the form "Name - Version" of just "Name" if the version is empty */ QString compilerID() const; /** * \brief System ID String * \return a string of the form "OS Verison Processor" */ QString systemID() const; }; extern const Config BuildConfig; PrismLauncher-10.0.5/buildconfig/BuildConfig.cpp.in0000644000175100017510000001301615144136756021557 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include "BuildConfig.h" const Config BuildConfig; Config::Config() { // Name and copyright LAUNCHER_NAME = "@Launcher_Name@"; LAUNCHER_APP_BINARY_NAME = "@Launcher_APP_BINARY_NAME@"; LAUNCHER_DISPLAYNAME = "@Launcher_DisplayName@"; LAUNCHER_COPYRIGHT = "@Launcher_Copyright@"; LAUNCHER_DOMAIN = "@Launcher_Domain@"; LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@"; LAUNCHER_GIT = "@Launcher_Git@"; LAUNCHER_APPID = "@Launcher_AppID@"; LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; USER_AGENT = "@Launcher_UserAgent@"; // Version information VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@; VERSION_PATCH = @Launcher_VERSION_PATCH@; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@"; UPDATER_GITHUB_REPO = "@Launcher_UPDATER_GITHUB_REPO@"; COMPILER_NAME = "@Launcher_COMPILER_NAME@"; COMPILER_VERSION = "@Launcher_COMPILER_VERSION@"; COMPILER_TARGET_SYSTEM = "@Launcher_COMPILER_TARGET_SYSTEM@"; COMPILER_TARGET_SYSTEM_VERSION = "@Launcher_COMPILER_TARGET_SYSTEM_VERSION@"; COMPILER_TARGET_SYSTEM_PROCESSOR = "@Launcher_COMPILER_TARGET_PROCESSOR@"; MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { UPDATER_ENABLED = true; } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { UPDATER_ENABLED = true; } #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_TAG = "@Launcher_GIT_TAG@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; // Assume that builds outside of Git repos are "stable" if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") || GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { GIT_REFSPEC = "refs/heads/stable"; GIT_TAG = versionString(); GIT_COMMIT = ""; } if (GIT_REFSPEC.startsWith("refs/heads/")) { VERSION_CHANNEL = GIT_REFSPEC; VERSION_CHANNEL.remove("refs/heads/"); } else if (!GIT_COMMIT.isEmpty()) { VERSION_CHANNEL = GIT_COMMIT.mid(0, 8); } else { VERSION_CHANNEL = "unknown"; } NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; HELP_URL = "@Launcher_HELP_URL@"; LOGIN_CALLBACK_URL = "@Launcher_LOGIN_CALLBACK_URL@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; META_URL = "@Launcher_META_URL@"; FMLLIBS_BASE_URL = "@Launcher_FMLLIBS_BASE_URL@"; GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; TRANSLATION_FILES_URL = "@Launcher_TRANSLATION_FILES_URL@"; MATRIX_URL = "@Launcher_MATRIX_URL@"; DISCORD_URL = "@Launcher_DISCORD_URL@"; SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; } QString Config::versionString() const { return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH); } QString Config::printableVersionString() const { QString vstr = versionString(); // If the build is not a main release, append the channel if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) { vstr += "-" + VERSION_CHANNEL; } return vstr; } QString Config::compilerID() const { if (COMPILER_VERSION.isEmpty()) return COMPILER_NAME; return QStringLiteral("%1 - %2").arg(COMPILER_NAME).arg(COMPILER_VERSION); } QString Config::systemID() const { return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); } PrismLauncher-10.0.5/flake.nix0000644000175100017510000001410115144136756015572 0ustar runnerrunner{ description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; nixConfig = { extra-substituters = [ "https://prismlauncher.cachix.org" ]; extra-trusted-public-keys = [ "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; inputs = { nixpkgs.url = "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz"; libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; }; }; outputs = { self, nixpkgs, libnbtplusplus, }: let inherit (nixpkgs) lib; # While we only officially support aarch and x86_64 on Linux and MacOS, # we expose a reasonable amount of other systems for users who want to # build for most exotic platforms systems = lib.systems.flakeExposed; forAllSystems = lib.genAttrs systems; nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); in { checks = forAllSystems ( system: let pkgs = nixpkgsFor.${system}; llvm = pkgs.llvmPackages_19; in { formatting = pkgs.runCommand "check-formatting" { nativeBuildInputs = with pkgs; [ deadnix llvm.clang-tools markdownlint-cli nixfmt-rfc-style statix ]; } '' cd ${self} echo "Running clang-format...." clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} echo "Running deadnix..." deadnix --fail echo "Running markdownlint..." markdownlint --dot . echo "Running nixfmt..." find -type f -name '*.nix' -exec nixfmt --check {} + echo "Running statix" statix check . touch $out ''; } ); devShells = forAllSystems ( system: let pkgs = nixpkgsFor.${system}; llvm = pkgs.llvmPackages_19; packages' = self.packages.${system}; welcomeMessage = '' Welcome to the Prism Launcher repository! 🌈 We just set some things up for you. To get building, you can run: ``` $ cd "$cmakeBuildDir" $ ninjaBuildPhase $ ninjaInstallPhase ``` Feel free to ask any questions in our Discord server or Matrix space: - https://prismlauncher.org/discord - https://matrix.to/#/#prismlauncher:matrix.org And thanks for helping out :) ''; # Re-use our package wrapper to wrap our development environment qt-wrapper-env = packages'.prismlauncher.overrideAttrs (old: { name = "qt-wrapper-env"; # Required to use script-based makeWrapper below strictDeps = true; # We don't need/want the unwrapped Prism package paths = [ ]; nativeBuildInputs = old.nativeBuildInputs or [ ] ++ [ # Ensure the wrapper is script based so it can be sourced pkgs.makeWrapper ]; # Inspired by https://discourse.nixos.org/t/python-qt-woes/11808/10 buildCommand = '' makeQtWrapper ${lib.getExe pkgs.runtimeShellPackage} "$out" sed -i '/^exec/d' "$out" ''; }); in { default = pkgs.mkShell { name = "prism-launcher"; inputsFrom = [ packages'.prismlauncher-unwrapped ]; packages = with pkgs; [ ccache llvm.clang-tools ]; cmakeBuildType = "Debug"; cmakeFlags = [ "-GNinja" ] ++ packages'.prismlauncher.cmakeFlags; dontFixCmake = true; shellHook = '' echo "Sourcing ${qt-wrapper-env}" source ${qt-wrapper-env} git submodule update --init --force if [ ! -f compile_commands.json ]; then cmakeConfigurePhase cd .. ln -s "$cmakeBuildDir"/compile_commands.json compile_commands.json fi echo ${lib.escapeShellArg welcomeMessage} ''; }; } ); formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); overlays.default = final: prev: { prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { inherit libnbtplusplus self ; }; prismlauncher = final.callPackage ./nix/wrapper.nix { }; }; packages = forAllSystems ( system: let pkgs = nixpkgsFor.${system}; # Build a scope from our overlay prismPackages = lib.makeScope pkgs.newScope (final: self.overlays.default final pkgs); # Grab our packages from it and set the default packages = { inherit (prismPackages) prismlauncher-unwrapped prismlauncher; default = prismPackages.prismlauncher; }; in # Only output them if they're available on the current system lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages ); # We put these under legacyPackages as they are meant for CI, not end user consumption legacyPackages = forAllSystems ( system: let packages' = self.packages.${system}; legacyPackages' = self.legacyPackages.${system}; in { prismlauncher-debug = packages'.prismlauncher.override { prismlauncher-unwrapped = legacyPackages'.prismlauncher-unwrapped-debug; }; prismlauncher-unwrapped-debug = packages'.prismlauncher-unwrapped.overrideAttrs { cmakeBuildType = "Debug"; dontStrip = true; }; } ); }; } PrismLauncher-10.0.5/.gitmodules0000644000175100017510000000020315144136756016143 0ustar runnerrunner[submodule "libraries/libnbtplusplus"] path = libraries/libnbtplusplus url = https://github.com/PrismLauncher/libnbtplusplus.git PrismLauncher-10.0.5/flake.lock0000644000175100017510000000213115144136756015724 0ustar runnerrunner{ "nodes": { "libnbtplusplus": { "flake": false, "locked": { "lastModified": 1744811532, "narHash": "sha256-qhmjaRkt+O7A+gu6HjUkl7QzOEb4r8y8vWZMG2R/C6o=", "owner": "PrismLauncher", "repo": "libnbtplusplus", "rev": "531449ba1c930c98e0bcf5d332b237a8566f9d78", "type": "github" }, "original": { "owner": "PrismLauncher", "repo": "libnbtplusplus", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1766473571, "narHash": "sha256-QvjEJNgMVuOootbR+DEfbiW+zSK57U32CE0jmVdcNjQ=", "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", "type": "tarball", "url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.2403.76701a179d3a/nixexprs.tar.xz" }, "original": { "type": "tarball", "url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz" } }, "root": { "inputs": { "libnbtplusplus": "libnbtplusplus", "nixpkgs": "nixpkgs" } } }, "root": "root", "version": 7 } PrismLauncher-10.0.5/CODE_OF_CONDUCT.md0000644000175100017510000001310415144136756016571 0ustar runnerrunner# Contributor Covenant Code of Conduct This is a modified version of the Contributor Covenant. See commit history to see our changes. ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling (antagonistic, inflammatory, insincere behaviour), insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement via email at [coc@scrumplex.net](mailto:coc@scrumplex.net) (Email address subject to change). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations PrismLauncher-10.0.5/.markdownlint.yaml0000644000175100017510000000050315144136756017444 0ustar runnerrunner# MD013/line-length - Line length MD013: false # MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content MD024: siblings-only: true # MD033/no-inline-html Inline HTML MD033: false # MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading MD041: false PrismLauncher-10.0.5/.gitignore0000644000175100017510000000122615144136756015764 0ustar runnerrunnerThumbs.db *.kdev4 .user .directory resources/CMakeFiles *~ *.swp html/ # Project Files *.pro.user CMakeLists.txt.user CMakeLists.txt.user.* CMakeSettings.json /CMakeFiles CMakeCache.txt CMakeUserPresets.json /.project /.settings /.idea /.vscode /.vs cmake-build-*/ Debug compile_commands.json # Build dirs build /build-* # Install dirs install /install-* # Ctags File tags # YouCompleteMe config stuff. .ycm_extra_conf.* #OSX Stuff .DS_Store branding/ secrets/ run/ .cache/ # Nix/NixOS .direnv/ ## Used when manually invoking stdenv phases outputs/ ## Regular artifacts result result-* repl-result-* # Flatpak .flatpak-builder flatbuild # Snap *.snap PrismLauncher-10.0.5/.envrc0000644000175100017510000000003515144136756015107 0ustar runnerrunneruse nix watch_file nix/*.nix PrismLauncher-10.0.5/launcher/0000755000175100017510000000000015144136757015575 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/Usable.h0000644000175100017510000000201715144136756017160 0ustar runnerrunner#pragma once #include #include #include "QObjectPtr.h" class Usable; /** * Base class for things that can be used by multiple other things and we want to track the use count. * * @see UseLock */ class Usable { friend class UseLock; public: virtual ~Usable() {} std::size_t useCount() const { return m_useCount; } bool isInUse() const { return m_useCount > 0; } protected: virtual void decrementUses() { m_useCount--; } virtual void incrementUses() { m_useCount++; } private: std::size_t m_useCount = 0; }; /** * Lock class to use for keeping track of uses of other things derived from Usable * * @see Usable */ class UseLock { public: UseLock(shared_qobject_ptr usable) : m_usable(usable) { // this doesn't use shared pointer use count, because that wouldn't be correct. this count is separate. m_usable->incrementUses(); } ~UseLock() { m_usable->decrementUses(); } private: shared_qobject_ptr m_usable; }; PrismLauncher-10.0.5/launcher/java/0000755000175100017510000000000015144136756016515 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/java/JavaUtils.h0000644000175100017510000000260415144136756020572 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "java/JavaInstall.h" #ifdef Q_OS_WIN #include #endif QString stripVariableEntries(QString name, QString target, QString remove); QProcessEnvironment CleanEnviroment(); QStringList getMinecraftJavaBundle(); QStringList getPrismJavaBundle(); class JavaUtils : public QObject { Q_OBJECT public: JavaUtils(); JavaInstallPtr MakeJavaPtr(QString path, QString id = "unknown", QString arch = "unknown"); QList FindJavaPaths(); JavaInstallPtr GetDefaultJava(); #ifdef Q_OS_WIN QList FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix = ""); #endif static QString getJavaCheckPath(); static const QString javaExecutable; }; PrismLauncher-10.0.5/launcher/java/download/0000755000175100017510000000000015144136756020324 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/java/download/ManifestDownloadTask.h0000644000175100017510000000256615144136756024567 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "tasks/Task.h" namespace Java { class ManifestDownloadTask : public Task { Q_OBJECT public: ManifestDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ManifestDownloadTask() = default; bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; private slots: void downloadJava(const QJsonDocument& doc); protected: QUrl m_url; QString m_final_path; QString m_checksum_type; QString m_checksum_hash; Task::Ptr m_task; }; } // namespace Java PrismLauncher-10.0.5/launcher/java/download/ArchiveDownloadTask.cpp0000644000175100017510000001103715144136756024726 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "java/download/ArchiveDownloadTask.h" #include #include "Application.h" #include "archive/ArchiveReader.h" #include "archive/ExtractZipTask.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" #include "tasks/Task.h" namespace Java { ArchiveDownloadTask::ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType, QString checksumHash) : m_url(url), m_final_path(final_path), m_checksum_type(checksumType), m_checksum_hash(checksumHash) {} void ArchiveDownloadTask::executeTask() { // JRE found ! download the zip setStatus(tr("Downloading Java")); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("java", m_url.fileName()); auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); auto action = Net::Download::makeCached(m_url, entry); if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { auto hashType = QCryptographicHash::Algorithm::Sha1; if (m_checksum_type == "sha256") { hashType = QCryptographicHash::Algorithm::Sha256; } action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); } download->addNetAction(action); auto fullPath = entry->getFullPath(); connect(download.get(), &Task::failed, this, &ArchiveDownloadTask::emitFailed); connect(download.get(), &Task::progress, this, &ArchiveDownloadTask::setProgress); connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus); connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails); connect(download.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); connect(download.get(), &Task::succeeded, [this, fullPath] { // This should do all of the extracting and creating folders extractJava(fullPath); }); m_task = download; m_task->start(); } void ArchiveDownloadTask::extractJava(QString input) { setStatus(tr("Extracting Java")); MMCZip::ArchiveReader zip(input); if (!zip.collectFiles()) { emitFailed(tr("Unable to open supplied zip file.")); return; } auto files = zip.getFiles(); if (files.isEmpty()) { emitFailed(tr("No files were found in the supplied zip file.")); return; } auto firstFolderParts = files[0].split('/', Qt::SkipEmptyParts); m_task = makeShared(input, m_final_path, firstFolderParts.value(0)); auto progressStep = std::make_shared(); connect(m_task.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); m_task->start(); return; } bool ArchiveDownloadTask::abort() { auto aborted = canAbort(); if (m_task) aborted = m_task->abort(); return aborted; }; } // namespace Java PrismLauncher-10.0.5/launcher/java/download/ManifestDownloadTask.cpp0000644000175100017510000001301215144136756025106 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "java/download/ManifestDownloadTask.h" #include "Application.h" #include "FileSystem.h" #include "Json.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" struct File { QString path; QString url; QByteArray hash; bool isExec; }; namespace Java { ManifestDownloadTask::ManifestDownloadTask(QUrl url, QString final_path, QString checksumType, QString checksumHash) : m_url(url), m_final_path(final_path), m_checksum_type(checksumType), m_checksum_hash(checksumHash) {} void ManifestDownloadTask::executeTask() { setStatus(tr("Downloading Java")); auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); auto files = std::make_shared(); auto action = Net::Download::makeByteArray(m_url, files); if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { auto hashType = QCryptographicHash::Algorithm::Sha1; if (m_checksum_type == "sha256") { hashType = QCryptographicHash::Algorithm::Sha256; } action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); } download->addNetAction(action); connect(download.get(), &Task::failed, this, &ManifestDownloadTask::emitFailed); connect(download.get(), &Task::progress, this, &ManifestDownloadTask::setProgress); connect(download.get(), &Task::stepProgress, this, &ManifestDownloadTask::propagateStepProgress); connect(download.get(), &Task::status, this, &ManifestDownloadTask::setStatus); connect(download.get(), &Task::details, this, &ManifestDownloadTask::setDetails); connect(download.get(), &Task::succeeded, [files, this] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*files, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *files; emitFailed(parse_error.errorString()); return; } downloadJava(doc); }); m_task = download; m_task->start(); } void ManifestDownloadTask::downloadJava(const QJsonDocument& doc) { // valid json doc, begin making jre spot FS::ensureFolderPathExists(m_final_path); std::vector toDownload; auto list = doc.object()["files"].toObject(); for (const auto& paths : list.keys()) { auto file = FS::PathCombine(m_final_path, paths); const QJsonObject& meta = list[paths].toObject(); auto type = meta["type"].toString(); if (type == "directory") { FS::ensureFolderPathExists(file); } else if (type == "link") { // this is *nix only ! auto path = meta["target"].toString(); if (!path.isEmpty()) { QFile::link(path, file); } } else if (type == "file") { // TODO download compressed version if it exists ? auto raw = meta["downloads"].toObject()["raw"].toObject(); auto isExec = meta["executable"].toBool(); auto url = raw["url"].toString(); if (!url.isEmpty() && QUrl(url).isValid()) { auto f = File{ file, url, QByteArray::fromHex(raw["sha1"].toString().toLatin1()), isExec }; toDownload.push_back(f); } } } auto elementDownload = makeShared("JRE::FileDownload", APPLICATION->network()); for (const auto& file : toDownload) { auto dl = Net::Download::makeFile(file.url, file.path); if (!file.hash.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, file.hash)); } if (file.isExec) { connect(dl.get(), &Net::Download::succeeded, [file] { QFile(file.path).setPermissions(QFile(file.path).permissions() | QFileDevice::Permissions(0x1111)); }); } elementDownload->addNetAction(dl); } connect(elementDownload.get(), &Task::failed, this, &ManifestDownloadTask::emitFailed); connect(elementDownload.get(), &Task::progress, this, &ManifestDownloadTask::setProgress); connect(elementDownload.get(), &Task::stepProgress, this, &ManifestDownloadTask::propagateStepProgress); connect(elementDownload.get(), &Task::status, this, &ManifestDownloadTask::setStatus); connect(elementDownload.get(), &Task::details, this, &ManifestDownloadTask::setDetails); connect(elementDownload.get(), &Task::succeeded, this, &ManifestDownloadTask::emitSucceeded); m_task = elementDownload; m_task->start(); } bool ManifestDownloadTask::abort() { auto aborted = canAbort(); if (m_task) aborted = m_task->abort(); emitAborted(); return aborted; }; } // namespace Java PrismLauncher-10.0.5/launcher/java/download/SymlinkTask.h0000644000175100017510000000203715144136756022750 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "tasks/Task.h" namespace Java { class SymlinkTask : public Task { Q_OBJECT public: SymlinkTask(QString final_path); virtual ~SymlinkTask() = default; void executeTask() override; protected: QString m_path; Task::Ptr m_task; }; } // namespace Java PrismLauncher-10.0.5/launcher/java/download/ArchiveDownloadTask.h0000644000175100017510000000254615144136756024400 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "tasks/Task.h" namespace Java { class ArchiveDownloadTask : public Task { Q_OBJECT public: ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ArchiveDownloadTask() = default; bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; private slots: void extractJava(QString input); protected: QUrl m_url; QString m_final_path; QString m_checksum_type; QString m_checksum_hash; Task::Ptr m_task; }; } // namespace Java PrismLauncher-10.0.5/launcher/java/download/SymlinkTask.cpp0000644000175100017510000000536615144136756023313 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "java/download/SymlinkTask.h" #include #include "FileSystem.h" namespace Java { SymlinkTask::SymlinkTask(QString final_path) : m_path(final_path) {} QString findBinPath(QString root, QString pattern) { auto path = FS::PathCombine(root, pattern); if (QFileInfo::exists(path)) { return path; } auto entries = QDir(root).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { path = FS::PathCombine(entry.absoluteFilePath(), pattern); if (QFileInfo::exists(path)) { return path; } } return {}; } void SymlinkTask::executeTask() { setStatus(tr("Checking for Java binary path")); const auto binPath = FS::PathCombine("bin", "java"); const auto wantedPath = FS::PathCombine(m_path, binPath); if (QFileInfo::exists(wantedPath)) { emitSucceeded(); return; } setStatus(tr("Searching for Java binary path")); const auto contentsPartialPath = FS::PathCombine("Contents", "Home", binPath); const auto relativePathToBin = findBinPath(m_path, contentsPartialPath); if (relativePathToBin.isEmpty()) { emitFailed(tr("Failed to find Java binary path")); return; } const auto folderToLink = relativePathToBin.chopped(binPath.length()); setStatus(tr("Collecting folders to symlink")); auto entries = QDir(folderToLink).entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries); QList files; setProgress(0, entries.length()); for (auto& entry : entries) { files.append({ entry.absoluteFilePath(), FS::PathCombine(m_path, entry.fileName()) }); } setStatus(tr("Symlinking Java binary path")); FS::create_link folderLink(files); connect(&folderLink, &FS::create_link::fileLinked, [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); if (!folderLink()) { emitFailed(folderLink.getOSError().message().c_str()); } else { emitSucceeded(); } } } // namespace Java PrismLauncher-10.0.5/launcher/java/JavaInstall.h0000644000175100017510000000312515144136756021077 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "BaseVersion.h" #include "JavaVersion.h" struct JavaInstall : public BaseVersion { JavaInstall() {} JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} virtual QString descriptor() const override { return id.toString(); } virtual QString name() const override { return id.toString(); } virtual QString typeString() const override { return arch; } virtual bool operator<(BaseVersion& a) const override; virtual bool operator>(BaseVersion& a) const override; bool operator<(const JavaInstall& rhs) const; bool operator==(const JavaInstall& rhs) const; bool operator>(const JavaInstall& rhs) const; JavaVersion id; QString arch; QString path; bool recommended = false; bool is_64bit = false; }; using JavaInstallPtr = std::shared_ptr; PrismLauncher-10.0.5/launcher/java/JavaMetadata.cpp0000644000175100017510000000716515144136756021554 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "java/JavaMetadata.h" #include #include "Json.h" #include "StringUtils.h" #include "java/JavaVersion.h" #include "minecraft/ParseUtils.h" namespace Java { DownloadType parseDownloadType(QString javaDownload) { if (javaDownload == "manifest") return DownloadType::Manifest; else if (javaDownload == "archive") return DownloadType::Archive; else return DownloadType::Unknown; } QString downloadTypeToString(DownloadType javaDownload) { switch (javaDownload) { case DownloadType::Manifest: return "manifest"; case DownloadType::Archive: return "archive"; case DownloadType::Unknown: break; } return "unknown"; } MetadataPtr parseJavaMeta(const QJsonObject& in) { auto meta = std::make_shared(); meta->m_name = in["name"].toString(""); meta->vendor = in["vendor"].toString(""); meta->url = in["url"].toString(""); meta->releaseTime = timeFromS3Time(in["releaseTime"].toString("")); meta->downloadType = parseDownloadType(in["downloadType"].toString("")); meta->packageType = in["packageType"].toString(""); meta->runtimeOS = in["runtimeOS"].toString("unknown"); if (in.contains("checksum")) { auto obj = Json::requireObject(in, "checksum"); meta->checksumHash = obj["hash"].toString(""); meta->checksumType = obj["type"].toString(""); } if (in.contains("version")) { auto obj = Json::requireObject(in, "version"); auto name = obj["name"].toString(""); auto major = obj["major"].toInteger(); auto minor = obj["minor"].toInteger(); auto security = obj["security"].toInteger(); auto build = obj["build"].toInteger(); meta->version = JavaVersion(major, minor, security, build, name); } return meta; } bool Metadata::operator<(const Metadata& rhs) const { auto id = version; if (id < rhs.version) { return true; } if (id > rhs.version) { return false; } auto date = releaseTime; if (date < rhs.releaseTime) { return true; } if (date > rhs.releaseTime) { return false; } return StringUtils::naturalCompare(m_name, rhs.m_name, Qt::CaseInsensitive) < 0; } bool Metadata::operator==(const Metadata& rhs) const { return version == rhs.version && m_name == rhs.m_name; } bool Metadata::operator>(const Metadata& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } bool Metadata::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); } catch (const std::bad_cast& e) { return BaseVersion::operator<(a); } } bool Metadata::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); } catch (const std::bad_cast& e) { return BaseVersion::operator>(a); } } } // namespace Java PrismLauncher-10.0.5/launcher/java/JavaVersion.cpp0000644000175100017510000001017015144136756021447 0ustar runnerrunner#include "JavaVersion.h" #include "StringUtils.h" #include #include JavaVersion& JavaVersion::operator=(const QString& javaVersionString) { m_string = javaVersionString; auto getCapturedInteger = [](const QRegularExpressionMatch& match, const QString& what) -> int { auto str = match.captured(what); if (str.isEmpty()) { return 0; } return str.toInt(); }; QRegularExpression pattern; if (javaVersionString.startsWith("1.")) { static const QRegularExpression s_withOne( "1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); pattern = s_withOne; } else { static const QRegularExpression s_withoutOne( "(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); pattern = s_withoutOne; } auto match = pattern.match(m_string); m_parseable = match.hasMatch(); m_major = getCapturedInteger(match, "major"); m_minor = getCapturedInteger(match, "minor"); m_security = getCapturedInteger(match, "security"); m_prerelease = match.captured("prerelease"); return *this; } JavaVersion::JavaVersion(const QString& rhs) { operator=(rhs); } QString JavaVersion::toString() const { return m_string; } bool JavaVersion::requiresPermGen() const { return !m_parseable || m_major < 8; } bool JavaVersion::defaultsToUtf8() const { // starting from Java 18, UTF-8 is the default charset: https://openjdk.org/jeps/400 return m_parseable && m_major >= 18; } bool JavaVersion::isModular() const { return m_parseable && m_major >= 9; } bool JavaVersion::operator<(const JavaVersion& rhs) const { if (m_parseable && rhs.m_parseable) { auto major = m_major; auto rmajor = rhs.m_major; if (major < rmajor) return true; if (major > rmajor) return false; if (m_minor < rhs.m_minor) return true; if (m_minor > rhs.m_minor) return false; if (m_security < rhs.m_security) return true; if (m_security > rhs.m_security) return false; // everything else being equal, consider prerelease status bool thisPre = !m_prerelease.isEmpty(); bool rhsPre = !rhs.m_prerelease.isEmpty(); if (thisPre && !rhsPre) { // this is a prerelease and the other one isn't -> lesser return true; } else if (!thisPre && rhsPre) { // this isn't a prerelease and the other one is -> greater return false; } else if (thisPre && rhsPre) { // both are prereleases - use natural compare... return StringUtils::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0; } // neither is prerelease, so they are the same -> this cannot be less than rhs return false; } else return StringUtils::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; } bool JavaVersion::operator==(const JavaVersion& rhs) const { if (m_parseable && rhs.m_parseable) { return m_major == rhs.m_major && m_minor == rhs.m_minor && m_security == rhs.m_security && m_prerelease == rhs.m_prerelease; } return m_string == rhs.m_string; } bool JavaVersion::operator>(const JavaVersion& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } JavaVersion::JavaVersion(int major, int minor, int security, int build, QString name) : m_major(major), m_minor(minor), m_security(security), m_name(name), m_parseable(true) { QStringList versions; if (build != 0) { m_prerelease = QString::number(build); versions.push_front(m_prerelease); } if (m_security != 0) versions.push_front(QString::number(m_security)); else if (!versions.isEmpty()) versions.push_front("0"); if (m_minor != 0) versions.push_front(QString::number(m_minor)); else if (!versions.isEmpty()) versions.push_front("0"); versions.push_front(QString::number(m_major)); m_string = versions.join("."); } PrismLauncher-10.0.5/launcher/java/JavaChecker.h0000644000175100017510000000242615144136756021040 0ustar runnerrunner#pragma once #include #include #include "JavaVersion.h" #include "QObjectPtr.h" #include "tasks/Task.h" class JavaChecker : public Task { Q_OBJECT public: using QProcessPtr = shared_qobject_ptr; using Ptr = shared_qobject_ptr; struct Result { QString path; int id; QString mojangPlatform; QString realPlatform; JavaVersion javaVersion; QString javaVendor; QString outLog; QString errorLog; bool is_64bit = false; enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored; }; explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0); signals: void checkFinished(const Result& result); protected: virtual void executeTask() override; private: QProcessPtr process; QTimer killTimer; QString m_stdout; QString m_stderr; QString m_path; QString m_args; int m_minMem = 0; int m_maxMem = 0; int m_permGen = 64; int m_id = 0; private slots: void timeout(); void finished(int exitcode, QProcess::ExitStatus); void error(QProcess::ProcessError); void stdoutReady(); void stderrReady(); }; PrismLauncher-10.0.5/launcher/java/JavaUtils.cpp0000644000175100017510000005770215144136756021136 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include "Application.h" #include "FileSystem.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #define IBUS "@im=ibus" JavaUtils::JavaUtils() {} QString stripVariableEntries(QString name, QString target, QString remove) { char delimiter = ':'; #ifdef Q_OS_WIN32 delimiter = ';'; #endif auto targetItems = target.split(delimiter); auto toRemove = remove.split(delimiter); for (QString item : toRemove) { bool removed = targetItems.removeOne(item); if (!removed) qWarning() << "Entry" << item << "could not be stripped from variable" << name; } return targetItems.join(delimiter); } QProcessEnvironment CleanEnviroment() { // prepare the process environment QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment(); QProcessEnvironment env; QStringList ignored = { "JAVA_ARGS", "CLASSPATH", "CONFIGPATH", "JAVA_HOME", "JRE_HOME", "_JAVA_OPTIONS", "JAVA_OPTIONS", "JAVA_TOOL_OPTIONS" }; QStringList stripped = { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) "LD_LIBRARY_PATH", "LD_PRELOAD", #endif "QT_PLUGIN_PATH", "QT_FONTPATH" }; for (auto key : rawenv.keys()) { auto value = rawenv.value(key); // filter out dangerous java crap if (ignored.contains(key)) { qDebug() << "Env: ignoring" << key << value; continue; } // These are used to strip the original variables // If there is "LD_LIBRARY_PATH" and "LAUNCHER_LD_LIBRARY_PATH", we want to // remove all values in "LAUNCHER_LD_LIBRARY_PATH" from "LD_LIBRARY_PATH" if (key.startsWith("LAUNCHER_")) { qDebug() << "Env: ignoring" << key << value; continue; } if (stripped.contains(key)) { QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key)); qDebug() << "Env: stripped" << key << value << "to" << newValue; value = newValue; } #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) // Strip IBus // IBus is a Linux IME framework. For some reason, it breaks MC? if (key == "XMODIFIERS" && value.contains(IBUS)) { QString save = value; value.replace(IBUS, ""); qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value; } #endif // qDebug() << "Env: " << key << value; env.insert(key, value); } #ifdef Q_OS_LINUX // HACK: Workaround for QTBUG-42500 if (!env.contains("LD_LIBRARY_PATH")) { env.insert("LD_LIBRARY_PATH", ""); } #endif return env; } JavaInstallPtr JavaUtils::MakeJavaPtr(QString path, QString id, QString arch) { JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = id; javaVersion->arch = arch; javaVersion->path = path; return javaVersion; } JavaInstallPtr JavaUtils::GetDefaultJava() { JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = "java"; javaVersion->arch = "unknown"; #if defined(Q_OS_WIN32) javaVersion->path = "javaw"; #else javaVersion->path = "java"; #endif return javaVersion; } QStringList addJavasFromEnv(QList javas) { auto env = qEnvironmentVariable("PRISMLAUNCHER_JAVA_PATHS"); // FIXME: use launcher name from buildconfig #if defined(Q_OS_WIN32) QList javaPaths = env.replace("\\", "/").split(QLatin1String(";")); auto envPath = qEnvironmentVariable("PATH"); QList javaPathsfromPath = envPath.replace("\\", "/").split(QLatin1String(";")); for (QString string : javaPathsfromPath) { javaPaths.append(string + "/javaw.exe"); } #else QList javaPaths = env.split(QLatin1String(":")); #endif for (QString i : javaPaths) { javas.append(i); }; return javas; } #if defined(Q_OS_WIN32) QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix) { QList javas; QString archType = "unknown"; if (keyType == KEY_WOW64_64KEY) archType = "64"; else if (keyType == KEY_WOW64_32KEY) archType = "32"; for (HKEY baseRegistry : { HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE }) { HKEY jreKey; if (RegOpenKeyExW(baseRegistry, keyName.toStdWString().c_str(), 0, KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) == ERROR_SUCCESS) { // Read the current type version from the registry. // This will be used to find any key that contains the JavaHome value. WCHAR subKeyName[255]; DWORD subKeyNameSize, numSubKeys, retCode; // Get the number of subkeys RegQueryInfoKeyW(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL); // Iterate until RegEnumKeyEx fails if (numSubKeys > 0) { for (DWORD i = 0; i < numSubKeys; i++) { subKeyNameSize = 255; retCode = RegEnumKeyExW(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, NULL); QString newSubkeyName = QString::fromWCharArray(subKeyName); if (retCode == ERROR_SUCCESS) { // Now open the registry key for the version that we just got. QString newKeyName = keyName + "\\" + newSubkeyName + subkeySuffix; HKEY newKey; if (RegOpenKeyExW(baseRegistry, newKeyName.toStdWString().c_str(), 0, KEY_READ | keyType, &newKey) == ERROR_SUCCESS) { // Read the JavaHome value to find where Java is installed. DWORD valueSz = 0; if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL, &valueSz) == ERROR_SUCCESS) { WCHAR* value = new WCHAR[valueSz]; RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE*)value, &valueSz); QString newValue = QString::fromWCharArray(value); delete[] value; // Now, we construct the version object and add it to the list. JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = newSubkeyName; javaVersion->arch = archType; javaVersion->path = QDir(FS::PathCombine(newValue, "bin")).absoluteFilePath("javaw.exe"); javas.append(javaVersion); } RegCloseKey(newKey); } } } } RegCloseKey(jreKey); } } return javas; } QList JavaUtils::FindJavaPaths() { QList java_candidates; // Oracle QList JRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList JDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); QList JRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); QList JDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); // Oracle for Java 9 and newer QList NEWJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); QList NEWJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); QList NEWJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); QList NEWJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); // AdoptOpenJDK QList ADOPTOPENJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); QList ADOPTOPENJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); QList ADOPTOPENJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); QList ADOPTOPENJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); // Eclipse Foundation QList FOUNDATIONJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); QList FOUNDATIONJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); // Eclipse Adoptium QList ADOPTIUMJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI"); QList ADOPTIUMJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI"); QList ADOPTIUMJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); QList ADOPTIUMJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); // IBM Semeru QList SEMERUJRE32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI"); QList SEMERUJRE64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JRE", "Path", "\\openj9\\MSI"); QList SEMERUJDK32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI"); QList SEMERUJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Semeru\\JDK", "Path", "\\openj9\\MSI"); // Microsoft QList MICROSOFTJDK64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); // Azul Zulu QList ZULU64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); QList ZULU32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); // BellSoft Liberica QList LIBERICA64s = this->FindJavaFromRegistryKey(KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); QList LIBERICA32s = this->FindJavaFromRegistryKey(KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); // List x64 before x86 java_candidates.append(JRE64s); java_candidates.append(NEWJRE64s); java_candidates.append(ADOPTOPENJRE64s); java_candidates.append(ADOPTIUMJRE64s); java_candidates.append(SEMERUJRE64s); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK64s); java_candidates.append(NEWJDK64s); java_candidates.append(ADOPTOPENJDK64s); java_candidates.append(FOUNDATIONJDK64s); java_candidates.append(ADOPTIUMJDK64s); java_candidates.append(SEMERUJDK64s); java_candidates.append(MICROSOFTJDK64s); java_candidates.append(ZULU64s); java_candidates.append(LIBERICA64s); java_candidates.append(JRE32s); java_candidates.append(NEWJRE32s); java_candidates.append(ADOPTOPENJRE32s); java_candidates.append(ADOPTIUMJRE32s); java_candidates.append(SEMERUJRE32s); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); java_candidates.append(JDK32s); java_candidates.append(NEWJDK32s); java_candidates.append(ADOPTOPENJDK32s); java_candidates.append(FOUNDATIONJDK32s); java_candidates.append(ADOPTIUMJDK32s); java_candidates.append(SEMERUJDK32s); java_candidates.append(ZULU32s); java_candidates.append(LIBERICA32s); java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); QList candidates; for (JavaInstallPtr java_candidate : java_candidates) { if (!candidates.contains(java_candidate->path)) { candidates.append(java_candidate->path); } } candidates.append(getMinecraftJavaBundle()); candidates.append(getPrismJavaBundle()); candidates = addJavasFromEnv(candidates); candidates.removeDuplicates(); return candidates; } #elif defined(Q_OS_MAC) QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); javas.append("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"); javas.append("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"); javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : libraryJVMJavas) { javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); } QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : systemLibraryJVMJavas) { javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } auto home = qEnvironmentVariable("HOME"); // javas downloaded by sdkman QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); QDir sdkmanJavaDir(FS::PathCombine(sdkmanDir, "candidates/java")); QStringList sdkmanJavas = sdkmanJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : sdkmanJavas) { javas.append(sdkmanJavaDir.absolutePath() + "/" + java + "/bin/java"); } // javas downloaded by asdf QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); QDir asdfJavaDir(FS::PathCombine(asdfDataDir, "installs/java")); QStringList asdfJavas = asdfJavaDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : asdfJavas) { javas.append(asdfJavaDir.absolutePath() + "/" + java + "/bin/java"); } // java in user library folder (like from intellij downloads) QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QString& java : userLibraryJVMJavas) { javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; } #elif defined(Q_OS_LINUX) || defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); auto scanJavaDir = [&javas]( const QString& dirPath, const std::function& filter = [](const QFileInfo&) { return true; }) { QDir dir(dirPath); if (!dir.exists()) return; auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { if (!filter(entry)) continue; QString prefix; prefix = entry.canonicalFilePath(); javas.append(FS::PathCombine(prefix, "jre/bin/java")); javas.append(FS::PathCombine(prefix, "bin/java")); } }; // java installed in a snap is installed in the standard directory, but underneath $SNAP auto snap = qEnvironmentVariable("SNAP"); auto scanJavaDirs = [scanJavaDir, snap](const QString& dirPath) { scanJavaDir(dirPath); if (!snap.isNull()) { scanJavaDir(snap + dirPath); } }; #if defined(Q_OS_LINUX) // oracle RPMs scanJavaDirs("/usr/java"); // general locations used by distro packaging scanJavaDirs("/usr/lib/jvm"); scanJavaDirs("/usr/lib64/jvm"); scanJavaDirs("/usr/lib32/jvm"); // Gentoo's locations for openjdk and openjdk-bin respectively auto gentooFilter = [](const QFileInfo& info) { QString fileName = info.fileName(); return fileName.startsWith("openjdk-") || fileName.startsWith("openj9-"); }; // AOSC OS's locations for openjdk auto aoscFilter = [](const QFileInfo& info) { QString fileName = info.fileName(); return fileName == "java" || fileName.startsWith("java-"); }; scanJavaDir("/usr/lib64", gentooFilter); scanJavaDir("/usr/lib", gentooFilter); scanJavaDir("/opt", gentooFilter); scanJavaDir("/usr/lib", aoscFilter); // javas stored in Prism Launcher's folder scanJavaDirs("java"); // manually installed JDKs in /opt scanJavaDirs("/opt/jdk"); scanJavaDirs("/opt/jdks"); scanJavaDirs("/opt/ibm"); // IBM Semeru Certified Edition // flatpak scanJavaDirs("/app/jdk"); #elif defined(Q_OS_OPENBSD) || defined(Q_OS_FREEBSD) // ports install to /usr/local on OpenBSD & FreeBSD scanJavaDirs("/usr/local"); #endif auto home = qEnvironmentVariable("HOME"); // javas downloaded by IntelliJ scanJavaDirs(FS::PathCombine(home, ".jdks")); // javas downloaded by sdkman QString sdkmanDir = qEnvironmentVariable("SDKMAN_DIR", FS::PathCombine(home, ".sdkman")); scanJavaDirs(FS::PathCombine(sdkmanDir, "candidates/java")); // javas downloaded by asdf QString asdfDataDir = qEnvironmentVariable("ASDF_DATA_DIR", FS::PathCombine(home, ".asdf")); scanJavaDirs(FS::PathCombine(asdfDataDir, "installs/java")); // javas downloaded by gradle (toolchains) QString gradleUserHome = qEnvironmentVariable("GRADLE_USER_HOME", FS::PathCombine(home, ".gradle")); scanJavaDirs(FS::PathCombine(gradleUserHome, "jdks")); javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; } #else QList JavaUtils::FindJavaPaths() { qDebug() << "Unknown operating system build - defaulting to \"java\""; QList javas; javas.append(this->GetDefaultJava()->path); javas.append(getMinecraftJavaBundle()); javas.append(getPrismJavaBundle()); javas.removeDuplicates(); return addJavasFromEnv(javas); } #endif QString JavaUtils::getJavaCheckPath() { return APPLICATION->getJarPath("JavaCheck.jar"); } QStringList getMinecraftJavaBundle() { QStringList processpaths; #if defined(Q_OS_MACOS) processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); #elif defined(Q_OS_WIN32) auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", ""); processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime"); // add the microsoft store version of the launcher to the search. the current path is: // C:\Users\USERNAME\AppData\Local\Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime auto localAppDataPath = QProcessEnvironment::systemEnvironment().value("LOCALAPPDATA", ""); auto minecraftMSStorePath = FS::PathCombine(QFileInfo(localAppDataPath).absoluteFilePath(), "Packages", "Microsoft.4297127D64EC6_8wekyb3d8bbwe"); processpaths << FS::PathCombine(minecraftMSStorePath, "LocalCache", "Local", "runtime"); #else processpaths << FS::PathCombine(QDir::homePath(), ".minecraft", "runtime"); #endif QStringList javas; while (!processpaths.isEmpty()) { auto dirPath = processpaths.takeFirst(); QDir dir(dirPath); if (!dir.exists()) continue; auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); auto binFound = false; for (auto& entry : entries) { if (entry.baseName() == "bin") { javas.append(FS::PathCombine(entry.canonicalFilePath(), JavaUtils::javaExecutable)); binFound = true; break; } } if (!binFound) { for (auto& entry : entries) { processpaths << entry.canonicalFilePath(); } } } return javas; } #if defined(Q_OS_WIN32) const QString JavaUtils::javaExecutable = "javaw.exe"; #else const QString JavaUtils::javaExecutable = "java"; #endif QStringList getPrismJavaBundle() { QList javas; auto scanDir = [&javas](QString prefix) { javas.append(FS::PathCombine(prefix, "jre", "bin", JavaUtils::javaExecutable)); javas.append(FS::PathCombine(prefix, "bin", JavaUtils::javaExecutable)); javas.append(FS::PathCombine(prefix, JavaUtils::javaExecutable)); }; auto scanJavaDir = [scanDir](const QString& dirPath) { QDir dir(dirPath); if (!dir.exists()) return; auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { scanDir(entry.canonicalFilePath()); } }; scanJavaDir(APPLICATION->javaPath()); return javas; } PrismLauncher-10.0.5/launcher/java/JavaInstall.cpp0000644000175100017510000000353115144136756021433 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "JavaInstall.h" #include "BaseVersion.h" #include "StringUtils.h" bool JavaInstall::operator<(const JavaInstall& rhs) const { auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); if (archCompare != 0) return archCompare < 0; if (id < rhs.id) { return true; } if (id > rhs.id) { return false; } return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; } bool JavaInstall::operator==(const JavaInstall& rhs) const { return arch == rhs.arch && id == rhs.id && path == rhs.path; } bool JavaInstall::operator>(const JavaInstall& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } bool JavaInstall::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); } catch (const std::bad_cast& e) { return BaseVersion::operator<(a); } } bool JavaInstall::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); } catch (const std::bad_cast& e) { return BaseVersion::operator>(a); } } PrismLauncher-10.0.5/launcher/java/JavaInstallList.cpp0000644000175100017510000001505715144136756022275 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include "Application.h" #include "java/JavaChecker.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "tasks/ConcurrentTask.h" JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions) : BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions) {} Task::Ptr JavaInstallList::getLoadTask() { load(); return getCurrentTask(); } Task::Ptr JavaInstallList::getCurrentTask() { if (m_status == Status::InProgress) { return m_load_task; } return nullptr; } void JavaInstallList::load() { if (m_status != Status::InProgress) { m_status = Status::InProgress; m_load_task.reset(new JavaListLoadTask(this, m_only_managed_versions)); m_load_task->start(); } } const BaseVersion::Ptr JavaInstallList::at(int i) const { return m_vlist.at(i); } bool JavaInstallList::isLoaded() { return m_status == JavaInstallList::Status::Done; } int JavaInstallList::count() const { return m_vlist.count(); } QVariant JavaInstallList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() > count()) return QVariant(); auto version = std::dynamic_pointer_cast(m_vlist[index.row()]); switch (role) { case SortRole: return -index.row(); case VersionPointerRole: return QVariant::fromValue(m_vlist[index.row()]); case VersionIdRole: return version->descriptor(); case VersionRole: return version->id.toString(); case RecommendedRole: return version->recommended; case PathRole: return version->path; case CPUArchitectureRole: return version->arch; default: return QVariant(); } } BaseVersionList::RoleList JavaInstallList::providesRoles() const { return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, CPUArchitectureRole }; } void JavaInstallList::updateListData(QList versions) { beginResetModel(); m_vlist = versions; sortVersions(); if (m_vlist.size()) { auto best = std::dynamic_pointer_cast(m_vlist[0]); best->recommended = true; } endResetModel(); m_status = Status::Done; m_load_task.reset(); } bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right) { auto rleft = std::dynamic_pointer_cast(right); auto rright = std::dynamic_pointer_cast(left); return (*rleft) > (*rright); } void JavaInstallList::sortVersions() { beginResetModel(); std::sort(m_vlist.begin(), m_vlist.end(), sortJavas); endResetModel(); } JavaListLoadTask::JavaListLoadTask(JavaInstallList* vlist, bool onlyManagedVersions) : Task(), m_only_managed_versions(onlyManagedVersions) { m_list = vlist; m_current_recommended = NULL; } void JavaListLoadTask::executeTask() { setStatus(tr("Detecting Java installations...")); JavaUtils ju; QList candidate_paths = m_only_managed_versions ? getPrismJavaBundle() : ju.FindJavaPaths(); ConcurrentTask::Ptr job(new ConcurrentTask("Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); m_job.reset(job); connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); connect(m_job.get(), &Task::progress, this, &Task::setProgress); qDebug() << "Probing the following Java paths: "; int id = 0; for (QString candidate : candidate_paths) { auto checker = new JavaChecker(candidate, "", 0, 0, 0, id); connect(checker, &JavaChecker::checkFinished, [this](const JavaChecker::Result& result) { m_results << result; }); job->addTask(Task::Ptr(checker)); id++; } m_job->start(); } void JavaListLoadTask::javaCheckerFinished() { QList candidates; std::sort(m_results.begin(), m_results.end(), [](const JavaChecker::Result& a, const JavaChecker::Result& b) { return a.id < b.id; }); qDebug() << "Found the following valid Java installations:"; for (auto result : m_results) { if (result.validity == JavaChecker::Result::Validity::Valid) { JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = result.javaVersion; javaVersion->arch = result.realPlatform; javaVersion->path = result.path; javaVersion->is_64bit = result.is_64bit; candidates.append(javaVersion); qDebug() << " " << javaVersion->id.toString() << javaVersion->arch << javaVersion->path; } } QList javas_bvp; for (auto java : candidates) { // qDebug() << java->id << java->arch << " at " << java->path; BaseVersion::Ptr bp_java = std::dynamic_pointer_cast(java); if (bp_java) { javas_bvp.append(java); } } m_list->updateListData(javas_bvp); emitSucceeded(); } PrismLauncher-10.0.5/launcher/java/JavaInstallList.h0000644000175100017510000000415515144136756021737 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "BaseVersionList.h" #include "java/JavaChecker.h" #include "tasks/Task.h" #include "JavaInstall.h" #include "QObjectPtr.h" class JavaListLoadTask; class JavaInstallList : public BaseVersionList { Q_OBJECT enum class Status { NotDone, InProgress, Done }; public: explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false); Task::Ptr getLoadTask() override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; QVariant data(const QModelIndex& index, int role) const override; RoleList providesRoles() const override; public slots: void updateListData(QList versions) override; protected: void load(); Task::Ptr getCurrentTask(); protected: Status m_status = Status::NotDone; shared_qobject_ptr m_load_task; QList m_vlist; bool m_only_managed_versions; }; class JavaListLoadTask : public Task { Q_OBJECT public: explicit JavaListLoadTask(JavaInstallList* vlist, bool onlyManagedVersions = false); virtual ~JavaListLoadTask() = default; protected: void executeTask() override; public slots: void javaCheckerFinished(); protected: Task::Ptr m_job; JavaInstallList* m_list; JavaInstall* m_current_recommended; QList m_results; bool m_only_managed_versions; }; PrismLauncher-10.0.5/launcher/java/JavaVersion.h0000644000175100017510000000217315144136756021120 0ustar runnerrunner#pragma once #include // NOTE: apparently the GNU C library pollutes the global namespace with these... undef them. #ifdef major #undef major #endif #ifdef minor #undef minor #endif class JavaVersion { friend class JavaVersionTest; public: JavaVersion() {} JavaVersion(const QString& rhs); JavaVersion(int major, int minor, int security, int build = 0, QString name = ""); JavaVersion& operator=(const QString& rhs); bool operator<(const JavaVersion& rhs) const; bool operator==(const JavaVersion& rhs) const; bool operator>(const JavaVersion& rhs) const; bool requiresPermGen() const; bool defaultsToUtf8() const; bool isModular() const; QString toString() const; int major() const { return m_major; } int minor() const { return m_minor; } int security() const { return m_security; } QString build() const { return m_prerelease; } QString name() const { return m_name; } private: QString m_string; int m_major = 0; int m_minor = 0; int m_security = 0; QString m_name = ""; bool m_parseable = false; QString m_prerelease; }; PrismLauncher-10.0.5/launcher/java/JavaChecker.cpp0000644000175100017510000001471515144136756021377 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "JavaChecker.h" #include #include #include #include #include "Commandline.h" #include "FileSystem.h" #include "java/JavaUtils.h" JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id) : Task(), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id) {} void JavaChecker::executeTask() { QString checkerJar = JavaUtils::getJavaCheckPath(); if (checkerJar.isEmpty()) { qDebug() << "Java checker library could not be found. Please check your installation."; return; } #ifdef Q_OS_WIN checkerJar = FS::getPathNameInLocal8bit(checkerJar); #endif QStringList args; process.reset(new QProcess()); if (m_args.size()) { auto extraArgs = Commandline::splitArgs(m_args); args.append(extraArgs); } if (m_minMem != 0) { args << QString("-Xms%1m").arg(m_minMem); } if (m_maxMem != 0) { args << QString("-Xmx%1m").arg(m_maxMem); } if (m_permGen != 64 && m_permGen != 0) { args << QString("-XX:PermSize=%1m").arg(m_permGen); } args.append({ "-jar", checkerJar }); process->setArguments(args); process->setProgram(m_path); process->setProcessChannelMode(QProcess::SeparateChannels); process->setProcessEnvironment(CleanEnviroment()); qDebug() << "Running java checker:" << m_path << args.join(" "); connect(process.get(), &QProcess::finished, this, &JavaChecker::finished); connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error); connect(process.get(), &QProcess::readyReadStandardOutput, this, &JavaChecker::stdoutReady); connect(process.get(), &QProcess::readyReadStandardError, this, &JavaChecker::stderrReady); connect(&killTimer, &QTimer::timeout, this, &JavaChecker::timeout); killTimer.setSingleShot(true); killTimer.start(15000); process->start(); } void JavaChecker::stdoutReady() { QByteArray data = process->readAllStandardOutput(); QString added = QString::fromLocal8Bit(data); added.remove('\r'); m_stdout += added; } void JavaChecker::stderrReady() { QByteArray data = process->readAllStandardError(); QString added = QString::fromLocal8Bit(data); added.remove('\r'); m_stderr += added; } void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) { killTimer.stop(); QProcessPtr _process = process; process.reset(); Result result = { m_path, m_id, }; result.errorLog = m_stderr; result.outLog = m_stdout; qDebug() << "STDOUT" << m_stdout; qWarning() << "STDERR" << m_stderr; qDebug() << "Java checker finished with status" << status << "exit code" << exitcode; if (status == QProcess::CrashExit || exitcode == 1) { result.validity = Result::Validity::Errored; emit checkFinished(result); emitSucceeded(); return; } bool success = true; QMap results; QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts); for (QString line : lines) { line = line.trimmed(); // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux if (line.contains("/bedrock/strata")) { continue; } auto parts = line.split('=', Qt::SkipEmptyParts); if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { continue; } else { results.insert(parts[0], parts[1]); } } if (!results.contains("os.arch") || !results.contains("java.version") || !results.contains("java.vendor") || !success) { result.validity = Result::Validity::ReturnedInvalidData; emit checkFinished(result); emitSucceeded(); return; } auto os_arch = results["os.arch"]; auto java_version = results["java.version"]; auto java_vendor = results["java.vendor"]; bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64" || os_arch == "riscv64"; result.validity = Result::Validity::Valid; result.is_64bit = is_64; result.mojangPlatform = is_64 ? "64" : "32"; result.realPlatform = os_arch; result.javaVersion = java_version; result.javaVendor = java_vendor; qDebug() << "Java checker succeeded."; emit checkFinished(result); emitSucceeded(); } void JavaChecker::error(QProcess::ProcessError err) { if (err == QProcess::FailedToStart) { qDebug() << "Java checker has failed to start."; qDebug() << "Process environment:"; qDebug() << process->environment(); qDebug() << "Native environment:"; qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); killTimer.stop(); emit checkFinished({ m_path, m_id }); } emitSucceeded(); } void JavaChecker::timeout() { // NO MERCY. NO ABUSE. if (process) { qDebug() << "Java checker has been killed by timeout."; process->kill(); } } PrismLauncher-10.0.5/launcher/java/JavaMetadata.h0000644000175100017510000000367015144136756021216 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "BaseVersion.h" #include "java/JavaVersion.h" namespace Java { enum class DownloadType { Manifest, Archive, Unknown }; class Metadata : public BaseVersion { public: virtual QString descriptor() const override { return version.toString(); } virtual QString name() const override { return m_name; } virtual QString typeString() const override { return vendor; } virtual bool operator<(BaseVersion& a) const override; virtual bool operator>(BaseVersion& a) const override; bool operator<(const Metadata& rhs) const; bool operator==(const Metadata& rhs) const; bool operator>(const Metadata& rhs) const; QString m_name; QString vendor; QString url; QDateTime releaseTime; QString checksumType; QString checksumHash; DownloadType downloadType; QString packageType; JavaVersion version; QString runtimeOS; }; using MetadataPtr = std::shared_ptr; DownloadType parseDownloadType(QString javaDownload); QString downloadTypeToString(DownloadType javaDownload); MetadataPtr parseJavaMeta(const QJsonObject& libObj); } // namespace Java PrismLauncher-10.0.5/launcher/MMCTime.h0000644000175100017510000000221515144136756017200 0ustar runnerrunner/* * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include namespace Time { QString prettifyDuration(int64_t duration, bool noDays = false); /** * @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`. * miliseconds are only included if `precision` is greater than 0. * * @param duration a number of seconds as floating point * @param precision number of decmial points to display on fractons of a second, defualts to 0. * @return QString */ QString humanReadableDuration(double duration, int precision = 0); } // namespace Time PrismLauncher-10.0.5/launcher/Application.h0000644000175100017510000002274315144136756020220 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include #include #include "launch/LogModel.h" #include "minecraft/launch/MinecraftTarget.h" class LaunchController; class LocalPeer; class InstanceWindow; class MainWindow; class ViewLogWindow; class SetupWizard; class GenericPageProvider; class QFile; class HttpMetaCache; class SettingsObject; class InstanceList; class AccountList; class IconList; class QNetworkAccessManager; class JavaInstallList; class ExternalUpdater; class BaseProfilerFactory; class BaseDetachedToolFactory; class TranslationsModel; class ITheme; class MCEditTool; class ThemeManager; class IconTheme; namespace Meta { class Index; } #if defined(APPLICATION) #undef APPLICATION #endif #define APPLICATION (static_cast(QCoreApplication::instance())) // Used for checking if is a test #if defined(APPLICATION_DYN) #undef APPLICATION_DYN #endif #define APPLICATION_DYN (dynamic_cast(QCoreApplication::instance())) class Application : public QApplication { // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: enum Status { StartingUp, Failed, Succeeded, Initialized }; enum Capability { None = 0, SupportsMSA = 1 << 0, SupportsFlame = 1 << 1, SupportsGameMode = 1 << 2, SupportsMangoHud = 1 << 3, }; Q_DECLARE_FLAGS(Capabilities, Capability) public: Application(int& argc, char** argv); virtual ~Application(); bool event(QEvent* event) override; std::shared_ptr settings() const { return m_settings; } qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); } QIcon logo(); ThemeManager* themeManager() { return m_themeManager.get(); } shared_qobject_ptr updater() { return m_updater; } void triggerUpdateCheck(); std::shared_ptr translations(); std::shared_ptr javalist(); std::shared_ptr instances() const { return m_instances; } std::shared_ptr icons() const { return m_icons; } MCEditTool* mcedit() const { return m_mcedit.get(); } shared_qobject_ptr accounts() const { return m_accounts; } Status status() const { return m_status; } const QMap>& profilers() const { return m_profilers; } void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password); shared_qobject_ptr network(); shared_qobject_ptr metacache(); shared_qobject_ptr metadataIndex(); void updateCapabilities(); void detectLibraries(); /*! * Finds and returns the full path to a jar file. * Returns a null-string if it could not be found. */ QString getJarPath(QString jarFile); QString getMSAClientID(); QString getFlameAPIKey(); QString getModrinthAPIToken(); QString getUserAgent(); /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } /// the data path the application is using const QString& dataRoot() { return m_dataPath; } /// the java installed path the application is using const QString javaPath(); bool isPortable() { return m_portable; } const Capabilities capabilities() { return m_capabilities; } /*! * Opens a json file using either a system default editor, or, if not empty, the editor * specified in the settings */ bool openJsonEditor(const QString& filename); InstanceWindow* showInstanceWindow(InstancePtr instance, QString page = QString()); MainWindow* showMainWindow(bool minimized = false); ViewLogWindow* showLogWindow(); void updateIsRunning(bool running); bool updatesAreAllowed(); void ShowGlobalSettings(class QWidget* parent, QString open_page = QString()); bool updaterEnabled(); QString updaterBinaryName(); QUrl normalizeImportUrl(QString const& url); signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); void globalSettingsApplied(); int currentCatChanged(int index); void oauthReplyRecieved(QVariantMap); #ifdef Q_OS_MACOS void clickedOnDock(); #endif public slots: bool launch(InstancePtr instance, bool online = true, bool demo = false, MinecraftTarget::Ptr targetToJoin = nullptr, MinecraftAccountPtr accountToUse = nullptr, const QString& offlineName = QString()); bool kill(InstancePtr instance); void closeCurrentWindow(); private slots: void on_windowClose(); void messageReceived(const QByteArray& message); void controllerSucceeded(); void controllerFailed(const QString& error); void setupWizardFinished(int status); private: bool handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, const QString& configFile) const; bool createSetupWizard(); void performMainStartupAction(); // sets the fatal error message and m_status to Failed. void showFatalErrorMessage(const QString& title, const QString& content); private: void addRunningInstance(); void subRunningInstance(); bool shouldExitNow() const; private: QDateTime m_startTime; shared_qobject_ptr m_network; shared_qobject_ptr m_updater; shared_qobject_ptr m_accounts; shared_qobject_ptr m_metacache; shared_qobject_ptr m_metadataIndex; std::shared_ptr m_settings; std::shared_ptr m_instances; std::shared_ptr m_icons; std::shared_ptr m_javalist; std::shared_ptr m_translations; std::shared_ptr m_globalSettingsProvider; std::unique_ptr m_mcedit; QSet m_features; std::unique_ptr m_themeManager; QMap> m_profilers; QString m_rootPath; QString m_dataPath; Status m_status = Application::StartingUp; Capabilities m_capabilities; bool m_portable = false; #ifdef Q_OS_MACOS Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive; #endif #if defined Q_OS_WIN32 // used on Windows to attach the standard IO streams bool consoleAttached = false; #endif // FIXME: attach to instances instead. struct InstanceXtras { InstanceWindow* window = nullptr; shared_qobject_ptr controller; }; std::map m_instanceExtras; mutable QMutex m_instanceExtrasMutex; // main state variables size_t m_openWindows = 0; size_t m_runningInstances = 0; bool m_updateRunning = false; // main window, if any MainWindow* m_mainWindow = nullptr; // log window, if any ViewLogWindow* m_viewLogWindow = nullptr; // peer launcher instance connector - used to implement single instance launcher and signalling LocalPeer* m_peerInstance = nullptr; SetupWizard* m_setupWizard = nullptr; public: QString m_detectedGLFWPath; QString m_detectedOpenALPath; QString m_instanceIdToLaunch; QString m_serverToJoin; QString m_worldToJoin; QString m_profileToUse; bool m_offline = false; QString m_offlineName; bool m_liveCheck = false; QList m_urlsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; shared_qobject_ptr logModel; public: void addQSavePath(QString); void removeQSavePath(QString); bool checkQSavePath(QString); private: QHash m_qsaveResources; mutable QMutex m_qsaveResourcesMutex; }; PrismLauncher-10.0.5/launcher/MMCZip.h0000644000175100017510000000770415144136756017054 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include #include #include "archive/ArchiveReader.h" #if defined(LAUNCHER_APPLICATION) #include "minecraft/mod/Mod.h" #endif namespace MMCZip { using FilterFileFunction = std::function; #if defined(LAUNCHER_APPLICATION) /** * take a source jar, add mods to it, resulting in target jar */ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); #endif /** * Extract a subdirectory from an archive */ std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target); /** * Extract a whole archive. * * \param fileCompressed The name of the archive. * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ std::optional extractDir(QString fileCompressed, QString dir); /** * Extract a subdirectory from an archive * * \param fileCompressed The name of the archive. * \param subdir The directory within the archive to extract * \param dir The directory to extract to, the current directory if left empty. * \return The list of the full paths of the files extracted, empty on failure. */ std::optional extractDir(QString fileCompressed, QString subdir, QString dir); /** * Extract a single file from an archive into a directory * * \param fileCompressed The name of the archive. * \param file The file within the archive to extract * \param dir The directory to extract to, the current directory if left empty. * \return true for success or false for failure */ bool extractFile(QString fileCompressed, QString file, QString dir); /** * Populate a QFileInfoList with a directory tree recursively, while allowing to excludeFilter what shouldn't be included. * \param rootDir directory to start off * \param subDir subdirectory, should be nullptr for first invocation * \param files resulting list of QFileInfo * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) * \return true for success or false for failure */ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter); } // namespace MMCZip PrismLauncher-10.0.5/launcher/InstanceDirUpdate.h0000644000175100017510000000354315144136756021320 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "BaseInstance.h" /// Update instanceRoot to make it sync with name/id; return newRoot if a directory rename happened QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent); /// Check if there are linked instances, and display a warning; return true if the operation should proceed bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb); PrismLauncher-10.0.5/launcher/main.cpp0000644000175100017510000000471615144136756017234 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Application.h" int main(int argc, char* argv[]) { // initialize Qt Application app(argc, argv); switch (app.status()) { case Application::StartingUp: case Application::Initialized: { Q_INIT_RESOURCE(multimc); Q_INIT_RESOURCE(backgrounds); Q_INIT_RESOURCE(documents); Q_INIT_RESOURCE(prismlauncher); Q_INIT_RESOURCE(pe_dark); Q_INIT_RESOURCE(pe_light); Q_INIT_RESOURCE(pe_blue); Q_INIT_RESOURCE(pe_colored); Q_INIT_RESOURCE(breeze_dark); Q_INIT_RESOURCE(breeze_light); Q_INIT_RESOURCE(OSX); Q_INIT_RESOURCE(iOS); Q_INIT_RESOURCE(flat); Q_INIT_RESOURCE(flat_white); Q_INIT_RESOURCE(shaders); return app.exec(); } case Application::Failed: return 1; case Application::Succeeded: return 0; default: return -1; } } PrismLauncher-10.0.5/launcher/net/0000755000175100017510000000000015144136756016362 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/net/NetUtils.h0000644000175100017510000000435215144136756020306 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include namespace Net { inline bool isApplicationError(QNetworkReply::NetworkError x) { // Mainly taken from https://github.com/qt/qtbase/blob/dev/src/network/access/qhttpthreaddelegate.cpp static QSet errors = { QNetworkReply::ProtocolInvalidOperationError, QNetworkReply::AuthenticationRequiredError, QNetworkReply::ContentAccessDenied, QNetworkReply::ContentNotFoundError, QNetworkReply::ContentOperationNotPermittedError, QNetworkReply::ProxyAuthenticationRequiredError, QNetworkReply::ContentConflictError, QNetworkReply::ContentGoneError, QNetworkReply::InternalServerError, QNetworkReply::OperationNotImplementedError, QNetworkReply::ServiceUnavailableError, QNetworkReply::UnknownServerError, QNetworkReply::UnknownContentError }; return errors.contains(x); } } // namespace Net PrismLauncher-10.0.5/launcher/net/NetJob.h0000644000175100017510000000532715144136756017723 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "net/NetRequest.h" #include "tasks/ConcurrentTask.h" // Those are included so that they are also included by anyone using NetJob #include "net/Download.h" #include "net/HttpMetaCache.h" class NetJob : public ConcurrentTask { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent = -1); ~NetJob() override = default; auto size() const -> int; auto canAbort() const -> bool override; auto addNetAction(Net::NetRequest::Ptr action) -> bool; auto getFailedActions() -> QList; auto getFailedFiles() -> QList; void setAskRetry(bool askRetry); public slots: // Qt can't handle auto at the start for some reason? bool abort() override; void emitFailed(QString reason) override; protected slots: void executeNextSubTask() override; protected: void updateState() override; bool isOnline(); private: shared_qobject_ptr m_network; int m_try = 1; bool m_ask_retry = true; int m_manual_try = 0; }; PrismLauncher-10.0.5/launcher/net/ApiUpload.cpp0000644000175100017510000000211015144136756020736 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "net/ApiUpload.h" #include "net/ApiHeaderProxy.h" namespace Net { Upload::Ptr ApiUpload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) { auto up = Upload::makeByteArray(url, output, m_post_data); up->addHeaderProxy(new ApiHeaderProxy()); return up; } } // namespace Net PrismLauncher-10.0.5/launcher/net/ApiDownload.h0000644000175100017510000000233115144136756020733 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "Download.h" namespace Net { namespace ApiDownload { Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Download::Options options = Download::Option::NoOptions); Download::Ptr makeByteArray(QUrl url, std::shared_ptr output, Download::Options options = Download::Option::NoOptions); Download::Ptr makeFile(QUrl url, QString path, Download::Options options = Download::Option::NoOptions); }; // namespace ApiDownload } // namespace Net PrismLauncher-10.0.5/launcher/net/NetRequest.h0000644000175100017510000000774015144136756020642 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "HeaderProxy.h" #include "Sink.h" #include "Validator.h" #include "QObjectPtr.h" #include "net/Logging.h" #include "tasks/Task.h" namespace Net { class NetRequest : public Task { Q_OBJECT protected: explicit NetRequest(); public: using Ptr = shared_qobject_ptr; enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2, AutoRetry = 4 }; Q_DECLARE_FLAGS(Options, Option) public: ~NetRequest() override = default; void addValidator(Validator* v); auto abort() -> bool override; auto canAbort() const -> bool override { return true; } void setNetwork(shared_qobject_ptr network) { m_network = network; } void addHeaderProxy(Net::HeaderProxy* proxy) { m_headerProxies.push_back(std::shared_ptr(proxy)); } // automatically handle HTTP 429 Too Many Requests errors and retry void enableAutoRetry(bool enable); QUrl url() const; void setUrl(QUrl url) { m_url = url; } int replyStatusCode() const; QNetworkReply::NetworkError error() const; QString errorString() const; private: auto handleRedirect() -> bool; void handleAutoRetry(int64_t delay); virtual QNetworkReply* getReply(QNetworkRequest&) = 0; protected slots: void onProgress(qint64 bytesReceived, qint64 bytesTotal); void downloadError(QNetworkReply::NetworkError error); void sslErrors(const QList& errors); void downloadFinished(); void downloadReadyRead(); void executeTask() override; protected: std::unique_ptr m_sink; Options m_options; using logCatFunc = const QLoggingCategory& (*)(); logCatFunc logCat = taskUploadLogC; std::chrono::steady_clock m_clock; std::chrono::time_point m_last_progress_time; qint64 m_last_progress_bytes; shared_qobject_ptr m_network; /// the network reply unique_qobject_ptr m_reply; QByteArray m_errorResponse; /// source URL QUrl m_url; std::vector> m_headerProxies; int m_retryCount = 0; QTimer m_retryTimer; }; } // namespace Net Q_DECLARE_OPERATORS_FOR_FLAGS(Net::NetRequest::Options) PrismLauncher-10.0.5/launcher/net/Validator.h0000644000175100017510000000342315144136756020462 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include namespace Net { class Validator { public: /* con/des */ Validator() {} virtual ~Validator() {} public: /* methods */ virtual bool init(QNetworkRequest& request) = 0; virtual bool write(QByteArray& data) = 0; virtual bool abort() = 0; virtual bool validate(QNetworkReply& reply) = 0; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/ChecksumValidator.h0000644000175100017510000000542415144136756022150 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "Validator.h" #include #include namespace Net { class ChecksumValidator : public Validator { public: ChecksumValidator(QCryptographicHash::Algorithm algorithm, QString expectedHex) : Net::ChecksumValidator(algorithm, QByteArray::fromHex(expectedHex.toLatin1())) {} ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) : m_checksum(algorithm), m_expected(expected) {}; virtual ~ChecksumValidator() = default; public: auto init(QNetworkRequest&) -> bool override { m_checksum.reset(); return true; } auto write(QByteArray& data) -> bool override { m_checksum.addData(data); return true; } auto abort() -> bool override { m_checksum.reset(); return true; } auto validate(QNetworkReply&) -> bool override { if (m_expected.size() && m_expected != hash()) { qWarning() << "Checksum mismatch, download is bad."; return false; } return true; } auto hash() -> QByteArray { return m_checksum.result(); } void setExpected(QByteArray expected) { m_expected = expected; } private: QCryptographicHash m_checksum; QByteArray m_expected; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/Download.h0000644000175100017510000000455215144136756020310 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "HttpMetaCache.h" #include "QObjectPtr.h" #include "net/NetRequest.h" namespace Net { class Download : public NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit Download() : NetRequest() { logCat = taskDownloadLogC; } #if defined(LAUNCHER_APPLICATION) static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; #endif static auto makeByteArray(QUrl url, std::shared_ptr output, Options options = Option::NoOptions) -> Download::Ptr; static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/ApiUpload.h0000644000175100017510000000167615144136756020423 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "Upload.h" namespace Net { namespace ApiUpload { Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); }; } // namespace Net PrismLauncher-10.0.5/launcher/net/ApiDownload.cpp0000644000175100017510000000277015144136756021275 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "net/ApiDownload.h" #include "net/ApiHeaderProxy.h" namespace Net { Download::Ptr ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Download::Options options) { auto dl = Download::makeCached(url, entry, options); dl->addHeaderProxy(new ApiHeaderProxy()); return dl; } Download::Ptr ApiDownload::makeByteArray(QUrl url, std::shared_ptr output, Download::Options options) { auto dl = Download::makeByteArray(url, output, options); dl->addHeaderProxy(new ApiHeaderProxy()); return dl; } Download::Ptr ApiDownload::makeFile(QUrl url, QString path, Download::Options options) { auto dl = Download::makeFile(url, path, options); dl->addHeaderProxy(new ApiHeaderProxy()); return dl; } } // namespace Net PrismLauncher-10.0.5/launcher/net/Download.cpp0000644000175100017510000000605315144136756020641 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Download.h" #include #include #include #include #include "ByteArraySink.h" #include "ChecksumValidator.h" #include "MetaCacheSink.h" namespace Net { #if defined(LAUNCHER_APPLICATION) auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr { auto dl = makeShared(); dl->m_url = url; dl->setObjectName(QString("CACHE:") + url.toString()); dl->m_options = options; auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal)); dl->m_sink.reset(cachedNode); return dl; } #endif auto Download::makeByteArray(QUrl url, std::shared_ptr output, Options options) -> Download::Ptr { auto dl = makeShared(); dl->m_url = url; dl->setObjectName(QString("BYTES:") + url.toString()); dl->m_options = options; dl->m_sink.reset(new ByteArraySink(output)); return dl; } auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr { auto dl = makeShared(); dl->m_url = url; dl->setObjectName(QString("FILE:") + url.toString()); dl->m_options = options; dl->m_sink.reset(new FileSink(path)); return dl; } QNetworkReply* Download::getReply(QNetworkRequest& request) { return m_network->get(request); } } // namespace Net PrismLauncher-10.0.5/launcher/net/MetaCacheSink.cpp0000644000175100017510000001150115144136756021523 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MetaCacheSink.h" #include #include #include #include "Application.h" #include "net/Logging.h" namespace Net { /** Maximum time to hold a cache entry * = 1 week in seconds */ #define MAX_TIME_TO_EXPIRE 1 * 7 * 24 * 60 * 60 MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal) : Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum), m_is_eternal(is_eternal) { addValidator(md5sum); } Task::State MetaCacheSink::initCache(QNetworkRequest& request) { if (!m_entry->isStale()) { return Task::State::Succeeded; } // check if file exists, if it does, use its information for the request QFile current(m_filename); if (current.exists() && current.size() != 0) { if (m_entry->getRemoteChangedTimestamp().size()) { request.setRawHeader(QString("If-Modified-Since").toLatin1(), m_entry->getRemoteChangedTimestamp().toLatin1()); } if (m_entry->getETag().size()) { request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); } } return Task::State::Running; } Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply) { QFileInfo output_file_info(m_filename); if (m_wroteAnyData) { m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); } m_entry->setETag(reply.rawHeader("ETag").constData()); if (reply.hasRawHeader("Last-Modified")) { m_entry->setRemoteChangedTimestamp(reply.rawHeader("Last-Modified").constData()); } m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); { // Cache lifetime if (m_is_eternal) { qCDebug(taskMetaCacheLogC) << "Adding eternal cache entry:" << m_entry->getFullPath(); m_entry->makeEternal(true); } else if (reply.hasRawHeader("Cache-Control")) { auto cache_control_header = reply.rawHeader("Cache-Control"); qCDebug(taskMetaCacheLogC) << "Parsing 'Cache-Control' header with" << cache_control_header; static const QRegularExpression s_maxAgeExpr("max-age=([0-9]+)"); qint64 max_age = s_maxAgeExpr.match(cache_control_header).captured(1).toLongLong(); m_entry->setMaximumAge(max_age); } else if (reply.hasRawHeader("Expires")) { auto expires_header = reply.rawHeader("Expires"); qCDebug(taskMetaCacheLogC) << "Parsing 'Expires' header with" << expires_header; qint64 max_age = QDateTime::fromString(expires_header).toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch(); m_entry->setMaximumAge(max_age); } else { m_entry->setMaximumAge(MAX_TIME_TO_EXPIRE); } if (reply.hasRawHeader("Age")) { auto age_header = reply.rawHeader("Age"); qCDebug(taskMetaCacheLogC) << "Parsing 'Age' header with" << age_header; qint64 current_age = age_header.toLongLong(); m_entry->setCurrentAge(current_age); } else { m_entry->setCurrentAge(0); } } m_entry->setStale(false); APPLICATION->metacache()->updateEntry(m_entry); return Task::State::Succeeded; } bool MetaCacheSink::hasLocalData() { QFileInfo info(m_filename); return info.exists() && info.size() != 0; } } // namespace Net PrismLauncher-10.0.5/launcher/net/HttpMetaCache.h0000644000175100017510000001107715144136756021213 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include class HttpMetaCache; class MetaEntry { friend class HttpMetaCache; protected: MetaEntry() = default; public: auto isStale() -> bool { return m_stale; } void setStale(bool stale) { m_stale = stale; } auto getFullPath() -> QString; auto getRemoteChangedTimestamp() -> QString { return m_remote_changed_timestamp; } void setRemoteChangedTimestamp(QString remote_changed_timestamp) { m_remote_changed_timestamp = remote_changed_timestamp; } void setLocalChangedTimestamp(qint64 timestamp) { m_local_changed_timestamp = timestamp; } auto getETag() -> QString { return m_etag; } void setETag(QString etag) { m_etag = etag; } auto getMD5Sum() -> QString { return m_md5sum; } void setMD5Sum(QString md5sum) { m_md5sum = md5sum; } /* Whether the entry expires after some time (false) or not (true). */ void makeEternal(bool eternal) { m_is_eternal = eternal; } bool isEternal() const { return m_is_eternal; } auto getCurrentAge() -> qint64 { return m_current_age; } void setCurrentAge(qint64 age) { m_current_age = age; } auto getMaximumAge() -> qint64 { return m_max_age; } void setMaximumAge(qint64 age) { m_max_age = age; } bool isExpired(qint64 offset) { return !m_is_eternal && (m_current_age >= m_max_age - offset); } protected: QString m_baseId; QString m_basePath; QString m_relativePath; QString m_md5sum; QString m_etag; qint64 m_local_changed_timestamp = 0; QString m_remote_changed_timestamp; // QString for now, RFC 2822 encoded time qint64 m_current_age = 0; qint64 m_max_age = 0; bool m_is_eternal = false; bool m_stale = true; }; using MetaEntryPtr = std::shared_ptr; class HttpMetaCache : public QObject { Q_OBJECT public: // supply path to the cache index file HttpMetaCache(QString path = QString()); ~HttpMetaCache() override; // get the entry solely from the cache // you probably don't want this, unless you have some specific caching needs. auto getEntry(QString base, QString resource_path) -> MetaEntryPtr; // get the entry from cache and verify that it isn't stale (within reason) auto resolveEntry(QString base, QString resource_path, QString expected_etag = QString()) -> MetaEntryPtr; // add a previously resolved stale entry auto updateEntry(MetaEntryPtr stale_entry) -> bool; // evict selected entry from cache auto evictEntry(MetaEntryPtr entry) -> bool; bool evictAll(); void addBase(QString base, QString base_root); // (re)start a timer that calls SaveNow later. void SaveEventually(); void Load(); auto getBasePath(QString base) -> QString; public slots: void SaveNow(); private: // create a new stale entry, given the parameters auto staleEntry(QString base, QString resource_path) -> MetaEntryPtr; struct EntryMap { QString base_path; QMap entry_list; }; QMap m_entries; QString m_index_file; QTimer saveBatchingTimer; }; PrismLauncher-10.0.5/launcher/net/RawHeaderProxy.h0000644000175100017510000000332015144136756021435 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "net/HeaderProxy.h" namespace Net { class RawHeaderProxy : public HeaderProxy { public: RawHeaderProxy(QList headers = {}) : HeaderProxy(), m_headers(std::move(headers)) {}; virtual ~RawHeaderProxy() = default; public: virtual QList headers(const QNetworkRequest&) const override { return m_headers; }; void addHeader(const HeaderPair& header) { m_headers.append(header); } void addHeader(const QByteArray& headerName, const QByteArray& headerValue) { m_headers.append({ headerName, headerValue }); } void addHeaders(const QList& headers) { m_headers.append(headers); } void setHeaders(QList headers) { m_headers = headers; }; private: QList m_headers; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/MetaCacheSink.h0000644000175100017510000000406515144136756021177 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "ChecksumValidator.h" #include "FileSink.h" #include "net/HttpMetaCache.h" namespace Net { class MetaCacheSink : public FileSink { public: MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal = false); virtual ~MetaCacheSink() = default; auto hasLocalData() -> bool override; protected: auto initCache(QNetworkRequest& request) -> Task::State override; auto finalizeCache(QNetworkReply& reply) -> Task::State override; private: MetaEntryPtr m_entry; ChecksumValidator* m_md5Node; bool m_is_eternal; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/Upload.h0000644000175100017510000000405515144136756017763 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "net/NetRequest.h" namespace Net { class Upload : public NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit Upload() : NetRequest() { logCat = taskUploadLogC; }; static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; QByteArray m_post_data; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/Sink.h0000644000175100017510000000607015144136756017442 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "Validator.h" #include "tasks/Task.h" namespace Net { class Sink { public: Sink() = default; virtual ~Sink() = default; public: virtual auto init(QNetworkRequest& request) -> Task::State = 0; virtual auto write(QByteArray& data) -> Task::State = 0; virtual auto abort() -> Task::State = 0; virtual auto finalize(QNetworkReply& reply) -> Task::State = 0; virtual auto hasLocalData() -> bool = 0; QString failReason() const { return m_fail_reason; } void addValidator(Validator* validator) { if (validator) { validators.push_back(std::shared_ptr(validator)); } } protected: bool initAllValidators(QNetworkRequest& request) { for (auto& validator : validators) { if (!validator->init(request)) return false; } return true; } bool finalizeAllValidators(QNetworkReply& reply) { for (auto& validator : validators) { if (!validator->validate(reply)) return false; } return true; } bool failAllValidators() { bool success = true; for (auto& validator : validators) { success &= validator->abort(); } return success; } bool writeAllValidators(QByteArray& data) { for (auto& validator : validators) { if (!validator->write(data)) return false; } return true; } protected: std::vector> validators; QString m_fail_reason; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/Upload.cpp0000644000175100017510000000443615144136756020321 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Upload.h" #include #include #include "ByteArraySink.h" namespace Net { QNetworkReply* Upload::getReply(QNetworkRequest& request) { if (!request.hasRawHeader("Content-Type")) request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); return m_network->post(request, m_post_data); } Upload::Ptr Upload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) { auto up = makeShared(); up->m_url = std::move(url); up->m_sink.reset(new ByteArraySink(output)); up->m_post_data = std::move(m_post_data); return up; } } // namespace Net PrismLauncher-10.0.5/launcher/net/ApiHeaderProxy.h0000644000175100017510000000335415144136756021424 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include "Application.h" #include "BuildConfig.h" #include "net/HeaderProxy.h" namespace Net { class ApiHeaderProxy : public HeaderProxy { public: ApiHeaderProxy() : HeaderProxy() {} virtual ~ApiHeaderProxy() = default; public: virtual QList headers(const QNetworkRequest& request) const override { QList hdrs; if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { hdrs.append({ "x-api-key", APPLICATION->getFlameAPIKey().toUtf8() }); } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { QString token = APPLICATION->getModrinthAPIToken(); if (!token.isNull()) hdrs.append({ "Authorization", token.toUtf8() }); } return hdrs; }; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/ByteArraySink.h0000644000175100017510000000626215144136756021270 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "Sink.h" namespace Net { /* * Sink object for downloads that uses an external QByteArray it doesn't own as a target. */ class ByteArraySink : public Sink { public: ByteArraySink(std::shared_ptr output) : m_output(output) {}; virtual ~ByteArraySink() = default; public: auto init(QNetworkRequest& request) -> Task::State override { if (m_output) m_output->clear(); else qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable"; if (initAllValidators(request)) return Task::State::Running; m_fail_reason = "Failed to initialize validators"; return Task::State::Failed; }; auto write(QByteArray& data) -> Task::State override { if (m_output) m_output->append(data); else qWarning() << "ByteArraySink did not write the buffer because it's not addressable"; if (writeAllValidators(data)) return Task::State::Running; m_fail_reason = "Failed to write validators"; return Task::State::Failed; } auto abort() -> Task::State override { failAllValidators(); m_fail_reason = "Aborted"; return Task::State::Failed; } auto finalize(QNetworkReply& reply) -> Task::State override { if (finalizeAllValidators(reply)) return Task::State::Succeeded; m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; } auto hasLocalData() -> bool override { return false; } protected: std::shared_ptr m_output; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/Logging.h0000644000175100017510000000206215144136756020121 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include Q_DECLARE_LOGGING_CATEGORY(taskNetLogC) Q_DECLARE_LOGGING_CATEGORY(taskDownloadLogC) Q_DECLARE_LOGGING_CATEGORY(taskUploadLogC) Q_DECLARE_LOGGING_CATEGORY(taskMCSkinsLogC) Q_DECLARE_LOGGING_CATEGORY(taskMetaCacheLogC) Q_DECLARE_LOGGING_CATEGORY(taskHttpMetaCacheLogC) PrismLauncher-10.0.5/launcher/net/FileSink.h0000644000175100017510000000433115144136756020240 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "PSaveFile.h" #include "Sink.h" namespace Net { class FileSink : public Sink { public: FileSink(QString filename) : m_filename(filename) {}; virtual ~FileSink() = default; public: auto init(QNetworkRequest& request) -> Task::State override; auto write(QByteArray& data) -> Task::State override; auto abort() -> Task::State override; auto finalize(QNetworkReply& reply) -> Task::State override; auto hasLocalData() -> bool override; protected: virtual auto initCache(QNetworkRequest&) -> Task::State; virtual auto finalizeCache(QNetworkReply& reply) -> Task::State; protected: QString m_filename; bool m_wroteAnyData = false; std::unique_ptr m_output_file; }; } // namespace Net PrismLauncher-10.0.5/launcher/net/FileSink.cpp0000644000175100017510000001117015144136756020572 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "FileSink.h" #include "FileSystem.h" #include "net/Logging.h" namespace Net { Task::State FileSink::init(QNetworkRequest& request) { auto result = initCache(request); if (result != Task::State::Running) { return result; } // create a new save file and open it for writing if (!FS::ensureFilePathExists(m_filename)) { qCCritical(taskNetLogC) << "Could not create folder for " + m_filename; m_fail_reason = "Could not create folder"; return Task::State::Failed; } m_wroteAnyData = false; m_output_file.reset(new PSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; m_fail_reason = "Could not open file"; return Task::State::Failed; } if (initAllValidators(request)) return Task::State::Running; m_fail_reason = "Failed to initialize validators"; return Task::State::Failed; } Task::State FileSink::write(QByteArray& data) { if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) { qCCritical(taskNetLogC) << "Failed writing into " + m_filename; m_output_file->cancelWriting(); m_output_file.reset(); m_wroteAnyData = false; m_fail_reason = "Failed to write validators"; return Task::State::Failed; } m_wroteAnyData = true; return Task::State::Running; } Task::State FileSink::abort() { if (m_output_file) { m_output_file->cancelWriting(); } failAllValidators(); return Task::State::Failed; } Task::State FileSink::finalize(QNetworkReply& reply) { bool gotFile = false; QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); bool validStatus = false; int statusCode = statusCodeV.toInt(&validStatus); if (validStatus) { // this leaves out 304 Not Modified gotFile = statusCode == 200 || statusCode == 203; } // if we wrote any data to the save file, we try to commit the data to the real file. // if it actually got a proper file, we write it even if it was empty if (gotFile || m_wroteAnyData) { // ask validators for data consistency // we only do this for actual downloads, not 'your data is still the same' cache hits if (!finalizeAllValidators(reply)) { m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; } // nothing went wrong... if (!m_output_file->commit()) { qCCritical(taskNetLogC) << "Failed to commit changes to" << m_filename; m_output_file->cancelWriting(); m_fail_reason = "Failed to commit changes"; return Task::State::Failed; } } // then get rid of the save file m_output_file.reset(); return finalizeCache(reply); } Task::State FileSink::initCache(QNetworkRequest&) { return Task::State::Running; } Task::State FileSink::finalizeCache(QNetworkReply&) { return Task::State::Succeeded; } bool FileSink::hasLocalData() { QFileInfo info(m_filename); return info.exists() && info.size() != 0; } } // namespace Net PrismLauncher-10.0.5/launcher/net/HeaderProxy.h0000644000175100017510000000245015144136756020766 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include namespace Net { struct HeaderPair { QByteArray headerName; QByteArray headerValue; }; class HeaderProxy { public: HeaderProxy() {} virtual ~HeaderProxy() {} public: virtual QList headers(const QNetworkRequest& request) const = 0; public: void writeHeaders(QNetworkRequest& request) { for (auto header : headers(request)) { request.setRawHeader(header.headerName, header.headerValue); } } }; } // namespace Net PrismLauncher-10.0.5/launcher/net/PasteUpload.h0000644000175100017510000000562215144136756020761 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "net/ByteArraySink.h" #include "net/NetRequest.h" #include "tasks/Task.h" #include #include #include #include #include class PasteUpload : public Net::NetRequest { public: enum PasteType : int { // 0x0.st NullPointer, // hastebin.com Hastebin, // paste.gg PasteGG, // mclo.gs Mclogs, // Helpful to get the range of valid values on the enum for input sanitisation: First = NullPointer, Last = Mclogs }; struct PasteTypeInfo { const QString name; const QString defaultBase; const QString endpointPath; }; static const std::array PasteTypes; class Sink : public Net::ByteArraySink { public: Sink(PasteUpload* p) : Net::ByteArraySink(std::make_shared()), m_d(p) {}; virtual ~Sink() = default; public: auto finalize(QNetworkReply& reply) -> Task::State override; private: PasteUpload* m_d; }; friend Sink; PasteUpload(const QString& log, QString url, PasteType pasteType); virtual ~PasteUpload() = default; QString pasteLink() { return m_pasteLink; } private: virtual QNetworkReply* getReply(QNetworkRequest&) override; QString m_log; QString m_pasteLink; QString m_baseUrl; const PasteType m_paste_type; }; PrismLauncher-10.0.5/launcher/net/NetRequest.cpp0000644000175100017510000003507215144136756021174 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NetRequest.h" #include #include #include #include #include #include #include #if defined(LAUNCHER_APPLICATION) #include "Application.h" #endif #include "BuildConfig.h" #include "MMCTime.h" #include "StringUtils.h" namespace Net { NetRequest::NetRequest() : Task() { connect(&m_retryTimer, &QTimer::timeout, this, &NetRequest::executeTask); } void NetRequest::addValidator(Validator* v) { m_sink->addValidator(v); } void NetRequest::executeTask() { setStatus(tr("Requesting %1").arg(StringUtils::truncateUrlHumanFriendly(m_url, 80))); if (getState() == Task::State::AbortedByUser) { qCWarning(logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString(); emit aborted(); emit finished(); return; } QNetworkRequest request(m_url); m_state = m_sink->init(request); switch (m_state) { case State::Succeeded: qCDebug(logCat) << getUid().toString() << "Request cache hit" << m_url.toString(); emit succeeded(); emit finished(); return; case State::Running: qCDebug(logCat) << getUid().toString() << "Running" << m_url.toString(); break; case State::Inactive: case State::Failed: m_failReason = m_sink->failReason(); emit failed(m_sink->failReason()); emit finished(); return; case State::AbortedByUser: emit aborted(); emit finished(); return; } #if defined(LAUNCHER_APPLICATION) auto user_agent = APPLICATION->getUserAgent(); #else auto user_agent = BuildConfig.USER_AGENT; #endif request.setHeader(QNetworkRequest::UserAgentHeader, user_agent.toUtf8()); for (auto& header_proxy : m_headerProxies) { header_proxy->writeHeaders(request); } #if defined(LAUNCHER_APPLICATION) request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000); #else request.setTransferTimeout(); #endif m_last_progress_time = m_clock.now(); m_last_progress_bytes = 0; auto rep = getReply(request); if (rep == nullptr) // it failed return; m_reply.reset(rep); connect(rep, &QNetworkReply::uploadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished); connect(rep, &QNetworkReply::errorOccurred, this, &NetRequest::downloadError); connect(rep, &QNetworkReply::sslErrors, this, &NetRequest::sslErrors); connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead); } void NetRequest::onProgress(qint64 bytesReceived, qint64 bytesTotal) { auto now = m_clock.now(); auto elapsed = now - m_last_progress_time; // use milliseconds for speed precision auto elapsed_ms = std::chrono::duration_cast(elapsed); auto bytes_received_since = bytesReceived - m_last_progress_bytes; auto dl_speed_bps = (double)bytes_received_since / elapsed_ms.count() * 1000; auto remaining_time_s = (bytesTotal - bytesReceived) / dl_speed_bps; //: Current amount of bytes downloaded, out of the total amount of bytes in the download QString dl_progress = tr("%1 / %2").arg(StringUtils::humanReadableFileSize(bytesReceived)).arg(StringUtils::humanReadableFileSize(bytesTotal)); QString dl_speed_str; if (elapsed_ms.count() > 0) { auto str_eta = bytesTotal > 0 ? Time::humanReadableDuration(remaining_time_s) : tr("unknown"); //: Download speed, in bytes per second (remaining download time in parenthesis) dl_speed_str = tr("%1 /s (%2)").arg(StringUtils::humanReadableFileSize(dl_speed_bps)).arg(str_eta); } else { //: Download speed at 0 bytes per second dl_speed_str = tr("0 B/s"); } setDetails(dl_progress + "\n" + dl_speed_str); setProgress(bytesReceived, bytesTotal); } void NetRequest::downloadError(QNetworkReply::NetworkError error) { if (error == QNetworkReply::OperationCanceledError) { qCCritical(logCat) << getUid().toString() << "Aborted" << m_url.toString(); m_state = State::Failed; } else if (replyStatusCode() == 429 /* HTTP Too Many Requests*/ && m_options & Option::AutoRetry) { qCDebug(logCat) << getUid().toString() << "Rate Limited!"; int64_t delay = 10 * std::pow(2, m_retryCount); if (m_reply->hasRawHeader("Retry-After")) { auto retryAfter = m_reply->rawHeader("Retry-After"); if (retryAfter.trimmed().endsWith("GMT")) /* HTTP Date format */ { auto afterTimestamp = QDateTime::fromString(QString::fromUtf8(retryAfter.trimmed()), "ddd, dd MMM yyyy HH:mm:ss 'GMT'"); auto now = QDateTime::currentDateTime(); delay = now.secsTo(afterTimestamp); } else { delay = retryAfter.toLong(); } } handleAutoRetry(delay); } else { if (m_options & Option::AcceptLocalFiles) { if (m_sink->hasLocalData()) { m_state = State::Succeeded; return; } } // error happened during download. qCCritical(logCat) << getUid().toString() << "Failed" << m_url.toString() << "with error" << error; if (m_reply) qCCritical(logCat) << getUid().toString() << "HTTP status:" << replyStatusCode() << errorString(); if (m_errorResponse.size() > 0) qCCritical(logCat) << getUid().toString() << "Response from server:" << m_errorResponse; m_state = State::Failed; } } void NetRequest::sslErrors(const QList& errors) { int i = 1; for (auto error : errors) { qCCritical(logCat).nospace() << getUid().toString() << " Request " << m_url.toString() << " SSL Error #" << i << ": " << error.errorString(); auto cert = error.certificate(); qCCritical(logCat) << getUid().toString() << "Certificate in question:\n" << cert.toText(); i++; } } auto NetRequest::handleRedirect() -> bool { QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); if (!redirect.isValid()) { if (!m_reply->hasRawHeader("Location")) { // no redirect -> it's fine to continue return false; } // there is a Location header, but it's not correct. we need to apply some workarounds... QByteArray redirectBA = m_reply->rawHeader("Location"); if (redirectBA.size() == 0) { // empty, yet present redirect header? WTF? return false; } QString redirectStr = QString::fromUtf8(redirectBA); if (redirectStr.startsWith("//")) { /* * IF the URL begins with //, we need to insert the URL scheme. * See: https://bugreports.qt.io/browse/QTBUG-41061 * See: http://tools.ietf.org/html/rfc3986#section-4.2 */ redirectStr = m_reply->url().scheme() + ":" + redirectStr; } else if (redirectStr.startsWith("/")) { /* * IF the URL begins with /, we need to process it as a relative URL */ auto url = m_reply->url(); url.setPath(redirectStr, QUrl::TolerantMode); redirectStr = url.toString(); } /* * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. * FIXME: report Qt bug for this */ redirect = QUrl(redirectStr, QUrl::TolerantMode); if (!redirect.isValid()) { qCWarning(logCat) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; downloadError(QNetworkReply::ProtocolFailure); return false; } qCDebug(logCat) << getUid().toString() << "Fixed location header:" << redirect; } else { qCDebug(logCat) << getUid().toString() << "Location header:" << redirect; } m_url = QUrl(redirect.toString()); qCDebug(logCat) << getUid().toString() << "Following redirect to" << m_url.toString(); executeTask(); return true; } void NetRequest::handleAutoRetry(int64_t delay) { m_retryCount++; if (delay > 60 || m_retryCount > 4) { /* 1 minute is too long to wait for retry, fail for now */ m_state = State::Failed; auto retryAfter = QDateTime::currentDateTime().addSecs(delay); emitFailed(tr("Request Rate Limited for %n second(s): Retry After %1", "seconds", delay) .arg(retryAfter.toLocalTime().toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)))); return; } else { qCDebug(logCat) << getUid().toString() << "Retyring Request in" << delay << "seconds"; setStatus(tr("Rate Limited: Waiting %n second(s)", "seconds", delay)); m_retryTimer.setTimerType(Qt::VeryCoarseTimer); m_retryTimer.setSingleShot(true); m_retryTimer.setInterval(delay * 1000); m_retryTimer.start(); } } void NetRequest::downloadFinished() { // currently waiting for retry if (m_retryTimer.isActive()) { return; } // handle HTTP redirection first if (handleRedirect()) { qCDebug(logCat) << getUid().toString() << "Request redirected:" << m_url.toString(); return; } // if the download failed before this point ... if (m_state == State::Succeeded) // pretend to succeed so we continue processing :) { qCDebug(logCat) << getUid().toString() << "Request failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); emit succeeded(); emit finished(); return; } else if (m_state == State::Failed) { qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString(); m_sink->abort(); m_failReason = m_reply->errorString(); emit failed(m_reply->errorString()); emit finished(); return; } else if (m_state == State::AbortedByUser) { qCDebug(logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString(); m_sink->abort(); emit aborted(); emit finished(); return; } // make sure we got all the remaining data, if any auto data = m_reply->readAll(); if (data.size()) { qCDebug(logCat) << getUid().toString() << "Writing extra" << data.size() << "bytes"; m_state = m_sink->write(data); if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString(); m_sink->abort(); m_failReason = m_sink->failReason(); emit failed(m_sink->failReason()); emit finished(); return; } } // otherwise, finalize the whole graph m_state = m_sink->finalize(*m_reply.get()); if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString(); m_sink->abort(); m_failReason = m_sink->failReason(); emit failed(m_sink->failReason()); emit finished(); return; } qCDebug(logCat) << getUid().toString() << "Request succeeded:" << m_url.toString(); emit succeeded(); emit finished(); } void NetRequest::downloadReadyRead() { if (m_state == State::Running) { auto data = m_reply->readAll(); m_state = m_sink->write(data); if (replyStatusCode() >= 400) { m_errorResponse.append(data); } if (m_state == State::Failed) { qCCritical(logCat) << getUid().toString() << "Failed to process response chunk:" << m_sink->failReason(); } // qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes"; } else { qCCritical(logCat) << getUid().toString() << "Cannot write download data! illegal status" << m_status; } } auto NetRequest::abort() -> bool { m_state = State::AbortedByUser; if (m_reply) { disconnect(m_reply.get(), &QNetworkReply::errorOccurred, nullptr, nullptr); m_reply->abort(); } return true; } int NetRequest::replyStatusCode() const { return m_reply ? m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() : -1; } QNetworkReply::NetworkError NetRequest::error() const { return m_reply ? m_reply->error() : QNetworkReply::NoError; } QUrl NetRequest::url() const { return m_url; } QString NetRequest::errorString() const { return m_reply ? m_reply->errorString() : ""; } void NetRequest::enableAutoRetry(bool enable) { if (enable) { m_options |= Option::AutoRetry; } else { m_options &= ~static_cast(Option::AutoRetry); } } } // namespace Net PrismLauncher-10.0.5/launcher/net/Mode.h0000644000175100017510000000010515144136756017413 0ustar runnerrunner#pragma once namespace Net { enum class Mode { Offline, Online }; } PrismLauncher-10.0.5/launcher/net/Logging.cpp0000644000175100017510000000224415144136756020456 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "net/Logging.h" Q_LOGGING_CATEGORY(taskNetLogC, "launcher.task.net") Q_LOGGING_CATEGORY(taskDownloadLogC, "launcher.task.net.download") Q_LOGGING_CATEGORY(taskUploadLogC, "launcher.task.net.upload") Q_LOGGING_CATEGORY(taskMCSkinsLogC, "launcher.task.minecraft.skins") Q_LOGGING_CATEGORY(taskMetaCacheLogC, "launcher.task.net.metacache") Q_LOGGING_CATEGORY(taskHttpMetaCacheLogC, "launcher.task.net.metacache.http") PrismLauncher-10.0.5/launcher/net/PasteUpload.cpp0000644000175100017510000002350615144136756021315 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington * Copyright (C) 2022 Swirl * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PasteUpload.h" #include #include #include #include #include #include #include "logs/AnonymizeLog.h" const std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" }, { "hastebin", "https://hst.sh", "/documents" }, { "paste.gg", "https://paste.gg", "/api/v1/pastes" }, { "mclo.gs", "https://api.mclo.gs", "/1/log" } } }; QNetworkReply* PasteUpload::getReply(QNetworkRequest& request) { switch (m_paste_type) { case PasteUpload::NullPointer: { QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType, this }; QHttpPart filePart; filePart.setBody(m_log.toUtf8()); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); multiPart->append(filePart); return m_network->post(request, multiPart); } case PasteUpload::Hastebin: { return m_network->post(request, m_log.toUtf8()); } case PasteUpload::Mclogs: { QUrlQuery postData; postData.addQueryItem("content", m_log); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); return m_network->post(request, postData.toString().toUtf8()); } case PasteUpload::PasteGG: { QJsonObject obj; QJsonDocument doc; request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); obj.insert("expires", QDateTime::currentDateTimeUtc().addDays(100).toString(Qt::DateFormat::ISODate)); QJsonArray files; QJsonObject logFileInfo; QJsonObject logFileContentInfo; logFileContentInfo.insert("format", "text"); logFileContentInfo.insert("value", m_log); logFileInfo.insert("name", "log.txt"); logFileInfo.insert("content", logFileContentInfo); files.append(logFileInfo); obj.insert("files", files); doc.setObject(obj); return m_network->post(request, doc.toJson()); } } return nullptr; }; auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State { if (!finalizeAllValidators(reply)) { m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; } int statusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply.error() != QNetworkReply::NetworkError::NoError) { m_fail_reason = QObject::tr("Network error: %1").arg(reply.errorString()); return Task::State::Failed; } else if (statusCode != 200 && statusCode != 201) { QString reasonPhrase = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); m_fail_reason = QObject::tr("Error: %1 returned unexpected status code %2 %3").arg(m_d->url().toString()).arg(statusCode).arg(reasonPhrase); return Task::State::Failed; } switch (m_d->m_paste_type) { case PasteUpload::NullPointer: m_d->m_pasteLink = QString::fromUtf8(*m_output).trimmed(); break; case PasteUpload::Hastebin: { QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(*m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString(); m_fail_reason = QObject::tr("Failed to parse response from hastebin server: expected JSON but got an invalid response. Error: %1") .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("key") && obj["key"].isString()) { QString key = doc.object()["key"].toString(); m_d->m_pasteLink = m_d->m_baseUrl + "/" + key; } else { qDebug() << "Log upload failed:" << doc.toJson(); m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; } case PasteUpload::Mclogs: { QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(*m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString(); m_fail_reason = QObject::tr("Failed to parse response from mclogs server: expected JSON but got an invalid response. Error: %1") .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("success") && obj["success"].isBool()) { bool success = obj["success"].toBool(); if (success) { m_d->m_pasteLink = obj["url"].toString(); } else { QString error = obj["error"].toString(); m_fail_reason = QObject::tr("Error: %1 returned an error: %2").arg(m_d->url().toString(), error); return Task::State::Failed; } } else { qDebug() << "Log upload failed:" << doc.toJson(); m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; } case PasteUpload::PasteGG: QJsonParseError jsonError; auto doc = QJsonDocument::fromJson(*m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString(); m_fail_reason = QObject::tr("Failed to parse response from pasteGG server: expected JSON but got an invalid response. Error: %1") .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("status") && obj["status"].isString()) { QString status = obj["status"].toString(); if (status == "success") { m_d->m_pasteLink = m_d->m_baseUrl + "/p/anonymous/" + obj["result"].toObject()["id"].toString(); } else { QString error = obj["error"].toString(); QString message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none"; m_fail_reason = QObject::tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_d->url().toString(), error, message); return Task::State::Failed; } } else { qDebug() << "Log upload failed:" << doc.toJson(); m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; } return Task::State::Succeeded; } PasteUpload::PasteUpload(const QString& log, QString url, PasteType pasteType) : m_log(log), m_baseUrl(url), m_paste_type(pasteType) { anonymizeLog(m_log); auto base = PasteUpload::PasteTypes.at(pasteType); if (m_baseUrl.isEmpty()) m_baseUrl = base.defaultBase; // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that?? if (pasteType == PasteUpload::PasteGG && m_baseUrl == base.defaultBase) m_url = "https://api.paste.gg/v1/pastes"; else m_url = m_baseUrl + base.endpointPath; m_sink.reset(new Sink(this)); } PrismLauncher-10.0.5/launcher/net/NetJob.cpp0000644000175100017510000001373315144136756020256 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NetJob.h" #include #include "net/NetRequest.h" #include "tasks/ConcurrentTask.h" #if defined(LAUNCHER_APPLICATION) #include "Application.h" #include "ui/dialogs/CustomMessageBox.h" #endif NetJob::NetJob(QString job_name, shared_qobject_ptr network, int max_concurrent) : ConcurrentTask(job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) if (APPLICATION_DYN && max_concurrent < 0) max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt(); #endif if (max_concurrent > 0) setMaxConcurrent(max_concurrent); } auto NetJob::addNetAction(Net::NetRequest::Ptr action) -> bool { action->setNetwork(m_network); addTask(action); return true; } void NetJob::executeNextSubTask() { // We're finished, check for failures and retry if we can (up to 3 times) if (isRunning() && m_queue.isEmpty() && m_doing.isEmpty() && !m_failed.isEmpty() && m_try < 3) { m_try += 1; while (!m_failed.isEmpty()) { auto task = m_failed.take(*m_failed.keyBegin()); m_done.remove(task.get()); m_queue.enqueue(task); } } ConcurrentTask::executeNextSubTask(); } auto NetJob::size() const -> int { return m_queue.size() + m_doing.size() + m_done.size(); } auto NetJob::canAbort() const -> bool { bool canFullyAbort = true; // can abort the downloads on the queue? for (auto part : m_queue) canFullyAbort &= part->canAbort(); // can abort the active downloads? for (auto part : m_doing) canFullyAbort &= part->canAbort(); return canFullyAbort; } auto NetJob::abort() -> bool { bool fullyAborted = true; // fail all downloads on the queue for (auto task : m_queue) m_failed.insert(task.get(), task); m_queue.clear(); // abort active downloads auto toKill = m_doing.values(); for (auto part : toKill) { fullyAborted &= part->abort(); } if (fullyAborted) emitAborted(); else emitFailed(tr("Failed to abort all tasks in the NetJob!")); return fullyAborted; } auto NetJob::getFailedActions() -> QList { QList failed; for (auto index : m_failed) { failed.push_back(dynamic_cast(index.get())); } return failed; } auto NetJob::getFailedFiles() -> QList { QList failed; for (auto index : m_failed) { failed.append(static_cast(index.get())->url().toString()); } return failed; } void NetJob::updateState() { emit progress(m_done.count(), totalSize()); setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } bool NetJob::isOnline() { // check some errors that are ussually associated with the lack of internet for (auto job : getFailedActions()) { auto err = job->error(); if (err != QNetworkReply::HostNotFoundError && err != QNetworkReply::NetworkSessionFailedError) { return true; } } return false; }; void NetJob::emitFailed(QString reason) { #if defined(LAUNCHER_APPLICATION) if (APPLICATION_DYN && m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) { m_manual_try++; auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", "The tasks failed.\n" "Failed urls\n" + getFailedFiles().join("\n\t") + ".\n" "If this continues to happen please check the logs of the application.\n" "Do you want to retry?", QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response == QMessageBox::Yes) { m_try = 0; executeNextSubTask(); return; } } #endif ConcurrentTask::emitFailed(reason); } void NetJob::setAskRetry(bool askRetry) { m_ask_retry = askRetry; } PrismLauncher-10.0.5/launcher/net/HttpMetaCache.cpp0000644000175100017510000002534215144136756021546 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "HttpMetaCache.h" #include "FileSystem.h" #include "Json.h" #include #include #include #include #include #include "net/Logging.h" auto MetaEntry::getFullPath() -> QString { // FIXME: make local? return FS::PathCombine(m_basePath, m_relativePath); } HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path) { saveBatchingTimer.setSingleShot(true); saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); connect(&saveBatchingTimer, &QTimer::timeout, this, &HttpMetaCache::SaveNow); } HttpMetaCache::~HttpMetaCache() { saveBatchingTimer.stop(); SaveNow(); } auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr { // no base. no base path. can't store if (!m_entries.contains(base)) { // TODO: log problem return {}; } EntryMap& map = m_entries[base]; if (map.entry_list.contains(resource_path)) { return map.entry_list[resource_path]; } return {}; } auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr { resource_path = FS::RemoveInvalidPathChars(resource_path); auto entry = getEntry(base, resource_path); // it's not present? generate a default stale entry if (!entry) { return staleEntry(base, resource_path); } auto& selected_base = m_entries[base]; QString real_path = FS::PathCombine(selected_base.base_path, resource_path); QFileInfo finfo(real_path); // is the file really there? if not -> stale if (!finfo.isFile() || !finfo.isReadable()) { // if the file doesn't exist, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } if (!expected_etag.isEmpty() && expected_etag != entry->m_etag) { // if the etag doesn't match expected, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // if the file changed, check md5sum qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); if (file_last_changed != entry->m_local_changed_timestamp) { QFile input(real_path); if (!input.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file '" << input.fileName() << "' for reading!"; return staleEntry(base, resource_path); } QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); if (entry->m_md5sum != md5sum) { selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // md5sums matched... keep entry and save the new state to file entry->m_local_changed_timestamp = file_last_changed; SaveEventually(); } // Get rid of old entries, to prevent cache problems auto current_time = QDateTime::currentSecsSinceEpoch(); if (entry->isExpired(current_time - (file_last_changed / 1000))) { qCWarning(taskNetLogC) << "[HttpMetaCache]" << "Removing cache entry because of old age!"; selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // entry passed all the checks we cared about. entry->m_basePath = getBasePath(base); return entry; } auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool { if (!m_entries.contains(stale_entry->m_baseId)) { qCCritical(taskHttpMetaCacheLogC) << "Cannot add entry with unknown base:" << stale_entry->m_baseId.toLocal8Bit(); return false; } if (stale_entry->m_stale) { qCCritical(taskHttpMetaCacheLogC) << "Cannot add stale entry:" << stale_entry->getFullPath().toLocal8Bit(); return false; } m_entries[stale_entry->m_baseId].entry_list[stale_entry->m_relativePath] = stale_entry; SaveEventually(); return true; } auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool { if (!entry) return false; entry->m_stale = true; SaveEventually(); return true; } // returns true on success, false otherwise auto HttpMetaCache::evictAll() -> bool { bool ret = true; for (QString& base : m_entries.keys()) { EntryMap& map = m_entries[base]; qCDebug(taskHttpMetaCacheLogC) << "Evicting base" << base; for (MetaEntryPtr entry : map.entry_list) { if (!evictEntry(entry)) qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; } map.entry_list.clear(); // AND all return codes together so the result is true iff all runs of deletePath() are true ret &= FS::deletePath(map.base_path); } return ret; } auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr { auto foo = new MetaEntry(); foo->m_baseId = base; foo->m_basePath = getBasePath(base); foo->m_relativePath = resource_path; foo->m_stale = true; return MetaEntryPtr(foo); } void HttpMetaCache::addBase(QString base, QString base_root) { // TODO: report error if (m_entries.contains(base)) return; // TODO: check if the base path is valid EntryMap foo; foo.base_path = base_root; m_entries[base] = foo; } auto HttpMetaCache::getBasePath(QString base) -> QString { if (m_entries.contains(base)) { return m_entries[base].base_path; } return {}; } void HttpMetaCache::Load() { if (m_index_file.isNull()) return; QFile index(m_index_file); if (!index.open(QIODevice::ReadOnly)) return; QJsonParseError parseError; QJsonDocument json = QJsonDocument::fromJson(index.readAll(), &parseError); // Fail if the JSON is invalid. if (parseError.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse HttpMetaCache file: %1 at offset %2") .arg(parseError.errorString(), QString::number(parseError.offset)) .toUtf8(); return; } // Make sure the root is an object. if (!json.isObject()) { qCritical() << "HttpMetaCache root should be an object."; return; } auto root = json.object(); // check file version first auto version_val = root["version"].toString(); if (version_val != "1") return; // read the entry array auto array = root["entries"].toArray(); for (auto element : array) { auto element_obj = element.toObject(); auto base = element_obj["base"].toString(); if (!m_entries.contains(base)) continue; auto& entrymap = m_entries[base]; auto foo = new MetaEntry(); foo->m_baseId = base; foo->m_relativePath = element_obj["path"].toString(); foo->m_md5sum = element_obj["md5sum"].toString(); foo->m_etag = element_obj["etag"].toString(); foo->m_local_changed_timestamp = element_obj["last_changed_timestamp"].toDouble(); foo->m_remote_changed_timestamp = element_obj["remote_changed_timestamp"].toString(); foo->makeEternal(element_obj[QStringLiteral("eternal")].toBool()); if (!foo->isEternal()) { foo->m_current_age = element_obj["current_age"].toDouble(); foo->m_max_age = element_obj["max_age"].toDouble(); } // presumed innocent until closer examination foo->m_stale = false; entrymap.entry_list[foo->m_relativePath] = MetaEntryPtr(foo); } } void HttpMetaCache::SaveEventually() { // reset the save timer saveBatchingTimer.stop(); saveBatchingTimer.start(30000); } void HttpMetaCache::SaveNow() { if (m_index_file.isNull()) return; qCDebug(taskHttpMetaCacheLogC) << "Saving metacache with" << m_entries.size() << "entries"; QJsonObject toplevel; Json::writeString(toplevel, "version", "1"); QJsonArray entriesArr; for (auto group : m_entries) { for (auto entry : group.entry_list) { // do not save stale entries. they are dead. if (entry->m_stale) { continue; } QJsonObject entryObj; Json::writeString(entryObj, "base", entry->m_baseId); Json::writeString(entryObj, "path", entry->m_relativePath); Json::writeString(entryObj, "md5sum", entry->m_md5sum); Json::writeString(entryObj, "etag", entry->m_etag); entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->m_local_changed_timestamp))); if (!entry->m_remote_changed_timestamp.isEmpty()) entryObj.insert("remote_changed_timestamp", QJsonValue(entry->m_remote_changed_timestamp)); if (entry->isEternal()) { entryObj.insert("eternal", true); } else { entryObj.insert("current_age", QJsonValue(double(entry->m_current_age))); entryObj.insert("max_age", QJsonValue(double(entry->m_max_age))); } entriesArr.append(entryObj); } } toplevel.insert("entries", entriesArr); try { Json::write(toplevel, m_index_file); } catch (const Exception& e) { qCWarning(taskHttpMetaCacheLogC) << "Error writing cache:" << e.what(); } } PrismLauncher-10.0.5/launcher/WatchLock.h0000644000175100017510000000056315144136756017630 0ustar runnerrunner #pragma once #include #include struct WatchLock { WatchLock(QFileSystemWatcher* watcher, const QString& directory) : m_watcher(watcher), m_directory(directory) { m_watcher->removePath(m_directory); } ~WatchLock() { m_watcher->addPath(m_directory); } QFileSystemWatcher* m_watcher; QString m_directory; }; PrismLauncher-10.0.5/launcher/JavaCommon.cpp0000644000175100017510000001414515144136756020337 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "JavaCommon.h" #include "java/JavaUtils.h" #include "ui/dialogs/CustomMessageBox.h" #include bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) { static const QRegularExpression s_memRegex("-Xm[sx]"); static const QRegularExpression s_versionRegex("-version:.*"); if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize")) { auto warnStr = QObject::tr( "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" " "or \"-Xms\").\n" "There are dedicated boxes for these in the settings (Java tab, in the Memory group at the top).\n" "This message will be displayed until you remove them from the JVM arguments."); CustomMessageBox::selectable(parent, QObject::tr("JVM arguments warning"), warnStr, QMessageBox::Warning)->exec(); return false; } // block lunacy with passing required version to the JVM if (jvmargs.contains(s_versionRegex)) { auto warnStr = QObject::tr( "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be " "allowed.\n" "This message will be displayed until you remove this from the JVM arguments."); CustomMessageBox::selectable(parent, QObject::tr("JVM arguments warning"), warnStr, QMessageBox::Warning)->exec(); return false; } return true; } void JavaCommon::javaWasOk(QWidget* parent, const JavaChecker::Result& result) { QString text; text += QObject::tr( "Java test succeeded!
Platform reported: %1
Java version " "reported: %2
Java vendor " "reported: %3
") .arg(result.realPlatform, result.javaVersion.toString(), result.javaVendor); if (result.errorLog.size()) { auto htmlError = result.errorLog; htmlError.replace('\n', "
"); text += QObject::tr("
Warnings:
%1").arg(htmlError); } CustomMessageBox::selectable(parent, QObject::tr("Java test success"), text, QMessageBox::Information)->show(); } void JavaCommon::javaArgsWereBad(QWidget* parent, const JavaChecker::Result& result) { auto htmlError = result.errorLog; QString text; htmlError.replace('\n', "
"); text += QObject::tr("The specified Java binary didn't work with the arguments you provided:
"); text += QString("%1").arg(htmlError); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& result) { QString text; text += QObject::tr( "The specified Java binary didn't work.
You should press 'Detect', " "or set the path to the Java executable.
"); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } void JavaCommon::javaCheckNotFound(QWidget* parent) { QString text; text += QObject::tr("Java checker library could not be found. Please check your installation."); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } void JavaCommon::TestCheck::run() { if (!JavaCommon::checkJVMArgs(m_args, m_parent)) { emit finished(); return; } if (JavaUtils::getJavaCheckPath().isEmpty()) { javaCheckNotFound(m_parent); emit finished(); return; } checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished); checker->start(); } void JavaCommon::TestCheck::checkFinished(const JavaChecker::Result& result) { if (result.validity != JavaChecker::Result::Validity::Valid) { javaBinaryWasBad(m_parent, result); emit finished(); return; } checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs); checker->start(); } void JavaCommon::TestCheck::checkFinishedWithArgs(const JavaChecker::Result& result) { if (result.validity == JavaChecker::Result::Validity::Valid) { javaWasOk(m_parent, result); emit finished(); return; } javaArgsWereBad(m_parent, result); emit finished(); } PrismLauncher-10.0.5/launcher/PSaveFile.h0000644000175100017510000000477115144136756017574 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "Application.h" #if defined(LAUNCHER_APPLICATION) /* PSaveFile * A class that mimics QSaveFile for Windows. * * When reading resources, we need to avoid accessing temporary files * generated by QSaveFile. If we start reading such a file, we may * inadvertently keep it open while QSaveFile is trying to remove it, * or we might detect the file just before it is removed, leading to * race conditions and errors. * * Unfortunately, QSaveFile doesn't provide a way to retrieve the * temporary file name or to set a specific template for the temporary * file name it uses. By default, QSaveFile appends a `.XXXXXX` suffix * to the original file name, where the `XXXXXX` part is dynamically * generated to ensure uniqueness. * * This class acts like a lock by adding and removing the target file * name into/from a global string set, helping to manage access to * files during critical operations. * * Note: Please do not use the `setFileName` function directly, as it * is not virtual and cannot be overridden. */ class PSaveFile : public QSaveFile { public: PSaveFile(const QString& name) : QSaveFile(name) { addPath(name); } PSaveFile(const QString& name, QObject* parent) : QSaveFile(name, parent) { addPath(name); } virtual ~PSaveFile() { if (auto app = APPLICATION_DYN) { app->removeQSavePath(m_absoluteFilePath); } } private: void addPath(const QString& path) { m_absoluteFilePath = QFileInfo(path).absoluteFilePath() + "."; // add dot for tmp files only if (auto app = APPLICATION_DYN) { app->addQSavePath(m_absoluteFilePath); } } QString m_absoluteFilePath; }; #else #define PSaveFile QSaveFile #endif PrismLauncher-10.0.5/launcher/RecursiveFileSystemWatcher.h0000644000175100017510000000217115144136756023240 0ustar runnerrunner#pragma once #include #include #include "Filter.h" class RecursiveFileSystemWatcher : public QObject { Q_OBJECT public: RecursiveFileSystemWatcher(QObject* parent); void setRootDir(const QDir& root); QDir rootDir() const { return m_root; } // WARNING: setting this to true may be bad for performance void setWatchFiles(bool watchFiles); bool watchFiles() const { return m_watchFiles; } void setMatcher(Filter matcher) { m_matcher = std::move(matcher); } QStringList files() const { return m_files; } signals: void filesChanged(); void fileChanged(const QString& path); public slots: void enable(); void disable(); private: QDir m_root; bool m_watchFiles = false; bool m_isEnabled = false; Filter m_matcher; QFileSystemWatcher* m_watcher; QStringList m_files; void setFiles(const QStringList& files); void addFilesToWatcherRecursive(const QDir& dir); QStringList scanRecursive(const QDir& dir); private slots: void fileChange(const QString& path); void directoryChange(const QString& path); }; PrismLauncher-10.0.5/launcher/DataMigrationTask.cpp0000644000175100017510000000572615144136756021660 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only #include "DataMigrationTask.h" #include "FileSystem.h" #include #include #include #include DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathMatcher) : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) { m_copy.matcher(m_pathMatcher).whitelist(true); } void DataMigrationTask::executeTask() { setStatus(tr("Scanning files...")); // 1. Scan // Check how many files we gotta copy m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { return m_copy(true); // dry run to collect amount of files }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); m_copyFutureWatcher.setFuture(m_copyFuture); } void DataMigrationTask::dryRunFinished() { disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); if (!m_copyFuture.isValid() || !m_copyFuture.result()) { emitFailed(tr("Failed to scan source path.")); return; } // 2. Copy // Actually copy all files now. m_toCopy = m_copy.totalCopied(); connect(&m_copy, &FS::copy::fileCopied, [&, this](const QString& relativeName) { QString shortenedName = relativeName; // shorten the filename to hopefully fit into one line if (shortenedName.length() > 50) shortenedName = relativeName.left(20) + "…" + relativeName.right(29); setProgress(m_copy.totalCopied(), m_toCopy); setStatus(tr("Copying %1…").arg(shortenedName)); }); m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { return m_copy(false); // actually copy now }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); m_copyFutureWatcher.setFuture(m_copyFuture); } void DataMigrationTask::dryRunAborted() { emitFailed(tr("Aborted")); } void DataMigrationTask::copyFinished() { disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); if (!m_copyFuture.isValid() || !m_copyFuture.result()) { emitFailed(tr("Some paths could not be copied!")); return; } emitSucceeded(); } void DataMigrationTask::copyAborted() { emitFailed(tr("Aborted")); } PrismLauncher-10.0.5/launcher/InstanceCopyPrefs.h0000644000175100017510000000332215144136756021344 0ustar runnerrunner// // Created by marcelohdez on 10/22/22. // #pragma once #include struct InstanceCopyPrefs { public: bool allTrue() const; QString getSelectedFiltersAsRegex() const; QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; // Getters bool isCopySavesEnabled() const; bool isKeepPlaytimeEnabled() const; bool isCopyGameOptionsEnabled() const; bool isCopyResourcePacksEnabled() const; bool isCopyShaderPacksEnabled() const; bool isCopyServersEnabled() const; bool isCopyModsEnabled() const; bool isCopyScreenshotsEnabled() const; bool isUseSymLinksEnabled() const; bool isLinkRecursivelyEnabled() const; bool isUseHardLinksEnabled() const; bool isDontLinkSavesEnabled() const; bool isUseCloneEnabled() const; // Setters void enableCopySaves(bool b); void enableKeepPlaytime(bool b); void enableCopyGameOptions(bool b); void enableCopyResourcePacks(bool b); void enableCopyShaderPacks(bool b); void enableCopyServers(bool b); void enableCopyMods(bool b); void enableCopyScreenshots(bool b); void enableUseSymLinks(bool b); void enableLinkRecursively(bool b); void enableUseHardLinks(bool b); void enableDontLinkSaves(bool b); void enableUseClone(bool b); protected: // data bool copySaves = true; bool keepPlaytime = true; bool copyGameOptions = true; bool copyResourcePacks = true; bool copyShaderPacks = true; bool copyServers = true; bool copyMods = true; bool copyScreenshots = true; bool useSymLinks = false; bool linkRecursively = false; bool useHardLinks = false; bool dontLinkSaves = false; bool useClone = false; }; PrismLauncher-10.0.5/launcher/screenshots/0000755000175100017510000000000015144136756020134 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/screenshots/ImgurAlbumCreation.h0000644000175100017510000000474315144136756024046 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "Screenshot.h" #include "net/NetRequest.h" class ImgurAlbumCreation : public Net::NetRequest { public: virtual ~ImgurAlbumCreation() = default; struct Result { QString deleteHash; QString id; }; class Sink : public Net::Sink { public: Sink(std::shared_ptr res) : m_result(res) {}; virtual ~Sink() = default; public: auto init(QNetworkRequest& request) -> Task::State override; auto write(QByteArray& data) -> Task::State override; auto abort() -> Task::State override; auto finalize(QNetworkReply& reply) -> Task::State override; auto hasLocalData() -> bool override { return false; } private: std::shared_ptr m_result; QByteArray m_output; }; static NetRequest::Ptr make(std::shared_ptr output, QList screenshots); QNetworkReply* getReply(QNetworkRequest& request) override; private: QList m_screenshots; }; PrismLauncher-10.0.5/launcher/screenshots/ImgurAlbumCreation.cpp0000644000175100017510000000765015144136756024401 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ImgurAlbumCreation.h" #include #include #include #include #include #include #include #include #include "BuildConfig.h" #include "net/RawHeaderProxy.h" Net::NetRequest::Ptr ImgurAlbumCreation::make(std::shared_ptr output, QList screenshots) { auto up = makeShared(); up->m_url = BuildConfig.IMGUR_BASE_URL + "album"; up->m_sink.reset(new Sink(output)); up->m_screenshots = screenshots; up->addHeaderProxy(new Net::RawHeaderProxy( QList{ { "Content-Type", "application/x-www-form-urlencoded" }, { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } })); return up; } QNetworkReply* ImgurAlbumCreation::getReply(QNetworkRequest& request) { QStringList hashes; for (auto shot : m_screenshots) { hashes.append(shot->m_imgurDeleteHash); } const QByteArray data = "deletehashes=" + hashes.join(',').toUtf8() + "&title=Minecraft%20Screenshots&privacy=hidden"; return m_network->post(request, data); } auto ImgurAlbumCreation::Sink::init(QNetworkRequest& request) -> Task::State { m_output.clear(); return Task::State::Running; } auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State { m_output.append(data); return Task::State::Running; } auto ImgurAlbumCreation::Sink::abort() -> Task::State { m_output.clear(); m_fail_reason = "Aborted"; return Task::State::Failed; } auto ImgurAlbumCreation::Sink::finalize(QNetworkReply&) -> Task::State { QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << jsonError.errorString(); m_fail_reason = "Invalid json reply"; return Task::State::Failed; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << doc.toJson(); m_fail_reason = "Failed to create album"; return Task::State::Failed; } m_result->deleteHash = object.value("data").toObject().value("deletehash").toString(); m_result->id = object.value("data").toObject().value("id").toString(); return Task::State::Succeeded; } PrismLauncher-10.0.5/launcher/screenshots/Screenshot.h0000644000175100017510000000046415144136756022426 0ustar runnerrunner#pragma once #include #include #include #include struct ScreenShot { using Ptr = std::shared_ptr; ScreenShot(QFileInfo file) { m_file = file; } QFileInfo m_file; QString m_url; QString m_imgurId; QString m_imgurDeleteHash; }; PrismLauncher-10.0.5/launcher/screenshots/ImgurUpload.h0000644000175100017510000000461315144136756022541 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "Screenshot.h" #include "net/NetRequest.h" class ImgurUpload : public Net::NetRequest { public: class Sink : public Net::Sink { public: Sink(ScreenShot::Ptr shot) : m_shot(shot) {}; virtual ~Sink() = default; public: auto init(QNetworkRequest& request) -> Task::State override; auto write(QByteArray& data) -> Task::State override; auto abort() -> Task::State override; auto finalize(QNetworkReply& reply) -> Task::State override; auto hasLocalData() -> bool override { return false; } private: ScreenShot::Ptr m_shot; QByteArray m_output; }; ImgurUpload(QFileInfo info) : m_fileInfo(info) {} virtual ~ImgurUpload() = default; static NetRequest::Ptr make(ScreenShot::Ptr m_shot); private: virtual QNetworkReply* getReply(QNetworkRequest&) override; const QFileInfo m_fileInfo; }; PrismLauncher-10.0.5/launcher/screenshots/ImgurUpload.cpp0000644000175100017510000001110715144136756023070 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ImgurUpload.h" #include "BuildConfig.h" #include "net/RawHeaderProxy.h" #include #include #include #include #include #include #include #include QNetworkReply* ImgurUpload::getReply(QNetworkRequest& request) { auto file = new QFile(m_fileInfo.absoluteFilePath(), this); if (!file->open(QFile::ReadOnly)) { emitFailed(); return nullptr; } QHttpMultiPart* multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); file->setParent(multipart); QHttpPart filePart; filePart.setBodyDevice(file); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\"; filename=\"" + file->fileName() + "\""); multipart->append(filePart); QHttpPart typePart; typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\""); typePart.setBody("file"); multipart->append(typePart); QHttpPart namePart; namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"title\""); namePart.setBody(m_fileInfo.baseName().toUtf8()); multipart->append(namePart); return m_network->post(request, multipart); } auto ImgurUpload::Sink::init(QNetworkRequest& request) -> Task::State { m_output.clear(); return Task::State::Running; } auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State { m_output.append(data); return Task::State::Running; } auto ImgurUpload::Sink::abort() -> Task::State { m_output.clear(); m_fail_reason = "Aborted"; return Task::State::Failed; } auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State { QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); m_fail_reason = "Invalid json reply"; return Task::State::Failed; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << "Screenshot upload not successful:" << doc.toJson(); m_fail_reason = "Screenshot was not uploaded successfully"; return Task::State::Failed; } m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); m_shot->m_url = object.value("data").toObject().value("link").toString(); m_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").toString(); return Task::State::Succeeded; } Net::NetRequest::Ptr ImgurUpload::make(ScreenShot::Ptr m_shot) { auto up = makeShared(m_shot->m_file); up->m_url = std::move(BuildConfig.IMGUR_BASE_URL + "image"); up->m_sink.reset(new Sink(m_shot)); up->addHeaderProxy(new Net::RawHeaderProxy(QList{ { "Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toUtf8() }, { "Accept", "application/json" } })); return up; } PrismLauncher-10.0.5/launcher/Commandline.cpp0000644000175100017510000000522315144136756020530 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Commandline.h" /** * @file libutil/src/cmdutils.cpp */ namespace Commandline { // commandline splitter QStringList splitArgs(QString args) { QStringList argv; QString current; bool escape = false; QChar inquotes; for (int i = 0; i < args.length(); i++) { QChar cchar = args.at(i); // \ escaped if (escape) { current += cchar; escape = false; // in "quotes" } else if (!inquotes.isNull()) { if (cchar == '\\') escape = true; else if (cchar == inquotes) inquotes = QChar::Null; else current += cchar; // otherwise } else { if (cchar == ' ') { if (!current.isEmpty()) { argv << current; current.clear(); } } else if (cchar == '"' || cchar == '\'') inquotes = cchar; else current += cchar; } } if (!current.isEmpty()) argv << current; return argv; } } // namespace Commandline PrismLauncher-10.0.5/launcher/ui/0000755000175100017510000000000015144136757016212 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/MainWindow.cpp0000644000175100017510000017250315144136756021001 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Andrew Okin * Peterix * Orochimarufan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Application.h" #include "BuildConfig.h" #include "FileSystem.h" #include "MainWindow.h" #include "ui_MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "InstanceWindow.h" #include "ui/GuiUtil.h" #include "ui/ViewLogWindow.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CreateShortcutDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportPackDialog.h" #include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" #include "ui/instanceview/InstanceView.h" #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" #include "ui/widgets/LabeledToolButton.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFile.h" #include "minecraft/WorldList.h" #include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ResourcePackFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" #include "minecraft/mod/TexturePackFolderModel.h" #include "minecraft/mod/tasks/LocalResourceParse.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "KonamiCode.h" #include "InstanceCopyTask.h" #include "InstanceDirUpdate.h" #include "Json.h" #include "MMCTime.h" namespace { QString profileInUseFilter(const QString& profile, bool used) { if (used) { return QObject::tr("%1 (in use)").arg(profile); } else { return profile; } } } // namespace MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); setWindowIcon(APPLICATION->logo()); setWindowTitle(APPLICATION->applicationDisplayName()); #ifndef QT_NO_ACCESSIBILITY setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME); #endif // instance toolbar stuff { // Qt doesn't like vertical moving toolbars, so we have to force them... // See https://github.com/PolyMC/PolyMC/issues/493 connect(ui->instanceToolBar, &QToolBar::orientationChanged, [this](Qt::Orientation) { ui->instanceToolBar->setOrientation(Qt::Vertical); }); // if you try to add a widget to a toolbar in a .ui file // qt designer will delete it when you save the file >:( changeIconButton = new LabeledToolButton(this); changeIconButton->setObjectName(QStringLiteral("changeIconButton")); changeIconButton->setIcon(QIcon::fromTheme("news")); changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); connect(changeIconButton, &QToolButton::clicked, this, &MainWindow::on_actionChangeInstIcon_triggered); ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, changeIconButton); renameButton = new LabeledToolButton(this); renameButton->setObjectName(QStringLiteral("renameButton")); renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); connect(renameButton, &QToolButton::clicked, this, &MainWindow::on_actionRenameInstance_triggered); ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, renameButton); ui->instanceToolBar->insertSeparator(ui->actionLaunchInstance); // restore the instance toolbar settings auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); instanceToolbarSetting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->instanceToolBar->setVisibilityState(QByteArray::fromBase64(instanceToolbarSetting->get().toString().toUtf8())); ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->actionToggleStatusBar); ui->instanceToolBar->addContextMenuAction(ui->actionLockToolbars); } // set the menu for the folders help, accounts, and export tool buttons { auto foldersMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionFoldersButton)); ui->actionFoldersButton->setMenu(ui->foldersMenu); foldersMenuButton->setPopupMode(QToolButton::InstantPopup); helpMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionHelpButton)); ui->actionHelpButton->setMenu(new QMenu(this)); ui->actionHelpButton->menu()->addActions(ui->helpMenu->actions()); ui->actionHelpButton->menu()->removeAction(ui->actionCheckUpdate); helpMenuButton->setPopupMode(QToolButton::InstantPopup); auto accountMenuButton = dynamic_cast(ui->mainToolBar->widgetForAction(ui->actionAccountsButton)); accountMenuButton->setPopupMode(QToolButton::InstantPopup); auto exportInstanceMenu = new QMenu(this); exportInstanceMenu->addAction(ui->actionExportInstanceZip); exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); ui->actionExportInstance->setMenu(exportInstanceMenu); } // hide, disable and show stuff { ui->actionReportBug->setVisible(!BuildConfig.BUG_TRACKER_URL.isEmpty()); ui->actionMATRIX->setVisible(!BuildConfig.MATRIX_URL.isEmpty()); ui->actionDISCORD->setVisible(!BuildConfig.DISCORD_URL.isEmpty()); ui->actionREDDIT->setVisible(!BuildConfig.SUBREDDIT_URL.isEmpty()); ui->actionCheckUpdate->setVisible(APPLICATION->updaterEnabled()); #ifndef Q_OS_MAC ui->actionAddToPATH->setVisible(false); #endif // disabled until we have an instance selected ui->instanceToolBar->setEnabled(false); setInstanceActionsEnabled(false); // add a close button at the end of the main toolbar when running on gamescope / steam deck // this is only needed on gamescope because it defaults to an X11/XWayland session and // does not implement decorations if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { ui->mainToolBar->addAction(ui->actionCloseWindow); } ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); } { // logs viewing connect(ui->actionViewLog, &QAction::triggered, this, [] { APPLICATION->showLogWindow(); }); } // add the toolbar toggles to the view menu ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction()); ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction()); updateThemeMenu(); updateMainToolBar(); // OSX magic. setUnifiedTitleAndToolBarOnMac(true); // Global shortcuts { // you can't set QKeySequence::StandardKey shortcuts in qt designer >:( ui->actionAddInstance->setShortcut(QKeySequence::New); ui->actionSettings->setShortcut(QKeySequence::Preferences); ui->actionUndoTrashInstance->setShortcut(QKeySequence::Undo); ui->actionDeleteInstance->setShortcuts({ QKeySequence(tr("Backspace")), QKeySequence::Delete }); ui->actionCloseWindow->setShortcut(QKeySequence::Close); connect(ui->actionCloseWindow, &QAction::triggered, APPLICATION, &Application::closeCurrentWindow); // FIXME: This is kinda weird. and bad. We need some kind of managed shutdown. auto q = new QShortcut(QKeySequence::Quit, this); connect(q, &QShortcut::activated, APPLICATION, &Application::quit); } // Konami Code { secretEventFilter = new KonamiCode(this); connect(secretEventFilter, &KonamiCode::triggered, this, &MainWindow::konamiTriggered); } // Add the news label to the news toolbar. { m_newsChecker.reset(new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL)); newsLabel = new QToolButton(); newsLabel->setIcon(QIcon::fromTheme("news")); newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); newsLabel->setFocusPolicy(Qt::NoFocus); ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); updateNewsLabel(); } // Create the instance list widget { view = new InstanceView(ui->centralWidget); view->setSelectionMode(QAbstractItemView::SingleSelection); // FIXME: leaks ListViewDelegate auto delegate = new ListViewDelegate(this); view->setItemDelegate(delegate); view->setFrameShape(QFrame::NoFrame); // do not show ugly blue border on the mac view->setAttribute(Qt::WA_MacShowFocusRect, false); connect(delegate, &ListViewDelegate::textChanged, this, [this](QString before, QString after) { if (auto newRoot = askToUpdateInstanceDirName(m_selectedInstance, before, after, this); !newRoot.isEmpty()) { auto oldID = m_selectedInstance->id(); auto newID = QFileInfo(newRoot).fileName(); QString origGroup(APPLICATION->instances()->getInstanceGroup(oldID)); bool syncGroup = origGroup != GroupId() && oldID != newID; if (syncGroup) APPLICATION->instances()->setInstanceGroup(oldID, GroupId()); refreshInstances(); setSelectedInstanceById(newID); if (syncGroup) APPLICATION->instances()->setInstanceGroup(newID, origGroup); } }); view->installEventFilter(this); view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view, &QWidget::customContextMenuRequested, this, &MainWindow::showInstanceContextMenu); connect(view, &InstanceView::droppedURLs, this, &MainWindow::processURLs, Qt::QueuedConnection); proxymodel = new InstanceProxyModel(this); proxymodel->setSourceModel(APPLICATION->instances().get()); proxymodel->sort(0); connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged); view->setModel(proxymodel); view->setSourceOfGroupCollapseStatus( [](const QString& groupName) -> bool { return APPLICATION->instances()->isGroupCollapsed(groupName); }); connect(view, &InstanceView::groupStateChanged, APPLICATION->instances().get(), &InstanceList::on_GroupStateChanged); ui->horizontalLayout->addWidget(view); } // The cat background { // set the cat action priority here so you can still see the action in qt designer ui->actionCAT->setPriority(QAction::LowPriority); bool cat_enable = APPLICATION->settings()->get("TheCat").toBool(); ui->actionCAT->setChecked(cat_enable); connect(ui->actionCAT, &QAction::toggled, this, &MainWindow::onCatToggled); connect(APPLICATION, &Application::currentCatChanged, this, &MainWindow::onCatChanged); setCatBackground(cat_enable); } // Togglable status bar { bool statusBarVisible = APPLICATION->settings()->get("StatusBarVisible").toBool(); ui->actionToggleStatusBar->setChecked(statusBarVisible); connect(ui->actionToggleStatusBar, &QAction::toggled, this, &MainWindow::setStatusBarVisibility); setStatusBarVisibility(statusBarVisible); } // Lock toolbars { bool toolbarsLocked = APPLICATION->settings()->get("ToolbarsLocked").toBool(); ui->actionLockToolbars->setChecked(toolbarsLocked); connect(ui->actionLockToolbars, &QAction::toggled, this, &MainWindow::lockToolbars); lockToolbars(toolbarsLocked); } // start instance when double-clicked connect(view, &InstanceView::activated, this, &MainWindow::instanceActivated); // track the selection -- update the instance toolbar connect(view->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::instanceChanged); // track icon changes and update the toolbar! connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, &MainWindow::iconUpdated); // model reset -> selection is invalid. All the instance pointers are wrong. connect(APPLICATION->instances().get(), &InstanceList::dataIsInvalid, this, &MainWindow::selectionBad); // handle newly added instances connect(APPLICATION->instances().get(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest); // When the global settings page closes, we want to know about it and update our state connect(APPLICATION, &Application::globalSettingsApplied, this, &MainWindow::globalSettingsClosed); m_statusLeft = new QLabel(tr("No instance selected"), this); m_statusCenter = new QLabel(tr("Total playtime: 0s"), this); statusBar()->addPermanentWidget(m_statusLeft, 1); statusBar()->addPermanentWidget(m_statusCenter, 0); // Add "manage accounts" button, right align QWidget* spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); ui->mainToolBar->insertWidget(ui->actionAccountsButton, spacer); // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt ui->accountsMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); repopulateAccountsMenu(); // Update the menu when the active account changes. // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Template hell sucks... connect(APPLICATION->accounts().get(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); }); connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { defaultAccountChanged(); }); // Show initial account defaultAccountChanged(); // TODO: refresh accounts here? // auto accounts = APPLICATION->accounts(); // load the news { m_newsChecker->reloadNews(); updateNewsLabel(); } if (APPLICATION->updaterEnabled()) { bool updatesAllowed = APPLICATION->updatesAreAllowed(); updatesAllowedChanged(updatesAllowed); connect(ui->actionCheckUpdate, &QAction::triggered, this, &MainWindow::checkForUpdates); // set up the updater object. auto updater = APPLICATION->updater(); if (updater) { connect(updater.get(), &ExternalUpdater::canCheckForUpdatesChanged, this, &MainWindow::updatesAllowedChanged); } } connect(ui->actionUndoTrashInstance, &QAction::triggered, this, &MainWindow::undoTrashInstance); setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); // removing this looks stupid view->setFocus(); retranslateUi(); } // macOS always has a native menu bar, so these fixes are not applicable // Other systems may or may not have a native menu bar (most do not - it seems like only Ubuntu Unity does) #ifndef Q_OS_MAC void MainWindow::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Alt && !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()) ui->menuBar->setVisible(!ui->menuBar->isVisible()); else QMainWindow::keyReleaseEvent(event); } #endif void MainWindow::retranslateUi() { if (m_selectedInstance) { m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); } else { m_statusLeft->setText(tr("No instance selected")); } ui->retranslateUi(this); MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount(); if (defaultAccount) { auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); ui->actionAccountsButton->setText(profileLabel); } changeIconButton->setToolTip(ui->actionChangeInstIcon->toolTip()); renameButton->setToolTip(ui->actionRenameInstance->toolTip()); // replace the %1 with the launcher display name in some actions if (helpMenuButton->toolTip().contains("%1")) helpMenuButton->setToolTip(helpMenuButton->toolTip().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); for (auto action : ui->helpMenu->actions()) { if (action->text().contains("%1")) action->setText(action->text().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); if (action->toolTip().contains("%1")) action->setToolTip(action->toolTip().arg(BuildConfig.LAUNCHER_DISPLAYNAME)); } } MainWindow::~MainWindow() {} QMenu* MainWindow::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction(ui->mainToolBar->toggleViewAction()); filteredMenu->addAction(ui->actionToggleStatusBar); filteredMenu->addAction(ui->actionLockToolbars); return filteredMenu; } void MainWindow::setStatusBarVisibility(bool state) { statusBar()->setVisible(state); APPLICATION->settings()->set("StatusBarVisible", state); } void MainWindow::lockToolbars(bool state) { ui->mainToolBar->setMovable(!state); ui->instanceToolBar->setMovable(!state); ui->newsToolBar->setMovable(!state); APPLICATION->settings()->set("ToolbarsLocked", state); } void MainWindow::konamiTriggered() { QString gradient = " stop:0 rgba(125, 0, 0, 255), stop:0.166 rgba(125, 125, 0, 255), stop:0.333 rgba(0, 125, 0, 255), stop:0.5 rgba(0, 125, 125, " "255), stop:0.666 rgba(0, 0, 125, 255), stop:0.833 rgba(125, 0, 125, 255), stop:1 rgba(125, 0, 0, 255));"; QString stylesheet = "background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0," + gradient; if (ui->mainToolBar->styleSheet() == stylesheet) { ui->mainToolBar->setStyleSheet(""); ui->instanceToolBar->setStyleSheet(""); ui->centralWidget->setStyleSheet(""); ui->newsToolBar->setStyleSheet(""); ui->statusBar->setStyleSheet(""); qDebug() << "Super Secret Mode DEACTIVATED!"; } else { ui->mainToolBar->setStyleSheet(stylesheet); ui->instanceToolBar->setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1," + gradient); ui->centralWidget->setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:1," + gradient); ui->newsToolBar->setStyleSheet(stylesheet); ui->statusBar->setStyleSheet(stylesheet); qDebug() << "Super Secret Mode ACTIVATED!"; } } void MainWindow::showInstanceContextMenu(const QPoint& pos) { QList actions; QAction* actionSep = new QAction("", this); actionSep->setSeparator(true); bool onInstance = view->indexAt(pos).isValid(); if (onInstance) { // reuse the file menu actions actions = ui->fileMenu->actions(); // remove the add instance action, launcher settings action and close action actions.removeFirst(); actions.removeLast(); actions.removeLast(); actions.prepend(ui->actionChangeInstIcon); actions.prepend(ui->actionRenameInstance); // add header actions.prepend(actionSep); QAction* actionVoid = new QAction(m_selectedInstance->name(), this); actionVoid->setEnabled(false); actions.prepend(actionVoid); } else { auto group = view->groupNameAt(pos); QAction* actionVoid = new QAction(group.isNull() ? BuildConfig.LAUNCHER_DISPLAYNAME : group, this); actionVoid->setEnabled(false); QAction* actionCreateInstance = new QAction(tr("&Create instance"), this); actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip()); if (!group.isNull()) { QVariantMap instance_action_data; instance_action_data["group"] = group; actionCreateInstance->setData(instance_action_data); } connect(actionCreateInstance, &QAction::triggered, this, &MainWindow::on_actionAddInstance_triggered); actions.prepend(actionSep); actions.prepend(actionVoid); actions.append(actionCreateInstance); if (!group.isNull()) { QAction* actionDeleteGroup = new QAction(tr("&Delete group"), this); connect(actionDeleteGroup, &QAction::triggered, this, [this, group] { deleteGroup(group); }); actions.append(actionDeleteGroup); QAction* actionRenameGroup = new QAction(tr("&Rename group"), this); connect(actionRenameGroup, &QAction::triggered, this, [this, group] { renameGroup(group); }); actions.append(actionRenameGroup); } } QMenu myMenu; myMenu.addActions(actions); /* if (onInstance) myMenu.setEnabled(m_selectedInstance->canLaunch()); */ myMenu.exec(view->mapToGlobal(pos)); } void MainWindow::updateMainToolBar() { ui->menuBar->setVisible(APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); ui->mainToolBar->setVisible(ui->menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); } void MainWindow::updateLaunchButton() { QMenu* launchMenu = ui->actionLaunchInstance->menu(); if (launchMenu) launchMenu->clear(); else launchMenu = new QMenu(this); if (m_selectedInstance) m_selectedInstance->populateLaunchMenu(launchMenu); ui->actionLaunchInstance->setMenu(launchMenu); } void MainWindow::updateThemeMenu() { QMenu* themeMenu = ui->actionChangeTheme->menu(); if (themeMenu) { themeMenu->clear(); } else { themeMenu = new QMenu(this); } auto themes = APPLICATION->themeManager()->getValidApplicationThemes(); QActionGroup* themesGroup = new QActionGroup(this); for (auto* theme : themes) { QAction* themeAction = themeMenu->addAction(theme->name()); themeAction->setCheckable(true); if (APPLICATION->settings()->get("ApplicationTheme").toString() == theme->id()) { themeAction->setChecked(true); } themeAction->setActionGroup(themesGroup); connect(themeAction, &QAction::triggered, [theme]() { APPLICATION->themeManager()->setApplicationTheme(theme->id()); APPLICATION->settings()->set("ApplicationTheme", theme->id()); }); } ui->actionChangeTheme->setMenu(themeMenu); } void MainWindow::repopulateAccountsMenu() { ui->accountsMenu->clear(); // NOTE: this is done so the accounts button text is not set to the accounts menu title QMenu* accountsButtonMenu = ui->actionAccountsButton->menu(); if (accountsButtonMenu) { accountsButtonMenu->clear(); } else { accountsButtonMenu = new QMenu(this); ui->actionAccountsButton->setMenu(accountsButtonMenu); } auto accounts = APPLICATION->accounts(); MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); QString active_profileId = ""; if (defaultAccount) { // this can be called before accountMenuButton exists if (ui->actionAccountsButton) { auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); ui->actionAccountsButton->setText(profileLabel); } } QActionGroup* accountsGroup = new QActionGroup(this); if (accounts->count() <= 0) { ui->actionNoAccountsAdded->setEnabled(false); ui->accountsMenu->addAction(ui->actionNoAccountsAdded); } else { // TODO: Nicer way to iterate? for (int i = 0; i < accounts->count(); i++) { MinecraftAccountPtr account = accounts->at(i); auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); QAction* action = new QAction(profileLabel, this); action->setData(i); action->setCheckable(true); action->setActionGroup(accountsGroup); if (defaultAccount == account) { action->setChecked(true); } auto face = account->getFace(); if (!face.isNull()) { action->setIcon(face); } else { action->setIcon(QIcon::fromTheme("noaccount")); } const int highestNumberKey = 9; if (i < highestNumberKey) { action->setShortcut(QKeySequence(tr("Ctrl+%1").arg(i + 1))); } ui->accountsMenu->addAction(action); connect(action, &QAction::triggered, this, &MainWindow::changeActiveAccount); } } ui->accountsMenu->addSeparator(); ui->actionNoDefaultAccount->setData(-1); ui->actionNoDefaultAccount->setChecked(!defaultAccount); ui->actionNoDefaultAccount->setActionGroup(accountsGroup); ui->accountsMenu->addAction(ui->actionNoDefaultAccount); connect(ui->actionNoDefaultAccount, &QAction::triggered, this, &MainWindow::changeActiveAccount); ui->accountsMenu->addSeparator(); ui->accountsMenu->addAction(ui->actionManageAccounts); accountsButtonMenu->addActions(ui->accountsMenu->actions()); } void MainWindow::updatesAllowedChanged(bool allowed) { if (!APPLICATION->updaterEnabled()) { return; } ui->actionCheckUpdate->setEnabled(allowed); } /* * Assumes the sender is a QAction */ void MainWindow::changeActiveAccount() { QAction* sAction = (QAction*)sender(); // Profile's associated Mojang username if (sAction->data().typeId() != QMetaType::Int) return; QVariant action_data = sAction->data(); bool valid = false; int index = action_data.toInt(&valid); if (!valid) { index = -1; } auto accounts = APPLICATION->accounts(); accounts->setDefaultAccount(index == -1 ? nullptr : accounts->at(index)); defaultAccountChanged(); } void MainWindow::defaultAccountChanged() { repopulateAccountsMenu(); MinecraftAccountPtr account = APPLICATION->accounts()->defaultAccount(); // FIXME: this needs adjustment for MSA if (account && account->profileName() != "") { auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); ui->actionAccountsButton->setText(profileLabel); auto face = account->getFace(); if (face.isNull()) { ui->actionAccountsButton->setIcon(QIcon::fromTheme("noaccount")); } else { ui->actionAccountsButton->setIcon(face); } return; } // Set the icon to the "no account" icon. ui->actionAccountsButton->setIcon(QIcon::fromTheme("noaccount")); ui->actionAccountsButton->setText(tr("Accounts")); } bool MainWindow::eventFilter(QObject* obj, QEvent* ev) { if (obj == view) { if (ev->type() == QEvent::KeyPress) { secretEventFilter->input(ev); QKeyEvent* keyEvent = static_cast(ev); switch (keyEvent->key()) { /* case Qt::Key_Enter: case Qt::Key_Return: activateInstance(m_selectedInstance); return true; */ case Qt::Key_Delete: on_actionDeleteInstance_triggered(); return true; case Qt::Key_F5: refreshInstances(); return true; case Qt::Key_F2: on_actionRenameInstance_triggered(); return true; default: break; } } } return QMainWindow::eventFilter(obj, ev); } void MainWindow::updateNewsLabel() { if (m_newsChecker->isLoadingNews()) { newsLabel->setText(tr("Loading news...")); newsLabel->setEnabled(false); ui->actionMoreNews->setVisible(false); } else { QList entries = m_newsChecker->getNewsEntries(); if (entries.length() > 0) { newsLabel->setText(entries[0]->title); newsLabel->setEnabled(true); ui->actionMoreNews->setVisible(true); } else { newsLabel->setText(tr("No news available.")); newsLabel->setEnabled(false); ui->actionMoreNews->setVisible(false); } } } QList stringToIntList(const QString& string) { QStringList split = string.split(',', Qt::SkipEmptyParts); QList out; for (int i = 0; i < split.size(); ++i) { out.append(split.at(i).toInt()); } return out; } QString intListToString(const QList& list) { QStringList slist; for (int i = 0; i < list.size(); ++i) { slist.append(QString::number(list.at(i))); } return slist.join(','); } void MainWindow::onCatToggled(bool state) { setCatBackground(state); APPLICATION->settings()->set("TheCat", state); } void MainWindow::setCatBackground(bool enabled) { view->setPaintCat(enabled); view->viewport()->repaint(); } void MainWindow::runModalTask(Task* task) { connect(task, &Task::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(task, &Task::succeeded, [this, task]() { QStringList warnings = task->warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } }); connect(task, &Task::aborted, [this] { CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) ->show(); }); ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(task); } void MainWindow::instanceFromInstanceTask(InstanceTask* rawTask) { unique_qobject_ptr task(APPLICATION->instances()->wrapInstanceTask(rawTask)); runModalTask(task.get()); } void MainWindow::on_actionCopyInstance_triggered() { if (!m_selectedInstance) return; CopyInstanceDialog copyInstDlg(m_selectedInstance, this); if (!copyInstDlg.exec()) return; auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.getChosenOptions()); copyTask->setName(copyInstDlg.instName()); copyTask->setGroup(copyInstDlg.instGroup()); copyTask->setIcon(copyInstDlg.iconKey()); unique_qobject_ptr task(APPLICATION->instances()->wrapInstanceTask(copyTask)); runModalTask(task.get()); } void MainWindow::addInstance(const QString& url, const QMap& extra_info) { QString groupName; do { QObject* obj = sender(); if (!obj) break; QAction* action = qobject_cast(obj); if (!action) break; auto map = action->data().toMap(); if (!map.contains("group")) break; groupName = map["group"].toString(); } while (0); if (groupName.isEmpty()) { groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString(); } NewInstanceDialog newInstDlg(groupName, url, extra_info, this); if (!newInstDlg.exec()) return; APPLICATION->settings()->set("LastUsedGroupForNewInstance", newInstDlg.instGroup()); InstanceTask* creationTask = newInstDlg.extractTask(); if (creationTask) { instanceFromInstanceTask(creationTask); } } void MainWindow::on_actionAddInstance_triggered() { addInstance(); } void MainWindow::processURLs(QList urls) { // NOTE: This loop only processes one dropped file! for (auto& url : urls) { qDebug() << "Processing" << url; // The isLocalFile() check below doesn't work as intended without an explicit scheme. if (url.scheme().isEmpty()) url.setScheme("file"); ModPlatform::IndexedVersion version; QMap extra_info; QUrl local_url; if (!url.isLocalFile()) { // download the remote resource and identify QUrl dl_url; if (url.scheme() == "curseforge") { // need to find the download link for the modpack / resource // format of url curseforge://install?addonId=IDHERE&fileId=IDHERE QUrlQuery query(url); if (query.allQueryItemValues("addonId").isEmpty() || query.allQueryItemValues("fileId").isEmpty()) { qDebug() << "Invalid curseforge link:" << url; continue; } auto addonId = query.allQueryItemValues("addonId")[0]; auto fileId = query.allQueryItemValues("fileId")[0]; extra_info.insert("pack_id", addonId); extra_info.insert("pack_version_id", fileId); auto array = std::make_shared(); auto api = FlameAPI(); auto job = api.getFile(addonId, fileId, array); connect(job.get(), &Task::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &version] { qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); auto doc = Json::requireDocument(*array); auto data = doc.object()["data"].toObject(); // No way to find out if it's a mod or a modpack before here // And also we need to check if it ends with .zip, instead of any better way version = FlameMod::loadIndexedPackVersion(data); auto fileName = version.fileName; // Have to use ensureString then use QUrl to get proper url encoding dl_url = QUrl(version.downloadUrl); if (!dl_url.isValid()) { CustomMessageBox::selectable( this, tr("Error"), tr("The modpack, mod, or resource %1 is blocked for third-parties! Please download it manually.").arg(fileName), QMessageBox::Critical) ->show(); return; } QFileInfo dl_file(dl_url.fileName()); }); { // drop stack ProgressDialog dlUrlDialod(this); dlUrlDialod.setSkipButton(true, tr("Abort")); dlUrlDialod.execWithTask(job.get()); } } else if (url.scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME) { QVariantMap receivedData; const QUrlQuery query(url.query()); const auto items = query.queryItems(); for (auto it = items.begin(), end = items.end(); it != end; ++it) receivedData.insert(it->first, it->second); emit APPLICATION->oauthReplyRecieved(receivedData); continue; } else { dl_url = url; } if (!dl_url.isValid()) { continue; // no valid url to download this resource } const QString path = dl_url.host() + '/' + dl_url.path(); auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); auto dl_job = unique_qobject_ptr(new NetJob(tr("Modpack download"), APPLICATION->network())); dl_job->addNetAction(Net::ApiDownload::makeCached(dl_url, entry)); auto archivePath = entry->getFullPath(); bool dl_success = false; connect(dl_job.get(), &Task::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(dl_job.get(), &Task::succeeded, this, [&dl_success] { dl_success = true; }); { // drop stack ProgressDialog dlUrlDialod(this); dlUrlDialod.setSkipButton(true, tr("Abort")); dlUrlDialod.execWithTask(dl_job.get()); } if (!dl_success) { continue; // no local file to identify } local_url = QUrl::fromLocalFile(archivePath); } else { local_url = url; } auto localFileName = QDir::toNativeSeparators(local_url.toLocalFile()); QFileInfo localFileInfo(localFileName); auto type = ResourceUtils::identify(localFileInfo); if (ModPlatform::ResourceTypeUtils::VALID_RESOURCES.count(type) == 0) { // probably instance/modpack addInstance(localFileName, extra_info); continue; } if (APPLICATION->instances()->count() <= 0) { CustomMessageBox::selectable(this, tr("No instance!"), tr("No instance available to add the resource to.\nPlease create a new instance before " "attempting to install this resource again."), QMessageBox::Critical) ->show(); continue; } ImportResourceDialog dlg(localFileName, type, this); if (dlg.exec() != QDialog::Accepted) continue; qDebug() << "Adding resource" << localFileName << "to" << dlg.selectedInstanceKey; auto inst = APPLICATION->instances()->getInstanceById(dlg.selectedInstanceKey); auto minecraftInst = std::dynamic_pointer_cast(inst); switch (type) { case ModPlatform::ResourceType::ResourcePack: minecraftInst->resourcePackList()->installResourceWithFlameMetadata(localFileName, version); break; case ModPlatform::ResourceType::TexturePack: minecraftInst->texturePackList()->installResourceWithFlameMetadata(localFileName, version); break; case ModPlatform::ResourceType::DataPack: qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; break; case ModPlatform::ResourceType::Mod: minecraftInst->loaderModList()->installResourceWithFlameMetadata(localFileName, version); break; case ModPlatform::ResourceType::ShaderPack: minecraftInst->shaderPackList()->installResourceWithFlameMetadata(localFileName, version); break; case ModPlatform::ResourceType::World: minecraftInst->worldList()->installWorld(localFileInfo); break; case ModPlatform::ResourceType::Unknown: default: qDebug() << "Can't Identify" << localFileName << "Ignoring it."; break; } } } void MainWindow::on_actionREDDIT_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.SUBREDDIT_URL)); } void MainWindow::on_actionDISCORD_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.DISCORD_URL)); } void MainWindow::on_actionMATRIX_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.MATRIX_URL)); } void MainWindow::on_actionChangeInstIcon_triggered() { if (!m_selectedInstance) return; IconPickerDialog dlg(this); dlg.execWithSelection(m_selectedInstance->iconKey()); if (dlg.result() == QDialog::Accepted) { m_selectedInstance->setIconKey(dlg.selectedIconKey); auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey); ui->actionChangeInstIcon->setIcon(icon); changeIconButton->setIcon(icon); } } void MainWindow::iconUpdated(QString icon) { if (icon == m_currentInstIcon) { auto new_icon = APPLICATION->icons()->getIcon(m_currentInstIcon); ui->actionChangeInstIcon->setIcon(new_icon); changeIconButton->setIcon(new_icon); } } void MainWindow::updateInstanceToolIcon(QString new_icon) { m_currentInstIcon = new_icon; auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); ui->actionChangeInstIcon->setIcon(icon); changeIconButton->setIcon(icon); } void MainWindow::setSelectedInstanceById(const QString& id) { if (id.isNull()) return; const QModelIndex index = APPLICATION->instances()->getInstanceIndexById(id); if (index.isValid()) { QModelIndex selectionIndex = proxymodel->mapFromSource(index); view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect); updateStatusCenter(); } } void MainWindow::on_actionChangeInstGroup_triggered() { if (!m_selectedInstance) return; InstanceId instId = m_selectedInstance->id(); QString src(APPLICATION->instances()->getInstanceGroup(instId)); QStringList groups = APPLICATION->instances()->getGroups(); groups.prepend(""); int index = groups.indexOf(src); bool ok = false; QString dst = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, index, true, &ok); dst = dst.simplified(); if (ok) { APPLICATION->instances()->setInstanceGroup(instId, dst); } } void MainWindow::deleteGroup(QString group) { Q_ASSERT(!group.isEmpty()); const int reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group '%1'?").arg(group), QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) APPLICATION->instances()->deleteGroup(group); } void MainWindow::renameGroup(QString group) { Q_ASSERT(!group.isEmpty()); QString name = QInputDialog::getText(this, tr("Rename group"), tr("Enter a new group name."), QLineEdit::Normal, group); name = name.simplified(); if (name.isNull() || name == group) return; const bool empty = name.isEmpty(); const bool duplicate = APPLICATION->instances()->getGroups().contains(name, Qt::CaseInsensitive) && group.toLower() != name.toLower(); if (empty || duplicate) { QMessageBox::warning(this, tr("Cannot rename group"), empty ? tr("Cannot set empty name.") : tr("Group already exists. :/")); return; } APPLICATION->instances()->renameGroup(group, name); } void MainWindow::undoTrashInstance() { if (!APPLICATION->instances()->undoTrashInstance()) QMessageBox::warning( this, tr("Failed to undo trashing instance"), tr("Some instances and shortcuts could not be restored.\nPlease check your trashbin to manually restore them.")); ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); } void MainWindow::on_actionViewLauncherRootFolder_triggered() { DesktopServices::openPath("."); } void MainWindow::on_actionViewInstanceFolder_triggered() { QString str = APPLICATION->settings()->get("InstanceDir").toString(); DesktopServices::openPath(str); } void MainWindow::on_actionViewCentralModsFolder_triggered() { DesktopServices::openPath(APPLICATION->settings()->get("CentralModsDir").toString(), true); } void MainWindow::on_actionViewSkinsFolder_triggered() { DesktopServices::openPath(APPLICATION->settings()->get("SkinsDir").toString(), true); } void MainWindow::on_actionViewIconThemeFolder_triggered() { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path(), true); } void MainWindow::on_actionViewWidgetThemeFolder_triggered() { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path(), true); } void MainWindow::on_actionViewCatPackFolder_triggered() { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path(), true); } void MainWindow::on_actionViewIconsFolder_triggered() { DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true); } void MainWindow::on_actionViewLogsFolder_triggered() { DesktopServices::openPath("logs", true); } void MainWindow::on_actionViewJavaFolder_triggered() { DesktopServices::openPath(APPLICATION->javaPath(), true); } void MainWindow::refreshInstances() { APPLICATION->instances()->loadList(); } void MainWindow::checkForUpdates() { if (APPLICATION->updaterEnabled()) { APPLICATION->triggerUpdateCheck(); } else { qWarning() << "Updater not set up. Cannot check for updates."; } } void MainWindow::on_actionSettings_triggered() { APPLICATION->ShowGlobalSettings(this, "global-settings"); } void MainWindow::globalSettingsClosed() { // FIXME: quick HACK to make this work. improve, optimize. APPLICATION->instances()->loadList(); proxymodel->invalidate(); proxymodel->sort(0); updateMainToolBar(); updateLaunchButton(); updateThemeMenu(); updateStatusCenter(); // This needs to be done to prevent UI elements disappearing in the event the config is changed // but Prism Launcher exits abnormally, causing the window state to never be saved: APPLICATION->settings()->set("MainWindowState", QString::fromUtf8(saveState().toBase64())); update(); } void MainWindow::on_actionEditInstance_triggered() { if (!m_selectedInstance) return; if (m_selectedInstance->canEdit()) { APPLICATION->showInstanceWindow(m_selectedInstance); } else { CustomMessageBox::selectable(this, tr("Instance not editable"), tr("This instance is not editable. It may be broken, invalid, or too old. Check logs for details."), QMessageBox::Critical) ->show(); } } void MainWindow::on_actionManageAccounts_triggered() { APPLICATION->ShowGlobalSettings(this, "accounts"); } void MainWindow::on_actionReportBug_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.BUG_TRACKER_URL)); } void MainWindow::on_actionClearMetadata_triggered() { // This if contains side effects! if (!APPLICATION->metacache()->evictAll()) { CustomMessageBox::selectable(this, tr("Error"), tr("Metadata cache clear Failed!\nTo clear the metadata cache manually, press Folders -> View " "Launcher Root Folder, and after closing the launcher delete the folder named \"meta\"\n"), QMessageBox::Warning) ->show(); } APPLICATION->metacache()->SaveNow(); } #ifdef Q_OS_MAC void MainWindow::on_actionAddToPATH_triggered() { auto binaryPath = APPLICATION->applicationFilePath(); auto targetPath = QString("/usr/local/bin/%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); qDebug() << "Symlinking" << binaryPath << "to" << targetPath; QStringList args; args << "-e"; args << QString("do shell script \"mkdir -p /usr/local/bin && ln -sf '%1' '%2'\" with administrator privileges") .arg(binaryPath, targetPath); auto outcome = QProcess::execute("/usr/bin/osascript", args); if (!outcome) { QMessageBox::information(this, tr("Successfully added %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME), tr("%1 was successfully added to your PATH. You can now start it by running `%2`.") .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.LAUNCHER_APP_BINARY_NAME)); } else { QMessageBox::critical(this, tr("Failed to add %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME), tr("An error occurred while trying to add %1 to PATH").arg(BuildConfig.LAUNCHER_DISPLAYNAME)); } } #endif void MainWindow::on_actionOpenWiki_triggered() { DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(""))); } void MainWindow::on_actionMoreNews_triggered() { auto entries = m_newsChecker->getNewsEntries(); NewsDialog news_dialog(entries, this); news_dialog.exec(); } void MainWindow::newsButtonClicked() { auto entries = m_newsChecker->getNewsEntries(); NewsDialog news_dialog(entries, this); news_dialog.toggleArticleList(); news_dialog.exec(); } void MainWindow::onCatChanged(int) { setCatBackground(APPLICATION->settings()->get("TheCat").toBool()); } void MainWindow::on_actionAbout_triggered() { AboutDialog dialog(this); dialog.exec(); } void MainWindow::on_actionDeleteInstance_triggered() { if (!m_selectedInstance) { return; } if (m_selectedInstance->isRunning()) { CustomMessageBox::selectable(this, tr("Cannot Delete Running Instance"), tr("The selected instance is currently running and cannot be deleted. Please stop the instance before " "attempting to delete it."), QMessageBox::Warning, QMessageBox::Ok) ->exec(); return; } auto id = m_selectedInstance->id(); QString shortcutStr; auto shortcuts = m_selectedInstance->shortcuts(); if (!shortcuts.isEmpty()) shortcutStr = tr(" and its %n registered shortcut(s)", "", shortcuts.size()); auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), tr("You are about to delete \"%1\"%2.\n" "This may be permanent and will completely delete the instance.\n\n" "Are you sure?") .arg(m_selectedInstance->name(), shortcutStr), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; if (!checkLinkedInstances(id, this, tr("Deleting"))) return; if (APPLICATION->instances()->trashInstance(id)) { ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); } else { APPLICATION->instances()->deleteInstance(id); } APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); } void MainWindow::on_actionExportInstanceZip_triggered() { if (m_selectedInstance) { ExportInstanceDialog dlg(m_selectedInstance, this); dlg.exec(); } } void MainWindow::on_actionExportInstanceMrPack_triggered() { if (m_selectedInstance) { auto instance = std::dynamic_pointer_cast(m_selectedInstance); if (instance != nullptr) { ExportPackDialog dlg(instance, this); dlg.exec(); } } } void MainWindow::on_actionExportInstanceFlamePack_triggered() { if (m_selectedInstance) { auto instance = std::dynamic_pointer_cast(m_selectedInstance); if (instance) { if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft"); cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") { QMessageBox msgBox(this); msgBox.setText("Snapshots are currently not supported by CurseForge modpacks."); msgBox.exec(); return; } ExportPackDialog dlg(instance, this, ModPlatform::ResourceProvider::FLAME); dlg.exec(); } } } void MainWindow::on_actionRenameInstance_triggered() { if (m_selectedInstance) { view->edit(view->currentIndex()); } } void MainWindow::on_actionViewSelectedInstFolder_triggered() { if (m_selectedInstance) { QString str = m_selectedInstance->instanceRoot(); DesktopServices::openPath(QFileInfo(str)); } } void MainWindow::closeEvent(QCloseEvent* event) { // Save the window state and geometry. APPLICATION->settings()->set("MainWindowState", QString::fromUtf8(saveState().toBase64())); APPLICATION->settings()->set("MainWindowGeometry", QString::fromUtf8(saveGeometry().toBase64())); instanceToolbarSetting->set(QString::fromUtf8(ui->instanceToolBar->getVisibilityState().toBase64())); event->accept(); emit isClosing(); } void MainWindow::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { retranslateUi(); } QMainWindow::changeEvent(event); } void MainWindow::instanceActivated(QModelIndex index) { if (!index.isValid()) return; QString id = index.data(InstanceList::InstanceIDRole).toString(); InstancePtr inst = APPLICATION->instances()->getInstanceById(id); if (!inst) return; activateInstance(inst); } void MainWindow::on_actionLaunchInstance_triggered() { if (m_selectedInstance && !m_selectedInstance->isRunning()) { APPLICATION->launch(m_selectedInstance); } } void MainWindow::activateInstance(InstancePtr instance) { APPLICATION->launch(instance); } void MainWindow::on_actionKillInstance_triggered() { if (m_selectedInstance && m_selectedInstance->isRunning()) { APPLICATION->kill(m_selectedInstance); } } void MainWindow::on_actionCreateInstanceShortcut_triggered() { if (!m_selectedInstance) return; CreateShortcutDialog shortcutDlg(m_selectedInstance, this); if (!shortcutDlg.exec()) return; shortcutDlg.createShortcut(); } void MainWindow::taskEnd() { QObject* sender = QObject::sender(); if (sender == m_versionLoadTask) m_versionLoadTask = NULL; sender->deleteLater(); } void MainWindow::startTask(Task* task) { connect(task, &Task::succeeded, this, &MainWindow::taskEnd); connect(task, &Task::failed, this, &MainWindow::taskEnd); task->start(); } void MainWindow::instanceChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { if (!current.isValid()) { APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); return; } if (m_selectedInstance) { disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); disconnect(m_selectedInstance.get(), &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); if (m_selectedInstance) { ui->instanceToolBar->setEnabled(true); setInstanceActionsEnabled(true); ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning()); ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); renameButton->setText(m_selectedInstance->name()); m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); updateStatusCenter(); updateInstanceToolIcon(m_selectedInstance->iconKey()); updateLaunchButton(); APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); connect(m_selectedInstance.get(), &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); } else { APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); return; } } void MainWindow::instanceSelectRequest(QString id) { setSelectedInstanceById(id); } void MainWindow::instanceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { auto current = view->selectionModel()->currentIndex(); QItemSelection test(topLeft, bottomRight); if (test.contains(current)) { instanceChanged(current, current); } } void MainWindow::selectionBad() { // start by reseting everything... m_selectedInstance = nullptr; m_statusLeft->setText(tr("No instance selected")); statusBar()->clearMessage(); ui->instanceToolBar->setEnabled(false); setInstanceActionsEnabled(false); updateLaunchButton(); renameButton->setText(tr("Rename Instance")); updateInstanceToolIcon("grass"); // ...and then see if we can enable the previously selected instance setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); } void MainWindow::checkInstancePathForProblems() { QString instanceFolder = APPLICATION->settings()->get("InstanceDir").toString(); if (FS::checkProblemticPathJava(QDir(instanceFolder))) { QMessageBox warning(this); warning.setText(tr("Your instance folder contains \'!\' and this is known to cause Java problems!")); warning.setInformativeText(tr("You have now two options:
" " - change the instance folder in the settings
" " - move this installation of %1 to a different folder") .arg(BuildConfig.LAUNCHER_DISPLAYNAME)); warning.setDefaultButton(QMessageBox::Ok); warning.exec(); } auto tempFolderText = tr("This is a problem:
" " - The launcher will likely be deleted without warning by the operating system
" " - close the launcher now and extract it to a real location, not a temporary folder"); QString pathfoldername = QDir(instanceFolder).absolutePath(); if (pathfoldername.contains("Rar$", Qt::CaseInsensitive)) { QMessageBox warning(this); warning.setText(tr("Your instance folder contains \'Rar$\' - that means you haven't extracted the launcher archive!")); warning.setInformativeText(tempFolderText); warning.setDefaultButton(QMessageBox::Ok); warning.exec(); } else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/")) { QMessageBox warning(this); warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath())); warning.setInformativeText(tempFolderText); warning.setDefaultButton(QMessageBox::Ok); warning.exec(); } } void MainWindow::updateStatusCenter() { m_statusCenter->setVisible(APPLICATION->settings()->get("ShowGlobalGameTime").toBool()); int timePlayed = APPLICATION->instances()->getTotalPlayTime(); if (timePlayed > 0) { m_statusCenter->setText( tr("Total playtime: %1") .arg(Time::prettifyDuration(timePlayed, APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) // Actions that also require other conditions (e.g. a running instance) won't be changed. void MainWindow::setInstanceActionsEnabled(bool enabled) { ui->actionEditInstance->setEnabled(enabled); ui->actionChangeInstGroup->setEnabled(enabled); ui->actionViewSelectedInstFolder->setEnabled(enabled); ui->actionExportInstance->setEnabled(enabled); ui->actionDeleteInstance->setEnabled(enabled); ui->actionCopyInstance->setEnabled(enabled); ui->actionCreateInstanceShortcut->setEnabled(enabled); } void MainWindow::refreshCurrentInstance() { auto current = view->selectionModel()->currentIndex(); instanceChanged(current, current); } PrismLauncher-10.0.5/launcher/ui/java/0000755000175100017510000000000015144136756017132 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/java/VersionList.cpp0000644000175100017510000000740315144136756022123 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "VersionList.h" #include #include "BaseVersionList.h" #include "SysInfo.h" #include "java/JavaMetadata.h" #include "meta/VersionList.h" namespace Java { VersionList::VersionList(Meta::Version::Ptr version, QObject* parent) : BaseVersionList(parent), m_version(version) { if (version->isLoaded()) sortVersions(); } Task::Ptr VersionList::getLoadTask() { auto task = m_version->loadTask(Net::Mode::Online); connect(task.get(), &Task::finished, this, &VersionList::sortVersions); return task; } const BaseVersion::Ptr VersionList::at(int i) const { return m_vlist.at(i); } bool VersionList::isLoaded() { return m_version->isLoaded(); } int VersionList::count() const { return m_vlist.count(); } QVariant VersionList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() > count()) return QVariant(); auto version = (m_vlist[index.row()]); switch (role) { case SortRole: return -index.row(); case VersionPointerRole: return QVariant::fromValue(std::dynamic_pointer_cast(m_vlist[index.row()])); case VersionIdRole: return version->descriptor(); case VersionRole: return version->version.toString(); case RecommendedRole: return false; // do not recommend any version case JavaNameRole: return version->name(); case JavaMajorRole: { auto major = version->version.toString(); if (major.startsWith("java")) { major = "Java " + major.mid(4); } return major; } case TypeRole: return version->packageType; case Meta::VersionList::TimeRole: return version->releaseTime; default: return QVariant(); } } BaseVersionList::RoleList VersionList::providesRoles() const { return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, JavaNameRole, TypeRole, Meta::VersionList::TimeRole }; } bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right) { auto rleft = std::dynamic_pointer_cast(right); auto rright = std::dynamic_pointer_cast(left); return (*rleft) < (*rright); } void VersionList::sortVersions() { if (!m_version || !m_version->data()) return; QString versionStr = SysInfo::getSupportedJavaArchitecture(); beginResetModel(); auto runtimes = m_version->data()->runtimes; m_vlist = {}; if (!versionStr.isEmpty() && !runtimes.isEmpty()) { std::copy_if(runtimes.begin(), runtimes.end(), std::back_inserter(m_vlist), [versionStr](Java::MetadataPtr val) { return val->runtimeOS == versionStr; }); std::sort(m_vlist.begin(), m_vlist.end(), sortJavas); } else { qWarning() << "No Java versions found for your operating system:" << SysInfo::currentSystem() << SysInfo::useQTForArch(); } endResetModel(); } } // namespace Java PrismLauncher-10.0.5/launcher/ui/java/InstallJavaDialog.cpp0000644000175100017510000003231415144136756023171 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "InstallJavaDialog.h" #include #include #include #include #include #include #include "Application.h" #include "BaseVersionList.h" #include "FileSystem.h" #include "Filter.h" #include "java/download/ArchiveDownloadTask.h" #include "java/download/ManifestDownloadTask.h" #include "java/download/SymlinkTask.h" #include "meta/Index.h" #include "meta/VersionList.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "tasks/SequentialTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/java/VersionList.h" #include "ui/widgets/PageContainer.h" #include "ui/widgets/VersionSelectWidget.h" class InstallJavaPage : public QWidget, public BasePage { public: Q_OBJECT public: explicit InstallJavaPage(const QString& id, const QString& iconName, const QString& name, QWidget* parent = nullptr) : QWidget(parent), uid(id), iconName(iconName), name(name) { setObjectName(QStringLiteral("VersionSelectWidget")); horizontalLayout = new QHBoxLayout(this); horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); horizontalLayout->setContentsMargins(0, 0, 0, 0); majorVersionSelect = new VersionSelectWidget(this); majorVersionSelect->selectCurrent(); majorVersionSelect->setEmptyString(tr("No Java versions are currently available in the meta.")); majorVersionSelect->setEmptyErrorString(tr("Couldn't load or download the Java version lists!")); horizontalLayout->addWidget(majorVersionSelect, 1); javaVersionSelect = new VersionSelectWidget(this); javaVersionSelect->setEmptyString(tr("No Java versions are currently available for your OS.")); javaVersionSelect->setEmptyErrorString(tr("Couldn't load or download the Java version lists!")); horizontalLayout->addWidget(javaVersionSelect, 4); connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::setSelectedVersion); connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); connect(javaVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); QMetaObject::connectSlotsByName(this); } ~InstallJavaPage() { delete horizontalLayout; delete majorVersionSelect; delete javaVersionSelect; } //! loads the list if needed. void initialize(Meta::VersionList::Ptr vlist) { vlist->setProvidedRoles({ BaseVersionList::JavaMajorRole, BaseVersionList::RecommendedRole, BaseVersionList::VersionPointerRole }); majorVersionSelect->initialize(vlist.get()); } void setSelectedVersion(BaseVersion::Ptr version) { auto dcast = std::dynamic_pointer_cast(version); if (!dcast) { return; } javaVersionSelect->initialize(new Java::VersionList(dcast, this)); javaVersionSelect->selectCurrent(); } QString id() const override { return uid; } QString displayName() const override { return name; } QIcon icon() const override { return QIcon::fromTheme(iconName); } void openedImpl() override { if (loaded) return; const auto versions = APPLICATION->metadataIndex()->get(uid); if (!versions) return; initialize(versions); loaded = true; } void setParentContainer(BasePageContainer* container) override { auto dialog = dynamic_cast(dynamic_cast(container)->parent()); connect(javaVersionSelect->view(), &QAbstractItemView::doubleClicked, dialog, &QDialog::accept); } BaseVersion::Ptr selectedVersion() const { return javaVersionSelect->selectedVersion(); } void selectSearch() { javaVersionSelect->selectSearch(); } void loadList() { majorVersionSelect->loadList(); javaVersionSelect->loadList(); } public slots: void setRecommendedMajors(const QStringList& majors) { m_recommended_majors = majors; recommendedFilterChanged(); } void setRecommend(bool recommend) { m_recommend = recommend; recommendedFilterChanged(); } void recommendedFilterChanged() { if (m_recommend) { majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, Filters::equalsAny(m_recommended_majors)); } else { majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, Filters::equalsAny()); } } signals: void selectionChanged(); private: const QString uid; const QString iconName; const QString name; bool loaded = false; QHBoxLayout* horizontalLayout = nullptr; VersionSelectWidget* majorVersionSelect = nullptr; VersionSelectWidget* javaVersionSelect = nullptr; QStringList m_recommended_majors; bool m_recommend; }; static InstallJavaPage* pageCast(BasePage* page) { auto result = dynamic_cast(page); Q_ASSERT(result != nullptr); return result; } namespace Java { QStringList getRecommendedJavaVersionsFromVersionList(Meta::VersionList::Ptr list) { QStringList recommendedJavas; for (auto ver : list->versions()) { auto major = ver->version(); if (major.startsWith("java")) { major = "Java " + major.mid(4); } recommendedJavas.append(major); } return recommendedJavas; } InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, QWidget* parent) : QDialog(parent), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) { auto layout = new QVBoxLayout(this); // small margins look ugly on macOS on modal windows #ifndef Q_OS_MACOS layout->setContentsMargins(0, 0, 0, 0); #endif container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); layout->addWidget(container); auto buttonLayout = new QHBoxLayout(this); // small margins look ugly on macOS on modal windows #ifndef Q_OS_MACOS buttonLayout->setContentsMargins(0, 0, 6, 6); #endif auto refreshLayout = new QHBoxLayout(this); auto refreshButton = new QPushButton(tr("&Refresh"), this); connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); }); refreshLayout->addWidget(refreshButton); auto recommendedCheckBox = new QCheckBox("Recommended", this); recommendedCheckBox->setCheckState(Qt::CheckState::Checked); connect(recommendedCheckBox, &QCheckBox::stateChanged, this, [this](int state) { for (BasePage* page : container->getPages()) { pageCast(page)->setRecommend(state == Qt::Checked); } }); refreshLayout->addWidget(recommendedCheckBox); buttonLayout->addLayout(refreshLayout); buttons->setOrientation(Qt::Horizontal); buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); buttons->button(QDialogButtonBox::Ok)->setText(tr("Download")); buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonLayout->addWidget(buttons); container->addButtons(buttonLayout); setWindowTitle(dialogTitle()); setWindowModality(Qt::WindowModal); resize(840, 480); QStringList recommendedJavas; if (auto mcInst = dynamic_cast(instance); mcInst) { auto mc = mcInst->getPackProfile()->getComponent("net.minecraft"); if (mc) { auto file = mc->getVersionFile(); // no need for load as it should already be loaded if (file) { for (auto major : file->compatibleJavaMajors) { recommendedJavas.append(QString("Java %1").arg(major)); } } } } else { const auto versions = APPLICATION->metadataIndex()->get("net.minecraft.java"); if (versions) { if (versions->isLoaded()) { recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); } else { auto newTask = versions->getLoadTask(); if (newTask) { connect(newTask.get(), &Task::succeeded, this, [this, versions] { auto recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); for (BasePage* page : container->getPages()) { pageCast(page)->setRecommendedMajors(recommendedJavas); } }); if (!newTask->isRunning()) newTask->start(); } else { recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); } } } } for (BasePage* page : container->getPages()) { if (page->id() == uid) container->selectPage(page->id()); auto cast = pageCast(page); cast->setRecommend(true); connect(cast, &InstallJavaPage::selectionChanged, this, [this, cast] { validate(cast); }); if (!recommendedJavas.isEmpty()) { cast->setRecommendedMajors(recommendedJavas); } } connect(container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* selected) { validate(selected); }); pageCast(container->selectedPage())->selectSearch(); validate(container->selectedPage()); } QList InstallDialog::getPages() { return { // Mojang new InstallJavaPage("net.minecraft.java", "mojang", tr("Mojang")), // Adoptium new InstallJavaPage("net.adoptium.java", "adoptium", tr("Adoptium")), // Azul new InstallJavaPage("com.azul.java", "azul", tr("Azul Zulu")), }; } QString InstallDialog::dialogTitle() { return tr("Install Java"); } void InstallDialog::validate(BasePage* selected) { buttons->button(QDialogButtonBox::Ok)->setEnabled(!!std::dynamic_pointer_cast(pageCast(selected)->selectedVersion())); } void InstallDialog::done(int result) { if (result == Accepted) { auto* page = pageCast(container->selectedPage()); if (page->selectedVersion()) { auto meta = std::dynamic_pointer_cast(page->selectedVersion()); if (meta) { Task::Ptr task; auto final_path = FS::PathCombine(APPLICATION->javaPath(), meta->m_name); auto deletePath = [final_path] { FS::deletePath(final_path); }; switch (meta->downloadType) { case Java::DownloadType::Manifest: task = makeShared(meta->url, final_path, meta->checksumType, meta->checksumHash); break; case Java::DownloadType::Archive: task = makeShared(meta->url, final_path, meta->checksumType, meta->checksumHash); break; case Java::DownloadType::Unknown: QString error = QString(tr("Could not determine Java download type!")); CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); deletePath(); return; } #if defined(Q_OS_MACOS) auto seq = makeShared(tr("Install Java")); seq->addTask(task); seq->addTask(makeShared(final_path)); task = seq; #endif connect(task.get(), &Task::failed, this, [this, &deletePath](QString reason) { QString error = QString("Java download failed: %1").arg(reason); CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); deletePath(); }); connect(task.get(), &Task::aborted, this, deletePath); ProgressDialog pg(this); pg.setSkipButton(true, tr("Abort")); pg.execWithTask(task.get()); } else { return; } } else { return; } } QDialog::done(result); } } // namespace Java #include "InstallJavaDialog.moc" PrismLauncher-10.0.5/launcher/ui/java/VersionList.h0000644000175100017510000000274615144136756021575 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "BaseVersionList.h" #include "java/JavaMetadata.h" #include "meta/Version.h" namespace Java { class VersionList : public BaseVersionList { Q_OBJECT public: explicit VersionList(Meta::Version::Ptr m_version, QObject* parent = 0); Task::Ptr getLoadTask() override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; QVariant data(const QModelIndex& index, int role) const override; RoleList providesRoles() const override; protected slots: void updateListData(QList) override {} protected: Meta::Version::Ptr m_version; QList m_vlist; }; } // namespace Java PrismLauncher-10.0.5/launcher/ui/java/InstallJavaDialog.h0000644000175100017510000000261615144136756022640 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "BaseInstance.h" #include "ui/pages/BasePageProvider.h" class MinecraftInstance; class PageContainer; class PackProfile; class QDialogButtonBox; namespace Java { class InstallDialog final : public QDialog, private BasePageProvider { Q_OBJECT public: explicit InstallDialog(const QString& uid = QString(), BaseInstance* instance = nullptr, QWidget* parent = nullptr); QList getPages() override; QString dialogTitle() override; void validate(BasePage* selected); void done(int result) override; private: PageContainer* container; QDialogButtonBox* buttons; }; } // namespace Java PrismLauncher-10.0.5/launcher/ui/themes/0000755000175100017510000000000015144136757017477 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/themes/CatPainter.cpp0000644000175100017510000000451515144136757022242 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ui/themes/CatPainter.h" #include #include "Application.h" CatPainter::CatPainter(const QString& path, QObject* parent) : QObject(parent) { // Attempt to load as a movie m_movie = new QMovie(path, QByteArray(), this); if (m_movie->isValid()) { // Start the animation if it's a valid movie file connect(m_movie, &QMovie::frameChanged, this, &CatPainter::updateFrame); m_movie->start(); } else { // Otherwise, load it as a static image delete m_movie; m_movie = nullptr; m_image = QPixmap(path); } } void CatPainter::paint(QPainter* painter, const QRect& viewport) { QPixmap frame = m_image; if (m_movie && m_movie->isValid()) { frame = m_movie->currentPixmap(); } auto fit = APPLICATION->settings()->get("CatFit").toString(); painter->setOpacity(APPLICATION->settings()->get("CatOpacity").toFloat() / 100); int widWidth = viewport.width(); int widHeight = viewport.height(); auto aspectMode = Qt::IgnoreAspectRatio; if (fit == "fill") { aspectMode = Qt::KeepAspectRatio; } else if (fit == "fit") { aspectMode = Qt::KeepAspectRatio; if (frame.width() < widWidth) widWidth = frame.width(); if (frame.height() < widHeight) widHeight = frame.height(); } auto pixmap = frame.scaled(widWidth, widHeight, aspectMode, Qt::SmoothTransformation); QRect rectOfPixmap = pixmap.rect(); rectOfPixmap.moveBottomRight(viewport.bottomRight()); painter->drawPixmap(rectOfPixmap.topLeft(), pixmap); painter->setOpacity(1.0); }; PrismLauncher-10.0.5/launcher/ui/themes/CustomTheme.h0000644000175100017510000000510315144136757022104 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ITheme.h" class CustomTheme : public ITheme { public: CustomTheme(ITheme* baseTheme, QFileInfo& file, bool isManifest); virtual ~CustomTheme() {} QString id() override; QString name() override; QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; QString qtTheme() override; LogColors logColorScheme() override { return m_logColors; } QStringList searchPaths() override; private: bool read(const QString& path, bool& hasCustomLogColors); QPalette m_palette; QColor m_fadeColor; double m_fadeAmount; QString m_styleSheet; QString m_name; QString m_id; QString m_widgets; QString m_qssFilePath; LogColors m_logColors; /** * The tooltip could be defined in the theme json, * or composed of other fields that could be in there. * like author, license, etc. */ QString m_tooltip = ""; }; PrismLauncher-10.0.5/launcher/ui/themes/IconTheme.cpp0000644000175100017510000000215215144136757022056 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "IconTheme.h" #include #include bool IconTheme::load() { const QString path = m_path + "/index.theme"; if (!QFile::exists(path)) return false; QSettings settings(path, QSettings::IniFormat); settings.beginGroup("Icon Theme"); m_name = settings.value("Name").toString(); settings.endGroup(); return !m_name.isNull(); } PrismLauncher-10.0.5/launcher/ui/themes/DarkTheme.cpp0000644000175100017510000000575215144136757022060 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "DarkTheme.h" #include QString DarkTheme::id() { return "dark"; } QString DarkTheme::name() { return QObject::tr("Dark"); } QPalette DarkTheme::colorScheme() { QPalette darkPalette; darkPalette.setColor(QPalette::Window, QColor(49, 49, 49)); darkPalette.setColor(QPalette::WindowText, Qt::white); darkPalette.setColor(QPalette::Base, QColor(34, 34, 34)); darkPalette.setColor(QPalette::AlternateBase, QColor(42, 42, 42)); darkPalette.setColor(QPalette::ToolTipBase, Qt::white); darkPalette.setColor(QPalette::ToolTipText, Qt::white); darkPalette.setColor(QPalette::Text, Qt::white); darkPalette.setColor(QPalette::Button, QColor(48, 48, 48)); darkPalette.setColor(QPalette::ButtonText, Qt::white); darkPalette.setColor(QPalette::BrightText, Qt::red); darkPalette.setColor(QPalette::Link, QColor(47, 163, 198)); darkPalette.setColor(QPalette::Highlight, QColor(150, 219, 89)); darkPalette.setColor(QPalette::HighlightedText, Qt::black); darkPalette.setColor(QPalette::PlaceholderText, Qt::darkGray); return fadeInactive(darkPalette, fadeAmount(), fadeColor()); } double DarkTheme::fadeAmount() { return 0.5; } QColor DarkTheme::fadeColor() { return QColor(49, 49, 49); } bool DarkTheme::hasStyleSheet() { return true; } QString DarkTheme::appStyleSheet() { return "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"; } QString DarkTheme::tooltip() { return ""; } PrismLauncher-10.0.5/launcher/ui/themes/HintOverrideProxyStyle.cpp0000644000175100017510000000276015144136757024675 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "HintOverrideProxyStyle.h" HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) { setObjectName(baseStyle()->objectName()); } int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint, const QStyleOption* option, const QWidget* widget, QStyleHintReturn* returnData) const { if (hint == QStyle::SH_ItemView_ActivateItemOnSingleClick) return 0; if (hint == QStyle::SH_Slider_AbsoluteSetButtons) return Qt::LeftButton | Qt::MiddleButton; if (hint == QStyle::SH_Slider_PageSetButtons) return Qt::RightButton; return QProxyStyle::styleHint(hint, option, widget, returnData); } PrismLauncher-10.0.5/launcher/ui/themes/ThemeManager.cpp0000644000175100017510000003002515144136757022540 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ThemeManager.h" #include #include #include #include #include #include #include #include "Exception.h" #include "ui/themes/BrightTheme.h" #include "ui/themes/CatPack.h" #include "ui/themes/CustomTheme.h" #include "ui/themes/DarkTheme.h" #include "ui/themes/SystemTheme.h" #include "Application.h" ThemeManager::ThemeManager() { QIcon::setFallbackThemeName(QIcon::themeName()); QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path()); themeDebugLog() << "Determining System Widget Theme..."; const auto& style = QApplication::style(); m_defaultStyle = style->objectName(); themeDebugLog() << "System theme seems to be:" << m_defaultStyle; m_defaultPalette = QApplication::palette(); initializeThemes(); initializeCatPacks(); } ThemeManager::~ThemeManager() { stopSettingNewWindowColorsOnMac(); } /// @brief Adds the Theme to the list of themes /// @param theme The Theme to add /// @return Theme ID QString ThemeManager::addTheme(std::unique_ptr theme) { QString id = theme->id(); if (m_themes.find(id) == m_themes.end()) m_themes.emplace(id, std::move(theme)); else themeWarningLog() << "Theme(" << id << ") not added to prevent id duplication"; return id; } /// @brief Gets the Theme from the List via ID /// @param themeId Theme ID of theme to fetch /// @return Theme at themeId ITheme* ThemeManager::getTheme(QString themeId) { return m_themes[themeId].get(); } QString ThemeManager::addIconTheme(IconTheme theme) { QString id = theme.id(); if (m_icons.find(id) == m_icons.end()) m_icons.emplace(id, std::move(theme)); else themeWarningLog() << "IconTheme(" << id << ") not added to prevent id duplication"; return id; } void ThemeManager::initializeThemes() { // Icon themes initializeIcons(); // Initialize widget themes initializeWidgets(); } void ThemeManager::initializeIcons() { // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies! // set icon theme search path! themeDebugLog() << "<> Initializing Icon Themes"; for (const QString& id : builtinIcons) { IconTheme theme(id, QString(":/icons/%1").arg(id)); if (!theme.load()) { themeWarningLog() << "Couldn't load built-in icon theme" << id; continue; } addIconTheme(std::move(theme)); themeDebugLog() << "Loaded Built-In Icon Theme" << id; } if (!m_iconThemeFolder.mkpath(".")) themeWarningLog() << "Couldn't create icon theme folder"; themeDebugLog() << "Icon Theme Folder Path:" << m_iconThemeFolder.absolutePath(); QDirIterator directoryIterator(m_iconThemeFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot); while (directoryIterator.hasNext()) { QDir dir(directoryIterator.next()); IconTheme theme(dir.dirName(), dir.path()); if (!theme.load()) continue; addIconTheme(std::move(theme)); themeDebugLog() << "Loaded Custom Icon Theme from" << dir.path(); } themeDebugLog() << "<> Icon themes initialized."; } void ThemeManager::initializeWidgets() { themeDebugLog() << "<> Initializing Widget Themes"; themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique(m_defaultStyle, m_defaultPalette, true)); auto darkThemeId = addTheme(std::make_unique()); themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); themeDebugLog() << "<> Initializing System Widget Themes"; QStringList styles = QStyleFactory::keys(); for (auto& st : styles) { #ifdef Q_OS_WINDOWS if (QSysInfo::productVersion() != "11" && st == "windows11") { continue; } #endif themeDebugLog() << "Loading System Theme:" << addTheme(std::make_unique(st, m_defaultPalette, false)); } // TODO: need some way to differentiate same name themes in different subdirectories // (maybe smaller grey text next to theme name in dropdown?) if (!m_applicationThemeFolder.mkpath(".")) themeWarningLog() << "Couldn't create theme folder"; themeDebugLog() << "Theme Folder Path:" << m_applicationThemeFolder.absolutePath(); QDirIterator directoryIterator(m_applicationThemeFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot); while (directoryIterator.hasNext()) { QDir dir(directoryIterator.next()); QFileInfo themeJson(dir.absoluteFilePath("theme.json")); if (themeJson.exists()) { // Load "theme.json" based themes themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath(); addTheme(std::make_unique(getTheme(darkThemeId), themeJson, true)); } else { // Load pure QSS Themes QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files); while (stylesheetFileIterator.hasNext()) { QFile customThemeFile(stylesheetFileIterator.next()); QFileInfo customThemeFileInfo(customThemeFile); themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath(); addTheme(std::make_unique(getTheme(darkThemeId), customThemeFileInfo, false)); } } } themeDebugLog() << "<> Widget themes initialized."; } #ifndef Q_OS_MACOS void ThemeManager::setTitlebarColorOnMac(WId windowId, QColor color) {} void ThemeManager::setTitlebarColorOfAllWindowsOnMac(QColor color) {} void ThemeManager::stopSettingNewWindowColorsOnMac() {} #endif QList ThemeManager::getValidIconThemes() { QList ret; ret.reserve(m_icons.size()); for (auto&& [id, theme] : m_icons) { ret.append(&theme); } return ret; } QList ThemeManager::getValidApplicationThemes() { QList ret; ret.reserve(m_themes.size()); for (auto&& [id, theme] : m_themes) { ret.append(theme.get()); } return ret; } QList ThemeManager::getValidCatPacks() { QList ret; ret.reserve(m_catPacks.size()); for (auto&& [id, theme] : m_catPacks) { ret.append(theme.get()); } return ret; } bool ThemeManager::isValidIconTheme(const QString& id) { return !id.isEmpty() && m_icons.find(id) != m_icons.end(); } bool ThemeManager::isValidApplicationTheme(const QString& id) { return !id.isEmpty() && m_themes.find(id) != m_themes.end(); } QDir ThemeManager::getIconThemesFolder() { return m_iconThemeFolder; } QDir ThemeManager::getApplicationThemesFolder() { return m_applicationThemeFolder; } QDir ThemeManager::getCatPacksFolder() { return m_catPacksFolder; } void ThemeManager::setIconTheme(const QString& name) { if (m_icons.find(name) == m_icons.end()) { themeWarningLog() << "Tried to set invalid icon theme:" << name; return; } QIcon::setThemeName(name); } void ThemeManager::setApplicationTheme(const QString& name, bool initial) { auto systemPalette = qApp->palette(); auto themeIter = m_themes.find(name); if (themeIter != m_themes.end()) { auto& theme = themeIter->second; themeDebugLog() << "applying theme" << theme->name(); theme->apply(initial); setTitlebarColorOfAllWindowsOnMac(qApp->palette().window().color()); m_logColors = theme->logColorScheme(); } else { themeWarningLog() << "Tried to set invalid theme:" << name; } } void ThemeManager::applyCurrentlySelectedTheme(bool initial) { auto settings = APPLICATION->settings(); setIconTheme(settings->get("IconTheme").toString()); themeDebugLog() << "<> Icon theme set."; auto applicationTheme = settings->get("ApplicationTheme").toString(); if (applicationTheme == "") { applicationTheme = m_defaultStyle; } setApplicationTheme(applicationTheme, initial); themeDebugLog() << "<> Application theme set."; } QString ThemeManager::getCatPack(QString catName) { auto catIter = m_catPacks.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString()); if (catIter != m_catPacks.end()) { auto& catPack = catIter->second; themeDebugLog() << "applying catpack" << catPack->id(); return catPack->path(); } else { themeWarningLog() << "Tried to get invalid catPack:" << catName; } return m_catPacks.begin()->second->path(); } QString ThemeManager::addCatPack(std::unique_ptr catPack) { QString id = catPack->id(); if (m_catPacks.find(id) == m_catPacks.end()) m_catPacks.emplace(id, std::move(catPack)); else themeWarningLog() << "CatPack(" << id << ") not added to prevent id duplication"; return id; } void ThemeManager::initializeCatPacks() { QList> defaultCats{ { "kitteh", QObject::tr("Background Cat (from MultiMC)") }, { "rory", QObject::tr("Rory ID 11 (drawn by Ashtaka)") }, { "rory-flat", QObject::tr("Rory ID 11 (flat edition, drawn by Ashtaka)") }, { "teawie", QObject::tr("Teawie (drawn by SympathyTea)") } }; for (auto [id, name] : defaultCats) { addCatPack(std::unique_ptr(new BasicCatPack(id, name))); } if (!m_catPacksFolder.mkpath(".")) themeWarningLog() << "Couldn't create catpacks folder"; themeDebugLog() << "CatPacks Folder Path:" << m_catPacksFolder.absolutePath(); QStringList supportedImageFormats; for (auto format : QImageReader::supportedImageFormats()) { supportedImageFormats.append("*." + format); } auto loadFiles = [this, supportedImageFormats](QDir dir) { // Load image files directly QDirIterator ImageFileIterator(dir.absoluteFilePath(""), supportedImageFormats, QDir::Files); while (ImageFileIterator.hasNext()) { QFile customCatFile(ImageFileIterator.next()); QFileInfo customCatFileInfo(customCatFile); themeDebugLog() << "Loading CatPack from:" << customCatFileInfo.absoluteFilePath(); addCatPack(std::unique_ptr(new FileCatPack(customCatFileInfo))); } }; loadFiles(m_catPacksFolder); QDirIterator directoryIterator(m_catPacksFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot); while (directoryIterator.hasNext()) { QDir dir(directoryIterator.next()); QFileInfo manifest(dir.absoluteFilePath("catpack.json")); if (manifest.isFile()) { try { // Load background manifest themeDebugLog() << "Loading background manifest from:" << manifest.absoluteFilePath(); addCatPack(std::unique_ptr(new JsonCatPack(manifest))); } catch (const Exception& e) { themeWarningLog() << "Couldn't load catpack json:" << e.cause(); } } else { loadFiles(dir); } } } void ThemeManager::refresh() { m_themes.clear(); m_icons.clear(); m_catPacks.clear(); initializeThemes(); initializeCatPacks(); } PrismLauncher-10.0.5/launcher/ui/themes/CatPack.cpp0000644000175100017510000001175715144136757021524 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ui/themes/CatPack.h" #include #include #include #include #include #include #include "FileSystem.h" #include "Json.h" QString BasicCatPack::path() const { const auto now = QDate::currentDate(); const auto birthday = QDate(now.year(), 11, 1); const auto xmas = QDate(now.year(), 12, 25); const auto halloween = QDate(now.year(), 10, 31); QString cat = QString(":/backgrounds/%1").arg(m_id); if (std::abs(now.daysTo(xmas)) <= 4) { cat += "-xmas"; } else if (std::abs(now.daysTo(halloween)) <= 4) { cat += "-spooky"; } else if (std::abs(now.daysTo(birthday)) <= 12) { cat += "-bday"; } return cat; } JsonCatPack::PartialDate partialDate(QJsonObject date) { auto month = date["month"].toInt(1); if (month > 12) month = 12; else if (month <= 0) month = 1; auto day = date["day"].toInt(1); if (day > 31) day = 31; else if (day <= 0) day = 1; return { month, day }; }; JsonCatPack::JsonCatPack(QFileInfo& manifestInfo) : BasicCatPack(manifestInfo.dir().dirName()) { QString path = manifestInfo.path(); auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "CatPack JSON file"); const auto root = doc.object(); m_name = Json::requireString(root, "name", "Catpack name"); m_default_path = FS::PathCombine(path, Json::requireString(root, "default", "Default Cat")); auto variants = root["variants"].toArray(); for (auto v : variants) { auto variant = v.toObject(); m_variants << Variant{ FS::PathCombine(path, Json::requireString(variant, "path", "Variant path")), partialDate(Json::requireObject(variant, "startTime", "Variant startTime")), partialDate(Json::requireObject(variant, "endTime", "Variant endTime")) }; } } QDate ensureDay(int year, int month, int day) { QDate date(year, month, 1); if (day > date.daysInMonth()) day = date.daysInMonth(); return QDate(year, month, day); } QString JsonCatPack::path() const { return path(QDate::currentDate()); } QString JsonCatPack::path(QDate now) const { for (auto var : m_variants) { QDate startDate = ensureDay(now.year(), var.startTime.month, var.startTime.day); QDate endDate = ensureDay(now.year(), var.endTime.month, var.endTime.day); if (startDate > endDate) { // it's spans over multiple years if (endDate < now) // end date is in the past so jump one year into the future for endDate endDate = endDate.addYears(1); else // end date is in the future so jump one year into the past for startDate startDate = startDate.addYears(-1); } if (startDate <= now && now <= endDate) return var.path; } auto dInfo = QFileInfo(m_default_path); if (!dInfo.isDir()) return m_default_path; QStringList supportedImageFormats; for (auto format : QImageReader::supportedImageFormats()) { supportedImageFormats.append("*." + format); } auto files = QDir(m_default_path).entryInfoList(supportedImageFormats, QDir::Files, QDir::Name); if (files.length() == 0) return ""; auto idx = (now.dayOfYear() - 1) % files.length(); auto isRandom = dInfo.fileName().compare("random", Qt::CaseInsensitive) == 0; if (isRandom) idx = QRandomGenerator::global()->bounded(0, files.length()); return files[idx].absoluteFilePath(); } PrismLauncher-10.0.5/launcher/ui/themes/CatPainter.h0000644000175100017510000000217015144136757021702 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include class CatPainter : public QObject { Q_OBJECT public: CatPainter(const QString& path, QObject* parent = nullptr); virtual ~CatPainter() = default; void paint(QPainter*, const QRect&); signals: void updateFrame(); private: QMovie* m_movie = nullptr; QPixmap m_image; }; PrismLauncher-10.0.5/launcher/ui/themes/IconTheme.h0000644000175100017510000000215715144136757021530 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include class IconTheme { public: IconTheme(const QString& id, const QString& path) : m_id(id), m_path(path) {} IconTheme() = default; bool load(); QString id() const { return m_id; } QString path() const { return m_path; } QString name() const { return m_name; } private: QString m_id; QString m_path; QString m_name; }; PrismLauncher-10.0.5/launcher/ui/themes/FusionTheme.cpp0000644000175100017510000000012215144136757022424 0ustar runnerrunner#include "FusionTheme.h" QString FusionTheme::qtTheme() { return "Fusion"; } PrismLauncher-10.0.5/launcher/ui/themes/DarkTheme.h0000644000175100017510000000351115144136757021514 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "FusionTheme.h" class DarkTheme : public FusionTheme { public: virtual ~DarkTheme() {} QString id() override; QString name() override; QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; }; PrismLauncher-10.0.5/launcher/ui/themes/ThemeManager.h0000644000175100017510000000646315144136757022216 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "IconTheme.h" #include "ui/themes/CatPack.h" #include "ui/themes/ITheme.h" inline auto themeDebugLog() { return qDebug() << "[Theme]"; } inline auto themeWarningLog() { return qWarning() << "[Theme]"; } class ThemeManager { public: ThemeManager(); ~ThemeManager(); QList getValidIconThemes(); QList getValidApplicationThemes(); bool isValidIconTheme(const QString& id); bool isValidApplicationTheme(const QString& id); QDir getIconThemesFolder(); QDir getApplicationThemesFolder(); QDir getCatPacksFolder(); void applyCurrentlySelectedTheme(bool initial = false); void setIconTheme(const QString& name); void setApplicationTheme(const QString& name, bool initial = false); /// @brief Returns the background based on selected and with events (Birthday, XMas, etc.) /// @param catName Optional, if you need a specific background. /// @return QString getCatPack(QString catName = ""); QList getValidCatPacks(); const LogColors& getLogColors() { return m_logColors; } void refresh(); private: std::map> m_themes; std::map m_icons; QDir m_iconThemeFolder{"iconthemes"}; QDir m_applicationThemeFolder{"themes"}; QDir m_catPacksFolder{"catpacks"}; std::map> m_catPacks; QPalette m_defaultPalette; QString m_defaultStyle; LogColors m_logColors; void initializeThemes(); void initializeCatPacks(); QString addTheme(std::unique_ptr theme); ITheme* getTheme(QString themeId); QString addIconTheme(IconTheme theme); QString addCatPack(std::unique_ptr catPack); void initializeIcons(); void initializeWidgets(); // On non-Mac systems, this is a no-op. void setTitlebarColorOnMac(WId windowId, QColor color); // This also will set the titlebar color of newly opened windows after this method is called. // On non-Mac systems, this is a no-op. void setTitlebarColorOfAllWindowsOnMac(QColor color); // On non-Mac systems, this is a no-op. void stopSettingNewWindowColorsOnMac(); #ifdef Q_OS_MACOS NSObject* m_windowTitlebarObserver = nullptr; #endif const QStringList builtinIcons{"pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", "OSX", "iOS", "flat", "flat_white", "multimc"}; }; PrismLauncher-10.0.5/launcher/ui/themes/BrightTheme.h0000644000175100017510000000351515144136757022056 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "FusionTheme.h" class BrightTheme : public FusionTheme { public: virtual ~BrightTheme() {} QString id() override; QString name() override; QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; }; PrismLauncher-10.0.5/launcher/ui/themes/ITheme.h0000644000175100017510000000500315144136757021021 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Tayou * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include class QStyle; struct LogColors { QMap background; QMap foreground; }; // TODO: rename to Theme; this is not an interface as it contains method implementations // TODO: make methods const class ITheme { public: virtual ~ITheme() {} virtual void apply(bool initial); virtual QString id() = 0; virtual QString name() = 0; virtual QString tooltip() = 0; virtual bool hasStyleSheet() = 0; virtual QString appStyleSheet() = 0; virtual QString qtTheme() = 0; virtual QPalette colorScheme() = 0; virtual QColor fadeColor() = 0; virtual double fadeAmount() = 0; virtual LogColors logColorScheme() { return defaultLogColors(colorScheme()); } virtual QStringList searchPaths() { return {}; } static QPalette fadeInactive(QPalette in, qreal bias, QColor color); static LogColors defaultLogColors(const QPalette& palette); }; PrismLauncher-10.0.5/launcher/ui/themes/SystemTheme.cpp0000644000175100017510000001042315144136757022452 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "SystemTheme.h" #include #include #include #include "HintOverrideProxyStyle.h" #include "ThemeManager.h" // See https://github.com/MultiMC/Launcher/issues/1790 // or https://github.com/PrismLauncher/PrismLauncher/issues/490 static const QStringList S_NATIVE_STYLES{ "windows11", "windowsvista", "macos", "system", "windows" }; SystemTheme::SystemTheme(const QString& styleName, const QPalette& defaultPalette, bool isDefaultTheme) { m_themeName = isDefaultTheme ? "system" : styleName; m_widgetTheme = styleName; // NOTE: SystemTheme is reconstructed on page refresh. We can't accurately determine the system palette here // See also S_NATIVE_STYLES comment if (S_NATIVE_STYLES.contains(m_themeName)) { m_colorPalette = defaultPalette; } else { auto style = QStyleFactory::create(styleName); m_colorPalette = style != nullptr ? style->standardPalette() : defaultPalette; delete style; } } void SystemTheme::apply(bool initial) { // See S_NATIVE_STYLES comment if (initial && S_NATIVE_STYLES.contains(m_themeName)) { QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme()))); return; } ITheme::apply(initial); } QString SystemTheme::id() { return m_themeName; } QString SystemTheme::name() { if (m_themeName.toLower() == "windowsvista") { return QObject::tr("Windows Vista"); } else if (m_themeName.toLower() == "windows") { return QObject::tr("Windows 9x"); } else if (m_themeName.toLower() == "windows11") { return QObject::tr("Windows 11"); } else if (m_themeName.toLower() == "system") { return QObject::tr("System"); } else { return m_themeName; } } QString SystemTheme::tooltip() { if (m_themeName.toLower() == "windowsvista") { return QObject::tr("Widget style trying to look like your win32 theme"); } else if (m_themeName.toLower() == "windows") { return QObject::tr("Windows 9x inspired widget style"); } else if (m_themeName.toLower() == "windows11") { return QObject::tr("WinUI 3 inspired Qt widget style"); } else if (m_themeName.toLower() == "fusion") { return QObject::tr("The default Qt widget style"); } else if (m_themeName.toLower() == "system") { return QObject::tr("Your current system theme"); } else { return ""; } } QString SystemTheme::qtTheme() { return m_widgetTheme; } QPalette SystemTheme::colorScheme() { return m_colorPalette; } QString SystemTheme::appStyleSheet() { return QString(); } double SystemTheme::fadeAmount() { return 0.5; } QColor SystemTheme::fadeColor() { return QColor(128, 128, 128); } bool SystemTheme::hasStyleSheet() { return false; } PrismLauncher-10.0.5/launcher/ui/themes/FusionTheme.h0000644000175100017510000000022415144136757022074 0ustar runnerrunner#pragma once #include "ITheme.h" class FusionTheme : public ITheme { public: virtual ~FusionTheme() {} QString qtTheme() override; }; PrismLauncher-10.0.5/launcher/ui/themes/BrightTheme.cpp0000644000175100017510000000561315144136757022412 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "BrightTheme.h" #include QString BrightTheme::id() { return "bright"; } QString BrightTheme::name() { return QObject::tr("Bright"); } QPalette BrightTheme::colorScheme() { QPalette brightPalette; brightPalette.setColor(QPalette::Window, QColor(255, 255, 255)); brightPalette.setColor(QPalette::WindowText, QColor(17, 17, 17)); brightPalette.setColor(QPalette::Base, QColor(250, 250, 250)); brightPalette.setColor(QPalette::AlternateBase, QColor(240, 240, 240)); brightPalette.setColor(QPalette::ToolTipBase, QColor(17, 17, 17)); brightPalette.setColor(QPalette::ToolTipText, QColor(255, 255, 255)); brightPalette.setColor(QPalette::Text, Qt::black); brightPalette.setColor(QPalette::Button, QColor(249, 249, 249)); brightPalette.setColor(QPalette::ButtonText, Qt::black); brightPalette.setColor(QPalette::BrightText, Qt::red); brightPalette.setColor(QPalette::Link, QColor(37, 137, 164)); brightPalette.setColor(QPalette::Highlight, QColor(137, 207, 84)); brightPalette.setColor(QPalette::HighlightedText, Qt::black); return fadeInactive(brightPalette, fadeAmount(), fadeColor()); } double BrightTheme::fadeAmount() { return 0.5; } QColor BrightTheme::fadeColor() { return QColor(255, 255, 255); } bool BrightTheme::hasStyleSheet() { return false; } QString BrightTheme::appStyleSheet() { return QString(); } QString BrightTheme::tooltip() { return QString(); } PrismLauncher-10.0.5/launcher/ui/themes/ITheme.cpp0000644000175100017510000000710515144136757021361 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Tayou * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ITheme.h" #include #include #include "Application.h" #include "HintOverrideProxyStyle.h" #include "rainbow.h" void ITheme::apply(bool) { APPLICATION->setStyleSheet(QString()); QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme()))); QApplication::setPalette(colorScheme()); APPLICATION->setStyleSheet(appStyleSheet()); QDir::setSearchPaths("theme", searchPaths()); } QPalette ITheme::fadeInactive(QPalette in, qreal bias, QColor color) { auto blend = [&in, bias, color](QPalette::ColorRole role) { QColor from = in.color(QPalette::Active, role); QColor blended = Rainbow::mix(from, color, bias); in.setColor(QPalette::Disabled, role, blended); }; blend(QPalette::Window); blend(QPalette::WindowText); blend(QPalette::Base); blend(QPalette::AlternateBase); blend(QPalette::ToolTipBase); blend(QPalette::ToolTipText); blend(QPalette::Text); blend(QPalette::Button); blend(QPalette::ButtonText); blend(QPalette::BrightText); blend(QPalette::Link); blend(QPalette::Highlight); blend(QPalette::HighlightedText); return in; } LogColors ITheme::defaultLogColors(const QPalette& palette) { LogColors result; const QColor& bg = palette.color(QPalette::Base); const QColor& fg = palette.color(QPalette::Text); auto blend = [bg, fg](QColor color) { if (Rainbow::luma(fg) > Rainbow::luma(bg)) { // for dark color schemes, produce a fitting color first color = Rainbow::tint(fg, color, 0.5); } // adapt contrast return Rainbow::mix(fg, color, 1); }; result.background[MessageLevel::Fatal] = Qt::black; result.foreground[MessageLevel::Launcher] = blend(QColor("purple")); result.foreground[MessageLevel::Debug] = blend(QColor("green")); result.foreground[MessageLevel::Warning] = blend(QColor("orange")); result.foreground[MessageLevel::Error] = blend(QColor("red")); result.foreground[MessageLevel::Fatal] = blend(QColor("red")); return result; } PrismLauncher-10.0.5/launcher/ui/themes/CustomTheme.cpp0000644000175100017510000002366415144136757022453 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "CustomTheme.h" #include #include #include "ThemeManager.h" const char* themeFile = "theme.json"; /// @param baseTheme Base Theme /// @param fileInfo FileInfo object for file to load /// @param isManifest whether to load a theme manifest or a qss file CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest) { if (isManifest) { m_id = fileInfo.dir().dirName(); QString path = FS::PathCombine("themes", m_id); QString pathResources = FS::PathCombine("themes", m_id, "resources"); if (!FS::ensureFolderPathExists(path)) { themeWarningLog() << "Theme directory for" << m_id << "could not be created. This theme might be invalid"; return; } if (!FS::ensureFolderPathExists(pathResources)) { themeWarningLog() << "Resources directory for" << m_id << "could not be created"; } auto themeFilePath = FS::PathCombine(path, themeFile); m_palette = baseTheme->colorScheme(); bool hasCustomLogColors = false; if (read(themeFilePath, hasCustomLogColors)) { // If theme data was found, fade "Disabled" color of each role according to FadeAmount m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor); if (!hasCustomLogColors) m_logColors = defaultLogColors(m_palette); } else { themeDebugLog() << "Did not read theme json file correctly, not changing theme, keeping previous."; m_logColors = defaultLogColors(m_palette); return; } auto qssFilePath = FS::PathCombine(path, m_qssFilePath); QFileInfo info(qssFilePath); if (info.isFile()) { try { // TODO: validate qss? m_styleSheet = QString::fromUtf8(FS::read(qssFilePath)); } catch (const Exception& e) { themeWarningLog() << "Couldn't load qss:" << e.cause() << "from" << qssFilePath; return; } } else { themeDebugLog() << "No theme qss present."; } } else { m_id = fileInfo.fileName(); m_name = fileInfo.baseName(); QString path = fileInfo.filePath(); // themeDebugLog << "Theme ID: " << m_id; // themeDebugLog << "Theme Name: " << m_name; // themeDebugLog << "Theme Path: " << path; if (!FS::ensureFilePathExists(path)) { themeWarningLog().nospace() << m_name << ": Theme file path doesn't exist!"; m_palette = baseTheme->colorScheme(); m_styleSheet = baseTheme->appStyleSheet(); return; } m_palette = baseTheme->colorScheme(); try { // TODO: validate qss? m_styleSheet = QString::fromUtf8(FS::read(path)); } catch (const Exception& e) { themeWarningLog() << "Couldn't load qss:" << e.cause() << "from" << path; m_styleSheet = baseTheme->appStyleSheet(); } } } QStringList CustomTheme::searchPaths() { QString pathResources = FS::PathCombine("themes", m_id, "resources"); if (QFileInfo::exists(pathResources)) return { pathResources }; return {}; } QString CustomTheme::id() { return m_id; } QString CustomTheme::name() { return m_name; } QPalette CustomTheme::colorScheme() { return m_palette; } bool CustomTheme::hasStyleSheet() { return true; } QString CustomTheme::appStyleSheet() { return m_styleSheet; } double CustomTheme::fadeAmount() { return m_fadeAmount; } QColor CustomTheme::fadeColor() { return m_fadeColor; } QString CustomTheme::qtTheme() { return m_widgets; } QString CustomTheme::tooltip() { return m_tooltip; } bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) { QFileInfo pathInfo(path); if (pathInfo.exists() && pathInfo.isFile()) { try { auto doc = Json::requireDocument(path, "Theme JSON file"); const QJsonObject root = doc.object(); m_name = Json::requireString(root, "name", "Theme name"); m_widgets = Json::requireString(root, "widgets", "Qt widget theme"); m_qssFilePath = root["qssFilePath"].toString("themeStyle.css"); auto readColor = [](const QJsonObject& colors, const QString& colorName) -> QColor { auto colorValue = colors[colorName].toString(); if (!colorValue.isEmpty()) { QColor color(colorValue); if (!color.isValid()) { themeWarningLog() << "Color value" << colorValue << "for" << colorName << "was not recognized."; return {}; } return color; } return {}; }; if (root.contains("colors")) { auto colorsRoot = Json::requireObject(root, "colors"); auto readAndSetPaletteColor = [this, readColor, colorsRoot](QPalette::ColorRole role, const QString& colorName) { auto color = readColor(colorsRoot, colorName); if (color.isValid()) { m_palette.setColor(role, color); } else { themeDebugLog() << "Color value for" << colorName << "was not present."; } }; // palette readAndSetPaletteColor(QPalette::Window, "Window"); readAndSetPaletteColor(QPalette::WindowText, "WindowText"); readAndSetPaletteColor(QPalette::Base, "Base"); readAndSetPaletteColor(QPalette::AlternateBase, "AlternateBase"); readAndSetPaletteColor(QPalette::ToolTipBase, "ToolTipBase"); readAndSetPaletteColor(QPalette::ToolTipText, "ToolTipText"); readAndSetPaletteColor(QPalette::Text, "Text"); readAndSetPaletteColor(QPalette::Button, "Button"); readAndSetPaletteColor(QPalette::ButtonText, "ButtonText"); readAndSetPaletteColor(QPalette::BrightText, "BrightText"); readAndSetPaletteColor(QPalette::Link, "Link"); readAndSetPaletteColor(QPalette::Highlight, "Highlight"); readAndSetPaletteColor(QPalette::HighlightedText, "HighlightedText"); // fade m_fadeColor = readColor(colorsRoot, "fadeColor"); m_fadeAmount = colorsRoot["fadeAmount"].toDouble(0.5); } if (root.contains("logColors")) { hasCustomLogColors = true; auto logColorsRoot = Json::requireObject(root, "logColors"); auto readAndSetLogColor = [this, readColor, logColorsRoot](MessageLevel level, bool fg, const QString& colorName) { auto color = readColor(logColorsRoot, colorName); if (color.isValid()) { if (fg) m_logColors.foreground[level] = color; else m_logColors.background[level] = color; } else { themeDebugLog() << "Color value for" << colorName << "was not present."; } }; readAndSetLogColor(MessageLevel::Message, false, "MessageHighlight"); readAndSetLogColor(MessageLevel::Launcher, false, "LauncherHighlight"); readAndSetLogColor(MessageLevel::Debug, false, "DebugHighlight"); readAndSetLogColor(MessageLevel::Warning, false, "WarningHighlight"); readAndSetLogColor(MessageLevel::Error, false, "ErrorHighlight"); readAndSetLogColor(MessageLevel::Fatal, false, "FatalHighlight"); readAndSetLogColor(MessageLevel::Message, true, "Message"); readAndSetLogColor(MessageLevel::Launcher, true, "Launcher"); readAndSetLogColor(MessageLevel::Debug, true, "Debug"); readAndSetLogColor(MessageLevel::Warning, true, "Warning"); readAndSetLogColor(MessageLevel::Error, true, "Error"); readAndSetLogColor(MessageLevel::Fatal, true, "Fatal"); } } catch (const Exception& e) { themeWarningLog() << "Couldn't load theme json:" << e.cause(); return false; } } else { themeDebugLog() << "No theme json present."; return false; } return true; } PrismLauncher-10.0.5/launcher/ui/themes/SystemTheme.h0000644000175100017510000000411015144136757022113 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "ITheme.h" class SystemTheme : public ITheme { public: SystemTheme(const QString& styleName, const QPalette& defaultPalette, bool isDefaultTheme); virtual ~SystemTheme() {} void apply(bool initial) override; QString id() override; QString name() override; QString tooltip() override; QString qtTheme() override; bool hasStyleSheet() override; QString appStyleSheet() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; private: QPalette m_colorPalette; QString m_widgetTheme; QString m_themeName; }; PrismLauncher-10.0.5/launcher/ui/themes/ThemeManager.mm0000644000175100017510000000635515144136757022400 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Kenneth Chew <79120643+kthchew@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ThemeManager.h" #include void ThemeManager::setTitlebarColorOnMac(WId windowId, QColor color) { if (windowId == 0) { return; } NSView* view = (NSView*)windowId; NSWindow* window = [view window]; window.titlebarAppearsTransparent = YES; window.backgroundColor = [NSColor colorWithRed:color.redF() green:color.greenF() blue:color.blueF() alpha:color.alphaF()]; // Unfortunately there seems to be no easy way to set the titlebar text color. // The closest we can do without dubious hacks is set the dark/light mode state based on the brightness of the // background color, which should at least make the text readable even if we can't use the theme's text color. // It's a good idea to set this anyway since it also affects some other UI elements like text shadows (PrismLauncher#3825). if (color.lightnessF() < 0.5) { window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; } else { window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; } } void ThemeManager::setTitlebarColorOfAllWindowsOnMac(QColor color) { NSArray* windows = [NSApp windows]; for (NSWindow* window : windows) { setTitlebarColorOnMac((WId)window.contentView, color); } // We want to change the titlebar color of newly opened windows as well. // There's no notification for when a new window is opened, but we can set the color when a window switches // from occluded to visible, which also fires on open. NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; stopSettingNewWindowColorsOnMac(); m_windowTitlebarObserver = [center addObserverForName:NSWindowDidChangeOcclusionStateNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* notification) { NSWindow* window = notification.object; setTitlebarColorOnMac((WId)window.contentView, color); }]; } void ThemeManager::stopSettingNewWindowColorsOnMac() { if (m_windowTitlebarObserver) { NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; [center removeObserver:m_windowTitlebarObserver]; m_windowTitlebarObserver = nil; } } PrismLauncher-10.0.5/launcher/ui/themes/CatPack.h0000644000175100017510000000553415144136757021165 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include class CatPack { public: virtual ~CatPack() {} virtual QString id() const = 0; virtual QString name() const = 0; virtual QString path() const = 0; }; class BasicCatPack : public CatPack { public: BasicCatPack(QString id, QString name) : m_id(id), m_name(name) {} BasicCatPack(QString id) : BasicCatPack(id, id) {} virtual QString id() const override { return m_id; } virtual QString name() const override { return m_name; } virtual QString path() const override; protected: QString m_id; QString m_name; }; class FileCatPack : public BasicCatPack { public: FileCatPack(QString id, QFileInfo& fileInfo) : BasicCatPack(id), m_path(fileInfo.absoluteFilePath()) {} FileCatPack(QFileInfo& fileInfo) : FileCatPack(fileInfo.baseName(), fileInfo) {} virtual QString path() const { return m_path; } private: QString m_path; }; class JsonCatPack : public BasicCatPack { public: struct PartialDate { int month; int day; }; struct Variant { QString path; PartialDate startTime; PartialDate endTime; }; JsonCatPack(QFileInfo& manifestInfo); virtual QString path() const override; QString path(QDate now) const; private: QString m_default_path; QList m_variants; }; PrismLauncher-10.0.5/launcher/ui/themes/HintOverrideProxyStyle.h0000644000175100017510000000233615144136757024341 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include /// Used to override platform-specific behaviours which the launcher does work well with. class HintOverrideProxyStyle : public QProxyStyle { Q_OBJECT public: explicit HintOverrideProxyStyle(QStyle* style); int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override; }; PrismLauncher-10.0.5/launcher/ui/ToolTipFilter.cpp0000644000175100017510000000167715144136756021470 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Mark Deneen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "ToolTipFilter.h" bool ToolTipFilter::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() == QEvent::ToolTip) { return true; } else { return QObject::eventFilter(obj, ev); } } PrismLauncher-10.0.5/launcher/ui/pagedialog/0000755000175100017510000000000015144136756020305 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pagedialog/PageDialog.h0000644000175100017510000000210715144136756022452 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ui/pages/BasePageProvider.h" class PageContainer; class PageDialog : public QDialog { Q_OBJECT public: explicit PageDialog(BasePageProvider* pageProvider, QString defaultId = QString(), QWidget* parent = 0); virtual ~PageDialog() {} signals: void applied(); private: void accept() override; void closeEvent(QCloseEvent* event) override; bool handleClose(); private: PageContainer* m_container; }; PrismLauncher-10.0.5/launcher/ui/pagedialog/PageDialog.cpp0000644000175100017510000000534015144136756023007 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PageDialog.h" #include #include #include #include #include "Application.h" #include "ui/widgets/PageContainer.h" PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidget* parent) : QDialog(parent) { setWindowTitle(pageProvider->dialogTitle()); m_container = new PageContainer(pageProvider, std::move(defaultId), this); auto* mainLayout = new QVBoxLayout(this); auto* focusStealer = new QPushButton(this); mainLayout->addWidget(focusStealer); focusStealer->setDefault(true); focusStealer->hide(); mainLayout->addWidget(m_container); mainLayout->setSpacing(0); mainLayout->setContentsMargins(0, 0, 0, 0); setLayout(mainLayout); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); buttons->button(QDialogButtonBox::Ok)->setText(tr("&OK")); buttons->button(QDialogButtonBox::Cancel)->setText(tr("&Cancel")); buttons->button(QDialogButtonBox::Help)->setText(tr("Help")); buttons->setContentsMargins(0, 0, 6, 6); m_container->addButtons(buttons); connect(buttons->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &PageDialog::accept); connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &PageDialog::reject); connect(buttons->button(QDialogButtonBox::Help), &QPushButton::clicked, m_container, &PageContainer::help); restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toString().toUtf8())); } void PageDialog::accept() { if (handleClose()) QDialog::accept(); } void PageDialog::closeEvent(QCloseEvent* event) { if (handleClose()) QDialog::closeEvent(event); } bool PageDialog::handleClose() { qDebug() << "Paged dialog close requested"; if (!m_container->prepareToClose()) return false; qDebug() << "Paged dialog close approved"; APPLICATION->settings()->set("PagedGeometry", QString::fromUtf8(saveGeometry().toBase64())); qDebug() << "Paged dialog geometry saved"; emit applied(); return true; } PrismLauncher-10.0.5/launcher/ui/InstanceWindow.h0000644000175100017510000000563715144136756021331 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "LaunchController.h" #include "launch/LaunchTask.h" #include "ui/pages/BasePageContainer.h" #include "QObjectPtr.h" class QPushButton; class PageContainer; class InstanceWindow : public QMainWindow, public BasePageContainer { Q_OBJECT public: explicit InstanceWindow(InstancePtr proc, QWidget* parent = 0); virtual ~InstanceWindow() = default; bool selectPage(QString pageId) override; BasePage* selectedPage() const override; void refreshContainer() override; QString instanceId(); // save all settings and changes (prepare for launch) bool saveAll(); // request closing the window (from a page) bool requestClose() override; signals: void isClosing(); private slots: void instanceLaunchTaskChanged(shared_qobject_ptr proc); void runningStateChanged(bool running); void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus); protected: void closeEvent(QCloseEvent*) override; private: void updateButtons(); private: shared_qobject_ptr m_proc; InstancePtr m_instance; bool m_doNotSave = false; PageContainer* m_container = nullptr; QPushButton* m_closeButton = nullptr; QToolButton* m_launchButton = nullptr; QPushButton* m_killButton = nullptr; }; PrismLauncher-10.0.5/launcher/ui/widgets/0000755000175100017510000000000015144136757017660 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/widgets/VersionSelectWidget.h0000644000175100017510000000707215144136757023770 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "BaseVersionList.h" #include "Filter.h" #include "VersionListView.h" class VersionProxyModel; class VersionListView; class QVBoxLayout; class QProgressBar; class VersionSelectWidget : public QWidget { Q_OBJECT public: explicit VersionSelectWidget(QWidget* parent); ~VersionSelectWidget(); //! loads the list if needed. void initialize(BaseVersionList* vlist, bool forceLoad = false); //! Starts a task that loads the list. void loadList(); bool hasVersions() const; BaseVersion::Ptr selectedVersion() const; void selectRecommended(); void selectCurrent(); void selectSearch(); VersionListView* view(); void setCurrentVersion(const QString& version); void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); void setExactFilter(BaseVersionList::ModelRoles role, QString filter); void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter); void setFilter(BaseVersionList::ModelRoles role, Filter filter); void setEmptyString(QString emptyString); void setEmptyErrorString(QString emptyErrorString); void setEmptyMode(VersionListView::EmptyMode mode); void setResizeOn(int column); bool eventFilter(QObject* watched, QEvent* event) override; signals: void selectedVersionChanged(BaseVersion::Ptr version); protected: virtual void closeEvent(QCloseEvent*) override; private slots: void onTaskSucceeded(); void onTaskFailed(const QString& reason); void changeProgress(qint64 current, qint64 total); void currentRowChanged(const QModelIndex& current, const QModelIndex&); private: void preselect(); private: QString m_currentVersion; BaseVersionList* m_vlist = nullptr; VersionProxyModel* m_proxyModel = nullptr; int resizeOnColumn = 0; Task::Ptr m_load_task; bool preselectedAlready = false; QVBoxLayout* verticalLayout = nullptr; VersionListView* listView = nullptr; QLineEdit* search; QProgressBar* sneakyProgressBar = nullptr; }; PrismLauncher-10.0.5/launcher/ui/widgets/ModFilterWidget.cpp0000644000175100017510000003574215144136757023430 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModFilterWidget.h" #include #include #include #include #include #include "BaseVersionList.h" #include "Json.h" #include "Version.h" #include "meta/Index.h" #include "modplatform/ModIndex.h" #include "ui/widgets/CheckComboBox.h" #include "ui_ModFilterWidget.h" #include "Application.h" #include "minecraft/PackProfile.h" std::unique_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended) { return std::unique_ptr(new ModFilterWidget(instance, extended)); } class VersionBasicModel : public QIdentityProxyModel { Q_OBJECT public: explicit VersionBasicModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override { if (role == Qt::DisplayRole) return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); if (role == Qt::UserRole) return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); return {}; } }; class AllVersionProxyModel : public QSortFilterProxyModel { Q_OBJECT public: AllVersionProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} int rowCount(const QModelIndex& parent = QModelIndex()) const override { return QSortFilterProxyModel::rowCount(parent) + 1; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override { if (!index.isValid()) { return {}; } if (index.row() == 0) { if (role == Qt::DisplayRole) { return tr("All Versions"); } if (role == Qt::UserRole) { return "all"; } return {}; } QModelIndex newIndex = QSortFilterProxyModel::index(index.row() - 1, index.column()); return QSortFilterProxyModel::data(newIndex, role); } Qt::ItemFlags flags(const QModelIndex& index) const override { if (index.row() == 0) { return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } return QSortFilterProxyModel::flags(index); } }; ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended) : QTabWidget(), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) { ui->setupUi(this); m_versions_proxy = new VersionProxyModel(this); m_versions_proxy->setFilter(BaseVersionList::TypeRole, Filters::equals("release")); QAbstractProxyModel* proxy = new VersionBasicModel(this); proxy->setSourceModel(m_versions_proxy); if (extended) { if (!m_instance) { ui->environmentGroup->hide(); } ui->versions->setSourceModel(proxy); ui->versions->setSeparator(", "); ui->versions->setDefaultText(tr("All Versions")); ui->version->hide(); } else { auto allVersions = new AllVersionProxyModel(this); allVersions->setSourceModel(proxy); proxy = allVersions; ui->version->setModel(proxy); ui->versions->hide(); ui->showAllVersions->hide(); ui->environmentGroup->hide(); ui->openSource->hide(); } ui->versions->setStyleSheet("combobox-popup: 0;"); ui->version->setStyleSheet("combobox-popup: 0;"); connect(ui->showAllVersions, &QCheckBox::stateChanged, this, &ModFilterWidget::onShowAllVersionsChanged); connect(ui->versions, &QComboBox::currentIndexChanged, this, &ModFilterWidget::onVersionFilterChanged); connect(ui->versions, &CheckComboBox::checkedItemsChanged, this, [this] { onVersionFilterChanged(0); }); connect(ui->version, &QComboBox::currentTextChanged, this, &ModFilterWidget::onVersionFilterTextChanged); connect(ui->neoForge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->liteLoader, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->babric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->btaBabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->legacyFabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->ornithe, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->rift, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->showMoreButton, &QPushButton::clicked, this, &ModFilterWidget::onShowMoreClicked); if (!extended) { ui->showMoreButton->setVisible(false); ui->extendedModLoadersWidget->setVisible(false); } if (extended) { connect(ui->clientSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); connect(ui->serverSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); } connect(ui->hideInstalled, &QCheckBox::stateChanged, this, &ModFilterWidget::onHideInstalledFilterChanged); connect(ui->openSource, &QCheckBox::stateChanged, this, &ModFilterWidget::onOpenSourceFilterChanged); connect(ui->releaseCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); connect(ui->betaCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); connect(ui->alphaCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); connect(ui->unknownCb, &QCheckBox::stateChanged, this, &ModFilterWidget::onReleaseFilterChanged); setHidden(true); loadVersionList(); prepareBasicFilter(); } auto ModFilterWidget::getFilter() -> std::shared_ptr { m_filter_changed = false; return m_filter; } ModFilterWidget::~ModFilterWidget() { delete ui; } void ModFilterWidget::loadVersionList() { m_version_list = APPLICATION->metadataIndex()->get("net.minecraft"); if (!m_version_list->isLoaded()) { QEventLoop load_version_list_loop; QTimer time_limit_for_list_load; time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer); time_limit_for_list_load.setSingleShot(true); time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit); time_limit_for_list_load.start(4000); auto task = m_version_list->getLoadTask(); connect(task.get(), &Task::failed, [this] { ui->versions->setEnabled(false); ui->showAllVersions->setEnabled(false); }); connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit); if (!task->isRunning()) task->start(); load_version_list_loop.exec(); if (time_limit_for_list_load.isActive()) time_limit_for_list_load.stop(); } m_versions_proxy->setSourceModel(m_version_list.get()); } void ModFilterWidget::prepareBasicFilter() { m_filter->openSource = false; if (m_instance) { m_filter->hideInstalled = false; m_filter->side = ModPlatform::Side::NoSide; // or "both" ModPlatform::ModLoaderTypes loaders; if (m_instance->settings()->get("OverrideModDownloadLoaders").toBool()) { for (auto loader : Json::toStringList(m_instance->settings()->get("ModDownloadLoaders").toString())) { loaders |= ModPlatform::getModLoaderFromString(loader); } } else { loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); } ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); ui->forge->setChecked(loaders & ModPlatform::Forge); ui->fabric->setChecked(loaders & ModPlatform::Fabric); ui->quilt->setChecked(loaders & ModPlatform::Quilt); ui->liteLoader->setChecked(loaders & ModPlatform::LiteLoader); ui->babric->setChecked(loaders & ModPlatform::Babric); ui->btaBabric->setChecked(loaders & ModPlatform::BTA); ui->legacyFabric->setChecked(loaders & ModPlatform::LegacyFabric); ui->ornithe->setChecked(loaders & ModPlatform::Ornithe); ui->rift->setChecked(loaders & ModPlatform::Rift); m_filter->loaders = loaders; auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); m_filter->versions.emplace_front(def); ui->versions->setCheckedItems({ def }); ui->version->setCurrentIndex(ui->version->findText(def)); } else { ui->hideInstalled->hide(); } } void ModFilterWidget::onShowAllVersionsChanged() { if (ui->showAllVersions->isChecked()) m_versions_proxy->clearFilters(); else m_versions_proxy->setFilter(BaseVersionList::TypeRole, Filters::equals("release")); } void ModFilterWidget::onVersionFilterChanged(int) { auto versions = ui->versions->checkedItems(); versions.sort(); std::list current_list; for (const QString& version : versions) current_list.emplace_back(version); m_filter_changed = m_filter->versions.size() != current_list.size() || !std::equal(m_filter->versions.begin(), m_filter->versions.end(), current_list.begin(), current_list.end()); m_filter->versions = current_list; if (m_filter_changed) emit filterChanged(); } void ModFilterWidget::onLoadersFilterChanged() { ModPlatform::ModLoaderTypes loaders; if (ui->neoForge->isChecked()) loaders |= ModPlatform::NeoForge; if (ui->forge->isChecked()) loaders |= ModPlatform::Forge; if (ui->fabric->isChecked()) loaders |= ModPlatform::Fabric; if (ui->quilt->isChecked()) loaders |= ModPlatform::Quilt; if (ui->liteLoader->isChecked()) loaders |= ModPlatform::LiteLoader; if (ui->babric->isChecked()) loaders |= ModPlatform::Babric; if (ui->btaBabric->isChecked()) loaders |= ModPlatform::BTA; if (ui->legacyFabric->isChecked()) loaders |= ModPlatform::LegacyFabric; if (ui->ornithe->isChecked()) loaders |= ModPlatform::Ornithe; if (ui->rift->isChecked()) loaders |= ModPlatform::Rift; m_filter_changed = loaders != m_filter->loaders; m_filter->loaders = loaders; if (m_filter_changed) emit filterChanged(); } void ModFilterWidget::onSideFilterChanged() { ModPlatform::Side side; if (ui->clientSide->isChecked() && !ui->serverSide->isChecked()) { side = ModPlatform::Side::ClientSide; } else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked()) { side = ModPlatform::Side::ServerSide; } else if (ui->clientSide->isChecked() && ui->serverSide->isChecked()) { side = ModPlatform::Side::UniversalSide; } else { side = ModPlatform::Side::NoSide; } m_filter_changed = side != m_filter->side; m_filter->side = side; if (m_filter_changed) emit filterChanged(); } void ModFilterWidget::onHideInstalledFilterChanged() { auto hide = ui->hideInstalled->isChecked(); m_filter_changed = hide != m_filter->hideInstalled; m_filter->hideInstalled = hide; if (m_filter_changed) emit filterChanged(); } void ModFilterWidget::onVersionFilterTextChanged(const QString& version) { m_filter->versions.clear(); if (ui->version->currentData(Qt::UserRole) != "all") { m_filter->versions.emplace_back(version); } m_filter_changed = true; emit filterChanged(); } void ModFilterWidget::setCategories(const QList& categories) { m_categories = categories; delete ui->categoryGroup->layout(); auto layout = new QVBoxLayout(ui->categoryGroup); for (const auto& category : categories) { auto name = category.name; name.replace("-", " "); name.replace("&", "&&"); auto checkbox = new QCheckBox(name); auto font = checkbox->font(); font.setCapitalization(QFont::Capitalize); checkbox->setFont(font); layout->addWidget(checkbox); const QString id = category.id; connect(checkbox, &QCheckBox::toggled, this, [this, id](bool checked) { if (checked) m_filter->categoryIds.append(id); else m_filter->categoryIds.removeOne(id); m_filter_changed = true; emit filterChanged(); }); } } void ModFilterWidget::onOpenSourceFilterChanged() { auto open = ui->openSource->isChecked(); m_filter_changed = open != m_filter->openSource; m_filter->openSource = open; if (m_filter_changed) emit filterChanged(); } void ModFilterWidget::onReleaseFilterChanged() { std::list releases; if (ui->releaseCb->isChecked()) releases.push_back(ModPlatform::IndexedVersionType::Release); if (ui->betaCb->isChecked()) releases.push_back(ModPlatform::IndexedVersionType::Beta); if (ui->alphaCb->isChecked()) releases.push_back(ModPlatform::IndexedVersionType::Alpha); if (ui->unknownCb->isChecked()) releases.push_back(ModPlatform::IndexedVersionType::Unknown); m_filter_changed = releases != m_filter->releases; m_filter->releases = releases; if (m_filter_changed) emit filterChanged(); } void ModFilterWidget::onShowMoreClicked() { ui->extendedModLoadersWidget->setVisible(true); ui->showMoreButton->setVisible(false); } #include "ModFilterWidget.moc" PrismLauncher-10.0.5/launcher/ui/widgets/LabeledToolButton.h0000644000175100017510000000203615144136757023414 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class QLabel; class LabeledToolButton : public QToolButton { Q_OBJECT QLabel* m_label; QIcon m_icon; public: LabeledToolButton(QWidget* parent = 0); QString text() const; void setText(const QString& text); void setIcon(QIcon icon); virtual QSize sizeHint() const; protected: void resizeEvent(QResizeEvent* event); void resetIcon(); }; PrismLauncher-10.0.5/launcher/ui/widgets/ModListView.h0000644000175100017510000000161615144136757022243 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class ModListView : public QTreeView { Q_OBJECT public: explicit ModListView(QWidget* parent = 0); virtual void setModel(QAbstractItemModel* model); virtual void setResizeModes(const QList& modes); }; PrismLauncher-10.0.5/launcher/ui/widgets/ProgressWidget.h0000644000175100017510000000264715144136757023012 0ustar runnerrunner// Licensed under the Apache-2.0 license. See README.md for details. #pragma once #include #include class Task; class QProgressBar; class QLabel; class ProgressWidget : public QWidget { Q_OBJECT public: explicit ProgressWidget(QWidget* parent = nullptr, bool show_label = true); /** Whether to hide the widget automatically if it's watching no running task. */ void hideIfInactive(bool hide) { m_hide_if_inactive = hide; } /** Reset the displayed progress to 0 */ void reset(); /** The text that shows up in the middle of the progress bar. * By default it's '%p%', with '%p' being the total progress in percentage. */ void progressFormat(QString); public slots: /** Watch the progress of a task. */ void watch(Task* task); /** Watch the progress of a task, and start it if needed */ void start(Task* task); /** Blocking way of waiting for a task to finish. */ bool exec(std::shared_ptr task); /** Un-hide the widget if needed. */ void show(); /** Make the widget invisible. */ void hide(); private slots: void handleTaskFinish(); void handleTaskStatus(const QString& status); void handleTaskProgress(qint64 current, qint64 total); void taskDestroyed(); private: QLabel* m_label = nullptr; QProgressBar* m_bar = nullptr; Task* m_task = nullptr; bool m_hide_if_inactive = false; }; PrismLauncher-10.0.5/launcher/ui/widgets/ProjectItem.cpp0000644000175100017510000001641715144136757022622 0ustar runnerrunner#include "ProjectItem.h" #include #include #include #include #include "Common.h" ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {} void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { painter->save(); QStyleOptionViewItem opt(option); initStyleOption(&opt, index); auto isInstalled = index.data(UserDataTypes::INSTALLED).toBool(); auto isChecked = opt.checkState == Qt::Checked; auto isSelected = option.state & QStyle::State_Selected; const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); auto rect = opt.rect; bool windows = style->objectName().startsWith("windows"); if (!windows) style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); if (isSelected) { if (windows) painter->fillRect(rect, opt.palette.highlight()); painter->setPen(opt.palette.highlightedText().color()); } if (opt.features & QStyleOptionViewItem::HasCheckIndicator) { QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &checkboxOpt, painter, opt.widget); rect.setX(checkboxOpt.rect.right()); } if (!isSelected && !isChecked && isInstalled) { painter->setOpacity(0.4); // Fade out the entire item } // The default icon size will be a square (and height is usually the lower value). auto icon_width = rect.height(), icon_height = rect.height(); int icon_x_margin = (rect.height() - icon_width) / 2; int icon_y_margin = (rect.height() - icon_height) / 2; if (!opt.icon.isNull()) { // Icon painting { auto icon_size = opt.decorationSize; icon_width = icon_size.width(); icon_height = icon_size.height(); icon_y_margin = (rect.height() - icon_height) / 2; icon_x_margin = icon_y_margin; // use same margins for consistency } // Centralize icon with a margin to separate from the other elements int x = rect.x() + icon_x_margin; int y = rect.y() + icon_y_margin; if (opt.features & QStyleOptionViewItem::HasCheckIndicator) rect.translate(icon_x_margin / 2, 0); // Prevent 'scaling null pixmap' warnings if (icon_width > 0 && icon_height > 0) opt.icon.paint(painter, x, y, icon_width, icon_height); } // Change the rect so that funther painting is easier auto remaining_width = rect.width() - icon_width - 2 * icon_x_margin; rect.setRect(rect.x() + icon_width + 2 * icon_x_margin, rect.y(), remaining_width, rect.height()); int title_height = 0; { // Title painting auto title = index.data(UserDataTypes::TITLE).toString(); painter->save(); auto font = opt.font; if (isChecked) { font.setBold(true); } if (isInstalled) { title = tr("%1 [installed]").arg(title); } font.setPointSize(font.pointSize() + 2); painter->setFont(font); title_height = QFontMetrics(font).height(); // On the top, aligned to the left after the icon painter->drawText(rect.x(), rect.y() + title_height, title); painter->restore(); } { // Description painting auto description = index.data(UserDataTypes::DESCRIPTION).toString().simplified(); QTextLayout text_layout(description, opt.font); qreal height = 0; auto cut_text = viewItemTextLayout(text_layout, remaining_width, height); // Get first line unconditionally description = cut_text.first().second; auto num_lines = 1; // Get second line, elided if needed if (cut_text.size() > 1) { // 2.5x so because there should be some margin left from the 2x so things don't get too squishy. if (rect.height() - title_height <= 2.5 * opt.fontMetrics.height()) { // If there's not enough space, show only a single line, elided. description = opt.fontMetrics.elidedText(description, opt.textElideMode, cut_text.at(0).first); } else { if (cut_text.size() > 2) { description += opt.fontMetrics.elidedText(cut_text.at(1).second, opt.textElideMode, cut_text.at(1).first); } else { description += cut_text.at(1).second; } num_lines += 1; } } int description_x = rect.x(); // Have the y-value be set based on the number of lines in the description, to centralize the // description text with the space between the base and the title. int description_y = rect.y() + title_height + (rect.height() - title_height) / 2; if (num_lines == 1) description_y -= opt.fontMetrics.height() / 2; else description_y -= opt.fontMetrics.height(); // On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare) painter->drawText(description_x, description_y, remaining_width, cut_text.size() * opt.fontMetrics.height(), Qt::TextWordWrap, description); } painter->restore(); } bool ProjectItemDelegate::editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) { if (!(event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick)) return false; auto mouseEvent = (QMouseEvent*)event; if (mouseEvent->button() != Qt::LeftButton) return false; QStyleOptionViewItem opt(option); initStyleOption(&opt, index); const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); const QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); if (!checkboxOpt.rect.contains(mouseEvent->pos().x(), mouseEvent->pos().y())) return false; // swallow other events // (prevents item being selected or double click action triggering) if (event->type() != QEvent::MouseButtonRelease) return true; emit checkboxClicked(index); return true; } QStyleOptionViewItem ProjectItemDelegate::makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const { QStyleOptionViewItem checkboxOpt = opt; checkboxOpt.state &= ~QStyle::State_HasFocus; if (checkboxOpt.checkState == Qt::Checked) checkboxOpt.state |= QStyle::State_On; else checkboxOpt.state |= QStyle::State_Off; QRect checkboxRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &checkboxOpt, opt.widget); // 5px is the typical top margin for image // we don't want the checkboxes to be all over the place :) checkboxOpt.rect = QRect(opt.rect.x() + 5, opt.rect.y() + (opt.rect.height() / 2 - checkboxRect.height() / 2), checkboxRect.width(), checkboxRect.height()); return checkboxOpt; } PrismLauncher-10.0.5/launcher/ui/widgets/ModListView.cpp0000644000175100017510000000443415144136757022577 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModListView.h" #include #include #include #include #include ModListView::ModListView(QWidget* parent) : QTreeView(parent) { setAllColumnsShowFocus(true); setExpandsOnDoubleClick(false); setRootIsDecorated(false); setSortingEnabled(true); setAlternatingRowColors(true); setSelectionMode(QAbstractItemView::ExtendedSelection); setHeaderHidden(false); setSelectionBehavior(QAbstractItemView::SelectRows); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); setDropIndicatorShown(true); setDragEnabled(true); setDragDropMode(QAbstractItemView::DropOnly); viewport()->setAcceptDrops(true); } void ModListView::setModel(QAbstractItemModel* model) { QTreeView::setModel(model); auto head = header(); head->setStretchLastSection(false); // HACK: this is true for the checkbox column of mod lists auto string = model->headerData(0, head->orientation()).toString(); if (head->count() < 1) { return; } if (!string.size()) { head->setSectionResizeMode(0, QHeaderView::Interactive); head->setSectionResizeMode(1, QHeaderView::Stretch); for (int i = 2; i < head->count(); i++) head->setSectionResizeMode(i, QHeaderView::Interactive); } else { head->setSectionResizeMode(0, QHeaderView::Stretch); for (int i = 1; i < head->count(); i++) head->setSectionResizeMode(i, QHeaderView::Interactive); } } void ModListView::setResizeModes(const QList& modes) { auto head = header(); for (int i = 0; i < modes.count(); i++) { head->setSectionResizeMode(i, modes[i]); } } PrismLauncher-10.0.5/launcher/ui/widgets/EnvironmentVariables.cpp0000644000175100017510000000724115144136757024525 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "Application.h" #include "EnvironmentVariables.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_EnvironmentVariables.h" EnvironmentVariables::EnvironmentVariables(QWidget* parent) : QWidget(parent), ui(new Ui::EnvironmentVariables) { ui->setupUi(this); ui->list->installEventFilter(this); ui->list->sortItems(0, Qt::AscendingOrder); ui->list->setSortingEnabled(true); ui->list->header()->resizeSections(QHeaderView::Interactive); ui->list->header()->resizeSection(0, 200); connect(ui->add, &QPushButton::clicked, this, [this] { auto item = new QTreeWidgetItem(ui->list); item->setText(0, "ENV_VAR"); item->setText(1, "value"); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->list->addTopLevelItem(item); ui->list->selectionModel()->select(ui->list->model()->index(ui->list->indexOfTopLevelItem(item), 0), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::SelectionFlag::Rows); ui->list->editItem(item); }); connect(ui->remove, &QPushButton::clicked, this, [this] { for (QTreeWidgetItem* item : ui->list->selectedItems()) ui->list->takeTopLevelItem(ui->list->indexOfTopLevelItem(item)); }); connect(ui->clear, &QPushButton::clicked, this, [this] { ui->list->clear(); }); connect(ui->overrideCheckBox, &QCheckBox::toggled, ui->settingsWidget, &QWidget::setEnabled); } EnvironmentVariables::~EnvironmentVariables() { delete ui; } void EnvironmentVariables::initialize(bool instance, bool override, const QMap& value) { // update widgets to settings ui->overrideCheckBox->setVisible(instance); ui->overrideCheckBox->setChecked(override); // populate ui->list->clear(); for (auto iter = value.begin(); iter != value.end(); iter++) { auto item = new QTreeWidgetItem(ui->list); item->setText(0, iter.key()); item->setText(1, iter.value().toString()); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->list->addTopLevelItem(item); } } bool EnvironmentVariables::eventFilter(QObject* watched, QEvent* event) { if (watched == ui->list && event->type() == QEvent::KeyPress) { const QKeyEvent* keyEvent = (QKeyEvent*)event; if (keyEvent->key() == Qt::Key_Delete) { emit ui->remove->clicked(); return true; } } return QObject::eventFilter(watched, event); } void EnvironmentVariables::retranslate() { ui->retranslateUi(this); } bool EnvironmentVariables::override() const { return ui->overrideCheckBox->isChecked(); } QMap EnvironmentVariables::value() const { QMap result; QTreeWidgetItem* item = ui->list->topLevelItem(0); for (int i = 1; item != nullptr; item = ui->list->topLevelItem(i++)) result[item->text(0)] = item->text(1); return result; } PrismLauncher-10.0.5/launcher/ui/widgets/ModFilterWidget.h0000644000175100017510000001054115144136757023063 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "Version.h" #include "VersionProxyModel.h" #include "meta/VersionList.h" #include "minecraft/MinecraftInstance.h" #include "modplatform/ModIndex.h" class MinecraftInstance; namespace Ui { class ModFilterWidget; } class ModFilterWidget : public QTabWidget { Q_OBJECT public: struct Filter { std::list versions; std::list releases; ModPlatform::ModLoaderTypes loaders; ModPlatform::Side side; bool hideInstalled; QStringList categoryIds; bool openSource; bool operator==(const Filter& other) const { return hideInstalled == other.hideInstalled && side == other.side && loaders == other.loaders && versions == other.versions && releases == other.releases && categoryIds == other.categoryIds && openSource == other.openSource; } bool operator!=(const Filter& other) const { return !(*this == other); } bool checkMcVersions(QStringList value) { for (auto mcVersion : versions) if (value.contains(mcVersion.toString())) return true; return versions.empty(); } bool checkModpackFilters(const ModPlatform::IndexedVersion& v) { return ((!loaders || !v.loaders || loaders & v.loaders) && // loaders (releases.empty() || // releases std::find(releases.cbegin(), releases.cend(), v.version_type) != releases.cend()) && checkMcVersions({ v.mcVersion })); // gameVersion} } }; static std::unique_ptr create(MinecraftInstance* instance, bool extended); virtual ~ModFilterWidget(); auto getFilter() -> std::shared_ptr; auto changed() const -> bool { return m_filter_changed; } signals: void filterChanged(); public slots: void setCategories(const QList&); private: ModFilterWidget(MinecraftInstance* instance, bool extendedSupport); void loadVersionList(); void prepareBasicFilter(); private slots: void onVersionFilterChanged(int); void onVersionFilterTextChanged(const QString& version); void onLoadersFilterChanged(); void onSideFilterChanged(); void onHideInstalledFilterChanged(); void onShowAllVersionsChanged(); void onOpenSourceFilterChanged(); void onReleaseFilterChanged(); void onShowMoreClicked(); private: Ui::ModFilterWidget* ui; MinecraftInstance* m_instance = nullptr; std::shared_ptr m_filter; bool m_filter_changed = false; Meta::VersionList::Ptr m_version_list; VersionProxyModel* m_versions_proxy = nullptr; QList m_categories; }; PrismLauncher-10.0.5/launcher/ui/widgets/WideBar.h0000644000175100017510000000401015144136757021341 0ustar runnerrunner#pragma once #include #include #include #include #include class WideBar : public QToolBar { Q_OBJECT // Why: so we can enable / disable alt shortcuts in toolbuttons // with toolbuttons using setDefaultAction, theres no alt shortcuts Q_PROPERTY(bool useDefaultAction MEMBER m_use_default_action) public: explicit WideBar(const QString& title, QWidget* parent = nullptr); explicit WideBar(QWidget* parent = nullptr); ~WideBar() override = default; void addAction(QAction* action); void addSeparator(); void insertSpacer(QAction* action); void insertSeparator(QAction* before); void insertActionBefore(QAction* before, QAction* action); void insertActionAfter(QAction* after, QAction* action); void insertWidgetBefore(QAction* before, QWidget* widget); QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString()); void showVisibilityMenu(const QPoint&); void addContextMenuAction(QAction* action); // Ideally we would use a QBitArray for this, but it doesn't support string conversion, // so using it in settings is very messy. QByteArray getVisibilityState() const; void setVisibilityState(QByteArray&&); void removeAction(QAction* action); private: struct BarEntry { enum class Type { None, Action, Separator, Spacer } type = Type::None; QAction* bar_action = nullptr; QAction* menu_action = nullptr; }; auto getMatching(QAction* act) -> QList::iterator; /** Used to distinguish between versions of the WideBar with different actions */ QByteArray getHash() const; bool checkHash(QByteArray const&) const; private: QList m_entries; QList m_context_menu_actions; bool m_use_default_action = false; // Menu to toggle visibility from buttons in the bar std::unique_ptr m_bar_menu = nullptr; enum class MenuState { Fresh, Dirty } m_menu_state = MenuState::Dirty; }; PrismLauncher-10.0.5/launcher/ui/widgets/PageContainer.h0000644000175100017510000000713715144136757022560 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "ui/pages/BasePageContainer.h" #include "ui/pages/BasePageProvider.h" class QLayout; class IconLabel; class QSortFilterProxyModel; class PageModel; class QLabel; class QListView; class QLineEdit; class QStackedLayout; class QGridLayout; class PageContainer : public QWidget, public BasePageContainer { Q_OBJECT public: explicit PageContainer(BasePageProvider* pageProvider, QString defaultId = QString(), QWidget* parent = 0); virtual ~PageContainer() {} void addButtons(QWidget* buttons); void addButtons(QLayout* buttons); void useSidebarStyle(bool sidebar); /* * Save any unsaved state and prepare to be closed. * @return true if everything can be saved, false if there is something that requires attention */ bool prepareToClose(); bool saveAll(); /* request close - used by individual pages */ bool requestClose() override { if (m_container) { return m_container->requestClose(); } return false; } bool selectPage(QString pageId) override; BasePage* selectedPage() const override; BasePage* getPage(QString pageId) override; const QList& getPages() const; void refreshContainer() override; virtual void setParentContainer(BasePageContainer* container) { m_container = container; }; void changeEvent(QEvent*) override; void hidePageList() { m_pageList->hide(); } private: void createUI(); void retranslate(); public slots: void help(); signals: /** Emitted when the currently selected page is changed */ void selectedPageChanged(BasePage* previous, BasePage* selected); private slots: void currentChanged(const QModelIndex& current); void showPage(int row); private: BasePageContainer* m_container = nullptr; BasePage* m_currentPage = 0; QSortFilterProxyModel* m_proxyModel; PageModel* m_model; QStackedLayout* m_pageStack; QListView* m_pageList; QLabel* m_header; QGridLayout* m_layout; }; PrismLauncher-10.0.5/launcher/ui/widgets/InfoFrame.cpp0000644000175100017510000003172215144136757022237 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include "InfoFrame.h" #include "ui_InfoFrame.h" #include "ui/dialogs/CustomMessageBox.h" void setupLinkToolTip(QLabel* label) { QObject::connect(label, &QLabel::linkHovered, [label](const QString& link) { if (auto url = QUrl(link); !url.isValid() || (url.scheme() != "http" && url.scheme() != "https")) return; label->setToolTip(link); }); } InfoFrame::InfoFrame(QWidget* parent) : QFrame(parent), ui(new Ui::InfoFrame) { ui->setupUi(this); ui->descriptionLabel->setHidden(true); ui->nameLabel->setHidden(true); ui->licenseLabel->setHidden(true); ui->issueTrackerLabel->setHidden(true); setupLinkToolTip(ui->iconLabel); setupLinkToolTip(ui->descriptionLabel); setupLinkToolTip(ui->nameLabel); setupLinkToolTip(ui->licenseLabel); setupLinkToolTip(ui->issueTrackerLabel); updateHiddenState(); } InfoFrame::~InfoFrame() { delete ui; } void InfoFrame::updateWithMod(Mod const& m) { if (m.type() == ResourceType::FOLDER) { clear(); return; } QString text = ""; QString name = ""; QString link = m.homepage(); if (m.name().isEmpty()) name = m.internal_id(); else name = m.name(); if (link.isEmpty()) text = name; else { text = "" + name + ""; } if (!m.authors().isEmpty()) text += " by " + m.authors().join(", "); setName(text); if (m.description().isEmpty()) { setDescription(QString()); } else { setDescription(m.description()); } setImage(m.icon({ 64, 64 })); auto licenses = m.licenses(); QString licenseText = ""; if (!licenses.empty()) { for (auto l : licenses) { if (!licenseText.isEmpty()) { licenseText += "\n"; // add newline between licenses } if (!l.name.isEmpty()) { if (l.url.isEmpty()) { licenseText += l.name; } else { licenseText += "" + l.name + ""; } } else if (!l.url.isEmpty()) { licenseText += "" + l.url + ""; } if (!l.description.isEmpty() && l.description != l.name) { licenseText += " " + l.description; } } } if (!licenseText.isEmpty()) { setLicense(tr("License: %1").arg(licenseText)); } else { setLicense(); } QString issueTracker = ""; if (!m.issueTracker().isEmpty()) { issueTracker += tr("Report issues to: "); issueTracker += "" + m.issueTracker() + ""; } setIssueTracker(issueTracker); } void InfoFrame::updateWithResource(const Resource& resource) { const QString homepage = resource.homepage(); if (!homepage.isEmpty()) setName("" + resource.name() + ""); else setName(resource.name()); setImage(); } QString InfoFrame::renderColorCodes(QString input) { // We have to manually set the colors for use. // // A color is set using §x, with x = a hex number from 0 to f. // // We traverse the description and, when one of those is found, we create // a span element with that color set. // // TODO: Wrap links inside tags // https://minecraft.wiki/w/Formatting_codes#Color_codes const QMap color_codes_map = { { '0', "#000000" }, { '1', "#0000AA" }, { '2', "#00AA00" }, { '3', "#00AAAA" }, { '4', "#AA0000" }, { '5', "#AA00AA" }, { '6', "#FFAA00" }, { '7', "#AAAAAA" }, { '8', "#555555" }, { '9', "#5555FF" }, { 'a', "#55FF55" }, { 'b', "#55FFFF" }, { 'c', "#FF5555" }, { 'd', "#FF55FF" }, { 'e', "#FFFF55" }, { 'f', "#FFFFFF" } }; // https://minecraft.wiki/w/Formatting_codes#Formatting_codes const QMap formatting_codes_map = { { 'l', "b" }, { 'm', "s" }, { 'n', "u" }, { 'o', "i" } }; QString html(""); QList tags{}; auto it = input.constBegin(); while (it != input.constEnd()) { // is current char § and is there a following char if (*it == u'§' && (it + 1) != input.constEnd()) { auto const& code = *(++it); // incrementing here! auto const color_entry = color_codes_map.constFind(code); auto const tag_entry = formatting_codes_map.constFind(code); if (color_entry != color_codes_map.constEnd()) { // color code html += QString("").arg(color_entry.value()); tags << "span"; } else if (tag_entry != formatting_codes_map.constEnd()) { // formatting code html += QString("<%1>").arg(tag_entry.value()); tags << tag_entry.value(); } else if (code == 'r') { // reset all formatting while (!tags.isEmpty()) { html += QString("").arg(tags.takeLast()); } } else { // pass unknown codes through html += QString("§%1").arg(code); } } else { html += *it; } it++; } while (!tags.isEmpty()) { html += QString("").arg(tags.takeLast()); } html += ""; html.replace("\n", "
"); return html; } void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) { QString name = renderColorCodes(resource_pack.name()); const QString homepage = resource_pack.homepage(); if (!homepage.isEmpty()) { name = "
" + name + ""; } setName(name); setDescription(renderColorCodes(resource_pack.description())); setImage(resource_pack.image({ 64, 64 })); } void InfoFrame::updateWithDataPack(DataPack& data_pack) { setName(renderColorCodes(data_pack.name())); setDescription(renderColorCodes(data_pack.description())); setImage(data_pack.image({ 64, 64 })); } void InfoFrame::updateWithTexturePack(TexturePack& texture_pack) { QString name = renderColorCodes(texture_pack.name()); const QString homepage = texture_pack.homepage(); if (!homepage.isEmpty()) { name = "" + name + ""; } setName(name); setDescription(renderColorCodes(texture_pack.description())); setImage(texture_pack.image({ 64, 64 })); } void InfoFrame::clear() { setName(); setDescription(); setImage(); setLicense(); setIssueTracker(); } void InfoFrame::updateHiddenState() { if (ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden() && ui->licenseLabel->isHidden() && ui->issueTrackerLabel->isHidden()) { setHidden(true); } else { setHidden(false); } } void InfoFrame::setName(QString text) { if (text.isEmpty()) { ui->nameLabel->setHidden(true); } else { ui->nameLabel->setText(text); ui->nameLabel->setHidden(false); } updateHiddenState(); } void InfoFrame::setDescription(QString text) { if (text.isEmpty()) { ui->descriptionLabel->setHidden(true); updateHiddenState(); return; } else { ui->descriptionLabel->setHidden(false); updateHiddenState(); } ui->descriptionLabel->setToolTip(""); QString intermediatetext = text.trimmed(); bool prev(false); QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); for (const QChar& c : intermediatetext) { if (c == rem && prev) { continue; } prev = c == rem; finaltext += c; } QString labeltext; labeltext.reserve(300); // elide rich text by getting characters without formatting const int maxCharacterElide = 290; QTextDocument doc; doc.setHtml(text); if (doc.characterCount() > maxCharacterElide) { ui->descriptionLabel->setOpenExternalLinks(false); ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText); // This allows injecting HTML here. m_description = text; // move the cursor to the character elide, doesn't see html QTextCursor cursor(&doc); cursor.movePosition(QTextCursor::End); cursor.setPosition(maxCharacterElide, QTextCursor::KeepAnchor); cursor.removeSelectedText(); // insert the post fix at the cursor cursor.insertHtml("..."); labeltext.append(doc.toHtml()); connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); } else { ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } ui->descriptionLabel->setText(labeltext); } void InfoFrame::setLicense(QString text) { if (text.isEmpty()) { ui->licenseLabel->setHidden(true); updateHiddenState(); return; } else { ui->licenseLabel->setHidden(false); updateHiddenState(); } ui->licenseLabel->setToolTip(""); QString intermediatetext = text.trimmed(); bool prev(false); QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); for (const QChar& c : intermediatetext) { if (c == rem && prev) { continue; } prev = c == rem; finaltext += c; } QString labeltext; labeltext.reserve(300); if (finaltext.length() > 290) { ui->licenseLabel->setOpenExternalLinks(false); ui->licenseLabel->setTextFormat(Qt::TextFormat::RichText); m_license = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); } else { ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); } ui->licenseLabel->setText(labeltext); } void InfoFrame::setIssueTracker(QString text) { if (text.isEmpty()) { ui->issueTrackerLabel->setHidden(true); } else { ui->issueTrackerLabel->setText(text); ui->issueTrackerLabel->setHidden(false); } updateHiddenState(); } void InfoFrame::setImage(QPixmap img) { if (img.isNull()) { ui->iconLabel->setHidden(true); } else { ui->iconLabel->setHidden(false); ui->iconLabel->setPixmap(img); } } void InfoFrame::descriptionEllipsisHandler([[maybe_unused]] QString link) { if (!m_current_box) { m_current_box = CustomMessageBox::selectable(this, "", m_description); connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); m_current_box->show(); } else { m_current_box->setText(m_description); } } void InfoFrame::licenseEllipsisHandler([[maybe_unused]] QString link) { if (!m_current_box) { m_current_box = CustomMessageBox::selectable(this, "", m_license); connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed); m_current_box->show(); } else { m_current_box->setText(m_license); } } void InfoFrame::boxClosed([[maybe_unused]] int result) { m_current_box = nullptr; } PrismLauncher-10.0.5/launcher/ui/widgets/LogView.h0000644000175100017510000000211415144136757021403 0ustar runnerrunner#pragma once #include #include class QAbstractItemModel; class LogView : public QPlainTextEdit { Q_OBJECT public: explicit LogView(QWidget* parent = nullptr); virtual ~LogView(); virtual void setModel(QAbstractItemModel* model); QAbstractItemModel* model() const; public slots: void setWordWrap(bool wrapping); void setColorLines(bool colorLines); void findNext(const QString& what, bool reverse); void scrollToBottom(); protected slots: void repopulate(); // note: this supports only appending void rowsInserted(const QModelIndex& parent, int first, int last); void rowsAboutToBeInserted(const QModelIndex& parent, int first, int last); // note: this supports only removing from front void rowsRemoved(const QModelIndex& parent, int first, int last); void modelDestroyed(QObject* model); protected: QAbstractItemModel* m_model = nullptr; QTextCharFormat* m_defaultFormat = nullptr; bool m_scroll = false; bool m_scrolling = false; bool m_colorLines = true; }; PrismLauncher-10.0.5/launcher/ui/widgets/LanguageSelectionWidget.h0000644000175100017510000000244315144136757024571 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include class QVBoxLayout; class QTreeView; class QLabel; class Setting; class QCheckBox; class LanguageSelectionWidget : public QWidget { Q_OBJECT public: explicit LanguageSelectionWidget(QWidget* parent = 0); virtual ~LanguageSelectionWidget() {}; QString getSelectedLanguageKey() const; void retranslate(); protected slots: void languageRowChanged(const QModelIndex& current, const QModelIndex& previous); void languageSettingChanged(const Setting&, const QVariant&); private: QVBoxLayout* verticalLayout = nullptr; QTreeView* languageView = nullptr; QLabel* helpUsLabel = nullptr; QCheckBox* formatCheckbox = nullptr; }; PrismLauncher-10.0.5/launcher/ui/widgets/Common.h0000644000175100017510000000064015144136757021261 0ustar runnerrunner#pragma once #include /** Cuts out the text in textLayout into smaller pieces, according to the lineWidth. * Returns a list of pairs, each containing the width of that line and that line's string, respectively. * The total height of those lines is set in the last argument, 'height'. */ QList> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height); PrismLauncher-10.0.5/launcher/ui/widgets/ProjectItem.h0000644000175100017510000000156415144136757022264 0ustar runnerrunner#pragma once #include /* Custom data types for our custom list models :) */ enum UserDataTypes { TITLE = 257, // QString DESCRIPTION = 258, // QString INSTALLED = 259 // bool }; /** This is an item delegate composed of: * - An Icon on the left * - A title * - A description * */ class ProjectItemDelegate final : public QStyledItemDelegate { Q_OBJECT public: ProjectItemDelegate(QWidget* parent); void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) override; signals: void checkboxClicked(const QModelIndex& index); private: QStyleOptionViewItem makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const; }; PrismLauncher-10.0.5/launcher/ui/widgets/CustomCommands.cpp0000644000175100017510000000520015144136757023315 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "CustomCommands.h" #include "ui_CustomCommands.h" CustomCommands::~CustomCommands() { delete ui; } CustomCommands::CustomCommands(QWidget* parent) : QWidget(parent), ui(new Ui::CustomCommands) { ui->setupUi(this); connect(ui->overrideCheckBox, &QCheckBox::toggled, ui->customCommandsWidget, &QWidget::setEnabled); } void CustomCommands::initialize(bool checkable, bool checked, const QString& prelaunch, const QString& wrapper, const QString& postexit) { ui->overrideCheckBox->setVisible(checkable); if (checkable) { ui->overrideCheckBox->setChecked(checked); } ui->preLaunchCmdTextBox->setText(prelaunch); ui->wrapperCmdTextBox->setText(wrapper); ui->postExitCmdTextBox->setText(postexit); } void CustomCommands::retranslate() { ui->retranslateUi(this); } bool CustomCommands::checked() const { return ui->overrideCheckBox->isChecked(); } QString CustomCommands::prelaunchCommand() const { return ui->preLaunchCmdTextBox->text(); } QString CustomCommands::wrapperCommand() const { return ui->wrapperCmdTextBox->text(); } QString CustomCommands::postexitCommand() const { return ui->postExitCmdTextBox->text(); } PrismLauncher-10.0.5/launcher/ui/widgets/JavaSettingsWidget.h0000644000175100017510000000435515144136757023606 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "BaseInstance.h" #include "JavaCommon.h" namespace Ui { class JavaSettingsWidget; } class JavaSettingsWidget : public QWidget { Q_OBJECT public: explicit JavaSettingsWidget(QWidget* parent = nullptr) : JavaSettingsWidget(nullptr, nullptr) {} explicit JavaSettingsWidget(InstancePtr instance, QWidget* parent = nullptr); ~JavaSettingsWidget() override; void loadSettings(); void saveSettings(); private slots: void onJavaBrowse(); void onJavaAutodetect(); void onJavaTest(); void updateThresholds(); private: InstancePtr m_instance; Ui::JavaSettingsWidget* m_ui; unique_qobject_ptr m_checker; }; PrismLauncher-10.0.5/launcher/ui/widgets/AppearanceWidget.cpp0000644000175100017510000002434415144136757023576 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 TheKodeToad * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AppearanceWidget.h" #include "ui_AppearanceWidget.h" #include #include #include "BuildConfig.h" #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent) : QWidget(parent), m_ui(new Ui::AppearanceWidget), m_themesOnly(themesOnly) { m_ui->setupUi(this); m_ui->catPreview->setGraphicsEffect(new QGraphicsOpacityEffect(this)); m_defaultFormat = QTextCharFormat(m_ui->consolePreview->currentCharFormat()); if (themesOnly) { m_ui->catPackLabel->hide(); m_ui->catPackComboBox->hide(); m_ui->catPackFolder->hide(); m_ui->settingsBox->hide(); m_ui->consolePreview->hide(); m_ui->catPreview->hide(); loadThemeSettings(); } else { loadSettings(); loadThemeSettings(); updateConsolePreview(); updateCatPreview(); } connect(m_ui->fontSizeBox, &QSpinBox::valueChanged, this, &AppearanceWidget::updateConsolePreview); connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearanceWidget::updateConsolePreview); connect(m_ui->iconsComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyIconTheme); connect(m_ui->widgetStyleComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyWidgetTheme); connect(m_ui->catPackComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyCatTheme); connect(m_ui->catOpacitySlider, &QAbstractSlider::valueChanged, this, &AppearanceWidget::updateCatPreview); connect(m_ui->iconsFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); }); connect(m_ui->widgetStyleFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); connect(m_ui->catPackFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); connect(m_ui->reloadThemesButton, &QPushButton::pressed, this, &AppearanceWidget::loadThemeSettings); } AppearanceWidget::~AppearanceWidget() { delete m_ui; } void AppearanceWidget::applySettings() { SettingsObjectPtr settings = APPLICATION->settings(); QString consoleFontFamily = m_ui->consoleFont->currentFont().family(); settings->set("ConsoleFont", consoleFontFamily); settings->set("ConsoleFontSize", m_ui->fontSizeBox->value()); settings->set("CatOpacity", m_ui->catOpacitySlider->value()); auto catFit = m_ui->catFitComboBox->currentIndex(); settings->set("CatFit", catFit == 0 ? "fit" : catFit == 1 ? "fill" : "strech"); } void AppearanceWidget::loadSettings() { SettingsObjectPtr settings = APPLICATION->settings(); QString fontFamily = settings->get("ConsoleFont").toString(); QFont consoleFont(fontFamily); m_ui->consoleFont->setCurrentFont(consoleFont); bool conversionOk = true; int fontSize = settings->get("ConsoleFontSize").toInt(&conversionOk); if (!conversionOk) { fontSize = 11; } m_ui->fontSizeBox->setValue(fontSize); m_ui->catOpacitySlider->setValue(settings->get("CatOpacity").toInt()); auto catFit = settings->get("CatFit").toString(); m_ui->catFitComboBox->setCurrentIndex(catFit == "fit" ? 0 : catFit == "fill" ? 1 : 2); } void AppearanceWidget::retranslateUi() { m_ui->retranslateUi(this); } void AppearanceWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); auto originalIconTheme = settings->get("IconTheme").toString(); auto newIconTheme = m_ui->iconsComboBox->itemData(index).toString(); if (originalIconTheme != newIconTheme) { settings->set("IconTheme", newIconTheme); APPLICATION->themeManager()->applyCurrentlySelectedTheme(); } } void AppearanceWidget::applyWidgetTheme(int index) { auto settings = APPLICATION->settings(); auto originalAppTheme = settings->get("ApplicationTheme").toString(); auto newAppTheme = m_ui->widgetStyleComboBox->itemData(index).toString(); if (originalAppTheme != newAppTheme) { settings->set("ApplicationTheme", newAppTheme); APPLICATION->themeManager()->applyCurrentlySelectedTheme(); } updateConsolePreview(); } void AppearanceWidget::applyCatTheme(int index) { auto settings = APPLICATION->settings(); auto originalCat = settings->get("BackgroundCat").toString(); auto newCat = m_ui->catPackComboBox->itemData(index).toString(); if (originalCat != newCat) { settings->set("BackgroundCat", newCat); } APPLICATION->currentCatChanged(index); updateCatPreview(); } void AppearanceWidget::loadThemeSettings() { APPLICATION->themeManager()->refresh(); m_ui->iconsComboBox->blockSignals(true); m_ui->widgetStyleComboBox->blockSignals(true); m_ui->catPackComboBox->blockSignals(true); m_ui->iconsComboBox->clear(); m_ui->widgetStyleComboBox->clear(); m_ui->catPackComboBox->clear(); const SettingsObjectPtr settings = APPLICATION->settings(); const QString currentIconTheme = settings->get("IconTheme").toString(); const auto iconThemes = APPLICATION->themeManager()->getValidIconThemes(); for (int i = 0; i < iconThemes.count(); ++i) { const IconTheme* theme = iconThemes[i]; QIcon iconForComboBox = QIcon(theme->path() + "/scalable/settings"); m_ui->iconsComboBox->addItem(iconForComboBox, theme->name(), theme->id()); if (currentIconTheme == theme->id()) m_ui->iconsComboBox->setCurrentIndex(i); } const QString currentTheme = settings->get("ApplicationTheme").toString(); auto themes = APPLICATION->themeManager()->getValidApplicationThemes(); for (int i = 0; i < themes.count(); ++i) { ITheme* theme = themes[i]; m_ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); if (!theme->tooltip().isEmpty()) m_ui->widgetStyleComboBox->setItemData(i, theme->tooltip(), Qt::ToolTipRole); if (currentTheme == theme->id()) m_ui->widgetStyleComboBox->setCurrentIndex(i); } if (!m_themesOnly) { const QString currentCat = settings->get("BackgroundCat").toString(); const auto cats = APPLICATION->themeManager()->getValidCatPacks(); for (int i = 0; i < cats.count(); ++i) { const CatPack* cat = cats[i]; QIcon catIcon = QIcon(QString("%1").arg(cat->path())); m_ui->catPackComboBox->addItem(catIcon, cat->name(), cat->id()); if (currentCat == cat->id()) m_ui->catPackComboBox->setCurrentIndex(i); } } m_ui->iconsComboBox->blockSignals(false); m_ui->widgetStyleComboBox->blockSignals(false); m_ui->catPackComboBox->blockSignals(false); } void AppearanceWidget::updateConsolePreview() { const LogColors& colors = APPLICATION->themeManager()->getLogColors(); int fontSize = m_ui->fontSizeBox->value(); QString fontFamily = m_ui->consoleFont->currentFont().family(); m_ui->consolePreview->clear(); m_defaultFormat.setFont(QFont(fontFamily, fontSize)); auto print = [this, colors](const QString& message, MessageLevel level) { QTextCharFormat format(m_defaultFormat); QColor bg = colors.background.value(level); QColor fg = colors.foreground.value(level); if (bg.isValid()) format.setBackground(bg); if (fg.isValid()) format.setForeground(fg); // append a paragraph/line auto workCursor = m_ui->consolePreview->textCursor(); workCursor.movePosition(QTextCursor::End); workCursor.insertText(message, format); workCursor.insertBlock(); }; print(QString("%1 version: %2\n").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()), MessageLevel::Launcher); QDate today = QDate::currentDate(); if (today.month() == 10 && today.day() == 31) print(tr("[ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); else print(tr("[ERROR] A spooky error!"), MessageLevel::Error); print(tr("[INFO] A harmless message..."), MessageLevel::Info); print(tr("[WARN] A not so spooky warning."), MessageLevel::Warning); print(tr("[DEBUG] A secret debugging message..."), MessageLevel::Debug); print(tr("[FATAL] A terrifying fatal error!"), MessageLevel::Fatal); } void AppearanceWidget::updateCatPreview() { QIcon catPackIcon(APPLICATION->themeManager()->getCatPack()); m_ui->catPreview->setIcon(catPackIcon); auto effect = dynamic_cast(m_ui->catPreview->graphicsEffect()); if (effect) effect->setOpacity(m_ui->catOpacitySlider->value() / 100.0); } PrismLauncher-10.0.5/launcher/ui/widgets/ProjectDescriptionPage.h0000644000175100017510000000136715144136757024447 0ustar runnerrunner#pragma once #include #include "QObjectPtr.h" QT_BEGIN_NAMESPACE class VariableSizedImageObject; QT_END_NAMESPACE /** This subclasses QTextBrowser to provide additional capabilities * to it, like allowing for images to be shown. */ class ProjectDescriptionPage final : public QTextBrowser { Q_OBJECT public: ProjectDescriptionPage(QWidget* parent = nullptr); void setMetaEntry(QString entry); public slots: /** Flushes the current processing happening in the page. * * Should be called when changing the page's content entirely, to * prevent old tasks from changing the new content. */ void flush(); private: shared_qobject_ptr m_image_text_object; }; PrismLauncher-10.0.5/launcher/ui/widgets/VersionListView.h0000644000175100017510000000314015144136757023143 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include class VersionListView : public QTreeView { Q_OBJECT public: explicit VersionListView(QWidget* parent = 0); virtual void paintEvent(QPaintEvent* event) override; virtual void setModel(QAbstractItemModel* model) override; enum EmptyMode { Empty, String, ErrorString }; void setEmptyString(QString emptyString); void setEmptyErrorString(QString emptyErrorString); void setEmptyMode(EmptyMode mode); public slots: virtual void reset() override; protected slots: virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; virtual void rowsInserted(const QModelIndex& parent, int start, int end) override; private: /* methods */ void paintInfoLabel(QPaintEvent* event) const; void updateEmptyViewPort(); QString currentEmptyString() const; private: /* variables */ int m_itemCount = 0; QString m_emptyString; QString m_emptyErrorString; EmptyMode m_emptyMode = Empty; }; PrismLauncher-10.0.5/launcher/ui/widgets/AppearanceWidget.h0000644000175100017510000000315715144136757023242 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 TheKodeToad * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" class QTextCharFormat; class SettingsObject; namespace Ui { class AppearanceWidget; } class AppearanceWidget : public QWidget { Q_OBJECT public: explicit AppearanceWidget(bool simple, QWidget* parent = 0); virtual ~AppearanceWidget(); public: void applySettings(); void loadSettings(); void retranslateUi(); private: void applyIconTheme(int index); void applyWidgetTheme(int index); void applyCatTheme(int index); void loadThemeSettings(); void updateConsolePreview(); void updateCatPreview(); Ui::AppearanceWidget* m_ui; QTextCharFormat m_defaultFormat; bool m_themesOnly; }; PrismLauncher-10.0.5/launcher/ui/widgets/WideBar.cpp0000644000175100017510000002113215144136757021700 0ustar runnerrunner#include "WideBar.h" #include #include #include class ActionButton : public QToolButton { Q_OBJECT public: ActionButton(QAction* action, QWidget* parent = nullptr, bool use_default_action = false) : QToolButton(parent), m_action(action), m_use_default_action(use_default_action) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); setToolButtonStyle(Qt::ToolButtonTextBesideIcon); // workaround for breeze and breeze forks setProperty("_kde_toolButton_alignment", Qt::AlignLeft); if (m_use_default_action) { setDefaultAction(action); } else { connect(this, &ActionButton::clicked, action, &QAction::trigger); } connect(action, &QAction::changed, this, &ActionButton::actionChanged); actionChanged(); }; public slots: void actionChanged() { setEnabled(m_action->isEnabled()); // better pop up mode if (m_action->menu()) { setPopupMode(QToolButton::MenuButtonPopup); } if (!m_use_default_action) { setChecked(m_action->isChecked()); setCheckable(m_action->isCheckable()); setText(m_action->text()); setIcon(m_action->icon()); setToolTip(m_action->toolTip()); setHidden(!m_action->isVisible()); } setFocusPolicy(Qt::NoFocus); } private: QAction* m_action; bool m_use_default_action; }; WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent) { setFloatable(false); setMovable(false); setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); connect(this, &QToolBar::customContextMenuRequested, this, &WideBar::showVisibilityMenu); } WideBar::WideBar(QWidget* parent) : QToolBar(parent) { setFloatable(false); setMovable(false); setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); connect(this, &QToolBar::customContextMenuRequested, this, &WideBar::showVisibilityMenu); } void WideBar::addAction(QAction* action) { BarEntry entry; entry.bar_action = addWidget(new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; m_entries.push_back(entry); m_menu_state = MenuState::Dirty; } void WideBar::addSeparator() { BarEntry entry; entry.bar_action = QToolBar::addSeparator(); entry.type = BarEntry::Type::Separator; m_entries.push_back(entry); } auto WideBar::getMatching(QAction* act) -> QList::iterator { auto iter = std::find_if(m_entries.begin(), m_entries.end(), [act](BarEntry const& entry) { return entry.menu_action == act; }); return iter; } void WideBar::insertActionBefore(QAction* before, QAction* action) { auto iter = getMatching(before); if (iter == m_entries.end()) return; BarEntry entry; entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; m_entries.insert(iter, entry); m_menu_state = MenuState::Dirty; } void WideBar::insertActionAfter(QAction* after, QAction* action) { auto iter = getMatching(after); if (iter == m_entries.end()) return; iter++; // the action to insert after is present // however, the element after it isn't valid if (iter == m_entries.end()) { // append the action instead of inserting it addAction(action); return; } BarEntry entry; entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this, m_use_default_action)); entry.menu_action = action; entry.type = BarEntry::Type::Action; m_entries.insert(iter, entry); m_menu_state = MenuState::Dirty; } void WideBar::insertWidgetBefore(QAction* before, QWidget* widget) { auto iter = getMatching(before); if (iter == m_entries.end()) return; insertWidget(iter->bar_action, widget); } void WideBar::insertSpacer(QAction* action) { auto iter = getMatching(action); if (iter == m_entries.end()) return; auto* spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); BarEntry entry; entry.bar_action = insertWidget(iter->bar_action, spacer); entry.type = BarEntry::Type::Spacer; m_entries.insert(iter, entry); } void WideBar::insertSeparator(QAction* before) { auto iter = getMatching(before); if (iter == m_entries.end()) return; BarEntry entry; entry.bar_action = QToolBar::insertSeparator(iter->bar_action); entry.type = BarEntry::Type::Separator; m_entries.insert(iter, entry); } QMenu* WideBar::createContextMenu(QWidget* parent, const QString& title) { auto* contextMenu = new QMenu(title, parent); for (auto& item : m_entries) { switch (item.type) { default: case BarEntry::Type::None: break; case BarEntry::Type::Separator: case BarEntry::Type::Spacer: contextMenu->addSeparator(); break; case BarEntry::Type::Action: contextMenu->addAction(item.menu_action); break; } } return contextMenu; } static void copyAction(QAction* from, QAction* to) { Q_ASSERT(from); Q_ASSERT(to); to->setText(from->text()); to->setIcon(from->icon()); to->setToolTip(from->toolTip()); } void WideBar::showVisibilityMenu(QPoint const& position) { if (!m_bar_menu) { m_bar_menu = std::make_unique(this); m_bar_menu->setTearOffEnabled(true); } if (m_menu_state == MenuState::Dirty) { for (auto* old_action : m_bar_menu->actions()) old_action->deleteLater(); m_bar_menu->clear(); m_bar_menu->addActions(m_context_menu_actions); m_bar_menu->addSeparator()->setText(tr("Customize toolbar actions")); for (auto& entry : m_entries) { if (entry.type != BarEntry::Type::Action) continue; auto act = new QAction(); copyAction(entry.menu_action, act); act->setCheckable(true); act->setChecked(entry.bar_action->isVisible()); connect(act, &QAction::toggled, entry.bar_action, [this, &entry](bool toggled) { entry.bar_action->setVisible(toggled); // NOTE: This is needed so that disabled actions get reflected on the button when it is made visible. static_cast(widgetForAction(entry.bar_action))->actionChanged(); }); m_bar_menu->addAction(act); } m_menu_state = MenuState::Fresh; } m_bar_menu->popup(mapToGlobal(position)); } void WideBar::addContextMenuAction(QAction* action) { m_context_menu_actions.append(action); } QByteArray WideBar::getVisibilityState() const { QByteArray state; for (auto const& entry : m_entries) { if (entry.type != BarEntry::Type::Action) continue; state.append(entry.bar_action->isVisible() ? '1' : '0'); } state.append(','); state.append(getHash()); return state; } void WideBar::setVisibilityState(QByteArray&& state) { auto split = state.split(','); auto bits = split.first(); auto hash = split.last(); // If the actions changed, we better not try to load the old one to avoid unwanted hiding if (!checkHash(hash)) return; qsizetype i = 0; for (auto& entry : m_entries) { if (entry.type != BarEntry::Type::Action) continue; if (i == bits.size()) break; entry.bar_action->setVisible(bits.at(i++) == '1'); // NOTE: This is needed so that disabled actions get reflected on the button when it is made visible. static_cast(widgetForAction(entry.bar_action))->actionChanged(); } } QByteArray WideBar::getHash() const { QCryptographicHash hash(QCryptographicHash::Sha1); for (auto const& entry : m_entries) { if (entry.type != BarEntry::Type::Action) continue; hash.addData(entry.menu_action->text().toLatin1()); } return hash.result().toBase64(); } bool WideBar::checkHash(QByteArray const& old_hash) const { return old_hash == getHash(); } void WideBar::removeAction(QAction* action) { auto iter = getMatching(action); if (iter == m_entries.end()) return; iter->bar_action->setVisible(false); removeAction(iter->bar_action); m_entries.erase(iter); } #include "WideBar.moc" PrismLauncher-10.0.5/launcher/ui/widgets/ProjectDescriptionPage.cpp0000644000175100017510000000117115144136757024773 0ustar runnerrunner#include "ProjectDescriptionPage.h" #include "VariableSizedImageObject.h" #include ProjectDescriptionPage::ProjectDescriptionPage(QWidget* parent) : QTextBrowser(parent), m_image_text_object(new VariableSizedImageObject) { m_image_text_object->setParent(this); document()->documentLayout()->registerHandler(QTextFormat::ImageObject, m_image_text_object.get()); } void ProjectDescriptionPage::setMetaEntry(QString entry) { if (m_image_text_object) m_image_text_object->setMetaEntry(entry); } void ProjectDescriptionPage::flush() { if (m_image_text_object) m_image_text_object->flush(); } PrismLauncher-10.0.5/launcher/ui/widgets/CustomCommands.ui0000644000175100017510000001213015144136757023150 0ustar runnerrunner CustomCommands 0 0 518 646 0 0 0 0 Override &Global Settings true true 0 0 0 &Pre-launch Command preLaunchCmdTextBox Qt::Vertical QSizePolicy::Fixed 0 6 P&ost-exit Command postExitCmdTextBox &Wrapper Command wrapperCmdTextBox Qt::Vertical QSizePolicy::Fixed 0 6 <html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of Minecraft</li><li>$INST_JAVA - Java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch (warning: will not work correctly if arguments contain spaces)</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html> Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse Qt::Vertical 20 40 PrismLauncher-10.0.5/launcher/ui/widgets/SubTaskProgressBar.h0000644000175100017510000000240515144136757023560 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PrismLaucher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include "QObjectPtr.h" namespace Ui { class SubTaskProgressBar; } class SubTaskProgressBar : public QWidget { Q_OBJECT public: static unique_qobject_ptr create(QWidget* parent = nullptr); SubTaskProgressBar(QWidget* parent = nullptr); ~SubTaskProgressBar(); void setRange(int min, int max); void setValue(int value); void setStatus(QString status); void setDetails(QString details); private: Ui::SubTaskProgressBar* ui; }; PrismLauncher-10.0.5/launcher/ui/widgets/LogView.cpp0000644000175100017510000001427115144136757021745 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LogView.h" #include #include #include LogView::LogView(QWidget* parent) : QPlainTextEdit(parent) { setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); m_defaultFormat = new QTextCharFormat(currentCharFormat()); setUndoRedoEnabled(false); } LogView::~LogView() { delete m_defaultFormat; } void LogView::setWordWrap(bool wrapping) { if (wrapping) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setLineWrapMode(QPlainTextEdit::WidgetWidth); } else { setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); setLineWrapMode(QPlainTextEdit::NoWrap); } } void LogView::setColorLines(bool colorLines) { if (m_colorLines == colorLines) return; m_colorLines = colorLines; repopulate(); } void LogView::setModel(QAbstractItemModel* model) { if (m_model) { disconnect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate); disconnect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted); disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted); disconnect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved); } m_model = model; if (m_model) { connect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate); connect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted); connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted); connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved); connect(m_model, &QAbstractItemModel::destroyed, this, &LogView::modelDestroyed); } repopulate(); } QAbstractItemModel* LogView::model() const { return m_model; } void LogView::modelDestroyed(QObject* model) { if (m_model == model) { setModel(nullptr); } } void LogView::repopulate() { auto doc = document(); doc->clear(); if (!m_model) { return; } rowsInserted(QModelIndex(), 0, m_model->rowCount() - 1); } void LogView::rowsAboutToBeInserted(const QModelIndex& parent, int first, int last) { Q_UNUSED(parent) Q_UNUSED(first) Q_UNUSED(last) QScrollBar* bar = verticalScrollBar(); int max_bar = bar->maximum(); int val_bar = bar->value(); if (m_scroll) { m_scroll = (max_bar - val_bar) <= 1; } else { m_scroll = val_bar == max_bar; } } void LogView::rowsInserted(const QModelIndex& parent, int first, int last) { QTextDocument document; QTextCursor cursor(&document); cursor.movePosition(QTextCursor::End); cursor.beginEditBlock(); for (int i = first; i <= last; i++) { auto idx = m_model->index(i, 0, parent); auto text = m_model->data(idx, Qt::DisplayRole).toString(); QTextCharFormat format(*m_defaultFormat); auto font = m_model->data(idx, Qt::FontRole); if (font.isValid()) { format.setFont(font.value()); } auto fg = m_model->data(idx, Qt::ForegroundRole); if (fg.isValid() && m_colorLines) { format.setForeground(fg.value()); } auto bg = m_model->data(idx, Qt::BackgroundRole); if (bg.isValid() && m_colorLines) { format.setBackground(bg.value()); } cursor.insertText(text, format); cursor.insertBlock(); } cursor.endEditBlock(); QTextDocumentFragment fragment(&document); QTextCursor workCursor = textCursor(); workCursor.movePosition(QTextCursor::End); workCursor.insertFragment(fragment); if (m_scroll && !m_scrolling) { m_scrolling = true; QMetaObject::invokeMethod(this, "scrollToBottom", Qt::QueuedConnection); } } void LogView::rowsRemoved(const QModelIndex& parent, int first, int last) { // TODO: some day... maybe Q_UNUSED(parent) Q_UNUSED(first) Q_UNUSED(last) } void LogView::scrollToBottom() { m_scrolling = false; verticalScrollBar()->setSliderPosition(verticalScrollBar()->maximum()); } void LogView::findNext(const QString& what, bool reverse) { if (what.isEmpty()) return; const QTextDocument::FindFlags flags(reverse ? QTextDocument::FindBackward : 0); if (find(what, flags)) return; QTextCursor cursor = textCursor(); if (reverse) { if (cursor.atEnd()) return; cursor.movePosition(QTextCursor::End); } else { if (cursor.atStart()) return; cursor.movePosition(QTextCursor::Start); } cursor = document()->find(what, cursor, flags); if (!cursor.isNull()) setTextCursor(cursor); } PrismLauncher-10.0.5/launcher/ui/widgets/JavaSettingsWidget.ui0000644000175100017510000002714515144136757023776 0ustar runnerrunner JavaSettingsWidget 0 0 500 1000 Form true Java Insta&llation false false Auto-&detect Java version &Detect &Browse Qt::Horizontal 40 20 Test S&ettings Open Java &Downloader Qt::Horizontal 0 0 Qt::Vertical QSizePolicy::Fixed 0 6 Automatically downloads and selects the Java build recommended by Mojang. Auto-download &Mojang Java If enabled, the launcher will not check if an instance is compatible with the selected Java version. Skip Java compatibility checks Java &Executable javaPathTextBox If enabled, the launcher won't prompt you to choose a Java version if one is not found on startup. Skip Java setup prompt on startup Qt::Vertical QSizePolicy::Fixed 0 6 true Memor&y false false (-XX:PermSize) 0 0 The amount of memory available to store loaded Java classes. MiB 4 1048576 8 64 0 0 The maximum amount of memory Minecraft is allowed to use. MiB 8 1048576 128 1024 (-Xmx) 0 0 The amount of memory Minecraft is started with. MiB 8 1048576 128 256 &PermGen Size: permGenSpinBox (-Xms) Ma&ximum Memory Usage: maxMemSpinBox M&inimum Memory Usage: minMemSpinBox Qt::Horizontal 0 0 Memory Notice true Java Argumen&ts false false javaPathTextBox javaDetectBtn javaBrowseBtn skipCompatibilityCheckBox skipWizardCheckBox autodetectJavaCheckBox autodownloadJavaCheckBox javaTestBtn javaDownloadBtn minMemSpinBox maxMemSpinBox permGenSpinBox jvmArgsTextBox PrismLauncher-10.0.5/launcher/ui/widgets/VersionSelectWidget.cpp0000644000175100017510000001743115144136757024323 0ustar runnerrunner#include "VersionSelectWidget.h" #include #include #include #include #include #include #include "VersionProxyModel.h" #include "ui/dialogs/CustomMessageBox.h" VersionSelectWidget::VersionSelectWidget(QWidget* parent) : QWidget(parent) { setObjectName(QStringLiteral("VersionSelectWidget")); verticalLayout = new QVBoxLayout(this); verticalLayout->setObjectName(QStringLiteral("verticalLayout")); verticalLayout->setContentsMargins(0, 0, 0, 0); m_proxyModel = new VersionProxyModel(this); listView = new VersionListView(this); listView->setObjectName(QStringLiteral("listView")); listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); listView->setAlternatingRowColors(true); listView->setRootIsDecorated(false); listView->setItemsExpandable(false); listView->setWordWrap(true); listView->header()->setCascadingSectionResizes(true); listView->header()->setStretchLastSection(false); listView->setModel(m_proxyModel); verticalLayout->addWidget(listView); search = new QLineEdit(this); search->setPlaceholderText(tr("Search")); search->setClearButtonEnabled(true); verticalLayout->addWidget(search); connect(search, &QLineEdit::textEdited, [this](const QString& value) { m_proxyModel->setSearch(value); if (!value.isEmpty() || !listView->selectionModel()->hasSelection()) { const QModelIndex first = listView->model()->index(0, 0); listView->selectionModel()->setCurrentIndex(first, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); listView->scrollToTop(); } else listView->scrollTo(listView->selectionModel()->currentIndex(), QAbstractItemView::PositionAtCenter); }); search->installEventFilter(this); sneakyProgressBar = new QProgressBar(this); sneakyProgressBar->setObjectName(QStringLiteral("sneakyProgressBar")); sneakyProgressBar->setFormat(QStringLiteral("%p%")); verticalLayout->addWidget(sneakyProgressBar); sneakyProgressBar->setHidden(true); connect(listView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &VersionSelectWidget::currentRowChanged); QMetaObject::connectSlotsByName(this); } void VersionSelectWidget::setCurrentVersion(const QString& version) { m_currentVersion = version; m_proxyModel->setCurrentVersion(version); } void VersionSelectWidget::setEmptyString(QString emptyString) { listView->setEmptyString(emptyString); } void VersionSelectWidget::setEmptyErrorString(QString emptyErrorString) { listView->setEmptyErrorString(emptyErrorString); } void VersionSelectWidget::setEmptyMode(VersionListView::EmptyMode mode) { listView->setEmptyMode(mode); } VersionSelectWidget::~VersionSelectWidget() {} void VersionSelectWidget::setResizeOn(int column) { listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::ResizeToContents); resizeOnColumn = column; listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); } bool VersionSelectWidget::eventFilter(QObject* watched, QEvent* event) { if (watched == search && event->type() == QEvent::KeyPress) { const QKeyEvent* keyEvent = (QKeyEvent*)event; const bool up = keyEvent->key() == Qt::Key_Up; const bool down = keyEvent->key() == Qt::Key_Down; if (up || down) { const QModelIndex index = listView->model()->index(listView->currentIndex().row() + (up ? -1 : 1), 0); if (index.row() >= 0 && index.row() < listView->model()->rowCount()) { listView->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); return true; } } } return QObject::eventFilter(watched, event); } void VersionSelectWidget::initialize(BaseVersionList* vlist, bool forceLoad) { m_vlist = vlist; m_proxyModel->setSourceModel(vlist); listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); if (!m_vlist->isLoaded() || forceLoad) { loadList(); } else { if (m_proxyModel->rowCount() == 0) { listView->setEmptyMode(VersionListView::String); } preselect(); } } void VersionSelectWidget::closeEvent(QCloseEvent* event) { QWidget::closeEvent(event); } void VersionSelectWidget::loadList() { m_load_task = m_vlist->getLoadTask(); connect(m_load_task.get(), &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded); connect(m_load_task.get(), &Task::failed, this, &VersionSelectWidget::onTaskFailed); connect(m_load_task.get(), &Task::progress, this, &VersionSelectWidget::changeProgress); if (!m_load_task->isRunning()) { m_load_task->start(); } sneakyProgressBar->setHidden(false); } void VersionSelectWidget::onTaskSucceeded() { if (m_proxyModel->rowCount() == 0) { listView->setEmptyMode(VersionListView::String); } sneakyProgressBar->setHidden(true); preselect(); m_load_task.reset(); } void VersionSelectWidget::onTaskFailed(const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), tr("List update failed:\n%1").arg(reason), QMessageBox::Warning)->show(); onTaskSucceeded(); } void VersionSelectWidget::changeProgress(qint64 current, qint64 total) { sneakyProgressBar->setMaximum(total); sneakyProgressBar->setValue(current); } void VersionSelectWidget::currentRowChanged(const QModelIndex& current, const QModelIndex&) { auto variant = m_proxyModel->data(current, BaseVersionList::VersionPointerRole); emit selectedVersionChanged(variant.value()); } void VersionSelectWidget::preselect() { if (preselectedAlready) return; selectCurrent(); if (preselectedAlready) return; selectRecommended(); } void VersionSelectWidget::selectCurrent() { if (m_currentVersion.isEmpty()) { return; } auto idx = m_proxyModel->getVersion(m_currentVersion); if (idx.isValid()) { preselectedAlready = true; listView->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); listView->scrollTo(idx, QAbstractItemView::PositionAtCenter); } } void VersionSelectWidget::selectSearch() { search->setFocus(); } VersionListView* VersionSelectWidget::view() { return listView; } void VersionSelectWidget::selectRecommended() { auto idx = m_proxyModel->getRecommended(); if (idx.isValid()) { preselectedAlready = true; listView->selectionModel()->setCurrentIndex(idx, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); listView->scrollTo(idx, QAbstractItemView::PositionAtCenter); } } bool VersionSelectWidget::hasVersions() const { return m_proxyModel->rowCount(QModelIndex()) != 0; } BaseVersion::Ptr VersionSelectWidget::selectedVersion() const { auto currentIndex = listView->selectionModel()->currentIndex(); auto variant = m_proxyModel->data(currentIndex, BaseVersionList::VersionPointerRole); return variant.value(); } void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) { m_proxyModel->setFilter(role, Filters::contains(filter)); } void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role, QString filter) { m_proxyModel->setFilter(role, Filters::equals(filter)); } void VersionSelectWidget::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter) { m_proxyModel->setFilter(role, Filters::equalsOrEmpty(filter)); } void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter filter) { m_proxyModel->setFilter(role, filter); } PrismLauncher-10.0.5/launcher/ui/widgets/InfoFrame.h0000644000175100017510000000517015144136757021702 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "minecraft/mod/DataPack.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/ResourcePack.h" #include "minecraft/mod/TexturePack.h" namespace Ui { class InfoFrame; } class InfoFrame : public QFrame { Q_OBJECT public: InfoFrame(QWidget* parent = nullptr); ~InfoFrame() override; void setName(QString text = {}); void setDescription(QString text = {}); void setImage(QPixmap img = {}); void setLicense(QString text = {}); void setIssueTracker(QString text = {}); void clear(); void updateWithMod(Mod const& m); void updateWithResource(Resource const& resource); void updateWithResourcePack(ResourcePack& rp); void updateWithDataPack(DataPack& rp); void updateWithTexturePack(TexturePack& tp); static QString renderColorCodes(QString input); public slots: void descriptionEllipsisHandler(QString link); void licenseEllipsisHandler(QString link); void boxClosed(int result); private: void updateHiddenState(); private: Ui::InfoFrame* ui; QString m_description; QString m_license; class QMessageBox* m_current_box = nullptr; }; PrismLauncher-10.0.5/launcher/ui/widgets/ProgressWidget.cpp0000644000175100017510000000472415144136757023343 0ustar runnerrunner// Licensed under the Apache-2.0 license. See README.md for details. #include "ProgressWidget.h" #include #include #include #include #include "tasks/Task.h" ProgressWidget::ProgressWidget(QWidget* parent, bool show_label) : QWidget(parent) { auto* layout = new QVBoxLayout(this); if (show_label) { m_label = new QLabel(this); m_label->setWordWrap(true); layout->addWidget(m_label); } m_bar = new QProgressBar(this); m_bar->setMinimum(0); m_bar->setMaximum(100); layout->addWidget(m_bar); setLayout(layout); } void ProgressWidget::reset() { m_bar->reset(); } void ProgressWidget::progressFormat(QString format) { if (format.isEmpty()) m_bar->setTextVisible(false); else m_bar->setFormat(format); } void ProgressWidget::watch(Task* task) { if (!task) return; if (m_task) disconnect(m_task, nullptr, this, nullptr); m_task = task; connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish); connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus); // TODO: should we connect &Task::details connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress); connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed); if (m_task->isRunning()) show(); else connect(m_task, &Task::started, this, &ProgressWidget::show); } void ProgressWidget::start(Task* task) { watch(task); if (!m_task->isRunning()) QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection); } bool ProgressWidget::exec(std::shared_ptr task) { QEventLoop loop; connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); start(task.get()); if (task->isRunning()) loop.exec(); return task->wasSuccessful(); } void ProgressWidget::show() { setHidden(false); } void ProgressWidget::hide() { setHidden(true); } void ProgressWidget::handleTaskFinish() { if (!m_task->wasSuccessful() && m_label) m_label->setText(m_task->failReason()); if (m_hide_if_inactive) hide(); } void ProgressWidget::handleTaskStatus(const QString& status) { if (m_label) m_label->setText(status); } void ProgressWidget::handleTaskProgress(qint64 current, qint64 total) { m_bar->setMaximum(total); m_bar->setValue(current); } void ProgressWidget::taskDestroyed() { m_task = nullptr; } PrismLauncher-10.0.5/launcher/ui/widgets/MinecraftSettingsWidget.cpp0000644000175100017510000006333615144136757025174 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MinecraftSettingsWidget.h" #include "modplatform/ModIndex.h" #include "ui_MinecraftSettingsWidget.h" #include #include "Application.h" #include "BuildConfig.h" #include "Json.h" #include "minecraft/PackProfile.h" #include "minecraft/WorldList.h" #include "minecraft/auth/AccountList.h" #include "settings/Setting.h" MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent) : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::MinecraftSettingsWidget) { m_ui->setupUi(this); if (m_instance == nullptr) { m_ui->settingsTabs->removeTab(1); m_ui->openGlobalSettingsButton->setVisible(false); m_ui->instanceAccountGroupBox->hide(); m_ui->serverJoinGroupBox->hide(); m_ui->globalDataPacksGroupBox->hide(); m_ui->loaderGroup->hide(); } else { m_javaSettings = new JavaSettingsWidget(m_instance, this); m_ui->javaScrollArea->setWidget(m_javaSettings); m_ui->showGameTime->setText(tr("Show time &playing this instance")); m_ui->recordGameTime->setText(tr("&Record time playing this instance")); m_ui->showGlobalGameTime->hide(); m_ui->showGameTimeWithoutDays->hide(); m_ui->maximizedWarning->setText( tr("Warning: The maximized option is " "not fully supported on this Minecraft version.")); m_ui->consoleSettingsBox->setCheckable(true); m_ui->windowSizeGroupBox->setCheckable(true); m_ui->nativeWorkaroundsGroupBox->setCheckable(true); m_ui->perfomanceGroupBox->setCheckable(true); m_ui->gameTimeGroupBox->setCheckable(true); m_ui->legacySettingsGroupBox->setCheckable(true); m_quickPlaySingleplayer = m_instance->traits().contains("feature:is_quick_play_singleplayer"); if (m_quickPlaySingleplayer) { auto worlds = m_instance->worldList(); worlds->update(); for (const auto& world : worlds->allWorlds()) { m_ui->worldsCb->addItem(world.folderName()); } } else { m_ui->worldsCb->hide(); m_ui->worldJoinButton->hide(); m_ui->serverJoinAddressButton->setChecked(true); m_ui->serverJoinAddress->setEnabled(true); m_ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }"); } connect(m_ui->openGlobalSettingsButton, &QCommandLinkButton::clicked, this, &MinecraftSettingsWidget::openGlobalSettings); connect(m_ui->serverJoinAddressButton, &QAbstractButton::toggled, m_ui->serverJoinAddress, &QWidget::setEnabled); connect(m_ui->worldJoinButton, &QAbstractButton::toggled, m_ui->worldsCb, &QWidget::setEnabled); connect(m_ui->globalDataPacksGroupBox, &QGroupBox::toggled, this, [this](bool value) { m_instance->settings()->set("GlobalDataPacksEnabled", value); if (!value) m_instance->settings()->reset("GlobalDataPacksPath"); }); connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, &MinecraftSettingsWidget::saveDataPacksPath); connect(m_ui->dataPacksPathBrowse, &QPushButton::clicked, this, &MinecraftSettingsWidget::selectDataPacksFolder); connect(m_ui->loaderGroup, &QGroupBox::toggled, this, [this](bool value) { m_instance->settings()->set("OverrideModDownloadLoaders", value); if (value) saveSelectedLoaders(); else m_instance->settings()->reset("ModDownloadLoaders"); }); for (auto c : { m_ui->neoForge, m_ui->forge, m_ui->fabric, m_ui->quilt, m_ui->liteLoader, m_ui->babric, m_ui->btaBabric, m_ui->legacyFabric, m_ui->ornithe, m_ui->rift }) { connect(c, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); } } m_ui->maximizedWarning->hide(); connect(m_ui->maximizedCheckBox, &QCheckBox::toggled, this, [this](const bool value) { m_ui->maximizedWarning->setVisible(value && (m_instance == nullptr || !m_instance->isLegacy())); }); #if !defined(Q_OS_LINUX) m_ui->perfomanceGroupBox->hide(); #endif if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) { m_ui->enableFeralGamemodeCheck->setDisabled(true); m_ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system.")); } if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) { m_ui->enableMangoHud->setEnabled(false); m_ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system.")); } connect(m_ui->useNativeOpenALCheck, &QAbstractButton::toggled, m_ui->lineEditOpenALPath, &QWidget::setEnabled); connect(m_ui->useNativeGLFWCheck, &QAbstractButton::toggled, m_ui->lineEditGLFWPath, &QWidget::setEnabled); loadSettings(); } MinecraftSettingsWidget::~MinecraftSettingsWidget() { delete m_ui; } void MinecraftSettingsWidget::loadSettings() { SettingsObjectPtr settings; if (m_instance != nullptr) settings = m_instance->settings(); else settings = APPLICATION->settings(); // Game Window m_ui->windowSizeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideWindow").toBool() || settings->get("OverrideMiscellaneous").toBool()); m_ui->maximizedCheckBox->setChecked(settings->get("LaunchMaximized").toBool()); m_ui->windowWidthSpinBox->setValue(settings->get("MinecraftWinWidth").toInt()); m_ui->windowHeightSpinBox->setValue(settings->get("MinecraftWinHeight").toInt()); m_ui->closeAfterLaunchCheck->setChecked(settings->get("CloseAfterLaunch").toBool()); m_ui->quitAfterGameStopCheck->setChecked(settings->get("QuitAfterGameStop").toBool()); // Game Time m_ui->gameTimeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideGameTime").toBool()); m_ui->showGameTime->setChecked(settings->get("ShowGameTime").toBool()); m_ui->recordGameTime->setChecked(settings->get("RecordGameTime").toBool()); m_ui->showGlobalGameTime->setChecked(m_instance == nullptr && settings->get("ShowGlobalGameTime").toBool()); m_ui->showGameTimeWithoutDays->setChecked(m_instance == nullptr && settings->get("ShowGameTimeWithoutDays").toBool()); // Console m_ui->consoleSettingsBox->setChecked(m_instance == nullptr || settings->get("OverrideConsole").toBool()); m_ui->showConsoleCheck->setChecked(settings->get("ShowConsole").toBool()); m_ui->autoCloseConsoleCheck->setChecked(settings->get("AutoCloseConsole").toBool()); m_ui->showConsoleErrorCheck->setChecked(settings->get("ShowConsoleOnError").toBool()); if (m_javaSettings != nullptr) m_javaSettings->loadSettings(); // Custom commands m_ui->customCommands->initialize(m_instance != nullptr, m_instance == nullptr || settings->get("OverrideCommands").toBool(), settings->get("PreLaunchCommand").toString(), settings->get("WrapperCommand").toString(), settings->get("PostExitCommand").toString()); // Environment variables m_ui->environmentVariables->initialize(m_instance != nullptr, m_instance == nullptr || settings->get("OverrideEnv").toBool(), Json::toMap(settings->get("Env").toString())); // Legacy Tweaks m_ui->legacySettingsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideLegacySettings").toBool()); m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool()); // Native Libraries m_ui->nativeWorkaroundsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideNativeWorkarounds").toBool()); m_ui->useNativeGLFWCheck->setChecked(settings->get("UseNativeGLFW").toBool()); m_ui->lineEditGLFWPath->setText(settings->get("CustomGLFWPath").toString().trimmed()); #ifdef Q_OS_LINUX m_ui->lineEditGLFWPath->setPlaceholderText(APPLICATION->m_detectedGLFWPath); #else m_ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); #endif m_ui->useNativeOpenALCheck->setChecked(settings->get("UseNativeOpenAL").toBool()); m_ui->lineEditOpenALPath->setText(settings->get("CustomOpenALPath").toString().trimmed()); #ifdef Q_OS_LINUX m_ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath); #else m_ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); #endif // Performance m_ui->perfomanceGroupBox->setChecked(m_instance == nullptr || settings->get("OverridePerformance").toBool()); m_ui->enableFeralGamemodeCheck->setChecked(settings->get("EnableFeralGamemode").toBool()); m_ui->enableMangoHud->setChecked(settings->get("EnableMangoHud").toBool()); m_ui->useDiscreteGpuCheck->setChecked(settings->get("UseDiscreteGpu").toBool()); m_ui->useZink->setChecked(settings->get("UseZink").toBool()); if (m_instance != nullptr) { // HACK: if we change enable state of child widgets while it's unchecked this creates inconsistency m_ui->serverJoinGroupBox->setChecked(true); if (auto server = settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty()) { m_ui->serverJoinAddress->setText(server); m_ui->serverJoinAddressButton->setChecked(true); m_ui->worldJoinButton->setChecked(false); m_ui->serverJoinAddress->setEnabled(true); m_ui->worldsCb->setEnabled(false); } else if (auto world = settings->get("JoinWorldOnLaunch").toString(); !world.isEmpty() && m_quickPlaySingleplayer) { m_ui->worldsCb->setCurrentText(world); m_ui->serverJoinAddressButton->setChecked(false); m_ui->worldJoinButton->setChecked(true); m_ui->serverJoinAddress->setEnabled(false); m_ui->worldsCb->setEnabled(true); } else { m_ui->serverJoinAddressButton->setChecked(true); m_ui->worldJoinButton->setChecked(false); m_ui->serverJoinAddress->setEnabled(true); m_ui->worldsCb->setEnabled(false); } m_ui->serverJoinGroupBox->setChecked(settings->get("JoinServerOnLaunch").toBool()); m_ui->instanceAccountGroupBox->setChecked(settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(*settings); auto blockSignalsCheckBoxes = { m_ui->neoForge, m_ui->forge, m_ui->fabric, m_ui->quilt, m_ui->liteLoader, m_ui->babric, m_ui->btaBabric, m_ui->legacyFabric, m_ui->ornithe, m_ui->rift }; m_ui->loaderGroup->blockSignals(true); for (auto c : blockSignalsCheckBoxes) { c->blockSignals(true); } const bool overrideLoaders = settings->get("OverrideModDownloadLoaders").toBool(); const QStringList loaders = Json::toStringList(settings->get("ModDownloadLoaders").toString()); m_ui->loaderGroup->setChecked(overrideLoaders); if (overrideLoaders) { m_ui->neoForge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::NeoForge))); m_ui->forge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Forge))); m_ui->fabric->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Fabric))); m_ui->quilt->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Quilt))); m_ui->liteLoader->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::LiteLoader))); m_ui->babric->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Babric))); m_ui->btaBabric->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::BTA))); m_ui->legacyFabric->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::LegacyFabric))); m_ui->ornithe->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Ornithe))); m_ui->rift->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Rift))); } else { auto instLoaders = m_instance->getPackProfile()->getSupportedModLoaders().value_or(ModPlatform::ModLoaderTypes(0)); m_ui->neoForge->setChecked(instLoaders & ModPlatform::NeoForge); m_ui->forge->setChecked(instLoaders & ModPlatform::Forge); m_ui->fabric->setChecked(instLoaders & ModPlatform::Fabric); m_ui->quilt->setChecked(instLoaders & ModPlatform::Quilt); m_ui->liteLoader->setChecked(instLoaders & ModPlatform::LiteLoader); m_ui->babric->setChecked(instLoaders & ModPlatform::Babric); m_ui->btaBabric->setChecked(instLoaders & ModPlatform::BTA); m_ui->legacyFabric->setChecked(instLoaders & ModPlatform::LegacyFabric); m_ui->ornithe->setChecked(instLoaders & ModPlatform::Ornithe); m_ui->rift->setChecked(instLoaders & ModPlatform::Rift); } m_ui->loaderGroup->blockSignals(false); for (auto c : blockSignalsCheckBoxes) { c->blockSignals(false); } } m_ui->legacySettingsGroupBox->setChecked(settings->get("OverrideLegacySettings").toBool()); m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool()); m_ui->globalDataPacksGroupBox->blockSignals(true); m_ui->dataPacksPathEdit->blockSignals(true); m_ui->globalDataPacksGroupBox->setChecked(settings->get("GlobalDataPacksEnabled").toBool()); m_ui->dataPacksPathEdit->setText(settings->get("GlobalDataPacksPath").toString().trimmed()); m_ui->globalDataPacksGroupBox->blockSignals(false); m_ui->dataPacksPathEdit->blockSignals(false); } void MinecraftSettingsWidget::saveSettings() { SettingsObjectPtr settings; if (m_instance != nullptr) settings = m_instance->settings(); else settings = APPLICATION->settings(); { SettingsObject::Lock lock(settings); // Console bool console = m_instance == nullptr || m_ui->consoleSettingsBox->isChecked(); if (m_instance != nullptr) settings->set("OverrideConsole", console); if (console) { settings->set("ShowConsole", m_ui->showConsoleCheck->isChecked()); settings->set("AutoCloseConsole", m_ui->autoCloseConsoleCheck->isChecked()); settings->set("ShowConsoleOnError", m_ui->showConsoleErrorCheck->isChecked()); } else { settings->reset("ShowConsole"); settings->reset("AutoCloseConsole"); settings->reset("ShowConsoleOnError"); } // Game Window bool window = m_instance == nullptr || m_ui->windowSizeGroupBox->isChecked(); if (m_instance != nullptr) { settings->set("OverrideWindow", window); settings->set("OverrideMiscellaneous", window); } if (window) { settings->set("LaunchMaximized", m_ui->maximizedCheckBox->isChecked()); settings->set("MinecraftWinWidth", m_ui->windowWidthSpinBox->value()); settings->set("MinecraftWinHeight", m_ui->windowHeightSpinBox->value()); settings->set("CloseAfterLaunch", m_ui->closeAfterLaunchCheck->isChecked()); settings->set("QuitAfterGameStop", m_ui->quitAfterGameStopCheck->isChecked()); } else { settings->reset("LaunchMaximized"); settings->reset("MinecraftWinWidth"); settings->reset("MinecraftWinHeight"); settings->reset("CloseAfterLaunch"); settings->reset("QuitAfterGameStop"); } // Custom Commands bool custcmd = m_instance == nullptr || m_ui->customCommands->checked(); if (m_instance != nullptr) settings->set("OverrideCommands", custcmd); if (custcmd) { settings->set("PreLaunchCommand", m_ui->customCommands->prelaunchCommand()); settings->set("WrapperCommand", m_ui->customCommands->wrapperCommand()); settings->set("PostExitCommand", m_ui->customCommands->postexitCommand()); } else { settings->reset("PreLaunchCommand"); settings->reset("WrapperCommand"); settings->reset("PostExitCommand"); } // Environment Variables auto env = m_instance == nullptr || m_ui->environmentVariables->override(); if (m_instance != nullptr) settings->set("OverrideEnv", env); if (env) settings->set("Env", Json::fromMap(m_ui->environmentVariables->value())); else settings->reset("Env"); // Workarounds bool workarounds = m_instance == nullptr || m_ui->nativeWorkaroundsGroupBox->isChecked(); if (m_instance != nullptr) settings->set("OverrideNativeWorkarounds", workarounds); if (workarounds) { settings->set("UseNativeGLFW", m_ui->useNativeGLFWCheck->isChecked()); settings->set("CustomGLFWPath", m_ui->lineEditGLFWPath->text()); settings->set("UseNativeOpenAL", m_ui->useNativeOpenALCheck->isChecked()); settings->set("CustomOpenALPath", m_ui->lineEditOpenALPath->text()); } else { settings->reset("UseNativeGLFW"); settings->reset("CustomGLFWPath"); settings->reset("UseNativeOpenAL"); settings->reset("CustomOpenALPath"); } // Performance bool performance = m_instance == nullptr || m_ui->perfomanceGroupBox->isChecked(); if (m_instance != nullptr) settings->set("OverridePerformance", performance); if (performance) { settings->set("EnableFeralGamemode", m_ui->enableFeralGamemodeCheck->isChecked()); settings->set("EnableMangoHud", m_ui->enableMangoHud->isChecked()); settings->set("UseDiscreteGpu", m_ui->useDiscreteGpuCheck->isChecked()); settings->set("UseZink", m_ui->useZink->isChecked()); } else { settings->reset("EnableFeralGamemode"); settings->reset("EnableMangoHud"); settings->reset("UseDiscreteGpu"); settings->reset("UseZink"); } // Game time bool gameTime = m_instance == nullptr || m_ui->gameTimeGroupBox->isChecked(); if (m_instance != nullptr) settings->set("OverrideGameTime", gameTime); if (gameTime) { settings->set("ShowGameTime", m_ui->showGameTime->isChecked()); settings->set("RecordGameTime", m_ui->recordGameTime->isChecked()); } else { settings->reset("ShowGameTime"); settings->reset("RecordGameTime"); } if (m_instance == nullptr) { settings->set("ShowGlobalGameTime", m_ui->showGlobalGameTime->isChecked()); settings->set("ShowGameTimeWithoutDays", m_ui->showGameTimeWithoutDays->isChecked()); } if (m_instance != nullptr) { // Join server on launch bool joinServerOnLaunch = m_ui->serverJoinGroupBox->isChecked(); settings->set("JoinServerOnLaunch", joinServerOnLaunch); if (joinServerOnLaunch) { if (m_ui->serverJoinAddressButton->isChecked() || !m_quickPlaySingleplayer) { settings->set("JoinServerOnLaunchAddress", m_ui->serverJoinAddress->text()); settings->reset("JoinWorldOnLaunch"); } else { settings->set("JoinWorldOnLaunch", m_ui->worldsCb->currentText()); settings->reset("JoinServerOnLaunchAddress"); } } else { settings->reset("JoinServerOnLaunchAddress"); settings->reset("JoinWorldOnLaunch"); } // Use an account for this instance bool useAccountForInstance = m_ui->instanceAccountGroupBox->isChecked(); settings->set("UseAccountForInstance", useAccountForInstance); if (useAccountForInstance) { int accountIndex = m_ui->instanceAccountSelector->currentIndex(); if (accountIndex != -1) { const MinecraftAccountPtr account = APPLICATION->accounts()->at(accountIndex); if (account != nullptr) settings->set("InstanceAccountId", account->profileId()); } } else { settings->reset("InstanceAccountId"); } } bool overrideLegacySettings = m_instance == nullptr || m_ui->legacySettingsGroupBox->isChecked(); if (m_instance != nullptr) settings->set("OverrideLegacySettings", overrideLegacySettings); if (overrideLegacySettings) { settings->set("OnlineFixes", m_ui->onlineFixes->isChecked()); } else { settings->reset("OnlineFixes"); } } if (m_javaSettings != nullptr) m_javaSettings->saveSettings(); } void MinecraftSettingsWidget::openGlobalSettings() { const QString id = m_ui->settingsTabs->currentWidget()->objectName(); qDebug() << id; if (id == "javaPage") APPLICATION->ShowGlobalSettings(this, "java-settings"); else // TODO select tab APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); } void MinecraftSettingsWidget::updateAccountsMenu(SettingsObject& settings) { m_ui->instanceAccountSelector->clear(); auto accounts = APPLICATION->accounts(); int accountIndex = accounts->findAccountByProfileId(settings.get("InstanceAccountId").toString()); for (int i = 0; i < accounts->count(); i++) { MinecraftAccountPtr account = accounts->at(i); QIcon face = account->getFace(); if (face.isNull()) face = QIcon::fromTheme("noaccount"); m_ui->instanceAccountSelector->addItem(face, account->profileName(), i); if (i == accountIndex) m_ui->instanceAccountSelector->setCurrentIndex(i); } } bool MinecraftSettingsWidget::isQuickPlaySupported() { return m_instance->traits().contains("feature:is_quick_play_singleplayer"); } void MinecraftSettingsWidget::saveSelectedLoaders() { QStringList loaders; if (m_ui->neoForge->isChecked()) loaders << getModLoaderAsString(ModPlatform::NeoForge); if (m_ui->forge->isChecked()) loaders << getModLoaderAsString(ModPlatform::Forge); if (m_ui->fabric->isChecked()) loaders << getModLoaderAsString(ModPlatform::Fabric); if (m_ui->quilt->isChecked()) loaders << getModLoaderAsString(ModPlatform::Quilt); if (m_ui->liteLoader->isChecked()) loaders << getModLoaderAsString(ModPlatform::LiteLoader); if (m_ui->babric->isChecked()) loaders << getModLoaderAsString(ModPlatform::Babric); if (m_ui->btaBabric->isChecked()) loaders << getModLoaderAsString(ModPlatform::BTA); if (m_ui->legacyFabric->isChecked()) loaders << getModLoaderAsString(ModPlatform::LegacyFabric); if (m_ui->ornithe->isChecked()) loaders << getModLoaderAsString(ModPlatform::Ornithe); if (m_ui->rift->isChecked()) loaders << getModLoaderAsString(ModPlatform::Rift); m_instance->settings()->set("ModDownloadLoaders", Json::fromStringList(loaders)); } void MinecraftSettingsWidget::saveDataPacksPath() { if (QDir::separator() != '/') m_ui->dataPacksPathEdit->setText(m_ui->dataPacksPathEdit->text().replace(QDir::separator(), '/')); m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); } void MinecraftSettingsWidget::selectDataPacksFolder() { QString path = QFileDialog::getExistingDirectory(this, tr("Select Global Data Packs Folder"), m_instance->gameRoot()); if (path.isEmpty()) return; // if it's inside the instance dir, set path relative to .minecraft // (so that if it's directly in instance dir it will still lead with .. but more than two levels up are kept absolute) const QUrl instanceRootUrl = QUrl::fromLocalFile(m_instance->instanceRoot()); const QUrl pathUrl = QUrl::fromLocalFile(path); if (instanceRootUrl.isParentOf(pathUrl)) path = QDir(m_instance->gameRoot()).relativeFilePath(path); m_ui->dataPacksPathEdit->setText(path); m_instance->settings()->set("GlobalDataPacksPath", path); } PrismLauncher-10.0.5/launcher/ui/widgets/Common.cpp0000644000175100017510000000156715144136757021625 0ustar runnerrunner#include "Common.h" // Origin: Qt // More specifically, this is a trimmed down version on the algorithm in: // https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#846 QList> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height) { QList> lines; height = 0; textLayout.beginLayout(); QString str = textLayout.text(); while (true) { QTextLine line = textLayout.createLine(); if (!line.isValid()) break; if (line.textLength() == 0) break; line.setLineWidth(lineWidth); line.setPosition(QPointF(0, height)); height += line.height(); lines.append(std::make_pair(line.naturalTextWidth(), str.mid(line.textStart(), line.textLength()))); } textLayout.endLayout(); return lines; } PrismLauncher-10.0.5/launcher/ui/widgets/VariableSizedImageObject.cpp0000644000175100017510000001467315144136757025215 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "VariableSizedImageObject.h" #include #include #include #include #include #include "Application.h" #include "net/ApiDownload.h" #include "net/NetJob.h" enum FormatProperties { ImageData = QTextFormat::UserProperty + 1 }; QSizeF VariableSizedImageObject::intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format) { Q_UNUSED(posInDocument); auto image = qvariant_cast(format.property(ImageData)); auto size = image.size(); if (size.isEmpty()) // can't resize an empty image return { size }; // calculate the new image size based on the properties int width = 0; int height = 0; auto widthVar = format.property(QTextFormat::ImageWidth); if (widthVar.isValid()) { width = widthVar.toInt(); } auto heigthVar = format.property(QTextFormat::ImageHeight); if (heigthVar.isValid()) { height = heigthVar.toInt(); } if (width != 0 && height != 0) { size.setWidth(width); size.setHeight(height); } else if (width != 0) { size.setHeight((width * size.height()) / size.width()); size.setWidth(width); } else if (height != 0) { size.setWidth((height * size.width()) / size.height()); size.setHeight(height); } // Get the width of the text content to make the image similar sized. // doc->textWidth() includes the margin, so we need to remove it. auto doc_width = doc->textWidth() - 2 * doc->documentMargin(); if (size.width() > doc_width) size *= doc_width / (double)size.width(); return { size }; } void VariableSizedImageObject::drawObject(QPainter* painter, const QRectF& rect, QTextDocument* doc, int posInDocument, const QTextFormat& format) { if (!format.hasProperty(ImageData)) { QUrl image_url{ qvariant_cast(format.property(QTextFormat::ImageName)) }; if (m_fetching_images.contains(image_url) || image_url.isEmpty()) return; auto meta = std::make_shared(); meta->posInDocument = posInDocument; meta->url = image_url; auto widthVar = format.property(QTextFormat::ImageWidth); if (widthVar.isValid()) { meta->width = widthVar.toInt(); } auto heigthVar = format.property(QTextFormat::ImageHeight); if (heigthVar.isValid()) { meta->height = heigthVar.toInt(); } loadImage(doc, meta); return; } auto image = qvariant_cast(format.property(ImageData)); painter->setRenderHint(QPainter::RenderHint::SmoothPixmapTransform); painter->drawImage(rect, image); } void VariableSizedImageObject::flush() { m_fetching_images.clear(); } void VariableSizedImageObject::parseImage(QTextDocument* doc, std::shared_ptr meta) { QTextCursor cursor(doc); cursor.setPosition(meta->posInDocument); cursor.setKeepPositionOnInsert(true); auto image_char_format = cursor.charFormat(); image_char_format.setObjectType(QTextFormat::ImageObject); image_char_format.setProperty(ImageData, meta->image); image_char_format.setProperty(QTextFormat::ImageName, meta->url.toDisplayString()); image_char_format.setProperty(QTextFormat::ImageWidth, meta->width); image_char_format.setProperty(QTextFormat::ImageHeight, meta->height); // Qt doesn't allow us to modify the properties of an existing object in the document. // So we remove the old one and add the new one with the ImageData property set. cursor.deleteChar(); cursor.insertText(QString(QChar::ObjectReplacementCharacter), image_char_format); } void VariableSizedImageObject::loadImage(QTextDocument* doc, std::shared_ptr meta) { m_fetching_images.insert(meta->url); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry( m_meta_entry, QString("images/%1").arg(QString(QCryptographicHash::hash(meta->url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); auto job = new NetJob(QString("Load Image: %1").arg(meta->url.fileName()), APPLICATION->network()); job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(meta->url, entry)); auto full_entry_path = entry->getFullPath(); auto source_url = meta->url; auto loadImage = [this, doc, full_entry_path, source_url, meta](const QImage& image) { doc->addResource(QTextDocument::ImageResource, source_url, image); meta->image = image; parseImage(doc, meta); // This size hack is needed to prevent the content from being laid out in an area smaller // than the total width available (weird). auto size = doc->pageSize(); doc->adjustSize(); doc->setPageSize(size); m_fetching_images.remove(source_url); }; connect(job, &NetJob::succeeded, this, [this, full_entry_path, source_url, loadImage] { qDebug() << "Loaded resource at:" << full_entry_path; // If we flushed, don't proceed. if (!m_fetching_images.contains(source_url)) return; QImage image(full_entry_path); loadImage(image); }); connect(job, &NetJob::failed, this, [this, full_entry_path, source_url, loadImage](QString reason) { qWarning() << "Failed resource at:" << full_entry_path << "because:" << reason; // If we flushed, don't proceed. if (!m_fetching_images.contains(source_url)) return; loadImage(QImage()); }); connect(job, &NetJob::finished, job, &NetJob::deleteLater); job->start(); } PrismLauncher-10.0.5/launcher/ui/widgets/CheckComboBox.cpp0000644000175100017510000001454115144136757023037 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "CheckComboBox.h" #include #include #include #include #include #include #include #include #include #include class CheckComboModel : public QIdentityProxyModel { Q_OBJECT public: explicit CheckComboModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} virtual Qt::ItemFlags flags(const QModelIndex& index) const { return QIdentityProxyModel::flags(index) | Qt::ItemIsUserCheckable; } virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const { if (role == Qt::CheckStateRole) { auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString(); return m_checked.contains(txt) ? Qt::Checked : Qt::Unchecked; } if (role == Qt::DisplayRole) return QIdentityProxyModel::data(index, Qt::DisplayRole); return {}; } virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) { if (role == Qt::CheckStateRole) { auto txt = QIdentityProxyModel::data(index, Qt::DisplayRole).toString(); if (m_checked.contains(txt)) { m_checked.removeOne(txt); } else { m_checked.push_back(txt); } emit dataChanged(index, index); emit checkStateChanged(); return true; } return QIdentityProxyModel::setData(index, value, role); } QStringList getChecked() { return m_checked; } signals: void checkStateChanged(); private: QStringList m_checked; }; CheckComboBox::CheckComboBox(QWidget* parent) : QComboBox(parent), m_separator(", ") { view()->installEventFilter(this); view()->window()->installEventFilter(this); view()->viewport()->installEventFilter(this); this->installEventFilter(this); } void CheckComboBox::setSourceModel(QAbstractItemModel* new_model) { auto proxy = new CheckComboModel(this); proxy->setSourceModel(new_model); model()->disconnect(this); QComboBox::setModel(proxy); connect(this, &QComboBox::activated, this, &CheckComboBox::toggleCheckState); connect(proxy, &CheckComboModel::checkStateChanged, this, &CheckComboBox::emitCheckedItemsChanged); connect(model(), &CheckComboModel::rowsInserted, this, &CheckComboBox::emitCheckedItemsChanged); connect(model(), &CheckComboModel::rowsRemoved, this, &CheckComboBox::emitCheckedItemsChanged); } void CheckComboBox::hidePopup() { if (!m_containerMousePress) QComboBox::hidePopup(); } void CheckComboBox::emitCheckedItemsChanged() { emit checkedItemsChanged(checkedItems()); } QString CheckComboBox::defaultText() const { return m_default_text; } void CheckComboBox::setDefaultText(const QString& text) { m_default_text = text; } QString CheckComboBox::separator() const { return m_separator; } void CheckComboBox::setSeparator(const QString& separator) { m_separator = separator; } bool CheckComboBox::eventFilter(QObject* receiver, QEvent* event) { switch (event->type()) { case QEvent::KeyPress: case QEvent::KeyRelease: { QKeyEvent* keyEvent = static_cast(event); if (receiver == this && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down)) { showPopup(); return true; } else if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Escape) { QComboBox::hidePopup(); return (keyEvent->key() != Qt::Key_Escape); } break; } case QEvent::MouseButtonPress: { auto ev = static_cast(event); m_containerMousePress = ev && view()->indexAt(ev->pos()).isValid() && view()->rect().contains(ev->pos()); break; } case QEvent::Wheel: return receiver == this; default: break; } return false; } void CheckComboBox::toggleCheckState(int index) { QVariant value = itemData(index, Qt::CheckStateRole); if (value.isValid()) { Qt::CheckState state = static_cast(value.toInt()); setItemData(index, (state == Qt::Unchecked ? Qt::Checked : Qt::Unchecked), Qt::CheckStateRole); } emitCheckedItemsChanged(); } Qt::CheckState CheckComboBox::itemCheckState(int index) const { return static_cast(itemData(index, Qt::CheckStateRole).toInt()); } void CheckComboBox::setItemCheckState(int index, Qt::CheckState state) { setItemData(index, state, Qt::CheckStateRole); } QStringList CheckComboBox::checkedItems() const { if (model()) return dynamic_cast(model())->getChecked(); return {}; } void CheckComboBox::setCheckedItems(const QStringList& items) { for (auto text : items) { auto index = findText(text); setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked); } } void CheckComboBox::paintEvent(QPaintEvent*) { QStylePainter painter(this); painter.setPen(palette().color(QPalette::Text)); // draw the combobox frame, focusrect and selected etc. QStyleOptionComboBox opt; initStyleOption(&opt); QStringList items = checkedItems(); if (items.isEmpty()) opt.currentText = defaultText(); else opt.currentText = items.join(separator()); painter.drawComplexControl(QStyle::CC_ComboBox, opt); // draw the icon and text painter.drawControl(QStyle::CE_ComboBoxLabel, opt); } #include "CheckComboBox.moc" PrismLauncher-10.0.5/launcher/ui/widgets/CustomCommands.h0000644000175100017510000000377615144136757023002 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include namespace Ui { class CustomCommands; } class CustomCommands : public QWidget { Q_OBJECT public: explicit CustomCommands(QWidget* parent = 0); virtual ~CustomCommands(); void initialize(bool checkable, bool checked, const QString& prelaunch, const QString& wrapper, const QString& postexit); void retranslate(); bool checked() const; QString prelaunchCommand() const; QString wrapperCommand() const; QString postexitCommand() const; private: Ui::CustomCommands* ui; }; PrismLauncher-10.0.5/launcher/ui/widgets/InfoFrame.ui0000644000175100017510000001073615144136757022074 0ustar runnerrunner InfoFrame 0 0 527 113 0 0 16777215 120 0 0 0 0 0 0 64 64 false 0 Qt::RichText Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse Qt::RichText Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse Qt::RichText Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse Qt::RichText Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse PrismLauncher-10.0.5/launcher/ui/widgets/SubTaskProgressBar.ui0000644000175100017510000000555115144136757023753 0ustar runnerrunner SubTaskProgressBar 0 0 312 86 0 0 Form 0 8 0 0 8 Sub Task Status... true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 0 0 8 Status Details Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse 8 24 true PrismLauncher-10.0.5/launcher/ui/widgets/AppearanceWidget.ui0000644000175100017510000004617015144136757023432 0ustar runnerrunner AppearanceWidget 0 0 600 583 300 0 false View cat packs folder. Open Folder View widget themes folder. Open Folder View icon themes folder. Open Folder &Cat Pack: catPackComboBox 0 0 Qt::FocusPolicy::StrongFocus 0 0 Qt::FocusPolicy::StrongFocus 0 0 Reload All Theme: widgetStyleComboBox &Icons: iconsComboBox Console Font: 0 0 5 16 11 Qt::Orientation::Horizontal 0 0 Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 0 6 Cat Opacity 300 16777215 0 0 0 0 300 16777215 0 0 0 0 false Opaque Qt::Orientation::Horizontal 0 0 false Transparent 0 0 100 Qt::Orientation::Horizontal Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 0 6 0 0 Cat Scaling 0 0 81 32 0 Fit Fill Stretch Preview 0 0 Qt::FocusPolicy::NoFocus 64 128 true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::FocusPolicy::NoFocus true Qt::Orientation::Horizontal 0 0 0 0 Qt::ScrollBarPolicy::ScrollBarAsNeeded false Qt::TextInteractionFlag::TextSelectableByKeyboard|Qt::TextInteractionFlag::TextSelectableByMouse widgetStyleComboBox widgetStyleFolder iconsComboBox iconsFolder catPackComboBox catPackFolder reloadThemesButton consoleFont fontSizeBox catFitComboBox catOpacitySlider consolePreview PrismLauncher-10.0.5/launcher/ui/widgets/CheckComboBox.h0000644000175100017510000000343615144136757022505 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include class CheckComboBox : public QComboBox { Q_OBJECT public: explicit CheckComboBox(QWidget* parent = nullptr); virtual ~CheckComboBox() = default; void hidePopup() override; QString defaultText() const; void setDefaultText(const QString& text); Qt::CheckState itemCheckState(int index) const; void setItemCheckState(int index, Qt::CheckState state); QString separator() const; void setSeparator(const QString& separator); QStringList checkedItems() const; void setSourceModel(QAbstractItemModel* model); public slots: void setCheckedItems(const QStringList& items); signals: void checkedItemsChanged(const QStringList& items); protected: void paintEvent(QPaintEvent*) override; private: void emitCheckedItemsChanged(); bool eventFilter(QObject* receiver, QEvent* event) override; void toggleCheckState(int index); private: QString m_default_text; QString m_separator; bool m_containerMousePress = false; }; PrismLauncher-10.0.5/launcher/ui/widgets/JavaSettingsWidget.cpp0000644000175100017510000002760415144136757024143 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "JavaSettingsWidget.h" #include #include #include "Application.h" #include "BuildConfig.h" #include "FileSystem.h" #include "JavaCommon.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "settings/Setting.h" #include "sys.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/VersionSelectDialog.h" #include "ui/java/InstallJavaDialog.h" #include "ui_JavaSettingsWidget.h" JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::JavaSettingsWidget) { m_ui->setupUi(this); if (m_instance == nullptr) { m_ui->javaDownloadBtn->hide(); if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { connect(m_ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this](bool state) { m_ui->autodownloadJavaCheckBox->setEnabled(state); if (!state) m_ui->autodownloadJavaCheckBox->setChecked(false); }); } else { m_ui->autodownloadJavaCheckBox->hide(); } } else { m_ui->javaDownloadBtn->setVisible(BuildConfig.JAVA_DOWNLOADER_ENABLED); m_ui->skipWizardCheckBox->hide(); m_ui->autodetectJavaCheckBox->hide(); m_ui->autodownloadJavaCheckBox->hide(); m_ui->javaInstallationGroupBox->setCheckable(true); m_ui->memoryGroupBox->setCheckable(true); m_ui->javaArgumentsGroupBox->setCheckable(true); SettingsObjectPtr settings = m_instance->settings(); connect(settings->getSetting("OverrideJavaLocation").get(), &Setting::SettingChanged, m_ui->javaInstallationGroupBox, [this, settings] { m_ui->javaInstallationGroupBox->setChecked(settings->get("OverrideJavaLocation").toBool()); }); connect(settings->getSetting("JavaPath").get(), &Setting::SettingChanged, m_ui->javaInstallationGroupBox, [this, settings] { m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString()); }); connect(m_ui->javaDownloadBtn, &QPushButton::clicked, this, [this] { auto javaDialog = new Java::InstallDialog({}, m_instance.get(), this); javaDialog->exec(); }); connect(m_ui->javaPathTextBox, &QLineEdit::textChanged, [this](QString newValue) { if (m_instance->settings()->get("JavaPath").toString() != newValue) { m_instance->settings()->set("AutomaticJava", false); } }); } connect(m_ui->javaTestBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaTest); connect(m_ui->javaDetectBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaAutodetect); connect(m_ui->javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaBrowse); connect(m_ui->maxMemSpinBox, &QSpinBox::valueChanged, this, &JavaSettingsWidget::updateThresholds); connect(m_ui->minMemSpinBox, &QSpinBox::valueChanged, this, &JavaSettingsWidget::updateThresholds); loadSettings(); updateThresholds(); } JavaSettingsWidget::~JavaSettingsWidget() { delete m_ui; } void JavaSettingsWidget::loadSettings() { SettingsObjectPtr settings; if (m_instance != nullptr) settings = m_instance->settings(); else settings = APPLICATION->settings(); // Java Settings m_ui->javaInstallationGroupBox->setChecked(settings->get("OverrideJavaLocation").toBool()); m_ui->javaPathTextBox->setText(settings->get("JavaPath").toString()); m_ui->skipCompatibilityCheckBox->setChecked(settings->get("IgnoreJavaCompatibility").toBool()); m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool()); m_ui->jvmArgsTextBox->setPlainText(settings->get("JvmArgs").toString()); if (m_instance == nullptr) { m_ui->skipWizardCheckBox->setChecked(settings->get("IgnoreJavaWizard").toBool()); m_ui->autodetectJavaCheckBox->setChecked(settings->get("AutomaticJavaSwitch").toBool()); m_ui->autodetectJavaCheckBox->stateChanged(m_ui->autodetectJavaCheckBox->isChecked()); m_ui->autodownloadJavaCheckBox->setChecked(settings->get("AutomaticJavaDownload").toBool()); } // Memory m_ui->memoryGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideMemory").toBool()); int min = settings->get("MinMemAlloc").toInt(); int max = settings->get("MaxMemAlloc").toInt(); if (min < max) { m_ui->minMemSpinBox->setValue(min); m_ui->maxMemSpinBox->setValue(max); } else { m_ui->minMemSpinBox->setValue(max); m_ui->maxMemSpinBox->setValue(min); } m_ui->permGenSpinBox->setValue(settings->get("PermGen").toInt()); // Java arguments m_ui->javaArgumentsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideJavaArgs").toBool()); m_ui->jvmArgsTextBox->setPlainText(settings->get("JvmArgs").toString()); } void JavaSettingsWidget::saveSettings() { SettingsObjectPtr settings; if (m_instance != nullptr) settings = m_instance->settings(); else settings = APPLICATION->settings(); SettingsObject::Lock lock(settings); // Java Install Settings bool javaInstall = m_instance == nullptr || m_ui->javaInstallationGroupBox->isChecked(); if (m_instance != nullptr) settings->set("OverrideJavaLocation", javaInstall); if (javaInstall) { settings->set("JavaPath", m_ui->javaPathTextBox->text()); settings->set("IgnoreJavaCompatibility", m_ui->skipCompatibilityCheckBox->isChecked()); } else { settings->reset("JavaPath"); settings->reset("IgnoreJavaCompatibility"); } if (m_instance == nullptr) { settings->set("IgnoreJavaWizard", m_ui->skipWizardCheckBox->isChecked()); settings->set("AutomaticJavaSwitch", m_ui->autodetectJavaCheckBox->isChecked()); settings->set("AutomaticJavaDownload", m_ui->autodownloadJavaCheckBox->isChecked()); } // Memory bool memory = m_instance == nullptr || m_ui->memoryGroupBox->isChecked(); if (m_instance != nullptr) settings->set("OverrideMemory", memory); if (memory) { int min = m_ui->minMemSpinBox->value(); int max = m_ui->maxMemSpinBox->value(); if (min < max) { settings->set("MinMemAlloc", min); settings->set("MaxMemAlloc", max); } else { settings->set("MinMemAlloc", max); settings->set("MaxMemAlloc", min); } settings->set("PermGen", m_ui->permGenSpinBox->value()); } else { settings->reset("MinMemAlloc"); settings->reset("MaxMemAlloc"); settings->reset("PermGen"); } // Java arguments bool javaArgs = m_instance == nullptr || m_ui->javaArgumentsGroupBox->isChecked(); if (m_instance != nullptr) settings->set("OverrideJavaArgs", javaArgs); if (javaArgs) { settings->set("JvmArgs", m_ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); } else { settings->reset("JvmArgs"); } } void JavaSettingsWidget::onJavaBrowse() { QString rawPath = QFileDialog::getOpenFileName(this, tr("Find Java executable")); // do not allow current dir - it's dirty. Do not allow dirs that don't exist if (rawPath.isEmpty()) { return; } QString cookedPath = FS::NormalizePath(rawPath); QFileInfo javaInfo(cookedPath); if (!javaInfo.exists() || !javaInfo.isExecutable()) { return; } m_ui->javaPathTextBox->setText(cookedPath); } void JavaSettingsWidget::onJavaTest() { if (m_checker != nullptr) return; QString jvmArgs; if (m_instance == nullptr || m_ui->javaArgumentsGroupBox->isChecked()) jvmArgs = m_ui->jvmArgsTextBox->toPlainText().replace("\n", " "); else jvmArgs = APPLICATION->settings()->get("JvmArgs").toString(); m_checker.reset(new JavaCommon::TestCheck(this, m_ui->javaPathTextBox->text(), jvmArgs, m_ui->minMemSpinBox->value(), m_ui->maxMemSpinBox->value(), m_ui->permGenSpinBox->value())); connect(m_checker.get(), &JavaCommon::TestCheck::finished, this, [this] { m_checker.reset(); }); m_checker->run(); } void JavaSettingsWidget::onJavaAutodetect() { if (JavaUtils::getJavaCheckPath().isEmpty()) { JavaCommon::javaCheckNotFound(this); return; } VersionSelectDialog versionDialog(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); versionDialog.setResizeOn(2); versionDialog.exec(); if (versionDialog.result() == QDialog::Accepted && versionDialog.selectedVersion()) { JavaInstallPtr java = std::dynamic_pointer_cast(versionDialog.selectedVersion()); m_ui->javaPathTextBox->setText(java->path); if (!java->is_64bit && m_ui->maxMemSpinBox->value() > 2048) { CustomMessageBox::selectable(this, tr("Confirm Selection"), tr("You selected a 32-bit version of Java.\n" "This installation does not support more than 2048MiB of RAM.\n" "Please make sure that the maximum memory value is lower."), QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) ->exec(); } } } void JavaSettingsWidget::updateThresholds() { auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; unsigned int maxMem = m_ui->maxMemSpinBox->value(); unsigned int minMem = m_ui->minMemSpinBox->value(); const QString warningColour(QStringLiteral("%1")); if (maxMem >= sysMiB) { m_ui->labelMaxMemNotice->setText( QString("%1").arg(tr("Your maximum memory allocation exceeds your system memory capacity."))); m_ui->labelMaxMemNotice->show(); } else if (maxMem > (sysMiB * 0.9)) { m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is close to your system memory capacity."))); m_ui->labelMaxMemNotice->show(); } else if (maxMem < minMem) { m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is below the minimum memory allocation."))); m_ui->labelMaxMemNotice->show(); } else { m_ui->labelMaxMemNotice->hide(); } } PrismLauncher-10.0.5/launcher/ui/widgets/MinecraftSettingsWidget.ui0000644000175100017510000007767415144136757025041 0ustar runnerrunner MinecraftSettingsWidget 0 0 653 600 0 0 6 0 Open &Global Settings The settings here are overrides for global settings. 0 General 0 0 true 0 0 623 1352 true Game &Window false false The base game only supports resolution. In order to simulate the maximized behavior the current implementation approximates the maximum display size. <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: The maximized option may not be fully supported on all Minecraft versions.</span></p></body></html> When the game window closes, quit the launcher Start Minecraft maximized When the game window opens, hide the launcher 0 0 1 65536 1 854 Qt::Vertical QSizePolicy::Fixed 0 6 0 0 1 65536 480 &Window Size: windowWidthSpinBox × pixels Qt::Horizontal 0 0 true &Console Window false false When the game is launched, show the console window When the game crashes, show the console window When the game quits, hide the console window &Global Data Packs true true Allows installing data packs across all worlds if an applicable mod is installed. It is most likely you will need to change the path - please refer to the mod's website. Qt::Vertical QSizePolicy::Fixed 0 6 Folder Path datapacks Browse true Game &Time false false Show time spent &playing instances &Record time spent playing instances Show the &total time played across instances Always show durations in &hours Override &Default Account true false Account: 0 0 Qt::Horizontal 0 0 Enable Auto-&join true false 0 0 Singleplayer world: Server address: 200 16777215 Qt::Horizontal 0 0 Override Mod Download &Loaders true false NeoForge Forge Fabric Quilt LiteLoader Babric BTA (Babric) Legacy Fabric Ornithe Rift Qt::Vertical 20 40 Java true 0 0 623 484 Tweaks true 0 0 609 499 &Legacy Tweaks false false <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>Current fixes include: skin and online mode support.</p></body></html> Enable online fixes (experimental) true &Native Libraries false false Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter &GLFW library path: lineEditGLFWPath Qt::Vertical QSizePolicy::Fixed 0 6 &OpenAL library path: lineEditOpenALPath false Use system installation of GLFW Use system installation of OpenAL false true &Performance false false <html><head/><body><p>Enable Feral Interactive's GameMode, to potentially improve gaming performance.</p></body></html> Enable Feral GameMode <html><head/><body><p>Enable MangoHud's advanced performance overlay.</p></body></html> Enable MangoHud <html><head/><body><p>Use the discrete GPU instead of the primary GPU.</p></body></html> Use discrete GPU Use Zink, a Mesa OpenGL driver that implements OpenGL on top of Vulkan. Performance may vary depending on the situation. Note: If no suitable Vulkan driver is found, software rendering will be used. Use Zink Qt::Vertical 20 40 Custom Commands Environment Variables 0 0 0 0 CustomCommands QWidget
ui/widgets/CustomCommands.h
1
EnvironmentVariables QWidget
ui/widgets/EnvironmentVariables.h
1
openGlobalSettingsButton settingsTabs scrollArea maximizedCheckBox windowHeightSpinBox windowWidthSpinBox closeAfterLaunchCheck quitAfterGameStopCheck showConsoleCheck showConsoleErrorCheck autoCloseConsoleCheck showGameTime recordGameTime showGlobalGameTime showGameTimeWithoutDays instanceAccountGroupBox instanceAccountSelector serverJoinGroupBox serverJoinAddressButton serverJoinAddress worldJoinButton worldsCb javaScrollArea scrollArea_2 onlineFixes useNativeGLFWCheck lineEditGLFWPath useNativeOpenALCheck lineEditOpenALPath enableFeralGamemodeCheck enableMangoHud useDiscreteGpuCheck useZink
PrismLauncher-10.0.5/launcher/ui/widgets/VariableSizedImageObject.h0000644000175100017510000000461215144136757024652 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include /** Custom image text object to be used instead of the normal one in ProjectDescriptionPage. * * Why? Because we want to re-scale images dynamically based on the document's size, in order to * not have images being weirdly cropped out in different resolutions. */ class VariableSizedImageObject final : public QObject, public QTextObjectInterface { Q_OBJECT Q_INTERFACES(QTextObjectInterface) struct ImageMetadata { int posInDocument; QUrl url; QImage image; int width; int height; }; public: QSizeF intrinsicSize(QTextDocument* doc, int posInDocument, const QTextFormat& format) override; void drawObject(QPainter* painter, const QRectF& rect, QTextDocument* doc, int posInDocument, const QTextFormat& format) override; void setMetaEntry(QString meta_entry) { m_meta_entry = meta_entry; } public slots: /** Stops all currently loading images from modifying the document. * * This does not stop the ongoing network tasks, it only prevents their result * from impacting the document any further. */ void flush(); private: /** Adds the image to the document, in the given position. */ void parseImage(QTextDocument* doc, std::shared_ptr meta); /** Loads an image from an external source, and adds it to the document. * * This uses m_meta_entry to cache the image. */ void loadImage(QTextDocument* doc, std::shared_ptr meta); private: QString m_meta_entry; QSet m_fetching_images; }; PrismLauncher-10.0.5/launcher/ui/widgets/ModFilterWidget.ui0000644000175100017510000002261115144136757023252 0ustar runnerrunner ModFilterWidget 0 0 310 600 0 0 275 0 310 16777215 Form 0 0 0 0 275 0 QAbstractScrollArea::AdjustToContentsOnFirstShow true 0 0 294 817 Categories false false Loaders false false NeoForge Forge Fabric Quilt Show More false 0 0 0 0 LiteLoader Babric BTA (Babric) Legacy Fabric Ornithe Rift Versions false false Show all versions Environments false false Client Server Hide installed items Open source only Release type Release Beta Alpha Unknown Qt::Vertical 20 40 CheckComboBox QComboBox
ui/widgets/CheckComboBox.h
PrismLauncher-10.0.5/launcher/ui/widgets/PageContainer_p.h0000644000175100017510000000647015144136757023076 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include class BasePage; const int pageIconSize = 24; class PageViewDelegate : public QStyledItemDelegate { public: PageViewDelegate(QObject* parent) : QStyledItemDelegate(parent) {} QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QSize size = QStyledItemDelegate::sizeHint(option, index); size.setHeight(qMax(size.height(), 32)); return size; } }; class PageModel : public QAbstractListModel { public: PageModel(QObject* parent = 0) : QAbstractListModel(parent) { QPixmap empty(pageIconSize, pageIconSize); empty.fill(Qt::transparent); m_emptyIcon = QIcon(empty); } virtual ~PageModel() {} int rowCount(const QModelIndex& parent = QModelIndex()) const { return parent.isValid() ? 0 : m_pages.size(); } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const { switch (role) { case Qt::DisplayRole: return m_pages.at(index.row())->displayName(); case Qt::DecorationRole: { QIcon icon = m_pages.at(index.row())->icon(); if (icon.isNull()) icon = m_emptyIcon; // HACK: fixes icon stretching on windows. TODO: report Qt bug for this return QIcon(icon.pixmap(QSize(48, 48))); } } return QVariant(); } void setPages(const QList& pages) { beginResetModel(); m_pages = pages; endResetModel(); } const QList& pages() const { return m_pages; } BasePage* findPageEntryById(QString id) { for (auto page : m_pages) { if (page->id() == id) return page; } return nullptr; } QList m_pages; QIcon m_emptyIcon; }; class PageView : public QListView { public: PageView(QWidget* parent = 0) : QListView(parent) { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); setItemDelegate(new PageViewDelegate(this)); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } virtual QSize sizeHint() const { int width = sizeHintForColumn(0) + frameWidth() * 2 + 5; if (verticalScrollBar()->isVisible()) width += verticalScrollBar()->width(); return QSize(width, 100); } virtual bool eventFilter(QObject* obj, QEvent* event) { if (obj == verticalScrollBar() && (event->type() == QEvent::Show || event->type() == QEvent::Hide)) updateGeometry(); return QListView::eventFilter(obj, event); } }; PrismLauncher-10.0.5/launcher/ui/widgets/VersionListView.cpp0000644000175100017510000001150315144136757023500 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "VersionListView.h" #include #include #include #include #include VersionListView::VersionListView(QWidget* parent) : QTreeView(parent) { m_emptyString = tr("No versions are currently available."); } void VersionListView::rowsInserted(const QModelIndex& parent, int start, int end) { m_itemCount += end - start + 1; updateEmptyViewPort(); QTreeView::rowsInserted(parent, start, end); } void VersionListView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { m_itemCount -= end - start + 1; updateEmptyViewPort(); QTreeView::rowsInserted(parent, start, end); } void VersionListView::setModel(QAbstractItemModel* model) { m_itemCount = model->rowCount(); updateEmptyViewPort(); QTreeView::setModel(model); } void VersionListView::reset() { if (model()) { m_itemCount = model()->rowCount(); } else { m_itemCount = 0; } updateEmptyViewPort(); QTreeView::reset(); } void VersionListView::setEmptyString(QString emptyString) { m_emptyString = emptyString; updateEmptyViewPort(); } void VersionListView::setEmptyErrorString(QString emptyErrorString) { m_emptyErrorString = emptyErrorString; updateEmptyViewPort(); } void VersionListView::setEmptyMode(VersionListView::EmptyMode mode) { m_emptyMode = mode; updateEmptyViewPort(); } void VersionListView::updateEmptyViewPort() { #ifndef QT_NO_ACCESSIBILITY setAccessibleDescription(currentEmptyString()); #endif /* !QT_NO_ACCESSIBILITY */ if (!m_itemCount) { viewport()->update(); } } void VersionListView::paintEvent(QPaintEvent* event) { if (m_itemCount) { QTreeView::paintEvent(event); } else { paintInfoLabel(event); } } QString VersionListView::currentEmptyString() const { switch (m_emptyMode) { default: case VersionListView::String: return m_emptyString; case VersionListView::ErrorString: return m_emptyErrorString; } } void VersionListView::paintInfoLabel(QPaintEvent* event) const { QString emptyString = currentEmptyString(); // calculate the rect for the overlay QPainter painter(viewport()); painter.setRenderHint(QPainter::Antialiasing, true); QFont font("sans", 20); font.setBold(true); QRect bounds = viewport()->geometry(); bounds.moveTop(0); auto innerBounds = bounds; innerBounds.adjust(10, 10, -10, -10); QColor background = QApplication::palette().color(QPalette::WindowText); QColor foreground = QApplication::palette().color(QPalette::Base); foreground.setAlpha(190); painter.setFont(font); auto fontMetrics = painter.fontMetrics(); auto textRect = fontMetrics.boundingRect(innerBounds, Qt::AlignHCenter | Qt::TextWordWrap, emptyString); textRect.moveCenter(bounds.center()); auto wrapRect = textRect; wrapRect.adjust(-10, -10, 10, 10); // check if we are allowed to draw in our area if (!event->rect().intersects(wrapRect)) { return; } painter.setBrush(QBrush(background)); painter.setPen(foreground); painter.drawRoundedRect(wrapRect, 5.0, 5.0); painter.setPen(foreground); painter.setFont(font); painter.drawText(textRect, Qt::AlignHCenter | Qt::TextWordWrap, emptyString); } PrismLauncher-10.0.5/launcher/ui/widgets/EnvironmentVariables.ui0000644000175100017510000000624415144136757024362 0ustar runnerrunner EnvironmentVariables 0 0 565 410 Form Override &Global Settings true true 0 0 0 0 &Add &Remove Qt::Horizontal 40 20 &Clear true QAbstractItemView::ExtendedSelection false false true false Name Value PrismLauncher-10.0.5/launcher/ui/widgets/SubTaskProgressBar.cpp0000644000175100017510000000312015144136757024106 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PrismLaucher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "SubTaskProgressBar.h" #include "ui_SubTaskProgressBar.h" unique_qobject_ptr SubTaskProgressBar::create(QWidget* parent) { auto progress_bar = new SubTaskProgressBar(parent); return unique_qobject_ptr(progress_bar); } SubTaskProgressBar::SubTaskProgressBar(QWidget* parent) : QWidget(parent), ui(new Ui::SubTaskProgressBar) { ui->setupUi(this); } SubTaskProgressBar::~SubTaskProgressBar() { delete ui; } void SubTaskProgressBar::setRange(int min, int max) { ui->progressBar->setRange(min, max); } void SubTaskProgressBar::setValue(int value) { ui->progressBar->setValue(value); } void SubTaskProgressBar::setStatus(QString status) { ui->statusLabel->setText(status); } void SubTaskProgressBar::setDetails(QString details) { ui->statusDetailsLabel->setText(details); } PrismLauncher-10.0.5/launcher/ui/widgets/LanguageSelectionWidget.cpp0000644000175100017510000000700115144136757025117 0ustar runnerrunner#include "LanguageSelectionWidget.h" #include #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "settings/Setting.h" #include "translations/TranslationsModel.h" LanguageSelectionWidget::LanguageSelectionWidget(QWidget* parent) : QWidget(parent) { verticalLayout = new QVBoxLayout(this); verticalLayout->setObjectName(QStringLiteral("verticalLayout")); languageView = new QTreeView(this); languageView->setObjectName(QStringLiteral("languageView")); languageView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); languageView->setAlternatingRowColors(true); languageView->setRootIsDecorated(false); languageView->setItemsExpandable(false); languageView->setWordWrap(true); languageView->header()->setCascadingSectionResizes(true); languageView->header()->setStretchLastSection(false); verticalLayout->addWidget(languageView); helpUsLabel = new QLabel(this); helpUsLabel->setObjectName(QStringLiteral("helpUsLabel")); helpUsLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse); helpUsLabel->setOpenExternalLinks(true); helpUsLabel->setWordWrap(true); verticalLayout->addWidget(helpUsLabel); formatCheckbox = new QCheckBox(this); formatCheckbox->setObjectName(QStringLiteral("formatCheckbox")); formatCheckbox->setCheckState(APPLICATION->settings()->get("UseSystemLocale").toBool() ? Qt::Checked : Qt::Unchecked); connect(formatCheckbox, &QCheckBox::stateChanged, [this]() { APPLICATION->translations()->setUseSystemLocale(formatCheckbox->isChecked()); }); verticalLayout->addWidget(formatCheckbox); auto translations = APPLICATION->translations(); auto index = translations->selectedIndex(); languageView->setModel(translations.get()); languageView->setCurrentIndex(index); languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch); connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged); verticalLayout->setContentsMargins(0, 0, 0, 0); auto language_setting = APPLICATION->settings()->getSetting("Language"); connect(language_setting.get(), &Setting::SettingChanged, this, &LanguageSelectionWidget::languageSettingChanged); } QString LanguageSelectionWidget::getSelectedLanguageKey() const { auto translations = APPLICATION->translations(); return translations->data(languageView->currentIndex(), Qt::UserRole).toString(); } void LanguageSelectionWidget::retranslate() { QString text = tr("Don't see your language or the quality is poor?
Help us with translations!") .arg(BuildConfig.TRANSLATIONS_URL); helpUsLabel->setText(text); formatCheckbox->setText(tr("Use system locales")); } void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous) { if (current == previous) { return; } auto translations = APPLICATION->translations(); QString key = translations->data(current, Qt::UserRole).toString(); translations->selectLanguage(key); translations->updateLanguage(key); } void LanguageSelectionWidget::languageSettingChanged(const Setting&, const QVariant&) { auto translations = APPLICATION->translations(); auto index = translations->selectedIndex(); languageView->setCurrentIndex(index); } PrismLauncher-10.0.5/launcher/ui/widgets/JavaWizardWidget.h0000644000175100017510000000561415144136757023245 0ustar runnerrunner#pragma once #include #include #include #include #include class QCheckBox; class QLineEdit; class VersionSelectWidget; class QSpinBox; class QPushButton; class QVBoxLayout; class QHBoxLayout; class QGroupBox; class QGridLayout; class QLabel; class QToolButton; class QSpacerItem; class JavaWizardWidget : public QWidget { Q_OBJECT public: explicit JavaWizardWidget(QWidget* parent); virtual ~JavaWizardWidget(); enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; enum class ValidationStatus { Bad, JavaBad, AllOK }; void refresh(); void initialize(); ValidationStatus validate(); void retranslate(); bool permGenEnabled() const; int permGenSize() const; int minHeapSize() const; int maxHeapSize() const; QString javaPath() const; bool autoDetectJava() const; bool autoDownloadJava() const; void updateThresholds(); protected slots: void onSpinBoxValueChanged(int); void memoryValueChanged(); void javaPathEdited(const QString& path); void javaVersionSelected(BaseVersion::Ptr version); void on_javaBrowseBtn_clicked(); void on_javaStatusBtn_clicked(); void javaDownloadBtn_clicked(); void checkFinished(const JavaChecker::Result& result); protected: /* methods */ void checkJavaPathOnEdit(const QString& path); void checkJavaPath(const QString& path); void setJavaStatus(JavaStatus status); void setupUi(); private: /* data */ VersionSelectWidget* m_versionWidget = nullptr; QVBoxLayout* m_verticalLayout = nullptr; QSpacerItem* m_verticalSpacer = nullptr; QLineEdit* m_javaPathTextBox = nullptr; QPushButton* m_javaBrowseBtn = nullptr; QToolButton* m_javaStatusBtn = nullptr; QHBoxLayout* m_horizontalLayout = nullptr; QGroupBox* m_memoryGroupBox = nullptr; QGridLayout* m_gridLayout_2 = nullptr; QSpinBox* m_maxMemSpinBox = nullptr; QLabel* m_labelMinMem = nullptr; QLabel* m_labelMaxMem = nullptr; QLabel* m_labelMaxMemIcon = nullptr; QSpinBox* m_minMemSpinBox = nullptr; QLabel* m_labelPermGen = nullptr; QSpinBox* m_permGenSpinBox = nullptr; QHBoxLayout* m_horizontalBtnLayout = nullptr; QPushButton* m_javaDownloadBtn = nullptr; QIcon goodIcon; QIcon yellowIcon; QIcon badIcon; QGroupBox* m_autoJavaGroupBox = nullptr; QVBoxLayout* m_veriticalJavaLayout = nullptr; QCheckBox* m_autodetectJavaCheckBox = nullptr; QCheckBox* m_autodownloadCheckBox = nullptr; unsigned int observedMinMemory = 0; unsigned int observedMaxMemory = 0; unsigned int observedPermGenMemory = 0; QString queuedCheck; uint64_t m_availableMemory = 0ull; shared_qobject_ptr m_checker; JavaChecker::Result m_result; QTimer* m_memoryTimer; }; PrismLauncher-10.0.5/launcher/ui/widgets/PageContainer.cpp0000644000175100017510000002052715144136757023111 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PageContainer.h" #include "BuildConfig.h" #include "PageContainer_p.h" #include #include #include #include #include #include #include #include #include #include #include "settings/SettingsObject.h" #include "ui/widgets/IconLabel.h" #include "Application.h" #include "DesktopServices.h" class PageEntryFilterModel : public QSortFilterProxyModel { public: explicit PageEntryFilterModel(QObject* parent = 0) : QSortFilterProxyModel(parent) {} protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { const QString pattern = filterRegularExpression().pattern(); const auto model = static_cast(sourceModel()); const auto page = model->pages().at(sourceRow); if (!page->shouldDisplay()) return false; // Regular contents check, then check page-filter. return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); } }; PageContainer::PageContainer(BasePageProvider* pageProvider, QString defaultId, QWidget* parent) : QWidget(parent) { createUI(); useSidebarStyle(true); m_model = new PageModel(this); m_proxyModel = new PageEntryFilterModel(this); int counter = 0; auto pages = pageProvider->getPages(); for (auto page : pages) { auto widget = dynamic_cast(page); widget->setParent(this); page->stackIndex = m_pageStack->addWidget(widget); page->listIndex = counter; page->setParentContainer(this); counter++; page->updateExtraInfo = [this](QString id, QString info) { if (m_currentPage && id == m_currentPage->id()) m_header->setText(m_currentPage->displayName() + info); }; } m_model->setPages(pages); m_proxyModel->setSourceModel(m_model); m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_pageList->setIconSize(QSize(pageIconSize, pageIconSize)); m_pageList->setSelectionMode(QAbstractItemView::SingleSelection); m_pageList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_pageList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); m_pageList->setModel(m_proxyModel); connect(m_pageList->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &PageContainer::currentChanged); m_pageStack->setStackingMode(QStackedLayout::StackOne); m_pageList->setFocus(); selectPage(defaultId); } bool PageContainer::selectPage(QString pageId) { // now find what we want to have selected... auto page = m_model->findPageEntryById(pageId); QModelIndex index; if (page) { index = m_proxyModel->mapFromSource(m_model->index(page->listIndex)); } if (!index.isValid()) { index = m_proxyModel->index(0, 0); } if (index.isValid()) { m_pageList->setCurrentIndex(index); return true; } return false; } BasePage* PageContainer::getPage(QString pageId) { return m_model->findPageEntryById(pageId); } BasePage* PageContainer::selectedPage() const { return m_currentPage; } const QList& PageContainer::getPages() const { return m_model->pages(); } void PageContainer::refreshContainer() { m_proxyModel->invalidate(); if (!m_currentPage->shouldDisplay()) { auto index = m_proxyModel->index(0, 0); if (index.isValid()) { m_pageList->setCurrentIndex(index); } else { // FIXME: unhandled corner case: what to do when there's no page to select? } } } void PageContainer::createUI() { m_pageStack = new QStackedLayout; m_pageList = new PageView; m_header = new QLabel(); QFont headerLabelFont = m_header->font(); headerLabelFont.setBold(true); const int pointSize = headerLabelFont.pointSize(); if (pointSize > 0) headerLabelFont.setPointSize(pointSize + 2); m_header->setFont(headerLabelFont); QHBoxLayout* headerHLayout = new QHBoxLayout; const int leftMargin = APPLICATION->style()->pixelMetric(QStyle::PM_LayoutLeftMargin); headerHLayout->addSpacerItem(new QSpacerItem(leftMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored)); headerHLayout->addWidget(m_header); headerHLayout->setContentsMargins(0, 6, 0, 0); m_pageStack->setContentsMargins(0, 0, 0, 0); m_pageStack->addWidget(new QWidget(this)); m_layout = new QGridLayout; m_layout->addLayout(headerHLayout, 0, 1, 1, 1); m_layout->addWidget(m_pageList, 0, 0, 3, 1); m_layout->addLayout(m_pageStack, 1, 1, 1, 1); m_layout->setColumnStretch(1, 4); m_layout->setContentsMargins(0, 0, 0, 0); setLayout(m_layout); } void PageContainer::retranslate() { if (m_currentPage) m_header->setText(m_currentPage->displayName()); for (auto page : m_model->pages()) page->retranslate(); } void PageContainer::addButtons(QWidget* buttons) { m_layout->addWidget(buttons, 2, 1, 1, 2); } void PageContainer::addButtons(QLayout* buttons) { m_layout->addLayout(buttons, 2, 1, 1, 2); } void PageContainer::useSidebarStyle(bool sidebar) { m_pageList->setProperty("_kde_side_panel_view", sidebar); } void PageContainer::showPage(int row) { if (m_currentPage) { m_currentPage->closed(); } if (row != -1) { m_currentPage = m_model->pages().at(row); } else { m_currentPage = nullptr; } if (m_currentPage) { m_pageStack->setCurrentIndex(m_currentPage->stackIndex); m_header->setText(m_currentPage->displayName()); m_currentPage->opened(); } else { m_pageStack->setCurrentIndex(0); m_header->setText(QString()); } } void PageContainer::help() { if (m_currentPage) { QString pageId = m_currentPage->helpPage(); if (pageId.isEmpty()) return; DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg(pageId))); } } void PageContainer::currentChanged(const QModelIndex& current) { int selected_index = current.isValid() ? m_proxyModel->mapToSource(current).row() : -1; auto* selected = m_model->pages().at(selected_index); auto* previous = m_currentPage; emit selectedPageChanged(previous, selected); showPage(selected_index); } bool PageContainer::prepareToClose() { if (!saveAll()) { return false; } if (m_currentPage) { m_currentPage->closed(); } return true; } bool PageContainer::saveAll() { for (auto page : m_model->pages()) { if (!page->apply()) return false; } return true; } void PageContainer::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { retranslate(); } QWidget::changeEvent(event); } PrismLauncher-10.0.5/launcher/ui/widgets/IconLabel.cpp0000644000175100017510000000151615144136757022217 0ustar runnerrunner#include "IconLabel.h" #include #include #include #include #include IconLabel::IconLabel(QWidget* parent, QIcon icon, QSize size) : QWidget(parent), m_size(size), m_icon(icon) { setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); } QSize IconLabel::sizeHint() const { return m_size; } void IconLabel::setIcon(QIcon icon) { m_icon = icon; update(); } void IconLabel::paintEvent(QPaintEvent*) { QPainter p(this); QRect rect = contentsRect(); int width = rect.width(); int height = rect.height(); if (width < height) { rect.setHeight(width); rect.translate(0, (height - width) / 2); } else if (width > height) { rect.setWidth(height); rect.translate((width - height) / 2, 0); } m_icon.paint(&p, rect); } PrismLauncher-10.0.5/launcher/ui/widgets/IconLabel.h0000644000175100017510000000100315144136757021653 0ustar runnerrunner#pragma once #include #include class QStyleOption; /** * This is a trivial widget that paints a QIcon of the specified size. */ class IconLabel : public QWidget { Q_OBJECT public: /// Create a line separator. orientation is the orientation of the line. explicit IconLabel(QWidget* parent, QIcon icon, QSize size); virtual QSize sizeHint() const; virtual void paintEvent(QPaintEvent*); void setIcon(QIcon icon); private: QSize m_size; QIcon m_icon; }; PrismLauncher-10.0.5/launcher/ui/widgets/JavaWizardWidget.cpp0000644000175100017510000005070615144136757023602 0ustar runnerrunner#include "JavaWizardWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DesktopServices.h" #include "FileSystem.h" #include "JavaCommon.h" #include "java/JavaChecker.h" #include "java/JavaInstall.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/java/InstallJavaDialog.h" #include "ui/widgets/VersionSelectWidget.h" #include "Application.h" #include "BuildConfig.h" JavaWizardWidget::JavaWizardWidget(QWidget* parent) : QWidget(parent) { m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; goodIcon = QIcon::fromTheme("status-good"); yellowIcon = QIcon::fromTheme("status-yellow"); badIcon = QIcon::fromTheme("status-bad"); m_memoryTimer = new QTimer(this); setupUi(); connect(m_minMemSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); connect(m_maxMemSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); connect(m_permGenSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); connect(m_memoryTimer, &QTimer::timeout, this, &JavaWizardWidget::memoryValueChanged); connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaWizardWidget::javaVersionSelected); connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaWizardWidget::on_javaBrowseBtn_clicked); connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaWizardWidget::javaPathEdited); connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaWizardWidget::on_javaStatusBtn_clicked); if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { connect(m_javaDownloadBtn, &QPushButton::clicked, this, &JavaWizardWidget::javaDownloadBtn_clicked); } } void JavaWizardWidget::setupUi() { setObjectName(QStringLiteral("javaSettingsWidget")); m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); m_versionWidget = new VersionSelectWidget(this); m_horizontalLayout = new QHBoxLayout(); m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); m_javaPathTextBox = new QLineEdit(this); m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox")); m_horizontalLayout->addWidget(m_javaPathTextBox); m_javaBrowseBtn = new QPushButton(this); m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn")); m_horizontalLayout->addWidget(m_javaBrowseBtn); m_javaStatusBtn = new QToolButton(this); m_javaStatusBtn->setIcon(yellowIcon); m_horizontalLayout->addWidget(m_javaStatusBtn); m_memoryGroupBox = new QGroupBox(this); m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox")); m_gridLayout_2 = new QGridLayout(m_memoryGroupBox); m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2")); m_gridLayout_2->setColumnStretch(0, 1); m_labelMinMem = new QLabel(m_memoryGroupBox); m_labelMinMem->setObjectName(QStringLiteral("labelMinMem")); m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1); m_minMemSpinBox = new QSpinBox(m_memoryGroupBox); m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox")); m_minMemSpinBox->setSuffix(QStringLiteral(" MiB")); m_minMemSpinBox->setMinimum(8); m_minMemSpinBox->setMaximum(1048576); m_minMemSpinBox->setSingleStep(128); m_labelMinMem->setBuddy(m_minMemSpinBox); m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1); m_labelMaxMem = new QLabel(m_memoryGroupBox); m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem")); m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1); m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox); m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox")); m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB")); m_maxMemSpinBox->setMinimum(8); m_maxMemSpinBox->setMaximum(1048576); m_maxMemSpinBox->setSingleStep(128); m_labelMaxMem->setBuddy(m_maxMemSpinBox); m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1); m_labelMaxMemIcon = new QLabel(m_memoryGroupBox); m_labelMaxMemIcon->setObjectName(QStringLiteral("labelMaxMemIcon")); m_gridLayout_2->addWidget(m_labelMaxMemIcon, 1, 2, 1, 1); m_labelPermGen = new QLabel(m_memoryGroupBox); m_labelPermGen->setObjectName(QStringLiteral("labelPermGen")); m_labelPermGen->setText(QStringLiteral("PermGen:")); m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1); m_labelPermGen->setVisible(false); m_permGenSpinBox = new QSpinBox(m_memoryGroupBox); m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox")); m_permGenSpinBox->setSuffix(QStringLiteral(" MiB")); m_permGenSpinBox->setMinimum(4); m_permGenSpinBox->setMaximum(1048576); m_permGenSpinBox->setSingleStep(8); m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1); m_permGenSpinBox->setVisible(false); m_verticalLayout->addWidget(m_memoryGroupBox); m_horizontalBtnLayout = new QHBoxLayout(); m_horizontalBtnLayout->setObjectName(QStringLiteral("horizontalBtnLayout")); if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { m_javaDownloadBtn = new QPushButton(tr("Download Java"), this); m_horizontalBtnLayout->addWidget(m_javaDownloadBtn); } m_autoJavaGroupBox = new QGroupBox(this); m_autoJavaGroupBox->setObjectName(QStringLiteral("autoJavaGroupBox")); m_veriticalJavaLayout = new QVBoxLayout(m_autoJavaGroupBox); m_veriticalJavaLayout->setObjectName(QStringLiteral("veriticalJavaLayout")); m_autodetectJavaCheckBox = new QCheckBox(m_autoJavaGroupBox); m_autodetectJavaCheckBox->setObjectName("autodetectJavaCheckBox"); m_autodetectJavaCheckBox->setChecked(true); m_veriticalJavaLayout->addWidget(m_autodetectJavaCheckBox); if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { m_autodownloadCheckBox = new QCheckBox(m_autoJavaGroupBox); m_autodownloadCheckBox->setObjectName("autodownloadCheckBox"); m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); m_veriticalJavaLayout->addWidget(m_autodownloadCheckBox); connect(m_autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); if (!m_autodetectJavaCheckBox->isChecked()) m_autodownloadCheckBox->setChecked(false); }); connect(m_autodownloadCheckBox, &QCheckBox::stateChanged, this, [this] { auto isChecked = m_autodownloadCheckBox->isChecked(); m_versionWidget->setVisible(!isChecked); m_javaStatusBtn->setVisible(!isChecked); m_javaBrowseBtn->setVisible(!isChecked); m_javaPathTextBox->setVisible(!isChecked); m_javaDownloadBtn->setVisible(!isChecked); if (!isChecked) { m_verticalLayout->removeItem(m_verticalSpacer); } else { m_verticalLayout->addSpacerItem(m_verticalSpacer); } }); } m_verticalLayout->addWidget(m_autoJavaGroupBox); m_verticalLayout->addLayout(m_horizontalBtnLayout); m_verticalLayout->addWidget(m_versionWidget); m_verticalLayout->addLayout(m_horizontalLayout); m_verticalSpacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding); retranslate(); } void JavaWizardWidget::initialize() { m_versionWidget->initialize(APPLICATION->javalist().get()); m_versionWidget->selectSearch(); m_versionWidget->setResizeOn(2); auto s = APPLICATION->settings(); // Memory observedMinMemory = s->get("MinMemAlloc").toInt(); observedMaxMemory = s->get("MaxMemAlloc").toInt(); observedPermGenMemory = s->get("PermGen").toInt(); m_minMemSpinBox->setValue(observedMinMemory); m_maxMemSpinBox->setValue(observedMaxMemory); m_permGenSpinBox->setValue(observedPermGenMemory); updateThresholds(); if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { m_autodownloadCheckBox->setChecked(true); } } void JavaWizardWidget::refresh() { if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { return; } if (JavaUtils::getJavaCheckPath().isEmpty()) { JavaCommon::javaCheckNotFound(this); return; } m_versionWidget->loadList(); } JavaWizardWidget::ValidationStatus JavaWizardWidget::validate() { switch (javaStatus) { default: case JavaStatus::NotSet: /* fallthrough */ case JavaStatus::DoesNotExist: /* fallthrough */ case JavaStatus::DoesNotStart: /* fallthrough */ case JavaStatus::ReturnedInvalidData: { if (!(BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked())) { // the java will not be autodownloaded int button = QMessageBox::No; if (m_result.mojangPlatform == "32" && maxHeapSize() > 2048) { button = CustomMessageBox::selectable( this, tr("32-bit Java detected"), tr("You selected a 32-bit installation of Java, but allocated more than 2048MiB as maximum memory.\n" "%1 will not be able to start Minecraft.\n" "Do you wish to proceed?" "\n\n" "You can change the Java version in the settings later.\n") .arg(BuildConfig.LAUNCHER_DISPLAYNAME), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, QMessageBox::NoButton) ->exec(); } else { button = CustomMessageBox::selectable(this, tr("No Java version selected"), tr("You either didn't select a Java version or selected one that does not work.\n" "%1 will not be able to start Minecraft.\n" "Do you wish to proceed without a functional version of Java?" "\n\n" "You can change the Java version in the settings later.\n") .arg(BuildConfig.LAUNCHER_DISPLAYNAME), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, QMessageBox::NoButton) ->exec(); } switch (button) { case QMessageBox::Yes: return ValidationStatus::JavaBad; case QMessageBox::Help: DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("java-wizard"))); /* fallthrough */ case QMessageBox::No: /* fallthrough */ default: return ValidationStatus::Bad; } if (button == QMessageBox::No) { return ValidationStatus::Bad; } } return ValidationStatus::JavaBad; } break; case JavaStatus::Pending: { return ValidationStatus::Bad; } case JavaStatus::Good: { return ValidationStatus::AllOK; } } } QString JavaWizardWidget::javaPath() const { return m_javaPathTextBox->text(); } int JavaWizardWidget::maxHeapSize() const { auto min = m_minMemSpinBox->value(); auto max = m_maxMemSpinBox->value(); if (max < min) max = min; return max; } int JavaWizardWidget::minHeapSize() const { auto min = m_minMemSpinBox->value(); auto max = m_maxMemSpinBox->value(); if (min > max) min = max; return min; } bool JavaWizardWidget::permGenEnabled() const { return m_permGenSpinBox->isVisible(); } int JavaWizardWidget::permGenSize() const { return m_permGenSpinBox->value(); } void JavaWizardWidget::memoryValueChanged() { bool actuallyChanged = false; unsigned int min = m_minMemSpinBox->value(); unsigned int max = m_maxMemSpinBox->value(); unsigned int permgen = m_permGenSpinBox->value(); if (min != observedMinMemory) { observedMinMemory = min; actuallyChanged = true; } if (max != observedMaxMemory) { observedMaxMemory = max; actuallyChanged = true; } if (permgen != observedPermGenMemory) { observedPermGenMemory = permgen; actuallyChanged = true; } if (actuallyChanged) { checkJavaPathOnEdit(m_javaPathTextBox->text()); updateThresholds(); } } void JavaWizardWidget::javaVersionSelected(BaseVersion::Ptr version) { auto java = std::dynamic_pointer_cast(version); if (!java) { return; } auto visible = java->id.requiresPermGen(); m_labelPermGen->setVisible(visible); m_permGenSpinBox->setVisible(visible); m_javaPathTextBox->setText(java->path); checkJavaPath(java->path); } void JavaWizardWidget::on_javaBrowseBtn_clicked() { auto filter = QString("Java (%1)").arg(JavaUtils::javaExecutable); auto raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter); if (raw_path.isEmpty()) { return; } auto cooked_path = FS::NormalizePath(raw_path); m_javaPathTextBox->setText(cooked_path); checkJavaPath(cooked_path); } void JavaWizardWidget::javaDownloadBtn_clicked() { auto jdialog = new Java::InstallDialog({}, nullptr, this); jdialog->exec(); } void JavaWizardWidget::on_javaStatusBtn_clicked() { QString text; bool failed = false; switch (javaStatus) { case JavaStatus::NotSet: checkJavaPath(m_javaPathTextBox->text()); return; case JavaStatus::DoesNotExist: text += QObject::tr("The specified file either doesn't exist or is not a proper executable."); failed = true; break; case JavaStatus::DoesNotStart: { text += QObject::tr("The specified Java binary didn't start properly.
"); auto htmlError = m_result.errorLog; if (!htmlError.isEmpty()) { htmlError.replace('\n', "
"); text += QString("%1").arg(htmlError); } failed = true; break; } case JavaStatus::ReturnedInvalidData: { text += QObject::tr("The specified Java binary returned unexpected results:
"); auto htmlOut = m_result.outLog; if (!htmlOut.isEmpty()) { htmlOut.replace('\n', "
"); text += QString("%1").arg(htmlOut); } failed = true; break; } case JavaStatus::Good: text += QObject::tr( "Java test succeeded!
Platform reported: %1
Java version " "reported: %2
") .arg(m_result.realPlatform, m_result.javaVersion.toString()); break; case JavaStatus::Pending: // TODO: abort here? return; } CustomMessageBox::selectable(this, failed ? QObject::tr("Java test failure") : QObject::tr("Java test success"), text, failed ? QMessageBox::Critical : QMessageBox::Information) ->show(); } void JavaWizardWidget::setJavaStatus(JavaWizardWidget::JavaStatus status) { javaStatus = status; switch (javaStatus) { case JavaStatus::Good: m_javaStatusBtn->setIcon(goodIcon); break; case JavaStatus::NotSet: case JavaStatus::Pending: m_javaStatusBtn->setIcon(yellowIcon); break; default: m_javaStatusBtn->setIcon(badIcon); break; } } void JavaWizardWidget::javaPathEdited(const QString& path) { checkJavaPathOnEdit(path); } void JavaWizardWidget::checkJavaPathOnEdit(const QString& path) { auto realPath = FS::ResolveExecutable(path); QFileInfo pathInfo(realPath); if (pathInfo.baseName().toLower().contains("java")) { checkJavaPath(path); } else { if (!m_checker) { setJavaStatus(JavaStatus::NotSet); } } } void JavaWizardWidget::checkJavaPath(const QString& path) { if (m_checker) { queuedCheck = path; return; } auto realPath = FS::ResolveExecutable(path); if (realPath.isNull()) { setJavaStatus(JavaStatus::DoesNotExist); return; } setJavaStatus(JavaStatus::Pending); m_checker.reset( new JavaChecker(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0)); connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaWizardWidget::checkFinished); m_checker->start(); } void JavaWizardWidget::checkFinished(const JavaChecker::Result& result) { m_result = result; switch (result.validity) { case JavaChecker::Result::Validity::Valid: { setJavaStatus(JavaStatus::Good); break; } case JavaChecker::Result::Validity::ReturnedInvalidData: { setJavaStatus(JavaStatus::ReturnedInvalidData); break; } case JavaChecker::Result::Validity::Errored: { setJavaStatus(JavaStatus::DoesNotStart); break; } } updateThresholds(); m_checker.reset(); if (!queuedCheck.isNull()) { checkJavaPath(queuedCheck); queuedCheck.clear(); } } void JavaWizardWidget::retranslate() { m_memoryGroupBox->setTitle(tr("Memory")); m_maxMemSpinBox->setToolTip(tr("The maximum amount of memory Minecraft is allowed to use.")); m_labelMinMem->setText(tr("Minimum memory allocation:")); m_labelMaxMem->setText(tr("Maximum memory allocation:")); m_minMemSpinBox->setToolTip(tr("The amount of memory Minecraft is started with.")); m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes.")); m_javaBrowseBtn->setText(tr("Browse")); if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { m_autodownloadCheckBox->setText(tr("Auto-download Mojang Java")); } m_autodetectJavaCheckBox->setText(tr("Auto-detect Java version")); m_autoJavaGroupBox->setTitle(tr("Autodetect Java")); } void JavaWizardWidget::updateThresholds() { QString iconName; if (observedMaxMemory >= m_availableMemory) { iconName = "status-bad"; m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); } else if (observedMaxMemory > (m_availableMemory * 0.9)) { iconName = "status-yellow"; m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); } else if (observedMaxMemory < observedMinMemory) { iconName = "status-yellow"; m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); } else if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked()) { iconName = "status-good"; m_labelMaxMemIcon->setToolTip(""); } else if (observedMaxMemory > 2048 && !m_result.is_64bit) { iconName = "status-bad"; m_labelMaxMemIcon->setToolTip(tr("You are exceeding the maximum allocation supported by 32-bit installations of Java.")); } else { iconName = "status-good"; m_labelMaxMemIcon->setToolTip(""); } { auto height = m_labelMaxMemIcon->fontInfo().pixelSize(); QIcon icon = QIcon::fromTheme(iconName); QPixmap pix = icon.pixmap(height, height); m_labelMaxMemIcon->setPixmap(pix); } } bool JavaWizardWidget::autoDownloadJava() const { return m_autodownloadCheckBox && m_autodownloadCheckBox->isChecked(); } bool JavaWizardWidget::autoDetectJava() const { return m_autodetectJavaCheckBox->isChecked(); } void JavaWizardWidget::onSpinBoxValueChanged(int) { m_memoryTimer->start(500); } JavaWizardWidget::~JavaWizardWidget() { delete m_verticalSpacer; }; PrismLauncher-10.0.5/launcher/ui/widgets/MinecraftSettingsWidget.h0000644000175100017510000000450315144136757024630 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "JavaSettingsWidget.h" #include "minecraft/MinecraftInstance.h" namespace Ui { class MinecraftSettingsWidget; } class MinecraftSettingsWidget : public QWidget { public: MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent = nullptr); ~MinecraftSettingsWidget() override; void loadSettings(); void saveSettings(); private: void openGlobalSettings(); void updateAccountsMenu(SettingsObject& settings); bool isQuickPlaySupported(); private slots: void saveSelectedLoaders(); void saveDataPacksPath(); void selectDataPacksFolder(); MinecraftInstancePtr m_instance; Ui::MinecraftSettingsWidget* m_ui; JavaSettingsWidget* m_javaSettings = nullptr; bool m_quickPlaySingleplayer = false; }; PrismLauncher-10.0.5/launcher/ui/widgets/EnvironmentVariables.h0000644000175100017510000000243315144136757024170 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include namespace Ui { class EnvironmentVariables; } class EnvironmentVariables : public QWidget { Q_OBJECT public: explicit EnvironmentVariables(QWidget* state = nullptr); ~EnvironmentVariables() override; void initialize(bool instance, bool override, const QMap& value); bool eventFilter(QObject* watched, QEvent* event) override; void retranslate(); bool override() const; QMap value() const; private: Ui::EnvironmentVariables* ui; }; PrismLauncher-10.0.5/launcher/ui/widgets/LabeledToolButton.cpp0000644000175100017510000000725715144136757023761 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LabeledToolButton.h" #include #include #include #include #include #include /* * * Tool Button with a label on it, instead of the normal text rendering * */ LabeledToolButton::LabeledToolButton(QWidget* parent) : QToolButton(parent), m_label(new QLabel(this)) { // QToolButton::setText(" "); m_label->setWordWrap(true); m_label->setMouseTracking(false); m_label->setAlignment(Qt::AlignCenter); m_label->setTextInteractionFlags(Qt::NoTextInteraction); // somehow, this makes word wrap work in the QLabel. yay. // m_label->setMinimumWidth(100); } QString LabeledToolButton::text() const { return m_label->text(); } void LabeledToolButton::setText(const QString& text) { m_label->setText(text); } void LabeledToolButton::setIcon(QIcon icon) { m_icon = icon; resetIcon(); } /*! \reimp */ QSize LabeledToolButton::sizeHint() const { /* Q_D(const QToolButton); if (d->sizeHint.isValid()) return d->sizeHint; */ ensurePolished(); int w = 0, h = 0; QStyleOptionToolButton opt; initStyleOption(&opt); QSize sz = m_label->sizeHint(); w = sz.width(); h = sz.height(); opt.rect.setSize(QSize(w, h)); // PM_MenuButtonIndicator depends on the height if (popupMode() == MenuButtonPopup) w += style()->pixelMetric(QStyle::PM_MenuButtonIndicator, &opt, this); return style()->sizeFromContents(QStyle::CT_ToolButton, &opt, QSize(w, h), this); } void LabeledToolButton::resizeEvent(QResizeEvent* event) { m_label->setGeometry(QRect(4, 4, width() - 8, height() - 8)); if (!m_icon.isNull()) { resetIcon(); } QWidget::resizeEvent(event); } void LabeledToolButton::resetIcon() { auto iconSz = m_icon.actualSize(QSize(160, 80)); float w = iconSz.width(); float h = iconSz.height(); float ar = w / h; // FIXME: hardcoded max size of 160x80 int newW = 80 * ar; if (newW > 160) newW = 160; QSize newSz(newW, 80); auto pixmap = m_icon.pixmap(newSz); m_label->setPixmap(pixmap); m_label->setMinimumHeight(80); m_label->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); } PrismLauncher-10.0.5/launcher/ui/instanceview/0000755000175100017510000000000015144136756020710 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/instanceview/InstanceView.h0000644000175100017510000001405215144136756023462 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include "VisualGroup.h" #include "ui/themes/CatPainter.h" struct InstanceViewRoles { enum { GroupRole = Qt::UserRole, ProgressValueRole, ProgressMaximumRole }; }; class InstanceView : public QAbstractItemView { Q_OBJECT public: InstanceView(QWidget* parent = 0); ~InstanceView(); void setModel(QAbstractItemModel* model) override; using visibilityFunction = std::function; void setSourceOfGroupCollapseStatus(visibilityFunction f) { m_fVisibility = f; } /// return geometry rectangle occupied by the specified model item QRect geometryRect(const QModelIndex& index) const; /// return visual rectangle occupied by the specified model item virtual QRect visualRect(const QModelIndex& index) const override; /// get the model index at the specified visual point virtual QModelIndex indexAt(const QPoint& point) const override; QString groupNameAt(const QPoint& point); void setSelection(const QRect& rect, QItemSelectionModel::SelectionFlags commands) override; virtual int horizontalOffset() const override; virtual int verticalOffset() const override; virtual void scrollContentsBy(int dx, int dy) override; virtual void scrollTo(const QModelIndex& index, ScrollHint hint = EnsureVisible) override; virtual QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override; virtual QRegion visualRegionForSelection(const QItemSelection& selection) const override; int spacing() const { return m_spacing; }; void setPaintCat(bool visible); public slots: virtual void updateGeometries() override; protected slots: virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) override; virtual void rowsInserted(const QModelIndex& parent, int start, int end) override; virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; void modelReset(); void rowsRemoved(); void currentChanged(const QModelIndex& current, const QModelIndex& previous) override; signals: void droppedURLs(QList urls); void groupStateChanged(QString group, bool collapsed); protected: bool isIndexHidden(const QModelIndex& index) const override; void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override; void paintEvent(QPaintEvent* event) override; void resizeEvent(QResizeEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; void dragLeaveEvent(QDragLeaveEvent* event) override; void dropEvent(QDropEvent* event) override; void startDrag(Qt::DropActions supportedActions) override; void updateScrollbar(); private: friend struct VisualGroup; QList m_groups; visibilityFunction m_fVisibility; // geometry int m_leftMargin = 5; int m_rightMargin = 5; int m_bottomMargin = 5; int m_categoryMargin = 5; int m_spacing = 5; int m_itemWidth = 100; int m_currentItemsPerRow = -1; int m_currentCursorColumn = -1; mutable QCache m_geometryCache; CatPainter* m_cat = nullptr; // point where the currently active mouse action started in geometry coordinates QPoint m_pressedPosition; QPersistentModelIndex m_pressedIndex; bool m_pressedAlreadySelected; VisualGroup* m_pressedCategory; QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag; QPoint m_lastDragPosition; VisualGroup* category(const QModelIndex& index) const; VisualGroup* category(const QString& cat) const; VisualGroup* categoryAt(const QPoint& pos, VisualGroup::HitResults& result) const; int itemsPerRow() const { return m_currentItemsPerRow; }; int contentWidth() const; private: /* methods */ int itemWidth() const; int calculateItemsPerRow() const; int verticalScrollToValue(const QModelIndex& index, const QRect& rect, QListView::ScrollHint hint) const; QPixmap renderToPixmap(const QModelIndexList& indices, QRect* r) const; QList> draggablePaintPairs(const QModelIndexList& indices, QRect* r) const; bool isDragEventAccepted(QDropEvent* event); std::pair rowDropPos(const QPoint& pos); QPoint offset() const; }; PrismLauncher-10.0.5/launcher/ui/instanceview/VisualGroup.cpp0000644000175100017510000002101015144136756023666 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "VisualGroup.h" #include #include #include #include #include #include #include "InstanceView.h" VisualGroup::VisualGroup(QString text, InstanceView* view) : view(view), text(std::move(text)), collapsed(false) {} VisualGroup::VisualGroup(const VisualGroup* other) : view(other->view), text(other->text), collapsed(other->collapsed) {} void VisualGroup::update() { auto temp_items = items(); auto itemsPerRow = view->itemsPerRow(); int numRows = qMax(1, qCeil((qreal)temp_items.size() / (qreal)itemsPerRow)); rows = QList(numRows); int maxRowHeight = 0; int positionInRow = 0; int currentRow = 0; int offsetFromTop = 0; for (auto item : temp_items) { if (positionInRow == itemsPerRow) { rows[currentRow].height = maxRowHeight; rows[currentRow].top = offsetFromTop; currentRow++; if (currentRow >= rows.size()) { currentRow = rows.size() - 1; } offsetFromTop += maxRowHeight + 5; positionInRow = 0; maxRowHeight = 0; } QStyleOptionViewItem viewItemOption; view->initViewItemOption(&viewItemOption); auto itemHeight = view->itemDelegate()->sizeHint(viewItemOption, item).height(); if (itemHeight > maxRowHeight) { maxRowHeight = itemHeight; } rows[currentRow].items.append(item); positionInRow++; } rows[currentRow].height = maxRowHeight; rows[currentRow].top = offsetFromTop; } QPair VisualGroup::positionOf(const QModelIndex& index) const { int y = 0; for (auto& row : rows) { for (auto x = 0; x < row.items.size(); x++) { if (row.items[x] == index) { return qMakePair(x, y); } } y++; } qWarning() << "Item" << index.row() << index.data(Qt::DisplayRole).toString() << "not found in visual group" << text; return qMakePair(0, 0); } int VisualGroup::rowTopOf(const QModelIndex& index) const { auto position = positionOf(index); return rows[position.second].top; } int VisualGroup::rowHeightOf(const QModelIndex& index) const { auto position = positionOf(index); return rows[position.second].height; } VisualGroup::HitResults VisualGroup::hitScan(const QPoint& pos) const { VisualGroup::HitResults results = VisualGroup::NoHit; int y_start = verticalPosition(); int body_start = y_start + headerHeight(); int body_end = body_start + contentHeight(); int y = pos.y(); // int x = pos.x(); if (y < y_start) { results = VisualGroup::NoHit; } else if (y < body_start) { results = VisualGroup::HeaderHit; int collapseSize = headerHeight() - 4; // the icon QRect iconRect = QRect(view->m_leftMargin + 2, 2 + y_start, view->width() - 4, collapseSize); if (iconRect.contains(pos)) { results |= VisualGroup::CheckboxHit; } } else if (y < body_end) { results |= VisualGroup::BodyHit; } return results; } void VisualGroup::drawHeader(QPainter* painter, const QStyleOptionViewItem& option) const { QRect optRect = option.rect; optRect.setTop(optRect.top() + 7); QFont font(QApplication::font()); font.setBold(true); const QFontMetrics fontMetrics = QFontMetrics(font); painter->setFont(font); QPen pen; pen.setWidth(2); QColor penColor = option.palette.text().color(); penColor.setAlphaF(0.6); pen.setColor(penColor); painter->setPen(pen); painter->setRenderHint(QPainter::Antialiasing); // sizes and offsets, to keep things consistent below const int arrowOffsetLeft = fontMetrics.height() / 2 + 7; const int textOffsetLeft = arrowOffsetLeft * 2; const int centerHeight = optRect.top() + fontMetrics.height() / 2; const QString& textToDraw = text.isEmpty() ? QObject::tr("Ungrouped") : text; // BEGIN: arrow { constexpr int arrowSize = 6; QPolygon arrowPolygon; if (collapsed) { arrowPolygon << QPoint(arrowOffsetLeft - arrowSize / 2, centerHeight - arrowSize) << QPoint(arrowOffsetLeft + arrowSize / 2, centerHeight) << QPoint(arrowOffsetLeft - arrowSize / 2, centerHeight + arrowSize); painter->drawPolyline(arrowPolygon); } else { arrowPolygon << QPoint(arrowOffsetLeft - arrowSize, centerHeight - arrowSize / 2) << QPoint(arrowOffsetLeft, centerHeight + arrowSize / 2) << QPoint(arrowOffsetLeft + arrowSize, centerHeight - arrowSize / 2); painter->drawPolyline(arrowPolygon); } } // END: arrow // BEGIN: text { QRect textRect(optRect); textRect.setTop(textRect.top()); textRect.setLeft(textOffsetLeft); textRect.setHeight(fontMetrics.height()); textRect.setRight(textRect.right() - 7); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, textToDraw); } // END: text // BEGIN: horizontal line { penColor.setAlphaF(0.05); pen.setColor(penColor); painter->setPen(pen); // startPoint is left + arrow + text + space const int startPoint = optRect.left() + fontMetrics.height() + fontMetrics.size(Qt::AlignLeft | Qt::AlignVCenter, textToDraw).width() + 20; painter->setRenderHint(QPainter::Antialiasing, false); QPolygon polygon; // for some reason the height (yPos) doesn't look centered, so we are adding 1 to the center height const int lineHeight = centerHeight + 1; polygon << QPoint(startPoint, lineHeight) << QPoint(optRect.right() - 3, lineHeight); painter->drawPolyline(polygon); } // END: horizontal line } int VisualGroup::totalHeight() const { return headerHeight() + contentHeight(); } int VisualGroup::headerHeight() { QFont font(QApplication::font()); font.setBold(true); QFontMetrics fontMetrics(font); const int height = fontMetrics.height() + 1 /* 1 pixel-width gradient */ + 11 /* top and bottom separation */; return height; /* int raw = view->viewport()->fontMetrics().height() + 4; // add english. maybe. depends on font height. if (raw % 2 == 0) raw++; return std::min(raw, 25); */ } int VisualGroup::contentHeight() const { if (collapsed) { return 0; } auto last = rows[numRows() - 1]; return last.top + last.height; } int VisualGroup::numRows() const { return (int)rows.size(); } int VisualGroup::verticalPosition() const { return m_verticalPosition; } QList VisualGroup::items() const { QList indices; for (int i = 0; i < view->model()->rowCount(); ++i) { const QModelIndex index = view->model()->index(i, 0); if (index.data(InstanceViewRoles::GroupRole).toString() == text) { indices.append(index); } } return indices; } PrismLauncher-10.0.5/launcher/ui/instanceview/InstanceDelegate.h0000644000175100017510000000310215144136756024254 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class ListViewDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit ListViewDelegate(QObject* parent = 0); virtual ~ListViewDelegate() {} void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const override; QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; void setEditorData(QWidget* editor, const QModelIndex& index) const override; void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; signals: void textChanged(QString before, QString after) const; private slots: void editingDone(); }; PrismLauncher-10.0.5/launcher/ui/instanceview/InstanceProxyModel.cpp0000644000175100017510000000505715144136756025212 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "InstanceProxyModel.h" #include #include #include "Application.h" #include "InstanceView.h" #include InstanceProxyModel::InstanceProxyModel(QObject* parent) : QSortFilterProxyModel(parent) { m_naturalSort.setNumericMode(true); m_naturalSort.setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); // FIXME: use loaded translation as source of locale instead, hook this up to translation changes m_naturalSort.setLocale(QLocale::system()); } QVariant InstanceProxyModel::data(const QModelIndex& index, int role) const { QVariant data = QSortFilterProxyModel::data(index, role); if (role == Qt::DecorationRole) { return QVariant(APPLICATION->icons()->getIcon(data.toString())); } return data; } bool InstanceProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { const QString leftCategory = left.data(InstanceViewRoles::GroupRole).toString(); const QString rightCategory = right.data(InstanceViewRoles::GroupRole).toString(); if (leftCategory == rightCategory) { return subSortLessThan(left, right); } else { // FIXME: real group sorting happens in InstanceView::updateGeometries(), see LocaleString auto result = leftCategory.localeAwareCompare(rightCategory); if (result == 0) { return subSortLessThan(left, right); } return result < 0; } } bool InstanceProxyModel::subSortLessThan(const QModelIndex& left, const QModelIndex& right) const { BaseInstance* pdataLeft = static_cast(left.internalPointer()); BaseInstance* pdataRight = static_cast(right.internalPointer()); QString sortMode = APPLICATION->settings()->get("InstSortMode").toString(); if (sortMode == "LastLaunch") { return pdataLeft->lastLaunch() > pdataRight->lastLaunch(); } else { return m_naturalSort.compare(pdataLeft->name(), pdataRight->name()) < 0; } } PrismLauncher-10.0.5/launcher/ui/instanceview/InstanceView.cpp0000644000175100017510000007777315144136756024040 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "InstanceView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "VisualGroup.h" #include "ui/themes/CatPainter.h" #include "ui/themes/ThemeManager.h" #include #include template bool listsIntersect(const QList& l1, const QList t2) { for (auto& item : l1) { if (t2.contains(item)) { return true; } } return false; } InstanceView::InstanceView(QWidget* parent) : QAbstractItemView(parent) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setAcceptDrops(true); setAutoScroll(true); setPaintCat(APPLICATION->settings()->get("TheCat").toBool()); connect(verticalScrollBar(), &QScrollBar::valueChanged, viewport(), QOverload<>::of(&QWidget::update)); connect(horizontalScrollBar(), &QScrollBar::valueChanged, viewport(), QOverload<>::of(&QWidget::update)); } InstanceView::~InstanceView() { qDeleteAll(m_groups); m_groups.clear(); if (m_cat) { m_cat->deleteLater(); } } void InstanceView::setModel(QAbstractItemModel* model) { QAbstractItemView::setModel(model); connect(model, &QAbstractItemModel::modelReset, this, &InstanceView::modelReset); connect(model, &QAbstractItemModel::rowsRemoved, this, &InstanceView::rowsRemoved); } void InstanceView::dataChanged([[maybe_unused]] const QModelIndex& topLeft, [[maybe_unused]] const QModelIndex& bottomRight, [[maybe_unused]] const QList& roles) { scheduleDelayedItemsLayout(); } void InstanceView::rowsInserted([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int start, [[maybe_unused]] int end) { scheduleDelayedItemsLayout(); } void InstanceView::rowsAboutToBeRemoved([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int start, [[maybe_unused]] int end) { scheduleDelayedItemsLayout(); } void InstanceView::modelReset() { scheduleDelayedItemsLayout(); } void InstanceView::rowsRemoved() { scheduleDelayedItemsLayout(); } void InstanceView::currentChanged(const QModelIndex& current, const QModelIndex& previous) { QAbstractItemView::currentChanged(current, previous); // TODO: for accessibility support, implement+register a factory, steal QAccessibleTable from Qt and return an instance of it for // InstanceView. #ifndef QT_NO_ACCESSIBILITY if (QAccessible::isActive() && current.isValid()) { QAccessibleEvent event(this, QAccessible::Focus); event.setChild(current.row()); QAccessible::updateAccessibility(&event); } #endif /* !QT_NO_ACCESSIBILITY */ } class LocaleString : public QString { public: LocaleString(const char* s) : QString(s) {} LocaleString(const QString& s) : QString(s) {} }; inline bool operator<(const LocaleString& lhs, const LocaleString& rhs) { return (QString::localeAwareCompare(lhs, rhs) < 0); } void InstanceView::updateScrollbar() { int previousScroll = verticalScrollBar()->value(); if (m_groups.isEmpty()) { verticalScrollBar()->setRange(0, 0); } else { int totalHeight = 0; // top margin totalHeight += m_categoryMargin; int itemScroll = 0; for (auto category : m_groups) { category->m_verticalPosition = totalHeight; totalHeight += category->totalHeight() + m_categoryMargin; if (!itemScroll && category->totalHeight() != 0) { itemScroll = category->contentHeight() / category->numRows(); } } // do not divide by zero if (itemScroll == 0) itemScroll = 64; totalHeight += m_bottomMargin; verticalScrollBar()->setSingleStep(itemScroll); const int rowsPerPage = qMax(viewport()->height() / itemScroll, 1); verticalScrollBar()->setPageStep(rowsPerPage * itemScroll); verticalScrollBar()->setRange(0, totalHeight - height()); } verticalScrollBar()->setValue(qMin(previousScroll, verticalScrollBar()->maximum())); } void InstanceView::updateGeometries() { m_geometryCache.clear(); QMap cats; for (int i = 0; i < model()->rowCount(); ++i) { const QString groupName = model()->index(i, 0).data(InstanceViewRoles::GroupRole).toString(); if (!cats.contains(groupName)) { VisualGroup* old = this->category(groupName); if (old) { auto cat = new VisualGroup(old); cats.insert(groupName, cat); cat->update(); } else { auto cat = new VisualGroup(groupName, this); if (m_fVisibility) { cat->collapsed = m_fVisibility(groupName); } cats.insert(groupName, cat); cat->update(); } } } qDeleteAll(m_groups); m_groups = cats.values(); updateScrollbar(); viewport()->update(); } bool InstanceView::isIndexHidden(const QModelIndex& index) const { VisualGroup* cat = category(index); if (cat) { return cat->collapsed; } else { return false; } } VisualGroup* InstanceView::category(const QModelIndex& index) const { return category(index.data(InstanceViewRoles::GroupRole).toString()); } VisualGroup* InstanceView::category(const QString& cat) const { for (auto group : m_groups) { if (group->text == cat) { return group; } } return nullptr; } VisualGroup* InstanceView::categoryAt(const QPoint& pos, VisualGroup::HitResults& result) const { for (auto group : m_groups) { result = group->hitScan(pos); if (result != VisualGroup::NoHit) { return group; } } result = VisualGroup::NoHit; return nullptr; } QString InstanceView::groupNameAt(const QPoint& point) { executeDelayedItemsLayout(); VisualGroup::HitResults hitResult; auto group = categoryAt(point + offset(), hitResult); if (group && (hitResult & (VisualGroup::HeaderHit | VisualGroup::BodyHit))) { return group->text; } return QString(); } int InstanceView::calculateItemsPerRow() const { return qFloor((qreal)(contentWidth()) / (qreal)(itemWidth() + m_spacing)); } int InstanceView::contentWidth() const { return width() - m_leftMargin - m_rightMargin; } int InstanceView::itemWidth() const { return m_itemWidth; } void InstanceView::mousePressEvent(QMouseEvent* event) { executeDelayedItemsLayout(); QPoint visualPos = event->pos(); QPoint geometryPos = event->pos() + offset(); QPersistentModelIndex index = indexAt(visualPos); m_pressedIndex = index; m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex); m_pressedPosition = geometryPos; if (event->button() == Qt::LeftButton) { VisualGroup::HitResults hitResult; m_pressedCategory = categoryAt(geometryPos, hitResult); if (m_pressedCategory && hitResult & VisualGroup::CheckboxHit) { setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState); event->accept(); return; } } if (index.isValid() && (index.flags() & Qt::ItemIsEnabled)) { if (index != currentIndex()) { // FIXME: better! m_currentCursorColumn = -1; } // we disable scrollTo for mouse press so the item doesn't change position // when the user is interacting with it (ie. clicking on it) bool autoScroll = hasAutoScroll(); setAutoScroll(false); selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); setAutoScroll(autoScroll); QRect rect(visualPos, visualPos); setSelection(rect, QItemSelectionModel::ClearAndSelect); // signal handlers may change the model emit pressed(index); } else { // Forces a finalize() even if mouse is pressed, but not on a item selectionModel()->select(QModelIndex(), QItemSelectionModel::Select); } } void InstanceView::mouseMoveEvent(QMouseEvent* event) { executeDelayedItemsLayout(); QPoint topLeft; QPoint visualPos = event->pos(); QPoint geometryPos = event->pos() + offset(); if (state() == ExpandingState || state() == CollapsingState) { return; } if (state() == DraggingState) { topLeft = m_pressedPosition - offset(); if ((topLeft - event->pos()).manhattanLength() > QApplication::startDragDistance()) { m_pressedIndex = QModelIndex(); startDrag(model()->supportedDragActions()); setState(NoState); stopAutoScroll(); } return; } if (selectionMode() != SingleSelection) { topLeft = m_pressedPosition - offset(); } else { topLeft = geometryPos; } if (m_pressedIndex.isValid() && (state() != DragSelectingState) && (event->buttons() != Qt::NoButton) && !selectedIndexes().isEmpty()) { setState(DraggingState); return; } if ((event->buttons() & Qt::LeftButton) && selectionModel()) { setState(DragSelectingState); setSelection(QRect(visualPos, visualPos), QItemSelectionModel::ClearAndSelect); QModelIndex index = indexAt(visualPos); // set at the end because it might scroll the view if (index.isValid() && (index != selectionModel()->currentIndex()) && (index.flags() & Qt::ItemIsEnabled)) { selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); } } } void InstanceView::mouseReleaseEvent(QMouseEvent* event) { executeDelayedItemsLayout(); QPoint visualPos = event->pos(); QPoint geometryPos = event->pos() + offset(); QPersistentModelIndex index = indexAt(visualPos); VisualGroup::HitResults hitResult; if (event->button() == Qt::LeftButton && m_pressedCategory != nullptr && m_pressedCategory == categoryAt(geometryPos, hitResult)) { if (state() == ExpandingState) { m_pressedCategory->collapsed = false; emit groupStateChanged(m_pressedCategory->text, false); updateGeometries(); viewport()->update(); event->accept(); m_pressedCategory = nullptr; setState(NoState); return; } else if (state() == CollapsingState) { m_pressedCategory->collapsed = true; emit groupStateChanged(m_pressedCategory->text, true); updateGeometries(); viewport()->update(); event->accept(); m_pressedCategory = nullptr; setState(NoState); return; } } m_ctrlDragSelectionFlag = QItemSelectionModel::NoUpdate; setState(NoState); if (index == m_pressedIndex && index.isValid()) { if (event->button() == Qt::LeftButton) { emit clicked(index); } QStyleOptionViewItem option; initViewItemOption(&option); if (m_pressedAlreadySelected) { option.state |= QStyle::State_Selected; } if ((model()->flags(index) & Qt::ItemIsEnabled) && style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) { emit activated(index); } } } void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) { executeDelayedItemsLayout(); QModelIndex index = indexAt(event->pos()); if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index)) { QMouseEvent me(QEvent::MouseButtonPress, event->position(), event->scenePosition(), event->globalPosition(), event->button(), event->buttons(), event->modifiers()); mousePressEvent(&me); return; } // signal handlers may change the model QPersistentModelIndex persistent = index; emit doubleClicked(persistent); QStyleOptionViewItem option; initViewItemOption(&option); if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) { emit activated(index); } } void InstanceView::setPaintCat(bool visible) { if (m_cat) { disconnect(m_cat, &CatPainter::updateFrame, this, nullptr); delete m_cat; m_cat = nullptr; } if (visible) { m_cat = new CatPainter(APPLICATION->themeManager()->getCatPack(), this); connect(m_cat, &CatPainter::updateFrame, this, [this] { viewport()->update(); }); } } void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) { executeDelayedItemsLayout(); QPainter painter(this->viewport()); if (m_cat) { m_cat->paint(&painter, this->viewport()->rect()); } QStyleOptionViewItem option; initViewItemOption(&option); option.widget = this; if (model()->rowCount() == 0) { painter.save(); QString emptyString = tr("Welcome!") + "\n" + tr("Click \"Add Instance\" to get started."); // calculate the rect for the overlay painter.setRenderHint(QPainter::Antialiasing, true); QFont font("sans", 20); font.setBold(true); QRect bounds = viewport()->geometry(); bounds.moveTop(0); auto innerBounds = bounds; innerBounds.adjust(10, 10, -10, -10); QColor background = QApplication::palette().color(QPalette::WindowText); QColor foreground = QApplication::palette().color(QPalette::Base); foreground.setAlpha(190); painter.setFont(font); auto fontMetrics = painter.fontMetrics(); auto textRect = fontMetrics.boundingRect(innerBounds, Qt::AlignHCenter | Qt::TextWordWrap, emptyString); textRect.moveCenter(bounds.center()); auto wrapRect = textRect; wrapRect.adjust(-10, -10, 10, 10); // check if we are allowed to draw in our area if (!event->rect().intersects(wrapRect)) { return; } painter.setBrush(QBrush(background)); painter.setPen(foreground); painter.drawRoundedRect(wrapRect, 5.0, 5.0); painter.setPen(foreground); painter.setFont(font); painter.drawText(textRect, Qt::AlignHCenter | Qt::TextWordWrap, emptyString); painter.restore(); return; } int wpWidth = viewport()->width(); option.rect.setWidth(wpWidth); for (int i = 0; i < m_groups.size(); ++i) { VisualGroup* category = m_groups.at(i); int y = category->verticalPosition(); y -= verticalOffset(); QRect backup = option.rect; int height = category->totalHeight(); option.rect.setTop(y); option.rect.setHeight(height); option.rect.setLeft(m_leftMargin); option.rect.setRight(wpWidth - m_rightMargin); category->drawHeader(&painter, option); y += category->totalHeight() + m_categoryMargin; option.rect = backup; } for (int i = 0; i < model()->rowCount(); ++i) { const QModelIndex index = model()->index(i, 0); if (isIndexHidden(index)) { continue; } Qt::ItemFlags flags = index.flags(); option.rect = visualRect(index); option.features |= QStyleOptionViewItem::WrapText; if (flags & Qt::ItemIsSelectable && selectionModel()->isSelected(index)) { option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected : QStyle::State_None; } else { option.state &= ~QStyle::State_Selected; } option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None; if (!(flags & Qt::ItemIsEnabled)) { option.state &= ~QStyle::State_Enabled; } itemDelegate()->paint(&painter, option, index); } /* * Drop indicators for manual reordering... */ #if 0 if (!m_lastDragPosition.isNull()) { std::pair pair = rowDropPos(m_lastDragPosition); VisualGroup *category = pair.first; VisualGroup::HitResults row = pair.second; if (category) { int internalRow = row - category->firstItemIndex; QLine line; if (internalRow >= category->numItems()) { QRect toTheRightOfRect = visualRect(category->lastItem()); line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight()); } else { QRect toTheLeftOfRect = visualRect(model()->index(row, 0)); line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft()); } painter.save(); painter.setPen(QPen(Qt::black, 3)); painter.drawLine(line); painter.restore(); } } #endif } void InstanceView::resizeEvent([[maybe_unused]] QResizeEvent* event) { int newItemsPerRow = calculateItemsPerRow(); if (newItemsPerRow != m_currentItemsPerRow) { m_currentCursorColumn = -1; m_currentItemsPerRow = newItemsPerRow; updateGeometries(); } else { updateScrollbar(); } } void InstanceView::dragEnterEvent(QDragEnterEvent* event) { executeDelayedItemsLayout(); if (!isDragEventAccepted(event)) { return; } m_lastDragPosition = event->position().toPoint() + offset(); viewport()->update(); event->accept(); } void InstanceView::dragMoveEvent(QDragMoveEvent* event) { executeDelayedItemsLayout(); if (!isDragEventAccepted(event)) { return; } m_lastDragPosition = event->position().toPoint() + offset(); viewport()->update(); event->accept(); } void InstanceView::dragLeaveEvent([[maybe_unused]] QDragLeaveEvent* event) { executeDelayedItemsLayout(); m_lastDragPosition = QPoint(); viewport()->update(); } void InstanceView::dropEvent(QDropEvent* event) { executeDelayedItemsLayout(); m_lastDragPosition = QPoint(); stopAutoScroll(); setState(NoState); auto mimedata = event->mimeData(); if (event->source() == this) { if (event->possibleActions() & Qt::MoveAction) { std::pair dropPos = rowDropPos(event->position().toPoint()); const VisualGroup* group = dropPos.first; auto hitResult = dropPos.second; if (hitResult == VisualGroup::HitResult::NoHit) { viewport()->update(); return; } auto instanceId = QString::fromUtf8(mimedata->data("application/x-instanceid")); auto instanceList = APPLICATION->instances().get(); instanceList->setInstanceGroup(instanceId, group->text); event->setDropAction(Qt::MoveAction); event->accept(); updateGeometries(); viewport()->update(); } return; } // check if the action is supported if (!mimedata) { return; } // files dropped from outside? if (mimedata->hasUrls()) { auto urls = mimedata->urls(); event->accept(); emit droppedURLs(urls); } } void InstanceView::startDrag(Qt::DropActions supportedActions) { executeDelayedItemsLayout(); QModelIndexList indexes = selectionModel()->selectedIndexes(); if (indexes.count() == 0) return; QMimeData* mimeData = model()->mimeData(indexes); if (!mimeData) { return; } QRect rect; QPixmap pixmap = renderToPixmap(indexes, &rect); QDrag* drag = new QDrag(this); drag->setPixmap(pixmap); drag->setMimeData(mimeData); drag->setHotSpot(m_pressedPosition - rect.topLeft()); Qt::DropAction defaultDropAction = Qt::IgnoreAction; if (this->defaultDropAction() != Qt::IgnoreAction && (supportedActions & this->defaultDropAction())) { defaultDropAction = this->defaultDropAction(); } /*auto action = */ drag->exec(supportedActions, defaultDropAction); } QRect InstanceView::visualRect(const QModelIndex& index) const { const_cast(this)->executeDelayedItemsLayout(); return geometryRect(index).translated(-offset()); } QRect InstanceView::geometryRect(const QModelIndex& index) const { const_cast(this)->executeDelayedItemsLayout(); if (!index.isValid() || isIndexHidden(index) || index.column() > 0) { return QRect(); } int row = index.row(); if (m_geometryCache.contains(row)) { return *m_geometryCache[row]; } const VisualGroup* cat = category(index); QPair pos = cat->positionOf(index); int x = pos.first; // int y = pos.second; QStyleOptionViewItem option; initViewItemOption(&option); QRect out; out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index)); out.setLeft(m_spacing + x * (itemWidth() + m_spacing)); out.setSize(itemDelegate()->sizeHint(option, index)); m_geometryCache.insert(row, new QRect(out)); return out; } QModelIndex InstanceView::indexAt(const QPoint& point) const { const_cast(this)->executeDelayedItemsLayout(); for (int i = 0; i < model()->rowCount(); ++i) { QModelIndex index = model()->index(i, 0); if (visualRect(index).contains(point)) { return index; } } return QModelIndex(); } void InstanceView::setSelection(const QRect& rect, const QItemSelectionModel::SelectionFlags commands) { executeDelayedItemsLayout(); for (int i = 0; i < model()->rowCount(); ++i) { QModelIndex index = model()->index(i, 0); QRect itemRect = visualRect(index); if (itemRect.intersects(rect)) { selectionModel()->select(index, commands); update(itemRect.translated(-offset())); } } } QPixmap InstanceView::renderToPixmap(const QModelIndexList& indices, QRect* r) const { Q_ASSERT(r); auto paintPairs = draggablePaintPairs(indices, r); if (paintPairs.isEmpty()) { return QPixmap(); } QPixmap pixmap(r->size()); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); QStyleOptionViewItem option; initViewItemOption(&option); option.state |= QStyle::State_Selected; for (int j = 0; j < paintPairs.count(); ++j) { option.rect = paintPairs.at(j).first.translated(-r->topLeft()); const QModelIndex& current = paintPairs.at(j).second; itemDelegate()->paint(&painter, option, current); } return pixmap; } QList> InstanceView::draggablePaintPairs(const QModelIndexList& indices, QRect* r) const { Q_ASSERT(r); QRect& rect = *r; QList> ret; for (int i = 0; i < indices.count(); ++i) { const QModelIndex& index = indices.at(i); const QRect current = geometryRect(index); ret += std::make_pair(current, index); rect |= current; } return ret; } bool InstanceView::isDragEventAccepted([[maybe_unused]] QDropEvent* event) { return true; } std::pair InstanceView::rowDropPos(const QPoint& pos) { VisualGroup::HitResults hitResult; auto group = categoryAt(pos + offset(), hitResult); return std::make_pair(group, hitResult); } QPoint InstanceView::offset() const { return QPoint(horizontalOffset(), verticalOffset()); } QRegion InstanceView::visualRegionForSelection(const QItemSelection& selection) const { QRegion region; for (auto& range : selection) { int start_row = range.top(); int end_row = range.bottom(); for (int row = start_row; row <= end_row; ++row) { int start_column = range.left(); int end_column = range.right(); for (int column = start_column; column <= end_column; ++column) { QModelIndex index = model()->index(row, column, rootIndex()); region += visualRect(index); // OK } } } return region; } QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { auto current = currentIndex(); if (!current.isValid()) { return current; } auto cat = category(current); int group_index = m_groups.indexOf(cat); if (group_index < 0) return current; QPair pos = cat->positionOf(current); int column = pos.first; int row = pos.second; if (m_currentCursorColumn < 0) { m_currentCursorColumn = column; } // Handle different movement actions. switch (cursorAction) { case MoveUp: { if (row == 0) { int prevGroupIndex = group_index - 1; while (prevGroupIndex >= 0) { auto prevGroup = m_groups[prevGroupIndex]; if (prevGroup->collapsed) { prevGroupIndex--; continue; } int newRow = prevGroup->numRows() - 1; int newRowSize = prevGroup->rows[newRow].size(); int newColumn = m_currentCursorColumn; if (m_currentCursorColumn >= newRowSize) { newColumn = newRowSize - 1; } return prevGroup->rows[newRow][newColumn]; } } else { int newRow = row - 1; int newRowSize = cat->rows[newRow].size(); int newColumn = m_currentCursorColumn; if (m_currentCursorColumn >= newRowSize) { newColumn = newRowSize - 1; } return cat->rows[newRow][newColumn]; } return current; } case MoveDown: { if (row == cat->rows.size() - 1) { int nextGroupIndex = group_index + 1; while (nextGroupIndex < m_groups.size()) { auto nextGroup = m_groups[nextGroupIndex]; if (nextGroup->collapsed) { nextGroupIndex++; continue; } int newRowSize = nextGroup->rows[0].size(); int newColumn = m_currentCursorColumn; if (m_currentCursorColumn >= newRowSize) { newColumn = newRowSize - 1; } return nextGroup->rows[0][newColumn]; } } else { int newRow = row + 1; int newRowSize = cat->rows[newRow].size(); int newColumn = m_currentCursorColumn; if (m_currentCursorColumn >= newRowSize) { newColumn = newRowSize - 1; } return cat->rows[newRow][newColumn]; } return current; } case MoveLeft: { if (column > 0) { m_currentCursorColumn = column - 1; return cat->rows[row][column - 1]; } else if (row > 0) { row -= 1; int newRowSize = cat->rows[row].size(); m_currentCursorColumn = newRowSize - 1; return cat->rows[row][m_currentCursorColumn]; } else { int prevGroupIndex = group_index - 1; while (prevGroupIndex >= 0) { auto prevGroup = m_groups[prevGroupIndex]; if (prevGroup->collapsed) { prevGroupIndex--; continue; } int lastRow = prevGroup->numRows() - 1; int lastCol = prevGroup->rows[lastRow].size() - 1; m_currentCursorColumn = lastCol; return prevGroup->rows[lastRow][lastCol]; } } return current; } case MoveRight: { if (column < cat->rows[row].size() - 1) { m_currentCursorColumn = column + 1; return cat->rows[row][column + 1]; } else if (row < cat->rows.size() - 1) { row += 1; m_currentCursorColumn = 0; return cat->rows[row][m_currentCursorColumn]; } else { int nextGroupIndex = group_index + 1; while (nextGroupIndex < m_groups.size()) { auto nextGroup = m_groups[nextGroupIndex]; if (nextGroup->collapsed) { nextGroupIndex++; continue; } m_currentCursorColumn = 0; return nextGroup->rows[0][0]; } } return current; } case MoveHome: { m_currentCursorColumn = 0; return cat->rows[row][0]; } case MoveEnd: { auto last = cat->rows[row].size() - 1; m_currentCursorColumn = last; return cat->rows[row][last]; } default: // For unsupported cursor actions, return the current index. break; } return current; } int InstanceView::horizontalOffset() const { return horizontalScrollBar()->value(); } int InstanceView::verticalOffset() const { return verticalScrollBar()->value(); } void InstanceView::scrollContentsBy(int dx, int dy) { scrollDirtyRegion(dx, dy); viewport()->scroll(dx, dy); } void InstanceView::scrollTo(const QModelIndex& index, ScrollHint hint) { if (!index.isValid()) return; const QRect rect = visualRect(index); if (hint == EnsureVisible && viewport()->rect().contains(rect)) { viewport()->update(rect); return; } verticalScrollBar()->setValue(verticalScrollToValue(index, rect, hint)); } int InstanceView::verticalScrollToValue([[maybe_unused]] const QModelIndex& index, const QRect& rect, QListView::ScrollHint hint) const { const QRect area = viewport()->rect(); const bool above = (hint == QListView::EnsureVisible && rect.top() < area.top()); const bool below = (hint == QListView::EnsureVisible && rect.bottom() > area.bottom()); int verticalValue = verticalScrollBar()->value(); QRect adjusted = rect.adjusted(-spacing(), -spacing(), spacing(), spacing()); if (hint == QListView::PositionAtTop || above) verticalValue += adjusted.top(); else if (hint == QListView::PositionAtBottom || below) verticalValue += qMin(adjusted.top(), adjusted.bottom() - area.height() + 1); else if (hint == QListView::PositionAtCenter) verticalValue += adjusted.top() - ((area.height() - adjusted.height()) / 2); return verticalValue; } PrismLauncher-10.0.5/launcher/ui/instanceview/InstanceDelegate.cpp0000644000175100017510000003624715144136756024627 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "InstanceDelegate.h" #include #include #include #include #include #include #include #include #include "BaseInstance.h" #include "InstanceList.h" #include "InstanceView.h" // Origin: Qt static void viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height, qreal& widthUsed) { height = 0; widthUsed = 0; textLayout.beginLayout(); QString str = textLayout.text(); while (true) { QTextLine line = textLayout.createLine(); if (!line.isValid()) break; if (line.textLength() == 0) break; line.setLineWidth(lineWidth); line.setPosition(QPointF(0, height)); height += line.height(); widthUsed = qMax(widthUsed, line.naturalTextWidth()); } textLayout.endLayout(); } ListViewDelegate::ListViewDelegate(QObject* parent) : QStyledItemDelegate(parent) {} void drawSelectionRect(QPainter* painter, const QStyleOptionViewItem& option, const QRect& rect) { if ((option.state & QStyle::State_Selected)) painter->fillRect(rect, option.palette.brush(QPalette::Highlight)); else { QColor backgroundColor = option.palette.color(QPalette::Window); backgroundColor.setAlpha(160); painter->fillRect(rect, QBrush(backgroundColor)); } } void drawFocusRect(QPainter* painter, const QStyleOptionViewItem& option, const QRect& rect) { if (!(option.state & QStyle::State_HasFocus)) return; QStyleOptionFocusRect opt; opt.direction = option.direction; opt.fontMetrics = option.fontMetrics; opt.palette = option.palette; opt.rect = rect; // opt.state = option.state | QStyle::State_KeyboardFocusChange | // QStyle::State_Item; auto col = option.state & QStyle::State_Selected ? QPalette::Highlight : QPalette::Base; opt.backgroundColor = option.palette.color(col); // Apparently some widget styles expect this hint to not be set painter->setRenderHint(QPainter::Antialiasing, false); QStyle* style = option.widget ? option.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_FrameFocusRect, &opt, painter, option.widget); painter->setRenderHint(QPainter::Antialiasing); } // TODO this can be made a lot prettier void drawProgressOverlay(QPainter* painter, const QStyleOptionViewItem& option, const int value, const int maximum) { if (maximum == 0 || value == maximum) { return; } painter->save(); qreal percent = (qreal)value / (qreal)maximum; QColor color = option.palette.color(QPalette::Dark); color.setAlphaF(0.70f); painter->setBrush(color); painter->setPen(QPen(QBrush(), 0)); painter->drawPie(option.rect, 90 * 16, -percent * 360 * 16); painter->restore(); } void drawBadges(QPainter* painter, const QStyleOptionViewItem& option, BaseInstance* instance, QIcon::Mode mode, QIcon::State state) { QList pixmaps; if (instance->isRunning()) { pixmaps.append("status-running"); } else if (instance->hasCrashed() || instance->hasVersionBroken()) { pixmaps.append("status-bad"); } if (instance->hasUpdateAvailable()) { pixmaps.append("checkupdate"); } static const int itemSide = 24; static const int spacing = 1; const int itemsPerRow = qMax(1, qFloor(double(option.rect.width() + spacing) / double(itemSide + spacing))); const int rows = qCeil((double)pixmaps.size() / (double)itemsPerRow); QListIterator it(pixmaps); painter->translate(option.rect.topLeft()); for (int y = 0; y < rows; ++y) { for (int x = 0; x < itemsPerRow; ++x) { if (!it.hasNext()) { return; } // FIXME: inject this. auto icon = QIcon::fromTheme(it.next()); // opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state); const QPixmap pixmap; // itemSide QRect badgeRect(option.rect.width() - x * itemSide + qMax(x - 1, 0) * spacing - itemSide, y * itemSide + qMax(y - 1, 0) * spacing, itemSide, itemSide); icon.paint(painter, badgeRect, Qt::AlignCenter, mode, state); } } painter->translate(-option.rect.topLeft()); } static QSize viewItemTextSize(const QStyleOptionViewItem* option) { QStyle* style = option->widget ? option->widget->style() : QApplication::style(); QTextOption textOption; textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); QTextLayout textLayout; textLayout.setTextOption(textOption); textLayout.setFont(option->font); textLayout.setText(option->text); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, option, option->widget) + 1; QRect bounds(0, 0, 100 - 2 * textMargin, 600); qreal height = 0, widthUsed = 0; viewItemTextLayout(textLayout, bounds.width(), height, widthUsed); const QSize size(qCeil(widthUsed), qCeil(height)); return QSize(size.width() + 2 * textMargin, size.height()); } void ListViewDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); painter->save(); painter->setClipRect(opt.rect); opt.features |= QStyleOptionViewItem::WrapText; opt.text = index.data().toString(); opt.textElideMode = Qt::ElideRight; opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; QStyle* style = opt.widget ? opt.widget->style() : QApplication::style(); // const int iconSize = style->pixelMetric(QStyle::PM_IconViewIconSize); const int iconSize = 48; QRect iconbox = opt.rect; const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, opt.widget) + 1; QRect textRect = opt.rect; QRect textHighlightRect = textRect; // clip the decoration on top, remove width padding textRect.adjust(textMargin, iconSize + textMargin + 5, -textMargin, 0); textHighlightRect.adjust(0, iconSize + 5, 0, 0); // draw background { // FIXME: unused // QSize textSize = viewItemTextSize ( &opt ); drawSelectionRect(painter, opt, textHighlightRect); /* QPalette::ColorGroup cg; QStyleOptionViewItem opt2(opt); if ((opt.widget && opt.widget->isEnabled()) || (opt.state & QStyle::State_Enabled)) { if (!(opt.state & QStyle::State_Active)) cg = QPalette::Inactive; else cg = QPalette::Normal; } else { cg = QPalette::Disabled; } */ /* opt2.palette.setCurrentColorGroup(cg); // fill in background, if any if (opt.backgroundBrush.style() != Qt::NoBrush) { QPointF oldBO = painter->brushOrigin(); painter->setBrushOrigin(opt.rect.topLeft()); painter->fillRect(opt.rect, opt.backgroundBrush); painter->setBrushOrigin(oldBO); } drawSelectionRect(painter, opt2, textHighlightRect); */ /* if (opt.showDecorationSelected) { drawSelectionRect(painter, opt2, opt.rect); drawFocusRect(painter, opt2, opt.rect); // painter->fillRect ( opt.rect, opt.palette.brush ( cg, QPalette::Highlight ) ); } else { // if ( opt.state & QStyle::State_Selected ) { // QRect textRect = subElementRect ( QStyle::SE_ItemViewItemText, opt, // opt.widget ); // painter->fillRect ( textHighlightRect, opt.palette.brush ( cg, // QPalette::Highlight ) ); drawSelectionRect(painter, opt2, textHighlightRect); drawFocusRect(painter, opt2, textHighlightRect); } } */ } // icon mode and state, also used for badges QIcon::Mode mode = QIcon::Normal; if (!(opt.state & QStyle::State_Enabled)) mode = QIcon::Disabled; else if (opt.state & QStyle::State_Selected) mode = QIcon::Selected; QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off; // draw the icon { iconbox.setHeight(iconSize); opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state); } // set the text colors QPalette::ColorGroup cg = opt.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; if (cg == QPalette::Normal && !(opt.state & QStyle::State_Active)) cg = QPalette::Inactive; if (opt.state & QStyle::State_Selected) { painter->setPen(opt.palette.color(cg, QPalette::HighlightedText)); } else { painter->setPen(opt.palette.color(cg, QPalette::Text)); } // draw the text QTextOption textOption; textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); textOption.setTextDirection(opt.direction); textOption.setAlignment(QStyle::visualAlignment(opt.direction, opt.displayAlignment)); QTextLayout textLayout; textLayout.setTextOption(textOption); textLayout.setFont(opt.font); textLayout.setText(opt.text); qreal width, height; viewItemTextLayout(textLayout, textRect.width(), height, width); const int lineCount = textLayout.lineCount(); const QRect layoutRect = QStyle::alignedRect(opt.direction, opt.displayAlignment, QSize(textRect.width(), int(height)), textRect); const QPointF position = layoutRect.topLeft(); for (int i = 0; i < lineCount; ++i) { const QTextLine line = textLayout.lineAt(i); line.draw(painter, position); } // FIXME: this really has no business of being here. Make generic. auto instance = (BaseInstance*)index.data(InstanceList::InstancePointerRole).value(); if (instance) { drawBadges(painter, opt, instance, mode, state); } drawProgressOverlay(painter, opt, index.data(InstanceViewRoles::ProgressValueRole).toInt(), index.data(InstanceViewRoles::ProgressMaximumRole).toInt()); painter->restore(); } QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); opt.features |= QStyleOptionViewItem::WrapText; opt.text = index.data().toString(); opt.textElideMode = Qt::ElideRight; opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; QStyle* style = opt.widget ? opt.widget->style() : QApplication::style(); const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, opt.widget) + 1; int height = 48 + textMargin * 2 + 5; // TODO: turn constants into variables QSize szz = viewItemTextSize(&opt); height += szz.height(); // FIXME: maybe the icon items could scale and keep proportions? QSize sz(100, height); return sz; } class NoReturnTextEdit : public QTextEdit { Q_OBJECT public: explicit NoReturnTextEdit(QWidget* parent) : QTextEdit(parent) { setTextInteractionFlags(Qt::TextEditorInteraction); setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff); } bool event(QEvent* event) override { auto eventType = event->type(); if (eventType == QEvent::KeyPress || eventType == QEvent::KeyRelease) { QKeyEvent* keyEvent = static_cast(event); auto key = keyEvent->key(); if ((key == Qt::Key_Return || key == Qt::Key_Enter) && eventType == QEvent::KeyPress) { emit editingDone(); return true; } if (key == Qt::Key_Tab) { return true; } } return QTextEdit::event(event); } signals: void editingDone(); }; void ListViewDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, [[maybe_unused]] const QModelIndex& index) const { const int iconSize = 48; QRect textRect = option.rect; // QStyle *style = option.widget ? option.widget->style() : QApplication::style(); textRect.adjust(0, iconSize + 5, 0, 0); editor->setGeometry(textRect); } void ListViewDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { auto text = index.data(Qt::EditRole).toString(); QTextEdit* realEditor = qobject_cast(editor); realEditor->setAlignment(Qt::AlignHCenter | Qt::AlignTop); realEditor->append(text); realEditor->selectAll(); realEditor->document()->clearUndoRedoStacks(); } void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { QTextEdit* realEditor = qobject_cast(editor); QString text = realEditor->toPlainText(); text.replace(QChar('\n'), QChar(' ')); text = text.trimmed(); // Prevent instance names longer than 128 chars text.truncate(128); if (text.size() != 0) { emit textChanged(model->data(index).toString(), text); model->setData(index, text); } } QWidget* ListViewDelegate::createEditor(QWidget* parent, [[maybe_unused]] const QStyleOptionViewItem& option, [[maybe_unused]] const QModelIndex& index) const { auto editor = new NoReturnTextEdit(parent); connect(editor, &NoReturnTextEdit::editingDone, this, &ListViewDelegate::editingDone); return editor; } void ListViewDelegate::editingDone() { NoReturnTextEdit* editor = qobject_cast(sender()); emit commitData(editor); emit closeEditor(editor); } #include "InstanceDelegate.moc" PrismLauncher-10.0.5/launcher/ui/instanceview/VisualGroup.h0000644000175100017510000000676315144136756023355 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include class InstanceView; class QPainter; class QModelIndex; struct VisualRow { QList items; int height = 0; int top = 0; inline int size() const { return items.size(); } inline QModelIndex& operator[](int i) { return items[i]; } }; struct VisualGroup { /* constructors */ VisualGroup(QString text, InstanceView* view); explicit VisualGroup(const VisualGroup* other); /* data */ InstanceView* view = nullptr; QString text; bool collapsed = false; QList rows; int firstItemIndex = 0; int m_verticalPosition = 0; /* logic */ /// update the internal list of items and flow them into the rows. void update(); /// draw the header at y-position. void drawHeader(QPainter* painter, const QStyleOptionViewItem& option) const; /// height of the group, in total. includes a small bit of padding. int totalHeight() const; /// height of the group header, in pixels static int headerHeight(); /// height of the group content, in pixels int contentHeight() const; /// the number of visual rows this group has int numRows() const; /// actually calculate the above value int calculateNumRows() const; /// the height at which this group starts, in pixels int verticalPosition() const; /// relative geometry - top of the row of the given item int rowTopOf(const QModelIndex& index) const; /// height of the row of the given item int rowHeightOf(const QModelIndex& index) const; /// x/y position of the given item inside the group (in items!) QPair positionOf(const QModelIndex& index) const; enum HitResult { NoHit = 0x0, TextHit = 0x1, CheckboxHit = 0x2, HeaderHit = 0x4, BodyHit = 0x8 }; Q_DECLARE_FLAGS(HitResults, HitResult) /// shoot! BANG! what did we hit? HitResults hitScan(const QPoint& pos) const; QList items() const; }; Q_DECLARE_OPERATORS_FOR_FLAGS(VisualGroup::HitResults) PrismLauncher-10.0.5/launcher/ui/instanceview/AccessibleInstanceView.cpp0000644000175100017510000005466615144136756026012 0ustar runnerrunner#include "AccessibleInstanceView.h" #include "AccessibleInstanceView_p.h" #include "InstanceView.h" #ifndef QT_NO_ACCESSIBILITY QAccessibleInterface* groupViewAccessibleFactory(const QString& classname, QObject* object) { QAccessibleInterface* iface = 0; if (!object || !object->isWidgetType()) return iface; QWidget* widget = static_cast(object); if (classname == QLatin1String("InstanceView")) { iface = new AccessibleInstanceView((InstanceView*)widget); } return iface; } QAbstractItemView* AccessibleInstanceView::view() const { return qobject_cast(object()); } int AccessibleInstanceView::logicalIndex(const QModelIndex& index) const { if (!view()->model() || !index.isValid()) return -1; return index.row() * (index.model()->columnCount()) + index.column(); } AccessibleInstanceView::AccessibleInstanceView(QWidget* w) : QAccessibleObject(w) { Q_ASSERT(view()); } bool AccessibleInstanceView::isValid() const { return view(); } AccessibleInstanceView::~AccessibleInstanceView() { for (QAccessible::Id id : childToId) { QAccessible::deleteAccessibleInterface(id); } } QAccessibleInterface* AccessibleInstanceView::cellAt(int row, int column) const { if (!view()->model()) { return 0; } QModelIndex index = view()->model()->index(row, column, view()->rootIndex()); if (Q_UNLIKELY(!index.isValid())) { qWarning() << "AccessibleInstanceView::cellAt: invalid index:" << index << "for" << view(); return 0; } return child(logicalIndex(index)); } QAccessibleInterface* AccessibleInstanceView::caption() const { return 0; } QString AccessibleInstanceView::columnDescription(int column) const { if (!view()->model()) return QString(); return view()->model()->headerData(column, Qt::Horizontal).toString(); } int AccessibleInstanceView::columnCount() const { if (!view()->model()) return 0; return 1; } int AccessibleInstanceView::rowCount() const { if (!view()->model()) return 0; return view()->model()->rowCount(); } int AccessibleInstanceView::selectedCellCount() const { if (!view()->selectionModel()) return 0; return view()->selectionModel()->selectedIndexes().count(); } int AccessibleInstanceView::selectedColumnCount() const { if (!view()->selectionModel()) return 0; return view()->selectionModel()->selectedColumns().count(); } int AccessibleInstanceView::selectedRowCount() const { if (!view()->selectionModel()) return 0; return view()->selectionModel()->selectedRows().count(); } QString AccessibleInstanceView::rowDescription(int row) const { if (!view()->model()) return QString(); return view()->model()->headerData(row, Qt::Vertical).toString(); } QList AccessibleInstanceView::selectedCells() const { QList cells; if (!view()->selectionModel()) return cells; const QModelIndexList selectedIndexes = view()->selectionModel()->selectedIndexes(); cells.reserve(selectedIndexes.size()); for (const QModelIndex& index : selectedIndexes) cells.append(child(logicalIndex(index))); return cells; } QList AccessibleInstanceView::selectedColumns() const { if (!view()->selectionModel()) { return QList(); } const QModelIndexList selectedColumns = view()->selectionModel()->selectedColumns(); QList columns; columns.reserve(selectedColumns.size()); for (const QModelIndex& index : selectedColumns) { columns.append(index.column()); } return columns; } QList AccessibleInstanceView::selectedRows() const { if (!view()->selectionModel()) { return QList(); } QList rows; const QModelIndexList selectedRows = view()->selectionModel()->selectedRows(); rows.reserve(selectedRows.size()); for (const QModelIndex& index : selectedRows) { rows.append(index.row()); } return rows; } QAccessibleInterface* AccessibleInstanceView::summary() const { return 0; } bool AccessibleInstanceView::isColumnSelected(int column) const { if (!view()->selectionModel()) { return false; } return view()->selectionModel()->isColumnSelected(column, QModelIndex()); } bool AccessibleInstanceView::isRowSelected(int row) const { if (!view()->selectionModel()) { return false; } return view()->selectionModel()->isRowSelected(row, QModelIndex()); } bool AccessibleInstanceView::selectRow(int row) { if (!view()->model() || !view()->selectionModel()) { return false; } QModelIndex index = view()->model()->index(row, 0, view()->rootIndex()); if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectColumns) { return false; } switch (view()->selectionMode()) { case QAbstractItemView::NoSelection: { return false; } case QAbstractItemView::SingleSelection: { if (view()->selectionBehavior() != QAbstractItemView::SelectRows && columnCount() > 1) return false; view()->clearSelection(); break; } case QAbstractItemView::ContiguousSelection: { if ((!row || !view()->selectionModel()->isRowSelected(row - 1, view()->rootIndex())) && !view()->selectionModel()->isRowSelected(row + 1, view()->rootIndex())) { view()->clearSelection(); } break; } default: { break; } } view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); return true; } bool AccessibleInstanceView::selectColumn(int column) { if (!view()->model() || !view()->selectionModel()) { return false; } QModelIndex index = view()->model()->index(0, column, view()->rootIndex()); if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectRows) { return false; } switch (view()->selectionMode()) { case QAbstractItemView::NoSelection: { return false; } case QAbstractItemView::SingleSelection: { if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) { return false; } } /* fallthrough */ case QAbstractItemView::ContiguousSelection: { if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) { view()->clearSelection(); } break; } default: { break; } } view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Columns); return true; } bool AccessibleInstanceView::unselectRow(int row) { if (!view()->model() || !view()->selectionModel()) { return false; } QModelIndex index = view()->model()->index(row, 0, view()->rootIndex()); if (!index.isValid()) { return false; } QItemSelection selection(index, index); auto selectionModel = view()->selectionModel(); switch (view()->selectionMode()) { case QAbstractItemView::SingleSelection: // no unselect if (selectedRowCount() == 1) { return false; } break; case QAbstractItemView::ContiguousSelection: { // no unselect if (selectedRowCount() == 1) { return false; } if ((!row || selectionModel->isRowSelected(row - 1, view()->rootIndex())) && selectionModel->isRowSelected(row + 1, view()->rootIndex())) { // If there are rows selected both up the current row and down the current rown, // the ones which are down the current row will be deselected selection = QItemSelection(index, view()->model()->index(rowCount() - 1, 0, view()->rootIndex())); } } default: { break; } } selectionModel->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); return true; } bool AccessibleInstanceView::unselectColumn(int column) { auto model = view()->model(); if (!model || !view()->selectionModel()) { return false; } QModelIndex index = model->index(0, column, view()->rootIndex()); if (!index.isValid()) { return false; } QItemSelection selection(index, index); switch (view()->selectionMode()) { case QAbstractItemView::SingleSelection: { // In SingleSelection and ContiguousSelection once an item // is selected, there's no way for the user to unselect all items if (selectedColumnCount() == 1) { return false; } break; } case QAbstractItemView::ContiguousSelection: if (selectedColumnCount() == 1) { return false; } if ((!column || view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) { // If there are columns selected both at the left of the current row and at the right // of the current row, the ones which are at the right will be deselected selection = QItemSelection(index, model->index(0, columnCount() - 1, view()->rootIndex())); } default: break; } view()->selectionModel()->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Columns); return true; } QAccessible::Role AccessibleInstanceView::role() const { return QAccessible::List; } QAccessible::State AccessibleInstanceView::state() const { return QAccessible::State(); } QAccessibleInterface* AccessibleInstanceView::childAt(int x, int y) const { QPoint viewportOffset = view()->viewport()->mapTo(view(), QPoint(0, 0)); QPoint indexPosition = view()->mapFromGlobal(QPoint(x, y) - viewportOffset); // FIXME: if indexPosition < 0 in one coordinate, return header QModelIndex index = view()->indexAt(indexPosition); if (index.isValid()) { return child(logicalIndex(index)); } return 0; } int AccessibleInstanceView::childCount() const { if (!view()->model()) { return 0; } return (view()->model()->rowCount()) * (view()->model()->columnCount()); } int AccessibleInstanceView::indexOfChild(const QAccessibleInterface* iface) const { if (!view()->model()) return -1; QAccessibleInterface* parent = iface->parent(); if (parent->object() != view()) return -1; Q_ASSERT(iface->role() != QAccessible::TreeItem); // should be handled by tree class if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) { const AccessibleInstanceViewItem* cell = static_cast(iface); return logicalIndex(cell->m_index); } else if (iface->role() == QAccessible::Pane) { return 0; // corner button } else { qWarning() << "AccessibleInstanceView::indexOfChild has a child with unknown role..." << iface->role() << iface->text(QAccessible::Name); } // FIXME: we are in denial of our children. this should stop. return -1; } QString AccessibleInstanceView::text(QAccessible::Text t) const { if (t == QAccessible::Description) return view()->accessibleDescription(); return view()->accessibleName(); } QRect AccessibleInstanceView::rect() const { if (!view()->isVisible()) return QRect(); QPoint pos = view()->mapToGlobal(QPoint(0, 0)); return QRect(pos.x(), pos.y(), view()->width(), view()->height()); } QAccessibleInterface* AccessibleInstanceView::parent() const { if (view() && view()->parent()) { if (qstrcmp("QComboBoxPrivateContainer", view()->parent()->metaObject()->className()) == 0) { return QAccessible::queryAccessibleInterface(view()->parent()->parent()); } return QAccessible::queryAccessibleInterface(view()->parent()); } return 0; } QAccessibleInterface* AccessibleInstanceView::child(int logicalIndex) const { if (!view()->model()) return 0; auto id = childToId.constFind(logicalIndex); if (id != childToId.constEnd()) return QAccessible::accessibleInterface(id.value()); int columns = view()->model()->columnCount(); int row = logicalIndex / columns; int column = logicalIndex % columns; QAccessibleInterface* iface = 0; QModelIndex index = view()->model()->index(row, column, view()->rootIndex()); if (Q_UNLIKELY(!index.isValid())) { qWarning("AccessibleInstanceView::child: Invalid index at: %d %d", row, column); return 0; } iface = new AccessibleInstanceViewItem(view(), index); QAccessible::registerAccessibleInterface(iface); childToId.insert(logicalIndex, QAccessible::uniqueId(iface)); return iface; } void* AccessibleInstanceView::interface_cast(QAccessible::InterfaceType t) { if (t == QAccessible::TableInterface) return static_cast(this); return 0; } void AccessibleInstanceView::modelChange(QAccessibleTableModelChangeEvent* event) { // if there is no cache yet, we don't update anything if (childToId.isEmpty()) return; switch (event->modelChangeType()) { case QAccessibleTableModelChangeEvent::ModelReset: for (QAccessible::Id id : childToId) QAccessible::deleteAccessibleInterface(id); childToId.clear(); break; // rows are inserted: move every row after that case QAccessibleTableModelChangeEvent::RowsInserted: case QAccessibleTableModelChangeEvent::ColumnsInserted: { ChildCache newCache; ChildCache::ConstIterator iter = childToId.constBegin(); while (iter != childToId.constEnd()) { QAccessible::Id id = iter.value(); QAccessibleInterface* iface = QAccessible::accessibleInterface(id); Q_ASSERT(iface); if (indexOfChild(iface) >= 0) { newCache.insert(indexOfChild(iface), id); } else { // ### This should really not happen, // but it might if the view has a root index set. // This needs to be fixed. QAccessible::deleteAccessibleInterface(id); } ++iter; } childToId = newCache; break; } case QAccessibleTableModelChangeEvent::ColumnsRemoved: case QAccessibleTableModelChangeEvent::RowsRemoved: { ChildCache newCache; ChildCache::ConstIterator iter = childToId.constBegin(); while (iter != childToId.constEnd()) { QAccessible::Id id = iter.value(); QAccessibleInterface* iface = QAccessible::accessibleInterface(id); Q_ASSERT(iface); if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) { Q_ASSERT(iface->tableCellInterface()); AccessibleInstanceViewItem* cell = static_cast(iface->tableCellInterface()); // Since it is a QPersistentModelIndex, we only need to check if it is valid if (cell->m_index.isValid()) newCache.insert(indexOfChild(cell), id); else QAccessible::deleteAccessibleInterface(id); } ++iter; } childToId = newCache; break; } case QAccessibleTableModelChangeEvent::DataChanged: // nothing to do in this case break; } } // TABLE CELL AccessibleInstanceViewItem::AccessibleInstanceViewItem(QAbstractItemView* view_, const QModelIndex& index_) : view(view_), m_index(index_) { if (Q_UNLIKELY(!index_.isValid())) qWarning() << "AccessibleInstanceViewItem::AccessibleInstanceViewItem with invalid index:" << index_; } void* AccessibleInstanceViewItem::interface_cast(QAccessible::InterfaceType t) { if (t == QAccessible::TableCellInterface) return static_cast(this); if (t == QAccessible::ActionInterface) return static_cast(this); return 0; } int AccessibleInstanceViewItem::columnExtent() const { return 1; } int AccessibleInstanceViewItem::rowExtent() const { return 1; } QList AccessibleInstanceViewItem::rowHeaderCells() const { return {}; } QList AccessibleInstanceViewItem::columnHeaderCells() const { return {}; } int AccessibleInstanceViewItem::columnIndex() const { if (!isValid()) { return -1; } return m_index.column(); } int AccessibleInstanceViewItem::rowIndex() const { if (!isValid()) { return -1; } return m_index.row(); } bool AccessibleInstanceViewItem::isSelected() const { if (!isValid()) { return false; } return view->selectionModel()->isSelected(m_index); } QStringList AccessibleInstanceViewItem::actionNames() const { QStringList names; names << toggleAction(); return names; } void AccessibleInstanceViewItem::doAction(const QString& actionName) { if (actionName == toggleAction()) { if (isSelected()) { unselectCell(); } else { selectCell(); } } } QStringList AccessibleInstanceViewItem::keyBindingsForAction(const QString&) const { return QStringList(); } void AccessibleInstanceViewItem::selectCell() { if (!isValid()) { return; } QAbstractItemView::SelectionMode selectionMode = view->selectionMode(); if (selectionMode == QAbstractItemView::NoSelection) { return; } Q_ASSERT(table()); QAccessibleTableInterface* cellTable = table()->tableInterface(); switch (view->selectionBehavior()) { case QAbstractItemView::SelectItems: break; case QAbstractItemView::SelectColumns: if (cellTable) cellTable->selectColumn(m_index.column()); return; case QAbstractItemView::SelectRows: if (cellTable) cellTable->selectRow(m_index.row()); return; } if (selectionMode == QAbstractItemView::SingleSelection) { view->clearSelection(); } view->selectionModel()->select(m_index, QItemSelectionModel::Select); } void AccessibleInstanceViewItem::unselectCell() { if (!isValid()) return; QAbstractItemView::SelectionMode selectionMode = view->selectionMode(); if (selectionMode == QAbstractItemView::NoSelection) return; QAccessibleTableInterface* cellTable = table()->tableInterface(); switch (view->selectionBehavior()) { case QAbstractItemView::SelectItems: break; case QAbstractItemView::SelectColumns: if (cellTable) cellTable->unselectColumn(m_index.column()); return; case QAbstractItemView::SelectRows: if (cellTable) cellTable->unselectRow(m_index.row()); return; } // If the mode is not MultiSelection or ExtendedSelection and only // one cell is selected it cannot be unselected by the user if ((selectionMode != QAbstractItemView::MultiSelection) && (selectionMode != QAbstractItemView::ExtendedSelection) && (view->selectionModel()->selectedIndexes().count() <= 1)) return; view->selectionModel()->select(m_index, QItemSelectionModel::Deselect); } QAccessibleInterface* AccessibleInstanceViewItem::table() const { return QAccessible::queryAccessibleInterface(view); } QAccessible::Role AccessibleInstanceViewItem::role() const { return QAccessible::ListItem; } QAccessible::State AccessibleInstanceViewItem::state() const { QAccessible::State st; if (!isValid()) return st; QRect globalRect = view->rect(); globalRect.translate(view->mapToGlobal(QPoint(0, 0))); if (!globalRect.intersects(rect())) st.invisible = true; if (view->selectionModel()->isSelected(m_index)) st.selected = true; if (view->selectionModel()->currentIndex() == m_index) st.focused = true; if (m_index.model()->data(m_index, Qt::CheckStateRole).toInt() == Qt::Checked) st.checked = true; Qt::ItemFlags flags = m_index.flags(); if (flags & Qt::ItemIsSelectable) { st.selectable = true; st.focusable = true; if (view->selectionMode() == QAbstractItemView::MultiSelection) st.multiSelectable = true; if (view->selectionMode() == QAbstractItemView::ExtendedSelection) st.extSelectable = true; } return st; } QRect AccessibleInstanceViewItem::rect() const { QRect r; if (!isValid()) return r; r = view->visualRect(m_index); if (!r.isNull()) { r.translate(view->viewport()->mapTo(view, QPoint(0, 0))); r.translate(view->mapToGlobal(QPoint(0, 0))); } return r; } QString AccessibleInstanceViewItem::text(QAccessible::Text t) const { QString value; if (!isValid()) return value; QAbstractItemModel* model = view->model(); switch (t) { case QAccessible::Name: value = model->data(m_index, Qt::AccessibleTextRole).toString(); if (value.isEmpty()) value = model->data(m_index, Qt::DisplayRole).toString(); break; case QAccessible::Description: value = model->data(m_index, Qt::AccessibleDescriptionRole).toString(); break; default: break; } return value; } void AccessibleInstanceViewItem::setText(QAccessible::Text /*t*/, const QString& text) { if (!isValid() || !(m_index.flags() & Qt::ItemIsEditable)) return; view->model()->setData(m_index, text); } bool AccessibleInstanceViewItem::isValid() const { return view && view->model() && m_index.isValid(); } QAccessibleInterface* AccessibleInstanceViewItem::parent() const { return QAccessible::queryAccessibleInterface(view); } QAccessibleInterface* AccessibleInstanceViewItem::child(int) const { return 0; } #endif /* !QT_NO_ACCESSIBILITY */ PrismLauncher-10.0.5/launcher/ui/instanceview/InstanceProxyModel.h0000644000175100017510000000211615144136756024650 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class InstanceProxyModel : public QSortFilterProxyModel { Q_OBJECT public: InstanceProxyModel(QObject* parent = 0); protected: QVariant data(const QModelIndex& index, int role) const override; bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; bool subSortLessThan(const QModelIndex& left, const QModelIndex& right) const; private: QCollator m_naturalSort; }; PrismLauncher-10.0.5/launcher/ui/instanceview/AccessibleInstanceView.h0000644000175100017510000000025615144136756025441 0ustar runnerrunner#pragma once #include #include class QAccessibleInterface; QAccessibleInterface* groupViewAccessibleFactory(const QString& classname, QObject* object); PrismLauncher-10.0.5/launcher/ui/instanceview/AccessibleInstanceView_p.h0000644000175100017510000000774615144136756025773 0ustar runnerrunner#pragma once #include #include #include #include "QtCore/qpointer.h" #ifndef QT_NO_ACCESSIBILITY #include "InstanceView.h" // #include class QAccessibleTableCell; class QAccessibleTableHeaderCell; class AccessibleInstanceView : public QAccessibleTableInterface, public QAccessibleObject { public: explicit AccessibleInstanceView(QWidget* w); bool isValid() const override; QAccessible::Role role() const override; QAccessible::State state() const override; QString text(QAccessible::Text t) const override; QRect rect() const override; QAccessibleInterface* childAt(int x, int y) const override; int childCount() const override; int indexOfChild(const QAccessibleInterface*) const override; QAccessibleInterface* parent() const override; QAccessibleInterface* child(int index) const override; void* interface_cast(QAccessible::InterfaceType t) override; // table interface QAccessibleInterface* cellAt(int row, int column) const override; QAccessibleInterface* caption() const override; QAccessibleInterface* summary() const override; QString columnDescription(int column) const override; QString rowDescription(int row) const override; int columnCount() const override; int rowCount() const override; // selection int selectedCellCount() const override; int selectedColumnCount() const override; int selectedRowCount() const override; QList selectedCells() const override; QList selectedColumns() const override; QList selectedRows() const override; bool isColumnSelected(int column) const override; bool isRowSelected(int row) const override; bool selectRow(int row) override; bool selectColumn(int column) override; bool unselectRow(int row) override; bool unselectColumn(int column) override; QAbstractItemView* view() const; void modelChange(QAccessibleTableModelChangeEvent* event) override; protected: // maybe vector using ChildCache = QHash; mutable ChildCache childToId; virtual ~AccessibleInstanceView(); private: inline int logicalIndex(const QModelIndex& index) const; }; class AccessibleInstanceViewItem : public QAccessibleInterface, public QAccessibleTableCellInterface, public QAccessibleActionInterface { public: AccessibleInstanceViewItem(QAbstractItemView* view, const QModelIndex& m_index); void* interface_cast(QAccessible::InterfaceType t) override; QObject* object() const override { return nullptr; } QAccessible::Role role() const override; QAccessible::State state() const override; QRect rect() const override; bool isValid() const override; QAccessibleInterface* childAt(int, int) const override { return nullptr; } int childCount() const override { return 0; } int indexOfChild(const QAccessibleInterface*) const override { return -1; } QString text(QAccessible::Text t) const override; void setText(QAccessible::Text t, const QString& text) override; QAccessibleInterface* parent() const override; QAccessibleInterface* child(int) const override; // cell interface int columnExtent() const override; QList columnHeaderCells() const override; int columnIndex() const override; int rowExtent() const override; QList rowHeaderCells() const override; int rowIndex() const override; bool isSelected() const override; QAccessibleInterface* table() const override; // action interface QStringList actionNames() const override; void doAction(const QString& actionName) override; QStringList keyBindingsForAction(const QString& actionName) const override; private: QPointer view; QPersistentModelIndex m_index; void selectCell(); void unselectCell(); friend class AccessibleInstanceView; }; #endif /* !QT_NO_ACCESSIBILITY */ PrismLauncher-10.0.5/launcher/ui/MainWindow.ui0000644000175100017510000005301715144136756020632 0ustar runnerrunner MainWindow 0 0 800 600 0 0 0 0 0 Main Toolbar Qt::BottomToolBarArea|Qt::TopToolBarArea Qt::ToolButtonTextBesideIcon false TopToolBarArea false News Toolbar Qt::BottomToolBarArea|Qt::TopToolBarArea 16 16 Qt::ToolButtonTextBesideIcon false BottomToolBarArea false Instance Toolbar Qt::LeftToolBarArea|Qt::RightToolBarArea 16 16 Qt::ToolButtonTextBesideIcon false true RightToolBarArea false 0 0 800 22 &File true &Edit true &View true F&olders true &Accounts &Help true More News... Open the development blog to read more news about %1. true &Meow It's a fluffy kitty :3 true Status Bar true Lock Toolbars false &Undo Last Instance Deletion Add Instanc&e... Add a new instance. &Update... Check for new updates for %1. QAction::ApplicationSpecificRole Setti&ngs... Change settings. QAction::PreferencesRole &Manage Accounts... &Launch Launch the selected instance. &Kill Kill the running instance. Ctrl+K Rename Rename the selected instance. &Change Group... Change the selected instance's group. Ctrl+G Change Icon Change the selected instance's icon. &Edit... Change the instance settings, mods and versions. Ctrl+I &Folder Open the selected instance's root folder in a file browser. Dele&te Delete the selected instance. false Cop&y... Copy the selected instance. Ctrl+D E&xport... Export the selected instance to supported formats. Prism Launcher (zip) Modrinth (mrpack) CurseForge (zip) Create Shortcut Creates a shortcut on a selected folder to launch the selected instance. No accounts added! true No Default Account Ctrl+0 Close &Window Close the current window QAction::QuitRole &Instances Open the instances folder in a file browser. Launcher &Root Open the launcher's root folder in a file browser. &Central Mods Open the central mods folder in a file browser. &Skins Open the skins folder in a file browser. Instance Icons Open the instance icons folder in a file browser. Logs Open the logs folder in a file browser. Themes Report a Bug or Suggest a Feature Open the bug tracker to report a bug with %1. &Discord Guild Open %1 Discord guild. &Matrix Space Open %1 Matrix space. Sub&reddit Open %1 subreddit. &About %1 View information about %1. QAction::AboutRole &Clear Metadata Cache Clear cached metadata .. View logs View current and previous launcher logs Install to &PATH Install a %1 symlink to /usr/local/bin Folders Open one of the folders shared between instances. Help Get help with %1 or Minecraft. Accounts %1 &Help Open the %1 wiki &Widget Themes Open the widget themes folder in a file browser. I&con Theme Open the icon theme folder in a file browser. Cat Packs Open the cat packs folder in a file browser. Java Open the Java folder in a file browser. Only available if the built-in Java downloader is used. WideBar QToolBar
ui/widgets/WideBar.h
PrismLauncher-10.0.5/launcher/ui/ToolTipFilter.h0000644000175100017510000000161715144136756021127 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Mark Deneen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include class ToolTipFilter : public QObject { Q_OBJECT protected: bool eventFilter(QObject* obj, QEvent* event); }; PrismLauncher-10.0.5/launcher/ui/ViewLogWindow.cpp0000644000175100017510000000152115144136756021460 0ustar runnerrunner#include #include "ViewLogWindow.h" #include "ui/pages/instance/OtherLogsPage.h" ViewLogWindow::ViewLogWindow(QWidget* parent) : QMainWindow(parent), m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) { setAttribute(Qt::WA_DeleteOnClose); setWindowIcon(QIcon::fromTheme("log")); setWindowTitle(tr("View Launcher Logs")); setCentralWidget(m_page); setMinimumSize(m_page->size()); setContentsMargins(6, 6, 0, 6); // the "Other Logs" instance page has 6px padding on the right, // to have equal padding in all directions in the dialog we add it to all other sides. m_page->opened(); show(); } void ViewLogWindow::closeEvent(QCloseEvent* event) { m_page->closed(); emit isClosing(); event->accept(); } PrismLauncher-10.0.5/launcher/ui/GuiUtil.h0000644000175100017510000000113315144136756017742 0ustar runnerrunner#pragma once #include #include #include namespace GuiUtil { std::optional uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget); std::optional uploadPaste(const QString& name, const QString& data, QWidget* parentWidget); void setClipboardText(QString text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); } // namespace GuiUtil PrismLauncher-10.0.5/launcher/ui/MainWindow.h0000644000175100017510000001534315144136756020444 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Andrew Okin * Peterix * Orochimarufan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "BaseInstance.h" #include "minecraft/auth/MinecraftAccount.h" class LaunchController; class NewsChecker; class QToolButton; class InstanceProxyModel; class LabeledToolButton; class QLabel; class MinecraftLauncher; class BaseProfilerFactory; class InstanceView; class KonamiCode; class InstanceTask; class LabeledToolButton; namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget* parent = 0); ~MainWindow(); bool eventFilter(QObject* obj, QEvent* ev) override; void closeEvent(QCloseEvent* event) override; void changeEvent(QEvent* event) override; void checkInstancePathForProblems(); void updatesAllowedChanged(bool allowed); void processURLs(QList urls); signals: void isClosing(); protected: QMenu* createPopupMenu() override; private slots: void onCatToggled(bool); void onCatChanged(int); void on_actionAbout_triggered(); void on_actionAddInstance_triggered(); void on_actionREDDIT_triggered(); void on_actionMATRIX_triggered(); void on_actionDISCORD_triggered(); void on_actionCopyInstance_triggered(); void on_actionChangeInstGroup_triggered(); void on_actionChangeInstIcon_triggered(); void on_actionViewLauncherRootFolder_triggered(); void on_actionViewInstanceFolder_triggered(); void on_actionViewCentralModsFolder_triggered(); void on_actionViewIconThemeFolder_triggered(); void on_actionViewWidgetThemeFolder_triggered(); void on_actionViewCatPackFolder_triggered(); void on_actionViewIconsFolder_triggered(); void on_actionViewLogsFolder_triggered(); void on_actionViewJavaFolder_triggered(); void on_actionViewSkinsFolder_triggered(); void on_actionViewSelectedInstFolder_triggered(); void refreshInstances(); void checkForUpdates(); void on_actionSettings_triggered(); void on_actionManageAccounts_triggered(); void on_actionReportBug_triggered(); void on_actionClearMetadata_triggered(); #ifdef Q_OS_MAC void on_actionAddToPATH_triggered(); #endif void on_actionOpenWiki_triggered(); void on_actionMoreNews_triggered(); void newsButtonClicked(); void on_actionLaunchInstance_triggered(); void on_actionKillInstance_triggered(); void on_actionDeleteInstance_triggered(); void deleteGroup(QString group); void renameGroup(QString group); void undoTrashInstance(); inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); } void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceFlamePack_triggered(); void on_actionRenameInstance_triggered(); void on_actionEditInstance_triggered(); void on_actionCreateInstanceShortcut_triggered(); void taskEnd(); /** * called when an icon is changed in the icon model. */ void iconUpdated(QString); void showInstanceContextMenu(const QPoint&); void updateMainToolBar(); void updateLaunchButton(); void updateThemeMenu(); void instanceActivated(QModelIndex); void instanceChanged(const QModelIndex& current, const QModelIndex& previous); void instanceSelectRequest(QString id); void instanceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void selectionBad(); void startTask(Task* task); void defaultAccountChanged(); void changeActiveAccount(); void repopulateAccountsMenu(); void updateNewsLabel(); void konamiTriggered(); void globalSettingsClosed(); void setStatusBarVisibility(bool); void lockToolbars(bool); #ifndef Q_OS_MAC void keyReleaseEvent(QKeyEvent* event) override; #endif void refreshCurrentInstance(); private: void retranslateUi(); void addInstance(const QString& url = QString(), const QMap& extra_info = {}); void activateInstance(InstancePtr instance); void setCatBackground(bool enabled); void updateInstanceToolIcon(QString new_icon); void setSelectedInstanceById(const QString& id); void updateStatusCenter(); void setInstanceActionsEnabled(bool enabled); void runModalTask(Task* task); void instanceFromInstanceTask(InstanceTask* task); private: Ui::MainWindow* ui; // these are managed by Qt's memory management model! InstanceView* view = nullptr; InstanceProxyModel* proxymodel = nullptr; QToolButton* newsLabel = nullptr; QLabel* m_statusLeft = nullptr; QLabel* m_statusCenter = nullptr; LabeledToolButton* changeIconButton = nullptr; LabeledToolButton* renameButton = nullptr; QToolButton* helpMenuButton = nullptr; KonamiCode* secretEventFilter = nullptr; std::shared_ptr instanceToolbarSetting = nullptr; unique_qobject_ptr m_newsChecker; InstancePtr m_selectedInstance; QString m_currentInstIcon; // managed by the application object Task* m_versionLoadTask = nullptr; }; PrismLauncher-10.0.5/launcher/ui/dialogs/0000755000175100017510000000000015144136756017633 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/dialogs/ReviewMessageBox.cpp0000644000175100017510000001070415144136756023560 0ustar runnerrunner#include "ReviewMessageBox.h" #include "ui_ReviewMessageBox.h" #include #include #include ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString const& title, [[maybe_unused]] QString const& icon) : QDialog(parent), ui(new Ui::ReviewMessageBox) { ui->setupUi(this); auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); back_button->setText(tr("Back")); ui->toggleDepsButton->hide(); ui->modTreeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->modTreeWidget->header()->setStretchLastSection(false); ui->modTreeWidget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); // Overwrite Ctrl+C functionality to exclude the label when copying text from tree auto shortcut = new QShortcut(QKeySequence::Copy, ui->modTreeWidget); connect(shortcut, &QShortcut::activated, [this]() { auto currentItem = this->ui->modTreeWidget->currentItem(); if (!currentItem) return; auto currentColumn = this->ui->modTreeWidget->currentColumn(); auto data = currentItem->data(currentColumn, Qt::UserRole); QString txt; if (data.isValid()) { txt = data.toString(); } else { txt = currentItem->text(currentColumn); } QApplication::clipboard()->setText(txt); }); } ReviewMessageBox::~ReviewMessageBox() { delete ui; } auto ReviewMessageBox::create(QWidget* parent, QString&& title, QString&& icon) -> ReviewMessageBox* { return new ReviewMessageBox(parent, title, icon); } void ReviewMessageBox::appendResource(ResourceInformation&& info) { auto itemTop = new QTreeWidgetItem(ui->modTreeWidget); itemTop->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); itemTop->setText(0, info.name); if (!info.enabled) { itemTop->setToolTip(0, tr("Mod was disabled as it may be already installed.")); } auto filenameItem = new QTreeWidgetItem(itemTop); filenameItem->setText(0, tr("Filename: %1").arg(info.filename)); filenameItem->setData(0, Qt::UserRole, info.filename); auto providerItem = new QTreeWidgetItem(itemTop); providerItem->setText(0, tr("Provider: %1").arg(info.provider)); providerItem->setData(0, Qt::UserRole, info.provider); if (!info.required_by.isEmpty()) { auto requiredByItem = new QTreeWidgetItem(itemTop); if (info.required_by.length() == 1) { requiredByItem->setText(0, tr("Required by: %1").arg(info.required_by.back())); requiredByItem->setData(0, Qt::UserRole, info.required_by.back()); } else { requiredByItem->setText(0, tr("Required by:")); for (auto req : info.required_by) { auto reqItem = new QTreeWidgetItem(requiredByItem); reqItem->setText(0, req); } } ui->toggleDepsButton->show(); m_deps << itemTop; } auto versionTypeItem = new QTreeWidgetItem(itemTop); versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type)); versionTypeItem->setData(0, Qt::UserRole, info.version_type); ui->modTreeWidget->addTopLevelItem(itemTop); } auto ReviewMessageBox::deselectedResources() -> QStringList { QStringList list; auto* item = ui->modTreeWidget->topLevelItem(0); for (int i = 1; item != nullptr; ++i) { if (item->checkState(0) == Qt::CheckState::Unchecked) { list.append(item->text(0)); } item = ui->modTreeWidget->topLevelItem(i); } return list; } void ReviewMessageBox::retranslateUi(QString resources_name) { setWindowTitle(tr("Confirm %1 selection").arg(resources_name)); ui->explainLabel->setText(tr("You're about to download the following %1:").arg(resources_name)); ui->onlyCheckedLabel->setText(tr("Only %1 with a check will be downloaded!").arg(resources_name)); } void ReviewMessageBox::on_toggleDepsButton_clicked() { m_deps_checked = !m_deps_checked; auto state = m_deps_checked ? Qt::Checked : Qt::Unchecked; for (auto dep : m_deps) dep->setCheckState(0, state); }; PrismLauncher-10.0.5/launcher/ui/dialogs/ProgressDialog.ui0000644000175100017510000000743015144136756023122 0ustar runnerrunner ProgressDialog 0 0 480 210 1 1 480 210 Please wait... true 0 0 0 15 Global Task Status... true Global Status Details... Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter true 0 24 24 0 0 0 100 QFrame::StyledPanel Qt::ScrollBarAsNeeded QAbstractScrollArea::AdjustToContents true 0 0 460 108 2 0 0 Skip PrismLauncher-10.0.5/launcher/ui/dialogs/ProfileSelectDialog.h0000644000175100017510000000506015144136756023665 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "minecraft/auth/AccountList.h" namespace Ui { class ProfileSelectDialog; } class ProfileSelectDialog : public QDialog { Q_OBJECT public: enum Flags { NoFlags = 0, /*! * Shows a check box on the dialog that allows the user to specify that the account * they've selected should be used as the global default for all instances. */ GlobalDefaultCheckbox, /*! * Shows a check box on the dialog that allows the user to specify that the account * they've selected should be used as the default for the instance they are currently launching. * This is not currently implemented. */ InstanceDefaultCheckbox, }; /*! * Constructs a new account select dialog with the given parent and message. * The message will be shown at the top of the dialog. It is an empty string by default. */ explicit ProfileSelectDialog(const QString& message = "", int flags = 0, QWidget* parent = 0); ~ProfileSelectDialog(); /*! * Gets a pointer to the account that the user selected. * This is null if the user clicked cancel or hasn't clicked OK yet. */ MinecraftAccountPtr selectedAccount() const; /*! * Returns true if the user checked the "use as global default" checkbox. * If the checkbox wasn't shown, this function returns false. */ bool useAsGlobalDefault() const; /*! * Returns true if the user checked the "use as instance default" checkbox. * If the checkbox wasn't shown, this function returns false. */ bool useAsInstDefaullt() const; public slots: void on_buttonBox_accepted(); void on_buttonBox_rejected(); protected: shared_qobject_ptr m_accounts; //! The account that was selected when the user clicked OK. MinecraftAccountPtr m_selected; private: Ui::ProfileSelectDialog* ui; }; PrismLauncher-10.0.5/launcher/ui/dialogs/VersionSelectDialog.cpp0000644000175100017510000001230015144136756024240 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "VersionSelectDialog.h" #include #include #include #include #include #include #include "ui/widgets/VersionSelectWidget.h" #include "BaseVersion.h" #include "BaseVersionList.h" VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent, bool cancelable) : QDialog(parent) { setObjectName(QStringLiteral("VersionSelectDialog")); resize(400, 347); m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); m_versionWidget = new VersionSelectWidget(parent); m_verticalLayout->addWidget(m_versionWidget); m_horizontalLayout = new QHBoxLayout(); m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); m_refreshButton = new QPushButton(this); m_refreshButton->setObjectName(QStringLiteral("refreshButton")); m_horizontalLayout->addWidget(m_refreshButton); m_buttonBox = new QDialogButtonBox(this); m_buttonBox->setObjectName(QStringLiteral("buttonBox")); m_buttonBox->setOrientation(Qt::Horizontal); m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Ok")); m_buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_horizontalLayout->addWidget(m_buttonBox); m_verticalLayout->addLayout(m_horizontalLayout); retranslate(); connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_versionWidget->view(), &QAbstractItemView::doubleClicked, this, &QDialog::accept); connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QMetaObject::connectSlotsByName(this); setWindowModality(Qt::WindowModal); setWindowTitle(title); m_vlist = vlist; if (!cancelable) { m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); } } void VersionSelectDialog::retranslate() { // FIXME: overrides custom title given in constructor! setWindowTitle(tr("Choose Version")); m_refreshButton->setToolTip(tr("Reloads the version list.")); m_refreshButton->setText(tr("&Refresh")); } void VersionSelectDialog::setCurrentVersion(const QString& version) { m_currentVersion = version; m_versionWidget->setCurrentVersion(version); } void VersionSelectDialog::setEmptyString(QString emptyString) { m_versionWidget->setEmptyString(emptyString); } void VersionSelectDialog::setEmptyErrorString(QString emptyErrorString) { m_versionWidget->setEmptyErrorString(emptyErrorString); } void VersionSelectDialog::setResizeOn(int column) { resizeOnColumn = column; } int VersionSelectDialog::exec() { QDialog::open(); m_versionWidget->initialize(m_vlist, true); m_versionWidget->selectSearch(); if (resizeOnColumn != -1) { m_versionWidget->setResizeOn(resizeOnColumn); } return QDialog::exec(); } void VersionSelectDialog::selectRecommended() { m_versionWidget->selectRecommended(); } BaseVersion::Ptr VersionSelectDialog::selectedVersion() const { return m_versionWidget->selectedVersion(); } void VersionSelectDialog::on_refreshButton_clicked() { m_versionWidget->loadList(); } void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QString filter) { m_versionWidget->setExactFilter(role, filter); } void VersionSelectDialog::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter) { m_versionWidget->setExactIfPresentFilter(role, filter); } void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) { m_versionWidget->setFuzzyFilter(role, filter); } PrismLauncher-10.0.5/launcher/ui/dialogs/InstallLoaderDialog.h0000644000175100017510000000260415144136756023663 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "ui/pages/BasePageProvider.h" class MinecraftInstance; class PageContainer; class PackProfile; class QDialogButtonBox; class InstallLoaderDialog final : public QDialog, protected BasePageProvider { Q_OBJECT public: explicit InstallLoaderDialog(std::shared_ptr instance, const QString& uid = QString(), QWidget* parent = nullptr); QList getPages() override; QString dialogTitle() override; void validate(BasePage* page); void done(int result) override; private: std::shared_ptr profile; PageContainer* container; QDialogButtonBox* buttons; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ImportResourceDialog.cpp0000644000175100017510000000540515144136756024445 0ustar runnerrunner#include "ImportResourceDialog.h" #include "ui_ImportResourceDialog.h" #include #include #include "Application.h" #include "InstanceList.h" #include #include "modplatform/ResourceType.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" ImportResourceDialog::ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent) : QDialog(parent), ui(new Ui::ImportResourceDialog), m_resource_type(type), m_file_path(file_path) { ui->setupUi(this); setWindowModality(Qt::WindowModal); auto contentsWidget = ui->instanceView; contentsWidget->setViewMode(QListView::ListMode); contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setIconSize(QSize(48, 48)); contentsWidget->setMovement(QListView::Static); contentsWidget->setResizeMode(QListView::Adjust); contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); contentsWidget->setSpacing(5); contentsWidget->setWordWrap(true); contentsWidget->setWrapping(true); // NOTE: We can't have uniform sizes because the text may wrap if it's too long. If we set this, it will cut off the wrapped text. contentsWidget->setUniformItemSizes(false); contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); contentsWidget->setItemDelegate(new ListViewDelegate()); proxyModel = new InstanceProxyModel(this); proxyModel->setSourceModel(APPLICATION->instances().get()); proxyModel->sort(0); contentsWidget->setModel(proxyModel); connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &ImportResourceDialog::activated); connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ImportResourceDialog::selectionChanged); ui->label->setText( tr("Choose the instance you would like to import this %1 to.").arg(ModPlatform::ResourceTypeUtils::getName(m_resource_type))); ui->label_file_path->setText(tr("File: %1").arg(m_file_path)); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } void ImportResourceDialog::activated(QModelIndex index) { selectedInstanceKey = index.data(InstanceList::InstanceIDRole).toString(); accept(); } void ImportResourceDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) { if (selected.empty()) return; QString key = selected.first().indexes().first().data(InstanceList::InstanceIDRole).toString(); if (!key.isEmpty()) { selectedInstanceKey = key; } } ImportResourceDialog::~ImportResourceDialog() { delete ui; } PrismLauncher-10.0.5/launcher/ui/dialogs/BlockedModsDialog.h0000644000175100017510000000547015144136756023320 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // SPDX-FileCopyrightText: 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "tasks/ConcurrentTask.h" class QPushButton; struct BlockedMod { QString name; QString websiteUrl; QString hash; bool matched; QString localPath; QString targetFolder; bool disabled = false; bool move = false; }; QT_BEGIN_NAMESPACE namespace Ui { class BlockedModsDialog; } QT_END_NAMESPACE class BlockedModsDialog : public QDialog { Q_OBJECT public: BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods, QString hash_type = "sha1"); ~BlockedModsDialog() override; protected: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; protected slots: void done(int r) override; private: Ui::BlockedModsDialog* ui; QList& m_mods; QFileSystemWatcher m_watcher; shared_qobject_ptr m_hashingTask; QSet m_pendingHashPaths; bool m_rehashPending; QString m_hashType; void openAll(bool missingOnly); void addDownloadFolder(); void update(); void directoryChanged(QString path); void setupWatch(); void watchPath(QString path, bool watch_recursive = false); void scanPaths(); void scanPath(QString path, bool start_task); void addHashTask(QString path); void buildHashTask(QString path); void checkMatchHash(QString hash, QString path); void validateMatchedMods(); void runHashTask(); void hashTaskFinished(); bool checkValidPath(QString path); bool allModsMatched(); }; QDebug operator<<(QDebug debug, const BlockedMod& m); PrismLauncher-10.0.5/launcher/ui/dialogs/ExportPackDialog.h0000644000175100017510000000311615144136756023205 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "BaseInstance.h" #include "FastFileIconProvider.h" #include "FileIgnoreProxy.h" #include "minecraft/MinecraftInstance.h" #include "modplatform/ModIndex.h" namespace Ui { class ExportPackDialog; } class ExportPackDialog : public QDialog { Q_OBJECT public: explicit ExportPackDialog(MinecraftInstancePtr instance, QWidget* parent = nullptr, ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH); ~ExportPackDialog(); void done(int result) override; void validate(); private: QString ignoreFileName(); private: const MinecraftInstancePtr m_instance; Ui::ExportPackDialog* m_ui; FileIgnoreProxy* m_proxy; FastFileIconProvider m_icons; const ModPlatform::ResourceProvider m_provider; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ProfileSetupDialog.ui0000644000175100017510000000447615144136756023746 0ustar runnerrunner ProfileSetupDialog 0 0 615 208 Choose Minecraft name 0 0 You just need to take one more step to be able to play Minecraft on this account. Choose your name carefully: true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse nameEdit QDialogButtonBox::Cancel|QDialogButtonBox::Ok true Errors go here true true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse nameEdit PrismLauncher-10.0.5/launcher/ui/dialogs/ScrollMessageBox.h0000644000175100017510000000055315144136756023223 0ustar runnerrunner#pragma once #include QT_BEGIN_NAMESPACE namespace Ui { class ScrollMessageBox; } QT_END_NAMESPACE class ScrollMessageBox : public QDialog { Q_OBJECT public: ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body); ~ScrollMessageBox() override; private: Ui::ScrollMessageBox* ui; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ExportPackDialog.cpp0000644000175100017510000002256115144136756023545 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExportPackDialog.h" #include "minecraft/mod/ResourceFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlamePackExportTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui_ExportPackDialog.h" #include #include #include #include #include #include "FileSystem.h" #include "MMCZip.h" #include "modplatform/modrinth/ModrinthPackExportTask.h" ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) : QDialog(parent), m_instance(instance), m_ui(new Ui::ExportPackDialog), m_provider(provider) { Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME); m_ui->setupUi(this); m_ui->name->setPlaceholderText(instance->name()); m_ui->name->setText(instance->settings()->get("ExportName").toString()); m_ui->version->setText(instance->settings()->get("ExportVersion").toString()); m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); connect(m_ui->recommendedMemoryCheckBox, &QCheckBox::toggled, m_ui->recommendedMemory, &QWidget::setEnabled); if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { setWindowTitle(tr("Export Modrinth Pack")); m_ui->authorLabel->hide(); m_ui->author->hide(); m_ui->recommendedMemoryWidget->hide(); m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); } else { setWindowTitle(tr("Export CurseForge Pack")); m_ui->summaryLabel->hide(); m_ui->summary->hide(); const int recommendedRAM = instance->settings()->get("ExportRecommendedRAM").toInt(); if (recommendedRAM > 0) { m_ui->recommendedMemoryCheckBox->setChecked(true); m_ui->recommendedMemory->setValue(recommendedRAM); } else { m_ui->recommendedMemoryCheckBox->setChecked(false); // recommend based on setting - limited to 12 GiB (CurseForge warns above this amount) const int defaultRecommendation = qMin(m_instance->settings()->get("MaxMemAlloc").toInt(), 1024 * 12); m_ui->recommendedMemory->setValue(defaultRecommendation); } m_ui->author->setText(instance->settings()->get("ExportAuthor").toString()); } // ensure a valid pack is generated // the name and version fields mustn't be empty connect(m_ui->name, &QLineEdit::textEdited, this, &ExportPackDialog::validate); connect(m_ui->version, &QLineEdit::textEdited, this, &ExportPackDialog::validate); // the instance name can technically be empty validate(); QFileSystemModel* model = new QFileSystemModel(this); model->setIconProvider(&m_icons); // use the game root - everything outside cannot be exported const QDir instanceRoot(instance->instanceRoot()); m_proxy = new FileIgnoreProxy(instance->instanceRoot(), this); auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" }) { m_proxy->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path)); } m_proxy->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); m_proxy->ignoreFilesWithSuffix().append(".pw.toml"); m_proxy->setSourceModel(model); m_proxy->loadBlockedPathsFromFile(ignoreFileName()); const QDir::Filters filter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); MinecraftInstance* mcInstance = dynamic_cast(instance.get()); if (mcInstance) { for (auto resourceModel : mcInstance->resourceLists()) { if (resourceModel == nullptr) { continue; } if (!resourceModel->indexDir().exists()) { continue; } if (resourceModel->dir() == resourceModel->indexDir()) { continue; } m_proxy->ignoreFilesWithPath().insert(instanceRoot.relativeFilePath(resourceModel->indexDir().absolutePath())); } } m_ui->files->setModel(m_proxy); m_ui->files->setRootIndex(m_proxy->mapFromSource(model->index(instance->gameRoot()))); m_ui->files->sortByColumn(0, Qt::AscendingOrder); model->setFilter(filter); model->setRootPath(instance->gameRoot()); QHeaderView* headerView = m_ui->files->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportPackDialog::~ExportPackDialog() { delete m_ui; } void ExportPackDialog::done(int result) { m_proxy->saveBlockedPathsToFile(ignoreFileName()); auto settings = m_instance->settings(); settings->set("ExportName", m_ui->name->text()); settings->set("ExportVersion", m_ui->version->text()); settings->set("ExportOptionalFiles", m_ui->optionalFiles->isChecked()); if (m_provider == ModPlatform::ResourceProvider::MODRINTH) settings->set("ExportSummary", m_ui->summary->toPlainText()); else { settings->set("ExportAuthor", m_ui->author->text()); if (m_ui->recommendedMemoryCheckBox->isChecked()) settings->set("ExportRecommendedRAM", m_ui->recommendedMemory->value()); else settings->reset("ExportRecommendedRAM"); } if (result == Accepted) { const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text(); const QString filename = FS::RemoveInvalidFilenameChars(name); QString output; if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".mrpack"), tr("Modrinth pack") + " (*.mrpack *.zip)", nullptr); if (output.isEmpty()) return; if (!(output.endsWith(".zip") || output.endsWith(".mrpack"))) output.append(".mrpack"); } else { output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".zip"), tr("CurseForge pack") + " (*.zip)", nullptr); if (output.isEmpty()) return; if (!output.endsWith(".zip")) output.append(".zip"); } Task* task; if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { task = new ModrinthPackExportTask(name, m_ui->version->text(), m_ui->summary->toPlainText(), m_ui->optionalFiles->isChecked(), m_instance, output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); } else { FlamePackExportOptions options{}; options.name = name; options.version = m_ui->version->text(); options.author = m_ui->author->text(); options.optionalFiles = m_ui->optionalFiles->isChecked(); options.instance = m_instance; options.output = output; options.filter = std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1); options.recommendedRAM = m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0; task = new FlamePackExportTask(std::move(options)); } connect(task, &Task::failed, [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(task, &Task::aborted, [this] { CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) ->show(); }); connect(task, &Task::finished, [task] { task->deleteLater(); }); ProgressDialog progress(this); progress.setSkipButton(true, tr("Abort")); if (progress.execWithTask(task) != QDialog::Accepted) return; } QDialog::done(result); } void ExportPackDialog::validate() { m_ui->buttonBox->button(QDialogButtonBox::Ok) ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && m_ui->version->text().isEmpty()); } QString ExportPackDialog::ignoreFileName() { return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); } PrismLauncher-10.0.5/launcher/ui/dialogs/NewComponentDialog.h0000644000175100017510000000231215144136756023536 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include namespace Ui { class NewComponentDialog; } class NewComponentDialog : public QDialog { Q_OBJECT public: explicit NewComponentDialog(const QString& initialName = QString(), const QString& initialUid = QString(), QWidget* parent = 0); virtual ~NewComponentDialog(); void setBlacklist(QStringList badUids); QString name() const; QString uid() const; private slots: void updateDialogState(); private: Ui::NewComponentDialog* ui; QString originalPlaceholderText; QStringList uidBlacklist; }; PrismLauncher-10.0.5/launcher/ui/dialogs/NewsDialog.h0000644000175100017510000000100715144136756022036 0ustar runnerrunner#pragma once #include #include #include "news/NewsEntry.h" namespace Ui { class NewsDialog; } class NewsDialog : public QDialog { Q_OBJECT public: NewsDialog(QList entries, QWidget* parent = nullptr); ~NewsDialog(); public slots: void toggleArticleList(); private slots: void selectedArticleChanged(const QString& new_title); private: Ui::NewsDialog* ui; QHash m_entries; bool m_article_list_hidden = false; }; PrismLauncher-10.0.5/launcher/ui/dialogs/NewComponentDialog.cpp0000644000175100017510000000757115144136756024105 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NewComponentDialog.h" #include "Application.h" #include "ui_NewComponentDialog.h" #include #include #include #include #include "IconPickerDialog.h" #include "ProgressDialog.h" #include "VersionSelectDialog.h" #include #include #include #include #include #include NewComponentDialog::NewComponentDialog(const QString& initialName, const QString& initialUid, QWidget* parent) : QDialog(parent), ui(new Ui::NewComponentDialog) { ui->setupUi(this); resize(minimumSizeHint()); ui->nameTextBox->setText(initialName); ui->uidTextBox->setText(initialUid); connect(ui->nameTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); connect(ui->uidTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); ui->nameTextBox->setFocus(); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); originalPlaceholderText = ui->uidTextBox->placeholderText(); updateDialogState(); } NewComponentDialog::~NewComponentDialog() { delete ui; } void NewComponentDialog::updateDialogState() { auto protoUid = ui->nameTextBox->text().toLower(); static const QRegularExpression s_removeChars("[^a-z]"); protoUid.remove(s_removeChars); if (protoUid.isEmpty()) { ui->uidTextBox->setPlaceholderText(originalPlaceholderText); } else { QString suggestedUid = "org.multimc.custom." + protoUid; ui->uidTextBox->setPlaceholderText(suggestedUid); } bool allowOK = !name().isEmpty() && !uid().isEmpty() && !uidBlacklist.contains(uid()); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowOK); } QString NewComponentDialog::name() const { auto result = ui->nameTextBox->text(); if (result.size()) { return result.trimmed(); } return QString(); } QString NewComponentDialog::uid() const { auto result = ui->uidTextBox->text(); if (result.size()) { return result.trimmed(); } result = ui->uidTextBox->placeholderText(); if (result.size() && result != originalPlaceholderText) { return result.trimmed(); } return QString(); } void NewComponentDialog::setBlacklist(QStringList badUids) { uidBlacklist = badUids; } PrismLauncher-10.0.5/launcher/ui/dialogs/IconPickerDialog.cpp0000644000175100017510000001466415144136756023520 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include "Application.h" #include "IconPickerDialog.h" #include "ui_IconPickerDialog.h" #include "ui/instanceview/InstanceDelegate.h" #include #include "icons/IconList.h" #include "icons/IconUtils.h" IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui::IconPickerDialog) { ui->setupUi(this); setWindowModality(Qt::WindowModal); searchBar = new QLineEdit(this); searchBar->setPlaceholderText(tr("Search...")); ui->verticalLayout->insertWidget(0, searchBar); proxyModel = new QSortFilterProxyModel(this); proxyModel->setSourceModel(APPLICATION->icons().get()); proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); ui->iconView->setModel(proxyModel); auto contentsWidget = ui->iconView; contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setIconSize(QSize(48, 48)); contentsWidget->setMovement(QListView::Static); contentsWidget->setResizeMode(QListView::Adjust); contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); contentsWidget->setSpacing(5); contentsWidget->setWordWrap(false); contentsWidget->setWrapping(true); contentsWidget->setUniformItemSizes(true); contentsWidget->setTextElideMode(Qt::ElideRight); contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); contentsWidget->setItemDelegate(new ListViewDelegate(contentsWidget)); // contentsWidget->setAcceptDrops(true); contentsWidget->setDropIndicatorShown(true); contentsWidget->viewport()->setAcceptDrops(true); contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); contentsWidget->setDefaultDropAction(Qt::CopyAction); contentsWidget->installEventFilter(this); contentsWidget->setModel(proxyModel); // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); connect(buttonAdd, &QPushButton::clicked, this, &IconPickerDialog::addNewIcon); connect(buttonRemove, &QPushButton::clicked, this, &IconPickerDialog::removeSelectedIcon); connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &IconPickerDialog::activated); connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &IconPickerDialog::selectionChanged); auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons); // Prevent incorrect indices from e.g. filesystem changes connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); }); } bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt) { if (obj != ui->iconView) return QDialog::eventFilter(obj, evt); if (evt->type() != QEvent::KeyPress) { return QDialog::eventFilter(obj, evt); } QKeyEvent* keyEvent = static_cast(evt); switch (keyEvent->key()) { case Qt::Key_Delete: removeSelectedIcon(); return true; case Qt::Key_Plus: addNewIcon(); return true; default: break; } return QDialog::eventFilter(obj, evt); } void IconPickerDialog::addNewIcon() { //: The title of the select icons open file dialog QString selectIcons = tr("Select Icons"); //: The type of icon files auto filter = IconUtils::getIconFilter(); QStringList fileNames = QFileDialog::getOpenFileNames(this, selectIcons, QString(), tr("Icons %1").arg(filter)); APPLICATION->icons()->installIcons(fileNames); } void IconPickerDialog::removeSelectedIcon() { if (APPLICATION->icons()->trashIcon(selectedIconKey)) return; APPLICATION->icons()->deleteIcon(selectedIconKey); } void IconPickerDialog::activated(QModelIndex index) { selectedIconKey = index.data(Qt::UserRole).toString(); accept(); } void IconPickerDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) { if (selected.empty()) return; QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); if (!key.isEmpty()) { selectedIconKey = key; } buttonRemove->setEnabled(APPLICATION->icons()->iconFileExists(selectedIconKey)); } int IconPickerDialog::execWithSelection(QString selection) { auto list = APPLICATION->icons(); auto contentsWidget = ui->iconView; selectedIconKey = selection; int index_nr = list->getIconIndex(selection); auto model_index = list->index(index_nr); contentsWidget->selectionModel()->select(model_index, QItemSelectionModel::Current | QItemSelectionModel::Select); QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection, Q_ARG(QModelIndex, model_index)); return QDialog::exec(); } void IconPickerDialog::delayed_scroll(QModelIndex model_index) { auto contentsWidget = ui->iconView; contentsWidget->scrollTo(model_index); } IconPickerDialog::~IconPickerDialog() { delete ui; } void IconPickerDialog::openFolder() { DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true); } void IconPickerDialog::filterIcons(const QString& query) { proxyModel->setFilterFixedString(query); } PrismLauncher-10.0.5/launcher/ui/dialogs/ProfileSetupDialog.h0000644000175100017510000000404715144136756023552 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include "net/Download.h" #include "net/Upload.h" namespace Ui { class ProfileSetupDialog; } class ProfileSetupDialog : public QDialog { Q_OBJECT public: explicit ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget* parent = 0); ~ProfileSetupDialog(); enum class NameStatus { NotSet, Pending, Available, Exists, Error } nameStatus = NameStatus::NotSet; private slots: void on_buttonBox_accepted(); void on_buttonBox_rejected(); void nameEdited(const QString& name); void startCheck(); void checkFinished(); void setupProfileFinished(); protected: void scheduleCheck(const QString& name); void checkName(const QString& name); void setNameStatus(NameStatus status, QString errorString); void setupProfile(const QString& profileName); private: MinecraftAccountPtr m_accountToSetup; Ui::ProfileSetupDialog* ui; QIcon goodIcon; QIcon yellowIcon; QIcon badIcon; QAction* validityAction = nullptr; QString queuedCheck; bool isChecking = false; bool isWorking = false; QString currentCheck; QTimer checkStartTimer; std::shared_ptr m_check_response; Net::Download::Ptr m_check_task; std::shared_ptr m_profile_response; Net::Upload::Ptr m_profile_task; }; PrismLauncher-10.0.5/launcher/ui/dialogs/CreateShortcutDialog.ui0000644000175100017510000001660015144136756024254 0ustar runnerrunner CreateShortcutDialog Qt::WindowModality::ApplicationModal 0 0 450 370 Create Instance Shortcut true :/icons/instances/grass:/icons/instances/grass 80 80 Save To: 0 0 Name: Name Use a different account than the default specified. Override the default account false 0 0 0 0 Specify a world or server to automatically join on launch. Select a target to join on launch false 0 0 0 World: targetBtnGroup 0 0 0 Server Address: targetBtnGroup false Server Address: Server Address Note: If a shortcut is moved after creation, it won't be deleted when deleting the instance. You'll need to delete them manually if that is the case. Qt::Orientation::Horizontal QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok iconButton buttonBox accepted() CreateShortcutDialog accept() 20 20 20 20 buttonBox rejected() CreateShortcutDialog reject() 20 20 20 20 PrismLauncher-10.0.5/launcher/ui/dialogs/ExportToModListDialog.ui0000644000175100017510000001773415144136756024406 0ustar runnerrunner ExportToModListDialog 0 0 650 522 Export Pack to ModList true Settings HTML Markdown Plaintext JSON CSV Custom 0 0 Template 0 0 This text supports the following placeholders: {name} - Mod name {mod_id} - Mod ID {url} - Mod URL {version} - Mod version {authors} - Mod authors 0 0 Optional Info Version Authors URL Filename Version Authors URL Filename QFrame::NoFrame QFrame::Plain 1 Format Result 0 143 true true This depends on the mods' metadata. To ensure it is available, run an update on the instance. Installing the updates isn't necessary. true Copy QDialogButtonBox::Cancel|QDialogButtonBox::Save buttonBox accepted() ExportToModListDialog accept() 334 435 324 206 buttonBox rejected() ExportToModListDialog reject() 324 390 324 206 PrismLauncher-10.0.5/launcher/ui/dialogs/ResourceUpdateDialog.cpp0000644000175100017510000005147215144136756024422 0ustar runnerrunner#include "ResourceUpdateDialog.h" #include "Application.h" #include "ChooseProviderDialog.h" #include "CustomMessageBox.h" #include "ProgressDialog.h" #include "ScrollMessageBox.h" #include "StringUtils.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" #include "tasks/SequentialTask.h" #include "ui_ReviewMessageBox.h" #include "Markdown.h" #include "tasks/ConcurrentTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/EnsureMetadataTask.h" #include "modplatform/flame/FlameCheckUpdate.h" #include "modplatform/modrinth/ModrinthCheckUpdate.h" #include #include #include #include #include static std::list mcVersions(BaseInstance* inst) { return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr resourceModel, QList& searchFor, bool includeDeps, QList loadersList) : ReviewMessageBox(parent, tr("Confirm resources to update"), "") , m_parent(parent) , m_resourceModel(resourceModel) , m_candidates(searchFor) , m_secondTryMetadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) , m_instance(instance) , m_includeDeps(includeDeps) , m_loadersList(std::move(loadersList)) { ReviewMessageBox::setGeometry(0, 0, 800, 600); ui->explainLabel->setText(tr("You're about to update the following resources:")); ui->onlyCheckedLabel->setText(tr("Only resources with a check will be updated!")); } void ResourceUpdateDialog::checkCandidates() { // Ensure mods have valid metadata auto went_well = ensureMetadata(); if (!went_well) { m_aborted = true; return; } // Report failed metadata generation if (!m_failedMetadata.empty()) { QString text; for (const auto& failed : m_failedMetadata) { const auto& mod = std::get<0>(failed); const auto& reason = std::get<1>(failed); text += tr("Mod name: %1
File name: %2
Reason: %3

").arg(mod->name(), mod->fileinfo().fileName(), reason); } ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"), tr("Could not generate metadata for the following resources:
" "Do you wish to proceed without those resources?"), text); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Rejected) { m_aborted = true; QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; } } auto versions = mcVersions(m_instance); SequentialTask check_task(tr("Checking for updates")); if (!m_modrinthToUpdate.empty()) { m_modrinthCheckTask.reset(new ModrinthCheckUpdate(m_modrinthToUpdate, versions, m_loadersList, m_resourceModel)); connect(m_modrinthCheckTask.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { m_failedCheckUpdate.append({ resource, reason, recover_url }); }); check_task.addTask(m_modrinthCheckTask); } if (!m_flameToUpdate.empty()) { m_flameCheckTask.reset(new FlameCheckUpdate(m_flameToUpdate, versions, m_loadersList, m_resourceModel)); connect(m_flameCheckTask.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { m_failedCheckUpdate.append({ resource, reason, recover_url }); }); check_task.addTask(m_flameCheckTask); } connect(&check_task, &Task::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); connect(&check_task, &Task::succeeded, this, [this, &check_task]() { QStringList warnings = check_task.warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); } }); // Check for updates ProgressDialog progress_dialog(m_parent); progress_dialog.setSkipButton(true, tr("Abort")); progress_dialog.setWindowTitle(tr("Checking for updates...")); auto ret = progress_dialog.execWithTask(&check_task); // If the dialog was skipped / some download error happened if (ret == QDialog::DialogCode::Rejected) { m_aborted = true; QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; } QList> selectedVers; // Add found updates for Modrinth if (m_modrinthCheckTask) { auto modrinth_updates = m_modrinthCheckTask->getUpdates(); for (auto& updatable : modrinth_updates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); appendResource(updatable); m_tasks.insert(updatable.name, updatable.download); } selectedVers.append(m_modrinthCheckTask->getDependencies()); } // Add found updated for Flame if (m_flameCheckTask) { auto flame_updates = m_flameCheckTask->getUpdates(); for (auto& updatable : flame_updates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); appendResource(updatable); m_tasks.insert(updatable.name, updatable.download); } selectedVers.append(m_flameCheckTask->getDependencies()); } // Report failed update checking if (!m_failedCheckUpdate.empty()) { QString text; for (const auto& failed : m_failedCheckUpdate) { const auto& mod = std::get<0>(failed); const auto& reason = std::get<1>(failed); const auto& recover_url = std::get<2>(failed); qDebug() << mod->name() << "failed to check for updates!"; text += tr("Mod name: %1").arg(mod->name()) + "
"; if (!reason.isEmpty()) text += tr("Reason: %1").arg(reason) + "
"; if (!recover_url.isEmpty()) //: %1 is the link to download it manually text += tr("Possible solution: Getting the latest version manually:
%1
") .arg(QString("%1").arg(recover_url.toString())); text += "
"; } ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"), tr("Could not check or get the following resources for updates:
" "Do you wish to proceed without those resources?"), text); message_dialog.setModal(true); if (message_dialog.exec() == QDialog::Rejected) { m_aborted = true; QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; } } if (m_includeDeps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies auto* mod_model = dynamic_cast(m_resourceModel.get()); if (mod_model != nullptr) { auto depTask = makeShared(m_instance, mod_model, selectedVers); connect(depTask.get(), &Task::failed, this, [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); auto weak = depTask.toWeakRef(); connect(depTask.get(), &Task::succeeded, this, [this, weak]() { QStringList warnings; if (auto depTask = weak.lock()) { warnings = depTask->warnings(); } if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); } }); ProgressDialog progress_dialog_deps(m_parent); progress_dialog_deps.setSkipButton(true, tr("Abort")); progress_dialog_deps.setWindowTitle(tr("Checking for dependencies...")); auto dret = progress_dialog_deps.execWithTask(depTask.get()); // If the dialog was skipped / some download error happened if (dret == QDialog::DialogCode::Rejected) { m_aborted = true; QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; } static FlameAPI api; auto dependencyExtraInfo = depTask->getExtraInfo(); for (const auto& dep : depTask->getDependecies()) { auto changelog = dep->version.changelog; if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); auto download_task = makeShared(dep->pack, dep->version, m_resourceModel); auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString()); CheckUpdateTask::Update updatable = { dep->pack->name, dep->version.hash, tr("Not installed"), dep->version.version, dep->version.version_type, changelog, dep->pack->provider, download_task, !extraInfo.maybe_installed }; appendResource(updatable, extraInfo.required_by); m_tasks.insert(updatable.name, updatable.download); } } } // If there's no resource to be updated if (ui->modTreeWidget->topLevelItemCount() == 0) { m_noUpdates = true; } else { // FIXME: Find a more efficient way of doing this! // Sort major items in alphabetical order (also sorts the children unfortunately) ui->modTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder); // Re-sort the children auto* item = ui->modTreeWidget->topLevelItem(0); for (int i = 1; item != nullptr; ++i) { item->sortChildren(0, Qt::SortOrder::DescendingOrder); item = ui->modTreeWidget->topLevelItem(i); } } if (m_aborted || m_noUpdates) QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); } // Part 1: Ensure we have a valid metadata auto ResourceUpdateDialog::ensureMetadata() -> bool { auto index_dir = indexDir(); SequentialTask seq(tr("Looking for metadata")); // A better use of data structures here could remove the need for this QHash QHash should_try_others; QList modrinth_tmp; QList flame_tmp; bool confirm_rest = false; bool try_others_rest = false; bool skip_rest = false; ModPlatform::ResourceProvider provider_rest = ModPlatform::ResourceProvider::MODRINTH; // adds resource to list based on provider auto addToTmp = [&modrinth_tmp, &flame_tmp](Resource* resource, ModPlatform::ResourceProvider p) { switch (p) { case ModPlatform::ResourceProvider::MODRINTH: modrinth_tmp.push_back(resource); break; case ModPlatform::ResourceProvider::FLAME: flame_tmp.push_back(resource); break; } }; // ask the user on what provider to seach for the mod first for (auto candidate : m_candidates) { if (candidate->status() != ResourceStatus::NO_METADATA) { onMetadataEnsured(candidate); continue; } if (skip_rest) continue; if (candidate->type() == ResourceType::FOLDER) { continue; } if (confirm_rest) { addToTmp(candidate, provider_rest); should_try_others.insert(candidate->internal_id(), try_others_rest); continue; } ChooseProviderDialog chooser(this); chooser.setDescription(tr("The resource '%1' does not have a metadata yet. We need to generate it in order to track relevant " "information on how to update this mod. " "To do this, please select a mod provider which we can use to check for updates for this mod.") .arg(candidate->name())); auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted; auto response = chooser.getResponse(); if (response.skip_all) skip_rest = true; if (response.confirm_all) { confirm_rest = true; provider_rest = response.chosen; try_others_rest = response.try_others; } should_try_others.insert(candidate->internal_id(), response.try_others); if (confirmed) addToTmp(candidate, response.chosen); } // prepare task for the modrinth mods if (!modrinth_tmp.empty()) { auto modrinth_task = makeShared(modrinth_tmp, index_dir, ModPlatform::ResourceProvider::MODRINTH); connect(modrinth_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); connect(modrinth_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Resource* candidate) { onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::MODRINTH); }); connect(modrinth_task.get(), &EnsureMetadataTask::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); if (modrinth_task->getHashingTask()) seq.addTask(modrinth_task->getHashingTask()); seq.addTask(modrinth_task); } // prepare task for the flame mods if (!flame_tmp.empty()) { auto flame_task = makeShared(flame_tmp, index_dir, ModPlatform::ResourceProvider::FLAME); connect(flame_task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); connect(flame_task.get(), &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Resource* candidate) { onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::ResourceProvider::FLAME); }); connect(flame_task.get(), &EnsureMetadataTask::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); if (flame_task->getHashingTask()) seq.addTask(flame_task->getHashingTask()); seq.addTask(flame_task); } seq.addTask(m_secondTryMetadata); // execute all the tasks ProgressDialog checking_dialog(m_parent); checking_dialog.setSkipButton(true, tr("Abort")); checking_dialog.setWindowTitle(tr("Generating metadata...")); auto ret_metadata = checking_dialog.execWithTask(&seq); return (ret_metadata != QDialog::DialogCode::Rejected); } void ResourceUpdateDialog::onMetadataEnsured(Resource* resource) { // When the mod is a folder, for instance if (!resource->metadata()) return; switch (resource->metadata()->provider) { case ModPlatform::ResourceProvider::MODRINTH: m_modrinthToUpdate.push_back(resource); break; case ModPlatform::ResourceProvider::FLAME: m_flameToUpdate.push_back(resource); break; } } ModPlatform::ResourceProvider next(ModPlatform::ResourceProvider p) { switch (p) { case ModPlatform::ResourceProvider::MODRINTH: return ModPlatform::ResourceProvider::FLAME; case ModPlatform::ResourceProvider::FLAME: return ModPlatform::ResourceProvider::MODRINTH; } return ModPlatform::ResourceProvider::FLAME; } void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others, ModPlatform::ResourceProvider first_choice) { if (try_others) { auto index_dir = indexDir(); auto task = makeShared(resource, index_dir, next(first_choice)); connect(task.get(), &EnsureMetadataTask::metadataReady, [this](Resource* candidate) { onMetadataEnsured(candidate); }); connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); }); connect(task.get(), &EnsureMetadataTask::failed, [this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); if (task->getHashingTask()) { auto seq = makeShared(); seq->addTask(task->getHashingTask()); seq->addTask(task); m_secondTryMetadata->addTask(seq); } else { m_secondTryMetadata->addTask(task); } } else { QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; m_failedMetadata.append({ resource, reason }); } } void ResourceUpdateDialog::appendResource(CheckUpdateTask::Update const& info, QStringList requiredBy) { auto item_top = new QTreeWidgetItem(ui->modTreeWidget); item_top->setCheckState(0, info.enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); if (!info.enabled) { item_top->setToolTip(0, tr("Mod was disabled as it may be already installed.")); } item_top->setText(0, info.name); item_top->setExpanded(true); auto provider_item = new QTreeWidgetItem(item_top); QString provider_name = ModPlatform::ProviderCapabilities::readableName(info.provider); provider_item->setText(0, tr("Provider: %1").arg(provider_name)); provider_item->setData(0, Qt::UserRole, provider_name); auto old_version_item = new QTreeWidgetItem(item_top); old_version_item->setText(0, tr("Old version: %1").arg(info.old_version)); old_version_item->setData(0, Qt::UserRole, info.old_version); auto new_version_item = new QTreeWidgetItem(item_top); new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); new_version_item->setData(0, Qt::UserRole, info.new_version); if (info.new_version_type.has_value()) { auto new_version_type_item = new QTreeWidgetItem(item_top); new_version_type_item->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString())); new_version_type_item->setData(0, Qt::UserRole, info.new_version_type.value().toString()); } if (!requiredBy.isEmpty()) { auto requiredByItem = new QTreeWidgetItem(item_top); if (requiredBy.length() == 1) { requiredByItem->setText(0, tr("Required by: %1").arg(requiredBy.back())); requiredByItem->setData(0, Qt::UserRole, requiredBy.back()); } else { requiredByItem->setText(0, tr("Required by:")); for (auto req : requiredBy) { auto reqItem = new QTreeWidgetItem(requiredByItem); reqItem->setText(0, req); } } ui->toggleDepsButton->show(); m_deps << item_top; } auto changelog_item = new QTreeWidgetItem(item_top); changelog_item->setText(0, tr("Changelog of the latest version")); auto changelog = new QTreeWidgetItem(changelog_item); auto changelog_area = new QTextBrowser(); QString text = info.changelog; changelog->setData(0, Qt::UserRole, text); if (info.provider == ModPlatform::ResourceProvider::MODRINTH) { text = markdownToHTML(info.changelog.toUtf8()); } changelog_area->setHtml(StringUtils::htmlListPatch(text)); changelog_area->setOpenExternalLinks(true); changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area); ui->modTreeWidget->addTopLevelItem(item_top); } auto ResourceUpdateDialog::getTasks() -> const QList { QList list; auto* item = ui->modTreeWidget->topLevelItem(0); for (int i = 1; item != nullptr; ++i) { if (item->checkState(0) == Qt::CheckState::Checked) { list.push_back(m_tasks.find(item->text(0)).value()); } item = ui->modTreeWidget->topLevelItem(i); } return list; } PrismLauncher-10.0.5/launcher/ui/dialogs/BlockedModsDialog.ui0000644000175100017510000001347515144136756023512 0ustar runnerrunner BlockedModsDialog 0 0 800 500 2 1 700 350 BlockedModsDialog Placeholder description Qt::RichText true <html><head/><body><p>Your configured global mods folder and default downloads folder are automatically checked for the downloaded mods and they will be copied to the instance if found.</p><p>Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch if you did not download the mods to a default location.</p><p><span style=" font-weight:600;">Click 'Open Missing' to open all the download links in the browser. </span></p></body></html> true 0 Blocked Mods true true Open Missing Qt::Horizontal 40 20 Watched Folders 0 12 true false Add Download Folder Qt::Horizontal 40 20 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() BlockedModsDialog accept() 199 425 199 227 buttonBox rejected() BlockedModsDialog reject() 199 425 199 227 PrismLauncher-10.0.5/launcher/ui/dialogs/ScrollMessageBox.cpp0000644000175100017510000000111015144136756023544 0ustar runnerrunner#include "ScrollMessageBox.h" #include #include "ui_ScrollMessageBox.h" ScrollMessageBox::ScrollMessageBox(QWidget* parent, const QString& title, const QString& text, const QString& body) : QDialog(parent), ui(new Ui::ScrollMessageBox) { ui->setupUi(this); this->setWindowTitle(title); ui->label->setText(text); ui->textBrowser->setText(body); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ScrollMessageBox::~ScrollMessageBox() { delete ui; } PrismLauncher-10.0.5/launcher/ui/dialogs/UpdateAvailableDialog.h0000644000175100017510000000264715144136756024160 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include namespace Ui { class UpdateAvailableDialog; } class UpdateAvailableDialog : public QDialog { Q_OBJECT public: enum ResultCode { Install = 10, DontInstall = 11, Skip = 12, }; explicit UpdateAvailableDialog(const QString& currentVersion, const QString& availableVersion, const QString& releaseNotes, QWidget* parent = 0); ~UpdateAvailableDialog() = default; private: Ui::UpdateAvailableDialog* ui; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ProgressDialog.cpp0000644000175100017510000002324315144136756023267 0ustar runnerrunner/// SPDX-License-Identifier: GPL-3.0-only /* * PrismLaucher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ProgressDialog.h" #include #include "ui_ProgressDialog.h" #include #include #include #include "tasks/Task.h" #include "ui/widgets/SubTaskProgressBar.h" // map a value in a numeric range of an arbitrary type to between 0 and INT_MAX // for getting the best precision out of the qt progress bar template , bool> = true> std::tuple map_int_zero_max(T current, T range_max, T range_min) { int int_max = std::numeric_limits::max(); auto type_range = range_max - range_min; double percentage = static_cast(current - range_min) / static_cast(type_range); int mapped_current = percentage * int_max; return { mapped_current, int_max }; } ProgressDialog::ProgressDialog(QWidget* parent) : QDialog(parent), ui(new Ui::ProgressDialog) { ui->setupUi(this); ui->taskProgressScrollArea->setHidden(true); this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); setAttribute(Qt::WidgetAttribute::WA_QuitOnClose, true); changeProgress(0, 100); updateSize(true); setSkipButton(false); } void ProgressDialog::setSkipButton(bool present, QString label) { ui->skipButton->setAutoDefault(false); ui->skipButton->setDefault(false); ui->skipButton->setFocusPolicy(Qt::ClickFocus); ui->skipButton->setEnabled(present); ui->skipButton->setVisible(present); ui->skipButton->setText(label); updateSize(); } void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); if (ui->skipButton->isEnabled()) // prevent other triggers from aborting m_task->abort(); } ProgressDialog::~ProgressDialog() { for (auto conn : this->m_taskConnections) { disconnect(conn); } delete ui; } void ProgressDialog::updateSize(bool recenterParent) { QSize lastSize = this->size(); QPoint lastPos = this->pos(); int minHeight = ui->globalStatusDetailsLabel->minimumSize().height() + (ui->verticalLayout->spacing() * 2); minHeight += ui->globalProgressBar->minimumSize().height() + ui->verticalLayout->spacing(); if (!ui->taskProgressScrollArea->isHidden()) minHeight += ui->taskProgressScrollArea->minimumSizeHint().height() + ui->verticalLayout->spacing(); if (ui->skipButton->isVisible()) minHeight += ui->skipButton->height() + ui->verticalLayout->spacing(); minHeight = std::max(minHeight, 60); QSize minSize = QSize(480, minHeight); setMinimumSize(minSize); adjustSize(); QSize newSize = this->size(); // if the current window is a different size auto parent = this->parentWidget(); if (recenterParent && parent) { auto newX = std::max(0, parent->x() + ((parent->width() - newSize.width()) / 2)); auto newY = std::max(0, parent->y() + ((parent->height() - newSize.height()) / 2)); this->move(newX, newY); } else if (lastSize != newSize) { // center on old position after resize QSize sizeDiff = lastSize - newSize; // last size was smaller, the results should be negative auto newX = std::max(0, lastPos.x() + (sizeDiff.width() / 2)); auto newY = std::max(0, lastPos.y() + (sizeDiff.height() / 2)); this->move(newX, newY); } } int ProgressDialog::execWithTask(Task* task) { this->m_task = task; if (!task) { qDebug() << "Programmer error: Progress dialog created with null task."; return QDialog::DialogCode::Accepted; } QDialog::DialogCode result{}; if (handleImmediateResult(result)) { return result; } // Connect signals. this->m_taskConnections.push_back(connect(task, &Task::started, this, &ProgressDialog::onTaskStarted)); this->m_taskConnections.push_back(connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed)); this->m_taskConnections.push_back(connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded)); this->m_taskConnections.push_back(connect(task, &Task::status, this, &ProgressDialog::changeStatus)); this->m_taskConnections.push_back(connect(task, &Task::details, this, &ProgressDialog::changeStatus)); this->m_taskConnections.push_back(connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress)); this->m_taskConnections.push_back(connect(task, &Task::progress, this, &ProgressDialog::changeProgress)); this->m_taskConnections.push_back(connect(task, &Task::aborted, this, &ProgressDialog::hide)); this->m_taskConnections.push_back(connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled)); m_is_multi_step = task->isMultiStep(); ui->taskProgressScrollArea->setHidden(!m_is_multi_step); updateSize(); // It's a good idea to start the task after we entered the dialog's event loop :^) if (!task->isRunning()) { QMetaObject::invokeMethod(task, &Task::start, Qt::QueuedConnection); } else { changeStatus(task->getStatus()); changeProgress(task->getProgress(), task->getTotalProgress()); } return QDialog::exec(); } // TODO: only provide the unique_ptr overloads int ProgressDialog::execWithTask(std::unique_ptr&& task) { connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); return execWithTask(task.release()); } int ProgressDialog::execWithTask(std::unique_ptr& task) { connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); return execWithTask(task.release()); } bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result) { if (m_task->isFinished()) { if (m_task->wasSuccessful()) { result = QDialog::Accepted; } else { result = QDialog::Rejected; } return true; } return false; } Task* ProgressDialog::getTask() { return m_task; } void ProgressDialog::onTaskStarted() {} void ProgressDialog::onTaskFailed([[maybe_unused]] QString failure) { reject(); hide(); } void ProgressDialog::onTaskSucceeded() { accept(); hide(); } void ProgressDialog::changeStatus([[maybe_unused]] const QString& status) { ui->globalStatusLabel->setText(m_task->getStatus()); ui->globalStatusLabel->adjustSize(); ui->globalStatusDetailsLabel->setText(m_task->getDetails()); ui->globalStatusDetailsLabel->adjustSize(); updateSize(); } void ProgressDialog::addTaskProgress(TaskStepProgress const& progress) { SubTaskProgressBar* task_bar = new SubTaskProgressBar(this); taskProgress.insert(progress.uid, task_bar); ui->taskProgressLayout->addWidget(task_bar); } void ProgressDialog::changeStepProgress(TaskStepProgress const& task_progress) { m_is_multi_step = true; if (ui->taskProgressScrollArea->isHidden()) { ui->taskProgressScrollArea->setHidden(false); updateSize(); } if (!taskProgress.contains(task_progress.uid)) addTaskProgress(task_progress); auto task_bar = taskProgress.value(task_progress.uid); auto const [mapped_current, mapped_total] = map_int_zero_max(task_progress.current, task_progress.total, 0); if (task_progress.total <= 0) { task_bar->setRange(0, 0); } else { task_bar->setRange(0, mapped_total); } task_bar->setValue(mapped_current); task_bar->setStatus(task_progress.status); task_bar->setDetails(task_progress.details); if (task_progress.isDone()) { task_bar->setVisible(false); } } void ProgressDialog::changeProgress(qint64 current, qint64 total) { ui->globalProgressBar->setMaximum(total); ui->globalProgressBar->setValue(current); } void ProgressDialog::keyPressEvent(QKeyEvent* e) { if (ui->skipButton->isVisible()) { if (e->key() == Qt::Key_Escape) { on_skipButton_clicked(true); return; } else if (e->key() == Qt::Key_Tab) { ui->skipButton->setFocusPolicy(Qt::StrongFocus); ui->skipButton->setFocus(); ui->skipButton->setAutoDefault(true); ui->skipButton->setDefault(true); return; } } QDialog::keyPressEvent(e); } void ProgressDialog::closeEvent(QCloseEvent* e) { if (m_task && m_task->isRunning()) { e->ignore(); } else { QDialog::closeEvent(e); } } PrismLauncher-10.0.5/launcher/ui/dialogs/ResourceDownloadDialog.cpp0000644000175100017510000003733015144136756024744 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ResourceDownloadDialog.h" #include #include #include #include "Application.h" #include "ResourceDownloadTask.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ResourcePackFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" #include "minecraft/mod/TexturePackFolderModel.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ReviewMessageBox.h" #include "ui/pages/modplatform/ResourcePage.h" #include "ui/pages/modplatform/flame/FlameResourcePages.h" #include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/widgets/PageContainer.h" namespace ResourceDownload { ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::shared_ptr base_model) : QDialog(parent) , m_base_model(base_model) , m_buttons(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel) , m_vertical_layout(this) { setObjectName(QStringLiteral("ResourceDownloadDialog")); resize(static_cast(std::max(0.5 * parent->width(), 400.0)), static_cast(std::max(0.75 * parent->height(), 400.0))); setWindowIcon(QIcon::fromTheme("new")); // small margins look ugly on macOS on modal windows #ifndef Q_OS_MACOS m_buttons.setContentsMargins(0, 0, 6, 6); #endif // Bonk Qt over its stupid head and make sure it understands which button is the default one... // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button auto OkButton = m_buttons.button(QDialogButtonBox::Ok); OkButton->setEnabled(false); OkButton->setDefault(true); OkButton->setAutoDefault(true); OkButton->setText(tr("Review and confirm")); OkButton->setShortcut(tr("Ctrl+Return")); auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); CancelButton->setDefault(false); CancelButton->setAutoDefault(false); auto HelpButton = m_buttons.button(QDialogButtonBox::Help); HelpButton->setDefault(false); HelpButton->setAutoDefault(false); setWindowModality(Qt::WindowModal); } void ResourceDownloadDialog::accept() { if (!geometrySaveKey().isEmpty()) APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); QDialog::accept(); } void ResourceDownloadDialog::reject() { auto selected = getTasks(); if (selected.count() > 0) { auto reply = CustomMessageBox::selectable(this, tr("Confirmation Needed"), tr("You have %1 selected resources.\n" "Are you sure you want to close this dialog?") .arg(selected.count()), QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (reply != QMessageBox::Yes) { return; } } if (!geometrySaveKey().isEmpty()) APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); QDialog::reject(); } // NOTE: We can't have this in the ctor because PageContainer calls a virtual function, and so // won't work with subclasses if we put it in this ctor. void ResourceDownloadDialog::initializeContainer() { // small margins look ugly on macOS on modal windows #ifndef Q_OS_MACOS layout()->setContentsMargins(0, 0, 0, 0); #endif m_container = new PageContainer(this, {}, this); m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); m_container->layout()->setContentsMargins(0, 0, 0, 0); m_vertical_layout.addWidget(m_container); m_container->addButtons(&m_buttons); connect(m_container, &PageContainer::selectedPageChanged, this, &ResourceDownloadDialog::selectedPageChanged); } void ResourceDownloadDialog::connectButtons() { auto OkButton = m_buttons.button(QDialogButtonBox::Ok); OkButton->setToolTip( tr("Opens a new popup to review your selected %1 and confirm your selection. Shortcut: Ctrl+Return").arg(resourcesString())); connect(OkButton, &QPushButton::clicked, this, &ResourceDownloadDialog::confirm); auto CancelButton = m_buttons.button(QDialogButtonBox::Cancel); connect(CancelButton, &QPushButton::clicked, this, &ResourceDownloadDialog::reject); auto HelpButton = m_buttons.button(QDialogButtonBox::Help); connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); } void ResourceDownloadDialog::confirm() { auto confirm_dialog = ReviewMessageBox::create(this, tr("Confirm %1 to download").arg(resourcesString())); confirm_dialog->retranslateUi(resourcesString()); QHash dependencyExtraInfo; QStringList depNames; if (auto task = getModDependenciesTask(); task) { connect(task.get(), &Task::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); auto weak = task.toWeakRef(); connect(task.get(), &Task::succeeded, this, [this, weak]() { QStringList warnings; if (auto task = weak.lock()) { warnings = task->warnings(); } if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec(); } }); // Check for updates ProgressDialog progress_dialog(this); progress_dialog.setSkipButton(true, tr("Abort")); progress_dialog.setWindowTitle(tr("Checking for dependencies...")); auto ret = progress_dialog.execWithTask(task.get()); // If the dialog was skipped / some download error happened if (ret == QDialog::DialogCode::Rejected) { QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); return; } else { for (auto dep : task->getDependecies()) { addResource(dep->pack, dep->version); depNames << dep->pack->name; } dependencyExtraInfo = task->getExtraInfo(); } } auto selected = getTasks(); std::sort(selected.begin(), selected.end(), [](const DownloadTaskPtr& a, const DownloadTaskPtr& b) { return QString::compare(a->getName(), b->getName(), Qt::CaseInsensitive) < 0; }); for (auto& task : selected) { auto extraInfo = dependencyExtraInfo.value(task->getPack()->addonId.toString()); confirm_dialog->appendResource({ task->getName(), task->getFilename(), ModPlatform::ProviderCapabilities::name(task->getProvider()), extraInfo.required_by, task->getVersion().version_type.toString(), !extraInfo.maybe_installed }); } if (confirm_dialog->exec()) { auto deselected = confirm_dialog->deselectedResources(); for (auto page : m_container->getPages()) { auto res = static_cast(page); for (auto name : deselected) res->removeResourceFromPage(name); } this->accept(); } else { for (auto name : depNames) removeResource(name); } } bool ResourceDownloadDialog::selectPage(QString pageId) { return m_container->selectPage(pageId); } ResourcePage* ResourceDownloadDialog::selectedPage() { ResourcePage* result = dynamic_cast(m_container->selectedPage()); Q_ASSERT(result != nullptr); return result; } void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver) { removeResource(pack->name); selectedPage()->addResourceToPage(pack, ver, getBaseModel()); setButtonStatus(); } void ResourceDownloadDialog::removeResource(const QString& pack_name) { for (auto page : m_container->getPages()) { static_cast(page)->removeResourceFromPage(pack_name); } setButtonStatus(); } void ResourceDownloadDialog::setButtonStatus() { auto selected = false; for (auto page : m_container->getPages()) { auto res = static_cast(page); selected = selected || res->hasSelectedPacks(); } m_buttons.button(QDialogButtonBox::Ok)->setEnabled(selected); } const QList ResourceDownloadDialog::getTasks() { QList selected; for (auto page : m_container->getPages()) { auto res = static_cast(page); selected.append(res->selectedPacks()); } return selected; } void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) { auto* prev_page = dynamic_cast(previous); if (!prev_page) { qCritical() << "Page '" << previous->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!"; return; } // Same effect as having a global search bar ResourcePage* result = dynamic_cast(selected); Q_ASSERT(result != nullptr); result->setSearchTerm(prev_page->getSearchTerm()); } ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) : ResourceDownloadDialog(parent, mods), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); if (!geometrySaveKey().isEmpty()) restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ModDownloadDialog::getPages() { QList pages; auto loaders = static_cast(m_instance)->getPackProfile()->getSupportedModLoaders().value(); if (ModrinthAPI::validateModLoaders(loaders)) pages.append(ModrinthModPage::create(this, *m_instance)); if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders)) pages.append(FlameModPage::create(this, *m_instance)); return pages; } GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() { if (!APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies if (auto model = dynamic_cast(getBaseModel().get()); model) { QList> selectedVers; for (auto& selected : getTasks()) { selectedVers.append(std::make_shared(selected->getPack(), selected->getVersion())); } return makeShared(m_instance, model, selectedVers); } } return nullptr; } ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, const std::shared_ptr& resource_packs, BaseInstance* instance) : ResourceDownloadDialog(parent, resource_packs), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); if (!geometrySaveKey().isEmpty()) restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ResourcePackDownloadDialog::getPages() { QList pages; pages.append(ModrinthResourcePackPage::create(this, *m_instance)); if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameResourcePackPage::create(this, *m_instance)); return pages; } TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, const std::shared_ptr& resource_packs, BaseInstance* instance) : ResourceDownloadDialog(parent, resource_packs), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); if (!geometrySaveKey().isEmpty()) restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList TexturePackDownloadDialog::getPages() { QList pages; pages.append(ModrinthTexturePackPage::create(this, *m_instance)); if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameTexturePackPage::create(this, *m_instance)); return pages; } ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, const std::shared_ptr& shaders, BaseInstance* instance) : ResourceDownloadDialog(parent, shaders), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); if (!geometrySaveKey().isEmpty()) restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ShaderPackDownloadDialog::getPages() { QList pages; pages.append(ModrinthShaderPackPage::create(this, *m_instance)); if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameShaderPackPage::create(this, *m_instance)); return pages; } void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptr& meta) { switch (meta->provider) { case ModPlatform::ResourceProvider::MODRINTH: selectPage(Modrinth::id()); break; case ModPlatform::ResourceProvider::FLAME: selectPage(Flame::id()); break; } setWindowTitle(tr("Change %1 version").arg(meta->name)); m_container->hidePageList(); m_buttons.hide(); auto page = selectedPage(); page->openProject(meta->project_id); } DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent, const std::shared_ptr& data_packs, BaseInstance* instance) : ResourceDownloadDialog(parent, data_packs), m_instance(instance) { setWindowTitle(dialogTitle()); initializeContainer(); connectButtons(); if (!geometrySaveKey().isEmpty()) restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); } QList DataPackDownloadDialog::getPages() { QList pages; pages.append(ModrinthDataPackPage::create(this, *m_instance)); if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameDataPackPage::create(this, *m_instance)); return pages; } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/dialogs/ExportToModListDialog.cpp0000644000175100017510000002017115144136756024540 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExportToModListDialog.h" #include #include #include #include "FileSystem.h" #include "Markdown.h" #include "StringUtils.h" #include "modplatform/helpers/ExportToModList.h" #include "ui_ExportToModListDialog.h" #include #include #include #include #include const QHash ExportToModListDialog::exampleLines = { { ExportToModList::HTML, "
  • {name} [{version}] by {authors}
  • " }, { ExportToModList::MARKDOWN, "[{name}]({url}) [{version}] by {authors}" }, { ExportToModList::PLAINTXT, "{name} ({url}) [{version}] by {authors}" }, { ExportToModList::JSON, "{\"name\":\"{name}\",\"url\":\"{url}\",\"version\":\"{version}\",\"authors\":\"{authors}\"}," }, { ExportToModList::CSV, "{name},{url},{version},\"{authors}\"" }, }; ExportToModListDialog::ExportToModListDialog(QString name, QList mods, QWidget* parent) : QDialog(parent), m_mods(mods), m_template_changed(false), m_name(name), ui(new Ui::ExportToModListDialog) { ui->setupUi(this); enableCustom(false); connect(ui->formatComboBox, &QComboBox::currentIndexChanged, this, &ExportToModListDialog::formatChanged); connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->filenameCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->authorsButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Authors); }); connect(ui->versionButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Version); }); connect(ui->urlButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::Url); }); connect(ui->filenameButton, &QPushButton::clicked, this, [this](bool) { addExtra(ExportToModList::FileName); }); connect(ui->templateText, &QTextEdit::textChanged, this, [this] { if (ui->templateText->toPlainText() != exampleLines[m_format]) ui->formatComboBox->setCurrentIndex(5); triggerImp(); }); connect(ui->copyButton, &QPushButton::clicked, this, [this](bool) { this->ui->finalText->selectAll(); this->ui->finalText->copy(); }); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Save)->setText(tr("Save")); triggerImp(); } ExportToModListDialog::~ExportToModListDialog() { delete ui; } void ExportToModListDialog::formatChanged(int index) { switch (index) { case 0: { enableCustom(false); ui->resultText->show(); m_format = ExportToModList::HTML; break; } case 1: { enableCustom(false); ui->resultText->show(); m_format = ExportToModList::MARKDOWN; break; } case 2: { enableCustom(false); ui->resultText->hide(); m_format = ExportToModList::PLAINTXT; break; } case 3: { enableCustom(false); ui->resultText->hide(); m_format = ExportToModList::JSON; break; } case 4: { enableCustom(false); ui->resultText->hide(); m_format = ExportToModList::CSV; break; } case 5: { m_template_changed = true; enableCustom(true); ui->resultText->hide(); m_format = ExportToModList::CUSTOM; break; } } triggerImp(); } void ExportToModListDialog::triggerImp() { if (m_format == ExportToModList::CUSTOM) { ui->finalText->setPlainText(ExportToModList::exportToModList(m_mods, ui->templateText->toPlainText())); return; } auto opt = 0; if (ui->authorsCheckBox->isChecked()) opt |= ExportToModList::Authors; if (ui->versionCheckBox->isChecked()) opt |= ExportToModList::Version; if (ui->urlCheckBox->isChecked()) opt |= ExportToModList::Url; if (ui->filenameCheckBox->isChecked()) opt |= ExportToModList::FileName; auto txt = ExportToModList::exportToModList(m_mods, m_format, static_cast(opt)); ui->finalText->setPlainText(txt); switch (m_format) { case ExportToModList::CUSTOM: return; case ExportToModList::HTML: ui->resultText->setHtml(StringUtils::htmlListPatch(txt)); break; case ExportToModList::MARKDOWN: ui->resultText->setHtml(StringUtils::htmlListPatch(markdownToHTML(txt))); break; case ExportToModList::PLAINTXT: break; case ExportToModList::JSON: break; case ExportToModList::CSV: break; } auto exampleLine = exampleLines[m_format]; if (!m_template_changed && ui->templateText->toPlainText() != exampleLine) ui->templateText->setPlainText(exampleLine); } void ExportToModListDialog::done(int result) { if (result == Accepted) { const QString filename = FS::RemoveInvalidFilenameChars(m_name); const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_name), FS::PathCombine(QDir::homePath(), filename + extension()), tr("File") + " (*.txt *.html *.md *.json *.csv)", nullptr); if (output.isEmpty()) return; try { FS::write(output, ui->finalText->toPlainText().toUtf8()); } catch (const FS::FileSystemException& e) { qCritical() << "Failed to save mod list file :" << e.cause(); } } QDialog::done(result); } QString ExportToModListDialog::extension() { switch (m_format) { case ExportToModList::HTML: return ".html"; case ExportToModList::MARKDOWN: return ".md"; case ExportToModList::PLAINTXT: return ".txt"; case ExportToModList::CUSTOM: return ".txt"; case ExportToModList::JSON: return ".json"; case ExportToModList::CSV: return ".csv"; } return ".txt"; } void ExportToModListDialog::addExtra(ExportToModList::OptionalData option) { if (m_format != ExportToModList::CUSTOM) return; switch (option) { case ExportToModList::Authors: ui->templateText->insertPlainText("{authors}"); break; case ExportToModList::Url: ui->templateText->insertPlainText("{url}"); break; case ExportToModList::Version: ui->templateText->insertPlainText("{version}"); break; case ExportToModList::FileName: ui->templateText->insertPlainText("{filename}"); break; } } void ExportToModListDialog::enableCustom(bool enabled) { ui->authorsCheckBox->setHidden(enabled); ui->authorsButton->setHidden(!enabled); ui->versionCheckBox->setHidden(enabled); ui->versionButton->setHidden(!enabled); ui->urlCheckBox->setHidden(enabled); ui->urlButton->setHidden(!enabled); ui->filenameCheckBox->setHidden(enabled); ui->filenameButton->setHidden(!enabled); } PrismLauncher-10.0.5/launcher/ui/dialogs/ExportInstanceDialog.h0000644000175100017510000000442115144136756024073 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "FastFileIconProvider.h" #include "FileIgnoreProxy.h" class BaseInstance; using InstancePtr = std::shared_ptr; namespace Ui { class ExportInstanceDialog; } class ExportInstanceDialog : public QDialog { Q_OBJECT public: explicit ExportInstanceDialog(InstancePtr instance, QWidget* parent = 0); ~ExportInstanceDialog(); virtual void done(int result); private: void doExport(); QString ignoreFileName(); private: Ui::ExportInstanceDialog* m_ui; InstancePtr m_instance; FileIgnoreProxy* m_proxyModel; FastFileIconProvider m_icons; private slots: void rowsInserted(QModelIndex parent, int top, int bottom); }; PrismLauncher-10.0.5/launcher/ui/dialogs/IconPickerDialog.h0000644000175100017510000000266015144136756023156 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include namespace Ui { class IconPickerDialog; } class IconPickerDialog : public QDialog { Q_OBJECT public: explicit IconPickerDialog(QWidget* parent = 0); ~IconPickerDialog(); int execWithSelection(QString selection); QString selectedIconKey; protected: virtual bool eventFilter(QObject*, QEvent*); private: Ui::IconPickerDialog* ui; QPushButton* buttonRemove; QLineEdit* searchBar; QSortFilterProxyModel* proxyModel; private slots: void selectionChanged(QItemSelection, QItemSelection); void activated(QModelIndex); void delayed_scroll(QModelIndex); void addNewIcon(); void removeSelectedIcon(); void openFolder(); void filterIcons(const QString& text); }; PrismLauncher-10.0.5/launcher/ui/dialogs/CustomMessageBox.h0000644000175100017510000000212115144136756023230 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include namespace CustomMessageBox { QMessageBox* selectable(QWidget* parent, const QString& title, const QString& text, QMessageBox::Icon icon = QMessageBox::NoIcon, QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton, QCheckBox* checkBox = nullptr); } PrismLauncher-10.0.5/launcher/ui/dialogs/ResourceDownloadDialog.h0000644000175100017510000001441515144136756024410 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "QObjectPtr.h" #include "minecraft/mod/DataPackFolderModel.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "ui/pages/BasePageProvider.h" class BaseInstance; class ModFolderModel; class PageContainer; class QVBoxLayout; class QDialogButtonBox; class ResourceDownloadTask; class ResourceFolderModel; class ResourcePackFolderModel; class TexturePackFolderModel; class ShaderPackFolderModel; namespace ResourceDownload { class ResourcePage; class ResourceDownloadDialog : public QDialog, public BasePageProvider { Q_OBJECT public: using DownloadTaskPtr = shared_qobject_ptr; ResourceDownloadDialog(QWidget* parent, std::shared_ptr base_model); void initializeContainer(); void connectButtons(); //: String that gets appended to the download dialog title ("Download " + resourcesString()) virtual QString resourcesString() const { return tr("resources"); } QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); }; bool selectPage(QString pageId); ResourcePage* selectedPage(); void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); void removeResource(const QString&); const QList getTasks(); const std::shared_ptr getBaseModel() const { return m_base_model; } void setResourceMetadata(const std::shared_ptr& meta); public slots: void accept() override; void reject() override; protected slots: void selectedPageChanged(BasePage* previous, BasePage* selected); virtual void confirm(); protected: virtual QString geometrySaveKey() const { return ""; } void setButtonStatus(); virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; } protected: const std::shared_ptr m_base_model; PageContainer* m_container = nullptr; QDialogButtonBox m_buttons; QVBoxLayout m_vertical_layout; }; class ModDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: explicit ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance); ~ModDownloadDialog() override = default; //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) QString resourcesString() const override { return tr("mods"); } QString geometrySaveKey() const override { return "ModDownloadGeometry"; } QList getPages() override; GetModDependenciesTask::Ptr getModDependenciesTask() override; private: BaseInstance* m_instance; }; class ResourcePackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: explicit ResourcePackDownloadDialog(QWidget* parent, const std::shared_ptr& resource_packs, BaseInstance* instance); ~ResourcePackDownloadDialog() override = default; //: String that gets appended to the resource pack download dialog title ("Download " + resourcesString()) QString resourcesString() const override { return tr("resource packs"); } QString geometrySaveKey() const override { return "RPDownloadGeometry"; } QList getPages() override; private: BaseInstance* m_instance; }; class TexturePackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: explicit TexturePackDownloadDialog(QWidget* parent, const std::shared_ptr& resource_packs, BaseInstance* instance); ~TexturePackDownloadDialog() override = default; //: String that gets appended to the texture pack download dialog title ("Download " + resourcesString()) QString resourcesString() const override { return tr("texture packs"); } QString geometrySaveKey() const override { return "TPDownloadGeometry"; } QList getPages() override; private: BaseInstance* m_instance; }; class ShaderPackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: explicit ShaderPackDownloadDialog(QWidget* parent, const std::shared_ptr& shader_packs, BaseInstance* instance); ~ShaderPackDownloadDialog() override = default; //: String that gets appended to the shader pack download dialog title ("Download " + resourcesString()) QString resourcesString() const override { return tr("shader packs"); } QString geometrySaveKey() const override { return "ShaderDownloadGeometry"; } QList getPages() override; private: BaseInstance* m_instance; }; class DataPackDownloadDialog final : public ResourceDownloadDialog { Q_OBJECT public: explicit DataPackDownloadDialog(QWidget* parent, const std::shared_ptr& data_packs, BaseInstance* instance); ~DataPackDownloadDialog() override = default; //: String that gets appended to the data pack download dialog title ("Download " + resourcesString()) QString resourcesString() const override { return tr("data packs"); } QString geometrySaveKey() const override { return "DataPackDownloadGeometry"; } QList getPages() override; private: BaseInstance* m_instance; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/dialogs/AboutDialog.h0000644000175100017510000000152015144136756022174 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include namespace Ui { class AboutDialog; } class AboutDialog : public QDialog { Q_OBJECT public: explicit AboutDialog(QWidget* parent = 0); ~AboutDialog(); private: Ui::AboutDialog* ui; }; PrismLauncher-10.0.5/launcher/ui/dialogs/NewInstanceDialog.h0000644000175100017510000000636515144136756023354 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "InstanceTask.h" #include "ui/pages/BasePageProvider.h" namespace Ui { class NewInstanceDialog; } class PageContainer; class QDialogButtonBox; class ImportPage; class FlamePage; class NewInstanceDialog : public QDialog, public BasePageProvider { Q_OBJECT public: explicit NewInstanceDialog(const QString& initialGroup, const QString& url = QString(), const QMap& extra_info = {}, QWidget* parent = 0); ~NewInstanceDialog(); void updateDialogState(); void setSuggestedPack(const QString& name = QString(), InstanceTask* task = nullptr); void setSuggestedPack(const QString& name, QString version, InstanceTask* task = nullptr); void setSuggestedIconFromFile(const QString& path, const QString& name); void setSuggestedIcon(const QString& key); InstanceTask* extractTask(); QString dialogTitle() override; QList getPages() override; QString instName() const; QString instGroup() const; QString iconKey() const; public slots: void accept() override; void reject() override; private slots: void on_iconButton_clicked(); void on_instNameTextBox_textChanged(const QString& arg1); void selectedPageChanged(BasePage* previous, BasePage* selected); private: Ui::NewInstanceDialog* ui = nullptr; PageContainer* m_container = nullptr; QDialogButtonBox* m_buttons = nullptr; QString InstIconKey; ImportPage* importPage = nullptr; std::unique_ptr creationTask; bool importIcon = false; QString importIconPath; QString importIconName; QString importVersion; QString m_searchTerm; void importIconNow(); }; PrismLauncher-10.0.5/launcher/ui/dialogs/AboutDialog.ui0000644000175100017510000002244215144136756022370 0ustar runnerrunner AboutDialog 0 0 573 600 450 400 Qt::Horizontal 40 20 0 0 64 64 64 64 Qt::Horizontal 40 20 15 Launcher Qt::AlignCenter IBeamCursor Qt::TextSelectableByMouse 0 About true <html><head/><body><p>A custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.</p></body></html> Qt::AlignCenter true 10 GIT URL Qt::AlignCenter Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse 8 true COPYRIGHT Qt::AlignCenter Qt::Horizontal IBeamCursor Platform: Qt::AlignCenter Qt::TextSelectableByMouse IBeamCursor Build Date: Qt::AlignCenter Qt::TextSelectableByMouse IBeamCursor Commit: Qt::AlignCenter Qt::TextSelectableByMouse IBeamCursor Channel: Qt::AlignCenter Qt::TextSelectableByMouse Qt::Vertical 20 212 Credits true License 0 0 DejaVu Sans Mono true Qt::TextBrowserInteraction false About Qt Qt::Horizontal 40 20 Close tabWidget creditsText licenseText aboutQt closeButton PrismLauncher-10.0.5/launcher/ui/dialogs/ProfileSetupDialog.cpp0000644000175100017510000002331215144136756024101 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ProfileSetupDialog.h" #include "net/RawHeaderProxy.h" #include "ui_ProfileSetupDialog.h" #include #include #include #include #include #include "ui/dialogs/ProgressDialog.h" #include #include "minecraft/auth/Parsers.h" #include "net/Upload.h" ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget* parent) : QDialog(parent), m_accountToSetup(accountToSetup), ui(new Ui::ProfileSetupDialog) { ui->setupUi(this); ui->errorLabel->setVisible(false); goodIcon = QIcon::fromTheme("status-good"); yellowIcon = QIcon::fromTheme("status-yellow"); badIcon = QIcon::fromTheme("status-bad"); static const QRegularExpression s_permittedNames("[a-zA-Z0-9_]{3,16}"); auto nameEdit = ui->nameEdit; nameEdit->setValidator(new QRegularExpressionValidator(s_permittedNames)); nameEdit->setClearButtonEnabled(true); validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited); checkStartTimer.setSingleShot(true); connect(&checkStartTimer, &QTimer::timeout, this, &ProfileSetupDialog::startCheck); setNameStatus(NameStatus::NotSet, QString()); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ProfileSetupDialog::~ProfileSetupDialog() { delete ui; } void ProfileSetupDialog::on_buttonBox_accepted() { setupProfile(currentCheck); } void ProfileSetupDialog::on_buttonBox_rejected() { reject(); } void ProfileSetupDialog::setNameStatus(ProfileSetupDialog::NameStatus status, QString errorString = QString()) { nameStatus = status; auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok); switch (nameStatus) { case NameStatus::Available: { validityAction->setIcon(goodIcon); okButton->setEnabled(true); } break; case NameStatus::NotSet: case NameStatus::Pending: validityAction->setIcon(yellowIcon); okButton->setEnabled(false); break; case NameStatus::Exists: case NameStatus::Error: validityAction->setIcon(badIcon); okButton->setEnabled(false); break; } if (!errorString.isEmpty()) { ui->errorLabel->setText(errorString); ui->errorLabel->setVisible(true); } else { ui->errorLabel->setVisible(false); } } void ProfileSetupDialog::nameEdited(const QString& name) { if (!ui->nameEdit->hasAcceptableInput()) { setNameStatus(NameStatus::NotSet, tr("Name is too short - must be between 3 and 16 characters long.")); return; } scheduleCheck(name); } void ProfileSetupDialog::scheduleCheck(const QString& name) { queuedCheck = name; setNameStatus(NameStatus::Pending); checkStartTimer.start(1000); } void ProfileSetupDialog::startCheck() { if (isChecking) { return; } if (queuedCheck.isNull()) { return; } checkName(queuedCheck); } void ProfileSetupDialog::checkName(const QString& name) { if (isChecking) { return; } currentCheck = name; isChecking = true; QUrl url(QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name)); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } }; m_check_response.reset(new QByteArray()); if (m_check_task) disconnect(m_check_task.get(), nullptr, this, nullptr); m_check_task = Net::Download::makeByteArray(url, m_check_response); m_check_task->addHeaderProxy(new Net::RawHeaderProxy(headers)); connect(m_check_task.get(), &Task::finished, this, &ProfileSetupDialog::checkFinished); m_check_task->setNetwork(APPLICATION->network()); m_check_task->start(); } void ProfileSetupDialog::checkFinished() { if (m_check_task->error() == QNetworkReply::NoError) { auto doc = QJsonDocument::fromJson(*m_check_response); auto root = doc.object(); auto statusValue = root.value("status").toString("INVALID"); if (statusValue == "AVAILABLE") { setNameStatus(NameStatus::Available); } else if (statusValue == "DUPLICATE") { setNameStatus(NameStatus::Exists, tr("Minecraft profile with name %1 already exists.").arg(currentCheck)); } else if (statusValue == "NOT_ALLOWED") { setNameStatus(NameStatus::Exists, tr("The name %1 is not allowed.").arg(currentCheck)); } else { setNameStatus(NameStatus::Error, tr("Unhandled profile name status: %1").arg(statusValue)); } } else { setNameStatus(NameStatus::Error, tr("Failed to check name availability.")); } isChecking = false; } void ProfileSetupDialog::setupProfile(const QString& profileName) { if (isWorking) { return; } QString payloadTemplate("{\"profileName\":\"%1\"}"); QUrl url("https://api.minecraftservices.com/minecraft/profile"); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_accountToSetup->accessToken()).toUtf8() } }; m_profile_response.reset(new QByteArray()); m_profile_task = Net::Upload::makeByteArray(url, m_profile_response, payloadTemplate.arg(profileName).toUtf8()); m_profile_task->addHeaderProxy(new Net::RawHeaderProxy(headers)); connect(m_profile_task.get(), &Task::finished, this, &ProfileSetupDialog::setupProfileFinished); m_profile_task->setNetwork(APPLICATION->network()); m_profile_task->start(); isWorking = true; auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); button->setEnabled(false); } namespace { struct MojangError { static MojangError fromJSON(QByteArray data) { MojangError out; out.rawError = QString::fromUtf8(data); auto doc = QJsonDocument::fromJson(data, &out.parseError); out.fullyParsed = false; if (!out.parseError.error) { auto object = doc.object(); out.fullyParsed = true; out.fullyParsed &= Parsers::getString(object.value("path"), out.path); out.fullyParsed &= Parsers::getString(object.value("error"), out.error); out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage); } return out; } QString rawError; QJsonParseError parseError; bool fullyParsed; QString path; QString error; QString errorMessage; }; } // namespace void ProfileSetupDialog::setupProfileFinished() { isWorking = false; if (m_profile_task->error() == QNetworkReply::NoError) { /* * data contains the profile in the response * ... we could parse it and update the account, but let's just return back to the normal login flow instead... */ accept(); } else { auto parsedError = MojangError::fromJSON(*m_profile_response); ui->errorLabel->setVisible(true); QString errorMessage = tr("Network Error: %1\nHTTP Status: %2").arg(m_profile_task->errorString(), QString::number(m_profile_task->replyStatusCode())); if (parsedError.fullyParsed) { errorMessage += "Path: " + parsedError.path + "\n"; errorMessage += "Error: " + parsedError.error + "\n"; errorMessage += "Message: " + parsedError.errorMessage + "\n"; } else { errorMessage += "Failed to parse error from Mojang API: " + parsedError.parseError.errorString() + "\n"; errorMessage += "Log:\n" + parsedError.rawError + "\n"; } ui->errorLabel->setText(tr("The server responded with the following error:") + "\n\n" + errorMessage); qDebug() << parsedError.rawError; auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); button->setEnabled(true); } } PrismLauncher-10.0.5/launcher/ui/dialogs/ImportResourceDialog.h0000644000175100017510000000131515144136756024106 0ustar runnerrunner#pragma once #include #include #include "modplatform/ResourceType.h" #include "ui/instanceview/InstanceProxyModel.h" namespace Ui { class ImportResourceDialog; } class ImportResourceDialog : public QDialog { Q_OBJECT public: explicit ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent = nullptr); ~ImportResourceDialog() override; QString selectedInstanceKey; private: Ui::ImportResourceDialog* ui; ModPlatform::ResourceType m_resource_type; QString m_file_path; InstanceProxyModel* proxyModel; private slots: void selectionChanged(QItemSelection, QItemSelection); void activated(QModelIndex); }; PrismLauncher-10.0.5/launcher/ui/dialogs/NewComponentDialog.ui0000644000175100017510000000452315144136756023732 0ustar runnerrunner NewComponentDialog Qt::ApplicationModal 0 0 345 146 Add Empty Component :/icons/toolbar/copy:/icons/toolbar/copy true Name uid Qt::Horizontal Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok nameTextBox uidTextBox buttonBox accepted() NewComponentDialog accept() 248 254 157 274 buttonBox rejected() NewComponentDialog reject() 316 260 286 274 PrismLauncher-10.0.5/launcher/ui/dialogs/ChooseOfflineNameDialog.ui0000644000175100017510000000327215144136756024642 0ustar runnerrunner ChooseOfflineNameDialog 0 0 400 158 Choose Offline Name 0 0 Message label placeholder. Username A username is valid only if it is from 3 to 16 characters in length, uses English letters, numbers, and underscores. An invalid username may prevent joining servers and singleplayer worlds. Allow invalid usernames QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok PrismLauncher-10.0.5/launcher/ui/dialogs/ExportInstanceDialog.ui0000644000175100017510000000366215144136756024267 0ustar runnerrunner ExportInstanceDialog 0 0 720 625 Export Instance true QAbstractItemView::ExtendedSelection true false Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok treeView buttonBox accepted() ExportInstanceDialog accept() 248 254 157 274 buttonBox rejected() ExportInstanceDialog reject() 316 260 286 274 PrismLauncher-10.0.5/launcher/ui/dialogs/VersionSelectDialog.h0000644000175100017510000000411115144136756023706 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "BaseVersionList.h" class QVBoxLayout; class QHBoxLayout; class QDialogButtonBox; class VersionSelectWidget; class QPushButton; class VersionProxyModel; class VersionSelectDialog : public QDialog { Q_OBJECT public: explicit VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent = 0, bool cancelable = true); virtual ~VersionSelectDialog() = default; int exec() override; BaseVersion::Ptr selectedVersion() const; void setCurrentVersion(const QString& version); void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); void setExactFilter(BaseVersionList::ModelRoles role, QString filter); void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter); void setEmptyString(QString emptyString); void setEmptyErrorString(QString emptyErrorString); void setResizeOn(int column); private slots: void on_refreshButton_clicked(); private: void retranslate(); void selectRecommended(); private: QString m_currentVersion; VersionSelectWidget* m_versionWidget = nullptr; QVBoxLayout* m_verticalLayout = nullptr; QHBoxLayout* m_horizontalLayout = nullptr; QPushButton* m_refreshButton = nullptr; QDialogButtonBox* m_buttonBox = nullptr; BaseVersionList* m_vlist = nullptr; VersionProxyModel* m_proxyModel = nullptr; int resizeOnColumn = -1; Task* loadTask = nullptr; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ProgressDialog.h0000644000175100017510000000563215144136756022736 0ustar runnerrunner/// SPDX-License-Identifier: GPL-3.0-only /* * PrismLaucher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "QObjectPtr.h" #include "tasks/Task.h" #include "ui/widgets/SubTaskProgressBar.h" class Task; class SequentialTask; namespace Ui { class ProgressDialog; } class ProgressDialog : public QDialog { Q_OBJECT public: explicit ProgressDialog(QWidget* parent = 0); ~ProgressDialog(); void updateSize(bool recenterParent = false); int execWithTask(Task* task); int execWithTask(std::unique_ptr&& task); int execWithTask(std::unique_ptr& task); void setSkipButton(bool present, QString label = QString()); Task* getTask(); public slots: void onTaskStarted(); void onTaskFailed(QString failure); void onTaskSucceeded(); void changeStatus(const QString& status); void changeProgress(qint64 current, qint64 total); void changeStepProgress(TaskStepProgress const& task_progress); private slots: void on_skipButton_clicked(bool checked); protected: virtual void keyPressEvent(QKeyEvent* e); virtual void closeEvent(QCloseEvent* e); private: bool handleImmediateResult(QDialog::DialogCode& result); void addTaskProgress(TaskStepProgress const& progress); private: Ui::ProgressDialog* ui; Task* m_task; QList m_taskConnections; bool m_is_multi_step = false; QHash taskProgress; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ChooseOfflineNameDialog.h0000644000175100017510000000303015144136756024444 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include QT_BEGIN_NAMESPACE namespace Ui { class ChooseOfflineNameDialog; } QT_END_NAMESPACE class ChooseOfflineNameDialog final : public QDialog { Q_OBJECT public: explicit ChooseOfflineNameDialog(const QString& message, QWidget* parent = nullptr); ~ChooseOfflineNameDialog() override; QString getUsername() const; void setUsername(const QString& username) const; private: void updateAcceptAllowed(const QString& username) const; protected slots: void on_usernameTextBox_textEdited(const QString& newText) const; void on_allowInvalidUsernames_checkStateChanged(Qt::CheckState checkState) const; private: Ui::ChooseOfflineNameDialog* ui; QRegularExpressionValidator* m_usernameValidator; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ChooseProviderDialog.ui0000644000175100017510000000456315144136756024255 0ustar runnerrunner ChooseProviderDialog 0 0 453 197 Choose a mod provider Qt::AlignJustify|Qt::AlignTop true -1 Qt::AlignHCenter|Qt::AlignTop Qt::AlignHCenter|Qt::AlignTop Skip this mod Skip all Confirm for all Confirm true Try to automatically use other providers if the chosen one fails true PrismLauncher-10.0.5/launcher/ui/dialogs/ProfileSelectDialog.ui0000644000175100017510000000301215144136756024046 0ustar runnerrunner ProfileSelectDialog 0 0 465 300 Select an Account Select a profile. 1 Use as default? Use as default for this instance only? QDialogButtonBox::Cancel|QDialogButtonBox::Ok PrismLauncher-10.0.5/launcher/ui/dialogs/ReviewMessageBox.h0000644000175100017510000000165415144136756023231 0ustar runnerrunner#pragma once #include #include namespace Ui { class ReviewMessageBox; } class ReviewMessageBox : public QDialog { Q_OBJECT public: static auto create(QWidget* parent, QString&& title, QString&& icon = "") -> ReviewMessageBox*; using ResourceInformation = struct res_info { QString name; QString filename; QString provider; QStringList required_by; QString version_type; bool enabled = true; }; void appendResource(ResourceInformation&& info); auto deselectedResources() -> QStringList; void retranslateUi(QString resources_name); ~ReviewMessageBox() override; protected slots: void on_toggleDepsButton_clicked(); protected: ReviewMessageBox(QWidget* parent, const QString& title, const QString& icon); Ui::ReviewMessageBox* ui; QList m_deps; bool m_deps_checked = true; }; PrismLauncher-10.0.5/launcher/ui/dialogs/MSALoginDialog.h0000644000175100017510000000274115144136756022541 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "minecraft/auth/AuthFlow.h" #include "minecraft/auth/MinecraftAccount.h" namespace Ui { class MSALoginDialog; } class MSALoginDialog : public QDialog { Q_OBJECT public: ~MSALoginDialog(); static MinecraftAccountPtr newAccount(QWidget* parent); int exec() override; private: explicit MSALoginDialog(QWidget* parent = 0); protected slots: void onTaskFailed(QString reason); void onDeviceFlowStatus(QString status); void onAuthFlowStatus(QString status); void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); private: Ui::MSALoginDialog* ui; MinecraftAccountPtr m_account; shared_qobject_ptr m_devicecode_task; shared_qobject_ptr m_authflow_task; QUrl m_url; }; PrismLauncher-10.0.5/launcher/ui/dialogs/UpdateAvailableDialog.cpp0000644000175100017510000000461115144136756024504 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "UpdateAvailableDialog.h" #include #include "BuildConfig.h" #include "Markdown.h" #include "StringUtils.h" #include "ui_UpdateAvailableDialog.h" UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, const QString& availableVersion, const QString& releaseNotes, QWidget* parent) : QDialog(parent), ui(new Ui::UpdateAvailableDialog) { ui->setupUi(this); QString launcherName = BuildConfig.LAUNCHER_DISPLAYNAME; ui->headerLabel->setText(tr("A new version of %1 is available!").arg(launcherName)); ui->versionAvailableLabel->setText( tr("Version %1 is now available - you have %2 . Would you like to download it now?").arg(availableVersion).arg(currentVersion)); ui->icon->setPixmap(QIcon::fromTheme("checkupdate").pixmap(64)); auto releaseNotesHtml = markdownToHTML(releaseNotes); ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml)); ui->releaseNotes->setOpenExternalLinks(true); connect(ui->skipButton, &QPushButton::clicked, this, [this]() { setResult(ResultCode::Skip); done(ResultCode::Skip); }); connect(ui->delayButton, &QPushButton::clicked, this, [this]() { setResult(ResultCode::DontInstall); done(ResultCode::DontInstall); }); connect(ui->installButton, &QPushButton::clicked, this, [this]() { setResult(ResultCode::Install); done(ResultCode::Install); }); } PrismLauncher-10.0.5/launcher/ui/dialogs/ChooseProviderDialog.cpp0000644000175100017510000000512415144136756024414 0ustar runnerrunner#include "ChooseProviderDialog.h" #include "ui_ChooseProviderDialog.h" #include #include #include "modplatform/ModIndex.h" ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping) : QDialog(parent), ui(new Ui::ChooseProviderDialog) { ui->setupUi(this); addProviders(); m_providers.button(0)->click(); connect(ui->skipOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipOne); connect(ui->skipAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipAll); connect(ui->confirmOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmOne); connect(ui->confirmAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmAll); if (single_choice) { ui->providersLayout->removeWidget(ui->skipAllButton); ui->providersLayout->removeWidget(ui->confirmAllButton); } if (!allow_skipping) { ui->providersLayout->removeWidget(ui->skipOneButton); ui->providersLayout->removeWidget(ui->skipAllButton); } } ChooseProviderDialog::~ChooseProviderDialog() { delete ui; } void ChooseProviderDialog::setDescription(QString desc) { ui->explanationLabel->setText(desc); } void ChooseProviderDialog::skipOne() { reject(); } void ChooseProviderDialog::skipAll() { m_response.skip_all = true; reject(); } void ChooseProviderDialog::confirmOne() { m_response.chosen = getSelectedProvider(); m_response.try_others = ui->tryOthersCheckbox->isChecked(); accept(); } void ChooseProviderDialog::confirmAll() { m_response.chosen = getSelectedProvider(); m_response.confirm_all = true; m_response.try_others = ui->tryOthersCheckbox->isChecked(); accept(); } auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::ResourceProvider { return ModPlatform::ResourceProvider(m_providers.checkedId()); } void ChooseProviderDialog::addProviders() { int btn_index = 0; QRadioButton* btn; for (auto& provider : { ModPlatform::ResourceProvider::MODRINTH, ModPlatform::ResourceProvider::FLAME }) { btn = new QRadioButton(ModPlatform::ProviderCapabilities::readableName(provider), this); m_providers.addButton(btn, btn_index++); ui->providersLayout->addWidget(btn); } } void ChooseProviderDialog::disableInput() { for (auto& btn : m_providers.buttons()) btn->setEnabled(false); ui->skipOneButton->setEnabled(false); ui->skipAllButton->setEnabled(false); ui->confirmOneButton->setEnabled(false); ui->confirmAllButton->setEnabled(false); } PrismLauncher-10.0.5/launcher/ui/dialogs/ProfileSelectDialog.cpp0000644000175100017510000000665615144136756024234 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ProfileSelectDialog.h" #include "ui_ProfileSelectDialog.h" #include #include #include #include "Application.h" #include "ui/dialogs/ProgressDialog.h" ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWidget* parent) : QDialog(parent), ui(new Ui::ProfileSelectDialog) { ui->setupUi(this); m_accounts = APPLICATION->accounts(); auto view = ui->listView; // view->setModel(m_accounts.get()); // view->hideColumn(AccountList::ActiveColumn); view->setColumnCount(1); view->setRootIsDecorated(false); // FIXME: use a real model, not this if (QTreeWidgetItem* header = view->headerItem()) { header->setText(0, tr("Name")); } else { view->setHeaderLabel(tr("Name")); } QList items; for (int i = 0; i < m_accounts->count(); i++) { MinecraftAccountPtr account = m_accounts->at(i); QString profileLabel; if (account->isInUse()) { profileLabel = tr("%1 (in use)").arg(account->profileName()); } else { profileLabel = account->profileName(); } auto item = new QTreeWidgetItem(view); item->setText(0, profileLabel); item->setIcon(0, account->getFace()); item->setData(0, AccountList::PointerRole, QVariant::fromValue(account)); items.append(item); } view->addTopLevelItems(items); // Set the message label. ui->msgLabel->setVisible(!message.isEmpty()); ui->msgLabel->setText(message); // Flags... ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox); ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox); qDebug() << flags; // Select the first entry in the list. ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); connect(ui->listView, &QAbstractItemView::doubleClicked, this, &ProfileSelectDialog::on_buttonBox_accepted); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ProfileSelectDialog::~ProfileSelectDialog() { delete ui; } MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const { return m_selected; } bool ProfileSelectDialog::useAsGlobalDefault() const { return ui->globalDefaultCheck->isChecked(); } bool ProfileSelectDialog::useAsInstDefaullt() const { return ui->instDefaultCheck->isChecked(); } void ProfileSelectDialog::on_buttonBox_accepted() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); m_selected = selected.data(AccountList::PointerRole).value(); } close(); } void ProfileSelectDialog::on_buttonBox_rejected() { close(); } PrismLauncher-10.0.5/launcher/ui/dialogs/CopyInstanceDialog.cpp0000644000175100017510000002637115144136756024067 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include "Application.h" #include "BuildConfig.h" #include "CopyInstanceDialog.h" #include "ui_CopyInstanceDialog.h" #include "ui/dialogs/IconPickerDialog.h" #include "BaseInstance.h" #include "BaseVersion.h" #include "DesktopServices.h" #include "FileSystem.h" #include "InstanceList.h" #include "icons/IconList.h" CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) : QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) { ui->setupUi(this); resize(minimumSizeHint()); layout()->setSizeConstraint(QLayout::SetFixedSize); InstIconKey = original->iconKey(); ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); ui->instNameTextBox->setText(original->name()); ui->instNameTextBox->setFocus(); QStringList groups = APPLICATION->instances()->getGroups(); groups.prepend(""); ui->groupBox->addItems(groups); int index = groups.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id())); if (index == -1) index = 0; ui->groupBox->setCurrentIndex(index); ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); ui->copySavesCheckbox->setChecked(m_selectedOptions.isCopySavesEnabled()); ui->keepPlaytimeCheckbox->setChecked(m_selectedOptions.isKeepPlaytimeEnabled()); ui->copyGameOptionsCheckbox->setChecked(m_selectedOptions.isCopyGameOptionsEnabled()); ui->copyResPacksCheckbox->setChecked(m_selectedOptions.isCopyResourcePacksEnabled()); ui->copyShaderPacksCheckbox->setChecked(m_selectedOptions.isCopyShaderPacksEnabled()); ui->copyServersCheckbox->setChecked(m_selectedOptions.isCopyServersEnabled()); ui->copyModsCheckbox->setChecked(m_selectedOptions.isCopyModsEnabled()); ui->copyScreenshotsCheckbox->setChecked(m_selectedOptions.isCopyScreenshotsEnabled()); ui->symbolicLinksCheckbox->setChecked(m_selectedOptions.isUseSymLinksEnabled()); ui->hardLinksCheckbox->setChecked(m_selectedOptions.isUseHardLinksEnabled()); ui->recursiveLinkCheckbox->setChecked(m_selectedOptions.isLinkRecursivelyEnabled()); ui->dontLinkSavesCheckbox->setChecked(m_selectedOptions.isDontLinkSavesEnabled()); auto detectedFS = FS::statFS(m_original->instanceRoot()).fsType; m_cloneSupported = FS::canCloneOnFS(detectedFS); m_linkSupported = FS::canLinkOnFS(detectedFS); if (m_cloneSupported) { ui->cloneSupportedLabel->setText(tr("Reflinks are supported on %1").arg(FS::getFilesystemTypeName(detectedFS))); } else { ui->cloneSupportedLabel->setText(tr("Reflinks aren't supported on %1").arg(FS::getFilesystemTypeName(detectedFS))); } #if defined(Q_OS_WIN) ui->symbolicLinksCheckbox->setIcon(style()->standardIcon(QStyle::SP_VistaShield)); ui->symbolicLinksCheckbox->setToolTip(tr("Use symbolic links instead of copying files.") + "\n" + tr("On Windows, symbolic links may require admin permission to create.")); #endif updateLinkOptions(); updateUseCloneCheckbox(); auto HelpButton = ui->buttonBox->button(QDialogButtonBox::Help); connect(HelpButton, &QPushButton::clicked, this, &CopyInstanceDialog::help); HelpButton->setText(tr("Help")); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } CopyInstanceDialog::~CopyInstanceDialog() { delete ui; } void CopyInstanceDialog::updateDialogState() { auto allowOK = !instName().isEmpty(); auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); if (OkButton->isEnabled() != allowOK) { OkButton->setEnabled(allowOK); } } QString CopyInstanceDialog::instName() const { auto result = ui->instNameTextBox->text().trimmed(); if (result.size()) { return result; } return QString(); } QString CopyInstanceDialog::iconKey() const { return InstIconKey; } QString CopyInstanceDialog::instGroup() const { return ui->groupBox->currentText(); } const InstanceCopyPrefs& CopyInstanceDialog::getChosenOptions() const { return m_selectedOptions; } void CopyInstanceDialog::help() { DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("instance-copy"))); } void CopyInstanceDialog::checkAllCheckboxes(const bool& b) { ui->keepPlaytimeCheckbox->setChecked(b); ui->copySavesCheckbox->setChecked(b); ui->copyGameOptionsCheckbox->setChecked(b); ui->copyResPacksCheckbox->setChecked(b); ui->copyShaderPacksCheckbox->setChecked(b); ui->copyServersCheckbox->setChecked(b); ui->copyModsCheckbox->setChecked(b); ui->copyScreenshotsCheckbox->setChecked(b); } // Check the "Select all" checkbox if all options are already selected: void CopyInstanceDialog::updateSelectAllCheckbox() { ui->selectAllCheckbox->blockSignals(true); ui->selectAllCheckbox->setChecked(m_selectedOptions.allTrue()); ui->selectAllCheckbox->blockSignals(false); } void CopyInstanceDialog::updateUseCloneCheckbox() { ui->useCloneCheckbox->setEnabled(m_cloneSupported && !ui->symbolicLinksCheckbox->isChecked() && !ui->hardLinksCheckbox->isChecked()); ui->useCloneCheckbox->setChecked(m_cloneSupported && m_selectedOptions.isUseCloneEnabled() && !ui->symbolicLinksCheckbox->isChecked() && !ui->hardLinksCheckbox->isChecked()); } void CopyInstanceDialog::updateLinkOptions() { ui->symbolicLinksCheckbox->setEnabled(m_linkSupported && !ui->hardLinksCheckbox->isChecked() && !ui->useCloneCheckbox->isChecked()); ui->hardLinksCheckbox->setEnabled(m_linkSupported && !ui->symbolicLinksCheckbox->isChecked() && !ui->useCloneCheckbox->isChecked()); ui->symbolicLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseSymLinksEnabled() && !ui->useCloneCheckbox->isChecked()); ui->hardLinksCheckbox->setChecked(m_linkSupported && m_selectedOptions.isUseHardLinksEnabled() && !ui->useCloneCheckbox->isChecked()); bool linksInUse = (ui->symbolicLinksCheckbox->isChecked() || ui->hardLinksCheckbox->isChecked()); ui->recursiveLinkCheckbox->setEnabled(m_linkSupported && linksInUse && !ui->hardLinksCheckbox->isChecked()); ui->dontLinkSavesCheckbox->setEnabled(m_linkSupported && linksInUse); ui->recursiveLinkCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isLinkRecursivelyEnabled()); ui->dontLinkSavesCheckbox->setChecked(m_linkSupported && linksInUse && m_selectedOptions.isDontLinkSavesEnabled()); #if defined(Q_OS_WIN) auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); OkButton->setIcon(m_selectedOptions.isUseSymLinksEnabled() ? style()->standardIcon(QStyle::SP_VistaShield) : QIcon()); #endif } void CopyInstanceDialog::on_iconButton_clicked() { IconPickerDialog dlg(this); dlg.execWithSelection(InstIconKey); if (dlg.result() == QDialog::Accepted) { InstIconKey = dlg.selectedIconKey; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); } } void CopyInstanceDialog::on_instNameTextBox_textChanged([[maybe_unused]] const QString& arg1) { updateDialogState(); } void CopyInstanceDialog::on_selectAllCheckbox_stateChanged(int state) { bool checked; checked = (state == Qt::Checked); checkAllCheckboxes(checked); } void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) { m_selectedOptions.enableCopySaves(state == Qt::Checked); ui->dontLinkSavesCheckbox->setChecked((state == Qt::Checked) && ui->dontLinkSavesCheckbox->isChecked()); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) { m_selectedOptions.enableKeepPlaytime(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyGameOptionsCheckbox_stateChanged(int state) { m_selectedOptions.enableCopyGameOptions(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyResPacksCheckbox_stateChanged(int state) { m_selectedOptions.enableCopyResourcePacks(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyShaderPacksCheckbox_stateChanged(int state) { m_selectedOptions.enableCopyShaderPacks(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyServersCheckbox_stateChanged(int state) { m_selectedOptions.enableCopyServers(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyModsCheckbox_stateChanged(int state) { m_selectedOptions.enableCopyMods(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_copyScreenshotsCheckbox_stateChanged(int state) { m_selectedOptions.enableCopyScreenshots(state == Qt::Checked); updateSelectAllCheckbox(); } void CopyInstanceDialog::on_symbolicLinksCheckbox_stateChanged(int state) { m_selectedOptions.enableUseSymLinks(state == Qt::Checked); updateUseCloneCheckbox(); updateLinkOptions(); } void CopyInstanceDialog::on_hardLinksCheckbox_stateChanged(int state) { m_selectedOptions.enableUseHardLinks(state == Qt::Checked); if (state == Qt::Checked && !ui->recursiveLinkCheckbox->isChecked()) { ui->recursiveLinkCheckbox->setChecked(true); } updateUseCloneCheckbox(); updateLinkOptions(); } void CopyInstanceDialog::on_recursiveLinkCheckbox_stateChanged(int state) { m_selectedOptions.enableLinkRecursively(state == Qt::Checked); updateLinkOptions(); } void CopyInstanceDialog::on_dontLinkSavesCheckbox_stateChanged(int state) { m_selectedOptions.enableDontLinkSaves(state == Qt::Checked); } void CopyInstanceDialog::on_useCloneCheckbox_stateChanged(int state) { m_selectedOptions.enableUseClone(m_cloneSupported && (state == Qt::Checked)); updateUseCloneCheckbox(); updateLinkOptions(); } PrismLauncher-10.0.5/launcher/ui/dialogs/MSALoginDialog.cpp0000644000175100017510000002104215144136756023067 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MSALoginDialog.h" #include "Application.h" #include "ui_MSALoginDialog.h" #include "DesktopServices.h" #include "minecraft/auth/AuthFlow.h" #include #include #include #include #include #include #include #include #include "qrencode.h" MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog) { ui->setupUi(this); // make font monospace QFont font; font.setPixelSize(ui->code->fontInfo().pixelSize()); font.setFamily(APPLICATION->settings()->get("ConsoleFont").toString()); font.setStyleHint(QFont::Monospace); font.setFixedPitch(true); ui->code->setFont(font); connect(ui->copyCode, &QPushButton::clicked, this, [this] { QApplication::clipboard()->setText(ui->code->text()); }); connect(ui->loginButton, &QPushButton::clicked, this, [this] { if (m_url.isValid()) { if (!DesktopServices::openUrl(m_url)) { QApplication::clipboard()->setText(m_url.toString()); } } }); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); } int MSALoginDialog::exec() { // Setup the login task and start it m_account = MinecraftAccount::createBlankMSA(); m_authflow_task = m_account->login(false); connect(m_authflow_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_authflow_task.get(), &Task::succeeded, this, &QDialog::accept); connect(m_authflow_task.get(), &Task::aborted, this, &MSALoginDialog::reject); connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onAuthFlowStatus); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort); m_devicecode_task.reset(new AuthFlow(m_account->accountData(), AuthFlow::Action::DeviceCode)); connect(m_devicecode_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_devicecode_task.get(), &Task::succeeded, this, &QDialog::accept); connect(m_devicecode_task.get(), &Task::aborted, this, &MSALoginDialog::reject); connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onDeviceFlowStatus); connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort); QMetaObject::invokeMethod(m_authflow_task.get(), &Task::start, Qt::QueuedConnection); QMetaObject::invokeMethod(m_devicecode_task.get(), &Task::start, Qt::QueuedConnection); return QDialog::exec(); } MSALoginDialog::~MSALoginDialog() { delete ui; } void MSALoginDialog::onTaskFailed(QString reason) { // Set message m_authflow_task->disconnect(); m_devicecode_task->disconnect(); ui->stackedWidget->setCurrentIndex(0); auto lines = reason.split('\n'); QString processed; for (auto line : lines) { if (line.size()) { processed += "" + line + "
    "; } else { processed += "
    "; } } ui->status->setText(processed); auto task = m_authflow_task; if (task->failReason().isEmpty()) { task = m_devicecode_task; } if (task) { ui->loadingLabel->setText(task->getStatus()); } disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort); disconnect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &MSALoginDialog::reject); } void MSALoginDialog::authorizeWithBrowser(const QUrl& url) { ui->stackedWidget2->setCurrentIndex(1); ui->stackedWidget2->adjustSize(); ui->stackedWidget2->updateGeometry(); this->adjustSize(); ui->loginButton->setToolTip(QString("
    %1
    ").arg(url.toString())); m_url = url; } void paintQR(QPainter& painter, const QSize canvasSize, const QString& data, QColor fg) { const auto* qr = QRcode_encodeString(data.toUtf8().constData(), 0, QRecLevel::QR_ECLEVEL_M, QRencodeMode::QR_MODE_8, 1); if (!qr) { qWarning() << "Unable to encode" << data << "as QR code"; return; } painter.setPen(Qt::NoPen); painter.setBrush(fg); // Make sure the QR code fits in the canvas with some padding const auto qrSize = qr->width; const auto canvasWidth = canvasSize.width(); const auto canvasHeight = canvasSize.height(); const auto scale = 0.8 * std::min(canvasWidth / qrSize, canvasHeight / qrSize); // Find an offset to center it in the canvas const auto offsetX = (canvasWidth - qrSize * scale) / 2; const auto offsetY = (canvasHeight - qrSize * scale) / 2; for (int y = 0; y < qrSize; y++) { for (int x = 0; x < qrSize; x++) { auto shouldFillIn = qr->data[y * qrSize + x] & 1; if (shouldFillIn) { QRectF r(offsetX + x * scale, offsetY + y * scale, scale, scale); painter.drawRects(&r, 1); } } } } void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) { ui->stackedWidget->setCurrentIndex(1); ui->stackedWidget->adjustSize(); ui->stackedWidget->updateGeometry(); this->adjustSize(); const auto linkString = QString("%2").arg(url, url); if (url == "https://www.microsoft.com/link" && !code.isEmpty()) { url += QString("?otc=%1").arg(code); } ui->code->setText(code); auto size = QSize(150, 150); QPixmap pixmap(size); pixmap.fill(Qt::white); QPainter painter(&pixmap); paintQR(painter, size, url, Qt::black); // Set the generated pixmap to the label ui->qr->setPixmap(pixmap); ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code if needed.").arg(linkString)); } void MSALoginDialog::onDeviceFlowStatus(QString status) { ui->stackedWidget->setCurrentIndex(0); ui->stackedWidget->adjustSize(); ui->stackedWidget->updateGeometry(); this->adjustSize(); ui->status->setText(status); } void MSALoginDialog::onAuthFlowStatus(QString status) { ui->stackedWidget2->setCurrentIndex(0); ui->stackedWidget2->adjustSize(); ui->stackedWidget2->updateGeometry(); this->adjustSize(); ui->status2->setText(status); } // Public interface MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent) { MSALoginDialog dlg(parent); if (dlg.exec() == QDialog::Accepted) { return dlg.m_account; } return nullptr; } PrismLauncher-10.0.5/launcher/ui/dialogs/IconPickerDialog.ui0000644000175100017510000000275215144136756023346 0ustar runnerrunner IconPickerDialog 0 0 676 555 Pick icon Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() IconPickerDialog accept() 248 254 157 274 buttonBox rejected() IconPickerDialog reject() 316 260 286 274 PrismLauncher-10.0.5/launcher/ui/dialogs/CreateShortcutDialog.cpp0000644000175100017510000002020615144136756024416 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2025 Yihe Li * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include "BuildConfig.h" #include "CreateShortcutDialog.h" #include "ui_CreateShortcutDialog.h" #include "ui/dialogs/IconPickerDialog.h" #include "BaseInstance.h" #include "DesktopServices.h" #include "FileSystem.h" #include "InstanceList.h" #include "icons/IconList.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/ShortcutUtils.h" #include "minecraft/WorldList.h" #include "minecraft/auth/AccountList.h" CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent) : QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance) { ui->setupUi(this); InstIconKey = instance->iconKey(); ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); ui->instNameTextBox->setPlaceholderText(instance->name()); auto mInst = std::dynamic_pointer_cast(instance); m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); auto worldList = mInst->worldList(); worldList->update(); if (!m_QuickJoinSupported || worldList->empty()) { ui->worldTarget->hide(); ui->worldSelectionBox->hide(); ui->serverTarget->setChecked(true); ui->serverTarget->hide(); ui->serverLabel->show(); } // Populate save targets if (!DesktopServices::isFlatpak()) { QString desktopDir = FS::getDesktopDir(); QString applicationDir = FS::getApplicationsDir(); if (!desktopDir.isEmpty()) ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(ShortcutTarget::Desktop)); if (!applicationDir.isEmpty()) ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(ShortcutTarget::Applications)); } ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(ShortcutTarget::Other)); // Populate worlds if (m_QuickJoinSupported) { for (const auto& world : worldList->allWorlds()) { // Entry name: World Name [Game Mode] - Last Played: DateTime QString entry_name = tr("%1 [%2] - Last Played: %3") .arg(world.name(), world.gameType().toTranslatedString(), world.lastPlayed().toString(Qt::ISODate)); ui->worldSelectionBox->addItem(entry_name, world.name()); } } // Populate accounts auto accounts = APPLICATION->accounts(); MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); if (accounts->count() <= 0) { ui->overrideAccountCheckbox->setEnabled(false); } else { for (int i = 0; i < accounts->count(); i++) { MinecraftAccountPtr account = accounts->at(i); auto profileLabel = account->profileName(); if (account->isInUse()) profileLabel = tr("%1 (in use)").arg(profileLabel); auto face = account->getFace(); QIcon icon = face.isNull() ? QIcon::fromTheme("noaccount") : face; ui->accountSelectionBox->addItem(profileLabel, account->profileName()); ui->accountSelectionBox->setItemIcon(i, icon); if (defaultAccount == account) ui->accountSelectionBox->setCurrentIndex(i); } } } CreateShortcutDialog::~CreateShortcutDialog() { delete ui; } void CreateShortcutDialog::on_iconButton_clicked() { IconPickerDialog dlg(this); dlg.execWithSelection(InstIconKey); if (dlg.result() == QDialog::Accepted) { InstIconKey = dlg.selectedIconKey; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); } } void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) { ui->accountOptionsGroup->setEnabled(state == Qt::Checked); } void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) { ui->targetOptionsGroup->setEnabled(state == Qt::Checked); ui->worldSelectionBox->setEnabled(ui->worldTarget->isChecked()); ui->serverAddressBox->setEnabled(ui->serverTarget->isChecked()); stateChanged(); } void CreateShortcutDialog::on_worldTarget_toggled(bool checked) { ui->worldSelectionBox->setEnabled(checked); stateChanged(); } void CreateShortcutDialog::on_serverTarget_toggled(bool checked) { ui->serverAddressBox->setEnabled(checked); stateChanged(); } void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) { stateChanged(); } void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& text) { stateChanged(); } void CreateShortcutDialog::stateChanged() { QString result = m_instance->name(); if (ui->targetCheckbox->isChecked()) { if (ui->worldTarget->isChecked()) result = tr("%1 - %2").arg(result, ui->worldSelectionBox->currentData().toString()); else if (ui->serverTarget->isChecked()) result = tr("%1 - Server %2").arg(result, ui->serverAddressBox->text()); } ui->instNameTextBox->setPlaceholderText(result); if (!ui->targetCheckbox->isChecked()) ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); else { ui->buttonBox->button(QDialogButtonBox::Ok) ->setEnabled((ui->worldTarget->isChecked() && ui->worldSelectionBox->currentIndex() != -1) || (ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty())); } } // Real work void CreateShortcutDialog::createShortcut() { QString targetString = tr("instance"); QStringList extraArgs; if (ui->targetCheckbox->isChecked()) { if (ui->worldTarget->isChecked()) { targetString = tr("world"); extraArgs = { "--world", ui->worldSelectionBox->currentData().toString() }; } else if (ui->serverTarget->isChecked()) { targetString = tr("server"); extraArgs = { "--server", ui->serverAddressBox->text() }; } } auto target = ui->saveTargetSelectionBox->currentData().value(); auto name = ui->instNameTextBox->text(); if (name.isEmpty()) name = ui->instNameTextBox->placeholderText(); if (ui->overrideAccountCheckbox->isChecked()) extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() }); ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey, target }; if (target == ShortcutTarget::Desktop) ShortcutUtils::createInstanceShortcutOnDesktop(args); else if (target == ShortcutTarget::Applications) ShortcutUtils::createInstanceShortcutInApplications(args); else ShortcutUtils::createInstanceShortcutInOther(args); } PrismLauncher-10.0.5/launcher/ui/dialogs/NewsDialog.cpp0000644000175100017510000000330715144136756022376 0ustar runnerrunner#include "NewsDialog.h" #include "ui_NewsDialog.h" NewsDialog::NewsDialog(QList entries, QWidget* parent) : QDialog(parent), ui(new Ui::NewsDialog()) { ui->setupUi(this); for (auto entry : entries) { ui->articleListWidget->addItem(entry->title); m_entries.insert(entry->title, entry); } connect(ui->articleListWidget, &QListWidget::currentTextChanged, this, &NewsDialog::selectedArticleChanged); connect(ui->toggleListButton, &QPushButton::clicked, this, &NewsDialog::toggleArticleList); m_article_list_hidden = ui->articleListWidget->isHidden(); auto first_item = ui->articleListWidget->item(0); first_item->setSelected(true); auto article_entry = m_entries.constFind(first_item->text()).value(); ui->articleTitleLabel->setText(QString("%2").arg(article_entry->link, first_item->text())); ui->currentArticleContentBrowser->setText(article_entry->content); ui->currentArticleContentBrowser->flush(); } NewsDialog::~NewsDialog() { delete ui; } void NewsDialog::selectedArticleChanged(const QString& new_title) { auto article_entry = m_entries.constFind(new_title).value(); ui->articleTitleLabel->setText(QString("%2").arg(article_entry->link, new_title)); ui->currentArticleContentBrowser->setText(article_entry->content); ui->currentArticleContentBrowser->flush(); } void NewsDialog::toggleArticleList() { m_article_list_hidden = !m_article_list_hidden; ui->articleListWidget->setHidden(m_article_list_hidden); if (m_article_list_hidden) ui->toggleListButton->setText(tr("Show article list")); else ui->toggleListButton->setText(tr("Hide article list")); } PrismLauncher-10.0.5/launcher/ui/dialogs/ChooseOfflineNameDialog.cpp0000644000175100017510000000516215144136756025007 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ChooseOfflineNameDialog.h" #include #include #include "ui_ChooseOfflineNameDialog.h" ChooseOfflineNameDialog::ChooseOfflineNameDialog(const QString& message, QWidget* parent) : QDialog(parent), ui(new Ui::ChooseOfflineNameDialog) { ui->setupUi(this); ui->label->setText(message); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); const QRegularExpression usernameRegExp("^[A-Za-z0-9_]{3,16}$"); m_usernameValidator = new QRegularExpressionValidator(usernameRegExp, this); ui->usernameTextBox->setValidator(m_usernameValidator); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } ChooseOfflineNameDialog::~ChooseOfflineNameDialog() { delete ui; } QString ChooseOfflineNameDialog::getUsername() const { return ui->usernameTextBox->text(); } void ChooseOfflineNameDialog::setUsername(const QString& username) const { ui->usernameTextBox->setText(username); updateAcceptAllowed(username); } void ChooseOfflineNameDialog::updateAcceptAllowed(const QString& username) const { const bool allowed = ui->allowInvalidUsernames->isChecked() ? !username.isEmpty() : ui->usernameTextBox->hasAcceptableInput(); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowed); } void ChooseOfflineNameDialog::on_usernameTextBox_textEdited(const QString& newText) const { updateAcceptAllowed(newText); } void ChooseOfflineNameDialog::on_allowInvalidUsernames_checkStateChanged(const Qt::CheckState checkState) const { ui->usernameTextBox->setValidator(checkState == Qt::Checked ? nullptr : m_usernameValidator); updateAcceptAllowed(getUsername()); } PrismLauncher-10.0.5/launcher/ui/dialogs/CopyInstanceDialog.h0000644000175100017510000000466315144136756023534 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "BaseInstance.h" #include "BaseVersion.h" #include "InstanceCopyPrefs.h" class BaseInstance; namespace Ui { class CopyInstanceDialog; } class CopyInstanceDialog : public QDialog { Q_OBJECT public: explicit CopyInstanceDialog(InstancePtr original, QWidget* parent = 0); ~CopyInstanceDialog(); void updateDialogState(); QString instName() const; QString instGroup() const; QString iconKey() const; const InstanceCopyPrefs& getChosenOptions() const; public slots: void help(); private slots: void on_iconButton_clicked(); void on_instNameTextBox_textChanged(const QString& arg1); // Checkboxes void on_selectAllCheckbox_stateChanged(int state); void on_copySavesCheckbox_stateChanged(int state); void on_keepPlaytimeCheckbox_stateChanged(int state); void on_copyGameOptionsCheckbox_stateChanged(int state); void on_copyResPacksCheckbox_stateChanged(int state); void on_copyShaderPacksCheckbox_stateChanged(int state); void on_copyServersCheckbox_stateChanged(int state); void on_copyModsCheckbox_stateChanged(int state); void on_copyScreenshotsCheckbox_stateChanged(int state); void on_symbolicLinksCheckbox_stateChanged(int state); void on_hardLinksCheckbox_stateChanged(int state); void on_recursiveLinkCheckbox_stateChanged(int state); void on_dontLinkSavesCheckbox_stateChanged(int state); void on_useCloneCheckbox_stateChanged(int state); private: void checkAllCheckboxes(const bool& b); void updateSelectAllCheckbox(); void updateUseCloneCheckbox(); void updateLinkOptions(); /* data */ Ui::CopyInstanceDialog* ui; QString InstIconKey; InstancePtr m_original; InstanceCopyPrefs m_selectedOptions; bool m_cloneSupported = false; bool m_linkSupported = false; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ExportToModListDialog.h0000644000175100017510000000320215144136756024201 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/Mod.h" #include "modplatform/helpers/ExportToModList.h" namespace Ui { class ExportToModListDialog; } class ExportToModListDialog : public QDialog { Q_OBJECT public: explicit ExportToModListDialog(QString name, QList mods, QWidget* parent = nullptr); ~ExportToModListDialog(); void done(int result) override; protected slots: void formatChanged(int index); void triggerImp(); void trigger(int) { triggerImp(); }; void addExtra(ExportToModList::OptionalData option); private: QString extension(); void enableCustom(bool enabled); QList m_mods; bool m_template_changed; QString m_name; ExportToModList::Formats m_format = ExportToModList::Formats::HTML; Ui::ExportToModListDialog* ui; static const QHash exampleLines; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ExportInstanceDialog.cpp0000644000175100017510000001570415144136756024434 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ExportInstanceDialog.h" #include #include #include #include #include #include "FileIgnoreProxy.h" #include "QObjectPtr.h" #include "archive/ExportToZipTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui_ExportInstanceDialog.h" #include #include #include #include #include #include #include #include #include #include "Application.h" #include "SeparatorPrefixTree.h" ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent) : QDialog(parent), m_ui(new Ui::ExportInstanceDialog), m_instance(instance) { m_ui->setupUi(this); auto model = new QFileSystemModel(this); model->setIconProvider(&m_icons); auto root = instance->instanceRoot(); m_proxyModel = new FileIgnoreProxy(root, this); m_proxyModel->setSourceModel(model); auto prefix = QDir(instance->instanceRoot()).relativeFilePath(instance->gameRoot()); for (auto path : { "logs", "crash-reports", ".cache", ".fabric", ".quilt" }) { m_proxyModel->ignoreFilesWithPath().insert(FS::PathCombine(prefix, path)); } m_proxyModel->ignoreFilesWithName().append({ ".DS_Store", "thumbs.db", "Thumbs.db" }); m_proxyModel->loadBlockedPathsFromFile(ignoreFileName()); m_ui->treeView->setModel(m_proxyModel); m_ui->treeView->setRootIndex(m_proxyModel->mapFromSource(model->index(root))); m_ui->treeView->sortByColumn(0, Qt::AscendingOrder); connect(m_proxyModel, &QAbstractItemModel::rowsInserted, this, &ExportInstanceDialog::rowsInserted); model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); model->setRootPath(root); auto headerView = m_ui->treeView->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } ExportInstanceDialog::~ExportInstanceDialog() { delete m_ui; } /// Save icon to instance's folder is needed void SaveIcon(InstancePtr m_instance) { auto iconKey = m_instance->iconKey(); auto iconList = APPLICATION->icons(); auto mmcIcon = iconList->icon(iconKey); if (!mmcIcon || mmcIcon->isBuiltIn()) { return; } auto path = mmcIcon->getFilePath(); if (!path.isNull()) { QFileInfo inInfo(path); FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName()))(); return; } auto& image = mmcIcon->m_images[mmcIcon->type()]; auto& icon = image.icon; auto sizes = icon.availableSizes(); if (sizes.size() == 0) { return; } auto areaOf = [](QSize size) { return size.width() * size.height(); }; QSize largest = sizes[0]; // find variant with largest area for (auto size : sizes) { if (areaOf(largest) < areaOf(size)) { largest = size; } } auto pixmap = icon.pixmap(largest); pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); } void ExportInstanceDialog::doExport() { auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); const QString output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(m_instance->name()), FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr); if (output.isEmpty()) { QDialog::done(QDialog::Rejected); return; } SaveIcon(m_instance); auto files = QFileInfoList(); if (!MMCZip::collectFileListRecursively(m_instance->instanceRoot(), nullptr, &files, std::bind(&FileIgnoreProxy::filterFile, m_proxyModel, std::placeholders::_1))) { QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); QDialog::done(QDialog::Rejected); return; } auto task = makeShared(output, m_instance->instanceRoot(), files, "", true); connect(task.get(), &Task::failed, this, [this, output](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(task.get(), &Task::finished, this, [task] { task->deleteLater(); }); ProgressDialog progress(this); progress.setSkipButton(true, tr("Abort")); auto result = progress.execWithTask(task.get()); QDialog::done(result); } void ExportInstanceDialog::done(int result) { m_proxyModel->saveBlockedPathsToFile(ignoreFileName()); if (result == QDialog::Accepted) { doExport(); return; } QDialog::done(result); } void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) { // WARNING: possible off-by-one? for (int i = top; i < bottom; i++) { auto node = m_proxyModel->index(i, 0, parent); if (m_proxyModel->shouldExpand(node)) { auto expNode = node.parent(); if (!expNode.isValid()) { continue; } m_ui->treeView->expand(node); } } } QString ExportInstanceDialog::ignoreFileName() { return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); } PrismLauncher-10.0.5/launcher/ui/dialogs/skins/0000755000175100017510000000000015144136756020762 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/dialogs/skins/SkinManageDialog.cpp0000644000175100017510000005116715144136756024635 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SkinManageDialog.h" #include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" #include "ui_SkinManageDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "Application.h" #include "DesktopServices.h" #include "Json.h" #include "QObjectPtr.h" #include "minecraft/auth/Parsers.h" #include "minecraft/skins/CapeChange.h" #include "minecraft/skins/SkinDelete.h" #include "minecraft/skins/SkinList.h" #include "minecraft/skins/SkinModel.h" #include "minecraft/skins/SkinUpload.h" #include "net/Download.h" #include "net/NetJob.h" #include "tasks/Task.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/InstanceDelegate.h" SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) : QDialog(parent), m_acct(acct), m_ui(new Ui::SkinManageDialog), m_list(this, APPLICATION->settings()->get("SkinsDir").toString(), acct) { m_ui->setupUi(this); if (SkinOpenGLWindow::hasOpenGL()) { m_skinPreview = new SkinOpenGLWindow(this, palette().color(QPalette::Normal, QPalette::Base)); } else { m_skinPreviewLabel = new QLabel(this); m_skinPreviewLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); } setWindowModality(Qt::WindowModal); auto contentsWidget = m_ui->listView; contentsWidget->setViewMode(QListView::IconMode); contentsWidget->setFlow(QListView::LeftToRight); contentsWidget->setIconSize(QSize(48, 48)); contentsWidget->setMovement(QListView::Static); contentsWidget->setResizeMode(QListView::Adjust); contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); contentsWidget->setSpacing(5); contentsWidget->setWordWrap(false); contentsWidget->setWrapping(true); contentsWidget->setUniformItemSizes(true); contentsWidget->setTextElideMode(Qt::ElideRight); contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); contentsWidget->installEventFilter(this); contentsWidget->setItemDelegate(new ListViewDelegate(this)); contentsWidget->setAcceptDrops(true); contentsWidget->setDropIndicatorShown(true); contentsWidget->viewport()->setAcceptDrops(true); contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); contentsWidget->setDefaultDropAction(Qt::CopyAction); contentsWidget->installEventFilter(this); contentsWidget->setModel(&m_list); connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &SkinManageDialog::activated); connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SkinManageDialog::selectionChanged); connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); connect(m_ui->elytraCB, &QCheckBox::stateChanged, this, [this]() { if (m_skinPreview) { m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked()); } on_capeCombo_currentIndexChanged(0); }); setupCapes(); m_ui->listView->setCurrentIndex(m_list.index(m_list.getSelectedAccountSkin())); m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); if (m_skinPreview) { m_ui->skinLayout->insertWidget(0, QWidget::createWindowContainer(m_skinPreview, this)); } else { m_ui->skinLayout->insertWidget(0, m_skinPreviewLabel); } } SkinManageDialog::~SkinManageDialog() { delete m_ui; if (m_skinPreview) { delete m_skinPreview; } } void SkinManageDialog::activated(QModelIndex index) { m_selectedSkinKey = index.data(Qt::UserRole).toString(); accept(); } void SkinManageDialog::selectionChanged(QItemSelection selected, [[maybe_unused]] QItemSelection deselected) { if (selected.empty()) return; QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); if (key.isEmpty()) return; m_selectedSkinKey = key; auto skin = getSelectedSkin(); if (!skin) return; if (m_skinPreview) { m_skinPreview->updateScene(skin); } else { m_skinPreviewLabel->setPixmap( QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } m_ui->capeCombo->setCurrentIndex(m_capesIdx.value(skin->getCapeId())); m_ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); m_ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); } void SkinManageDialog::delayed_scroll(QModelIndex model_index) { auto contentsWidget = m_ui->listView; contentsWidget->scrollTo(model_index); } void SkinManageDialog::on_openDirBtn_clicked() { DesktopServices::openPath(m_list.getDir(), true); } void SkinManageDialog::on_fileBtn_clicked() { auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); if (raw_path.isNull()) { return; } auto message = m_list.installSkin(raw_path, {}); if (!message.isEmpty()) { CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical)->show(); return; } } QPixmap previewCape(QImage capeImage, bool elytra = false) { if (elytra) { auto wing = capeImage.copy(34, 2, 12, 20); QImage mirrored = wing.mirrored(true, false); QImage combined(wing.width() * 2 + 1, wing.height() + 14, capeImage.format()); combined.fill(Qt::transparent); QPainter painter(&combined); painter.drawImage(0, 7, wing); painter.drawImage(wing.width() + 1, 7, mirrored); painter.end(); return QPixmap::fromImage(combined.scaled(84, 128, Qt::KeepAspectRatio, Qt::FastTransformation)); } return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); } void SkinManageDialog::setupCapes() { // FIXME: add a model for this, download/refresh the capes on demand auto& accountData = *m_acct->accountData(); int index = 0; m_ui->capeCombo->addItem(tr("No Cape"), QVariant()); auto currentCape = accountData.minecraftProfile.currentCape; if (currentCape.isEmpty()) { m_ui->capeCombo->setCurrentIndex(index); } auto capesDir = FS::PathCombine(m_list.getDir(), "capes"); NetJob::Ptr job{ new NetJob(tr("Download capes"), APPLICATION->network()) }; bool needsToDownload = false; for (auto& cape : accountData.minecraftProfile.capes) { auto path = FS::PathCombine(capesDir, cape.id + ".png"); if (cape.data.size()) { QImage capeImage; if (capeImage.loadFromData(cape.data, "PNG") && capeImage.save(path)) { m_capes[cape.id] = capeImage; continue; } } if (QFileInfo(path).exists()) { continue; } if (!cape.url.isEmpty()) { needsToDownload = true; job->addNetAction(Net::Download::makeFile(cape.url, path)); } } if (needsToDownload) { ProgressDialog dlg(this); dlg.execWithTask(job.get()); } for (auto& cape : accountData.minecraftProfile.capes) { index++; QImage capeImage; if (!m_capes.contains(cape.id)) { auto path = FS::PathCombine(capesDir, cape.id + ".png"); if (QFileInfo(path).exists() && capeImage.load(path)) { m_capes[cape.id] = capeImage; } } if (!capeImage.isNull()) { m_ui->capeCombo->addItem(previewCape(capeImage, m_ui->elytraCB->isChecked()), cape.alias, cape.id); } else { m_ui->capeCombo->addItem(cape.alias, cape.id); } m_capesIdx[cape.id] = index; } } void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) { auto id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { m_ui->capeImage->setPixmap( previewCape(cape, m_ui->elytraCB->isChecked()).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); } else { m_ui->capeImage->clear(); } if (m_skinPreview) { m_skinPreview->updateCape(cape); } if (auto skin = getSelectedSkin(); skin) { skin->setCapeId(id.toString()); if (m_skinPreview) { m_skinPreview->updateScene(skin); } else { m_skinPreviewLabel->setPixmap( QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } } } void SkinManageDialog::on_steveBtn_toggled(bool checked) { if (auto skin = getSelectedSkin(); skin) { skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM); if (m_skinPreview) { m_skinPreview->updateScene(skin); } else { m_skinPreviewLabel->setPixmap( QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } } } void SkinManageDialog::accept() { auto skin = m_list.skin(m_selectedSkinKey); if (!skin) { reject(); return; } auto path = skin->getPath(); ProgressDialog prog(this); NetJob::Ptr skinUpload{ new NetJob(tr("Change skin"), APPLICATION->network(), 1) }; if (!QFile::exists(path)) { CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); reject(); return; } skinUpload->addNetAction(SkinUpload::make(m_acct->accessToken(), skin->getPath(), skin->getModelString())); auto selectedCape = skin->getCapeId(); if (selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { skinUpload->addNetAction(CapeChange::make(m_acct->accessToken(), selectedCape)); } skinUpload->addTask(m_acct->refresh().staticCast()); if (prog.execWithTask(skinUpload.get()) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); reject(); return; } skin->setURL(m_acct->accountData()->minecraftProfile.skin.url); QDialog::accept(); } void SkinManageDialog::on_resetBtn_clicked() { ProgressDialog prog(this); NetJob::Ptr skinReset{ new NetJob(tr("Reset skin"), APPLICATION->network(), 1) }; skinReset->addNetAction(SkinDelete::make(m_acct->accessToken())); skinReset->addTask(m_acct->refresh().staticCast()); if (prog.execWithTask(skinReset.get()) != QDialog::Accepted) { CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); reject(); return; } QDialog::accept(); } void SkinManageDialog::show_context_menu(const QPoint& pos) { QMenu myMenu(tr("Context menu"), this); myMenu.addAction(m_ui->action_Rename_Skin); myMenu.addAction(m_ui->action_Delete_Skin); myMenu.exec(m_ui->listView->mapToGlobal(pos)); } bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) { if (obj == m_ui->listView) { if (ev->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(ev); switch (keyEvent->key()) { case Qt::Key_Delete: on_action_Delete_Skin_triggered(false); return true; case Qt::Key_F2: on_action_Rename_Skin_triggered(false); return true; default: break; } } } return QDialog::eventFilter(obj, ev); } void SkinManageDialog::on_action_Rename_Skin_triggered(bool) { if (!m_selectedSkinKey.isEmpty()) { m_ui->listView->edit(m_ui->listView->currentIndex()); } } void SkinManageDialog::on_action_Delete_Skin_triggered(bool) { if (m_selectedSkinKey.isEmpty()) return; if (m_list.getSkinIndex(m_selectedSkinKey) == m_list.getSelectedAccountSkin()) { CustomMessageBox::selectable(this, tr("Delete error"), tr("Can not delete skin that is in use."), QMessageBox::Warning)->exec(); return; } auto skin = m_list.skin(m_selectedSkinKey); if (!skin) return; auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), tr("You are about to delete \"%1\".\n" "Are you sure?") .arg(skin->name()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response == QMessageBox::Yes) { if (!m_list.deleteSkin(m_selectedSkinKey, true)) { m_list.deleteSkin(m_selectedSkinKey, false); } } } void SkinManageDialog::on_urlBtn_clicked() { auto url = QUrl(m_ui->urlLine->text()); if (!url.isValid()) { CustomMessageBox::selectable(this, tr("Invalid url"), tr("Invalid url"), QMessageBox::Critical)->show(); return; } NetJob::Ptr job{ new NetJob(tr("Download skin"), APPLICATION->network()) }; job->setAskRetry(false); auto path = FS::PathCombine(m_list.getDir(), url.fileName()); job->addNetAction(Net::Download::makeFile(url, path)); ProgressDialog dlg(this); dlg.execWithTask(job.get()); SkinModel s(path); if (!s.isValid()) { CustomMessageBox::selectable(this, tr("URL is not a valid skin"), QFileInfo::exists(path) ? tr("Skin images must be 64x64 or 64x32 pixel PNG files.") : tr("Unable to download the skin: '%1'.").arg(m_ui->urlLine->text()), QMessageBox::Critical) ->show(); QFile::remove(path); return; } m_ui->urlLine->setText(""); if (QFileInfo(path).suffix().isEmpty()) { QFile::rename(path, path + ".png"); } } class WaitTask : public Task { public: WaitTask() : m_loop(), m_done(false) {}; virtual ~WaitTask() = default; public slots: void quit() { m_done = true; m_loop.quit(); } protected: virtual void executeTask() { if (!m_done) m_loop.exec(); emitSucceeded(); }; private: QEventLoop m_loop; bool m_done; }; void SkinManageDialog::on_userBtn_clicked() { auto user = m_ui->urlLine->text(); if (user.isEmpty()) { return; } MinecraftProfile mcProfile; auto path = FS::PathCombine(m_list.getDir(), user + ".png"); NetJob::Ptr job{ new NetJob(tr("Download user skin"), APPLICATION->network(), 1) }; job->setAskRetry(false); auto uuidOut = std::make_shared(); auto profileOut = std::make_shared(); auto uuidLoop = makeShared(); auto profileLoop = makeShared(); auto getUUID = Net::Download::makeByteArray("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + user, uuidOut); auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut); auto downloadSkin = Net::Download::makeFile(QUrl(), path); QString failReason; connect(getUUID.get(), &Task::aborted, uuidLoop.get(), &WaitTask::quit); connect(getUUID.get(), &Task::failed, this, [&failReason](QString reason) { qCritical() << "Couldn't get user UUID:" << reason; failReason = tr("failed to get user UUID"); }); connect(getUUID.get(), &Task::failed, uuidLoop.get(), &WaitTask::quit); connect(getProfile.get(), &Task::aborted, profileLoop.get(), &WaitTask::quit); connect(getProfile.get(), &Task::failed, profileLoop.get(), &WaitTask::quit); connect(getProfile.get(), &Task::failed, this, [&failReason](QString reason) { qCritical() << "Couldn't get user profile:" << reason; failReason = tr("failed to get user profile"); }); connect(downloadSkin.get(), &Task::failed, this, [&failReason](QString reason) { qCritical() << "Couldn't download skin:" << reason; failReason = tr("failed to download skin"); }); connect(getUUID.get(), &Task::succeeded, this, [uuidLoop, uuidOut, job, getProfile, &failReason] { try { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*uuidOut, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Minecraft skin service at" << parse_error.offset << "reason:" << parse_error.errorString(); failReason = tr("failed to parse get user UUID response"); uuidLoop->quit(); return; } const auto root = doc.object(); auto id = root["id"].toString(); if (!id.isEmpty()) { getProfile->setUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + id); } else { failReason = tr("user id is empty"); job->abort(); } } catch (const Exception& e) { qCritical() << "Couldn't load skin json:" << e.cause(); failReason = tr("failed to parse get user UUID response"); } uuidLoop->quit(); }); connect(getProfile.get(), &Task::succeeded, this, [profileLoop, profileOut, job, getProfile, &mcProfile, downloadSkin, &failReason] { if (Parsers::parseMinecraftProfileMojang(*profileOut, mcProfile)) { downloadSkin->setUrl(mcProfile.skin.url); } else { failReason = tr("failed to parse get user profile response"); job->abort(); } profileLoop->quit(); }); job->addNetAction(getUUID); job->addTask(uuidLoop); job->addNetAction(getProfile); job->addTask(profileLoop); job->addNetAction(downloadSkin); ProgressDialog dlg(this); dlg.execWithTask(job.get()); SkinModel s(path); if (!s.isValid()) { if (failReason.isEmpty()) { failReason = tr("the skin is invalid"); } CustomMessageBox::selectable(this, tr("Username not found"), tr("Unable to find the skin for '%1'\n because: %2.").arg(user, failReason), QMessageBox::Critical) ->show(); QFile::remove(path); return; } m_ui->urlLine->setText(""); s.setModel(mcProfile.skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setURL(mcProfile.skin.url); if (m_capes.contains(mcProfile.currentCape)) { s.setCapeId(mcProfile.currentCape); } m_list.updateSkin(&s); } void SkinManageDialog::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); QSize s = size() * (1. / 3); auto id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { m_ui->capeImage->setPixmap(previewCape(cape, m_ui->elytraCB->isChecked()).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); } else { m_ui->capeImage->clear(); } if (auto skin = getSelectedSkin(); skin && !m_skinPreview) { m_skinPreviewLabel->setPixmap( QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); } } SkinModel* SkinManageDialog::getSelectedSkin() { if (auto skin = m_list.skin(m_selectedSkinKey); skin && skin->isValid()) { return skin; } return nullptr; } QHash SkinManageDialog::capes() { return m_capes; } PrismLauncher-10.0.5/launcher/ui/dialogs/skins/SkinManageDialog.h0000644000175100017510000000453015144136756024272 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "minecraft/auth/MinecraftAccount.h" #include "minecraft/skins/SkinList.h" #include "minecraft/skins/SkinModel.h" #include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" namespace Ui { class SkinManageDialog; } class SkinManageDialog : public QDialog, public SkinProvider { Q_OBJECT public: explicit SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct); virtual ~SkinManageDialog(); void resizeEvent(QResizeEvent* event) override; virtual SkinModel* getSelectedSkin() override; virtual QHash capes() override; public slots: void selectionChanged(QItemSelection, QItemSelection); void activated(QModelIndex); void delayed_scroll(QModelIndex); void on_openDirBtn_clicked(); void on_fileBtn_clicked(); void on_urlBtn_clicked(); void on_userBtn_clicked(); void accept() override; void on_capeCombo_currentIndexChanged(int index); void on_steveBtn_toggled(bool checked); void on_resetBtn_clicked(); void show_context_menu(const QPoint& pos); bool eventFilter(QObject* obj, QEvent* ev) override; void on_action_Rename_Skin_triggered(bool checked); void on_action_Delete_Skin_triggered(bool checked); private: void setupCapes(); private: MinecraftAccountPtr m_acct; Ui::SkinManageDialog* m_ui; SkinList m_list; QString m_selectedSkinKey; QHash m_capes; QHash m_capesIdx; SkinOpenGLWindow* m_skinPreview = nullptr; QLabel* m_skinPreviewLabel = nullptr; }; PrismLauncher-10.0.5/launcher/ui/dialogs/skins/SkinManageDialog.ui0000644000175100017510000001353115144136756024461 0ustar runnerrunner SkinManageDialog 0 0 968 757 Skin Upload 0 0 Model Classic true Slim Cape Preview Elytra false Qt::AlignCenter Qt::CustomContextMenu false 0 Open Folder Reset Skin Import URL Import user Import File 0 0 QDialogButtonBox::Cancel|QDialogButtonBox::Ok &Delete Skin Deletes selected skin Del &Rename Skin Rename selected skin F2 buttonBox rejected() SkinManageDialog reject() 617 736 483 378 buttonBox accepted() SkinManageDialog accept() 617 736 483 378 PrismLauncher-10.0.5/launcher/ui/dialogs/skins/draw/0000755000175100017510000000000015144136756021717 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/dialogs/skins/draw/Scene.h0000644000175100017510000000335715144136756023135 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "ui/dialogs/skins/draw/BoxGeometry.h" #include namespace opengl { class Scene : protected QOpenGLFunctions { public: Scene(const QImage& skin, bool slim, const QImage& cape); virtual ~Scene(); void draw(QOpenGLShaderProgram* program); void setSkin(const QImage& skin); void setCape(const QImage& cape); void setMode(bool slim); void setCapeVisible(bool visible); void setElytraVisible(bool elytraVisible); private: QList m_staticComponents; QList m_normalArms; QList m_slimArms; QList m_staticComponentsOverlay; QList m_normalArmsOverlay; QList m_slimArmsOverlay; BoxGeometry* m_cape = nullptr; QList m_elytra; QOpenGLTexture* m_skinTexture = nullptr; QOpenGLTexture* m_capeTexture = nullptr; bool m_slim = false; bool m_capeVisible = false; bool m_elytraVisible = false; }; } // namespace opengl PrismLauncher-10.0.5/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h0000644000175100017510000000502315144136756025231 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include #include "minecraft/skins/SkinModel.h" #include "ui/dialogs/skins/draw/BoxGeometry.h" #include "ui/dialogs/skins/draw/Scene.h" class SkinProvider { public: virtual ~SkinProvider() = default; virtual SkinModel* getSelectedSkin() = 0; virtual QHash capes() = 0; }; class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions { Q_OBJECT public: SkinOpenGLWindow(SkinProvider* parent, QColor color); virtual ~SkinOpenGLWindow(); void updateScene(SkinModel* skin); void updateCape(const QImage& cape); void setElytraVisible(bool visible); static bool hasOpenGL(); protected: void mousePressEvent(QMouseEvent* e) override; void mouseReleaseEvent(QMouseEvent* e) override; void mouseMoveEvent(QMouseEvent* event) override; void wheelEvent(QWheelEvent* event) override; void initializeGL() override; void resizeGL(int w, int h) override; void paintGL() override; void initShaders(); void generateBackgroundTexture(int width, int height, int tileSize); void renderBackground(); private: QOpenGLShaderProgram* m_modelProgram; QOpenGLShaderProgram* m_backgroundProgram; opengl::Scene* m_scene = nullptr; QMatrix4x4 m_projection; QVector2D m_mousePosition; bool m_isMousePressed = false; float m_distance = 48; float m_yaw = 90; // Horizontal rotation angle float m_pitch = 0; // Vertical rotation angle bool m_isFirstFrame = true; opengl::BoxGeometry* m_background = nullptr; QOpenGLTexture* m_backgroundTexture = nullptr; QColor m_baseColor; SkinProvider* m_parent = nullptr; }; PrismLauncher-10.0.5/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp0000644000175100017510000002373215144136756025573 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ui/dialogs/skins/draw/SkinOpenGLWindow.h" #include #include #include #include #include #include #include "minecraft/skins/SkinModel.h" #include "rainbow.h" #include "ui/dialogs/skins/draw/BoxGeometry.h" #include "ui/dialogs/skins/draw/Scene.h" SkinOpenGLWindow::SkinOpenGLWindow(SkinProvider* parent, QColor color) : QOpenGLWindow(), QOpenGLFunctions(), m_baseColor(color), m_parent(parent) { QSurfaceFormat format = QSurfaceFormat::defaultFormat(); format.setDepthBufferSize(24); setFormat(format); } SkinOpenGLWindow::~SkinOpenGLWindow() { // Make sure the context is current when deleting the texture // and the buffers. makeCurrent(); // double check if resources were initialized because they are not // initialized together with the object if (m_scene) { delete m_scene; } if (m_background) { delete m_background; } if (m_backgroundTexture) { if (m_backgroundTexture->isCreated()) { m_backgroundTexture->destroy(); } delete m_backgroundTexture; } if (m_modelProgram) { if (m_modelProgram->isLinked()) { m_modelProgram->release(); } m_modelProgram->removeAllShaders(); delete m_modelProgram; } if (m_backgroundProgram) { if (m_backgroundProgram->isLinked()) { m_backgroundProgram->release(); } m_backgroundProgram->removeAllShaders(); delete m_backgroundProgram; } doneCurrent(); } void SkinOpenGLWindow::mousePressEvent(QMouseEvent* e) { // Save mouse press position m_mousePosition = QVector2D(e->pos()); m_isMousePressed = true; } void SkinOpenGLWindow::mouseMoveEvent(QMouseEvent* event) { // Prevents mouse sticking on Wayland compositors if (!(event->buttons() & Qt::MouseButton::LeftButton)) { m_isMousePressed = false; return; } if (m_isMousePressed) { int dx = event->position().x() - m_mousePosition.x(); int dy = event->position().y() - m_mousePosition.y(); m_yaw += dx * 0.5f; m_pitch += dy * 0.5f; // Normalize yaw to keep it manageable if (m_yaw > 360.0f) m_yaw -= 360.0f; else if (m_yaw < 0.0f) m_yaw += 360.0f; m_mousePosition = QVector2D(event->pos()); update(); // Trigger a repaint } } void SkinOpenGLWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* e) { m_isMousePressed = false; } void SkinOpenGLWindow::initializeGL() { initializeOpenGLFunctions(); glClearColor(0, 0, 1, 1); initShaders(); generateBackgroundTexture(32, 32, 1); QImage skin, cape; bool slim = false; if (m_parent) { if (auto s = m_parent->getSelectedSkin()) { skin = s->getTexture(); slim = s->getModel() == SkinModel::SLIM; cape = m_parent->capes().value(s->getCapeId(), {}); } } m_scene = new opengl::Scene(skin, slim, cape); m_background = opengl::BoxGeometry::Plane(); glEnable(GL_TEXTURE_2D); } void SkinOpenGLWindow::initShaders() { // Skin model shaders m_modelProgram = new QOpenGLShaderProgram(this); // Compile vertex shader if (!m_modelProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader_skin_model.glsl")) close(); // Compile fragment shader if (!m_modelProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) close(); // Link shader pipeline if (!m_modelProgram->link()) close(); // Bind shader pipeline for use if (!m_modelProgram->bind()) close(); // Background shaders m_backgroundProgram = new QOpenGLShaderProgram(this); // Compile vertex shader if (!m_backgroundProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/vshader_skin_background.glsl")) close(); // Compile fragment shader if (!m_backgroundProgram->addCacheableShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/fshader.glsl")) close(); // Link shader pipeline if (!m_backgroundProgram->link()) close(); // Bind shader pipeline for use (verification) if (!m_backgroundProgram->bind()) close(); } void SkinOpenGLWindow::resizeGL(int w, int h) { // Calculate aspect ratio qreal aspect = qreal(w) / qreal(h ? h : 1); const qreal zNear = 15., fov = 45; // Reset projection m_projection.setToIdentity(); // Build the reverse z perspective projection matrix double radians = qDegreesToRadians(fov / 2.); double sine = std::sin(radians); if (sine == 0) return; double cotan = std::cos(radians) / sine; m_projection(0, 0) = cotan / aspect; m_projection(1, 1) = cotan; m_projection(2, 2) = 0.; m_projection(3, 2) = -1.; m_projection(2, 3) = zNear; m_projection(3, 3) = 0.; } void SkinOpenGLWindow::paintGL() { // Adjust the viewport to account for fractional scaling qreal dpr = devicePixelRatio(); if (dpr != 1.f) { QSize scaledSize = size() * dpr; glViewport(0, 0, scaledSize.width(), scaledSize.height()); } // Clear color and depth buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Enable depth buffer glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); // Enable back face culling glEnable(GL_CULL_FACE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); m_backgroundProgram->bind(); renderBackground(); m_backgroundProgram->release(); // Calculate model view transformation QMatrix4x4 matrix; float yawRad = qDegreesToRadians(m_yaw); float pitchRad = qDegreesToRadians(m_pitch); matrix.lookAt(QVector3D( // m_distance * qCos(pitchRad) * qCos(yawRad), // m_distance * qSin(pitchRad) - 8, // m_distance * qCos(pitchRad) * qSin(yawRad)), QVector3D(0, -8, 0), QVector3D(0, 1, 0)); // Set modelview-projection matrix m_modelProgram->bind(); m_modelProgram->setUniformValue("mvp_matrix", m_projection * matrix); m_scene->draw(m_modelProgram); m_modelProgram->release(); // Redraw the first frame; this is necessary because the pixel ratio for Wayland fractional scaling is not negotiated properly on the // first frame if (m_isFirstFrame) { m_isFirstFrame = false; update(); } } void SkinOpenGLWindow::updateScene(SkinModel* skin) { if (skin && m_scene) { m_scene->setMode(skin->getModel() == SkinModel::SLIM); m_scene->setSkin(skin->getTexture()); update(); } } void SkinOpenGLWindow::updateCape(const QImage& cape) { if (m_scene) { m_scene->setCapeVisible(!cape.isNull()); m_scene->setCape(cape); update(); } } QColor calculateContrastingColor(const QColor& color) { auto luma = Rainbow::luma(color); if (luma < 0.5) { constexpr float contrast = 0.05; return Rainbow::lighten(color, contrast); } else { constexpr float contrast = 0.2; return Rainbow::darken(color, contrast); } } QImage generateChessboardImage(int width, int height, int tileSize, QColor baseColor) { QImage image(width, height, QImage::Format_RGB888); bool isDarkBase = Rainbow::luma(baseColor) < 0.5; float contrast = isDarkBase ? 0.05 : 0.45; auto contrastFunc = std::bind(isDarkBase ? Rainbow::lighten : Rainbow::darken, std::placeholders::_1, contrast, 1.0); auto white = contrastFunc(baseColor); auto black = contrastFunc(calculateContrastingColor(baseColor)); for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { bool isWhite = ((x / tileSize) + (y / tileSize)) % 2 == 0; image.setPixelColor(x, y, isWhite ? white : black); } } return image; } void SkinOpenGLWindow::generateBackgroundTexture(int width, int height, int tileSize) { m_backgroundTexture = new QOpenGLTexture(generateChessboardImage(width, height, tileSize, m_baseColor)); m_backgroundTexture->setMinificationFilter(QOpenGLTexture::Nearest); m_backgroundTexture->setMagnificationFilter(QOpenGLTexture::Nearest); } void SkinOpenGLWindow::renderBackground() { glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); // Disable depth buffer writing m_backgroundTexture->bind(); m_backgroundProgram->setUniformValue("texture", 0); m_background->draw(m_backgroundProgram); m_backgroundTexture->release(); glDepthMask(GL_TRUE); // Re-enable depth buffer writing glEnable(GL_DEPTH_TEST); } void SkinOpenGLWindow::wheelEvent(QWheelEvent* event) { // Adjust distance based on scroll int delta = event->angleDelta().y(); // Positive for scroll up, negative for scroll down m_distance -= delta * 0.01f; // Adjust sensitivity factor m_distance = qMax(16.f, m_distance); // Clamp distance update(); // Trigger a repaint } void SkinOpenGLWindow::setElytraVisible(bool visible) { if (m_scene) m_scene->setElytraVisible(visible); } bool SkinOpenGLWindow::hasOpenGL() { QOpenGLContext ctx; return ctx.create(); } PrismLauncher-10.0.5/launcher/ui/dialogs/skins/draw/Scene.cpp0000644000175100017510000001523215144136756023463 0ustar runnerrunner // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ui/dialogs/skins/draw/Scene.h" #include #include #include #include namespace opengl { Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctions(), m_slim(slim), m_capeVisible(!cape.isNull()) { initializeOpenGLFunctions(); m_staticComponents = { // head new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)), // body new opengl::BoxGeometry(QVector3D(8, 12, 4), QVector3D(0, -6, 0), QPoint(16, 16), QVector3D(8, 12, 4)), // right leg new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-1.9, -18, -0.1), QPoint(0, 16), QVector3D(4, 12, 4)), // left leg new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(1.9, -18, -0.1), QPoint(16, 48), QVector3D(4, 12, 4)), }; m_staticComponentsOverlay = { // head new opengl::BoxGeometry(QVector3D(9, 9, 9), QVector3D(0, 4, 0), QPoint(32, 0), QVector3D(8, 8, 8)), // body new opengl::BoxGeometry(QVector3D(8.5, 12.5, 4.5), QVector3D(0, -6, 0), QPoint(16, 32), QVector3D(8, 12, 4)), // right leg new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-1.9, -18, -0.1), QPoint(0, 32), QVector3D(4, 12, 4)), // left leg new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(1.9, -18, -0.1), QPoint(0, 48), QVector3D(4, 12, 4)), }; m_normalArms = { // Right Arm new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-6, -6, 0), QPoint(40, 16), QVector3D(4, 12, 4)), // Left Arm new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(6, -6, 0), QPoint(32, 48), QVector3D(4, 12, 4)), }; m_normalArmsOverlay = { // Right Arm new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-6, -6, 0), QPoint(40, 32), QVector3D(4, 12, 4)), // Left Arm new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(6, -6, 0), QPoint(48, 48), QVector3D(4, 12, 4)), }; m_slimArms = { // Right Arm new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(-5.5, -6, 0), QPoint(40, 16), QVector3D(3, 12, 4)), // Left Arm new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(5.5, -6, 0), QPoint(32, 48), QVector3D(3, 12, 4)), }; m_slimArmsOverlay = { // Right Arm new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(-5.5, -6, 0), QPoint(40, 32), QVector3D(3, 12, 4)), // Left Arm new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(5.5, -6, 0), QPoint(48, 48), QVector3D(3, 12, 4)), }; m_cape = new opengl::BoxGeometry(QVector3D(10, 16, 1), QVector3D(0, -8, 2.5), QPoint(0, 0), QVector3D(10, 16, 1), QSize(64, 32)); m_cape->rotate(10.8, QVector3D(1, 0, 0)); m_cape->rotate(180, QVector3D(0, 1, 0)); auto leftWing = new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32)); leftWing->rotate(15, QVector3D(1, 0, 0)); leftWing->rotate(15, QVector3D(0, 0, 1)); leftWing->rotate(1, QVector3D(1, 0, 0)); auto rightWing = new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32)); rightWing->scale(QVector3D(-1, 1, 1)); rightWing->rotate(15, QVector3D(1, 0, 0)); rightWing->rotate(15, QVector3D(0, 0, 1)); rightWing->rotate(1, QVector3D(1, 0, 0)); m_elytra << leftWing << rightWing; // texture init m_skinTexture = new QOpenGLTexture(skin.mirrored()); m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest); m_skinTexture->setMagnificationFilter(QOpenGLTexture::Nearest); m_capeTexture = new QOpenGLTexture(cape.mirrored()); m_capeTexture->setMinificationFilter(QOpenGLTexture::Nearest); m_capeTexture->setMagnificationFilter(QOpenGLTexture::Nearest); } Scene::~Scene() { for (auto array : { m_staticComponents, m_normalArms, m_slimArms, m_elytra, m_staticComponentsOverlay, m_normalArmsOverlay, m_slimArmsOverlay }) { for (auto g : array) { delete g; } } delete m_cape; m_skinTexture->destroy(); delete m_skinTexture; m_capeTexture->destroy(); delete m_capeTexture; } void Scene::draw(QOpenGLShaderProgram* program) { m_skinTexture->bind(); program->setUniformValue("texture", 0); for (auto toDraw : { m_staticComponents, m_slim ? m_slimArms : m_normalArms, m_staticComponentsOverlay, m_slim ? m_slimArmsOverlay : m_normalArmsOverlay }) { for (auto g : toDraw) { g->draw(program); } } m_skinTexture->release(); if (m_capeVisible) { m_capeTexture->bind(); program->setUniformValue("texture", 0); if (!m_elytraVisible) { m_cape->draw(program); } else { glDisable(GL_CULL_FACE); for (auto e : m_elytra) { e->draw(program); } glEnable(GL_CULL_FACE); } m_capeTexture->release(); } } void updateTexture(QOpenGLTexture* texture, const QImage& img) { if (texture) { if (texture->isBound()) texture->release(); texture->destroy(); texture->create(); texture->setSize(img.width(), img.height()); texture->setData(img); texture->setMinificationFilter(QOpenGLTexture::Nearest); texture->setMagnificationFilter(QOpenGLTexture::Nearest); } } void Scene::setSkin(const QImage& skin) { updateTexture(m_skinTexture, skin.mirrored()); } void Scene::setMode(bool slim) { m_slim = slim; } void Scene::setCape(const QImage& cape) { updateTexture(m_capeTexture, cape.mirrored()); } void Scene::setCapeVisible(bool visible) { m_capeVisible = visible; } void Scene::setElytraVisible(bool elytraVisible) { m_elytraVisible = elytraVisible; } } // namespace opengl PrismLauncher-10.0.5/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp0000644000175100017510000002337515144136756024701 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "BoxGeometry.h" #include #include #include #include struct VertexData { QVector4D position; QVector2D texCoord; VertexData(const QVector4D& pos, const QVector2D& tex) : position(pos), texCoord(tex) {} }; // For cube we would need only 8 vertices but we have to // duplicate vertex for each face because texture coordinate // is different. static const QList vertices = { // Vertex data for face 0 QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v0 QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v1 QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v2 QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v3 // Vertex data for face 1 QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v4 QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v5 QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v6 QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v7 // Vertex data for face 2 QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v8 QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v9 QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v10 QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v11 // Vertex data for face 3 QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v12 QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v13 QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v14 QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v15 // Vertex data for face 4 QVector4D(-0.5f, -0.5f, -0.5f, 1.0f), // v16 QVector4D(0.5f, -0.5f, -0.5f, 1.0f), // v17 QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v18 QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v19 // Vertex data for face 5 QVector4D(-0.5f, 0.5f, 0.5f, 1.0f), // v20 QVector4D(0.5f, 0.5f, 0.5f, 1.0f), // v21 QVector4D(-0.5f, 0.5f, -0.5f, 1.0f), // v22 QVector4D(0.5f, 0.5f, -0.5f, 1.0f), // v23 }; // Indices for drawing cube faces using triangle strips. // Triangle strips can be connected by duplicating indices // between the strips. If connecting strips have opposite // vertex order then last index of the first strip and first // index of the second strip needs to be duplicated. If // connecting strips have same vertex order then only last // index of the first strip needs to be duplicated. static const QList indices = { 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) 12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15) 16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19) 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) }; static const QList planeVertices = { { QVector4D(-1.0f, -1.0f, -0.5f, 1.0f), QVector2D(0.0f, 0.0f) }, // Bottom-left { QVector4D(1.0f, -1.0f, -0.5f, 1.0f), QVector2D(1.0f, 0.0f) }, // Bottom-right { QVector4D(-1.0f, 1.0f, -0.5f, 1.0f), QVector2D(0.0f, 1.0f) }, // Top-left { QVector4D(1.0f, 1.0f, -0.5f, 1.0f), QVector2D(1.0f, 1.0f) }, // Top-right }; static const QList planeIndices = { 0, 1, 2, 3, 3 // Face 0 - triangle strip ( v0, v1, v2, v3) }; QList transformVectors(const QMatrix4x4& matrix, const QList& vectors) { QList transformedVectors; transformedVectors.reserve(vectors.size()); for (const QVector4D& vec : vectors) { if (!matrix.isIdentity()) { transformedVectors.append(matrix * vec); } else { transformedVectors.append(vec); } } return transformedVectors; } // Function to calculate UV coordinates // this is pure magic (if something is wrong with textures this is at fault) QList getCubeUVs(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) { auto toFaceVertices = [textureHeight, textureWidth](float x1, float y1, float x2, float y2) -> QList { return { QVector2D(x1 / textureWidth, 1.0 - y2 / textureHeight), QVector2D(x2 / textureWidth, 1.0 - y2 / textureHeight), QVector2D(x2 / textureWidth, 1.0 - y1 / textureHeight), QVector2D(x1 / textureWidth, 1.0 - y1 / textureHeight), }; }; auto top = toFaceVertices(u + depth, v, u + width + depth, v + depth); auto bottom = toFaceVertices(u + width + depth, v, u + width * 2 + depth, v + depth); auto left = toFaceVertices(u, v + depth, u + depth, v + depth + height); auto front = toFaceVertices(u + depth, v + depth, u + width + depth, v + depth + height); auto right = toFaceVertices(u + width + depth, v + depth, u + width + depth * 2, v + height + depth); auto back = toFaceVertices(u + width + depth * 2, v + depth, u + width * 2 + depth * 2, v + height + depth); auto uvRight = { right[0], right[1], right[3], right[2], }; auto uvLeft = { left[0], left[1], left[3], left[2], }; auto uvTop = { top[0], top[1], top[3], top[2], }; auto uvBottom = { bottom[3], bottom[2], bottom[0], bottom[1], }; auto uvFront = { front[0], front[1], front[3], front[2], }; auto uvBack = { back[0], back[1], back[3], back[2], }; // Create a new array to hold the modified UV data QList uvData; uvData.reserve(24); // Iterate over the arrays and copy the data to newUVData for (const auto& uvArray : { uvFront, uvRight, uvBack, uvLeft, uvBottom, uvTop }) { uvData.append(uvArray); } return uvData; } namespace opengl { BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) : QOpenGLFunctions(), m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position) { initializeOpenGLFunctions(); // Generate 2 VBOs m_vertexBuf.create(); m_indexBuf.create(); } BoxGeometry::BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize) : BoxGeometry(size, position) { initGeometry(uv.x(), uv.y(), textureDim.x(), textureDim.y(), textureDim.z(), textureSize.width(), textureSize.height()); } BoxGeometry::~BoxGeometry() { m_vertexBuf.destroy(); m_indexBuf.destroy(); } void BoxGeometry::draw(QOpenGLShaderProgram* program) { // Tell OpenGL which VBOs to use program->setUniformValue("model_matrix", m_matrix); m_vertexBuf.bind(); m_indexBuf.bind(); // Offset for position quintptr offset = 0; // Tell OpenGL programmable pipeline how to locate vertex position data int vertexLocation = program->attributeLocation("a_position"); program->enableAttributeArray(vertexLocation); program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 4, sizeof(VertexData)); // Offset for texture coordinate offset += sizeof(QVector4D); // Tell OpenGL programmable pipeline how to locate vertex texture coordinate data int texcoordLocation = program->attributeLocation("a_texcoord"); program->enableAttributeArray(texcoordLocation); program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData)); // Draw cube geometry using indices from VBO 1 glDrawElements(GL_TRIANGLE_STRIP, m_indecesCount, GL_UNSIGNED_SHORT, nullptr); } void BoxGeometry::initGeometry(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) { auto textureCord = getCubeUVs(u, v, width, height, depth, textureWidth, textureHeight); // this should not be needed to be done on each render for most of the objects QMatrix4x4 transformation; transformation.translate(m_position); transformation.scale(m_size); auto positions = transformVectors(transformation, vertices); QList verticesData; verticesData.reserve(positions.size()); // Reserve space for efficiency for (int i = 0; i < positions.size(); ++i) { verticesData.append(VertexData(positions[i], textureCord[i])); } // Transfer vertex data to VBO 0 m_vertexBuf.bind(); m_vertexBuf.allocate(verticesData.constData(), verticesData.size() * sizeof(VertexData)); // Transfer index data to VBO 1 m_indexBuf.bind(); m_indexBuf.allocate(indices.constData(), indices.size() * sizeof(GLushort)); m_indecesCount = indices.size(); } void BoxGeometry::rotate(float angle, const QVector3D& vector) { m_matrix.rotate(angle, vector); } BoxGeometry* BoxGeometry::Plane() { auto b = new BoxGeometry(QVector3D(), QVector3D()); // Transfer vertex data to VBO 0 b->m_vertexBuf.bind(); b->m_vertexBuf.allocate(planeVertices.constData(), planeVertices.size() * sizeof(VertexData)); // Transfer index data to VBO 1 b->m_indexBuf.bind(); b->m_indexBuf.allocate(planeIndices.constData(), planeIndices.size() * sizeof(GLushort)); b->m_indecesCount = planeIndices.size(); return b; } void BoxGeometry::scale(const QVector3D& vector) { m_matrix.scale(vector); } } // namespace opengl PrismLauncher-10.0.5/launcher/ui/dialogs/skins/draw/BoxGeometry.h0000644000175100017510000000321515144136756024335 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include namespace opengl { class BoxGeometry : protected QOpenGLFunctions { public: BoxGeometry(QVector3D size, QVector3D position); BoxGeometry(QVector3D size, QVector3D position, QPoint uv, QVector3D textureDim, QSize textureSize = { 64, 64 }); static BoxGeometry* Plane(); virtual ~BoxGeometry(); void draw(QOpenGLShaderProgram* program); void initGeometry(float u, float v, float width, float height, float depth, float textureWidth = 64, float textureHeight = 64); void rotate(float angle, const QVector3D& vector); void scale(const QVector3D& vector); private: QOpenGLBuffer m_vertexBuf; QOpenGLBuffer m_indexBuf; QVector3D m_size; QVector3D m_position; QMatrix4x4 m_matrix; GLsizei m_indecesCount; }; } // namespace opengl PrismLauncher-10.0.5/launcher/ui/dialogs/InstallLoaderDialog.cpp0000644000175100017510000001472415144136756024224 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "InstallLoaderDialog.h" #include #include #include #include "Application.h" #include "BuildConfig.h" #include "DesktopServices.h" #include "meta/Index.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/widgets/PageContainer.h" #include "ui/widgets/VersionSelectWidget.h" class InstallLoaderPage : public VersionSelectWidget, public BasePage { Q_OBJECT public: InstallLoaderPage(const QString& id, const QString& iconName, const QString& name, const Version& oldestVersion, const std::shared_ptr profile) : VersionSelectWidget(nullptr), uid(id), iconName(iconName), name(name) { const QString minecraftVersion = profile->getComponentVersion("net.minecraft"); setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); setExactIfPresentFilter(BaseVersionList::ParentVersionRole, minecraftVersion); if (oldestVersion != Version() && Version(minecraftVersion) < oldestVersion) setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); if (const QString currentVersion = profile->getComponentVersion(id); !currentVersion.isNull()) setCurrentVersion(currentVersion); } QString id() const override { return uid; } QString displayName() const override { return name; } QIcon icon() const override { return QIcon::fromTheme(iconName); } void openedImpl() override { if (loaded) return; const auto versions = APPLICATION->metadataIndex()->get(uid); if (!versions) return; initialize(versions.get()); loaded = true; } void setParentContainer(BasePageContainer* container) override { auto dialog = dynamic_cast(dynamic_cast(container)->parent()); connect(view(), &QAbstractItemView::doubleClicked, dialog, &QDialog::accept); } private: const QString uid; const QString iconName; const QString name; bool loaded = false; }; static InstallLoaderPage* pageCast(BasePage* page) { auto result = dynamic_cast(page); Q_ASSERT(result != nullptr); return result; } InstallLoaderDialog::InstallLoaderDialog(std::shared_ptr profile, const QString& uid, QWidget* parent) : QDialog(parent), profile(std::move(profile)), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) { auto layout = new QVBoxLayout(this); // small margins look ugly on macOS on modal windows #ifndef Q_OS_MACOS layout->setContentsMargins(0, 0, 0, 0); #endif container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); layout->addWidget(container); auto buttonLayout = new QHBoxLayout(this); // small margins look ugly on macOS on modal windows #ifndef Q_OS_MACOS buttonLayout->setContentsMargins(0, 0, 6, 6); #endif auto refreshButton = new QPushButton(tr("&Refresh"), this); connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); }); buttonLayout->addWidget(refreshButton); buttons->setOrientation(Qt::Horizontal); buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); buttons->button(QDialogButtonBox::Ok)->setText(tr("Ok")); buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); buttonLayout->addWidget(buttons); container->addButtons(buttonLayout); setWindowTitle(dialogTitle()); setWindowModality(Qt::WindowModal); resize(520, 347); for (BasePage* page : container->getPages()) { if (page->id() == uid) container->selectPage(page->id()); connect(pageCast(page), &VersionSelectWidget::selectedVersionChanged, this, [this, page] { if (page->id() == container->selectedPage()->id()) validate(container->selectedPage()); }); } connect(container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* current) { validate(current); }); pageCast(container->selectedPage())->selectSearch(); validate(container->selectedPage()); } QList InstallLoaderDialog::getPages() { return { // NeoForge new InstallLoaderPage("net.neoforged", "neoforged", tr("NeoForge"), {}, profile), // Forge new InstallLoaderPage("net.minecraftforge", "forge", tr("Forge"), {}, profile), // Fabric new InstallLoaderPage("net.fabricmc.fabric-loader", "fabricmc", tr("Fabric"), Version("1.14"), profile), // Quilt new InstallLoaderPage("org.quiltmc.quilt-loader", "quiltmc", tr("Quilt"), Version("1.14"), profile), // LiteLoader new InstallLoaderPage("com.mumfrey.liteloader", "liteloader", tr("LiteLoader"), {}, profile) }; } QString InstallLoaderDialog::dialogTitle() { return tr("Install Loader"); } void InstallLoaderDialog::validate(BasePage* page) { buttons->button(QDialogButtonBox::Ok)->setEnabled(pageCast(page)->selectedVersion() != nullptr); } void InstallLoaderDialog::done(int result) { if (result == Accepted) { auto* page = pageCast(container->selectedPage()); if (page->selectedVersion()) { profile->setComponentVersion(page->id(), page->selectedVersion()->descriptor()); profile->resolve(Net::Mode::Online); } } QDialog::done(result); } #include "InstallLoaderDialog.moc" PrismLauncher-10.0.5/launcher/ui/dialogs/UpdateAvailableDialog.ui0000644000175100017510000000776315144136756024352 0ustar runnerrunner UpdateAvailableDialog 0 0 636 352 Update Available 64 64 Qt::Vertical 20 40 9 9 9 9 11 75 true A new version is available! Version %1 is now available - you have %2 . Would you like to download it now? 75 true Release Notes: Skip This Version Qt::Horizontal 40 20 Remind Me Later false false Install Update true PrismLauncher-10.0.5/launcher/ui/dialogs/CopyInstanceDialog.ui0000644000175100017510000003254115144136756023716 0ustar runnerrunner CopyInstanceDialog Qt::ApplicationModal 0 0 575 695 Copy Instance :/icons/toolbar/copy:/icons/toolbar/copy true Qt::Horizontal 60 20 :/icons/instances/grass:/icons/instances/grass 80 80 Qt::Horizontal 60 20 Name Qt::Horizontal 6 &Group groupBox 0 0 true Instance Copy Options Keep play time Disabling this will still keep the mod loader (ex: Fabric, Quilt, etc.) but erase the mods folder and their configs. Copy mods true Copy resource packs Copy the in-game options like FOV, max framerate, etc. Copy game options Copy shader packs Copy servers Copy saves Copy screenshots 0 0 Qt::LeftToRight Select all false Qt::Horizontal Advanced Copy Options Qt::AlignCenter Use symbolic or hard links instead of copying files. Symbolic and Hard Link Options false false false Links are supported on most filesystems except FAT Qt::AlignCenter 6 6 6 6 false Link each resource individually instead of linking whole folders at once Link files recursively false If "copy saves" is selected world save data will be copied instead of linked and thus not shared between instances. Don't link saves false true Use hard links instead of copying files. Use hard links Use symbolic links instead of copying files. Use symbolic links CoW (Copy-on-Write) Options false Files cloned with reflinks take up no extra space until they are modified. Clone instead of copying Qt::Horizontal 40 20 1 0 Your filesystem and/or OS doesn't support reflinks Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 4 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok iconButton instNameTextBox groupBox keepPlaytimeCheckbox copyScreenshotsCheckbox copySavesCheckbox copyShaderPacksCheckbox copyGameOptionsCheckbox copyServersCheckbox copyResPacksCheckbox copyModsCheckbox symbolicLinksCheckbox recursiveLinkCheckbox hardLinksCheckbox dontLinkSavesCheckbox useCloneCheckbox buttonBox accepted() CopyInstanceDialog accept() 269 692 157 274 buttonBox rejected() CopyInstanceDialog reject() 337 692 286 274 PrismLauncher-10.0.5/launcher/ui/dialogs/ImportResourceDialog.ui0000644000175100017510000000357015144136756024301 0ustar runnerrunner ImportResourceDialog 0 0 676 555 Choose instance to import to Choose the instance you would like to import this resource pack to. Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() ImportResourceDialog accept() 248 254 157 274 buttonBox rejected() ImportResourceDialog reject() 316 260 286 274 PrismLauncher-10.0.5/launcher/ui/dialogs/MSALoginDialog.ui0000644000175100017510000002757015144136756022736 0ustar runnerrunner MSALoginDialog 0 0 440 447 0 430 Add Microsoft Account 1 Qt::Vertical 20 40 16 75 true Please wait... Qt::AlignCenter true Status Qt::AlignCenter true Qt::Vertical 20 40 Qt::Horizontal 40 20 250 40 Sign in with Microsoft true Qt::Horizontal 40 20 0 0 Qt::Horizontal 16 Or Qt::AlignCenter 0 0 Qt::Horizontal 1 Qt::Vertical 20 40 16 75 true Please wait... Qt::AlignCenter true Status Qt::AlignCenter true Qt::Vertical 20 40 Qt::Horizontal 40 20 0 0 150 150 150 150 true Qt::AlignCenter Qt::Horizontal 40 20 Qt::Horizontal 40 20 30 75 true IBeamCursor CODE Qt::AlignCenter Qt::TextBrowserInteraction Copy code to clipboard .. 22 22 true Qt::Horizontal 40 20 Info Qt::AlignCenter true true Qt::TextBrowserInteraction QDialogButtonBox::Cancel PrismLauncher-10.0.5/launcher/ui/dialogs/CustomMessageBox.cpp0000644000175100017510000000277215144136756023577 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "CustomMessageBox.h" namespace CustomMessageBox { QMessageBox* selectable(QWidget* parent, const QString& title, const QString& text, QMessageBox::Icon icon, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton, QCheckBox* checkBox) { QMessageBox* messageBox = new QMessageBox(parent); messageBox->setWindowTitle(title); messageBox->setText(text); messageBox->setStandardButtons(buttons); messageBox->setDefaultButton(defaultButton); messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); messageBox->setIcon(icon); messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); if (checkBox) messageBox->setCheckBox(checkBox); return messageBox; } } // namespace CustomMessageBox PrismLauncher-10.0.5/launcher/ui/dialogs/ResourceUpdateDialog.h0000644000175100017510000000422715144136756024063 0ustar runnerrunner#pragma once #include "BaseInstance.h" #include "ResourceDownloadTask.h" #include "ReviewMessageBox.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/CheckUpdateTask.h" class Mod; class ModrinthCheckUpdate; class FlameCheckUpdate; class ConcurrentTask; class ResourceUpdateDialog final : public ReviewMessageBox { Q_OBJECT public: explicit ResourceUpdateDialog(QWidget* parent, BaseInstance* instance, std::shared_ptr resourceModel, QList& searchFor, bool includeDeps, QList loadersList = {}); void checkCandidates(); void appendResource(const CheckUpdateTask::Update& info, QStringList requiredBy = {}); const QList getTasks(); auto indexDir() const -> QDir { return m_resourceModel->indexDir(); } auto noUpdates() const -> bool { return m_noUpdates; }; auto aborted() const -> bool { return m_aborted; }; private: auto ensureMetadata() -> bool; private slots: void onMetadataEnsured(Resource* resource); void onMetadataFailed(Resource* resource, bool try_others = false, ModPlatform::ResourceProvider firstChoice = ModPlatform::ResourceProvider::MODRINTH); private: QWidget* m_parent; shared_qobject_ptr m_modrinthCheckTask; shared_qobject_ptr m_flameCheckTask; const std::shared_ptr m_resourceModel; QList& m_candidates; QList m_modrinthToUpdate; QList m_flameToUpdate; ConcurrentTask::Ptr m_secondTryMetadata; QList> m_failedMetadata; QList> m_failedCheckUpdate; QHash m_tasks; BaseInstance* m_instance; bool m_noUpdates = false; bool m_aborted = false; bool m_includeDeps = false; QList m_loadersList; }; PrismLauncher-10.0.5/launcher/ui/dialogs/NewInstanceDialog.cpp0000644000175100017510000002520215144136756023676 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NewInstanceDialog.h" #include "Application.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/pages/modplatform/import_ftb/ImportFTBPage.h" #include "ui_NewInstanceDialog.h" #include #include #include #include #include "IconPickerDialog.h" #include "ProgressDialog.h" #include "VersionSelectDialog.h" #include #include #include #include #include #include #include #include "ui/pages/modplatform/CustomPage.h" #include "ui/pages/modplatform/ImportPage.h" #include "ui/pages/modplatform/atlauncher/AtlPage.h" #include "ui/pages/modplatform/flame/FlamePage.h" #include "ui/pages/modplatform/legacy_ftb/Page.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h" #include "ui/widgets/PageContainer.h" NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, const QString& url, const QMap& extra_info, QWidget* parent) : QDialog(parent), ui(new Ui::NewInstanceDialog) { ui->setupUi(this); setWindowIcon(QIcon::fromTheme("new")); InstIconKey = "default"; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); QStringList groups = APPLICATION->instances()->getGroups(); groups.prepend(""); int index = groups.indexOf(initialGroup); if (index == -1) { index = 1; groups.insert(index, initialGroup); } ui->groupBox->addItems(groups); ui->groupBox->setCurrentIndex(index); ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not // move this below. m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); m_container = new PageContainer(this, {}, this); m_container->useSidebarStyle(false); m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); m_container->layout()->setContentsMargins(0, 0, 0, 0); ui->verticalLayout->insertWidget(2, m_container); m_container->addButtons(m_buttons); connect(m_container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* selected) { m_buttons->button(QDialogButtonBox::Ok)->setEnabled(creationTask && !instName().isEmpty()); }); // Bonk Qt over its stupid head and make sure it understands which button is the default one... // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button auto OkButton = m_buttons->button(QDialogButtonBox::Ok); OkButton->setDefault(true); OkButton->setAutoDefault(true); OkButton->setText(tr("OK")); connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); CancelButton->setDefault(false); CancelButton->setAutoDefault(false); CancelButton->setText(tr("Cancel")); connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject); auto HelpButton = m_buttons->button(QDialogButtonBox::Help); HelpButton->setDefault(false); HelpButton->setAutoDefault(false); HelpButton->setText(tr("Help")); connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); if (!url.isEmpty()) { QUrl actualUrl(url); m_container->selectPage("import"); importPage->setUrl(url); importPage->setExtraInfo(extra_info); } updateDialogState(); if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) { restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toString().toUtf8())); } else { auto screen = parent->screen(); auto geometry = screen->availableSize(); resize(width(), qMin(geometry.height() - 50, 710)); } connect(m_container, &PageContainer::selectedPageChanged, this, &NewInstanceDialog::selectedPageChanged); } void NewInstanceDialog::reject() { APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); // This is just so that the pages get the close() call and can react to it, if needed. m_container->prepareToClose(); QDialog::reject(); } void NewInstanceDialog::accept() { APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); importIconNow(); // This is just so that the pages get the close() call and can react to it, if needed. m_container->prepareToClose(); QDialog::accept(); } QList NewInstanceDialog::getPages() { QList pages; importPage = new ImportPage(this); pages.append(new CustomPage(this)); pages.append(importPage); pages.append(new AtlPage(this)); if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(new FlamePage(this)); pages.append(new LegacyFTB::Page(this)); pages.append(new FTBImportAPP::ImportFTBPage(this)); pages.append(new ModrinthPage(this)); pages.append(new TechnicPage(this)); return pages; } QString NewInstanceDialog::dialogTitle() { return tr("New Instance"); } NewInstanceDialog::~NewInstanceDialog() { delete ui; } void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task) { creationTask.reset(task); ui->instNameTextBox->setPlaceholderText(name); importVersion.clear(); if (!task) { ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); importIcon = false; } auto allowOK = task && !instName().isEmpty(); m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK); } void NewInstanceDialog::setSuggestedPack(const QString& name, QString version, InstanceTask* task) { creationTask.reset(task); ui->instNameTextBox->setPlaceholderText(name); importVersion = std::move(version); if (!task) { ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); importIcon = false; } auto allowOK = task && !instName().isEmpty(); m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK); } void NewInstanceDialog::setSuggestedIconFromFile(const QString& path, const QString& name) { importIcon = true; importIconPath = path; importIconName = name; // Hmm, for some reason they can be to small ui->iconButton->setIcon(QIcon(path)); } void NewInstanceDialog::setSuggestedIcon(const QString& key) { if (key == "default") return; auto icon = APPLICATION->icons()->getIcon(key); importIcon = false; ui->iconButton->setIcon(icon); } InstanceTask* NewInstanceDialog::extractTask() { InstanceTask* extracted = creationTask.release(); InstanceName inst_name(ui->instNameTextBox->placeholderText().trimmed(), importVersion); inst_name.setName(ui->instNameTextBox->text().trimmed()); extracted->setName(inst_name); extracted->setGroup(instGroup()); extracted->setIcon(iconKey()); return extracted; } void NewInstanceDialog::updateDialogState() { auto allowOK = creationTask && !instName().isEmpty(); auto OkButton = m_buttons->button(QDialogButtonBox::Ok); if (OkButton->isEnabled() != allowOK) { OkButton->setEnabled(allowOK); } } QString NewInstanceDialog::instName() const { auto result = ui->instNameTextBox->text().trimmed(); if (result.size()) { return result; } result = ui->instNameTextBox->placeholderText().trimmed(); if (result.size()) { return result; } return QString(); } QString NewInstanceDialog::instGroup() const { return ui->groupBox->currentText(); } QString NewInstanceDialog::iconKey() const { return InstIconKey; } void NewInstanceDialog::on_iconButton_clicked() { importIconNow(); // so the user can switch back IconPickerDialog dlg(this); dlg.execWithSelection(InstIconKey); if (dlg.result() == QDialog::Accepted) { InstIconKey = dlg.selectedIconKey; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); importIcon = false; } } void NewInstanceDialog::on_instNameTextBox_textChanged([[maybe_unused]] const QString& arg1) { updateDialogState(); } void NewInstanceDialog::importIconNow() { if (importIcon) { APPLICATION->icons()->installIcon(importIconPath, importIconName); InstIconKey = importIconName.mid(0, importIconName.lastIndexOf('.')); importIcon = false; } APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); } void NewInstanceDialog::selectedPageChanged(BasePage* previous, BasePage* selected) { auto prevPage = dynamic_cast(previous); if (prevPage) { m_searchTerm = prevPage->getSerachTerm(); } auto nextPage = dynamic_cast(selected); if (nextPage) { nextPage->setSearchTerm(m_searchTerm); } } PrismLauncher-10.0.5/launcher/ui/dialogs/ChooseProviderDialog.h0000644000175100017510000000177615144136756024072 0ustar runnerrunner#pragma once #include #include namespace Ui { class ChooseProviderDialog; } namespace ModPlatform { enum class ResourceProvider; } class Mod; class NetJob; class ChooseProviderDialog : public QDialog { Q_OBJECT struct Response { bool skip_all = false; bool confirm_all = false; bool try_others = false; ModPlatform::ResourceProvider chosen; }; public: explicit ChooseProviderDialog(QWidget* parent, bool single_choice = false, bool allow_skipping = true); ~ChooseProviderDialog(); auto getResponse() const -> Response { return m_response; } void setDescription(QString desc); private slots: void skipOne(); void skipAll(); void confirmOne(); void confirmAll(); private: void addProviders(); void disableInput(); auto getSelectedProvider() const -> ModPlatform::ResourceProvider; private: Ui::ChooseProviderDialog* ui; QButtonGroup m_providers; Response m_response; }; PrismLauncher-10.0.5/launcher/ui/dialogs/ReviewMessageBox.ui0000644000175100017510000000355415144136756023420 0ustar runnerrunner ReviewMessageBox 0 0 500 350 true true true QAbstractItemView::NoSelection QAbstractItemView::SelectItems false Toggle Dependencies QDialogButtonBox::Cancel|QDialogButtonBox::Ok PrismLauncher-10.0.5/launcher/ui/dialogs/NewsDialog.ui0000644000175100017510000000641015144136756022227 0ustar runnerrunner NewsDialog 0 0 800 500 News true 0 0 Placeholder Qt::AlignCenter true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse true true 10 0 Close Hide article list ProjectDescriptionPage QTextBrowser
    ui/widgets/ProjectDescriptionPage.h
    closeButton clicked() NewsDialog accept() 199 277 199 149
    PrismLauncher-10.0.5/launcher/ui/dialogs/AboutDialog.cpp0000644000175100017510000001050415144136756022531 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AboutDialog.h" #include #include "Application.h" #include "BuildConfig.h" #include "Markdown.h" #include "StringUtils.h" #include "ui_AboutDialog.h" #include namespace { QString getCreditsHtml() { QFile dataFile(":/documents/credits.html"); if (!dataFile.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; return {}; } QString fileContent = QString::fromUtf8(dataFile.readAll()); dataFile.close(); return fileContent.arg(QObject::tr("%1 Developers").arg(BuildConfig.LAUNCHER_DISPLAYNAME), QObject::tr("MultiMC Developers"), QObject::tr("With special thanks to")); } QString getLicenseHtml() { QFile dataFile(":/documents/COPYING.md"); if (dataFile.open(QIODevice::ReadOnly)) { QString output = markdownToHTML(dataFile.readAll()); dataFile.close(); return output; } else { qWarning() << "Failed to open file '" << dataFile.fileName() << "' for reading!"; return QString(); } } } // namespace AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDialog) { ui->setupUi(this); QString launcherName = BuildConfig.LAUNCHER_DISPLAYNAME; setWindowTitle(tr("About %1").arg(launcherName)); QString chtml = getCreditsHtml(); ui->creditsText->setHtml(StringUtils::htmlListPatch(chtml)); QString lhtml = getLicenseHtml(); ui->licenseText->setHtml(StringUtils::htmlListPatch(lhtml)); ui->urlLabel->setOpenExternalLinks(true); ui->icon->setPixmap(APPLICATION->logo().pixmap(64)); ui->title->setText(launcherName); ui->versionLabel->setText(BuildConfig.printableVersionString()); if (!BuildConfig.BUILD_PLATFORM.isEmpty()) ui->platformLabel->setText(tr("Platform") + ": " + BuildConfig.BUILD_PLATFORM); else ui->platformLabel->setVisible(false); if (!BuildConfig.GIT_COMMIT.isEmpty()) ui->commitLabel->setText(tr("Commit: %1").arg(BuildConfig.GIT_COMMIT)); else ui->commitLabel->setVisible(false); if (!BuildConfig.BUILD_DATE.isEmpty()) ui->buildDateLabel->setText(tr("Build date: %1").arg(BuildConfig.BUILD_DATE)); else ui->buildDateLabel->setVisible(false); if (!BuildConfig.VERSION_CHANNEL.isEmpty()) ui->channelLabel->setText(tr("Channel") + ": " + BuildConfig.VERSION_CHANNEL); else ui->channelLabel->setVisible(false); QString urlText("

    %1

    "); ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT)); ui->copyLabel->setText(BuildConfig.LAUNCHER_COPYRIGHT); connect(ui->closeButton, &QPushButton::clicked, this, &AboutDialog::close); connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt); } AboutDialog::~AboutDialog() { delete ui; } PrismLauncher-10.0.5/launcher/ui/dialogs/CreateShortcutDialog.h0000644000175100017510000000312715144136756024066 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "BaseInstance.h" class BaseInstance; namespace Ui { class CreateShortcutDialog; } class CreateShortcutDialog : public QDialog { Q_OBJECT public: explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr); ~CreateShortcutDialog(); void createShortcut(); private slots: // Icon, target and name void on_iconButton_clicked(); // Override account void on_overrideAccountCheckbox_stateChanged(int state); // Override target (world, server) void on_targetCheckbox_stateChanged(int state); void on_worldTarget_toggled(bool checked); void on_serverTarget_toggled(bool checked); void on_worldSelectionBox_currentIndexChanged(int index); void on_serverAddressBox_textChanged(const QString& text); private: // Data Ui::CreateShortcutDialog* ui; QString InstIconKey; InstancePtr m_instance; bool m_QuickJoinSupported = false; // Functions void stateChanged(); }; PrismLauncher-10.0.5/launcher/ui/dialogs/NewInstanceDialog.ui0000644000175100017510000000444515144136756023537 0ustar runnerrunner NewInstanceDialog Qt::ApplicationModal 0 0 730 127 New Instance :/icons/toolbar/new:/icons/toolbar/new true true &Group: groupBox 128 &Name: instNameTextBox 80 80 Qt::Horizontal iconButton instNameTextBox groupBox PrismLauncher-10.0.5/launcher/ui/dialogs/ExportPackDialog.ui0000644000175100017510000001650615144136756023402 0ustar runnerrunner ExportPackDialog 0 0 650 532 true &Description Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter &Name: name &Version: version 1.0.0 &Author: author &Summary summary 0 0 0 100 16777215 100 true &Options 0 0 0 0 &Recommended Memory: false 0 0 MiB 8 32768 128 Qt::Horizontal 40 20 &Files files true QAbstractItemView::ExtendedSelection true false &Mark disabled files as optional true QDialogButtonBox::Cancel|QDialogButtonBox::Ok files optionalFiles buttonBox accepted() ExportPackDialog accept() 324 390 324 206 buttonBox rejected() ExportPackDialog reject() 324 390 324 206 PrismLauncher-10.0.5/launcher/ui/dialogs/ScrollMessageBox.ui0000644000175100017510000000371615144136756023415 0ustar runnerrunner ScrollMessageBox 0 0 500 455 ScrollMessageBox Qt::RichText Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok true true buttonBox accepted() ScrollMessageBox accept() 199 425 199 227 buttonBox rejected() ScrollMessageBox reject() 199 425 199 227 PrismLauncher-10.0.5/launcher/ui/dialogs/BlockedModsDialog.cpp0000644000175100017510000003705415144136756023656 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // SPDX-FileCopyrightText: 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * Copyright (C) 2022 kumquat-ir <66188216+kumquat-ir@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "BlockedModsDialog.h" #include "ui_BlockedModsDialog.h" #include "Application.h" #include "modplatform/helpers/HashUtils.h" #include #include #include #include #include #include #include #include #include #include #include #include BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods, QString hash_type) : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hashType(hash_type) { m_hashingTask = shared_qobject_ptr( new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); connect(m_hashingTask.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); ui->setupUi(this); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); connect(ui->openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); connect(ui->downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); qDebug() << "[Blocked Mods Dialog] Mods List:" << mods; // defer setup of file system watchers until after the dialog is shown // this allows OS (namely macOS) permission prompts to show after the relevant dialog appears QTimer::singleShot(0, this, [this] { setupWatch(); scanPaths(); update(); }); this->setWindowTitle(title); ui->labelDescription->setText(text); // force all URL handling as external connect(ui->textBrowserWatched, &QTextBrowser::anchorClicked, this, [](const QUrl url) { QDesktopServices::openUrl(url); }); setAcceptDrops(true); update(); } BlockedModsDialog::~BlockedModsDialog() { delete ui; } void BlockedModsDialog::dragEnterEvent(QDragEnterEvent* e) { if (e->mimeData()->hasUrls()) { e->acceptProposedAction(); } } void BlockedModsDialog::dropEvent(QDropEvent* e) { for (QUrl& url : e->mimeData()->urls()) { if (url.scheme().isEmpty()) { // ensure isLocalFile() works correctly url.setScheme("file"); } if (!url.isLocalFile()) { // can't drop external files here. continue; } QString filePath = url.toLocalFile(); qDebug() << "[Blocked Mods Dialog] Dropped file:" << filePath; addHashTask(filePath); // watch for changes QFileInfo file = QFileInfo(filePath); QString path = file.dir().absolutePath(); qDebug() << "[Blocked Mods Dialog] Adding watch path:" << path; m_watcher.addPath(path); } scanPaths(); update(); } void BlockedModsDialog::done(int r) { QDialog::done(r); disconnect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); } void BlockedModsDialog::openAll(bool missingOnly) { for (auto& mod : m_mods) { if (!missingOnly || !mod.matched) { QDesktopServices::openUrl(mod.websiteUrl); } } } void BlockedModsDialog::addDownloadFolder() { QString dir = QFileDialog::getExistingDirectory(this, tr("Select directory where you downloaded the mods"), QStandardPaths::writableLocation(QStandardPaths::DownloadLocation), QFileDialog::ShowDirsOnly); qDebug() << "[Blocked Mods Dialog] Adding watch path:" << dir; m_watcher.addPath(dir); scanPath(dir, true); update(); } /// @brief update UI with current status of the blocked mod detection void BlockedModsDialog::update() { QString text; QString span; for (auto& mod : m_mods) { if (mod.matched) { // ✔ -> html for HEAVY CHECK MARK : ✔ span = QString(tr(" ✔ Found at %1 ")).arg(mod.localPath); } else { // ✘ -> html for HEAVY BALLOT X : ✘ span = QString(tr(" ✘ Not Found ")); } text += QString(tr("%1: %2

    Hash: %3 %4


    ")).arg(mod.name, mod.websiteUrl, mod.hash, span); } ui->textBrowserModsListing->setText(text); QString watching; for (auto& dir : m_watcher.directories()) { QUrl fileURL = QUrl::fromLocalFile(dir); watching += QString("%2
    ").arg(fileURL.toString(), dir); } ui->textBrowserWatched->setText(watching); if (allModsMatched()) { ui->labelModsFound->setText("✔" + tr("All mods found")); ui->openMissingButton->setDisabled(true); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } else { ui->labelModsFound->setText(tr("Please download the missing mods.")); ui->openMissingButton->setDisabled(false); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Skip")); } } /// @brief Signal fired when a watched directory has changed /// @param path the path to the changed directory void BlockedModsDialog::directoryChanged(QString path) { qDebug() << "[Blocked Mods Dialog] Directory changed:" << path; validateMatchedMods(); scanPath(path, true); } /// @brief add the user downloads folder and the global mods folder to the filesystem watcher void BlockedModsDialog::setupWatch() { const QString downloadsFolder = APPLICATION->settings()->get("DownloadsDir").toString(); const QString modsFolder = APPLICATION->settings()->get("CentralModsDir").toString(); const bool downloadsFolderWatchRecursive = APPLICATION->settings()->get("DownloadsDirWatchRecursive").toBool(); watchPath(downloadsFolder, downloadsFolderWatchRecursive); watchPath(modsFolder, true); } void BlockedModsDialog::watchPath(QString path, bool watch_recursive) { auto to_watch = QFileInfo(path); if (!to_watch.isReadable()) { qWarning() << "[Blocked Mods Dialog] Failed to add Watch Path (unable to read):" << path; return; } auto to_watch_path = to_watch.canonicalFilePath(); if (m_watcher.directories().contains(to_watch_path)) return; // don't watch the same path twice (no loops!) qDebug() << "[Blocked Mods Dialog] Adding Watch Path:" << path; m_watcher.addPath(to_watch_path); if (!to_watch.isDir() || !watch_recursive) return; QDirIterator it(to_watch_path, QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot, QDirIterator::NoIteratorFlags); while (it.hasNext()) { QString watch_dir = QDir(it.next()).canonicalPath(); // resolve symlinks and relative paths watchPath(watch_dir, watch_recursive); } } /// @brief scan all watched folder void BlockedModsDialog::scanPaths() { for (auto& dir : m_watcher.directories()) { scanPath(dir, false); } runHashTask(); } /// @brief Scan the directory at path, skip paths that do not contain a file name /// of a blocked mod we are looking for /// @param path the directory to scan void BlockedModsDialog::scanPath(QString path, bool start_task) { QDir scan_dir(path); QDirIterator scan_it(path, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::NoIteratorFlags); while (scan_it.hasNext()) { QString file = scan_it.next(); if (!checkValidPath(file)) { continue; } addHashTask(file); } if (start_task) { runHashTask(); } } /// @brief add a hashing task for the file located at path, add the path to the pending set if the hashing task is already running /// @param path the path to the local file being hashed void BlockedModsDialog::addHashTask(QString path) { qDebug() << "[Blocked Mods Dialog] adding a Hash task for" << path << "to the pending set."; m_pendingHashPaths.insert(path); } /// @brief add a hashing task for the file located at path and connect it to check that hash against /// our blocked mods list /// @param path the path to the local file being hashed void BlockedModsDialog::buildHashTask(QString path) { auto hash_task = Hashing::createHasher(path, m_hashType); qDebug() << "[Blocked Mods Dialog] Creating Hash task for path:" << path; connect(hash_task.get(), &Task::succeeded, this, [this, hash_task, path] { checkMatchHash(hash_task->getResult(), path); }); connect(hash_task.get(), &Task::failed, this, [path] { qDebug() << "Failed to hash path:" << path; }); m_hashingTask->addTask(hash_task); } /// @brief check if the computed hash for the provided path matches a blocked /// mod we are looking for /// @param hash the computed hash for the provided path /// @param path the path to the local file being compared void BlockedModsDialog::checkMatchHash(QString hash, QString path) { bool match = false; qDebug() << "[Blocked Mods Dialog] Checking for match on hash:" << hash << "| From path:" << path; auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath(); auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool(); for (auto& mod : m_mods) { if (mod.matched) { continue; } if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) { mod.matched = true; mod.localPath = path; if (moveFiles) { mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir); } match = true; qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path; break; } } if (match) { update(); } } /// @brief Check if the name of the file at path matches the name of a blocked mod we are searching for /// @param path the path to check /// @return boolean: did the path match the name of a blocked mod? bool BlockedModsDialog::checkValidPath(QString path) { const QFileInfo file = QFileInfo(path); const QString filename = file.fileName(); auto compare = [](QString fsFilename, QString metadataFilename) { return metadataFilename.compare(fsFilename, Qt::CaseInsensitive) == 0; }; // super lax compare (but not fuzzy) // convert to lowercase // convert all speratores to whitespace // simplify sequence of internal whitespace to a single space // efectivly compare two strings ignoring all separators and case auto laxCompare = [](QString fsfilename, QString metadataFilename) { // allowed character seperators QList allowedSeperators = { '-', '+', '.', '_' }; // copy in lowercase auto fsName = fsfilename.toLower(); auto metaName = metadataFilename.toLower(); // replace all potential allowed seperatores with whitespace for (auto sep : allowedSeperators) { fsName = fsName.replace(sep, ' '); metaName = metaName.replace(sep, ' '); } // remove extraneous whitespace fsName = fsName.simplified(); metaName = metaName.simplified(); return fsName.compare(metaName) == 0; }; auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath(); auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool(); for (auto& mod : m_mods) { if (compare(filename, mod.name)) { // if the mod is not yet matched and doesn't have a hash then // just match it with the file that has the exact same name if (!mod.matched && mod.hash.isEmpty()) { mod.matched = true; mod.localPath = path; if (moveFiles) { mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir); } return false; } qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; return true; } if (laxCompare(filename, mod.name)) { qDebug() << "[Blocked Mods Dialog] Lax name match found:" << mod.name << "| From path:" << path; return true; } } return false; } bool BlockedModsDialog::allModsMatched() { return std::all_of(m_mods.begin(), m_mods.end(), [](auto const& mod) { return mod.matched; }); } /// @brief ensure matched file paths still exist void BlockedModsDialog::validateMatchedMods() { bool changed = false; for (auto& mod : m_mods) { if (mod.matched) { QFileInfo file = QFileInfo(mod.localPath); if (!file.exists() || !file.isFile()) { qDebug() << "[Blocked Mods Dialog] File" << mod.localPath << "for mod" << mod.name << "has vanshed! marking as not matched."; mod.localPath = ""; mod.matched = false; changed = true; } } } if (changed) { update(); } } /// @brief run hash task or mark a pending run if it is already running void BlockedModsDialog::runHashTask() { if (!m_hashingTask->isRunning()) { m_rehashPending = false; if (!m_pendingHashPaths.isEmpty()) { qDebug() << "[Blocked Mods Dialog] there are pending hash tasks, building and running tasks"; auto path = m_pendingHashPaths.begin(); while (path != m_pendingHashPaths.end()) { buildHashTask(*path); path = m_pendingHashPaths.erase(path); } m_hashingTask->start(); } } else { qDebug() << "[Blocked Mods Dialog] queueing another run of the hashing task"; qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pendingHashPaths; m_rehashPending = true; } } void BlockedModsDialog::hashTaskFinished() { qDebug() << "[Blocked Mods Dialog] All hash tasks finished"; if (m_rehashPending) { qDebug() << "[Blocked Mods Dialog] task finished with a rehash pending, rerunning"; runHashTask(); } } /// qDebug print support for the BlockedMod struct QDebug operator<<(QDebug debug, const BlockedMod& m) { QDebugStateSaver saver(debug); debug.nospace() << "{ name: " << m.name << ", websiteUrl: " << m.websiteUrl << ", hash: " << m.hash << ", matched: " << m.matched << ", localPath: " << m.localPath << "}"; return debug; } PrismLauncher-10.0.5/launcher/ui/setupwizard/0000755000175100017510000000000015144136757020573 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/setupwizard/PasteWizardPage.cpp0000644000175100017510000000166715144136757024343 0ustar runnerrunner#include "PasteWizardPage.h" #include "ui_PasteWizardPage.h" #include "Application.h" #include "net/PasteUpload.h" PasteWizardPage::PasteWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::PasteWizardPage) { ui->setupUi(this); } PasteWizardPage::~PasteWizardPage() { delete ui; } void PasteWizardPage::initializePage() {} bool PasteWizardPage::validatePage() { auto s = APPLICATION->settings(); QString prevPasteURL = s->get("PastebinURL").toString(); s->reset("PastebinURL"); if (ui->previousSettingsRadioButton->isChecked()) { bool usingDefaultBase = prevPasteURL == PasteUpload::PasteTypes.at(PasteUpload::PasteType::NullPointer).defaultBase; s->set("PastebinType", PasteUpload::PasteType::NullPointer); if (!usingDefaultBase) s->set("PastebinCustomAPIBase", prevPasteURL); } return true; } void PasteWizardPage::retranslate() { ui->retranslateUi(this); } PrismLauncher-10.0.5/launcher/ui/setupwizard/LoginWizardPage.ui0000644000175100017510000000366115144136757024166 0ustar runnerrunner LoginWizardPage 0 0 400 300 Form <html><head/><body><p><span style=" font-size:14pt; font-weight:600;">Add Microsoft account</span></p></body></html> Qt::RichText true In order to play Minecraft, you must have at least one Microsoft account logged in. Do you want to log in now? true Qt::Horizontal Add Microsoft account Qt::Vertical 20 156 PrismLauncher-10.0.5/launcher/ui/setupwizard/JavaWizardPage.cpp0000644000175100017510000000436315144136757024144 0ustar runnerrunner#include "JavaWizardPage.h" #include "Application.h" #include #include #include #include #include #include #include #include #include #include "JavaCommon.h" #include "ui/widgets/JavaWizardWidget.h" #include "ui/widgets/VersionSelectWidget.h" JavaWizardPage::JavaWizardPage(QWidget* parent) : BaseWizardPage(parent) { setupUi(); } void JavaWizardPage::setupUi() { setObjectName(QStringLiteral("javaPage")); QVBoxLayout* layout = new QVBoxLayout(this); m_java_widget = new JavaWizardWidget(this); layout->addWidget(m_java_widget); setLayout(layout); retranslate(); } void JavaWizardPage::refresh() { m_java_widget->refresh(); } void JavaWizardPage::initializePage() { m_java_widget->initialize(); } bool JavaWizardPage::wantsRefreshButton() { return true; } bool JavaWizardPage::validatePage() { auto settings = APPLICATION->settings(); auto result = m_java_widget->validate(); settings->set("AutomaticJavaSwitch", m_java_widget->autoDetectJava()); settings->set("AutomaticJavaDownload", m_java_widget->autoDownloadJava()); settings->set("UserAskedAboutAutomaticJavaDownload", true); switch (result) { default: case JavaWizardWidget::ValidationStatus::Bad: { return false; } case JavaWizardWidget::ValidationStatus::AllOK: { settings->set("JavaPath", m_java_widget->javaPath()); } /* fallthrough */ case JavaWizardWidget::ValidationStatus::JavaBad: { // Memory auto s = APPLICATION->settings(); s->set("MinMemAlloc", m_java_widget->minHeapSize()); s->set("MaxMemAlloc", m_java_widget->maxHeapSize()); if (m_java_widget->permGenEnabled()) { s->set("PermGen", m_java_widget->permGenSize()); } else { s->reset("PermGen"); } return true; } } } void JavaWizardPage::retranslate() { setTitle(tr("Java")); setSubTitle( tr("Please select how much memory to allocate to instances and if Prism Launcher should manage Java automatically or manually.")); m_java_widget->retranslate(); } PrismLauncher-10.0.5/launcher/ui/setupwizard/AutoJavaWizardPage.cpp0000644000175100017510000000133015144136757024764 0ustar runnerrunner#include "AutoJavaWizardPage.h" #include "ui_AutoJavaWizardPage.h" #include "Application.h" AutoJavaWizardPage::AutoJavaWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::AutoJavaWizardPage) { ui->setupUi(this); } AutoJavaWizardPage::~AutoJavaWizardPage() { delete ui; } void AutoJavaWizardPage::initializePage() {} bool AutoJavaWizardPage::validatePage() { auto s = APPLICATION->settings(); if (!ui->previousSettingsRadioButton->isChecked()) { s->set("AutomaticJavaSwitch", true); s->set("AutomaticJavaDownload", true); } s->set("UserAskedAboutAutomaticJavaDownload", true); return true; } void AutoJavaWizardPage::retranslate() { ui->retranslateUi(this); } PrismLauncher-10.0.5/launcher/ui/setupwizard/BaseWizardPage.h0000644000175100017510000000105615144136757023576 0ustar runnerrunner#pragma once #include #include class BaseWizardPage : public QWizardPage { public: explicit BaseWizardPage(QWidget* parent = Q_NULLPTR) : QWizardPage(parent) {} virtual ~BaseWizardPage() {}; virtual bool wantsRefreshButton() { return false; } virtual void refresh() {} protected: virtual void retranslate() = 0; void changeEvent(QEvent* event) override { if (event->type() == QEvent::LanguageChange) { retranslate(); } QWizardPage::changeEvent(event); } }; PrismLauncher-10.0.5/launcher/ui/setupwizard/PasteWizardPage.ui0000644000175100017510000000377015144136757024173 0ustar runnerrunner PasteWizardPage 0 0 400 300 Form The default paste service has changed to mclo.gs, please choose what you want to do with your settings. true Qt::Horizontal Use new default service true buttonGroup Keep previous settings false buttonGroup Qt::Vertical 20 156 PrismLauncher-10.0.5/launcher/ui/setupwizard/PasteWizardPage.h0000644000175100017510000000101315144136757023771 0ustar runnerrunner#ifndef PASTEDEFAULTSCONFIRMATIONWIZARD_H #define PASTEDEFAULTSCONFIRMATIONWIZARD_H #include #include "BaseWizardPage.h" namespace Ui { class PasteWizardPage; } class PasteWizardPage : public BaseWizardPage { Q_OBJECT public: explicit PasteWizardPage(QWidget* parent = nullptr); ~PasteWizardPage(); void initializePage() override; bool validatePage() override; void retranslate() override; private: Ui::PasteWizardPage* ui; }; #endif // PASTEDEFAULTSCONFIRMATIONWIZARD_H PrismLauncher-10.0.5/launcher/ui/setupwizard/AutoJavaWizardPage.ui0000644000175100017510000000506715144136757024632 0ustar runnerrunner AutoJavaWizardPage 0 0 400 300 Form <html><head/><body><p><span style=" font-size:14pt; font-weight:600;">New Feature Alert!</span></p></body></html> Qt::RichText true We've added a feature to automatically download the correct Java version for each version of Minecraft (this can be changed in the Java Settings). Would you like to enable or disable this feature? true Qt::Horizontal Enable Auto-Download true buttonGroup Disable Auto-Download false buttonGroup Qt::Vertical 20 156 PrismLauncher-10.0.5/launcher/ui/setupwizard/SetupWizard.h0000644000175100017510000000210515144136757023223 0ustar runnerrunner/* Copyright 2017-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include namespace Ui { class SetupWizard; } class BaseWizardPage; class SetupWizard : public QWizard { Q_OBJECT public: /* con/destructors */ explicit SetupWizard(QWidget* parent = 0); virtual ~SetupWizard(); void changeEvent(QEvent* event) override; BaseWizardPage* getBasePage(int id); BaseWizardPage* getCurrentBasePage(); private slots: void pageChanged(int id); private: /* methods */ void retranslate(); }; PrismLauncher-10.0.5/launcher/ui/setupwizard/ThemeWizardPage.h0000644000175100017510000000276715144136757024000 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include "BaseWizardPage.h" class ThemeWizardPage : public BaseWizardPage { Q_OBJECT public: ThemeWizardPage(QWidget* parent = nullptr) : BaseWizardPage(parent) { auto layout = new QVBoxLayout(this); layout->addWidget(&widget); layout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); setTitle(tr("Appearance")); setSubTitle(tr("Select theme and icons to use")); } bool validatePage() override { return true; }; void retranslate() override { widget.retranslateUi(); } private: AppearanceWidget widget{ true }; }; PrismLauncher-10.0.5/launcher/ui/setupwizard/LoginWizardPage.cpp0000644000175100017510000000171515144136757024331 0ustar runnerrunner#include "LoginWizardPage.h" #include "minecraft/auth/AccountList.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui_LoginWizardPage.h" #include "Application.h" LoginWizardPage::LoginWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::LoginWizardPage) { ui->setupUi(this); } LoginWizardPage::~LoginWizardPage() { delete ui; } void LoginWizardPage::initializePage() {} bool LoginWizardPage::validatePage() { return true; } void LoginWizardPage::retranslate() { ui->retranslateUi(this); } void LoginWizardPage::on_pushButton_clicked() { wizard()->hide(); auto account = MSALoginDialog::newAccount(nullptr); wizard()->show(); if (account) { APPLICATION->accounts()->addAccount(account); APPLICATION->accounts()->setDefaultAccount(account); if (wizard()->currentId() == wizard()->pageIds().last()) { wizard()->accept(); } else { wizard()->next(); } } } PrismLauncher-10.0.5/launcher/ui/setupwizard/JavaWizardPage.h0000644000175100017510000000102715144136757023603 0ustar runnerrunner#pragma once #include "BaseWizardPage.h" class JavaWizardWidget; class JavaWizardPage : public BaseWizardPage { Q_OBJECT public: explicit JavaWizardPage(QWidget* parent = Q_NULLPTR); virtual ~JavaWizardPage() = default; bool wantsRefreshButton() override; void refresh() override; void initializePage() override; bool validatePage() override; protected: /* methods */ void setupUi(); void retranslate() override; private: /* data */ JavaWizardWidget* m_java_widget = nullptr; }; PrismLauncher-10.0.5/launcher/ui/setupwizard/LoginWizardPage.h0000644000175100017510000000071115144136757023771 0ustar runnerrunner#pragma once #include #include "BaseWizardPage.h" namespace Ui { class LoginWizardPage; } class LoginWizardPage : public BaseWizardPage { Q_OBJECT public: explicit LoginWizardPage(QWidget* parent = nullptr); ~LoginWizardPage(); void initializePage() override; bool validatePage() override; void retranslate() override; private slots: void on_pushButton_clicked(); private: Ui::LoginWizardPage* ui; }; PrismLauncher-10.0.5/launcher/ui/setupwizard/LanguageWizardPage.cpp0000644000175100017510000000222015144136757024774 0ustar runnerrunner#include "LanguageWizardPage.h" #include #include #include #include #include "ui/widgets/LanguageSelectionWidget.h" LanguageWizardPage::LanguageWizardPage(QWidget* parent) : BaseWizardPage(parent) { setObjectName(QStringLiteral("languagePage")); auto layout = new QVBoxLayout(this); mainWidget = new LanguageSelectionWidget(this); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(mainWidget); retranslate(); } LanguageWizardPage::~LanguageWizardPage() {} bool LanguageWizardPage::wantsRefreshButton() { return true; } void LanguageWizardPage::refresh() { auto translations = APPLICATION->translations(); translations->downloadIndex(); } bool LanguageWizardPage::validatePage() { auto settings = APPLICATION->settings(); QString key = mainWidget->getSelectedLanguageKey(); settings->set("Language", key); return true; } void LanguageWizardPage::retranslate() { setTitle(tr("Language")); setSubTitle(tr("Select the language to use in %1").arg(BuildConfig.LAUNCHER_DISPLAYNAME)); mainWidget->retranslate(); } PrismLauncher-10.0.5/launcher/ui/setupwizard/AutoJavaWizardPage.h0000644000175100017510000000064415144136757024440 0ustar runnerrunner#pragma once #include #include "BaseWizardPage.h" namespace Ui { class AutoJavaWizardPage; } class AutoJavaWizardPage : public BaseWizardPage { Q_OBJECT public: explicit AutoJavaWizardPage(QWidget* parent = nullptr); ~AutoJavaWizardPage(); void initializePage() override; bool validatePage() override; void retranslate() override; private: Ui::AutoJavaWizardPage* ui; }; PrismLauncher-10.0.5/launcher/ui/setupwizard/SetupWizard.cpp0000644000175100017510000000434015144136757023561 0ustar runnerrunner#include "SetupWizard.h" #include "JavaWizardPage.h" #include "LanguageWizardPage.h" #include #include #include "translations/TranslationsModel.h" #include #include SetupWizard::SetupWizard(QWidget* parent) : QWizard(parent) { setObjectName(QStringLiteral("SetupWizard")); resize(620, 660); setMinimumSize(300, 400); // make it ugly everywhere to avoid variability in theming setWizardStyle(QWizard::ClassicStyle); setOptions(QWizard::NoCancelButton | QWizard::IndependentPages | QWizard::HaveCustomButton1); retranslate(); connect(this, &QWizard::currentIdChanged, this, &SetupWizard::pageChanged); } void SetupWizard::retranslate() { setButtonText(QWizard::NextButton, tr("&Next >")); setButtonText(QWizard::BackButton, tr("< &Back")); setButtonText(QWizard::FinishButton, tr("&Finish")); setButtonText(QWizard::CustomButton1, tr("&Refresh")); setWindowTitle(tr("%1 Quick Setup").arg(BuildConfig.LAUNCHER_DISPLAYNAME)); } BaseWizardPage* SetupWizard::getBasePage(int id) { if (id == -1) return nullptr; auto pagePtr = page(id); if (!pagePtr) return nullptr; return dynamic_cast(pagePtr); } BaseWizardPage* SetupWizard::getCurrentBasePage() { return getBasePage(currentId()); } void SetupWizard::pageChanged(int id) { auto basePagePtr = getBasePage(id); if (!basePagePtr) { return; } if (basePagePtr->wantsRefreshButton()) { setButtonLayout({ QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton }); auto customButton = button(QWizard::CustomButton1); connect(customButton, &QAbstractButton::clicked, [this]() { auto basePagePtr = getCurrentBasePage(); if (basePagePtr) { basePagePtr->refresh(); } }); } else { setButtonLayout({ QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton }); } } void SetupWizard::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { retranslate(); } QWizard::changeEvent(event); } SetupWizard::~SetupWizard() {} PrismLauncher-10.0.5/launcher/ui/setupwizard/LanguageWizardPage.h0000644000175100017510000000072515144136757024451 0ustar runnerrunner#pragma once #include "BaseWizardPage.h" class LanguageSelectionWidget; class LanguageWizardPage : public BaseWizardPage { Q_OBJECT public: explicit LanguageWizardPage(QWidget* parent = Q_NULLPTR); virtual ~LanguageWizardPage(); bool wantsRefreshButton() override; void refresh() override; bool validatePage() override; protected: void retranslate() override; private: LanguageSelectionWidget* mainWidget = nullptr; }; PrismLauncher-10.0.5/launcher/ui/InstanceWindow.cpp0000644000175100017510000001653415144136756021662 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "InstanceWindow.h" #include "Application.h" #include #include #include #include #include #include "ui/widgets/PageContainer.h" #include "InstancePageProvider.h" #include "icons/IconList.h" InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWindow(parent), m_instance(instance) { setAttribute(Qt::WA_DeleteOnClose); auto icon = APPLICATION->icons()->getIcon(m_instance->iconKey()); QString windowTitle = tr("Console window for ") + m_instance->name(); // Set window properties { setWindowIcon(icon); setWindowTitle(windowTitle); } // Add page container { auto provider = std::make_shared(m_instance); m_container = new PageContainer(provider.get(), "console", this); m_container->setParentContainer(this); setCentralWidget(m_container); setContentsMargins(0, 0, 0, 0); } // Add custom buttons to the page container layout. { auto horizontalLayout = new QHBoxLayout(this); horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); horizontalLayout->setContentsMargins(0, 0, 6, 6); auto btnHelp = new QPushButton(this); btnHelp->setText(tr("Help")); horizontalLayout->addWidget(btnHelp); connect(btnHelp, &QPushButton::clicked, m_container, &PageContainer::help); auto spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); horizontalLayout->addSpacerItem(spacer); m_launchButton = new QToolButton(this); m_launchButton->setText(tr("&Launch")); m_launchButton->setToolTip(tr("Launch the instance")); m_launchButton->setPopupMode(QToolButton::MenuButtonPopup); m_launchButton->setMinimumWidth(80); // HACK!! horizontalLayout->addWidget(m_launchButton); connect(m_launchButton, &QPushButton::clicked, this, [this] { APPLICATION->launch(m_instance); }); m_killButton = new QPushButton(this); m_killButton->setText(tr("&Kill")); m_killButton->setToolTip(tr("Kill the running instance")); m_killButton->setShortcut(QKeySequence(tr("Ctrl+K"))); horizontalLayout->addWidget(m_killButton); connect(m_killButton, &QPushButton::clicked, this, [this] { APPLICATION->kill(m_instance); }); updateButtons(); m_closeButton = new QPushButton(this); m_closeButton->setText(tr("Close")); horizontalLayout->addWidget(m_closeButton); connect(m_closeButton, &QPushButton::clicked, this, &QMainWindow::close); m_container->addButtons(horizontalLayout); connect(m_instance.get(), &BaseInstance::profilerChanged, this, &InstanceWindow::updateButtons); connect(APPLICATION, &Application::globalSettingsApplied, this, &InstanceWindow::updateButtons); } // restore window state { auto base64State = APPLICATION->settings()->get("ConsoleWindowState").toString().toUtf8(); restoreState(QByteArray::fromBase64(base64State)); auto base64Geometry = APPLICATION->settings()->get("ConsoleWindowGeometry").toString().toUtf8(); restoreGeometry(QByteArray::fromBase64(base64Geometry)); } // set up instance and launch process recognition { auto launchTask = m_instance->getLaunchTask(); instanceLaunchTaskChanged(launchTask); connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &InstanceWindow::instanceLaunchTaskChanged); connect(m_instance.get(), &BaseInstance::runningStatusChanged, this, &InstanceWindow::runningStateChanged); } // set up instance destruction detection { connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged); } // add ourself as the modpack page's instance window { static_cast(m_container->getPage("managed_pack"))->setInstanceWindow(this); } show(); } void InstanceWindow::on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus) { if (newStatus == BaseInstance::Status::Gone) { m_doNotSave = true; close(); } } void InstanceWindow::updateButtons() { m_launchButton->setEnabled(m_instance->canLaunch()); m_killButton->setEnabled(m_instance->isRunning()); QMenu* launchMenu = m_launchButton->menu(); if (launchMenu) launchMenu->clear(); else launchMenu = new QMenu(this); m_instance->populateLaunchMenu(launchMenu); m_launchButton->setMenu(launchMenu); } void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr proc) { m_proc = proc; } void InstanceWindow::runningStateChanged(bool running) { updateButtons(); m_container->refreshContainer(); if (running) { selectPage("log"); } } void InstanceWindow::closeEvent(QCloseEvent* event) { bool proceed = true; if (!m_doNotSave) { proceed &= m_container->prepareToClose(); } if (!proceed) { return; } APPLICATION->settings()->set("ConsoleWindowState", QString::fromUtf8(saveState().toBase64())); APPLICATION->settings()->set("ConsoleWindowGeometry", QString::fromUtf8(saveGeometry().toBase64())); emit isClosing(); event->accept(); } bool InstanceWindow::saveAll() { return m_container->saveAll(); } QString InstanceWindow::instanceId() { return m_instance->id(); } bool InstanceWindow::selectPage(QString pageId) { return m_container->selectPage(pageId); } void InstanceWindow::refreshContainer() { m_container->refreshContainer(); } BasePage* InstanceWindow::selectedPage() const { return m_container->selectedPage(); } bool InstanceWindow::requestClose() { if (m_container->prepareToClose()) { close(); return true; } return false; } PrismLauncher-10.0.5/launcher/ui/GuiUtil.cpp0000644000175100017510000002344415144136756020306 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Lenny McLennington * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "GuiUtil.h" #include #include #include #include #include #include "FileSystem.h" #include "logs/AnonymizeLog.h" #include "net/NetJob.h" #include "net/NetRequest.h" #include "net/PasteUpload.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include #include #include #include "Application.h" constexpr int MaxMclogsLines = 25000; constexpr int InitialMclogsLines = 10000; constexpr int FinalMclogsLines = 14900; QString truncateLogForMclogs(const QString& logContent) { QStringList lines = logContent.split("\n"); if (lines.size() > MaxMclogsLines) { QString truncatedLog = lines.mid(0, InitialMclogsLines).join("\n"); truncatedLog += "\n\n\n\n\n\n\n\n\n\n" "------------------------------------------------------------\n" "----------------------- Log truncated ----------------------\n" "------------------------------------------------------------\n" "----- Middle portion omitted to fit mclo.gs size limits ----\n" "------------------------------------------------------------\n" "\n\n\n\n\n\n\n\n\n\n"; truncatedLog += lines.mid(lines.size() - FinalMclogsLines - 1).join("\n"); return truncatedLog; } return logContent; } std::optional GuiUtil::uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget) { return uploadPaste(name, FS::read(filePath.absoluteFilePath()), parentWidget); }; std::optional GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget) { ProgressDialog dialog(parentWidget); auto pasteType = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); auto baseURL = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); bool shouldTruncate = false; if (baseURL.isEmpty()) baseURL = PasteUpload::PasteTypes[pasteType].defaultBase; if (auto url = QUrl(baseURL); url.isValid()) { auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), QObject::tr("You are about to upload \"%1\" to %2.\n" "You should double-check for personal information.\n\n" "Are you sure?") .arg(name, url.host()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return {}; if (baseURL == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) { auto truncateResponse = CustomMessageBox::selectable( parentWidget, QObject::tr("Confirm Truncation"), QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n" "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n" "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off " "potentially useful info like crashes at the end.\n\n" "Proceed with truncation?") .arg(text.count("\n")) .arg(MaxMclogsLines) .arg(InitialMclogsLines) .arg(FinalMclogsLines), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) ->exec(); if (truncateResponse == QMessageBox::Cancel) { return {}; } shouldTruncate = truncateResponse == QMessageBox::Yes; } } QString textToUpload = text; if (shouldTruncate) { textToUpload = truncateLogForMclogs(text); } auto job = NetJob::Ptr(new NetJob("Log Upload", APPLICATION->network())); auto pasteJob = new PasteUpload(textToUpload, baseURL, pasteType); job->addNetAction(Net::NetRequest::Ptr(pasteJob)); QObject::connect(job.get(), &Task::failed, [parentWidget](QString reason) { CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), reason, QMessageBox::Critical)->show(); }); QObject::connect(job.get(), &Task::aborted, [parentWidget] { CustomMessageBox::selectable(parentWidget, QObject::tr("Logs upload aborted"), QObject::tr("The task has been aborted by the user."), QMessageBox::Information) ->show(); }); if (dialog.execWithTask(job.get()) == QDialog::Accepted) { if (pasteJob->pasteLink().isEmpty()) { CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), "The upload link is empty", QMessageBox::Critical) ->show(); return {}; } setClipboardText(pasteJob->pasteLink()); CustomMessageBox::selectable( parentWidget, QObject::tr("Upload finished"), QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(pasteJob->pasteLink()), QMessageBox::Information) ->exec(); return pasteJob->pasteLink(); } return {}; } void GuiUtil::setClipboardText(QString text) { anonymizeLog(text); QApplication::clipboard()->setText(text); } static QStringList BrowseForFileInternal(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget, bool single) { static QMap savedPaths; QFileDialog w(parentWidget, caption); QSet locations; auto f = [&locations](QStandardPaths::StandardLocation l) { QString location = QStandardPaths::writableLocation(l); QFileInfo finfo(location); if (!finfo.exists()) { return; } locations.insert(location); }; f(QStandardPaths::DesktopLocation); f(QStandardPaths::DocumentsLocation); f(QStandardPaths::DownloadLocation); f(QStandardPaths::HomeLocation); QList urls; for (auto location : locations) { urls.append(QUrl::fromLocalFile(location)); } urls.append(QUrl::fromLocalFile(defaultPath)); w.setFileMode(single ? QFileDialog::ExistingFile : QFileDialog::ExistingFiles); w.setAcceptMode(QFileDialog::AcceptOpen); w.setNameFilter(filter); QString pathToOpen; if (savedPaths.contains(context)) { pathToOpen = savedPaths[context]; } else { pathToOpen = defaultPath; } if (!pathToOpen.isEmpty()) { QFileInfo finfo(pathToOpen); if (finfo.exists() && finfo.isDir()) { w.setDirectory(finfo.absoluteFilePath()); } } w.setSidebarUrls(urls); if (w.exec()) { savedPaths[context] = w.directory().absolutePath(); return w.selectedFiles(); } savedPaths[context] = w.directory().absolutePath(); return {}; } QString GuiUtil::BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget) { auto resultList = BrowseForFileInternal(context, caption, filter, defaultPath, parentWidget, true); if (resultList.size()) { return resultList[0]; } return QString(); } QStringList GuiUtil::BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget) { return BrowseForFileInternal(context, caption, filter, defaultPath, parentWidget, false); } PrismLauncher-10.0.5/launcher/ui/ViewLogWindow.h0000644000175100017510000000053215144136756021126 0ustar runnerrunner#pragma once #include #include "Application.h" class OtherLogsPage; class ViewLogWindow : public QMainWindow { Q_OBJECT public: explicit ViewLogWindow(QWidget* parent = nullptr); signals: void isClosing(); protected: void closeEvent(QCloseEvent*) override; private: OtherLogsPage* m_page; }; PrismLauncher-10.0.5/launcher/ui/pages/0000755000175100017510000000000015144136757017311 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/instance/0000755000175100017510000000000015144136757021115 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/instance/GameOptionsPage.h0000644000175100017510000000451115144136756024310 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "ui/pages/BasePage.h" namespace Ui { class GameOptionsPage; } class GameOptions; class MinecraftInstance; class GameOptionsPage : public QWidget, public BasePage { Q_OBJECT public: explicit GameOptionsPage(MinecraftInstance* inst, QWidget* parent = 0); virtual ~GameOptionsPage(); void openedImpl() override; void closedImpl() override; virtual QString displayName() const override { return tr("Game Options"); } virtual QIcon icon() const override { return QIcon::fromTheme("settings"); } virtual QString id() const override { return "gameoptions"; } virtual QString helpPage() const override { return "Game-Options-management"; } void retranslate() override; private: // data Ui::GameOptionsPage* ui = nullptr; std::shared_ptr m_model; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/ShaderPackPage.cpp0000644000175100017510000002510015144136756024420 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ShaderPackPage.h" #include "ui_ExternalResourcesPage.h" #include "ResourceDownloadTask.h" #include "minecraft/mod/ShaderPackFolderModel.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" ShaderPackPage::ShaderPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Packs")); ui->actionDownloadItem->setToolTip(tr("Download shader packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); connect(ui->actionDownloadItem, &QAction::triggered, this, &ShaderPackPage::downloadShaderPack); ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected shader packs (all shader packs if none are selected)")); connect(ui->actionUpdateItem, &QAction::triggered, this, &ShaderPackPage::updateShaderPacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); auto updateMenu = new QMenu(this); auto update = updateMenu->addAction(ui->actionUpdateItem->text()); connect(update, &QAction::triggered, this, &ShaderPackPage::updateShaderPacks); updateMenu->addAction(ui->actionResetItemMetadata); connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ShaderPackPage::deleteShaderPackMetadata); ui->actionUpdateItem->setMenu(updateMenu); ui->actionChangeVersion->setToolTip(tr("Change a shader pack's version.")); connect(ui->actionChangeVersion, &QAction::triggered, this, &ShaderPackPage::changeShaderPackVersion); ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); } void ShaderPackPage::downloadShaderPack() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); m_downloadDialog->open(); } void ShaderPackPage::downloadDialogFinished(int result) { if (result) { auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); tasks->deleteLater(); }); if (m_downloadDialog) { for (auto& task : m_downloadDialog->getTasks()) { tasks->addTask(task); } } else { qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } if (m_downloadDialog) m_downloadDialog->deleteLater(); } void ShaderPackPage::updateShaderPacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Shader pack updates are unavailable when metadata is disabled!")); return; } if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Update"), tr("Updating shader packs while the game is running may pack duplication and game crashes.\n" "The old files may not be deleted as they are in use.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto mods_list = m_model->selectedResources(selection); bool use_all = mods_list.empty(); if (use_all) mods_list = m_model->allResources(); ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The shader pack updater was aborted!"), QMessageBox::Warning)->show(); return; } if (update_dialog.noUpdates()) { QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; if (mods_list.size() > 1) { if (use_all) { message = tr("All shader packs are up-to-date! :)"); } else { message = tr("All selected shader packs are up-to-date! :)"); } } CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); return; } if (update_dialog.exec()) { auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } tasks->deleteLater(); }); for (auto task : update_dialog.getTasks()) { tasks->addTask(task); } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } } void ShaderPackPage::deleteShaderPackMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedShaderPacks(selection).length(); if (selectionCount == 0) return; if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 shader packs.\n" "Are you sure?") .arg(selectionCount), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } m_model->deleteMetadata(selection); } void ShaderPackPage::changeShaderPackVersion() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Shader pack updates are unavailable when metadata is disabled!")); return; } const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); if (rows.count() != 1) return; Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); if (resource.metadata() == nullptr) return; m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); m_downloadDialog->setResourceMetadata(resource.metadata()); m_downloadDialog->open(); } PrismLauncher-10.0.5/launcher/ui/pages/instance/McClient.cpp0000644000175100017510000001154115144136756023320 0ustar runnerrunner#include #include #include #include #include #include "Json.h" #include "McClient.h" // 7 first bits #define SEGMENT_BITS 0x7F // last bit #define CONTINUE_BIT 0x80 McClient::McClient(QObject* parent, QString domain, QString ip, short port) : QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {} void McClient::getStatusData() { qDebug() << "Connecting to socket.."; connect(&m_socket, &QTcpSocket::connected, this, [this]() { qDebug() << "Connected to socket successfully"; sendRequest(); connect(&m_socket, &QTcpSocket::readyRead, this, &McClient::readRawResponse); }); connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() { emitFail("Socket disconnected: " + m_socket.errorString()); }); m_socket.connectToHost(m_ip, m_port); } void McClient::sendRequest() { QByteArray data; writeVarInt(data, 0x00); // packet ID writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1) writeVarInt(data, m_domain.size()); // server address length writeString(data, m_domain.toStdString()); // server address writeFixedInt(data, m_port, 2); // server port writeVarInt(data, 0x01); // next state writePacketToSocket(data); // send handshake packet writeVarInt(data, 0x00); // packet ID writePacketToSocket(data); // send status packet } void McClient::readRawResponse() { if (m_responseReadState == 2) { return; } m_resp.append(m_socket.readAll()); if (m_responseReadState == 0 && m_resp.size() >= 5) { m_wantedRespLength = readVarInt(m_resp); m_responseReadState = 1; } if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) { if (m_resp.size() > m_wantedRespLength) { qDebug().nospace() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " << m_resp.size() << " received)"; } parseResponse(); m_responseReadState = 2; } } void McClient::parseResponse() { qDebug() << "Received response successfully"; int packetID = readVarInt(m_resp); if (packetID != 0x00) { throw Exception(QString("Packet ID doesn't match expected value (0x00 vs 0x%1)").arg(packetID, 0, 16)); } Q_UNUSED(readVarInt(m_resp)); // json length // 'resp' should now be the JSON string QJsonParseError parseError; QJsonDocument doc = Json::parseUntilGarbage(m_resp, &parseError); if (parseError.error != QJsonParseError::NoError) { qDebug() << "Failed to parse JSON:" << parseError.errorString(); emitFail(parseError.errorString()); return; } emitSucceed(doc.object()); } // From https://wiki.vg/Protocol#VarInt_and_VarLong void McClient::writeVarInt(QByteArray& data, int value) { while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits // Write 7 bits data.append((value & SEGMENT_BITS) | CONTINUE_BIT); // Erase theses 7 bits from the value to write // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone value >>= 7; } data.append(value); } // From https://wiki.vg/Protocol#VarInt_and_VarLong int McClient::readVarInt(QByteArray& data) { int value = 0; int position = 0; char currentByte; while (position < 32) { currentByte = readByte(data); value |= (currentByte & SEGMENT_BITS) << position; if ((currentByte & CONTINUE_BIT) == 0) break; position += 7; } if (position >= 32) throw Exception("VarInt is too big"); return value; } char McClient::readByte(QByteArray& data) { if (data.isEmpty()) { throw Exception("No more bytes to read"); } char byte = data.at(0); data.remove(0, 1); return byte; } // write number with specified size in big endian format void McClient::writeFixedInt(QByteArray& data, int value, int size) { for (int i = size - 1; i >= 0; i--) { data.append((value >> (i * 8)) & 0xFF); } } void McClient::writeString(QByteArray& data, const std::string& value) { data.append(value.c_str()); } void McClient::writePacketToSocket(QByteArray& data) { // we prefix the packet with its length QByteArray dataWithSize; writeVarInt(dataWithSize, data.size()); dataWithSize.append(data); // write it to the socket m_socket.write(dataWithSize); m_socket.flush(); data.clear(); } void McClient::emitFail(QString error) { qDebug() << "Minecraft server ping for status error:" << error; emit failed(error); emit finished(); } void McClient::emitSucceed(QJsonObject data) { emit succeeded(data); emit finished(); } PrismLauncher-10.0.5/launcher/ui/pages/instance/NotesPage.ui0000644000175100017510000000217515144136756023345 0ustar runnerrunner NotesPage 0 0 731 538 0 0 0 true false Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse noteEditor PrismLauncher-10.0.5/launcher/ui/pages/instance/ModFolderPage.cpp0000644000175100017510000004215715144136756024301 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModFolderPage.h" #include "ui/dialogs/ExportToModListDialog.h" #include "ui/dialogs/InstallLoaderDialog.h" #include "ui_ExternalResourcesPage.h" #include #include #include #include #include #include #include #include #include #include "Application.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFilterData.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModFolderModel.h" #include "tasks/ConcurrentTask.h" #include "tasks/Task.h" #include "ui/dialogs/ProgressDialog.h" ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr model, QWidget* parent) : ExternalResourcesPage(inst, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Mods")); ui->actionDownloadItem->setToolTip(tr("Download mods from online mod platforms")); ui->actionDownloadItem->setEnabled(true); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::downloadMods); ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)")); connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); auto updateMenu = new QMenu(this); auto update = updateMenu->addAction(tr("Check for Updates")); connect(update, &QAction::triggered, this, &ModFolderPage::updateMods); updateMenu->addAction(ui->actionVerifyItemDependencies); connect(ui->actionVerifyItemDependencies, &QAction::triggered, this, [this] { updateMods(true); }); auto depsDisabled = APPLICATION->settings()->getSetting("ModDependenciesDisabled"); ui->actionVerifyItemDependencies->setVisible(!depsDisabled->get().toBool()); connect(depsDisabled.get(), &Setting::SettingChanged, this, [this](const Setting& setting, const QVariant& value) { ui->actionVerifyItemDependencies->setVisible(!value.toBool()); }); updateMenu->addAction(ui->actionResetItemMetadata); connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); ui->actionUpdateItem->setMenu(updateMenu); ui->actionChangeVersion->setToolTip(tr("Change a mod's version.")); connect(ui->actionChangeVersion, &QAction::triggered, this, &ModFolderPage::changeModVersion); ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); ui->actionViewHomepage->setToolTip(tr("View the homepages of all selected mods.")); ui->actionExportMetadata->setToolTip(tr("Export mod's metadata to text.")); connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata); ui->actionsToolbar->insertActionAfter(ui->actionViewHomepage, ui->actionExportMetadata); ui->actionsToolbar->insertActionAfter(ui->actionViewFolder, ui->actionViewConfigs); } bool ModFolderPage::shouldDisplay() const { return true; } void ModFolderPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); const Mod& mod = m_model->at(row); ui->frame->updateWithMod(mod); } void ModFolderPage::removeItems(const QItemSelection& selection) { if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Delete"), tr("If you remove mods while the game is running it may crash your game.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } m_model->deleteResources(selection.indexes()); } void ModFolderPage::downloadMods() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { if (handleNoModLoader()) { return; } } m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); m_downloadDialog->open(); } void ModFolderPage::downloadDialogFinished(int result) { if (result) { auto tasks = new ConcurrentTask(tr("Download Mods"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); tasks->deleteLater(); }); if (m_downloadDialog) { for (auto& task : m_downloadDialog->getTasks()) { tasks->addTask(task); } } else { qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } if (m_downloadDialog) m_downloadDialog->deleteLater(); } void ModFolderPage::updateMods(bool includeDeps) { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { if (handleNoModLoader()) { return; } } if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!")); return; } if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Update"), tr("Updating mods while the game is running may cause mod duplication and game crashes.\n" "The old files may not be deleted as they are in use.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto mods_list = m_model->selectedResources(selection); bool use_all = mods_list.empty(); if (use_all) mods_list = m_model->allResources(); ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps, profile->getModLoadersList()); update_dialog.checkCandidates(); if (update_dialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The mod updater was aborted!"), QMessageBox::Warning)->show(); return; } if (update_dialog.noUpdates()) { QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; if (mods_list.size() > 1) { if (use_all) { message = tr("All mods are up-to-date! :)"); } else { message = tr("All selected mods are up-to-date! :)"); } } CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); return; } if (update_dialog.exec()) { auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } tasks->deleteLater(); }); for (auto task : update_dialog.getTasks()) { tasks->addTask(task); } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } } void ModFolderPage::deleteModMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedMods(selection).length(); if (selectionCount == 0) return; if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 mods.\n" "Are you sure?") .arg(selectionCount), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } m_model->deleteMetadata(selection); } void ModFolderPage::changeModVersion() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); if (!profile->getModLoaders().has_value()) { if (handleNoModLoader()) { return; } } if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Mod updates are unavailable when metadata is disabled!")); return; } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto mods_list = m_model->selectedMods(selection); if (mods_list.length() != 1 || mods_list[0]->metadata() == nullptr) return; m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); m_downloadDialog->setResourceMetadata((*mods_list.begin())->metadata()); m_downloadDialog->open(); } void ModFolderPage::exportModMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectedMods = m_model->selectedMods(selection); if (selectedMods.length() == 0) selectedMods = m_model->allMods(); std::sort(selectedMods.begin(), selectedMods.end(), [](const Mod* a, const Mod* b) { return a->name() < b->name(); }); ExportToModListDialog dlg(m_instance->name(), selectedMods, this); dlg.exec(); } CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) : ModFolderPage(inst, mods, parent) { auto mcInst = dynamic_cast(m_instance); if (mcInst) { auto version = mcInst->getPackProfile(); if (version && version->getComponent("net.minecraftforge") && version->getComponent("net.minecraft")) { auto minecraftCmp = version->getComponent("net.minecraft"); if (!minecraftCmp->m_loaded) { version->reload(Net::Mode::Offline); auto update = version->getCurrentTask(); if (update) { connect(update.get(), &Task::finished, this, [this] { if (m_container) { m_container->refreshContainer(); } }); if (!update->isRunning()) { update->start(); } } } } } } bool CoreModFolderPage::shouldDisplay() const { if (ModFolderPage::shouldDisplay()) { auto inst = dynamic_cast(m_instance); if (!inst) return true; auto version = inst->getPackProfile(); if (!version || !version->getComponent("net.minecraftforge") || !version->getComponent("net.minecraft")) return false; auto minecraftCmp = version->getComponent("net.minecraft"); return minecraftCmp->m_loaded && minecraftCmp->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; } return false; } NilModFolderPage::NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) : ModFolderPage(inst, mods, parent) {} bool NilModFolderPage::shouldDisplay() const { return m_model->dir().exists(); } // Helper function so this doesn't need to be duplicated 3 times inline bool ModFolderPage::handleNoModLoader() { int resp = QMessageBox::question(this, this->tr("Missing Mod Loader"), this->tr("You need to install a compatible mod loader before installing mods. Would you like to do so?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); switch (resp) { case QMessageBox::Yes: { // Should be safe auto profile = static_cast(this->m_instance)->getPackProfile(); InstallLoaderDialog dialog(profile, QString(), this); bool ret = dialog.exec(); this->m_container->refreshContainer(); // returning negation of dialog.exec which'll be true if the install loader dialog got canceled/closed // and false if the user went through and installed a loader return !ret; } case QMessageBox::No: { // Nothing happens the dialog is already closing // returning true so the caller doesn't go and continue with opening it's dialog without a mod loader return true; } default: { // Unreachable // returning true as a safety measure return true; } } } PrismLauncher-10.0.5/launcher/ui/pages/instance/ResourcePackPage.h0000644000175100017510000000553715144136756024462 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ExternalResourcesPage.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/ResourcePackFolderModel.h" class ResourcePackPage : public ExternalResourcesPage { Q_OBJECT public: explicit ResourcePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = 0); QString displayName() const override { return tr("Resource Packs"); } QIcon icon() const override { return QIcon::fromTheme("resourcepacks"); } QString id() const override { return "resourcepacks"; } QString helpPage() const override { return "Resource-packs"; } virtual bool shouldDisplay() const override { return !m_instance->traits().contains("no-texturepacks") && !m_instance->traits().contains("texturepacks"); } public slots: void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; private slots: void downloadResourcePacks(); void downloadDialogFinished(int result); void updateResourcePacks(); void deleteResourcePackMetadata(); void changeResourcePackVersion(); protected: std::shared_ptr m_model; QPointer m_downloadDialog; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/DataPackPage.cpp0000644000175100017510000003261015144136756024067 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "DataPackPage.h" #include "minecraft/PackProfile.h" #include "ui_ExternalResourcesPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" DataPackPage::DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Packs")); ui->actionDownloadItem->setToolTip(tr("Download data packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); connect(ui->actionDownloadItem, &QAction::triggered, this, &DataPackPage::downloadDataPacks); ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected data packs (all data packs if none are selected)")); connect(ui->actionUpdateItem, &QAction::triggered, this, &DataPackPage::updateDataPacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); auto updateMenu = new QMenu(this); auto update = updateMenu->addAction(ui->actionUpdateItem->text()); connect(update, &QAction::triggered, this, &DataPackPage::updateDataPacks); updateMenu->addAction(ui->actionResetItemMetadata); connect(ui->actionResetItemMetadata, &QAction::triggered, this, &DataPackPage::deleteDataPackMetadata); ui->actionUpdateItem->setMenu(updateMenu); ui->actionChangeVersion->setToolTip(tr("Change a data pack's version.")); connect(ui->actionChangeVersion, &QAction::triggered, this, &DataPackPage::changeDataPackVersion); ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); } void DataPackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); auto& dp = m_model->at(row); ui->frame->updateWithDataPack(dp); } void DataPackPage::downloadDataPacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); m_downloadDialog = new ResourceDownload::DataPackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &DataPackPage::downloadDialogFinished); m_downloadDialog->open(); } void DataPackPage::downloadDialogFinished(int result) { if (result) { auto tasks = new ConcurrentTask(tr("Download Data Packs"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); tasks->deleteLater(); }); if (m_downloadDialog) { for (auto& task : m_downloadDialog->getTasks()) { tasks->addTask(task); } } else { qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } if (m_downloadDialog) m_downloadDialog->deleteLater(); } void DataPackPage::updateDataPacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Data pack updates are unavailable when metadata is disabled!")); return; } if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Update"), tr("Updating data packs while the game is running may cause pack duplication and game crashes.\n" "The old files may not be deleted as they are in use.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto mods_list = m_model->selectedResources(selection); bool use_all = mods_list.empty(); if (use_all) mods_list = m_model->allResources(); ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, { ModPlatform::ModLoaderType::DataPack }); update_dialog.checkCandidates(); if (update_dialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The data pack updater was aborted!"), QMessageBox::Warning)->show(); return; } if (update_dialog.noUpdates()) { QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; if (mods_list.size() > 1) { if (use_all) { message = tr("All data packs are up-to-date! :)"); } else { message = tr("All selected data packs are up-to-date! :)"); } } CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); return; } if (update_dialog.exec()) { auto tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } tasks->deleteLater(); }); for (auto task : update_dialog.getTasks()) { tasks->addTask(task); } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } } void DataPackPage::deleteDataPackMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedDataPacks(selection).length(); if (selectionCount == 0) return; if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 data packs.\n" "Are you sure?") .arg(selectionCount), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } m_model->deleteMetadata(selection); } void DataPackPage::changeDataPackVersion() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Data pack updates are unavailable when metadata is disabled!")); return; } const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); if (rows.count() != 1) return; Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); if (resource.metadata() == nullptr) return; ResourceDownload::DataPackDownloadDialog mdownload(this, m_model, m_instance); mdownload.setResourceMetadata(resource.metadata()); if (mdownload.exec()) { auto tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); tasks->deleteLater(); }); for (auto& task : mdownload.getTasks()) { tasks->addTask(task); } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } } GlobalDataPackPage::GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent) : QWidget(parent), m_instance(instance) { auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); connect(instance->settings()->getSetting("GlobalDataPacksEnabled").get(), &Setting::SettingChanged, this, [this] { updateContent(); if (m_container != nullptr) m_container->refreshContainer(); }); connect(instance->settings()->getSetting("GlobalDataPacksPath").get(), &Setting::SettingChanged, this, &GlobalDataPackPage::updateContent); } QString GlobalDataPackPage::displayName() const { if (m_underlyingPage == nullptr) return {}; return m_underlyingPage->displayName(); } QIcon GlobalDataPackPage::icon() const { if (m_underlyingPage == nullptr) return {}; return m_underlyingPage->icon(); } QString GlobalDataPackPage::helpPage() const { if (m_underlyingPage == nullptr) return {}; return m_underlyingPage->helpPage(); } bool GlobalDataPackPage::shouldDisplay() const { return m_instance->settings()->get("GlobalDataPacksEnabled").toBool(); } bool GlobalDataPackPage::apply() { return m_underlyingPage == nullptr || m_underlyingPage->apply(); } void GlobalDataPackPage::openedImpl() { if (m_underlyingPage != nullptr) m_underlyingPage->openedImpl(); } void GlobalDataPackPage::closedImpl() { if (m_underlyingPage != nullptr) m_underlyingPage->closedImpl(); } void GlobalDataPackPage::updateContent() { if (m_underlyingPage != nullptr) { if (m_container->selectedPage() == this) m_underlyingPage->closedImpl(); m_underlyingPage->apply(); layout()->removeWidget(m_underlyingPage); delete m_underlyingPage; m_underlyingPage = nullptr; } if (shouldDisplay()) { m_underlyingPage = new DataPackPage(m_instance, m_instance->dataPackList()); m_underlyingPage->setParentContainer(m_container); m_underlyingPage->updateExtraInfo = [this](QString id, QString value) { updateExtraInfo(std::move(id), std::move(value)); }; if (m_container->selectedPage() == this) m_underlyingPage->openedImpl(); layout()->addWidget(m_underlyingPage); } } void GlobalDataPackPage::setParentContainer(BasePageContainer* container) { BasePage::setParentContainer(container); updateContent(); } PrismLauncher-10.0.5/launcher/ui/pages/instance/OtherLogsPage.cpp0000644000175100017510000004266215144136756024335 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "OtherLogsPage.h" #include "ui_OtherLogsPage.h" #include #include "ui/GuiUtil.h" #include "ui/themes/ThemeManager.h" #include #include #include #include #include #include #include OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, InstancePtr instance, QWidget* parent) : QWidget(parent) , m_id(id) , m_displayName(displayName) , m_helpPage(helpPage) , ui(new Ui::OtherLogsPage) , m_instance(instance) , m_basePath(instance ? instance->gameRoot() : APPLICATION->dataRoot()) , m_logSearchPaths(instance ? instance->getLogFileSearchPaths() : QStringList{ "logs" }) { ui->setupUi(this); m_proxy = new LogFormatProxyModel(this); if (m_instance) { m_model.reset(new LogModel(this)); ui->trackLogCheckbox->hide(); } else { m_model = APPLICATION->logModel; } // set up fonts in the log proxy { QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); bool conversionOk = false; int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); if (!conversionOk) { fontSize = 11; } m_proxy->setFont(QFont(fontFamily, fontSize)); } ui->text->setModel(m_proxy); if (m_instance) { m_model->setMaxLines(getConsoleMaxLines(m_instance->settings())); m_model->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); } else { modelStateToUI(); } m_proxy->setSourceModel(m_model.get()); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &OtherLogsPage::populateSelectLogBox); auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); connect(findShortcut, &QShortcut::activated, this, &OtherLogsPage::findActivated); auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); connect(findNextShortcut, &QShortcut::activated, this, &OtherLogsPage::findNextActivated); auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); connect(findPreviousShortcut, &QShortcut::activated, this, &OtherLogsPage::findPreviousActivated); connect(ui->searchBar, &QLineEdit::returnPressed, this, &OtherLogsPage::on_findButton_clicked); } OtherLogsPage::~OtherLogsPage() { delete ui; } void OtherLogsPage::modelStateToUI() { if (m_model->wrapLines()) { ui->text->setWordWrap(true); ui->wrapCheckbox->setCheckState(Qt::Checked); } else { ui->text->setWordWrap(false); ui->wrapCheckbox->setCheckState(Qt::Unchecked); } if (m_model->colorLines()) { ui->text->setColorLines(true); ui->colorCheckbox->setCheckState(Qt::Checked); } else { ui->text->setColorLines(false); ui->colorCheckbox->setCheckState(Qt::Unchecked); } if (m_model->suspended()) { ui->trackLogCheckbox->setCheckState(Qt::Unchecked); } else { ui->trackLogCheckbox->setCheckState(Qt::Checked); } } void OtherLogsPage::UIToModelState() { if (!m_model) { return; } m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); } void OtherLogsPage::retranslate() { ui->retranslateUi(this); } void OtherLogsPage::openedImpl() { const QStringList failedPaths = m_watcher.addPaths(m_logSearchPaths); for (const QString& path : m_logSearchPaths) { if (failedPaths.contains(path)) qDebug() << "Failed to start watching" << path; else qDebug() << "Started watching" << path; } populateSelectLogBox(); } void OtherLogsPage::closedImpl() { const QStringList failedPaths = m_watcher.removePaths(m_logSearchPaths); for (const QString& path : m_logSearchPaths) { if (failedPaths.contains(path)) qDebug() << "Failed to stop watching" << path; else qDebug() << "Stopped watching" << path; } } void OtherLogsPage::populateSelectLogBox() { const QString prevCurrentFile = m_currentFile; ui->selectLogBox->blockSignals(true); ui->selectLogBox->clear(); if (!m_instance) ui->selectLogBox->addItem("Current logs"); ui->selectLogBox->addItems(getPaths()); ui->selectLogBox->blockSignals(false); if (!prevCurrentFile.isEmpty()) { const int index = ui->selectLogBox->findText(prevCurrentFile); if (index != -1) { ui->selectLogBox->blockSignals(true); ui->selectLogBox->setCurrentIndex(index); ui->selectLogBox->blockSignals(false); setControlsEnabled(true); // don't refresh file return; } else { setControlsEnabled(false); } } else if (!m_instance) { ui->selectLogBox->setCurrentIndex(0); setControlsEnabled(true); } on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); } void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) { QString file; if (index > 0 || (index == 0 && m_instance)) { file = ui->selectLogBox->itemText(index); } if ((index != 0 || m_instance) && (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file)))) { m_currentFile = QString(); ui->text->clear(); setControlsEnabled(false); } else { m_currentFile = file; reload(); setControlsEnabled(true); } } void OtherLogsPage::on_btnReload_clicked() { if (!m_instance && m_currentFile.isEmpty()) { if (!m_model) return; m_model->clear(); if (m_container) m_container->refreshContainer(); } else { reload(); } } void OtherLogsPage::reload() { if (m_currentFile.isEmpty()) { if (m_instance) { setControlsEnabled(false); } else { m_model = APPLICATION->logModel; m_proxy->setSourceModel(m_model.get()); ui->text->setModel(m_proxy); ui->text->scrollToBottom(); UIToModelState(); setControlsEnabled(true); } return; } QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (!file.open(QFile::ReadOnly)) { setControlsEnabled(false); ui->btnReload->setEnabled(true); // allow reload m_currentFile = QString(); QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2").arg(m_currentFile, file.errorString())); } else { auto setPlainText = [this](const QString& text) { QTextDocument* doc = ui->text->document(); doc->setDefaultFont(m_proxy->getFont()); ui->text->setPlainText(text); }; auto showTooBig = [setPlainText, &file]() { setPlainText(tr("The file (%1) is too big. You may want to open it in a viewer optimized " "for large files.") .arg(file.fileName())); }; if (file.size() > (1024ll * 1024ll * 12ll)) { showTooBig(); return; } MessageLevel last = MessageLevel::Unknown; auto handleLine = [this, &last](QString line) { if (line.isEmpty()) return false; if (line.back() == '\n') line = line.remove(line.size() - 1, 1); MessageLevel level = MessageLevel::Unknown; QString lineTemp = line; // don't edit out the time and level for clarity if (!m_instance) { level = MessageLevel::takeFromLauncherLine(lineTemp); } else { level = LogParser::guessLevel(line, last); } last = level; m_model->append(level, line); return m_model->isOverFlow(); }; // Try to determine a level for each line ui->text->clear(); ui->text->setModel(nullptr); if (!m_instance) { m_model.reset(new LogModel(this)); m_model->setMaxLines(getConsoleMaxLines(APPLICATION->settings())); m_model->setStopOnOverflow(shouldStopOnConsoleOverflow(APPLICATION->settings())); m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); } m_model->clear(); if (file.fileName().endsWith(".gz")) { QString line; auto error = GZip::readGzFileByBlocks(&file, [&line, handleLine](const QByteArray& d) { auto block = d; int newlineIndex = block.indexOf('\n'); while (newlineIndex != -1) { line += QString::fromUtf8(block).left(newlineIndex); block.remove(0, newlineIndex + 1); if (handleLine(line)) { line.clear(); return false; } line.clear(); newlineIndex = block.indexOf('\n'); } line += QString::fromUtf8(block); return true; }); if (!error.isEmpty()) { setPlainText(tr("The file (%1) encountered an error when reading: %2.").arg(file.fileName(), error)); return; } else if (!line.isEmpty()) { handleLine(line); } } else { while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { } } if (m_instance) { ui->text->setModel(m_proxy); ui->text->scrollToBottom(); } else { m_proxy->setSourceModel(m_model.get()); ui->text->setModel(m_proxy); ui->text->scrollToBottom(); UIToModelState(); setControlsEnabled(true); } } } void OtherLogsPage::on_btnPaste_clicked() { QString name = m_currentFile.isEmpty() ? displayName() : m_currentFile; GuiUtil::uploadPaste(name, ui->text->toPlainText(), this); } void OtherLogsPage::on_btnCopy_clicked() { GuiUtil::setClipboardText(ui->text->toPlainText()); } void OtherLogsPage::on_btnBottom_clicked() { ui->text->scrollToBottom(); } void OtherLogsPage::on_trackLogCheckbox_clicked(bool checked) { if (!m_model) return; m_model->suspend(!checked); } void OtherLogsPage::on_btnDelete_clicked() { if (m_currentFile.isEmpty()) { setControlsEnabled(false); return; } if (QMessageBox::question(this, tr("Confirm Deletion"), tr("You are about to delete \"%1\".\n" "This may be permanent and it will be gone from the logs folder.\n\n" "Are you sure?") .arg(m_currentFile), QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { return; } QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (FS::trash(file.fileName())) { return; } if (!file.remove()) { QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2").arg(m_currentFile, file.errorString())); } } void OtherLogsPage::on_btnClean_clicked() { auto toDelete = getPaths(); if (toDelete.isEmpty()) { return; } QMessageBox* messageBox = new QMessageBox(this); messageBox->setWindowTitle(tr("Confirm Cleanup")); if (toDelete.size() > 5) { messageBox->setText(tr("Are you sure you want to delete all log files?")); messageBox->setDetailedText(toDelete.join('\n')); } else { messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n'))); } messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); messageBox->setDefaultButton(QMessageBox::Ok); messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); messageBox->setIcon(QMessageBox::Question); messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); if (messageBox->exec() != QMessageBox::Ok) { return; } QStringList failed; for (auto item : toDelete) { QString absolutePath = FS::PathCombine(m_basePath, item); QFile file(absolutePath); qDebug() << "Deleting log" << absolutePath; if (FS::trash(file.fileName())) { continue; } if (!file.remove()) { failed.push_back(item); } } if (!failed.empty()) { QMessageBox* messageBoxFailure = new QMessageBox(this); messageBoxFailure->setWindowTitle(tr("Error")); if (failed.size() > 5) { messageBoxFailure->setText(tr("Couldn't delete some files!")); messageBoxFailure->setDetailedText(failed.join('\n')); } else { messageBoxFailure->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); } messageBoxFailure->setStandardButtons(QMessageBox::Ok); messageBoxFailure->setDefaultButton(QMessageBox::Ok); messageBoxFailure->setTextInteractionFlags(Qt::TextSelectableByMouse); messageBoxFailure->setIcon(QMessageBox::Critical); messageBoxFailure->setTextInteractionFlags(Qt::TextBrowserInteraction); messageBoxFailure->exec(); } } void OtherLogsPage::on_wrapCheckbox_clicked(bool checked) { ui->text->setWordWrap(checked); if (!m_model) return; m_model->setLineWrap(checked); ui->text->scrollToBottom(); } void OtherLogsPage::on_colorCheckbox_clicked(bool checked) { ui->text->setColorLines(checked); if (!m_model) return; m_model->setColorLines(checked); ui->text->scrollToBottom(); } void OtherLogsPage::setControlsEnabled(const bool enabled) { if (m_instance) { ui->btnDelete->setEnabled(enabled); ui->btnClean->setEnabled(enabled); } else if (!m_currentFile.isEmpty()) { ui->btnReload->setText("&Reload"); ui->btnReload->setToolTip("Reload the contents of the log from the disk"); ui->btnDelete->setEnabled(enabled); ui->btnClean->setEnabled(enabled); ui->trackLogCheckbox->setEnabled(false); } else { ui->btnReload->setText("Clear"); ui->btnReload->setToolTip("Clear the log"); ui->btnDelete->setEnabled(false); ui->btnClean->setEnabled(false); ui->trackLogCheckbox->setEnabled(enabled); } ui->btnReload->setEnabled(enabled); ui->btnCopy->setEnabled(enabled); ui->btnPaste->setEnabled(enabled); ui->text->setEnabled(enabled); } QStringList OtherLogsPage::getPaths() { QDir baseDir(m_basePath); QStringList result; for (QString searchPath : m_logSearchPaths) { QDir searchDir(searchPath); QStringList filters{ "*.log", "*.log.gz" }; if (searchPath != m_basePath) filters.append("*.txt"); QStringList entries = searchDir.entryList(filters, QDir::Files | QDir::Readable, QDir::SortFlag::Time); for (const QString& name : entries) result.append(baseDir.relativeFilePath(searchDir.filePath(name))); } return result; } void OtherLogsPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); bool reverse = modifiers & Qt::ShiftModifier; ui->text->findNext(ui->searchBar->text(), reverse); } void OtherLogsPage::findNextActivated() { ui->text->findNext(ui->searchBar->text(), false); } void OtherLogsPage::findPreviousActivated() { ui->text->findNext(ui->searchBar->text(), true); } void OtherLogsPage::findActivated() { // focus the search bar if it doesn't have focus if (!ui->searchBar->hasFocus()) { ui->searchBar->setFocus(); ui->searchBar->selectAll(); } } PrismLauncher-10.0.5/launcher/ui/pages/instance/ExternalResourcesPage.cpp0000644000175100017510000003211015144136756026067 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ExternalResourcesPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ExternalResourcesPage.h" #include "DesktopServices.h" #include "Version.h" #include "minecraft/mod/ResourceFolderModel.h" #include "ui/GuiUtil.h" #include #include #include #include ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) : QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model) { ui->setupUi(this); ui->actionsToolbar->insertSpacer(ui->actionViewFolder); m_filterModel = model->createFilterProxyModel(this); m_filterModel->setDynamicSortFilter(true); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSourceModel(m_model.get()); m_filterModel->setFilterKeyColumn(-1); ui->treeView->setModel(m_filterModel); // must come after setModel ui->treeView->setResizeModes(m_model->columnResizeModes()); ui->treeView->installEventFilter(this); ui->treeView->sortByColumn(1, Qt::AscendingOrder); ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu); // The default function names by Qt are pretty ugly, so let's just connect the actions manually, // to make it easier to read :) connect(ui->actionAddItem, &QAction::triggered, this, &ExternalResourcesPage::addItem); connect(ui->actionRemoveItem, &QAction::triggered, this, &ExternalResourcesPage::removeItem); connect(ui->actionEnableItem, &QAction::triggered, this, &ExternalResourcesPage::enableItem); connect(ui->actionDisableItem, &QAction::triggered, this, &ExternalResourcesPage::disableItem); connect(ui->actionViewHomepage, &QAction::triggered, this, &ExternalResourcesPage::viewHomepage); connect(ui->actionViewConfigs, &QAction::triggered, this, &ExternalResourcesPage::viewConfigs); connect(ui->actionViewFolder, &QAction::triggered, this, &ExternalResourcesPage::viewFolder); connect(ui->treeView, &ModListView::customContextMenuRequested, this, &ExternalResourcesPage::ShowContextMenu); connect(ui->treeView, &ModListView::activated, this, &ExternalResourcesPage::itemActivated); auto selection_model = ui->treeView->selectionModel(); connect(selection_model, &QItemSelectionModel::currentChanged, this, [this](const QModelIndex& current, const QModelIndex& previous) { if (!current.isValid()) { ui->frame->clear(); return; } updateFrame(current, previous); }); auto updateExtra = [this]() { if (updateExtraInfo) updateExtraInfo(id(), extraHeaderInfoString()); }; connect(selection_model, &QItemSelectionModel::selectionChanged, this, updateExtra); connect(model.get(), &ResourceFolderModel::updateFinished, this, updateExtra); connect(model.get(), &ResourceFolderModel::parseFinished, this, updateExtra); connect(selection_model, &QItemSelectionModel::selectionChanged, this, [this] { updateActions(); }); connect(m_model.get(), &ResourceFolderModel::rowsInserted, this, [this] { updateActions(); }); connect(m_model.get(), &ResourceFolderModel::rowsRemoved, this, [this] { updateActions(); }); auto viewHeader = ui->treeView->header(); viewHeader->setContextMenuPolicy(Qt::CustomContextMenu); connect(viewHeader, &QHeaderView::customContextMenuRequested, this, &ExternalResourcesPage::ShowHeaderContextMenu); m_model->loadColumns(ui->treeView); connect(ui->treeView->header(), &QHeaderView::sectionResized, this, [this] { m_model->saveColumns(ui->treeView); }); connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); updateActions(); } ExternalResourcesPage::~ExternalResourcesPage() { delete ui; } QMenu* ExternalResourcesPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction()); return filteredMenu; } void ExternalResourcesPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu")); menu->exec(ui->treeView->mapToGlobal(pos)); delete menu; } void ExternalResourcesPage::ShowHeaderContextMenu(const QPoint& pos) { auto menu = m_model->createHeaderContextMenu(ui->treeView); menu->exec(ui->treeView->mapToGlobal(pos)); menu->deleteLater(); } void ExternalResourcesPage::openedImpl() { m_model->startWatching(); auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->actionsToolbar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void ExternalResourcesPage::closedImpl() { m_model->stopWatching(); m_wide_bar_setting->set(QString::fromUtf8(ui->actionsToolbar->getVisibilityState().toBase64())); } void ExternalResourcesPage::retranslate() { ui->retranslateUi(this); } void ExternalResourcesPage::itemActivated(const QModelIndex&) { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE); } void ExternalResourcesPage::filterTextChanged(const QString& newContents) { m_viewFilter = newContents; m_filterModel->setFilterRegularExpression(m_viewFilter); } bool ExternalResourcesPage::shouldDisplay() const { return true; } bool ExternalResourcesPage::listFilter(QKeyEvent* keyEvent) { switch (keyEvent->key()) { case Qt::Key_Delete: removeItem(); return true; case Qt::Key_Plus: addItem(); return true; default: break; } return QWidget::eventFilter(ui->treeView, keyEvent); } bool ExternalResourcesPage::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() != QEvent::KeyPress) return QWidget::eventFilter(obj, ev); QKeyEvent* keyEvent = static_cast(ev); if (obj == ui->treeView) return listFilter(keyEvent); return QWidget::eventFilter(obj, ev); } void ExternalResourcesPage::addItem() { auto list = GuiUtil::BrowseForFiles( helpPage(), tr("Select %1", "Select whatever type of files the page contains. Example: 'Loader Mods'").arg(displayName()), m_fileSelectionFilter.arg(displayName()), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!list.isEmpty()) { for (auto filename : list) { m_model->installResource(filename); } } } void ExternalResourcesPage::removeItem() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); int count = 0; bool folder = false; for (auto& i : selection.indexes()) { if (i.column() == 0) { count++; // if a folder is selected, show the confirmation dialog if (m_model->at(i.row()).fileinfo().isDir()) folder = true; } } QString text; bool multiple = count > 1; if (multiple) { text = tr("You are about to remove %1 items.\n" "This may be permanent and they will be gone from the folder.\n\n" "Are you sure?") .arg(count); } else if (folder) { text = tr("You are about to remove the folder \"%1\".\n" "This may be permanent and it will be gone from the parent folder.\n\n" "Are you sure?") .arg(m_model->at(selection.indexes().at(0).row()).fileinfo().fileName()); } if (!text.isEmpty()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } removeItems(selection); } void ExternalResourcesPage::removeItems(const QItemSelection& selection) { if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Delete"), tr("If you remove this resource while the game is running it may crash your game.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } m_model->deleteResources(selection.indexes()); } void ExternalResourcesPage::enableItem() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE); } void ExternalResourcesPage::disableItem() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE); } void ExternalResourcesPage::viewHomepage() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); for (auto resource : m_model->selectedResources(selection)) { auto url = resource->homepage(); if (!url.isEmpty()) DesktopServices::openUrl(url); } } void ExternalResourcesPage::viewConfigs() { DesktopServices::openPath(m_instance->instanceConfigFolder(), true); } void ExternalResourcesPage::viewFolder() { DesktopServices::openPath(m_model->dir().absolutePath(), true); } void ExternalResourcesPage::updateActions() { const bool hasSelection = ui->treeView->selectionModel()->hasSelection(); const QModelIndexList selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); const QList selectedResources = m_model->selectedResources(selection); ui->actionUpdateItem->setEnabled(!m_model->empty()); ui->actionResetItemMetadata->setEnabled(hasSelection); ui->actionChangeVersion->setEnabled(selectedResources.size() == 1 && selectedResources[0]->metadata() != nullptr); ui->actionRemoveItem->setEnabled(hasSelection); ui->actionEnableItem->setEnabled(hasSelection); ui->actionDisableItem->setEnabled(hasSelection); ui->actionViewHomepage->setEnabled(hasSelection && std::any_of(selectedResources.begin(), selectedResources.end(), [](Resource* resource) { return !resource->homepage().isEmpty(); })); ui->actionExportMetadata->setEnabled(!m_model->empty()); } void ExternalResourcesPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); Resource const& resource = m_model->at(row); ui->frame->updateWithResource(resource); } QString ExternalResourcesPage::extraHeaderInfoString() { if (ui && ui->treeView && ui->treeView->selectionModel()) { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); if (auto count = std::count_if(selection.cbegin(), selection.cend(), [](auto v) { return v.column() == 0; }); count != 0) return tr(" (%1 installed, %2 selected)").arg(m_model->size()).arg(count); } return tr(" (%1 installed)").arg(m_model->size()); } PrismLauncher-10.0.5/launcher/ui/pages/instance/LogPage.cpp0000644000175100017510000002316215144136756023142 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LogPage.h" #include "ui_LogPage.h" #include "Application.h" #include #include #include #include "launch/LaunchTask.h" #include "settings/Setting.h" #include "ui/GuiUtil.h" #include "ui/themes/ThemeManager.h" #include QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const { const LogColors& colors = APPLICATION->themeManager()->getLogColors(); switch (role) { case Qt::FontRole: return m_font; case Qt::ForegroundRole: { MessageLevel level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); QColor result = colors.foreground.value(level); if (result.isValid()) return result; break; } case Qt::BackgroundRole: { MessageLevel level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); QColor result = colors.background.value(level); if (result.isValid()) return result; break; } } return QIdentityProxyModel::data(index, role); } QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const { QModelIndex parentIndex = parent(start); auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { QModelIndex idx = index(r, start.column(), parentIndex); if (!idx.isValid() || idx == start) { return QModelIndex(); } QVariant v = data(idx, Qt::DisplayRole); QString t = v.toString(); if (t.contains(value, Qt::CaseInsensitive)) return idx; return QModelIndex(); }; if (reverse) { int from = start.row(); int to = 0; for (int i = 0; i < 2; ++i) { for (int r = from; (r >= to); --r) { auto idx = compare(r); if (idx.isValid()) return idx; } // prepare for the next iteration from = rowCount() - 1; to = start.row(); } } else { int from = start.row(); int to = rowCount(parentIndex); for (int i = 0; i < 2; ++i) { for (int r = from; (r < to); ++r) { auto idx = compare(r); if (idx.isValid()) return idx; } // prepare for the next iteration from = 0; to = start.row(); } } return QModelIndex(); } LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { ui->setupUi(this); m_proxy = new LogFormatProxyModel(this); // set up fonts in the log proxy { QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); bool conversionOk = false; int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); if (!conversionOk) { fontSize = 11; } m_proxy->setFont(QFont(fontFamily, fontSize)); } ui->text->setModel(m_proxy); // set up instance and launch process recognition { auto launchTask = m_instance->getLaunchTask(); if (launchTask) { setInstanceLaunchTaskChanged(launchTask, true); } connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &LogPage::onInstanceLaunchTaskChanged); } auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); connect(findShortcut, &QShortcut::activated, this, &LogPage::findActivated); auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); connect(findNextShortcut, &QShortcut::activated, this, &LogPage::findNextActivated); connect(ui->searchBar, &QLineEdit::returnPressed, this, &LogPage::on_findButton_clicked); auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); connect(findPreviousShortcut, &QShortcut::activated, this, &LogPage::findPreviousActivated); } LogPage::~LogPage() { delete ui; } void LogPage::modelStateToUI() { if (m_model->wrapLines()) { ui->text->setWordWrap(true); ui->wrapCheckbox->setCheckState(Qt::Checked); } else { ui->text->setWordWrap(false); ui->wrapCheckbox->setCheckState(Qt::Unchecked); } if (m_model->colorLines()) { ui->text->setColorLines(true); ui->colorCheckbox->setCheckState(Qt::Checked); } else { ui->text->setColorLines(false); ui->colorCheckbox->setCheckState(Qt::Unchecked); } if (m_model->suspended()) { ui->trackLogCheckbox->setCheckState(Qt::Unchecked); } else { ui->trackLogCheckbox->setCheckState(Qt::Checked); } } void LogPage::UIToModelState() { if (!m_model) { return; } m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); } void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr proc, bool initial) { m_process = proc; if (m_process) { m_model = proc->getLogModel(); m_proxy->setSourceModel(m_model.get()); if (initial) { modelStateToUI(); } else { UIToModelState(); } } else { m_proxy->setSourceModel(nullptr); m_model.reset(); } } void LogPage::onInstanceLaunchTaskChanged(shared_qobject_ptr proc) { setInstanceLaunchTaskChanged(proc, false); } bool LogPage::apply() { return true; } bool LogPage::shouldDisplay() const { return true; } void LogPage::on_btnPaste_clicked() { if (!m_model) return; // FIXME: turn this into a proper task and move the upload logic out of GuiUtil! m_model->append(MessageLevel::Launcher, QString("Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); auto url = GuiUtil::uploadPaste(tr("Minecraft Log"), m_model->toPlainText(), this); if (!url.has_value()) { m_model->append(MessageLevel::Error, QString("Log upload canceled")); } else if (url->isNull()) { m_model->append(MessageLevel::Error, QString("Log upload failed!")); } else { m_model->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value())); } } void LogPage::on_btnCopy_clicked() { if (!m_model) return; m_model->append(MessageLevel::Launcher, QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); GuiUtil::setClipboardText(m_model->toPlainText()); } void LogPage::on_btnClear_clicked() { if (!m_model) return; m_model->clear(); m_container->refreshContainer(); } void LogPage::on_btnBottom_clicked() { ui->text->scrollToBottom(); } void LogPage::on_trackLogCheckbox_clicked(bool checked) { if (!m_model) return; m_model->suspend(!checked); } void LogPage::on_wrapCheckbox_clicked(bool checked) { ui->text->setWordWrap(checked); if (!m_model) return; m_model->setLineWrap(checked); } void LogPage::on_colorCheckbox_clicked(bool checked) { ui->text->setColorLines(checked); if (!m_model) return; m_model->setColorLines(checked); } void LogPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); bool reverse = modifiers & Qt::ShiftModifier; ui->text->findNext(ui->searchBar->text(), reverse); } void LogPage::findNextActivated() { ui->text->findNext(ui->searchBar->text(), false); } void LogPage::findPreviousActivated() { ui->text->findNext(ui->searchBar->text(), true); } void LogPage::findActivated() { // focus the search bar if it doesn't have focus if (!ui->searchBar->hasFocus()) { ui->searchBar->setFocus(); ui->searchBar->selectAll(); } } void LogPage::retranslate() { ui->retranslateUi(this); } PrismLauncher-10.0.5/launcher/ui/pages/instance/ExternalResourcesPage.h0000644000175100017510000000422215144136756025537 0ustar runnerrunner#pragma once #include #include #include "Application.h" #include "minecraft/MinecraftInstance.h" #include "settings/Setting.h" #include "ui/pages/BasePage.h" class ResourceFolderModel; namespace Ui { class ExternalResourcesPage; } /* This page is used as a base for pages in which the user can manage external resources * related to the game, such as mods, shaders or resource packs. */ class ExternalResourcesPage : public QMainWindow, public BasePage { Q_OBJECT public: explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); virtual ~ExternalResourcesPage(); virtual QString displayName() const override = 0; virtual QIcon icon() const override = 0; virtual QString id() const override = 0; virtual QString helpPage() const override = 0; virtual bool shouldDisplay() const override = 0; QString extraHeaderInfoString(); void openedImpl() override; void closedImpl() override; void retranslate() override; protected: bool eventFilter(QObject* obj, QEvent* ev) override; bool listFilter(QKeyEvent* ev); QMenu* createPopupMenu() override; public slots: virtual void updateActions(); virtual void updateFrame(const QModelIndex& current, const QModelIndex& previous); protected slots: void itemActivated(const QModelIndex& index); void filterTextChanged(const QString& newContents); virtual void addItem(); void removeItem(); virtual void removeItems(const QItemSelection& selection); virtual void enableItem(); virtual void disableItem(); virtual void viewHomepage(); virtual void viewFolder(); virtual void viewConfigs(); void ShowContextMenu(const QPoint& pos); void ShowHeaderContextMenu(const QPoint& pos); protected: BaseInstance* m_instance = nullptr; Ui::ExternalResourcesPage* ui = nullptr; std::shared_ptr m_model; QSortFilterProxyModel* m_filterModel = nullptr; QString m_fileSelectionFilter; QString m_viewFilter; std::shared_ptr m_wide_bar_setting = nullptr; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/ExternalResourcesPage.ui0000644000175100017510000001526115144136756025732 0ustar runnerrunner ExternalResourcesPage 0 0 1042 501 0 0 0 0 0 0 true QAbstractItemView::DragDropMode::DropOnly true 0 0 Search Actions Qt::ToolButtonStyle::ToolButtonIconOnly true RightToolBarArea false &Add File Add a locally downloaded file. false &Remove Remove all selected items. false &Enable Enable all selected items. false &Disable Disable all selected items. View &Configs Open the 'config' folder in the system file manager. View &Folder Open the folder in the system file manager. false &Download Download resources from online mod platforms. false Check for &Updates Try to check or update all selected resources (all resources if none are selected). Reset Update Metadata QAction::MenuRole::NoRole Verify Dependencies QAction::MenuRole::NoRole false Export List Export resource's metadata to text. false Change Version Change a resource's version. QAction::MenuRole::NoRole false View Homepage View the homepages of all selected items. ModListView QTreeView
    ui/widgets/ModListView.h
    InfoFrame QFrame
    ui/widgets/InfoFrame.h
    1
    WideBar QToolBar
    ui/widgets/WideBar.h
    treeView
    PrismLauncher-10.0.5/launcher/ui/pages/instance/TexturePackPage.h0000644000175100017510000000540115144136757024322 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ExternalResourcesPage.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/TexturePackFolderModel.h" class TexturePackPage : public ExternalResourcesPage { Q_OBJECT public: explicit TexturePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); QString displayName() const override { return tr("Texture packs"); } QIcon icon() const override { return QIcon::fromTheme("resourcepacks"); } QString id() const override { return "texturepacks"; } QString helpPage() const override { return "Texture-packs"; } virtual bool shouldDisplay() const override { return m_instance->traits().contains("texturepacks"); } public slots: void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; void downloadTexturePacks(); void downloadDialogFinished(int result); void updateTexturePacks(); void deleteTexturePackMetadata(); void changeTexturePackVersion(); private: std::shared_ptr m_model; QPointer m_downloadDialog; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/ServerPingTask.h0000644000175100017510000000065115144136756024176 0ustar runnerrunner#pragma once #include #include #include class ServerPingTask : public Task { Q_OBJECT public: explicit ServerPingTask(QString domain, int port) : Task(), m_domain(domain), m_port(port) {} ~ServerPingTask() override = default; int m_outputOnlinePlayers = -1; private: QString m_domain; int m_port; protected: virtual void executeTask() override; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/WorldListPage.ui0000644000175100017510000001051115144136757024172 0ustar runnerrunner WorldListPage 0 0 800 600 0 0 0 0 0 0 true QAbstractItemView::DragDrop true false false true true false Actions Qt::LeftToolBarArea|Qt::RightToolBarArea Qt::ToolButtonTextOnly false RightToolBarArea false Add Join Rename Copy Delete MCEdit Copy Seed Refresh View Folder Reset Icon Remove world icon to make the game re-generate it on next load. Data Packs Manage data packs inside the world. WideBar QToolBar
    ui/widgets/WideBar.h
    PrismLauncher-10.0.5/launcher/ui/pages/instance/ShaderPackPage.h0000644000175100017510000000507115144136756024072 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ExternalResourcesPage.h" #include "ui/dialogs/ResourceDownloadDialog.h" class ShaderPackPage : public ExternalResourcesPage { Q_OBJECT public: explicit ShaderPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); ~ShaderPackPage() override = default; QString displayName() const override { return tr("Shader Packs"); } QIcon icon() const override { return QIcon::fromTheme("shaderpacks"); } QString id() const override { return "shaderpacks"; } QString helpPage() const override { return "shader-packs"; } bool shouldDisplay() const override { return true; } public slots: void downloadShaderPack(); void downloadDialogFinished(int result); void updateShaderPacks(); void deleteShaderPackMetadata(); void changeShaderPackVersion(); private: std::shared_ptr m_model; QPointer m_downloadDialog; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/NotesPage.h0000644000175100017510000000441615144136756023157 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "BaseInstance.h" #include "ui/pages/BasePage.h" namespace Ui { class NotesPage; } class NotesPage : public QWidget, public BasePage { Q_OBJECT public: explicit NotesPage(BaseInstance* inst, QWidget* parent = 0); virtual ~NotesPage(); virtual QString displayName() const override { return tr("Notes"); } virtual QIcon icon() const override { auto icon = QIcon::fromTheme("notes"); if (icon.isNull()) icon = QIcon::fromTheme("news"); return icon; } virtual QString id() const override { return "notes"; } virtual bool apply() override; virtual QString helpPage() const override { return "Notes"; } void retranslate() override; private: Ui::NotesPage* ui; BaseInstance* m_inst; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/OtherLogsPage.h0000644000175100017510000000653115144136756023775 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "LogPage.h" #include "ui/pages/BasePage.h" namespace Ui { class OtherLogsPage; } class RecursiveFileSystemWatcher; class OtherLogsPage : public QWidget, public BasePage { Q_OBJECT public: explicit OtherLogsPage(QString id, QString displayName, QString helpPage, InstancePtr instance = nullptr, QWidget* parent = 0); ~OtherLogsPage(); QString id() const override { return m_id; } QString displayName() const override { return m_displayName; } QIcon icon() const override { return QIcon::fromTheme("log"); } QString helpPage() const override { return m_helpPage; } void retranslate() override; void openedImpl() override; void closedImpl() override; private slots: void populateSelectLogBox(); void on_selectLogBox_currentIndexChanged(int index); void on_btnReload_clicked(); void on_btnPaste_clicked(); void on_btnCopy_clicked(); void on_btnDelete_clicked(); void on_btnClean_clicked(); void on_btnBottom_clicked(); void on_trackLogCheckbox_clicked(bool checked); void on_wrapCheckbox_clicked(bool checked); void on_colorCheckbox_clicked(bool checked); void on_findButton_clicked(); void findActivated(); void findNextActivated(); void findPreviousActivated(); private: void reload(); void modelStateToUI(); void UIToModelState(); void setControlsEnabled(bool enabled); QStringList getPaths(); private: QString m_id; QString m_displayName; QString m_helpPage; Ui::OtherLogsPage* ui; InstancePtr m_instance; /** Path to display log paths relative to. */ QString m_basePath; QStringList m_logSearchPaths; QString m_currentFile; QFileSystemWatcher m_watcher; LogFormatProxyModel* m_proxy; shared_qobject_ptr m_model; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/LogPage.ui0000644000175100017510000001157115144136756022776 0ustar runnerrunner LogPage 0 0 825 782 0 0 0 false true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse false Keep updating true Wrap lines true Color lines true Qt::Horizontal 40 20 Copy the whole log into the clipboard &Copy Upload the log to the paste service configured in preferences Upload Clear the log Clear 0 0 Find 0 0 Scroll all the way to bottom Bottom Qt::Vertical Search LogView QPlainTextEdit
    ui/widgets/LogView.h
    trackLogCheckbox wrapCheckbox colorCheckbox btnCopy btnPaste btnClear text findButton
    PrismLauncher-10.0.5/launcher/ui/pages/instance/ScreenshotsPage.cpp0000644000175100017510000005024715144136756024725 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ScreenshotsPage.h" #include "BuildConfig.h" #include "ui_ScreenshotsPage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "net/NetJob.h" #include "screenshots/ImgurAlbumCreation.h" #include "screenshots/ImgurUpload.h" #include "tasks/SequentialTask.h" #include #include #include "RWStorage.h" using SharedIconCache = RWStorage; using SharedIconCachePtr = std::shared_ptr; class ThumbnailingResult : public QObject { Q_OBJECT public slots: inline void emitResultsReady(const QString& path) { emit resultsReady(path); } inline void emitResultsFailed(const QString& path) { emit resultsFailed(path); } signals: void resultsReady(const QString& path); void resultsFailed(const QString& path); }; class ThumbnailRunnable : public QRunnable { public: ThumbnailRunnable(QString path, SharedIconCachePtr cache) { m_path = path; m_cache = cache; } void run() { QFileInfo info(m_path); if (info.isDir()) return; if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0)) return; if (!m_cache->stale(m_path)) return; QImage image(m_path); if (image.isNull()) { m_resultEmitter.emitResultsFailed(m_path); qDebug() << "Error loading screenshot (perhaps too large?):" + m_path; return; } QImage small; if (image.width() > image.height()) small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); else small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); QImage square(QSize(256, 256), QImage::Format_ARGB32); square.fill(Qt::transparent); QPainter painter(&square); painter.drawImage(offset, small); painter.end(); QIcon icon(QPixmap::fromImage(square)); m_cache->add(m_path, icon); m_resultEmitter.emitResultsReady(m_path); } QString m_path; SharedIconCachePtr m_cache; ThumbnailingResult m_resultEmitter; }; // this is about as elegant and well written as a bag of bricks with scribbles done by insane // asylum patients. class FilterModel : public QIdentityProxyModel { Q_OBJECT public: explicit FilterModel(QObject* parent = 0) : QIdentityProxyModel(parent) { m_thumbnailingPool.setMaxThreadCount(4); m_thumbnailCache = std::make_shared(); m_thumbnailCache->add("placeholder", QIcon::fromTheme("screenshot-placeholder")); connect(&watcher, &QFileSystemWatcher::fileChanged, this, &FilterModel::fileChanged); } virtual ~FilterModel() { m_thumbnailingPool.clear(); if (!m_thumbnailingPool.waitForDone(500)) qDebug() << "Thumbnail pool took longer than 500ms to finish"; } virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const { auto model = sourceModel(); if (!model) return QVariant(); if (role == Qt::DisplayRole || role == Qt::EditRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); static const QRegularExpression s_removeChars("\\.png$"); return result.toString().remove(s_removeChars); } if (role == Qt::DecorationRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); QString filePath = result.toString(); QIcon temp; if (!watched.contains(filePath)) { ((QFileSystemWatcher&)watcher).addPath(filePath); ((QSet&)watched).insert(filePath); } if (m_thumbnailCache->get(filePath, temp)) { return temp; } if (!m_failed.contains(filePath)) { ((FilterModel*)this)->thumbnailImage(filePath); } return (m_thumbnailCache->get("placeholder")); } return sourceModel()->data(mapToSource(proxyIndex), role); } virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) { auto model = sourceModel(); if (!model) return false; if (role != Qt::EditRole) return false; // FIXME: this is a workaround for a bug in QFileSystemModel, where it doesn't // sort after renames { ((QFileSystemModel*)model)->setNameFilterDisables(true); ((QFileSystemModel*)model)->setNameFilterDisables(false); } return model->setData(mapToSource(index), value.toString() + ".png", role); } private: void thumbnailImage(QString path) { auto runnable = new ThumbnailRunnable(path, m_thumbnailCache); connect(&runnable->m_resultEmitter, &ThumbnailingResult::resultsReady, this, &FilterModel::thumbnailReady); connect(&runnable->m_resultEmitter, &ThumbnailingResult::resultsFailed, this, &FilterModel::thumbnailFailed); m_thumbnailingPool.start(runnable); } private slots: void thumbnailReady(QString path) { emit layoutChanged(); } void thumbnailFailed(QString path) { m_failed.insert(path); } void fileChanged(QString filepath) { m_thumbnailCache->setStale(filepath); // reinsert the path... watcher.removePath(filepath); if (QFile::exists(filepath)) { watcher.addPath(filepath); thumbnailImage(filepath); } } private: SharedIconCachePtr m_thumbnailCache; QThreadPool m_thumbnailingPool; QSet m_failed; QSet watched; QFileSystemWatcher watcher; }; class CenteredEditingDelegate : public QStyledItemDelegate { public: explicit CenteredEditingDelegate(QObject* parent = 0) : QStyledItemDelegate(parent) {} virtual ~CenteredEditingDelegate() {} virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { auto widget = QStyledItemDelegate::createEditor(parent, option, index); auto foo = dynamic_cast(widget); if (foo) { foo->setAlignment(Qt::AlignHCenter); foo->setFrame(true); foo->setMaximumWidth(192); } return widget; } }; ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(parent), ui(new Ui::ScreenshotsPage) { m_model.reset(new QFileSystemModel()); m_filterModel.reset(new FilterModel()); m_filterModel->setSourceModel(m_model.get()); m_model->setFilter(QDir::Files); m_model->setReadOnly(false); m_model->setNameFilters({ "*.png" }); m_model->setNameFilterDisables(false); // Sorts by modified date instead of creation date because that column is not available and would require subclassing, this should work // considering screenshots aren't modified after creation. constexpr int file_modified_column_index = 3; m_model->sort(file_modified_column_index, Qt::DescendingOrder); m_folder = path; m_valid = FS::ensureFolderPathExists(m_folder); ui->setupUi(this); ui->toolBar->insertSpacer(ui->actionView_Folder); ui->listView->setIconSize(QSize(128, 128)); ui->listView->setGridSize(QSize(192, 160)); ui->listView->setSpacing(9); // ui->listView->setUniformItemSizes(true); ui->listView->setLayoutMode(QListView::Batched); ui->listView->setViewMode(QListView::IconMode); ui->listView->setResizeMode(QListView::Adjust); ui->listView->installEventFilter(this); ui->listView->setEditTriggers(QAbstractItemView::NoEditTriggers); ui->listView->setItemDelegate(new CenteredEditingDelegate(this)); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu); connect(ui->listView, &QAbstractItemView::activated, this, &ScreenshotsPage::onItemActivated); } bool ScreenshotsPage::eventFilter(QObject* obj, QEvent* evt) { if (obj != ui->listView) return QWidget::eventFilter(obj, evt); if (evt->type() != QEvent::KeyPress) { return QWidget::eventFilter(obj, evt); } QKeyEvent* keyEvent = static_cast(evt); if (keyEvent->matches(QKeySequence::Copy)) { on_actionCopy_File_s_triggered(); return true; } switch (keyEvent->key()) { case Qt::Key_Delete: on_actionDelete_triggered(); return true; case Qt::Key_F2: on_actionRename_triggered(); return true; default: break; } return QWidget::eventFilter(obj, evt); } void ScreenshotsPage::retranslate() { ui->retranslateUi(this); } ScreenshotsPage::~ScreenshotsPage() { delete ui; } void ScreenshotsPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); if (ui->listView->selectionModel()->selectedRows().size() > 1) { menu->removeAction(ui->actionCopy_Image); } menu->exec(ui->listView->mapToGlobal(pos)); delete menu; } QMenu* ScreenshotsPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction(ui->toolBar->toggleViewAction()); return filteredMenu; } void ScreenshotsPage::onItemActivated(QModelIndex index) { if (!index.isValid()) return; auto info = m_model->fileInfo(index); DesktopServices::openPath(info); } void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection& selected) { bool allReadable = !selected.isEmpty(); bool allWritable = !selected.isEmpty(); for (auto index : selected.indexes()) { if (!index.isValid()) break; auto info = m_model->fileInfo(index); if (!info.isReadable()) allReadable = false; if (!info.isWritable()) allWritable = false; } ui->actionUpload->setEnabled(allReadable); ui->actionCopy_Image->setEnabled(allReadable); ui->actionCopy_File_s->setEnabled(allReadable); ui->actionDelete->setEnabled(allWritable); ui->actionRename->setEnabled(allWritable); } void ScreenshotsPage::on_actionView_Folder_triggered() { DesktopServices::openPath(m_folder, true); } void ScreenshotsPage::on_actionUpload_triggered() { auto selection = ui->listView->selectionModel()->selectedRows(); if (selection.isEmpty()) return; QString text; QUrl baseUrl(BuildConfig.IMGUR_BASE_URL); if (selection.size() > 1) text = tr("You are about to upload %1 screenshots to %2.\n" "You should double-check for personal information.\n\n" "Are you sure?") .arg(QString::number(selection.size()), baseUrl.host()); else text = tr("You are about to upload the selected screenshot to %1.\n" "You should double-check for personal information.\n\n" "Are you sure?") .arg(baseUrl.host()); auto response = CustomMessageBox::selectable(this, "Confirm Upload", text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; QList uploaded; auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network())); ProgressDialog dialog(this); dialog.setSkipButton(true, tr("Abort")); if (selection.size() < 2) { auto item = selection.at(0); auto info = m_model->fileInfo(item); auto screenshot = std::make_shared(info); job->addNetAction(ImgurUpload::make(screenshot)); connect(job.get(), &Task::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), reason, QMessageBox::Critical)->show(); }); connect(job.get(), &Task::aborted, [this] { CustomMessageBox::selectable(this, tr("Screenshots upload aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) ->show(); }); m_uploadActive = true; if (dialog.execWithTask(job.get()) == QDialog::Accepted) { auto link = screenshot->m_url; QClipboard* clipboard = QApplication::clipboard(); qDebug() << "ImgurUpload link" << link; clipboard->setText(link); CustomMessageBox::selectable( this, tr("Upload finished"), tr("The link to the uploaded screenshot has been placed in your clipboard.").arg(link), QMessageBox::Information) ->exec(); } m_uploadActive = false; return; } for (auto item : selection) { auto info = m_model->fileInfo(item); auto screenshot = std::make_shared(info); uploaded.push_back(screenshot); job->addNetAction(ImgurUpload::make(screenshot)); } SequentialTask task; auto albumTask = NetJob::Ptr(new NetJob("Imgur Album Creation", APPLICATION->network())); auto imgurResult = std::make_shared(); auto imgurAlbum = ImgurAlbumCreation::make(imgurResult, uploaded); albumTask->addNetAction(imgurAlbum); task.addTask(job); task.addTask(albumTask); connect(&task, &Task::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), reason, QMessageBox::Critical)->show(); }); connect(&task, &Task::aborted, [this] { CustomMessageBox::selectable(this, tr("Screenshots upload aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) ->show(); }); m_uploadActive = true; if (dialog.execWithTask(&task) == QDialog::Accepted) { if (imgurResult->id.isEmpty()) { CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), tr("Unknown error"), QMessageBox::Warning)->exec(); } else { auto link = QString("https://imgur.com/a/%1").arg(imgurResult->id); qDebug() << "ImgurUpload link" << link; QClipboard* clipboard = QApplication::clipboard(); clipboard->setText(link); CustomMessageBox::selectable( this, tr("Upload finished"), tr("The link to the uploaded album has been placed in your clipboard.").arg(link), QMessageBox::Information) ->exec(); } } m_uploadActive = false; } void ScreenshotsPage::on_actionCopy_Image_triggered() { auto selection = ui->listView->selectionModel()->selectedRows(); if (selection.size() < 1) { return; } // You can only copy one image to the clipboard. In the case of multiple selected files, only the first one gets copied. auto item = selection[0]; auto info = m_model->fileInfo(item); QImage image(info.absoluteFilePath()); Q_ASSERT(!image.isNull()); QApplication::clipboard()->setImage(image, QClipboard::Clipboard); } void ScreenshotsPage::on_actionCopy_File_s_triggered() { auto selection = ui->listView->selectionModel()->selectedRows(); if (selection.size() < 1) { // Don't do anything so we don't empty the users clipboard return; } QString buf = ""; for (auto item : selection) { auto info = m_model->fileInfo(item); buf += "file:///" + info.absoluteFilePath() + "\r\n"; } QMimeData* mimeData = new QMimeData(); mimeData->setData("text/uri-list", buf.toLocal8Bit()); QApplication::clipboard()->setMimeData(mimeData); } void ScreenshotsPage::on_actionDelete_triggered() { auto selected = ui->listView->selectionModel()->selectedIndexes(); int count = ui->listView->selectionModel()->selectedRows().size(); QString text; if (count > 1) text = tr("You are about to delete %1 screenshots.\n" "This may be permanent and they will be gone from the folder.\n\n" "Are you sure?") .arg(count); else text = tr("You are about to delete the selected screenshot.\n" "This may be permanent and it will be gone from the folder.\n\n" "Are you sure?") .arg(count); auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), text, QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No)->exec(); if (response != QMessageBox::Yes) return; for (auto item : selected) { if (FS::trash(m_model->filePath(item))) continue; m_model->remove(item); } } void ScreenshotsPage::on_actionRename_triggered() { auto selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.isEmpty()) return; ui->listView->edit(selection[0]); // TODO: mass renaming } void ScreenshotsPage::openedImpl() { if (!m_valid) { m_valid = FS::ensureFolderPathExists(m_folder); } if (m_valid) { QString path = QDir(m_folder).absolutePath(); auto idx = m_model->setRootPath(path); if (idx.isValid()) { ui->listView->setModel(m_filterModel.get()); connect(ui->listView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ScreenshotsPage::onCurrentSelectionChanged); onCurrentSelectionChanged(ui->listView->selectionModel()->selection()); // set initial button enable states ui->listView->setRootIndex(m_filterModel->mapFromSource(idx)); } else { ui->listView->setModel(nullptr); } } auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void ScreenshotsPage::closedImpl() { m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } #include "ScreenshotsPage.moc" PrismLauncher-10.0.5/launcher/ui/pages/instance/ResourcePackPage.cpp0000644000175100017510000002547015144136756025013 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ResourcePackPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Packs")); ui->actionDownloadItem->setToolTip(tr("Download resource packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); connect(ui->actionDownloadItem, &QAction::triggered, this, &ResourcePackPage::downloadResourcePacks); ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected resource packs (all resource packs if none are selected)")); connect(ui->actionUpdateItem, &QAction::triggered, this, &ResourcePackPage::updateResourcePacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); auto updateMenu = new QMenu(this); auto update = updateMenu->addAction(ui->actionUpdateItem->text()); connect(update, &QAction::triggered, this, &ResourcePackPage::updateResourcePacks); updateMenu->addAction(ui->actionResetItemMetadata); connect(ui->actionResetItemMetadata, &QAction::triggered, this, &ResourcePackPage::deleteResourcePackMetadata); ui->actionUpdateItem->setMenu(updateMenu); ui->actionChangeVersion->setToolTip(tr("Change a mod's version.")); connect(ui->actionChangeVersion, &QAction::triggered, this, &ResourcePackPage::changeResourcePackVersion); ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); } void ResourcePackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); auto& rp = static_cast(m_model->at(row)); ui->frame->updateWithResourcePack(rp); } void ResourcePackPage::downloadResourcePacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); m_downloadDialog->open(); } void ResourcePackPage::downloadDialogFinished(int result) { if (result) { auto tasks = new ConcurrentTask("Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); tasks->deleteLater(); }); if (m_downloadDialog) { for (auto& task : m_downloadDialog->getTasks()) { tasks->addTask(task); } } else { qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } if (m_downloadDialog) m_downloadDialog->deleteLater(); } void ResourcePackPage::updateResourcePacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Resource pack updates are unavailable when metadata is disabled!")); return; } if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable( this, tr("Confirm Update"), tr("Updating resource packs while the game is running may cause pack duplication and game crashes.\n" "The old files may not be deleted as they are in use.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto mods_list = m_model->selectedResources(selection); bool use_all = mods_list.empty(); if (use_all) mods_list = m_model->allResources(); ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The resource pack updater was aborted!"), QMessageBox::Warning)->show(); return; } if (update_dialog.noUpdates()) { QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; if (mods_list.size() > 1) { if (use_all) { message = tr("All resource packs are up-to-date! :)"); } else { message = tr("All selected resource packs are up-to-date! :)"); } } CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); return; } if (update_dialog.exec()) { auto tasks = new ConcurrentTask("Download Resource Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } tasks->deleteLater(); }); for (auto task : update_dialog.getTasks()) { tasks->addTask(task); } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } } void ResourcePackPage::deleteResourcePackMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedResourcePacks(selection).length(); if (selectionCount == 0) return; if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 resource packs.\n" "Are you sure?") .arg(selectionCount), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } m_model->deleteMetadata(selection); } void ResourcePackPage::changeResourcePackVersion() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Resource pack updates are unavailable when metadata is disabled!")); return; } const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); if (rows.count() != 1) return; Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); if (resource.metadata() == nullptr) return; m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); m_downloadDialog->setResourceMetadata(resource.metadata()); m_downloadDialog->open(); } PrismLauncher-10.0.5/launcher/ui/pages/instance/NotesPage.cpp0000644000175100017510000000365115144136756023512 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NotesPage.h" #include #include "ui_NotesPage.h" NotesPage::NotesPage(BaseInstance* inst, QWidget* parent) : QWidget(parent), ui(new Ui::NotesPage), m_inst(inst) { ui->setupUi(this); ui->noteEditor->setText(m_inst->notes()); } NotesPage::~NotesPage() { delete ui; } bool NotesPage::apply() { m_inst->setNotes(ui->noteEditor->toPlainText()); return true; } void NotesPage::retranslate() { ui->retranslateUi(this); } PrismLauncher-10.0.5/launcher/ui/pages/instance/ManagedPackPage.h0000644000175100017510000001047515144136756024224 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "BaseInstance.h" #include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/flame/FlameAPI.h" #include "net/NetJob.h" #include "ui/pages/BasePage.h" #include namespace Ui { class ManagedPackPage; } class InstanceTask; class InstanceWindow; class ManagedPackPage : public QWidget, public BasePage { Q_OBJECT public: inline static ManagedPackPage* createPage(BaseInstance* inst, QWidget* parent = nullptr) { return ManagedPackPage::createPage(inst, inst->getManagedPackType(), parent); } static ManagedPackPage* createPage(BaseInstance* inst, QString type, QWidget* parent = nullptr); ~ManagedPackPage() override; QString displayName() const override; QIcon icon() const override; QString helpPage() const override; QString id() const override { return "managed_pack"; } bool shouldDisplay() const override; void openedImpl() override; bool apply() override { return true; } void retranslate() override; /** Gets the necessary information about the managed pack, such as * available versions*/ virtual void parseManagedPack() {}; /** URL of the managed pack. * Not the version-specific one. */ virtual QString url() const { return {}; }; void setInstanceWindow(InstanceWindow* window) { m_instance_window = window; } public slots: /** Gets the current version selection and update the UI, including the update button and the changelog. */ virtual void suggestVersion(); virtual void update() {}; virtual void updateFromFile() {}; protected slots: /** Does the necessary UI changes for when something failed. * * This includes: * - Setting an appropriate text on the version selector to indicate a fail; * - Setting an appropriate text on the changelog text browser to indicate a fail; * - Disable the update button. */ void setFailState(); protected: ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); /** Run the InstanceTask, with a progress dialog and all. * Similar to MainWindow::instanceFromInstanceTask * * Returns whether the task was successful. */ bool runUpdateTask(InstanceTask*); protected: InstanceWindow* m_instance_window = nullptr; Ui::ManagedPackPage* ui; BaseInstance* m_inst; bool m_loaded = false; void onUpdateTaskCompleted(bool did_succeed) const; }; /** Simple page for when we aren't a managed pack. */ class GenericManagedPackPage final : public ManagedPackPage { Q_OBJECT public: GenericManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr) : ManagedPackPage(inst, instance_window, parent) {} ~GenericManagedPackPage() override = default; // TODO: We may want to show this page with some useful info at some point. bool shouldDisplay() const override { return false; }; }; class ModrinthManagedPackPage final : public ManagedPackPage { Q_OBJECT public: ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); ~ModrinthManagedPackPage() override = default; void parseManagedPack() override; QString url() const override; QString helpPage() const override { return "modrinth-managed-pack"; } public slots: void suggestVersion() override; void update() override; void updateFromFile() override; private: Task::Ptr m_fetch_job = nullptr; ModPlatform::IndexedPack m_pack; ModrinthAPI m_api; }; class FlameManagedPackPage final : public ManagedPackPage { Q_OBJECT public: FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); ~FlameManagedPackPage() override = default; void parseManagedPack() override; QString url() const override; QString helpPage() const override { return "curseforge-managed-pack"; } public slots: void suggestVersion() override; void update() override; void updateFromFile() override; private: Task::Ptr m_fetch_job = nullptr; ModPlatform::IndexedPack m_pack; FlameAPI m_api; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/ManagedPackPage.cpp0000644000175100017510000004554615144136756024566 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ManagedPackPage.h" #include #include #include #include "modplatform/ModIndex.h" #include "ui_ManagedPackPage.h" #include #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "InstanceImportTask.h" #include "InstanceList.h" #include "InstanceTask.h" #include "Json.h" #include "Markdown.h" #include "StringUtils.h" #include "ui/InstanceWindow.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "net/ApiDownload.h" /** This is just to override the combo box popup behavior so that the combo box doesn't take the whole screen. * ... thanks Qt. */ class NoBigComboBoxStyle : public QProxyStyle { Q_OBJECT public: // clang-format off int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override { if (hint == QStyle::SH_ComboBox_Popup) return false; return QProxyStyle::styleHint(hint, option, widget, returnData); } // clang-format on /** * Something about QProxyStyle and QStyle objects means they can't be free'd just * because all the widgets using them are gone. * They seems to be tied to the QApplicaiton lifecycle. * So make singletons tied to the lifetime of the application to clean them up and ensure they aren't * being remade over and over again, thus leaking memory. */ public: static NoBigComboBoxStyle* getInstance(QStyle* style) { static QHash s_singleton_instances_ = {}; static std::mutex s_singleton_instances_mutex_; std::lock_guard lock(s_singleton_instances_mutex_); auto inst_iter = s_singleton_instances_.constFind(style); NoBigComboBoxStyle* inst = nullptr; if (inst_iter == s_singleton_instances_.constEnd() || *inst_iter == nullptr) { inst = new NoBigComboBoxStyle(style); inst->setParent(APPLICATION); s_singleton_instances_.insert(style, inst); qDebug() << "QProxyStyle NoBigComboBox created for" << style->objectName() << style; } else { inst = *inst_iter; } return inst; } private: NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} }; ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent) { if (type == "modrinth") return new ModrinthManagedPackPage(inst, nullptr, parent); if (type == "flame" && (APPLICATION->capabilities() & Application::SupportsFlame)) return new FlameManagedPackPage(inst, nullptr, parent); return new GenericManagedPackPage(inst, nullptr, parent); } ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) : QWidget(parent), m_instance_window(instance_window), ui(new Ui::ManagedPackPage), m_inst(inst) { Q_ASSERT(inst); ui->setupUi(this); // NOTE: GTK2 themes crash with the proxy style. // This seems like an upstream bug, so there's not much else that can be done. if (!QStyleFactory::keys().contains("gtk2")) { auto comboStyle = NoBigComboBoxStyle::getInstance(ui->versionsComboBox->style()); ui->versionsComboBox->setStyle(comboStyle); } ui->reloadButton->setVisible(false); connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool) { ui->reloadButton->setVisible(false); m_loaded = false; // Pretend we're opening the page again openedImpl(); }); connect(ui->changelogTextBrowser, &QTextBrowser::anchorClicked, this, [](const QUrl url) { if (url.scheme().isEmpty()) { auto querry = QUrlQuery(url.query()).queryItemValue("remoteUrl", QUrl::FullyDecoded); // curseforge workaround for linkout?remoteUrl= auto decoded = QUrl::fromPercentEncoding(querry.toUtf8()); auto newUrl = QUrl(decoded); if (newUrl.isValid() && (newUrl.scheme() == "http" || newUrl.scheme() == "https")) QDesktopServices ::openUrl(newUrl); return; } QDesktopServices::openUrl(url); }); } ManagedPackPage::~ManagedPackPage() { delete ui; } void ManagedPackPage::openedImpl() { if (m_inst->getManagedPackID().isEmpty()) { ui->packVersion->hide(); ui->packVersionLabel->hide(); ui->packOrigin->hide(); ui->packOriginLabel->hide(); ui->versionsComboBox->hide(); ui->updateButton->hide(); ui->updateToVersionLabel->hide(); ui->updateFromFileButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); ui->packName->setText(m_inst->name()); ui->changelogTextBrowser->setText(tr("This is a local modpack.\n" "This can be updated only using a file in %1 format\n") .arg(displayName())); return; } ui->packName->setText(m_inst->getManagedPackName()); ui->packVersion->setText(m_inst->getManagedPackVersionName()); ui->packOrigin->setText(tr("Website: %2 | Pack ID: %3 | Version ID: %4") .arg(url(), displayName(), m_inst->getManagedPackID(), m_inst->getManagedPackVersionID())); parseManagedPack(); } QString ManagedPackPage::displayName() const { auto type = m_inst->getManagedPackType(); if (type.isEmpty()) return {}; if (type == "flame") type = "CurseForge"; return type.replace(0, 1, type[0].toUpper()); } QIcon ManagedPackPage::icon() const { return QIcon::fromTheme(m_inst->getManagedPackType()); } QString ManagedPackPage::helpPage() const { return {}; } void ManagedPackPage::retranslate() { ui->retranslateUi(this); } bool ManagedPackPage::shouldDisplay() const { return m_inst->isManagedPack(); } bool ManagedPackPage::runUpdateTask(InstanceTask* task) { Q_ASSERT(task); unique_qobject_ptr wrapped_task(APPLICATION->instances()->wrapInstanceTask(task)); connect(task, &Task::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(task, &Task::succeeded, [this, task]() { QStringList warnings = task->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); }); connect(task, &Task::aborted, [this] { CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) ->show(); }); ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(task); return task->wasSuccessful(); } void ManagedPackPage::suggestVersion() { ui->updateButton->setText(tr("Update Pack")); ui->updateButton->setDisabled(false); } void ManagedPackPage::setFailState() { qDebug() << "Setting fail state!"; // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. ui->versionsComboBox->blockSignals(true); ui->versionsComboBox->clear(); ui->versionsComboBox->addItem(tr("Failed to search for available versions."), {}); ui->versionsComboBox->blockSignals(false); ui->changelogTextBrowser->setText(tr("Failed to request changelog data for this modpack.")); ui->updateButton->setText(tr("Cannot update!")); ui->updateButton->setDisabled(true); ui->reloadButton->setVisible(true); } ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) : ManagedPackPage(inst, instance_window, parent) { Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, &QComboBox::currentIndexChanged, this, &ModrinthManagedPackPage::suggestVersion); connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update); connect(ui->updateFromFileButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::updateFromFile); } // MODRINTH void ModrinthManagedPackPage::parseManagedPack() { qDebug() << "Parsing Modrinth pack"; // No need for the extra work because we already have everything we need. if (m_loaded) return; if (m_fetch_job && m_fetch_job->isRunning()) m_fetch_job->abort(); ResourceAPI::Callback> callbacks{}; m_pack = { m_inst->getManagedPackID() }; // Use default if no callbacks are set callbacks.on_succeed = [this](auto& doc) { m_pack.versions = doc; m_pack.versionsLoaded = true; // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. ui->versionsComboBox->blockSignals(true); ui->versionsComboBox->clear(); ui->versionsComboBox->blockSignals(false); for (const auto& version : m_pack.versions) { QString name = version.getVersionDisplayString(); // NOTE: the id from version isn't the same id in the modpack format spec... // e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index.............. if (version.version == m_inst->getManagedPackVersionName()) name = tr("%1 (Current)").arg(name); ui->versionsComboBox->addItem(name, version.fileId); } suggestVersion(); m_loaded = true; }; callbacks.on_fail = [this](QString reason, int) { setFailState(); }; callbacks.on_abort = [this]() { setFailState(); }; m_fetch_job = m_api.getProjectVersions( { std::make_shared(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); m_fetch_job->start(); } QString ModrinthManagedPackPage::url() const { return "https://modrinth.com/mod/" + m_inst->getManagedPackID(); } void ModrinthManagedPackPage::suggestVersion() { auto index = ui->versionsComboBox->currentIndex(); if (m_pack.versions.length() == 0) { setFailState(); return; } auto version = m_pack.versions.at(index); ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(markdownToHTML(version.changelog.toUtf8()))); ManagedPackPage::suggestVersion(); } /// @brief Called when the update task has completed. /// Internally handles the closing of the instance window if the update was successful and shows a message box. /// @param did_succeed Whether the update task was successful. void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const { // Close the window if the update was successful if (did_succeed) { if (m_instance_window != nullptr) m_instance_window->close(); CustomMessageBox::selectable(nullptr, tr("Update Successful"), tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Information) ->show(); } else { CustomMessageBox::selectable( nullptr, tr("Update Failed"), tr("The instance failed to update to pack version %1. Please check launcher logs for more information.") .arg(m_inst->getManagedPackVersionName()), QMessageBox::Critical) ->show(); } } void ModrinthManagedPackPage::update() { auto index = ui->versionsComboBox->currentIndex(); if (m_pack.versions.length() == 0) { setFailState(); return; } auto version = m_pack.versions.at(index); QMap extra_info; // NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field. extra_info.insert("pack_id", m_inst->getManagedPackID()); extra_info.insert("pack_version_id", version.fileId.toString()); extra_info.insert("original_instance_id", m_inst->id()); auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); InstanceName inst_name(m_inst->getManagedPackName(), version.version); inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version)); extracted->setName(inst_name); extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); extracted->setIcon(m_inst->iconKey()); extracted->setConfirmUpdate(false); // Run our task then handle the result auto did_succeed = runUpdateTask(extracted); onUpdateTaskCompleted(did_succeed); } void ModrinthManagedPackPage::updateFromFile() { auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), tr("Modrinth pack") + " (*.mrpack *.zip)"); if (output.isEmpty()) return; QMap extra_info; extra_info.insert("pack_id", m_inst->getManagedPackID()); extra_info.insert("pack_version_id", QString()); extra_info.insert("original_instance_id", m_inst->id()); auto extracted = new InstanceImportTask(output, this, std::move(extra_info)); extracted->setName(m_inst->name()); extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); extracted->setIcon(m_inst->iconKey()); extracted->setConfirmUpdate(false); // Run our task then handle the result auto did_succeed = runUpdateTask(extracted); onUpdateTaskCompleted(did_succeed); } // FLAME FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) : ManagedPackPage(inst, instance_window, parent) { Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, &QComboBox::currentIndexChanged, this, &FlameManagedPackPage::suggestVersion); connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update); connect(ui->updateFromFileButton, &QPushButton::clicked, this, &FlameManagedPackPage::updateFromFile); } void FlameManagedPackPage::parseManagedPack() { qDebug() << "Parsing Flame pack"; // We need to tell the user to redownload the pack, since we didn't save the required info previously if (m_inst->getManagedPackID().isEmpty()) { setFailState(); QString message = tr("

    Hey there!

    " "

    " "It seems like your Pack ID is null. This is because of a bug in older versions of the launcher.
    " "Unfortunately, we can't do the proper API requests without this information.
    " "
    " "So, in order for this feature to work, you will need to re-download the modpack from the built-in downloader.
    " "
    " "Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!" "

    "); ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(message)); return; } // No need for the extra work because we already have everything we need. if (m_loaded) return; if (m_fetch_job && m_fetch_job->isRunning()) m_fetch_job->abort(); QString id = m_inst->getManagedPackID(); m_pack = { id }; ResourceAPI::Callback> callbacks{}; // Use default if no callbacks are set callbacks.on_succeed = [this](auto& doc) { m_pack.versions = doc; m_pack.versionsLoaded = true; // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. ui->versionsComboBox->blockSignals(true); ui->versionsComboBox->clear(); ui->versionsComboBox->blockSignals(false); for (const auto& version : m_pack.versions) { QString name = version.getVersionDisplayString(); if (version.fileId == m_inst->getManagedPackVersionID().toInt()) name = tr("%1 (Current)").arg(name); ui->versionsComboBox->addItem(name, QVariant(version.fileId)); } suggestVersion(); m_loaded = true; }; callbacks.on_fail = [this](QString reason, int) { setFailState(); }; callbacks.on_abort = [this]() { setFailState(); }; m_fetch_job = m_api.getProjectVersions( { std::make_shared(m_pack), {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); m_fetch_job->start(); } QString FlameManagedPackPage::url() const { // FIXME: We should display the websiteUrl field, but this requires doing the API request first :( return "https://www.curseforge.com/projects/" + m_inst->getManagedPackID(); } void FlameManagedPackPage::suggestVersion() { auto index = ui->versionsComboBox->currentIndex(); if (m_pack.versions.length() == 0) { setFailState(); return; } auto version = m_pack.versions.at(index); ui->changelogTextBrowser->setHtml( StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId.toInt()))); ManagedPackPage::suggestVersion(); } void FlameManagedPackPage::update() { auto index = ui->versionsComboBox->currentIndex(); if (m_pack.versions.length() == 0) { setFailState(); return; } auto version = m_pack.versions.at(index); QMap extra_info; extra_info.insert("pack_id", m_inst->getManagedPackID()); extra_info.insert("pack_version_id", version.fileId.toString()); extra_info.insert("original_instance_id", m_inst->id()); auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); extracted->setName(m_inst->name()); extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); extracted->setIcon(m_inst->iconKey()); extracted->setConfirmUpdate(false); auto did_succeed = runUpdateTask(extracted); onUpdateTaskCompleted(did_succeed); } void FlameManagedPackPage::updateFromFile() { auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), tr("CurseForge pack") + " (*.zip)"); if (output.isEmpty()) return; QMap extra_info; extra_info.insert("pack_id", m_inst->getManagedPackID()); extra_info.insert("pack_version_id", QString()); extra_info.insert("original_instance_id", m_inst->id()); auto extracted = new InstanceImportTask(output, this, std::move(extra_info)); extracted->setName(m_inst->name()); extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); extracted->setIcon(m_inst->iconKey()); extracted->setConfirmUpdate(false); auto did_succeed = runUpdateTask(extracted); onUpdateTaskCompleted(did_succeed); } #include "ManagedPackPage.moc" PrismLauncher-10.0.5/launcher/ui/pages/instance/InstanceSettingsPage.h0000644000175100017510000000474615144136756025362 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "BaseInstance.h" #include "ui/pages/BasePage.h" #include "ui/widgets/MinecraftSettingsWidget.h" class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage { Q_OBJECT public: explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr) : MinecraftSettingsWidget(std::move(instance), parent) { connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings); connect(APPLICATION, &Application::globalSettingsApplied, this, &InstanceSettingsPage::loadSettings); } ~InstanceSettingsPage() override {} QString displayName() const override { return tr("Settings"); } QIcon icon() const override { return QIcon::fromTheme("instance-settings"); } QString id() const override { return "settings"; } bool apply() override { saveSettings(); return true; } QString helpPage() const override { return "Instance-settings"; } }; PrismLauncher-10.0.5/launcher/ui/pages/instance/VersionPage.ui0000644000175100017510000001626715144136757023712 0ustar runnerrunner VersionPage 0 0 961 1091 0 0 0 0 Qt::ScrollBarAlwaysOff false false true Search 0 0 Actions Qt::LeftToolBarArea|Qt::RightToolBarArea Qt::ToolButtonTextOnly false RightToolBarArea false Change Version Change version of the selected component. Move Up Make the selected component apply sooner. Move Down Make the selected component apply later. Remove Remove selected component from the instance. Customize Customize selected component. Edit Edit selected component. Revert Revert the selected component to default. Install Loader Install a mod loader. Add to Minecraft.jar Add a mod into the Minecraft jar file. Replace Minecraft.jar Add Agents Add Java agents. Add Empty Add an empty custom component. Reload Reload all components. Download all Download the files needed to launch the instance now. Open .minecraft Open the instance's .minecraft folder. Open libraries Open the instance's local libraries folder. Import Components Import existing component JSON files. ModListView QTreeView
    ui/widgets/ModListView.h
    InfoFrame QFrame
    ui/widgets/InfoFrame.h
    1
    WideBar QToolBar
    ui/widgets/WideBar.h
    PrismLauncher-10.0.5/launcher/ui/pages/instance/McResolver.cpp0000644000175100017510000000430015144136756023676 0ustar runnerrunner#include #include #include #include #include "McResolver.h" McResolver::McResolver(QObject* parent, QString domain, int port) : QObject(parent), m_constrDomain(domain), m_constrPort(port) {} void McResolver::ping() { pingWithDomainSRV(m_constrDomain, m_constrPort); } void McResolver::pingWithDomainSRV(QString domain, int port) { QDnsLookup* lookup = new QDnsLookup(this); lookup->setName(QString("_minecraft._tcp.%1").arg(domain)); lookup->setType(QDnsLookup::SRV); connect(lookup, &QDnsLookup::finished, this, [this, domain, port]() { QDnsLookup* lookup = qobject_cast(sender()); lookup->deleteLater(); if (lookup->error() != QDnsLookup::NoError) { qDebug() << QString("Warning: SRV record lookup failed (%1), trying A record lookup").arg(lookup->errorString()); pingWithDomainA(domain, port); return; } auto records = lookup->serviceRecords(); if (records.isEmpty()) { qDebug() << "Warning: no SRV entries found for domain, trying A record lookup"; pingWithDomainA(domain, port); return; } const auto& firstRecord = records.at(0); QString newDomain = firstRecord.target(); int newPort = firstRecord.port(); pingWithDomainA(newDomain, newPort); }); lookup->lookup(); } void McResolver::pingWithDomainA(QString domain, int port) { QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo& hostInfo) { if (hostInfo.error() != QHostInfo::NoError) { emitFail("A record lookup failed"); return; } auto records = hostInfo.addresses(); if (records.isEmpty()) { emitFail("No A entries found for domain"); return; } const auto& firstRecord = records.at(0); emitSucceed(firstRecord.toString(), port); }); } void McResolver::emitFail(QString error) { qDebug() << "DNS resolver error:" << error; emit failed(error); emit finished(); } void McResolver::emitSucceed(QString ip, int port) { emit succeeded(ip, port); emit finished(); } PrismLauncher-10.0.5/launcher/ui/pages/instance/ScreenshotsPage.h0000644000175100017510000000632715144136756024372 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ui/pages/BasePage.h" #include "settings/Setting.h" class QFileSystemModel; class QIdentityProxyModel; class QItemSelection; namespace Ui { class ScreenshotsPage; } struct ScreenShot; class ScreenshotList; class ImgurAlbumCreation; class ScreenshotsPage : public QMainWindow, public BasePage { Q_OBJECT public: explicit ScreenshotsPage(QString path, QWidget* parent = 0); virtual ~ScreenshotsPage(); void openedImpl() override; void closedImpl() override; enum { NothingDone = 0x42 }; virtual bool eventFilter(QObject*, QEvent*) override; virtual QString displayName() const override { return tr("Screenshots"); } virtual QIcon icon() const override { return QIcon::fromTheme("screenshots"); } virtual QString id() const override { return "screenshots"; } virtual QString helpPage() const override { return "Screenshots-management"; } virtual bool apply() override { return !m_uploadActive; } void retranslate() override; protected: QMenu* createPopupMenu() override; private slots: void on_actionUpload_triggered(); void on_actionCopy_Image_triggered(); void on_actionCopy_File_s_triggered(); void on_actionDelete_triggered(); void on_actionRename_triggered(); void on_actionView_Folder_triggered(); void onItemActivated(QModelIndex); void onCurrentSelectionChanged(const QItemSelection& selected); void ShowContextMenu(const QPoint& pos); private: Ui::ScreenshotsPage* ui; std::shared_ptr m_model; std::shared_ptr m_filterModel; QString m_folder; bool m_valid = false; bool m_uploadActive = false; std::shared_ptr m_wide_bar_setting = nullptr; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/ServerPingTask.cpp0000644000175100017510000000333315144136756024531 0ustar runnerrunner#include #include #include "Exception.h" #include "McClient.h" #include "McResolver.h" #include "ServerPingTask.h" unsigned getOnlinePlayers(QJsonObject data) { try { return Json::requireInteger(Json::requireObject(data, "players"), "online"); } catch (Exception& e) { qWarning() << "server ping failed to parse response" << e.what(); return 0; } } void ServerPingTask::executeTask() { qDebug() << "Querying status of" << QString("%1:%2").arg(m_domain).arg(m_port); // Resolve the actual IP and port for the server McResolver* resolver = new McResolver(nullptr, m_domain, m_port); connect(resolver, &McResolver::succeeded, this, [this](QString ip, int port) { qDebug().nospace().noquote() << "Resolved address for " << m_domain << ": " << ip << ":" << port; // Now that we have the IP and port, query the server McClient* client = new McClient(nullptr, m_domain, ip, port); connect(client, &McClient::succeeded, this, [this](QJsonObject data) { m_outputOnlinePlayers = getOnlinePlayers(data); qDebug() << "Online players:" << m_outputOnlinePlayers; emitSucceeded(); }); connect(client, &McClient::failed, this, [this](QString error) { emitFailed(error); }); // Delete McClient object when done connect(client, &McClient::finished, this, [client]() { client->deleteLater(); }); client->getStatusData(); }); connect(resolver, &McResolver::failed, this, [this](QString error) { emitFailed(error); }); // Delete McResolver object when done connect(resolver, &McResolver::finished, [resolver]() { resolver->deleteLater(); }); resolver->ping(); } PrismLauncher-10.0.5/launcher/ui/pages/instance/TexturePackPage.cpp0000644000175100017510000002570315144136756024663 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "TexturePackPage.h" #include "ResourceDownloadTask.h" #include "minecraft/mod/TexturePack.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/dialogs/ResourceUpdateDialog.h" TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) { ui->actionDownloadItem->setText(tr("Download Packs")); ui->actionDownloadItem->setToolTip(tr("Download texture packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); connect(ui->actionDownloadItem, &QAction::triggered, this, &TexturePackPage::downloadTexturePacks); ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected texture packs (all texture packs if none are selected)")); connect(ui->actionUpdateItem, &QAction::triggered, this, &TexturePackPage::updateTexturePacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); auto updateMenu = new QMenu(this); auto update = updateMenu->addAction(ui->actionUpdateItem->text()); connect(update, &QAction::triggered, this, &TexturePackPage::updateTexturePacks); updateMenu->addAction(ui->actionResetItemMetadata); connect(ui->actionResetItemMetadata, &QAction::triggered, this, &TexturePackPage::deleteTexturePackMetadata); ui->actionUpdateItem->setMenu(updateMenu); ui->actionChangeVersion->setToolTip(tr("Change a texture pack's version.")); connect(ui->actionChangeVersion, &QAction::triggered, this, &TexturePackPage::changeTexturePackVersion); ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); ui->actionViewHomepage->setToolTip(tr("View the homepages of all selected texture packs.")); } void TexturePackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); auto& rp = static_cast(m_model->at(row)); ui->frame->updateWithTexturePack(rp); } void TexturePackPage::downloadTexturePacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); m_downloadDialog->open(); } void TexturePackPage::downloadDialogFinished(int result) { if (result) { auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); tasks->deleteLater(); }); if (m_downloadDialog) { for (auto& task : m_downloadDialog->getTasks()) { tasks->addTask(task); } } else { qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } if (m_downloadDialog) m_downloadDialog->deleteLater(); } void TexturePackPage::updateTexturePacks() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Texture pack updates are unavailable when metadata is disabled!")); return; } if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable( this, tr("Confirm Update"), tr("Updating texture packs while the game is running may cause pack duplication and game crashes.\n" "The old files may not be deleted as they are in use.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto mods_list = m_model->selectedResources(selection); bool use_all = mods_list.empty(); if (use_all) mods_list = m_model->allResources(); ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { CustomMessageBox::selectable(this, tr("Aborted"), tr("The texture pack updater was aborted!"), QMessageBox::Warning)->show(); return; } if (update_dialog.noUpdates()) { QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; if (mods_list.size() > 1) { if (use_all) { message = tr("All texture packs are up-to-date! :)"); } else { message = tr("All selected texture packs are up-to-date! :)"); } } CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); return; } if (update_dialog.exec()) { auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); }); connect(tasks, &Task::aborted, [this, tasks]() { CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); tasks->deleteLater(); }); connect(tasks, &Task::succeeded, [this, tasks]() { QStringList warnings = tasks->warnings(); if (warnings.count()) { CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } tasks->deleteLater(); }); for (auto task : update_dialog.getTasks()) { tasks->addTask(task); } ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(tasks); m_model->update(); } } void TexturePackPage::deleteTexturePackMetadata() { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto selectionCount = m_model->selectedTexturePacks(selection).length(); if (selectionCount == 0) return; if (selectionCount > 1) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove the metadata for %1 texture packs.\n" "Are you sure?") .arg(selectionCount), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } m_model->deleteMetadata(selection); } void TexturePackPage::changeTexturePackVersion() { if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { QMessageBox::critical(this, tr("Error"), tr("Texture pack updates are unavailable when metadata is disabled!")); return; } const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); if (rows.count() != 1) return; Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); if (resource.metadata() == nullptr) return; m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); m_downloadDialog->setResourceMetadata(resource.metadata()); m_downloadDialog->open(); } PrismLauncher-10.0.5/launcher/ui/pages/instance/ScreenshotsPage.ui0000644000175100017510000000557515144136756024564 0ustar runnerrunner ScreenshotsPage 0 0 800 600 0 0 0 0 false QAbstractItemView::SelectionMode::ExtendedSelection QAbstractItemView::SelectionBehavior::SelectRows QListView::Movement::Static Actions Qt::ToolButtonStyle::ToolButtonTextOnly RightToolBarArea false Upload Delete Rename View Folder Copy Image Copy Image Copy File(s) Copy File(s) WideBar QToolBar
    ui/widgets/WideBar.h
    PrismLauncher-10.0.5/launcher/ui/pages/instance/LogPage.h0000644000175100017510000000675715144136756022622 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "ui/pages/BasePage.h" namespace Ui { class LogPage; } class QTextCharFormat; class LogFormatProxyModel : public QIdentityProxyModel { public: LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} QVariant data(const QModelIndex& index, int role) const override; QFont getFont() const { return m_font; } void setFont(QFont font) { m_font = font; } QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; private: QFont m_font; }; class LogPage : public QWidget, public BasePage { Q_OBJECT public: explicit LogPage(InstancePtr instance, QWidget* parent = 0); virtual ~LogPage(); virtual QString displayName() const override { return tr("Minecraft Log"); } virtual QIcon icon() const override { return QIcon::fromTheme("log"); } virtual QString id() const override { return "console"; } virtual bool apply() override; virtual QString helpPage() const override { return "Minecraft-Logs"; } virtual bool shouldDisplay() const override; void retranslate() override; private slots: void on_btnPaste_clicked(); void on_btnCopy_clicked(); void on_btnClear_clicked(); void on_btnBottom_clicked(); void on_trackLogCheckbox_clicked(bool checked); void on_wrapCheckbox_clicked(bool checked); void on_colorCheckbox_clicked(bool checked); void on_findButton_clicked(); void findActivated(); void findNextActivated(); void findPreviousActivated(); void onInstanceLaunchTaskChanged(shared_qobject_ptr proc); private: void modelStateToUI(); void UIToModelState(); void setInstanceLaunchTaskChanged(shared_qobject_ptr proc, bool initial); private: Ui::LogPage* ui; InstancePtr m_instance; shared_qobject_ptr m_process; LogFormatProxyModel* m_proxy; shared_qobject_ptr m_model; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/McClient.h0000644000175100017510000000274415144136756022772 0ustar runnerrunner#pragma once #include #include #include #include #include #include // Client for the Minecraft protocol class McClient : public QObject { Q_OBJECT QString m_domain; QString m_ip; short m_port; QTcpSocket m_socket; // 0: did not start reading the response yet // 1: read the response length, still reading the response // 2: finished reading the response unsigned m_responseReadState = 0; unsigned m_wantedRespLength = 0; QByteArray m_resp; public: explicit McClient(QObject* parent, QString domain, QString ip, short port); //! Read status data of the server, and calls the succeeded() signal with the parsed JSON data void getStatusData(); private: void sendRequest(); //! Accumulate data until we have a full response, then call parseResponse() once void readRawResponse(); void parseResponse(); void writeVarInt(QByteArray& data, int value); int readVarInt(QByteArray& data); char readByte(QByteArray& data); //! write number with specified size in big endian format void writeFixedInt(QByteArray& data, int value, int size); void writeString(QByteArray& data, const std::string& value); void writePacketToSocket(QByteArray& data); void emitFail(QString error); void emitSucceed(QJsonObject data); signals: void succeeded(QJsonObject data); void failed(QString error); void finished(); }; PrismLauncher-10.0.5/launcher/ui/pages/instance/DataPackPage.h0000644000175100017510000000521615144136756023536 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "ExternalResourcesPage.h" #include "minecraft/mod/DataPackFolderModel.h" #include "ui/dialogs/ResourceDownloadDialog.h" class DataPackPage : public ExternalResourcesPage { Q_OBJECT public: explicit DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); QString displayName() const override { return QObject::tr("Data Packs"); } QIcon icon() const override { return QIcon::fromTheme("datapacks"); } QString id() const override { return "datapacks"; } QString helpPage() const override { return "Data-packs"; } bool shouldDisplay() const override { return true; } public slots: void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; void downloadDataPacks(); void downloadDialogFinished(int result); void updateDataPacks(); void deleteDataPackMetadata(); void changeDataPackVersion(); private: std::shared_ptr m_model; QPointer m_downloadDialog; }; /** * Syncs DataPackPage with GlobalDataPacksPath and shows/hides based on GlobalDataPacksEnabled. */ class GlobalDataPackPage : public QWidget, public BasePage { public: explicit GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent = nullptr); QString displayName() const override; QIcon icon() const override; QString id() const override { return "datapacks"; } QString helpPage() const override; bool shouldDisplay() const override; bool apply() override; void openedImpl() override; void closedImpl() override; void setParentContainer(BasePageContainer* container) override; private: void updateContent(); QVBoxLayout* layout() { return static_cast(QWidget::layout()); } MinecraftInstance* m_instance; DataPackPage* m_underlyingPage = nullptr; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/VersionPage.cpp0000644000175100017510000004701615144136757024053 0ustar runnerrunner// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022-2023 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Application.h" #include #include #include #include #include #include #include #include #include #include #include "QObjectPtr.h" #include "VersionPage.h" #include "meta/JsonFormat.h" #include "tasks/SequentialTask.h" #include "ui/dialogs/InstallLoaderDialog.h" #include "ui_VersionPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/NewComponentDialog.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/VersionSelectDialog.h" #include "ui/GuiUtil.h" #include "DesktopServices.h" #include "Exception.h" #include "icons/IconList.h" #include "minecraft/PackProfile.h" #include "minecraft/auth/AccountList.h" #include "meta/Index.h" #include "meta/VersionList.h" class IconProxy : public QIdentityProxyModel { Q_OBJECT public: IconProxy(QWidget* parentWidget) : QIdentityProxyModel(parentWidget) { connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone); m_parentWidget = parentWidget; } virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const override { QVariant var = QIdentityProxyModel::data(proxyIndex, role); int column = proxyIndex.column(); if (column == 0 && role == Qt::DecorationRole && m_parentWidget) { if (!var.isNull()) { auto string = var.toString(); if (string == "warning") { return QIcon::fromTheme("status-yellow"); } else if (string == "error") { return QIcon::fromTheme("status-bad"); } } return QIcon::fromTheme("status-good"); } return var; } private slots: void widgetGone() { m_parentWidget = nullptr; } private: QWidget* m_parentWidget = nullptr; }; QIcon VersionPage::icon() const { return APPLICATION->icons()->getIcon(m_inst->iconKey()); } bool VersionPage::shouldDisplay() const { return true; } void VersionPage::retranslate() { ui->retranslateUi(this); } void VersionPage::openedImpl() { auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void VersionPage::closedImpl() { m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } QMenu* VersionPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction(ui->toolBar->toggleViewAction()); return filteredMenu; } VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst) { ui->setupUi(this); ui->toolBar->insertSpacer(ui->actionReload); m_profile = m_inst->getPackProfile(); reloadPackProfile(); auto proxy = new IconProxy(ui->packageView); proxy->setSourceModel(m_profile.get()); m_filterModel = new QSortFilterProxyModel(this); m_filterModel->setDynamicSortFilter(true); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); m_filterModel->setSourceModel(proxy); m_filterModel->setFilterKeyColumn(-1); ui->packageView->setModel(m_filterModel); ui->packageView->installEventFilter(this); ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection); ui->packageView->setContextMenuPolicy(Qt::CustomContextMenu); auto smodel = ui->packageView->selectionModel(); connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent); connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent); connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); updateVersionControls(); preselect(0); connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); connect(ui->packageView, &QAbstractItemView::activated, this, [this](const QModelIndex& index) { auto component = m_profile->getComponent(index.row()); component->setEnabled(!component->isEnabled()); }); connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged); } VersionPage::~VersionPage() { delete ui; } void VersionPage::showContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); menu->exec(ui->packageView->mapToGlobal(pos)); delete menu; } void VersionPage::packageCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { if (!current.isValid()) { ui->frame->clear(); return; } int row = current.row(); auto patch = m_profile->getComponent(row); auto severity = patch->getProblemSeverity(); switch (severity) { case ProblemSeverity::Warning: ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName())); break; case ProblemSeverity::Error: ui->frame->setName(tr("%1 has issues!").arg(patch->getName())); break; default: case ProblemSeverity::None: ui->frame->clear(); return; } auto& problems = patch->getProblems(); QString problemOut; for (auto& problem : problems) { if (problem.m_severity == ProblemSeverity::Error) { problemOut += tr("Error: "); } else if (problem.m_severity == ProblemSeverity::Warning) { problemOut += tr("Warning: "); } problemOut += problem.m_description; problemOut += "\n"; } ui->frame->setDescription(problemOut); } void VersionPage::updateVersionControls() { updateButtons(); } void VersionPage::updateButtons(int row) { if (row == -1) row = currentRow(); auto patch = m_profile->getComponent(row); ui->actionRemove->setEnabled(patch && patch->isRemovable()); ui->actionMove_down->setEnabled(patch && patch->isMoveable()); ui->actionMove_up->setEnabled(patch && patch->isMoveable()); ui->actionChange_version->setEnabled(patch && patch->isVersionChangeable(false)); ui->actionEdit->setEnabled(patch && patch->isCustom()); ui->actionCustomize->setEnabled(patch && patch->isCustomizable()); ui->actionRevert->setEnabled(patch && patch->isRevertible()); } bool VersionPage::reloadPackProfile() { try { auto result = m_profile->reload(Net::Mode::Online); if (!result) { QMessageBox::critical(this, tr("Error"), result.error); } return result; } catch (const Exception& e) { QMessageBox::critical(this, tr("Error"), e.cause()); return false; } catch (...) { QMessageBox::critical(this, tr("Error"), tr("Couldn't load the instance profile.")); return false; } } void VersionPage::on_actionReload_triggered() { reloadPackProfile(); m_container->refreshContainer(); } void VersionPage::on_actionRemove_triggered() { if (!ui->packageView->currentIndex().isValid()) { return; } int index = ui->packageView->currentIndex().row(); auto component = m_profile->getComponent(index); if (component->isCustom()) { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove \"%1\".\n" "This is permanent and will completely remove the custom component.\n\n" "Are you sure?") .arg(component->getName()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; } // FIXME: use actual model, not reloading. if (!m_profile->remove(index)) { QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); } updateButtons(); reloadPackProfile(); m_container->refreshContainer(); } void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() { auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods") + " (*.zip *.jar)", APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!list.empty()) { m_profile->installJarMods(list); } updateButtons(); } void VersionPage::on_actionReplace_Minecraft_jar_triggered() { auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement") + " (*.jar)", APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!jarPath.isEmpty()) { m_profile->installCustomJar(jarPath); } updateButtons(); } void VersionPage::on_actionImport_Components_triggered() { QStringList list = GuiUtil::BrowseForFiles("component", tr("Select components"), tr("Components") + " (*.json)", APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!list.isEmpty()) { if (!m_profile->installComponents(list)) { QMessageBox::warning(this, tr("Failed to import components"), tr("Some components could not be imported. Check logs for details")); } } updateButtons(); } void VersionPage::on_actionAdd_Agents_triggered() { QStringList list = GuiUtil::BrowseForFiles("agent", tr("Select agents"), tr("Java agents") + " (*.jar)", APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); if (!list.isEmpty()) m_profile->installAgents(list); updateButtons(); } void VersionPage::on_actionMove_up_triggered() { try { m_profile->move(currentRow(), PackProfile::MoveUp); } catch (const Exception& e) { QMessageBox::critical(this, tr("Error"), e.cause()); } updateButtons(); } void VersionPage::on_actionMove_down_triggered() { try { m_profile->move(currentRow(), PackProfile::MoveDown); } catch (const Exception& e) { QMessageBox::critical(this, tr("Error"), e.cause()); } updateButtons(); } void VersionPage::on_actionChange_version_triggered() { auto versionRow = currentRow(); if (versionRow == -1) { return; } auto patch = m_profile->getComponent(versionRow); auto name = patch->getName(); auto list = patch->getVersionList(); list->clearExternalRecommends(); if (!list) { return; } auto uid = list->uid(); // recommend the correct lwjgl version for the current minecraft version if (uid == "org.lwjgl" || uid == "org.lwjgl3") { auto minecraft = m_profile->getComponent("net.minecraft"); auto lwjglReq = std::find_if(minecraft->m_cachedRequires.cbegin(), minecraft->m_cachedRequires.cend(), [uid](const Meta::Require& req) -> bool { return req.uid == uid; }); if (lwjglReq != minecraft->m_cachedRequires.cend()) { auto lwjglVersion = !lwjglReq->equalsVersion.isEmpty() ? lwjglReq->equalsVersion : lwjglReq->suggests; if (!lwjglVersion.isEmpty()) { list->addExternalRecommends({ lwjglVersion }); } } } VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") { vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); } vselect.setExactIfPresentFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); auto currentVersion = patch->getVersion(); if (!currentVersion.isEmpty()) { vselect.setCurrentVersion(currentVersion); } if (!vselect.exec() || !vselect.selectedVersion()) return; qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor(); bool important = false; if (uid == "net.minecraft") { important = true; if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_inst->settings()->get("AutomaticJava").toBool() && m_inst->settings()->get("OverrideJavaLocation").toBool()) { m_inst->settings()->set("OverrideJavaLocation", false); m_inst->settings()->set("JavaPath", ""); } } m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important); m_profile->resolve(Net::Mode::Online); m_container->refreshContainer(); } void VersionPage::on_actionDownload_All_triggered() { if (!APPLICATION->accounts()->anyAccountIsValid()) { CustomMessageBox::selectable(this, tr("Error"), tr("Cannot download Minecraft or update instances unless you have at least " "one account added.\nPlease add a Microsoft account."), QMessageBox::Warning) ->show(); return; } auto updateTasks = m_inst->createUpdateTask(); if (updateTasks.isEmpty()) { return; } auto task = makeShared(); for (auto t : updateTasks) { task->addTask(t); } ProgressDialog tDialog(this); connect(task.get(), &Task::failed, this, &VersionPage::onGameUpdateError); // FIXME: unused return value tDialog.execWithTask(task.get()); updateButtons(); m_container->refreshContainer(); } void VersionPage::on_actionInstall_Loader_triggered() { InstallLoaderDialog dialog(m_inst->getPackProfile(), QString(), this); dialog.exec(); m_container->refreshContainer(); } void VersionPage::on_actionAdd_Empty_triggered() { NewComponentDialog compdialog(QString(), QString(), this); QStringList blacklist; for (int i = 0; i < m_profile->rowCount(); i++) { auto comp = m_profile->getComponent(i); blacklist.push_back(comp->getID()); } compdialog.setBlacklist(blacklist); if (compdialog.exec()) { qDebug() << "name:" << compdialog.name(); qDebug() << "uid:" << compdialog.uid(); m_profile->installEmpty(compdialog.uid(), compdialog.name()); } } void VersionPage::on_actionLibrariesFolder_triggered() { DesktopServices::openPath(m_inst->getLocalLibraryPath(), true); } void VersionPage::on_actionMinecraftFolder_triggered() { DesktopServices::openPath(m_inst->gameRoot(), true); } void VersionPage::versionCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { currentIdx = current.row(); updateButtons(currentIdx); } void VersionPage::preselect(int row) { if (row < 0) { row = 0; } if (row >= m_profile->rowCount(QModelIndex())) { row = m_profile->rowCount(QModelIndex()) - 1; } if (row < 0) { return; } auto model_index = m_profile->index(row); ui->packageView->selectionModel()->select(model_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); updateButtons(row); } void VersionPage::onGameUpdateError(QString error) { CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show(); } ComponentPtr VersionPage::current() { auto row = currentRow(); if (row < 0) { return nullptr; } return m_profile->getComponent(row); } int VersionPage::currentRow() { if (ui->packageView->selectionModel()->selectedRows().isEmpty()) { return -1; } return ui->packageView->selectionModel()->selectedRows().first().row(); } void VersionPage::on_actionCustomize_triggered() { auto version = currentRow(); if (version == -1) { return; } auto patch = m_profile->getComponent(version); if (!patch->getVersionFile()) { // TODO: wait for the update task to finish here... return; } if (!m_profile->customize(version)) { // TODO: some error box here } updateButtons(); preselect(currentIdx); } void VersionPage::on_actionEdit_triggered() { auto version = current(); if (!version) { return; } auto filename = version->getFilename(); if (!QFileInfo::exists(filename)) { qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!"; return; } APPLICATION->openJsonEditor(filename); } void VersionPage::on_actionRevert_triggered() { auto version = currentRow(); if (version == -1) { return; } auto component = m_profile->getComponent(version); auto response = CustomMessageBox::selectable(this, tr("Confirm Reversion"), tr("You are about to revert \"%1\".\n" "This is permanent and will completely revert your customizations.\n\n" "Are you sure?") .arg(component->getName()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; if (!m_profile->revertToBase(version)) { // TODO: some error box here } updateButtons(); preselect(currentIdx); m_container->refreshContainer(); } void VersionPage::onFilterTextChanged(const QString& newContents) { m_filterModel->setFilterFixedString(newContents); } #include "VersionPage.moc" PrismLauncher-10.0.5/launcher/ui/pages/instance/WorldListPage.cpp0000644000175100017510000003630115144136757024344 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "WorldListPage.h" #include "minecraft/WorldList.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_WorldListPage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "FileSystem.h" #include "tools/MCEditTool.h" #include "DesktopServices.h" #include "ui/GuiUtil.h" #include "Application.h" #include "DataPackPage.h" class WorldListProxyModel : public QSortFilterProxyModel { Q_OBJECT public: WorldListProxyModel(QObject* parent) : QSortFilterProxyModel(parent) {} virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const { QModelIndex sourceIndex = mapToSource(index); if (index.column() == 0 && role == Qt::DecorationRole) { WorldList* worlds = qobject_cast(sourceModel()); auto iconFile = worlds->data(sourceIndex, WorldList::IconFileRole).toString(); if (iconFile.isNull()) { // NOTE: Minecraft uses the same placeholder for servers AND worlds return QIcon::fromTheme("unknown_server"); } return QIcon(iconFile); } return sourceIndex.data(role); } }; WorldListPage::WorldListPage(MinecraftInstancePtr inst, std::shared_ptr worlds, QWidget* parent) : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) { ui->setupUi(this); ui->toolBar->insertSpacer(ui->actionRefresh); WorldListProxyModel* proxy = new WorldListProxyModel(this); proxy->setSortCaseSensitivity(Qt::CaseInsensitive); proxy->setSourceModel(m_worlds.get()); proxy->setSortRole(Qt::UserRole); ui->worldTreeView->setSortingEnabled(true); ui->worldTreeView->setModel(proxy); ui->worldTreeView->installEventFilter(this); ui->worldTreeView->setContextMenuPolicy(Qt::CustomContextMenu); ui->worldTreeView->setIconSize(QSize(64, 64)); connect(ui->worldTreeView, &QTreeView::customContextMenuRequested, this, &WorldListPage::ShowContextMenu); auto head = ui->worldTreeView->header(); head->setSectionResizeMode(0, QHeaderView::Stretch); head->setSectionResizeMode(1, QHeaderView::ResizeToContents); head->setSectionResizeMode(4, QHeaderView::ResizeToContents); connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged); worldChanged(QModelIndex(), QModelIndex()); } void WorldListPage::openedImpl() { m_worlds->startWatching(); auto mInst = std::dynamic_pointer_cast(m_inst); if (!mInst || !mInst->traits().contains("feature:is_quick_play_singleplayer")) { ui->toolBar->removeAction(ui->actionJoin); } auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void WorldListPage::closedImpl() { m_worlds->stopWatching(); m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } WorldListPage::~WorldListPage() { m_worlds->stopWatching(); delete ui; } void WorldListPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); menu->exec(ui->worldTreeView->mapToGlobal(pos)); delete menu; } QMenu* WorldListPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction(ui->toolBar->toggleViewAction()); return filteredMenu; } bool WorldListPage::shouldDisplay() const { return true; } void WorldListPage::retranslate() { ui->retranslateUi(this); } bool WorldListPage::worldListFilter(QKeyEvent* keyEvent) { if (keyEvent->key() == Qt::Key_Delete) { on_actionRemove_triggered(); return true; } return QWidget::eventFilter(ui->worldTreeView, keyEvent); } bool WorldListPage::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() != QEvent::KeyPress) { return QWidget::eventFilter(obj, ev); } QKeyEvent* keyEvent = static_cast(ev); if (obj == ui->worldTreeView) return worldListFilter(keyEvent); return QWidget::eventFilter(obj, ev); } void WorldListPage::on_actionRemove_triggered() { auto proxiedIndex = getSelectedWorld(); if (!proxiedIndex.isValid()) return; auto result = CustomMessageBox::selectable(this, tr("Confirm Deletion"), tr("You are about to delete \"%1\".\n" "The world may be gone forever (A LONG TIME).\n\n" "Are you sure?") .arg(m_worlds->allWorlds().at(proxiedIndex.row()).name()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (result != QMessageBox::Yes) { return; } m_worlds->stopWatching(); m_worlds->deleteWorld(proxiedIndex.row()); m_worlds->startWatching(); } void WorldListPage::on_actionView_Folder_triggered() { DesktopServices::openPath(m_worlds->dir().absolutePath(), true); } void WorldListPage::on_actionData_Packs_triggered() { QModelIndex index = getSelectedWorld(); if (!index.isValid()) { return; } if (!worldSafetyNagQuestion(tr("Manage Data Packs"))) return; const QString fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); const QString folder = FS::PathCombine(fullPath, "datapacks"); auto dialog = new QDialog(this); dialog->setWindowTitle(tr("Data packs for %1").arg(m_worlds->data(index, WorldList::NameRole).toString())); dialog->setWindowModality(Qt::WindowModal); dialog->resize(static_cast(std::max(0.5 * window()->width(), 400.0)), static_cast(std::max(0.75 * window()->height(), 400.0))); dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("DataPackDownloadGeometry").toByteArray())); GenericPageProvider provider(dialog->windowTitle()); provider.addPageCreator([this, folder] { bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); auto model = std::make_shared(folder, m_inst.get(), isIndexed, true); return new DataPackPage(m_inst.get(), std::move(model)); }); auto layout = new QVBoxLayout(dialog); auto focusStealer = new QPushButton(dialog); layout->addWidget(focusStealer); focusStealer->setDefault(true); focusStealer->hide(); auto pageContainer = new PageContainer(&provider, {}, dialog); pageContainer->hidePageList(); layout->addWidget(pageContainer); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help); connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); connect(buttonBox, &QDialogButtonBox::helpRequested, pageContainer, &PageContainer::help); layout->addWidget(buttonBox); dialog->setLayout(layout); dialog->exec(); APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); } void WorldListPage::on_actionReset_Icon_triggered() { auto proxiedIndex = getSelectedWorld(); if (!proxiedIndex.isValid()) return; if (m_worlds->resetIcon(proxiedIndex.row())) { ui->actionReset_Icon->setEnabled(false); } } QModelIndex WorldListPage::getSelectedWorld() { auto index = ui->worldTreeView->selectionModel()->currentIndex(); auto proxy = (QSortFilterProxyModel*)ui->worldTreeView->model(); return proxy->mapToSource(index); } void WorldListPage::on_actionCopy_Seed_triggered() { QModelIndex index = getSelectedWorld(); if (!index.isValid()) { return; } int64_t seed = m_worlds->data(index, WorldList::SeedRole).toLongLong(); APPLICATION->clipboard()->setText(QString::number(seed)); } void WorldListPage::on_actionMCEdit_triggered() { if (m_mceditStarting) return; auto mcedit = APPLICATION->mcedit(); const QString mceditPath = mcedit->path(); QModelIndex index = getSelectedWorld(); if (!index.isValid()) { return; } if (!worldSafetyNagQuestion(tr("Open World in MCEdit"))) return; auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); auto program = mcedit->getProgramPath(); if (program.size()) { #ifdef Q_OS_WIN32 if (!QProcess::startDetached(program, { fullPath }, mceditPath)) { mceditError(); } #else m_mceditProcess.reset(new LoggedProcess()); m_mceditProcess->setDetachable(true); connect(m_mceditProcess.get(), &LoggedProcess::stateChanged, this, &WorldListPage::mceditState); m_mceditProcess->start(program, { fullPath }); m_mceditProcess->setWorkingDirectory(mceditPath); m_mceditStarting = true; #endif } else { QMessageBox::warning(this->parentWidget(), tr("No MCEdit found or set up!"), tr("You do not have MCEdit set up or it was moved.\nYou can set it up in the global settings.")); } } void WorldListPage::mceditError() { QMessageBox::warning(this->parentWidget(), tr("MCEdit failed to start!"), tr("MCEdit failed to start.\nIt may be necessary to reinstall it.")); } void WorldListPage::mceditState(LoggedProcess::State state) { bool failed = false; switch (state) { case LoggedProcess::NotRunning: case LoggedProcess::Starting: return; case LoggedProcess::FailedToStart: case LoggedProcess::Crashed: case LoggedProcess::Aborted: { failed = true; } /* fallthrough */ case LoggedProcess::Running: case LoggedProcess::Finished: { m_mceditStarting = false; break; } } if (failed) { mceditError(); } } void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { QModelIndex index = getSelectedWorld(); bool enable = index.isValid(); ui->actionCopy_Seed->setEnabled(enable); ui->actionMCEdit->setEnabled(enable); ui->actionRemove->setEnabled(enable); ui->actionCopy->setEnabled(enable); ui->actionRename->setEnabled(enable); ui->actionData_Packs->setEnabled(enable); bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); ui->actionReset_Icon->setEnabled(enable && hasIcon); auto mInst = std::dynamic_pointer_cast(m_inst); auto supportsJoin = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); ui->actionJoin->setEnabled(enable && supportsJoin); if (!supportsJoin) { ui->toolBar->removeAction(ui->actionJoin); } } void WorldListPage::on_actionAdd_triggered() { auto list = GuiUtil::BrowseForFiles(displayName(), tr("Select a Minecraft world zip"), tr("Minecraft World Zip File") + " (*.zip)", QString(), this->parentWidget()); if (!list.empty()) { m_worlds->stopWatching(); for (auto filename : list) { m_worlds->installWorld(QFileInfo(filename)); } m_worlds->startWatching(); } } bool WorldListPage::isWorldSafe(QModelIndex) { return !m_inst->isRunning(); } bool WorldListPage::worldSafetyNagQuestion(const QString& actionType) { if (!isWorldSafe(getSelectedWorld())) { auto result = QMessageBox::question( this, actionType, tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?")); if (result == QMessageBox::No) { return false; } } return true; } void WorldListPage::on_actionCopy_triggered() { QModelIndex index = getSelectedWorld(); if (!index.isValid()) { return; } if (!worldSafetyNagQuestion(tr("Copy World"))) return; auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); auto world = (World*)worldVariant.value(); bool ok = false; QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world->name(), &ok); if (ok && name.length() > 0) { world->install(m_worlds->dir().absolutePath(), name); } } void WorldListPage::on_actionRename_triggered() { QModelIndex index = getSelectedWorld(); if (!index.isValid()) { return; } if (!worldSafetyNagQuestion(tr("Rename World"))) return; auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); auto world = (World*)worldVariant.value(); bool ok = false; QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new world name."), QLineEdit::Normal, world->name(), &ok); if (ok && name.length() > 0) { world->rename(name); } } void WorldListPage::on_actionRefresh_triggered() { m_worlds->update(); } void WorldListPage::on_actionJoin_triggered() { QModelIndex index = getSelectedWorld(); if (!index.isValid()) { return; } auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); auto world = (World*)worldVariant.value(); APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(world->folderName(), true))); } #include "WorldListPage.moc" PrismLauncher-10.0.5/launcher/ui/pages/instance/ModFolderPage.h0000644000175100017510000001006615144136756023740 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ExternalResourcesPage.h" #include "ui/dialogs/ResourceDownloadDialog.h" class ModFolderPage : public ExternalResourcesPage { Q_OBJECT inline bool handleNoModLoader(); public: explicit ModFolderPage(BaseInstance* inst, std::shared_ptr model, QWidget* parent = nullptr); virtual ~ModFolderPage() = default; void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } virtual QString displayName() const override { return tr("Mods"); } virtual QIcon icon() const override { return QIcon::fromTheme("loadermods"); } virtual QString id() const override { return "mods"; } virtual QString helpPage() const override { return "Loader-mods"; } virtual bool shouldDisplay() const override; public slots: void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; private slots: void removeItems(const QItemSelection& selection) override; void downloadMods(); void downloadDialogFinished(int result); void updateMods(bool includeDeps = false); void deleteModMetadata(); void exportModMetadata(); void changeModVersion(); protected: std::shared_ptr m_model; QPointer m_downloadDialog; }; class CoreModFolderPage : public ModFolderPage { Q_OBJECT public: explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); virtual ~CoreModFolderPage() = default; virtual QString displayName() const override { return tr("Core Mods"); } virtual QIcon icon() const override { return QIcon::fromTheme("coremods"); } virtual QString id() const override { return "coremods"; } virtual QString helpPage() const override { return "Core-mods"; } virtual bool shouldDisplay() const override; }; class NilModFolderPage : public ModFolderPage { Q_OBJECT public: explicit NilModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); virtual ~NilModFolderPage() = default; virtual QString displayName() const override { return tr("Nilmods"); } virtual QIcon icon() const override { return QIcon::fromTheme("coremods"); } virtual QString id() const override { return "nilmods"; } virtual QString helpPage() const override { return "Nilmods"; } virtual bool shouldDisplay() const override; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/ServersPage.cpp0000644000175100017510000005452715144136756024063 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ServersPage.h" #include "Application.h" #include "ServerPingTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ServersPage.h" #include #include #include #include #include #include #include #include #include #include #include #include static const int COLUMN_COUNT = 3; // 3 , TBD: latency and other nice things. struct Server { // Types enum class AcceptsTextures : int { ASK = 0, ALWAYS = 1, NEVER = 2 }; // Methods Server() { m_name = QObject::tr("Minecraft Server"); } Server(const QString& name, const QString& address) { m_name = name; m_address = address; } Server(nbt::tag_compound& server) { std::string addressStr(server["ip"]); m_address = QString::fromUtf8(addressStr.c_str()); std::string nameStr(server["name"]); m_name = QString::fromUtf8(nameStr.c_str()); if (server["icon"]) { std::string base64str(server["icon"]); m_icon = QByteArray::fromBase64(base64str.c_str()); } if (server.has_key("acceptTextures", nbt::tag_type::Byte)) { bool value = server["acceptTextures"].as().get(); if (value) { m_acceptsTextures = AcceptsTextures::ALWAYS; } else { m_acceptsTextures = AcceptsTextures::NEVER; } } } void serialize(nbt::tag_compound& server) { server.insert("name", m_name.trimmed().toUtf8().toStdString()); server.insert("ip", m_address.trimmed().toUtf8().toStdString()); if (m_icon.size()) { server.insert("icon", m_icon.toBase64().toStdString()); } if (m_acceptsTextures != AcceptsTextures::ASK) { server.insert("acceptTextures", nbt::tag_byte(m_acceptsTextures == AcceptsTextures::ALWAYS)); } } // Data - persistent and user changeable QString m_name; QString m_address; AcceptsTextures m_acceptsTextures = AcceptsTextures::ASK; // Data - persistent and automatically updated QByteArray m_icon; // Data - temporary std::optional m_currentPlayers; // nullopt if not calculated/calculating }; static std::unique_ptr parseServersDat(const QString& filename) { try { QByteArray input = FS::read(filename); std::istringstream foo(std::string(input.constData(), input.size())); auto pair = nbt::io::read_compound(foo); if (pair.first != "") return nullptr; if (pair.second == nullptr) return nullptr; return std::move(pair.second); } catch (...) { return nullptr; } } static bool serializeServerDat(const QString& filename, nbt::tag_compound* levelInfo) { try { if (!FS::ensureFilePathExists(filename)) { return false; } std::ostringstream s; nbt::io::write_tag("", *levelInfo, s); QByteArray val(s.str().data(), (int)s.str().size()); FS::write(filename, val); return true; } catch (...) { return false; } } class ServersModel : public QAbstractListModel { Q_OBJECT public: enum Roles { ServerPtrRole = Qt::UserRole, }; explicit ServersModel(const QString& path, QObject* parent = 0) : QAbstractListModel(parent) { m_path = path; m_watcher = new QFileSystemWatcher(this); connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ServersModel::fileChanged); connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &ServersModel::dirChanged); m_saveTimer.setSingleShot(true); m_saveTimer.setInterval(5000); connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal); } virtual ~ServersModel() = default; void observe() { if (m_observed) { return; } m_observed = true; if (!m_loaded) { load(); } updateFSObserver(); } void unobserve() { if (!m_observed) { return; } m_observed = false; updateFSObserver(); } void lock() { if (m_locked) { return; } saveNow(); m_locked = true; updateFSObserver(); } void unlock() { if (!m_locked) { return; } m_locked = false; updateFSObserver(); } int addEmptyRow(int position) { if (m_locked) { return -1; } if (position < 0 || position >= rowCount()) { position = rowCount(); } beginInsertRows(QModelIndex(), position, position); m_servers.insert(position, Server()); endInsertRows(); scheduleSave(); return position; } bool removeRow(int row) { if (m_locked) { return false; } if (row < 0 || row >= rowCount()) { return false; } beginRemoveRows(QModelIndex(), row, row); m_servers.removeAt(row); endRemoveRows(); // does absolutely nothing, the selected server stays as the next line... scheduleSave(); return true; } bool moveUp(int row) { if (m_locked) { return false; } if (row <= 0) { return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); m_servers.swapItemsAt(row - 1, row); endMoveRows(); scheduleSave(); return true; } bool moveDown(int row) { if (m_locked) { return false; } int count = rowCount(); if (row + 1 >= count) { return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); m_servers.swapItemsAt(row + 1, row); endMoveRows(); scheduleSave(); return true; } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if (section < 0 || section >= COLUMN_COUNT) return QVariant(); if (role == Qt::DisplayRole) { switch (section) { case 0: return tr("Name"); case 1: return tr("Address"); case 2: return tr("Online"); } } return QAbstractListModel::headerData(section, orientation, role); } virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override { if (!index.isValid()) return QVariant(); int row = index.row(); int column = index.column(); if (column < 0 || column >= COLUMN_COUNT) return QVariant(); if (row < 0 || row >= m_servers.size()) return QVariant(); switch (role) { case Qt::DecorationRole: { if (column == 0) { auto& bytes = m_servers[row].m_icon; if (bytes.size()) { QPixmap px; if (px.loadFromData(bytes)) return QIcon(px); } return QIcon::fromTheme("unknown_server"); } else { return QVariant(); } } case Qt::DisplayRole: switch (column) { case 0: return m_servers[row].m_name; case 1: return m_servers[row].m_address; case 2: if (m_servers[row].m_currentPlayers) { return *m_servers[row].m_currentPlayers; } else { return "..."; } default: return QVariant(); } case ServerPtrRole: if (column == 0) return QVariant::fromValue((void*)&m_servers[row]); else return QVariant(); default: return QVariant(); } } virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_servers.size(); } int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : COLUMN_COUNT; } Server* at(int index) { if (index < 0 || index >= rowCount()) { return nullptr; } return &m_servers[index]; } void setName(int row, const QString& name) { if (m_locked) { return; } auto server = at(row); if (!server || server->m_name == name) { return; } server->m_name = name; emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); scheduleSave(); } void setAddress(int row, const QString& address) { if (m_locked) { return; } auto server = at(row); if (!server || server->m_address == address) { return; } server->m_address = address; emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); scheduleSave(); } void setAcceptsTextures(int row, Server::AcceptsTextures textures) { if (m_locked) { return; } auto server = at(row); if (!server || server->m_acceptsTextures == textures) { return; } server->m_acceptsTextures = textures; emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); scheduleSave(); } void load() { cancelSave(); beginResetModel(); QList servers; auto serversDat = parseServersDat(serversPath()); if (serversDat) { auto& serversList = serversDat->at("servers").as(); for (auto iter = serversList.begin(); iter != serversList.end(); iter++) { auto& serverTag = (*iter).as(); Server s(serverTag); servers.append(s); } } m_servers.swap(servers); m_loaded = true; endResetModel(); } void saveNow() { if (saveIsScheduled()) { save_internal(); } } void queryServersStatus() { // Abort the currently running task if present if (m_currentQueryTask != nullptr) { m_currentQueryTask->abort(); qDebug() << "Aborted previous server query task"; } m_currentQueryTask = ConcurrentTask::Ptr( new ConcurrentTask("Query servers status", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); int row = 0; for (Server& server : m_servers) { // reset current players server.m_currentPlayers = {}; emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); // Start task to query server status auto target = MinecraftTarget::parse(server.m_address, false); auto* task = new ServerPingTask(target.address, target.port); m_currentQueryTask->addTask(Task::Ptr(task)); // Update the model when the task is done connect(task, &Task::finished, this, [this, task, row]() { if (m_servers.size() < row) return; m_servers[row].m_currentPlayers = task->m_outputOnlinePlayers; emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); }); row++; } m_currentQueryTask->start(); } public slots: void dirChanged(const QString& path) { qDebug() << "Changed:" << path; load(); } void fileChanged(const QString& path) { qDebug() << "Changed:" << path; } private slots: void save_internal() { cancelSave(); QString path = serversPath(); qDebug() << "Server list about to be saved to" << path; nbt::tag_compound out; nbt::tag_list list; for (auto& server : m_servers) { nbt::tag_compound serverNbt; server.serialize(serverNbt); list.push_back(std::move(serverNbt)); } out.insert("servers", nbt::value(std::move(list))); if (!serializeServerDat(path, &out)) { qDebug() << "Failed to save server list:" << path << "Will try again."; scheduleSave(); } } private: void scheduleSave() { if (!m_loaded) { qDebug() << "Server list should never save if it didn't successfully load, path:" << m_path; return; } if (!m_dirty) { m_dirty = true; qDebug() << "Server list save is scheduled for" << m_path; } m_saveTimer.start(); } void cancelSave() { m_dirty = false; m_saveTimer.stop(); } bool saveIsScheduled() const { return m_dirty; } void updateFSObserver() { bool observingFS = m_watcher->directories().contains(m_path); if (m_observed && m_locked) { if (!observingFS) { qWarning() << "Will watch" << m_path; if (!m_watcher->addPath(m_path)) { qWarning() << "Failed to start watching" << m_path; } } } else { if (observingFS) { qWarning() << "Will stop watching" << m_path; if (!m_watcher->removePath(m_path)) { qWarning() << "Failed to stop watching" << m_path; } } } } QString serversPath() { QFileInfo foo(FS::PathCombine(m_path, "servers.dat")); return foo.filePath(); } private: bool m_loaded = false; bool m_locked = false; bool m_observed = false; bool m_dirty = false; QString m_path; QList m_servers; QFileSystemWatcher* m_watcher = nullptr; QTimer m_saveTimer; ConcurrentTask::Ptr m_currentQueryTask = nullptr; }; ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent), ui(new Ui::ServersPage) { ui->setupUi(this); m_inst = inst; m_model = new ServersModel(inst->gameRoot(), this); ui->serversView->setIconSize(QSize(64, 64)); ui->serversView->setModel(m_model); ui->serversView->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->serversView, &QTreeView::customContextMenuRequested, this, &ServersPage::ShowContextMenu); auto head = ui->serversView->header(); if (head->count()) { head->setSectionResizeMode(0, QHeaderView::Stretch); for (int i = 1; i < head->count(); i++) { head->setSectionResizeMode(i, QHeaderView::ResizeToContents); } } auto selectionModel = ui->serversView->selectionModel(); connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged); connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::runningStateChanged); connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); connect(ui->resourceComboBox, &QComboBox::currentIndexChanged, this, &ServersPage::resourceIndexChanged); connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); m_locked = m_inst->isRunning(); if (m_locked) { m_model->lock(); } updateState(); } ServersPage::~ServersPage() { m_model->saveNow(); delete ui; } void ServersPage::retranslate() { ui->retranslateUi(this); } void ServersPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); menu->exec(ui->serversView->mapToGlobal(pos)); delete menu; } QMenu* ServersPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction(ui->toolBar->toggleViewAction()); return filteredMenu; } void ServersPage::runningStateChanged(bool running) { if (m_locked == running) { return; } m_locked = running; if (m_locked) { m_model->lock(); } else { m_model->unlock(); } updateState(); } void ServersPage::currentChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { int nextServer = -1; if (!current.isValid()) { nextServer = -1; } else { nextServer = current.row(); } currentServer = nextServer; updateState(); } // WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal. void ServersPage::rowsRemoved([[maybe_unused]] const QModelIndex& parent, int first, int last) { if (currentServer < first) { // current was before the removal return; } else if (currentServer >= first && currentServer <= last) { // current got removed... return; } else { // current was past the removal int count = last - first + 1; currentServer -= count; } } void ServersPage::nameEdited(const QString& name) { m_model->setName(currentServer, name); } void ServersPage::addressEdited(const QString& address) { m_model->setAddress(currentServer, address); } void ServersPage::resourceIndexChanged(int index) { auto acceptsTextures = Server::AcceptsTextures(index); m_model->setAcceptsTextures(currentServer, acceptsTextures); } void ServersPage::updateState() { auto server = m_model->at(currentServer); bool serverEditEnabled = server && !m_locked; ui->addressLine->setEnabled(serverEditEnabled); ui->nameLine->setEnabled(serverEditEnabled); ui->resourceComboBox->setEnabled(serverEditEnabled); ui->actionMove_Down->setEnabled(serverEditEnabled); ui->actionMove_Up->setEnabled(serverEditEnabled); ui->actionRemove->setEnabled(serverEditEnabled); ui->actionJoin->setEnabled(serverEditEnabled); if (server) { ui->addressLine->setText(server->m_address); ui->nameLine->setText(server->m_name); ui->resourceComboBox->setCurrentIndex(int(server->m_acceptsTextures)); } else { ui->addressLine->setText(QString()); ui->nameLine->setText(QString()); ui->resourceComboBox->setCurrentIndex(0); } ui->actionAdd->setDisabled(m_locked); } void ServersPage::openedImpl() { m_model->observe(); auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); // ping servers m_model->queryServersStatus(); } void ServersPage::closedImpl() { m_model->unobserve(); m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } void ServersPage::on_actionAdd_triggered() { int position = m_model->addEmptyRow(currentServer + 1); if (position < 0) { return; } // select the new row ui->serversView->selectionModel()->setCurrentIndex( m_model->index(position), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear | QItemSelectionModel::Rows); currentServer = position; } void ServersPage::on_actionRemove_triggered() { auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), tr("You are about to remove \"%1\".\n" "This is permanent and the server will be gone from your list forever (A LONG TIME).\n\n" "Are you sure?") .arg(m_model->at(currentServer)->m_name), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return; m_model->removeRow(currentServer); } void ServersPage::on_actionMove_Up_triggered() { if (m_model->moveUp(currentServer)) { currentServer--; } } void ServersPage::on_actionMove_Down_triggered() { if (m_model->moveDown(currentServer)) { currentServer++; } } void ServersPage::on_actionJoin_triggered() { const auto& address = m_model->at(currentServer)->m_address; APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(address, false))); } void ServersPage::on_actionRefresh_triggered() { m_model->queryServersStatus(); } #include "ServersPage.moc" PrismLauncher-10.0.5/launcher/ui/pages/instance/ServersPage.h0000644000175100017510000000650015144136756023514 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "BaseInstance.h" #include "ui/pages/BasePage.h" #include "settings/Setting.h" namespace Ui { class ServersPage; } struct Server; class ServersModel; class MinecraftInstance; class ServersPage : public QMainWindow, public BasePage { Q_OBJECT public: explicit ServersPage(InstancePtr inst, QWidget* parent = 0); virtual ~ServersPage(); void openedImpl() override; void closedImpl() override; virtual QString displayName() const override { return tr("Servers"); } virtual QIcon icon() const override { return QIcon::fromTheme("server"); } virtual QString id() const override { return "servers"; } virtual QString helpPage() const override { return "Servers-management"; } void retranslate() override; protected: QMenu* createPopupMenu() override; private: void updateState(); void scheduleSave(); bool saveIsScheduled() const; private slots: void currentChanged(const QModelIndex& current, const QModelIndex& previous); void rowsRemoved(const QModelIndex& parent, int first, int last); void on_actionAdd_triggered(); void on_actionRemove_triggered(); void on_actionMove_Up_triggered(); void on_actionMove_Down_triggered(); void on_actionJoin_triggered(); void on_actionRefresh_triggered(); void runningStateChanged(bool running); void nameEdited(const QString& name); void addressEdited(const QString& address); void resourceIndexChanged(int index); void ShowContextMenu(const QPoint& pos); private: // data int currentServer = -1; bool m_locked = true; Ui::ServersPage* ui = nullptr; ServersModel* m_model = nullptr; InstancePtr m_inst = nullptr; std::shared_ptr m_wide_bar_setting = nullptr; }; PrismLauncher-10.0.5/launcher/ui/pages/instance/ServersPage.ui0000644000175100017510000001237715144136756023713 0ustar runnerrunner ServersPage 0 0 1318 879 0 0 0 0 0 0 true true QAbstractItemView::SingleSelection QAbstractItemView::SelectRows 64 64 false false 6 6 &Name nameLine Address addressLine Reso&urces resourceComboBox Ask to download Always download Never download Actions Qt::LeftToolBarArea|Qt::RightToolBarArea Qt::ToolButtonTextOnly false RightToolBarArea false Add Remove Move Up Move Down Join Refresh WideBar QToolBar
    ui/widgets/WideBar.h
    serversView nameLine addressLine resourceComboBox
    PrismLauncher-10.0.5/launcher/ui/pages/instance/ManagedPackPage.ui0000644000175100017510000001476115144136756024414 0ustar runnerrunner ManagedPackPage 0 0 731 538 0 0 6 0 0 0 Pack Information Pack Name: placeholder Current version: IBeamCursor placeholder Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse Provider information: IBeamCursor placeholder Qt::RichText true Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse Qt::Horizontal 0 0 Update to version: false 0 0 Fetching versions... 0 0 Update From File 0 0 Changelog No changelog available for this version! false Reload page ProjectDescriptionPage QTextBrowser
    ui/widgets/ProjectDescriptionPage.h
    PrismLauncher-10.0.5/launcher/ui/pages/instance/WorldListPage.h0000644000175100017510000000705215144136757024012 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" #include "settings/Setting.h" class WorldList; namespace Ui { class WorldListPage; } class WorldListPage : public QMainWindow, public BasePage { Q_OBJECT public: explicit WorldListPage(MinecraftInstancePtr inst, std::shared_ptr worlds, QWidget* parent = 0); virtual ~WorldListPage(); virtual QString displayName() const override { return tr("Worlds"); } virtual QIcon icon() const override { return QIcon::fromTheme("worlds"); } virtual QString id() const override { return "worlds"; } virtual QString helpPage() const override { return "Worlds"; } virtual bool shouldDisplay() const override; void retranslate() override; virtual void openedImpl() override; virtual void closedImpl() override; protected: bool eventFilter(QObject* obj, QEvent* ev) override; bool worldListFilter(QKeyEvent* ev); QMenu* createPopupMenu() override; protected: MinecraftInstancePtr m_inst; private: QModelIndex getSelectedWorld(); bool isWorldSafe(QModelIndex index); bool worldSafetyNagQuestion(const QString& actionType); void mceditError(); private: Ui::WorldListPage* ui; std::shared_ptr m_worlds; unique_qobject_ptr m_mceditProcess; bool m_mceditStarting = false; std::shared_ptr m_wide_bar_setting = nullptr; private slots: void on_actionCopy_Seed_triggered(); void on_actionMCEdit_triggered(); void on_actionRemove_triggered(); void on_actionAdd_triggered(); void on_actionCopy_triggered(); void on_actionRename_triggered(); void on_actionRefresh_triggered(); void on_actionView_Folder_triggered(); void on_actionData_Packs_triggered(); void on_actionReset_Icon_triggered(); void worldChanged(const QModelIndex& current, const QModelIndex& previous); void mceditState(LoggedProcess::State state); void on_actionJoin_triggered(); void ShowContextMenu(const QPoint& pos); }; PrismLauncher-10.0.5/launcher/ui/pages/instance/VersionPage.h0000644000175100017510000001012615144136757023510 0ustar runnerrunner// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022-2023 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/pages/BasePage.h" namespace Ui { class VersionPage; } class VersionPage : public QMainWindow, public BasePage { Q_OBJECT public: explicit VersionPage(MinecraftInstance* inst, QWidget* parent = 0); virtual ~VersionPage(); virtual QString displayName() const override { return tr("Version"); } virtual QIcon icon() const override; virtual QString id() const override { return "version"; } virtual QString helpPage() const override { return "Instance-Version"; } virtual bool shouldDisplay() const override; void retranslate() override; void openedImpl() override; void closedImpl() override; private slots: void on_actionChange_version_triggered(); void on_actionInstall_Loader_triggered(); void on_actionAdd_Empty_triggered(); void on_actionReload_triggered(); void on_actionRemove_triggered(); void on_actionMove_up_triggered(); void on_actionMove_down_triggered(); void on_actionAdd_to_Minecraft_jar_triggered(); void on_actionReplace_Minecraft_jar_triggered(); void on_actionImport_Components_triggered(); void on_actionAdd_Agents_triggered(); void on_actionRevert_triggered(); void on_actionEdit_triggered(); void on_actionCustomize_triggered(); void on_actionDownload_All_triggered(); void on_actionMinecraftFolder_triggered(); void on_actionLibrariesFolder_triggered(); void updateVersionControls(); private: ComponentPtr current(); int currentRow(); void updateButtons(int row = -1); void preselect(int row = 0); int doUpdate(); protected: QMenu* createPopupMenu() override; /// FIXME: this shouldn't be necessary! bool reloadPackProfile(); private: Ui::VersionPage* ui; QSortFilterProxyModel* m_filterModel; std::shared_ptr m_profile; MinecraftInstance* m_inst; int currentIdx = 0; std::shared_ptr m_wide_bar_setting = nullptr; public slots: void versionCurrent(const QModelIndex& current, const QModelIndex& previous); private slots: void onGameUpdateError(QString error); void packageCurrent(const QModelIndex& current, const QModelIndex& previous); void showContextMenu(const QPoint& pos); void onFilterTextChanged(const QString& newContents); }; PrismLauncher-10.0.5/launcher/ui/pages/instance/OtherLogsPage.ui0000644000175100017510000001473415144136756024167 0ustar runnerrunner OtherLogsPage 0 0 657 538 0 0 6 0 0 0 &Find Qt::Vertical 0 0 Scroll all the way to bottom &Bottom false false true Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse false 0 0 Delete the selected log &Delete Selected Delete all the logs Delete &All Keep updating true Wrap lines true Color lines true Qt::Horizontal 40 20 Copy the whole log into the clipboard &Copy Upload the log to the paste service configured in preferences &Upload Reload the contents of the log from the disk &Reload Search LogView QPlainTextEdit
    ui/widgets/LogView.h
    selectLogBox btnReload btnCopy btnPaste btnDelete btnClean wrapCheckbox colorCheckbox text searchBar findButton
    PrismLauncher-10.0.5/launcher/ui/pages/instance/McResolver.h0000644000175100017510000000123615144136756023350 0ustar runnerrunner#include #include #include #include #include // resolve the IP and port of a Minecraft server class McResolver : public QObject { Q_OBJECT QString m_constrDomain; int m_constrPort; public: explicit McResolver(QObject* parent, QString domain, int port); void ping(); private: void pingWithDomainSRV(QString domain, int port); void pingWithDomainA(QString domain, int port); void emitFail(QString error); void emitSucceed(QString ip, int port); signals: void succeeded(QString ip, int port); void failed(QString error); void finished(); }; PrismLauncher-10.0.5/launcher/ui/pages/BasePage.h0000644000175100017510000000506615144136756021137 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "BasePageContainer.h" class BasePage { public: using updateExtraInfoFunc = std::function; virtual ~BasePage() {} virtual QString id() const = 0; virtual QString displayName() const = 0; virtual QIcon icon() const = 0; virtual bool apply() { return true; } virtual bool shouldDisplay() const { return true; } virtual QString helpPage() const { return QString(); } void opened() { isOpened = true; openedImpl(); } void closed() { isOpened = false; closedImpl(); } virtual void openedImpl() {} virtual void closedImpl() {} virtual void setParentContainer(BasePageContainer* container) { m_container = container; }; virtual void retranslate() {} public: int stackIndex = -1; int listIndex = -1; updateExtraInfoFunc updateExtraInfo; protected: BasePageContainer* m_container = nullptr; bool isOpened = false; }; using BasePagePtr = std::shared_ptr; PrismLauncher-10.0.5/launcher/ui/pages/BasePageProvider.h0000644000175100017510000000317415144136756022650 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ui/pages/BasePage.h" class BasePageProvider { public: virtual QList getPages() = 0; virtual QString dialogTitle() = 0; }; class GenericPageProvider : public BasePageProvider { using PageCreator = std::function; public: explicit GenericPageProvider(const QString& dialogTitle) : m_dialogTitle(dialogTitle) {} virtual ~GenericPageProvider() {} QList getPages() override { QList pages; for (PageCreator creator : m_creators) { pages.append(creator()); } return pages; } QString dialogTitle() override { return m_dialogTitle; } void setDialogTitle(const QString& title) { m_dialogTitle = title; } void addPageCreator(PageCreator page) { m_creators.append(page); } template void addPage() { addPageCreator([]() { return new PageClass(); }); } private: QList m_creators; QString m_dialogTitle; }; PrismLauncher-10.0.5/launcher/ui/pages/BasePageContainer.h0000644000175100017510000000053615144136756022777 0ustar runnerrunner#pragma once class BasePage; class BasePageContainer { public: virtual ~BasePageContainer() {}; virtual bool selectPage(QString pageId) = 0; virtual BasePage* selectedPage() const = 0; virtual BasePage* getPage(QString pageId) { return nullptr; }; virtual void refreshContainer() = 0; virtual bool requestClose() = 0; }; PrismLauncher-10.0.5/launcher/ui/pages/global/0000755000175100017510000000000015144136756020550 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/global/APIPage.ui0000644000175100017510000003265615144136756022331 0ustar runnerrunner APIPage 0 0 841 620 0 0 0 0 true 0 -216 816 832 &Pastebin Service Paste Service &Type pasteTypeComboBox 0 0 Base &URL baseURLEntry Use Default true Note: you probably want to change or clear the Base URL after changing the paste service type. true Meta&data Server You can set this to a third-party metadata server to use patched libraries or other hacks. Qt::RichText true true Use Default Assets Server You can set this to another server if you have problems with downloading assets. Qt::RichText true true Use Default 0 0 User Agent Use Default Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. true &API Keys &Microsoft Authentication Qt::RichText true true msaClientID Use Default Note: you probably don't need to set this if logging in via Microsoft Authentication already works. Qt::RichText true Qt::Vertical QSizePolicy::Fixed 0 6 Mod&rinth Qt::RichText true true modrinthToken true Use None <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html> true true Qt::Vertical QSizePolicy::Fixed 0 6 &CurseForge Qt::RichText true true flameKey true Use Default Note: you probably don't need to set this if CurseForge already works. true Qt::Vertical QSizePolicy::Fixed 0 6 &Technic technicClientID Use Default <html><head/><body><p>Note: you only need to set this to access private data.</p></body></html> true Qt::Vertical 0 0 PrismLauncher-10.0.5/launcher/ui/pages/global/JavaPage.cpp0000644000175100017510000001043315144136756022733 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "JavaPage.h" #include "BuildConfig.h" #include "JavaCommon.h" #include "java/JavaInstall.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/java/InstallJavaDialog.h" #include "ui_JavaPage.h" #include #include #include #include #include #include #include "ui/dialogs/VersionSelectDialog.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include #include #include "Application.h" #include "settings/SettingsObject.h" JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) { ui->setupUi(this); if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { ui->managedJavaList->initialize(new JavaInstallList(this, true)); ui->managedJavaList->setResizeOn(2); ui->managedJavaList->selectCurrent(); ui->managedJavaList->setEmptyString(tr("No managed Java versions are installed")); ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed Java list!")); } else ui->tabWidget->tabBar()->hide(); } JavaPage::~JavaPage() { delete ui; } void JavaPage::retranslate() { ui->retranslateUi(this); } bool JavaPage::apply() { ui->javaSettings->saveSettings(); JavaCommon::checkJVMArgs(APPLICATION->settings()->get("JvmArgs").toString(), this); return true; } void JavaPage::on_downloadJavaButton_clicked() { auto jdialog = new Java::InstallDialog({}, nullptr, this); jdialog->exec(); ui->managedJavaList->loadList(); } void JavaPage::on_removeJavaButton_clicked() { auto version = ui->managedJavaList->selectedVersion(); auto dcast = std::dynamic_pointer_cast(version); if (!dcast) { return; } QDir dir(APPLICATION->javaPath()); auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { if (dcast->path.startsWith(entry.canonicalFilePath())) { auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), tr("You are about to remove the Java installation named \"%1\".\n" "Are you sure?") .arg(entry.fileName()), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response == QMessageBox::Yes) { FS::deletePath(entry.canonicalFilePath()); ui->managedJavaList->loadList(); } break; } } } void JavaPage::on_refreshJavaButton_clicked() { ui->managedJavaList->loadList(); } PrismLauncher-10.0.5/launcher/ui/pages/global/APIPage.h0000644000175100017510000000455415144136756022137 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * Copyright (c) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ui/pages/BasePage.h" namespace Ui { class APIPage; } class APIPage : public QWidget, public BasePage { Q_OBJECT public: explicit APIPage(QWidget* parent = 0); ~APIPage(); QString displayName() const override { return tr("Services"); } QIcon icon() const override { return QIcon::fromTheme("worlds"); } QString id() const override { return "apis"; } QString helpPage() const override { return "APIs"; } virtual bool apply() override; void retranslate() override; private: int baseURLPasteType; void resetBaseURLNote(); void updateBaseURLNote(int index); void updateBaseURLPlaceholder(int index); void loadSettings(); void applySettings(); private: Ui::APIPage* ui; }; PrismLauncher-10.0.5/launcher/ui/pages/global/ExternalToolsPage.ui0000644000175100017510000002312015144136756024505 0ustar runnerrunner ExternalToolsPage 0 0 673 823 0 0 0 0 true 0 0 669 819 &Editors &Text Editor jsonEditorTextBox Browse Used to edit component JSON files. Qt::Vertical QSizePolicy::Fixed 0 6 &MCEdit mceditPathEdit Browse 0 0 Check <html><head/><body><p><a href="https://www.mcedit.net/">MCEdit Website</a> - Used as world editor in the instance Worlds menu.</p></body></html> &Profilers Profilers are accessible through the Launch dropdown menu. jsonEditorTextBox Qt::Vertical QSizePolicy::Fixed 0 6 J&Profiler jprofilerPathEdit Browse 0 0 Check <html><head/><body><p><a href="https://www.ej-technologies.com/products/jprofiler/overview.html">JProfiler Website</a></p></body></html> Qt::Vertical QSizePolicy::Fixed 0 6 &VisualVM jvisualvmPathEdit Browse 0 0 Check <html><head/><body><p><a href="https://visualvm.github.io/">VisualVM Website</a></p></body></html> Qt::Vertical 0 0 PrismLauncher-10.0.5/launcher/ui/pages/global/JavaPage.ui0000644000175100017510000001047515144136756022574 0ustar runnerrunner JavaPage 0 0 559 659 0 0 0 0 6 0 0 General true 0 0 535 612 0 0 0 0 Installations Download Remove Qt::Horizontal 40 20 Refresh 0 0 VersionSelectWidget QWidget
    ui/widgets/VersionSelectWidget.h
    1
    JavaSettingsWidget QWidget
    ui/widgets/JavaSettingsWidget.h
    1
    PrismLauncher-10.0.5/launcher/ui/pages/global/ExternalToolsPage.cpp0000644000175100017510000001676115144136756024667 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ExternalToolsPage.h" #include "ui_ExternalToolsPage.h" #include #include #include #include #include #include #include "Application.h" #include "settings/SettingsObject.h" #include "tools/BaseProfiler.h" ExternalToolsPage::ExternalToolsPage(QWidget* parent) : QWidget(parent), ui(new Ui::ExternalToolsPage) { ui->setupUi(this); ui->jsonEditorTextBox->setClearButtonEnabled(true); ui->mceditLink->setOpenExternalLinks(true); ui->jvisualvmLink->setOpenExternalLinks(true); ui->jprofilerLink->setOpenExternalLinks(true); loadSettings(); } ExternalToolsPage::~ExternalToolsPage() { delete ui; } void ExternalToolsPage::loadSettings() { auto s = APPLICATION->settings(); ui->jprofilerPathEdit->setText(s->get("JProfilerPath").toString()); ui->jvisualvmPathEdit->setText(s->get("JVisualVMPath").toString()); ui->mceditPathEdit->setText(s->get("MCEditPath").toString()); // Editors ui->jsonEditorTextBox->setText(s->get("JsonEditor").toString()); } void ExternalToolsPage::applySettings() { auto s = APPLICATION->settings(); s->set("JProfilerPath", ui->jprofilerPathEdit->text()); s->set("JVisualVMPath", ui->jvisualvmPathEdit->text()); s->set("MCEditPath", ui->mceditPathEdit->text()); // Editors QString jsonEditor = ui->jsonEditorTextBox->text(); if (!jsonEditor.isEmpty() && (!QFileInfo(jsonEditor).exists() || !QFileInfo(jsonEditor).isExecutable())) { QString found = QStandardPaths::findExecutable(jsonEditor); if (!found.isEmpty()) { jsonEditor = found; } } s->set("JsonEditor", jsonEditor); } void ExternalToolsPage::on_jprofilerPathBtn_clicked() { QString raw_dir = ui->jprofilerPathEdit->text(); QString error; do { raw_dir = QFileDialog::getExistingDirectory(this, tr("JProfiler Folder"), raw_dir); if (raw_dir.isEmpty()) { break; } QString cooked_dir = FS::NormalizePath(raw_dir); if (!APPLICATION->profilers()["jprofiler"]->check(cooked_dir, &error)) { QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error)); continue; } else { ui->jprofilerPathEdit->setText(cooked_dir); break; } } while (1); } void ExternalToolsPage::on_jprofilerCheckBtn_clicked() { QString error; if (!APPLICATION->profilers()["jprofiler"]->check(ui->jprofilerPathEdit->text(), &error)) { QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error)); } else { QMessageBox::information(this, tr("OK"), tr("JProfiler setup seems to be OK")); } } void ExternalToolsPage::on_jvisualvmPathBtn_clicked() { QString raw_dir = ui->jvisualvmPathEdit->text(); QString error; do { raw_dir = QFileDialog::getOpenFileName(this, tr("VisualVM Executable"), raw_dir); if (raw_dir.isEmpty()) { break; } QString cooked_dir = FS::NormalizePath(raw_dir); if (!APPLICATION->profilers()["jvisualvm"]->check(cooked_dir, &error)) { QMessageBox::critical(this, tr("Error"), tr("Error while checking VisualVM install:\n%1").arg(error)); continue; } else { ui->jvisualvmPathEdit->setText(cooked_dir); break; } } while (1); } void ExternalToolsPage::on_jvisualvmCheckBtn_clicked() { QString error; if (!APPLICATION->profilers()["jvisualvm"]->check(ui->jvisualvmPathEdit->text(), &error)) { QMessageBox::critical(this, tr("Error"), tr("Error while checking VisualVM install:\n%1").arg(error)); } else { QMessageBox::information(this, tr("OK"), tr("VisualVM setup seems to be OK")); } } void ExternalToolsPage::on_mceditPathBtn_clicked() { QString raw_dir = ui->mceditPathEdit->text(); QString error; do { #ifdef Q_OS_MACOS raw_dir = QFileDialog::getOpenFileName(this, tr("MCEdit Application"), raw_dir); #else raw_dir = QFileDialog::getExistingDirectory(this, tr("MCEdit Folder"), raw_dir); #endif if (raw_dir.isEmpty()) { break; } QString cooked_dir = FS::NormalizePath(raw_dir); if (!APPLICATION->mcedit()->check(cooked_dir, error)) { QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error)); continue; } else { ui->mceditPathEdit->setText(cooked_dir); break; } } while (1); } void ExternalToolsPage::on_mceditCheckBtn_clicked() { QString error; if (!APPLICATION->mcedit()->check(ui->mceditPathEdit->text(), error)) { QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error)); } else { QMessageBox::information(this, tr("OK"), tr("MCEdit setup seems to be OK")); } } void ExternalToolsPage::on_jsonEditorBrowseBtn_clicked() { QString raw_file = QFileDialog::getOpenFileName(this, tr("Text Editor"), ui->jsonEditorTextBox->text().isEmpty() #if defined(Q_OS_LINUX) ? QString("/usr/bin") #else ? QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).first() #endif : ui->jsonEditorTextBox->text()); if (raw_file.isEmpty()) { return; } QString cooked_file = FS::NormalizePath(raw_file); // it has to exist and be an executable if (QFileInfo(cooked_file).exists() && QFileInfo(cooked_file).isExecutable()) { ui->jsonEditorTextBox->setText(cooked_file); } else { QMessageBox::warning(this, tr("Invalid"), tr("The file chosen does not seem to be an executable")); } } bool ExternalToolsPage::apply() { applySettings(); return true; } void ExternalToolsPage::retranslate() { ui->retranslateUi(this); } PrismLauncher-10.0.5/launcher/ui/pages/global/ProxyPage.cpp0000644000175100017510000000773515144136756023206 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ProxyPage.h" #include "ui_ProxyPage.h" #include #include #include "Application.h" #include "settings/SettingsObject.h" ProxyPage::ProxyPage(QWidget* parent) : QWidget(parent), ui(new Ui::ProxyPage) { ui->setupUi(this); loadSettings(); updateCheckboxStuff(); connect(ui->proxyGroup, &QButtonGroup::buttonClicked, this, &ProxyPage::proxyGroupChanged); } ProxyPage::~ProxyPage() { delete ui; } bool ProxyPage::apply() { applySettings(); return true; } void ProxyPage::updateCheckboxStuff() { bool enableEditing = ui->proxyHTTPBtn->isChecked() || ui->proxySOCKS5Btn->isChecked(); ui->proxyAddrBox->setEnabled(enableEditing); ui->proxyAuthBox->setEnabled(enableEditing); } void ProxyPage::proxyGroupChanged([[maybe_unused]] QAbstractButton* button) { updateCheckboxStuff(); } void ProxyPage::applySettings() { auto s = APPLICATION->settings(); // Proxy QString proxyType = "None"; if (ui->proxyDefaultBtn->isChecked()) proxyType = "Default"; else if (ui->proxyNoneBtn->isChecked()) proxyType = "None"; else if (ui->proxySOCKS5Btn->isChecked()) proxyType = "SOCKS5"; else if (ui->proxyHTTPBtn->isChecked()) proxyType = "HTTP"; s->set("ProxyType", proxyType); s->set("ProxyAddr", ui->proxyAddrEdit->text()); s->set("ProxyPort", ui->proxyPortEdit->value()); s->set("ProxyUser", ui->proxyUserEdit->text()); s->set("ProxyPass", ui->proxyPassEdit->text()); APPLICATION->updateProxySettings(proxyType, ui->proxyAddrEdit->text(), ui->proxyPortEdit->value(), ui->proxyUserEdit->text(), ui->proxyPassEdit->text()); } void ProxyPage::loadSettings() { auto s = APPLICATION->settings(); // Proxy QString proxyType = s->get("ProxyType").toString(); if (proxyType == "Default") ui->proxyDefaultBtn->setChecked(true); else if (proxyType == "None") ui->proxyNoneBtn->setChecked(true); else if (proxyType == "SOCKS5") ui->proxySOCKS5Btn->setChecked(true); else if (proxyType == "HTTP") ui->proxyHTTPBtn->setChecked(true); ui->proxyAddrEdit->setText(s->get("ProxyAddr").toString()); ui->proxyPortEdit->setValue(s->get("ProxyPort").value()); ui->proxyUserEdit->setText(s->get("ProxyUser").toString()); ui->proxyPassEdit->setText(s->get("ProxyPass").toString()); } void ProxyPage::retranslate() { ui->retranslateUi(this); } PrismLauncher-10.0.5/launcher/ui/pages/global/ProxyPage.ui0000644000175100017510000001503415144136756023030 0ustar runnerrunner ProxyPage 0 0 598 617 0 0 0 0 0 This only applies to the launcher. Minecraft does not accept proxy settings. Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true Type Uses your system's default proxy settings. Use s&ystem settings proxyGroup &None proxyGroup &SOCKS5 proxyGroup &HTTP proxyGroup &Address and Port 0 0 300 0 127.0.0.1 Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter QAbstractSpinBox::PlusMinus 65535 8080 Qt::Horizontal 40 20 Authentication &Username: proxyUserEdit &Password: proxyPassEdit QLineEdit::Password Note: Proxy username and password are stored in plain text inside the launcher's configuration file! true Qt::Vertical 20 40 proxyDefaultBtn proxyNoneBtn proxySOCKS5Btn proxyHTTPBtn proxyAddrEdit proxyPortEdit proxyUserEdit proxyPassEdit PrismLauncher-10.0.5/launcher/ui/pages/global/LauncherPage.h0000644000175100017510000000511615144136756023262 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" class QTextCharFormat; class SettingsObject; namespace Ui { class LauncherPage; } class LauncherPage : public QWidget, public BasePage { Q_OBJECT public: explicit LauncherPage(QWidget* parent = 0); ~LauncherPage(); QString displayName() const override { return tr("General"); } QIcon icon() const override { return QIcon::fromTheme("settings"); } QString id() const override { return "launcher-settings"; } QString helpPage() const override { return "Launcher-settings"; } bool apply() override; void retranslate() override; private: void applySettings(); void loadSettings(); private slots: void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); void on_downloadsDirBrowseBtn_clicked(); void on_javaDirBrowseBtn_clicked(); void on_skinsDirBrowseBtn_clicked(); void on_metadataEnableBtn_clicked(); private: Ui::LauncherPage* ui; }; PrismLauncher-10.0.5/launcher/ui/pages/global/LanguagePage.cpp0000644000175100017510000000452715144136756023604 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LanguagePage.h" #include #include "Application.h" #include "ui/widgets/LanguageSelectionWidget.h" LanguagePage::LanguagePage(QWidget* parent) : QWidget(parent) { setObjectName(QStringLiteral("languagePage")); auto layout = new QVBoxLayout(this); mainWidget = new LanguageSelectionWidget(this); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(mainWidget); retranslate(); } LanguagePage::~LanguagePage() {} bool LanguagePage::apply() { applySettings(); return true; } void LanguagePage::applySettings() { auto settings = APPLICATION->settings(); QString key = mainWidget->getSelectedLanguageKey(); settings->set("Language", key); } void LanguagePage::loadSettings() { // NIL } void LanguagePage::retranslate() { mainWidget->retranslate(); } PrismLauncher-10.0.5/launcher/ui/pages/global/LauncherPage.cpp0000644000175100017510000002726115144136756023622 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (c) 2022 dada513 * Copyright (C) 2022 Tayou * Copyright (C) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LauncherPage.h" #include "ui_LauncherPage.h" #include #include #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "DesktopServices.h" #include "settings/SettingsObject.h" #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" #include "updater/ExternalUpdater.h" #include #include // FIXME: possibly move elsewhere enum InstSortMode { // Sort alphabetically by name. Sort_Name, // Sort by which instance was launched most recently. Sort_LastLaunch }; LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) { ui->setupUi(this); ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); loadSettings(); ui->updateSettingsBox->setHidden(!APPLICATION->updater()); } LauncherPage::~LauncherPage() { delete ui; } bool LauncherPage::apply() { applySettings(); return true; } void LauncherPage::on_instDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Instance Folder"), ui->instDirTextBox->text()); // do not allow current dir - it's dirty. Do not allow dirs that don't exist if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { QString cooked_dir = FS::NormalizePath(raw_dir); if (FS::checkProblemticPathJava(QDir(cooked_dir))) { QMessageBox warning; warning.setText( tr("You're trying to specify an instance folder which\'s path " "contains at least one \'!\'. " "Java is known to cause problems if that is the case, your " "instances (probably) won't start!")); warning.setInformativeText( tr("Do you really want to use this path? " "Selecting \"No\" will close this and not alter your instance path.")); warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); int result = warning.exec(); if (result == QMessageBox::Ok) { ui->instDirTextBox->setText(cooked_dir); } } else if (DesktopServices::isFlatpak() && raw_dir.startsWith("/run/user")) { QMessageBox warning; warning.setText(tr("You're trying to specify an instance folder " "which was granted temporarily via Flatpak.\n" "This is known to cause problems. " "After a restart the launcher might break, " "because it will no longer have access to that directory.\n\n" "Granting %1 access to it via Flatseal is recommended.") .arg(BuildConfig.LAUNCHER_DISPLAYNAME)); warning.setInformativeText(tr("Do you want to proceed anyway?")); warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); int result = warning.exec(); if (result == QMessageBox::Ok) { ui->instDirTextBox->setText(cooked_dir); } } else { ui->instDirTextBox->setText(cooked_dir); } } } void LauncherPage::on_iconsDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Icons Folder"), ui->iconsDirTextBox->text()); // do not allow current dir - it's dirty. Do not allow dirs that don't exist if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { QString cooked_dir = FS::NormalizePath(raw_dir); ui->iconsDirTextBox->setText(cooked_dir); } } void LauncherPage::on_modsDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text()); // do not allow current dir - it's dirty. Do not allow dirs that don't exist if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { QString cooked_dir = FS::NormalizePath(raw_dir); ui->modsDirTextBox->setText(cooked_dir); } } void LauncherPage::on_downloadsDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Downloads Folder"), ui->downloadsDirTextBox->text()); if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { QString cooked_dir = FS::NormalizePath(raw_dir); ui->downloadsDirTextBox->setText(cooked_dir); } } void LauncherPage::on_javaDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Java Folder"), ui->javaDirTextBox->text()); if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { QString cooked_dir = FS::NormalizePath(raw_dir); ui->javaDirTextBox->setText(cooked_dir); } } void LauncherPage::on_skinsDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Skins Folder"), ui->skinsDirTextBox->text()); // do not allow current dir - it's dirty. Do not allow dirs that don't exist if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { QString cooked_dir = FS::NormalizePath(raw_dir); ui->skinsDirTextBox->setText(cooked_dir); } } void LauncherPage::on_metadataEnableBtn_clicked() { ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); } void LauncherPage::applySettings() { auto s = APPLICATION->settings(); // Updates if (APPLICATION->updater()) { APPLICATION->updater()->setAutomaticallyChecksForUpdates(ui->autoUpdateCheckBox->isChecked()); APPLICATION->updater()->setUpdateCheckInterval(ui->updateIntervalSpinBox->value() * 3600); } s->set("MenuBarInsteadOfToolBar", ui->preferMenuBarCheckBox->isChecked()); s->set("NumberOfConcurrentTasks", ui->numberOfConcurrentTasksSpinBox->value()); s->set("NumberOfConcurrentDownloads", ui->numberOfConcurrentDownloadsSpinBox->value()); s->set("NumberOfManualRetries", ui->numberOfManualRetriesSpinBox->value()); s->set("RequestTimeout", ui->timeoutSecondsSpinBox->value()); // Console settings s->set("ConsoleMaxLines", ui->lineLimitSpinBox->value()); s->set("ConsoleOverflowStop", ui->checkStopLogging->checkState() != Qt::Unchecked); // Folders // TODO: Offer to move instances to new instance folder. s->set("InstanceDir", ui->instDirTextBox->text()); s->set("CentralModsDir", ui->modsDirTextBox->text()); s->set("IconsDir", ui->iconsDirTextBox->text()); s->set("DownloadsDir", ui->downloadsDirTextBox->text()); s->set("SkinsDir", ui->skinsDirTextBox->text()); s->set("JavaDir", ui->javaDirTextBox->text()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked()); // Instance auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); switch (sortMode) { case Sort_LastLaunch: s->set("InstSortMode", "LastLaunch"); break; case Sort_Name: default: s->set("InstSortMode", "Name"); break; } if (ui->askToRenameDirBtn->isChecked()) { s->set("InstRenamingMode", "AskEverytime"); } else if (ui->alwaysRenameDirBtn->isChecked()) { s->set("InstRenamingMode", "PhysicalDir"); } else if (ui->neverRenameDirBtn->isChecked()) { s->set("InstRenamingMode", "MetadataOnly"); } // Mods s->set("ModMetadataDisabled", !ui->metadataEnableBtn->isChecked()); s->set("ModDependenciesDisabled", !ui->dependenciesEnableBtn->isChecked()); s->set("SkipModpackUpdatePrompt", !ui->modpackUpdatePromptBtn->isChecked()); } void LauncherPage::loadSettings() { auto s = APPLICATION->settings(); // Updates if (APPLICATION->updater()) { ui->autoUpdateCheckBox->setChecked(APPLICATION->updater()->getAutomaticallyChecksForUpdates()); ui->updateIntervalSpinBox->setValue(APPLICATION->updater()->getUpdateCheckInterval() / 3600); } ui->preferMenuBarCheckBox->setChecked(s->get("MenuBarInsteadOfToolBar").toBool()); ui->numberOfConcurrentTasksSpinBox->setValue(s->get("NumberOfConcurrentTasks").toInt()); ui->numberOfConcurrentDownloadsSpinBox->setValue(s->get("NumberOfConcurrentDownloads").toInt()); ui->numberOfManualRetriesSpinBox->setValue(s->get("NumberOfManualRetries").toInt()); ui->timeoutSecondsSpinBox->setValue(s->get("RequestTimeout").toInt()); // Console settings ui->lineLimitSpinBox->setValue(s->get("ConsoleMaxLines").toInt()); ui->checkStopLogging->setChecked(s->get("ConsoleOverflowStop").toBool()); // Folders ui->instDirTextBox->setText(s->get("InstanceDir").toString()); ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString()); ui->skinsDirTextBox->setText(s->get("SkinsDir").toString()); ui->javaDirTextBox->setText(s->get("JavaDir").toString()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool()); // Instance QString sortMode = s->get("InstSortMode").toString(); if (sortMode == "LastLaunch") { ui->sortLastLaunchedBtn->setChecked(true); } else { ui->sortByNameBtn->setChecked(true); } QString renamingMode = s->get("InstRenamingMode").toString(); ui->askToRenameDirBtn->setChecked(renamingMode == "AskEverytime"); ui->alwaysRenameDirBtn->setChecked(renamingMode == "PhysicalDir"); ui->neverRenameDirBtn->setChecked(renamingMode == "MetadataOnly"); // Mods ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool()); ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); ui->dependenciesEnableBtn->setChecked(!s->get("ModDependenciesDisabled").toBool()); ui->modpackUpdatePromptBtn->setChecked(!s->get("SkipModpackUpdatePrompt").toBool()); } void LauncherPage::retranslate() { ui->retranslateUi(this); } PrismLauncher-10.0.5/launcher/ui/pages/global/AccountListPage.h0000644000175100017510000000571615144136756023757 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "ui/pages/BasePage.h" #include "minecraft/auth/AccountList.h" namespace Ui { class AccountListPage; } class AuthenticateTask; class AccountListPage : public QMainWindow, public BasePage { Q_OBJECT public: explicit AccountListPage(QWidget* parent = 0); ~AccountListPage(); QString displayName() const override { return tr("Accounts"); } QIcon icon() const override { auto icon = QIcon::fromTheme("accounts"); if (icon.isNull()) { icon = QIcon::fromTheme("noaccount"); } return icon; } QString id() const override { return "accounts"; } QString helpPage() const override { return "getting-started/adding-an-account"; } void retranslate() override; public slots: void on_actionAddMicrosoft_triggered(); void on_actionAddOffline_triggered(); void on_actionRemove_triggered(); void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); void on_actionNoDefault_triggered(); void on_actionManageSkins_triggered(); void listChanged(); //! Updates the states of the dialog's buttons. void updateButtonStates(); protected slots: void ShowContextMenu(const QPoint& pos); private: void changeEvent(QEvent* event) override; QMenu* createPopupMenu() override; shared_qobject_ptr m_accounts; Ui::AccountListPage* ui; }; PrismLauncher-10.0.5/launcher/ui/pages/global/AppearancePage.h0000644000175100017510000000453215144136756023561 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "java/JavaChecker.h" #include "translations/TranslationsModel.h" #include "ui/pages/BasePage.h" #include "ui/widgets/AppearanceWidget.h" class QTextCharFormat; class SettingsObject; class AppearancePage : public AppearanceWidget, public BasePage { Q_OBJECT public: explicit AppearancePage(QWidget* parent = nullptr) : AppearanceWidget(false, parent) { layout()->setContentsMargins(0, 0, 6, 0); } QString displayName() const override { return tr("Appearance"); } QIcon icon() const override { return QIcon::fromTheme("appearance"); } QString id() const override { return "appearance-settings"; } QString helpPage() const override { return "Launcher-settings"; } bool apply() override { applySettings(); return true; } void retranslate() override { retranslateUi(); } }; PrismLauncher-10.0.5/launcher/ui/pages/global/AccountListPage.ui0000644000175100017510000000623315144136756024140 0ustar runnerrunner AccountListPage 0 0 800 600 0 0 0 0 true false false true false RightToolBarArea false &Set Default true &No Default &Manage Skins Manage Skins &Add Microsoft Add &Offline &Refresh Refresh the account tokens Remo&ve VersionListView QTreeView
    ui/widgets/VersionListView.h
    WideBar QToolBar
    ui/widgets/WideBar.h
    PrismLauncher-10.0.5/launcher/ui/pages/global/ExternalToolsPage.h0000644000175100017510000000512515144136756024324 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ui/pages/BasePage.h" namespace Ui { class ExternalToolsPage; } class ExternalToolsPage : public QWidget, public BasePage { Q_OBJECT public: explicit ExternalToolsPage(QWidget* parent = 0); ~ExternalToolsPage(); QString displayName() const override { return tr("Tools"); } QIcon icon() const override { auto icon = QIcon::fromTheme("externaltools"); if (icon.isNull()) { icon = QIcon::fromTheme("loadermods"); } return icon; } QString id() const override { return "external-tools"; } QString helpPage() const override { return "Tools"; } virtual bool apply() override; void retranslate() override; private: void loadSettings(); void applySettings(); private: Ui::ExternalToolsPage* ui; private slots: void on_jprofilerPathBtn_clicked(); void on_jprofilerCheckBtn_clicked(); void on_jvisualvmPathBtn_clicked(); void on_jvisualvmCheckBtn_clicked(); void on_mceditPathBtn_clicked(); void on_mceditCheckBtn_clicked(); void on_jsonEditorBrowseBtn_clicked(); }; PrismLauncher-10.0.5/launcher/ui/pages/global/APIPage.cpp0000644000175100017510000001712515144136756022470 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * Copyright (c) 2022 Lenny McLennington * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "APIPage.h" #include "ui_APIPage.h" #include #include #include #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "net/PasteUpload.h" #include "settings/SettingsObject.h" #include "tools/BaseProfiler.h" APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) { // This is here so you can reorder the entries in the combobox without messing stuff up int comboBoxEntries[] = { PasteUpload::PasteType::Mclogs, PasteUpload::PasteType::NullPointer, PasteUpload::PasteType::PasteGG, PasteUpload::PasteType::Hastebin }; static const QRegularExpression s_validUrlRegExp("https?://.+"); static const QRegularExpression s_validMSAClientID( QRegularExpression::anchoredPattern("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")); ui->setupUi(this); for (auto pasteType : comboBoxEntries) { ui->pasteTypeComboBox->addItem(PasteUpload::PasteTypes.at(pasteType).name, pasteType); } void (QComboBox::*currentIndexChangedSignal)(int)(&QComboBox::currentIndexChanged); connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder); // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); // NOTE: this allows http://, but we replace that with https later anyway ui->metaURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->metaURL)); ui->resourceURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->resourceURL)); ui->baseURLEntry->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->baseURLEntry)); ui->msaClientID->setValidator(new QRegularExpressionValidator(s_validMSAClientID, ui->msaClientID)); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); loadSettings(); resetBaseURLNote(); connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLNote); connect(ui->baseURLEntry, &QLineEdit::textEdited, this, &APIPage::resetBaseURLNote); } APIPage::~APIPage() { delete ui; } void APIPage::resetBaseURLNote() { ui->baseURLNote->hide(); baseURLPasteType = ui->pasteTypeComboBox->currentIndex(); } void APIPage::updateBaseURLNote(int index) { if (baseURLPasteType == index) { ui->baseURLNote->hide(); } else if (!ui->baseURLEntry->text().isEmpty()) { ui->baseURLNote->show(); } } void APIPage::updateBaseURLPlaceholder(int index) { int pasteType = ui->pasteTypeComboBox->itemData(index).toInt(); QString pasteDefaultURL = PasteUpload::PasteTypes.at(pasteType).defaultBase; ui->baseURLEntry->setPlaceholderText(pasteDefaultURL); } void APIPage::loadSettings() { auto s = APPLICATION->settings(); int pasteType = s->get("PastebinType").toInt(); QString pastebinURL = s->get("PastebinCustomAPIBase").toString(); ui->baseURLEntry->setText(pastebinURL); int pasteTypeIndex = ui->pasteTypeComboBox->findData(pasteType); if (pasteTypeIndex == -1) { pasteTypeIndex = ui->pasteTypeComboBox->findData(PasteUpload::PasteType::Mclogs); ui->baseURLEntry->clear(); } ui->pasteTypeComboBox->setCurrentIndex(pasteTypeIndex); QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); QString metaURL = s->get("MetaURLOverride").toString(); ui->metaURL->setText(metaURL); QString resourceURL = s->get("ResourceURL").toString(); ui->resourceURL->setText(resourceURL); QString flameKey = s->get("FlameKeyOverride").toString(); ui->flameKey->setText(flameKey); QString modrinthToken = s->get("ModrinthToken").toString(); ui->modrinthToken->setText(modrinthToken); QString customUserAgent = s->get("UserAgentOverride").toString(); ui->userAgentLineEdit->setText(customUserAgent); ui->technicClientID->setText(s->get("TechnicClientID").toString()); } void APIPage::applySettings() { auto s = APPLICATION->settings(); s->set("PastebinType", ui->pasteTypeComboBox->currentData().toInt()); s->set("PastebinCustomAPIBase", ui->baseURLEntry->text()); QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); QUrl metaURL(ui->metaURL->text()); QUrl resourceURL(ui->resourceURL->text()); // Add required trailing slash if (!metaURL.isEmpty() && !metaURL.path().endsWith('/')) { QString path = metaURL.path(); path.append('/'); metaURL.setPath(path); } if (!resourceURL.isEmpty() && !resourceURL.path().endsWith('/')) { QString path = resourceURL.path(); path.append('/'); resourceURL.setPath(path); } auto isLocalhost = [](const QUrl& url) { return url.host() == "localhost" || url.host() == "127.0.0.1" || url.host() == "::1"; }; auto isUnsafe = [isLocalhost](const QUrl& url) { return !url.isEmpty() && url.scheme() == "http" && !isLocalhost(url); }; // Don't allow HTTP, since meta is basically RCE with all the jar files. if (isUnsafe(metaURL)) { metaURL.setScheme("https"); } // Also don't allow HTTP if (isUnsafe(resourceURL)) { resourceURL.setScheme("https"); } s->set("MetaURLOverride", metaURL.toString()); s->set("ResourceURL", resourceURL.toString()); QString flameKey = ui->flameKey->text(); s->set("FlameKeyOverride", flameKey); QString modrinthToken = ui->modrinthToken->text(); s->set("ModrinthToken", modrinthToken); s->set("UserAgentOverride", ui->userAgentLineEdit->text()); s->set("TechnicClientID", ui->technicClientID->text()); } bool APIPage::apply() { applySettings(); return true; } void APIPage::retranslate() { ui->retranslateUi(this); } PrismLauncher-10.0.5/launcher/ui/pages/global/LauncherPage.ui0000644000175100017510000005621415144136756023455 0ustar runnerrunner LauncherPage 0 0 767 796 0 0 0 0 0 0 Qt::ScrollBarPolicy::ScrollBarAsNeeded true 0 0 746 1194 true User Interface Instance Sorting By &name sortingModeGroup &By last launched sortingModeGroup Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 0 6 Instance Renaming Ask what to do renamingBehaviorGroup Always rename the folder renamingBehaviorGroup Never rename the folder renamingBehaviorGroup Qt::Orientation::Vertical QSizePolicy::Policy::Fixed 0 6 The menubar is more friendly for keyboard-driven interaction. &Replace toolbar with menubar Updater How Often? 0 0 Set to 0 to only check on launch On Launch hours Every 0 168 Qt::Orientation::Horizontal 0 0 Check for updates automatically Folders Browse &Java: javaDirTextBox Browse &Skins: skinsDirTextBox &Mods: modsDirTextBox Browse &Downloads: downloadsDirTextBox I&nstances: instDirTextBox Browse Browse Browse &Icons: iconsDirTextBox Mods and Modpacks When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). Check &subfolders for blocked mods When enabled, it will move blocked resources instead of copying them. Move blocked mods instead of copying them Store version information provided by mod providers (like Modrinth or CurseForge) for mods. Keep track of mod metadata <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> true Automatically detect, install, and update mod dependencies. Install dependencies automatically When creating a new modpack instance, suggest updating an existing instance instead. Suggest to update an existing instance during modpack installation Console Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter 0 0 Log History &Limit: lineLimitSpinBox 0 0 lines 10000 1000000 10000 100000 &Stop logging when log overflows Tasks 0 0 60 0 0 0 0 60 0 1 0 0 60 0 s Retry Limit: Concurrent Download Limit: Seconds to wait until the requests are terminated HTTP Timeout: 0 0 60 0 1 Concurrent Task Limit: Qt::Orientation::Horizontal 0 0 Qt::Orientation::Vertical 0 0 scrollArea preferMenuBarCheckBox autoUpdateCheckBox updateIntervalSpinBox instDirTextBox instDirBrowseBtn modsDirTextBox modsDirBrowseBtn iconsDirTextBox iconsDirBrowseBtn javaDirTextBox javaDirBrowseBtn skinsDirTextBox skinsDirBrowseBtn downloadsDirTextBox downloadsDirBrowseBtn downloadsDirWatchRecursiveCheckBox downloadsDirMoveCheckBox metadataEnableBtn dependenciesEnableBtn modpackUpdatePromptBtn lineLimitSpinBox checkStopLogging numberOfConcurrentTasksSpinBox numberOfConcurrentDownloadsSpinBox numberOfManualRetriesSpinBox timeoutSecondsSpinBox PrismLauncher-10.0.5/launcher/ui/pages/global/JavaPage.h0000644000175100017510000000451215144136756022401 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "JavaCommon.h" #include "ui/pages/BasePage.h" #include "ui/widgets/JavaSettingsWidget.h" class SettingsObject; namespace Ui { class JavaPage; } class JavaPage : public QWidget, public BasePage { Q_OBJECT public: explicit JavaPage(QWidget* parent = 0); ~JavaPage(); QString displayName() const override { return tr("Java"); } QIcon icon() const override { return QIcon::fromTheme("java"); } QString id() const override { return "java-settings"; } QString helpPage() const override { return "Java-settings"; } void retranslate() override; bool apply() override; private slots: void on_downloadJavaButton_clicked(); void on_removeJavaButton_clicked(); void on_refreshJavaButton_clicked(); private: Ui::JavaPage* ui; }; PrismLauncher-10.0.5/launcher/ui/pages/global/MinecraftPage.h0000644000175100017510000000435015144136756023430 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" #include "ui/widgets/MinecraftSettingsWidget.h" class SettingsObject; class MinecraftPage : public MinecraftSettingsWidget, public BasePage { Q_OBJECT public: explicit MinecraftPage(QWidget* parent = nullptr) : MinecraftSettingsWidget(nullptr, parent) {} ~MinecraftPage() override {} QString displayName() const override { return tr("Minecraft"); } QIcon icon() const override { return QIcon::fromTheme("minecraft"); } QString id() const override { return "minecraft-settings"; } QString helpPage() const override { return "Minecraft-settings"; } bool apply() override { saveSettings(); return true; } }; PrismLauncher-10.0.5/launcher/ui/pages/global/AccountListPage.cpp0000644000175100017510000002216315144136756024305 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AccountListPage.h" #include "ui/dialogs/skins/SkinManageDialog.h" #include "ui_AccountListPage.h" #include #include #include #include #include "ui/dialogs/ChooseOfflineNameDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/MSALoginDialog.h" #include "Application.h" AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new Ui::AccountListPage) { ui->setupUi(this); ui->listView->setEmptyString( tr("Welcome!\n" "If you're new here, you can select the \"Add Microsoft\" button to link your Microsoft account.")); ui->listView->setEmptyMode(VersionListView::String); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); m_accounts = APPLICATION->accounts(); ui->listView->setModel(m_accounts.get()); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::ProfileNameColumn, QHeaderView::Stretch); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::NameColumn, QHeaderView::Stretch); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::TypeColumn, QHeaderView::ResizeToContents); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::StatusColumn, QHeaderView::ResizeToContents); ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); // Expand the account column QItemSelectionModel* selectionModel = ui->listView->selectionModel(); connect(selectionModel, &QItemSelectionModel::selectionChanged, [this]([[maybe_unused]] const QItemSelection& sel, [[maybe_unused]] const QItemSelection& dsel) { updateButtonStates(); }); connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu); connect(ui->listView, &VersionListView::activated, this, [this](const QModelIndex& index) { m_accounts->setDefaultAccount(m_accounts->at(index.row())); }); connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); connect(m_accounts.get(), &AccountList::listActivityChanged, this, &AccountListPage::listChanged); connect(m_accounts.get(), &AccountList::defaultAccountChanged, this, &AccountListPage::listChanged); updateButtonStates(); // Xbox authentication won't work without a client identifier, so disable the button if it is missing if (~APPLICATION->capabilities() & Application::SupportsMSA) { ui->actionAddMicrosoft->setVisible(false); ui->actionAddMicrosoft->setToolTip(tr("No Microsoft Authentication client ID was set.")); } } AccountListPage::~AccountListPage() { delete ui; } void AccountListPage::retranslate() { ui->retranslateUi(this); } void AccountListPage::ShowContextMenu(const QPoint& pos) { auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); menu->exec(ui->listView->mapToGlobal(pos)); delete menu; } void AccountListPage::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); } QMainWindow::changeEvent(event); } QMenu* AccountListPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); filteredMenu->removeAction(ui->toolBar->toggleViewAction()); return filteredMenu; } void AccountListPage::listChanged() { updateButtonStates(); } void AccountListPage::on_actionAddMicrosoft_triggered() { auto account = MSALoginDialog::newAccount(this); if (account) { m_accounts->addAccount(account); if (m_accounts->count() == 1) { m_accounts->setDefaultAccount(account); } } } void AccountListPage::on_actionAddOffline_triggered() { if (!m_accounts->anyAccountIsValid()) { QMessageBox::warning(this, tr("Error"), tr("You must add a Microsoft account that owns Minecraft before you can add an offline account." "

    " "If you have lost your account you can contact Microsoft for support.")); return; } ChooseOfflineNameDialog dialog(tr("Please enter your desired username to add your offline account."), this); if (dialog.exec() != QDialog::Accepted) { return; } if (const MinecraftAccountPtr account = MinecraftAccount::createOffline(dialog.getUsername())) { account->login()->start(); // The task will complete here. m_accounts->addAccount(account); if (m_accounts->count() == 1) { m_accounts->setDefaultAccount(account); } } } void AccountListPage::on_actionRemove_triggered() { auto response = CustomMessageBox::selectable(this, tr("Remove account?"), tr("Do you really want to delete this account?"), QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) { return; } QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); m_accounts->removeAccount(selected); } } void AccountListPage::on_actionRefresh_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); m_accounts->requestRefresh(account->internalId()); } } void AccountListPage::on_actionSetDefault_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); m_accounts->setDefaultAccount(account); } } void AccountListPage::on_actionNoDefault_triggered() { m_accounts->setDefaultAccount(nullptr); } void AccountListPage::updateButtonStates() { // If there is no selection, disable buttons that require something selected. QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); bool hasSelection = !selection.empty(); bool accountIsReady = false; bool accountIsOnline = false; if (hasSelection) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); accountIsReady = !account->isActive(); accountIsOnline = account->accountType() != AccountType::Offline; } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); ui->actionManageSkins->setEnabled(accountIsReady && accountIsOnline); ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if (m_accounts->defaultAccount().get() == nullptr) { ui->actionNoDefault->setEnabled(false); ui->actionNoDefault->setChecked(true); } else { ui->actionNoDefault->setEnabled(true); ui->actionNoDefault->setChecked(false); } ui->listView->resizeColumnToContents(3); } void AccountListPage::on_actionManageSkins_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); if (selection.size() > 0) { QModelIndex selected = selection.first(); MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); SkinManageDialog dialog(this, account); dialog.exec(); } } PrismLauncher-10.0.5/launcher/ui/pages/global/LanguagePage.h0000644000175100017510000000436115144136756023245 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "ui/pages/BasePage.h" class LanguageSelectionWidget; class LanguagePage : public QWidget, public BasePage { Q_OBJECT public: explicit LanguagePage(QWidget* parent = 0); virtual ~LanguagePage(); QString displayName() const override { return tr("Language"); } QIcon icon() const override { return QIcon::fromTheme("language"); } QString id() const override { return "language-settings"; } QString helpPage() const override { return "Language-settings"; } bool apply() override; void retranslate() override; private: void applySettings(); void loadSettings(); private: LanguageSelectionWidget* mainWidget; }; PrismLauncher-10.0.5/launcher/ui/pages/global/ProxyPage.h0000644000175100017510000000451015144136756022637 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "ui/pages/BasePage.h" namespace Ui { class ProxyPage; } class ProxyPage : public QWidget, public BasePage { Q_OBJECT public: explicit ProxyPage(QWidget* parent = 0); ~ProxyPage(); QString displayName() const override { return tr("Proxy"); } QIcon icon() const override { return QIcon::fromTheme("proxy"); } QString id() const override { return "proxy-settings"; } QString helpPage() const override { return "Proxy-settings"; } bool apply() override; void retranslate() override; private slots: void proxyGroupChanged(QAbstractButton* button); private: void updateCheckboxStuff(); void applySettings(); void loadSettings(); private: Ui::ProxyPage* ui; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/0000755000175100017510000000000015144136757021635 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/modplatform/ShaderPackPage.cpp0000644000175100017510000000401215144136757025140 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ShaderPackPage.h" #include "modplatform/ModIndex.h" #include "ui_ResourcePage.h" #include "ShaderPackModel.h" #include "Application.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include namespace ResourceDownload { ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} /******** Callbacks to events in the UI (set up in the derived classes) ********/ void ShaderPackResourcePage::triggerSearch() { m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); m_ui->packView->clearSelection(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ShaderPackResourcePage::urlHandlers() const { QMap map; map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/shaders\\/([^\\/]+)\\/?"), "modrinth"); map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/customization\\/([^\\/]+)\\/?"), "curseforge"); map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); return map; } void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, const std::shared_ptr base_model) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_model->addPack(pack, version, base_model, is_indexed); } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/import_ftb/0000755000175100017510000000000015144136757024002 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui0000644000175100017510000000522515144136757026750 0ustar runnerrunner FTBImportAPP::ImportFTBPage 0 0 1461 1011 true Note: Many recent FTB modpacks are also available from CurseForge! Also, if your FTB instances are not in the default location, select it using the button next to search. Qt::AlignmentFlag::AlignCenter true Search and filter... true Select FTBApp instances directory true 16777215 16777215 265 0 Qt::Orientation::Horizontal 40 20 PrismLauncher-10.0.5/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp0000644000175100017510000001465215144136757026412 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ListModel.h" #include #include #include #include #include #include "Application.h" #include "Exception.h" #include "FileSystem.h" #include "Json.h" #include "StringUtils.h" #include "modplatform/import_ftb/PackHelpers.h" #include "ui/widgets/ProjectItem.h" namespace FTBImportAPP { QString getFTBRoot() { QString partialPath = QDir::homePath(); #if defined(Q_OS_MACOS) partialPath = FS::PathCombine(partialPath, "Library/Application Support"); #endif return FS::PathCombine(partialPath, ".ftba"); } QString getDynamicPath() { auto settingsPath = FS::PathCombine(getFTBRoot(), "storage", "settings.json"); if (!QFileInfo::exists(settingsPath)) settingsPath = FS::PathCombine(getFTBRoot(), "bin", "settings.json"); if (!QFileInfo::exists(settingsPath)) { qWarning() << "The ftb app setings doesn't exist."; return {}; } try { auto doc = Json::requireDocument(FS::read(settingsPath)); return Json::requireString(Json::requireObject(doc), "instanceLocation"); } catch (const Exception& e) { qCritical() << "Could not read ftb settings file:" << e.cause(); } return {}; } ListModel::ListModel(QObject* parent) : QAbstractListModel(parent), m_instances_path(getDynamicPath()) {} void ListModel::update() { beginResetModel(); m_modpacks.clear(); auto wasPathAdded = [this](QString path) { for (auto pack : m_modpacks) { if (pack.path == path) return true; } return false; }; auto scanPath = [this, wasPathAdded](QString path) { if (path.isEmpty()) return; if (auto instancesInfo = QFileInfo(path); !instancesInfo.exists() || !instancesInfo.isDir()) return; QDirIterator directoryIterator(path, QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (directoryIterator.hasNext()) { auto currentPath = directoryIterator.next(); if (!wasPathAdded(currentPath)) { auto modpack = parseDirectory(currentPath); if (!modpack.path.isEmpty()) m_modpacks.append(modpack); } } }; scanPath(APPLICATION->settings()->get("FTBAppInstancesPath").toString()); scanPath(m_instances_path); endResetModel(); } QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QVariant(); } auto pack = m_modpacks.at(pos); switch (role) { case Qt::ToolTipRole: return tr("Minecraft %1").arg(pack.mcVersion); case Qt::DecorationRole: return pack.icon; case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } case Qt::DisplayRole: return pack.name; case Qt::SizeHintRole: return QSize(0, 58); // Custom data case UserDataTypes::TITLE: return pack.name; case UserDataTypes::DESCRIPTION: return tr("Minecraft %1").arg(pack.mcVersion); case UserDataTypes::INSTALLED: return false; default: break; } return {}; } FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) { m_currentSorting = Sorting::ByGameVersion; m_sortings.insert(tr("Sort by Name"), Sorting::ByName); m_sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); } bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); Q_ASSERT(leftRaw.canConvert()); auto leftPack = leftRaw.value(); QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); Q_ASSERT(rightRaw.canConvert()); auto rightPack = rightRaw.value(); if (m_currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); Version rv(rightPack.mcVersion); return lv < rv; } else if (m_currentSorting == Sorting::ByName) { return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } // UHM, some inavlid value set?! qWarning() << "Invalid sorting set!"; return true; } bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const { if (m_searchTerm.isEmpty()) { return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QVariant raw = sourceModel()->data(index, Qt::UserRole); Q_ASSERT(raw.canConvert()); auto pack = raw.value(); return pack.name.contains(m_searchTerm, Qt::CaseInsensitive); } void FilterModel::setSearchTerm(const QString term) { m_searchTerm = term.trimmed(); invalidate(); } const QMap FilterModel::getAvailableSortings() { return m_sortings; } QString FilterModel::translateCurrentSorting() { return m_sortings.key(m_currentSorting); } void FilterModel::setSorting(Sorting s) { m_currentSorting = s; invalidate(); } FilterModel::Sorting FilterModel::getCurrentSorting() { return m_currentSorting; } void ListModel::setPath(QString path) { APPLICATION->settings()->set("FTBAppInstancesPath", path); update(); } QString ListModel::getUserPath() { auto path = APPLICATION->settings()->get("FTBAppInstancesPath").toString(); if (path.isEmpty()) path = m_instances_path; return path; } } // namespace FTBImportAPP PrismLauncher-10.0.5/launcher/ui/pages/modplatform/import_ftb/ListModel.h0000644000175100017510000000414015144136757026046 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "modplatform/import_ftb/PackHelpers.h" namespace FTBImportAPP { class FilterModel : public QSortFilterProxyModel { Q_OBJECT public: FilterModel(QObject* parent = Q_NULLPTR); enum Sorting { ByName, ByGameVersion }; const QMap getAvailableSortings(); QString translateCurrentSorting(); void setSorting(Sorting sorting); Sorting getCurrentSorting(); void setSearchTerm(QString term); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; private: QMap m_sortings; Sorting m_currentSorting; QString m_searchTerm; }; class ListModel : public QAbstractListModel { Q_OBJECT public: ListModel(QObject* parent); virtual ~ListModel() = default; int rowCount(const QModelIndex& parent) const { return m_modpacks.size(); } int columnCount(const QModelIndex& parent) const { return 1; } QVariant data(const QModelIndex& index, int role) const; void update(); QString getUserPath(); void setPath(QString path); private: ModpackList m_modpacks; const QString m_instances_path; }; } // namespace FTBImportAPP PrismLauncher-10.0.5/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp0000644000175100017510000001237315144136757027117 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ImportFTBPage.h" #include "ui/widgets/ProjectItem.h" #include "ui_ImportFTBPage.h" #include #include #include #include #include "FileSystem.h" #include "ListModel.h" #include "modplatform/import_ftb/PackInstallTask.h" #include "ui/dialogs/NewInstanceDialog.h" namespace FTBImportAPP { ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::ImportFTBPage) { ui->setupUi(this); { currentModel = new FilterModel(this); listModel = new ListModel(this); currentModel->setSourceModel(listModel); ui->modpackList->setModel(currentModel); ui->modpackList->setSortingEnabled(true); ui->modpackList->header()->hide(); ui->modpackList->setIndentation(0); ui->modpackList->setIconSize(QSize(42, 42)); for (int i = 0; i < currentModel->getAvailableSortings().size(); i++) { ui->sortByBox->addItem(currentModel->getAvailableSortings().keys().at(i)); } ui->sortByBox->setCurrentText(currentModel->translateCurrentSorting()); } connect(ui->modpackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &ImportFTBPage::onPublicPackSelectionChanged); connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &ImportFTBPage::onSortingSelectionChanged); connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); connect(ui->browseButton, &QPushButton::clicked, this, [this] { QString dir = QFileDialog::getExistingDirectory(this, tr("Select FTBApp instances directory"), listModel->getUserPath(), QFileDialog::ShowDirsOnly); if (!dir.isEmpty()) listModel->setPath(dir); }); ui->modpackList->setItemDelegate(new ProjectItemDelegate(this)); ui->modpackList->selectionModel()->reset(); } ImportFTBPage::~ImportFTBPage() { delete ui; } void ImportFTBPage::openedImpl() { if (!initialized) { listModel->update(); initialized = true; } suggestCurrent(); } void ImportFTBPage::retranslate() { ui->retranslateUi(this); } QString saveIconToTempFile(const QIcon& icon) { if (icon.isNull()) { return QString(); } QPixmap pixmap = icon.pixmap(icon.availableSizes().last()); if (pixmap.isNull()) { return QString(); } QTemporaryFile tempFile(QDir::tempPath() + "/iconXXXXXX.png"); tempFile.setAutoRemove(false); if (!tempFile.open()) { return QString(); } QString tempPath = tempFile.fileName(); tempFile.close(); if (!pixmap.save(tempPath, "PNG")) { QFile::remove(tempPath); return QString(); } return tempPath; // Success } void ImportFTBPage::suggestCurrent() { if (!isOpened) return; if (selected.path.isEmpty()) { dialog->setSuggestedPack(); return; } dialog->setSuggestedPack(selected.name, new PackInstallTask(selected)); QString editedLogoName = QString("ftb_%1_%2.jpg").arg(selected.name, QString::number(selected.id)); auto iconPath = FS::PathCombine(selected.path, "folder.jpg"); if (!QFileInfo::exists(iconPath)) { // need to save the icon as that actual logo is not a image on the disk iconPath = saveIconToTempFile(selected.icon); } if (!iconPath.isEmpty() && QFileInfo::exists(iconPath)) { dialog->setSuggestedIconFromFile(iconPath, editedLogoName); } } void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex) { if (!now.isValid()) { onPackSelectionChanged(); return; } QVariant raw = currentModel->data(now, Qt::UserRole); Q_ASSERT(raw.canConvert()); auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } void ImportFTBPage::onPackSelectionChanged(Modpack* pack) { if (pack) { selected = *pack; suggestCurrent(); return; } if (isOpened) dialog->setSuggestedPack(); } void ImportFTBPage::onSortingSelectionChanged(QString sort) { FilterModel::Sorting toSet = currentModel->getAvailableSortings().value(sort); currentModel->setSorting(toSet); } void ImportFTBPage::triggerSearch() { currentModel->setSearchTerm(ui->searchEdit->text()); } void ImportFTBPage::setSearchTerm(QString term) { ui->searchEdit->setText(term); } QString ImportFTBPage::getSerachTerm() const { return ui->searchEdit->text(); } } // namespace FTBImportAPP PrismLauncher-10.0.5/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h0000644000175100017510000000457015144136757026564 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "modplatform/import_ftb/PackHelpers.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/pages/modplatform/import_ftb/ListModel.h" class NewInstanceDialog; namespace FTBImportAPP { namespace Ui { class ImportFTBPage; } class ImportFTBPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: explicit ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~ImportFTBPage(); QString displayName() const override { return tr("FTB App Import"); } QIcon icon() const override { return QIcon::fromTheme("ftb_logo"); } QString id() const override { return "import_ftb"; } QString helpPage() const override { return "FTB-import"; } bool shouldDisplay() const override { return true; } void openedImpl() override; void retranslate() override; /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ virtual QString getSerachTerm() const override; private: void suggestCurrent(); void onPackSelectionChanged(Modpack* pack = nullptr); private slots: void onSortingSelectionChanged(QString data); void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); void triggerSearch(); private: bool initialized = false; Modpack selected; ListModel* listModel = nullptr; FilterModel* currentModel = nullptr; NewInstanceDialog* dialog = nullptr; Ui::ImportFTBPage* ui = nullptr; }; } // namespace FTBImportAPP PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourcePage.ui0000644000175100017510000000537315144136757024570 0ustar runnerrunner ResourcePage 0 0 837 685 Filter options Qt::Horizontal false Qt::ScrollBarAlwaysOff true 48 48 false false Version selected: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ProjectDescriptionPage QTextBrowser
    ui/widgets/ProjectDescriptionPage.h
    packView packDescription sortByBox versionSelectionBox
    PrismLauncher-10.0.5/launcher/ui/pages/modplatform/legacy_ftb/0000755000175100017510000000000015144136757023734 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp0000644000175100017510000002137215144136757026341 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ListModel.h" #include "Application.h" #include "net/ApiDownload.h" #include "net/HttpMetaCache.h" #include "net/NetJob.h" #include #include "StringUtils.h" #include "ui/widgets/ProjectItem.h" #include #include #include #include namespace LegacyFTB { FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) { currentSorting = Sorting::ByGameVersion; sortings.insert(tr("Sort by Name"), Sorting::ByName); sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); } bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); Q_ASSERT(leftRaw.canConvert()); auto leftPack = leftRaw.value(); QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); Q_ASSERT(rightRaw.canConvert()); auto rightPack = rightRaw.value(); if (currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); Version rv(rightPack.mcVersion); return lv < rv; } else if (currentSorting == Sorting::ByName) { return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } // UHM, some inavlid value set?! qWarning() << "Invalid sorting set!"; return true; } bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const { if (searchTerm.isEmpty()) { return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QVariant raw = sourceModel()->data(index, Qt::UserRole); Q_ASSERT(raw.canConvert()); auto pack = raw.value(); if (searchTerm.startsWith("#")) return pack.packCode == searchTerm.mid(1); return pack.name.contains(searchTerm, Qt::CaseInsensitive); } void FilterModel::setSearchTerm(const QString term) { searchTerm = term.trimmed(); invalidate(); } const QMap FilterModel::getAvailableSortings() { return sortings; } QString FilterModel::translateCurrentSorting() { return sortings.key(currentSorting); } void FilterModel::setSorting(Sorting s) { currentSorting = s; invalidate(); } FilterModel::Sorting FilterModel::getCurrentSorting() { return currentSorting; } ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} ListModel::~ListModel() {} QString ListModel::translatePackType(PackType type) const { switch (type) { case PackType::Public: return tr("Public Modpack"); case PackType::ThirdParty: return tr("Third Party Modpack"); case PackType::Private: return tr("Private Modpack"); } qWarning() << "Unknown FTB modpack type:" << int(type); return QString(); } int ListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } Modpack pack = modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { if (pack.description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack.description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } return pack.description; } case Qt::DecorationRole: { if (m_logoMap.contains(pack.logo)) { return (m_logoMap.value(pack.logo)); } QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack.logo); return icon; } case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } case Qt::ForegroundRole: { if (pack.broken) { // FIXME: Hardcoded color return QColor(255, 0, 50); } else if (pack.bugged) { // FIXME: Hardcoded color // bugged pack, currently only indicates bugged xml return QColor(244, 229, 66); } } case Qt::DisplayRole: return pack.name; case Qt::SizeHintRole: return QSize(0, 58); // Custom data case UserDataTypes::TITLE: return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; case UserDataTypes::INSTALLED: return false; default: break; } return {}; } void ListModel::fill(ModpackList modpacks_) { beginResetModel(); this->modpacks = modpacks_; endResetModel(); } void ListModel::addPack(const Modpack& modpack) { beginResetModel(); this->modpacks.append(modpack); endResetModel(); } void ListModel::clear() { beginResetModel(); modpacks.clear(); endResetModel(); } Modpack ListModel::at(int row) { return modpacks.at(row); } void ListModel::remove(int row) { if (row < 0 || row >= modpacks.size()) { qWarning() << "Attempt to remove FTB modpacks with invalid row" << row; return; } beginRemoveRows(QModelIndex(), row, row); modpacks.removeAt(row); endRemoveRows(); } void ListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); emit dataChanged(createIndex(0, 0), createIndex(1, 0)); } void ListModel::logoFailed(QString logo) { m_failedLogos.append(logo); m_loadingLogos.removeAll(logo); } void ListModel::requestLogo(QString file) { if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file)); NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); auto fullPath = entry->getFullPath(); connect(job, &NetJob::finished, this, [this, file, fullPath, job] { job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); if (waitingCallbacks.contains(file)) { waitingCallbacks.value(file)(fullPath); } }); connect(job, &NetJob::failed, this, [this, file, job] { job->deleteLater(); emit logoFailed(file); }); job->start(); m_loadingLogos.append(file); } void ListModel::getLogo(const QString& logo, LogoCallback callback) { if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo); } } Qt::ItemFlags ListModel::flags(const QModelIndex& index) const { return QAbstractListModel::flags(index); } } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/ui/pages/modplatform/legacy_ftb/Page.h0000644000175100017510000001026715144136757024767 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "QObjectPtr.h" #include "modplatform/legacy_ftb/PackFetchTask.h" #include "modplatform/legacy_ftb/PackHelpers.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" class NewInstanceDialog; namespace LegacyFTB { namespace Ui { class Page; } class ListModel; class FilterModel; class PrivatePackManager; class Page : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: explicit Page(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~Page(); QString displayName() const override { return "FTB Legacy"; } QIcon icon() const override { return QIcon::fromTheme("ftb_logo"); } QString id() const override { return "legacy_ftb"; } QString helpPage() const override { return "FTB-legacy"; } bool shouldDisplay() const override; void openedImpl() override; void retranslate() override; /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ virtual QString getSerachTerm() const override; private: void suggestCurrent(); void onPackSelectionChanged(Modpack* pack = nullptr); private slots: void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks); void ftbPackDataDownloadFailed(QString reason); void ftbPackDataDownloadAborted(); void ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack); void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); void onSortingSelectionChanged(QString data); void onVersionSelectionItemChanged(QString data); void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); void onThirdPartyPackSelectionChanged(QModelIndex first, QModelIndex second); void onPrivatePackSelectionChanged(QModelIndex first, QModelIndex second); void onTabChanged(int tab); void onAddPackClicked(); void onRemovePackClicked(); void triggerSearch(); private: FilterModel* currentModel = nullptr; QTreeView* currentList = nullptr; QTextBrowser* currentModpackInfo = nullptr; bool initialized = false; Modpack selected; QString selectedVersion; ListModel* publicListModel = nullptr; FilterModel* publicFilterModel = nullptr; ListModel* thirdPartyModel = nullptr; FilterModel* thirdPartyFilterModel = nullptr; ListModel* privateListModel = nullptr; FilterModel* privateFilterModel = nullptr; unique_qobject_ptr ftbFetchTask; std::unique_ptr ftbPrivatePacks; NewInstanceDialog* dialog = nullptr; Ui::Page* ui = nullptr; }; } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h0000644000175100017510000000373615144136757026012 0ustar runnerrunner#pragma once #include #include #include #include #include #include #include #include namespace LegacyFTB { using FTBLogoMap = QMap; using LogoCallback = std::function; class FilterModel : public QSortFilterProxyModel { Q_OBJECT public: FilterModel(QObject* parent = Q_NULLPTR); enum Sorting { ByName, ByGameVersion }; const QMap getAvailableSortings(); QString translateCurrentSorting(); void setSorting(Sorting sorting); Sorting getCurrentSorting(); void setSearchTerm(QString term); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; private: QMap sortings; Sorting currentSorting; QString searchTerm; }; class ListModel : public QAbstractListModel { Q_OBJECT private: ModpackList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; FTBLogoMap m_logoMap; QMap waitingCallbacks; void requestLogo(QString file); QString translatePackType(PackType type) const; private slots: void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); public: ListModel(QObject* parent); ~ListModel(); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& index, int role) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; void fill(ModpackList modpacks); void addPack(const Modpack& modpack); void clear(); void remove(int row); Modpack at(int row); void getLogo(const QString& logo, LogoCallback callback); }; } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp0000644000175100017510000003252715144136757025325 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Page.h" #include "StringUtils.h" #include "ui/widgets/ProjectItem.h" #include "ui_Page.h" #include #include "Application.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ListModel.h" #include "modplatform/legacy_ftb/PackFetchTask.h" #include "modplatform/legacy_ftb/PackInstallTask.h" #include "modplatform/legacy_ftb/PrivatePackManager.h" namespace LegacyFTB { Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::Page) { ftbFetchTask.reset(new PackFetchTask(APPLICATION->network())); ftbPrivatePacks.reset(new PrivatePackManager()); ui->setupUi(this); { publicFilterModel = new FilterModel(this); publicListModel = new ListModel(this); publicFilterModel->setSourceModel(publicListModel); ui->publicPackList->setModel(publicFilterModel); ui->publicPackList->setSortingEnabled(true); ui->publicPackList->header()->hide(); ui->publicPackList->setIndentation(0); ui->publicPackList->setIconSize(QSize(42, 42)); for (int i = 0; i < publicFilterModel->getAvailableSortings().size(); i++) { ui->sortByBox->addItem(publicFilterModel->getAvailableSortings().keys().at(i)); } ui->sortByBox->setCurrentText(publicFilterModel->translateCurrentSorting()); } { thirdPartyFilterModel = new FilterModel(this); thirdPartyModel = new ListModel(this); thirdPartyFilterModel->setSourceModel(thirdPartyModel); ui->thirdPartyPackList->setModel(thirdPartyFilterModel); ui->thirdPartyPackList->setSortingEnabled(true); ui->thirdPartyPackList->header()->hide(); ui->thirdPartyPackList->setIndentation(0); ui->thirdPartyPackList->setIconSize(QSize(42, 42)); thirdPartyFilterModel->setSorting(publicFilterModel->getCurrentSorting()); } { privateFilterModel = new FilterModel(this); privateListModel = new ListModel(this); privateFilterModel->setSourceModel(privateListModel); ui->privatePackList->setModel(privateFilterModel); ui->privatePackList->setSortingEnabled(true); ui->privatePackList->header()->hide(); ui->privatePackList->setIndentation(0); ui->privatePackList->setIconSize(QSize(42, 42)); privateFilterModel->setSorting(publicFilterModel->getCurrentSorting()); } ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged); connect(ui->searchEdit, &QLineEdit::textChanged, this, &Page::triggerSearch); connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged); connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); connect(ui->addPackBtn, &QPushButton::clicked, this, &Page::onAddPackClicked); connect(ui->removePackBtn, &QPushButton::clicked, this, &Page::onRemovePackClicked); connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged); // ui->modpackInfo->setOpenExternalLinks(true); ui->publicPackList->selectionModel()->reset(); ui->thirdPartyPackList->selectionModel()->reset(); ui->privatePackList->selectionModel()->reset(); ui->publicPackList->setItemDelegate(new ProjectItemDelegate(this)); ui->thirdPartyPackList->setItemDelegate(new ProjectItemDelegate(this)); ui->privatePackList->setItemDelegate(new ProjectItemDelegate(this)); onTabChanged(ui->tabWidget->currentIndex()); } Page::~Page() { delete ui; } bool Page::shouldDisplay() const { return true; } void Page::openedImpl() { if (!initialized) { connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully); connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed); connect(ftbFetchTask.get(), &PackFetchTask::aborted, this, &Page::ftbPackDataDownloadAborted); connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully); connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed); ftbFetchTask->fetch(); ftbPrivatePacks->load(); ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().values()); initialized = true; } suggestCurrent(); } void Page::retranslate() { ui->retranslateUi(this); } void Page::suggestCurrent() { if (!isOpened) { return; } if (selected.broken || selectedVersion.isEmpty()) { dialog->setSuggestedPack(); return; } dialog->setSuggestedPack(selected.name, selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); QString editedLogoName = selected.logo; if (!selected.logo.toLower().startsWith("ftb")) { editedLogoName = "ftb_" + editedLogoName; } if (selected.type == PackType::Public) { publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } else if (selected.type == PackType::ThirdParty) { thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } else if (selected.type == PackType::Private) { privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } } void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks) { publicListModel->fill(publicPacks); thirdPartyModel->fill(thirdPartyPacks); } void Page::ftbPackDataDownloadFailed(QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); } void Page::ftbPackDataDownloadAborted() { CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); } void Page::ftbPrivatePackDataDownloadSuccessfully(const Modpack& pack) { privateListModel->addPack(pack); } void Page::ftbPrivatePackDataDownloadFailed([[maybe_unused]] QString reason, QString packCode) { auto reply = QMessageBox::question(this, tr("FTB private packs"), tr("Failed to download pack information for code %1.\nShould it be removed now?").arg(packCode)); if (reply == QMessageBox::Yes) { ftbPrivatePacks->remove(packCode); } } void Page::onPublicPackSelectionChanged(QModelIndex now, [[maybe_unused]] QModelIndex prev) { if (!now.isValid()) { onPackSelectionChanged(); return; } QVariant raw = publicFilterModel->data(now, Qt::UserRole); Q_ASSERT(raw.canConvert()); auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } void Page::onThirdPartyPackSelectionChanged(QModelIndex now, [[maybe_unused]] QModelIndex prev) { if (!now.isValid()) { onPackSelectionChanged(); return; } QVariant raw = thirdPartyFilterModel->data(now, Qt::UserRole); Q_ASSERT(raw.canConvert()); auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } void Page::onPrivatePackSelectionChanged(QModelIndex now, [[maybe_unused]] QModelIndex prev) { if (!now.isValid()) { onPackSelectionChanged(); return; } QVariant raw = privateFilterModel->data(now, Qt::UserRole); Q_ASSERT(raw.canConvert()); auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } void Page::onPackSelectionChanged(Modpack* pack) { ui->versionSelectionBox->clear(); if (pack) { currentModpackInfo->setHtml(StringUtils::htmlListPatch("Pack by " + pack->author + "" + "
    Minecraft " + pack->mcVersion + "
    " + "
    " + pack->description + "
    • " + pack->mods.replace(";", "
    • ") + "
    ")); bool currentAdded = false; for (int i = 0; i < pack->oldVersions.size(); i++) { if (pack->currentVersion == pack->oldVersions.at(i)) { currentAdded = true; } ui->versionSelectionBox->addItem(pack->oldVersions.at(i)); } if (!currentAdded) { ui->versionSelectionBox->addItem(pack->currentVersion); } selected = *pack; } else { currentModpackInfo->setHtml(""); ui->versionSelectionBox->clear(); if (isOpened) { dialog->setSuggestedPack(); } return; } suggestCurrent(); } void Page::onVersionSelectionItemChanged(QString version) { if (version.isNull() || version.isEmpty()) { selectedVersion = ""; return; } selectedVersion = version; suggestCurrent(); } void Page::onSortingSelectionChanged(QString sort) { FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(sort); publicFilterModel->setSorting(toSet); thirdPartyFilterModel->setSorting(toSet); privateFilterModel->setSorting(toSet); } void Page::onTabChanged(int tab) { if (tab == 1) { currentModel = thirdPartyFilterModel; currentList = ui->thirdPartyPackList; currentModpackInfo = ui->thirdPartyPackDescription; } else if (tab == 2) { currentModel = privateFilterModel; currentList = ui->privatePackList; currentModpackInfo = ui->privatePackDescription; } else { currentModel = publicFilterModel; currentList = ui->publicPackList; currentModpackInfo = ui->publicPackDescription; } triggerSearch(); currentList->selectionModel()->reset(); QModelIndex idx = currentList->currentIndex(); if (idx.isValid()) { QVariant raw = currentModel->data(idx, Qt::UserRole); Q_ASSERT(raw.canConvert()); auto pack = raw.value(); onPackSelectionChanged(&pack); } else { onPackSelectionChanged(); } } void Page::onAddPackClicked() { bool ok; QString text = QInputDialog::getText(this, tr("Add FTB pack"), tr("Enter pack code:"), QLineEdit::Normal, QString(), &ok); if (ok && !text.isEmpty()) { ftbPrivatePacks->add(text); ftbFetchTask->fetchPrivate({ text }); } } void Page::onRemovePackClicked() { auto index = ui->privatePackList->currentIndex(); if (!index.isValid()) { return; } auto row = index.row(); Modpack pack = privateListModel->at(row); auto answer = QMessageBox::question(this, tr("Remove pack"), tr("Are you sure you want to remove pack %1?").arg(pack.name), QMessageBox::Yes | QMessageBox::No); if (answer != QMessageBox::Yes) { return; } ftbPrivatePacks->remove(pack.packCode); privateListModel->remove(row); onPackSelectionChanged(); } void Page::triggerSearch() { currentModel->setSearchTerm(ui->searchEdit->text()); } void Page::setSearchTerm(QString term) { ui->searchEdit->setText(term); } QString Page::getSerachTerm() const { return ui->searchEdit->text(); } } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/ui/pages/modplatform/legacy_ftb/Page.ui0000644000175100017510000001073415144136757025154 0ustar runnerrunner LegacyFTB::Page 0 0 709 602 Search and filter... true 0 Public 16777215 16777215 true true 3rd Party true 16777215 16777215 true Private 16777215 16777215 true Add pack Remove selected pack true 265 0 Version selected: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourcePackPage.h0000644000175100017510000000312715144136757025174 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "ui/pages/modplatform/ResourcePackModel.h" #include "ui/pages/modplatform/ResourcePage.h" namespace Ui { class ResourcePage; } namespace ResourceDownload { class ResourcePackDownloadDialog; class ResourcePackResourcePage : public ResourcePage { Q_OBJECT public: template static T* create(ResourcePackDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'resource pack' inline QString resourcesString() const override { return tr("resource packs"); } //: The singular version of 'resource packs' inline QString resourceString() const override { return tr("resource pack"); } bool supportsFiltering() const override { return false; }; QMap urlHandlers() const override; inline auto helpPage() const -> QString override { return "resourcepack-platform"; } protected: ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance); protected slots: void triggerSearch() override; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/DataPackPage.cpp0000644000175100017510000000276415144136757024617 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // SPDX-FileCopyrightText: 2023 TheKodeToad // // SPDX-License-Identifier: GPL-3.0-only #include "DataPackPage.h" #include "ui_ResourcePage.h" #include "DataPackModel.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include namespace ResourceDownload { DataPackResourcePage::DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} /******** Callbacks to events in the UI (set up in the derived classes) ********/ void DataPackResourcePage::triggerSearch() { m_ui->packView->clearSelection(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap DataPackResourcePage::urlHandlers() const { QMap map; map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/resourcepack\\/([^\\/]+)\\/?"), "modrinth"); map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/texture-packs\\/([^\\/]+)\\/?"), "curseforge"); map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); return map; } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/CustomPage.cpp0000644000175100017510000002055015144136757024412 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "CustomPage.h" #include "ui_CustomPage.h" #include #include "Application.h" #include "Filter.h" #include "Version.h" #include "meta/Index.h" #include "meta/VersionList.h" #include "minecraft/VanillaInstanceCreationTask.h" #include "ui/dialogs/NewInstanceDialog.h" CustomPage::CustomPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage) { ui->setupUi(this); connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion); filterChanged(); connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh); connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedLoaderVersion); connect(ui->noneFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); connect(ui->forgeFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); connect(ui->fabricFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); connect(ui->quiltFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &CustomPage::loaderRefresh); } void CustomPage::openedImpl() { if (!initialized) { auto vlist = APPLICATION->metadataIndex()->get("net.minecraft"); ui->versionList->initialize(vlist.get()); initialized = true; } else { suggestCurrent(); } } void CustomPage::refresh() { ui->versionList->loadList(); } void CustomPage::loaderRefresh() { if (ui->noneFilter->isChecked()) return; ui->loaderVersionList->loadList(); } void CustomPage::filterChanged() { QStringList out; if (ui->alphaFilter->isChecked()) out << "(alpha)"; if (ui->betaFilter->isChecked()) out << "(beta)"; if (ui->snapshotFilter->isChecked()) out << "(snapshot)"; if (ui->releaseFilter->isChecked()) out << "(release)"; if (ui->experimentsFilter->isChecked()) out << "(experiment)"; auto regexp = out.join('|'); ui->versionList->setFilter(BaseVersionList::TypeRole, Filters::regexp(QRegularExpression(regexp))); } void CustomPage::loaderFilterChanged() { QString minecraftVersion; if (m_selectedVersion) { minecraftVersion = m_selectedVersion->descriptor(); } else { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list ui->loaderVersionList->setEmptyString(tr("No Minecraft version is selected.")); ui->loaderVersionList->setEmptyMode(VersionListView::String); return; } if (ui->noneFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list ui->loaderVersionList->setEmptyString(tr("No mod loader is selected.")); ui->loaderVersionList->setEmptyMode(VersionListView::String); return; } else if (ui->neoForgeFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); m_selectedLoader = "net.neoforged"; } else if (ui->forgeFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); m_selectedLoader = "net.minecraftforge"; } else if (ui->fabricFilter->isChecked()) { // FIXME: dirty hack because the launcher is unaware of Fabric's dependencies if (Version(minecraftVersion) >= Version("1.14")) // Fabric/Quilt supported ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, ""); else // Fabric/Quilt unsupported ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // clear list m_selectedLoader = "net.fabricmc.fabric-loader"; } else if (ui->quiltFilter->isChecked()) { // FIXME: dirty hack because the launcher is unaware of Quilt's dependencies (same as Fabric) if (Version(minecraftVersion) >= Version("1.14")) // Fabric/Quilt supported ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, ""); else // Fabric/Quilt unsupported ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // clear list m_selectedLoader = "org.quiltmc.quilt-loader"; } else if (ui->liteLoaderFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); m_selectedLoader = "com.mumfrey.liteloader"; } auto vlist = APPLICATION->metadataIndex()->get(m_selectedLoader); ui->loaderVersionList->initialize(vlist.get()); ui->loaderVersionList->selectRecommended(); ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); } CustomPage::~CustomPage() { delete ui; } bool CustomPage::shouldDisplay() const { return true; } void CustomPage::retranslate() { ui->retranslateUi(this); } BaseVersion::Ptr CustomPage::selectedVersion() const { return m_selectedVersion; } BaseVersion::Ptr CustomPage::selectedLoaderVersion() const { return m_selectedLoaderVersion; } QString CustomPage::selectedLoader() const { return m_selectedLoader; } void CustomPage::suggestCurrent() { if (!isOpened) { return; } if (!m_selectedVersion) { dialog->setSuggestedPack(); return; } // There isn't a selected version if the version list is empty if (ui->loaderVersionList->selectedVersion() == nullptr) dialog->setSuggestedPack(m_selectedVersion->descriptor(), new VanillaCreationTask(m_selectedVersion)); else { dialog->setSuggestedPack(m_selectedVersion->descriptor(), new VanillaCreationTask(m_selectedVersion, m_selectedLoader, m_selectedLoaderVersion)); } dialog->setSuggestedIcon("default"); } void CustomPage::setSelectedVersion(BaseVersion::Ptr version) { m_selectedVersion = version; suggestCurrent(); loaderFilterChanged(); } void CustomPage::setSelectedLoaderVersion(BaseVersion::Ptr version) { m_selectedLoaderVersion = version; suggestCurrent(); } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ModpackProviderBasePage.h0000644000175100017510000000204215144136757026465 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "ui/pages/BasePage.h" class ModpackProviderBasePage : public BasePage { public: /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) = 0; /** Get the current term in the search bar. */ virtual QString getSerachTerm() const = 0; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourcePage.cpp0000644000175100017510000004736515144136757024744 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ResourcePage.h" #include "modplatform/ModIndex.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ResourcePage.h" #include #include #include #include "Markdown.h" #include "Application.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ResourceModel.h" #include "ui/widgets/ProjectItem.h" namespace ResourceDownload { ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_instance) : QWidget(parent), m_baseInstance(base_instance), m_ui(new Ui::ResourcePage), m_parentDialog(parent), m_fetchProgress(this, false) { m_ui->setupUi(this); m_ui->searchEdit->installEventFilter(this); m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_searchTimer.setTimerType(Qt::TimerType::CoarseTimer); m_searchTimer.setSingleShot(true); connect(&m_searchTimer, &QTimer::timeout, this, &ResourcePage::triggerSearch); // hide progress bar to prevent weird artifact m_fetchProgress.hide(); m_fetchProgress.hideIfInactive(true); m_fetchProgress.setFixedHeight(24); m_fetchProgress.progressFormat(""); m_ui->verticalLayout->insertWidget(1, &m_fetchProgress); auto delegate = new ProjectItemDelegate(this); m_ui->packView->setItemDelegate(delegate); m_ui->packView->installEventFilter(this); m_ui->packView->viewport()->installEventFilter(this); connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); connect(m_ui->packView, &QAbstractItemView::doubleClicked, this, &ResourcePage::onResourceToggle); connect(delegate, &ProjectItemDelegate::checkboxClicked, this, &ResourcePage::onResourceToggle); } ResourcePage::~ResourcePage() { delete m_ui; if (m_model) delete m_model; } void ResourcePage::retranslate() { m_ui->retranslateUi(this); } void ResourcePage::openedImpl() { if (!supportsFiltering()) { m_ui->resourceFilterButton->setVisible(false); m_ui->filterWidget->hide(); } //: String in the search bar of the mod downloading dialog m_ui->searchEdit->setPlaceholderText(tr("Search for %1...").arg(resourcesString())); m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); updateSelectionButton(); triggerSearch(); m_ui->searchEdit->setFocus(); } auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool { if (event->type() == QEvent::KeyPress) { auto* keyEvent = static_cast(event); if (watched == m_ui->searchEdit) { if (keyEvent->key() == Qt::Key_Return) { triggerSearch(); keyEvent->accept(); return true; } else { if (m_searchTimer.isActive()) m_searchTimer.stop(); m_searchTimer.start(350); } } else if (watched == m_ui->packView) { // stop the event from going to the confirm button if (keyEvent->key() == Qt::Key_Return) { onResourceToggle(m_ui->packView->currentIndex()); keyEvent->accept(); return true; } } } else if (watched == m_ui->packView->viewport() && event->type() == QEvent::MouseButtonPress) { auto* mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::MiddleButton) { onResourceToggle(m_ui->packView->indexAt(mouseEvent->pos())); return true; } } return QWidget::eventFilter(watched, event); } QString ResourcePage::getSearchTerm() const { return m_ui->searchEdit->text(); } void ResourcePage::setSearchTerm(QString term) { m_ui->searchEdit->setText(term); } void ResourcePage::addSortings() { Q_ASSERT(m_model); auto sorts = m_model->getSortingMethods(); std::sort(sorts.begin(), sorts.end(), [](auto const& l, auto const& r) { return l.index < r.index; }); for (auto&& sorting : sorts) m_ui->sortByBox->addItem(sorting.readable_name, QVariant(sorting.index)); } bool ResourcePage::setCurrentPack(ModPlatform::IndexedPack::Ptr pack) { QVariant v; v.setValue(pack); return m_model->setData(m_ui->packView->currentIndex(), v, Qt::UserRole); } ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const { return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value(); } void ResourcePage::updateUi(const QModelIndex& index) { if (index != m_ui->packView->currentIndex()) return; auto current_pack = getCurrentPack(); if (!current_pack) { m_ui->packDescription->setHtml({}); m_ui->packDescription->flush(); return; } QString text = ""; QString name = current_pack->name; if (current_pack->websiteUrl.isEmpty()) text = name; else text = "websiteUrl + "\">" + name + ""; if (!current_pack->authors.empty()) { auto authorToStr = [](ModPlatform::ModpackAuthor& author) -> QString { if (author.url.isEmpty()) { return author.name; } return QString("%2").arg(author.url, author.name); }; QStringList authorStrs; for (auto& author : current_pack->authors) { authorStrs.push_back(authorToStr(author)); } text += "
    " + tr(" by ") + authorStrs.join(", "); } if (current_pack->extraDataLoaded) { if (current_pack->extraData.status == "archived") { text += "

    " + tr("This project has been archived. It will not receive any further updates unless the author decides " "to unarchive the project."); } if (!current_pack->extraData.donate.isEmpty()) { text += "

    " + tr("Donate information: "); auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { return QString("%2").arg(donate.url, donate.platform); }; QStringList donates; for (auto& donate : current_pack->extraData.donate) { donates.append(donateToStr(donate)); } text += donates.join(", "); } if (!current_pack->extraData.issuesUrl.isEmpty() || !current_pack->extraData.sourceUrl.isEmpty() || !current_pack->extraData.wikiUrl.isEmpty() || !current_pack->extraData.discordUrl.isEmpty()) { text += "

    " + tr("External links:") + "
    "; } if (!current_pack->extraData.issuesUrl.isEmpty()) text += "- " + tr("Issues: %1").arg(current_pack->extraData.issuesUrl) + "
    "; if (!current_pack->extraData.wikiUrl.isEmpty()) text += "- " + tr("Wiki: %1").arg(current_pack->extraData.wikiUrl) + "
    "; if (!current_pack->extraData.sourceUrl.isEmpty()) text += "- " + tr("Source code: %1").arg(current_pack->extraData.sourceUrl) + "
    "; if (!current_pack->extraData.discordUrl.isEmpty()) text += "- " + tr("Discord: %1").arg(current_pack->extraData.discordUrl) + "
    "; } text += "
    "; m_ui->packDescription->setHtml(StringUtils::htmlListPatch( text + (current_pack->extraData.body.isEmpty() ? current_pack->description : markdownToHTML(current_pack->extraData.body)))); m_ui->packDescription->flush(); } void ResourcePage::updateSelectionButton() { if (!isOpened || m_selectedVersionIndex < 0) { m_ui->resourceSelectionButton->setEnabled(false); return; } m_ui->resourceSelectionButton->setEnabled(true); if (auto current_pack = getCurrentPack(); current_pack) { if (current_pack->versionsLoaded && current_pack->versions.empty()) { m_ui->resourceSelectionButton->setEnabled(false); qWarning() << tr("No version available for the selected pack"); } else if (!current_pack->isVersionSelected(m_selectedVersionIndex)) m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); else m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); } else { qWarning() << "Tried to update the selected button but there is not a pack selected"; } } void ResourcePage::versionListUpdated(const QModelIndex& index) { if (index == m_ui->packView->currentIndex()) { auto current_pack = getCurrentPack(); m_ui->versionSelectionBox->blockSignals(true); m_ui->versionSelectionBox->clear(); m_ui->versionSelectionBox->blockSignals(false); if (current_pack) { auto installedVersion = m_model->getInstalledPackVersion(current_pack); for (int i = 0; i < current_pack->versions.size(); i++) { auto& version = current_pack->versions[i]; if (!m_model->checkVersionFilters(version)) continue; auto versionText = version.version; if (version.version_type.isValid()) { versionText += QString(" [%1]").arg(version.version_type.toString()); } if (version.fileId == installedVersion) { versionText += tr(" [installed]", "Mod version select"); } m_ui->versionSelectionBox->addItem(versionText, QVariant(i)); } } if (m_ui->versionSelectionBox->count() == 0) { m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); } if (m_enableQueue.contains(index.row())) { m_enableQueue.remove(index.row()); onResourceToggle(index); } else updateSelectionButton(); } else if (m_enableQueue.contains(index.row())) { m_enableQueue.remove(index.row()); onResourceToggle(index); } } void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { if (!curr.isValid()) { return; } auto current_pack = getCurrentPack(); bool request_load = false; if (!current_pack || !current_pack->versionsLoaded) { m_ui->resourceSelectionButton->setText(tr("Loading versions...")); m_ui->resourceSelectionButton->setEnabled(false); request_load = true; } else { versionListUpdated(curr); } if (current_pack && !current_pack->extraDataLoaded) request_load = true; // we are already requesting this if (m_enableQueue.contains(curr.row())) request_load = false; if (request_load) m_model->loadEntry(curr); updateUi(curr); } void ResourcePage::onVersionSelectionChanged(int index) { m_selectedVersionIndex = m_ui->versionSelectionBox->itemData(index).toInt(); updateSelectionButton(); } void ResourcePage::addResourceToDialog(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version) { m_parentDialog->addResource(pack, version); } void ResourcePage::removeResourceFromDialog(const QString& pack_name) { m_parentDialog->removeResource(pack_name); } void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver, const std::shared_ptr base_model) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_model->addPack(pack, ver, base_model, is_indexed); } void ResourcePage::modelReset() { m_enableQueue.clear(); } void ResourcePage::removeResourceFromPage(const QString& name) { m_model->removePack(name); } void ResourcePage::onResourceSelected() { if (m_selectedVersionIndex < 0) return; auto current_pack = getCurrentPack(); if (!current_pack || !current_pack->versionsLoaded || current_pack->versions.size() < m_selectedVersionIndex) return; auto& version = current_pack->versions[m_selectedVersionIndex]; Q_ASSERT(!version.downloadUrl.isNull()); if (version.is_currently_selected) removeResourceFromDialog(current_pack->name); else addResourceToDialog(current_pack, version); // Save the modified pack (and prevent warning in release build) [[maybe_unused]] bool set = setCurrentPack(current_pack); Q_ASSERT(set); updateSelectionButton(); /* Force redraw on the resource list when the selection changes */ m_ui->packView->repaint(); } void ResourcePage::onResourceToggle(const QModelIndex& index) { const bool isSelected = index == m_ui->packView->currentIndex(); auto pack = m_model->data(index, Qt::UserRole).value(); if (pack->versionsLoaded) { if (pack->isAnyVersionSelected()) removeResourceFromDialog(pack->name); else { auto version = std::find_if(pack->versions.begin(), pack->versions.end(), [this](const ModPlatform::IndexedVersion& version) { return m_model->checkVersionFilters(version); }); if (version == pack->versions.end()) { auto errorMessage = new QMessageBox( QMessageBox::Warning, tr("No versions available"), tr("No versions for '%1' are available.\nThe author likely blocked third-party launchers.").arg(pack->name), QMessageBox::Ok, this); errorMessage->open(); } else addResourceToDialog(pack, *version); } if (isSelected) updateSelectionButton(); // force update QVariant variant; variant.setValue(pack); m_model->setData(index, variant, Qt::UserRole); } else { // the model is just 1 dimensional so this is fine m_enableQueue.insert(index.row()); // we can't be sure that this hasn't already been requested... // but this does the job well enough and there's not much point preventing edgecases if (!isSelected) m_model->loadEntry(index); } } void ResourcePage::openUrl(const QUrl& url) { // do not allow other url schemes for security reasons if (!(url.scheme() == "http" || url.scheme() == "https")) { qWarning() << "Unsupported scheme" << url.scheme(); return; } // detect URLs and search instead const QString address = url.host() + url.path(); QRegularExpressionMatch match; QString page; auto handlers = urlHandlers(); for (auto it = handlers.constKeyValueBegin(); it != handlers.constKeyValueEnd(); it++) { auto&& [regex, candidate] = *it; if (match = QRegularExpression(regex).match(address); match.hasMatch()) { page = candidate; break; } } if (!page.isNull() && !m_doNotJumpToMod) { const QString slug = match.captured(1); // ensure the user isn't opening the same mod if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) { m_parentDialog->selectPage(page); auto newPage = m_parentDialog->selectedPage(); QLineEdit* searchEdit = newPage->m_ui->searchEdit; auto model = newPage->m_model; QListView* view = newPage->m_ui->packView; auto jump = [url, slug, model, view] { for (int row = 0; row < model->rowCount({}); row++) { const QModelIndex index = model->index(row); const auto pack = model->data(index, Qt::UserRole).value(); if (pack->slug == slug) { view->setCurrentIndex(index); return; } } // The final fallback. QDesktopServices::openUrl(url); }; searchEdit->setText(slug); newPage->triggerSearch(); if (model->hasActiveSearchJob()) connect(model->activeSearchJob().get(), &Task::finished, jump); else jump(); return; } } // open in the user's web browser QDesktopServices::openUrl(url); } void ResourcePage::openProject(QVariant projectID) { m_ui->sortByBox->hide(); m_ui->searchEdit->hide(); m_ui->resourceFilterButton->hide(); m_ui->packView->hide(); m_ui->resourceSelectionButton->hide(); m_doNotJumpToMod = true; auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); auto okBtn = buttonBox->button(QDialogButtonBox::Ok); okBtn->setDefault(true); okBtn->setAutoDefault(true); okBtn->setText(tr("Reinstall")); okBtn->setShortcut(tr("Ctrl+Return")); okBtn->setEnabled(false); auto cancelBtn = buttonBox->button(QDialogButtonBox::Cancel); cancelBtn->setDefault(false); cancelBtn->setAutoDefault(false); cancelBtn->setText(tr("Cancel")); connect(okBtn, &QPushButton::clicked, this, [this] { onResourceSelected(); m_parentDialog->accept(); }); connect(cancelBtn, &QPushButton::clicked, m_parentDialog, &ResourceDownloadDialog::reject); m_ui->gridLayout_4->addWidget(buttonBox, 1, 2); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, [this, okBtn](int index) { okBtn->setEnabled(m_ui->versionSelectionBox->itemData(index).toInt() >= 0); }); auto jump = [this] { for (int row = 0; row < m_model->rowCount({}); row++) { const QModelIndex index = m_model->index(row); m_ui->packView->setCurrentIndex(index); return; } m_ui->packDescription->setText(tr("The resource was not found")); }; m_ui->searchEdit->setText("#" + projectID.toString()); triggerSearch(); if (m_model->hasActiveSearchJob()) connect(m_model->activeSearchJob().get(), &Task::finished, jump); else jump(); } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ModModel.cpp0000644000175100017510000001126715144136757024050 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ModModel.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include #include namespace ResourceDownload { ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) {} /******** Make data requests ********/ ResourceAPI::SearchArgs ModModel::createSearchArguments() { auto profile = static_cast(m_base_instance).getPackProfile(); Q_ASSERT(profile); Q_ASSERT(m_filter); std::optional> versions{}; std::optional categories{}; auto loaders = profile->getSupportedModLoaders(); // Version filter if (!m_filter->versions.empty()) versions = m_filter->versions; if (m_filter->loaders) loaders = m_filter->loaders; if (!m_filter->categoryIds.empty()) categories = m_filter->categoryIds; auto side = m_filter->side; auto sort = getCurrentSortingMethodByIndex(); return { ModPlatform::ResourceType::Mod, m_next_search_offset, m_search_term, sort, loaders, versions, side, categories, m_filter->openSource }; } ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; auto profile = static_cast(m_base_instance).getPackProfile(); Q_ASSERT(profile); Q_ASSERT(m_filter); std::optional> versions{}; auto loaders = profile->getSupportedModLoaders(); if (!m_filter->versions.empty()) versions = m_filter->versions; if (m_filter->loaders) loaders = m_filter->loaders; return { pack, versions, loaders, ModPlatform::ResourceType::Mod }; } ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; return { pack }; } void ModModel::searchWithTerm(const QString& term, unsigned int sort, bool filter_changed) { if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort && !filter_changed) { return; } setSearchTerm(term); m_current_sort_index = sort; refresh(); } bool ModModel::isPackInstalled(ModPlatform::IndexedPack::Ptr pack) const { auto allMods = static_cast(m_base_instance).loaderModList()->allMods(); return std::any_of(allMods.cbegin(), allMods.cend(), [pack](Mod* mod) { if (auto meta = mod->metadata(); meta) return meta->provider == pack->provider && meta->project_id == pack->addonId; return false; }); } QVariant ModModel::getInstalledPackVersion(ModPlatform::IndexedPack::Ptr pack) const { auto allMods = static_cast(m_base_instance).loaderModList()->allMods(); for (auto mod : allMods) { if (auto meta = mod->metadata(); meta && meta->provider == pack->provider && meta->project_id == pack->addonId) { return meta->version(); } } return {}; } bool checkSide(ModPlatform::Side filter, ModPlatform::Side value) { return (filter != ModPlatform::Side::ClientSide && filter != ModPlatform::Side::ServerSide) || (value != ModPlatform::Side::ClientSide && value != ModPlatform::Side::ServerSide) || filter == value; } bool ModModel::checkFilters(ModPlatform::IndexedPack::Ptr pack) { if (!m_filter) return true; return !(m_filter->hideInstalled && isPackInstalled(pack)) && checkSide(m_filter->side, pack->side); } bool ModModel::checkVersionFilters(const ModPlatform::IndexedVersion& v) { if (!m_filter) return true; auto loaders = static_cast(m_base_instance).getPackProfile()->getSupportedModLoaders(); if (m_filter->loaders) loaders = m_filter->loaders; return (!optedOut(v) && // is opted out(aka curseforge download link) (!loaders.has_value() || !v.loaders || loaders.value() & v.loaders) && // loaders checkSide(m_filter->side, v.side) && // side (m_filter->releases.empty() || // releases std::find(m_filter->releases.cbegin(), m_filter->releases.cend(), v.version_type) != m_filter->releases.cend()) && m_filter->checkMcVersions(v.mcVersion)); // mcVersions } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/CustomPage.ui0000644000175100017510000002145215144136757024247 0ustar runnerrunner CustomPage 0 0 815 607 0 0 0 0 true 0 0 813 605 0 0 Filter Qt::AlignCenter Releases true true Snapshots true Betas true Alphas true Experiments true Qt::Vertical 20 40 Refresh 0 0 Qt::Horizontal 0 0 Mod Loader Qt::AlignCenter None true loaderBtnGroup NeoForge loaderBtnGroup Forge loaderBtnGroup Fabric loaderBtnGroup Quilt loaderBtnGroup LiteLoader loaderBtnGroup Qt::Vertical 20 40 Refresh VersionSelectWidget QWidget
    ui/widgets/VersionSelectWidget.h
    1
    releaseFilter snapshotFilter betaFilter alphaFilter experimentsFilter refreshBtn
    PrismLauncher-10.0.5/launcher/ui/pages/modplatform/TexturePackPage.h0000644000175100017510000000271715144136757025051 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ResourcePackPage.h" #include "ui/pages/modplatform/TexturePackModel.h" #include "ui_ResourcePage.h" namespace Ui { class ResourcePage; } namespace ResourceDownload { class TexturePackDownloadDialog; class TexturePackResourcePage : public ResourcePackResourcePage { Q_OBJECT public: template static T* create(TexturePackDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'texture pack' inline QString resourcesString() const override { return tr("texture packs"); } //: The singular version of 'texture packs' inline QString resourceString() const override { return tr("texture pack"); } protected: TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) {} }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ShaderPackPage.h0000644000175100017510000000331415144136757024611 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "ui/pages/modplatform/ResourcePage.h" #include "ui/pages/modplatform/ShaderPackModel.h" namespace Ui { class ResourcePage; } namespace ResourceDownload { class ShaderPackDownloadDialog; class ShaderPackResourcePage : public ResourcePage { Q_OBJECT public: template static T* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'shader pack' inline QString resourcesString() const override { return tr("shader packs"); } //: The singular version of 'shader packs' inline QString resourceString() const override { return tr("shader pack"); } bool supportsFiltering() const override { return false; }; void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; QMap urlHandlers() const override; inline auto helpPage() const -> QString override { return "shaderpack-platform"; } protected: ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); protected slots: void triggerSearch() override; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/modrinth/0000755000175100017510000000000015144136757023461 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h0000644000175100017510000000712315144136757026216 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * Copyright 2021-2022 kb1000 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" #include #include namespace Ui { class ModrinthPage; } namespace Modrinth { class ModpackListModel; } class ModrinthPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: explicit ModrinthPage(NewInstanceDialog* dialog, QWidget* parent = nullptr); ~ModrinthPage() override; QString displayName() const override { return tr("Modrinth"); } QIcon icon() const override { return QIcon::fromTheme("modrinth"); } QString id() const override { return "modrinth"; } QString helpPage() const override { return "Modrinth-platform"; } inline QString debugName() const { return "Modrinth"; } inline QString metaEntryBase() const { return "ModrinthModpacks"; }; ModPlatform::IndexedPack::Ptr getCurrent() { return m_current; } void suggestCurrent(); void updateUI(); void retranslate() override; void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ virtual QString getSerachTerm() const override; private slots: void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void triggerSearch(); void createFilterWidget(); private: Ui::ModrinthPage* m_ui; NewInstanceDialog* m_dialog; Modrinth::ModpackListModel* m_model; ModPlatform::IndexedPack::Ptr m_current; QString m_selectedVersion; ProgressWidget m_fetch_progress; // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; std::unique_ptr m_filterWidget; Task::Ptr m_categoriesTask; ModrinthAPI m_api; Task::Ptr m_job; Task::Ptr m_job2; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h0000644000175100017510000001065515144136757026406 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "modplatform/ModIndex.h" #include "net/NetJob.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" class ModPage; class Version; namespace Modrinth { using LogoMap = QMap; using LogoCallback = std::function; class ModpackListModel : public QAbstractListModel { Q_OBJECT public: ModpackListModel(ModrinthPage* parent); ~ModpackListModel() override = default; inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : m_modpacks.size(); }; inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; auto debugName() const -> QString; /* Retrieve information from the model at a given index with the given role */ auto data(const QModelIndex& index, int role) const -> QVariant override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; inline void setActiveJob(NetJob::Ptr ptr) { m_jobPtr = ptr; } /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); bool hasActiveSearchJob() const { return m_jobPtr && m_jobPtr->isRunning(); } Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_jobPtr : nullptr; } void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return parent.isValid() ? false : m_searchState == CanPossiblyFetchMore; }; public slots: void searchRequestFinished(QList& doc_all); void searchRequestFailed(QString reason); void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr); protected slots: void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); void performPaginatedSearch(); protected: void requestLogo(QString file, QString url); inline auto getMineVersions() const -> std::list; protected: ModrinthPage* m_parent; QList m_modpacks; LogoMap m_logoMap; QMap m_waitingCallbacks; QStringList m_failedLogos; QStringList m_loadingLogos; QString m_currentSearchTerm; QString m_currentSort; std::shared_ptr m_filter; int m_nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } m_searchState = None; Task::Ptr m_jobPtr; std::shared_ptr m_allResponse = std::make_shared(); QByteArray m_specific_response; int m_modpacks_per_page = 20; }; } // namespace Modrinth PrismLauncher-10.0.5/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp0000644000175100017510000002566515144136757026750 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModrinthModel.h" #include "Application.h" #include "BuildConfig.h" #include "Json.h" #include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "net/NetJob.h" #include "ui/widgets/ProjectItem.h" #include "net/ApiDownload.h" #include #include namespace Modrinth { ModpackListModel::ModpackListModel(ModrinthPage* parent) : QAbstractListModel(parent), m_parent(parent) {} auto ModpackListModel::debugName() const -> QString { return m_parent->debugName(); } /******** Make data requests ********/ void ModpackListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; if (m_nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; } performPaginatedSearch(); } auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVariant { int pos = index.row(); if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } auto pack = m_modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { if (pack->description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack->description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } return pack->description; } case Qt::DecorationRole: { if (m_logoMap.contains(pack->logoName)) return m_logoMap.value(pack->logoName); QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ModpackListModel*)this)->requestLogo(pack->logoName, pack->logoUrl); return icon; } case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } case Qt::SizeHintRole: return QSize(0, 58); // Custom data case UserDataTypes::TITLE: return pack->name; case UserDataTypes::DESCRIPTION: return pack->description; case UserDataTypes::INSTALLED: return false; default: break; } return {}; } bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) return false; m_modpacks[pos] = value.value(); return true; } void ModpackListModel::performPaginatedSearch() { if (hasActiveSearchJob()) return; static const ModrinthAPI api; if (m_currentSearchTerm.startsWith("#")) { auto projectId = m_currentSearchTerm.mid(1); if (!projectId.isEmpty()) { ResourceAPI::Callback callbacks; callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); }; auto project = std::make_shared(); project->addonId = projectId; if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) { m_jobPtr = job; m_jobPtr->start(); } return; } } // TODO: Move to standalone API ResourceAPI::SortingMethod sort{}; sort.name = m_currentSort; ResourceAPI::Callback> callbacks{}; callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); }; auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }, std::move(callbacks)); m_jobPtr = netJob; m_jobPtr->start(); } void ModpackListModel::refresh() { if (hasActiveSearchJob()) { m_jobPtr->abort(); m_searchState = ResetRequested; return; } beginResetModel(); m_modpacks.clear(); endResetModel(); m_searchState = None; m_nextSearchOffset = 0; performPaginatedSearch(); } static auto sortFromIndex(int index) -> QString { switch (index) { default: case 0: return "relevance"; case 1: return "downloads"; case 2: return "follows"; case 3: return "newest"; case 4: return "updated"; } } void ModpackListModel::searchWithTerm(const QString& term, const int sort, std::shared_ptr filter, bool filterChanged) { if (sort > 5 || sort < 0) return; auto sort_str = sortFromIndex(sort); if (m_currentSearchTerm == term && m_currentSearchTerm.isNull() == term.isNull() && m_currentSort == sort_str && !filterChanged) { return; } m_currentSearchTerm = term; m_currentSort = sort_str; m_filter = filter; refresh(); } void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo, logoUrl); } } void ModpackListModel::requestLogo(QString logo, QString url) { if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || url.isEmpty()) { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo)); auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); if (m_waitingCallbacks.contains(logo)) { m_waitingCallbacks.value(logo)(fullPath); } }); connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); emit logoFailed(logo); }); job->start(); m_loadingLogos.append(logo); } /******** Request callbacks ********/ void ModpackListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); for (int i = 0; i < m_modpacks.size(); i++) { if (m_modpacks[i]->logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } void ModpackListModel::logoFailed(QString logo) { m_failedLogos.append(logo); m_loadingLogos.removeAll(logo); } void ModpackListModel::searchRequestFinished(QList& newList) { m_jobPtr.reset(); if (newList.size() < m_modpacks_per_page) { m_searchState = Finished; } else { m_nextSearchOffset += m_modpacks_per_page; m_searchState = CanPossiblyFetchMore; } // When you have a Qt build with assertions turned on, proceeding here will abort the application if (newList.size() == 0) return; beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + newList.size() - 1); m_modpacks.append(newList); endInsertRows(); } void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack) { m_jobPtr.reset(); beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1); m_modpacks.append(pack); endInsertRows(); } void ModpackListModel::searchRequestFailed(QString) { auto failed_action = dynamic_cast(m_jobPtr.get())->getFailedActions().at(0); if (failed_action->replyStatusCode() == -1) { // Network error QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); } else if (failed_action->replyStatusCode() == 409) { // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), //: %1 refers to the launcher itself QString("%1 %2") .arg(m_parent->displayName()) .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); } m_jobPtr.reset(); if (m_searchState == ResetRequested) { beginResetModel(); m_modpacks.clear(); endResetModel(); m_nextSearchOffset = 0; performPaginatedSearch(); } else { m_searchState = Finished; } } } // namespace Modrinth /******** Helpers ********/ PrismLauncher-10.0.5/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp0000644000175100017510000003267115144136757026557 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * Copyright 2021-2022 kb1000 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModrinthPage.h" #include "Version.h" #include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ModrinthPage.h" #include "ModrinthModel.h" #include "BuildConfig.h" #include "InstanceImportTask.h" #include "Json.h" #include "Markdown.h" #include "StringUtils.h" #include "ui/widgets/ProjectItem.h" #include "net/ApiDownload.h" #include #include #include ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), m_ui(new Ui::ModrinthPage), m_dialog(dialog), m_fetch_progress(this, false) { m_ui->setupUi(this); createFilterWidget(); m_ui->searchEdit->installEventFilter(this); m_model = new Modrinth::ModpackListModel(this); m_ui->packView->setModel(m_model); m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); connect(&m_search_timer, &QTimer::timeout, this, &ModrinthPage::triggerSearch); m_fetch_progress.hideIfInactive(true); m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); m_ui->verticalLayout->insertWidget(1, &m_fetch_progress); m_ui->sortByBox->addItem(tr("Sort by Relevance")); m_ui->sortByBox->addItem(tr("Sort by Total Downloads")); m_ui->sortByBox->addItem(tr("Sort by Follows")); m_ui->sortByBox->addItem(tr("Sort by Newest")); m_ui->sortByBox->addItem(tr("Sort by Last Updated")); connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::onVersionSelectionChanged); m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); m_ui->packDescription->setMetaEntry(metaEntryBase()); } ModrinthPage::~ModrinthPage() { delete m_ui; } void ModrinthPage::retranslate() { m_ui->retranslateUi(this); } void ModrinthPage::openedImpl() { BasePage::openedImpl(); suggestCurrent(); triggerSearch(); } bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) { if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) { auto* keyEvent = reinterpret_cast(event); if (keyEvent->key() == Qt::Key_Return) { this->triggerSearch(); keyEvent->accept(); return true; } else { if (m_search_timer.isActive()) m_search_timer.stop(); m_search_timer.start(350); } } return QObject::eventFilter(watched, event); } void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { m_ui->versionSelectionBox->clear(); if (!curr.isValid()) { if (isOpened) { m_dialog->setSuggestedPack(); } return; } m_current = m_model->data(curr, Qt::UserRole).value(); auto name = m_current->name; if (!m_current->extraDataLoaded) { qDebug() << "Loading modrinth modpack information"; ResourceAPI::Callback callbacks; auto id = m_current->addonId; callbacks.on_fail = [this](QString reason, int) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }; callbacks.on_succeed = [this, id, curr](auto& pack) { if (id != m_current->addonId) { return; // wrong request? } QVariant current_updated; current_updated.setValue(pack); if (!m_model->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache extra info for the current pack!"; suggestCurrent(); updateUI(); }; if (auto netJob = m_api.getProjectInfo({ m_current }, std::move(callbacks)); netJob) { m_job = netJob; m_job->start(); } } else updateUI(); if (!m_current->versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading modrinth modpack versions"; ResourceAPI::Callback> callbacks{}; auto addonId = m_current->addonId; // Use default if no callbacks are set callbacks.on_succeed = [this, curr, addonId](auto& doc) { if (addonId != m_current->addonId) { return; // wrong request } m_current->versions = doc; m_current->versionsLoaded = true; auto pred = [this](const ModPlatform::IndexedVersion& v) { if (auto filter = m_filterWidget->getFilter()) return !filter->checkModpackFilters(v); return false; }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) m_current->versions.removeIf(pred); #else for (auto it = m_current->versions.begin(); it != m_current->versions.end();) if (pred(*it)) it = m_current->versions.erase(it); else ++it; #endif for (const auto& version : m_current->versions) { m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.fileId)); } QVariant current_updated; current_updated.setValue(m_current); if (!m_model->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache versions for the current pack!"; suggestCurrent(); }; callbacks.on_fail = [this](QString reason, int) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }; auto netJob = m_api.getProjectVersions({ m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); m_job2 = netJob; m_job2->start(); } else { for (auto version : m_current->versions) { if (!version.version.contains(version.version)) m_ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.version, version.version_number), QVariant(version.fileId)); else m_ui->versionSelectionBox->addItem(version.version, QVariant(version.fileId)); } suggestCurrent(); } } void ModrinthPage::updateUI() { QString text = ""; if (m_current->websiteUrl.isEmpty()) text = m_current->name; else text = "websiteUrl + "\">" + m_current->name + ""; if (!m_current->authors.empty()) { auto authorToStr = [](ModPlatform::ModpackAuthor& author) { if (author.url.isEmpty()) { return author.name; } return QString("%2").arg(author.url, author.name); }; QStringList authorStrs; for (auto& author : m_current->authors) { authorStrs.push_back(authorToStr(author)); } text += "
    " + tr(" by ") + authorStrs.join(", "); } if (m_current->extraDataLoaded) { if (m_current->extraData.status == "archived") { text += "

    " + tr("This project has been archived. It will not receive any further updates unless the author decides " "to unarchive the project."); } if (!m_current->extraData.donate.isEmpty()) { text += "

    " + tr("Donate information: "); auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { return QString("%2").arg(donate.url, donate.platform); }; QStringList donates; for (auto& donate : m_current->extraData.donate) { donates.append(donateToStr(donate)); } text += donates.join(", "); } if (!m_current->extraData.issuesUrl.isEmpty() || !m_current->extraData.sourceUrl.isEmpty() || !m_current->extraData.wikiUrl.isEmpty() || !m_current->extraData.discordUrl.isEmpty()) { text += "

    " + tr("External links:") + "
    "; } if (!m_current->extraData.issuesUrl.isEmpty()) text += "- " + tr("Issues: %1").arg(m_current->extraData.issuesUrl) + "
    "; if (!m_current->extraData.wikiUrl.isEmpty()) text += "- " + tr("Wiki: %1").arg(m_current->extraData.wikiUrl) + "
    "; if (!m_current->extraData.sourceUrl.isEmpty()) text += "- " + tr("Source code: %1").arg(m_current->extraData.sourceUrl) + "
    "; if (!m_current->extraData.discordUrl.isEmpty()) text += "- " + tr("Discord: %1").arg(m_current->extraData.discordUrl) + "
    "; } text += "
    "; text += markdownToHTML(m_current->extraData.body.toUtf8()); m_ui->packDescription->setHtml(StringUtils::htmlListPatch(text + m_current->description)); m_ui->packDescription->flush(); } void ModrinthPage::suggestCurrent() { if (!isOpened) { return; } if (m_selectedVersion.isEmpty()) { m_dialog->setSuggestedPack(); return; } for (auto& ver : m_current->versions) { if (ver.fileId == m_selectedVersion) { QMap extra_info; extra_info.insert("pack_id", m_current->addonId.toString()); extra_info.insert("pack_version_id", ver.fileId.toString()); m_dialog->setSuggestedPack(m_current->name, ver.version, new InstanceImportTask(ver.downloadUrl, this, std::move(extra_info))); QString editedLogoName = "modrinth_" + m_current->logoName; m_model->getLogo(m_current->logoName, m_current->logoUrl, [this, editedLogoName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, editedLogoName); }); break; } } } void ModrinthPage::triggerSearch() { m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); m_ui->packView->clearSelection(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); bool filterChanged = m_filterWidget->changed(); m_model->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); m_fetch_progress.watch(m_model->activeSearchJob().get()); } void ModrinthPage::onVersionSelectionChanged(int index) { if (index == -1) { m_selectedVersion = ""; return; } m_selectedVersion = m_ui->versionSelectionBox->itemData(index).toString(); suggestCurrent(); } void ModrinthPage::setSearchTerm(QString term) { m_ui->searchEdit->setText(term); } QString ModrinthPage::getSerachTerm() const { return m_ui->searchEdit->text(); } void ModrinthPage::createFilterWidget() { auto widget = ModFilterWidget::create(nullptr, true); m_filterWidget.swap(widget); auto old = m_ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it if (old) { delete old; } connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = ModrinthAPI::getModCategories(response); connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = ModrinthAPI::loadCategories(response, "modpack"); m_filterWidget->setCategories(categories); }); m_categoriesTask->start(); }PrismLauncher-10.0.5/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp0000644000175100017510000002005715144136757030445 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModrinthResourcePages.h" #include "ui/pages/modplatform/DataPackModel.h" #include "ui_ResourcePage.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/ResourceDownloadDialog.h" namespace ResourceDownload { ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { m_model = new ModModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthModPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { m_model = new ResourcePackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthResourcePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthResourcePackPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : TexturePackResourcePage(dialog, instance) { m_model = new TexturePackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthTexturePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthTexturePackPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ShaderPackResourcePage(dialog, instance) { m_model = new ShaderPackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthShaderPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthShaderPackPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) : DataPackResourcePage(dialog, instance) { m_model = new DataPackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthDataPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthDataPackPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthDataPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthDataPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } // I don't know why, but doing this on the parent class makes it so that // other mod providers start loading before being selected, at least with // my Qt, so we need to implement this in every derived class... auto ModrinthModPage::shouldDisplay() const -> bool { return true; } auto ModrinthResourcePackPage::shouldDisplay() const -> bool { return true; } auto ModrinthTexturePackPage::shouldDisplay() const -> bool { return true; } auto ModrinthShaderPackPage::shouldDisplay() const -> bool { return true; } auto ModrinthDataPackPage::shouldDisplay() const -> bool { return true; } std::unique_ptr ModrinthModPage::createFilterWidget() { return ModFilterWidget::create(&static_cast(m_baseInstance), true); } void ModrinthModPage::prepareProviderCategories() { auto response = std::make_shared(); m_categoriesTask = ModrinthAPI::getModCategories(response); connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = ModrinthAPI::loadModCategories(response); m_filter_widget->setCategories(categories); }); m_categoriesTask->start(); }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h0000644000175100017510000001621715144136757030115 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "modplatform/ResourceAPI.h" #include "ui/pages/modplatform/DataPackPage.h" #include "ui/pages/modplatform/ModPage.h" #include "ui/pages/modplatform/ResourcePackPage.h" #include "ui/pages/modplatform/ShaderPackPage.h" #include "ui/pages/modplatform/TexturePackPage.h" namespace ResourceDownload { namespace Modrinth { static inline QString displayName() { return "Modrinth"; } static inline QIcon icon() { return QIcon::fromTheme("modrinth"); } static inline QString id() { return "modrinth"; } static inline QString debugName() { return "Modrinth"; } static inline QString metaEntryBase() { return "ModrinthPacks"; } } // namespace Modrinth class ModrinthModPage : public ModPage { Q_OBJECT public: static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance& instance) { return ModPage::create(dialog, instance); } ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~ModrinthModPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Modrinth::displayName(); } inline auto icon() const -> QIcon override { return Modrinth::icon(); } inline auto id() const -> QString override { return Modrinth::id(); } inline auto debugName() const -> QString override { return Modrinth::debugName(); } inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } inline auto helpPage() const -> QString override { return "Mod-platform"; } std::unique_ptr createFilterWidget() override; protected: virtual void prepareProviderCategories() override; Task::Ptr m_categoriesTask; }; class ModrinthResourcePackPage : public ResourcePackResourcePage { Q_OBJECT public: static ModrinthResourcePackPage* create(ResourcePackDownloadDialog* dialog, BaseInstance& instance) { return ResourcePackResourcePage::create(dialog, instance); } ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthResourcePackPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Modrinth::displayName(); } inline auto icon() const -> QIcon override { return Modrinth::icon(); } inline auto id() const -> QString override { return Modrinth::id(); } inline auto debugName() const -> QString override { return Modrinth::debugName(); } inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } inline auto helpPage() const -> QString override { return ""; } }; class ModrinthTexturePackPage : public TexturePackResourcePage { Q_OBJECT public: static ModrinthTexturePackPage* create(TexturePackDownloadDialog* dialog, BaseInstance& instance) { return TexturePackResourcePage::create(dialog, instance); } ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthTexturePackPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Modrinth::displayName(); } inline auto icon() const -> QIcon override { return Modrinth::icon(); } inline auto id() const -> QString override { return Modrinth::id(); } inline auto debugName() const -> QString override { return Modrinth::debugName(); } inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } inline auto helpPage() const -> QString override { return ""; } }; class ModrinthShaderPackPage : public ShaderPackResourcePage { Q_OBJECT public: static ModrinthShaderPackPage* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance) { return ShaderPackResourcePage::create(dialog, instance); } ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthShaderPackPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Modrinth::displayName(); } inline auto icon() const -> QIcon override { return Modrinth::icon(); } inline auto id() const -> QString override { return Modrinth::id(); } inline auto debugName() const -> QString override { return Modrinth::debugName(); } inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } inline auto helpPage() const -> QString override { return ""; } }; class ModrinthDataPackPage : public DataPackResourcePage { Q_OBJECT public: static ModrinthDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance) { return DataPackResourcePage::create(dialog, instance); } ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthDataPackPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Modrinth::displayName(); } inline auto icon() const -> QIcon override { return Modrinth::icon(); } inline auto id() const -> QString override { return Modrinth::id(); } inline auto debugName() const -> QString override { return Modrinth::debugName(); } inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } inline auto helpPage() const -> QString override { return ""; } }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui0000644000175100017510000000540715144136757026407 0ustar runnerrunner ModrinthPage 0 0 800 600 Filter options Search and filter... 0 0 Qt::Horizontal Qt::ScrollBarAlwaysOff true 48 48 true true Version selected: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ProjectDescriptionPage QTextBrowser
    ui/widgets/ProjectDescriptionPage.h
    packView packDescription sortByBox versionSelectionBox
    PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ImportPage.cpp0000644000175100017510000002063315144136757024414 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ImportPage.h" #include "ui/dialogs/ProgressDialog.h" #include "ui_ImportPage.h" #include #include #include #include #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/NewInstanceDialog.h" #include "modplatform/flame/FlameAPI.h" #include "Json.h" #include "InstanceImportTask.h" #include "net/NetJob.h" class UrlValidator : public QValidator { public: using QValidator::QValidator; State validate(QString& in, [[maybe_unused]] int& pos) const { const QUrl url(in); if (url.isValid() && !url.isRelative() && !url.isEmpty()) { return Acceptable; } else if (QFile::exists(in)) { return Acceptable; } else { return Intermediate; } } }; ImportPage::ImportPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ImportPage), dialog(dialog) { ui->setupUi(this); ui->modpackEdit->setValidator(new UrlValidator(ui->modpackEdit)); connect(ui->modpackEdit, &QLineEdit::textChanged, this, &ImportPage::updateState); } ImportPage::~ImportPage() { delete ui; } bool ImportPage::shouldDisplay() const { return true; } void ImportPage::retranslate() { ui->retranslateUi(this); } void ImportPage::openedImpl() { updateState(); } void ImportPage::updateState() { if (!isOpened) { return; } if (ui->modpackEdit->hasAcceptableInput()) { QString input = ui->modpackEdit->text().trimmed(); auto url = QUrl::fromUserInput(input); if (url.isLocalFile()) { // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); // Allow non-latin people to use ZIP files! bool isZip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); // mrpack is a modrinth pack bool isMRPack = fi.suffix() == "mrpack"; if (fi.exists() && (isZip || isMRPack)) { auto extra_info = QMap(m_extra_info); qDebug() << "Pack Extra Info" << extra_info << m_extra_info; dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url, this, std::move(extra_info))); dialog->setSuggestedIcon("default"); } } else if (url.scheme() == "curseforge") { // need to find the download link for the modpack // format of url curseforge://install?addonId=IDHERE&fileId=IDHERE QUrlQuery query(url); if (query.allQueryItemValues("addonId").isEmpty() || query.allQueryItemValues("fileId").isEmpty()) { qDebug() << "Invalid curseforge link:" << url; return; } auto addonId = query.allQueryItemValues("addonId")[0]; auto fileId = query.allQueryItemValues("fileId")[0]; auto array = std::make_shared(); auto api = FlameAPI(); auto job = api.getFile(addonId, fileId, array); connect(job.get(), &NetJob::failed, this, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(job.get(), &NetJob::succeeded, this, [this, array, addonId, fileId] { qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); auto doc = Json::requireDocument(*array); auto data = doc.object()["data"].toObject(); // No way to find out if it's a mod or a modpack before here // And also we need to check if it ends with .zip, instead of any better way auto fileName = data["fileName"].toString(); if (fileName.endsWith(".zip")) { // Have to use ensureString then use QUrl to get proper url encoding auto dl_url = QUrl(data["downloadUrl"].toString("")); if (!dl_url.isValid()) { CustomMessageBox::selectable( this, tr("Error"), tr("The modpack %1 is blocked for third-parties! Please download it manually.").arg(fileName), QMessageBox::Critical) ->show(); return; } QFileInfo dl_file(dl_url.fileName()); QString pack_name = data["displayName"].toString(dl_file.completeBaseName()); QMap extra_info; extra_info.insert("pack_id", addonId); extra_info.insert("pack_version_id", fileId); dialog->setSuggestedPack(pack_name, new InstanceImportTask(dl_url, this, std::move(extra_info))); dialog->setSuggestedIcon("default"); } else { CustomMessageBox::selectable(this, tr("Error"), tr("This url isn't a valid modpack !"), QMessageBox::Critical)->show(); } }); ProgressDialog dlUrlDialod(this); dlUrlDialod.setSkipButton(true, tr("Abort")); dlUrlDialod.execWithTask(job.get()); return; } else { if (input.endsWith("?client=y")) { input.chop(9); input.append("/file"); url = QUrl::fromUserInput(input); } // hook, line and sinker. QFileInfo fi(url.fileName()); auto extra_info = QMap(m_extra_info); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url, this, std::move(extra_info))); dialog->setSuggestedIcon("default"); } } else { dialog->setSuggestedPack(); } } void ImportPage::setUrl(const QString& url) { ui->modpackEdit->setText(url); updateState(); } void ImportPage::setExtraInfo(const QMap& extra_info) { m_extra_info = extra_info; updateState(); } void ImportPage::on_modpackBtn_clicked() { const QMimeType zip = QMimeDatabase().mimeTypeForName("application/zip"); auto filter = tr("Supported files") + QString(" (%1 *.mrpack)").arg(zip.globPatterns().join(" ")); filter += ";;" + zip.filterString(); //: Option for filtering for *.mrpack files when importing filter += ";;" + tr("Modrinth pack") + " (*.mrpack)"; const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { if (url.isLocalFile()) { ui->modpackEdit->setText(url.toLocalFile()); } else { ui->modpackEdit->setText(url.toString()); } } } QUrl ImportPage::modpackUrl() const { const QUrl url(ui->modpackEdit->text()); if (url.isValid() && !url.isRelative() && !url.host().isEmpty()) { return url; } else { return QUrl::fromLocalFile(ui->modpackEdit->text()); } } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourcePackModel.cpp0000644000175100017510000000311015144136757025703 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ResourcePackModel.h" #include namespace ResourceDownload { ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) {} /******** Make data requests ********/ ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); return { ModPlatform::ResourceType::ResourcePack, m_next_search_offset, m_search_term, sort }; } ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; return { pack, {}, {}, ModPlatform::ResourceType::ResourcePack }; } ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; return { pack }; } void ResourcePackResourceModel::searchWithTerm(const QString& term, unsigned int sort) { if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) { return; } setSearchTerm(term); m_current_sort_index = sort; refresh(); } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/DataPackModel.cpp0000644000175100017510000000275415144136757025002 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // SPDX-FileCopyrightText: 2023 TheKodeToad // // SPDX-License-Identifier: GPL-3.0-only #include "DataPackModel.h" #include namespace ResourceDownload { DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) {} /******** Make data requests ********/ ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); return { ModPlatform::ResourceType::DataPack, m_next_search_offset, m_search_term, sort, ModPlatform::ModLoaderType::DataPack }; } ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; return { pack, {}, ModPlatform::ModLoaderType::DataPack }; } ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; return { pack }; } void DataPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) { if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) { return; } setSearchTerm(term); m_current_sort_index = sort; refresh(); } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourcePackPage.cpp0000644000175100017510000000307315144136757025527 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ResourcePackPage.h" #include "ui_ResourcePage.h" #include "ResourcePackModel.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include namespace ResourceDownload { ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} /******** Callbacks to events in the UI (set up in the derived classes) ********/ void ResourcePackResourcePage::triggerSearch() { m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); m_ui->packView->clearSelection(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ResourcePackResourcePage::urlHandlers() const { QMap map; map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/resourcepack\\/([^\\/]+)\\/?"), "modrinth"); map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/texture-packs\\/([^\\/]+)\\/?"), "curseforge"); map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); return map; } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/OptionalModDialog.h0000644000175100017510000000211215144136757025347 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include namespace Ui { class OptionalModDialog; } class OptionalModDialog : public QDialog { Q_OBJECT public: OptionalModDialog(QWidget* parent, const QStringList& mods); ~OptionalModDialog() override; QStringList getResult(); private: Ui::OptionalModDialog* ui; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/technic/0000755000175100017510000000000015144136757023252 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/modplatform/technic/TechnicPage.h0000644000175100017510000000650715144136757025605 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021-2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "TechnicData.h" #include "net/NetJob.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ProgressWidget.h" namespace Ui { class TechnicPage; } class NewInstanceDialog; namespace Technic { class ListModel; } class TechnicPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: explicit TechnicPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~TechnicPage(); virtual QString displayName() const override { return "Technic"; } virtual QIcon icon() const override { return QIcon::fromTheme("technic"); } virtual QString id() const override { return "technic"; } virtual QString helpPage() const override { return "Technic-platform"; } virtual bool shouldDisplay() const override; void retranslate() override; void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ virtual QString getSerachTerm() const override; private: void suggestCurrent(); void metadataLoaded(); void selectVersion(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); void onSolderLoaded(); void onVersionSelectionChanged(QString data); private: Ui::TechnicPage* ui = nullptr; NewInstanceDialog* dialog = nullptr; Technic::ListModel* model = nullptr; Technic::Modpack current; QString selectedVersion; NetJob::Ptr jobPtr; std::shared_ptr response = std::make_shared(); ProgressWidget m_fetch_progress; // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/technic/TechnicPage.ui0000644000175100017510000000401715144136757025765 0ustar runnerrunner TechnicPage 0 0 546 405 Search and filter... true 48 48 true Qt::Horizontal QSizePolicy::Preferred 1 1 Version selected: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter PrismLauncher-10.0.5/launcher/ui/pages/modplatform/technic/TechnicData.h0000644000175100017510000000403515144136757025574 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021-2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include namespace Technic { struct Modpack { QString slug; QString name; QString logoUrl; QString logoName; bool broken = true; QString url; bool isSolder = false; QString minecraftVersion; bool metadataLoaded = false; QString websiteUrl; QString author; QString description; QString currentVersion; bool versionsLoaded = false; QString recommended; QList versions; }; } // namespace Technic Q_DECLARE_METATYPE(Technic::Modpack) PrismLauncher-10.0.5/launcher/ui/pages/modplatform/technic/TechnicPage.cpp0000644000175100017510000002630715144136757026140 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021-2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "TechnicPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/widgets/ProjectItem.h" #include "ui_TechnicPage.h" #include #include "ui/dialogs/NewInstanceDialog.h" #include "BuildConfig.h" #include "Json.h" #include "StringUtils.h" #include "TechnicModel.h" #include "modplatform/technic/SingleZipPackInstallTask.h" #include "modplatform/technic/SolderPackInstallTask.h" #include "Application.h" #include "modplatform/technic/SolderPackManifest.h" #include "net/ApiDownload.h" TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); ui->searchEdit->installEventFilter(this); model = new Technic::ListModel(this); ui->packView->setModel(model); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); connect(&m_search_timer, &QTimer::timeout, this, &TechnicPage::triggerSearch); m_fetch_progress.hideIfInactive(true); m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); ui->verticalLayout->insertWidget(1, &m_fetch_progress); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); ui->packView->setItemDelegate(new ProjectItemDelegate(this)); } bool TechnicPage::eventFilter(QObject* watched, QEvent* event) { if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Return) { triggerSearch(); keyEvent->accept(); return true; } else { if (m_search_timer.isActive()) m_search_timer.stop(); m_search_timer.start(350); } } return QWidget::eventFilter(watched, event); } TechnicPage::~TechnicPage() { delete ui; } bool TechnicPage::shouldDisplay() const { return true; } void TechnicPage::retranslate() { ui->retranslateUi(this); } void TechnicPage::openedImpl() { suggestCurrent(); triggerSearch(); } void TechnicPage::triggerSearch() { model->searchWithTerm(ui->searchEdit->text()); m_fetch_progress.watch(model->activeSearchJob().get()); } void TechnicPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex second) { ui->versionSelectionBox->clear(); if (!first.isValid()) { if (isOpened) { dialog->setSuggestedPack(); } return; } QVariant raw = model->data(first, Qt::UserRole); Q_ASSERT(raw.canConvert()); current = raw.value(); suggestCurrent(); } void TechnicPage::suggestCurrent() { if (!isOpened) { return; } if (current.broken) { dialog->setSuggestedPack(); return; } QString editedLogoName = "technic_" + current.logoName; model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); if (current.metadataLoaded) { metadataLoaded(); return; } auto netJob = makeShared(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; netJob->addNetAction(Net::ApiDownload::makeByteArray( QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), response)); connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); if (current.slug != slug) { return; } QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonObject obj = doc.object(); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Technic at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } if (!obj.contains("url")) { qWarning() << "Json doesn't contain an url key"; return; } QJsonValueRef url = obj["url"]; if (url.isString()) { current.url = url.toString(); } else { if (!obj.contains("solder")) { qWarning() << "Json doesn't contain a valid url or solder key"; return; } QJsonValueRef solderUrl = obj["solder"]; if (solderUrl.isString()) { current.url = solderUrl.toString(); current.isSolder = true; } else { qWarning() << "Json doesn't contain a valid url or solder key"; return; } } current.minecraftVersion = obj["minecraft"].toString(); current.websiteUrl = obj["platformUrl"].toString(); current.author = obj["user"].toString(); current.description = obj["description"].toString(); current.currentVersion = obj["version"].toString(); current.metadataLoaded = true; metadataLoaded(); }); connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); jobPtr = netJob; jobPtr->start(); } // expects current.metadataLoaded to be true void TechnicPage::metadataLoaded() { QString text = ""; QString name = current.name; if (current.websiteUrl.isEmpty()) text = name.toHtmlEscaped(); else text = "" + name.toHtmlEscaped() + ""; if (!current.author.isEmpty()) { text += "
    " + tr(" by ") + current.author.toHtmlEscaped(); } text += "

    "; ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); // Strip trailing forward-slashes from Solder URL's if (current.isSolder) { while (current.url.endsWith('/')) current.url.chop(1); } // Display versions from Solder if (!current.isSolder) { // If the pack isn't a Solder pack, it only has the single version ui->versionSelectionBox->addItem(current.currentVersion); } else if (current.versionsLoaded) { // reverse foreach, so that the newest versions are first for (auto i = current.versions.size(); i--;) { ui->versionSelectionBox->addItem(current.versions.at(i)); } ui->versionSelectionBox->setCurrentText(current.recommended); } else { // For now, until the versions are pulled from the Solder instance, display the current // version so we can display something quicker ui->versionSelectionBox->addItem(current.currentVersion); auto netJob = makeShared(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); auto url = QString("%1/modpack/%2").arg(current.url, current.slug); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response)); connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); jobPtr = netJob; jobPtr->start(); } selectVersion(); } void TechnicPage::selectVersion() { if (!isOpened) { return; } if (current.broken) { dialog->setSuggestedPack(); return; } if (!current.isSolder) { dialog->setSuggestedPack(current.name, selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); } else { dialog->setSuggestedPack(current.name, selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url, current.slug, selectedVersion, current.minecraftVersion)); } } void TechnicPage::onSolderLoaded() { jobPtr.reset(); auto fallback = [this]() { current.versionsLoaded = true; current.versions.clear(); current.versions.append(current.currentVersion); }; current.versions.clear(); QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; fallback(); return; } auto obj = doc.object(); TechnicSolder::Pack pack; try { TechnicSolder::loadPack(pack, obj); } catch (const JSONValidationError& err) { qCritical() << "Couldn't parse Solder pack metadata:" << err.cause(); fallback(); return; } current.versionsLoaded = true; current.recommended = pack.recommended; current.versions.append(pack.builds); // Finally, let's reload :) ui->versionSelectionBox->clear(); metadataLoaded(); } void TechnicPage::onVersionSelectionChanged(QString version) { if (version.isNull() || version.isEmpty()) { selectedVersion = ""; return; } selectedVersion = version; selectVersion(); } void TechnicPage::setSearchTerm(QString term) { ui->searchEdit->setText(term); } QString TechnicPage::getSerachTerm() const { return ui->searchEdit->text(); } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/technic/TechnicModel.cpp0000644000175100017510000002471215144136757026322 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "TechnicModel.h" #include "Application.h" #include "BuildConfig.h" #include "Json.h" #include "net/ApiDownload.h" #include "ui/widgets/ProjectItem.h" #include #include #include Technic::ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} Technic::ListModel::~ListModel() {} QVariant Technic::ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } Modpack pack = modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { if (pack.description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack.description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } return pack.description; } case Qt::DecorationRole: { if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; } case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } case Qt::DisplayRole: return pack.name; case Qt::SizeHintRole: return QSize(0, 58); // Custom data case UserDataTypes::TITLE: return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; case UserDataTypes::INSTALLED: return false; default: break; } return {}; } int Technic::ListModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } int Technic::ListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : modpacks.size(); } void Technic::ListModel::searchWithTerm(const QString& term) { if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { return; } currentSearchTerm = term; if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; return; } beginResetModel(); modpacks.clear(); endResetModel(); searchState = None; performSearch(); } void Technic::ListModel::performSearch() { if (hasActiveSearchJob()) return; auto netJob = makeShared("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { searchUrl = QString("%1trending?build=%2").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); searchMode = List; } else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { searchUrl = QString("https://%1?build=%2").arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); searchMode = Single; } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); searchMode = Single; } else if (currentSearchTerm.startsWith("#")) { searchUrl = QString("https://api.technicpack.net/modpack/%1?build=%2").arg(currentSearchTerm.mid(1), BuildConfig.TECHNIC_API_BUILD); searchMode = Single; } else { searchUrl = QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } auto clientId = APPLICATION->settings()->get("TechnicClientID").toString(); if (!clientId.isEmpty()) { searchUrl += "?cid=" + clientId; } netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } void Technic::ListModel::searchRequestFinished() { jobPtr.reset(); QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Technic at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } QList newList; try { auto root = Json::requireObject(doc); switch (searchMode) { case List: { auto objs = Json::requireArray(root, "modpacks"); for (auto technicPack : objs) { Modpack pack; auto technicPackObject = Json::requireObject(technicPack); pack.name = Json::requireString(technicPackObject, "name"); pack.slug = Json::requireString(technicPackObject, "slug"); if (pack.slug == "vanilla") continue; auto rawURL = technicPackObject["iconUrl"].toString("null"); if (rawURL == "null") { pack.logoUrl = "null"; pack.logoName = "null"; } else { pack.logoUrl = rawURL; pack.logoName = pack.slug + "." + QFileInfo(QUrl(rawURL).fileName()).suffix(); } pack.broken = false; newList.append(pack); } break; } case Single: { if (root.contains("error")) { // Invalid API url break; } Modpack pack; pack.name = Json::requireString(root, "displayName"); pack.slug = Json::requireString(root, "name"); if (root.contains("icon")) { auto iconObj = Json::requireObject(root, "icon"); auto iconUrl = Json::requireString(iconObj, "url"); pack.logoUrl = iconUrl; pack.logoName = pack.slug + "." + QFileInfo(QUrl(iconUrl).fileName()).suffix(); } else { pack.logoUrl = "null"; pack.logoName = "null"; } pack.broken = false; newList.append(pack); break; } } } catch (const JSONValidationError& err) { qCritical() << "Couldn't parse technic search results:" << err.cause(); return; } searchState = Finished; // When you have a Qt build with assertions turned on, proceeding here will abort the application if (newList.size() == 0) return; beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); modpacks.append(newList); endInsertRows(); } void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback) { if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo, logoUrl); } } void Technic::ListModel::searchRequestFailed() { jobPtr.reset(); if (searchState == ResetRequested) { beginResetModel(); modpacks.clear(); endResetModel(); performSearch(); } else { searchState = Finished; } } void Technic::ListModel::logoLoaded(QString logo, QString out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, QIcon(out)); for (int i = 0; i < modpacks.size(); i++) { if (modpacks[i].logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } void Technic::ListModel::logoFailed(QString logo) { m_failedLogos.append(logo); m_loadingLogos.removeAll(logo); } void Technic::ListModel::requestLogo(QString logo, QString url) { if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); auto job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); logoLoaded(logo, fullPath); }); connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); logoFailed(logo); }); job->start(); m_loadingLogos.append(logo); } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/technic/TechnicModel.h0000644000175100017510000000573415144136757025772 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "TechnicData.h" #include "net/NetJob.h" namespace Technic { using LogoCallback = std::function; class ListModel : public QAbstractListModel { Q_OBJECT public: ListModel(QObject* parent); virtual ~ListModel(); virtual QVariant data(const QModelIndex& index, int role) const; virtual int columnCount(const QModelIndex& parent) const; virtual int rowCount(const QModelIndex& parent) const; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term); bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } private slots: void searchRequestFinished(); void searchRequestFailed(); void logoFailed(QString logo); void logoLoaded(QString logo, QString out); private: void performSearch(); void requestLogo(QString logo, QString url); private: QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; QMap m_logoMap; QMap waitingCallbacks; QString currentSearchTerm; enum SearchState { None, ResetRequested, Finished } searchState = None; enum SearchMode { List, Single, } searchMode = List; NetJob::Ptr jobPtr; std::shared_ptr response = std::make_shared(); }; } // namespace Technic PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/0000755000175100017510000000000015144136757023763 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp0000644000175100017510000000621115144136757027336 0ustar runnerrunner/* * Copyright 2020-2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AtlFilterModel.h" #include #include #include #include "StringUtils.h" namespace Atl { FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) { currentSorting = Sorting::ByPopularity; sortings.insert(tr("Sort by Popularity"), Sorting::ByPopularity); sortings.insert(tr("Sort by Name"), Sorting::ByName); sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); searchTerm = ""; } const QMap FilterModel::getAvailableSortings() { return sortings; } QString FilterModel::translateCurrentSorting() { return sortings.key(currentSorting); } void FilterModel::setSorting(Sorting sorting) { currentSorting = sorting; invalidate(); } FilterModel::Sorting FilterModel::getCurrentSorting() { return currentSorting; } void FilterModel::setSearchTerm(const QString term) { searchTerm = term.trimmed(); invalidate(); } bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { if (searchTerm.isEmpty()) { return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QVariant raw = sourceModel()->data(index, Qt::UserRole); Q_ASSERT(raw.canConvert()); auto pack = raw.value(); if (searchTerm.startsWith("#")) return QString::number(pack.id) == searchTerm.mid(1); return pack.name.contains(searchTerm, Qt::CaseInsensitive); } bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); Q_ASSERT(leftRaw.canConvert()); auto leftPack = leftRaw.value(); QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); Q_ASSERT(rightRaw.canConvert()); auto rightPack = rightRaw.value(); if (currentSorting == ByPopularity) { return leftPack.position > rightPack.position; } else if (currentSorting == ByGameVersion) { Version lv(leftPack.versions.at(0).minecraft); Version rv(rightPack.versions.at(0).minecraft); return lv < rv; } else if (currentSorting == ByName) { return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; } // Invalid sorting set, somehow... qWarning() << "Invalid sorting set!"; return true; } } // namespace Atl PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui0000644000175100017510000000321615144136757030332 0ustar runnerrunner AtlOptionalModDialog 0 0 550 310 Select Mods To Install Install true true Use Share Code Select Recommended Clear All ModListView QTreeView
    ui/widgets/ModListView.h
    PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp0000644000175100017510000002726015144136757030504 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AtlOptionalModDialog.h" #include "ui_AtlOptionalModDialog.h" #include #include #include "Application.h" #include "BuildConfig.h" #include "Json.h" #include "modplatform/atlauncher/ATLShareCode.h" #include "net/ApiDownload.h" AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, QList mods) : QAbstractListModel(parent), m_version(version), m_mods(mods) { // fill mod index for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); m_index[mod.name] = i; } // set initial state for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); m_selection[mod.name] = false; setMod(mod, i, mod.selected, false); } } QList AtlOptionalModListModel::getResult() { QList result; for (const auto& mod : m_mods) { if (m_selection[mod.name]) { result.push_back(mod.name); } } return result; } int AtlOptionalModListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_mods.size(); } int AtlOptionalModListModel::columnCount(const QModelIndex& parent) const { // Enabled, Name, Description return parent.isValid() ? 0 : 3; } QVariant AtlOptionalModListModel::data(const QModelIndex& index, int role) const { auto row = index.row(); auto mod = m_mods.at(row); if (role == Qt::DisplayRole) { if (index.column() == NameColumn) { return mod.name; } if (index.column() == DescriptionColumn) { return mod.description; } } else if (role == Qt::ToolTipRole) { if (index.column() == DescriptionColumn) { return mod.description; } } else if (role == Qt::ForegroundRole) { if (!mod.colour.isEmpty() && m_version.colours.contains(mod.colour)) { return QColor(QString("#%1").arg(m_version.colours[mod.colour])); } } else if (role == Qt::CheckStateRole) { if (index.column() == EnabledColumn) { return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked; } } return {}; } bool AtlOptionalModListModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { if (role == Qt::CheckStateRole) { auto row = index.row(); auto mod = m_mods.at(row); toggleMod(mod, row); return true; } return false; } QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { switch (section) { case EnabledColumn: return QString(); case NameColumn: return QString("Name"); case DescriptionColumn: return QString("Description"); } } return {}; } Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex& index) const { auto flags = QAbstractListModel::flags(index); if (index.isValid() && index.column() == EnabledColumn) { flags |= Qt::ItemIsUserCheckable; } return flags; } void AtlOptionalModListModel::useShareCode(const QString& code) { m_jobPtr.reset(new NetJob("Atl::Request", APPLICATION->network())); auto url = QString(BuildConfig.ATL_API_BASE_URL + "share-codes/" + code); m_jobPtr->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), m_response)); connect(m_jobPtr.get(), &NetJob::succeeded, this, &AtlOptionalModListModel::shareCodeSuccess); connect(m_jobPtr.get(), &NetJob::failed, this, &AtlOptionalModListModel::shareCodeFailure); m_jobPtr->start(); } void AtlOptionalModListModel::shareCodeSuccess() { m_jobPtr.reset(); QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*m_response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATL at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *m_response; return; } auto obj = doc.object(); ATLauncher::ShareCodeResponse response; try { ATLauncher::loadShareCodeResponse(response, obj); } catch (const JSONValidationError& e) { qDebug() << QString::fromUtf8(*m_response); qWarning() << "Error while reading response from ATLauncher:" << e.cause(); return; } if (response.error) { // fixme: plumb in an error message qWarning() << "ATLauncher API Response Error" << response.message; return; } // FIXME: verify pack and version, error if not matching. // Clear the current selection for (const auto& mod : m_mods) { m_selection[mod.name] = false; } // Make the selections, as per the share code. for (const auto& mod : response.data.mods) { m_selection[mod.name] = mod.selected; } emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); } void AtlOptionalModListModel::shareCodeFailure([[maybe_unused]] const QString& reason) { m_jobPtr.reset(); // fixme: plumb in an error message } void AtlOptionalModListModel::selectRecommended() { for (const auto& mod : m_mods) { m_selection[mod.name] = mod.recommended; } emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); } void AtlOptionalModListModel::clearAll() { for (const auto& mod : m_mods) { m_selection[mod.name] = false; } emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); } void AtlOptionalModListModel::toggleMod(const ATLauncher::VersionMod& mod, int index) { auto enable = !m_selection[mod.name]; // If there is a warning for the mod, display that first (if we would be enabling the mod) if (enable && !mod.warning.isEmpty() && m_version.warnings.contains(mod.warning)) { auto message = QString("%1

    %2").arg(m_version.warnings[mod.warning], tr("Are you sure that you want to enable this mod?")); // fixme: avoid casting here auto result = QMessageBox::warning((QWidget*)this->parent(), tr("Warning"), message, QMessageBox::Yes | QMessageBox::No); if (result != QMessageBox::Yes) { return; } } setMod(mod, index, enable); } void AtlOptionalModListModel::setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit) { if (m_selection[mod.name] == enable) return; m_selection[mod.name] = enable; // disable other mods in the group, if applicable if (enable && !mod.group.isEmpty()) { for (int i = 0; i < m_mods.size(); i++) { if (index == i) continue; auto other = m_mods.at(i); if (mod.group == other.group) { setMod(other, i, false, shouldEmit); } } } for (const auto& dependencyName : mod.depends) { auto dependencyIndex = m_index[dependencyName]; auto dependencyMod = m_mods.at(dependencyIndex); // enable/disable dependencies if (enable) { setMod(dependencyMod, dependencyIndex, true, shouldEmit); } // if the dependency is 'effectively hidden', then track which mods // depend on it - so we can efficiently disable it when no more dependents // depend on it. auto dependents = m_dependents[dependencyName]; if (enable) { dependents.append(mod.name); } else { dependents.removeAll(mod.name); // if there are no longer any dependents, let's disable the mod if (dependencyMod.effectively_hidden && dependents.isEmpty()) { setMod(dependencyMod, dependencyIndex, false, shouldEmit); } } } // disable mods that depend on this one, if disabling if (!enable) { auto dependents = m_dependents[mod.name]; for (const auto& dependencyName : dependents) { auto dependencyIndex = m_index[dependencyName]; auto dependencyMod = m_mods.at(dependencyIndex); setMod(dependencyMod, dependencyIndex, false, shouldEmit); } } if (shouldEmit) { emit dataChanged(AtlOptionalModListModel::index(index, EnabledColumn), AtlOptionalModListModel::index(index, EnabledColumn)); } } AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QList mods) : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { ui->setupUi(this); listModel = new AtlOptionalModListModel(this, version, mods); ui->treeView->setModel(listModel); ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); ui->treeView->header()->setSectionResizeMode(AtlOptionalModListModel::NameColumn, QHeaderView::ResizeToContents); ui->treeView->header()->setSectionResizeMode(AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); connect(ui->shareCodeButton, &QPushButton::clicked, this, &AtlOptionalModDialog::useShareCode); connect(ui->selectRecommendedButton, &QPushButton::clicked, listModel, &AtlOptionalModListModel::selectRecommended); connect(ui->clearAllButton, &QPushButton::clicked, listModel, &AtlOptionalModListModel::clearAll); connect(ui->installButton, &QPushButton::clicked, this, &QDialog::accept); } AtlOptionalModDialog::~AtlOptionalModDialog() { delete ui; } void AtlOptionalModDialog::useShareCode() { bool ok; auto shareCode = QInputDialog::getText(this, tr("Select a share code"), tr("Share code:"), QLineEdit::Normal, "", &ok); if (!ok) { // If the user cancels the dialog, we don't need to show any error dialogs. return; } if (shareCode.isEmpty()) { QMessageBox box; box.setIcon(QMessageBox::Warning); box.setText(tr("No share code specified!")); box.exec(); return; } listModel->useShareCode(shareCode); } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp0000644000175100017510000001471115144136757027030 0ustar runnerrunner/* * Copyright 2020-2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AtlListModel.h" #include #include #include #include "net/ApiDownload.h" #include "ui/widgets/ProjectItem.h" namespace Atl { ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} ListModel::~ListModel() {} int ListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : modpacks.size(); } int ListModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } ATLauncher::IndexedPack pack = modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { if (pack.description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack.description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } return pack.description; } case Qt::DecorationRole: { if (m_logoMap.contains(pack.safeName)) { return (m_logoMap.value(pack.safeName)); } auto icon = QIcon::fromTheme("atlauncher-placeholder"); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1").arg(pack.safeName); ((ListModel*)this)->requestLogo(pack.safeName, url); return icon; } case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } case Qt::DisplayRole: return pack.name; case Qt::SizeHintRole: return QSize(0, 58); // Custom data case UserDataTypes::TITLE: return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; case UserDataTypes::INSTALLED: return false; default: break; } return {}; } void ListModel::request() { beginResetModel(); modpacks.clear(); endResetModel(); auto netJob = makeShared("Atl::Request", APPLICATION->network()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response)); jobPtr = netJob; jobPtr->start(); connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); } void ListModel::requestFinished() { jobPtr.reset(); QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATL at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } QList newList; auto packs = doc.array(); for (auto packRaw : packs) { auto packObj = packRaw.toObject(); ATLauncher::IndexedPack pack; try { ATLauncher::loadIndexedPack(pack, packObj); } catch (const JSONValidationError& e) { qDebug() << QString::fromUtf8(*response); qWarning() << "Error while reading pack manifest from ATLauncher:" << e.cause(); return; } // ignore packs without a published version if (pack.versions.length() == 0) continue; // only display public packs (for now) if (pack.type != ATLauncher::PackType::Public) continue; // ignore "system" packs (Vanilla, Vanilla with Forge, etc) if (pack.system) continue; newList.append(pack); } beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); modpacks.append(newList); endInsertRows(); } void ListModel::requestFailed(QString reason) { jobPtr.reset(); } void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo, logoUrl); } } void ListModel::logoFailed(QString logo) { m_failedLogos.append(logo); m_loadingLogos.removeAll(logo); } void ListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); for (int i = 0; i < modpacks.size(); i++) { if (modpacks[i].safeName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } void ListModel::requestLogo(QString file, QString url) { if (m_loadingLogos.contains(file) || m_failedLogos.contains(file)) { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file)); auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] { job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); if (waitingCallbacks.contains(file)) { waitingCallbacks.value(file)(fullPath); } }); connect(job, &NetJob::failed, this, [this, file, job] { job->deleteLater(); emit logoFailed(file); }); job->start(); m_loadingLogos.append(file); } } // namespace Atl PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h0000644000175100017510000000347715144136757026504 0ustar runnerrunner/* * Copyright 2020-2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "net/NetJob.h" namespace Atl { using LogoMap = QMap; using LogoCallback = std::function; class ListModel : public QAbstractListModel { Q_OBJECT public: ListModel(QObject* parent); virtual ~ListModel(); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& index, int role) const override; void request(); void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); private slots: void requestFinished(); void requestFailed(QString reason); void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); private: void requestLogo(QString file, QString url); private: QList modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; LogoMap m_logoMap; QMap waitingCallbacks; NetJob::Ptr jobPtr; std::shared_ptr response = std::make_shared(); }; } // namespace Atl PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlPage.h0000644000175100017510000000604615144136757025457 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "AtlFilterModel.h" #include "AtlListModel.h" #include #include #include "ui/pages/modplatform/ModpackProviderBasePage.h" namespace Ui { class AtlPage; } class NewInstanceDialog; class AtlPage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: explicit AtlPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~AtlPage(); virtual QString displayName() const override { return "ATLauncher"; } virtual QIcon icon() const override { return QIcon::fromTheme("atlauncher"); } virtual QString id() const override { return "atl"; } virtual QString helpPage() const override { return "ATL-platform"; } virtual bool shouldDisplay() const override; void retranslate() override; void openedImpl() override; /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ virtual QString getSerachTerm() const override; private: void suggestCurrent(); private slots: void triggerSearch(); void onSortingSelectionChanged(QString data); void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(QString data); private: Ui::AtlPage* ui = nullptr; NewInstanceDialog* dialog = nullptr; Atl::ListModel* listModel = nullptr; Atl::FilterModel* filterModel = nullptr; ATLauncher::IndexedPack selected; QString selectedVersion; bool initialized = false; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui0000644000175100017510000000503115144136757025636 0ustar runnerrunner AtlPage 0 0 837 685 true Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug. Qt::AlignCenter true Search and filter... true true 96 48 true true Version selected: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter searchEdit packView packDescription sortByBox versionSelectionBox PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h0000644000175100017510000000417715144136757032143 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "modplatform/atlauncher/ATLPackInstallTask.h" class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInteractionSupport { Q_OBJECT public: AtlUserInteractionSupportImpl(QWidget* parent); virtual ~AtlUserInteractionSupportImpl() = default; private: QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override; std::optional> chooseOptionalMods(const ATLauncher::PackVersion& version, QList mods) override; void displayMessage(QString message) override; private: QWidget* m_parent; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp0000644000175100017510000001260215144136757026005 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Philip T * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AtlPage.h" #include "ui/widgets/ProjectItem.h" #include "ui_AtlPage.h" #include "BuildConfig.h" #include "StringUtils.h" #include "AtlUserInteractionSupportImpl.h" #include "modplatform/atlauncher/ATLPackInstallTask.h" #include "ui/dialogs/NewInstanceDialog.h" #include AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) { ui->setupUi(this); filterModel = new Atl::FilterModel(this); listModel = new Atl::ListModel(this); filterModel->setSourceModel(listModel); ui->packView->setModel(filterModel); ui->packView->setSortingEnabled(true); ui->packView->header()->hide(); ui->packView->setIndentation(0); ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); for (int i = 0; i < filterModel->getAvailableSortings().size(); i++) { ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); } ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); connect(ui->searchEdit, &QLineEdit::textChanged, this, &AtlPage::triggerSearch); connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); ui->packView->setItemDelegate(new ProjectItemDelegate(this)); } AtlPage::~AtlPage() { delete ui; } bool AtlPage::shouldDisplay() const { return true; } void AtlPage::retranslate() { ui->retranslateUi(this); } void AtlPage::openedImpl() { if (!initialized) { listModel->request(); initialized = true; } suggestCurrent(); } void AtlPage::suggestCurrent() { if (!isOpened) { return; } if (selectedVersion.isEmpty()) { dialog->setSuggestedPack(); return; } auto uiSupport = new AtlUserInteractionSupportImpl(this); dialog->setSuggestedPack(selected.name, selectedVersion, new ATLauncher::PackInstallTask(uiSupport, selected.name, selectedVersion)); auto editedLogoName = "atl_" + selected.safeName; auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1").arg(selected.safeName); listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } void AtlPage::triggerSearch() { filterModel->setSearchTerm(ui->searchEdit->text()); } void AtlPage::onSortingSelectionChanged(QString sort) { auto toSet = filterModel->getAvailableSortings().value(sort); filterModel->setSorting(toSet); } void AtlPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex second) { ui->versionSelectionBox->clear(); if (!first.isValid()) { if (isOpened) { dialog->setSuggestedPack(); } return; } QVariant raw = filterModel->data(first, Qt::UserRole); Q_ASSERT(raw.canConvert()); selected = raw.value(); ui->packDescription->setHtml(StringUtils::htmlListPatch(selected.description.replace("\n", "
    "))); for (const auto& version : selected.versions) { ui->versionSelectionBox->addItem(version.version); } suggestCurrent(); } void AtlPage::onVersionSelectionChanged(QString version) { if (version.isNull() || version.isEmpty()) { selectedVersion = ""; return; } selectedVersion = version; suggestCurrent(); } void AtlPage::setSearchTerm(QString term) { ui->searchEdit->setText(term); } QString AtlPage::getSerachTerm() const { return ui->searchEdit->text(); } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h0000644000175100017510000000700415144136757030143 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "modplatform/atlauncher/ATLPackManifest.h" #include "net/NetJob.h" namespace Ui { class AtlOptionalModDialog; } class AtlOptionalModListModel : public QAbstractListModel { Q_OBJECT public: enum Columns { EnabledColumn = 0, NameColumn, DescriptionColumn, }; AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, QList mods); QList getResult(); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& index, int role) const override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; void useShareCode(const QString& code); public slots: void shareCodeSuccess(); void shareCodeFailure(const QString& reason); void selectRecommended(); void clearAll(); private: void toggleMod(const ATLauncher::VersionMod& mod, int index); void setMod(const ATLauncher::VersionMod& mod, int index, bool enable, bool shouldEmit = true); private: NetJob::Ptr m_jobPtr; std::shared_ptr m_response = std::make_shared(); ATLauncher::PackVersion m_version; QList m_mods; QMap m_selection; QMap m_index; QMap> m_dependents; }; class AtlOptionalModDialog : public QDialog { Q_OBJECT public: AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QList mods); ~AtlOptionalModDialog() override; QList getResult() { return listModel->getResult(); } void useShareCode(); private: Ui::AtlOptionalModDialog* ui; AtlOptionalModListModel* listModel; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp0000644000175100017510000000740015144136757032466 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AtlUserInteractionSupportImpl.h" #include #include "AtlOptionalModDialog.h" #include "ui/dialogs/VersionSelectDialog.h" AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget* parent) : m_parent(parent) {} std::optional> AtlUserInteractionSupportImpl::chooseOptionalMods(const ATLauncher::PackVersion& version, QList mods) { AtlOptionalModDialog optionalModDialog(m_parent, version, mods); auto result = optionalModDialog.exec(); if (result == QDialog::Rejected) { return {}; } return optionalModDialog.getResult(); } QString AtlUserInteractionSupportImpl::chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) { VersionSelectDialog vselect(vlist.get(), "Choose Version", m_parent, false); if (minecraftVersion != nullptr) { vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); } else { vselect.setEmptyString(tr("No versions are currently available")); } vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!")); // select recommended build for (int i = 0; i < vlist->versions().size(); i++) { auto version = vlist->versions().at(i); auto reqs = version->requiredSet(); // filter by minecraft version, if the loader depends on a certain version. if (minecraftVersion != nullptr) { auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require& req) { return req.uid == "net.minecraft"; }); if (iter == reqs.end()) continue; if (iter->equalsVersion != minecraftVersion) continue; } // first recommended build we find, we use. if (version->isRecommended()) { vselect.setCurrentVersion(version->descriptor()); break; } } vselect.exec(); return vselect.selectedVersion()->descriptor(); } void AtlUserInteractionSupportImpl::displayMessage(QString message) { QMessageBox::information(m_parent, tr("Installing"), message); } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h0000644000175100017510000000263015144136757027004 0ustar runnerrunner/* * Copyright 2020-2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include namespace Atl { class FilterModel : public QSortFilterProxyModel { Q_OBJECT public: FilterModel(QObject* parent = Q_NULLPTR); enum Sorting { ByPopularity, ByGameVersion, ByName, }; const QMap getAvailableSortings(); QString translateCurrentSorting(); void setSorting(Sorting sorting); Sorting getCurrentSorting(); void setSearchTerm(QString term); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; private: QMap sortings; Sorting currentSorting; QString searchTerm; }; } // namespace Atl PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ShaderPackModel.cpp0000644000175100017510000000262715144136757025336 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ShaderPackModel.h" #include namespace ResourceDownload { ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) {} /******** Make data requests ********/ ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); return { ModPlatform::ResourceType::ShaderPack, m_next_search_offset, m_search_term, sort }; } ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; return { pack, {}, {}, ModPlatform::ResourceType::ShaderPack }; } ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry) { auto pack = m_packs[entry.row()]; return { pack }; } void ShaderPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) { if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) { return; } setSearchTerm(term); m_current_sort_index = sort; refresh(); } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ModPage.h0000644000175100017510000000441115144136757023322 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ModModel.h" #include "ui/pages/modplatform/ResourcePage.h" #include "ui/widgets/ModFilterWidget.h" namespace Ui { class ResourcePage; } namespace ResourceDownload { class ModDownloadDialog; /* This page handles most logic related to browsing and selecting mods to download. */ class ModPage : public ResourcePage { Q_OBJECT public: template static T* create(ModDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); auto filter_widget = page->createFilterWidget(); page->setFilterWidget(filter_widget); model->setFilter(page->getFilter()); connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'mod' inline QString resourcesString() const override { return tr("mods"); } //: The singular version of 'mods' inline QString resourceString() const override { return tr("mod"); } QMap urlHandlers() const override; void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; virtual std::unique_ptr createFilterWidget() = 0; bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } void setFilterWidget(std::unique_ptr&); protected: ModPage(ModDownloadDialog* dialog, BaseInstance& instance); virtual void prepareProviderCategories() {}; protected slots: virtual void filterMods(); void triggerSearch() override; protected: std::unique_ptr m_filter_widget; std::shared_ptr m_filter; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ImportPage.ui0000644000175100017510000000555615144136757024256 0ustar runnerrunner ImportPage 0 0 546 405 http:// Browse The following file types are implemented (both for local files and URLs): Qt::AlignCenter - CurseForge modpacks (ZIP / curseforge:// URL) Qt::AlignCenter - Modrinth modpacks (ZIP and mrpack) Qt::AlignCenter - Prism Launcher, PolyMC or MultiMC exported instances (ZIP) Qt::AlignCenter - Technic modpacks (ZIP) Qt::AlignCenter Qt::Vertical 20 40 Local file or link to a direct download: PrismLauncher-10.0.5/launcher/ui/pages/modplatform/CustomPage.h0000644000175100017510000000563215144136757024063 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "BaseVersion.h" #include "tasks/Task.h" #include "ui/pages/BasePage.h" namespace Ui { class CustomPage; } class NewInstanceDialog; class CustomPage : public QWidget, public BasePage { Q_OBJECT public: explicit CustomPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~CustomPage(); virtual QString displayName() const override { return tr("Custom"); } virtual QIcon icon() const override { return QIcon::fromTheme("minecraft"); } virtual QString id() const override { return "vanilla"; } virtual QString helpPage() const override { return "Vanilla-platform"; } virtual bool shouldDisplay() const override; void retranslate() override; void openedImpl() override; BaseVersion::Ptr selectedVersion() const; BaseVersion::Ptr selectedLoaderVersion() const; QString selectedLoader() const; public slots: void setSelectedVersion(BaseVersion::Ptr version); void setSelectedLoaderVersion(BaseVersion::Ptr version); private slots: void filterChanged(); void loaderFilterChanged(); private: void refresh(); void loaderRefresh(); void suggestCurrent(); private: bool initialized = false; NewInstanceDialog* dialog = nullptr; Ui::CustomPage* ui = nullptr; bool m_versionSetByUser = false; BaseVersion::Ptr m_selectedVersion; BaseVersion::Ptr m_selectedLoaderVersion; QString m_selectedLoader; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/OptionalModDialog.ui0000644000175100017510000000541115144136757025542 0ustar runnerrunner OptionalModDialog 0 0 550 310 Select Optional Mods Qt::IgnoreAction true Select All Deselect All Qt::Horizontal 40 20 Unchecked mods will be disabled. QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() OptionalModDialog accept() 274 284 274 154 buttonBox rejected() OptionalModDialog reject() 274 284 274 154 PrismLauncher-10.0.5/launcher/ui/pages/modplatform/TexturePackModel.cpp0000644000175100017510000000571115144136757025565 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "TexturePackModel.h" #include "Application.h" #include "meta/Index.h" #include "meta/Version.h" static std::list s_availableVersions = {}; namespace ResourceDownload { TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase) : ResourcePackResourceModel(inst, api, debugName, metaEntryBase), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft")) { if (!m_version_list->isLoaded()) { qDebug() << "Loading version list..."; m_task = m_version_list->getLoadTask(); if (!m_task->isRunning()) m_task->start(); } } void waitOnVersionListLoad(Meta::VersionList::Ptr version_list) { QEventLoop load_version_list_loop; QTimer time_limit_for_list_load; time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer); time_limit_for_list_load.setSingleShot(true); time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit); time_limit_for_list_load.start(4000); auto task = version_list->getLoadTask(); QObject::connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit); if (!task->isRunning()) task->start(); load_version_list_loop.exec(); if (time_limit_for_list_load.isActive()) time_limit_for_list_load.stop(); } ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments() { if (s_availableVersions.empty()) waitOnVersionListLoad(m_version_list); auto args = ResourcePackResourceModel::createSearchArguments(); if (!m_version_list->isLoaded()) { qCritical() << "The version list could not be loaded. Falling back to showing all entries."; return args; } if (s_availableVersions.empty()) { for (auto&& version : m_version_list->versions()) { // FIXME: This duplicates the logic in meta for the 'texturepacks' trait. However, we don't have access to that // information from the index file alone. Also, downloading every version's file isn't a very good idea. if (auto ver = version->toComparableVersion(); ver <= maximumTexturePackVersion()) s_availableVersions.push_back(ver); } } Q_ASSERT(!s_availableVersions.empty()); args.versions = s_availableVersions; return args; } ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto args = ResourcePackResourceModel::createVersionsArguments(entry); args.resourceType = ModPlatform::ResourceType::TexturePack; if (!m_version_list->isLoaded()) { qCritical() << "The version list could not be loaded. Falling back to showing all entries."; return args; } args.mcVersions = s_availableVersions; return args; } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/DataPackModel.h0000644000175100017510000000230315144136757024435 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // SPDX-FileCopyrightText: 2023 TheKodeToad // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include "BaseInstance.h" #include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ResourceModel.h" class Version; namespace ResourceDownload { class DataPackResourceModel : public ResourceModel { Q_OBJECT public: DataPackResourceModel(BaseInstance const&, ResourceAPI*, QString, QString); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); [[nodiscard]] QString debugName() const override { return m_debugName; } [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; private: QString m_debugName; QString m_metaEntryBase; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourcePackModel.h0000644000175100017510000000223715144136757025361 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include "BaseInstance.h" #include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ResourceModel.h" class Version; namespace ResourceDownload { class ResourcePackResourceModel : public ResourceModel { Q_OBJECT public: ResourcePackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); [[nodiscard]] QString debugName() const override { return m_debugName; } [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; private: QString m_debugName; QString m_metaEntryBase; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/DataPackPage.h0000644000175100017510000000303115144136757024250 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // SPDX-FileCopyrightText: 2023 TheKodeToad // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "ui/pages/modplatform/DataPackModel.h" #include "ui/pages/modplatform/ResourcePage.h" namespace Ui { class ResourcePage; } namespace ResourceDownload { class DataPackDownloadDialog; class DataPackResourcePage : public ResourcePage { Q_OBJECT public: template static T* create(DataPackDownloadDialog* dialog, BaseInstance& instance) { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } //: The plural version of 'data pack' inline QString resourcesString() const override { return tr("data packs"); } //: The singular version of 'data packs' inline QString resourceString() const override { return tr("data pack"); } bool supportsFiltering() const override { return false; }; QMap urlHandlers() const override; protected: DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance); protected slots: void triggerSearch() override; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/0000755000175100017510000000000015144136757022721 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlamePage.h0000644000175100017510000000652615144136757024724 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" namespace Ui { class FlamePage; } class NewInstanceDialog; namespace Flame { class ListModel; } class FlamePage : public QWidget, public ModpackProviderBasePage { Q_OBJECT public: explicit FlamePage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~FlamePage(); virtual QString displayName() const override { return "CurseForge"; } virtual QIcon icon() const override { return QIcon::fromTheme("flame"); } virtual QString id() const override { return "flame"; } virtual QString helpPage() const override { return "Flame-platform"; } virtual bool shouldDisplay() const override; void retranslate() override; void updateUi(); void openedImpl() override; bool eventFilter(QObject* watched, QEvent* event) override; /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ virtual QString getSerachTerm() const override; private: void suggestCurrent(); private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void createFilterWidget(); private: Ui::FlamePage* m_ui = nullptr; NewInstanceDialog* m_dialog = nullptr; Flame::ListModel* m_listModel = nullptr; ModPlatform::IndexedPack::Ptr m_current; int m_selected_version_index = -1; ProgressWidget m_fetch_progress; // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; std::unique_ptr m_filterWidget; Task::Ptr m_categoriesTask; Task::Ptr m_job; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlameResourceModels.h0000644000175100017510000000155615144136757027001 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "ui/pages/modplatform/ModModel.h" #include "ui/pages/modplatform/flame/FlameResourcePages.h" namespace ResourceDownload { class FlameTexturePackModel : public TexturePackResourceModel { Q_OBJECT public: FlameTexturePackModel(const BaseInstance&); ~FlameTexturePackModel() override = default; bool optedOut(const ModPlatform::IndexedVersion& ver) const override; private: QString debugName() const override { return Flame::debugName() + " (Model)"; } QString metaEntryBase() const override { return Flame::metaEntryBase(); } ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlamePage.cpp0000644000175100017510000002764015144136757025257 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "FlamePage.h" #include "Version.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/widgets/ModFilterWidget.h" #include "ui_FlamePage.h" #include #include #include "FlameModel.h" #include "InstanceImportTask.h" #include "StringUtils.h" #include "modplatform/flame/FlameAPI.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/widgets/ProjectItem.h" static FlameAPI api; FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), m_ui(new Ui::FlamePage), m_dialog(dialog), m_fetch_progress(this, false) { m_ui->setupUi(this); m_ui->searchEdit->installEventFilter(this); m_listModel = new Flame::ListModel(this); m_ui->packView->setModel(m_listModel); m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); connect(&m_search_timer, &QTimer::timeout, this, &FlamePage::triggerSearch); m_fetch_progress.hideIfInactive(true); m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); m_ui->verticalLayout->insertWidget(2, &m_fetch_progress); // index is used to set the sorting with the curseforge api m_ui->sortByBox->addItem(tr("Sort by Featured")); m_ui->sortByBox->addItem(tr("Sort by Popularity")); m_ui->sortByBox->addItem(tr("Sort by Last Updated")); m_ui->sortByBox->addItem(tr("Sort by Name")); m_ui->sortByBox->addItem(tr("Sort by Author")); m_ui->sortByBox->addItem(tr("Sort by Total Downloads")); connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlamePage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlamePage::onVersionSelectionChanged); m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); m_ui->packDescription->setMetaEntry("FlamePacks"); createFilterWidget(); } FlamePage::~FlamePage() { delete m_ui; } bool FlamePage::eventFilter(QObject* watched, QEvent* event) { if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Return) { triggerSearch(); keyEvent->accept(); return true; } else { if (m_search_timer.isActive()) m_search_timer.stop(); m_search_timer.start(350); } } return QWidget::eventFilter(watched, event); } bool FlamePage::shouldDisplay() const { return true; } void FlamePage::retranslate() { m_ui->retranslateUi(this); } void FlamePage::openedImpl() { suggestCurrent(); triggerSearch(); } void FlamePage::triggerSearch() { m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); m_ui->packView->clearSelection(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); bool filterChanged = m_filterWidget->changed(); m_listModel->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); m_fetch_progress.watch(m_listModel->activeSearchJob().get()); } void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { m_ui->versionSelectionBox->clear(); if (!curr.isValid()) { if (isOpened) { m_dialog->setSuggestedPack(); } return; } m_current = m_listModel->data(curr, Qt::UserRole).value(); if (!m_current->versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading flame modpack versions"; ResourceAPI::Callback > callbacks{}; auto addonId = m_current->addonId; // Use default if no callbacks are set callbacks.on_succeed = [this, curr, addonId](auto& doc) { if (addonId != m_current->addonId) { return; // wrong request } m_current->versions = doc; m_current->versionsLoaded = true; auto pred = [this](const ModPlatform::IndexedVersion& v) { if (auto filter = m_filterWidget->getFilter()) return !filter->checkModpackFilters(v); return false; }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) m_current->versions.removeIf(pred); #else for (auto it = m_current->versions.begin(); it != m_current->versions.end();) if (pred(*it)) it = m_current->versions.erase(it); else ++it; #endif for (auto version : m_current->versions) { m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.downloadUrl)); } QVariant current_updated; current_updated.setValue(m_current); if (!m_listModel->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache versions for the current pack!"; // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution. if (m_current->versionsLoaded && m_ui->versionSelectionBox->count() < 1) { m_ui->versionSelectionBox->addItem(tr("No version is available!"), -1); } suggestCurrent(); }; callbacks.on_fail = [this](QString reason, int) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }; auto netJob = api.getProjectVersions({ m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); m_job = netJob; netJob->start(); } else { for (auto version : m_current->versions) { m_ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); } suggestCurrent(); } // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution. if (m_current->versionsLoaded && m_ui->versionSelectionBox->count() < 1) { m_ui->versionSelectionBox->addItem(tr("No version is available!"), -1); } updateUi(); } void FlamePage::suggestCurrent() { if (!isOpened) { return; } if (m_selected_version_index == -1) { m_dialog->setSuggestedPack(); return; } auto version = m_current->versions.at(m_selected_version_index); QMap extra_info; extra_info.insert("pack_id", m_current->addonId.toString()); extra_info.insert("pack_version_id", version.fileId.toString()); m_dialog->setSuggestedPack(m_current->name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); QString editedLogoName = "curseforge_" + m_current->logoName; m_listModel->getLogo(m_current->logoName, m_current->logoUrl, [this, editedLogoName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } void FlamePage::onVersionSelectionChanged(int index) { bool is_blocked = false; m_ui->versionSelectionBox->itemData(index).toInt(&is_blocked); if (index == -1 || is_blocked) { m_selected_version_index = -1; return; } m_selected_version_index = index; Q_ASSERT(m_current->versions.at(m_selected_version_index).downloadUrl == m_ui->versionSelectionBox->currentData().toString()); suggestCurrent(); } void FlamePage::updateUi() { QString text = ""; QString name = m_current->name; if (m_current->websiteUrl.isEmpty()) text = name; else text = "websiteUrl + "\">" + name + ""; if (!m_current->authors.empty()) { auto authorToStr = [](ModPlatform::ModpackAuthor& author) { if (author.url.isEmpty()) { return author.name; } return QString("%2").arg(author.url, author.name); }; QStringList authorStrs; for (auto& author : m_current->authors) { authorStrs.push_back(authorToStr(author)); } text += "
    " + tr(" by ") + authorStrs.join(", "); } if (m_current->extraDataLoaded) { if (!m_current->extraData.issuesUrl.isEmpty() || !m_current->extraData.sourceUrl.isEmpty() || !m_current->extraData.wikiUrl.isEmpty()) { text += "

    " + tr("External links:") + "
    "; } if (!m_current->extraData.issuesUrl.isEmpty()) text += "- " + tr("Issues: %1").arg(m_current->extraData.issuesUrl) + "
    "; if (!m_current->extraData.wikiUrl.isEmpty()) text += "- " + tr("Wiki: %1").arg(m_current->extraData.wikiUrl) + "
    "; if (!m_current->extraData.sourceUrl.isEmpty()) text += "- " + tr("Source code: %1").arg(m_current->extraData.sourceUrl) + "
    "; } text += "
    "; text += api.getModDescription(m_current->addonId.toInt()).toUtf8(); m_ui->packDescription->setHtml(StringUtils::htmlListPatch(text + m_current->description)); m_ui->packDescription->flush(); } QString FlamePage::getSerachTerm() const { return m_ui->searchEdit->text(); } void FlamePage::setSearchTerm(QString term) { m_ui->searchEdit->setText(term); } void FlamePage::createFilterWidget() { auto widget = ModFilterWidget::create(nullptr, false); m_filterWidget.swap(widget); auto old = m_ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it if (old) { delete old; } connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::Modpack); connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = FlameAPI::loadModCategories(response); m_filterWidget->setCategories(categories); }); m_categoriesTask->start(); } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp0000644000175100017510000002437015144136757027147 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "FlameResourcePages.h" #include #include #include "modplatform/flame/FlameAPI.h" #include "ui_ResourcePage.h" #include "FlameResourceModels.h" #include "ui/dialogs/ResourceDownloadDialog.h" namespace ResourceDownload { FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { m_model = new ModModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameModPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } void FlameModPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { QString query = url.query(QUrl::FullyDecoded); if (query.startsWith("remoteUrl=")) { // attempt to resolve url from warning page query.remove(0, 10); ModPage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary return; } } ModPage::openUrl(url); } FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { m_model = new ResourcePackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameResourcePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameResourcePackPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } void FlameResourcePackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { QString query = url.query(QUrl::FullyDecoded); if (query.startsWith("remoteUrl=")) { // attempt to resolve url from warning page query.remove(0, 10); ResourcePackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary return; } } ResourcePackResourcePage::openUrl(url); } FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : TexturePackResourcePage(dialog, instance) { m_model = new FlameTexturePackModel(instance); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameTexturePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameTexturePackPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } void FlameTexturePackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { QString query = url.query(QUrl::FullyDecoded); if (query.startsWith("remoteUrl=")) { // attempt to resolve url from warning page query.remove(0, 10); ResourcePackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary return; } } TexturePackResourcePage::openUrl(url); } void FlameDataPackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { QString query = url.query(QUrl::FullyDecoded); if (query.startsWith("remoteUrl=")) { // attempt to resolve url from warning page query.remove(0, 10); DataPackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary return; } } DataPackResourcePage::openUrl(url); } FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ShaderPackResourcePage(dialog, instance) { m_model = new ShaderPackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameShaderPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameShaderPackPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) : DataPackResourcePage(dialog, instance) { m_model = new DataPackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameDataPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameDataPackPage::onSelectionChanged); connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameDataPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameDataPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); } void FlameShaderPackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { QString query = url.query(QUrl::FullyDecoded); if (query.startsWith("remoteUrl=")) { // attempt to resolve url from warning page query.remove(0, 10); ShaderPackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary return; } } ShaderPackResourcePage::openUrl(url); } // I don't know why, but doing this on the parent class makes it so that // other mod providers start loading before being selected, at least with // my Qt, so we need to implement this in every derived class... auto FlameModPage::shouldDisplay() const -> bool { return true; } auto FlameResourcePackPage::shouldDisplay() const -> bool { return true; } auto FlameTexturePackPage::shouldDisplay() const -> bool { return true; } auto FlameShaderPackPage::shouldDisplay() const -> bool { return true; } auto FlameDataPackPage::shouldDisplay() const -> bool { return true; } std::unique_ptr FlameModPage::createFilterWidget() { return ModFilterWidget::create(&static_cast(m_baseInstance), false); } void FlameModPage::prepareProviderCategories() { auto response = std::make_shared(); m_categoriesTask = FlameAPI::getModCategories(response); connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = FlameAPI::loadModCategories(response); m_filter_widget->setCategories(categories); }); m_categoriesTask->start(); }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlamePage.ui0000644000175100017510000000650515144136757025107 0ustar runnerrunner FlamePage 0 0 800 600 true Note: CurseForge allows creators to block access to third-party tools like Prism Launcher. As such, you may need to manually download some mods to be able to install a modpack. Qt::AlignCenter true Filter options Search and filter... 0 0 Qt::Horizontal Qt::ScrollBarAlwaysOff true 48 48 true true Version selected: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ProjectDescriptionPage QTextBrowser
    ui/widgets/ProjectDescriptionPage.h
    packView packDescription sortByBox versionSelectionBox
    PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlameModel.h0000644000175100017510000000441215144136757025100 0ustar runnerrunner#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include "ui/widgets/ModFilterWidget.h" namespace Flame { using LogoMap = QMap; using LogoCallback = std::function; class ListModel : public QAbstractListModel { Q_OBJECT public: ListModel(QObject* parent); virtual ~ListModel(); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QVariant data(const QModelIndex& index, int role) const override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; Qt::ItemFlags flags(const QModelIndex& index) const override; bool canFetchMore(const QModelIndex& parent) const override; void fetchMore(const QModelIndex& parent) override; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); bool hasActiveSearchJob() const { return m_jobPtr && m_jobPtr->isRunning(); } Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_jobPtr : nullptr; } private slots: void performPaginatedSearch(); void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); void searchRequestFinished(QList&); void searchRequestFailed(QString reason); void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr); private: void requestLogo(QString file, QString url); private: QList m_modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; LogoMap m_logoMap; QMap m_waitingCallbacks; QString m_currentSearchTerm; int m_currentSort = 0; std::shared_ptr m_filter; int m_nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } m_searchState = None; Task::Ptr m_jobPtr; }; } // namespace Flame PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlameResourcePages.h0000644000175100017510000001642615144136757026617 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "modplatform/ResourceAPI.h" #include "ui/pages/modplatform/ModPage.h" #include "ui/pages/modplatform/ResourcePackPage.h" #include "ui/pages/modplatform/ShaderPackPage.h" #include "ui/pages/modplatform/TexturePackPage.h" namespace ResourceDownload { namespace Flame { static inline QString displayName() { return "CurseForge"; } static inline QIcon icon() { return QIcon::fromTheme("flame"); } static inline QString id() { return "curseforge"; } static inline QString debugName() { return "Flame"; } static inline QString metaEntryBase() { return "FlameMods"; } } // namespace Flame class FlameModPage : public ModPage { Q_OBJECT public: static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance& instance) { return ModPage::create(dialog, instance); } FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~FlameModPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Flame::displayName(); } inline auto icon() const -> QIcon override { return Flame::icon(); } inline auto id() const -> QString override { return Flame::id(); } inline auto debugName() const -> QString override { return Flame::debugName(); } inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } inline auto helpPage() const -> QString override { return "Mod-platform"; } void openUrl(const QUrl& url) override; std::unique_ptr createFilterWidget() override; protected: virtual void prepareProviderCategories() override; private: Task::Ptr m_categoriesTask; }; class FlameResourcePackPage : public ResourcePackResourcePage { Q_OBJECT public: static FlameResourcePackPage* create(ResourcePackDownloadDialog* dialog, BaseInstance& instance) { return ResourcePackResourcePage::create(dialog, instance); } FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance); ~FlameResourcePackPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Flame::displayName(); } inline auto icon() const -> QIcon override { return Flame::icon(); } inline auto id() const -> QString override { return Flame::id(); } inline auto debugName() const -> QString override { return Flame::debugName(); } inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; class FlameTexturePackPage : public TexturePackResourcePage { Q_OBJECT public: static FlameTexturePackPage* create(TexturePackDownloadDialog* dialog, BaseInstance& instance) { return TexturePackResourcePage::create(dialog, instance); } FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance); ~FlameTexturePackPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Flame::displayName(); } inline auto icon() const -> QIcon override { return Flame::icon(); } inline auto id() const -> QString override { return Flame::id(); } inline auto debugName() const -> QString override { return Flame::debugName(); } inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; class FlameShaderPackPage : public ShaderPackResourcePage { Q_OBJECT public: static FlameShaderPackPage* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance) { return ShaderPackResourcePage::create(dialog, instance); } FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); ~FlameShaderPackPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Flame::displayName(); } inline auto icon() const -> QIcon override { return Flame::icon(); } inline auto id() const -> QString override { return Flame::id(); } inline auto debugName() const -> QString override { return Flame::debugName(); } inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; class FlameDataPackPage : public DataPackResourcePage { Q_OBJECT public: static FlameDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance) { return DataPackResourcePage::create(dialog, instance); } FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); ~FlameDataPackPage() override = default; bool shouldDisplay() const override; inline auto displayName() const -> QString override { return Flame::displayName(); } inline auto icon() const -> QIcon override { return Flame::icon(); } inline auto id() const -> QString override { return Flame::id(); } inline auto debugName() const -> QString override { return Flame::debugName(); } inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp0000644000175100017510000000302715144136757027327 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "FlameResourceModels.h" #include "Json.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FlameAPI.h" #include "ui/pages/modplatform/flame/FlameResourcePages.h" namespace ResourceDownload { static bool isOptedOut(const ModPlatform::IndexedVersion& ver) { return ver.downloadUrl.isEmpty(); } FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new FlameAPI, Flame::debugName(), Flame::metaEntryBase()) {} ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments() { auto args = TexturePackResourceModel::createSearchArguments(); auto profile = static_cast(m_base_instance).getPackProfile(); QString instance_minecraft_version = profile->getComponentVersion("net.minecraft"); // Bypass the texture pack logic, because we can't do multiple versions in the API query args.versions = { instance_minecraft_version }; return args; } ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(const QModelIndex& entry) { auto args = TexturePackResourceModel::createVersionsArguments(entry); // Bypass the texture pack logic, because we can't do multiple versions in the API query args.mcVersions = {}; return args; } bool FlameTexturePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const { return isOptedOut(ver); } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/flame/FlameModel.cpp0000644000175100017510000002020115144136757025425 0ustar runnerrunner#include "FlameModel.h" #include #include "Application.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameAPI.h" #include "ui/widgets/ProjectItem.h" #include "net/ApiDownload.h" #include #include #include namespace Flame { ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} ListModel::~ListModel() {} int ListModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_modpacks.size(); } int ListModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } auto pack = m_modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { if (pack->description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack->description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } return pack->description; } case Qt::DecorationRole: { if (m_logoMap.contains(pack->logoName)) { return (m_logoMap.value(pack->logoName)); } QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack->logoName, pack->logoUrl); return icon; } case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } case Qt::SizeHintRole: return QSize(0, 58); case UserDataTypes::TITLE: return pack->name; case UserDataTypes::DESCRIPTION: return pack->description; case UserDataTypes::INSTALLED: return false; default: break; } return QVariant(); } bool ListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) return false; m_modpacks[pos] = value.value(); return true; } void ListModel::logoLoaded(QString logo, QIcon out) { m_loadingLogos.removeAll(logo); m_logoMap.insert(logo, out); for (int i = 0; i < m_modpacks.size(); i++) { if (m_modpacks[i]->logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } } void ListModel::logoFailed(QString logo) { m_failedLogos.append(logo); m_loadingLogos.removeAll(logo); } void ListModel::requestLogo(QString logo, QString url) { if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; } MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo)); auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); job->setAskRetry(false); job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); if (m_waitingCallbacks.contains(logo)) { m_waitingCallbacks.value(logo)(fullPath); } }); connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); emit logoFailed(logo); }); job->start(); m_loadingLogos.append(logo); } void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo, logoUrl); } } Qt::ItemFlags ListModel::flags(const QModelIndex& index) const { return QAbstractListModel::flags(index); } bool ListModel::canFetchMore([[maybe_unused]] const QModelIndex& parent) const { return m_searchState == CanPossiblyFetchMore; } void ListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; if (m_nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; } performPaginatedSearch(); } void ListModel::performPaginatedSearch() { static const FlameAPI api; if (m_currentSearchTerm.startsWith("#")) { auto projectId = m_currentSearchTerm.mid(1); if (!projectId.isEmpty()) { ResourceAPI::Callback callbacks; callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); }; auto project = std::make_shared(); project->addonId = projectId; if (auto job = api.getProjectInfo({ project }, std::move(callbacks)); job) { m_jobPtr = job; m_jobPtr->start(); } return; } } ResourceAPI::SortingMethod sort{}; sort.index = m_currentSort + 1; ResourceAPI::Callback> callbacks{}; callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); }; auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }, std::move(callbacks)); m_jobPtr = netJob; m_jobPtr->start(); } void ListModel::searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged) { if (m_currentSearchTerm == term && m_currentSearchTerm.isNull() == term.isNull() && m_currentSort == sort && !filterChanged) { return; } m_currentSearchTerm = term; m_currentSort = sort; m_filter = filter; if (hasActiveSearchJob()) { m_jobPtr->abort(); m_searchState = ResetRequested; return; } beginResetModel(); m_modpacks.clear(); endResetModel(); m_searchState = None; m_nextSearchOffset = 0; performPaginatedSearch(); } void Flame::ListModel::searchRequestFinished(QList& newList) { if (hasActiveSearchJob()) return; if (newList.size() < 25) { m_searchState = Finished; } else { m_nextSearchOffset += 25; m_searchState = CanPossiblyFetchMore; } // When you have a Qt build with assertions turned on, proceeding here will abort the application if (newList.size() == 0) return; beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + newList.size() - 1); m_modpacks.append(newList); endInsertRows(); } void Flame::ListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack) { m_jobPtr.reset(); beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1); m_modpacks.append(pack); endInsertRows(); } void Flame::ListModel::searchRequestFailed(QString reason) { m_jobPtr.reset(); if (m_searchState == ResetRequested) { beginResetModel(); m_modpacks.clear(); endResetModel(); m_nextSearchOffset = 0; performPaginatedSearch(); } else { m_searchState = Finished; } } } // namespace Flame PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ImportPage.h0000644000175100017510000000503615144136757024061 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "tasks/Task.h" #include "ui/pages/BasePage.h" namespace Ui { class ImportPage; } class NewInstanceDialog; class ImportPage : public QWidget, public BasePage { Q_OBJECT public: explicit ImportPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~ImportPage(); virtual QString displayName() const override { return tr("Import"); } virtual QIcon icon() const override { return QIcon::fromTheme("viewfolder"); } virtual QString id() const override { return "import"; } virtual QString helpPage() const override { return "Zip-import"; } virtual bool shouldDisplay() const override; void retranslate() override; void setUrl(const QString& url); void openedImpl() override; void setExtraInfo(const QMap& extra_info); private slots: void on_modpackBtn_clicked(); void updateState(); private: QUrl modpackUrl() const; private: Ui::ImportPage* ui = nullptr; NewInstanceDialog* dialog = nullptr; QMap m_extra_info = {}; }; PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ModModel.h0000644000175100017510000000333315144136757023510 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include "BaseInstance.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "ui/pages/modplatform/ResourceModel.h" #include "ui/widgets/ModFilterWidget.h" class Version; namespace ResourceDownload { class ModPage; class ModModel : public ResourceModel { Q_OBJECT public: ModModel(BaseInstance&, ResourceAPI* api, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed); void setFilter(std::shared_ptr filter) { m_filter = filter; } virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const override; [[nodiscard]] QString debugName() const override { return m_debugName; } [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override; virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) override; virtual bool checkVersionFilters(const ModPlatform::IndexedVersion&) override; protected: BaseInstance& m_base_instance; std::shared_ptr m_filter = nullptr; private: QString m_debugName; QString m_metaEntryBase; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/TexturePackModel.h0000644000175100017510000000142615144136757025231 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "meta/VersionList.h" #include "ui/pages/modplatform/ResourcePackModel.h" namespace ResourceDownload { class TexturePackResourceModel : public ResourcePackResourceModel { Q_OBJECT public: TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase); inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; protected: Meta::VersionList::Ptr m_version_list; Task::Ptr m_task; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ShaderPackModel.h0000644000175100017510000000223315144136757024774 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include "BaseInstance.h" #include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ResourceModel.h" class Version; namespace ResourceDownload { class ShaderPackResourceModel : public ResourceModel { Q_OBJECT public: ShaderPackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); [[nodiscard]] QString debugName() const override { return m_debugName; } [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; private: QString m_debugName; QString m_metaEntryBase; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ModPage.cpp0000644000175100017510000001061215144136757023655 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModPage.h" #include "ui_ResourcePage.h" #include #include #include #include #include "Application.h" #include "ResourceDownloadTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ResourceDownloadDialog.h" namespace ResourceDownload { ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) { connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); } void ModPage::setFilterWidget(std::unique_ptr& widget) { if (m_filter_widget) disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); auto old = m_ui->splitter->replaceWidget(0, widget.get()); // because we replaced the widget we also need to delete it if (old) { delete old; } m_filter_widget.swap(widget); m_filter = m_filter_widget->getFilter(); connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch); prepareProviderCategories(); } /******** Callbacks to events in the UI (set up in the derived classes) ********/ void ModPage::filterMods() { m_filter_widget->setHidden(!m_filter_widget->isHidden()); } void ModPage::triggerSearch() { auto changed = m_filter_widget->changed(); m_filter = m_filter_widget->getFilter(); m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); m_ui->packView->clearSelection(); m_ui->packDescription->clear(); m_ui->versionSelectionBox->clear(); updateSelectionButton(); static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt(), changed); m_fetchProgress.watch(m_model->activeSearchJob().get()); } QMap ModPage::urlHandlers() const { QMap map; map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/mod\\/([^\\/]+)\\/?"), "modrinth"); map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/mc-mods\\/([^\\/]+)\\/?"), "curseforge"); map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); return map; } /******** Make changes to the UI ********/ void ModPage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, const std::shared_ptr base_model) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_model->addPack(pack, version, base_model, is_indexed); } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/OptionalModDialog.cpp0000644000175100017510000000452615144136757025715 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "OptionalModDialog.h" #include "ui_OptionalModDialog.h" OptionalModDialog::OptionalModDialog(QWidget* parent, const QStringList& mods) : QDialog(parent), ui(new Ui::OptionalModDialog) { ui->setupUi(this); for (const QString& mod : mods) { auto item = new QListWidgetItem(mod, ui->list); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); item->setData(Qt::UserRole, mod); } connect(ui->selectAllButton, &QPushButton::clicked, ui->list, [this] { for (int i = 0; i < ui->list->count(); i++) ui->list->item(i)->setCheckState(Qt::Checked); }); connect(ui->clearAllButton, &QPushButton::clicked, ui->list, [this] { for (int i = 0; i < ui->list->count(); i++) ui->list->item(i)->setCheckState(Qt::Unchecked); }); connect(ui->list, &QListWidget::itemActivated, [](QListWidgetItem* item) { if (item->checkState() == Qt::Checked) item->setCheckState(Qt::Unchecked); else item->setCheckState(Qt::Checked); }); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } OptionalModDialog::~OptionalModDialog() { delete ui; } QStringList OptionalModDialog::getResult() { QStringList result; result.reserve(ui->list->count()); for (int i = 0; i < ui->list->count(); i++) { auto item = ui->list->item(i); if (item->checkState() == Qt::Checked) result.append(item->data(Qt::UserRole).toString()); } return result; } PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourceModel.cpp0000644000175100017510000003720715144136757025122 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ResourceModel.h" #include #include #include #include #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "modplatform/ResourceAPI.h" #include "net/ApiDownload.h" #include "net/NetJob.h" #include "modplatform/ModIndex.h" #include "ui/widgets/ProjectItem.h" namespace ResourceDownload { QHash ResourceModel::s_running_models; ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api) { s_running_models.insert(this, true); if (APPLICATION_DYN) { m_current_info_job.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); } } ResourceModel::~ResourceModel() { s_running_models.find(this).value() = false; } auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant { int pos = index.row(); if (pos >= m_packs.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } auto pack = m_packs.at(pos); switch (role) { case Qt::ToolTipRole: { if (pack->description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks QString edit = pack->description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } return pack->description; } case Qt::DecorationRole: { if (APPLICATION_DYN) { if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); icon_or_none.has_value()) return icon_or_none.value(); return QIcon::fromTheme("screenshot-placeholder"); } else { return {}; } } case Qt::SizeHintRole: return QSize(0, 58); case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } // Custom data case UserDataTypes::TITLE: return pack->name; case UserDataTypes::DESCRIPTION: return pack->description; case Qt::CheckStateRole: return pack->isAnyVersionSelected() ? Qt::Checked : Qt::Unchecked; case UserDataTypes::INSTALLED: return this->isPackInstalled(pack); default: break; } return {}; } QHash ResourceModel::roleNames() const { QHash roles; roles[Qt::ToolTipRole] = "toolTip"; roles[Qt::DecorationRole] = "decoration"; roles[Qt::SizeHintRole] = "sizeHint"; roles[Qt::UserRole] = "pack"; roles[UserDataTypes::TITLE] = "title"; roles[UserDataTypes::DESCRIPTION] = "description"; roles[UserDataTypes::INSTALLED] = "installed"; return roles; } bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); if (pos >= m_packs.size() || pos < 0 || !index.isValid()) return false; m_packs[pos] = value.value(); emit dataChanged(index, index); return true; } QString ResourceModel::debugName() const { return "ResourceDownload (Model)"; } void ResourceModel::fetchMore(const QModelIndex& parent) { if (parent.isValid() || m_search_state == SearchState::Finished) return; search(); } void ResourceModel::search() { if (hasActiveSearchJob()) return; if (m_search_term.startsWith("#")) { auto projectId = m_search_term.mid(1); if (!projectId.isEmpty()) { ResourceAPI::Callback callbacks; callbacks.on_fail = [this](QString reason, int) { if (!s_running_models.constFind(this).value()) return; searchRequestFailed(reason, -1); }; callbacks.on_abort = [this] { if (!s_running_models.constFind(this).value()) return; searchRequestAborted(); }; callbacks.on_succeed = [this](auto& pack) { if (!s_running_models.constFind(this).value()) return; searchRequestForOneSucceeded(pack); }; auto project = std::make_shared(); project->addonId = projectId; if (auto job = m_api->getProjectInfo({ project }, std::move(callbacks)); job) runSearchJob(job); return; } } auto args{ createSearchArguments() }; ResourceAPI::Callback> callbacks{}; callbacks.on_succeed = [this](auto& doc) { if (!s_running_models.constFind(this).value()) return; searchRequestSucceeded(doc); }; callbacks.on_fail = [this](QString reason, int network_error_code) { if (!s_running_models.constFind(this).value()) return; searchRequestFailed(reason, network_error_code); }; callbacks.on_abort = [this] { if (!s_running_models.constFind(this).value()) return; searchRequestAborted(); }; if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job) runSearchJob(job); } void ResourceModel::loadEntry(const QModelIndex& entry) { auto const& pack = m_packs[entry.row()]; if (!hasActiveInfoJob()) m_current_info_job.clear(); if (!pack->versionsLoaded) { auto args{ createVersionsArguments(entry) }; ResourceAPI::Callback> callbacks{}; auto addonId = pack->addonId; // Use default if no callbacks are set if (!callbacks.on_succeed) callbacks.on_succeed = [this, entry, addonId](auto& doc) { if (!s_running_models.constFind(this).value()) return; versionRequestSucceeded(doc, addonId, entry); }; if (!callbacks.on_fail) callbacks.on_fail = [](QString reason, int) { QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project versions: %1").arg(reason)); }; if (auto job = m_api->getProjectVersions(std::move(args), std::move(callbacks)); job) runInfoJob(job); } if (!pack->extraDataLoaded) { auto args{ createInfoArguments(entry) }; ResourceAPI::Callback callbacks{}; callbacks.on_succeed = [this, entry](auto& newpack) { if (!s_running_models.constFind(this).value()) return; infoRequestSucceeded(newpack, entry); }; callbacks.on_fail = [this](QString reason, int) { if (!s_running_models.constFind(this).value()) return; QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason)); }; callbacks.on_abort = [this] { if (!s_running_models.constFind(this).value()) return; qCritical() << tr("The request was aborted for an unknown reason"); }; if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) runInfoJob(job); } } void ResourceModel::refresh() { bool reset_requested = false; if (hasActiveInfoJob()) { m_current_info_job.abort(); reset_requested = true; } if (hasActiveSearchJob()) { m_current_search_job->abort(); reset_requested = true; } if (reset_requested) { m_search_state = SearchState::ResetRequested; return; } clearData(); m_search_state = SearchState::None; m_next_search_offset = 0; search(); } void ResourceModel::clearData() { beginResetModel(); m_packs.clear(); endResetModel(); } void ResourceModel::runSearchJob(Task::Ptr ptr) { m_current_search_job.reset(ptr); // clean up first m_current_search_job->start(); } void ResourceModel::runInfoJob(Task::Ptr ptr) { if (!m_current_info_job.isRunning()) m_current_info_job.clear(); m_current_info_job.addTask(ptr); if (!m_current_info_job.isRunning()) m_current_info_job.run(); } std::optional ResourceModel::getCurrentSortingMethodByIndex() const { std::optional sort{}; { // Find sorting method by ID auto sorting_methods = getSortingMethods(); auto method = std::find_if(sorting_methods.constBegin(), sorting_methods.constEnd(), [this](auto const& e) { return m_current_sort_index == e.index; }); if (method != sorting_methods.constEnd()) sort = *method; } return sort; } std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) { QPixmap pixmap; if (QPixmapCache::find(url.toString(), &pixmap)) return { pixmap }; if (!m_current_icon_job) { m_current_icon_job.reset(new NetJob("IconJob", APPLICATION->network())); m_current_icon_job->setAskRetry(false); } if (m_currently_running_icon_actions.contains(url)) return {}; if (m_failed_icon_actions.contains(url)) return {}; auto cache_entry = APPLICATION->metacache()->resolveEntry( metaEntryBase(), QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); auto icon_fetch_action = Net::ApiDownload::makeCached(url, cache_entry); auto full_file_path = cache_entry->getFullPath(); connect(icon_fetch_action.get(), &Task::succeeded, this, [this, url, full_file_path, index] { auto icon = QIcon(full_file_path); QPixmapCache::insert(url.toString(), icon.pixmap(icon.actualSize({ 64, 64 }))); m_currently_running_icon_actions.remove(url); emit dataChanged(index, index, { Qt::DecorationRole }); }); connect(icon_fetch_action.get(), &Task::failed, this, [this, url] { m_currently_running_icon_actions.remove(url); m_failed_icon_actions.insert(url); }); m_currently_running_icon_actions.insert(url); m_current_icon_job->addNetAction(icon_fetch_action); if (!m_current_icon_job->isRunning()) QMetaObject::invokeMethod(m_current_icon_job.get(), &NetJob::start); return {}; } /* Default callbacks */ void ResourceModel::searchRequestSucceeded(QList& newList) { QList filteredNewList; for (auto pack : newList) { ModPlatform::IndexedPack::Ptr p; if (auto sel = std::find_if(m_selected.begin(), m_selected.end(), [&pack](const DownloadTaskPtr i) { const auto ipack = i->getPack(); return ipack->provider == pack->provider && ipack->addonId == pack->addonId; }); sel != m_selected.end()) { p = sel->get()->getPack(); } else { p = pack; } if (checkFilters(p)) { filteredNewList << p; } } if (newList.size() < 25) { m_search_state = SearchState::Finished; } else { m_next_search_offset += 25; m_search_state = SearchState::CanFetchMore; } // When you have a Qt build with assertions turned on, proceeding here will abort the application if (filteredNewList.size() == 0) return; beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + filteredNewList.size() - 1); m_packs.append(filteredNewList); endInsertRows(); } void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr pack) { m_search_state = SearchState::Finished; beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1); m_packs.append(pack); endInsertRows(); } void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int network_error_code) { switch (network_error_code) { default: // Network error QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); break; case 409: // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), QString("%1").arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); break; } m_search_state = SearchState::Finished; } void ResourceModel::searchRequestAborted() { if (m_search_state != SearchState::ResetRequested) qCritical() << "Search task in" << debugName() << "aborted by an unknown reason!"; // Retry fetching clearData(); m_next_search_offset = 0; search(); } void ResourceModel::versionRequestSucceeded(QVector& doc, QVariant pack, const QModelIndex& index) { auto current_pack = data(index, Qt::UserRole).value(); // Check if the index is still valid for this resource or not if (pack != current_pack->addonId) return; current_pack->versions = doc; current_pack->versionsLoaded = true; // Cache info :^) QVariant new_pack; new_pack.setValue(current_pack); if (!setData(index, new_pack, Qt::UserRole)) { qWarning() << "Failed to cache resource versions!"; return; } emit versionListUpdated(index); } void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack::Ptr pack, const QModelIndex& index) { auto current_pack = data(index, Qt::UserRole).value(); // Check if the index is still valid for this resource or not if (pack->addonId != current_pack->addonId) return; // Cache info :^) QVariant new_pack; new_pack.setValue(pack); if (!setData(index, new_pack, Qt::UserRole)) { qWarning() << "Failed to cache resource info!"; return; } emit projectInfoUpdated(index); } void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, const std::shared_ptr packs, bool is_indexed) { version.is_currently_selected = true; m_selected.append(makeShared(pack, version, packs, is_indexed)); } void ResourceModel::removePack(const QString& rem) { auto pred = [&rem](const DownloadTaskPtr i) { return rem == i->getName(); }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) m_selected.removeIf(pred); #else { for (auto it = m_selected.begin(); it != m_selected.end();) if (pred(*it)) it = m_selected.erase(it); else ++it; } #endif auto pack = std::find_if(m_packs.begin(), m_packs.end(), [&rem](const ModPlatform::IndexedPack::Ptr i) { return rem == i->name; }); if (pack == m_packs.end()) { // ignore it if is not in the current search return; } if (!pack->get()->versionsLoaded) { return; } for (auto& ver : pack->get()->versions) ver.is_currently_selected = false; } bool ResourceModel::checkVersionFilters(const ModPlatform::IndexedVersion& v) { return (!optedOut(v)); } } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourcePage.h0000644000175100017510000000737115144136757024402 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include "ResourceDownloadTask.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "ui/pages/BasePage.h" #include "ui/pages/modplatform/ResourceModel.h" #include "ui/widgets/ProgressWidget.h" namespace Ui { class ResourcePage; } class BaseInstance; namespace ResourceDownload { class ResourceDownloadDialog; class ResourceModel; class ResourcePage : public QWidget, public BasePage { Q_OBJECT public: using DownloadTaskPtr = shared_qobject_ptr; ~ResourcePage() override; /* Affects what the user sees */ auto displayName() const -> QString override = 0; auto icon() const -> QIcon override = 0; auto id() const -> QString override = 0; auto helpPage() const -> QString override = 0; bool shouldDisplay() const override = 0; /* Used internally */ virtual auto metaEntryBase() const -> QString = 0; virtual auto debugName() const -> QString = 0; //: The plural version of 'resource' virtual inline QString resourcesString() const { return tr("resources"); } //: The singular version of 'resources' virtual inline QString resourceString() const { return tr("resource"); } /* Features this resource's page supports */ virtual bool supportsFiltering() const = 0; void retranslate() override; void openedImpl() override; auto eventFilter(QObject* watched, QEvent* event) -> bool override; /** Get the current term in the search bar. */ auto getSearchTerm() const -> QString; /** Programatically set the term in the search bar. */ void setSearchTerm(QString); bool setCurrentPack(ModPlatform::IndexedPack::Ptr); auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr; auto getDialog() const -> const ResourceDownloadDialog* { return m_parentDialog; } auto getModel() const -> ResourceModel* { return m_model; } protected: ResourcePage(ResourceDownloadDialog* parent, BaseInstance&); void addSortings(); public slots: virtual void updateUi(const QModelIndex& index); virtual void updateSelectionButton(); virtual void versionListUpdated(const QModelIndex& index); void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); void removeResourceFromDialog(const QString& pack_name); virtual void removeResourceFromPage(const QString& name); virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr); virtual void modelReset(); QList selectedPacks() { return m_model->selectedPacks(); } bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); } virtual void openProject(QVariant projectID); protected slots: virtual void triggerSearch() = 0; void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void onResourceSelected(); void onResourceToggle(const QModelIndex& index); /** Associates regex expressions to pages in the order they're given in the map. */ virtual QMap urlHandlers() const = 0; virtual void openUrl(const QUrl&); public: BaseInstance& m_baseInstance; protected: Ui::ResourcePage* m_ui; ResourceDownloadDialog* m_parentDialog = nullptr; ResourceModel* m_model = nullptr; int m_selectedVersionIndex = -1; ProgressWidget m_fetchProgress; // Used to do instant searching with a delay to cache quick changes QTimer m_searchTimer; bool m_doNotJumpToMod = false; QSet m_enableQueue; }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/ui/pages/modplatform/ResourceModel.h0000644000175100017510000001274215144136757024564 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include "QObjectPtr.h" #include "ResourceDownloadTask.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "tasks/ConcurrentTask.h" class NetJob; class ResourceAPI; namespace ModPlatform { struct IndexedPack; } namespace ResourceDownload { class ResourceModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString search_term MEMBER m_search_term WRITE setSearchTerm) public: using DownloadTaskPtr = shared_qobject_ptr; ResourceModel(ResourceAPI* api); ~ResourceModel() override; auto data(const QModelIndex&, int role) const -> QVariant override; auto roleNames() const -> QHash override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; virtual auto debugName() const -> QString; virtual auto metaEntryBase() const -> QString = 0; inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : static_cast(m_packs.size()); } inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); } bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); } Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; } auto getSortingMethods() const { return m_api->getSortingMethods(); } virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const { return {}; } /** Whether the version is opted out or not. Currently only makes sense in CF. */ virtual bool optedOut(const ModPlatform::IndexedVersion& ver) const { Q_UNUSED(ver); return false; }; virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) { return true; } virtual bool checkVersionFilters(const ModPlatform::IndexedVersion&); public slots: void fetchMore(const QModelIndex& parent) override; inline bool canFetchMore(const QModelIndex& parent) const override { return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore; } void setSearchTerm(QString term) { m_search_term = term; } virtual ResourceAPI::SearchArgs createSearchArguments() = 0; virtual ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) = 0; virtual ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) = 0; /** Requests the API for more entries. */ virtual void search(); /** Applies any processing / extra requests needed to fully load the specified entry's information. */ virtual void loadEntry(const QModelIndex&); /** Schedule a refresh, clearing the current state. */ void refresh(); /** Gets the icon at the URL for the given index. If it's not fetched yet, fetch it and update when fisinhed. */ std::optional getIcon(QModelIndex&, const QUrl&); void addPack(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& version, std::shared_ptr packs, bool is_indexed = false); void removePack(const QString& rem); QList selectedPacks() { return m_selected; } protected: /** Resets the model's data. */ void clearData(); void runSearchJob(Task::Ptr); void runInfoJob(Task::Ptr); auto getCurrentSortingMethodByIndex() const -> std::optional; virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const { return false; } protected: /* Basic search parameters */ enum class SearchState { None, CanFetchMore, ResetRequested, Finished } m_search_state = SearchState::None; int m_next_search_offset = 0; QString m_search_term; unsigned int m_current_sort_index = 0; std::unique_ptr m_api; // Job for searching for new entries shared_qobject_ptr m_current_search_job; // Job for fetching versions and extra info on existing entries ConcurrentTask m_current_info_job; shared_qobject_ptr m_current_icon_job; QSet m_currently_running_icon_actions; QSet m_failed_icon_actions; QList m_packs; QList m_selected; // HACK: We need this to prevent callbacks from calling the model after it has already been deleted. // This leaks a tiny bit of memory per time the user has opened a resource dialog. How to make this better? static QHash s_running_models; private: /* Default search request callbacks */ void searchRequestSucceeded(QList&); void searchRequestForOneSucceeded(ModPlatform::IndexedPack::Ptr); void searchRequestFailed(QString reason, int network_error_code); void searchRequestAborted(); void versionRequestSucceeded(QVector&, QVariant, const QModelIndex&); void infoRequestSucceeded(ModPlatform::IndexedPack::Ptr, const QModelIndex&); signals: void versionListUpdated(const QModelIndex& index); void projectInfoUpdated(const QModelIndex& index); }; } // namespace ResourceDownload PrismLauncher-10.0.5/launcher/Json.h0000644000175100017510000002075315144136756016665 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include #include #include "Exception.h" namespace Json { class JsonException : public ::Exception { public: JsonException(const QString& message) : Exception(message) {} }; /// @throw FileSystemException void write(const QJsonDocument& doc, const QString& filename); /// @throw FileSystemException void write(const QJsonObject& object, const QString& filename); /// @throw FileSystemException void write(const QJsonArray& array, const QString& filename); QByteArray toText(const QJsonObject& obj); QByteArray toText(const QJsonArray& array); /// @throw JsonException QJsonDocument requireDocument(const QByteArray& data, const QString& what = "Document"); /// @throw JsonException QJsonDocument requireDocument(const QString& filename, const QString& what = "Document"); /// @throw JsonException QJsonObject requireObject(const QJsonDocument& doc, const QString& what = "Document"); /// @throw JsonException QJsonArray requireArray(const QJsonDocument& doc, const QString& what = "Document"); /////////////////// WRITING //////////////////// void writeString(QJsonObject& to, const QString& key, const QString& value); void writeStringList(QJsonObject& to, const QString& key, const QStringList& values); template QJsonValue toJson(const T& t) { return QJsonValue(t); } template <> QJsonValue toJson(const QUrl& url); template <> QJsonValue toJson(const QByteArray& data); template <> QJsonValue toJson(const QDateTime& datetime); template <> QJsonValue toJson(const QDir& dir); template <> QJsonValue toJson(const QUuid& uuid); template <> QJsonValue toJson(const QVariant& variant); template QJsonArray toJsonArray(const QList& container) { QJsonArray array; for (const T& item : container) { array.append(toJson(item)); } return array; } ////////////////// READING //////////////////// // Attempt to parse JSON up until garbage is encountered QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error = nullptr, QString* garbage = nullptr); /// @throw JsonException template T requireIsType(const QJsonValue& value, const QString& what = "Value"); /// @throw JsonException template <> double requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> bool requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> int requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QJsonObject requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QJsonArray requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QJsonValue requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QByteArray requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QDateTime requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QVariant requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QString requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QUuid requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QDir requireIsType(const QJsonValue& value, const QString& what); /// @throw JsonException template <> QUrl requireIsType(const QJsonValue& value, const QString& what); // the following functions are higher level functions, that make use of the above functions for // type conversion /// @throw JsonException template T requireIsType(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { throw JsonException(localWhat + "s parent does not contain " + localWhat); } return requireIsType(parent.value(key), localWhat); } template QList requireIsArrayOf(const QJsonDocument& doc) { const QJsonArray array = requireArray(doc); QList out; for (const QJsonValue val : array) { out.append(requireIsType(val, "Document")); } return out; } /// @throw JsonException template QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { throw JsonException(localWhat + "s parent does not contain " + localWhat); } const QJsonArray array = parent[key].toArray(); QList out; for (const QJsonValue val : array) { out.append(requireIsType(val, "Document")); } return out; } // this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers #define JSON_HELPERFUNCTIONS(NAME, TYPE) \ inline TYPE require##NAME(const QJsonValue& value, const QString& what = "Value") \ { \ return requireIsType(value, what); \ } \ inline TYPE require##NAME(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") \ { \ return requireIsType(parent, key, what); \ } JSON_HELPERFUNCTIONS(Array, QJsonArray) JSON_HELPERFUNCTIONS(Object, QJsonObject) JSON_HELPERFUNCTIONS(JsonValue, QJsonValue) JSON_HELPERFUNCTIONS(String, QString) JSON_HELPERFUNCTIONS(Boolean, bool) JSON_HELPERFUNCTIONS(Double, double) JSON_HELPERFUNCTIONS(Integer, int) JSON_HELPERFUNCTIONS(DateTime, QDateTime) JSON_HELPERFUNCTIONS(Url, QUrl) JSON_HELPERFUNCTIONS(ByteArray, QByteArray) JSON_HELPERFUNCTIONS(Dir, QDir) JSON_HELPERFUNCTIONS(Uuid, QUuid) JSON_HELPERFUNCTIONS(Variant, QVariant) #undef JSON_HELPERFUNCTIONS // helper functions for settings QStringList toStringList(const QString& jsonString); QString fromStringList(const QStringList& list); QVariantMap toMap(const QString& jsonString); QString fromMap(const QVariantMap& map); } // namespace Json using JSONValidationError = Json::JsonException; PrismLauncher-10.0.5/launcher/LoggedProcess.h0000644000175100017510000000561715144136756020516 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022,2023 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "MessageLevel.h" /* * This is a basic process. * It has line-based logging support and hides some of the nasty bits. */ class LoggedProcess : public QProcess { Q_OBJECT public: enum State { NotRunning, Starting, FailedToStart, Running, Finished, Crashed, Aborted }; public: explicit LoggedProcess(QStringConverter::Encoding outputEncoding = QStringConverter::System, QObject* parent = nullptr); virtual ~LoggedProcess(); State state() const; int exitCode() const; void setDetachable(bool detachable); signals: void log(QStringList lines, MessageLevel level); void stateChanged(LoggedProcess::State state); public slots: /** * @brief kill the process - equivalent to kill -9 */ void kill(); private slots: void on_stdErr(); void on_stdOut(); void on_exit(int exit_code, QProcess::ExitStatus status); void on_error(QProcess::ProcessError error); void on_stateChange(QProcess::ProcessState); private: void changeState(LoggedProcess::State state); QStringList reprocess(const QByteArray& data, QStringDecoder& decoder); private: QStringDecoder m_err_decoder; QStringDecoder m_out_decoder; QString m_leftover_line; bool m_killed = false; State m_state = NotRunning; int m_exit_code = 0; bool m_is_aborting = false; bool m_is_detachable = false; }; PrismLauncher-10.0.5/launcher/macsandbox/0000755000175100017510000000000015144136756017713 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/macsandbox/SecurityBookmarkFileAccess.h0000644000175100017510000001062115144136756025303 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef FILEACCESS_H #define FILEACCESS_H #include #include Q_FORWARD_DECLARE_OBJC_CLASS(NSData); Q_FORWARD_DECLARE_OBJC_CLASS(NSURL); Q_FORWARD_DECLARE_OBJC_CLASS(NSString); Q_FORWARD_DECLARE_OBJC_CLASS(NSAutoreleasePool); Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableDictionary); Q_FORWARD_DECLARE_OBJC_CLASS(NSMutableSet); class QString; class QByteArray; class QUrl; class SecurityBookmarkFileAccess { /// The keys are bookmarks and the values are URLs. NSMutableDictionary* m_bookmarks; /// The keys are paths and the values are bookmarks. NSMutableDictionary* m_paths; /// Contains URLs that are currently being accessed. NSMutableSet* m_activeURLs; bool m_readOnly; NSURL* securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale); public: /// \param readOnly A boolean indicating whether the bookmark should be read-only. SecurityBookmarkFileAccess(bool readOnly = false); ~SecurityBookmarkFileAccess(); /// Get a security scoped bookmark from a URL. /// /// The URL must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling /// this function. Note that this is called implicitly if the user selects the directory from a file picker. /// \param url The URL to get the security scoped bookmark from. /// \return A QByteArray containing the security scoped bookmark. QByteArray urlToSecurityScopedBookmark(const QUrl& url); /// Get a security scoped bookmark from a path. /// /// The path must be accessible before calling this function. That is, call `startAccessingSecurityScopedResource()` before calling /// this function. Note that this is called implicitly if the user selects the directory from a file picker. /// \param path The path to get the security scoped bookmark from. /// \return A QByteArray containing the security scoped bookmark. QByteArray pathToSecurityScopedBookmark(const QString& path); /// Get a QUrl from a security scoped bookmark. If the bookmark is stale, isStale will be set to true and the bookmark will be updated. /// /// You must check whether the URL is valid before using it. /// \param bookmark The security scoped bookmark to get the URL from. /// \param isStale A boolean that will be set to true if the bookmark is stale. /// \return The URL from the security scoped bookmark. QUrl securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale); /// Makes the file or directory at the path pointed to by the bookmark accessible. Unlike `startAccessingSecurityScopedResource()`, this /// class ensures that only one "access" is active at a time. Calling this function again after the security-scoped resource has /// already been used will do nothing, and a single call to `stopUsingSecurityScopedBookmark()` will release the resource provided that /// this is the only `SecurityBookmarkFileAccess` accessing the resource. /// /// If the bookmark is stale, `isStale` will be set to true and the bookmark will be updated. Stored copies of the bookmark need to be /// updated. /// \param bookmark The security scoped bookmark to start accessing. /// \param isStale A boolean that will be set to true if the bookmark is stale. /// \return A boolean indicating whether the bookmark was successfully accessed. bool startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale); void stopUsingSecurityScopedBookmark(QByteArray& bookmark); /// Returns true if access to the `path` is currently being maintained by this object. bool isAccessingPath(const QString& path); }; #endif // FILEACCESS_H PrismLauncher-10.0.5/launcher/macsandbox/SecurityBookmarkFileAccess.mm0000644000175100017510000001433515144136756025473 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Kenneth Chew <79120643+kthchew@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SecurityBookmarkFileAccess.h" #include #include #include QByteArray SecurityBookmarkFileAccess::urlToSecurityScopedBookmark(const QUrl& url) { if (!url.isLocalFile()) return {}; NSError* error = nil; NSURL* nsurl = [url.toNSURL() absoluteURL]; NSData* bookmark; if ([m_paths objectForKey:[nsurl path]]) { bookmark = m_paths[[nsurl path]]; } else { bookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope | (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; } if (error) { return {}; } // remove/reapply access to ensure that write access is immediately cut off for read-only bookmarks // sometimes you need to call this twice to actually stop access (extra calls aren't harmful) [nsurl stopAccessingSecurityScopedResource]; [nsurl stopAccessingSecurityScopedResource]; nsurl = [NSURL URLByResolvingBookmarkData:bookmark options:NSURLBookmarkResolutionWithSecurityScope | (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) relativeToURL:nil bookmarkDataIsStale:nil error:&error]; m_paths[[nsurl path]] = bookmark; m_bookmarks[bookmark] = nsurl; QByteArray qBookmark = QByteArray::fromNSData(bookmark); bool isStale = false; startUsingSecurityScopedBookmark(qBookmark, isStale); return qBookmark; } SecurityBookmarkFileAccess::SecurityBookmarkFileAccess(bool readOnly) : m_readOnly(readOnly) { m_bookmarks = [NSMutableDictionary new]; m_paths = [NSMutableDictionary new]; m_activeURLs = [NSMutableSet new]; } SecurityBookmarkFileAccess::~SecurityBookmarkFileAccess() { for (NSURL* url : m_activeURLs) { [url stopAccessingSecurityScopedResource]; } } QByteArray SecurityBookmarkFileAccess::pathToSecurityScopedBookmark(const QString& path) { return urlToSecurityScopedBookmark(QUrl::fromLocalFile(path)); } NSURL* SecurityBookmarkFileAccess::securityScopedBookmarkToNSURL(QByteArray& bookmark, bool& isStale) { NSError* error = nil; BOOL localStale = NO; NSURL* nsurl = [NSURL URLByResolvingBookmarkData:bookmark.toNSData() options:NSURLBookmarkResolutionWithSecurityScope | (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) relativeToURL:nil bookmarkDataIsStale:&localStale error:&error]; if (error) { return nil; } isStale = localStale; if (isStale) { NSData* nsBookmark = [nsurl bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope | (m_readOnly ? NSURLBookmarkCreationSecurityScopeAllowOnlyReadAccess : 0) includingResourceValuesForKeys:nil relativeToURL:nil error:&error]; if (error) { return nil; } bookmark = QByteArray::fromNSData(nsBookmark); } NSData* nsBookmark = bookmark.toNSData(); m_paths[[nsurl path]] = nsBookmark; m_bookmarks[nsBookmark] = nsurl; return nsurl; } QUrl SecurityBookmarkFileAccess::securityScopedBookmarkToURL(QByteArray& bookmark, bool& isStale) { if (bookmark.isEmpty()) return {}; NSURL* url = securityScopedBookmarkToNSURL(bookmark, isStale); if (!url) return {}; return QUrl::fromNSURL(url); } bool SecurityBookmarkFileAccess::startUsingSecurityScopedBookmark(QByteArray& bookmark, bool& isStale) { NSURL* url = [m_bookmarks objectForKey:bookmark.toNSData()] ? m_bookmarks[bookmark.toNSData()] : securityScopedBookmarkToNSURL(bookmark, isStale); if ([m_activeURLs containsObject:url]) return false; [url stopAccessingSecurityScopedResource]; if ([url startAccessingSecurityScopedResource]) { [m_activeURLs addObject:url]; return true; } return false; } void SecurityBookmarkFileAccess::stopUsingSecurityScopedBookmark(QByteArray& bookmark) { if (![m_bookmarks objectForKey:bookmark.toNSData()]) return; NSURL* url = m_bookmarks[bookmark.toNSData()]; if ([m_activeURLs containsObject:url]) { [url stopAccessingSecurityScopedResource]; [url stopAccessingSecurityScopedResource]; [m_activeURLs removeObject:url]; [m_paths removeObjectForKey:[url path]]; [m_bookmarks removeObjectForKey:bookmark.toNSData()]; } } bool SecurityBookmarkFileAccess::isAccessingPath(const QString& path) { NSData* bookmark = [m_paths objectForKey:path.toNSString()]; if (!bookmark && path.endsWith('/')) { bookmark = [m_paths objectForKey:path.left(path.length() - 1).toNSString()]; } if (!bookmark) { return false; } NSURL* url = [m_bookmarks objectForKey:bookmark]; return [m_activeURLs containsObject:url]; } PrismLauncher-10.0.5/launcher/logs/0000755000175100017510000000000015144136756016540 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/logs/LogParser.cpp0000644000175100017510000002652615144136756021155 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "LogParser.h" #include #include "MessageLevel.h" using namespace Qt::Literals::StringLiterals; void LogParser::appendLine(QAnyStringView data) { if (!m_partialData.isEmpty()) { m_buffer = QString(m_partialData); m_buffer.append("\n"); m_partialData.clear(); } m_buffer.append(data.toString()); } std::optional LogParser::getError() { return m_error; } std::optional LogParser::parseAttributes() { LogParser::LogEntry entry{ "", MessageLevel::Info, }; auto attributes = m_parser.attributes(); for (const auto& attr : attributes) { auto name = attr.name(); auto value = attr.value(); if (name == "logger"_L1) { entry.logger = value.trimmed().toString(); } else if (name == "timestamp"_L1) { if (value.trimmed().isEmpty()) { m_parser.raiseError("log4j:Event Missing required attribute: timestamp"); return {}; } entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); } else if (name == "level"_L1) { entry.levelText = value.trimmed().toString(); entry.level = MessageLevel::fromName(entry.levelText); } else if (name == "thread"_L1) { entry.thread = value.trimmed().toString(); } } if (entry.logger.isEmpty()) { m_parser.raiseError("log4j:Event Missing required attribute: logger"); return {}; } return entry; } void LogParser::setError() { m_error = { m_parser.errorString(), m_parser.error(), }; } void LogParser::clearError() { m_error = {}; // clear previous error } bool isPotentialLog4JStart(QStringView buffer) { static QString target = QStringLiteral(" LogParser::parseNext() { clearError(); if (m_buffer.isEmpty()) { return {}; } if (m_buffer.trimmed().isEmpty()) { auto text = QString(m_buffer); m_buffer.clear(); return LogParser::PlainText{ text }; } // check if we have a full xml log4j event bool isCompleteLog4j = false; m_parser.clear(); m_parser.setNamespaceProcessing(false); m_parser.addData(m_buffer); if (m_parser.readNextStartElement()) { if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { int depth = 1; bool eod = false; while (depth > 0 && !eod) { auto tok = m_parser.readNext(); switch (tok) { case QXmlStreamReader::TokenType::StartElement: { depth += 1; } break; case QXmlStreamReader::TokenType::EndElement: { depth -= 1; } break; case QXmlStreamReader::TokenType::EndDocument: { eod = true; // break outer while loop } break; default: { // no op } } if (m_parser.hasError()) { break; } } isCompleteLog4j = depth == 0; } } if (isCompleteLog4j) { return parseLog4J(); } else { if (isPotentialLog4JStart(m_buffer)) { m_partialData = QString(m_buffer); return LogParser::Partial{ QString(m_buffer) }; } int start = 0; auto bufView = QStringView(m_buffer); while (start < bufView.length()) { if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) { auto slicestart = start + pos; auto slice = bufView.right(bufView.length() - slicestart); if (isPotentialLog4JStart(slice)) { if (slicestart > 0) { auto text = m_buffer.left(slicestart); m_buffer = m_buffer.right(m_buffer.length() - slicestart); if (!text.trimmed().isEmpty()) { return LogParser::PlainText{ text }; } } m_partialData = QString(m_buffer); return LogParser::Partial{ QString(m_buffer) }; } start = slicestart + 1; } else { break; } } // no log4j found, all plain text auto text = QString(m_buffer); m_buffer.clear(); return LogParser::PlainText{ text }; } } QList LogParser::parseAvailable() { QList items; bool doNext = true; while (doNext) { auto item_ = parseNext(); if (m_error.has_value()) { return {}; } if (item_.has_value()) { auto item = item_.value(); if (std::holds_alternative(item)) { break; } else { items.push_back(item); } } else { doNext = false; } } return items; } std::optional LogParser::parseLog4J() { m_parser.clear(); m_parser.setNamespaceProcessing(false); m_parser.addData(m_buffer); m_parser.readNextStartElement(); if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { auto entry_ = parseAttributes(); if (!entry_.has_value()) { setError(); return {}; } auto entry = entry_.value(); bool foundMessage = false; int depth = 1; enum parseOp { noOp, entryReady, parseError }; auto foundStart = [&]() -> parseOp { depth += 1; if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { QString message; bool messageComplete = false; while (!messageComplete) { auto tok = m_parser.readNext(); switch (tok) { case QXmlStreamReader::TokenType::Characters: { message.append(m_parser.text()); } break; case QXmlStreamReader::TokenType::EndElement: { if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { messageComplete = true; } } break; case QXmlStreamReader::TokenType::EndDocument: { return parseError; // parse fail } break; default: { // no op } } if (m_parser.hasError()) { return parseError; } } entry.message = message; foundMessage = true; depth -= 1; } return noOp; }; auto foundEnd = [&]() -> parseOp { depth -= 1; if (depth == 0 && m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { if (foundMessage) { auto consumed = m_parser.characterOffset(); if (consumed > 0 && consumed <= m_buffer.length()) { m_buffer = m_buffer.right(m_buffer.length() - consumed); // potential whitespace preserved for next item } clearError(); return entryReady; } m_parser.raiseError("log4j:Event Missing required attribute: message"); setError(); return parseError; } return noOp; }; while (!m_parser.atEnd()) { auto tok = m_parser.readNext(); parseOp op = noOp; switch (tok) { case QXmlStreamReader::TokenType::StartElement: { op = foundStart(); } break; case QXmlStreamReader::TokenType::EndElement: { op = foundEnd(); } break; case QXmlStreamReader::TokenType::EndDocument: { return {}; } break; default: { // no op } } switch (op) { case parseError: return {}; // parse fail or error case entryReady: return entry; case noOp: default: { // no op } } if (m_parser.hasError()) { return {}; } } } throw std::runtime_error("unreachable: already verified this was a complete log4j:Event"); } MessageLevel LogParser::guessLevel(const QString& line, MessageLevel previous) { static const QRegularExpression LINE_WITH_LEVEL("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); auto match = LINE_WITH_LEVEL.match(line); if (match.hasMatch()) { // New style logs from log4j QString timestamp = match.captured("timestamp"); QString levelStr = match.captured("level"); return MessageLevel::fromName(levelStr); } else { // Old style forge logs if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || line.contains("[FINEST]")) return MessageLevel::Info; if (line.contains("[SEVERE]") || line.contains("[STDERR]")) return MessageLevel::Error; if (line.contains("[WARNING]")) return MessageLevel::Warning; if (line.contains("[DEBUG]")) return MessageLevel::Debug; } if (line.contains("Exception: ") || line.contains("Throwable: ")) return MessageLevel::Error; if (line.startsWith("Caused by: ") || line.startsWith("Exception in thread")) return MessageLevel::Error; if (line.contains("overwriting existing")) return MessageLevel::Fatal; if (line.startsWith("\t") || line.startsWith(" ")) return previous; return MessageLevel::Unknown; } PrismLauncher-10.0.5/launcher/logs/AnonymizeLog.h0000644000175100017510000000303315144136756021323 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include void anonymizeLog(QString& log); PrismLauncher-10.0.5/launcher/logs/AnonymizeLog.cpp0000644000175100017510000000601715144136756021663 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AnonymizeLog.h" #include struct RegReplace { RegReplace(QRegularExpression r, QString w) : reg(r), with(w) { reg.optimize(); } QRegularExpression reg; QString with; }; static const QVector anonymizeRules = { RegReplace(QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption), "C:\\Users\\********\\"), // windows RegReplace(QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption), "C:/Users/********/"), // windows with forward slashes RegReplace(QRegularExpression("(?)"), // SESSION_TOKEN RegReplace(QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), "new refresh token: \"\""), // refresh token RegReplace(QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), "\"device_code\" : \"\""), // device code }; void anonymizeLog(QString& log) { for (auto rule : anonymizeRules) { log.replace(rule.reg, rule.with); } } PrismLauncher-10.0.5/launcher/logs/LogParser.h0000644000175100017510000000402415144136756020607 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include #include #include #include #include #include #include "MessageLevel.h" class LogParser { public: struct LogEntry { QString logger; MessageLevel level; QString levelText; QDateTime timestamp; QString thread; QString message; }; struct Partial { QString data; }; struct PlainText { QString message; }; struct Error { QString errMessage; QXmlStreamReader::Error error; }; using ParsedItem = std::variant; public: LogParser() = default; void appendLine(QAnyStringView data); std::optional parseNext(); QList parseAvailable(); std::optional getError(); /// guess log level from a line of game log static MessageLevel guessLevel(const QString& line, MessageLevel previous); protected: std::optional parseAttributes(); void setError(); void clearError(); std::optional parseLog4J(); private: QString m_buffer; QString m_partialData; QXmlStreamReader m_parser; std::optional m_error; }; PrismLauncher-10.0.5/launcher/RuntimeContext.h0000644000175100017510000000450515144136756020741 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "SysInfo.h" #include "settings/SettingsObject.h" struct RuntimeContext { QString javaArchitecture; QString javaRealArchitecture; QString system = SysInfo::currentSystem(); QString mappedJavaRealArchitecture() const { if (javaRealArchitecture == "amd64") return "x86_64"; if (javaRealArchitecture == "i386" || javaRealArchitecture == "i686") return "x86"; if (javaRealArchitecture == "aarch64") return "arm64"; if (javaRealArchitecture == "arm" || javaRealArchitecture == "armhf") return "arm32"; return javaRealArchitecture; } void updateFromInstanceSettings(SettingsObjectPtr instanceSettings) { javaArchitecture = instanceSettings->get("JavaArchitecture").toString(); javaRealArchitecture = instanceSettings->get("JavaRealArchitecture").toString(); } QString getClassifier() const { return system + "-" + mappedJavaRealArchitecture(); } // "Legacy" refers to the fact that Mojang assumed that these are the only two architectures bool isLegacyArch() const { const QString mapped = mappedJavaRealArchitecture(); return mapped == "x86_64" || mapped == "x86"; } bool classifierMatches(QString target) const { // try to match precise classifier "[os]-[arch]" bool x = target == getClassifier(); // try to match imprecise classifier on legacy architectures "[os]" if (!x && isLegacyArch()) x = target == system; return x; } }; PrismLauncher-10.0.5/launcher/minecraft/0000755000175100017510000000000015144136756017544 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/minecraft/World.h0000644000175100017510000000604415144136756021010 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include struct GameType { GameType() = default; GameType(std::optional original); QString toTranslatedString() const; QString toLogString() const; enum { Unknown = -1, Survival, Creative, Adventure, Spectator } type = Unknown; std::optional original; }; class World { public: World(const QFileInfo& file); QString folderName() const { return m_folderName; } QString name() const { return m_actualName; } QString iconFile() const { return m_iconFile; } int64_t bytes() const { return m_size; } QDateTime lastPlayed() const { return m_lastPlayed; } GameType gameType() const { return m_gameType; } int64_t seed() const { return m_randomSeed; } bool isValid() const { return m_isValid; } bool isOnFS() const { return m_containerFile.isDir(); } QFileInfo container() const { return m_containerFile; } // delete all the files of this world bool destroy(); // replace this world with a copy of the other bool replace(World& with); // change the world's filesystem path (used by world lists for *MAGIC* purposes) void repath(const QFileInfo& file); // remove the icon file, if any bool resetIcon(); bool rename(const QString& to); bool install(const QString& to, const QString& name = QString()); void setSize(int64_t size); // WEAK compare operator - used for replacing worlds bool operator==(const World& other) const; auto isSymLink() const -> bool { return m_containerFile.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance * * @param instPath path to an instance directory * @return true * @return false */ bool isSymLinkUnder(const QString& instPath) const; bool isMoreThanOneHardLink() const; QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); } private: void readFromZip(const QFileInfo& file); void readFromFS(const QFileInfo& file); void loadFromLevelDat(QByteArray data); protected: QFileInfo m_containerFile; QString m_containerOffsetPath; QString m_folderName; QString m_actualName; QString m_iconFile; QDateTime m_levelDatTime; QDateTime m_lastPlayed; int64_t m_size = 0; int64_t m_randomSeed = 0; GameType m_gameType; bool m_isValid = false; }; PrismLauncher-10.0.5/launcher/minecraft/ComponentUpdateTask.h0000644000175100017510000000165515144136756023654 0ustar runnerrunner#pragma once #include "minecraft/Component.h" #include "net/Mode.h" #include "tasks/Task.h" #include class PackProfile; struct ComponentUpdateTaskData; class ComponentUpdateTask : public Task { Q_OBJECT public: enum class Mode { Launch, Resolution }; public: explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list); virtual ~ComponentUpdateTask(); protected: void executeTask(); private: void loadComponents(); /// collects components that are dependent on or dependencies of the component QList collectTreeLinked(const QString& uid); void resolveDependencies(bool checkOnly); void performUpdateActions(); void finalizeComponents(); void remoteLoadSucceeded(size_t index); void remoteLoadFailed(size_t index, const QString& msg); void checkIfAllFinished(); private: std::unique_ptr d; }; PrismLauncher-10.0.5/launcher/minecraft/VanillaInstanceCreationTask.cpp0000644000175100017510000000241515144136756025635 0ustar runnerrunner#include "VanillaInstanceCreationTask.h" #include #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "settings/INISettingsObject.h" VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version) : InstanceCreationTask() , m_version(std::move(version)) , m_using_loader(true) , m_loader(std::move(loader)) , m_loader_version(std::move(loader_version)) {} bool VanillaCreationTask::createInstance() { setStatus(tr("Creating instance from version %1").arg(m_version->name())); auto instance_settings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); instance_settings->suspendSave(); { MinecraftInstance inst(m_globalSettings, instance_settings, m_stagingPath); auto components = inst.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_version->descriptor(), true); if (m_using_loader) components->setComponentVersion(m_loader, m_loader_version->descriptor()); inst.setName(name()); inst.setIconKey(m_instIcon); } instance_settings->resumeSave(); return true; } PrismLauncher-10.0.5/launcher/minecraft/ComponentUpdateTask.cpp0000644000175100017510000010314015144136756024177 0ustar runnerrunner#include "ComponentUpdateTask.h" #include #include "Component.h" #include "ComponentUpdateTask_p.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "ProblemProvider.h" #include "Version.h" #include "cassert" #include "meta/Index.h" #include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/ProfileUtils.h" #include "net/Mode.h" #include "Application.h" #include "tasks/Task.h" #include "minecraft/Logging.h" /* * This is responsible for loading the components of a component list AND resolving dependency issues between them */ /* * FIXME: the 'one shot async task' nature of this does not fit the intended usage * Really, it should be a reactor/state machine that receives input from the application * and dynamically adapts to changing requirements... * * The reactor should be the only entry into manipulating the PackProfile. * See: https://en.wikipedia.org/wiki/Reactor_pattern */ /* * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and PackProfile didn't change? * If the component list changes, start over. */ ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task() { d.reset(new ComponentUpdateTaskData); d->m_profile = list; d->mode = mode; d->netmode = netmode; } ComponentUpdateTask::~ComponentUpdateTask() {} void ComponentUpdateTask::executeTask() { qCDebug(instanceProfileResolveC) << "Loading components"; loadComponents(); } namespace { enum class LoadResult { LoadedLocal, RequiresRemote, Failed }; LoadResult composeLoadResult(LoadResult a, LoadResult b) { if (a < b) { return b; } return a; } static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) { if (component->m_loaded) { qCDebug(instanceProfileResolveC) << component->getName() << "is already loaded"; return LoadResult::LoadedLocal; } LoadResult result = LoadResult::Failed; auto customPatchFilename = component->getFilename(); if (QFile::exists(customPatchFilename)) { // if local file exists... // check for uid problems inside... bool fileChanged = false; auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false); if (file->uid != component->m_uid) { file->uid = component->m_uid; fileChanged = true; } if (fileChanged) { // FIXME: @QUALITY do not ignore return value ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename); } component->m_file = file; component->m_loaded = true; result = LoadResult::LoadedLocal; } else { auto metaVersion = APPLICATION->metadataIndex()->get(component->m_uid, component->m_version); component->m_metaVersion = metaVersion; if (metaVersion->isLoaded()) { component->m_loaded = true; result = LoadResult::LoadedLocal; } else { loadTask = APPLICATION->metadataIndex()->loadVersion(component->m_uid, component->m_version, netmode); loadTask->start(); if (netmode == Net::Mode::Online) result = LoadResult::RequiresRemote; else if (metaVersion->isLoaded()) result = LoadResult::LoadedLocal; else result = LoadResult::Failed; } } return result; } // FIXME: dead code. determine if this can still be useful? /* static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) { if(component->m_loaded) { qDebug() << component->getName() << "is already loaded"; return LoadResult::LoadedLocal; } LoadResult result = LoadResult::Failed; auto metaList = APPLICATION->metadataIndex()->get(component->m_uid); if(metaList->isLoaded()) { component->m_loaded = true; result = LoadResult::LoadedLocal; } else { metaList->load(netmode); loadTask = metaList->getCurrentTask(); result = LoadResult::RequiresRemote; } return result; } */ } // namespace void ComponentUpdateTask::loadComponents() { LoadResult result = LoadResult::LoadedLocal; size_t taskIndex = 0; size_t componentIndex = 0; d->remoteLoadSuccessful = true; // load all the components OR their lists... for (auto component : d->m_profile->d->components) { Task::Ptr loadTask; LoadResult singleResult; RemoteLoadStatus::Type loadType; component->resetComponentProblems(); // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, // ignore all that... #if 0 switch(d->mode) { case Mode::Launch: { singleResult = loadComponent(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::Version; break; } case Mode::Resolution: { singleResult = loadPackProfile(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::List; break; } } #else singleResult = loadComponent(component, loadTask, d->netmode); loadType = RemoteLoadStatus::Type::Version; #endif if (singleResult == LoadResult::LoadedLocal) { component->updateCachedData(); } result = composeLoadResult(result, singleResult); if (loadTask) { qCDebug(instanceProfileResolveC) << d->m_profile->d->m_instance->name() << "|" << "Remote loading is being run for" << component->getName(); connect(loadTask.get(), &Task::succeeded, this, [this, taskIndex]() { remoteLoadSucceeded(taskIndex); }); connect(loadTask.get(), &Task::failed, this, [this, taskIndex](const QString& error) { remoteLoadFailed(taskIndex, error); }); connect(loadTask.get(), &Task::aborted, this, [this, taskIndex]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); RemoteLoadStatus status; status.type = loadType; status.PackProfileIndex = componentIndex; status.task = loadTask; d->remoteLoadStatusList.append(status); taskIndex++; } componentIndex++; } d->remoteTasksInProgress = taskIndex; switch (result) { case LoadResult::LoadedLocal: { // Everything got loaded. Advance to dependency resolution. performUpdateActions(); resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline); break; } case LoadResult::RequiresRemote: { // we wait for signals. break; } case LoadResult::Failed: { emitFailed(tr("Some component metadata load tasks failed.")); break; } } } namespace { struct RequireEx : public Meta::Require { size_t indexOfFirstDependee = 0; }; struct RequireCompositionResult { bool ok; RequireEx outcome; }; using RequireExSet = std::set; } // namespace static RequireCompositionResult composeRequirement(const RequireEx& a, const RequireEx& b) { assert(a.uid == b.uid); RequireEx out; out.uid = a.uid; out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee); if (a.equalsVersion.isEmpty()) { out.equalsVersion = b.equalsVersion; } else if (b.equalsVersion.isEmpty()) { out.equalsVersion = a.equalsVersion; } else if (a.equalsVersion == b.equalsVersion) { out.equalsVersion = a.equalsVersion; } else { // FIXME: mark error as explicit version conflict return { false, out }; } if (a.suggests.isEmpty()) { out.suggests = b.suggests; } else if (b.suggests.isEmpty()) { out.suggests = a.suggests; } else { Version aVer(a.suggests); Version bVer(b.suggests); out.suggests = (aVer < bVer ? b.suggests : a.suggests); } return { true, out }; } // gather the requirements from all components, finding any obvious conflicts static bool gatherRequirementsFromComponents(const ComponentContainer& input, RequireExSet& output) { bool succeeded = true; size_t componentNum = 0; for (auto component : input) { auto& componentRequires = component->m_cachedRequires; for (const auto& componentRequire : componentRequires) { auto found = std::find_if(output.cbegin(), output.cend(), [componentRequire](const Meta::Require& req) { return req.uid == componentRequire.uid; }); RequireEx componenRequireEx; componenRequireEx.uid = componentRequire.uid; componenRequireEx.suggests = componentRequire.suggests; componenRequireEx.equalsVersion = componentRequire.equalsVersion; componenRequireEx.indexOfFirstDependee = componentNum; if (found != output.cend()) { // found... process it further auto result = composeRequirement(componenRequireEx, *found); if (result.ok) { output.erase(componenRequireEx); output.insert(result.outcome); } else { qCCritical(instanceProfileResolveC) << "Conflicting requirements:" << componentRequire.uid << "versions:" << componentRequire.equalsVersion << ";" << (*found).equalsVersion; } succeeded &= result.ok; } else { // not found, accumulate output.insert(componenRequireEx); } } componentNum++; } return succeeded; } /// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps) static void getTrivialRemovals(const ComponentContainer& components, const RequireExSet& reqs, QStringList& toRemove) { for (const auto& component : components) { if (!component->m_dependencyOnly) continue; if (!component->m_cachedVolatile) continue; RequireEx reqNeedle; reqNeedle.uid = component->m_uid; const auto iter = reqs.find(reqNeedle); if (iter == reqs.cend()) { toRemove.append(component->m_uid); } } } /** * handles: * - trivial addition (there is an unmet requirement and it can be trivially met by adding something) * - trivial version conflict of dependencies == explicit version required and installed is different * * toAdd - set of requirements than mean adding a new component * toChange - set of requirements that mean changing version of an existing component */ static bool getTrivialComponentChanges(const ComponentIndex& index, const RequireExSet& input, RequireExSet& toAdd, RequireExSet& toChange) { enum class Decision { Undetermined, Met, Missing, VersionNotSame, LockedVersionNotSame } decision = Decision::Undetermined; QString reqStr; bool succeeded = true; // list the composed requirements and say if they are met or unmet for (auto& req : input) { do { if (req.equalsVersion.isEmpty()) { reqStr = QString("Req: %1").arg(req.uid); if (index.contains(req.uid)) { decision = Decision::Met; } else { toAdd.insert(req); decision = Decision::Missing; } break; } else { reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion); const auto& compIter = index.find(req.uid); if (compIter == index.cend()) { toAdd.insert(req); decision = Decision::Missing; break; } auto& comp = (*compIter); if (comp->getVersion() != req.equalsVersion) { if (comp->isCustom()) { decision = Decision::LockedVersionNotSame; } else { if (comp->m_dependencyOnly) { decision = Decision::VersionNotSame; } else { decision = Decision::LockedVersionNotSame; } } break; } decision = Decision::Met; } } while (false); switch (decision) { case Decision::Undetermined: qCCritical(instanceProfileResolveC) << "No decision for" << reqStr; succeeded = false; break; case Decision::Met: qCDebug(instanceProfileResolveC) << reqStr << "Is met."; break; case Decision::Missing: qCDebug(instanceProfileResolveC) << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; toAdd.insert(req); break; case Decision::VersionNotSame: qCDebug(instanceProfileResolveC) << reqStr << "already has different version that can be changed."; toChange.insert(req); break; case Decision::LockedVersionNotSame: qCDebug(instanceProfileResolveC) << reqStr << "already has different version that cannot be changed."; succeeded = false; break; } } return succeeded; } ComponentContainer ComponentUpdateTask::collectTreeLinked(const QString& uid) { ComponentContainer linked; auto& components = d->m_profile->d->components; auto& componentIndex = d->m_profile->d->componentIndex; auto& instance = d->m_profile->d->m_instance; for (auto comp : components) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "scanning" << comp->getID() << ":" << comp->getVersion() << "for tree link"; auto dep = std::find_if(comp->m_cachedRequires.cbegin(), comp->m_cachedRequires.cend(), [uid](const Meta::Require& req) -> bool { return req.uid == uid; }); if (dep != comp->m_cachedRequires.cend()) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "depends on" << uid; linked.append(comp); } } auto iter = componentIndex.find(uid); if (iter != componentIndex.end()) { ComponentPtr comp = *iter; comp->updateCachedData(); qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "has" << comp->m_cachedRequires.size() << "dependencies"; for (auto dep : comp->m_cachedRequires) { qCDebug(instanceProfileC) << instance->name() << "|" << uid << "depends on" << dep.uid; auto found = componentIndex.find(dep.uid); if (found != componentIndex.end()) { qCDebug(instanceProfileC) << instance->name() << "|" << (*found)->getID() << "is present"; linked.append(*found); } } } return linked; } // FIXME, TODO: decouple dependency resolution from loading // FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. // FIXME: throw all this away and use a graph void ComponentUpdateTask::resolveDependencies(bool checkOnly) { qCDebug(instanceProfileResolveC) << "Resolving dependencies"; /* * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways: * 1. There are conflicting dependencies on the same uid with different exact version numbers * -> hard error * 2. A dependency has non-matching exact version number * -> hard error * 3. A dependency is entirely missing and needs to be injected before the dependee(s) * -> requirements are injected * * NOTE: this is a placeholder and should eventually be replaced with something 'serious' */ auto& components = d->m_profile->d->components; auto& componentIndex = d->m_profile->d->componentIndex; RequireExSet allRequires; QStringList toRemove; do { allRequires.clear(); toRemove.clear(); if (!gatherRequirementsFromComponents(components, allRequires)) { finalizeComponents(); emitFailed(tr("Conflicting requirements detected during dependency checking!")); return; } getTrivialRemovals(components, allRequires, toRemove); if (!toRemove.isEmpty()) { qCDebug(instanceProfileResolveC) << "Removing obsolete components..."; for (auto& remove : toRemove) { qCDebug(instanceProfileResolveC) << "Removing" << remove; d->m_profile->remove(remove); } } } while (!toRemove.isEmpty()); RequireExSet toAdd; RequireExSet toChange; bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange); if (!succeeded) { finalizeComponents(); emitFailed(tr("Instance has conflicting dependencies.")); return; } if (checkOnly) { finalizeComponents(); if (toAdd.size() || toChange.size()) { emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch.")); } else { emitSucceeded(); } return; } bool recursionNeeded = false; if (toAdd.size()) { // add stuff... for (auto& add : toAdd) { auto component = makeShared(d->m_profile, add.uid); if (!add.equalsVersion.isEmpty()) { // exact version qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee; component->m_version = add.equalsVersion; } else { // version needs to be decided qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; // ############################################################################################################ // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. if (!add.suggests.isEmpty()) { component->m_version = add.suggests; } else { if (add.uid == "org.lwjgl") { component->m_version = "2.9.1"; } else if (add.uid == "org.lwjgl3") { component->m_version = "3.1.2"; } else if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.hashed") { auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr& cmp) { return cmp->getID() == "net.minecraft"; }); if (minecraft != components.end()) { component->m_version = (*minecraft)->getVersion(); } } } // HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. // ############################################################################################################ } component->m_dependencyOnly = true; // FIXME: this should not work directly with the component list d->m_profile->insertComponent(add.indexOfFirstDependee, component); componentIndex[add.uid] = component; } recursionNeeded = true; } if (toChange.size()) { // change a version of something that exists for (auto& change : toChange) { // FIXME: this should not work directly with the component list qCDebug(instanceProfileResolveC) << "Setting version of" << change.uid << "to" << change.equalsVersion; auto component = componentIndex[change.uid]; component->setVersion(change.equalsVersion); } recursionNeeded = true; } if (recursionNeeded) { loadComponents(); } else { finalizeComponents(); emitSucceeded(); } } // Variant visitation via lambda template struct overload : Ts... { using Ts::operator()...; }; template overload(Ts...) -> overload; void ComponentUpdateTask::performUpdateActions() { auto& instance = d->m_profile->d->m_instance; bool addedActions; QStringList toRemove; do { addedActions = false; toRemove.clear(); auto& components = d->m_profile->d->components; auto& componentIndex = d->m_profile->d->componentIndex; for (auto component : components) { if (!component) { continue; } auto action = component->getUpdateAction(); auto visitor = overload{ [](const UpdateActionNone&) { // noop }, [&component, &instance](const UpdateActionChangeVersion& cv) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionChangeVersion" << component->getID() << ":" << component->getVersion() << "change to" << cv.targetVersion; component->setVersion(cv.targetVersion); component->waitLoadMeta(); }, [&component, &instance](const UpdateActionLatestRecommendedCompatible& lrc) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" << component->getVersion() << "updating to latest recommend or compatible with" << lrc.parentUid << lrc.version; auto versionList = APPLICATION->metadataIndex()->get(component->getID()); if (versionList) { versionList->waitToLoad(); auto recommended = versionList->getRecommendedForParent(lrc.parentUid, lrc.version); if (!recommended) { recommended = versionList->getLatestForParent(lrc.parentUid, lrc.version); } if (recommended) { component->setVersion(recommended->version()); component->waitLoadMeta(); return; } else { component->addComponentProblem(ProblemSeverity::Error, QObject::tr("No compatible version of %1 found for %2 %3") .arg(component->getName(), lrc.parentName, lrc.version)); } } else { component->addComponentProblem( ProblemSeverity::Error, QObject::tr("No version list in metadata index for %1").arg(component->getID())); } }, [&component, &instance, &toRemove](const UpdateActionRemove&) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionRemove" << component->getID() << ":" << component->getVersion() << "removing"; toRemove.append(component->getID()); }, [this, &component, &instance, &addedActions, &componentIndex](const UpdateActionImportantChanged& ic) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateImportantChanged" << component->getID() << ":" << component->getVersion() << "was changed from" << ic.oldVersion << "updating linked components"; auto oldVersion = APPLICATION->metadataIndex()->getLoadedVersion(component->getID(), ic.oldVersion); for (auto oldReq : oldVersion->requiredSet()) { auto currentlyRequired = component->m_cachedRequires.find(oldReq); if (currentlyRequired == component->m_cachedRequires.cend()) { auto oldReqComp = componentIndex.find(oldReq.uid); if (oldReqComp != componentIndex.cend()) { (*oldReqComp)->setUpdateAction(UpdateAction{ UpdateActionRemove{} }); addedActions = true; } } } auto linked = collectTreeLinked(component->getID()); for (auto comp : linked) { if (comp->isCustom()) { continue; } auto compUid = comp->getID(); auto parentReq = std::find_if(component->m_cachedRequires.begin(), component->m_cachedRequires.end(), [compUid](const Meta::Require& req) { return req.uid == compUid; }); if (parentReq != component->m_cachedRequires.end()) { auto newVersion = parentReq->equalsVersion.isEmpty() ? parentReq->suggests : parentReq->equalsVersion; if (!newVersion.isEmpty()) { comp->setUpdateAction(UpdateAction{ UpdateActionChangeVersion{ newVersion } }); } else { comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ component->getID(), component->getName(), component->getVersion(), } }); } } else { comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ component->getID(), component->getName(), component->getVersion(), } }); } addedActions = true; } } }; std::visit(visitor, action); component->clearUpdateAction(); for (auto uid : toRemove) { d->m_profile->remove(uid); } } } while (addedActions); } void ComponentUpdateTask::finalizeComponents() { auto& components = d->m_profile->d->components; auto& componentIndex = d->m_profile->d->componentIndex; for (auto component : components) { for (auto req : component->m_cachedRequires) { auto found = componentIndex.find(req.uid); if (found == componentIndex.cend()) { component->addComponentProblem( ProblemSeverity::Error, QObject::tr("%1 is missing requirement %2 %3") .arg(component->getName(), req.uid, req.equalsVersion.isEmpty() ? req.suggests : req.equalsVersion)); } else { auto reqComp = *found; if (!reqComp->getProblems().isEmpty()) { component->addComponentProblem( reqComp->getProblemSeverity(), QObject::tr("%1, a dependency of this component, has reported issues").arg(reqComp->getName())); } if (!req.equalsVersion.isEmpty() && req.equalsVersion != reqComp->getVersion()) { component->addComponentProblem(ProblemSeverity::Error, QObject::tr("%1, a dependency of this component, is not the required version %2") .arg(reqComp->getName(), req.equalsVersion)); } else if (!req.suggests.isEmpty() && req.suggests != reqComp->getVersion()) { component->addComponentProblem(ProblemSeverity::Warning, QObject::tr("%1, a dependency of this component, is not the suggested version %2") .arg(reqComp->getName(), req.suggests)); } } } for (auto conflict : component->knownConflictingComponents()) { auto found = componentIndex.find(conflict); if (found != componentIndex.cend()) { auto foundComp = *found; if (foundComp->isCustom()) { continue; } component->addComponentProblem( ProblemSeverity::Warning, QObject::tr("%1 and %2 are known to not work together. It is recommended to remove one of them.") .arg(component->getName(), foundComp->getName())); } } } } void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) { if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; return; } auto& taskSlot = d->remoteLoadStatusList[taskIndex]; disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); if (taskSlot.finished) { qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; return; } qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "succeeded"; taskSlot.succeeded = false; taskSlot.finished = true; d->remoteTasksInProgress--; // update the cached data of the component from the downloaded version file. if (taskSlot.type == RemoteLoadStatus::Type::Version) { auto component = d->m_profile->getComponent(taskSlot.PackProfileIndex); component->m_loaded = true; component->updateCachedData(); } checkIfAllFinished(); } void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) { if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; return; } auto& taskSlot = d->remoteLoadStatusList[taskIndex]; disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); if (taskSlot.finished) { qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; return; } qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed:" << msg; d->remoteLoadSuccessful = false; taskSlot.succeeded = false; taskSlot.finished = true; d->remoteTasksInProgress--; checkIfAllFinished(); } void ComponentUpdateTask::checkIfAllFinished() { if (d->remoteTasksInProgress) { // not yet... return; } if (d->remoteLoadSuccessful) { // nothing bad happened... clear the temp load status and proceed with looking at dependencies d->remoteLoadStatusList.clear(); performUpdateActions(); resolveDependencies(d->mode == Mode::Launch); } else { // remote load failed... report error and bail QStringList allErrorsList; for (auto& item : d->remoteLoadStatusList) { if (!item.succeeded) { const ComponentPtr component = d->m_profile->getComponent(item.PackProfileIndex); allErrorsList.append(tr("Could not download metadata for %1 %2. Please change the version or try again later.") .arg(component->getName(), component->m_version)); } } d->remoteLoadStatusList.clear(); auto allErrors = allErrorsList.join("\n"); emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors)); } } PrismLauncher-10.0.5/launcher/minecraft/MojangVersionFormat.h0000644000175100017510000000153715144136756023655 0ustar runnerrunner#pragma once #include #include #include #include class MojangVersionFormat { friend class OneSixVersionFormat; protected: // does not include libraries static void readVersionProperties(const QJsonObject& in, VersionFile* out); // does not include libraries static void writeVersionProperties(const VersionFile* in, QJsonObject& out); public: // version files / profile patches static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename); static QJsonDocument versionFileToJson(const VersionFilePtr& patch); // libraries static LibraryPtr libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); static QJsonObject libraryToJson(Library* library); }; PrismLauncher-10.0.5/launcher/minecraft/OneSixVersionFormat.cpp0000644000175100017510000003636315144136756024207 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "OneSixVersionFormat.h" #include #include #include #include "java/JavaMetadata.h" #include "minecraft/Agent.h" #include "minecraft/ParseUtils.h" #include using namespace Json; static void readString(const QJsonObject& root, const QString& key, QString& variable) { if (root.contains(key)) { variable = requireString(root.value(key)); } } LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename) { LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename); readString(libObj, "MMC-hint", out->m_hint); readString(libObj, "MMC-absulute_url", out->m_absoluteURL); readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); readString(libObj, "MMC-filename", out->m_filename); readString(libObj, "MMC-displayname", out->m_displayname); return out; } QJsonObject OneSixVersionFormat::libraryToJson(Library* library) { QJsonObject libRoot = MojangVersionFormat::libraryToJson(library); if (!library->m_absoluteURL.isEmpty()) libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL); if (!library->m_hint.isEmpty()) libRoot.insert("MMC-hint", library->m_hint); if (!library->m_filename.isEmpty()) libRoot.insert("MMC-filename", library->m_filename); if (!library->m_displayname.isEmpty()) libRoot.insert("MMC-displayname", library->m_displayname); return libRoot; } VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc, const QString& filename, const bool requireOrder) { VersionFilePtr out(new VersionFile()); if (doc.isEmpty() || doc.isNull()) { throw JSONValidationError(filename + " is empty or null"); } if (!doc.isObject()) { throw JSONValidationError(filename + " is not an object"); } QJsonObject root = doc.object(); Meta::MetadataVersion formatVersion = Meta::parseFormatVersion(root, false); switch (formatVersion) { case Meta::MetadataVersion::InitialRelease: break; case Meta::MetadataVersion::Invalid: throw JSONValidationError(filename + " does not contain a recognizable version of the metadata format."); } if (requireOrder) { if (root.contains("order")) { out->order = requireInteger(root.value("order")); } else { // FIXME: evaluate if we don't want to throw exceptions here instead qCritical() << filename << "doesn't contain an order field"; } } out->name = root.value("name").toString(); if (root.contains("uid")) { out->uid = root.value("uid").toString(); } else { out->uid = root.value("fileId").toString(); } static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern( QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; if (!s_validUidRegex.match(out->uid).hasMatch()) { qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; out->addProblem(ProblemSeverity::Error, QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); } out->version = root.value("version").toString(); MojangVersionFormat::readVersionProperties(root, out.get()); // added for legacy Minecraft window embedding, TODO: remove readString(root, "appletClass", out->appletClass); if (root.contains("+tweakers")) { for (auto tweakerVal : requireArray(root.value("+tweakers"))) { out->addTweakers.append(requireString(tweakerVal)); } } if (root.contains("+traits")) { for (auto tweakerVal : requireArray(root.value("+traits"))) { out->traits.insert(requireString(tweakerVal)); } } if (root.contains("+jvmArgs")) { for (auto arg : requireArray(root.value("+jvmArgs"))) { out->addnJvmArguments.append(requireString(arg)); } } if (root.contains("jarMods")) { for (auto libVal : requireArray(root.value("jarMods"))) { QJsonObject libObj = requireObject(libVal); // parse the jarmod auto lib = OneSixVersionFormat::jarModFromJson(*out, libObj, filename); // and add to jar mods out->jarMods.append(lib); } } else if (root.contains("+jarMods")) // DEPRECATED: old style '+jarMods' are only here for backwards compatibility { for (auto libVal : requireArray(root.value("+jarMods"))) { QJsonObject libObj = requireObject(libVal); // parse the jarmod auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name); // and add to jar mods out->jarMods.append(lib); } } if (root.contains("mods")) { for (auto libVal : requireArray(root.value("mods"))) { QJsonObject libObj = requireObject(libVal); // parse the jarmod auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename); // and add to jar mods out->mods.append(lib); } } auto readLibs = [&root, &out, &filename](const char* which, QList& outList) { for (auto libVal : requireArray(root.value(which))) { QJsonObject libObj = requireObject(libVal); // parse the library auto lib = libraryFromJson(*out, libObj, filename); outList.append(lib); } }; bool hasPlusLibs = root.contains("+libraries"); bool hasLibs = root.contains("libraries"); if (hasPlusLibs && hasLibs) { out->addProblem(ProblemSeverity::Warning, QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); readLibs("libraries", out->libraries); readLibs("+libraries", out->libraries); } else if (hasLibs) { readLibs("libraries", out->libraries); } else if (hasPlusLibs) { readLibs("+libraries", out->libraries); } if (root.contains("mavenFiles")) { readLibs("mavenFiles", out->mavenFiles); } if (root.contains("+agents")) { for (auto agentVal : requireArray(root.value("+agents"))) { QJsonObject agentObj = requireObject(agentVal); auto lib = libraryFromJson(*out, agentObj, filename); QString arg = ""; readString(agentObj, "argument", arg); AgentPtr agent(new Agent(lib, arg)); out->agents.append(agent); } } // if we have mainJar, just use it if (root.contains("mainJar")) { QJsonObject libObj = requireObject(root, "mainJar"); out->mainJar = libraryFromJson(*out, libObj, filename); } // else reconstruct it from downloads and id ... if that's available else if (!out->minecraftVersion.isEmpty()) { auto lib = std::make_shared(); lib->setRawName(GradleSpecifier(QString("com.mojang:minecraft:%1:client").arg(out->minecraftVersion))); // we have a reliable client download, use it. if (out->mojangDownloads.contains("client")) { auto LibDLInfo = std::make_shared(); LibDLInfo->artifact = out->mojangDownloads["client"]; lib->setMojangDownloadInfo(LibDLInfo); } // we got nothing... else { out->addProblem( ProblemSeverity::Error, QObject::tr("URL for the main jar could not be determined - Mojang removed the server that we used as fallback.")); } out->mainJar = lib; } if (root.contains("requires")) { Meta::parseRequires(root, &out->m_requires); } QString dependsOnMinecraftVersion = root.value("mcVersion").toString(); if (!dependsOnMinecraftVersion.isEmpty()) { Meta::Require mcReq; mcReq.uid = "net.minecraft"; mcReq.equalsVersion = dependsOnMinecraftVersion; if (out->m_requires.count(mcReq) == 0) { out->m_requires.insert(mcReq); } } if (root.contains("conflicts")) { Meta::parseRequires(root, &out->conflicts); } if (root.contains("volatile")) { out->m_volatile = requireBoolean(root, "volatile"); } if (root.contains("runtimes")) { out->runtimes = {}; for (auto runtime : root["runtimes"].toArray()) { out->runtimes.append(Java::parseJavaMeta(runtime.toObject())); } } /* removed features that shouldn't be used */ if (root.contains("tweakers")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element 'tweakers'")); } if (root.contains("-libraries")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-libraries'")); } if (root.contains("-tweakers")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-tweakers'")); } if (root.contains("-minecraftArguments")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-minecraftArguments'")); } if (root.contains("+minecraftArguments")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '+minecraftArguments'")); } return out; } QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr& patch) { QJsonObject root; writeString(root, "name", patch->name); writeString(root, "uid", patch->uid); writeString(root, "version", patch->version); Meta::serializeFormatVersion(root, Meta::MetadataVersion::InitialRelease); MojangVersionFormat::writeVersionProperties(patch.get(), root); if (patch->mainJar) { root.insert("mainJar", libraryToJson(patch->mainJar.get())); } writeString(root, "appletClass", patch->appletClass); writeStringList(root, "+tweakers", patch->addTweakers); writeStringList(root, "+traits", patch->traits.values()); writeStringList(root, "+jvmArgs", patch->addnJvmArguments); if (!patch->agents.isEmpty()) { QJsonArray array; for (auto value : patch->agents) { QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get()); if (!value->argument().isEmpty()) agentOut.insert("argument", value->argument()); array.append(agentOut); } root.insert("+agents", array); } if (!patch->libraries.isEmpty()) { QJsonArray array; for (auto value : patch->libraries) { array.append(OneSixVersionFormat::libraryToJson(value.get())); } root.insert("libraries", array); } if (!patch->mavenFiles.isEmpty()) { QJsonArray array; for (auto value : patch->mavenFiles) { array.append(OneSixVersionFormat::libraryToJson(value.get())); } root.insert("mavenFiles", array); } if (!patch->jarMods.isEmpty()) { QJsonArray array; for (auto value : patch->jarMods) { array.append(OneSixVersionFormat::jarModtoJson(value.get())); } root.insert("jarMods", array); } if (!patch->mods.isEmpty()) { QJsonArray array; for (auto value : patch->jarMods) { array.append(OneSixVersionFormat::modtoJson(value.get())); } root.insert("mods", array); } if (!patch->m_requires.empty()) { Meta::serializeRequires(root, &patch->m_requires, "requires"); } if (!patch->conflicts.empty()) { Meta::serializeRequires(root, &patch->conflicts, "conflicts"); } if (patch->m_volatile) { root.insert("volatile", true); } // write the contents to a json document. { QJsonDocument out; out.setObject(root); return out; } } LibraryPtr OneSixVersionFormat::plusJarModFromJson([[maybe_unused]] ProblemContainer& problems, const QJsonObject& libObj, const QString& filename, const QString& originalName) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { throw JSONValidationError(filename + "contains a jarmod that doesn't have a 'name' field"); } // just make up something unique on the spot for the library name. auto uuid = QUuid::createUuid(); QString id = uuid.toString().remove('{').remove('}'); out->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); // filename override is the old name out->setFilename(libObj.value("name").toString()); // it needs to be local, it is stored in the instance jarmods folder out->setHint("local"); // read the original name if present - some versions did not set it // it is the original jar mod filename before it got renamed at the point of addition auto displayName = libObj.value("originalName").toString(); if (displayName.isEmpty()) { auto fixed = originalName; fixed.remove(" (jar mod)"); out->setDisplayName(fixed); } else { out->setDisplayName(displayName); } return out; } LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename) { return libraryFromJson(problems, libObj, filename); } QJsonObject OneSixVersionFormat::jarModtoJson(Library* jarmod) { return libraryToJson(jarmod); } LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename) { return libraryFromJson(problems, libObj, filename); } QJsonObject OneSixVersionFormat::modtoJson(Library* jarmod) { return libraryToJson(jarmod); } PrismLauncher-10.0.5/launcher/minecraft/auth/0000755000175100017510000000000015144136756020505 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/minecraft/auth/Parsers.h0000644000175100017510000000135215144136756022276 0ustar runnerrunner#pragma once #include "AccountData.h" namespace Parsers { bool getDateTime(QJsonValue value, QDateTime& out); bool getString(QJsonValue value, QString& out); bool getNumber(QJsonValue value, double& out); bool getNumber(QJsonValue value, int64_t& out); bool getBool(QJsonValue value, bool& out); bool parseXTokenResponse(QByteArray& data, Token& output, QString name); bool parseMojangResponse(QByteArray& data, Token& output); bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output); bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output); bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output); bool parseRolloutResponse(QByteArray& data, bool& result); } // namespace Parsers PrismLauncher-10.0.5/launcher/minecraft/auth/AuthStep.h0000644000175100017510000000237515144136756022422 0ustar runnerrunner#pragma once #include #include #include #include "QObjectPtr.h" #include "minecraft/auth/AccountData.h" /** * Enum for describing the state of the current task. * Used by the getStateMessage function to determine what the status message should be. */ enum class AccountTaskState { STATE_CREATED, STATE_WORKING, STATE_SUCCEEDED, STATE_DISABLED, //!< MSA Client ID has changed. Tell user to reloginn STATE_FAILED_SOFT, //!< soft failure. authentication went through partially STATE_FAILED_HARD, //!< hard failure. main tokens are invalid STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way }; class AuthStep : public QObject { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}; virtual ~AuthStep() noexcept = default; virtual QString describe() = 0; public slots: virtual void perform() = 0; virtual void abort() {} signals: void finished(AccountTaskState resultingState, QString message); protected: AccountData* m_data; }; PrismLauncher-10.0.5/launcher/minecraft/auth/MinecraftAccount.cpp0000644000175100017510000002376515144136756024453 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MinecraftAccount.h" #include #include #include #include #include #include #include #include #include #include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthFlow.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { static const QRegularExpression s_removeChars("[{}-]"); data.internalId = QUuid::createUuid().toString().remove(s_removeChars); } MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { MinecraftAccountPtr account(new MinecraftAccount()); if (account->data.resumeStateFromV3(json)) { return account; } return nullptr; } MinecraftAccountPtr MinecraftAccount::createBlankMSA() { MinecraftAccountPtr account(new MinecraftAccount()); account->data.type = AccountType::MSA; return account; } MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) { static const QRegularExpression s_removeChars("[{}-]"); auto account = makeShared(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "0"; account->data.yggdrasilToken.validity = Validity::Certain; account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(s_removeChars); account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(s_removeChars); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Validity::Certain; return account; } QJsonObject MinecraftAccount::saveToJson() const { return data.saveState(); } AccountState MinecraftAccount::accountState() const { return data.accountState; } QPixmap MinecraftAccount::getFace() const { QPixmap skinTexture; if (!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { return QPixmap(); } QPixmap skin = QPixmap(8, 8); skin.fill(QColorConstants::Transparent); QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); return skin.scaled(64, 64, Qt::KeepAspectRatio); } shared_qobject_ptr MinecraftAccount::login(bool useDeviceCode) { Q_ASSERT(m_currentTask.get() == nullptr); m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; } shared_qobject_ptr MinecraftAccount::refresh() { if (m_currentTask) { return m_currentTask; } m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh)); connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; } shared_qobject_ptr MinecraftAccount::currentTask() { return m_currentTask; } void MinecraftAccount::authSucceeded() { m_currentTask.reset(); emit changed(); emit activityChanged(false); } void MinecraftAccount::authFailed(QString reason) { switch (m_currentTask->taskState()) { case AccountTaskState::STATE_OFFLINE: case AccountTaskState::STATE_DISABLED: { // NOTE: user will need to fix this themselves. } case AccountTaskState::STATE_FAILED_SOFT: { // NOTE: this doesn't do much. There was an error of some sort. } break; case AccountTaskState::STATE_FAILED_HARD: { if (accountType() == AccountType::MSA) { data.msaToken.token = QString(); data.msaToken.refresh_token = QString(); data.msaToken.validity = Validity::None; data.validity_ = Validity::None; } else { data.yggdrasilToken.token = QString(); data.yggdrasilToken.validity = Validity::None; data.validity_ = Validity::None; } emit changed(); } break; case AccountTaskState::STATE_FAILED_GONE: { data.validity_ = Validity::None; emit changed(); } break; case AccountTaskState::STATE_WORKING: { data.accountState = AccountState::Unchecked; } break; case AccountTaskState::STATE_CREATED: case AccountTaskState::STATE_SUCCEEDED: { // Not reachable here, as they are not failures. } } m_currentTask.reset(); emit activityChanged(false); } bool MinecraftAccount::isActive() const { return !m_currentTask.isNull(); } bool MinecraftAccount::shouldRefresh() const { /* * Never refresh accounts that are being used by the game, it breaks the game session. * Always refresh accounts that have not been refreshed yet during this session. * Don't refresh broken accounts. * Refresh accounts that would expire in the next 12 hours (fresh token validity is 24 hours). */ if (isInUse()) { return false; } switch (data.validity_) { case Validity::Certain: { break; } case Validity::None: { return false; } case Validity::Assumed: { return true; } } auto now = QDateTime::currentDateTimeUtc(); auto issuedTimestamp = data.yggdrasilToken.issueInstant; auto expiresTimestamp = data.yggdrasilToken.notAfter; if (!expiresTimestamp.isValid()) { expiresTimestamp = issuedTimestamp.addSecs(24 * 3600); } if (now.secsTo(expiresTimestamp) < (12 * 3600)) { return true; } return false; } void MinecraftAccount::fillSession(AuthSessionPtr session) { static const QRegularExpression s_removeChars("[{}-]"); if (ownsMinecraft() && !hasProfile()) { session->status = AuthSession::RequiresProfileSetup; } else { if (session->wants_online) { session->status = AuthSession::PlayableOnline; } else { session->status = AuthSession::PlayableOffline; } } // volatile auth token session->access_token = data.accessToken(); // profile name session->player_name = data.profileName(); // profile ID session->uuid = data.profileId(); if (session->uuid.isEmpty()) session->uuid = uuidFromUsername(session->player_name).toString().remove(s_removeChars); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { session->session = "token:" + data.accessToken() + ":" + data.profileId(); } else { session->session = "-"; } } void MinecraftAccount::decrementUses() { Usable::decrementUses(); if (!isInUse()) { emit changed(); // FIXME: we now need a better way to identify accounts... qWarning() << "Profile" << data.profileId() << "is no longer in use."; } } void MinecraftAccount::incrementUses() { bool wasInUse = isInUse(); Usable::incrementUses(); if (!wasInUse) { emit changed(); // FIXME: we now need a better way to identify accounts... qWarning() << "Profile" << data.profileId() << "is now in use."; } } QUuid MinecraftAccount::uuidFromUsername(QString username) { auto input = QString("OfflinePlayer:%1").arg(username).toUtf8(); // basically a reimplementation of Java's UUID#nameUUIDFromBytes QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); auto bOr = [](QByteArray& array, qsizetype index, char value) { array[index] |= value; }; auto bAnd = [](QByteArray& array, qsizetype index, char value) { array[index] &= value; }; bAnd(digest, 6, (char)0x0f); // clear version bOr(digest, 6, (char)0x30); // set to version 3 bAnd(digest, 8, (char)0x3f); // clear variant bOr(digest, 8, (char)0x80); // set to IETF variant return QUuid::fromRfc4122(digest); } PrismLauncher-10.0.5/launcher/minecraft/auth/AccountData.cpp0000644000175100017510000003027615144136756023407 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AccountData.h" #include #include #include #include #include namespace { void tokenToJSONV3(QJsonObject& parent, const Token& t, const char* tokenName) { if (!t.persistent) { return; } QJsonObject out; if (t.issueInstant.isValid()) { out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000); } if (t.notAfter.isValid()) { out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000); } bool save = false; if (!t.token.isEmpty()) { out["token"] = QJsonValue(t.token); save = true; } if (!t.refresh_token.isEmpty()) { out["refresh_token"] = QJsonValue(t.refresh_token); save = true; } if (t.extra.size()) { out["extra"] = QJsonObject::fromVariantMap(t.extra); save = true; } if (save) { parent[tokenName] = out; } } Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName) { Token out; auto tokenObject = parent.value(tokenName).toObject(); if (tokenObject.isEmpty()) { return out; } auto issueInstant = tokenObject.value("iat"); if (issueInstant.isDouble()) { out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t)issueInstant.toDouble()) * 1000); } auto notAfter = tokenObject.value("exp"); if (notAfter.isDouble()) { out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t)notAfter.toDouble()) * 1000); } auto token = tokenObject.value("token"); if (token.isString()) { out.token = token.toString(); out.validity = Validity::Assumed; } auto refresh_token = tokenObject.value("refresh_token"); if (refresh_token.isString()) { out.refresh_token = refresh_token.toString(); } auto extra = tokenObject.value("extra"); if (extra.isObject()) { out.extra = extra.toObject().toVariantMap(); } return out; } void profileToJSONV3(QJsonObject& parent, MinecraftProfile p, const char* tokenName) { if (p.id.isEmpty()) { return; } QJsonObject out; out["id"] = QJsonValue(p.id); out["name"] = QJsonValue(p.name); if (!p.currentCape.isEmpty()) { out["cape"] = p.currentCape; } { QJsonObject skinObj; skinObj["id"] = p.skin.id; skinObj["url"] = p.skin.url; skinObj["variant"] = p.skin.variant; if (p.skin.data.size()) { skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); } out["skin"] = skinObj; } QJsonArray capesArray; for (auto& cape : p.capes) { QJsonObject capeObj; capeObj["id"] = cape.id; capeObj["url"] = cape.url; capeObj["alias"] = cape.alias; if (cape.data.size()) { capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); } capesArray.push_back(capeObj); } out["capes"] = capesArray; parent[tokenName] = out; } MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenName) { MinecraftProfile out; auto tokenObject = parent.value(tokenName).toObject(); if (tokenObject.isEmpty()) { return out; } { auto idV = tokenObject.value("id"); auto nameV = tokenObject.value("name"); if (!idV.isString() || !nameV.isString()) { qWarning() << "mandatory profile attributes are missing or of unexpected type"; return MinecraftProfile(); } out.name = nameV.toString(); out.id = idV.toString(); } { auto skinV = tokenObject.value("skin"); if (!skinV.isObject()) { qWarning() << "skin is missing"; return MinecraftProfile(); } auto skinObj = skinV.toObject(); auto idV = skinObj.value("id"); auto urlV = skinObj.value("url"); auto variantV = skinObj.value("variant"); if (!idV.isString() || !urlV.isString() || !variantV.isString()) { qWarning() << "mandatory skin attributes are missing or of unexpected type"; return MinecraftProfile(); } out.skin.id = idV.toString(); out.skin.url = urlV.toString(); out.skin.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); out.skin.variant = variantV.toString(); // data for skin is optional auto dataV = skinObj.value("data"); if (dataV.isString()) { // TODO: validate base64 out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1()); } else if (!dataV.isUndefined()) { qWarning() << "skin data is something unexpected"; return MinecraftProfile(); } } { auto capesV = tokenObject.value("capes"); if (!capesV.isArray()) { qWarning() << "capes is not an array!"; return MinecraftProfile(); } auto capesArray = capesV.toArray(); for (auto capeV : capesArray) { if (!capeV.isObject()) { qWarning() << "cape is not an object!"; return MinecraftProfile(); } auto capeObj = capeV.toObject(); auto idV = capeObj.value("id"); auto urlV = capeObj.value("url"); auto aliasV = capeObj.value("alias"); if (!idV.isString() || !urlV.isString() || !aliasV.isString()) { qWarning() << "mandatory skin attributes are missing or of unexpected type"; return MinecraftProfile(); } Cape cape; cape.id = idV.toString(); cape.url = urlV.toString(); cape.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); cape.alias = aliasV.toString(); // data for cape is optional. auto dataV = capeObj.value("data"); if (dataV.isString()) { // TODO: validate base64 cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); } else if (!dataV.isUndefined()) { qWarning() << "cape data is something unexpected"; return MinecraftProfile(); } out.capes[cape.id] = cape; } } // current cape { auto capeV = tokenObject.value("cape"); if (capeV.isString()) { auto currentCape = capeV.toString(); if (out.capes.contains(currentCape)) { out.currentCape = currentCape; } } } out.validity = Validity::Assumed; return out; } void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p) { if (p.validity == Validity::None) { return; } QJsonObject out; out["ownsMinecraft"] = QJsonValue(p.ownsMinecraft); out["canPlayMinecraft"] = QJsonValue(p.canPlayMinecraft); parent["entitlement"] = out; } bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out) { auto entitlementObject = parent.value("entitlement").toObject(); if (entitlementObject.isEmpty()) { return false; } { auto ownsMinecraftV = entitlementObject.value("ownsMinecraft"); auto canPlayMinecraftV = entitlementObject.value("canPlayMinecraft"); if (!ownsMinecraftV.isBool() || !canPlayMinecraftV.isBool()) { qWarning() << "mandatory attributes are missing or of unexpected type"; return false; } out.canPlayMinecraft = canPlayMinecraftV.toBool(false); out.ownsMinecraft = ownsMinecraftV.toBool(false); out.validity = Validity::Assumed; } return true; } } // namespace bool AccountData::resumeStateFromV3(QJsonObject data) { auto typeV = data.value("type"); if (!typeV.isString()) { qWarning() << "Failed to parse account data: type is missing."; return false; } auto typeS = typeV.toString(); if (typeS == "MSA") { type = AccountType::MSA; } else if (typeS == "Offline") { type = AccountType::Offline; } else { qWarning() << "Failed to parse account data: type is not recognized."; return false; } if (type == AccountType::MSA) { auto clientIDV = data.value("msa-client-id"); if (clientIDV.isString()) { msaClientID = clientIDV.toString(); } // leave msaClientID empty if it doesn't exist or isn't a string msaToken = tokenFromJSONV3(data, "msa"); userToken = tokenFromJSONV3(data, "utoken"); xboxApiToken = tokenFromJSONV3(data, "xrp-main"); mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); } yggdrasilToken = tokenFromJSONV3(data, "ygg"); // versions before 7.2 used "offline" as the offline token if (yggdrasilToken.token == "offline") yggdrasilToken.token = "0"; minecraftProfile = profileFromJSONV3(data, "profile"); if (!entitlementFromJSONV3(data, minecraftEntitlement)) { if (minecraftProfile.validity != Validity::None) { minecraftEntitlement.canPlayMinecraft = true; minecraftEntitlement.ownsMinecraft = true; minecraftEntitlement.validity = Validity::Assumed; } } validity_ = minecraftProfile.validity; return true; } QJsonObject AccountData::saveState() const { QJsonObject output; if (type == AccountType::MSA) { output["type"] = "MSA"; output["msa-client-id"] = msaClientID; tokenToJSONV3(output, msaToken, "msa"); tokenToJSONV3(output, userToken, "utoken"); tokenToJSONV3(output, xboxApiToken, "xrp-main"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); } else if (type == AccountType::Offline) { output["type"] = "Offline"; } tokenToJSONV3(output, yggdrasilToken, "ygg"); profileToJSONV3(output, minecraftProfile, "profile"); entitlementToJSONV3(output, minecraftEntitlement); return output; } QString AccountData::accessToken() const { return yggdrasilToken.token; } QString AccountData::profileId() const { return minecraftProfile.id; } QString AccountData::profileName() const { if (minecraftProfile.name.size() == 0) { return QObject::tr("No profile (%1)").arg(accountDisplayString()); } else { return minecraftProfile.name; } } QString AccountData::accountDisplayString() const { switch (type) { case AccountType::Offline: { return QObject::tr(""); } case AccountType::MSA: { if (xboxApiToken.extra.contains("gtg")) { return xboxApiToken.extra["gtg"].toString(); } return "Xbox profile missing"; } default: { return "Invalid Account"; } } } QString AccountData::lastError() const { return errorString; } PrismLauncher-10.0.5/launcher/minecraft/auth/AccountList.h0000644000175100017510000001310115144136756023102 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "MinecraftAccount.h" #include "minecraft/auth/AuthFlow.h" #include #include #include #include /*! * List of available Mojang accounts. * This should be loaded in the background by Prism Launcher on startup. */ class AccountList : public QAbstractListModel { Q_OBJECT public: enum ModelRoles { PointerRole = 0x34B1CB48 }; enum VListColumns { // TODO: Add icon column. ProfileNameColumn = 0, NameColumn, TypeColumn, StatusColumn, NUM_COLUMNS }; explicit AccountList(QObject* parent = 0); virtual ~AccountList() noexcept; const MinecraftAccountPtr at(int i) const; int count() const; //////// List Model Functions //////// QVariant data(const QModelIndex& index, int role) const override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; virtual int rowCount(const QModelIndex& parent) const override; virtual int columnCount(const QModelIndex& parent) const override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override; void addAccount(MinecraftAccountPtr account); void removeAccount(QModelIndex index); int findAccountByProfileId(const QString& profileId) const; MinecraftAccountPtr getAccountByProfileName(const QString& profileName) const; QStringList profileNames() const; // requesting a refresh pushes it to the front of the queue void requestRefresh(QString accountId); // queuing a refresh will let it go to the back of the queue (unless it's somewhere inside the queue already) void queueRefresh(QString accountId); /*! * Sets the path to load/save the list file from/to. * If autosave is true, this list will automatically save to the given path whenever it changes. * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately * after calling this function to ensure an autosaved change doesn't overwrite the list you intended * to load. */ void setListFilePath(QString path, bool autosave = false); bool loadList(); bool loadV3(QJsonObject& root); bool saveList(); MinecraftAccountPtr defaultAccount() const; void setDefaultAccount(MinecraftAccountPtr profileId); bool anyAccountIsValid(); bool isActive() const; protected: void beginActivity(); void endActivity(); private: uint32_t m_activityCount = 0; signals: void listChanged(); void listActivityChanged(); void defaultAccountChanged(); void activityChanged(bool active); public slots: /** * This is called when one of the accounts changes and the list needs to be updated */ void accountChanged(); /** * This is called when a (refresh/login) task involving the account starts or ends */ void accountActivityChanged(bool active); /** * This is initially to run background account refresh tasks, or on a hourly timer */ void fillQueue(); private slots: void tryNext(); void authSucceeded(); void authFailed(QString reason); protected: QList m_refreshQueue; QTimer* m_refreshTimer; QTimer* m_nextTimer; shared_qobject_ptr m_currentTask; /*! * Called whenever the list changes. * This emits the listChanged() signal and autosaves the list (if autosave is enabled). */ void onListChanged(); /*! * Called whenever the active account changes. * Emits the defaultAccountChanged() signal and autosaves the list if enabled. */ void onDefaultAccountChanged(); QList m_accounts; MinecraftAccountPtr m_defaultAccount; //! Path to the account list file. Empty string if there isn't one. QString m_listFilePath; /*! * If true, the account list will automatically save to the account list path when it changes. * Ignored if m_listFilePath is blank. */ bool m_autosave = false; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/0000755000175100017510000000000015144136756021643 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/minecraft/auth/steps/EntitlementsStep.cpp0000644000175100017510000000351615144136756025663 0ustar runnerrunner#include "EntitlementsStep.h" #include #include #include #include #include #include "Application.h" #include "Logging.h" #include "minecraft/auth/Parsers.h" #include "net/Download.h" #include "net/NetJob.h" #include "net/RawHeaderProxy.h" #include "tasks/Task.h" EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {} QString EntitlementsStep::describe() { return tr("Determining game ownership."); } void EntitlementsStep::perform() { auto uuid = QUuid::createUuid(); m_entitlements_request_id = uuid.toString().remove('{').remove('}'); QUrl url("https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlements_request_id); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; m_response.reset(new QByteArray()); m_request = Net::Download::makeByteArray(url, m_response); m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("EntitlementsStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &EntitlementsStep::onRequestDone); m_task->start(); qDebug() << "Getting entitlements..."; } void EntitlementsStep::onRequestDone() { qCDebug(authCredentials()) << *m_response; // TODO: check presence of same entitlementsRequestId? // TODO: validate JWTs? Parsers::parseMinecraftEntitlements(*m_response, m_data->minecraftEntitlement); emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); } PrismLauncher-10.0.5/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp0000644000175100017510000001717315144136756026715 0ustar runnerrunner#include "XboxAuthorizationStep.h" #include #include #include #include "Application.h" #include "Logging.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" #include "net/Upload.h" XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind) : AuthStep(data), m_token(token), m_relyingParty(relyingParty), m_authorizationKind(authorizationKind) {} QString XboxAuthorizationStep::describe() { return tr("Getting authorization to access %1 services.").arg(m_authorizationKind); } void XboxAuthorizationStep::perform() { QString xbox_auth_template = R"XXX( { "Properties": { "SandboxId": "RETAIL", "UserTokens": [ "%1" ] }, "RelyingParty": "%2", "TokenType": "JWT" } )XXX"; auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty); // http://xboxlive.com QUrl url("https://xsts.auth.xboxlive.com/xsts/authorize"); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, }; m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("XboxAuthorizationStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &XboxAuthorizationStep::onRequestDone); m_task->start(); qDebug() << "Getting authorization token for" << m_relyingParty; } void XboxAuthorizationStep::onRequestDone() { qCDebug(authCredentials()) << *m_response; if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); if (Net::isApplicationError(m_request->error())) { if (!processSTSError()) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, m_request->error())); } else { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); } } else { emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, m_request->errorString())); } return; } Token temp; if (!Parsers::parseXTokenResponse(*m_response, temp, m_authorizationKind)) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind)); return; } if (temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Server has changed %1 authorization user hash in the reply. Something is wrong.").arg(m_authorizationKind)); return; } auto& token = *m_token; token = temp; emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty)); } bool XboxAuthorizationStep::processSTSError() { if (m_request->error() == QNetworkReply::AuthenticationRequiredError) { QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(*m_response, &jsonError); if (jsonError.error) { qWarning() << "Cannot parse error XSTS response as JSON:" << jsonError.errorString(); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString())); return true; } int64_t errorCode = -1; auto obj = doc.object(); if (!Parsers::getNumber(obj.value("XErr"), errorCode)) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XErr element is missing from %1 authorization error response.").arg(m_authorizationKind)); return true; } switch (errorCode) { case 2148916233: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account does not have an Xbox Live profile. Buy the game on %1 first.") .arg("minecraft.net")); return true; } case 2148916235: { // NOTE: this is the Grulovia error emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox Live is not available in your country. You've been blocked.")); return true; } case 2148916238: { emit finished( AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") .arg("help.minecraft.net")); return true; } // the following codes where copied from: https://github.com/PrismarineJS/prismarine-auth/pull/44 case 2148916236: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account requires proof of age to play. Please login to %1 to provide proof of age.") .arg("login.live.com")); return true; } case 2148916237: emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has reached its limit for playtime. This " "Microsoft account has been blocked from logging in.")); return true; case 2148916227: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account was banned by Xbox for violating one or more " "Community Standards for Xbox and is unable to be used.")); return true; } case 2148916229: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account is currently restricted and your guardian has not given you permission to play " "online. Login to %1 and have your guardian change your permissions.") .arg("account.microsoft.com")); return true; } case 2148916234: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("This Microsoft account has not accepted Xbox's Terms of Service. Please login and accept them.")); return true; } default: { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorCode)); return true; } } } return false; } PrismLauncher-10.0.5/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp0000644000175100017510000000542415144136756026441 0ustar runnerrunner#include "MinecraftProfileStep.h" #include #include "Application.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {} QString MinecraftProfileStep::describe() { return tr("Fetching the Minecraft profile."); } void MinecraftProfileStep::perform() { QUrl url("https://api.minecraftservices.com/minecraft/profile"); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8() } }; m_response.reset(new QByteArray()); m_request = Net::Download::makeByteArray(url, m_response); m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("MinecraftProfileStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &MinecraftProfileStep::onRequestDone); m_task->start(); } void MinecraftProfileStep::onRequestDone() { if (m_request->error() == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. m_data->minecraftProfile = MinecraftProfile(); emit finished(AccountTaskState::STATE_WORKING, tr("Account has no Minecraft profile.")); return; } if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Error getting profile:"; qWarning() << " HTTP Status :" << m_request->replyStatusCode(); qWarning() << " Internal error no.:" << m_request->error(); qWarning() << " Error string :" << m_request->errorString(); qWarning() << " Response:"; qWarning() << QString::fromUtf8(*m_response); if (Net::isApplicationError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); } else { emit finished(AccountTaskState::STATE_OFFLINE, tr("Minecraft Java profile acquisition failed: %1").arg(m_request->errorString())); } return; } if (!Parsers::parseMinecraftProfile(*m_response, m_data->minecraftProfile)) { m_data->minecraftProfile = MinecraftProfile(); emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed")); return; } emit finished(AccountTaskState::STATE_WORKING, tr("Got Minecraft profile")); } PrismLauncher-10.0.5/launcher/minecraft/auth/steps/EntitlementsStep.h0000644000175100017510000000107715144136756025330 0ustar runnerrunner#pragma once #include #include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" #include "net/NetJob.h" class EntitlementsStep : public AuthStep { Q_OBJECT public: explicit EntitlementsStep(AccountData* data); virtual ~EntitlementsStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(); private: QString m_entitlements_request_id; std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/MSAStep.h0000644000175100017510000000373315144136756023276 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "minecraft/auth/AuthStep.h" #include class MSAStep : public AuthStep { Q_OBJECT public: explicit MSAStep(AccountData* data, bool silent = false); virtual ~MSAStep() noexcept = default; void perform() override; QString describe() override; signals: void authorizeWithBrowser(const QUrl& url); private: bool m_silent; QString m_clientId; QOAuth2AuthorizationCodeFlow m_oauth2; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/XboxAuthorizationStep.h0000644000175100017510000000133515144136756026353 0ustar runnerrunner#pragma once #include #include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" class XboxAuthorizationStep : public AuthStep { Q_OBJECT public: explicit XboxAuthorizationStep(AccountData* data, Token* token, QString relyingParty, QString authorizationKind); virtual ~XboxAuthorizationStep() noexcept = default; void perform() override; QString describe() override; private: bool processSTSError(); private slots: void onRequestDone(); private: Token* m_token; QString m_relyingParty; QString m_authorizationKind; std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/XboxProfileStep.cpp0000644000175100017510000000460715144136756025453 0ustar runnerrunner#include "XboxProfileStep.h" #include #include #include "Application.h" #include "Logging.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {} QString XboxProfileStep::describe() { return tr("Fetching Xbox profile."); } void XboxProfileStep::perform() { QUrl url("https://profile.xboxlive.com/users/me/profile/settings"); QUrlQuery q; q.addQueryItem("settings", "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," "PreferredColor,Location,Bio,Watermarks," "RealName,RealNameOverride,IsQuarantined"); url.setQuery(q); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, { "x-xbl-contract-version", "3" }, { "Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8() } }; m_response.reset(new QByteArray()); m_request = Net::Download::makeByteArray(url, m_response); m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_task.reset(new NetJob("XboxProfileStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &XboxProfileStep::onRequestDone); m_task->start(); qDebug() << "Getting Xbox profile..."; } void XboxProfileStep::onRequestDone() { if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); qCDebug(authCredentials()) << *m_response; if (Net::isApplicationError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to retrieve the Xbox profile: %1").arg(m_request->errorString())); } else { emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to retrieve the Xbox profile: %1").arg(m_request->errorString())); } return; } qCDebug(authCredentials()) << "Xbox profile:" << *m_response; emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); } PrismLauncher-10.0.5/launcher/minecraft/auth/steps/GetSkinStep.cpp0000644000175100017510000000160215144136756024546 0ustar runnerrunner #include "GetSkinStep.h" #include #include "Application.h" GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {} QString GetSkinStep::describe() { return tr("Getting skin."); } void GetSkinStep::perform() { QUrl url(m_data->minecraftProfile.skin.url); m_response.reset(new QByteArray()); m_request = Net::Download::makeByteArray(url, m_response); m_request->enableAutoRetry(true); m_task.reset(new NetJob("GetSkinStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &GetSkinStep::onRequestDone); m_task->start(); } void GetSkinStep::onRequestDone() { if (m_request->error() == QNetworkReply::NoError) m_data->minecraftProfile.skin.data = *m_response; emit finished(AccountTaskState::STATE_WORKING, tr("Got skin")); } PrismLauncher-10.0.5/launcher/minecraft/auth/steps/LauncherLoginStep.cpp0000644000175100017510000000465615144136756025750 0ustar runnerrunner#include "LauncherLoginStep.h" #include #include #include "Application.h" #include "Logging.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" #include "net/Upload.h" LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {} QString LauncherLoginStep::describe() { return tr("Fetching Minecraft access token"); } void LauncherLoginStep::perform() { QUrl url("https://api.minecraftservices.com/launcher/login"); auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); auto xToken = m_data->mojangservicesToken.token; QString mc_auth_template = R"XXX( { "xtoken": "XBL3.0 x=%1;%2", "platform": "PC_LAUNCHER" } )XXX"; auto requestBody = mc_auth_template.arg(uhs, xToken); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, }; m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("LauncherLoginStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &LauncherLoginStep::onRequestDone); m_task->start(); qDebug() << "Getting Minecraft access token..."; } void LauncherLoginStep::onRequestDone() { qCDebug(authCredentials()) << *m_response; if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); if (Net::isApplicationError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } else { emit finished(AccountTaskState::STATE_OFFLINE, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); } return; } if (!Parsers::parseMojangResponse(*m_response, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.")); return; } emit finished(AccountTaskState::STATE_WORKING, tr("Got Minecraft access token")); } PrismLauncher-10.0.5/launcher/minecraft/auth/steps/MinecraftProfileStep.h0000644000175100017510000000104415144136756026100 0ustar runnerrunner#pragma once #include #include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" #include "net/NetJob.h" class MinecraftProfileStep : public AuthStep { Q_OBJECT public: explicit MinecraftProfileStep(AccountData* data); virtual ~MinecraftProfileStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(); private: std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/MSADeviceCodeStep.cpp0000644000175100017510000002410015144136756025533 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MSADeviceCodeStep.h" #include #include #include "Application.h" #include "Json.h" #include "net/RawHeaderProxy.h" // https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code MSADeviceCodeStep::MSADeviceCodeStep(AccountData* data) : AuthStep(data) { m_clientId = APPLICATION->getMSAClientID(); connect(&m_expiration_timer, &QTimer::timeout, this, &MSADeviceCodeStep::abort); connect(&m_pool_timer, &QTimer::timeout, this, &MSADeviceCodeStep::authenticateUser); } QString MSADeviceCodeStep::describe() { return tr("Logging in with Microsoft account(device code)."); } void MSADeviceCodeStep::perform() { QUrlQuery data; data.addQueryItem("client_id", m_clientId); data.addQueryItem("scope", "XboxLive.SignIn XboxLive.offline_access"); auto payload = data.query(QUrl::FullyEncoded).toUtf8(); QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"); auto headers = QList{ { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response, payload); m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("MSADeviceCodeStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &MSADeviceCodeStep::deviceAuthorizationFinished); m_task->start(); } struct DeviceAuthorizationResponse { QString device_code; QString user_code; QString verification_uri; int expires_in; int interval; QString error; QString error_description; }; DeviceAuthorizationResponse parseDeviceAuthorizationResponse(const QByteArray& data) { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); return {}; } if (!doc.isObject()) { qWarning() << "Device authorization response is not an object"; return {}; } auto obj = doc.object(); return { obj["device_code"].toString(), obj["user_code"].toString(), obj["verification_uri"].toString(), obj["expires_in"].toInt(), obj["interval"].toInt(), obj["error"].toString(), obj["error_description"].toString(), }; } void MSADeviceCodeStep::deviceAuthorizationFinished() { auto rsp = parseDeviceAuthorizationResponse(*m_response); if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device authorization failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); return; } if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { qWarning() << "Device authorization failed:" << *m_response; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Failed to retrieve device authorization")); return; } if (rsp.device_code.isEmpty() || rsp.user_code.isEmpty() || rsp.verification_uri.isEmpty() || rsp.expires_in == 0) { qWarning() << "Device authorization failed: required fields missing"; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device authorization failed: required fields missing")); return; } if (rsp.interval != 0) { interval = rsp.interval; } m_device_code = rsp.device_code; emit authorizeWithBrowser(rsp.verification_uri, rsp.user_code, rsp.expires_in); m_expiration_timer.setTimerType(Qt::VeryCoarseTimer); m_expiration_timer.setInterval(rsp.expires_in * 1000); m_expiration_timer.setSingleShot(true); m_expiration_timer.start(); m_pool_timer.setTimerType(Qt::VeryCoarseTimer); m_pool_timer.setSingleShot(true); startPoolTimer(); } void MSADeviceCodeStep::abort() { m_expiration_timer.stop(); m_pool_timer.stop(); if (m_request) { m_request->abort(); } m_is_aborted = true; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Task aborted")); } void MSADeviceCodeStep::startPoolTimer() { if (m_is_aborted) { return; } if (m_expiration_timer.remainingTime() < interval * 1000) { perform(); return; } m_pool_timer.setInterval(interval * 1000); m_pool_timer.start(); } void MSADeviceCodeStep::authenticateUser() { QUrlQuery data; data.addQueryItem("client_id", m_clientId); data.addQueryItem("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); data.addQueryItem("device_code", m_device_code); auto payload = data.query(QUrl::FullyEncoded).toUtf8(); QUrl url("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"); auto headers = QList{ { "Content-Type", "application/x-www-form-urlencoded" }, { "Accept", "application/json" }, }; m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response, payload); m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); connect(m_request.get(), &Task::finished, this, &MSADeviceCodeStep::authenticationFinished); m_request->setNetwork(APPLICATION->network()); m_request->start(); } struct AuthenticationResponse { QString access_token; QString token_type; QString refresh_token; int expires_in; QString error; QString error_description; QVariantMap extra; }; AuthenticationResponse parseAuthenticationResponse(const QByteArray& data) { QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { qWarning() << "Failed to parse device authorization response due to err:" << err.errorString(); return {}; } if (!doc.isObject()) { qWarning() << "Device authorization response is not an object"; return {}; } auto obj = doc.object(); return { obj["access_token"].toString(), obj["token_type"].toString(), obj["refresh_token"].toString(), obj["expires_in"].toInt(), obj["error"].toString(), obj["error_description"].toString(), obj.toVariantMap() }; } void MSADeviceCodeStep::authenticationFinished() { if (m_request->error() == QNetworkReply::TimeoutError) { // rfc8628#section-3.5 // "On encountering a connection timeout, clients MUST unilaterally // reduce their polling frequency before retrying. The use of an // exponential backoff algorithm to achieve this, such as doubling the // polling interval on each such connection timeout, is RECOMMENDED." interval *= 2; startPoolTimer(); return; } auto rsp = parseAuthenticationResponse(*m_response); if (rsp.error == "slow_down") { // rfc8628#section-3.5 // "A variant of 'authorization_pending', the authorization request is // still pending and polling should continue, but the interval MUST // be increased by 5 seconds for this and all subsequent requests." interval += 5; startPoolTimer(); return; } if (rsp.error == "authorization_pending") { // keep trying - rfc8628#section-3.5 // "The authorization request is still pending as the end user hasn't // yet completed the user-interaction steps (Section 3.3)." startPoolTimer(); return; } if (!rsp.error.isEmpty() || !rsp.error_description.isEmpty()) { qWarning() << "Device Access failed:" << rsp.error; emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Device Access failed: %1").arg(rsp.error_description.isEmpty() ? rsp.error : rsp.error_description)); return; } if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) { startPoolTimer(); // it failed so just try again without increasing the interval return; } m_expiration_timer.stop(); m_data->msaClientID = m_clientId; m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc(); m_data->msaToken.notAfter = QDateTime::currentDateTime().addSecs(rsp.expires_in); m_data->msaToken.extra = rsp.extra; m_data->msaToken.refresh_token = rsp.refresh_token; m_data->msaToken.token = rsp.access_token; emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token")); } PrismLauncher-10.0.5/launcher/minecraft/auth/steps/XboxProfileStep.h0000644000175100017510000000102515144136756025107 0ustar runnerrunner#pragma once #include #include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" #include "net/NetJob.h" class XboxProfileStep : public AuthStep { Q_OBJECT public: explicit XboxProfileStep(AccountData* data); virtual ~XboxProfileStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(); private: std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/MSAStep.cpp0000644000175100017510000002011615144136756023623 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MSAStep.h" #include #include #include #include #include "Application.h" #include "BuildConfig.h" #include "FileSystem.h" #include #include #include bool isSchemeHandlerRegistered() { #ifdef Q_OS_LINUX QProcess process; process.start("xdg-mime", { "query", "default", "x-scheme-handler/" + BuildConfig.LAUNCHER_APP_BINARY_NAME }); process.waitForFinished(); QString output = process.readAllStandardOutput().trimmed(); return output.contains(APPLICATION->desktopFileName()); #elif defined(Q_OS_WIN) QString regPath = QString("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); QSettings settings(regPath, QSettings::NativeFormat); const QString registeredRunCommand = settings.value("shell/open/command/.").toString().replace("\\", "/"); return registeredRunCommand.contains(QCoreApplication::applicationFilePath()); #endif return true; } class CustomOAuthOobReplyHandler : public QOAuthOobReplyHandler { Q_OBJECT public: explicit CustomOAuthOobReplyHandler(QObject* parent = nullptr) : QOAuthOobReplyHandler(parent) { connect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); } ~CustomOAuthOobReplyHandler() override { disconnect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); } QString callback() const override { return BuildConfig.LAUNCHER_APP_BINARY_NAME + "://oauth/microsoft"; } protected: void networkReplyFinished(QNetworkReply* reply) override { if (reply->error() != QNetworkReply::NoError) { qWarning() << "OAuth2 request failed:" << reply->readAll(); } QOAuthOobReplyHandler::networkReplyFinished(reply); } }; class LoggingOAuthHttpServerReplyHandler final : public QOAuthHttpServerReplyHandler { Q_OBJECT public: explicit LoggingOAuthHttpServerReplyHandler(QObject* parent = nullptr) : QOAuthHttpServerReplyHandler(parent) {} protected: void networkReplyFinished(QNetworkReply* reply) override { if (reply->error() != QNetworkReply::NoError) { qWarning() << "OAuth2 request failed:" << reply->readAll(); } QOAuthHttpServerReplyHandler::networkReplyFinished(reply); } }; MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(silent) { m_clientId = APPLICATION->getMSAClientID(); if (QCoreApplication::applicationFilePath().startsWith("/tmp/.mount_") || APPLICATION->isPortable() || !isSchemeHandlerRegistered()) { auto replyHandler = new LoggingOAuthHttpServerReplyHandler(this); replyHandler->setCallbackText(QString(R"XXX( Login Successful, redirecting... )XXX") .arg(BuildConfig.LOGIN_CALLBACK_URL)); m_oauth2.setReplyHandler(replyHandler); } else { m_oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this)); } m_oauth2.setAuthorizationUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize")); m_oauth2.setAccessTokenUrl(QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")); m_oauth2.setScope("XboxLive.SignIn XboxLive.offline_access"); m_oauth2.setClientIdentifier(m_clientId); m_oauth2.setNetworkAccessManager(APPLICATION->network().get()); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this, [this] { m_data->msaClientID = m_oauth2.clientIdentifier(); m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc(); m_data->msaToken.notAfter = m_oauth2.expirationAt(); m_data->msaToken.extra = m_oauth2.extraTokens(); m_data->msaToken.refresh_token = m_oauth2.refreshToken(); m_data->msaToken.token = m_oauth2.token(); emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token")); }); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &MSAStep::authorizeWithBrowser); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this, [this, silent](const QAbstractOAuth2::Error err) { auto state = AccountTaskState::STATE_FAILED_HARD; if (m_oauth2.status() == QAbstractOAuth::Status::Granted || silent) { if (err == QAbstractOAuth2::Error::NetworkError) { state = AccountTaskState::STATE_OFFLINE; } else { state = AccountTaskState::STATE_FAILED_SOFT; } } auto message = tr("Microsoft user authentication failed."); if (silent) { message = tr("Failed to refresh token."); } qWarning() << message; emit finished(state, message); }); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::error, this, [this](const QString& error, const QString& errorDescription, const QUrl& uri) { qWarning() << "Failed to login because" << error << errorDescription; emit finished(AccountTaskState::STATE_FAILED_HARD, errorDescription); }); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::extraTokensChanged, this, [this](const QVariantMap& tokens) { m_data->msaToken.extra = tokens; }); connect(&m_oauth2, &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, this, [this](const QString& clientIdentifier) { m_data->msaClientID = clientIdentifier; }); } QString MSAStep::describe() { return tr("Logging in with Microsoft account."); } void MSAStep::perform() { if (m_silent) { if (m_data->msaClientID != m_clientId) { emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - client identification has changed.")); return; } if (m_data->msaToken.refresh_token.isEmpty()) { emit finished(AccountTaskState::STATE_DISABLED, tr("Microsoft user authentication failed - refresh token is empty.")); return; } m_oauth2.setRefreshToken(m_data->msaToken.refresh_token); m_oauth2.refreshAccessToken(); } else { m_oauth2.setModifyParametersFunction( [](QAbstractOAuth::Stage stage, QMultiMap* map) { map->insert("prompt", "select_account"); }); *m_data = AccountData(); m_data->msaClientID = m_clientId; m_oauth2.grant(); } } #include "MSAStep.moc" PrismLauncher-10.0.5/launcher/minecraft/auth/steps/XboxUserStep.cpp0000644000175100017510000000505615144136756024770 0ustar runnerrunner#include "XboxUserStep.h" #include #include "Application.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" #include "net/RawHeaderProxy.h" XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {} QString XboxUserStep::describe() { return tr("Logging in as an Xbox user."); } void XboxUserStep::perform() { QString xbox_auth_template = R"XXX( { "Properties": { "AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", "RpsTicket": "d=%1" }, "RelyingParty": "http://auth.xboxlive.com", "TokenType": "JWT" } )XXX"; auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token); QUrl url("https://user.auth.xboxlive.com/user/authenticate"); auto headers = QList{ { "Content-Type", "application/json" }, { "Accept", "application/json" }, // set contract-version header (prevent err 400 bad-request?) // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders { "x-xbl-contract-version", "1" } }; m_response.reset(new QByteArray()); m_request = Net::Upload::makeByteArray(url, m_response, xbox_auth_data.toUtf8()); m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); m_request->enableAutoRetry(true); m_task.reset(new NetJob("XboxUserStep", APPLICATION->network())); m_task->setAskRetry(false); m_task->addNetAction(m_request); connect(m_task.get(), &Task::finished, this, &XboxUserStep::onRequestDone); m_task->start(); qDebug() << "First layer of Xbox auth ... commencing."; } void XboxUserStep::onRequestDone() { if (m_request->error() != QNetworkReply::NoError) { qWarning() << "Reply error:" << m_request->error(); if (Net::isApplicationError(m_request->error())) { emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); } else { emit finished(AccountTaskState::STATE_OFFLINE, tr("Xbox user authentication failed: %1").arg(m_request->errorString())); } return; } Token temp; if (!Parsers::parseXTokenResponse(*m_response, temp, "UToken")) { qWarning() << "Could not parse user authentication response..."; emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Xbox user authentication response could not be understood.")); return; } m_data->userToken = temp; emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox user token")); } PrismLauncher-10.0.5/launcher/minecraft/auth/steps/XboxUserStep.h0000644000175100017510000000101015144136756024417 0ustar runnerrunner#pragma once #include #include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" class XboxUserStep : public AuthStep { Q_OBJECT public: explicit XboxUserStep(AccountData* data); virtual ~XboxUserStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(); private: std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/GetSkinStep.h0000644000175100017510000000101115144136756024205 0ustar runnerrunner#pragma once #include #include #include "minecraft/auth/AuthStep.h" #include "net/Download.h" #include "net/NetJob.h" class GetSkinStep : public AuthStep { Q_OBJECT public: explicit GetSkinStep(AccountData* data); virtual ~GetSkinStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(); private: std::shared_ptr m_response; Net::Download::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/MSADeviceCodeStep.h0000644000175100017510000000457415144136756025215 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" class MSADeviceCodeStep : public AuthStep { Q_OBJECT public: explicit MSADeviceCodeStep(AccountData* data); virtual ~MSADeviceCodeStep() noexcept = default; void perform() override; QString describe() override; public slots: void abort() override; signals: void authorizeWithBrowser(QString url, QString code, int expiresIn); private slots: void deviceAuthorizationFinished(); void startPoolTimer(); void authenticateUser(); void authenticationFinished(); private: QString m_clientId; QString m_device_code; bool m_is_aborted = false; int interval = 5; QTimer m_pool_timer; QTimer m_expiration_timer; std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-10.0.5/launcher/minecraft/auth/steps/LauncherLoginStep.h0000644000175100017510000000102715144136756025402 0ustar runnerrunner#pragma once #include #include #include "minecraft/auth/AuthStep.h" #include "net/NetJob.h" #include "net/Upload.h" class LauncherLoginStep : public AuthStep { Q_OBJECT public: explicit LauncherLoginStep(AccountData* data); virtual ~LauncherLoginStep() noexcept = default; void perform() override; QString describe() override; private slots: void onRequestDone(); private: std::shared_ptr m_response; Net::Upload::Ptr m_request; NetJob::Ptr m_task; }; PrismLauncher-10.0.5/launcher/minecraft/auth/AccountList.cpp0000644000175100017510000005166215144136756023453 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AccountList.h" #include "AccountData.h" #include "tasks/Task.h" #include #include #include #include #include #include #include #include #include #include #include #include enum AccountListVersion { MojangMSA = 3 }; AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) { m_refreshTimer = new QTimer(this); m_refreshTimer->setSingleShot(true); connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue); m_nextTimer = new QTimer(this); m_nextTimer->setSingleShot(true); connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext); } AccountList::~AccountList() noexcept {} int AccountList::findAccountByProfileId(const QString& profileId) const { for (int i = 0; i < count(); i++) { MinecraftAccountPtr account = at(i); if (account->profileId() == profileId) { return i; } } return -1; } MinecraftAccountPtr AccountList::getAccountByProfileName(const QString& profileName) const { for (int i = 0; i < count(); i++) { MinecraftAccountPtr account = at(i); if (account->profileName() == profileName) { return account; } } return nullptr; } const MinecraftAccountPtr AccountList::at(int i) const { return MinecraftAccountPtr(m_accounts.at(i)); } QStringList AccountList::profileNames() const { QStringList out; for (auto& account : m_accounts) { auto profileName = account->profileName(); if (profileName.isEmpty()) { continue; } out.append(profileName); } return out; } void AccountList::addAccount(const MinecraftAccountPtr account) { // NOTE: Do not allow adding something that's already there. We shouldn't let it continue // because of the signal / slot connections after this. if (m_accounts.contains(account)) { qDebug() << "Tried to add account that's already on the accounts list!"; return; } // hook up notifications for changes in the account connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); // override/replace existing account with the same profileId auto profileId = account->profileId(); if (profileId.size()) { auto existingAccount = findAccountByProfileId(profileId); if (existingAccount != -1) { qDebug() << "Replacing old account with a new one with the same profile ID!"; MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; m_accounts[existingAccount] = account; if (m_defaultAccount == existingAccountPtr) { m_defaultAccount = account; } // disconnect notifications for changes in the account being replaced existingAccountPtr->disconnect(this); emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); onListChanged(); return; } } // if we don't have this profileId yet, add the account to the end int row = m_accounts.count(); qDebug() << "Inserting account at index" << row; beginInsertRows(QModelIndex(), row, row); m_accounts.append(account); endInsertRows(); onListChanged(); } void AccountList::removeAccount(QModelIndex index) { int row = index.row(); if (index.isValid() && row >= 0 && row < m_accounts.size()) { auto& account = m_accounts[row]; if (account == m_defaultAccount) { m_defaultAccount = nullptr; onDefaultAccountChanged(); } account->disconnect(this); beginRemoveRows(QModelIndex(), row, row); m_accounts.removeAt(index.row()); endRemoveRows(); onListChanged(); } } MinecraftAccountPtr AccountList::defaultAccount() const { return m_defaultAccount; } void AccountList::setDefaultAccount(MinecraftAccountPtr newAccount) { if (!newAccount && m_defaultAccount) { int idx = 0; auto previousDefaultAccount = m_defaultAccount; m_defaultAccount = nullptr; for (MinecraftAccountPtr account : m_accounts) { if (account == previousDefaultAccount) { emit dataChanged(index(idx), index(idx, columnCount(QModelIndex()) - 1)); } idx++; } onDefaultAccountChanged(); } else { auto currentDefaultAccount = m_defaultAccount; int currentDefaultAccountIdx = -1; auto newDefaultAccount = m_defaultAccount; int newDefaultAccountIdx = -1; int idx = 0; for (MinecraftAccountPtr account : m_accounts) { if (account == newAccount) { newDefaultAccount = account; newDefaultAccountIdx = idx; } if (currentDefaultAccount == account) { currentDefaultAccountIdx = idx; } idx++; } if (currentDefaultAccount != newDefaultAccount) { emit dataChanged(index(currentDefaultAccountIdx), index(currentDefaultAccountIdx, columnCount(QModelIndex()) - 1)); emit dataChanged(index(newDefaultAccountIdx), index(newDefaultAccountIdx, columnCount(QModelIndex()) - 1)); m_defaultAccount = newDefaultAccount; onDefaultAccountChanged(); } } } void AccountList::accountChanged() { // the list changed. there is no doubt. onListChanged(); } void AccountList::accountActivityChanged(bool active) { MinecraftAccount* account = qobject_cast(sender()); bool found = false; for (int i = 0; i < count(); i++) { if (at(i).get() == account) { emit dataChanged(index(i), index(i, columnCount(QModelIndex()) - 1)); found = true; break; } } if (found) { emit listActivityChanged(); if (active) { beginActivity(); } else { endActivity(); } } } void AccountList::onListChanged() { if (m_autosave) // TODO: Alert the user if this fails. saveList(); emit listChanged(); } void AccountList::onDefaultAccountChanged() { if (m_autosave) saveList(); emit defaultAccountChanged(); } int AccountList::count() const { return m_accounts.count(); } QString getAccountStatus(AccountState status) { switch (status) { case AccountState::Unchecked: return QObject::tr("Unchecked", "Account status"); case AccountState::Offline: return QObject::tr("Offline", "Account status"); case AccountState::Online: return QObject::tr("Ready", "Account status"); case AccountState::Working: return QObject::tr("Working", "Account status"); case AccountState::Errored: return QObject::tr("Errored", "Account status"); case AccountState::Expired: return QObject::tr("Expired", "Account status"); case AccountState::Disabled: return QObject::tr("Disabled", "Account status"); case AccountState::Gone: return QObject::tr("Gone", "Account status"); default: return QObject::tr("Unknown", "Account status"); } } QVariant AccountList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() > count()) return QVariant(); MinecraftAccountPtr account = at(index.row()); switch (role) { case Qt::DisplayRole: switch (index.column()) { case ProfileNameColumn: return account->profileName(); case NameColumn: return account->accountDisplayString(); case TypeColumn: { switch (account->accountType()) { case AccountType::MSA: { return tr("MSA", "Account type"); } case AccountType::Offline: { return tr("Offline", "Account type"); } } return tr("Unknown", "Account type"); } case StatusColumn: return getAccountStatus(account->accountState()); default: return QVariant(); } case Qt::ToolTipRole: return account->accountDisplayString(); case PointerRole: return QVariant::fromValue(account); case Qt::CheckStateRole: if (index.column() == ProfileNameColumn) return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; return QVariant(); default: return QVariant(); } } QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ProfileNameColumn: return tr("Username"); case NameColumn: return tr("Account"); case TypeColumn: return tr("Type"); case StatusColumn: return tr("Status"); default: return QVariant(); } case Qt::ToolTipRole: switch (section) { case ProfileNameColumn: return tr("Minecraft username associated with the account."); case NameColumn: return tr("User name of the account."); case TypeColumn: return tr("Type of the account (MSA or Offline)"); case StatusColumn: return tr("Current status of the account."); default: return QVariant(); } default: return QVariant(); } } int AccountList::rowCount(const QModelIndex& parent) const { // Return count return parent.isValid() ? 0 : count(); } int AccountList::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } Qt::ItemFlags AccountList::flags(const QModelIndex& index) const { if (index.row() < 0 || index.row() >= rowCount(index.parent()) || !index.isValid()) { return Qt::NoItemFlags; } return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; } bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int role) { if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()) || !idx.isValid()) { return false; } if (role == Qt::CheckStateRole) { if (value == Qt::Checked) { MinecraftAccountPtr account = at(idx.row()); setDefaultAccount(account); } else if (m_defaultAccount == at(idx.row())) setDefaultAccount(nullptr); } emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); return true; } bool AccountList::loadList() { if (m_listFilePath.isEmpty()) { qCritical() << "Can't load Mojang account list. No file path given and no default set."; return false; } QFile file(m_listFilePath); // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::ReadOnly)) { qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); return false; } // Read the file and close it. QByteArray jsonData = file.readAll(); file.close(); QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); // Fail if the JSON is invalid. if (parseError.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse account list file: %1 at offset %2") .arg(parseError.errorString(), QString::number(parseError.offset)) .toUtf8(); return false; } // Make sure the root is an object. if (!jsonDoc.isObject()) { qCritical() << "Invalid account list JSON: Root should be an array."; return false; } QJsonObject root = jsonDoc.object(); // Make sure the format version matches. auto listVersion = root.value("formatVersion").toVariant().toInt(); if (listVersion == AccountListVersion::MojangMSA) return loadV3(root); QString newName = "accounts-old.json"; qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; // Attempt to rename the old version. file.rename(newName); return false; } bool AccountList::loadV3(QJsonObject& root) { beginResetModel(); QJsonArray accounts = root.value("accounts").toArray(); for (QJsonValue accountVal : accounts) { QJsonObject accountObj = accountVal.toObject(); MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj); if (account.get() != nullptr) { auto profileId = account->profileId(); if (profileId.size()) { if (findAccountByProfileId(profileId) != -1) { continue; } } connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); m_accounts.append(account); if (accountObj.value("active").toBool(false)) { m_defaultAccount = account; } } else { qWarning() << "Failed to load an account."; } } endResetModel(); return true; } bool AccountList::saveList() { if (m_listFilePath.isEmpty()) { qCritical() << "Can't save Mojang account list. No file path given and no default set."; return false; } // make sure the parent folder exists if (!FS::ensureFilePathExists(m_listFilePath)) return false; // make sure the file wasn't overwritten with a folder before (fixes a bug) QFileInfo finfo(m_listFilePath); if (finfo.isDir()) { QDir badDir(m_listFilePath); badDir.removeRecursively(); } qDebug() << "Writing account list to" << m_listFilePath; qDebug() << "Building JSON data structure."; // Build the JSON document to write to the list file. QJsonObject root; root.insert("formatVersion", AccountListVersion::MojangMSA); // Build a list of accounts. qDebug() << "Building account array."; QJsonArray accounts; for (MinecraftAccountPtr account : m_accounts) { QJsonObject accountObj = account->saveToJson(); if (m_defaultAccount == account) { accountObj["active"] = true; } accounts.append(accountObj); } // Insert the account list into the root object. root.insert("accounts", accounts); // Create a JSON document object to convert our JSON to bytes. QJsonDocument doc(root); // Now that we're done building the JSON object, we can write it to the file. qDebug() << "Writing account list to file."; QSaveFile file(m_listFilePath); // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::WriteOnly)) { qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); return false; } // Write the JSON to the file. file.write(doc.toJson()); file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); if (file.commit()) { qDebug() << "Saved account list to" << m_listFilePath; return true; } else { qDebug() << "Failed to save accounts to" << m_listFilePath; return false; } } void AccountList::setListFilePath(QString path, bool autosave) { m_listFilePath = path; m_autosave = autosave; } bool AccountList::anyAccountIsValid() { for (auto account : m_accounts) { if (account->ownsMinecraft()) { return true; } } return false; } void AccountList::fillQueue() { if (m_defaultAccount && m_defaultAccount->shouldRefresh()) { auto idToRefresh = m_defaultAccount->internalId(); m_refreshQueue.push_back(idToRefresh); qDebug() << "AccountList: Queued default account with internal ID" << idToRefresh << "to refresh first"; } for (int i = 0; i < count(); i++) { auto account = at(i); if (account == m_defaultAccount) { continue; } if (account->shouldRefresh()) { auto idToRefresh = account->internalId(); queueRefresh(idToRefresh); } } tryNext(); } void AccountList::requestRefresh(QString accountId) { auto index = m_refreshQueue.indexOf(accountId); if (index != -1) { m_refreshQueue.removeAt(index); } m_refreshQueue.push_front(accountId); qDebug() << "AccountList: Pushed account with internal ID" << accountId << "to the front of the queue"; if (!isActive()) { tryNext(); } } void AccountList::queueRefresh(QString accountId) { if (m_refreshQueue.indexOf(accountId) != -1) { return; } m_refreshQueue.push_back(accountId); qDebug() << "AccountList: Queued account with internal ID" << accountId << "to refresh"; } void AccountList::tryNext() { while (m_refreshQueue.length()) { auto accountId = m_refreshQueue.front(); m_refreshQueue.pop_front(); for (int i = 0; i < count(); i++) { auto account = at(i); if (account->internalId() == accountId) { m_currentTask = account->refresh(); if (m_currentTask) { connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed); m_currentTask->start(); qDebug() << "RefreshSchedule: Processing account" << account->accountDisplayString() << "with internal ID" << accountId; return; } } } qDebug() << "RefreshSchedule: Account with internal ID" << accountId << "not found."; } // if we get here, no account needed refreshing. Schedule refresh in an hour. m_refreshTimer->start(1000 * 3600); } void AccountList::authSucceeded() { qDebug() << "RefreshSchedule: Background account refresh succeeded"; m_currentTask.reset(); m_nextTimer->start(1000 * 20); } void AccountList::authFailed(QString reason) { qDebug() << "RefreshSchedule: Background account refresh failed:" << reason; m_currentTask.reset(); m_nextTimer->start(1000 * 20); } bool AccountList::isActive() const { return m_activityCount != 0; } void AccountList::beginActivity() { bool activating = m_activityCount == 0; m_activityCount++; if (activating) { emit activityChanged(true); } } void AccountList::endActivity() { if (m_activityCount == 0) { qWarning() << "Activity count would become below zero"; return; } bool deactivating = m_activityCount == 1; m_activityCount--; if (deactivating) { emit activityChanged(false); } } PrismLauncher-10.0.5/launcher/minecraft/auth/AuthSession.h0000644000175100017510000000171315144136756023125 0ustar runnerrunner#pragma once #include #include class MinecraftAccount; struct AuthSession { bool MakeOffline(QString offline_playername); void MakeDemo(QString name, QString uuid); QString serializeUserProperties(); enum Status { Undetermined, RequiresOAuth, RequiresPassword, RequiresProfileSetup, PlayableOffline, PlayableOnline, GoneOrMigrated } status = Undetermined; // combined session ID QString session; // volatile auth token QString access_token; // profile name QString player_name; // profile ID QString uuid; // 'legacy' or 'mojang', depending on account type QString user_type; // Did the auth server reply? bool auth_server_online = false; // Did the user request online mode? bool wants_online = true; // Is this a demo session? bool demo = false; }; using AuthSessionPtr = std::shared_ptr; PrismLauncher-10.0.5/launcher/minecraft/auth/AuthSession.cpp0000644000175100017510000000174515144136756023465 0ustar runnerrunner#include "AuthSession.h" #include #include #include #include QString AuthSession::serializeUserProperties() { QJsonObject userAttrs; /* for (auto key : u.properties.keys()) { auto array = QJsonArray::fromStringList(u.properties.values(key)); userAttrs.insert(key, array); } */ QJsonDocument value(userAttrs); return value.toJson(QJsonDocument::Compact); } bool AuthSession::MakeOffline(QString offline_playername) { if (status != PlayableOffline && status != PlayableOnline) { return false; } session = "-"; access_token = "0"; player_name = offline_playername; status = PlayableOffline; return true; } void AuthSession::MakeDemo(QString name, QString u) { wants_online = false; demo = true; uuid = u; session = "-"; access_token = "0"; player_name = name; status = PlayableOnline; // needs online to download the assets }; PrismLauncher-10.0.5/launcher/minecraft/auth/AuthFlow.cpp0000644000175100017510000001313615144136756022746 0ustar runnerrunner#include #include #include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/steps/EntitlementsStep.h" #include "minecraft/auth/steps/GetSkinStep.h" #include "minecraft/auth/steps/LauncherLoginStep.h" #include "minecraft/auth/steps/MSADeviceCodeStep.h" #include "minecraft/auth/steps/MSAStep.h" #include "minecraft/auth/steps/MinecraftProfileStep.h" #include "minecraft/auth/steps/XboxAuthorizationStep.h" #include "minecraft/auth/steps/XboxProfileStep.h" #include "minecraft/auth/steps/XboxUserStep.h" #include "tasks/Task.h" #include "AuthFlow.h" #include AuthFlow::AuthFlow(AccountData* data, Action action) : Task(), m_data(data) { if (data->type == AccountType::MSA) { if (action == Action::DeviceCode) { auto oauthStep = makeShared(m_data); connect(oauthStep.get(), &MSADeviceCodeStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowserWithExtra); connect(this, &Task::aborted, oauthStep.get(), &MSADeviceCodeStep::abort); m_steps.append(oauthStep); } else { auto oauthStep = makeShared(m_data, action == Action::Refresh); connect(oauthStep.get(), &MSAStep::authorizeWithBrowser, this, &AuthFlow::authorizeWithBrowser); m_steps.append(oauthStep); } m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); m_steps.append( makeShared(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); m_steps.append(makeShared(m_data)); } changeState(AccountTaskState::STATE_CREATED); } void AuthFlow::succeed() { m_data->validity_ = Validity::Certain; changeState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps")); } void AuthFlow::executeTask() { changeState(AccountTaskState::STATE_WORKING, tr("Initializing")); nextStep(); } void AuthFlow::nextStep() { if (!Task::isRunning()) { return; } if (m_steps.size() == 0) { // we got to the end without an incident... assume this is all. m_currentStep.reset(); succeed(); return; } m_currentStep = m_steps.front(); qDebug() << "AuthFlow:" << m_currentStep->describe(); setStatus(m_currentStep->describe()); m_steps.pop_front(); connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); m_currentStep->perform(); } void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) { if (changeState(resultingState, message)) nextStep(); } bool AuthFlow::changeState(AccountTaskState newState, QString reason) { m_taskState = newState; setDetails(reason); switch (newState) { case AccountTaskState::STATE_CREATED: { setStatus(tr("Waiting...")); m_data->errorString.clear(); return true; } case AccountTaskState::STATE_WORKING: { if (!m_currentStep) { setStatus(tr("Preparing to log in...")); } m_data->accountState = AccountState::Working; return true; } case AccountTaskState::STATE_SUCCEEDED: { setStatus(tr("Authentication task succeeded.")); m_data->accountState = AccountState::Online; emitSucceeded(); return false; } case AccountTaskState::STATE_OFFLINE: { setStatus(tr("Failed to contact the authentication server.")); m_data->errorString = reason; m_data->accountState = AccountState::Offline; emitFailed(reason); return false; } case AccountTaskState::STATE_DISABLED: { setStatus(tr("Client ID has changed. New session needs to be created.")); m_data->errorString = reason; m_data->accountState = AccountState::Disabled; emitFailed(reason); return false; } case AccountTaskState::STATE_FAILED_SOFT: { setStatus(tr("Encountered an error during authentication.")); m_data->errorString = reason; m_data->accountState = AccountState::Errored; emitFailed(reason); return false; } case AccountTaskState::STATE_FAILED_HARD: { setStatus(tr("Failed to authenticate. The session has expired.")); m_data->errorString = reason; m_data->accountState = AccountState::Expired; emitFailed(reason); return false; } case AccountTaskState::STATE_FAILED_GONE: { setStatus(tr("Failed to authenticate. The account no longer exists.")); m_data->errorString = reason; m_data->accountState = AccountState::Gone; emitFailed(reason); return false; } default: { setStatus(tr("...")); QString error = tr("Unknown account task state: %1").arg(int(newState)); m_data->accountState = AccountState::Errored; emitFailed(error); return false; } } } bool AuthFlow::abort() { if (m_currentStep) m_currentStep->abort(); emitAborted(); return true; } PrismLauncher-10.0.5/launcher/minecraft/auth/AccountData.h0000644000175100017510000000654315144136756023054 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include enum class Validity { None, Assumed, Certain }; struct Token { QDateTime issueInstant; QDateTime notAfter; QString token; QString refresh_token; QVariantMap extra; Validity validity = Validity::None; bool persistent = true; }; struct Skin { QString id; QString url; QString variant; QByteArray data; }; struct Cape { QString id; QString url; QString alias; QByteArray data; }; struct MinecraftEntitlement { bool ownsMinecraft = false; bool canPlayMinecraft = false; Validity validity = Validity::None; }; struct MinecraftProfile { QString id; QString name; Skin skin; QString currentCape; QMap capes; Validity validity = Validity::None; }; enum class AccountType { MSA, Offline }; enum class AccountState { Unchecked, Offline, Working, Online, Disabled, Errored, Expired, Gone }; struct AccountData { QJsonObject saveState() const; bool resumeStateFromV3(QJsonObject data); //! userName for Mojang accounts, gamertag for MSA QString accountDisplayString() const; //! Yggdrasil access token, as passed to the game. QString accessToken() const; QString profileId() const; QString profileName() const; QString lastError() const; AccountType type = AccountType::MSA; QString msaClientID; Token msaToken; Token userToken; Token xboxApiToken; Token mojangservicesToken; Token yggdrasilToken; MinecraftProfile minecraftProfile; MinecraftEntitlement minecraftEntitlement; Validity validity_ = Validity::None; // runtime only information (not saved with the account) QString internalId; QString errorString; AccountState accountState = AccountState::Unchecked; }; PrismLauncher-10.0.5/launcher/minecraft/auth/MinecraftAccount.h0000644000175100017510000001210015144136756024075 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include "AccountData.h" #include "AuthSession.h" #include "QObjectPtr.h" #include "Usable.h" #include "minecraft/auth/AuthFlow.h" class Task; class MinecraftAccount; using MinecraftAccountPtr = shared_qobject_ptr; Q_DECLARE_METATYPE(MinecraftAccountPtr) /** * A profile within someone's Mojang account. * * Currently, the profile system has not been implemented by Mojang yet, * but we might as well add some things for it in Prism Launcher right now so * we don't have to rip the code to pieces to add it later. */ struct AccountProfile { QString id; QString name; bool legacy; }; /** * Object that stores information about a certain Mojang account. * * Said information may include things such as that account's username, client token, and access * token if the user chose to stay logged in. */ class MinecraftAccount : public QObject, public Usable { Q_OBJECT public: /* construction */ //! Do not copy accounts. ever. explicit MinecraftAccount(const MinecraftAccount& other, QObject* parent) = delete; //! Default constructor explicit MinecraftAccount(QObject* parent = 0); static MinecraftAccountPtr createBlankMSA(); static MinecraftAccountPtr createOffline(const QString& username); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject& json); static QUuid uuidFromUsername(QString username); //! Saves a MinecraftAccount to a JSON object and returns it. QJsonObject saveToJson() const; public: /* manipulation */ shared_qobject_ptr login(bool useDeviceCode = false); shared_qobject_ptr refresh(); shared_qobject_ptr currentTask(); public: /* queries */ QString internalId() const { return data.internalId; } QString accountDisplayString() const { return data.accountDisplayString(); } QString accessToken() const { return data.accessToken(); } QString profileId() const { return data.profileId(); } QString profileName() const { return data.profileName(); } bool isActive() const; AccountType accountType() const noexcept { return data.type; } bool ownsMinecraft() const { return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; } bool hasProfile() const { return data.profileId().size() != 0; } QString typeString() const { switch (data.type) { case AccountType::MSA: { return "msa"; } break; case AccountType::Offline: { return "offline"; } break; default: { return "unknown"; } } } QPixmap getFace() const; //! Returns the current state of the account AccountState accountState() const; AccountData* accountData() { return &data; } bool shouldRefresh() const; void fillSession(AuthSessionPtr session); QString lastError() const { return data.lastError(); } signals: /** * This signal is emitted when the account changes */ void changed(); void activityChanged(bool active); // TODO: better signalling for the various possible state changes - especially errors protected: /* variables */ AccountData data; // current task we are executing here shared_qobject_ptr m_currentTask; protected: /* methods */ void incrementUses() override; void decrementUses() override; private slots: void authSucceeded(); void authFailed(QString reason); }; PrismLauncher-10.0.5/launcher/minecraft/auth/AuthFlow.h0000644000175100017510000000224515144136756022412 0ustar runnerrunner#pragma once #include #include #include #include #include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthStep.h" #include "tasks/Task.h" class AuthFlow : public Task { Q_OBJECT public: enum class Action { Refresh, Login, DeviceCode }; explicit AuthFlow(AccountData* data, Action action = Action::Refresh); virtual ~AuthFlow() = default; void executeTask() override; AccountTaskState taskState() { return m_taskState; } public slots: bool abort() override; signals: void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); protected: void succeed(); void nextStep(); private slots: // NOTE: true -> non-terminal state, false -> terminal state bool changeState(AccountTaskState newState, QString reason = QString()); void stepFinished(AccountTaskState resultingState, QString message); private: AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; QList m_steps; AuthStep::Ptr m_currentStep; AccountData* m_data = nullptr; }; PrismLauncher-10.0.5/launcher/minecraft/auth/Parsers.cpp0000644000175100017510000003526415144136756022642 0ustar runnerrunner#include "Parsers.h" #include "Json.h" #include "Logging.h" #include #include #include namespace Parsers { bool getDateTime(QJsonValue value, QDateTime& out) { if (!value.isString()) { return false; } out = QDateTime::fromString(value.toString(), Qt::ISODate); return out.isValid(); } bool getString(QJsonValue value, QString& out) { if (!value.isString()) { return false; } out = value.toString(); return true; } bool getNumber(QJsonValue value, double& out) { if (!value.isDouble()) { return false; } out = value.toDouble(); return true; } bool getNumber(QJsonValue value, int64_t& out) { if (!value.isDouble()) { return false; } out = (int64_t)value.toDouble(); return true; } bool getBool(QJsonValue value, bool& out) { if (!value.isBool()) { return false; } out = value.toBool(); return true; } /* { "IssueInstant":"2020-12-07T19:52:08.4463796Z", "NotAfter":"2020-12-21T19:52:08.4463796Z", "Token":"token", "DisplayClaims":{ "xui":[ { "uhs":"userhash" } ] } } */ // TODO: handle error responses ... /* { "Identity":"0", "XErr":2148916238, "Message":"", "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" } // 2148916233 = missing Xbox account // 2148916238 = child account not linked to a family */ bool parseXTokenResponse(QByteArray& data, Token& output, QString name) { qDebug() << "Parsing" << name << ":"; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } auto obj = doc.object(); if (!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { qWarning() << "User IssueInstant is not a timestamp"; return false; } if (!getDateTime(obj.value("NotAfter"), output.notAfter)) { qWarning() << "User NotAfter is not a timestamp"; return false; } if (!getString(obj.value("Token"), output.token)) { qWarning() << "User Token is not a string"; return false; } auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); if (!arrayVal.isArray()) { qWarning() << "Missing xui claims array"; return false; } bool foundUHS = false; for (auto item : arrayVal.toArray()) { if (!item.isObject()) { continue; } auto obj_ = item.toObject(); if (obj_.contains("uhs")) { foundUHS = true; } else { continue; } // consume all 'display claims' ... whatever that means for (auto iter = obj_.begin(); iter != obj_.end(); iter++) { QString claim; if (!getString(obj_.value(iter.key()), claim)) { qWarning() << "display claim" << iter.key() << "is not a string..."; return false; } output.extra[iter.key()] = claim; } break; } if (!foundUHS) { qWarning() << "Missing uhs"; return false; } output.validity = Validity::Certain; qDebug() << name << "is valid."; return true; } bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) { qDebug() << "Parsing Minecraft profile..."; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } auto obj = doc.object(); if (!getString(obj.value("id"), output.id)) { qWarning() << "Minecraft profile id is not a string"; return false; } if (!getString(obj.value("name"), output.name)) { qWarning() << "Minecraft profile name is not a string"; return false; } auto skinsArray = obj.value("skins").toArray(); for (auto skin : skinsArray) { auto skinObj = skin.toObject(); Skin skinOut; if (!getString(skinObj.value("id"), skinOut.id)) { continue; } QString state; if (!getString(skinObj.value("state"), state)) { continue; } if (state != "ACTIVE") { continue; } if (!getString(skinObj.value("url"), skinOut.url)) { continue; } skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(skinObj.value("variant"), skinOut.variant)) { continue; } // we deal with only the active skin output.skin = skinOut; break; } auto capesArray = obj.value("capes").toArray(); QString currentCape; for (auto cape : capesArray) { auto capeObj = cape.toObject(); Cape capeOut; if (!getString(capeObj.value("id"), capeOut.id)) { continue; } QString state; if (!getString(capeObj.value("state"), state)) { continue; } if (state == "ACTIVE") { currentCape = capeOut.id; } if (!getString(capeObj.value("url"), capeOut.url)) { continue; } capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(capeObj.value("alias"), capeOut.alias)) { continue; } output.capes[capeOut.id] = capeOut; } output.currentCape = currentCape; output.validity = Validity::Certain; return true; } namespace { // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) // they are needed because the session server doesn't return skin urls for default skins static const QString SKIN_URL_STEVE = "https://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; static const QString SKIN_URL_ALEX = "https://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; bool isDefaultModelSteve(QString uuid) { // need to calculate *Java* hashCode of UUID // if number is even, skin/model is steve, otherwise it is alex // just in case dashes are in the id uuid.remove('-'); if (uuid.size() != 32) { return true; } // qulonglong is guaranteed to be 64 bits // we need to use unsigned numbers to guarantee truncation below qulonglong most = uuid.left(16).toULongLong(nullptr, 16); qulonglong least = uuid.right(16).toULongLong(nullptr, 16); qulonglong xored = most ^ least; return ((static_cast(xored >> 32)) ^ static_cast(xored)) % 2 == 0; } } // namespace /** Uses session server for skin/cape lookup instead of profile, because locked Mojang accounts cannot access profile endpoint (https://api.minecraftservices.com/minecraft/profile/) ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape { "id": "", "name": "", "properties": [ { "name": "textures", "value": "" } ] } decoded base64 "value": { "timestamp": , "profileId": "", "profileName": "", "textures": { "SKIN": { "url": "" }, "CAPE": { "url": "" } } } */ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) { qDebug() << "Parsing Minecraft profile..."; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response as JSON:" << jsonError.errorString(); return false; } auto obj = Json::requireObject(doc, "mojang minecraft profile"); if (!getString(obj.value("id"), output.id)) { qWarning() << "Minecraft profile id is not a string"; return false; } if (!getString(obj.value("name"), output.name)) { qWarning() << "Minecraft profile name is not a string"; return false; } auto propsArray = obj.value("properties").toArray(); QByteArray texturePayload; for (auto p : propsArray) { auto pObj = p.toObject(); auto name = pObj.value("name"); if (!name.isString() || name.toString() != "textures") { continue; } auto value = pObj.value("value"); if (value.isString()) { texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); } if (!texturePayload.isEmpty()) { break; } } if (texturePayload.isNull()) { qWarning() << "No texture payload data"; return false; } doc = QJsonDocument::fromJson(texturePayload, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response as JSON:" << jsonError.errorString(); return false; } obj = Json::requireObject(doc, "session texture payload"); auto textures = obj.value("textures"); if (!textures.isObject()) { qWarning() << "No textures array in response"; return false; } Skin skinOut; // fill in default skin info ourselves, as this endpoint doesn't provide it bool steve = isDefaultModelSteve(output.id); skinOut.variant = steve ? "CLASSIC" : "SLIM"; skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; // sadly we can't figure this out, but I don't think it really matters... skinOut.id = "00000000-0000-0000-0000-000000000000"; Cape capeOut; auto tObj = textures.toObject(); for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) { if (idx->isObject()) { if (idx.key() == "SKIN") { auto skin = idx->toObject(); if (!getString(skin.value("url"), skinOut.url)) { qWarning() << "Skin url is not a string"; return false; } skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); auto maybeMeta = skin.find("metadata"); if (maybeMeta != skin.end() && maybeMeta->isObject()) { auto meta = maybeMeta->toObject(); // might not be present getString(meta.value("model"), skinOut.variant); } } else if (idx.key() == "CAPE") { auto cape = idx->toObject(); if (!getString(cape.value("url"), capeOut.url)) { qWarning() << "Cape url is not a string"; return false; } capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); // we don't know the cape ID as it is not returned from the session server // so just fake it - changing capes is probably locked anyway :( capeOut.alias = "cape"; } } } output.skin = skinOut; if (capeOut.alias == "cape") { output.capes = QMap({ { capeOut.alias, capeOut } }); output.currentCape = capeOut.alias; } output.validity = Validity::Certain; return true; } bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output) { qDebug() << "Parsing Minecraft entitlements..."; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON:" << jsonError.errorString(); return false; } auto obj = doc.object(); output.canPlayMinecraft = false; output.ownsMinecraft = false; auto itemsArray = obj.value("items").toArray(); for (auto item : itemsArray) { auto itemObj = item.toObject(); QString name; if (!getString(itemObj.value("name"), name)) { continue; } if (name == "game_minecraft") { output.canPlayMinecraft = true; } if (name == "product_minecraft") { output.ownsMinecraft = true; } } output.validity = Validity::Certain; return true; } bool parseRolloutResponse(QByteArray& data, bool& result) { qDebug() << "Parsing Rollout response..."; qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); return false; } auto obj = doc.object(); QString feature; if (!getString(obj.value("feature"), feature)) { qWarning() << "Rollout feature is not a string"; return false; } if (feature != "msamigration") { qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; return false; } if (!getBool(obj.value("rollout"), result)) { qWarning() << "Rollout feature is not a string"; return false; } return true; } bool parseMojangResponse(QByteArray& data, Token& output) { QJsonParseError jsonError; qDebug() << "Parsing Mojang response..."; qCDebug(authCredentials()) << data; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error) { qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON:" << jsonError.errorString(); return false; } auto obj = doc.object(); double expires_in = 0; if (!getNumber(obj.value("expires_in"), expires_in)) { qWarning() << "expires_in is not a valid number"; return false; } auto currentTime = QDateTime::currentDateTimeUtc(); output.issueInstant = currentTime; output.notAfter = currentTime.addSecs(expires_in); QString username; if (!getString(obj.value("username"), username)) { qWarning() << "username is not valid"; return false; } // TODO: it's a JWT... validate it? if (!getString(obj.value("access_token"), output.token)) { qWarning() << "access_token is not valid"; return false; } output.validity = Validity::Certain; qDebug() << "Mojang response is valid."; return true; } } // namespace Parsers PrismLauncher-10.0.5/launcher/minecraft/ParseUtils.cpp0000644000175100017510000000157415144136756022352 0ustar runnerrunner#include "ParseUtils.h" #include #include #include #include QDateTime timeFromS3Time(QString str) { return QDateTime::fromString(str, Qt::ISODate); } QString timeToS3Time(QDateTime time) { // this all because Qt can't format timestamps right. int offsetRaw = time.offsetFromUtc(); bool negative = offsetRaw < 0; int offsetAbs = std::abs(offsetRaw); int offsetSeconds = offsetAbs % 60; offsetAbs -= offsetSeconds; int offsetMinutes = offsetAbs % 3600; offsetAbs -= offsetMinutes; offsetMinutes /= 60; int offsetHours = offsetAbs / 3600; QString raw = time.toString("yyyy-MM-ddTHH:mm:ss"); raw += (negative ? QChar('-') : QChar('+')); raw += QString("%1").arg(offsetHours, 2, 10, QChar('0')); raw += ":"; raw += QString("%1").arg(offsetMinutes, 2, 10, QChar('0')); return raw; } PrismLauncher-10.0.5/launcher/minecraft/World.cpp0000644000175100017510000003364215144136756021347 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "World.h" #include #include #include #include #include #include #include #include #include #include #include "GZip.h" #include #include #include "FileSystem.h" #include "PSaveFile.h" #include "archive/ArchiveReader.h" using std::nullopt; using std::optional; GameType::GameType(std::optional original) : original(original) { if (!original) { return; } switch (*original) { case 0: type = GameType::Survival; break; case 1: type = GameType::Creative; break; case 2: type = GameType::Adventure; break; case 3: type = GameType::Spectator; break; default: break; } } QString GameType::toTranslatedString() const { switch (type) { case GameType::Survival: return QCoreApplication::translate("GameType", "Survival"); case GameType::Creative: return QCoreApplication::translate("GameType", "Creative"); case GameType::Adventure: return QCoreApplication::translate("GameType", "Adventure"); case GameType::Spectator: return QCoreApplication::translate("GameType", "Spectator"); default: break; } if (original) { return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original); } return QCoreApplication::translate("GameType", "Undefined"); } QString GameType::toLogString() const { switch (type) { case GameType::Survival: return "Survival"; case GameType::Creative: return "Creative"; case GameType::Adventure: return "Adventure"; case GameType::Spectator: return "Spectator"; default: break; } if (original) { return QString("Unknown (%1)").arg(*original); } return "Undefined"; } std::unique_ptr parseLevelDat(QByteArray data) { QByteArray output; if (!GZip::unzip(data, output)) { return nullptr; } std::istringstream foo(std::string(output.constData(), output.size())); try { auto pair = nbt::io::read_compound(foo); if (pair.first != "") return nullptr; if (pair.second == nullptr) return nullptr; return std::move(pair.second); } catch (const nbt::io::input_error& e) { qWarning() << "Unable to parse level.dat:" << e.what(); return nullptr; } } QByteArray serializeLevelDat(nbt::tag_compound* levelInfo) { std::ostringstream s; nbt::io::write_tag("", *levelInfo, s); QByteArray val(s.str().data(), (int)s.str().size()); return val; } QString getLevelDatFromFS(const QFileInfo& file) { QDir worldDir(file.filePath()); if (!file.isDir() || !worldDir.exists("level.dat")) { return QString(); } return worldDir.absoluteFilePath("level.dat"); } QByteArray getLevelDatDataFromFS(const QFileInfo& file) { auto fullFilePath = getLevelDatFromFS(file); if (fullFilePath.isNull()) { return QByteArray(); } QFile f(fullFilePath); if (!f.open(QIODevice::ReadOnly)) { return QByteArray(); } return f.readAll(); } bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) { auto fullFilePath = getLevelDatFromFS(file); if (fullFilePath.isNull()) { return false; } PSaveFile f(fullFilePath); if (!f.open(QIODevice::WriteOnly)) { return false; } QByteArray compressed; if (!GZip::zip(data, compressed)) { return false; } if (f.write(compressed) != compressed.size()) { f.cancelWriting(); return false; } return f.commit(); } World::World(const QFileInfo& file) { repath(file); } void World::repath(const QFileInfo& file) { m_containerFile = file; m_folderName = file.fileName(); if (file.isFile() && file.suffix() == "zip") { m_iconFile = QString(); readFromZip(file); } else if (file.isDir()) { QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); if (assumedIconPath.exists()) { m_iconFile = assumedIconPath.absoluteFilePath(); } readFromFS(file); } } bool World::resetIcon() { if (m_iconFile.isNull()) { return false; } if (QFile(m_iconFile).remove()) { m_iconFile = QString(); return true; } return false; } void World::readFromFS(const QFileInfo& file) { auto bytes = getLevelDatDataFromFS(file); if (bytes.isEmpty()) { m_isValid = false; return; } loadFromLevelDat(bytes); m_levelDatTime = file.lastModified(); } void World::readFromZip(const QFileInfo& file) { MMCZip::ArchiveReader r(file.absoluteFilePath()); m_isValid = false; r.parse([this](MMCZip::ArchiveReader::File* file, bool& stop) { const QString levelDat = "level.dat"; auto filePath = file->filename(); QFileInfo fi(filePath); if (fi.fileName().compare(levelDat, Qt::CaseInsensitive) == 0) { m_containerOffsetPath = filePath.chopped(levelDat.length()); if (!m_containerOffsetPath.isEmpty()) { return false; } m_levelDatTime = file->dateTime(); loadFromLevelDat(file->readAll()); m_isValid = true; stop = true; } return true; }); } bool World::install(const QString& to, const QString& name) { auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to)); if (!FS::ensureFolderPathExists(finalPath)) { return false; } bool ok = false; if (m_containerFile.isFile()) { MMCZip::ArchiveReader zip(m_containerFile.absoluteFilePath()); ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); } else if (m_containerFile.isDir()) { QString from = m_containerFile.filePath(); ok = FS::copy(from, finalPath)(); } if (ok && !name.isEmpty() && m_actualName != name) { QFileInfo finalPathInfo(finalPath); World newWorld(finalPathInfo); if (newWorld.isValid()) { newWorld.rename(name); } } return ok; } bool World::rename(const QString& newName) { if (m_containerFile.isFile()) { return false; } auto data = getLevelDatDataFromFS(m_containerFile); if (data.isEmpty()) { return false; } auto worldData = parseLevelDat(data); if (!worldData) { return false; } auto& val = worldData->at("Data"); if (val.get_type() != nbt::tag_type::Compound) { return false; } auto& dataCompound = val.as(); dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data())); data = serializeLevelDat(worldData.get()); putLevelDatDataToFS(m_containerFile, data); m_actualName = newName; QDir parentDir(m_containerFile.absoluteFilePath()); parentDir.cdUp(); QFile container(m_containerFile.absoluteFilePath()); auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath()); container.rename(parentDir.absoluteFilePath(dirName)); return true; } namespace { optional read_string(nbt::value& parent, const char* name) { try { auto& namedValue = parent.at(name); if (namedValue.get_type() != nbt::tag_type::String) { return nullopt; } auto& tag_str = namedValue.as(); return QString::fromUtf8(tag_str.get()); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "String NBT tag" << name << "could not be found."; return nullopt; } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to string."; return nullopt; } } optional read_long(nbt::value& parent, const char* name) { try { auto& namedValue = parent.at(name); if (namedValue.get_type() != nbt::tag_type::Long) { return nullopt; } auto& tag_str = namedValue.as(); return tag_str.get(); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "Long NBT tag" << name << "could not be found."; return nullopt; } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to long."; return nullopt; } } optional read_int(nbt::value& parent, const char* name) { try { auto& namedValue = parent.at(name); if (namedValue.get_type() != nbt::tag_type::Int) { return nullopt; } auto& tag_str = namedValue.as(); return tag_str.get(); } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "Int NBT tag" << name << "could not be found."; return nullopt; } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to int."; return nullopt; } } GameType read_gametype(nbt::value& parent, const char* name) { return GameType(read_int(parent, name)); } } // namespace void World::loadFromLevelDat(QByteArray data) { auto levelData = parseLevelDat(data); if (!levelData) { m_isValid = false; return; } nbt::value* valPtr = nullptr; try { valPtr = &levelData->at("Data"); } catch (const std::out_of_range& e) { qWarning().nospace() << "Unable to read NBT tags from " << m_folderName << ": " << e.what(); m_isValid = false; return; } nbt::value& val = *valPtr; m_isValid = val.get_type() == nbt::tag_type::Compound; if (!m_isValid) return; auto name = read_string(val, "LevelName"); m_actualName = name ? *name : m_folderName; auto timestamp = read_long(val, "LastPlayed"); m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; m_gameType = read_gametype(val, "GameType"); optional randomSeed; try { auto& WorldGen_val = val.at("WorldGenSettings"); randomSeed = read_long(WorldGen_val, "seed"); } catch (const std::out_of_range&) { } if (!randomSeed) { randomSeed = read_long(val, "RandomSeed"); } m_randomSeed = randomSeed ? *randomSeed : 0; qDebug() << "World Name:" << m_actualName; qDebug() << "Last Played:" << m_lastPlayed.toString(); if (randomSeed) { qDebug() << "Seed:" << *randomSeed; } qDebug() << "Size:" << m_size; qDebug() << "GameType:" << m_gameType.toLogString(); } bool World::replace(World& with) { if (!destroy()) return false; bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())(); if (success) { m_folderName = with.m_folderName; m_containerFile.refresh(); } return success; } bool World::destroy() { if (!m_isValid) return false; if (FS::trash(m_containerFile.filePath())) return true; if (m_containerFile.isDir()) { QDir d(m_containerFile.filePath()); return d.removeRecursively(); } else if (m_containerFile.isFile()) { QFile file(m_containerFile.absoluteFilePath()); return file.remove(); } return true; } bool World::operator==(const World& other) const { return m_isValid == other.m_isValid && folderName() == other.folderName(); } bool World::isSymLinkUnder(const QString& instPath) const { if (isSymLink()) return true; auto instDir = QDir(instPath); auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath()); auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath()); return relAbsPath != relCanonPath; } bool World::isMoreThanOneHardLink() const { if (m_containerFile.isDir()) { return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1; } return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; } void World::setSize(int64_t size) { m_size = size; } PrismLauncher-10.0.5/launcher/minecraft/Library.cpp0000644000175100017510000003476315144136756021671 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Library.h" #include "MinecraftInstance.h" #include "net/NetRequest.h" #include #include #include #include /** * @brief Collect applicable files for the library. * * Depending on whether the library is native or not, it adds paths to the * appropriate lists for jar files, native libraries for 32-bit, and native * libraries for 64-bit. * * @param runtimeContext The current runtime context. * @param jar List to store paths for jar files. * @param native List to store paths for native libraries. * @param native32 List to store paths for 32-bit native libraries. * @param native64 List to store paths for 64-bit native libraries. * @param overridePath Optional path to override the default storage path. */ void Library::getApplicableFiles(const RuntimeContext& runtimeContext, QStringList& jar, QStringList& native, QStringList& native32, QStringList& native64, const QString& overridePath) const { bool local = isLocal(); // Lambda function to get the absolute file path auto actualPath = [this, local, overridePath](QString relPath) { relPath = FS::RemoveInvalidPathChars(relPath); QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); if (local && !overridePath.isEmpty()) { QString fileName = out.fileName(); return QFileInfo(FS::PathCombine(overridePath, fileName)).absoluteFilePath(); } return out.absoluteFilePath(); }; QString raw_storage = storageSuffix(runtimeContext); if (isNative()) { if (raw_storage.contains("${arch}")) { auto nat32Storage = raw_storage; nat32Storage.replace("${arch}", "32"); auto nat64Storage = raw_storage; nat64Storage.replace("${arch}", "64"); native32 += actualPath(nat32Storage); native64 += actualPath(nat64Storage); } else { native += actualPath(raw_storage); } } else { jar += actualPath(raw_storage); } } /** * @brief Get download requests for the library files. * * Depending on whether the library is native or not, and the current runtime context, * this function prepares download requests for the necessary files. It handles both local * and remote files, checks for stale cache entries, and adds checksummed downloads. * * @param runtimeContext The current runtime context. * @param cache Pointer to the HTTP meta cache. * @param failedLocalFiles List to store paths for failed local files. * @param overridePath Optional path to override the default storage path. * @return QList List of download requests. */ QList Library::getDownloads(const RuntimeContext& runtimeContext, class HttpMetaCache* cache, QStringList& failedLocalFiles, const QString& overridePath) const { QList out; bool stale = isAlwaysStale(); bool local = isLocal(); // Lambda function to check if a local file exists auto check_local_file = [overridePath, &failedLocalFiles](QString storage) { QFileInfo fileinfo(storage); QString fileName = fileinfo.fileName(); auto fullPath = FS::PathCombine(overridePath, fileName); QFileInfo localFileInfo(fullPath); if (!localFileInfo.exists()) { failedLocalFiles.append(localFileInfo.filePath()); return false; } return true; }; // Lambda function to add a download request auto add_download = [this, local, check_local_file, cache, stale, &out](QString storage, QString url, QString sha1) { if (local) { return check_local_file(storage); } auto entry = cache->resolveEntry("libraries", storage); if (stale) { entry->setStale(true); } if (!entry->isStale()) return true; Net::Download::Options options; if (stale) { options |= Net::Download::Option::AcceptLocalFiles; } // Don't add a time limit for the libraries cache entry validity options |= Net::Download::Option::MakeEternal; if (sha1.size()) { auto dl = Net::ApiDownload::makeCached(url, entry, options); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; out.append(dl); } else { out.append(Net::ApiDownload::makeCached(url, entry, options)); qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; } return true; }; QString raw_storage = storageSuffix(runtimeContext); if (m_mojangDownloads) { if (isNative()) { auto nativeClassifier = getCompatibleNative(runtimeContext); if (!nativeClassifier.isNull()) { if (nativeClassifier.contains("${arch}")) { auto nat32Classifier = nativeClassifier; nat32Classifier.replace("${arch}", "32"); auto nat64Classifier = nativeClassifier; nat64Classifier.replace("${arch}", "64"); auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); if (nat32info) { auto cooked_storage = raw_storage; cooked_storage.replace("${arch}", "32"); add_download(cooked_storage, nat32info->url, nat32info->sha1); } auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); if (nat64info) { auto cooked_storage = raw_storage; cooked_storage.replace("${arch}", "64"); add_download(cooked_storage, nat64info->url, nat64info->sha1); } } else { auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier); if (info) { add_download(raw_storage, info->url, info->sha1); } } } else { qDebug() << "Ignoring native library" << m_name.serialize() << "because it has no classifier for current OS"; } } else { if (m_mojangDownloads->artifact) { auto artifact = m_mojangDownloads->artifact; add_download(raw_storage, artifact->url, artifact->sha1); } else { qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact"; } } } else { auto raw_dl = [this, raw_storage]() { if (!m_absoluteURL.isEmpty()) { return m_absoluteURL; } if (m_repositoryURL.isEmpty()) { return BuildConfig.LIBRARY_BASE + raw_storage; } if (m_repositoryURL.endsWith('/')) { return m_repositoryURL + raw_storage; } else { return m_repositoryURL + QChar('/') + raw_storage; } }(); if (raw_storage.contains("${arch}")) { QString cooked_storage = raw_storage; QString cooked_dl = raw_dl; add_download(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32"), QString()); cooked_storage = raw_storage; cooked_dl = raw_dl; add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64"), QString()); } else { add_download(raw_storage, raw_dl, QString()); } } return out; } /** * @brief Check if the library is active in the given runtime context. * * This function evaluates rules to determine if the library should be active, * considering both general rules and native compatibility. * * @param runtimeContext The current runtime context. * @return bool True if the library is active, false otherwise. */ bool Library::isActive(const RuntimeContext& runtimeContext) const { bool result = true; if (m_rules.empty()) { result = true; } else { Rule::Action ruleResult = Rule::Disallow; for (auto rule : m_rules) { Rule::Action temp = rule.apply(runtimeContext); if (temp != Rule::Defer) ruleResult = temp; } result = result && (ruleResult == Rule::Allow); } if (isNative()) { result = result && !getCompatibleNative(runtimeContext).isNull(); } return result; } /** * @brief Check if the library is considered local. * * @return bool True if the library is local, false otherwise. */ bool Library::isLocal() const { return m_hint == "local"; } /** * @brief Check if the library is always considered stale. * * @return bool True if the library is always stale, false otherwise. */ bool Library::isAlwaysStale() const { return m_hint == "always-stale"; } /** * @brief Get the compatible native classifier for the current runtime context. * * This function attempts to match the current runtime context with the appropriate * native classifier. * * @param runtimeContext The current runtime context. * @return QString The compatible native classifier, or an empty string if none is found. */ QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const { // try to match precise classifier "[os]-[arch]" auto entry = m_nativeClassifiers.constFind(runtimeContext.getClassifier()); // try to match imprecise classifier on legacy architectures "[os]" if (entry == m_nativeClassifiers.constEnd() && runtimeContext.isLegacyArch()) entry = m_nativeClassifiers.constFind(runtimeContext.system); if (entry == m_nativeClassifiers.constEnd()) return QString(); return entry.value(); } /** * @brief Set the storage prefix for the library. * * @param prefix The storage prefix to set. */ void Library::setStoragePrefix(QString prefix) { m_storagePrefix = prefix; } /** * @brief Get the default storage prefix for libraries. * * @return QString The default storage prefix. */ QString Library::defaultStoragePrefix() { return "libraries/"; } /** * @brief Get the current storage prefix for the library. * * @return QString The current storage prefix. */ QString Library::storagePrefix() const { if (m_storagePrefix.isEmpty()) { return defaultStoragePrefix(); } return m_storagePrefix; } /** * @brief Get the filename for the library in the current runtime context. * * This function determines the appropriate filename for the library, taking into * account native classifiers if applicable. * * @param runtimeContext The current runtime context. * @return QString The filename of the library. */ QString Library::filename(const RuntimeContext& runtimeContext) const { if (!m_filename.isEmpty()) { return m_filename; } // non-native? use only the gradle specifier if (!isNative()) { return m_name.getFileName(); } // otherwise native, override classifiers. Mojang HACK! GradleSpecifier nativeSpec = m_name; QString nativeClassifier = getCompatibleNative(runtimeContext); if (!nativeClassifier.isNull()) { nativeSpec.setClassifier(nativeClassifier); } else { nativeSpec.setClassifier("INVALID"); } return nativeSpec.getFileName(); } /** * @brief Get the display name for the library in the current runtime context. * * This function returns the display name for the library, defaulting to the filename * if no display name is set. * * @param runtimeContext The current runtime context. * @return QString The display name of the library. */ QString Library::displayName(const RuntimeContext& runtimeContext) const { if (!m_displayname.isEmpty()) return m_displayname; return filename(runtimeContext); } /** * @brief Get the storage suffix for the library in the current runtime context. * * This function determines the appropriate storage suffix for the library, taking into * account native classifiers if applicable. * * @param runtimeContext The current runtime context. * @return QString The storage suffix of the library. */ QString Library::storageSuffix(const RuntimeContext& runtimeContext) const { // non-native? use only the gradle specifier if (!isNative()) { return m_name.toPath(m_filename); } // otherwise native, override classifiers. Mojang HACK! GradleSpecifier nativeSpec = m_name; QString nativeClassifier = getCompatibleNative(runtimeContext); if (!nativeClassifier.isNull()) { nativeSpec.setClassifier(nativeClassifier); } else { nativeSpec.setClassifier("INVALID"); } return nativeSpec.toPath(m_filename); } PrismLauncher-10.0.5/launcher/minecraft/Agent.h0000644000175100017510000000114015144136756020747 0ustar runnerrunner#pragma once #include #include "Library.h" class Agent; using AgentPtr = std::shared_ptr; class Agent { public: Agent(LibraryPtr library, const QString& argument) { m_library = library; m_argument = argument; } public: /* methods */ LibraryPtr library() { return m_library; } QString argument() { return m_argument; } protected: /* data */ /// The library pointing to the jar this Java agent is contained within LibraryPtr m_library; /// The argument to the Java agent, passed after an = if present QString m_argument; }; PrismLauncher-10.0.5/launcher/minecraft/LaunchProfile.h0000644000175100017510000001347215144136756022457 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "Agent.h" #include "Library.h" class LaunchProfile : public ProblemProvider { public: virtual ~LaunchProfile() {} public: /* application of profile variables from patches */ void applyMinecraftVersion(const QString& id); void applyMainClass(const QString& mainClass); void applyAppletClass(const QString& appletClass); void applyMinecraftArguments(const QString& minecraftArguments); void applyAddnJvmArguments(const QStringList& minecraftArguments); void applyMinecraftVersionType(const QString& type); void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets); void applyTraits(const QSet& traits); void applyTweakers(const QStringList& tweakers); void applyJarMods(const QList& jarMods); void applyMods(const QList& jarMods); void applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext); void applyMavenFile(LibraryPtr library, const RuntimeContext& runtimeContext); void applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext); void applyCompatibleJavaMajors(QList& javaMajor); void applyCompatibleJavaName(QString javaName); void applyMainJar(LibraryPtr jar); void applyProblemSeverity(ProblemSeverity severity); /// clear the profile void clear(); public: /* getters for profile variables */ QString getMinecraftVersion() const; QString getMainClass() const; QString getAppletClass() const; QString getMinecraftVersionType() const; MojangAssetIndexInfo::Ptr getMinecraftAssets() const; QString getMinecraftArguments() const; const QStringList& getAddnJvmArguments() const; const QSet& getTraits() const; const QStringList& getTweakers() const; const QList& getJarMods() const; const QList& getLibraries() const; const QList& getNativeLibraries() const; const QList& getMavenFiles() const; const QList& getAgents() const; const QList& getCompatibleJavaMajors() const; const QString getCompatibleJavaName() const; const LibraryPtr getMainJar() const; void getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, const QString& overridePath, const QString& tempPath) const; bool hasTrait(const QString& trait) const; ProblemSeverity getProblemSeverity() const override; const QList getProblems() const override; private: /// the version of Minecraft - jar to use QString m_minecraftVersion; /// Release type - "release" or "snapshot" QString m_minecraftVersionType; /// Assets type - "legacy" or a version ID MojangAssetIndexInfo::Ptr m_minecraftAssets; /** * arguments that should be used for launching minecraft * * ex: "--username ${auth_player_name} --session ${auth_session} * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}" */ QString m_minecraftArguments; /** * Additional arguments to pass to the JVM in addition to those the user has configured, * memory settings, etc. */ QStringList m_addnJvmArguments; /// A list of all tweaker classes QStringList m_tweakers; /// The main class to load first QString m_mainClass; /// The applet class, for some very old minecraft releases QString m_appletClass; /// the list of libraries QList m_libraries; /// the list of maven files to be placed in the libraries folder, but not acted upon QList m_mavenFiles; /// the list of java agents to add to JVM arguments QList m_agents; /// the main jar LibraryPtr m_mainJar; /// the list of native libraries QList m_nativeLibraries; /// traits, collected from all the version files (version files can only add) QSet m_traits; /// A list of jar mods. version files can add those. QList m_jarMods; /// the list of mods QList m_mods; /// compatible java major versions QList m_compatibleJavaMajors; QString m_compatibleJavaName; ProblemSeverity m_problemSeverity = ProblemSeverity::None; }; PrismLauncher-10.0.5/launcher/minecraft/MinecraftInstance.cpp0000644000175100017510000013333715144136756023657 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Jamie Mansfield * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MinecraftInstance.h" #include "Application.h" #include "BuildConfig.h" #include "Json.h" #include "QObjectPtr.h" #include "settings/Setting.h" #include "settings/SettingsObject.h" #include "FileSystem.h" #include "MMCTime.h" #include "java/JavaVersion.h" #include "launch/LaunchTask.h" #include "launch/TaskStepWrapper.h" #include "launch/steps/CheckJava.h" #include "launch/steps/LookupServerAddress.h" #include "launch/steps/PostLaunchCommand.h" #include "launch/steps/PreLaunchCommand.h" #include "launch/steps/QuitAfterGameStop.h" #include "launch/steps/TextPrint.h" #include "minecraft/launch/AutoInstallJava.h" #include "minecraft/launch/ClaimAccount.h" #include "minecraft/launch/CreateGameFolders.h" #include "minecraft/launch/EnsureOfflineLibraries.h" #include "minecraft/launch/ExtractNatives.h" #include "minecraft/launch/LauncherPartLaunch.h" #include "minecraft/launch/ModMinecraftJar.h" #include "minecraft/launch/PrintInstanceInfo.h" #include "minecraft/launch/ReconstructAssets.h" #include "minecraft/launch/ScanModFolders.h" #include "minecraft/launch/VerifyJavaInstall.h" #include "minecraft/update/AssetUpdateTask.h" #include "minecraft/update/FMLLibrariesTask.h" #include "minecraft/update/FoldersTask.h" #include "minecraft/update/LibrariesTask.h" #include "java/JavaUtils.h" #include "icons/IconList.h" #include "mod/ModFolderModel.h" #include "mod/ResourcePackFolderModel.h" #include "mod/ShaderPackFolderModel.h" #include "mod/TexturePackFolderModel.h" #include "WorldList.h" #include "AssetsUtils.h" #include "MinecraftLoadAndCheck.h" #include "PackProfile.h" #include "tools/BaseProfiler.h" #include #include #include #include #include #ifdef Q_OS_LINUX #include "MangoHud.h" #endif #ifdef WITH_QTDBUS #include #endif #define IBUS "@im=ibus" [[maybe_unused]] static bool switcherooSetupGPU(QProcessEnvironment& env) { #ifdef WITH_QTDBUS if (!QDBusConnection::systemBus().isConnected()) return false; QDBusInterface switcheroo("net.hadess.SwitcherooControl", "/net/hadess/SwitcherooControl", "org.freedesktop.DBus.Properties", QDBusConnection::systemBus()); if (!switcheroo.isValid()) return false; QDBusReply reply = switcheroo.call(QStringLiteral("Get"), QStringLiteral("net.hadess.SwitcherooControl"), QStringLiteral("GPUs")); if (!reply.isValid()) return false; QDBusArgument arg = qvariant_cast(reply.value().variant()); QList gpus; arg >> gpus; for (const auto& gpu : gpus) { QString name = qvariant_cast(gpu[QStringLiteral("Name")]); bool defaultGpu = qvariant_cast(gpu[QStringLiteral("Default")]); if (!defaultGpu) { QStringList envList = qvariant_cast(gpu[QStringLiteral("Environment")]); for (int i = 0; i + 1 < envList.size(); i += 2) { env.insert(envList[i], envList[i + 1]); } return true; } } #endif return false; } // all of this because keeping things compatible with deprecated old settings // if either of the settings {a, b} is true, this also resolves to true class OrSetting : public Setting { Q_OBJECT public: OrSetting(QString id, std::shared_ptr a, std::shared_ptr b) : Setting({ id }, false), m_a(a), m_b(b) {} virtual QVariant get() const { bool a = m_a->get().toBool(); bool b = m_b->get().toBool(); return a || b; } virtual void reset() {} virtual void set(QVariant value) {} private: std::shared_ptr m_a; std::shared_ptr m_b; }; MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : BaseInstance(globalSettings, settings, rootDir) { m_components.reset(new PackProfile(this)); } void MinecraftInstance::saveNow() { m_components->saveNow(); } void MinecraftInstance::loadSpecificSettings() { if (isSpecificSettingsLoaded()) return; // Java Settings auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false); m_settings->registerSetting("AutomaticJava", false); if (auto global_settings = globalSettings()) { m_settings->registerOverride(global_settings->getSetting("JavaPath"), locationOverride); m_settings->registerOverride(global_settings->getSetting("JvmArgs"), argsOverride); m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), locationOverride); // special! m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), locationOverride); m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), locationOverride); m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), locationOverride); m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), locationOverride); m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), locationOverride); // Window Size auto windowSetting = m_settings->registerSetting("OverrideWindow", false); m_settings->registerOverride(global_settings->getSetting("LaunchMaximized"), windowSetting); m_settings->registerOverride(global_settings->getSetting("MinecraftWinWidth"), windowSetting); m_settings->registerOverride(global_settings->getSetting("MinecraftWinHeight"), windowSetting); // Memory auto memorySetting = m_settings->registerSetting("OverrideMemory", false); m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting); m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting); // Native library workarounds auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("CustomOpenALPath"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride); // Performance related options auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride); m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride); // Miscellaneous auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); // Legacy-related options auto legacySettings = m_settings->registerSetting("OverrideLegacySettings", false); m_settings->registerOverride(global_settings->getSetting("OnlineFixes"), legacySettings); auto envSetting = m_settings->registerSetting("OverrideEnv", false); m_settings->registerOverride(global_settings->getSetting("Env"), envSetting); m_settings->set("InstanceType", "OneSix"); } // Join server on launch, this does not have a global override m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); m_settings->registerSetting("JoinWorldOnLaunch", ""); // Use account for instance, this does not have a global override m_settings->registerSetting("UseAccountForInstance", false); m_settings->registerSetting("InstanceAccountId", ""); m_settings->registerSetting("ExportName", ""); m_settings->registerSetting("ExportVersion", "1.0.0"); m_settings->registerSetting("ExportSummary", ""); m_settings->registerSetting("ExportAuthor", ""); m_settings->registerSetting("ExportOptionalFiles", true); m_settings->registerSetting("ExportRecommendedRAM"); auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); // Join server on launch, this does not have a global override m_settings->registerSetting("OverrideModDownloadLoaders", false); m_settings->registerSetting("ModDownloadLoaders", "[]"); qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); updateRuntimeContext(); } void MinecraftInstance::updateRuntimeContext() { m_runtimeContext.updateFromInstanceSettings(m_settings); m_components->invalidateLaunchProfile(); } QString MinecraftInstance::typeName() const { return "Minecraft"; } std::shared_ptr MinecraftInstance::getPackProfile() const { return m_components; } QSet MinecraftInstance::traits() const { auto components = getPackProfile(); if (!components) { return { "version-incomplete" }; } auto profile = components->getProfile(); if (!profile) { return { "version-incomplete" }; } return profile->getTraits(); } // FIXME: move UI code out of MinecraftInstance void MinecraftInstance::populateLaunchMenu(QMenu* menu) { QAction* normalLaunch = menu->addAction(tr("&Launch")); normalLaunch->setShortcut(QKeySequence::Open); QAction* normalLaunchOffline = menu->addAction(tr("Launch &Offline")); normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); QAction* normalLaunchDemo = menu->addAction(tr("Launch &Demo")); normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O"))); normalLaunchDemo->setEnabled(supportsDemo()); connect(normalLaunch, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this()); }); connect(normalLaunchOffline, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, false); }); connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, true); }); QString profilersTitle = tr("Profilers"); menu->addSeparator()->setText(profilersTitle); auto profilers = new QActionGroup(menu); profilers->setExclusive(true); connect(profilers, &QActionGroup::triggered, [this](QAction* action) { settings()->set("Profiler", action->data()); emit profilerChanged(); }); QAction* noProfilerAction = menu->addAction(tr("&No Profiler")); noProfilerAction->setData(""); noProfilerAction->setCheckable(true); noProfilerAction->setChecked(true); profilers->addAction(noProfilerAction); for (auto profiler = APPLICATION->profilers().begin(); profiler != APPLICATION->profilers().end(); profiler++) { QAction* profilerAction = menu->addAction(profiler.value()->name()); profilers->addAction(profilerAction); profilerAction->setData(profiler.key()); profilerAction->setCheckable(true); profilerAction->setChecked(settings()->get("Profiler").toString() == profiler.key()); QString error; profilerAction->setEnabled(profiler.value()->check(&error)); } } QString MinecraftInstance::gameRoot() const { QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); if (dotMCDir.exists() && !mcDir.exists()) return dotMCDir.filePath(); else return mcDir.filePath(); } QString MinecraftInstance::binRoot() const { return FS::PathCombine(gameRoot(), "bin"); } QString MinecraftInstance::getNativePath() const { QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); return natives_dir.absolutePath(); } QString MinecraftInstance::getLocalLibraryPath() const { QDir libraries_dir(FS::PathCombine(instanceRoot(), "libraries/")); return libraries_dir.absolutePath(); } bool MinecraftInstance::supportsDemo() const { Version instance_ver{ getPackProfile()->getComponentVersion("net.minecraft") }; // Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History // FIXME: Due to Version constraints atm, this can't handle well non-release versions return instance_ver >= Version("1.3.1"); } QString MinecraftInstance::jarModsDir() const { QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); return jarmods_dir.absolutePath(); } QString MinecraftInstance::modsRoot() const { return FS::PathCombine(gameRoot(), "mods"); } QString MinecraftInstance::modsCacheLocation() const { return FS::PathCombine(instanceRoot(), "mods.cache"); } QString MinecraftInstance::coreModsDir() const { return FS::PathCombine(gameRoot(), "coremods"); } QString MinecraftInstance::nilModsDir() const { return FS::PathCombine(gameRoot(), "nilmods"); } QString MinecraftInstance::dataPacksDir() { QString relativePath = settings()->get("GlobalDataPacksPath").toString(); if (relativePath.isEmpty()) relativePath = "datapacks"; return QDir(gameRoot()).filePath(relativePath); } QString MinecraftInstance::resourcePacksDir() const { return FS::PathCombine(gameRoot(), "resourcepacks"); } QString MinecraftInstance::texturePacksDir() const { return FS::PathCombine(gameRoot(), "texturepacks"); } QString MinecraftInstance::shaderPacksDir() const { return FS::PathCombine(gameRoot(), "shaderpacks"); } QString MinecraftInstance::instanceConfigFolder() const { return FS::PathCombine(gameRoot(), "config"); } QString MinecraftInstance::libDir() const { return FS::PathCombine(gameRoot(), "lib"); } QString MinecraftInstance::worldDir() const { return FS::PathCombine(gameRoot(), "saves"); } QString MinecraftInstance::resourcesDir() const { return FS::PathCombine(gameRoot(), "resources"); } QDir MinecraftInstance::librariesPath() const { return QDir::current().absoluteFilePath("libraries"); } QDir MinecraftInstance::jarmodsPath() const { return QDir(jarModsDir()); } QDir MinecraftInstance::versionsPath() const { return QDir::current().absoluteFilePath("versions"); } QStringList MinecraftInstance::getClassPath() { QStringList jars, nativeJars; auto profile = m_components->getProfile(); profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); return jars; } QString MinecraftInstance::getMainClass() const { auto profile = m_components->getProfile(); return profile->getMainClass(); } QStringList MinecraftInstance::getNativeJars() { QStringList jars, nativeJars; auto profile = m_components->getProfile(); profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); return nativeJars; } static QString replaceTokensIn(const QString& text, const QMap& with) { // TODO: does this still work?? QString result; static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); QStringList list; QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text); int lastCapturedEnd = 0; while (i.hasNext()) { QRegularExpressionMatch match = i.next(); result.append(text.mid(lastCapturedEnd, match.capturedStart())); QString key = match.captured(1); auto iter = with.find(key); if (iter != with.end()) { result.append(*iter); } lastCapturedEnd = match.capturedEnd(); } result.append(text.mid(lastCapturedEnd)); return result; } QStringList MinecraftInstance::extraArguments() { auto list = BaseInstance::extraArguments(); auto version = getPackProfile(); if (!version) return list; auto jarMods = getJarMods(); if (!jarMods.isEmpty()) { list.append({ "-Dfml.ignoreInvalidMinecraftCertificates=true", "-Dfml.ignorePatchDiscrepancies=true" }); } auto addn = m_components->getProfile()->getAddnJvmArguments(); if (!addn.isEmpty()) { QMap tokenMapping = makeProfileVarMapping(m_components->getProfile()); for (const QString& item : addn) { list.append(replaceTokensIn(item, tokenMapping)); } } auto agents = m_components->getProfile()->getAgents(); for (auto agent : agents) { QStringList jar, temp1, temp2, temp3; agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); list.append("-javaagent:" + jar[0] + (agent->argument().isEmpty() ? "" : "=" + agent->argument())); } { QString openALPath; QString glfwPath; if (settings()->get("UseNativeOpenAL").toBool()) { openALPath = APPLICATION->m_detectedOpenALPath; auto customPath = settings()->get("CustomOpenALPath").toString(); if (!customPath.isEmpty()) openALPath = customPath; } if (settings()->get("UseNativeGLFW").toBool()) { glfwPath = APPLICATION->m_detectedGLFWPath; auto customPath = settings()->get("CustomGLFWPath").toString(); if (!customPath.isEmpty()) glfwPath = customPath; } QFileInfo openALInfo(openALPath); QFileInfo glfwInfo(glfwPath); if (!openALPath.isEmpty() && openALInfo.exists()) list.append("-Dorg.lwjgl.openal.libname=" + openALInfo.absoluteFilePath()); if (!glfwPath.isEmpty() && glfwInfo.exists()) list.append("-Dorg.lwjgl.glfw.libname=" + glfwInfo.absoluteFilePath()); } return list; } QStringList MinecraftInstance::javaArguments() { QStringList args; // custom args go first. we want to override them if we have our own here. args.append(extraArguments()); // OSX dock icon and name #ifdef Q_OS_MAC args << "-Xdock:icon=icon.png"; args << QString("-Xdock:name=\"%1\"").arg(windowTitle()); #endif auto traits_ = traits(); // HACK: fix issues on macOS with 1.13 snapshots // NOTE: Oracle Java option. if there are alternate jvm implementations, this would be the place to customize this for them #ifdef Q_OS_MAC if (traits_.contains("FirstThreadOnMacOS")) { args << QString("-XstartOnFirstThread"); } #endif // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767 #ifdef Q_OS_WIN32 args << QString( "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_" "minecraft.exe.heapdump"); #endif // LWJGL2 reads `LWJGL_DISABLE_XRANDR` to force disable xrandr usage and fall back to xf86videomode. // It *SHOULD* check for the executable to exist before trying to use it for queries but it doesnt, // so WE can and force disable xrandr if it is not available. #ifdef Q_OS_LINUX // LWJGL2 is "org.lwjgl" LWJGL3 is "org.lwjgl3" if (m_components->getComponent("org.lwjgl") != nullptr && QStandardPaths::findExecutable("xrandr").isEmpty()) { args << QString("-DLWJGL_DISABLE_XRANDR=true"); } #endif int min = settings()->get("MinMemAlloc").toInt(); int max = settings()->get("MaxMemAlloc").toInt(); if (min < max) { args << QString("-Xms%1m").arg(min); args << QString("-Xmx%1m").arg(max); } else { args << QString("-Xms%1m").arg(max); args << QString("-Xmx%1m").arg(min); } // No PermGen in newer java. JavaVersion javaVersion = getJavaVersion(); if (javaVersion.requiresPermGen()) { auto permgen = settings()->get("PermGen").toInt(); if (permgen != 64) { args << QString("-XX:PermSize=%1m").arg(permgen); } } args << "-Duser.language=en"; if (javaVersion.isModular() && shouldApplyOnlineFixes()) // allow reflective access to java.net - required by the skin fix args << "--add-opens" << "java.base/java.net=ALL-UNNAMED"; return args; } QString MinecraftInstance::getLauncher() { // use legacy launcher if the traits are set if (isLegacy()) return "legacy"; return "standard"; } bool MinecraftInstance::shouldApplyOnlineFixes() { return traits().contains("legacyServices") && settings()->get("OnlineFixes").toBool(); } QMap MinecraftInstance::getVariables() { QMap out; out.insert("INST_NAME", name()); out.insert("INST_ID", id()); out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath())); out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); out.insert("NO_COLOR", "1"); #ifdef Q_OS_MACOS // get library for Steam overlay support QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES"); if (!steamDyldInsertLibraries.isEmpty()) { out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries); } #endif return out; } QProcessEnvironment MinecraftInstance::createEnvironment() { // prepare the process environment QProcessEnvironment env = CleanEnviroment(); // export some infos auto variables = getVariables(); for (auto it = variables.begin(); it != variables.end(); ++it) { env.insert(it.key(), it.value()); } // custom env auto insertEnv = [&env](QString value) { auto envMap = Json::toMap(value); if (envMap.isEmpty()) return; for (auto iter = envMap.begin(); iter != envMap.end(); iter++) env.insert(iter.key(), iter.value().toString()); }; bool overrideEnv = settings()->get("OverrideEnv").toBool(); if (!overrideEnv) insertEnv(APPLICATION->settings()->get("Env").toString()); else insertEnv(settings()->get("Env").toString()); return env; } QProcessEnvironment MinecraftInstance::createLaunchEnvironment() { // prepare the process environment QProcessEnvironment env = createEnvironment(); #ifdef Q_OS_LINUX if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud) { QStringList preloadList; if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) preloadList = value.split(QLatin1String(":")); auto mangoHudLibString = MangoHud::getLibraryString(); if (!mangoHudLibString.isEmpty()) { QFileInfo mangoHudLib(mangoHudLibString); QString libPath = mangoHudLib.absolutePath(); auto appendLib = [libPath, &preloadList](QString fileName) { if (QFileInfo(FS::PathCombine(libPath, fileName)).exists()) preloadList << FS::PathCombine(libPath, fileName); }; // dlsym variant is only needed for OpenGL and not included in the vulkan layer appendLib("libMangoHud_dlsym.so"); appendLib("libMangoHud_opengl.so"); appendLib("libMangoHud_shim.so"); preloadList << mangoHudLibString; } env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); env.insert("MANGOHUD", "1"); } if (settings()->get("UseDiscreteGpu").toBool()) { if (!switcherooSetupGPU(env)) { // Open Source Drivers env.insert("DRI_PRIME", "1"); // Proprietary Nvidia Drivers env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); } } if (settings()->get("UseZink").toBool()) { // taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink) env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa"); env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink"); env.insert("GALLIUM_DRIVER", "zink"); } #endif return env; } QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const { auto profile = m_components->getProfile(); QString args_pattern = profile->getMinecraftArguments(); for (auto tweaker : profile->getTweakers()) { args_pattern += " --tweakClass " + tweaker; } if (targetToJoin) { if (!targetToJoin->address.isEmpty()) { if (profile->hasTrait("feature:is_quick_play_multiplayer")) { args_pattern += " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port); } else { args_pattern += " --server " + targetToJoin->address; args_pattern += " --port " + QString::number(targetToJoin->port); } } else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) { args_pattern += " --quickPlaySingleplayer " + targetToJoin->world; } } QMap tokenMapping = makeProfileVarMapping(profile); // yggdrasil! if (session) { // token_mapping["auth_username"] = session->username; tokenMapping["auth_session"] = session->session; tokenMapping["auth_access_token"] = session->access_token; tokenMapping["auth_player_name"] = session->player_name; tokenMapping["auth_uuid"] = session->uuid; tokenMapping["user_properties"] = session->serializeUserProperties(); tokenMapping["user_type"] = session->user_type; if (session->demo) { args_pattern += " --demo"; } } QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); for (int i = 0; i < parts.length(); i++) { parts[i] = replaceTokensIn(parts[i], tokenMapping); } return parts; } QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { QString launchScript; if (!m_components) return QString(); auto profile = m_components->getProfile(); if (!profile) return QString(); auto mainClass = getMainClass(); if (!mainClass.isEmpty()) { launchScript += "mainClass " + mainClass + "\n"; } auto appletClass = profile->getAppletClass(); if (!appletClass.isEmpty()) { launchScript += "appletClass " + appletClass + "\n"; } if (targetToJoin) { if (!targetToJoin->address.isEmpty()) { launchScript += "serverAddress " + targetToJoin->address + "\n"; launchScript += "serverPort " + QString::number(targetToJoin->port) + "\n"; } else if (!targetToJoin->world.isEmpty()) { launchScript += "worldName " + targetToJoin->world + "\n"; } } // generic minecraft params for (auto param : processMinecraftArgs(session, nullptr /* When using a launch script, the server parameters are handled by it*/ )) { launchScript += "param " + param + "\n"; } // window size, title and state, legacy { QString windowParams; if (settings()->get("LaunchMaximized").toBool()) { // FIXME doesn't support maximisation if (!isLegacy()) { auto screen = QGuiApplication::primaryScreen(); auto screenGeometry = screen->availableSize(); // small hack to get the widow decorations for (auto w : QApplication::topLevelWidgets()) { auto mainWindow = qobject_cast(w); if (mainWindow) { auto m = mainWindow->windowHandle()->frameMargins(); screenGeometry = screenGeometry.shrunkBy(m); break; } } windowParams = QString("%1x%2").arg(screenGeometry.width()).arg(screenGeometry.height()); } else { windowParams = "maximized"; } } else { windowParams = QString("%1x%2").arg(settings()->get("MinecraftWinWidth").toInt()).arg(settings()->get("MinecraftWinHeight").toInt()); } launchScript += "windowTitle " + windowTitle() + "\n"; launchScript += "windowParams " + windowParams + "\n"; } // launcher info { launchScript += "launcherBrand " + BuildConfig.LAUNCHER_NAME + "\n"; launchScript += "launcherVersion " + BuildConfig.printableVersionString() + "\n"; } // instance info { launchScript += "instanceName " + name() + "\n"; launchScript += "instanceIconKey " + name() + "\n"; launchScript += "instanceIconPath icon.png\n"; // we already save a copy here } // legacy auth if (session) { launchScript += "userName " + session->player_name + "\n"; launchScript += "sessionId " + session->session + "\n"; } for (auto trait : profile->getTraits()) { launchScript += "traits " + trait + "\n"; } if (shouldApplyOnlineFixes()) launchScript += "onlineFixes true\n"; launchScript += "launcher " + getLauncher() + "\n"; // qDebug() << "Generated launch script:" << launchScript; return launchScript; } QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { QStringList out; out << "Main Class:" << " " + getMainClass() << ""; out << "Native path:" << " " + getNativePath() << ""; auto profile = m_components->getProfile(); // traits auto alltraits = traits(); if (alltraits.size()) { out << "Traits:"; for (auto trait : alltraits) { out << "traits " + trait; } out << ""; } // native libraries auto settings = this->settings(); bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); if (nativeOpenAL || nativeGLFW) { if (nativeOpenAL) out << "Using system OpenAL."; if (nativeGLFW) out << "Using system GLFW."; out << ""; } // libraries and class path. { out << "Libraries:"; QStringList jars, nativeJars; profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); for (auto file : jars) { out << " " + file; } out << ""; out << "Native libraries:"; for (auto file : nativeJars) { out << " " + file; } out << ""; } // mods and core mods auto printModList = [&out](const QString& label, ModFolderModel& model) { if (model.size()) { out << QString("%1:").arg(label); auto modList = model.allMods(); std::sort(modList.begin(), modList.end(), [](auto a, auto b) { auto aName = a->fileinfo().completeBaseName(); auto bName = b->fileinfo().completeBaseName(); return aName.localeAwareCompare(bName) < 0; }); for (auto mod : modList) { if (mod->type() == ResourceType::FOLDER) { out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)"; continue; } if (mod->enabled()) { out << u8" [✔] " + mod->fileinfo().completeBaseName(); } else { out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)"; } } out << ""; } }; printModList("Mods", *(loaderModList().get())); printModList("Core Mods", *(coreModList().get())); // jar mods auto& jarMods = profile->getJarMods(); if (jarMods.size()) { out << "Jar Mods:"; for (auto& jarmod : jarMods) { auto displayname = jarmod->displayName(runtimeContext()); auto realname = jarmod->filename(runtimeContext()); if (displayname != realname) { out << " " + displayname + " (" + realname + ")"; } else { out << " " + realname; } } out << ""; } // minecraft arguments auto params = processMinecraftArgs(nullptr, targetToJoin); out << "Params:"; out << " " + params.join(' '); out << ""; // window size QString windowParams; if (settings->get("LaunchMaximized").toBool()) { out << "Window size: max (if available)"; } else { auto width = settings->get("MinecraftWinWidth").toInt(); auto height = settings->get("MinecraftWinHeight").toInt(); out << "Window size: " + QString::number(width) + " x " + QString::number(height); } out << ""; out << "Launcher: " + getLauncher(); out << ""; return out; } QMap MinecraftInstance::createCensorFilterFromSession(AuthSessionPtr session) { if (!session) { return QMap(); } auto& sessionRef = *session.get(); QMap filter; auto addToFilter = [&filter](QString key, QString value) { if (key.trimmed().size()) { filter[key] = value; } }; if (sessionRef.session != "-") { addToFilter(sessionRef.session, tr("")); } if (sessionRef.access_token != "0") { addToFilter(sessionRef.access_token, tr("")); } addToFilter(sessionRef.uuid, tr("")); return filter; } QMap MinecraftInstance::makeProfileVarMapping(std::shared_ptr profile) const { QMap result; result["profile_name"] = name(); result["version_name"] = profile->getMinecraftVersion(); result["version_type"] = profile->getMinecraftVersionType(); QString absRootDir = QDir(gameRoot()).absolutePath(); result["game_directory"] = absRootDir; QString absAssetsDir = QDir("assets/").absolutePath(); auto assets = profile->getMinecraftAssets(); result["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); // 1.7.3+ assets tokens result["assets_root"] = absAssetsDir; result["assets_index_name"] = assets->id; result["library_directory"] = APPLICATION->metacache()->getBasePath("libraries"); return result; } QStringList MinecraftInstance::getLogFileSearchPaths() { return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; } QString MinecraftInstance::getStatusbarDescription() { QStringList traits; if (hasVersionBroken()) { traits.append(tr("broken")); } QString mcVersion = m_components->getComponentVersion("net.minecraft"); if (mcVersion.isEmpty()) { // Load component info if needed m_components->reload(Net::Mode::Offline); mcVersion = m_components->getComponentVersion("net.minecraft"); } QString description; description.append(tr("Minecraft %1").arg(mcVersion)); if (m_settings->get("ShowGameTime").toBool()) { if (lastTimePlayed() > 0 && lastLaunch() > 0) { QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); description.append( tr(", last played on %1 for %2") .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) .arg(Time::prettifyDuration(lastTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } if (totalTimePlayed() > 0) { description.append( tr(", total played for %1") .arg(Time::prettifyDuration(totalTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } if (hasCrashed()) { description.append(tr(", has crashed.")); } return description; } QList MinecraftInstance::createUpdateTask() { return { // create folders makeShared(this), // libraries download makeShared(this), // FML libraries download and copy into the instance makeShared(this), // assets update makeShared(this), }; } shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { updateRuntimeContext(); // FIXME: get rid of shared_from_this ... auto process = LaunchTask::create(std::dynamic_pointer_cast(shared_from_this())); auto pptr = process.get(); APPLICATION->icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); // print a header { process->appendStep(makeShared(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); } // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) { process->appendStep(makeShared(pptr)); } if (!targetToJoin && settings()->get("JoinServerOnLaunch").toBool()) { QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString(); if (!fullAddress.isEmpty()) { targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(fullAddress, false))); } else { QString world = settings()->get("JoinWorldOnLaunch").toString(); if (!world.isEmpty()) { targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(world, true))); } } } if (targetToJoin && targetToJoin->port == 25565) { // Resolve server address to join on launch auto step = makeShared(pptr); step->setLookupAddress(targetToJoin->address); step->setOutputAddressPtr(targetToJoin); process->appendStep(step); } // load meta { auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline; process->appendStep(makeShared(pptr, makeShared(this, mode))); } // check java { process->appendStep(makeShared(pptr)); process->appendStep(makeShared(pptr)); } // run pre-launch command if that's needed if (getPreLaunchCommand().size()) { auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } // if we aren't in offline mode,. if (session->status != AuthSession::PlayableOffline) { if (!session->demo) { process->appendStep(makeShared(pptr, session)); } for (auto t : createUpdateTask()) { process->appendStep(makeShared(pptr, t)); } } else { process->appendStep(makeShared(pptr, this)); } // if there are any jar mods { process->appendStep(makeShared(pptr)); } // Scan mods folders for mods { process->appendStep(makeShared(pptr)); } // print some instance info here... { process->appendStep(makeShared(pptr, session, targetToJoin)); } // extract native jars if needed { process->appendStep(makeShared(pptr)); } // reconstruct assets if needed { process->appendStep(makeShared(pptr)); } // verify that minimum Java requirements are met { process->appendStep(makeShared(pptr)); } { // actually launch the game auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); step->setTargetToJoin(targetToJoin); process->appendStep(step); } // run post-exit command if that's needed if (getPostExitCommand().size()) { auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } if (session) { process->setCensorFilter(createCensorFilterFromSession(session)); } if (m_settings->get("QuitAfterGameStop").toBool()) { process->appendStep(makeShared(pptr)); } m_launchProcess = process; emit launchTaskChanged(m_launchProcess); return m_launchProcess; } JavaVersion MinecraftInstance::getJavaVersion() { return JavaVersion(settings()->get("JavaVersion").toString()); } std::shared_ptr MinecraftInstance::loaderModList() { if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed, true)); } return m_loader_mod_list; } std::shared_ptr MinecraftInstance::coreModList() { if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed, true)); } return m_core_mod_list; } std::shared_ptr MinecraftInstance::nilModList() { if (!m_nil_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); } return m_nil_mod_list; } std::shared_ptr MinecraftInstance::resourcePackList() { if (!m_resource_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this, is_indexed, true)); } return m_resource_pack_list; } std::shared_ptr MinecraftInstance::texturePackList() { if (!m_texture_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this, is_indexed, true)); } return m_texture_pack_list; } std::shared_ptr MinecraftInstance::shaderPackList() { if (!m_shader_pack_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this, is_indexed, true)); } return m_shader_pack_list; } std::shared_ptr MinecraftInstance::dataPackList() { if (!m_data_pack_list && settings()->get("GlobalDataPacksEnabled").toBool()) { bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); m_data_pack_list.reset(new DataPackFolderModel(dataPacksDir(), this, isIndexed, true)); } return m_data_pack_list; } QList> MinecraftInstance::resourceLists() { return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() }; } std::shared_ptr MinecraftInstance::worldList() { if (!m_world_list) { m_world_list.reset(new WorldList(worldDir(), this)); } return m_world_list; } QList MinecraftInstance::getJarMods() const { auto profile = m_components->getProfile(); QList mods; for (auto jarmod : profile->getJarMods()) { QStringList jar, temp1, temp2, temp3; jarmod->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, jarmodsPath().absolutePath()); // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem)); mods.push_back(new Mod(QFileInfo(jar[0]))); } return mods; } #include "MinecraftInstance.moc" PrismLauncher-10.0.5/launcher/minecraft/VersionFilterData.h0000644000175100017510000000171615144136756023307 0ustar runnerrunner#pragma once #include #include #include #include struct FMLlib { QString filename; QString checksum; }; struct VersionFilterData { VersionFilterData(); // mapping between minecraft versions and FML libraries required QMap> fmlLibsMapping; // set of minecraft versions for which using forge installers is blacklisted QSet forgeInstallerBlacklist; // no new versions below this date will be accepted from Mojang servers QDateTime legacyCutoffDate; // Libraries that belong to LWJGL QSet lwjglWhitelist; // release date of first version to require Java 8 (17w13a) QDateTime java8BeginsDate; // release data of first version to require Java 16 (21w19a) QDateTime java16BeginsDate; // release data of first version to require Java 17 (1.18 Pre Release 2) QDateTime java17BeginsDate; }; extern VersionFilterData g_VersionFilterData; PrismLauncher-10.0.5/launcher/minecraft/LaunchProfile.cpp0000644000175100017510000002335315144136756023011 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LaunchProfile.h" #include void LaunchProfile::clear() { m_minecraftVersion.clear(); m_minecraftVersionType.clear(); m_minecraftAssets.reset(); m_minecraftArguments.clear(); m_addnJvmArguments.clear(); m_tweakers.clear(); m_mainClass.clear(); m_appletClass.clear(); m_libraries.clear(); m_mavenFiles.clear(); m_agents.clear(); m_traits.clear(); m_jarMods.clear(); m_mainJar.reset(); m_problemSeverity = ProblemSeverity::None; } static void applyString(const QString& from, QString& to) { if (from.isEmpty()) return; to = from; } void LaunchProfile::applyMinecraftVersion(const QString& id) { applyString(id, this->m_minecraftVersion); } void LaunchProfile::applyAppletClass(const QString& appletClass) { applyString(appletClass, this->m_appletClass); } void LaunchProfile::applyMainClass(const QString& mainClass) { applyString(mainClass, this->m_mainClass); } void LaunchProfile::applyMinecraftArguments(const QString& minecraftArguments) { applyString(minecraftArguments, this->m_minecraftArguments); } void LaunchProfile::applyAddnJvmArguments(const QStringList& addnJvmArguments) { this->m_addnJvmArguments.append(addnJvmArguments); } void LaunchProfile::applyMinecraftVersionType(const QString& type) { applyString(type, this->m_minecraftVersionType); } void LaunchProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets) { if (assets) { m_minecraftAssets = assets; } } void LaunchProfile::applyTraits(const QSet& traits) { this->m_traits.unite(traits); } void LaunchProfile::applyTweakers(const QStringList& tweakers) { // if the applied tweakers override an existing one, skip it. this effectively moves it later in the sequence QStringList newTweakers; for (auto& tweaker : m_tweakers) { if (tweakers.contains(tweaker)) { continue; } newTweakers.append(tweaker); } // then just append the new tweakers (or moved original ones) newTweakers += tweakers; m_tweakers = newTweakers; } void LaunchProfile::applyJarMods(const QList& jarMods) { this->m_jarMods.append(jarMods); } static int findLibraryByName(QList* haystack, const GradleSpecifier& needle) { int retval = -1; for (int i = 0; i < haystack->size(); ++i) { if (haystack->at(i)->rawName().matchName(needle)) { // only one is allowed. if (retval != -1) return -1; retval = i; } } return retval; } void LaunchProfile::applyMods(const QList& mods) { QList* list = &m_mods; for (auto& mod : mods) { auto modCopy = Library::limitedCopy(mod); // find the mod by name. const int index = findLibraryByName(list, mod->rawName()); // mod not found? just add it. if (index < 0) { list->append(modCopy); return; } auto existingLibrary = list->at(index); // if we are higher it means we should update if (Version(mod->version()) > Version(existingLibrary->version())) { list->replace(index, modCopy); } } } void LaunchProfile::applyCompatibleJavaMajors(QList& javaMajor) { m_compatibleJavaMajors.append(javaMajor); } void LaunchProfile::applyCompatibleJavaName(QString javaName) { if (!javaName.isEmpty()) m_compatibleJavaName = javaName; } void LaunchProfile::applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext) { if (!library->isActive(runtimeContext)) { return; } QList* list = &m_libraries; if (library->isNative()) { list = &m_nativeLibraries; } auto libraryCopy = Library::limitedCopy(library); // find the library by name. const int index = findLibraryByName(list, library->rawName()); // library not found? just add it. if (index < 0) { list->append(libraryCopy); return; } auto existingLibrary = list->at(index); // if we are higher it means we should update if (Version(library->version()) > Version(existingLibrary->version())) { list->replace(index, libraryCopy); } } void LaunchProfile::applyMavenFile(LibraryPtr mavenFile, const RuntimeContext& runtimeContext) { if (!mavenFile->isActive(runtimeContext)) { return; } if (mavenFile->isNative()) { return; } // unlike libraries, we do not keep only one version or try to dedupe them m_mavenFiles.append(Library::limitedCopy(mavenFile)); } void LaunchProfile::applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext) { auto lib = agent->library(); if (!lib->isActive(runtimeContext)) { return; } if (lib->isNative()) { return; } m_agents.append(agent); } const LibraryPtr LaunchProfile::getMainJar() const { return m_mainJar; } void LaunchProfile::applyMainJar(LibraryPtr jar) { if (jar) { m_mainJar = jar; } } void LaunchProfile::applyProblemSeverity(ProblemSeverity severity) { if (m_problemSeverity < severity) { m_problemSeverity = severity; } } const QList LaunchProfile::getProblems() const { // FIXME: implement something that actually makes sense here return {}; } QString LaunchProfile::getMinecraftVersion() const { return m_minecraftVersion; } QString LaunchProfile::getAppletClass() const { return m_appletClass; } QString LaunchProfile::getMainClass() const { return m_mainClass; } const QSet& LaunchProfile::getTraits() const { return m_traits; } const QStringList& LaunchProfile::getTweakers() const { return m_tweakers; } bool LaunchProfile::hasTrait(const QString& trait) const { return m_traits.contains(trait); } ProblemSeverity LaunchProfile::getProblemSeverity() const { return m_problemSeverity; } QString LaunchProfile::getMinecraftVersionType() const { return m_minecraftVersionType; } std::shared_ptr LaunchProfile::getMinecraftAssets() const { if (!m_minecraftAssets) { return std::make_shared("legacy"); } return m_minecraftAssets; } QString LaunchProfile::getMinecraftArguments() const { return m_minecraftArguments; } const QStringList& LaunchProfile::getAddnJvmArguments() const { return m_addnJvmArguments; } const QList& LaunchProfile::getJarMods() const { return m_jarMods; } const QList& LaunchProfile::getLibraries() const { return m_libraries; } const QList& LaunchProfile::getNativeLibraries() const { return m_nativeLibraries; } const QList& LaunchProfile::getMavenFiles() const { return m_mavenFiles; } const QList& LaunchProfile::getAgents() const { return m_agents; } const QList& LaunchProfile::getCompatibleJavaMajors() const { return m_compatibleJavaMajors; } const QString LaunchProfile::getCompatibleJavaName() const { return m_compatibleJavaName; } void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, const QString& overridePath, const QString& tempPath) const { QStringList native32, native64; jars.clear(); nativeJars.clear(); for (auto lib : getLibraries()) { lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); } // NOTE: order is important here, add main jar last to the lists if (m_mainJar) { // FIXME: HACK!! jar modding is weird and unsystematic! if (m_jarMods.size()) { QDir tempDir(tempPath); jars.append(tempDir.absoluteFilePath("minecraft.jar")); } else { m_mainJar->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); } } for (auto lib : getNativeLibraries()) { lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); } if (runtimeContext.javaArchitecture == "32") { nativeJars.append(native32); } else if (runtimeContext.javaArchitecture == "64") { nativeJars.append(native64); } } PrismLauncher-10.0.5/launcher/minecraft/VersionFile.cpp0000644000175100017510000000634015144136756022500 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include "ParseUtils.h" #include "minecraft/Library.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFile.h" #include static bool isMinecraftVersion(const QString& uid) { return uid == "net.minecraft"; } void VersionFile::applyTo(LaunchProfile* profile, const RuntimeContext& runtimeContext) { // Only real Minecraft can set those. Don't let anything override them. if (isMinecraftVersion(uid)) { profile->applyMinecraftVersion(version); profile->applyMinecraftVersionType(type); // HACK: ignore assets from other version files than Minecraft // workaround for stupid assets issue caused by amazon: // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ profile->applyMinecraftAssets(mojangAssetIndex); } profile->applyMainJar(mainJar); profile->applyMainClass(mainClass); profile->applyAppletClass(appletClass); profile->applyMinecraftArguments(minecraftArguments); profile->applyAddnJvmArguments(addnJvmArguments); profile->applyTweakers(addTweakers); profile->applyJarMods(jarMods); profile->applyMods(mods); profile->applyTraits(traits); profile->applyCompatibleJavaMajors(compatibleJavaMajors); profile->applyCompatibleJavaName(compatibleJavaName); for (auto library : libraries) { profile->applyLibrary(library, runtimeContext); } for (auto mavenFile : mavenFiles) { profile->applyMavenFile(mavenFile, runtimeContext); } for (auto agent : agents) { profile->applyAgent(agent, runtimeContext); } profile->applyProblemSeverity(getProblemSeverity()); } PrismLauncher-10.0.5/launcher/minecraft/WorldList.h0000644000175100017510000000634615144136756021651 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include "BaseInstance.h" #include "minecraft/World.h" class QFileSystemWatcher; class WorldList : public QAbstractListModel { Q_OBJECT public: enum Columns { NameColumn, GameModeColumn, LastPlayedColumn, SizeColumn, InfoColumn }; enum Roles { ObjectRole = Qt::UserRole + 1, FolderRole, SeedRole, NameRole, GameModeRole, LastPlayedRole, SizeRole, IconFileRole }; WorldList(const QString& dir, BaseInstance* instance); virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const { return parent.isValid() ? 0 : static_cast(size()); }; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; virtual int columnCount(const QModelIndex& parent) const; size_t size() const { return m_worlds.size(); }; bool empty() const { return size() == 0; } World& operator[](size_t index) { return m_worlds[index]; } /// Reloads the mod list and returns true if the list changed. virtual bool update(); /// Install a world from location void installWorld(QFileInfo filename); /// Deletes the mod at the given index. virtual bool deleteWorld(int index); /// Removes the world icon, if any virtual bool resetIcon(int index); /// Deletes all the selected mods virtual bool deleteWorlds(int first, int last); /// flags, mostly to support drag&drop virtual Qt::ItemFlags flags(const QModelIndex& index) const; /// get data for drag action virtual QMimeData* mimeData(const QModelIndexList& indexes) const; /// get the supported mime types virtual QStringList mimeTypes() const; /// process data from drop action virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); /// what drag actions do we support? virtual Qt::DropActions supportedDragActions() const; /// what drop actions do we support? virtual Qt::DropActions supportedDropActions() const; void startWatching(); void stopWatching(); virtual bool isValid(); QDir dir() const { return m_dir; } QString instDirPath() const; const QList& allWorlds() const { return m_worlds; } private slots: void directoryChanged(QString path); void loadWorldsAsync(); signals: void changed(); protected: BaseInstance* m_instance; QFileSystemWatcher* m_watcher; bool m_isWatching; QDir m_dir; QList m_worlds; }; PrismLauncher-10.0.5/launcher/minecraft/ProfileUtils.h0000644000175100017510000000424215144136756022340 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "Library.h" #include "VersionFile.h" namespace ProfileUtils { using PatchOrder = QStringList; /// Read and parse a OneSix format order file bool readOverrideOrders(QString path, PatchOrder& order); /// Write a OneSix format order file bool writeOverrideOrders(QString path, const PatchOrder& order); /// Parse a version file in JSON format VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, bool requireOrder); /// Save a JSON file (in any format) bool saveJsonFile(const QJsonDocument& doc, const QString& filename); /// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. void removeLwjglFromPatch(VersionFilePtr patch); } // namespace ProfileUtils PrismLauncher-10.0.5/launcher/minecraft/ParseUtils.h0000644000175100017510000000036515144136756022014 0ustar runnerrunner#pragma once #include #include /// take the timestamp used by S3 and turn it into QDateTime QDateTime timeFromS3Time(QString str); /// take a timestamp and convert it into an S3 timestamp QString timeToS3Time(QDateTime); PrismLauncher-10.0.5/launcher/minecraft/MojangVersionFormat.cpp0000644000175100017510000003267115144136756024213 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MojangVersionFormat.h" #include "MojangDownloadInfo.h" #include "OneSixVersionFormat.h" #include "Json.h" using namespace Json; #include #include "ParseUtils.h" static const int CURRENT_MINIMUM_LAUNCHER_VERSION = 18; static MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject& obj); static MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject& obj); static MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject& libObj); static QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr assetidxinfo); static QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo); static QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info); namespace Bits { static void readString(const QJsonObject& root, const QString& key, QString& variable) { if (root.contains(key)) { variable = requireString(root.value(key)); } } static void readDownloadInfo(MojangDownloadInfo::Ptr out, const QJsonObject& obj) { // optional, not used readString(obj, "path", out->path); // required! out->sha1 = requireString(obj, "sha1"); out->url = requireString(obj, "url"); out->size = requireInteger(obj, "size"); } static void readAssetIndex(MojangAssetIndexInfo::Ptr out, const QJsonObject& obj) { out->totalSize = requireInteger(obj, "totalSize"); out->id = requireString(obj, "id"); // out->known = true; } } // namespace Bits MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject& obj) { auto out = std::make_shared(); Bits::readDownloadInfo(out, obj); return out; } MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject& obj) { auto out = std::make_shared(); Bits::readDownloadInfo(out, obj); Bits::readAssetIndex(out, obj); return out; } QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info) { QJsonObject out; if (!info->path.isNull()) { out.insert("path", info->path); } out.insert("sha1", info->sha1); out.insert("size", info->size); out.insert("url", info->url); return out; } MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject& libObj) { auto out = std::make_shared(); auto dlObj = requireObject(libObj.value("downloads")); if (dlObj.contains("artifact")) { out->artifact = downloadInfoFromJson(requireObject(dlObj, "artifact")); } if (dlObj.contains("classifiers")) { auto classifiersObj = requireObject(dlObj, "classifiers"); for (auto iter = classifiersObj.begin(); iter != classifiersObj.end(); iter++) { auto classifier = iter.key(); auto classifierObj = requireObject(iter.value()); out->classifiers[classifier] = downloadInfoFromJson(classifierObj); } } return out; } QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) { QJsonObject out; if (libinfo->artifact) { out.insert("artifact", downloadInfoToJson(libinfo->artifact)); } if (!libinfo->classifiers.isEmpty()) { QJsonObject classifiersOut; for (auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) { classifiersOut.insert(iter.key(), downloadInfoToJson(iter.value())); } out.insert("classifiers", classifiersOut); } return out; } QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr info) { QJsonObject out; if (!info->path.isNull()) { out.insert("path", info->path); } out.insert("sha1", info->sha1); out.insert("size", info->size); out.insert("url", info->url); out.insert("totalSize", info->totalSize); out.insert("id", info->id); return out; } void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFile* out) { Bits::readString(in, "id", out->minecraftVersion); Bits::readString(in, "mainClass", out->mainClass); Bits::readString(in, "minecraftArguments", out->minecraftArguments); Bits::readString(in, "type", out->type); Bits::readString(in, "assets", out->assets); if (in.contains("assetIndex")) { out->mojangAssetIndex = assetIndexFromJson(requireObject(in, "assetIndex")); } else if (!out->assets.isNull()) { out->mojangAssetIndex = std::make_shared(out->assets); } out->releaseTime = timeFromS3Time(in.value("releaseTime").toString("")); out->updateTime = timeFromS3Time(in.value("time").toString("")); if (in.contains("minimumLauncherVersion")) { out->minimumLauncherVersion = requireInteger(in.value("minimumLauncherVersion")); if (out->minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION) { out->addProblem(ProblemSeverity::Warning, QObject::tr("The 'minimumLauncherVersion' value of this version (%1) is higher than " "supported by %3 (%2). It might not work properly!") .arg(out->minimumLauncherVersion) .arg(CURRENT_MINIMUM_LAUNCHER_VERSION) .arg(BuildConfig.LAUNCHER_DISPLAYNAME)); } } if (in.contains("compatibleJavaMajors")) { for (auto compatible : requireArray(in.value("compatibleJavaMajors"))) { out->compatibleJavaMajors.append(requireInteger(compatible)); } } if (in.contains("compatibleJavaName")) { out->compatibleJavaName = requireString(in.value("compatibleJavaName")); } if (in.contains("downloads")) { auto downloadsObj = requireObject(in, "downloads"); for (auto iter = downloadsObj.begin(); iter != downloadsObj.end(); iter++) { auto classifier = iter.key(); auto classifierObj = requireObject(iter.value()); out->mojangDownloads[classifier] = downloadInfoFromJson(classifierObj); } } } VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument& doc, const QString& filename) { VersionFilePtr out(new VersionFile()); if (doc.isEmpty() || doc.isNull()) { throw JSONValidationError(filename + " is empty or null"); } if (!doc.isObject()) { throw JSONValidationError(filename + " is not an object"); } QJsonObject root = doc.object(); readVersionProperties(root, out.get()); out->name = "Minecraft"; out->uid = "net.minecraft"; out->version = out->minecraftVersion; // out->filename = filename; if (root.contains("libraries")) { for (auto libVal : requireArray(root.value("libraries"))) { auto libObj = requireObject(libVal); auto lib = MojangVersionFormat::libraryFromJson(*out, libObj, filename); out->libraries.append(lib); } } return out; } void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObject& out) { writeString(out, "id", in->minecraftVersion); writeString(out, "mainClass", in->mainClass); writeString(out, "minecraftArguments", in->minecraftArguments); writeString(out, "type", in->type); if (!in->releaseTime.isNull()) { writeString(out, "releaseTime", timeToS3Time(in->releaseTime)); } if (!in->updateTime.isNull()) { writeString(out, "time", timeToS3Time(in->updateTime)); } if (in->minimumLauncherVersion != -1) { out.insert("minimumLauncherVersion", in->minimumLauncherVersion); } writeString(out, "assets", in->assets); if (in->mojangAssetIndex && in->mojangAssetIndex->known) { out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); } if (!in->mojangDownloads.isEmpty()) { QJsonObject downloadsOut; for (auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) { downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value())); } out.insert("downloads", downloadsOut); } if (!in->compatibleJavaMajors.isEmpty()) { QJsonArray compatibleJavaMajorsOut; for (auto compatibleJavaMajor : in->compatibleJavaMajors) { compatibleJavaMajorsOut.append(compatibleJavaMajor); } out.insert("compatibleJavaMajors", compatibleJavaMajorsOut); } if (!in->compatibleJavaName.isEmpty()) { writeString(out, "compatibleJavaName", in->compatibleJavaName); } } QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr& patch) { QJsonObject root; writeVersionProperties(patch.get(), root); if (!patch->libraries.isEmpty()) { QJsonArray array; for (auto value : patch->libraries) { array.append(MojangVersionFormat::libraryToJson(value.get())); } root.insert("libraries", array); } // write the contents to a json document. { QJsonDocument out; out.setObject(root); return out; } } LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename) { LibraryPtr out(new Library()); if (!libObj.contains("name")) { throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); } auto rawName = libObj.value("name").toString(); out->m_name = rawName; if (!out->m_name.valid()) { problems.addProblem(ProblemSeverity::Error, QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName)); } Bits::readString(libObj, "url", out->m_repositoryURL); if (libObj.contains("extract")) { out->m_hasExcludes = true; auto extractObj = requireObject(libObj.value("extract")); for (auto excludeVal : requireArray(extractObj.value("exclude"))) { out->m_extractExcludes.append(requireString(excludeVal)); } } if (libObj.contains("natives")) { QJsonObject nativesObj = requireObject(libObj.value("natives")); for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) { if (!it.value().isString()) { qWarning() << filename << "contains an invalid native (skipping)"; } // FIXME: Skip unknown platforms out->m_nativeClassifiers[it.key()] = it.value().toString(); } } if (libObj.contains("rules")) { out->applyRules = true; QJsonArray rulesArray = requireArray(libObj.value("rules")); for (auto rule : rulesArray) { out->m_rules.append(Rule::fromJson(requireObject(rule))); } } if (libObj.contains("downloads")) { out->m_mojangDownloads = libDownloadInfoFromJson(libObj); } return out; } QJsonObject MojangVersionFormat::libraryToJson(Library* library) { QJsonObject libRoot; libRoot.insert("name", library->m_name.serialize()); if (!library->m_repositoryURL.isEmpty()) { libRoot.insert("url", library->m_repositoryURL); } if (library->isNative()) { QJsonObject nativeList; auto iter = library->m_nativeClassifiers.begin(); while (iter != library->m_nativeClassifiers.end()) { nativeList.insert(iter.key(), iter.value()); iter++; } libRoot.insert("natives", nativeList); if (!library->m_extractExcludes.isEmpty()) { QJsonArray excludes; QJsonObject extract; for (auto exclude : library->m_extractExcludes) { excludes.append(exclude); } extract.insert("exclude", excludes); libRoot.insert("extract", extract); } } if (!library->m_rules.isEmpty()) { QJsonArray allRules; for (auto& rule : library->m_rules) { QJsonObject ruleObj = rule.toJson(); allRules.append(ruleObj); } libRoot.insert("rules", allRules); } if (library->m_mojangDownloads) { auto downloadsObj = libDownloadInfoToJson(library->m_mojangDownloads); libRoot.insert("downloads", downloadsObj); } return libRoot; } PrismLauncher-10.0.5/launcher/minecraft/WorldList.cpp0000644000175100017510000003117215144136756022177 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "WorldList.h" #include #include #include #include #include #include #include #include #include #include WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); m_isWatching = false; connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); } void WorldList::startWatching() { if (m_isWatching) { return; } update(); m_isWatching = m_watcher->addPath(m_dir.absolutePath()); if (m_isWatching) { qDebug() << "Started watching" << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching" << m_dir.absolutePath(); } } void WorldList::stopWatching() { if (!m_isWatching) { return; } m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); if (!m_isWatching) { qDebug() << "Stopped watching" << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching" << m_dir.absolutePath(); } } bool WorldList::update() { if (!isValid()) return false; QList newWorlds; m_dir.refresh(); auto folderContents = m_dir.entryInfoList(); // if there are any untracked files... for (QFileInfo entry : folderContents) { if (!entry.isDir()) continue; World w(entry); if (w.isValid()) { newWorlds.append(w); } } beginResetModel(); m_worlds.swap(newWorlds); endResetModel(); loadWorldsAsync(); return true; } void WorldList::directoryChanged(QString) { update(); } bool WorldList::isValid() { return m_dir.exists() && m_dir.isReadable(); } QString WorldList::instDirPath() const { return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } bool WorldList::deleteWorld(int index) { if (index >= m_worlds.size() || index < 0) return false; World& m = m_worlds[index]; if (m.destroy()) { beginRemoveRows(QModelIndex(), index, index); m_worlds.removeAt(index); endRemoveRows(); emit changed(); return true; } return false; } bool WorldList::deleteWorlds(int first, int last) { for (int i = first; i <= last; i++) { World& m = m_worlds[i]; m.destroy(); } beginRemoveRows(QModelIndex(), first, last); m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); endRemoveRows(); emit changed(); return true; } bool WorldList::resetIcon(int row) { if (row >= m_worlds.size() || row < 0) return false; World& m = m_worlds[row]; if (m.resetIcon()) { emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); return true; } return false; } int WorldList::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 5; } QVariant WorldList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); int column = index.column(); if (row < 0 || row >= m_worlds.size()) return QVariant(); QLocale locale; auto& world = m_worlds[row]; switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: return world.name(); case GameModeColumn: return world.gameType().toTranslatedString(); case LastPlayedColumn: return world.lastPlayed(); case SizeColumn: return locale.formattedDataSize(world.bytes()); case InfoColumn: if (world.isSymLinkUnder(instDirPath())) { return tr("This world is symbolically linked from elsewhere."); } if (world.isMoreThanOneHardLink()) { return tr("\nThis world is hard linked elsewhere."); } return ""; default: return QVariant(); } case Qt::UserRole: if (column == SizeColumn) return QVariant::fromValue(world.bytes()); return data(index, Qt::DisplayRole); case Qt::ToolTipRole: { if (column == InfoColumn) { if (world.isSymLinkUnder(instDirPath())) { return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") .arg(world.canonicalFilePath()); } if (world.isMoreThanOneHardLink()) { return tr("Warning: This world is hard linked elsewhere. Editing it will also change the original."); } } return world.folderName(); } case ObjectRole: { return QVariant::fromValue((void*)&world); } case FolderRole: { return QDir::toNativeSeparators(dir().absoluteFilePath(world.folderName())); } case SeedRole: { return QVariant::fromValue(world.seed()); } case NameRole: { return world.name(); } case LastPlayedRole: { return world.lastPlayed(); } case SizeRole: { return QVariant::fromValue(world.bytes()); } case IconFileRole: { return world.iconFile(); } default: return QVariant(); } } QVariant WorldList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case NameColumn: return tr("Name"); case GameModeColumn: return tr("Game Mode"); case LastPlayedColumn: return tr("Last Played"); case SizeColumn: //: World size on disk return tr("Size"); case InfoColumn: //: special warnings? return tr("Info"); default: return QVariant(); } case Qt::ToolTipRole: switch (section) { case NameColumn: return tr("The name of the world."); case GameModeColumn: return tr("Game mode of the world."); case LastPlayedColumn: return tr("Date and time the world was last played."); case SizeColumn: return tr("Size of the world on disk."); case InfoColumn: return tr("Information and warnings about the world."); default: return QVariant(); } default: return QVariant(); } } QStringList WorldList::mimeTypes() const { QStringList types; types << "text/uri-list"; return types; } QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const { QList urls; for (auto idx : indexes) { if (idx.column() != 0) continue; int row = idx.row(); if (row < 0 || row >= this->m_worlds.size()) continue; const World& world = m_worlds[row]; if (!world.isValid() || !world.isOnFS()) continue; QString worldPath = world.container().absoluteFilePath(); qDebug() << worldPath; urls.append(QUrl::fromLocalFile(worldPath)); } auto result = new QMimeData(); result->setUrls(urls); return result; } Qt::ItemFlags WorldList::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); if (index.isValid()) return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; else return Qt::ItemIsDropEnabled | defaultFlags; } Qt::DropActions WorldList::supportedDragActions() const { // move to other mod lists or VOID return Qt::MoveAction; } Qt::DropActions WorldList::supportedDropActions() const { // copy from outside, move from within and other mod lists return Qt::CopyAction | Qt::MoveAction; } void WorldList::installWorld(QFileInfo filename) { qDebug() << "installing:" << filename.absoluteFilePath(); World w(filename); if (!w.isValid()) { return; } w.install(m_dir.absolutePath()); } bool WorldList::dropMimeData(const QMimeData* data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) { if (action == Qt::IgnoreAction) return true; // check if the action is supported if (!data || !(action & supportedDropActions())) return false; // files dropped from outside? if (data->hasUrls()) { bool was_watching = m_isWatching; if (was_watching) stopWatching(); auto urls = data->urls(); for (auto url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; QString filename = url.toLocalFile(); QFileInfo worldInfo(filename); if (!m_dir.entryInfoList().contains(worldInfo)) { installWorld(worldInfo); } } if (was_watching) startWatching(); return true; } return false; } int64_t calculateWorldSize(const QFileInfo& file) { if (file.isFile() && file.suffix() == "zip") { return file.size(); } else if (file.isDir()) { QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); int64_t total = 0; while (it.hasNext()) { it.next(); total += it.fileInfo().size(); } return total; } return -1; } void WorldList::loadWorldsAsync() { for (int i = 0; i < m_worlds.size(); ++i) { auto file = m_worlds.at(i).container(); int row = i; QThreadPool::globalInstance()->start([this, file, row]() mutable { auto size = calculateWorldSize(file); QMetaObject::invokeMethod( this, [this, size, row, file]() { if (row < m_worlds.size() && m_worlds[row].container() == file) { m_worlds[row].setSize(size); // Notify views QModelIndex modelIndex = index(row); emit dataChanged(modelIndex, modelIndex, { SizeRole }); } }, Qt::QueuedConnection); }); } } PrismLauncher-10.0.5/launcher/minecraft/update/0000755000175100017510000000000015144136756021026 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/minecraft/update/LibrariesTask.h0000644000175100017510000000074115144136756023740 0ustar runnerrunner#pragma once #include "net/NetJob.h" #include "tasks/Task.h" class MinecraftInstance; class LibrariesTask : public Task { Q_OBJECT public: LibrariesTask(MinecraftInstance* inst); virtual ~LibrariesTask() = default; void executeTask() override; bool canAbort() const override; private slots: void jarlibFailed(QString reason); public slots: bool abort() override; private: MinecraftInstance* m_inst; NetJob::Ptr downloadJob; }; PrismLauncher-10.0.5/launcher/minecraft/update/FMLLibrariesTask.h0000644000175100017510000000112515144136756024274 0ustar runnerrunner#pragma once #include "minecraft/VersionFilterData.h" #include "net/NetJob.h" #include "tasks/Task.h" class MinecraftInstance; class FMLLibrariesTask : public Task { Q_OBJECT public: FMLLibrariesTask(MinecraftInstance* inst); virtual ~FMLLibrariesTask() = default; void executeTask() override; bool canAbort() const override; private slots: void fmllibsFinished(); void fmllibsFailed(QString reason); public slots: bool abort() override; private: MinecraftInstance* m_inst; NetJob::Ptr downloadJob; QList fmlLibsToProcess; }; PrismLauncher-10.0.5/launcher/minecraft/update/LibrariesTask.cpp0000644000175100017510000000637615144136756024305 0ustar runnerrunner#include "LibrariesTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "Application.h" LibrariesTask::LibrariesTask(MinecraftInstance* inst) { m_inst = inst; } void LibrariesTask::executeTask() { setStatus(tr("Downloading required library files...")); qDebug() << m_inst->name() << ": downloading libraries"; MinecraftInstance* inst = (MinecraftInstance*)m_inst; // Build a list of URLs that will need to be downloaded. auto components = inst->getPackProfile(); auto profile = components->getProfile(); NetJob::Ptr job{ new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()) }; downloadJob.reset(job); auto metacache = APPLICATION->metacache(); auto processArtifactPool = [this, inst, metacache](const QList& pool, QStringList& errors, const QString& localPath) { for (auto lib : pool) { if (!lib) { emitFailed(tr("Null jar is specified in the metadata, aborting.")); return false; } auto dls = lib->getDownloads(inst->runtimeContext(), metacache.get(), errors, localPath); for (auto dl : dls) { downloadJob->addNetAction(dl); } } return true; }; QStringList failedLocalLibraries; QList libArtifactPool; libArtifactPool.append(profile->getLibraries()); libArtifactPool.append(profile->getNativeLibraries()); libArtifactPool.append(profile->getMavenFiles()); for (auto agent : profile->getAgents()) { libArtifactPool.append(agent->library()); } libArtifactPool.append(profile->getMainJar()); processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); QStringList failedLocalJarMods; processArtifactPool(profile->getJarMods(), failedLocalJarMods, inst->jarModsDir()); if (!failedLocalJarMods.empty() || !failedLocalLibraries.empty()) { downloadJob.reset(); QString failed_all = (failedLocalLibraries + failedLocalJarMods).join("\n"); emitFailed(tr("Some artifacts marked as 'local' are missing their files:\n%1\n\nYou need to either add the files, or removed the " "packages that require them.\nYou'll have to correct this problem manually.") .arg(failed_all)); return; } connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); connect(downloadJob.get(), &NetJob::aborted, this, [this] { emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propagateStepProgress); downloadJob->start(); } bool LibrariesTask::canAbort() const { return true; } void LibrariesTask::jarlibFailed(QString reason) { emitFailed(tr("Game update failed: it was impossible to fetch the required libraries.\nReason:\n%1").arg(reason)); } bool LibrariesTask::abort() { if (downloadJob) { return downloadJob->abort(); } else { qWarning() << "Prematurely aborted LibrariesTask"; } return true; } PrismLauncher-10.0.5/launcher/minecraft/update/AssetUpdateTask.h0000644000175100017510000000106115144136756024242 0ustar runnerrunner#pragma once #include "net/NetJob.h" #include "tasks/Task.h" class MinecraftInstance; class AssetUpdateTask : public Task { Q_OBJECT public: AssetUpdateTask(MinecraftInstance* inst); virtual ~AssetUpdateTask() = default; void executeTask() override; bool canAbort() const override; private slots: void assetIndexFinished(); void assetIndexFailed(QString reason); void assetsFailed(QString reason); public slots: bool abort() override; private: MinecraftInstance* m_inst; NetJob::Ptr downloadJob; }; PrismLauncher-10.0.5/launcher/minecraft/update/AssetUpdateTask.cpp0000644000175100017510000001000115144136756024567 0ustar runnerrunner#include "AssetUpdateTask.h" #include "BuildConfig.h" #include "launch/LaunchStep.h" #include "minecraft/AssetsUtils.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/ChecksumValidator.h" #include "Application.h" #include "net/ApiDownload.h" AssetUpdateTask::AssetUpdateTask(MinecraftInstance* inst) { m_inst = inst; } void AssetUpdateTask::executeTask() { setStatus(tr("Updating assets index...")); auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QUrl indexUrl = assets->url; QString localPath = assets->id + ".json"; auto job = makeShared(tr("Asset index for %1").arg(m_inst->name()), APPLICATION->network()); auto metacache = APPLICATION->metacache(); auto entry = metacache->resolveEntry("asset_indexes", localPath); entry->setStale(true); auto hexSha1 = assets->sha1.toLatin1(); qDebug() << "Asset index SHA1:" << hexSha1; auto dl = Net::ApiDownload::makeCached(indexUrl, entry); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, assets->sha1)); job->addNetAction(dl); downloadJob.reset(job); connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::assetIndexFinished); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); connect(downloadJob.get(), &NetJob::aborted, this, [this] { emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); qDebug() << "Starting asset index download for" << m_inst->name(); downloadJob->start(); } bool AssetUpdateTask::canAbort() const { return true; } void AssetUpdateTask::assetIndexFinished() { AssetsIndex index; qDebug() << "Finished asset index download for" << m_inst->name(); auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); QString asset_fname = "assets/indexes/" + assets->id + ".json"; // FIXME: this looks like a job for a generic validator based on json schema? if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index)) { auto metacache = APPLICATION->metacache(); auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); metacache->evictEntry(entry); emitFailed(tr("Failed to read the assets index!")); return; } auto job = index.getDownloadJob(); if (job) { QString resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); QString source = tr("Mojang"); if (resourceURL != BuildConfig.DEFAULT_RESOURCE_BASE) { source = QUrl(resourceURL).host(); } setStatus(tr("Getting the asset files from %1...").arg(source)); downloadJob = job; connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); connect(downloadJob.get(), &NetJob::aborted, this, [this] { emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); downloadJob->start(); return; } emitSucceeded(); } void AssetUpdateTask::assetIndexFailed(QString reason) { qDebug() << m_inst->name() << ": Failed asset index download"; emitFailed(tr("Failed to download the assets index:\n%1").arg(reason)); } void AssetUpdateTask::assetsFailed(QString reason) { emitFailed(tr("Failed to download assets:\n%1").arg(reason)); } bool AssetUpdateTask::abort() { if (downloadJob) { return downloadJob->abort(); } else { qWarning() << "Prematurely aborted AssetUpdateTask"; } return true; } PrismLauncher-10.0.5/launcher/minecraft/update/FMLLibrariesTask.cpp0000644000175100017510000001020515144136756024626 0ustar runnerrunner#include "FMLLibrariesTask.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/VersionFilterData.h" #include "Application.h" #include "BuildConfig.h" #include "net/ApiDownload.h" FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance* inst) { m_inst = inst; } void FMLLibrariesTask::executeTask() { // Get the mod list MinecraftInstance* inst = (MinecraftInstance*)m_inst; auto components = inst->getPackProfile(); auto profile = components->getProfile(); if (!profile->hasTrait("legacyFML")) { emitSucceeded(); return; } QString version = components->getComponentVersion("net.minecraft"); auto& fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; if (!fmlLibsMapping.contains(version)) { emitSucceeded(); return; } auto& libList = fmlLibsMapping[version]; // determine if we need some libs for FML or forge setStatus(tr("Checking for FML libraries...")); if (!components->getComponent("net.minecraftforge")) { emitSucceeded(); return; } // now check the lib folder inside the instance for files. for (auto& lib : libList) { QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename)); if (libInfo.exists()) continue; fmlLibsToProcess.append(lib); } // if everything is in place, there's nothing to do here... if (fmlLibsToProcess.isEmpty()) { emitSucceeded(); return; } // download missing libs to our place setStatus(tr("Downloading FML libraries...")); NetJob::Ptr dljob{ new NetJob("FML libraries", APPLICATION->network()) }; auto metacache = APPLICATION->metacache(); Net::Download::Options options = Net::Download::Option::MakeEternal; for (auto& lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; dljob->addNetAction(Net::ApiDownload::makeCached(QUrl(urlString), entry, options)); } connect(dljob.get(), &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); connect(dljob.get(), &NetJob::aborted, this, [this] { emitFailed(tr("Aborted")); }); connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress); connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propagateStepProgress); downloadJob.reset(dljob); downloadJob->start(); } bool FMLLibrariesTask::canAbort() const { return true; } void FMLLibrariesTask::fmllibsFinished() { downloadJob.reset(); if (!fmlLibsToProcess.isEmpty()) { setStatus(tr("Copying FML libraries into the instance...")); MinecraftInstance* inst = (MinecraftInstance*)m_inst; auto metacache = APPLICATION->metacache(); int index = 0; for (auto& lib : fmlLibsToProcess) { progress(index, fmlLibsToProcess.size()); auto entry = metacache->resolveEntry("fmllibs", lib.filename); auto path = FS::PathCombine(inst->libDir(), lib.filename); if (!FS::ensureFilePathExists(path)) { emitFailed(tr("Failed creating FML library folder inside the instance.")); return; } if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) { emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); return; } index++; } progress(index, fmlLibsToProcess.size()); } emitSucceeded(); } void FMLLibrariesTask::fmllibsFailed(QString reason) { QStringList failed = downloadJob->getFailedFiles(); QString failed_all = failed.join("\n"); emitFailed(tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason)); } bool FMLLibrariesTask::abort() { if (downloadJob) { return downloadJob->abort(); } else { qWarning() << "Prematurely aborted FMLLibrariesTask"; } return true; } PrismLauncher-10.0.5/launcher/minecraft/update/FoldersTask.h0000644000175100017510000000043315144136756023420 0ustar runnerrunner#pragma once #include "tasks/Task.h" class MinecraftInstance; class FoldersTask : public Task { Q_OBJECT public: FoldersTask(MinecraftInstance* inst); virtual ~FoldersTask() = default; void executeTask() override; private: MinecraftInstance* m_inst; }; PrismLauncher-10.0.5/launcher/minecraft/update/FoldersTask.cpp0000644000175100017510000000357215144136756023762 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "FoldersTask.h" #include #include "minecraft/MinecraftInstance.h" FoldersTask::FoldersTask(MinecraftInstance* inst) { m_inst = inst; } void FoldersTask::executeTask() { // Make directories QDir mcDir(m_inst->gameRoot()); if (!mcDir.exists() && !mcDir.mkpath(".")) { emitFailed(tr("Failed to create folder for Minecraft binaries.")); return; } emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/Library.h0000644000175100017510000001644215144136756021330 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include #include "GradleSpecifier.h" #include "MojangDownloadInfo.h" #include "Rule.h" #include "RuntimeContext.h" #include "net/NetRequest.h" class Library; class MinecraftInstance; using LibraryPtr = std::shared_ptr; class Library { friend class OneSixVersionFormat; friend class MojangVersionFormat; friend class LibraryTest; public: Library() {} Library(const QString& name) { m_name = name; } /// limited copy without some data. TODO: why? static LibraryPtr limitedCopy(LibraryPtr base) { auto newlib = std::make_shared(); newlib->m_name = base->m_name; newlib->m_repositoryURL = base->m_repositoryURL; newlib->m_hint = base->m_hint; newlib->m_absoluteURL = base->m_absoluteURL; newlib->m_extractExcludes = base->m_extractExcludes; newlib->m_nativeClassifiers = base->m_nativeClassifiers; newlib->m_rules = base->m_rules; newlib->m_storagePrefix = base->m_storagePrefix; newlib->m_mojangDownloads = base->m_mojangDownloads; newlib->m_filename = base->m_filename; return newlib; } public: /* methods */ /// Returns the raw name field const GradleSpecifier& rawName() const { return m_name; } void setRawName(const GradleSpecifier& spec) { m_name = spec; } void setClassifier(const QString& spec) { m_name.setClassifier(spec); } /// returns the full group and artifact prefix QString artifactPrefix() const { return m_name.artifactPrefix(); } /// get the artifact ID QString artifactId() const { return m_name.artifactId(); } /// get the artifact version QString version() const { return m_name.version(); } /// Returns true if the library is native bool isNative() const { return m_nativeClassifiers.size() != 0; } void setStoragePrefix(QString prefix = QString()); /// Set the url base for downloads void setRepositoryURL(const QString& base_url) { m_repositoryURL = base_url; } void getApplicableFiles(const RuntimeContext& runtimeContext, QStringList& jar, QStringList& native, QStringList& native32, QStringList& native64, const QString& overridePath) const; void setAbsoluteUrl(const QString& absolute_url) { m_absoluteURL = absolute_url; } void setFilename(const QString& filename) { m_filename = filename; } /// Get the file name of the library QString filename(const RuntimeContext& runtimeContext) const; // DEPRECATED: set a display name, used by jar mods only void setDisplayName(const QString& displayName) { m_displayname = displayName; } /// Get the file name of the library QString displayName(const RuntimeContext& runtimeContext) const; void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info) { m_mojangDownloads = info; } void setHint(const QString& hint) { m_hint = hint; } /// Set the load rules void setRules(QList rules) { m_rules = rules; } /// Returns true if the library should be loaded (or extracted, in case of natives) bool isActive(const RuntimeContext& runtimeContext) const; /// Returns true if the library is contained in an instance and false if it is shared bool isLocal() const; /// Returns true if the library is to always be checked for updates bool isAlwaysStale() const; /// Return true if the library requires forge XZ hacks bool isForge() const; // Get a list of downloads for this library QList getDownloads(const RuntimeContext& runtimeContext, class HttpMetaCache* cache, QStringList& failedLocalFiles, const QString& overridePath) const; QString getCompatibleNative(const RuntimeContext& runtimeContext) const; private: /* methods */ /// the default storage prefix used by Prism Launcher static QString defaultStoragePrefix(); /// Get the prefix - root of the storage to be used QString storagePrefix() const; /// Get the relative file path where the library should be saved QString storageSuffix(const RuntimeContext& runtimeContext) const; QString hint() const { return m_hint; } protected: /* data */ /// the basic gradle dependency specifier. GradleSpecifier m_name; /// DEPRECATED URL prefix of the maven repo where the file can be downloaded QString m_repositoryURL; /// DEPRECATED: Prism Launcher-specific absolute URL. takes precedence over the implicit maven repo URL, if defined QString m_absoluteURL; /// Prism Launcher extension - filename override QString m_filename; /// DEPRECATED Prism Launcher extension - display name QString m_displayname; /** * Prism Launcher-specific type hint - modifies how the library is treated */ QString m_hint; /** * storage - by default the local libraries folder in Prism Launcher, but could be elsewhere * Prism Launcher specific, because of FTB. */ QString m_storagePrefix; /// true if the library had an extract/excludes section (even empty) bool m_hasExcludes = false; /// a list of files that shouldn't be extracted from the library QStringList m_extractExcludes; /// native suffixes per OS QMap m_nativeClassifiers; /// true if the library had a rules section (even empty) bool applyRules = false; /// rules associated with the library QList m_rules; /// MOJANG: container with Mojang style download info MojangLibraryDownloadInfo::Ptr m_mojangDownloads; }; PrismLauncher-10.0.5/launcher/minecraft/VanillaInstanceCreationTask.h0000644000175100017510000000113015144136756025273 0ustar runnerrunner#pragma once #include "InstanceCreationTask.h" #include class VanillaCreationTask final : public InstanceCreationTask { Q_OBJECT public: VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) {} VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version); bool createInstance() override; private: // Version to update to / create of the instance. BaseVersion::Ptr m_version; bool m_using_loader = false; QString m_loader; BaseVersion::Ptr m_loader_version; }; PrismLauncher-10.0.5/launcher/minecraft/MinecraftInstance.h0000644000175100017510000001463715144136756023325 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "BaseInstance.h" #include "minecraft/launch/MinecraftTarget.h" #include "minecraft/mod/Mod.h" class ModFolderModel; class ResourceFolderModel; class ResourcePackFolderModel; class ShaderPackFolderModel; class TexturePackFolderModel; class WorldList; class LaunchStep; class LaunchProfile; class PackProfile; class MinecraftInstance : public BaseInstance { Q_OBJECT public: MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); virtual ~MinecraftInstance() = default; virtual void saveNow() override; void loadSpecificSettings() override; // FIXME: remove QString typeName() const override; // FIXME: remove QSet traits() const override; bool canEdit() const override { return true; } bool canExport() const override { return true; } void populateLaunchMenu(QMenu* menu) override; ////// Directories and files ////// QString jarModsDir() const; QString resourcePacksDir() const; QString texturePacksDir() const; QString shaderPacksDir() const; QString modsRoot() const override; QString coreModsDir() const; QString nilModsDir() const; QString dataPacksDir(); QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; QString resourcesDir() const; QDir jarmodsPath() const; QDir librariesPath() const; QDir versionsPath() const; QString instanceConfigFolder() const override; // Path to the instance's minecraft directory. QString gameRoot() const override; // Path to the instance's minecraft bin directory. QString binRoot() const; // where to put the natives during/before launch QString getNativePath() const; // where the instance-local libraries should be QString getLocalLibraryPath() const; /** Returns whether the instance, with its version, has support for demo mode. */ bool supportsDemo() const; void updateRuntimeContext() override; ////// Profile management ////// std::shared_ptr getPackProfile() const; ////// Mod Lists ////// std::shared_ptr loaderModList(); std::shared_ptr coreModList(); std::shared_ptr nilModList(); std::shared_ptr resourcePackList(); std::shared_ptr texturePackList(); std::shared_ptr shaderPackList(); std::shared_ptr dataPackList(); QList> resourceLists(); std::shared_ptr worldList(); ////// Launch stuff ////// QList createUpdateTask() override; shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; QStringList extraArguments() override; QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; QList getJarMods() const; QString createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin); /// get arguments passed to java QStringList javaArguments(); QString getLauncher(); bool shouldApplyOnlineFixes(); /// get variables for launch command variable substitution/environment QMap getVariables() override; /// create an environment for launching processes QProcessEnvironment createEnvironment() override; QProcessEnvironment createLaunchEnvironment() override; QStringList getLogFileSearchPaths() override; QString getStatusbarDescription() override; // FIXME: remove virtual QStringList getClassPath(); // FIXME: remove virtual QStringList getNativeJars(); // FIXME: remove virtual QString getMainClass() const; // FIXME: remove virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) const; virtual JavaVersion getJavaVersion(); protected: QMap createCensorFilterFromSession(AuthSessionPtr session); QMap makeProfileVarMapping(std::shared_ptr profile) const; protected: // data std::shared_ptr m_components; mutable std::shared_ptr m_loader_mod_list; mutable std::shared_ptr m_core_mod_list; mutable std::shared_ptr m_nil_mod_list; mutable std::shared_ptr m_resource_pack_list; mutable std::shared_ptr m_shader_pack_list; mutable std::shared_ptr m_texture_pack_list; mutable std::shared_ptr m_data_pack_list; mutable std::shared_ptr m_world_list; }; using MinecraftInstancePtr = std::shared_ptr; PrismLauncher-10.0.5/launcher/minecraft/PackProfile_p.h0000644000175100017510000000132515144136756022434 0ustar runnerrunner#pragma once #include #include #include #include "Component.h" #include "tasks/Task.h" class MinecraftInstance; using ComponentContainer = QList; using ComponentIndex = QMap; struct PackProfileData { // the instance this belongs to MinecraftInstance* m_instance; // the launch profile (volatile, temporary thing created on demand) std::shared_ptr m_profile; // persistent list of components and related machinery ComponentContainer components; ComponentIndex componentIndex; bool dirty = false; QTimer m_saveTimer; Task::Ptr m_updateTask; bool loaded = false; bool interactionDisabled = true; }; PrismLauncher-10.0.5/launcher/minecraft/MinecraftLoadAndCheck.h0000644000175100017510000000212415144136756024005 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "net/Mode.h" #include "tasks/Task.h" class MinecraftInstance; class MinecraftLoadAndCheck : public Task { Q_OBJECT public: explicit MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode); virtual ~MinecraftLoadAndCheck() = default; void executeTask() override; bool canAbort() const override; public slots: bool abort() override; private: MinecraftInstance* m_inst = nullptr; Task::Ptr m_task; Net::Mode m_netmode; }; PrismLauncher-10.0.5/launcher/minecraft/mod/0000755000175100017510000000000015144136756020323 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/minecraft/mod/ModDetails.h0000644000175100017510000001136215144136756022524 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include struct ModLicense { QString name = {}; QString id = {}; QString url = {}; QString description = {}; ModLicense() {} ModLicense(const QString license) { // FIXME: come up with a better license parsing. // handle SPDX identifiers? https://spdx.org/licenses/ auto parts = license.split(' '); QStringList notNameParts = {}; for (auto part : parts) { auto _url = QUrl(part); if (part.startsWith("(") && part.endsWith(")")) _url = QUrl(part.mid(1, part.size() - 2)); if (_url.isValid() && !_url.scheme().isEmpty() && !_url.host().isEmpty()) { this->url = _url.toString(); notNameParts.append(part); continue; } } for (auto part : notNameParts) { parts.removeOne(part); } auto licensePart = parts.join(' '); this->name = licensePart; this->description = licensePart; if (parts.size() == 1) { this->id = parts.first(); } } ModLicense(const QString& name_, const QString& id_, const QString& url_, const QString& description_) : name(name_), id(id_), url(url_), description(description_) {} ModLicense(const ModLicense& other) : name(other.name), id(other.id), url(other.url), description(other.description) {} ModLicense& operator=(const ModLicense& other) { this->name = other.name; this->id = other.id; this->url = other.url; this->description = other.description; return *this; } ModLicense& operator=(const ModLicense&& other) { this->name = other.name; this->id = other.id; this->url = other.url; this->description = other.description; return *this; } bool isEmpty() { return this->name.isEmpty() && this->id.isEmpty() && this->url.isEmpty() && this->description.isEmpty(); } }; struct ModDetails { /* Mod ID as defined in the ModLoader-specific metadata */ QString mod_id = {}; /* Human-readable name */ QString name = {}; /* Human-readable mod version */ QString version = {}; /* Human-readable minecraft version */ QString mcversion = {}; /* URL for mod's home page */ QString homeurl = {}; /* Human-readable description */ QString description = {}; /* List of the author's names */ QStringList authors = {}; /* Issue Tracker URL */ QString issue_tracker = {}; /* License */ QList licenses = {}; /* Path of mod logo */ QString icon_file = {}; ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ ModDetails(const ModDetails& other) : mod_id(other.mod_id) , name(other.name) , version(other.version) , mcversion(other.mcversion) , homeurl(other.homeurl) , description(other.description) , authors(other.authors) , issue_tracker(other.issue_tracker) , licenses(other.licenses) , icon_file(other.icon_file) {} ModDetails& operator=(const ModDetails& other) = default; ModDetails& operator=(ModDetails&& other) = default; }; PrismLauncher-10.0.5/launcher/minecraft/mod/Mod.h0000644000175100017510000000713715144136756021223 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include "ModDetails.h" #include "Resource.h" class Mod : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; using WeakPtr = QPointer; Mod() = default; Mod(const QFileInfo& file); Mod(QString file_path) : Mod(QFileInfo(file_path)) {} auto details() const -> const ModDetails&; auto name() const -> QString override; auto mod_id() const -> QString; auto version() const -> QString; auto homepage() const -> QString override; auto description() const -> QString; auto authors() const -> QStringList; auto licenses() const -> const QList&; auto issueTracker() const -> QString; auto side() const -> QString; auto loaders() const -> QString; auto mcVersions() const -> QString; auto releaseType() const -> QString; /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ QPixmap setIcon(QImage new_image) const; void setDetails(const ModDetails& details); bool valid() const override; [[nodiscard]] int compare(const Resource& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; // Delete all the files of this mod auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; // Delete the metadata only void destroyMetadata(QDir& index_dir); void finishResolvingWithDetails(ModDetails&& details); protected: ModDetails m_local_details; mutable QMutex m_data_lock; struct { QPixmapCache::Key key; bool wasEverUsed = false; bool wasReadAttempt = false; } mutable m_packImageCacheKey; }; PrismLauncher-10.0.5/launcher/minecraft/mod/TexturePackFolderModel.h0000644000175100017510000000470715144136756025060 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "ResourceFolderModel.h" #include "TexturePack.h" class TexturePackFolderModel : public ResourceFolderModel { Q_OBJECT public: enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; explicit TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "texturepacks"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new TexturePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(TexturePack) }; PrismLauncher-10.0.5/launcher/minecraft/mod/Mod.cpp0000644000175100017510000001746515144136756021563 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Mod.h" #include #include #include #include "MTPixmapCache.h" #include "MetadataHandler.h" #include "Resource.h" #include "Version.h" #include "minecraft/mod/ModDetails.h" #include "minecraft/mod/tasks/LocalModParseTask.h" #include "modplatform/ModIndex.h" Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() { m_enabled = (file.suffix() != "disabled"); } void Mod::setDetails(const ModDetails& details) { m_local_details = details; } int Mod::compare(const Resource& other, SortType type) const { auto cast_other = dynamic_cast(&other); if (!cast_other) return Resource::compare(other, type); switch (type) { default: case SortType::ENABLED: case SortType::NAME: case SortType::DATE: case SortType::SIZE: return Resource::compare(other, type); case SortType::VERSION: { auto this_ver = Version(version()); auto other_ver = Version(cast_other->version()); if (this_ver > other_ver) return 1; if (this_ver < other_ver) return -1; break; } case SortType::SIDE: { auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } case SortType::MC_VERSIONS: { auto compare_result = QString::compare(mcVersions(), cast_other->mcVersions(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } case SortType::LOADERS: { auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } case SortType::RELEASE_TYPE: { auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } } return 0; } bool Mod::applyFilter(QRegularExpression filter) const { if (filter.match(description()).hasMatch()) return true; for (auto& author : authors()) { if (filter.match(author).hasMatch()) { return true; } } return Resource::applyFilter(filter); } auto Mod::details() const -> const ModDetails& { return m_local_details; } auto Mod::name() const -> QString { auto d_name = details().name; if (!d_name.isEmpty()) return d_name; return Resource::name(); } auto Mod::mod_id() const -> QString { auto d_mod_id = details().mod_id; if (!d_mod_id.isEmpty()) return d_mod_id; return Resource::name(); } auto Mod::version() const -> QString { return details().version; } auto Mod::homepage() const -> QString { QString metaUrl = Resource::homepage(); if (metaUrl.isEmpty()) return details().homeurl; else return metaUrl; } auto Mod::loaders() const -> QString { if (metadata()) { QStringList loaders; auto modLoaders = metadata()->loaders; for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) { loaders << getModLoaderAsString(loader); } return loaders.join(", "); } return {}; } auto Mod::side() const -> QString { if (metadata()) return ModPlatform::SideUtils::toString(metadata()->side); return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide); } auto Mod::mcVersions() const -> QString { if (metadata()) return metadata()->mcVersions.join(", "); return {}; } auto Mod::releaseType() const -> QString { if (metadata()) return metadata()->releaseType.toString(); return ModPlatform::IndexedVersionType(ModPlatform::IndexedVersionType::Unknown).toString(); } auto Mod::description() const -> QString { return details().description; } auto Mod::authors() const -> QStringList { return details().authors; } void Mod::finishResolvingWithDetails(ModDetails&& details) { m_is_resolving = false; m_is_resolved = true; m_local_details = std::move(details); if (!iconPath().isEmpty()) { m_packImageCacheKey.wasReadAttempt = false; } } auto Mod::licenses() const -> const QList& { return details().licenses; } auto Mod::issueTracker() const -> QString { return details().issue_tracker; } QPixmap Mod::setIcon(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); if (m_packImageCacheKey.key.isValid()) PixmapCache::remove(m_packImageCacheKey.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_packImageCacheKey.key = PixmapCache::insert(pixmap); m_packImageCacheKey.wasEverUsed = true; m_packImageCacheKey.wasReadAttempt = true; return pixmap; } QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const { auto pixmap_transform = [&size, &mode](QPixmap pixmap) { if (size.isNull()) return pixmap; return pixmap.scaled(size, mode, Qt::SmoothTransformation); }; QPixmap cached_image; if (PixmapCache::find(m_packImageCacheKey.key, &cached_image)) { return pixmap_transform(cached_image); } // No valid image we can get if ((!m_packImageCacheKey.wasEverUsed && m_packImageCacheKey.wasReadAttempt) || iconPath().isEmpty()) return {}; if (m_packImageCacheKey.wasEverUsed) { qDebug() << "Mod" << name() << "Had it's icon evicted from the cache. reloading..."; PixmapCache::markCacheMissByEviciton(); } // Image got evicted from the cache or an attempt to load it has not been made. load it and retry. m_packImageCacheKey.wasReadAttempt = true; if (ModUtils::loadIconFile(*this, &cached_image)) { return pixmap_transform(cached_image); } // Image failed to load return {}; } bool Mod::valid() const { return !m_local_details.mod_id.isEmpty(); } PrismLauncher-10.0.5/launcher/minecraft/mod/ShaderPackFolderModel.h0000644000175100017510000000233515144136756024621 0ustar runnerrunner#pragma once #include "ResourceFolderModel.h" #include "minecraft/mod/ShaderPack.h" #include "minecraft/mod/tasks/LocalShaderPackParseTask.h" class ShaderPackFolderModel : public ResourceFolderModel { Q_OBJECT public: explicit ShaderPackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr) : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) {} virtual QString id() const override { return "shaderpacks"; } [[nodiscard]] Resource* createResource(const QFileInfo& info) override { return new ShaderPack(info); } [[nodiscard]] Task* createParseTask(Resource& resource) override { return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast(resource)); } QDir indexDir() const override { return m_dir; } Task* createPreUpdateTask() override; // avoid watching twice virtual bool startWatching() override { return ResourceFolderModel::startWatching({ m_dir.absolutePath() }); } virtual bool stopWatching() override { return ResourceFolderModel::stopWatching({ m_dir.absolutePath() }); } RESOURCE_HELPERS(ShaderPack); private: QMutex m_migrateLock; }; PrismLauncher-10.0.5/launcher/minecraft/mod/ResourceFolderModel.h0000644000175100017510000002746015144136756024411 0ustar runnerrunner#pragma once #include #include #include #include #include #include #include #include #include #include "Resource.h" #include "BaseInstance.h" #include "tasks/ConcurrentTask.h" #include "tasks/Task.h" class QSortFilterProxyModel; /* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ #define RESOURCE_HELPERS(T) \ T& at(int index) \ { \ return *static_cast(m_resources[index].get()); \ } \ const T& at(int index) const \ { \ return *static_cast(m_resources.at(index).get()); \ } \ QList selected##T##s(const QModelIndexList& indexes) \ { \ QList result; \ for (const QModelIndex& index : indexes) { \ if (index.column() != 0) \ continue; \ \ result.append(&at(index.row())); \ } \ return result; \ } \ QList all##T##s() \ { \ QList result; \ result.reserve(m_resources.size()); \ \ for (const Resource::Ptr& resource : m_resources) \ result.append(static_cast(resource.get())); \ \ return result; \ } /** A basic model for external resources. * * This model manages a list of resources. As such, external users of such resources do not own them, * and the resource's lifetime is contingent on the model's lifetime. * * TODO: Make the resources unique pointers accessible through weak pointers. */ class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); ~ResourceFolderModel() override; virtual QString id() const { return "resource"; } /** Starts watching the paths for changes. * * Returns whether starting to watch all the paths was successful. * If one or more fails, it returns false. */ bool startWatching(const QStringList& paths); /** Stops watching the paths for changes. * * Returns whether stopping to watch all the paths was successful. * If one or more fails, it returns false. */ bool stopWatching(const QStringList& paths); /* Helper methods for subclasses, using a predetermined list of paths. */ virtual bool startWatching() { return startWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); } virtual bool stopWatching() { return stopWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); } virtual QDir indexDir() const { return { QString("%1/.index").arg(dir().absolutePath()) }; } /** Given a path in the system, install that resource, moving it to its place in the * instance file hierarchy. * * Returns whether the installation was succcessful. */ virtual bool installResource(QString path); virtual void installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers); /** Uninstall (i.e. remove all data about it) a resource, given its file name. * * Returns whether the removal was successful. */ virtual bool uninstallResource(const QString& file_name, bool preserve_metadata = false); virtual bool deleteResources(const QModelIndexList&); virtual void deleteMetadata(const QModelIndexList&); /** Applies the given 'action' to the resources in 'indexes'. * * Returns whether the action was successfully applied to all resources. */ virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action); /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */ virtual bool update(); /** Creates a new parse task, if needed, for 'res' and start it.*/ virtual void resolveResource(Resource::Ptr res); qsizetype size() const { return m_resources.size(); } [[nodiscard]] bool empty() const { return size() == 0; } Resource& at(int index) { return *m_resources[index].get(); } const Resource& at(int index) const { return *m_resources.at(index).get(); } QList selectedResources(const QModelIndexList& indexes); QList allResources(); Resource::Ptr find(QString id); QDir const& dir() const { return m_dir; } /** Checks whether there's any parse tasks being done. * * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having * such tasks would introduce an undefined behavior, most likely resulting in a crash. */ bool hasPendingParseTasks() const; /* Qt behavior */ /* Basic columns */ enum Columns { ActiveColumn = 0, NameColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } Qt::DropActions supportedDropActions() const override; /// flags, mostly to support drag&drop Qt::ItemFlags flags(const QModelIndex& index) const override; QStringList mimeTypes() const override; [[nodiscard]] bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; [[nodiscard]] bool validateIndex(const QModelIndex& index) const; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void setupHeaderAction(QAction* act, int column); void saveColumns(QTreeView* tree); void loadColumns(QTreeView* tree); QMenu* createHeaderContextMenu(QTreeView* tree); /** This creates a proxy model to filter / sort the model for a UI. * * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead! */ QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); SortType columnToSortKey(size_t column) const; QList columnResizeModes() const { return m_column_resize_modes; } class ProxyModel : public QSortFilterProxyModel { public: explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} protected: bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; }; QString instDirPath() const; signals: void updateFinished(); void parseFinished(); protected: [[nodiscard]] virtual Task* createPreUpdateTask() { return nullptr; } /** This creates a new update task to be executed by update(). * * The task should load and parse all resources necessary, and provide a way of accessing such results. * * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. * If such work is needed, try using it in the Task create by createParseTask() instead! */ [[nodiscard]] Task* createUpdateTask(); [[nodiscard]] virtual Resource* createResource(const QFileInfo& info) { return new Resource(info); } /** This creates a new parse task to be executed by onUpdateSucceeded(). * * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed * in the background, so it slowly updates the UI as tasks get done. */ [[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; } /** Standard implementation of the model update logic. * * It uses set operations to find differences between the current state and the updated state, * to act only on those disparities. * */ void applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources); protected slots: void directoryChanged(QString); /** Called when the update task is successful. * * This usually calls static_cast on the specific Task type returned by createUpdateTask, * so care must be taken in such cases. * TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro disallows that). */ virtual void onUpdateSucceeded(); virtual void onUpdateFailed() {} /** Called when the parse task with the given ticket is successful. * * This is just a simple reference implementation. You probably want to override it with your own logic in a subclass * if the resource is complex and has more stuff to parse. */ virtual void onParseSucceeded(int ticket, QString resource_id); virtual void onParseFailed(int ticket, QString resource_id); protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. // As such, the order in with they appear is very important! QList m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }; QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; QList m_columnsHideable = { false, false, true, true, true }; QDir m_dir; BaseInstance* m_instance; QFileSystemWatcher m_watcher; bool m_is_watching = false; bool m_is_indexed; bool m_first_folder_load = true; Task::Ptr m_current_update_task = nullptr; bool m_scheduled_update = false; QList m_resources; // Represents the relationship between a resource's internal ID and it's row position on the model. QMap m_resources_index; // Runs off-thread ConcurrentTask m_resourceResolver; bool m_resourceResolverRunning = false; QMap m_active_parse_tasks; std::atomic m_next_resolution_ticket = 0; }; PrismLauncher-10.0.5/launcher/minecraft/mod/ResourcePack.cpp0000644000175100017510000000426115144136756023420 0ustar runnerrunner#include "ResourcePack.h" #include #include #include #include "MTPixmapCache.h" #include "Version.h" // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats static const QMap> s_pack_format_versions = { { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } }, { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } }, { 14, { Version("23w14a"), Version("23w16a") } }, { 15, { Version("1.20"), Version("1.20.1") } }, { 16, { Version("23w31a"), Version("23w31a") } }, { 17, { Version("23w32a"), Version("23w35a") } }, { 18, { Version("1.20.2"), Version("23w16a") } }, { 19, { Version("23w42a"), Version("23w42a") } }, { 20, { Version("23w43a"), Version("23w44a") } }, { 21, { Version("23w45a"), Version("23w46a") } }, { 22, { Version("1.20.3-pre1"), Version("23w51b") } }, { 24, { Version("24w03a"), Version("24w04a") } }, { 25, { Version("24w05a"), Version("24w05b") } }, { 26, { Version("24w06a"), Version("24w07a") } }, { 28, { Version("24w09a"), Version("24w10a") } }, { 29, { Version("24w11a"), Version("24w11a") } }, { 30, { Version("24w12a"), Version("23w12a") } }, { 31, { Version("24w13a"), Version("1.20.5-pre3") } }, { 32, { Version("1.20.5-pre4"), Version("1.20.6") } }, { 33, { Version("24w18a"), Version("24w20a") } }, { 34, { Version("24w21a"), Version("1.21") } } }; std::pair ResourcePack::compatibleVersions() const { if (!s_pack_format_versions.contains(m_pack_format)) { return { {}, {} }; } return s_pack_format_versions.constFind(m_pack_format).value(); } PrismLauncher-10.0.5/launcher/minecraft/mod/Resource.cpp0000644000175100017510000001544015144136756022622 0ustar runnerrunner#include "Resource.h" #include #include #include #include #include "FileSystem.h" #include "StringUtils.h" Resource::Resource(QObject* parent) : QObject(parent) {} Resource::Resource(QFileInfo file_info) : QObject() { setFile(file_info); } void Resource::setFile(QFileInfo file_info) { m_file_info = file_info; parseFile(); } static std::tuple calculateFileSize(const QFileInfo& file) { if (file.isDir()) { auto dir = QDir(file.absoluteFilePath()); dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); auto count = dir.count(); auto str = QObject::tr("item"); if (count != 1) str = QObject::tr("items"); return { QString("%1 %2").arg(QString::number(count), str), count }; } return { StringUtils::humanReadableFileSize(file.size(), true), file.size() }; } void Resource::parseFile() { QString file_name{ m_file_info.fileName() }; m_type = ResourceType::UNKNOWN; m_internal_id = file_name; std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info); if (m_file_info.isDir()) { m_type = ResourceType::FOLDER; m_name = file_name; } else if (m_file_info.isFile()) { if (file_name.endsWith(".disabled")) { file_name.chop(9); m_enabled = false; } if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) { m_type = ResourceType::ZIPFILE; file_name.chop(4); } else if (file_name.endsWith(".nilmod")) { m_type = ResourceType::ZIPFILE; file_name.chop(7); } else if (file_name.endsWith(".litemod")) { m_type = ResourceType::LITEMOD; file_name.chop(8); } else { m_type = ResourceType::SINGLEFILE; } m_name = file_name; } m_changed_date_time = m_file_info.lastModified(); } auto Resource::name() const -> QString { if (metadata()) return metadata()->name; return m_name; } static void removeThePrefix(QString& string) { static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); string.remove(s_regex); string = string.trimmed(); } auto Resource::provider() const -> QString { if (metadata()) return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); return tr("Unknown"); } auto Resource::homepage() const -> QString { if (metadata()) return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); return {}; } void Resource::setMetadata(std::shared_ptr&& metadata) { if (status() == ResourceStatus::NO_METADATA) setStatus(ResourceStatus::INSTALLED); m_metadata = metadata; } int Resource::compare(const Resource& other, SortType type) const { switch (type) { default: case SortType::ENABLED: if (enabled() && !other.enabled()) return 1; if (!enabled() && other.enabled()) return -1; break; case SortType::NAME: { QString this_name{ name() }; QString other_name{ other.name() }; // TODO do we need this? it could result in 0 being returned removeThePrefix(this_name); removeThePrefix(other_name); return QString::compare(this_name, other_name, Qt::CaseInsensitive); } case SortType::DATE: if (dateTimeChanged() > other.dateTimeChanged()) return 1; if (dateTimeChanged() < other.dateTimeChanged()) return -1; break; case SortType::SIZE: { if (this->type() != other.type()) { if (this->type() == ResourceType::FOLDER) return -1; if (other.type() == ResourceType::FOLDER) return 1; } if (sizeInfo() > other.sizeInfo()) return 1; if (sizeInfo() < other.sizeInfo()) return -1; break; } case SortType::PROVIDER: { auto compare_result = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); if (compare_result != 0) return compare_result; break; } } return 0; } bool Resource::applyFilter(QRegularExpression filter) const { return filter.match(name()).hasMatch(); } bool Resource::enable(EnableAction action) { if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) return false; QString path = m_file_info.absoluteFilePath(); QFile file(path); bool enable = true; switch (action) { case EnableAction::ENABLE: enable = true; break; case EnableAction::DISABLE: enable = false; break; case EnableAction::TOGGLE: default: enable = !enabled(); break; } if (m_enabled == enable) return false; if (enable) { // m_enabled is false, but there's no '.disabled' suffix. // TODO: Report error? if (!path.endsWith(".disabled")) return false; path.chop(9); } else { path += ".disabled"; if (QFile::exists(path)) { path = FS::getUniqueResourceName(path); } } if (!file.rename(path)) return false; setFile(QFileInfo(path)); m_enabled = enable; return true; } auto Resource::destroy(const QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool { m_type = ResourceType::UNKNOWN; if (!preserve_metadata) { qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); destroyMetadata(index_dir); } return (attempt_trash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); } auto Resource::destroyMetadata(const QDir& index_dir) -> void { if (metadata()) { Metadata::remove(index_dir, metadata()->slug); } else { auto n = name(); Metadata::remove(index_dir, n); } m_metadata = nullptr; } bool Resource::isSymLinkUnder(const QString& instPath) const { if (isSymLink()) return true; auto instDir = QDir(instPath); auto relAbsPath = instDir.relativeFilePath(m_file_info.absoluteFilePath()); auto relCanonPath = instDir.relativeFilePath(m_file_info.canonicalFilePath()); return relAbsPath != relCanonPath; } bool Resource::isMoreThanOneHardLink() const { return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1; } auto Resource::getOriginalFileName() const -> QString { auto fileName = m_file_info.fileName(); if (!m_enabled) fileName.chop(9); return fileName; } PrismLauncher-10.0.5/launcher/minecraft/mod/WorldSave.h0000644000175100017510000000362615144136756022411 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "Resource.h" #include class Version; enum class WorldSaveFormat { SINGLE, MULTI, INVALID }; class WorldSave : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; WorldSave(QObject* parent = nullptr) : Resource(parent) {} WorldSave(QFileInfo file_info) : Resource(file_info) {} /** Gets the format of the save. */ WorldSaveFormat saveFormat() const { return m_save_format; } /** Gets the name of the save dir (first found in multi mode). */ QString saveDirName() const { return m_save_dir_name; } /** Thread-safe. */ void setSaveFormat(WorldSaveFormat new_save_format); /** Thread-safe. */ void setSaveDirName(QString dir_name); bool valid() const override; protected: mutable QMutex m_data_lock; /** The format in which the save file is in. * Since saves can be distributed in various slightly different ways, this allows us to treat them separately. */ WorldSaveFormat m_save_format = WorldSaveFormat::INVALID; QString m_save_dir_name; }; PrismLauncher-10.0.5/launcher/minecraft/mod/ShaderPack.h0000644000175100017510000000377415144136756022514 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "Resource.h" /* Info: * Currently For Optifine / Iris shader packs, * could be expanded to support others should they exist? * * This class and enum are mostly here as placeholders for validating * that a shaderpack exists and is in the right format, * namely that they contain a folder named 'shaders'. * * In the technical sense it would be possible to parse files like `shaders/shaders.properties` * to get information like the available profiles but this is not all that useful without more knowledge of the * shader mod used to be able to change settings. */ #include enum class ShaderPackFormat { VALID, INVALID }; class ShaderPack : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; ShaderPackFormat packFormat() const { return m_pack_format; } ShaderPack(QObject* parent = nullptr) : Resource(parent) {} ShaderPack(QFileInfo file_info) : Resource(file_info) {} /** Thread-safe. */ void setPackFormat(ShaderPackFormat new_format); bool valid() const override; protected: mutable QMutex m_data_lock; ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID; }; PrismLauncher-10.0.5/launcher/minecraft/mod/Resource.h0000644000175100017510000001606215144136756022270 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "MetadataHandler.h" #include "QObjectPtr.h" enum class ResourceType { UNKNOWN, //!< Indicates an unspecified resource type. ZIPFILE, //!< The resource is a zip file containing the resource's class files. SINGLEFILE, //!< The resource is a single file (not a zip file). FOLDER, //!< The resource is in a folder on the filesystem. LITEMOD, //!< The resource is a litemod }; enum class ResourceStatus { INSTALLED, // Both JAR and Metadata are present NOT_INSTALLED, // Only the Metadata is present NO_METADATA, // Only the JAR is present UNKNOWN, // Default status }; enum class SortType { NAME, DATE, VERSION, ENABLED, PACK_FORMAT, PROVIDER, SIZE, SIDE, MC_VERSIONS, LOADERS, RELEASE_TYPE }; enum class EnableAction { ENABLE, DISABLE, TOGGLE }; /** General class for managed resources. It mirrors a file in disk, with some more info * for display and house-keeping purposes. * * Subclass it to add additional data / behavior, such as Mods or Resource packs. */ class Resource : public QObject { Q_OBJECT Q_DISABLE_COPY(Resource) public: using Ptr = shared_qobject_ptr; using WeakPtr = QPointer; Resource(QObject* parent = nullptr); Resource(QFileInfo file_info); Resource(QString file_path) : Resource(QFileInfo(file_path)) {} ~Resource() override = default; void setFile(QFileInfo file_info); void parseFile(); auto fileinfo() const -> QFileInfo { return m_file_info; } auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } auto internal_id() const -> QString { return m_internal_id; } auto type() const -> ResourceType { return m_type; } bool enabled() const { return m_enabled; } auto getOriginalFileName() const -> QString; QString sizeStr() const { return m_size_str; } qint64 sizeInfo() const { return m_size_info; } virtual auto name() const -> QString; virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } auto status() const -> ResourceStatus { return m_status; }; auto metadata() -> std::shared_ptr { return m_metadata; } auto metadata() const -> std::shared_ptr { return m_metadata; } auto provider() const -> QString; virtual auto homepage() const -> QString; void setStatus(ResourceStatus status) { m_status = status; } void setMetadata(std::shared_ptr&& metadata); void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared(metadata)); } /** Compares two Resources, for sorting purposes, considering a ascending order, returning: * > 0: 'this' comes after 'other' * = 0: 'this' is equal to 'other' * < 0: 'this' comes before 'other' */ virtual int compare(Resource const& other, SortType type = SortType::NAME) const; /** Returns whether the given filter should filter out 'this' (false), * or if such filter includes the Resource (true). */ virtual bool applyFilter(QRegularExpression filter) const; /** Changes the enabled property, according to 'action'. * * Returns whether a change was applied to the Resource's properties. */ bool enable(EnableAction action); auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } auto isResolving() const -> bool { return m_is_resolving; } auto isResolved() const -> bool { return m_is_resolved; } auto resolutionTicket() const -> int { return m_resolution_ticket; } void setResolving(bool resolving, int resolutionTicket) { m_is_resolving = resolving; m_resolution_ticket = resolutionTicket; } // Delete all files of this resource. auto destroy(const QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; // Delete the metadata only. auto destroyMetadata(const QDir& index_dir) -> void; auto isSymLink() const -> bool { return m_file_info.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance * * @param instPath path to an instance directory * @return true * @return false */ bool isSymLinkUnder(const QString& instPath) const; bool isMoreThanOneHardLink() const; auto mod_id() const -> QString { return m_mod_id; } void setModId(const QString& modId) { m_mod_id = modId; } protected: /* The file corresponding to this resource. */ QFileInfo m_file_info; /* The cached date when this file was last changed. */ QDateTime m_changed_date_time; /* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */ QString m_internal_id; /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ QString m_name; QString m_mod_id; /* The type of file we're dealing with. */ ResourceType m_type = ResourceType::UNKNOWN; /* Installation status of the resource. */ ResourceStatus m_status = ResourceStatus::UNKNOWN; std::shared_ptr m_metadata = nullptr; /* Whether the resource is enabled (e.g. shows up in the game) or not. */ bool m_enabled = true; /* Used to keep trach of pending / concluded actions on the resource. */ bool m_is_resolving = false; bool m_is_resolved = false; int m_resolution_ticket = 0; QString m_size_str; qint64 m_size_info; }; PrismLauncher-10.0.5/launcher/minecraft/mod/DataPackFolderModel.h0000644000175100017510000000465015144136756024266 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "ResourceFolderModel.h" #include "DataPack.h" #include "ResourcePack.h" class DataPackFolderModel : public ResourceFolderModel { Q_OBJECT public: enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; explicit DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "datapacks"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override; [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(DataPack) }; PrismLauncher-10.0.5/launcher/minecraft/mod/TexturePackFolderModel.cpp0000644000175100017510000001450515144136756025410 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "TexturePackFolderModel.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" #include "minecraft/mod/tasks/ResourceFolderLoadTask.h" TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true }; } Task* TexturePackFolderModel::createParseTask(Resource& resource) { return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast(resource)); } QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: return m_resources[row]->name(); case DateColumn: return m_resources[row]->dateTimeChanged(); case ProviderColumn: return m_resources[row]->provider(); case SizeColumn: return m_resources[row]->sizeStr(); default: return {}; } case Qt::ToolTipRole: if (column == NameColumn) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") .arg(at(row).fileinfo().canonicalFilePath()); ; } if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } return {}; case Qt::CheckStateRole: if (column == ActiveColumn) { return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; } return {}; default: return {}; } } QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case DateColumn: case ImageColumn: case ProviderColumn: case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: { switch (section) { case ActiveColumn: return tr("Is the texture pack enabled?"); case NameColumn: return tr("The name of the texture pack."); case DateColumn: return tr("The date and time this texture pack was last changed (or added)."); case ProviderColumn: return tr("The source provider of the texture pack."); case SizeColumn: return tr("The size of the texture pack."); default: return {}; } } default: break; } return {}; } int TexturePackFolderModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } PrismLauncher-10.0.5/launcher/minecraft/mod/MetadataHandler.h0000644000175100017510000000315015144136756023511 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "modplatform/packwiz/Packwiz.h" namespace Metadata { using ModStruct = Packwiz::V1::Mod; inline ModStruct create(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) { return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); } inline void update(const QDir& index_dir, ModStruct& mod) { Packwiz::V1::updateModIndex(index_dir, mod); } inline void remove(const QDir& index_dir, QString mod_slug) { Packwiz::V1::deleteModIndex(index_dir, mod_slug); } inline ModStruct get(const QDir& index_dir, QString mod_slug) { return Packwiz::V1::getIndexForMod(index_dir, std::move(mod_slug)); } inline ModStruct get(const QDir& index_dir, QVariant& mod_id) { return Packwiz::V1::getIndexForMod(index_dir, mod_id); } }; // namespace Metadata PrismLauncher-10.0.5/launcher/minecraft/mod/TexturePack.cpp0000644000175100017510000000460515144136756023273 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "TexturePack.h" #include #include #include "MTPixmapCache.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" void TexturePack::setDescription(QString new_description) { QMutexLocker locker(&m_data_lock); m_description = new_description; } void TexturePack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); if (m_pack_image_cache_key.key.isValid()) PixmapCache::remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_pack_image_cache_key.key = PixmapCache::insert(pixmap); m_pack_image_cache_key.was_ever_used = true; } QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const { QPixmap cached_image; if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get if (!m_pack_image_cache_key.was_ever_used) { return {}; } else { qDebug() << "Texture Pack" << name() << "Had it's image evicted from the cache. reloading..."; PixmapCache::markCacheMissByEviciton(); } // Imaged got evicted from the cache. Re-process it and retry. TexturePackUtils::processPackPNG(*this); return image(size); } bool TexturePack::valid() const { return m_description != nullptr; } PrismLauncher-10.0.5/launcher/minecraft/mod/ModFolderModel.h0000644000175100017510000000570015144136756023332 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include "Mod.h" #include "ResourceFolderModel.h" class BaseInstance; class QFileSystemWatcher; /** * A legacy mod list. * Backed by a folder. */ class ModFolderModel : public ResourceFolderModel { Q_OBJECT public: enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, VersionColumn, DateColumn, ProviderColumn, SizeColumn, SideColumn, LoadersColumn, McVersionsColumn, ReleaseTypeColumn, NUM_COLUMNS }; ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); virtual QString id() const override { return "mods"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new Mod(file); } [[nodiscard]] Task* createParseTask(Resource&) override; bool isValid(); RESOURCE_HELPERS(Mod) private slots: void onParseSucceeded(int ticket, QString resource_id) override; }; PrismLauncher-10.0.5/launcher/minecraft/mod/ModFolderModel.cpp0000644000175100017510000002236615144136756023674 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModFolderModel.h" #include #include #include #include #include #include #include #include #include #include #include #include "minecraft/mod/tasks/LocalModParseTask.h" ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Version", "Last Modified", "Provider", "Size", "Side", "Loaders", "Minecraft Versions", "Release Type" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Version"), tr("Last Modified"), tr("Provider"), tr("Size"), tr("Side"), tr("Loaders"), tr("Minecraft Versions"), tr("Release Type") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true }; } QVariant ModFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: return m_resources[row]->name(); case VersionColumn: { switch (at(row).type()) { case ResourceType::FOLDER: return tr("Folder"); case ResourceType::SINGLEFILE: return tr("File"); default: break; } return at(row).version(); } case DateColumn: return at(row).dateTimeChanged(); case ProviderColumn: { return at(row).provider(); } case SideColumn: { return at(row).side(); } case LoadersColumn: { return at(row).loaders(); } case McVersionsColumn: { return at(row).mcVersions(); } case ReleaseTypeColumn: { return at(row).releaseType(); } case SizeColumn: return at(row).sizeStr(); default: return QVariant(); } case Qt::ToolTipRole: if (column == NameColumn) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") .arg(at(row).fileinfo().canonicalFilePath()); } if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } return {}; case Qt::CheckStateRole: if (column == ActiveColumn) return at(row).enabled() ? Qt::Checked : Qt::Unchecked; return QVariant(); default: return QVariant(); } } QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case VersionColumn: case DateColumn: case ProviderColumn: case ImageColumn: case SideColumn: case LoadersColumn: case McVersionsColumn: case ReleaseTypeColumn: case SizeColumn: return columnNames().at(section); default: return QVariant(); } case Qt::ToolTipRole: switch (section) { case ActiveColumn: return tr("Is the mod enabled?"); case NameColumn: return tr("The name of the mod."); case VersionColumn: return tr("The version of the mod."); case DateColumn: return tr("The date and time this mod was last changed (or added)."); case ProviderColumn: return tr("The source provider of the mod."); case SideColumn: return tr("On what environment the mod is running."); case LoadersColumn: return tr("The mod loader."); case McVersionsColumn: return tr("The supported minecraft versions."); case ReleaseTypeColumn: return tr("The release type."); case SizeColumn: return tr("The size of the mod."); default: return QVariant(); } default: return QVariant(); } return QVariant(); } int ModFolderModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } Task* ModFolderModel::createParseTask(Resource& resource) { return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); } bool ModFolderModel::isValid() { return m_dir.exists() && m_dir.isReadable(); } void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) { auto iter = m_active_parse_tasks.constFind(ticket); if (iter == m_active_parse_tasks.constEnd()) return; int row = m_resources_index[mod_id]; auto parse_task = *iter; auto cast_task = static_cast(parse_task.get()); Q_ASSERT(cast_task->token() == ticket); auto resource = find(mod_id); auto result = cast_task->result(); if (result && resource) static_cast(resource.get())->finishResolvingWithDetails(std::move(result->details)); emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } PrismLauncher-10.0.5/launcher/minecraft/mod/ResourceFolderModel.cpp0000644000175100017510000007374015144136756024746 0ustar runnerrunner#include "ResourceFolderModel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "Application.h" #include "FileSystem.h" #include "minecraft/mod/tasks/ResourceFolderLoadTask.h" #include "Json.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "settings/Setting.h" #include "tasks/SequentialTask.h" #include "tasks/Task.h" #include "ui/dialogs/CustomMessageBox.h" ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this), m_is_indexed(is_indexed) { if (create_dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); } m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); connect(&m_resourceResolver, &ConcurrentTask::finished, this, [this] { m_resourceResolver.clear(); m_resourceResolverRunning = false; }); if (APPLICATION_DYN) { // in tests the application macro doesn't work m_resourceResolver.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); } } ResourceFolderModel::~ResourceFolderModel() { while (!QThreadPool::globalInstance()->waitForDone(100)) QCoreApplication::processEvents(); } bool ResourceFolderModel::startWatching(const QStringList& paths) { // Remove orphaned metadata next time m_first_folder_load = true; if (m_is_watching) return false; auto couldnt_be_watched = m_watcher.addPaths(paths); for (auto path : paths) { if (couldnt_be_watched.contains(path)) qDebug() << "Failed to start watching" << path; else qDebug() << "Started watching" << path; } update(); m_is_watching = !m_is_watching; return m_is_watching; } bool ResourceFolderModel::stopWatching(const QStringList& paths) { if (!m_is_watching) return false; auto couldnt_be_stopped = m_watcher.removePaths(paths); for (auto path : paths) { if (couldnt_be_stopped.contains(path)) qDebug() << "Failed to stop watching" << path; else qDebug() << "Stopped watching" << path; } m_is_watching = !m_is_watching; return !m_is_watching; } bool ResourceFolderModel::installResource(QString original_path) { // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName original_path = FS::NormalizePath(original_path); QFileInfo file_info(original_path); if (!file_info.exists() || !file_info.isReadable()) { qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path; return false; } qDebug() << "Installing:" << file_info.absoluteFilePath(); Resource resource(file_info); if (!resource.valid()) { qWarning() << original_path << "is not a valid resource. Ignoring it."; return false; } auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName())); if (original_path == new_path) { qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense..."; return false; } switch (resource.type()) { case ResourceType::SINGLEFILE: case ResourceType::ZIPFILE: case ResourceType::LITEMOD: { if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) { if (!FS::deletePath(new_path)) { qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; return false; } qDebug() << new_path << "has been deleted."; } if (!QFile::copy(original_path, new_path)) { qCritical() << "Copy from" << original_path << "to" << new_path << "has failed."; return false; } FS::updateTimestamp(new_path); QFileInfo new_path_file_info(new_path); resource.setFile(new_path_file_info); if (!m_is_watching) return update(); return true; } case ResourceType::FOLDER: { if (QFile::exists(new_path)) { qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path; return false; } if (!FS::copy(original_path, new_path)()) { qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed."; return false; } QFileInfo newpathInfo(new_path); resource.setFile(newpathInfo); if (!m_is_watching) return update(); return true; } default: break; } return false; } void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) { auto install = [this, path] { installResource(std::move(path)); }; if (vers.addonId.isValid()) { ModPlatform::IndexedPack pack{ vers.addonId, ModPlatform::ResourceProvider::FLAME, }; auto response = std::make_shared(); auto job = FlameAPI().getProject(vers.addonId.toString(), response); connect(job.get(), &Task::failed, this, install); connect(job.get(), &Task::aborted, this, install); connect(job.get(), &Task::succeeded, [response, this, &vers, install, &pack] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response for mod info at" << parse_error.offset << "reason:" << parse_error.errorString(); qDebug() << *response; return; } try { auto obj = Json::requireObject(Json::requireObject(doc), "data"); FlameMod::loadIndexedPack(pack, obj); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading mod info:" << e.cause(); } LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); connect(&update_metadata, &Task::finished, this, install); update_metadata.start(); }); job->start(); } else { install(); } } bool ResourceFolderModel::uninstallResource(const QString& file_name, bool preserve_metadata) { for (auto& resource : m_resources) { auto resourceFileInfo = resource->fileinfo(); auto resourceFileName = resource->fileinfo().fileName(); if (!resource->enabled() && resourceFileName.endsWith(".disabled")) { resourceFileName.chop(9); } if (resourceFileName == file_name) { auto res = resource->destroy(indexDir(), preserve_metadata, false); update(); return res; } } return false; } bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) { if (indexes.isEmpty()) return true; for (auto i : indexes) { if (i.column() != 0) continue; auto& resource = m_resources.at(i.row()); resource->destroy(indexDir()); } update(); return true; } void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes) { if (indexes.isEmpty()) return; for (auto i : indexes) { if (i.column() != 0) continue; auto& resource = m_resources.at(i.row()); resource->destroyMetadata(indexDir()); } update(); } bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) { if (m_instance != nullptr && m_instance->isRunning()) { auto response = CustomMessageBox::selectable(nullptr, tr("Confirm toggle"), tr("If you enable/disable this resource while the game is running it may crash your game.\n" "Are you sure you want to do this?"), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return false; } if (indexes.isEmpty()) return true; bool succeeded = true; for (auto const& idx : indexes) { if (!validateIndex(idx) || idx.column() != 0) continue; int row = idx.row(); auto& resource = m_resources[row]; // Preserve the row, but change its ID auto old_id = resource->internal_id(); if (!resource->enable(action)) { succeeded = false; continue; } auto new_id = resource->internal_id(); m_resources_index.remove(old_id); m_resources_index[new_id] = row; emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } return succeeded; } static QMutex s_update_task_mutex; bool ResourceFolderModel::update() { // We hold a lock here to prevent race conditions on the m_current_update_task reset. QMutexLocker lock(&s_update_task_mutex); // Already updating, so we schedule a future update and return. if (m_current_update_task) { m_scheduled_update = true; return false; } m_current_update_task.reset(createUpdateTask()); if (!m_current_update_task) return false; connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded, Qt::ConnectionType::QueuedConnection); connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection); connect( m_current_update_task.get(), &Task::finished, this, [this] { m_current_update_task.reset(); if (m_scheduled_update) { m_scheduled_update = false; update(); } else { emit updateFinished(); } }, Qt::ConnectionType::QueuedConnection); Task::Ptr preUpdate{ createPreUpdateTask() }; if (preUpdate != nullptr) { auto task = new SequentialTask("ResourceFolderModel::update"); task->addTask(preUpdate); task->addTask(m_current_update_task); connect(task, &Task::finished, [task] { task->deleteLater(); }); QThreadPool::globalInstance()->start(task); } else { QThreadPool::globalInstance()->start(m_current_update_task.get()); } return true; } void ResourceFolderModel::resolveResource(Resource::Ptr res) { if (!res->shouldResolve()) { return; } Task::Ptr task{ createParseTask(*res) }; if (!task) return; int ticket = m_next_resolution_ticket.fetch_add(1); res->setResolving(true, ticket); m_active_parse_tasks.insert(ticket, task); connect( task.get(), &Task::succeeded, this, [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( task.get(), &Task::failed, this, [this, ticket, res] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( task.get(), &Task::finished, this, [this, ticket] { m_active_parse_tasks.remove(ticket); emit parseFinished(); }, Qt::ConnectionType::QueuedConnection); m_resourceResolver.addTask(task); if (!m_resourceResolverRunning) { QThreadPool::globalInstance()->start(&m_resourceResolver); m_resourceResolverRunning = true; } } void ResourceFolderModel::onUpdateSucceeded() { auto update_results = static_cast(m_current_update_task.get())->result(); auto& new_resources = update_results->resources; auto current_list = m_resources_index.keys(); QSet current_set(current_list.begin(), current_list.end()); auto new_list = new_resources.keys(); QSet new_set(new_list.begin(), new_list.end()); applyUpdates(current_set, new_set, new_resources); } void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) { auto iter = m_active_parse_tasks.constFind(ticket); if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) return; int row = m_resources_index[resource_id]; emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } Task* ResourceFolderModel::createUpdateTask() { auto index_dir = indexDir(); auto task = new ResourceFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, [this](const QFileInfo& file) { return createResource(file); }); m_first_folder_load = false; return task; } bool ResourceFolderModel::hasPendingParseTasks() const { return !m_active_parse_tasks.isEmpty(); } void ResourceFolderModel::directoryChanged(QString path) { update(); } Qt::DropActions ResourceFolderModel::supportedDropActions() const { // copy from outside, move from within and other resource lists return Qt::CopyAction | Qt::MoveAction; } Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); auto flags = defaultFlags | Qt::ItemIsDropEnabled; if (index.isValid()) flags |= Qt::ItemIsUserCheckable; return flags; } QStringList ResourceFolderModel::mimeTypes() const { QStringList types; types << "text/uri-list"; return types; } bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) { if (action == Qt::IgnoreAction) { return true; } // check if the action is supported if (!data || !(action & supportedDropActions())) { return false; } // files dropped from outside? if (data->hasUrls()) { auto urls = data->urls(); for (auto url : urls) { // only local files may be dropped... if (!url.isLocalFile()) { continue; } // TODO: implement not only copy, but also move // FIXME: handle errors here installResource(url.toLocalFile()); } return true; } return false; } bool ResourceFolderModel::validateIndex(const QModelIndex& index) const { if (!index.isValid()) return false; int row = index.row(); if (row < 0 || row >= m_resources.size()) return false; return true; } QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: return m_resources[row]->name(); case DateColumn: return m_resources[row]->dateTimeChanged(); case ProviderColumn: return m_resources[row]->provider(); case SizeColumn: return m_resources[row]->sizeStr(); default: return {}; } case Qt::ToolTipRole: if (column == NameColumn) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") .arg(at(row).fileinfo().canonicalFilePath()); ; } if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return QIcon::fromTheme("status-yellow"); return {}; } case Qt::CheckStateRole: if (column == ActiveColumn) return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; return {}; default: return {}; } } bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { int row = index.row(); if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) return false; if (role == Qt::CheckStateRole) { return setResourceEnabled({ index }, EnableAction::TOGGLE); } return false; } QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case DateColumn: case ProviderColumn: case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: { //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. switch (section) { case ActiveColumn: return tr("Is the resource enabled?"); case NameColumn: return tr("The name of the resource."); case DateColumn: return tr("The date and time this resource was last changed (or added)."); case ProviderColumn: return tr("The source provider of the resource."); case SizeColumn: return tr("The size of the resource."); default: return {}; } } default: break; } return {}; } void ResourceFolderModel::setupHeaderAction(QAction* act, int column) { Q_ASSERT(act); act->setText(columnNames().at(column)); } void ResourceFolderModel::saveColumns(QTreeView* tree) { auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); auto stateSetting = m_instance->settings()->getSetting(stateSettingName); stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false auto settings = m_instance->settings(); if (!settings->get(overrideSettingName).toBool()) { settings = APPLICATION->settings(); } auto visibility = Json::toMap(settings->get(visibilitySettingName).toString()); for (auto i = 0; i < m_column_names.size(); ++i) { if (m_columnsHideable[i]) { auto name = m_column_names[i]; visibility[name] = !tree->isColumnHidden(i); } } settings->set(visibilitySettingName, Json::fromMap(visibility)); } void ResourceFolderModel::loadColumns(QTreeView* tree) { auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, ""); tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8())); auto setVisible = [this, tree](QVariant value) { auto visibility = Json::toMap(value.toString()); for (auto i = 0; i < m_column_names.size(); ++i) { if (m_columnsHideable[i]) { auto name = m_column_names[i]; tree->setColumnHidden(i, !visibility.value(name, false).toBool()); } } }; auto const defaultValue = Json::fromMap({ { "Image", true }, { "Version", true }, { "Last Modified", true }, { "Provider", true }, { "Pack Format", true }, }); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false auto settings = m_instance->settings(); if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) { settings = APPLICATION->settings(); } auto visibility = settings->getOrRegisterSetting(visibilitySettingName, defaultValue); setVisible(visibility->get()); // allways connect the signal in case the setting is toggled on and off auto gSetting = APPLICATION->settings()->getOrRegisterSetting(visibilitySettingName, defaultValue); connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible, overrideSettingName](const Setting&, QVariant value) { if (!m_instance->settings()->get(overrideSettingName).toBool()) { setVisible(value); } }); } QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) { auto menu = new QMenu(tree); { // action to decide if the visibility is per instance or not auto act = new QAction(tr("Override Columns Visibility"), menu); auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); act->setCheckable(true); act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool()); connect(act, &QAction::toggled, tree, [this, tree, overrideSettingName](bool toggled) { m_instance->settings()->set(overrideSettingName, toggled); saveColumns(tree); }); menu->addAction(act); } menu->addSeparator()->setText(tr("Show / Hide Columns")); for (int col = 0; col < columnCount(); ++col) { // Skip creating actions for columns that should not be hidden if (!m_columnsHideable.at(col)) continue; auto act = new QAction(menu); setupHeaderAction(act, col); act->setCheckable(true); act->setChecked(!tree->isColumnHidden(col)); connect(act, &QAction::toggled, tree, [this, col, tree](bool toggled) { tree->setColumnHidden(col, !toggled); for (int c = 0; c < columnCount(); ++c) { if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents) tree->resizeColumnToContents(c); } saveColumns(tree); }); menu->addAction(act); } return menu; } QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent) { return new ProxyModel(parent); } SortType ResourceFolderModel::columnToSortKey(size_t column) const { Q_ASSERT(m_column_sort_keys.size() == columnCount()); return m_column_sort_keys.at(column); } /* Standard Proxy Model for createFilterProxyModel */ bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, [[maybe_unused]] const QModelIndex& source_parent) const { auto* model = qobject_cast(sourceModel()); if (!model) return true; const auto& resource = model->at(source_row); return resource.applyFilter(filterRegularExpression()); } bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const { auto* model = qobject_cast(sourceModel()); if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { return QSortFilterProxyModel::lessThan(source_left, source_right); } // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and // proceed. auto column_sort_key = model->columnToSortKey(source_left.column()); auto const& resource_left = model->at(source_left.row()); auto const& resource_right = model->at(source_right.row()); auto compare_result = resource_left.compare(resource_right, column_sort_key); if (compare_result == 0) return QSortFilterProxyModel::lessThan(source_left, source_right); return compare_result < 0; } QString ResourceFolderModel::instDirPath() const { return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); } void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) { auto iter = m_active_parse_tasks.constFind(ticket); if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) return; auto removed_index = m_resources_index[resource_id]; auto removed_it = m_resources.begin() + removed_index; Q_ASSERT(removed_it != m_resources.end()); beginRemoveRows(QModelIndex(), removed_index, removed_index); m_resources.erase(removed_it); // update index m_resources_index.clear(); int idx = 0; for (auto const& mod : qAsConst(m_resources)) { m_resources_index[mod->internal_id()] = idx; idx++; } endRemoveRows(); } void ResourceFolderModel::applyUpdates(QSet& current_set, QSet& new_set, QMap& new_resources) { // see if the kept resources changed in some way { QSet kept_set = current_set; kept_set.intersect(new_set); for (auto const& kept : kept_set) { auto row_it = m_resources_index.constFind(kept); Q_ASSERT(row_it != m_resources_index.constEnd()); auto row = row_it.value(); auto& new_resource = new_resources[kept]; auto const& current_resource = m_resources.at(row); if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) { // no significant change, ignore... continue; } // If the resource is resolving, but something about it changed, we don't want to // continue the resolving. if (current_resource->isResolving()) { auto ticket = current_resource->resolutionTicket(); if (m_active_parse_tasks.contains(ticket)) { auto task = (*m_active_parse_tasks.find(ticket)).get(); task->abort(); } } m_resources[row].reset(new_resource); resolveResource(m_resources.at(row)); emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); } } // remove resources no longer present { QSet removed_set = current_set; removed_set.subtract(new_set); QList removed_rows; for (auto& removed : removed_set) removed_rows.append(m_resources_index[removed]); std::sort(removed_rows.begin(), removed_rows.end(), std::greater()); for (auto& removed_index : removed_rows) { auto removed_it = m_resources.begin() + removed_index; Q_ASSERT(removed_it != m_resources.end()); if ((*removed_it)->isResolving()) { auto ticket = (*removed_it)->resolutionTicket(); if (m_active_parse_tasks.contains(ticket)) { auto task = (*m_active_parse_tasks.find(ticket)).get(); task->abort(); } } beginRemoveRows(QModelIndex(), removed_index, removed_index); m_resources.erase(removed_it); endRemoveRows(); } } // add new resources to the end { QSet added_set = new_set; added_set.subtract(current_set); // When you have a Qt build with assertions turned on, proceeding here will abort the application if (added_set.size() > 0) { beginInsertRows(QModelIndex(), static_cast(m_resources.size()), static_cast(m_resources.size() + added_set.size() - 1)); for (auto& added : added_set) { auto res = new_resources[added]; m_resources.append(res); resolveResource(m_resources.last()); } endInsertRows(); } } // update index { m_resources_index.clear(); int idx = 0; for (auto const& mod : qAsConst(m_resources)) { m_resources_index[mod->internal_id()] = idx; idx++; } } } Resource::Ptr ResourceFolderModel::find(QString id) { auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), [&](Resource::Ptr const& r) { return r->internal_id() == id; }); if (iter == m_resources.constEnd()) return nullptr; return *iter; } QList ResourceFolderModel::allResources() { QList result; result.reserve(m_resources.size()); for (const Resource ::Ptr& resource : m_resources) result.append((resource.get())); return result; } QList ResourceFolderModel::selectedResources(const QModelIndexList& indexes) { QList result; for (const QModelIndex& index : indexes) { if (index.column() != 0) continue; result.append(&at(index.row())); } return result; } PrismLauncher-10.0.5/launcher/minecraft/mod/ResourcePackFolderModel.h0000644000175100017510000000171315144136756025201 0ustar runnerrunner#pragma once #include "ResourceFolderModel.h" #include "ResourcePack.h" class ResourcePackFolderModel : public ResourceFolderModel { Q_OBJECT public: enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, ProviderColumn, SizeColumn, NUM_COLUMNS }; explicit ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent = nullptr); QString id() const override { return "resourcepacks"; } QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new ResourcePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; RESOURCE_HELPERS(ResourcePack) }; PrismLauncher-10.0.5/launcher/minecraft/mod/ShaderPack.cpp0000644000175100017510000000211615144136756023034 0ustar runnerrunner // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ShaderPack.h" void ShaderPack::setPackFormat(ShaderPackFormat new_format) { QMutexLocker locker(&m_data_lock); m_pack_format = new_format; } bool ShaderPack::valid() const { return m_pack_format != ShaderPackFormat::INVALID; } PrismLauncher-10.0.5/launcher/minecraft/mod/ResourcePack.h0000644000175100017510000000110315144136756023055 0ustar runnerrunner#pragma once #include "Resource.h" #include "minecraft/mod/DataPack.h" #include #include #include #include class Version; /* TODO: * * Store localized descriptions * */ class ResourcePack : public DataPack { Q_OBJECT public: ResourcePack(QObject* parent = nullptr) : DataPack(parent) {} ResourcePack(QFileInfo file_info) : DataPack(file_info) {} /** Gets, respectively, the lower and upper versions supported by the set pack format. */ std::pair compatibleVersions() const override; }; PrismLauncher-10.0.5/launcher/minecraft/mod/DataPack.cpp0000644000175100017510000002363215144136756022505 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "DataPack.h" #include #include #include #include "MTPixmapCache.h" #include "Version.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h" // Values taken from: // https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats static const QMap> s_pack_format_versions = { { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } }, { 10, { Version("1.19"), Version("1.19.3") } }, { 11, { Version("23w03a"), Version("23w05a") } }, { 12, { Version("1.19.4"), Version("1.19.4") } }, { 13, { Version("23w12a"), Version("23w14a") } }, { 14, { Version("23w16a"), Version("23w17a") } }, { 15, { Version("1.20"), Version("1.20.1") } }, { 16, { Version("23w31a"), Version("23w31a") } }, { 17, { Version("23w32a"), Version("23w35a") } }, { 18, { Version("1.20.2"), Version("1.20.2") } }, { 19, { Version("23w40a"), Version("23w40a") } }, { 20, { Version("23w41a"), Version("23w41a") } }, { 21, { Version("23w42a"), Version("23w42a") } }, { 22, { Version("23w43a"), Version("23w43b") } }, { 23, { Version("23w44a"), Version("23w44a") } }, { 24, { Version("23w45a"), Version("23w45a") } }, { 25, { Version("23w46a"), Version("23w46a") } }, { 26, { Version("1.20.3"), Version("1.20.4") } }, { 27, { Version("23w51a"), Version("23w51b") } }, { 28, { Version("24w05a"), Version("24w05b") } }, { 29, { Version("24w04a"), Version("24w04a") } }, { 30, { Version("24w05a"), Version("24w05b") } }, { 31, { Version("24w06a"), Version("24w06a") } }, { 32, { Version("24w07a"), Version("24w07a") } }, { 33, { Version("24w09a"), Version("24w09a") } }, { 34, { Version("24w10a"), Version("24w10a") } }, { 35, { Version("24w11a"), Version("24w11a") } }, { 36, { Version("24w12a"), Version("24w12a") } }, { 37, { Version("24w13a"), Version("24w13a") } }, { 38, { Version("24w14a"), Version("24w14a") } }, { 39, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } }, { 40, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } }, { 41, { Version("1.20.5"), Version("1.20.6") } }, { 42, { Version("24w18a"), Version("24w18a") } }, { 43, { Version("24w19a"), Version("24w19b") } }, { 44, { Version("24w20a"), Version("24w20a") } }, { 45, { Version("21w21a"), Version("21w21b") } }, { 46, { Version("1.21-pre1"), Version("1.21-pre1") } }, { 47, { Version("1.21-pre2"), Version("1.21-pre2") } }, { 48, { Version("1.21"), Version("1.21") } } }; void DataPack::setPackFormat(int new_format_id) { QMutexLocker locker(&m_data_lock); if (!s_pack_format_versions.contains(new_format_id)) { qWarning() << "Pack format '" << new_format_id << "' is not a recognized data pack id!"; } m_pack_format = new_format_id; } void DataPack::setDescription(QString new_description) { QMutexLocker locker(&m_data_lock); m_description = new_description; } void DataPack::setImage(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); if (m_pack_image_cache_key.key.isValid()) PixmapCache::instance().remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); m_pack_image_cache_key.was_ever_used = true; // This can happen if the pixmap is too big to fit in the cache :c if (!m_pack_image_cache_key.key.isValid()) { qWarning() << "Could not insert a image cache entry! Ignoring it."; m_pack_image_cache_key.was_ever_used = false; } } QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const { QPixmap cached_image; if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get if (!m_pack_image_cache_key.was_ever_used) { return {}; } else { qDebug() << "Data Pack" << name() << "Had it's image evicted from the cache. reloading..."; PixmapCache::markCacheMissByEviciton(); } // Imaged got evicted from the cache. Re-process it and retry. DataPackUtils::processPackPNG(this); return image(size); } std::pair DataPack::compatibleVersions() const { if (!s_pack_format_versions.contains(m_pack_format)) { return { {}, {} }; } return s_pack_format_versions.constFind(m_pack_format).value(); } int DataPack::compare(const Resource& other, SortType type) const { auto const& cast_other = static_cast(other); if (type == SortType::PACK_FORMAT) { auto this_ver = packFormat(); auto other_ver = cast_other.packFormat(); if (this_ver > other_ver) return 1; if (this_ver < other_ver) return -1; } else { return Resource::compare(other, type); } return 0; } bool DataPack::applyFilter(QRegularExpression filter) const { if (filter.match(description()).hasMatch()) return true; if (filter.match(QString::number(packFormat())).hasMatch()) return true; if (filter.match(compatibleVersions().first.toString()).hasMatch()) return true; if (filter.match(compatibleVersions().second.toString()).hasMatch()) return true; return Resource::applyFilter(filter); } bool DataPack::valid() const { return m_pack_format != 0; } PrismLauncher-10.0.5/launcher/minecraft/mod/TexturePack.h0000644000175100017510000000432515144136756022737 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "Resource.h" #include #include #include #include class Version; class TexturePack : public Resource { Q_OBJECT public: using Ptr = shared_qobject_ptr; TexturePack(QObject* parent = nullptr) : Resource(parent) {} TexturePack(QFileInfo file_info) : Resource(file_info) {} /** Gets the description of the texture pack. */ QString description() const { return m_description; } /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setDescription(QString new_description); /** Thread-safe. */ void setImage(QImage new_image) const; bool valid() const override; protected: mutable QMutex m_data_lock; /** The texture pack's description, as defined in the pack.txt file. */ QString m_description; /** The texture pack's image file cache key, for access in the QPixmapCache global instance. * * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. */ struct { QPixmapCache::Key key; bool was_ever_used = false; } mutable m_pack_image_cache_key; }; PrismLauncher-10.0.5/launcher/minecraft/mod/DataPack.h0000644000175100017510000000556115144136756022153 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "Resource.h" #include #include class Version; /* TODO: * * Store localized descriptions * */ class DataPack : public Resource { Q_OBJECT public: DataPack(QObject* parent = nullptr) : Resource(parent) {} DataPack(QFileInfo file_info) : Resource(file_info) {} /** Gets the numerical ID of the pack format. */ int packFormat() const { return m_pack_format; } /** Gets, respectively, the lower and upper versions supported by the set pack format. */ virtual std::pair compatibleVersions() const; /** Gets the description of the data pack. */ QString description() const { return m_description; } /** Gets the image of the data pack, converted to a QPixmap for drawing, and scaled to size. */ QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setPackFormat(int new_format_id); /** Thread-safe. */ void setDescription(QString new_description); /** Thread-safe. */ void setImage(QImage new_image) const; bool valid() const override; [[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; protected: mutable QMutex m_data_lock; /* The 'version' of a data pack, as defined in the pack.mcmeta file. * See https://minecraft.wiki/w/Data_pack#pack.mcmeta */ int m_pack_format = 0; /** The data pack's description, as defined in the pack.mcmeta file. */ QString m_description; /** The data pack's image file cache key, for access in the QPixmapCache global instance. * * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. */ struct { QPixmapCache::Key key; bool was_ever_used = false; } mutable m_pack_image_cache_key; }; PrismLauncher-10.0.5/launcher/minecraft/mod/WorldSave.cpp0000644000175100017510000000241215144136756022734 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "WorldSave.h" #include "minecraft/mod/tasks/LocalWorldSaveParseTask.h" void WorldSave::setSaveFormat(WorldSaveFormat new_save_format) { QMutexLocker locker(&m_data_lock); m_save_format = new_save_format; } void WorldSave::setSaveDirName(QString dir_name) { QMutexLocker locker(&m_data_lock); m_save_dir_name = dir_name; } bool WorldSave::valid() const { return m_save_format != WorldSaveFormat::INVALID; } PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/0000755000175100017510000000000015144136756021450 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h0000644000175100017510000000327415144136756026326 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/WorldSave.h" #include "tasks/Task.h" namespace WorldSaveUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(WorldSave& save, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); bool validate(QFileInfo file); } // namespace WorldSaveUtils class LocalWorldSaveParseTask : public Task { Q_OBJECT public: LocalWorldSaveParseTask(int token, WorldSave& save); bool canAbort() const override { return true; } bool abort() override; void executeTask() override; int token() const { return m_token; } private: int m_token; WorldSave& m_save; bool m_aborted = false; }; PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp0000644000175100017510000003555715144136756026504 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "GetModDependenciesTask.h" #include #include #include #include "Json.h" #include "QObjectPtr.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "tasks/SequentialTask.h" #include "ui/pages/modplatform/ModModel.h" static Version mcVersion(BaseInstance* inst) { return static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion(); } static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst) { return static_cast(inst)->getPackProfile()->getSupportedModLoaders().value(); } static bool checkDependencies(std::shared_ptr sel, Version mcVersion, ModPlatform::ModLoaderTypes loaders) { return (sel->pack->versions.isEmpty() || sel->version.mcVersion.contains(mcVersion.toString())) && (!loaders || !sel->version.loaders || sel->version.loaders & loaders); } GetModDependenciesTask::GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected) : SequentialTask(tr("Get dependencies")), m_selected(selected), m_version(mcVersion(instance)), m_loaderType(mcLoaders(instance)) { for (auto mod : folder->allMods()) { m_mods_file_names << mod->fileinfo().fileName(); if (auto meta = mod->metadata(); meta) m_mods.append(meta); } prepare(); } void GetModDependenciesTask::prepare() { for (auto sel : m_selected) { if (checkDependencies(sel, m_version, m_loaderType)) for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) { addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); } } } ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, const ModPlatform::ResourceProvider providerName) { if (auto isQuilt = m_loaderType & ModPlatform::Quilt; isQuilt || m_loaderType & ModPlatform::Fabric) { auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](const auto& o) { return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); }); if (over != overide.cend()) { return { isQuilt ? over->quilt : over->fabric, dep.type }; } } return dep; } QList GetModDependenciesTask::getDependenciesForVersion(const ModPlatform::IndexedVersion& version, const ModPlatform::ResourceProvider providerName) { QList c_dependencies; for (auto ver_dep : version.dependencies) { if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) continue; ver_dep = getOverride(ver_dep, providerName); auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(), [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) { return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId; }); dep != c_dependencies.end()) continue; // check the current dependency list if (auto dep = std::find_if(m_selected.begin(), m_selected.end(), [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.version : i->pack->addonId == ver_dep.addonId); }); dep != m_selected.end()) continue; // check the selected versions if (auto dep = std::find_if(m_mods.begin(), m_mods.end(), [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { return i->provider == providerName && (isOnlyVersion ? i->file_id == ver_dep.version : i->project_id == ver_dep.addonId); }); dep != m_mods.end()) continue; // check the existing mods if (auto dep = std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), [&ver_dep, providerName, isOnlyVersion](std::shared_ptr i) { return i->pack->provider == providerName && (isOnlyVersion ? i->version.version == ver_dep.addonId : i->pack->addonId == ver_dep.addonId); }); dep != m_pack_dependencies.end()) // check loaded dependencies continue; c_dependencies.append(ver_dep); } return c_dependencies; } Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) { auto provider = pDep->pack->provider; auto responseInfo = std::make_shared(); auto info = getAPI(provider)->getProject(pDep->pack->addonId.toString(), responseInfo); connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); if (parse_error.error != QJsonParseError::NoError) { removePack(pDep->pack->addonId); qWarning() << "Error while parsing JSON response for mod info at" << parse_error.offset << "reason:" << parse_error.errorString(); qDebug() << *responseInfo; return; } try { auto obj = provider == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data") : Json::requireObject(doc); getAPI(provider)->loadIndexedPack(*pDep->pack, obj); } catch (const JSONValidationError& e) { removePack(pDep->pack->addonId); qDebug() << doc; qWarning() << "Error while reading mod info:" << e.cause(); } }); return info; } Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Dependency& dep, const ModPlatform::ResourceProvider providerName, int level) { auto pDep = std::make_shared(); pDep->dependency = dep; pDep->pack = std::make_shared(); pDep->pack->addonId = dep.addonId; pDep->pack->provider = providerName; m_pack_dependencies.append(pDep); auto provider = providerName; auto tasks = makeShared( QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); if (!dep.addonId.toString().isEmpty()) { tasks->addTask(getProjectInfoTask(pDep)); } ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; ResourceAPI::Callback callbacks; callbacks.on_fail = [](QString reason, int) { qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason); }; callbacks.on_succeed = [dep, provider, pDep, level, this](auto& pack) { pDep->version = pack; if (!pDep->version.addonId.isValid()) { if (m_loaderType & ModPlatform::Quilt) { // falback for quilt auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, provider](auto o) { return o.provider == provider && dep.addonId == o.quilt; }); if (over != overide.cend()) { removePack(dep.addonId); addTask(prepareDependencyTask({ over->fabric, dep.type }, provider, level)); return; } } removePack(dep.addonId); return; } pDep->version.is_currently_selected = true; pDep->pack->versions = { pDep->version }; pDep->pack->versionsLoaded = true; if (level == 0) { removePack(dep.addonId); qWarning() << "Dependency cycle exceeded"; return; } if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) { pDep->pack->addonId = pDep->version.addonId; auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider); if (dep_.addonId != pDep->version.addonId) { removePack(pDep->version.addonId); addTask(prepareDependencyTask(dep_, provider, level)); } else { addTask(getProjectInfoTask(pDep)); } } if (isLocalyInstalled(pDep)) { removePack(pDep->version.addonId); return; } for (auto dep_ : getDependenciesForVersion(pDep->version, provider)) { addTask(prepareDependencyTask(dep_, provider, level - 1)); } }; auto version = getAPI(provider)->getDependencyVersion(std::move(args), std::move(callbacks)); tasks->addTask(version); return tasks; } void GetModDependenciesTask::removePack(const QVariant& addonId) { auto pred = [addonId](const std::shared_ptr& v) { return v->pack->addonId == addonId; }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) m_pack_dependencies.removeIf(pred); #else for (auto it = m_pack_dependencies.begin(); it != m_pack_dependencies.end();) if (pred(*it)) it = m_pack_dependencies.erase(it); else ++it; #endif } auto GetModDependenciesTask::getExtraInfo() -> QHash { QHash rby; auto fullList = m_selected + m_pack_dependencies; for (auto& mod : fullList) { auto addonId = mod->pack->addonId; auto provider = mod->pack->provider; auto version = mod->version.fileId; auto req = QStringList(); for (auto& smod : fullList) { if (provider != smod->pack->provider) continue; auto deps = smod->version.dependencies; if (auto dep = std::find_if(deps.begin(), deps.end(), [addonId, provider, version](const ModPlatform::Dependency& d) { return d.type == ModPlatform::DependencyType::REQUIRED && (provider == ModPlatform::ResourceProvider::MODRINTH && d.addonId.toString().isEmpty() ? version == d.version : d.addonId == addonId); }); dep != deps.end()) { req.append(smod->pack->name); } } rby[addonId.toString()] = { maybeInstalled(mod), req }; } return rby; } // super lax compare (but not fuzzy) // convert to lowercase // convert all speratores to whitespace // simplify sequence of internal whitespace to a single space // efectivly compare two strings ignoring all separators and case auto laxCompare = [](QString fsfilename, QString metadataFilename, bool excludeDigits = false) { // allowed character seperators QList allowedSeperators = { '-', '+', '.', '_' }; if (excludeDigits) allowedSeperators.append({ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }); // copy in lowercase auto fsName = fsfilename.toLower(); auto metaName = metadataFilename.toLower(); // replace all potential allowed seperatores with whitespace for (auto sep : allowedSeperators) { fsName = fsName.replace(sep, ' '); metaName = metaName.replace(sep, ' '); } // remove extraneous whitespace fsName = fsName.simplified(); metaName = metaName.simplified(); return fsName.compare(metaName) == 0; }; bool GetModDependenciesTask::isLocalyInstalled(std::shared_ptr pDep) { return pDep->version.fileName.isEmpty() || std::find_if(m_selected.begin(), m_selected.end(), [pDep](std::shared_ptr i) { return !i->version.fileName.isEmpty() && laxCompare(i->version.fileName, pDep->version.fileName); }) != m_selected.end() || // check the selected versions std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName); }) != m_mods_file_names.end() || // check the existing mods std::find_if(m_pack_dependencies.begin(), m_pack_dependencies.end(), [pDep](std::shared_ptr i) { return pDep->pack->addonId != i->pack->addonId && !i->version.fileName.isEmpty() && laxCompare(pDep->version.fileName, i->version.fileName); }) != m_pack_dependencies.end(); // check loaded dependencies } bool GetModDependenciesTask::maybeInstalled(std::shared_ptr pDep) { return std::find_if(m_mods_file_names.begin(), m_mods_file_names.end(), [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName, true); }) != m_mods_file_names.end(); // check the existing mods } PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/GetModDependenciesTask.h0000644000175100017510000000654015144136756026137 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/SequentialTask.h" #include "tasks/Task.h" #include "ui/pages/modplatform/ModModel.h" class GetModDependenciesTask : public SequentialTask { Q_OBJECT public: using Ptr = shared_qobject_ptr; struct PackDependency { ModPlatform::Dependency dependency; ModPlatform::IndexedPack::Ptr pack; ModPlatform::IndexedVersion version; PackDependency() = default; PackDependency(const ModPlatform::IndexedPack::Ptr p, const ModPlatform::IndexedVersion& v) { pack = p; version = v; } }; struct PackDependencyExtraInfo { bool maybe_installed; QStringList required_by; }; explicit GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected); auto getDependecies() const -> QList> { return m_pack_dependencies; } QHash getExtraInfo(); private: inline ResourceAPI* getAPI(ModPlatform::ResourceProvider provider) { if (provider == ModPlatform::ResourceProvider::FLAME) return &m_flameAPI; else return &m_modrinthAPI; } protected slots: Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); QList getDependenciesForVersion(const ModPlatform::IndexedVersion&, ModPlatform::ResourceProvider providerName); void prepare(); Task::Ptr getProjectInfoTask(std::shared_ptr pDep); ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName); void removePack(const QVariant& addonId); bool isLocalyInstalled(std::shared_ptr pDep); bool maybeInstalled(std::shared_ptr pDep); private: QList> m_pack_dependencies; QList> m_mods; QList> m_selected; QStringList m_mods_file_names; Version m_version; ModPlatform::ModLoaderTypes m_loaderType; ModrinthAPI m_modrinthAPI; FlameAPI m_flameAPI; }; PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalModParseTask.cpp0000644000175100017510000006367015144136756025500 0ustar runnerrunner#include "LocalModParseTask.h" #include #include #include #include #include #include #include #include #include "FileSystem.h" #include "Json.h" #include "archive/ArchiveReader.h" #include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" static const QRegularExpression s_newlineRegex("\r\n|\n|\r"); namespace ModUtils { // NEW format // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/c8d8f1929aff9979e322af79a59ce81f3e02db6a // OLD format: // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc ModDetails ReadMCModInfo(QByteArray contents) { auto getInfoFromArray = [](QJsonArray arr) -> ModDetails { if (!arr.at(0).isObject()) { return {}; } ModDetails details; auto firstObj = arr.at(0).toObject(); details.mod_id = firstObj.value("modid").toString(); auto name = firstObj.value("name").toString(); // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name if (name != "Example Mod") { details.name = name; } details.version = firstObj.value("version").toString(); auto homeurl = firstObj.value("url").toString().trimmed(); if (!homeurl.isEmpty()) { // fix up url. if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) { homeurl.prepend("http://"); } } details.homeurl = homeurl; details.description = firstObj.value("description").toString(); QJsonArray authors = firstObj.value("authorList").toArray(); if (authors.size() == 0) { // FIXME: what is the format of this? is there any? authors = firstObj.value("authors").toArray(); } if (firstObj.contains("logoFile")) { details.icon_file = firstObj.value("logoFile").toString(); } for (auto author : authors) { details.authors.append(author.toString()); } return details; }; QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); // this is the very old format that had just the array if (jsonDoc.isArray()) { return getInfoFromArray(jsonDoc.array()); } else if (jsonDoc.isObject()) { auto val = jsonDoc.object().value("modinfoversion"); if (val.isUndefined()) { val = jsonDoc.object().value("modListVersion"); } int version = val.toInt(-1); // Some mods set the number with "", so it's a String instead if (version < 0) version = val.toString("").toInt(); if (version != 2) { qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version); qWarning() << "The contents of 'mcmod.info' are as follows:"; qWarning() << contents; } auto arrVal = jsonDoc.object().value("modlist"); if (arrVal.isUndefined()) { arrVal = jsonDoc.object().value("modList"); } if (arrVal.isArray()) { return getInfoFromArray(arrVal.toArray()); } } return {}; } // https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md ModDetails ReadMCModTOML(QByteArray contents) { ModDetails details; toml::table tomlData; #if TOML_EXCEPTIONS try { tomlData = toml::parse(contents.toStdString()); } catch ([[maybe_unused]] const toml::parse_error& err) { return {}; } #else toml::parse_result result = toml::parse(contents.toStdString()); if (!result) { return {}; } tomlData = result.table(); #endif // array defined by [[mods]] auto tomlModsArr = tomlData["mods"].as_array(); if (!tomlModsArr) { qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; return {}; } // we only really care about the first element, since multiple mods in one file is not supported by us at the moment auto tomlModsTable0 = tomlModsArr->get(0); if (!tomlModsTable0) { qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!"; return {}; } auto modsTable = tomlModsTable0->as_table(); if (!modsTable) { qWarning() << "Corrupted mods.toml? [[mods]] was not a table!"; return {}; } // mandatory properties - always in [[mods]] if (auto modIdDatum = (*modsTable)["modId"].as_string()) { details.mod_id = QString::fromStdString(modIdDatum->get()); } if (auto versionDatum = (*modsTable)["version"].as_string()) { details.version = QString::fromStdString(versionDatum->get()); } if (auto displayNameDatum = (*modsTable)["displayName"].as_string()) { details.name = QString::fromStdString(displayNameDatum->get()); } if (auto descriptionDatum = (*modsTable)["description"].as_string()) { details.description = QString::fromStdString(descriptionDatum->get()); } // optional properties - can be in the root table or [[mods]] QString authors = ""; if (auto authorsDatum = tomlData["authors"].as_string()) { authors = QString::fromStdString(authorsDatum->get()); } else if (auto authorsDatumMods = (*modsTable)["authors"].as_string()) { authors = QString::fromStdString(authorsDatumMods->get()); } if (!authors.isEmpty()) { details.authors.append(authors); } QString homeurl = ""; if (auto homeurlDatum = tomlData["displayURL"].as_string()) { homeurl = QString::fromStdString(homeurlDatum->get()); } else if (auto homeurlDatumMods = (*modsTable)["displayURL"].as_string()) { homeurl = QString::fromStdString(homeurlDatumMods->get()); } // fix up url. if (!homeurl.isEmpty() && !homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) { homeurl.prepend("http://"); } details.homeurl = homeurl; QString issueTrackerURL = ""; if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) { issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); } else if (auto issueTrackerURLDatumMods = (*modsTable)["issueTrackerURL"].as_string()) { issueTrackerURL = QString::fromStdString(issueTrackerURLDatumMods->get()); } details.issue_tracker = issueTrackerURL; QString license = ""; if (auto licenseDatum = tomlData["license"].as_string()) { license = QString::fromStdString(licenseDatum->get()); } else if (auto licenseDatumMods = (*modsTable)["license"].as_string()) { license = QString::fromStdString(licenseDatumMods->get()); } if (!license.isEmpty()) details.licenses.append(ModLicense(license)); QString logoFile = ""; if (auto logoFileDatum = tomlData["logoFile"].as_string()) { logoFile = QString::fromStdString(logoFileDatum->get()); } else if (auto logoFileDatumMods = (*modsTable)["logoFile"].as_string()) { logoFile = QString::fromStdString(logoFileDatumMods->get()); } details.icon_file = logoFile; return details; } // https://fabricmc.net/wiki/documentation:fabric_mod_json ModDetails ReadFabricModInfo(QByteArray contents) { QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = jsonDoc.object(); auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; ModDetails details; details.mod_id = object.value("id").toString(); details.version = object.value("version").toString(); details.name = object.contains("name") ? object.value("name").toString() : details.mod_id; details.description = object.value("description").toString(); if (schemaVersion >= 1) { QJsonArray authors = object.value("authors").toArray(); for (auto author : authors) { if (author.isObject()) { details.authors.append(author.toObject().value("name").toString()); } else { details.authors.append(author.toString()); } } if (object.contains("contact")) { QJsonObject contact = object.value("contact").toObject(); if (contact.contains("homepage")) { details.homeurl = contact.value("homepage").toString(); } if (contact.contains("issues")) { details.issue_tracker = contact.value("issues").toString(); } } if (object.contains("license")) { auto license = object.value("license"); if (license.isArray()) { for (auto l : license.toArray()) { if (l.isString()) { details.licenses.append(ModLicense(l.toString())); } else if (l.isObject()) { auto obj = l.toObject(); details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), obj.value("description").toString())); } } } else if (license.isString()) { details.licenses.append(ModLicense(license.toString())); } else if (license.isObject()) { auto obj = license.toObject(); details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), obj.value("description").toString())); } } if (object.contains("icon")) { auto icon = object.value("icon"); if (icon.isObject()) { auto obj = icon.toObject(); // take the largest icon int largest = 0; for (auto key : obj.keys()) { auto size = key.split('x').first().toInt(); if (size > largest) { largest = size; } } if (largest > 0) { auto key = QString::number(largest) + "x" + QString::number(largest); details.icon_file = obj.value(key).toString(); } else { // parsing the sizes failed // take the first for (auto i : obj) { details.icon_file = i.toString(); break; } } } else if (icon.isString()) { details.icon_file = icon.toString(); } } } return details; } // https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md ModDetails ReadQuiltModInfo(QByteArray contents) { ModDetails details; try { QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); auto schemaVersion = object.value("schema_version").toInt(); // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md if (schemaVersion == 1) { auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); details.version = Json::requireString(modInfo.value("version"), "Mod version"); auto modMetadata = modInfo.value("metadata").toObject(); details.name = modMetadata.value("name").toString(details.mod_id); details.description = modMetadata.value("description").toString(); auto modContributors = modMetadata.value("contributors").toObject(); // We don't really care about the role of a contributor here details.authors += modContributors.keys(); auto modContact = modMetadata.value("contact").toObject(); if (modContact.contains("homepage")) { details.homeurl = Json::requireString(modContact.value("homepage")); } if (modContact.contains("issues")) { details.issue_tracker = Json::requireString(modContact.value("issues")); } if (modMetadata.contains("license")) { auto license = modMetadata.value("license"); if (license.isArray()) { for (auto l : license.toArray()) { if (l.isString()) { details.licenses.append(ModLicense(l.toString())); } else if (l.isObject()) { auto obj = l.toObject(); details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), obj.value("description").toString())); } } } else if (license.isString()) { details.licenses.append(ModLicense(license.toString())); } else if (license.isObject()) { auto obj = license.toObject(); details.licenses.append(ModLicense(obj.value("name").toString(), obj.value("id").toString(), obj.value("url").toString(), obj.value("description").toString())); } } if (modMetadata.contains("icon")) { auto icon = modMetadata.value("icon"); if (icon.isObject()) { auto obj = icon.toObject(); // take the largest icon int largest = 0; for (auto key : obj.keys()) { auto size = key.split('x').first().toInt(); if (size > largest) { largest = size; } } if (largest > 0) { auto key = QString::number(largest) + "x" + QString::number(largest); details.icon_file = obj.value(key).toString(); } else { // parsing the sizes failed // take the first for (auto i : obj) { details.icon_file = i.toString(); break; } } } else if (icon.isString()) { details.icon_file = icon.toString(); } } } } catch (const Exception& e) { qWarning() << "Unable to parse mod info:" << e.cause(); } return details; } ModDetails ReadForgeInfo(QByteArray contents) { ModDetails details; // Read the data details.name = "Minecraft Forge"; details.mod_id = "Forge"; details.homeurl = "http://www.minecraftforge.net/forum/"; INIFile ini; if (!ini.loadFile(contents)) return details; QString major = ini.get("forge.major.number", "0").toString(); QString minor = ini.get("forge.minor.number", "0").toString(); QString revision = ini.get("forge.revision.number", "0").toString(); QString build = ini.get("forge.build.number", "0").toString(); details.version = major + "." + minor + "." + revision + "." + build; return details; } ModDetails ReadLiteModInfo(QByteArray contents) { ModDetails details; QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); auto object = jsonDoc.object(); if (object.contains("name")) { details.mod_id = details.name = object.value("name").toString(); } if (object.contains("version")) { details.version = object.value("version").toString(""); } else { details.version = object.value("revision").toString(""); } details.mcversion = object.value("mcversion").toString(); auto author = object.value("author").toString(); if (!author.isEmpty()) { details.authors.append(author); } details.description = object.value("description").toString(); details.homeurl = object.value("url").toString(); return details; } // https://git.sleeping.town/unascribed/NilLoader/src/commit/d7fc87b255fc31019ff90f80d45894927fac6efc/src/main/java/nilloader/api/NilMetadata.java#L64 ModDetails ReadNilModInfo(QByteArray contents, QString fname) { ModDetails details; QDCSS cssData = QDCSS(contents); auto name = cssData.get("@nilmod.name"); auto desc = cssData.get("@nilmod.description"); auto authors = cssData.get("@nilmod.authors"); if (name->has_value()) { details.name = name->value(); } if (desc->has_value()) { details.description = desc->value(); } if (authors->has_value()) { details.authors.append(authors->value()); } details.version = cssData.get("@nilmod.version")->value_or("?"); details.mod_id = fname.remove(".nilmod.css"); return details; } bool process(Mod& mod, ProcessingLevel level) { switch (mod.type()) { case ResourceType::FOLDER: return processFolder(mod, level); case ResourceType::ZIPFILE: return processZIP(mod, level); case ResourceType::LITEMOD: return processLitemod(mod); default: qWarning() << "Invalid type for mod parse task!"; return false; } } bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); bool baseForgePopulated = false; bool isNilMod = false; bool isValid = false; QString manifestVersion = {}; QByteArray nilData = {}; QString nilFilePath = {}; if (!zip.parse([&details, &baseForgePopulated, &manifestVersion, &isValid, &nilData, &isNilMod, &nilFilePath]( MMCZip::ArchiveReader::File* file, bool& stop) { auto filePath = file->filename(); if (filePath == "META-INF/mods.toml" || filePath == "META-INF/neoforge.mods.toml") { details = ReadMCModTOML(file->readAll()); isValid = true; if (details.version == "${file.jarVersion}" && !manifestVersion.isEmpty()) { details.version = manifestVersion; } stop = details.version != "${file.jarVersion}"; baseForgePopulated = true; return true; } if (filePath == "META-INF/MANIFEST.MF") { // quick and dirty line-by-line parser auto manifestLines = QString(file->readAll()).split(s_newlineRegex); manifestVersion = ""; for (auto& line : manifestLines) { if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) { manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive); break; } } // some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF // also keep with forge's behavior of setting the version to "NONE" if none is found if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") { manifestVersion = "NONE"; } if (baseForgePopulated) { details.version = manifestVersion; stop = true; } return true; } if (filePath == "mcmod.info") { details = ReadMCModInfo(file->readAll()); isValid = true; stop = true; return true; } if (filePath == "quilt.mod.json") { details = ReadQuiltModInfo(file->readAll()); isValid = true; stop = true; return true; } if (filePath == "fabric.mod.json") { details = ReadFabricModInfo(file->readAll()); isValid = true; stop = true; return true; } if (filePath == "forgeversion.properties") { details = ReadForgeInfo(file->readAll()); isValid = true; stop = true; return true; } if (filePath == "META-INF/nil/mappings.json") { // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time isNilMod = true; stop = !nilFilePath.isEmpty(); file->skip(); return true; } // nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own meta file if (filePath.endsWith(".nilmod.css") && filePath != "nilloader.nilmod.css") { nilData = file->readAll(); nilFilePath = filePath; stop = isNilMod; return true; } file->skip(); return true; })) { return false; } if (isNilMod) { details = ReadNilModInfo(nilData, nilFilePath); isValid = true; } if (isValid) { mod.setDetails(details); return true; } return false; // no valid mod found in archive } bool processFolder(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; QFileInfo mcmod_info(FS::PathCombine(mod.fileinfo().filePath(), "mcmod.info")); if (mcmod_info.exists() && mcmod_info.isFile()) { QFile mcmod(mcmod_info.filePath()); if (!mcmod.open(QIODevice::ReadOnly)) return false; auto data = mcmod.readAll(); if (data.isEmpty() || data.isNull()) return false; details = ReadMCModInfo(data); mod.setDetails(details); return true; } return false; // no valid mcmod.info file found } bool processLitemod(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); if (auto file = zip.goToFile("litemod.json"); file) { details = ReadLiteModInfo(file->readAll()); mod.setDetails(details); return true; } return false; // no valid litemod.json found in archive } /** Checks whether a file is valid as a mod or not. */ bool validate(QFileInfo file) { Mod mod{ file }; return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { *pixmap = mod.setIcon(img); } else { qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); return false; } return true; } bool loadIconFile(const Mod& mod, QPixmap* pixmap) { if (mod.iconPath().isEmpty()) { qWarning() << "No Iconfile set, be sure to parse the mod first"; return false; } auto png_invalid = [&mod](const QString& reason) { qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon:" << reason; return false; }; switch (mod.type()) { case ResourceType::FOLDER: { QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath())); if (icon_info.exists() && icon_info.isFile()) { QFile icon(icon_info.filePath()); if (!icon.open(QIODevice::ReadOnly)) { return png_invalid("failed to open file " + icon_info.filePath()); } auto data = icon.readAll(); bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); icon.close(); if (!icon_result) { return png_invalid("invalid png image"); // icon invalid } return true; } return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); } case ResourceType::ZIPFILE: { MMCZip::ArchiveReader zip(mod.fileinfo().filePath()); auto file = zip.goToFile(mod.iconPath()); if (file) { auto data = file->readAll(); bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); if (!icon_result) { return png_invalid("invalid png image"); // icon png invalid } return true; } return png_invalid("Failed to set '" + mod.iconPath() + "' as current file in zip archive"); // could not set icon as current file. } case ResourceType::LITEMOD: { return png_invalid("litemods do not have icons"); // can lightmods even have icons? } default: return png_invalid("Invalid type for mod, can not load icon."); } } } // namespace ModUtils LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) : Task(false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) {} bool LocalModParseTask::abort() { m_aborted.store(true); return true; } void LocalModParseTask::executeTask() { Mod mod{ m_modFile }; ModUtils::process(mod, ModUtils::ProcessingLevel::Full); m_result->details = mod.details(); if (m_aborted) emitAborted(); else emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h0000644000175100017510000000372715144136756026662 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/TexturePack.h" #include "tasks/Task.h" namespace TexturePackUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processPackTXT(TexturePack& pack, QByteArray&& raw_data); bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data); /// processes ONLY the pack.png (rest of the pack may be invalid) bool processPackPNG(const TexturePack& pack); /** Checks whether a file is valid as a texture pack or not. */ bool validate(QFileInfo file); } // namespace TexturePackUtils class LocalTexturePackParseTask : public Task { Q_OBJECT public: LocalTexturePackParseTask(int token, TexturePack& rp); bool canAbort() const override { return true; } bool abort() override; void executeTask() override; int token() const { return m_token; } private: int m_token; TexturePack& m_texture_pack; bool m_aborted = false; }; PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalModParseTask.h0000644000175100017510000000303515144136756025132 0ustar runnerrunner#pragma once #include #include #include "minecraft/mod/Mod.h" #include "minecraft/mod/ModDetails.h" #include "tasks/Task.h" namespace ModUtils { ModDetails ReadFabricModInfo(QByteArray contents); ModDetails ReadQuiltModInfo(QByteArray contents); ModDetails ReadForgeInfo(QByteArray contents); ModDetails ReadLiteModInfo(QByteArray contents); enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); /** Checks whether a file is valid as a mod or not. */ bool validate(QFileInfo file); bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap); bool loadIconFile(const Mod& mod, QPixmap* pixmap); } // namespace ModUtils class LocalModParseTask : public Task { Q_OBJECT public: struct Result { ModDetails details; }; using ResultPtr = std::shared_ptr; ResultPtr result() const { return m_result; } bool canAbort() const override { return true; } bool abort() override; LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile); void executeTask() override; int token() const { return m_token; } private: int m_token; ResourceType m_type; QFileInfo m_modFile; ResultPtr m_result; std::atomic m_aborted = false; }; PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.h0000644000175100017510000000266215144136756026357 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "modplatform/ModIndex.h" #include "tasks/Task.h" class LocalResourceUpdateTask : public Task { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit LocalResourceUpdateTask(QDir index_dir, ModPlatform::IndexedPack& project, ModPlatform::IndexedVersion& version); auto canAbort() const -> bool override { return true; } auto abort() -> bool override; protected slots: //! Entry point for tasks. void executeTask() override; signals: void hasOldResource(QString name, QString filename); private: QDir m_index_dir; ModPlatform::IndexedPack m_project; ModPlatform::IndexedVersion m_version; }; PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp0000644000175100017510000002750015144136756026421 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalDataPackParseTask.h" #include "FileSystem.h" #include "Json.h" #include "archive/ArchiveReader.h" #include "minecraft/mod/ResourcePack.h" #include namespace DataPackUtils { bool process(DataPack* pack, ProcessingLevel level) { switch (pack->type()) { case ResourceType::FOLDER: return DataPackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: return DataPackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for data pack parse task!"; return false; } } bool processFolder(DataPack* pack, ProcessingLevel level) { Q_ASSERT(pack->type() == ResourceType::FOLDER); auto mcmeta_invalid = [&pack]() { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional }; QFileInfo mcmeta_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.mcmeta")); if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) return mcmeta_invalid(); // can't open mcmeta file auto data = mcmeta_file.readAll(); bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); mcmeta_file.close(); if (!mcmeta_result) { return mcmeta_invalid(); // mcmeta invalid } } else { return mcmeta_invalid(); // mcmeta file isn't a valid file } if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } auto png_invalid = [&pack]() { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; return true; // the png is optional }; QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { QFile pack_png_file(image_file_info.filePath()); if (!pack_png_file.open(QIODevice::ReadOnly)) return png_invalid(); // can't open pack.png file auto data = pack_png_file.readAll(); bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); pack_png_file.close(); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } else { return png_invalid(); // pack.png does not exists or is not a valid file. } return true; // all tests passed } bool processZIP(DataPack* pack, ProcessingLevel level) { Q_ASSERT(pack->type() == ResourceType::ZIPFILE); MMCZip::ArchiveReader zip(pack->fileinfo().filePath()); bool metaParsed = false; bool iconParsed = false; bool mcmeta_result = false; bool pack_png_result = false; if (!zip.parse( [&metaParsed, &iconParsed, &mcmeta_result, &pack_png_result, pack, level](MMCZip::ArchiveReader::File* f, bool& breakControl) { bool skip = true; if (!metaParsed && f->filename() == "pack.mcmeta") { metaParsed = true; skip = false; auto data = f->readAll(); mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); if (!mcmeta_result) { breakControl = true; return true; // mcmeta invalid } } if (!iconParsed && level != ProcessingLevel::BasicInfoOnly && f->filename() == "pack.png") { iconParsed = true; skip = false; auto data = f->readAll(); pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); if (!pack_png_result) { breakControl = true; return true; // pack.png invalid } } if (skip) { f->skip(); } if (metaParsed && (level == ProcessingLevel::BasicInfoOnly || iconParsed)) { breakControl = true; } return true; })) { return false; // can't open zip file } if (!mcmeta_result) { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; return false; // the mcmeta is not optional } if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } if (!pack_png_result) { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; return true; // the png is optional } return true; } // https://minecraft.wiki/w/Data_pack#pack.mcmeta // https://minecraft.wiki/w/Raw_JSON_text_format // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta bool processMCMeta(DataPack* pack, QByteArray&& raw_data) { QJsonParseError parse_error; auto json_doc = Json::parseUntilGarbage(raw_data, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Failed to parse JSON:" << parse_error.errorString(); return false; } try { auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); pack->setPackFormat(pack_obj["pack_format"].toInt()); pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); } catch (Json::JsonException& e) { qWarning() << "JsonException:" << e.what() << e.cause(); return false; } return true; } QString buildStyle(const QJsonObject& obj) { QStringList styles; if (auto color = obj["color"].toString(); !color.isEmpty()) { styles << QString("color: %1;").arg(color); } if (obj.contains("bold")) { QString weight = "normal"; if (obj["bold"].toBool()) { weight = "bold"; } styles << QString("font-weight: %1;").arg(weight); } if (obj.contains("italic")) { QString style = "normal"; if (obj["italic"].toBool()) { style = "italic"; } styles << QString("font-style: %1;").arg(style); } return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); } QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) { QString result; for (auto current : value) result += processComponent(current, strikethrough, underline); return result; } QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) { underline = obj["underlined"].toBool(underline); strikethrough = obj["strikethrough"].toBool(strikethrough); QString result = obj["text"].toString(); if (underline) { result = QString("%1").arg(result); } if (strikethrough) { result = QString("%1").arg(result); } // the extra needs to be a array result += processComponent(obj["extra"].toArray(), strikethrough, underline); if (auto style = buildStyle(obj); !style.isEmpty()) { result = QString("%2").arg(style, result); } if (obj.contains("clickEvent")) { auto click_event = obj["clickEvent"].toObject(); auto action = click_event["action"].toString(); auto value = click_event["value"].toString(); if (action == "open_url" && !value.isEmpty()) { result = QString("%2").arg(value, result); } } return result; } QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) { if (value.isString()) { return value.toString(); } if (value.isBool()) { return value.toBool() ? "true" : "false"; } if (value.isDouble()) { return QString::number(value.toDouble()); } if (value.isArray()) { return processComponent(value.toArray(), strikethrough, underline); } if (value.isObject()) { return processComponent(value.toObject(), strikethrough, underline); } qWarning() << "Invalid component type!"; return {}; } bool processPackPNG(const DataPack* pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { pack->setImage(img); } else { qWarning() << "Failed to parse pack.png."; return false; } return true; } bool processPackPNG(const DataPack* pack) { auto png_invalid = [&pack]() { qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; return false; }; switch (pack->type()) { case ResourceType::FOLDER: { QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { QFile pack_png_file(image_file_info.filePath()); if (!pack_png_file.open(QIODevice::ReadOnly)) return png_invalid(); // can't open pack.png file auto data = pack_png_file.readAll(); bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); pack_png_file.close(); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } else { return png_invalid(); // pack.png does not exists or is not a valid file. } return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 } case ResourceType::ZIPFILE: { MMCZip::ArchiveReader zip(pack->fileinfo().filePath()); auto f = zip.goToFile("pack.png"); if (!f) { return png_invalid(); } auto data = f->readAll(); bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); if (!pack_png_result) { return png_invalid(); // pack.png invalid } return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 } default: qWarning() << "Invalid type for data pack parse task!"; return false; } } bool validate(QFileInfo file) { DataPack dp{ file }; return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); } bool validateResourcePack(QFileInfo file) { ResourcePack rp{ file }; return DataPackUtils::process(&rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); } } // namespace DataPackUtils LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) {} void LocalDataPackParseTask::executeTask() { if (!DataPackUtils::process(m_data_pack)) { emitFailed("process failed"); return; } emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h0000644000175100017510000000410715144136756026064 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/DataPack.h" #include "tasks/Task.h" namespace DataPackUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); bool processMCMeta(DataPack* pack, QByteArray&& raw_data); QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); bool processPackPNG(const DataPack* pack, QByteArray&& raw_data); /// processes ONLY the pack.png (rest of the pack may be invalid) bool processPackPNG(const DataPack* pack); /** Checks whether a file is valid as a data pack or not. */ bool validate(QFileInfo file); /** Checks whether a file is valid as a resource pack or not. */ bool validateResourcePack(QFileInfo file); } // namespace DataPackUtils class LocalDataPackParseTask : public Task { Q_OBJECT public: LocalDataPackParseTask(int token, DataPack* dp); void executeTask() override; int token() const { return m_token; } private: int m_token; DataPack* m_data_pack; }; PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h0000644000175100017510000000341115144136756026416 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/ShaderPack.h" #include "tasks/Task.h" namespace ShaderPackUtils { enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processZIP(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); /** Checks whether a file is valid as a shader pack or not. */ bool validate(QFileInfo file); } // namespace ShaderPackUtils class LocalShaderPackParseTask : public Task { Q_OBJECT public: LocalShaderPackParseTask(int token, ShaderPack& sp); bool canAbort() const override { return true; } bool abort() override; void executeTask() override; int token() const { return m_token; } private: int m_token; ShaderPack& m_shader_pack; bool m_aborted = false; }; PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp0000644000175100017510000000756715144136756026771 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalShaderPackParseTask.h" #include "FileSystem.h" #include "archive/ArchiveReader.h" namespace ShaderPackUtils { bool process(ShaderPack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: return ShaderPackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: return ShaderPackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for shader pack parse task!"; return false; } } bool processFolder(ShaderPack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); QFileInfo shaders_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "shaders")); if (!shaders_dir_info.exists() || !shaders_dir_info.isDir()) { return false; // assets dir does not exists or isn't valid } pack.setPackFormat(ShaderPackFormat::VALID); if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } return true; // all tests passed } bool processZIP(ShaderPack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); if (!zip.collectFiles(false)) return false; // can't open zip file if (!zip.exists("/shaders")) { // assets dir does not exists at zip root, but shader packs // will sometimes be a zip file containing a folder with the // actual contents in it. This happens // e.g. when the shader pack is downloaded as code // from Github. so other than "/shaders", we // could also check for a "shaders" folder one level deep. QStringList files = zip.getFiles(); // the assumption here is that there is just one // folder with the "shader" subfolder. In case // there are multiple, the first one is picked. bool isShaderPresent = false; for (QString f : files) { if (f.contains("/shaders/", Qt::CaseInsensitive)) { isShaderPresent = true; break; } } if (!isShaderPresent) // assets dir does not exist. return false; } pack.setPackFormat(ShaderPackFormat::VALID); if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } return true; } bool validate(QFileInfo file) { ShaderPack sp{ file }; return ShaderPackUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); } } // namespace ShaderPackUtils LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(false), m_token(token), m_shader_pack(sp) {} bool LocalShaderPackParseTask::abort() { m_aborted = true; return true; } void LocalShaderPackParseTask::executeTask() { if (!ShaderPackUtils::process(m_shader_pack)) { emitFailed("this is not a shader pack"); return; } if (m_aborted) emitAborted(); else emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h0000644000175100017510000000535415144136756026176 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include "minecraft/mod/Mod.h" #include "tasks/Task.h" class ResourceFolderLoadTask : public Task { Q_OBJECT public: struct Result { QMap resources; }; using ResultPtr = std::shared_ptr; ResultPtr result() const { return m_result; } public: ResourceFolderLoadTask(const QDir& resource_dir, const QDir& index_dir, bool is_indexed, bool clean_orphan, std::function create_function); bool canAbort() const override { return true; } bool abort() override { m_aborted.store(true); return true; } void executeTask() override; private: void getFromMetadata(); private: QDir m_resource_dir, m_index_dir; bool m_is_indexed; bool m_clean_orphan; std::function m_create_func; ResultPtr m_result; std::atomic m_aborted = false; /** This is the thread in which we should put new mod objects */ QThread* m_thread_to_spawn_into; }; PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp0000644000175100017510000001453515144136756027214 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalTexturePackParseTask.h" #include "FileSystem.h" #include "archive/ArchiveReader.h" #include namespace TexturePackUtils { bool process(TexturePack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: return TexturePackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: return TexturePackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for resource pack parse task!"; return false; } } bool processFolder(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.txt")); if (mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) return false; auto data = mcmeta_file.readAll(); bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); mcmeta_file.close(); if (!packTXT_result) { return false; } } else { return false; } if (level == ProcessingLevel::BasicInfoOnly) return true; QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.isFile()) { QFile mcmeta_file(image_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) return false; auto data = mcmeta_file.readAll(); bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); mcmeta_file.close(); if (!packPNG_result) { return false; } } else { return false; } return true; } bool processZIP(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); bool packProcessed = false; bool iconProcessed = false; return zip.parse([&packProcessed, &iconProcessed, &pack, level](MMCZip::ArchiveReader::File* file, bool& stop) { if (!packProcessed && file->filename() == "pack.txt") { packProcessed = true; auto data = file->readAll(); stop = packProcessed && (iconProcessed || level == ProcessingLevel::BasicInfoOnly); return TexturePackUtils::processPackTXT(pack, std::move(data)); } if (!iconProcessed && file->filename() == "pack.png") { iconProcessed = true; auto data = file->readAll(); stop = packProcessed && iconProcessed; return TexturePackUtils::processPackPNG(pack, std::move(data)); } file->skip(); return true; }); } bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) { pack.setDescription(QString(raw_data)); return true; } bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { pack.setImage(img); } else { qWarning() << "Failed to parse pack.png."; return false; } return true; } bool processPackPNG(const TexturePack& pack) { auto png_invalid = [&pack]() { qWarning() << "Texture pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; return false; }; switch (pack.type()) { case ResourceType::FOLDER: { QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.exists() && image_file_info.isFile()) { QFile pack_png_file(image_file_info.filePath()); if (!pack_png_file.open(QIODevice::ReadOnly)) return png_invalid(); // can't open pack.png file auto data = pack_png_file.readAll(); bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); pack_png_file.close(); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } else { return png_invalid(); // pack.png does not exists or is not a valid file. } return false; } case ResourceType::ZIPFILE: { MMCZip::ArchiveReader zip(pack.fileinfo().filePath()); auto file = zip.goToFile("pack.png"); if (file) { auto data = file->readAll(); bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); if (!pack_png_result) { return png_invalid(); // pack.png invalid } } return png_invalid(); // could not set pack.mcmeta as current file. } default: qWarning() << "Invalid type for resource pack parse task!"; return false; } } bool validate(QFileInfo file) { TexturePack rp{ file }; return TexturePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); } } // namespace TexturePackUtils LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) : Task(false), m_token(token), m_texture_pack(rp) {} bool LocalTexturePackParseTask::abort() { m_aborted = true; return true; } void LocalTexturePackParseTask::executeTask() { if (!TexturePackUtils::process(m_texture_pack)) { emitFailed("this is not a texture pack"); return; } if (m_aborted) emitAborted(); else emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp0000644000175100017510000000504715144136756026712 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalResourceUpdateTask.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" #ifdef Q_OS_WIN32 #include #endif LocalResourceUpdateTask::LocalResourceUpdateTask(QDir index_dir, ModPlatform::IndexedPack& project, ModPlatform::IndexedVersion& version) : m_index_dir(index_dir), m_project(project), m_version(version) { // Ensure a '.index' folder exists in the mods folder, and create it if it does not if (!FS::ensureFolderPathExists(index_dir.path())) { emitFailed(QString("Unable to create index directory at %1!").arg(index_dir.absolutePath())); return; } #ifdef Q_OS_WIN32 std::wstring wpath = index_dir.path().toStdWString(); if (index_dir.dirName().startsWith('.')) { SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); } else { // fix shaderpacks folder being hidden by Prism Launcher 10.0.1 SetFileAttributesW(wpath.c_str(), FILE_ATTRIBUTE_NORMAL); } #endif } void LocalResourceUpdateTask::executeTask() { setStatus(tr("Updating index for resource:\n%1").arg(m_project.name)); auto old_metadata = Metadata::get(m_index_dir, m_project.addonId); if (old_metadata.isValid()) { emit hasOldResource(old_metadata.name, old_metadata.filename); if (m_project.slug.isEmpty()) m_project.slug = old_metadata.slug; } auto pw_mod = Metadata::create(m_index_dir, m_project, m_version); if (pw_mod.isValid()) { Metadata::update(m_index_dir, pw_mod); emitSucceeded(); } else { qCritical() << "Tried to update an invalid resource!"; emitFailed(tr("Invalid metadata")); } } auto LocalResourceUpdateTask::abort() -> bool { emitAborted(); return true; } PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalResourceParse.h0000644000175100017510000000200715144136756025355 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include "modplatform/ResourceType.h" namespace ResourceUtils { ModPlatform::ResourceType identify(QFileInfo file); } // namespace ResourceUtils PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp0000644000175100017510000001314415144136756026525 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ResourceFolderLoadTask.h" #include "Application.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" #include ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resource_dir, const QDir& index_dir, bool is_indexed, bool clean_orphan, std::function create_function) : Task(false) , m_resource_dir(resource_dir) , m_index_dir(index_dir) , m_is_indexed(is_indexed) , m_clean_orphan(clean_orphan) , m_create_func(create_function) , m_result(new Result()) , m_thread_to_spawn_into(thread()) {} void ResourceFolderLoadTask::executeTask() { if (thread() != m_thread_to_spawn_into) connect(this, &Task::finished, this->thread(), &QThread::quit); if (m_is_indexed) { // Read metadata first getFromMetadata(); } // Read JAR files that don't have metadata m_resource_dir.refresh(); for (auto entry : m_resource_dir.entryInfoList()) { auto filePath = entry.absoluteFilePath(); if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { continue; } auto newFilePath = FS::getUniqueResourceName(filePath); if (newFilePath != filePath) { FS::move(filePath, newFilePath); entry = QFileInfo(newFilePath); } Resource* resource = m_create_func(entry); if (resource->enabled()) { if (m_result->resources.contains(resource->internal_id())) { m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); // Delete the object we just created, since a valid one is already in the mods list. delete resource; } else { m_result->resources[resource->internal_id()].reset(resource); m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); } } else { QString chopped_id = resource->internal_id().chopped(9); if (m_result->resources.contains(chopped_id)) { m_result->resources[resource->internal_id()].reset(resource); auto metadata = m_result->resources[chopped_id]->metadata(); if (metadata) { resource->setMetadata(*metadata); m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); m_result->resources.remove(chopped_id); } } else { m_result->resources[resource->internal_id()].reset(resource); m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); } } } // Remove orphan metadata to prevent issues // See https://github.com/PolyMC/PolyMC/issues/996 if (m_clean_orphan) { QMutableMapIterator iter(m_result->resources); while (iter.hasNext()) { auto resource = iter.next().value(); if (resource->status() == ResourceStatus::NOT_INSTALLED) { resource->destroy(m_index_dir, false, false); iter.remove(); } } } for (auto mod : m_result->resources) mod->moveToThread(m_thread_to_spawn_into); if (m_aborted) emit finished(); else emitSucceeded(); } void ResourceFolderLoadTask::getFromMetadata() { m_index_dir.refresh(); for (auto entry : m_index_dir.entryList(QDir::Files)) { if (!entry.endsWith(".pw.toml")) { continue; } auto metadata = Metadata::get(m_index_dir, entry); if (!metadata.isValid()) continue; auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename))); resource->setMetadata(metadata); resource->setStatus(ResourceStatus::NOT_INSTALLED); m_result->resources[resource->internal_id()].reset(resource); } } PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalResourceParse.cpp0000644000175100017510000000505515144136756025716 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "LocalResourceParse.h" #include "LocalDataPackParseTask.h" #include "LocalModParseTask.h" #include "LocalShaderPackParseTask.h" #include "LocalTexturePackParseTask.h" #include "LocalWorldSaveParseTask.h" #include "modplatform/ResourceType.h" namespace ResourceUtils { ModPlatform::ResourceType identify(QFileInfo file) { if (file.exists() && file.isFile()) { if (ModUtils::validate(file)) { // mods can contain resource and data packs so they must be tested first qDebug() << file.fileName() << "is a mod"; return ModPlatform::ResourceType::Mod; } else if (DataPackUtils::validateResourcePack(file)) { qDebug() << file.fileName() << "is a resource pack"; return ModPlatform::ResourceType::ResourcePack; } else if (TexturePackUtils::validate(file)) { qDebug() << file.fileName() << "is a pre 1.6 texture pack"; return ModPlatform::ResourceType::TexturePack; } else if (DataPackUtils::validate(file)) { qDebug() << file.fileName() << "is a data pack"; return ModPlatform::ResourceType::DataPack; } else if (WorldSaveUtils::validate(file)) { qDebug() << file.fileName() << "is a world save"; return ModPlatform::ResourceType::World; } else if (ShaderPackUtils::validate(file)) { qDebug() << file.fileName() << "is a shader pack"; return ModPlatform::ResourceType::ShaderPack; } else { qDebug() << "Can't Identify" << file.fileName(); } } else { qDebug() << "Can't find" << file.absolutePath(); } return ModPlatform::ResourceType::Unknown; } } // namespace ResourceUtils PrismLauncher-10.0.5/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp0000644000175100017510000001335015144136756026655 0ustar runnerrunner // SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "LocalWorldSaveParseTask.h" #include "FileSystem.h" #include "archive/ArchiveReader.h" #include #include #include namespace WorldSaveUtils { bool process(WorldSave& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: return WorldSaveUtils::processFolder(pack, level); case ResourceType::ZIPFILE: return WorldSaveUtils::processZIP(pack, level); default: qWarning() << "Invalid type for world save parse task!"; return false; } } /// @brief checks a folder structure to see if it contains a level.dat /// @param dir the path to check /// @param saves used in recursive call if a "saves" dir was found /// @return std::tuple of ( /// bool , /// QString , /// bool /// ) static std::tuple contains_level_dat(QDir dir, bool saves = false) { for (auto const& entry : dir.entryInfoList()) { if (!entry.isDir()) { continue; } if (!saves && entry.fileName() == "saves") { return contains_level_dat(QDir(entry.filePath()), true); } QFileInfo level_dat(FS::PathCombine(entry.filePath(), "level.dat")); if (level_dat.exists() && level_dat.isFile()) { return std::make_tuple(true, entry.fileName(), saves); } } return std::make_tuple(false, "", saves); } bool processFolder(WorldSave& save, ProcessingLevel level) { Q_ASSERT(save.type() == ResourceType::FOLDER); auto [found, save_dir_name, found_saves_dir] = contains_level_dat(QDir(save.fileinfo().filePath())); if (!found) { return false; } save.setSaveDirName(save_dir_name); if (found_saves_dir) { save.setSaveFormat(WorldSaveFormat::MULTI); } else { save.setSaveFormat(WorldSaveFormat::SINGLE); } if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } // reserved for more intensive processing return true; // all tests passed } /// @brief checks a folder structure to see if it contains a level.dat /// @param zip the zip file to check /// @return std::tuple of ( /// bool , /// QString , /// bool /// ) static std::tuple contains_level_dat(QString fileName) { MMCZip::ArchiveReader zip(fileName); if (!zip.collectFiles()) { return std::make_tuple(false, "", false); } bool saves = false; if (zip.exists("/saves")) { saves = true; } for (auto file : zip.getFiles()) { QString relativePath = file; if (saves) { if (!relativePath.startsWith("saves/", Qt::CaseInsensitive)) continue; relativePath = relativePath.mid(QString("saves/").length()); } if (!relativePath.endsWith("/level.dat", Qt::CaseInsensitive)) continue; int slashIndex = relativePath.indexOf('/'); if (slashIndex == -1) continue; // malformed: no slash between saves/ and level.dat QString worldName = relativePath.left(slashIndex); QString remaining = relativePath.mid(slashIndex + 1); // Check that there's nothing between worldName/ and level.dat if (remaining == "level.dat") { return std::make_tuple(true, worldName, saves); } } return std::make_tuple(false, "", saves); } bool processZIP(WorldSave& save, ProcessingLevel level) { Q_ASSERT(save.type() == ResourceType::ZIPFILE); auto [found, save_dir_name, found_saves_dir] = contains_level_dat(save.fileinfo().filePath()); if (!found) { return false; } if (save_dir_name.endsWith("/")) { save_dir_name.chop(1); } save.setSaveDirName(save_dir_name); if (found_saves_dir) { save.setSaveFormat(WorldSaveFormat::MULTI); } else { save.setSaveFormat(WorldSaveFormat::SINGLE); } if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } // reserved for more intensive processing return true; } bool validate(QFileInfo file) { WorldSave sp{ file }; return WorldSaveUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); } } // namespace WorldSaveUtils LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(false), m_token(token), m_save(save) {} bool LocalWorldSaveParseTask::abort() { m_aborted = true; return true; } void LocalWorldSaveParseTask::executeTask() { if (!WorldSaveUtils::process(m_save)) { emitFailed("this is not a world"); return; } if (m_aborted) emitAborted(); else emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/mod/ShaderPackFolderModel.cpp0000644000175100017510000000303415144136756025151 0ustar runnerrunner#include "ShaderPackFolderModel.h" #include "FileSystem.h" namespace { class ShaderPackIndexMigrateTask : public Task { Q_OBJECT public: ShaderPackIndexMigrateTask(QDir resourceDir, QDir indexDir) : m_resourceDir(std::move(resourceDir)), m_indexDir(std::move(indexDir)) {} void executeTask() override { if (!m_indexDir.exists()) { qDebug() << m_indexDir.absolutePath() << "does not exist; nothing to migrate"; emitSucceeded(); return; } QStringList pwFiles = m_indexDir.entryList({ "*.pw.toml" }, QDir::Files); bool movedAll = true; for (const auto& file : pwFiles) { QString src = m_indexDir.filePath(file); QString dest = m_resourceDir.filePath(file); if (FS::move(src, dest)) { qDebug() << "Moved" << src << "to" << dest; } else { movedAll = false; } } if (!movedAll) { // FIXME: not shown in the UI emitFailed(tr("Failed to migrate shaderpack metadata from .index")); return; } if (!FS::deletePath(m_indexDir.absolutePath())) { emitFailed(tr("Failed to remove old .index dir")); return; } emitSucceeded(); } private: QDir m_resourceDir, m_indexDir; }; } // namespace Task* ShaderPackFolderModel::createPreUpdateTask() { return new ShaderPackIndexMigrateTask(m_dir, ResourceFolderModel::indexDir()); } #include "ShaderPackFolderModel.moc" PrismLauncher-10.0.5/launcher/minecraft/mod/DataPackFolderModel.cpp0000644000175100017510000001644015144136756024621 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "DataPackFolderModel.h" #include #include #include "Version.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h" DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true }; } QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: return m_resources[row]->name(); case PackFormatColumn: { auto& resource = at(row); auto pack_format = resource.packFormat(); if (pack_format == 0) return tr("Unrecognized"); auto version_bounds = resource.compatibleVersions(); if (version_bounds.first.toString().isEmpty()) return QString::number(pack_format); return QString("%1 (%2 - %3)") .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); } case DateColumn: return m_resources[row]->dateTimeChanged(); default: return {}; } case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); } if (column == NameColumn) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") .arg(at(row).fileinfo().canonicalFilePath()); ; } if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } return m_resources[row]->internal_id(); } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } return {}; case Qt::CheckStateRole: if (column == ActiveColumn) return at(row).enabled() ? Qt::Checked : Qt::Unchecked; else return {}; default: return {}; } } QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case PackFormatColumn: case DateColumn: case ImageColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: switch (section) { case ActiveColumn: return tr("Is the data pack enabled? (Only valid for ZIPs)"); case NameColumn: return tr("The name of the data pack."); case PackFormatColumn: //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); case DateColumn: return tr("The date and time this data pack was last changed (or added)."); default: return {}; } case Qt::SizeHintRole: if (section == ImageColumn) { return QSize(64, 0); } return {}; default: return {}; } } int DataPackFolderModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } Resource* DataPackFolderModel::createResource(const QFileInfo& file) { return new DataPack(file); } Task* DataPackFolderModel::createParseTask(Resource& resource) { return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast(&resource)); } PrismLauncher-10.0.5/launcher/minecraft/mod/ResourcePackFolderModel.cpp0000644000175100017510000001741515144136756025542 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ResourcePackFolderModel.h" #include #include #include "Version.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h" ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true, true }; } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const { if (!validateIndex(index)) return {}; int row = index.row(); int column = index.column(); switch (role) { case Qt::DisplayRole: switch (column) { case NameColumn: return m_resources[row]->name(); case PackFormatColumn: { auto& resource = at(row); auto pack_format = resource.packFormat(); if (pack_format == 0) return tr("Unrecognized"); auto version_bounds = resource.compatibleVersions(); if (version_bounds.first.toString().isEmpty()) return QString::number(pack_format); return QString("%1 (%2 - %3)") .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); } case DateColumn: return m_resources[row]->dateTimeChanged(); case ProviderColumn: return m_resources[row]->provider(); case SizeColumn: return m_resources[row]->sizeStr(); default: return {}; } case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } return {}; } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } if (column == NameColumn) { if (at(row).isSymLinkUnder(instDirPath())) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." "\nCanonical Path: %1") .arg(at(row).fileinfo().canonicalFilePath()); ; } if (at(row).isMoreThanOneHardLink()) { return m_resources[row]->internal_id() + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); } } return m_resources[row]->internal_id(); } case Qt::SizeHintRole: if (column == ImageColumn) { return QSize(32, 32); } return {}; case Qt::CheckStateRole: if (column == ActiveColumn) return at(row).enabled() ? Qt::Checked : Qt::Unchecked; return {}; default: return {}; } } QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: switch (section) { case ActiveColumn: case NameColumn: case PackFormatColumn: case DateColumn: case ImageColumn: case ProviderColumn: case SizeColumn: return columnNames().at(section); default: return {}; } case Qt::ToolTipRole: switch (section) { case ActiveColumn: return tr("Is the resource pack enabled?"); case NameColumn: return tr("The name of the resource pack."); case PackFormatColumn: //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); case DateColumn: return tr("The date and time this resource pack was last changed (or added)."); case ProviderColumn: return tr("The source provider of the resource pack."); case SizeColumn: return tr("The size of the resource pack."); default: return {}; } case Qt::SizeHintRole: if (section == ImageColumn) { return QSize(64, 0); } return {}; default: return {}; } } int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } Task* ResourcePackFolderModel::createParseTask(Resource& resource) { return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast(&resource)); } PrismLauncher-10.0.5/launcher/minecraft/ShortcutUtils.cpp0000644000175100017510000002275115144136756023113 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2025 Yihe Li * * parent program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * parent program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with parent program. If not, see . * * parent file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use parent file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ShortcutUtils.h" #include "FileSystem.h" #include #include #include #include #include namespace ShortcutUtils { bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) { if (!shortcut.instance) return false; QString appPath = QApplication::applicationFilePath(); auto icon = APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey); if (icon == nullptr) { icon = APPLICATION->icons()->icon("grass"); } QString iconPath; QStringList args; #if defined(Q_OS_MACOS) if (appPath.startsWith("/private/var/")) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); return false; } iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); return false; } QIcon iconObj = icon->icon(); bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS"); iconFile.close(); if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); return false; } #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (appPath.startsWith("/tmp/.mount_")) { // AppImage! appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); if (appPath.isEmpty()) { QMessageBox::critical( shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); } else if (appPath.endsWith("/")) { appPath.chop(1); } } iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png"); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); iconFile.close(); if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return false; } if (DesktopServices::isFlatpak()) { appPath = "flatpak"; args.append({ "run", BuildConfig.LAUNCHER_APPID }); } #elif defined(Q_OS_WIN) iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico"); // part of fix for weird bug involving the window icon being replaced // dunno why it happens, but parent 2-line fix seems to be enough, so w/e auto appIcon = APPLICATION->logo(); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); iconFile.close(); // restore original window icon QGuiApplication::setWindowIcon(appIcon); if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return false; } #else QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!")); return false; #endif args.append({ "--launch", shortcut.instance->id() }); args.append(shortcut.extraArgs); QString shortcutPath = FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath); if (shortcutPath.isEmpty()) { #if not defined(Q_OS_MACOS) iconFile.remove(); #endif QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString)); return false; } shortcut.instance->registerShortcut({ shortcut.name, shortcutPath, shortcut.target }); return true; } bool createInstanceShortcutOnDesktop(const Shortcut& shortcut) { if (!shortcut.instance) return false; QString desktopDir = FS::getDesktopDir(); if (desktopDir.isEmpty()) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!")); return false; } QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name)); if (!createInstanceShortcut(shortcut, shortcutFilePath)) return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString)); return true; } bool createInstanceShortcutInApplications(const Shortcut& shortcut) { if (!shortcut.instance) return false; QString applicationsDir = FS::getApplicationsDir(); if (applicationsDir.isEmpty()) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!")); return false; } #if defined(Q_OS_MACOS) || defined(Q_OS_WIN) applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); QDir applicationsDirQ(applicationsDir); if (!applicationsDirQ.mkpath(".")) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create instances folder in applications folder!")); return false; } #endif QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name)); if (!createInstanceShortcut(shortcut, shortcutFilePath)) return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString)); return true; } bool createInstanceShortcutInOther(const Shortcut& shortcut) { if (!shortcut.instance) return false; QString defaultedDir = FS::getDesktopDir(); #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) QString extension = ".desktop"; #elif defined(Q_OS_WINDOWS) QString extension = ".lnk"; #else QString extension = ""; #endif QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension); QFileDialog fileDialog; // workaround to make sure the portal file dialog opens in the desktop directory fileDialog.setDirectoryUrl(defaultedDir); shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, QObject::tr("Create Shortcut"), shortcutFilePath, QObject::tr("Desktop Entries") + " (*" + extension + ")"); if (shortcutFilePath.isEmpty()) return false; // file dialog canceled by user if (shortcutFilePath.endsWith(extension)) shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); if (!createInstanceShortcut(shortcut, shortcutFilePath)) return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString)); return true; } } // namespace ShortcutUtils PrismLauncher-10.0.5/launcher/minecraft/AssetsUtils.cpp0000644000175100017510000002363315144136756022542 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include #include #include "AssetsUtils.h" #include "BuildConfig.h" #include "FileSystem.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" #include "net/Download.h" #include "Application.h" #include "net/NetRequest.h" namespace { QSet collectPathsFromDir(QString dirPath) { QFileInfo dirInfo(dirPath); if (!dirInfo.exists()) { return {}; } QSet out; QDirIterator iter(dirPath, QDirIterator::Subdirectories); while (iter.hasNext()) { QString value = iter.next(); QFileInfo info(value); if (info.isFile()) { out.insert(value); qDebug() << value; } } return out; } } // namespace namespace AssetsUtils { /* * Returns true on success, with index populated * index is undefined otherwise */ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsIndex& index) { /* { "objects": { "icons/icon_16x16.png": { "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", "size": 3665 }, ... } } } */ QFile file(path); // Try to open the file and fail if we can't. // TODO: We should probably report this error to the user. if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to read assets index file" << path; return false; } index.id = assetsId; // Read the file and close it. QByteArray jsonData = file.readAll(); file.close(); QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); // Fail if the JSON is invalid. if (parseError.error != QJsonParseError::NoError) { qCritical() << "Failed to parse assets index file:" << parseError.errorString() << "at offset " << QString::number(parseError.offset); return false; } // Make sure the root is an object. if (!jsonDoc.isObject()) { qCritical() << "Invalid assets index JSON: Root should be an array."; return false; } QJsonObject root = jsonDoc.object(); QJsonValue isVirtual = root.value("virtual"); if (!isVirtual.isUndefined()) { index.isVirtual = isVirtual.toBool(false); } QJsonValue mapToResources = root.value("map_to_resources"); if (!mapToResources.isUndefined()) { index.mapToResources = mapToResources.toBool(false); } QJsonValue objects = root.value("objects"); QVariantMap map = objects.toVariant().toMap(); for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) { // qDebug() << iter.key(); QVariant variant = iter.value(); QVariantMap nested_objects = variant.toMap(); AssetObject object; for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); nested_iter != nested_objects.end(); ++nested_iter) { // qDebug() << nested_iter.key() << nested_iter.value().toString(); QString key = nested_iter.key(); QVariant value = nested_iter.value(); if (key == "hash") { object.hash = value.toString(); } else if (key == "size") { object.size = value.toLongLong(); } } index.objects.insert(iter.key(), object); } return true; } // FIXME: ugly code duplication QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder) { QDir assetsDir = QDir("assets/"); QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); QFile indexFile(indexPath); QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); if (!indexFile.exists()) { qCritical() << "No assets index file" << indexPath << "; can't determine assets path!"; return virtualRoot; } AssetsIndex index; if (!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) { qCritical() << "Failed to load asset index file" << indexPath << "; can't determine assets path!"; return virtualRoot; } QString targetPath; if (index.isVirtual) { return virtualRoot; } else if (index.mapToResources) { return QDir(resourcesFolder); } return virtualRoot; } // FIXME: ugly code duplication bool reconstructAssets(QString assetsId, QString resourcesFolder) { QDir assetsDir = QDir("assets/"); QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); QFile indexFile(indexPath); QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); if (!indexFile.exists()) { qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets!"; return false; } qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() << objectDir.path() << virtualDir.path() << virtualRoot.path(); AssetsIndex index; if (!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) { qCritical() << "Failed to load asset index file" << indexPath << "; can't reconstruct assets!"; return false; } QString targetPath; bool removeLeftovers = false; if (index.isVirtual) { targetPath = virtualRoot.path(); removeLeftovers = true; qDebug() << "Reconstructing virtual assets folder at" << targetPath; } else if (index.mapToResources) { targetPath = resourcesFolder; qDebug() << "Reconstructing resources folder at" << targetPath; } if (!targetPath.isNull()) { auto presentFiles = collectPathsFromDir(targetPath); for (QString map : index.objects.keys()) { AssetObject asset_object = index.objects.value(map); QString target_path = FS::PathCombine(targetPath, map); QFile target(target_path); QString tlk = asset_object.hash.left(2); QString original_path = FS::PathCombine(objectDir.path(), tlk, asset_object.hash); QFile original(original_path); if (!original.exists()) continue; presentFiles.remove(target_path); if (!target.exists()) { QFileInfo info(target_path); QDir target_dir = info.dir(); qDebug() << target_dir.path(); FS::ensureFolderPathExists(target_dir.path()); bool couldCopy = original.copy(target_path); qDebug() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy); } } // TODO: Write last used time to virtualRoot/.lastused if (removeLeftovers) { for (auto& file : presentFiles) { qDebug() << "Would remove" << file; } } } return true; } } // namespace AssetsUtils Net::NetRequest::Ptr AssetObject::getDownloadAction() { QFileInfo objectFile(getLocalPath()); if ((!objectFile.isFile()) || (objectFile.size() != size)) { auto objectDL = Net::ApiDownload::makeFile(getUrl(), objectFile.filePath()); if (hash.size()) { objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, hash)); } objectDL->setProgress(objectDL->getProgress(), size); return objectDL; } return nullptr; } QString AssetObject::getLocalPath() { return "assets/objects/" + getRelPath(); } QUrl AssetObject::getUrl() { auto resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); return resourceURL + getRelPath(); } QString AssetObject::getRelPath() { return hash.left(2) + "/" + hash; } NetJob::Ptr AssetsIndex::getDownloadJob() { auto job = makeShared(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); for (auto& object : objects.values()) { auto dl = object.getDownloadAction(); if (dl) { job->addNetAction(dl); } } if (job->size()) return job; return nullptr; } PrismLauncher-10.0.5/launcher/minecraft/ShortcutUtils.h0000644000175100017510000000472015144136756022554 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * Copyright (C) 2025 Yihe Li * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "Application.h" #include #include namespace ShortcutUtils { /// A struct to hold parameters for creating a shortcut struct Shortcut { BaseInstance* instance; QString name; QString targetString; QWidget* parent = nullptr; QStringList extraArgs = {}; QString iconKey = ""; ShortcutTarget target; }; /// Create an instance shortcut on the specified file path bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath); /// Create an instance shortcut on the desktop bool createInstanceShortcutOnDesktop(const Shortcut& shortcut); /// Create an instance shortcut in the Applications directory bool createInstanceShortcutInApplications(const Shortcut& shortcut); /// Create an instance shortcut in other directories bool createInstanceShortcutInOther(const Shortcut& shortcut); } // namespace ShortcutUtils PrismLauncher-10.0.5/launcher/minecraft/Logging.h0000644000175100017510000000164215144136756021306 0ustar runnerrunner // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include Q_DECLARE_LOGGING_CATEGORY(instanceProfileC) Q_DECLARE_LOGGING_CATEGORY(instanceProfileResolveC) PrismLauncher-10.0.5/launcher/minecraft/launch/0000755000175100017510000000000015144136756021016 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/minecraft/launch/ClaimAccount.cpp0000644000175100017510000000115715144136756024070 0ustar runnerrunner#include "ClaimAccount.h" #include #include "Application.h" #include "minecraft/auth/AccountList.h" ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session) : LaunchStep(parent) { if (session->status == AuthSession::Status::PlayableOnline && !session->demo) { auto accounts = APPLICATION->accounts(); m_account = accounts->getAccountByProfileName(session->player_name); } } void ClaimAccount::executeTask() { if (m_account) { lock.reset(new UseLock(m_account)); emitSucceeded(); } } void ClaimAccount::finalize() { lock.reset(); } PrismLauncher-10.0.5/launcher/minecraft/launch/ExtractNatives.cpp0000644000175100017510000000545115144136756024473 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ExtractNatives.h" #include #include #include #include "FileSystem.h" #include "archive/ArchiveReader.h" #include "archive/ArchiveWriter.h" #ifdef major #undef major #endif #ifdef minor #undef minor #endif static QString replaceSuffix(QString target, const QString& suffix, const QString& replacement) { if (!target.endsWith(suffix)) { return target; } target.resize(target.length() - suffix.length()); return target + replacement; } static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) { MMCZip::ArchiveReader zip(source); QDir directory(targetFolder); auto extPtr = MMCZip::ArchiveWriter::createDiskWriter(); auto ext = extPtr.get(); return zip.parse([applyJnilibHack, directory, ext](MMCZip::ArchiveReader::File* f) { QString name = f->filename(); auto lowercase = name.toLower(); if (applyJnilibHack) { name = replaceSuffix(name, ".jnilib", ".dylib"); } QString absFilePath = directory.absoluteFilePath(name); return f->writeFile(ext, absFilePath); }); } void ExtractNatives::executeTask() { auto instance = m_parent->instance(); auto toExtract = instance->getNativeJars(); if (toExtract.isEmpty()) { emitSucceeded(); return; } auto settings = instance->settings(); auto outputPath = instance->getNativePath(); FS::ensureFolderPathExists(outputPath); auto javaVersion = instance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for (const auto& source : toExtract) { if (!unzipNatives(source, outputPath, jniHackEnabled)) { const char* reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); emitFailed(tr(reason).arg(source, outputPath)); return; } } emitSucceeded(); } void ExtractNatives::finalize() { auto instance = m_parent->instance(); QString target_dir = FS::PathCombine(instance->instanceRoot(), "natives/"); QDir dir(target_dir); dir.removeRecursively(); } PrismLauncher-10.0.5/launcher/minecraft/launch/MinecraftTarget.h0000644000175100017510000000155215144136756024251 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include struct MinecraftTarget { QString address; quint16 port; QString world; static MinecraftTarget parse(const QString& fullAddress, bool useWorld); using Ptr = std::shared_ptr; }; PrismLauncher-10.0.5/launcher/minecraft/launch/ExtractNatives.h0000644000175100017510000000174215144136756024137 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include // FIXME: temporary wrapper for existing task. class ExtractNatives : public LaunchStep { Q_OBJECT public: explicit ExtractNatives(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~ExtractNatives() {}; void executeTask() override; bool canAbort() const override { return false; } void finalize() override; }; PrismLauncher-10.0.5/launcher/minecraft/launch/PrintInstanceInfo.cpp0000644000175100017510000000726415144136756025130 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include "PrintInstanceInfo.h" #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) namespace { #if defined(Q_OS_LINUX) void probeProcCpuinfo(QStringList& log) { std::ifstream cpuin("/proc/cpuinfo"); for (std::string line; std::getline(cpuin, line);) { if (strncmp(line.c_str(), "model name", 10) == 0) { log << QString::fromStdString(line.substr(13, std::string::npos)); break; } } } void runLspci(QStringList& log) { // FIXME: fixed size buffers... char buff[512]; int gpuline = -1; int cline = 0; FILE* lspci = popen("lspci -k", "r"); if (!lspci) return; while (fgets(buff, 512, lspci) != NULL) { std::string str(buff); if (str.length() < 9) continue; if (str.substr(8, 3) == "VGA") { gpuline = cline; log << QString::fromStdString(str.substr(35, std::string::npos)); } if (gpuline > -1 && gpuline != cline) { if (cline - gpuline < 3) { log << QString::fromStdString(str.substr(1, std::string::npos)); } } cline++; } pclose(lspci); } #elif defined(Q_OS_FREEBSD) void runSysctlHwModel(QStringList& log) { char buff[512]; FILE* hwmodel = popen("sysctl hw.model", "r"); while (fgets(buff, 512, hwmodel) != NULL) { log << QString::fromUtf8(buff); break; } pclose(hwmodel); } void runPciconf(QStringList& log) { char buff[512]; std::string strcard; FILE* pciconf = popen("pciconf -lv -a vgapci0", "r"); while (fgets(buff, 512, pciconf) != NULL) { if (strncmp(buff, " vendor", 10) == 0) { std::string str(buff); strcard.append(str.substr(str.find_first_of("'") + 1, str.find_last_not_of("'") - (str.find_first_of("'") + 2))); strcard.append(" "); } else if (strncmp(buff, " device", 10) == 0) { std::string str2(buff); strcard.append(str2.substr(str2.find_first_of("'") + 1, str2.find_last_not_of("'") - (str2.find_first_of("'") + 2))); } log << QString::fromStdString(strcard); break; } pclose(pciconf); } #endif void runGlxinfo(QStringList& log) { // FIXME: fixed size buffers... char buff[512]; FILE* glxinfo = popen("glxinfo", "r"); if (!glxinfo) return; while (fgets(buff, 512, glxinfo) != NULL) { if (strncmp(buff, "OpenGL version string:", 22) == 0) { log << QString::fromUtf8(buff); break; } } pclose(glxinfo); } } // namespace #endif void PrintInstanceInfo::executeTask() { auto instance = m_parent->instance(); QStringList log; #if defined(Q_OS_LINUX) ::probeProcCpuinfo(log); ::runLspci(log); ::runGlxinfo(log); #elif defined(Q_OS_FREEBSD) ::runSysctlHwModel(log); ::runPciconf(log); ::runGlxinfo(log); #endif logLines(log, MessageLevel::Launcher); logLines(instance->verboseDescription(m_session, m_targetToJoin), MessageLevel::Launcher); emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/launch/EnsureOfflineLibraries.cpp0000644000175100017510000000325715144136756026132 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "EnsureOfflineLibraries.h" #include "minecraft/PackProfile.h" EnsureOfflineLibraries::EnsureOfflineLibraries(LaunchTask* parent, MinecraftInstance* instance) : LaunchStep(parent), m_instance(instance) {} void EnsureOfflineLibraries::executeTask() { const auto profile = m_instance->getPackProfile()->getProfile(); QStringList allJars; profile->getLibraryFiles(m_instance->runtimeContext(), allJars, allJars, m_instance->getLocalLibraryPath(), m_instance->binRoot()); for (const auto& jar : allJars) { if (!QFileInfo::exists(jar)) { emit logLine(tr("This instance cannot be launched because some libraries are missing or have not been downloaded yet. Please " "try again in online mode with a working Internet connection"), MessageLevel::Fatal); emitFailed("Required libraries are missing"); return; } } emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/launch/AutoInstallJava.cpp0000644000175100017510000002663415144136756024576 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AutoInstallJava.h" #include #include #include #include "Application.h" #include "FileSystem.h" #include "MessageLevel.h" #include "QObjectPtr.h" #include "SysInfo.h" #include "java/JavaInstall.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "java/JavaVersion.h" #include "java/download/ArchiveDownloadTask.h" #include "java/download/ManifestDownloadTask.h" #include "java/download/SymlinkTask.h" #include "meta/Index.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/Mode.h" #include "tasks/SequentialTask.h" AutoInstallJava::AutoInstallJava(LaunchTask* parent) : LaunchStep(parent), m_instance(m_parent->instance()), m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; void AutoInstallJava::executeTask() { auto settings = m_instance->settings(); if (!APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() || (settings->get("OverrideJavaLocation").toBool() && QFileInfo::exists(settings->get("JavaPath").toString()))) { emitSucceeded(); return; } auto packProfile = m_instance->getPackProfile(); if (!APPLICATION->settings()->get("AutomaticJavaDownload").toBool()) { auto javas = APPLICATION->javalist(); m_current_task = javas->getLoadTask(); connect(m_current_task.get(), &Task::finished, this, [this, javas, packProfile] { for (auto i = 0; i < javas->count(); i++) { auto java = std::dynamic_pointer_cast(javas->at(i)); if (java && packProfile->getProfile()->getCompatibleJavaMajors().contains(java->id.major())) { if (!java->is_64bit) { emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), MessageLevel::Launcher); } setJavaPath(java->path); return; } } emit logLine(tr("No compatible Java version was found. Using the default one."), MessageLevel::Warning); emitSucceeded(); }); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); emit progressReportingRequest(); return; } if (m_supported_arch.isEmpty()) { emit logLine(tr("Your system (%1-%2) is not compatible with automatic Java installation. Using the default Java path.") .arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), MessageLevel::Warning); emitSucceeded(); return; } auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); if (wantedJavaName.isEmpty()) { emit logLine(tr("Your meta information is out of date or doesn't have the information necessary to determine what installation of " "Java should be used. " "Using the default Java path."), MessageLevel::Warning); emitSucceeded(); return; } QDir javaDir(APPLICATION->javaPath()); auto relativeBinary = FS::PathCombine(wantedJavaName, "bin", JavaUtils::javaExecutable); auto wantedJavaPath = javaDir.absoluteFilePath(relativeBinary); if (QFileInfo::exists(wantedJavaPath)) { setJavaPathFromPartial(); return; } auto versionList = APPLICATION->metadataIndex()->get("net.minecraft.java"); m_current_task = versionList->getLoadTask(); connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::tryNextMajorJava); connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::emitFailed); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); if (!m_current_task->isRunning()) { m_current_task->start(); } emit progressReportingRequest(); } void AutoInstallJava::setJavaPath(QString path) { auto settings = m_instance->settings(); settings->set("OverrideJavaLocation", true); settings->set("JavaPath", path); settings->set("AutomaticJava", true); emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Launcher); emitSucceeded(); } void AutoInstallJava::setJavaPathFromPartial() { auto packProfile = m_instance->getPackProfile(); auto javaName = packProfile->getProfile()->getCompatibleJavaName(); QDir javaDir(APPLICATION->javaPath()); // just checking if the executable is there should suffice // but if needed this can be achieved through refreshing the javalist // and retrieving the path that contains the java name auto relativeBinary = FS::PathCombine(javaName, "bin", JavaUtils::javaExecutable); auto finalPath = javaDir.absoluteFilePath(relativeBinary); if (QFileInfo::exists(finalPath)) { setJavaPath(finalPath); } else { emit logLine(tr("No compatible Java version was found (the binary file does not exist). Using the default one."), MessageLevel::Warning); emitSucceeded(); } return; } void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) { auto runtimes = version->data()->runtimes; for (auto java : runtimes) { if (java->runtimeOS == m_supported_arch && java->name() == javaName) { QDir javaDir(APPLICATION->javaPath()); auto final_path = javaDir.absoluteFilePath(java->m_name); auto deletePath = [final_path] { FS::deletePath(final_path); }; switch (java->downloadType) { case Java::DownloadType::Manifest: m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); break; case Java::DownloadType::Archive: m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); break; case Java::DownloadType::Unknown: deletePath(); emitFailed(tr("Could not determine Java download type!")); return; } #if defined(Q_OS_MACOS) auto seq = makeShared(tr("Install Java")); seq->addTask(m_current_task); seq->addTask(makeShared(final_path)); m_current_task = seq; #endif connect(m_current_task.get(), &Task::failed, this, [this, deletePath](QString reason) { deletePath(); emitFailed(reason); }); connect(m_current_task.get(), &Task::aborted, this, deletePath); connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::setJavaPathFromPartial); connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); m_current_task->start(); return; } } tryNextMajorJava(); } void AutoInstallJava::tryNextMajorJava() { if (!isRunning()) return; auto versionList = APPLICATION->metadataIndex()->get("net.minecraft.java"); auto packProfile = m_instance->getPackProfile(); auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); auto majorJavaVersions = packProfile->getProfile()->getCompatibleJavaMajors(); if (m_majorJavaVersionIndex >= majorJavaVersions.length()) { emit logLine( tr("No versions of Java were found for your operating system: %1-%2").arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), MessageLevel::Warning); emit logLine(tr("No compatible version of Java was found. Using the default one."), MessageLevel::Warning); emitSucceeded(); return; } auto majorJavaVersion = majorJavaVersions[m_majorJavaVersionIndex]; m_majorJavaVersionIndex++; auto javaMajor = versionList->getVersion(QString("java%1").arg(majorJavaVersion)); if (javaMajor->isLoaded()) { downloadJava(javaMajor, wantedJavaName); } else { m_current_task = APPLICATION->metadataIndex()->loadVersion("net.minecraft.java", javaMajor->version(), Net::Mode::Online); connect(m_current_task.get(), &Task::succeeded, this, [this, javaMajor, wantedJavaName] { downloadJava(javaMajor, wantedJavaName); }); connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); if (!m_current_task->isRunning()) { m_current_task->start(); } } } bool AutoInstallJava::abort() { if (m_current_task && m_current_task->canAbort()) { auto status = m_current_task->abort(); emitFailed("Aborted."); return status; } return Task::abort(); } PrismLauncher-10.0.5/launcher/minecraft/launch/CreateGameFolders.cpp0000644000175100017510000000151515144136756025040 0ustar runnerrunner#include "CreateGameFolders.h" #include "FileSystem.h" #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" CreateGameFolders::CreateGameFolders(LaunchTask* parent) : LaunchStep(parent) {} void CreateGameFolders::executeTask() { auto instance = m_parent->instance(); if (!FS::ensureFolderPathExists(instance->gameRoot())) { emit logLine("Couldn't create the main game folder", MessageLevel::Error); emitFailed(tr("Couldn't create the main game folder")); return; } // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. if (!FS::ensureFolderPathExists(FS::PathCombine(instance->gameRoot(), "server-resource-packs"))) { emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); } emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/launch/LauncherPartLaunch.h0000644000175100017510000000301515144136756024711 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "MinecraftTarget.h" class LauncherPartLaunch : public LaunchStep { Q_OBJECT public: explicit LauncherPartLaunch(LaunchTask* parent); virtual ~LauncherPartLaunch() = default; virtual void executeTask(); virtual bool abort(); virtual void proceed(); virtual bool canAbort() const { return true; } void setWorkingDirectory(const QString& wd); void setAuthSession(AuthSessionPtr session) { m_session = session; } void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) { m_targetToJoin = std::move(targetToJoin); } private slots: void on_state(LoggedProcess::State state); private: LoggedProcess m_process; QString m_command; AuthSessionPtr m_session; QString m_launchScript; MinecraftTarget::Ptr m_targetToJoin; bool mayProceed = false; }; PrismLauncher-10.0.5/launcher/minecraft/launch/ReconstructAssets.cpp0000644000175100017510000000226515144136756025225 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ReconstructAssets.h" #include "launch/LaunchTask.h" #include "minecraft/AssetsUtils.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" void ReconstructAssets::executeTask() { auto instance = m_parent->instance(); auto components = instance->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); if (!AssetsUtils::reconstructAssets(assets->id, instance->resourcesDir())) { emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); } emitSucceeded(); } PrismLauncher-10.0.5/launcher/minecraft/launch/AutoInstallJava.h0000644000175100017510000000444215144136756024234 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "tasks/Task.h" class AutoInstallJava : public LaunchStep { Q_OBJECT public: explicit AutoInstallJava(LaunchTask* parent); ~AutoInstallJava() override = default; void executeTask() override; bool canAbort() const override { return m_current_task ? m_current_task->canAbort() : false; } bool abort() override; protected: void setJavaPath(QString path); void setJavaPathFromPartial(); void downloadJava(Meta::Version::Ptr version, QString javaName); void tryNextMajorJava(); private: MinecraftInstancePtr m_instance; Task::Ptr m_current_task; qsizetype m_majorJavaVersionIndex = 0; const QString m_supported_arch; }; PrismLauncher-10.0.5/launcher/minecraft/launch/ScanModFolders.h0000644000175100017510000000224615144136756024036 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class ScanModFolders : public LaunchStep { Q_OBJECT public: explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~ScanModFolders() {}; virtual void executeTask() override; virtual bool canAbort() const override { return false; } private slots: void coreModsDone(); void modsDone(); void nilModsDone(); private: void checkDone(); private: // DATA bool m_modsDone = false; bool m_nilModsDone = false; bool m_coreModsDone = false; }; PrismLauncher-10.0.5/launcher/minecraft/launch/ModMinecraftJar.cpp0000644000175100017510000000635515144136756024540 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ModMinecraftJar.h" #include "FileSystem.h" #include "MMCZip.h" #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" void ModMinecraftJar::executeTask() { auto m_inst = m_parent->instance(); if (!m_inst->getJarMods().size()) { emitSucceeded(); return; } // nuke obsolete stripped jar(s) if needed if (!FS::ensureFolderPathExists(m_inst->binRoot())) { emitFailed(tr("Couldn't create the bin folder for Minecraft.jar")); return; } auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); if (!removeJar()) { emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath)); return; } // create temporary modded jar, if needed auto components = m_inst->getPackProfile(); auto profile = components->getProfile(); auto jarMods = m_inst->getJarMods(); if (jarMods.size()) { auto mainJar = profile->getMainJar(); QStringList jars, temp1, temp2, temp3, temp4; mainJar->getApplicableFiles(m_inst->runtimeContext(), jars, temp1, temp2, temp3, m_inst->getLocalLibraryPath()); auto sourceJarPath = jars[0]; if (!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods)) { emitFailed(tr("Failed to create the custom Minecraft jar file.")); return; } } emitSucceeded(); } void ModMinecraftJar::finalize() { removeJar(); } bool ModMinecraftJar::removeJar() { auto m_inst = m_parent->instance(); auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); QFile finalJar(finalJarPath); if (finalJar.exists()) { if (!finalJar.remove()) { return false; } } return true; } PrismLauncher-10.0.5/launcher/minecraft/launch/VerifyJavaInstall.cpp0000644000175100017510000000717615144136756025132 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "VerifyJavaInstall.h" #include #include "Application.h" #include "MessageLevel.h" #include "java/JavaInstall.h" #include "java/JavaInstallList.h" #include "java/JavaVersion.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" void VerifyJavaInstall::executeTask() { auto instance = m_parent->instance(); auto packProfile = instance->getPackProfile(); auto settings = instance->settings(); auto storedVersion = settings->get("JavaVersion").toString(); auto ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); auto javaArchitecture = settings->get("JavaArchitecture").toString(); auto maxMemAlloc = settings->get("MaxMemAlloc").toInt(); if (javaArchitecture == "32" && maxMemAlloc > 2048) { emit logLine(tr("Max memory allocation exceeds the supported value.\n" "The selected installation of Java is 32-bit and doesn't support more than 2048MiB of RAM.\n" "The instance may not start due to this."), MessageLevel::Error); } auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); JavaVersion javaVersion(storedVersion); if (compatibleMajors.isEmpty() || compatibleMajors.contains(javaVersion.major())) { emitSucceeded(); return; } if (ignoreCompatibility) { emit logLine(tr("Java major version is incompatible. Things might break."), MessageLevel::Warning); emitSucceeded(); return; } emit logLine(tr("This instance is not compatible with Java version %1.\n" "Please switch to one of the following Java versions for this instance:") .arg(javaVersion.major()), MessageLevel::Error); for (auto major : compatibleMajors) { emit logLine(tr("Java version %1").arg(major), MessageLevel::Error); } emit logLine(tr("Go to instance Java settings to change your Java version or disable the Java compatibility check if you know what " "you're doing."), MessageLevel::Error); emitFailed(QString("Incompatible Java major version")); } PrismLauncher-10.0.5/launcher/minecraft/launch/MinecraftTarget.cpp0000644000175100017510000000410515144136756024601 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MinecraftTarget.h" #include // FIXME: the way this is written, it can't ever do any sort of validation and can accept total junk MinecraftTarget MinecraftTarget::parse(const QString& fullAddress, bool useWorld) { if (useWorld) { MinecraftTarget target; target.world = fullAddress; return target; } QStringList split = fullAddress.split(":"); // The logic below replicates the exact logic minecraft uses for parsing server addresses. // While the conversion is not lossless and eats errors, it ensures the same behavior // within Minecraft and Prism Launcher when entering server addresses. if (fullAddress.startsWith("[")) { int bracket = fullAddress.indexOf("]"); if (bracket > 0) { QString ipv6 = fullAddress.mid(1, bracket - 1); QString port = fullAddress.mid(bracket + 1).trimmed(); if (port.startsWith(":") && !ipv6.isEmpty()) { port = port.mid(1); split = QStringList({ ipv6, port }); } else { split = QStringList({ ipv6 }); } } } if (split.size() > 2) { split = QStringList({ fullAddress }); } QString realAddress = split[0]; quint16 realPort = 25565; if (split.size() > 1) { bool ok; realPort = split[1].toUInt(&ok); if (!ok) { realPort = 25565; } } return MinecraftTarget{ realAddress, realPort }; } PrismLauncher-10.0.5/launcher/minecraft/launch/ScanModFolders.cpp0000644000175100017510000000527415144136756024375 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ScanModFolders.h" #include "FileSystem.h" #include "MMCZip.h" #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/mod/ModFolderModel.h" void ScanModFolders::executeTask() { auto m_inst = m_parent->instance(); auto loaders = m_inst->loaderModList(); connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); if (!loaders->update()) { m_modsDone = true; } auto cores = m_inst->coreModList(); connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); if (!cores->update()) { m_coreModsDone = true; } auto nils = m_inst->nilModList(); connect(nils.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); if (!nils->update()) { m_nilModsDone = true; } checkDone(); } void ScanModFolders::modsDone() { m_modsDone = true; checkDone(); } void ScanModFolders::coreModsDone() { m_coreModsDone = true; checkDone(); } void ScanModFolders::nilModsDone() { m_nilModsDone = true; checkDone(); } void ScanModFolders::checkDone() { if (m_modsDone && m_coreModsDone && m_nilModsDone) { emitSucceeded(); } } PrismLauncher-10.0.5/launcher/minecraft/launch/EnsureOfflineLibraries.h0000644000175100017510000000224015144136756025566 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2026 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "launch/LaunchStep.h" #include "minecraft/MinecraftInstance.h" class EnsureOfflineLibraries : public LaunchStep { Q_OBJECT public: explicit EnsureOfflineLibraries(LaunchTask* parent, MinecraftInstance* instance); ~EnsureOfflineLibraries() override = default; void executeTask() override; bool canAbort() const override { return false; } private: MinecraftInstance* m_instance; }; PrismLauncher-10.0.5/launcher/minecraft/launch/ModMinecraftJar.h0000644000175100017510000000177315144136756024204 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class ModMinecraftJar : public LaunchStep { Q_OBJECT public: explicit ModMinecraftJar(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~ModMinecraftJar() {}; virtual void executeTask() override; virtual bool canAbort() const override { return false; } void finalize() override; private: bool removeJar(); }; PrismLauncher-10.0.5/launcher/minecraft/launch/PrintInstanceInfo.h0000644000175100017510000000236115144136756024566 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "minecraft/auth/AuthSession.h" #include "minecraft/launch/MinecraftTarget.h" // FIXME: temporary wrapper for existing task. class PrintInstanceInfo : public LaunchStep { Q_OBJECT public: explicit PrintInstanceInfo(LaunchTask* parent, AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) : LaunchStep(parent), m_session(session), m_targetToJoin(targetToJoin) {}; virtual ~PrintInstanceInfo() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } private: AuthSessionPtr m_session; MinecraftTarget::Ptr m_targetToJoin; }; PrismLauncher-10.0.5/launcher/minecraft/launch/ReconstructAssets.h0000644000175100017510000000166015144136756024670 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class ReconstructAssets : public LaunchStep { Q_OBJECT public: explicit ReconstructAssets(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~ReconstructAssets() {}; void executeTask() override; bool canAbort() const override { return false; } }; PrismLauncher-10.0.5/launcher/minecraft/launch/ClaimAccount.h0000644000175100017510000000206415144136756023533 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class ClaimAccount : public LaunchStep { Q_OBJECT public: explicit ClaimAccount(LaunchTask* parent, AuthSessionPtr session); virtual ~ClaimAccount() = default; void executeTask() override; void finalize() override; bool canAbort() const override { return false; } private: std::unique_ptr lock; MinecraftAccountPtr m_account; }; PrismLauncher-10.0.5/launcher/minecraft/launch/VerifyJavaInstall.h0000644000175100017510000000347515144136756024575 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class VerifyJavaInstall : public LaunchStep { Q_OBJECT public: explicit VerifyJavaInstall(LaunchTask* parent) : LaunchStep(parent) {}; ~VerifyJavaInstall() override = default; void executeTask() override; bool canAbort() const override { return false; } }; PrismLauncher-10.0.5/launcher/minecraft/launch/LauncherPartLaunch.cpp0000644000175100017510000002111615144136756025246 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LauncherPartLaunch.h" #include #include #include "Application.h" #include "Commandline.h" #include "FileSystem.h" #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" #ifdef Q_OS_LINUX #include "gamemode_client.h" #endif LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent) , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QStringConverter::Utf8 : QStringConverter::System) { if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) { static const QRegularExpression s_settingUser(".*Setting user.+", QRegularExpression::CaseInsensitiveOption); std::shared_ptr connection{ new QMetaObject::Connection }; *connection = connect(&m_process, &LoggedProcess::log, this, [connection](const QStringList& lines, [[maybe_unused]] MessageLevel level) { qDebug() << lines; if (lines.filter(s_settingUser).length() != 0) { APPLICATION->closeAllWindows(); disconnect(*connection); } }); } connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); } void LauncherPartLaunch::executeTask() { QString jarPath = APPLICATION->getJarPath("NewLaunch.jar"); if (jarPath.isEmpty()) { const char* reason = QT_TR_NOOP("Launcher library could not be found. Please check your installation."); emit logLine(tr(reason), MessageLevel::Fatal); emitFailed(tr(reason)); return; } auto instance = m_parent->instance(); QString legacyJarPath; if (instance->getLauncher() == "legacy" || instance->shouldApplyOnlineFixes()) { legacyJarPath = APPLICATION->getJarPath("NewLaunchLegacy.jar"); if (legacyJarPath.isEmpty()) { const char* reason = QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation."); emit logLine(tr(reason), MessageLevel::Fatal); emitFailed(tr(reason)); return; } } m_launchScript = instance->createLaunchScript(m_session, m_targetToJoin); QStringList args = instance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); m_process.setProcessEnvironment(instance->createLaunchEnvironment()); // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); auto classPath = instance->getClassPath(); classPath.prepend(jarPath); if (!legacyJarPath.isEmpty()) classPath.prepend(legacyJarPath); auto natPath = instance->getNativePath(); #ifdef Q_OS_WIN natPath = FS::getPathNameInLocal8bit(natPath); #endif args << "-Djava.library.path=" + natPath; args << "-cp"; #ifdef Q_OS_WIN QStringList processed; for (auto& item : classPath) { processed << FS::getPathNameInLocal8bit(item); } args << processed.join(';'); #else args << classPath.join(':'); #endif args << "org.prismlauncher.EntryPoint"; qDebug() << args.join(' '); QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); if (!wrapperCommandStr.isEmpty()) { wrapperCommandStr = m_parent->substituteVariables(wrapperCommandStr); auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr); auto wrapperCommand = wrapperArgs.takeFirst(); auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); if (realWrapperCommand.isEmpty()) { const char* reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); emitFailed(tr(reason).arg(wrapperCommand)); return; } emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::Launcher); args.prepend(javaPath); m_process.start(wrapperCommand, wrapperArgs + args); } else { m_process.start(javaPath, args); } #ifdef Q_OS_LINUX if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode) { auto pid = m_process.processId(); if (pid) { gamemode_request_start_for(pid); } } #endif } void LauncherPartLaunch::on_state(LoggedProcess::State state) { switch (state) { case LoggedProcess::FailedToStart: { //: Error message displayed if instace can't start const char* reason = QT_TR_NOOP("Could not launch Minecraft!"); emit logLine(reason, MessageLevel::Fatal); emitFailed(tr(reason)); return; } case LoggedProcess::Aborted: case LoggedProcess::Crashed: { m_parent->setPid(-1); m_parent->instance()->setMinecraftRunning(false); emitFailed(tr("Game crashed.")); return; } case LoggedProcess::Finished: { auto instance = m_parent->instance(); if (instance->settings()->get("CloseAfterLaunch").toBool()) APPLICATION->showMainWindow(); m_parent->setPid(-1); m_parent->instance()->setMinecraftRunning(false); // if the exit code wasn't 0, report this as a crash auto exitCode = m_process.exitCode(); if (exitCode != 0) { emitFailed(tr("Game crashed.")); return; } // FIXME: make this work again // m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(exitCode)); // run post-exit emitSucceeded(); break; } case LoggedProcess::Running: emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::Launcher); m_parent->setPid(m_process.processId()); // send the launch script to the launcher part m_process.write(m_launchScript.toUtf8()); mayProceed = true; emit readyForLaunch(); break; default: break; } } void LauncherPartLaunch::setWorkingDirectory(const QString& wd) { m_process.setWorkingDirectory(wd); } void LauncherPartLaunch::proceed() { if (mayProceed) { m_parent->instance()->setMinecraftRunning(true); QString launchString("launch\n"); m_process.write(launchString.toUtf8()); mayProceed = false; } } bool LauncherPartLaunch::abort() { if (mayProceed) { mayProceed = false; QString launchString("abort\n"); m_process.write(launchString.toUtf8()); } else { auto state = m_process.state(); if (state == LoggedProcess::Running || state == LoggedProcess::Starting) { m_process.kill(); } } return true; } PrismLauncher-10.0.5/launcher/minecraft/launch/CreateGameFolders.h0000644000175100017510000000202615144136756024503 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include // Create the main .minecraft for the instance and any other necessary folders class CreateGameFolders : public LaunchStep { Q_OBJECT public: explicit CreateGameFolders(LaunchTask* parent); virtual ~CreateGameFolders() {}; virtual void executeTask(); virtual bool canAbort() const { return false; } }; PrismLauncher-10.0.5/launcher/minecraft/VersionFile.h0000644000175100017510000001236315144136756022147 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include #include "Agent.h" #include "Library.h" #include "ProblemProvider.h" #include "java/JavaMetadata.h" #include "minecraft/Rule.h" class PackProfile; class VersionFile; class LaunchProfile; struct MojangDownloadInfo; struct MojangAssetIndexInfo; using VersionFilePtr = std::shared_ptr; class VersionFile : public ProblemContainer { friend class MojangVersionFormat; friend class OneSixVersionFormat; public: /* methods */ void applyTo(LaunchProfile* profile, const RuntimeContext& runtimeContext); public: /* data */ /// Prism Launcher: order hint for this version file if no explicit order is set int order = 0; /// Prism Launcher: human readable name of this package QString name; /// Prism Launcher: package ID of this package QString uid; /// Prism Launcher: version of this package QString version; /// Prism Launcher: DEPRECATED dependency on a Minecraft version QString dependsOnMinecraftVersion; /// Mojang: DEPRECATED used to version the Mojang version format int minimumLauncherVersion = -1; /// Mojang: DEPRECATED version of Minecraft this is QString minecraftVersion; /// Mojang: class to launch Minecraft with QString mainClass; /// Prism Launcher: class to launch legacy Minecraft with (embed in a custom window) QString appletClass; /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) QString minecraftArguments; /// Prism Launcher: Additional JVM launch arguments QStringList addnJvmArguments; /// Mojang: list of compatible java majors QList compatibleJavaMajors; /// Mojang: the name of recommended java version QString compatibleJavaName; /// Mojang: type of the Minecraft version QString type; /// Mojang: the time this version was actually released by Mojang QDateTime releaseTime; /// Mojang: DEPRECATED the time this version was last updated by Mojang QDateTime updateTime; /// Mojang: DEPRECATED asset group to be used with Minecraft QString assets; /// Prism Launcher: list of tweaker mod arguments for launchwrapper QStringList addTweakers; /// Mojang: list of libraries to add to the version QList libraries; /// Prism Launcher: list of maven files to put in the libraries folder, but not in classpath QList mavenFiles; /// Prism Launcher: list of agents to add to JVM arguments QList agents; /// The main jar (Minecraft version library, normally) LibraryPtr mainJar; /// Prism Launcher: list of attached traits of this version file - used to enable features QSet traits; /// Prism Launcher: list of jar mods added to this version QList jarMods; /// Prism Launcher: list of mods added to this version QList mods; /** * Prism Launcher: set of packages this depends on * NOTE: this is shared with the meta format!!! */ Meta::RequireSet m_requires; /** * Prism Launcher: set of packages this conflicts with * NOTE: this is shared with the meta format!!! */ Meta::RequireSet conflicts; /// is volatile -- may be removed as soon as it is no longer needed by something else bool m_volatile = false; QList runtimes; public: // Mojang: DEPRECATED list of 'downloads' - client jar, server jar, windows server exe, maybe more. QMap> mojangDownloads; // Mojang: extended asset index download information std::shared_ptr mojangAssetIndex; }; PrismLauncher-10.0.5/launcher/minecraft/Rule.h0000644000175100017510000000407715144136756020634 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2025 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "RuntimeContext.h" class Library; class Rule { public: enum Action { Allow, Disallow, Defer }; static Rule fromJson(const QJsonObject& json); QJsonObject toJson(); Action apply(const RuntimeContext& runtimeContext); private: struct OS { QString name; // FIXME: unsupported // retained to avoid information being lost from files QString version; }; Action m_action = Defer; std::optional m_os; }; PrismLauncher-10.0.5/launcher/minecraft/AssetsUtils.h0000644000175100017510000000271615144136756022206 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "net/NetJob.h" #include "net/NetRequest.h" struct AssetObject { QString getRelPath(); QUrl getUrl(); QString getLocalPath(); Net::NetRequest::Ptr getDownloadAction(); QString hash; qint64 size; }; struct AssetsIndex { NetJob::Ptr getDownloadJob(); QString id; QMap objects; bool isVirtual = false; bool mapToResources = false; }; /// FIXME: this is absolutely horrendous. REDO!!!! namespace AssetsUtils { bool loadAssetsIndexJson(const QString& id, const QString& file, AssetsIndex& index); QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder); /// Reconstruct a virtual assets folder for the given assets ID and return the folder bool reconstructAssets(QString assetsId, QString resourcesFolder); } // namespace AssetsUtils PrismLauncher-10.0.5/launcher/minecraft/VersionFilterData.cpp0000644000175100017510000000774415144136756023651 0ustar runnerrunner#include "VersionFilterData.h" #include "ParseUtils.h" VersionFilterData g_VersionFilterData = VersionFilterData(); VersionFilterData::VersionFilterData() { // 1.3.* auto libs13 = QList{ { "argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b" }, { "guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f" }, { "asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82" } }; fmlLibsMapping["1.3.2"] = libs13; // 1.4.* auto libs14 = QList{ { "argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b" }, { "guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f" }, { "asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82" }, { "bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb" } }; fmlLibsMapping["1.4"] = libs14; fmlLibsMapping["1.4.1"] = libs14; fmlLibsMapping["1.4.2"] = libs14; fmlLibsMapping["1.4.3"] = libs14; fmlLibsMapping["1.4.4"] = libs14; fmlLibsMapping["1.4.5"] = libs14; fmlLibsMapping["1.4.6"] = libs14; fmlLibsMapping["1.4.7"] = libs14; // 1.5 fmlLibsMapping["1.5"] = QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, { "deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8" }, { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; // 1.5.1 fmlLibsMapping["1.5.1"] = QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, { "deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6" }, { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; // 1.5.2 fmlLibsMapping["1.5.2"] = QList{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, { "deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9" }, { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; // don't use installers for those. forgeInstallerBlacklist = QSet({ "1.5.2" }); // FIXME: remove, used for deciding when core mods should display legacyCutoffDate = timeFromS3Time("2013-06-25T15:08:56+02:00"); lwjglWhitelist = QSet{ "net.java.jinput:jinput", "net.java.jinput:jinput-platform", "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl", "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform" }; java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00"); } PrismLauncher-10.0.5/launcher/minecraft/skins/0000755000175100017510000000000015144136756020673 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/minecraft/skins/SkinModel.cpp0000644000175100017510000001660115144136756023270 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023-2025 Trial97 * Copyright (c) 2025 Rinth, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SkinModel.h" #include #include #include "FileSystem.h" static void setAlpha(QImage& image, const QRect& region, const int alpha) { for (int y = region.top(); y < region.bottom(); ++y) { QRgb* line = reinterpret_cast(image.scanLine(y)); for (int x = region.left(); x < region.right(); ++x) { QRgb pixel = line[x]; line[x] = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), alpha); } } } static void doNotchTransparencyHack(QImage& image) { for (int y = 0; y < 32; y++) { QRgb* line = reinterpret_cast(image.scanLine(y)); for (int x = 32; x < 64; x++) { if (qAlpha(line[x]) < 128) { return; } } } setAlpha(image, { 32, 0, 32, 32 }, 0); } static QImage improveSkin(QImage skin) { int height = skin.height(); int width = skin.width(); if (width != 64 || (height != 32 && height != 64)) { // this is no minecraft skin return skin; } // It seems some older skins may use this format, which can't be drawn onto // https://github.com/PrismLauncher/PrismLauncher/issues/4032 // https://doc.qt.io/qt-6/qpainter.html#begin if (skin.format() == QImage::Format_Indexed8) { skin = skin.convertToFormat(QImage::Format_ARGB32); } auto isLegacy = height == 32; // old format if (isLegacy) { auto newSkin = QImage(QSize(64, 64), skin.format()); newSkin.fill(Qt::transparent); QPainter p(&newSkin); p.drawImage(0, 0, skin); auto copyRect = [&p, &newSkin](int startX, int startY, int offsetX, int offsetY, int sizeX, int sizeY) { QImage region = newSkin.copy(startX, startY, sizeX, sizeY); region = region.mirrored(true, false); p.drawImage(startX + offsetX, startY + offsetY, region); }; static const struct { int x; int y; int offsetX; int offsetY; int width; int height; } faces[] = { { 4, 16, 16, 32, 4, 4 }, { 8, 16, 16, 32, 4, 4 }, { 0, 20, 24, 32, 4, 12 }, { 4, 20, 16, 32, 4, 12 }, { 8, 20, 8, 32, 4, 12 }, { 12, 20, 16, 32, 4, 12 }, { 44, 16, -8, 32, 4, 4 }, { 48, 16, -8, 32, 4, 4 }, { 40, 20, 0, 32, 4, 12 }, { 44, 20, -8, 32, 4, 12 }, { 48, 20, -16, 32, 4, 12 }, { 52, 20, -8, 32, 4, 12 }, }; for (const auto& face : faces) { copyRect(face.x, face.y, face.offsetX, face.offsetY, face.width, face.height); } doNotchTransparencyHack(newSkin); skin = newSkin; } static const QRect opaqueParts[] = { { 0, 0, 32, 16 }, { 0, 16, 64, 16 }, { 16, 48, 32, 16 }, }; for (const auto& p : opaqueParts) { setAlpha(skin, p, 255); } return skin; } static QImage getSkin(const QString path) { return improveSkin(QImage(path)); } static QImage generatePreviews(QImage texture, bool slim) { QImage preview(36, 36, QImage::Format_ARGB32); preview.fill(Qt::transparent); QPainter paint(&preview); // head paint.drawImage(4, 2, texture.copy(8, 8, 8, 8)); paint.drawImage(4, 2, texture.copy(40, 8, 8, 8)); // torso paint.drawImage(4, 10, texture.copy(20, 20, 8, 12)); paint.drawImage(4, 10, texture.copy(20, 36, 8, 12)); // right leg paint.drawImage(4, 22, texture.copy(4, 20, 4, 12)); paint.drawImage(4, 22, texture.copy(4, 36, 4, 12)); // left leg paint.drawImage(8, 22, texture.copy(20, 52, 4, 12)); paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); auto armWidth = slim ? 3 : 4; auto armPosX = slim ? 1 : 0; // right arm paint.drawImage(armPosX, 10, texture.copy(44, 20, armWidth, 12)); paint.drawImage(armPosX, 10, texture.copy(44, 36, armWidth, 12)); // left arm paint.drawImage(12, 10, texture.copy(36, 52, armWidth, 12)); paint.drawImage(12, 10, texture.copy(52, 52, armWidth, 12)); // back // head paint.drawImage(24, 2, texture.copy(24, 8, 8, 8)); paint.drawImage(24, 2, texture.copy(56, 8, 8, 8)); // torso paint.drawImage(24, 10, texture.copy(32, 20, 8, 12)); paint.drawImage(24, 10, texture.copy(32, 36, 8, 12)); // right leg paint.drawImage(24, 22, texture.copy(12, 20, 4, 12)); paint.drawImage(24, 22, texture.copy(12, 36, 4, 12)); // left leg paint.drawImage(28, 22, texture.copy(28, 52, 4, 12)); paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); // right arm paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 20, armWidth, 12)); paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 36, armWidth, 12)); // left arm paint.drawImage(32, 10, texture.copy(40 + armWidth, 52, armWidth, 12)); paint.drawImage(32, 10, texture.copy(56 + armWidth, 52, armWidth, 12)); return preview; } SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_model(Model::CLASSIC) { m_preview = generatePreviews(m_texture, false); } SkinModel::SkinModel(QDir skinDir, QJsonObject obj) : m_capeId(obj["capeId"].toString()), m_model(Model::CLASSIC), m_url(obj["url"].toString()) { auto name = obj["name"].toString(); if (auto model = obj["model"].toString(); model == "SLIM") { m_model = Model::SLIM; } m_path = skinDir.absoluteFilePath(name) + ".png"; m_texture = getSkin(m_path); m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } QString SkinModel::name() const { return QFileInfo(m_path).completeBaseName(); } bool SkinModel::rename(QString newName) { auto info = QFileInfo(m_path); auto new_path = FS::PathCombine(info.absolutePath(), newName + ".png"); if (QFileInfo::exists(new_path)) { return false; } m_path = new_path; return FS::move(info.absoluteFilePath(), m_path); } QJsonObject SkinModel::toJSON() const { QJsonObject obj; obj["name"] = name(); obj["capeId"] = m_capeId; obj["url"] = m_url; obj["model"] = getModelString(); return obj; } QString SkinModel::getModelString() const { switch (m_model) { case CLASSIC: return "CLASSIC"; case SLIM: return "SLIM"; } return {}; } bool SkinModel::isValid() const { return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) && m_texture.size().width() == 64; } void SkinModel::refresh() { m_texture = getSkin(m_path); m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } void SkinModel::setModel(Model model) { m_model = model; m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } PrismLauncher-10.0.5/launcher/minecraft/skins/CapeChange.cpp0000644000175100017510000000510415144136756023355 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "CapeChange.h" #include #include "net/ByteArraySink.h" #include "net/RawHeaderProxy.h" CapeChange::CapeChange(QString cape) : NetRequest(), m_capeId(cape) { logCat = taskMCSkinsLogC; } QNetworkReply* CapeChange::getReply(QNetworkRequest& request) { if (m_capeId.isEmpty()) { setStatus(tr("Removing cape")); return m_network->deleteResource(request); } else { setStatus(tr("Equipping cape")); return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); } } CapeChange::Ptr CapeChange::make(QString token, QString capeId) { auto up = makeShared(capeId); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); up->setObjectName(QString("BYTES:") + up->m_url.toString()); up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); up->addHeaderProxy(new Net::RawHeaderProxy(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; } PrismLauncher-10.0.5/launcher/minecraft/skins/SkinList.cpp0000644000175100017510000002761415144136756023151 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SkinList.h" #include #include #include "FileSystem.h" #include "Json.h" #include "minecraft/skins/SkinModel.h" SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher.reset(new QFileSystemWatcher(this)); m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); directoryChanged(path); } void SkinList::startWatching() { if (m_isWatching) { return; } update(); m_isWatching = m_watcher->addPath(m_dir.absolutePath()); if (m_isWatching) { qDebug() << "Started watching" << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching" << m_dir.absolutePath(); } } void SkinList::stopWatching() { save(); if (!m_isWatching) { return; } m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); if (!m_isWatching) { qDebug() << "Stopped watching" << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching" << m_dir.absolutePath(); } } bool SkinList::update() { QList newSkins; m_dir.refresh(); auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); if (manifestInfo.exists()) { try { auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); const auto root = doc.object(); auto skins = root["skins"].toArray(); for (auto jSkin : skins) { SkinModel s(m_dir, jSkin.toObject()); if (s.isValid()) { newSkins << s; } } } catch (const Exception& e) { qCritical() << "Couldn't load skins json:" << e.cause(); } } bool needsSave = false; const auto& skin = m_acct->accountData()->minecraftProfile.skin; if (!skin.url.isEmpty() && !skin.data.isEmpty()) { QPixmap skinTexture; SkinModel* nskin = nullptr; for (auto i = 0; i < newSkins.size(); i++) { if (newSkins[i].getURL() == skin.url) { nskin = &newSkins[i]; break; } } if (!nskin) { auto name = m_acct->profileName() + ".png"; if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) { name = QUrl(skin.url).fileName() + ".png"; } auto path = m_dir.absoluteFilePath(name); if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) { SkinModel s(path); s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); s.setURL(skin.url); newSkins << s; needsSave = true; } } else { nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); } } auto folderContents = m_dir.entryInfoList(); // if there are any untracked files... for (QFileInfo entry : folderContents) { if (!entry.isFile() && entry.suffix() != "png") continue; SkinModel w(entry.absoluteFilePath()); if (w.isValid()) { auto add = true; for (auto s : newSkins) { if (s.name() == w.name()) { add = false; break; } } if (add) { newSkins.append(w); needsSave = true; } } } std::sort(newSkins.begin(), newSkins.end(), [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); beginResetModel(); m_skinList.swap(newSkins); endResetModel(); if (needsSave) save(); return true; } void SkinList::directoryChanged(const QString& path) { QDir new_dir(path); if (!new_dir.exists()) if (!FS::ensureFolderPathExists(new_dir.absolutePath())) return; if (m_dir.absolutePath() != new_dir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); if (m_isWatching) stopWatching(); startWatching(); } update(); } void SkinList::fileChanged(const QString& path) { qDebug() << "Checking" << path; QFileInfo checkfile(path); if (!checkfile.exists()) return; for (int i = 0; i < m_skinList.count(); i++) { if (m_skinList[i].getPath() == checkfile.absoluteFilePath()) { m_skinList[i].refresh(); dataChanged(index(i), index(i)); break; } } } QStringList SkinList::mimeTypes() const { return { "text/uri-list" }; } Qt::DropActions SkinList::supportedDropActions() const { return Qt::CopyAction; } bool SkinList::dropMimeData(const QMimeData* data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) { if (action == Qt::IgnoreAction) return true; // check if the action is supported if (!data || !(action & supportedDropActions())) return false; // files dropped from outside? if (data->hasUrls()) { auto urls = data->urls(); QStringList skinFiles; for (auto url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; skinFiles << url.toLocalFile(); } installSkins(skinFiles); return true; } return false; } Qt::ItemFlags SkinList::flags(const QModelIndex& index) const { Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index); if (index.isValid()) { f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } return f; } QVariant SkinList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); if (row < 0 || row >= m_skinList.size()) return QVariant(); auto skin = m_skinList[row]; switch (role) { case Qt::DecorationRole: { auto preview = skin.getPreview(); if (preview.isNull()) { preview = skin.getTexture(); } return preview; } case Qt::DisplayRole: return skin.name(); case Qt::UserRole: return skin.name(); case Qt::EditRole: return skin.name(); default: return QVariant(); } } int SkinList::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_skinList.size(); } void SkinList::installSkins(const QStringList& iconFiles) { for (QString file : iconFiles) installSkin(file); } QString getUniqueFile(const QString& root, const QString& file) { auto result = FS::PathCombine(root, file); if (!QFileInfo::exists(result)) { return result; } QString baseName = QFileInfo(file).completeBaseName(); QString extension = QFileInfo(file).suffix(); int tries = 0; while (QFileInfo::exists(result)) { if (++tries > 256) return {}; QString key = QString("%1%2.%3").arg(baseName).arg(tries).arg(extension); result = FS::PathCombine(root, key); } return result; } QString SkinList::installSkin(const QString& file, const QString& name) { if (file.isEmpty()) return tr("Path is empty."); QFileInfo fileinfo(file); if (!fileinfo.exists()) return tr("File doesn't exist."); if (!fileinfo.isFile()) return tr("Not a file."); if (!fileinfo.isReadable()) return tr("File is not readable."); if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); QString target = getUniqueFile(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); return QFile::copy(file, target) ? "" : tr("Unable to copy file"); } int SkinList::getSkinIndex(const QString& key) const { for (int i = 0; i < m_skinList.count(); i++) { if (m_skinList[i].name() == key) { return i; } } return -1; } const SkinModel* SkinList::skin(const QString& key) const { int idx = getSkinIndex(key); if (idx == -1) return nullptr; return &m_skinList[idx]; } SkinModel* SkinList::skin(const QString& key) { int idx = getSkinIndex(key); if (idx == -1) return nullptr; return &m_skinList[idx]; } bool SkinList::deleteSkin(const QString& key, bool trash) { int idx = getSkinIndex(key); if (idx != -1) { auto s = m_skinList[idx]; if (trash) { if (FS::trash(s.getPath(), nullptr)) { m_skinList.remove(idx); save(); return true; } } else if (QFile::remove(s.getPath())) { m_skinList.remove(idx); save(); return true; } } return false; } void SkinList::save() { QJsonObject doc; QJsonArray arr; for (auto s : m_skinList) { arr << s.toJSON(); } doc["skins"] = arr; try { Json::write(doc, m_dir.absoluteFilePath("index.json")); } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write skin index file :" << e.cause(); } } int SkinList::getSelectedAccountSkin() { const auto& skin = m_acct->accountData()->minecraftProfile.skin; for (int i = 0; i < m_skinList.count(); i++) { if (m_skinList[i].getURL() == skin.url) { return i; } } return -1; } bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) { if (!idx.isValid() || role != Qt::EditRole) { return false; } int row = idx.row(); if (row < 0 || row >= m_skinList.size()) return false; auto& skin = m_skinList[row]; auto newName = value.toString(); if (skin.name() != newName) { if (!skin.rename(newName)) return false; save(); } return true; } void SkinList::updateSkin(SkinModel* s) { auto done = false; for (auto i = 0; i < m_skinList.size(); i++) { if (m_skinList[i].getPath() == s->getPath()) { m_skinList[i].setCapeId(s->getCapeId()); m_skinList[i].setModel(s->getModel()); m_skinList[i].setURL(s->getURL()); done = true; break; } } if (!done) { beginInsertRows(QModelIndex(), m_skinList.count(), m_skinList.count() + 1); m_skinList.append(*s); endInsertRows(); } save(); } PrismLauncher-10.0.5/launcher/minecraft/skins/SkinModel.h0000644000175100017510000000334715144136756022740 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include class SkinModel { public: enum Model { CLASSIC, SLIM }; SkinModel() = default; SkinModel(QString path); SkinModel(QDir skinDir, QJsonObject obj); virtual ~SkinModel() = default; QString name() const; QString getModelString() const; bool isValid() const; QString getPath() const { return m_path; } QImage getTexture() const { return m_texture; } QImage getPreview() const { return m_preview; } QString getCapeId() const { return m_capeId; } Model getModel() const { return m_model; } QString getURL() const { return m_url; } bool rename(QString newName); void setCapeId(QString capeID) { m_capeId = capeID; } void setModel(Model model); void setURL(QString url) { m_url = url; } void refresh(); QJsonObject toJSON() const; private: QString m_path; QImage m_texture; QImage m_preview; QString m_capeId; Model m_model; QString m_url; }; PrismLauncher-10.0.5/launcher/minecraft/skins/CapeChange.h0000644000175100017510000000220415144136756023020 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "net/NetRequest.h" class CapeChange : public Net::NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; CapeChange(QString capeId); virtual ~CapeChange() = default; static CapeChange::Ptr make(QString token, QString capeId); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; private: QString m_capeId; }; PrismLauncher-10.0.5/launcher/minecraft/skins/SkinUpload.h0000644000175100017510000000235415144136756023121 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "net/NetRequest.h" class SkinUpload : public Net::NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; // Note this class takes ownership of the file. SkinUpload(QString path, QString variant); virtual ~SkinUpload() = default; static SkinUpload::Ptr make(QString token, QString path, QString variant); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; private: QString m_path; QString m_variant; }; PrismLauncher-10.0.5/launcher/minecraft/skins/SkinUpload.cpp0000644000175100017510000000602615144136756023454 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "SkinUpload.h" #include #include "FileSystem.h" #include "net/ByteArraySink.h" #include "net/RawHeaderProxy.h" SkinUpload::SkinUpload(QString path, QString variant) : NetRequest(), m_path(path), m_variant(variant) { logCat = taskMCSkinsLogC; } QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) { QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); QHttpPart skin; skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); skin.setBody(FS::read(m_path)); QHttpPart model; model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); model.setBody(m_variant.toUtf8()); multiPart->append(skin); multiPart->append(model); setStatus(tr("Uploading skin")); return m_network->post(request, multiPart); } SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant) { auto up = makeShared(path, variant); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); up->setObjectName(QString("BYTES:") + up->m_url.toString()); up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); up->addHeaderProxy(new Net::RawHeaderProxy(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; } PrismLauncher-10.0.5/launcher/minecraft/skins/SkinDelete.h0000644000175100017510000000210315144136756023067 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "net/NetRequest.h" class SkinDelete : public Net::NetRequest { Q_OBJECT public: using Ptr = shared_qobject_ptr; SkinDelete(); virtual ~SkinDelete() = default; static SkinDelete::Ptr make(QString token); protected: virtual QNetworkReply* getReply(QNetworkRequest&) override; }; PrismLauncher-10.0.5/launcher/minecraft/skins/SkinList.h0000644000175100017510000000520515144136756022606 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include "QObjectPtr.h" #include "SkinModel.h" #include "minecraft/auth/MinecraftAccount.h" class SkinList : public QAbstractListModel { Q_OBJECT public: explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct); virtual ~SkinList() { save(); }; int getSkinIndex(const QString& key) const; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& idx, const QVariant& value, int role) override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual QStringList mimeTypes() const override; virtual Qt::DropActions supportedDropActions() const override; virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; bool deleteSkin(const QString& key, bool trash); void installSkins(const QStringList& iconFiles); QString installSkin(const QString& file, const QString& name = {}); const SkinModel* skin(const QString& key) const; SkinModel* skin(const QString& key); void startWatching(); void stopWatching(); QString getDir() const { return m_dir.absolutePath(); } void save(); int getSelectedAccountSkin(); void updateSkin(SkinModel* s); private: // hide copy constructor SkinList(const SkinList&) = delete; // hide assign op SkinList& operator=(const SkinList&) = delete; protected slots: void directoryChanged(const QString& path); void fileChanged(const QString& path); bool update(); private: shared_qobject_ptr m_watcher; bool m_isWatching; QList m_skinList; QDir m_dir; MinecraftAccountPtr m_acct; }; PrismLauncher-10.0.5/launcher/minecraft/skins/SkinDelete.cpp0000644000175100017510000000437515144136756023437 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "SkinDelete.h" #include "net/ByteArraySink.h" #include "net/RawHeaderProxy.h" SkinDelete::SkinDelete() : NetRequest() { logCat = taskMCSkinsLogC; } QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) { setStatus(tr("Deleting skin")); return m_network->deleteResource(request); } SkinDelete::Ptr SkinDelete::make(QString token) { auto up = makeShared(); up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"); up->m_sink.reset(new Net::ByteArraySink(std::make_shared())); up->addHeaderProxy(new Net::RawHeaderProxy(QList{ { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, })); return up; } PrismLauncher-10.0.5/launcher/minecraft/Logging.cpp0000644000175100017510000000170715144136756021643 0ustar runnerrunner // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "minecraft/Logging.h" Q_LOGGING_CATEGORY(instanceProfileC, "launcher.instance.profile") Q_LOGGING_CATEGORY(instanceProfileResolveC, "launcher.instance.profile.resolve") PrismLauncher-10.0.5/launcher/minecraft/ComponentUpdateTask_p.h0000644000175100017510000000120415144136756024161 0ustar runnerrunner#pragma once #include #include #include #include "net/Mode.h" #include "tasks/Task.h" #include "minecraft/ComponentUpdateTask.h" class PackProfile; struct RemoteLoadStatus { enum class Type { Index, List, Version } type = Type::Version; size_t PackProfileIndex = 0; bool finished = false; bool succeeded = false; Task::Ptr task; }; struct ComponentUpdateTaskData { PackProfile* m_profile = nullptr; QList remoteLoadStatusList; bool remoteLoadSuccessful = true; size_t remoteTasksInProgress = 0; ComponentUpdateTask::Mode mode; Net::Mode netmode; }; PrismLauncher-10.0.5/launcher/minecraft/MinecraftLoadAndCheck.cpp0000644000175100017510000000277515144136756024354 0ustar runnerrunner#include "MinecraftLoadAndCheck.h" #include "MinecraftInstance.h" #include "PackProfile.h" MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode) : m_inst(inst), m_netmode(netmode) {} void MinecraftLoadAndCheck::executeTask() { // add offline metadata load task auto components = m_inst->getPackProfile(); if (auto result = components->reload(m_netmode); !result) { emitFailed(result.error); return; } m_task = components->getCurrentTask(); if (!m_task) { emitSucceeded(); return; } connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::emitSucceeded); connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::emitFailed); connect(m_task.get(), &Task::aborted, this, [this] { emitFailed(tr("Aborted")); }); connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::setProgress); connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propagateStepProgress); connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); connect(m_task.get(), &Task::details, this, &MinecraftLoadAndCheck::setDetails); } bool MinecraftLoadAndCheck::canAbort() const { if (m_task) { return m_task->canAbort(); } return true; } bool MinecraftLoadAndCheck::abort() { if (m_task && m_task->canAbort()) { auto status = m_task->abort(); emitFailed("Aborted."); return status; } return Task::abort(); } PrismLauncher-10.0.5/launcher/minecraft/Component.h0000644000175100017510000001141615144136756021662 0ustar runnerrunner#pragma once #include #include #include #include #include #include #include "ProblemProvider.h" #include "QObjectPtr.h" #include "meta/JsonFormat.h" #include "modplatform/ModIndex.h" class PackProfile; class LaunchProfile; namespace Meta { class Version; class VersionList; } // namespace Meta class VersionFile; struct UpdateActionChangeVersion { /// version to change to QString targetVersion; }; struct UpdateActionLatestRecommendedCompatible { /// Parent uid QString parentUid; QString parentName; /// Parent version QString version; /// }; struct UpdateActionRemove {}; struct UpdateActionImportantChanged { QString oldVersion; }; using UpdateActionNone = std::monostate; using UpdateAction = std::variant; struct ModloaderMapEntry { ModPlatform::ModLoaderType type; QStringList knownConflictingComponents; }; class Component : public QObject, public ProblemProvider { Q_OBJECT public: Component(PackProfile* parent, const QString& uid); // DEPRECATED: remove these constructors? Component(PackProfile* parent, const QString& uid, std::shared_ptr file); virtual ~Component() {} static const QMap KNOWN_MODLOADERS; void applyTo(LaunchProfile* profile); bool isEnabled(); bool setEnabled(bool state); bool canBeDisabled(); bool isMoveable(); bool isCustomizable(); bool isRevertible(); bool isRemovable(); bool isCustom(); bool isVersionChangeable(bool wait = true); bool isKnownModloader(); QStringList knownConflictingComponents(); // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code void setOrder(int order); int getOrder(); QString getID(); QString getName(); QString getVersion(); std::shared_ptr getMeta(); QDateTime getReleaseDateTime(); QString getFilename(); std::shared_ptr getVersionFile() const; std::shared_ptr getVersionList() const; void setImportant(bool state); const QList getProblems() const override; ProblemSeverity getProblemSeverity() const override; void addComponentProblem(ProblemSeverity severity, const QString& description); void resetComponentProblems(); void setVersion(const QString& version); bool customize(); bool revert(); void updateCachedData(); void waitLoadMeta(); void setUpdateAction(const UpdateAction& action); void clearUpdateAction(); UpdateAction getUpdateAction(); signals: void dataChanged(); public: /* data */ PackProfile* m_parent; // BEGIN: persistent component list properties /// ID of the component QString m_uid; /// version of the component - when there's a custom json override, this is also the version the component reverts to QString m_version; /// if true, this has been added automatically to satisfy dependencies and may be automatically removed bool m_dependencyOnly = false; /// if true, the component is either the main component of the instance, or otherwise important and cannot be removed. bool m_important = false; /// if true, the component is disabled bool m_disabled = false; /// cached name for display purposes, taken from the version file (meta or local override) QString m_cachedName; /// cached version for display AND other purposes, taken from the version file (meta or local override) QString m_cachedVersion; /// cached set of requirements, taken from the version file (meta or local override) Meta::RequireSet m_cachedRequires; Meta::RequireSet m_cachedConflicts; /// if true, the component is volatile and may be automatically removed when no longer needed bool m_cachedVolatile = false; // END: persistent component list properties // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code bool m_orderOverride = false; int m_order = 0; // load state std::shared_ptr m_metaVersion; std::shared_ptr m_file; bool m_loaded = false; private: QList m_componentProblems; ProblemSeverity m_componentProblemSeverity = ProblemSeverity::None; UpdateAction m_updateAction = UpdateAction{ UpdateActionNone{} }; }; using ComponentPtr = shared_qobject_ptr; PrismLauncher-10.0.5/launcher/minecraft/OneSixVersionFormat.h0000644000175100017510000000257115144136756023646 0ustar runnerrunner#pragma once #include #include #include #include #include class OneSixVersionFormat { public: // version files / profile patches static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename, bool requireOrder); static QJsonDocument versionFileToJson(const VersionFilePtr& patch); // libraries static LibraryPtr libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); static QJsonObject libraryToJson(Library* library); // DEPRECATED: old 'plus' jar mods generated by the application static LibraryPtr plusJarModFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename, const QString& originalName); // new jar mods derived from libraries static LibraryPtr jarModFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); static QJsonObject jarModtoJson(Library* jarmod); // mods, also derived from libraries static LibraryPtr modFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); static QJsonObject modtoJson(Library* jarmod); }; PrismLauncher-10.0.5/launcher/minecraft/MojangDownloadInfo.h0000644000175100017510000000352115144136756023435 0ustar runnerrunner#pragma once #include #include #include struct MojangDownloadInfo { // types using Ptr = std::shared_ptr; // data /// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested! QString path; /// absolute URL of this file QString url; /// sha-1 checksum of the file QString sha1; /// size of the file in bytes int size; }; struct MojangLibraryDownloadInfo { MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact_) : artifact(artifact_) {} MojangLibraryDownloadInfo() {} // types using Ptr = std::shared_ptr; // methods MojangDownloadInfo* getDownloadInfo(QString classifier) { if (classifier.isNull()) { return artifact.get(); } return classifiers[classifier].get(); } // data MojangDownloadInfo::Ptr artifact; QMap classifiers; }; struct MojangAssetIndexInfo : public MojangDownloadInfo { // types using Ptr = std::shared_ptr; // methods MojangAssetIndexInfo() {} MojangAssetIndexInfo(QString id_) { this->id = id_; // HACK: ignore assets from other version files than Minecraft // workaround for stupid assets issue caused by amazon: // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ if (id_ == "legacy") { url = "https://piston-meta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"; } // HACK else { url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id_ + ".json"; } known = false; } // data int totalSize; QString id; bool known = true; }; PrismLauncher-10.0.5/launcher/minecraft/PackProfile.cpp0000644000175100017510000010472215144136756022455 0ustar runnerrunner// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022-2023 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include #include #include #include #include #include "Application.h" #include "Exception.h" #include "FileSystem.h" #include "Json.h" #include "meta/Index.h" #include "meta/JsonFormat.h" #include "minecraft/Component.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/ProfileUtils.h" #include "ComponentUpdateTask.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "modplatform/ModIndex.h" #include "minecraft/Logging.h" #include "ui/dialogs/CustomMessageBox.h" PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel() { d.reset(new PackProfileData); d->m_instance = instance; d->m_saveTimer.setSingleShot(true); d->m_saveTimer.setInterval(5000); d->interactionDisabled = instance->isRunning(); connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction); connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal); } PackProfile::~PackProfile() { saveNow(); } // BEGIN: component file format static const int currentComponentsFileVersion = 1; static QJsonObject componentToJsonV1(ComponentPtr component) { QJsonObject obj; // critical obj.insert("uid", component->m_uid); if (!component->m_version.isEmpty()) { obj.insert("version", component->m_version); } if (component->m_dependencyOnly) { obj.insert("dependencyOnly", true); } if (component->m_important) { obj.insert("important", true); } if (component->m_disabled) { obj.insert("disabled", true); } // cached if (!component->m_cachedVersion.isEmpty()) { obj.insert("cachedVersion", component->m_cachedVersion); } if (!component->m_cachedName.isEmpty()) { obj.insert("cachedName", component->m_cachedName); } Meta::serializeRequires(obj, &component->m_cachedRequires, "cachedRequires"); Meta::serializeRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); if (component->m_cachedVolatile) { obj.insert("cachedVolatile", true); } return obj; } static ComponentPtr componentFromJsonV1(PackProfile* parent, const QString& componentJsonPattern, const QJsonObject& obj) { // critical auto uid = Json::requireString(obj.value("uid")); auto filePath = componentJsonPattern.arg(uid); auto component = makeShared(parent, uid); component->m_version = obj.value("version").toString(); component->m_dependencyOnly = obj.value("dependencyOnly").toBool(); component->m_important = obj.value("important").toBool(); // cached // TODO @RESILIENCE: ignore invalid values/structure here? component->m_cachedVersion = obj.value("cachedVersion").toString(); component->m_cachedName = obj.value("cachedName").toString(); Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires"); Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); component->m_cachedVolatile = obj.value("volatile").toBool(); bool disabled = obj.value("disabled").toBool(); component->setEnabled(!disabled); return component; } // Save the given component container data to a file static bool savePackProfile(const QString& filename, const ComponentContainer& container) { QJsonObject obj; obj.insert("formatVersion", currentComponentsFileVersion); QJsonArray orderArray; for (auto component : container) { orderArray.append(componentToJsonV1(component)); } obj.insert("components", orderArray); QSaveFile outFile(filename); if (!outFile.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << "Couldn't open" << outFile.fileName() << "for writing:" << outFile.errorString(); return false; } auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); if (outFile.write(data) != data.size()) { qCCritical(instanceProfileC) << "Couldn't write all the data into" << outFile.fileName() << "because:" << outFile.errorString(); return false; } if (!outFile.commit()) { qCCritical(instanceProfileC) << "Couldn't save" << outFile.fileName() << "because:" << outFile.errorString(); return false; } return true; } // Read the given file into component containers static PackProfile::Result loadPackProfile(PackProfile* parent, const QString& filename, const QString& componentJsonPattern, ComponentContainer& container) { QFile componentsFile(filename); if (!componentsFile.exists()) { auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename); qCWarning(instanceProfileC) << message; return PackProfile::Result::Error(message); } if (!componentsFile.open(QFile::ReadOnly)) { auto message = QObject::tr("Couldn't open %1 for reading: %2").arg(componentsFile.fileName(), componentsFile.errorString()); qCCritical(instanceProfileC) << message; qCWarning(instanceProfileC) << "Ignoring overridden order"; return PackProfile::Result::Error(message); } // and it's valid JSON QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString()); qCCritical(instanceProfileC) << message; qCWarning(instanceProfileC) << "Ignoring overridden order"; return PackProfile::Result::Error(message); } // and then read it and process it if all above is true. try { auto obj = Json::requireObject(doc); // check order file version. auto version = Json::requireInteger(obj.value("formatVersion")); if (version != currentComponentsFileVersion) { throw JSONValidationError(QObject::tr("Invalid component file version, expected %1").arg(currentComponentsFileVersion)); } auto orderArray = Json::requireArray(obj.value("components")); for (auto item : orderArray) { auto comp_obj = Json::requireObject(item, "Component must be an object."); container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); } } catch ([[maybe_unused]] const JSONValidationError& err) { auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName()); qCCritical(instanceProfileC) << message; qCWarning(instanceProfileC) << "error:" << err.what(); container.clear(); return PackProfile::Result::Error(message); } return PackProfile::Result::Success(); } // END: component file format // BEGIN: save/load logic void PackProfile::saveNow() { if (saveIsScheduled() && save_internal()) { d->m_saveTimer.stop(); } } bool PackProfile::saveIsScheduled() const { return d->dirty; } void PackProfile::buildingFromScratch() { d->loaded = true; d->dirty = true; } void PackProfile::scheduleSave() { if (!d->loaded) { qDebug() << d->m_instance->name() << "|" << "Component list should never save if it didn't successfully load"; return; } if (!d->dirty) { d->dirty = true; qDebug() << d->m_instance->name() << "|" << "Component list save is scheduled"; } d->m_saveTimer.start(); } RuntimeContext PackProfile::runtimeContext() { return d->m_instance->runtimeContext(); } QString PackProfile::componentsFilePath() const { return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); } QString PackProfile::patchesPattern() const { return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); } QString PackProfile::patchFilePathForUid(const QString& uid) const { return patchesPattern().arg(uid); } bool PackProfile::save_internal() { qDebug() << d->m_instance->name() << "|" << "Component list save performed now"; auto filename = componentsFilePath(); if (savePackProfile(filename, d->components)) { d->dirty = false; return true; } return false; } PackProfile::Result PackProfile::load() { auto filename = componentsFilePath(); // load the new component list and swap it with the current one... ComponentContainer newComponents; if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) { qCritical() << d->m_instance->name() << "|" << "Failed to load the component config"; return result; } // FIXME: actually use fine-grained updates, not this... beginResetModel(); // disconnect all the old components for (auto component : d->components) { disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); } d->components.clear(); d->componentIndex.clear(); for (auto component : newComponents) { if (d->componentIndex.contains(component->m_uid)) { qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid; continue; } connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); d->components.append(component); d->componentIndex[component->m_uid] = component; } endResetModel(); d->loaded = true; return Result::Success(); } PackProfile::Result PackProfile::reload(Net::Mode netmode) { // Do not reload when the update/resolve task is running. It is in control. if (d->m_updateTask) { return Result::Success(); } // flush any scheduled saves to not lose state saveNow(); // FIXME: differentiate when a reapply is required by propagating state from components invalidateLaunchProfile(); if (auto result = load(); !result) { return result; } resolve(netmode); return Result::Success(); } Task::Ptr PackProfile::getCurrentTask() { return d->m_updateTask; } void PackProfile::resolve(Net::Mode netmode) { auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this); d->m_updateTask.reset(updateTask); connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded); connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed); connect(updateTask, &ComponentUpdateTask::aborted, this, [this] { updateFailed(tr("Aborted")); }); d->m_updateTask->start(); } void PackProfile::updateSucceeded() { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Component list update/resolve task succeeded"; d->m_updateTask.reset(); invalidateLaunchProfile(); } void PackProfile::updateFailed(const QString& error) { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Component list update/resolve task failed. Reason:" << error; d->m_updateTask.reset(); invalidateLaunchProfile(); } // END: save/load void PackProfile::appendComponent(ComponentPtr component) { insertComponent(d->components.size(), component); } void PackProfile::insertComponent(size_t index, ComponentPtr component) { auto id = component->getID(); if (id.isEmpty()) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Attempt to add a component with empty ID!"; return; } if (d->componentIndex.contains(id)) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Attempt to add a component that is already present!"; return; } beginInsertRows(QModelIndex(), static_cast(index), static_cast(index)); d->components.insert(index, component); d->componentIndex[id] = component; endInsertRows(); connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); scheduleSave(); } void PackProfile::componentDataChanged() { auto objPtr = qobject_cast(sender()); if (!objPtr) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "PackProfile got dataChanged signal from a non-Component!"; return; } if (objPtr->getID() == "net.minecraft") { emit minecraftChanged(); } // figure out which one is it... in a seriously dumb way. int index = 0; for (auto component : d->components) { if (component.get() == objPtr) { emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); scheduleSave(); return; } index++; } qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "PackProfile got dataChanged signal from a Component which does not belong to it!"; } bool PackProfile::remove(const int index) { auto patch = getComponent(index); if (!patch->isRemovable()) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is non-removable"; return false; } if (!removeComponent_internal(patch)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be removed"; return false; } beginRemoveRows(QModelIndex(), index, index); d->components.removeAt(index); d->componentIndex.remove(patch->getID()); endRemoveRows(); invalidateLaunchProfile(); scheduleSave(); return true; } bool PackProfile::remove(const QString& id) { int i = 0; for (auto patch : d->components) { if (patch->getID() == id) { return remove(i); } i++; } return false; } bool PackProfile::customize(int index) { auto patch = getComponent(index); if (!patch->isCustomizable()) { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is not customizable"; return false; } if (!patch->customize()) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be customized"; return false; } invalidateLaunchProfile(); scheduleSave(); return true; } bool PackProfile::revertToBase(int index) { auto patch = getComponent(index); if (!patch->isRevertible()) { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "is not revertible"; return false; } if (!patch->revert()) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Patch" << patch->getID() << "could not be reverted"; return false; } invalidateLaunchProfile(); scheduleSave(); return true; } ComponentPtr PackProfile::getComponent(const QString& id) { auto iter = d->componentIndex.find(id); if (iter == d->componentIndex.end()) { return nullptr; } return (*iter); } ComponentPtr PackProfile::getComponent(size_t index) { if (index >= static_cast(d->components.size())) { return nullptr; } return d->components[index]; } QVariant PackProfile::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); int column = index.column(); if (row < 0 || row >= d->components.size()) return QVariant(); auto patch = d->components.at(row); switch (role) { case Qt::CheckStateRole: { if (column == NameColumn) return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; return QVariant(); } case Qt::DisplayRole: { switch (column) { case NameColumn: return patch->getName(); case VersionColumn: { if (patch->isCustom()) { return QString("%1 (Custom)").arg(patch->getVersion()); } else { return patch->getVersion(); } } default: return QVariant(); } } case Qt::DecorationRole: { if (column == NameColumn) { auto severity = patch->getProblemSeverity(); switch (severity) { case ProblemSeverity::Warning: return "warning"; case ProblemSeverity::Error: return "error"; default: return QVariant(); } } return QVariant(); } } return QVariant(); } bool PackProfile::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())) { return false; } if (role == Qt::CheckStateRole) { auto component = d->components[index.row()]; if (component->setEnabled(!component->isEnabled())) { return true; } } return false; } QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { if (role == Qt::DisplayRole) { switch (section) { case NameColumn: return tr("Name"); case VersionColumn: return tr("Version"); default: return QVariant(); } } } return QVariant(); } // FIXME: zero precision mess Qt::ItemFlags PackProfile::flags(const QModelIndex& index) const { if (!index.isValid()) { return Qt::NoItemFlags; } Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; int row = index.row(); if (row < 0 || row >= d->components.size()) { return Qt::NoItemFlags; } auto patch = d->components.at(row); // TODO: this will need fine-tuning later... if (patch->canBeDisabled() && !d->interactionDisabled) { outFlags |= Qt::ItemIsUserCheckable; } return outFlags; } int PackProfile::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : d->components.size(); } int PackProfile::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : NUM_COLUMNS; } void PackProfile::move(const int index, const MoveDirection direction) { int theirIndex; if (direction == MoveUp) { theirIndex = index - 1; } else { theirIndex = index + 1; } if (index < 0 || index >= d->components.size()) return; if (theirIndex >= rowCount()) theirIndex = rowCount() - 1; if (theirIndex == -1) theirIndex = rowCount() - 1; if (index == theirIndex) return; int togap = theirIndex > index ? theirIndex + 1 : theirIndex; auto from = getComponent(index); auto to = getComponent(theirIndex); if (!from || !to || !to->isMoveable() || !from->isMoveable()) { return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); d->components.swapItemsAt(index, theirIndex); endMoveRows(); invalidateLaunchProfile(); scheduleSave(); } void PackProfile::invalidateLaunchProfile() { d->m_profile.reset(); } void PackProfile::installJarMods(QStringList selectedFiles) { // FIXME: get rid of _internal installJarMods_internal(selectedFiles); } void PackProfile::installCustomJar(QString selectedFile) { // FIXME: get rid of _internal installCustomJar_internal(selectedFile); } bool PackProfile::installComponents(QStringList selectedFiles) { const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) return false; bool result = true; for (const QString& source : selectedFiles) { const QFileInfo sourceInfo(source); auto versionFile = ProfileUtils::parseJsonFile(sourceInfo, false); const QString target = FS::PathCombine(patchDir, versionFile->uid + ".json"); if (!QFile::copy(source, target)) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Component" << source << "could not be copied to target" << target; result = false; continue; } appendComponent(makeShared(this, versionFile->uid, versionFile)); } scheduleSave(); invalidateLaunchProfile(); return result; } void PackProfile::installAgents(QStringList selectedFiles) { // FIXME: get rid of _internal installAgents_internal(selectedFiles); } bool PackProfile::installEmpty(const QString& uid, const QString& name) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } auto f = std::make_shared(); f->name = name; f->uid = uid; f->version = "1"; QString patchFileName = FS::PathCombine(patchDir, uid + ".json"); QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); appendComponent(makeShared(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); return true; } bool PackProfile::removeComponent_internal(ComponentPtr patch) { bool ok = true; // first, remove the patch file. this ensures it's not used anymore auto fileName = patch->getFilename(); if (fileName.size()) { QFile patchFile(fileName); if (patchFile.exists() && !patchFile.remove()) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "File" << fileName << "could not be removed because:" << patchFile.errorString(); return false; } } // FIXME: we need a generic way of removing local resources, not just jar mods... auto preRemoveJarMod = [this](LibraryPtr jarMod) -> bool { if (!jarMod->isLocal()) { return true; } QStringList jar, temp1, temp2, temp3; jarMod->getApplicableFiles(d->m_instance->runtimeContext(), jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath()); QFileInfo finfo(jar[0]); if (finfo.exists()) { QFile jarModFile(jar[0]); if (!jarModFile.remove()) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "File" << jar[0] << "could not be removed because:" << jarModFile.errorString(); return false; } return true; } return true; }; auto vFile = patch->getVersionFile(); if (vFile) { auto& jarMods = vFile->jarMods; for (auto& jarmod : jarMods) { ok &= preRemoveJarMod(jarmod); } } return ok; } bool PackProfile::installJarMods_internal(QStringList filepaths) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir())) { return false; } for (auto filepath : filepaths) { QFileInfo sourceInfo(filepath); QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); QString target_filename = id + ".jar"; QString target_id = "custom.jarmod." + id; QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename); QFileInfo targetInfo(finalPath); Q_ASSERT(!targetInfo.exists()); if (!QFile::copy(sourceInfo.absoluteFilePath(), QFileInfo(finalPath).absoluteFilePath())) { return false; } auto f = std::make_shared(); auto jarMod = std::make_shared(); jarMod->setRawName(GradleSpecifier("custom.jarmods:" + id + ":1")); jarMod->setFilename(target_filename); jarMod->setDisplayName(sourceInfo.completeBaseName()); jarMod->setHint("local"); f->jarMods.append(jarMod); f->name = target_name; f->uid = target_id; QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); appendComponent(makeShared(this, f->uid, f)); } scheduleSave(); invalidateLaunchProfile(); return true; } bool PackProfile::installCustomJar_internal(QString filepath) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } QString libDir = d->m_instance->getLocalLibraryPath(); if (!FS::ensureFolderPathExists(libDir)) { return false; } auto specifier = GradleSpecifier("custom:customjar:1"); QFileInfo sourceInfo(filepath); QString target_filename = specifier.getFileName(); QString target_id = specifier.artifactId(); QString target_name = sourceInfo.completeBaseName() + " (custom jar)"; QString finalPath = FS::PathCombine(libDir, target_filename); QFileInfo jarInfo(finalPath); if (jarInfo.exists()) { if (!FS::deletePath(finalPath)) { return false; } } if (!QFile::copy(filepath, finalPath)) { return false; } auto f = std::make_shared(); auto jarMod = std::make_shared(); jarMod->setRawName(specifier); jarMod->setDisplayName(sourceInfo.completeBaseName()); jarMod->setHint("local"); f->mainJar = jarMod; f->name = target_name; f->uid = target_id; QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); appendComponent(makeShared(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); return true; } bool PackProfile::installAgents_internal(QStringList filepaths) { // FIXME code duplication const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); if (!FS::ensureFolderPathExists(patchDir)) return false; const QString libDir = d->m_instance->getLocalLibraryPath(); if (!FS::ensureFolderPathExists(libDir)) return false; for (const QString& source : filepaths) { const QFileInfo sourceInfo(source); const QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); const QString targetBaseName = id + ".jar"; const QString targetId = "custom.agent." + id; const QString targetName = sourceInfo.completeBaseName() + " (agent)"; const QString target = FS::PathCombine(d->m_instance->getLocalLibraryPath(), targetBaseName); const QFileInfo targetInfo(target); Q_ASSERT(!targetInfo.exists()); if (!QFile::copy(source, target)) return false; auto versionFile = std::make_shared(); auto agent = std::make_shared(); agent->setRawName("custom.agents:" + id + ":1"); agent->setFilename(targetBaseName); agent->setDisplayName(sourceInfo.completeBaseName()); agent->setHint("local"); versionFile->agents.append(std::make_shared(agent, QString())); versionFile->name = targetName; versionFile->uid = targetId; QFile patchFile(FS::PathCombine(patchDir, targetId + ".json")); if (!patchFile.open(QFile::WriteOnly)) { qCCritical(instanceProfileC) << d->m_instance->name() << "|" << "Error opening" << patchFile.fileName() << "for reading:" << patchFile.errorString(); return false; } patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); patchFile.close(); appendComponent(makeShared(this, versionFile->uid, versionFile)); } scheduleSave(); invalidateLaunchProfile(); return true; } std::shared_ptr PackProfile::getProfile() const { if (!d->m_profile) { try { auto profile = std::make_shared(); for (auto file : d->components) { qCDebug(instanceProfileC) << d->m_instance->name() << "|" << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); file->applyTo(profile.get()); } d->m_profile = profile; } catch (const Exception& error) { qCWarning(instanceProfileC) << d->m_instance->name() << "|" << "Couldn't apply profile patches because:" << error.cause(); } } return d->m_profile; } bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important) { auto iter = d->componentIndex.find(uid); if (iter != d->componentIndex.end()) { ComponentPtr component = *iter; // set existing if (component->revert()) { // set new version auto oldVersion = component->getVersion(); component->setVersion(version); component->setImportant(important); if (important) { component->setUpdateAction(UpdateAction{ UpdateActionImportantChanged{ oldVersion } }); resolve(Net::Mode::Online); } return true; } return false; } else { // add new auto component = makeShared(this, uid); component->m_version = version; component->m_important = important; appendComponent(component); return true; } } QString PackProfile::getComponentVersion(const QString& uid) const { const auto iter = d->componentIndex.find(uid); if (iter != d->componentIndex.end()) { return (*iter)->getVersion(); } return QString(); } void PackProfile::disableInteraction(bool disable) { if (d->interactionDisabled != disable) { d->interactionDisabled = disable; auto size = d->components.size(); if (size) { emit dataChanged(index(0), index(size - 1)); } } } std::optional PackProfile::getModLoaders() { ModPlatform::ModLoaderTypes result; bool has_any_loader = false; QMapIterator i(Component::KNOWN_MODLOADERS); while (i.hasNext()) { i.next(); if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) { result |= i.value().type; has_any_loader = true; } } if (!has_any_loader) return {}; return result; } std::optional PackProfile::getSupportedModLoaders() { auto loadersOpt = getModLoaders(); if (!loadersOpt.has_value()) return loadersOpt; auto loaders = loadersOpt.value(); // TODO: remove this or add version condition once Quilt drops official Fabric support if (loaders & ModPlatform::Quilt) loaders |= ModPlatform::Fabric; if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge)) loaders |= ModPlatform::Forge; return loaders; } QList PackProfile::getModLoadersList() { QList result; for (auto c : d->components) { if (c->isEnabled() && Component::KNOWN_MODLOADERS.contains(c->getID())) { result.append(Component::KNOWN_MODLOADERS[c->getID()].type); } } // TODO: remove this or add version condition once Quilt drops official Fabric support if (result.contains(ModPlatform::Quilt) && !result.contains(ModPlatform::Fabric)) { result.append(ModPlatform::Fabric); } if (getComponentVersion("net.minecraft") == "1.20.1" && result.contains(ModPlatform::NeoForge) && !result.contains(ModPlatform::Forge)) { result.append(ModPlatform::Forge); } return result; } PrismLauncher-10.0.5/launcher/minecraft/Rule.cpp0000644000175100017510000000521615144136756021163 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2025 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include "Rule.h" Rule Rule::fromJson(const QJsonObject& object) { Rule result; if (object["action"] == "allow") result.m_action = Allow; else if (object["action"] == "disallow") result.m_action = Disallow; if (auto os = object["os"]; os.isObject()) { if (auto name = os["name"].toString(); !name.isNull()) { result.m_os = OS{ name, os["version"].toString(), }; } } return result; } QJsonObject Rule::toJson() { QJsonObject result; if (m_action == Allow) result["action"] = "allow"; else if (m_action == Disallow) result["action"] = "disallow"; if (m_os.has_value()) { QJsonObject os; os["name"] = m_os->name; if (!m_os->version.isEmpty()) os["version"] = m_os->version; result["os"] = os; } return result; } Rule::Action Rule::apply(const RuntimeContext& runtimeContext) { if (m_os.has_value() && !runtimeContext.classifierMatches(m_os->name)) return Defer; return m_action; } PrismLauncher-10.0.5/launcher/minecraft/PackProfile.h0000644000175100017510000001473415144136756022125 0ustar runnerrunner// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022-2023 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include "Component.h" #include "LaunchProfile.h" #include "modplatform/ModIndex.h" #include "net/Mode.h" class MinecraftInstance; struct PackProfileData; class ComponentUpdateTask; class PackProfile : public QAbstractListModel { Q_OBJECT friend ComponentUpdateTask; public: enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS }; struct Result { bool success; QString error; // Implicit conversion to bool operator bool() const { return success; } // Factory methods for convenience static Result Success() { return { true, "" }; } static Result Error(const QString& errorMessage) { return { false, errorMessage }; } }; explicit PackProfile(MinecraftInstance* instance); virtual ~PackProfile(); virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual int columnCount(const QModelIndex& parent) const override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch. void buildingFromScratch(); /// install more jar mods void installJarMods(QStringList selectedFiles); /// install a jar/zip as a replacement for the main jar void installCustomJar(QString selectedFile); /// install MMC/Prism component files bool installComponents(QStringList selectedFiles); /// install Java agent files void installAgents(QStringList selectedFiles); enum MoveDirection { MoveUp, MoveDown }; /// move component file # up or down the list void move(int index, MoveDirection direction); /// remove component file # - including files/records bool remove(int index); /// remove component file by id - including files/records bool remove(const QString& id); bool customize(int index); bool revertToBase(int index); /// reload the list, reload all components, resolve dependencies Result reload(Net::Mode netmode); // reload all components, resolve dependencies void resolve(Net::Mode netmode); /// get current running task... Task::Ptr getCurrentTask(); std::shared_ptr getProfile() const; // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config void setOldConfigVersion(const QString& uid, const QString& version); QString getComponentVersion(const QString& uid) const; bool setComponentVersion(const QString& uid, const QString& version, bool important = false); bool installEmpty(const QString& uid, const QString& name); QString patchFilePathForUid(const QString& uid) const; /// if there is a save scheduled, do it now. void saveNow(); /// helper method, returns RuntimeContext of instance RuntimeContext runtimeContext(); signals: void minecraftChanged(); public: /// get the profile component by id ComponentPtr getComponent(const QString& id); /// get the profile component by index ComponentPtr getComponent(size_t index); /// Add the component to the internal list of patches // todo(merged): is this the best approach void appendComponent(ComponentPtr component); std::optional getModLoaders(); // this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge) std::optional getSupportedModLoaders(); QList getModLoadersList(); /// apply the component patches. Catches all the errors and returns true/false for success/failure void invalidateLaunchProfile(); private: void scheduleSave(); bool saveIsScheduled() const; /// insert component so that its index is ideally the specified one (returns real index) void insertComponent(size_t index, ComponentPtr component); QString componentsFilePath() const; QString patchesPattern() const; private slots: bool save_internal(); void updateSucceeded(); void updateFailed(const QString& error); void componentDataChanged(); void disableInteraction(bool disable); private: Result load(); bool installJarMods_internal(QStringList filepaths); bool installCustomJar_internal(QString filepath); bool installAgents_internal(QStringList filepaths); bool removeComponent_internal(ComponentPtr patch); private: /* data */ std::unique_ptr d; }; PrismLauncher-10.0.5/launcher/minecraft/ProfileUtils.cpp0000644000175100017510000001424315144136756022675 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ProfileUtils.h" #include #include "Json.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/VersionFilterData.h" #include #include #include namespace ProfileUtils { static const int currentOrderFileVersion = 1; bool readOverrideOrders(QString path, PatchOrder& order) { QFile orderFile(path); if (!orderFile.exists()) { qWarning() << "Order file doesn't exist. Ignoring."; return false; } if (!orderFile.open(QFile::ReadOnly)) { qCritical() << "Couldn't open" << orderFile.fileName() << "for reading:" << orderFile.errorString(); qWarning() << "Ignoring overridden order"; return false; } // and it's valid JSON QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); if (error.error != QJsonParseError::NoError) { qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); qWarning() << "Ignoring overridden order"; return false; } // and then read it and process it if all above is true. try { auto obj = Json::requireObject(doc); // check order file version. auto version = Json::requireInteger(obj.value("version")); if (version != currentOrderFileVersion) { throw JSONValidationError(QObject::tr("Invalid order file version, expected %1").arg(currentOrderFileVersion)); } auto orderArray = Json::requireArray(obj.value("order")); for (auto item : orderArray) { order.append(Json::requireString(item)); } } catch ([[maybe_unused]] const JSONValidationError& err) { qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; qWarning() << "Ignoring overridden order"; order.clear(); return false; } return true; } static VersionFilePtr createErrorVersionFile(QString fileId, QString filepath, QString error) { auto outError = std::make_shared(); outError->uid = outError->name = fileId; // outError->filename = filepath; outError->addProblem(ProblemSeverity::Error, error); return outError; } static VersionFilePtr guardedParseJson(const QJsonDocument& doc, const QString& fileId, const QString& filepath, const bool& requireOrder) { try { return OneSixVersionFormat::versionFileFromJson(doc, filepath, requireOrder); } catch (const Exception& e) { return createErrorVersionFile(fileId, filepath, e.cause()); } } VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, const bool requireOrder) { QFile file(fileInfo.absoluteFilePath()); if (!file.open(QFile::ReadOnly)) { auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); } QJsonParseError error; auto data = file.readAll(); QJsonDocument doc = QJsonDocument::fromJson(data, &error); file.close(); if (error.error != QJsonParseError::NoError) { int line = 1; int column = 0; for (int i = 0; i < error.offset; i++) { if (data[i] == '\n') { line++; column = 0; continue; } column++; } auto errorStr = QObject::tr("Unable to process the version file %1: %2 at line %3 column %4.") .arg(fileInfo.fileName(), error.errorString()) .arg(line) .arg(column); return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); } return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); } bool saveJsonFile(const QJsonDocument& doc, const QString& filename) { auto data = doc.toJson(); QSaveFile jsonFile(filename); if (!jsonFile.open(QIODevice::WriteOnly)) { jsonFile.cancelWriting(); qWarning() << "Couldn't open" << filename << "for writing"; return false; } jsonFile.write(data); if (!jsonFile.commit()) { qWarning() << "Couldn't save" << filename; return false; } return true; } void removeLwjglFromPatch(VersionFilePtr patch) { auto filter = [](QList& libs) { QList filteredLibs; for (auto lib : libs) { if (!g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) { filteredLibs.append(lib); } } libs = filteredLibs; }; filter(patch->libraries); } } // namespace ProfileUtils PrismLauncher-10.0.5/launcher/minecraft/GradleSpecifier.h0000644000175100017510000001250415144136756022747 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "DefaultVariable.h" struct GradleSpecifier { GradleSpecifier() { m_valid = false; } GradleSpecifier(QString value) { operator=(value); } GradleSpecifier& operator=(const QString& value) { /* org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" 1 "org.gradle.test.classifiers" 2 "service" 3 "1.0" 4 "jdk15" 5 "jar" */ static const QRegularExpression s_matcher( QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?")); QRegularExpressionMatch match = s_matcher.match(value); m_valid = match.hasMatch(); if (!m_valid) { m_invalidValue = value; return *this; } auto elements = match.captured(); m_groupId = match.captured(1); m_artifactId = match.captured(2); m_version = match.captured(3); m_classifier = match.captured(4); if (match.lastCapturedIndex() >= 5) { m_extension = match.captured(5); } return *this; } QString serialize() const { if (!m_valid) { return m_invalidValue; } QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; if (!m_classifier.isEmpty()) { retval += ":" + m_classifier; } if (m_extension.isExplicit()) { retval += "@" + m_extension; } return retval; } QString getFileName() const { if (!m_valid) { return QString(); } QString filename = m_artifactId + '-' + m_version; if (!m_classifier.isEmpty()) { filename += "-" + m_classifier; } filename += "." + m_extension; return filename; } QString toPath(const QString& filenameOverride = QString()) const { if (!m_valid) { return QString(); } QString filename; if (filenameOverride.isEmpty()) { filename = getFileName(); } else { filename = filenameOverride; } QString path = m_groupId; path.replace('.', '/'); path += '/' + m_artifactId + '/' + m_version + '/' + filename; return path; } inline bool valid() const { return m_valid; } inline QString version() const { return m_version; } inline QString groupId() const { return m_groupId; } inline QString artifactId() const { return m_artifactId; } inline void setClassifier(const QString& classifier) { m_classifier = classifier; } inline QString classifier() const { return m_classifier; } inline QString extension() const { return m_extension; } inline QString artifactPrefix() const { return m_groupId + ":" + m_artifactId; } bool matchName(const GradleSpecifier& other) const { return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); } bool operator==(const GradleSpecifier& other) const { if (m_groupId != other.m_groupId) return false; if (m_artifactId != other.m_artifactId) return false; if (m_version != other.m_version) return false; if (m_classifier != other.m_classifier) return false; if (m_extension != other.m_extension) return false; return true; } private: QString m_invalidValue; QString m_groupId; QString m_artifactId; QString m_version; QString m_classifier; DefaultVariable m_extension = DefaultVariable("jar"); bool m_valid = false; }; PrismLauncher-10.0.5/launcher/minecraft/Component.cpp0000644000175100017510000003041015144136756022210 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Component.h" #include #include #include #include "Application.h" #include "FileSystem.h" #include "OneSixVersionFormat.h" #include "VersionFile.h" #include "meta/Version.h" #include "minecraft/Component.h" #include "minecraft/PackProfile.h" #include const QMap Component::KNOWN_MODLOADERS = { { "net.neoforged", { ModPlatform::NeoForge, { "net.minecraftforge", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, { "net.minecraftforge", { ModPlatform::Forge, { "net.neoforged", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, { "net.fabricmc.fabric-loader", { ModPlatform::Fabric, { "net.minecraftforge", "net.neoforged", "org.quiltmc.quilt-loader" } } }, { "org.quiltmc.quilt-loader", { ModPlatform::Quilt, { "net.minecraftforge", "net.neoforged", "net.fabricmc.fabric-loader" } } }, { "com.mumfrey.liteloader", { ModPlatform::LiteLoader, {} } } }; Component::Component(PackProfile* parent, const QString& uid) { assert(parent); m_parent = parent; m_uid = uid; } Component::Component(PackProfile* parent, const QString& uid, std::shared_ptr file) { assert(parent); m_parent = parent; m_file = file; m_uid = uid; m_cachedVersion = m_file->version; m_cachedName = m_file->name; m_loaded = true; } std::shared_ptr Component::getMeta() { return m_metaVersion; } void Component::applyTo(LaunchProfile* profile) { // do not apply disabled components if (!isEnabled()) { return; } auto vfile = getVersionFile(); if (vfile) { vfile->applyTo(profile, m_parent->runtimeContext()); } else { profile->applyProblemSeverity(getProblemSeverity()); } } std::shared_ptr Component::getVersionFile() const { if (m_metaVersion) { return m_metaVersion->data(); } else { return m_file; } } std::shared_ptr Component::getVersionList() const { // FIXME: what if the metadata index isn't loaded yet? if (APPLICATION->metadataIndex()->hasUid(m_uid)) { return APPLICATION->metadataIndex()->get(m_uid); } return nullptr; } int Component::getOrder() { if (m_orderOverride) return m_order; auto vfile = getVersionFile(); if (vfile) { return vfile->order; } return 0; } void Component::setOrder(int order) { m_orderOverride = true; m_order = order; } QString Component::getID() { return m_uid; } QString Component::getName() { if (!m_cachedName.isEmpty()) return m_cachedName; return m_uid; } QString Component::getVersion() { return m_cachedVersion; } QString Component::getFilename() { return m_parent->patchFilePathForUid(m_uid); } QDateTime Component::getReleaseDateTime() { if (m_metaVersion) { return m_metaVersion->time(); } auto vfile = getVersionFile(); if (vfile) { return vfile->releaseTime; } // FIXME: fake return QDateTime::currentDateTime(); } bool Component::isEnabled() { return !canBeDisabled() || !m_disabled; } bool Component::canBeDisabled() { return isRemovable() && !m_dependencyOnly; } bool Component::setEnabled(bool state) { bool intendedDisabled = !state; if (!canBeDisabled()) { intendedDisabled = false; } if (intendedDisabled != m_disabled) { m_disabled = intendedDisabled; emit dataChanged(); return true; } return false; } bool Component::isCustom() { return m_file != nullptr; } bool Component::isCustomizable() { return m_metaVersion && getVersionFile(); } bool Component::isRemovable() { return !m_important; } bool Component::isRevertible() { if (isCustom()) { if (APPLICATION->metadataIndex()->hasUid(m_uid)) { return true; } } return false; } bool Component::isMoveable() { // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'. return true; } bool Component::isVersionChangeable(bool wait) { auto list = getVersionList(); if (list) { if (wait) list->waitToLoad(); return list->count() != 0; } return false; } bool Component::isKnownModloader() { auto iter = KNOWN_MODLOADERS.find(m_uid); return iter != KNOWN_MODLOADERS.cend(); } QStringList Component::knownConflictingComponents() { auto iter = KNOWN_MODLOADERS.find(m_uid); if (iter != KNOWN_MODLOADERS.cend()) { return (*iter).knownConflictingComponents; } else { return {}; } } void Component::setImportant(bool state) { if (m_important != state) { m_important = state; emit dataChanged(); } } ProblemSeverity Component::getProblemSeverity() const { auto file = getVersionFile(); if (file) { auto severity = file->getProblemSeverity(); return m_componentProblemSeverity > severity ? m_componentProblemSeverity : severity; } return ProblemSeverity::Error; } const QList Component::getProblems() const { auto file = getVersionFile(); if (file) { auto problems = file->getProblems(); problems.append(m_componentProblems); return problems; } return { { ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.") } }; } void Component::addComponentProblem(ProblemSeverity severity, const QString& description) { if (severity > m_componentProblemSeverity) { m_componentProblemSeverity = severity; } m_componentProblems.append({ severity, description }); emit dataChanged(); } void Component::resetComponentProblems() { m_componentProblems.clear(); m_componentProblemSeverity = ProblemSeverity::None; emit dataChanged(); } void Component::setVersion(const QString& version) { if (version == m_version) { return; } m_version = version; if (m_loaded) { // we are loaded and potentially have state to invalidate if (m_file) { // we have a file... explicit version has been changed and there is nothing else to do. } else { // we don't have a file, therefore we are loaded with metadata m_cachedVersion = version; // see if the meta version is loaded auto metaVersion = APPLICATION->metadataIndex()->get(m_uid, version); if (metaVersion->isLoaded()) { // if yes, we can continue with that. m_metaVersion = metaVersion; } else { // if not, we need loading m_metaVersion.reset(); m_loaded = false; } updateCachedData(); } } else { // not loaded... assume it will be sorted out later by the update task } emit dataChanged(); } bool Component::customize() { if (isCustom()) { return false; } auto filename = getFilename(); if (!FS::ensureFilePathExists(filename)) { return false; } // FIXME: get rid of this try-catch. try { QSaveFile jsonFile(filename); if (!jsonFile.open(QIODevice::WriteOnly)) { return false; } auto vfile = getVersionFile(); if (!vfile) { return false; } auto document = OneSixVersionFormat::versionFileToJson(vfile); jsonFile.write(document.toJson()); if (!jsonFile.commit()) { return false; } m_file = vfile; m_metaVersion.reset(); emit dataChanged(); } catch (const Exception& error) { qWarning() << "Version could not be loaded:" << error.cause(); } return true; } bool Component::revert() { if (!isCustom()) { // already not custom return true; } auto filename = getFilename(); bool result = true; // just kill the file and reload if (QFile::exists(filename)) { result = FS::deletePath(filename); } if (result) { // file gone... m_file.reset(); // check local cache for metadata... auto version = APPLICATION->metadataIndex()->get(m_uid, m_version); if (version->isLoaded()) { m_metaVersion = version; } else { m_metaVersion.reset(); m_loaded = false; } emit dataChanged(); } return result; } /** * deep inspecting compare for requirement sets * By default, only uids are compared for set operations. * This compares all fields of the Require structs in the sets. */ static bool deepCompare(const std::set& a, const std::set& b) { // NOTE: this needs to be rewritten if the type of Meta::RequireSet changes if (a.size() != b.size()) { return false; } for (const auto& reqA : a) { const auto& iter2 = b.find(reqA); if (iter2 == b.cend()) { return false; } const auto& reqB = *iter2; if (!reqA.deepEquals(reqB)) { return false; } } return true; } void Component::updateCachedData() { auto file = getVersionFile(); if (file) { bool changed = false; if (m_cachedName != file->name) { m_cachedName = file->name; changed = true; } if (m_cachedVersion != file->version) { m_cachedVersion = file->version; changed = true; } if (m_cachedVolatile != file->m_volatile) { m_cachedVolatile = file->m_volatile; changed = true; } if (!deepCompare(m_cachedRequires, file->m_requires)) { m_cachedRequires = file->m_requires; changed = true; } if (!deepCompare(m_cachedConflicts, file->conflicts)) { m_cachedConflicts = file->conflicts; changed = true; } if (changed) { emit dataChanged(); } } else { // in case we removed all the metadata m_cachedRequires.clear(); m_cachedConflicts.clear(); emit dataChanged(); } } void Component::waitLoadMeta() { if (!m_loaded) { if (!m_metaVersion || !m_metaVersion->isLoaded()) { // wait for the loaded version from meta m_metaVersion = APPLICATION->metadataIndex()->getLoadedVersion(m_uid, m_version); } m_loaded = true; updateCachedData(); } } void Component::setUpdateAction(const UpdateAction& action) { m_updateAction = action; } UpdateAction Component::getUpdateAction() { return m_updateAction; } void Component::clearUpdateAction() { m_updateAction = UpdateAction{ UpdateActionNone{} }; } QDebug operator<<(QDebug d, const Component& comp) { QDebugStateSaver saver(d); d.nospace() << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; return d; } PrismLauncher-10.0.5/launcher/MessageLevel.cpp0000644000175100017510000000446715144136756020667 0ustar runnerrunner#include "MessageLevel.h" MessageLevel MessageLevel::fromName(const QString& levelName) { QString name = levelName.toUpper(); if (name == "LAUNCHER") return MessageLevel::Launcher; else if (name == "TRACE") return MessageLevel::Trace; else if (name == "DEBUG") return MessageLevel::Debug; else if (name == "INFO") return MessageLevel::Info; else if (name == "MESSAGE") return MessageLevel::Message; else if (name == "WARNING" || name == "WARN") return MessageLevel::Warning; else if (name == "ERROR" || name == "CRITICAL") return MessageLevel::Error; else if (name == "FATAL") return MessageLevel::Fatal; // Skip PrePost, it's not exposed to !![]! // Also skip StdErr and StdOut else return MessageLevel::Unknown; } MessageLevel MessageLevel::fromQtMsgType(const QtMsgType& type) { switch (type) { case QtDebugMsg: return MessageLevel::Debug; case QtInfoMsg: return MessageLevel::Info; case QtWarningMsg: return MessageLevel::Warning; case QtCriticalMsg: return MessageLevel::Error; case QtFatalMsg: return MessageLevel::Fatal; default: return MessageLevel::Unknown; } } /* Get message level from a line. Line is modified if it was successful. */ MessageLevel MessageLevel::takeFromLine(QString& line) { // Level prefix int endmark = line.indexOf("]!"); if (line.startsWith("!![") && endmark != -1) { auto level = MessageLevel::fromName(line.left(endmark).mid(3)); line = line.mid(endmark + 2); return level; } return MessageLevel::Unknown; } /* Get message level from a line from the launcher log. Line is modified if it was successful. */ MessageLevel MessageLevel::takeFromLauncherLine(QString& line) { // Level prefix int startMark = 0; while (startMark < line.size() && (line[startMark].isDigit() || line[startMark].isSpace() || line[startMark] == '.')) ++startMark; int endmark = line.indexOf(":"); if (startMark < line.size() && endmark != -1) { auto level = MessageLevel::fromName(line.left(endmark).mid(startMark)); line = line.mid(endmark + 2); return level; } return MessageLevel::Unknown; } PrismLauncher-10.0.5/launcher/LaunchController.cpp0000644000175100017510000004750215144136756021566 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LaunchController.h" #include "Application.h" #include "launch/steps/PrintServers.h" #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountList.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/ProfileSelectDialog.h" #include "ui/dialogs/ProfileSetupDialog.h" #include "ui/dialogs/ProgressDialog.h" #include #include #include #include #include #include #include #include #include "BuildConfig.h" #include "JavaCommon.h" #include "launch/steps/TextPrint.h" #include "tasks/Task.h" #include "ui/dialogs/ChooseOfflineNameDialog.h" LaunchController::LaunchController() : Task() {} void LaunchController::executeTask() { if (!m_instance) { emitFailed(tr("No instance specified!")); return; } if (!JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget)) { emitFailed(tr("Invalid Java arguments specified. Please fix this first.")); return; } login(); } void LaunchController::decideAccount() { if (m_accountToUse) { return; } // Find an account to use. auto accounts = APPLICATION->accounts(); if (accounts->count() <= 0 || !accounts->anyAccountIsValid()) { // Tell the user they need to log in at least one account in order to play. auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), tr("In order to play Minecraft, you must have at least one Microsoft " "account which owns Minecraft logged in. " "Would you like to open the account manager to add an account now?"), QMessageBox::Information, QMessageBox::Yes | QMessageBox::No) ->exec(); if (reply == QMessageBox::Yes) { // Open the account manager. APPLICATION->ShowGlobalSettings(m_parentWidget, "accounts"); } else if (reply == QMessageBox::No) { // Do not open "profile select" dialog. return; } } // Select the account to use. If the instance has a specific account set, that will be used. Otherwise, the default account will be used auto instanceAccountId = m_instance->settings()->get("InstanceAccountId").toString(); auto instanceAccountIndex = accounts->findAccountByProfileId(instanceAccountId); if (instanceAccountIndex == -1 || instanceAccountId.isEmpty()) { m_accountToUse = accounts->defaultAccount(); } else { m_accountToUse = accounts->at(instanceAccountIndex); } if (!m_accountToUse) { // If no default account is set, ask the user which one to use. ProfileSelectDialog selectDialog(tr("Which account would you like to use?"), ProfileSelectDialog::GlobalDefaultCheckbox, m_parentWidget); selectDialog.exec(); // Launch the instance with the selected account. m_accountToUse = selectDialog.selectedAccount(); // If the user said to use the account as default, do that. if (selectDialog.useAsGlobalDefault() && m_accountToUse) { accounts->setDefaultAccount(m_accountToUse); } } } bool LaunchController::askPlayDemo() { QMessageBox box(m_parentWidget); box.setWindowTitle(tr("Play demo?")); box.setText( tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play " "the demo?")); box.setIcon(QMessageBox::Warning); auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); box.setDefaultButton(cancelButton); box.exec(); return box.clickedButton() == demoButton; } QString LaunchController::askOfflineName(QString playerName, bool demo, bool* ok) { if (ok != nullptr) { *ok = false; } // we ask the user for a player name QString message = tr("Choose your offline mode player name."); if (demo) { message = tr("Choose your demo mode player name."); } QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); QString usedname = lastOfflinePlayerName.isEmpty() ? playerName : lastOfflinePlayerName; ChooseOfflineNameDialog dialog(message, m_parentWidget); dialog.setWindowTitle(tr("Player name")); dialog.setUsername(usedname); if (dialog.exec() != QDialog::Accepted) { return {}; } const QString name = dialog.getUsername(); usedname = name; APPLICATION->settings()->set("LastOfflinePlayerName", usedname); if (ok != nullptr) { *ok = true; } return usedname; } void LaunchController::login() { decideAccount(); if (!m_accountToUse) { // if no account is selected, ask about demo if (!m_demo) { m_demo = askPlayDemo(); } if (m_demo) { // we ask the user for a player name bool ok = false; auto name = askOfflineName("Player", m_demo, &ok); if (ok) { m_session = std::make_shared(); static const QRegularExpression s_removeChars("[{}-]"); m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(s_removeChars)); launchInstance(); return; } } // if no account is selected, we bail emitFailed(tr("No account selected for launch.")); return; } // we loop until the user succeeds in logging in or gives up bool tryagain = true; unsigned int tries = 0; if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) || m_accountToUse->shouldRefresh()) { // Force account refresh on the account used to launch the instance updating the AccountState // only on first try and if it is not meant to be offline m_accountToUse->refresh(); } while (tryagain) { if (tries > 0 && tries % 3 == 0) { auto result = QMessageBox::question(m_parentWidget, tr("Continue launch?"), tr("It looks like we couldn't launch after %1 tries. Usually this can be fixed by logging out and " "logging back in your Microsoft account. If that doesn't work, Minecraft authentication servers " "may be having an outage or you may need a VPN in your region. Do you want to continue trying?") .arg(tries)); if (result == QMessageBox::No) { emitAborted(); return; } } tries++; m_session = std::make_shared(); m_session->wants_online = m_online; m_session->demo = m_demo; m_accountToUse->fillSession(m_session); MinecraftAccountPtr accountToCheck; if (m_accountToUse->ownsMinecraft()) accountToCheck = m_accountToUse; else if (const MinecraftAccountPtr defaultAccount = APPLICATION->accounts()->defaultAccount(); defaultAccount != nullptr && defaultAccount->ownsMinecraft()) { accountToCheck = defaultAccount; } else { for (int i = 0; i < APPLICATION->accounts()->count(); i++) { MinecraftAccountPtr account = APPLICATION->accounts()->at(i); if (account->ownsMinecraft()) accountToCheck = account; } } if (accountToCheck == nullptr) { if (!m_session->demo) m_session->demo = askPlayDemo(); if (m_session->demo) launchInstance(); else emitFailed(tr("Launch cancelled - account does not own Minecraft.")); return; } switch (accountToCheck->accountState()) { case AccountState::Offline: { m_session->wants_online = false; } /* fallthrough */ case AccountState::Online: { if (!m_session->wants_online && m_accountToUse->accountType() != AccountType::Offline) { // we ask the user for a player name bool ok = false; QString name; if (m_offlineName.isEmpty()) { name = askOfflineName(m_session->player_name, m_session->demo, &ok); if (!ok) { tryagain = false; break; } } else { name = m_offlineName; } m_session->MakeOffline(name); // offline flavored game from here :3 } else if (m_accountToUse == accountToCheck && !m_accountToUse->hasProfile()) { // Now handle setting up a profile name here... ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); if (dialog.exec() == QDialog::Accepted) { tryagain = true; continue; } else { emitFailed(tr("Received undetermined session status during login.")); return; } } if (m_accountToUse->accountType() == AccountType::Offline) m_session->wants_online = false; // we own Minecraft, there is a profile, it's all ready to go! launchInstance(); return; } case AccountState::Errored: // This means some sort of soft error that we can fix with a refresh ... so let's refresh. case AccountState::Unchecked: { accountToCheck->refresh(); } /* fallthrough */ case AccountState::Working: { // refresh is in progress, we need to wait for it to finish to proceed. ProgressDialog progDialog(m_parentWidget); progDialog.setSkipButton(true, tr("Abort")); auto task = accountToCheck->currentTask(); progDialog.execWithTask(task.get()); // don't retry if aborted if (task->getState() == Task::State::AbortedByUser) tryagain = false; continue; } case AccountState::Expired: { if (reauthenticateAccount(accountToCheck)) continue; return; } case AccountState::Disabled: { auto errorString = tr("The launcher's client identification has changed. Please remove '%1' and try again.") .arg(accountToCheck->profileName()); QMessageBox::warning(m_parentWidget, tr("Client identification changed"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); emitFailed(errorString); return; } case AccountState::Gone: { auto errorString = tr("'%1' no longer exists on the servers. It may have been migrated, in which case please add the new account " "you migrated this one to.") .arg(accountToCheck->profileName()); QMessageBox::warning(m_parentWidget, tr("Account gone"), errorString, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); emitFailed(errorString); return; } } } emitFailed(tr("Failed to launch.")); } bool LaunchController::reauthenticateAccount(MinecraftAccountPtr account) { auto button = QMessageBox::warning( m_parentWidget, tr("Account refresh failed"), tr("'%1' has expired and needs to be reauthenticated. Do you want to reauthenticate this account?").arg(account->profileName()), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::Yes); if (button == QMessageBox::StandardButton::Yes) { auto accounts = APPLICATION->accounts(); bool isDefault = accounts->defaultAccount() == account; accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(account->profileId()))); if (account->accountType() == AccountType::MSA) { auto newAccount = MSALoginDialog::newAccount(m_parentWidget); if (newAccount != nullptr) { accounts->addAccount(newAccount); if (isDefault) accounts->setDefaultAccount(newAccount); if (m_accountToUse == account) { m_accountToUse = nullptr; decideAccount(); } return true; } } } emitFailed(tr("The account has expired and needs to be reauthenticated")); return false; } void LaunchController::launchInstance() { Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); if (!m_instance->reloadSettings()) { QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); emitFailed(tr("Couldn't load the instance profile.")); return; } m_launcher = m_instance->createLaunchTask(m_session, m_targetToJoin); if (!m_launcher) { emitFailed(tr("Couldn't instantiate a launcher.")); return; } auto console = qobject_cast(m_parentWidget); auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); if (!console && showConsole) { APPLICATION->showInstanceWindow(m_instance); } connect(m_launcher.get(), &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); connect(m_launcher.get(), &LaunchTask::succeeded, this, &LaunchController::onSucceeded); connect(m_launcher.get(), &LaunchTask::failed, this, &LaunchController::onFailed); connect(m_launcher.get(), &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); // Prepend Online and Auth Status QString online_mode; if (m_session->wants_online) { online_mode = "online"; // Prepend Server Status QStringList servers = { "login.microsoftonline.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com" }; m_launcher->prependStep(makeShared(m_launcher.get(), servers)); } else { online_mode = m_demo ? "demo" : "offline"; } m_launcher->prependStep( makeShared(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); // Prepend Version { auto versionString = QString("%1 version: %2 (%3)") .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM); m_launcher->prependStep(makeShared(m_launcher.get(), versionString + "\n\n", MessageLevel::Launcher)); } m_launcher->start(); } void LaunchController::readyForLaunch() { if (!m_profiler) { m_launcher->proceed(); return; } QString error; if (!m_profiler->check(&error)) { m_launcher->abort(); emitFailed("Profiler startup failed!"); QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Profiler check for %1 failed: %2").arg(m_profiler->name(), error)); return; } BaseProfiler* profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this); connect(profilerInstance, &BaseProfiler::readyToLaunch, [this](const QString& message) { QMessageBox msg(m_parentWidget); msg.setText(tr("The game launch is delayed until you press the " "button. This is the right time to setup the profiler, as the " "profiler server is running now.\n\n%1") .arg(message)); msg.setWindowTitle(tr("Waiting.")); msg.setIcon(QMessageBox::Information); msg.addButton(tr("&Launch"), QMessageBox::AcceptRole); msg.exec(); m_launcher->proceed(); }); connect(profilerInstance, &BaseProfiler::abortLaunch, [this](const QString& message) { QMessageBox msg; msg.setText(tr("Couldn't start the profiler: %1").arg(message)); msg.setWindowTitle(tr("Error")); msg.setIcon(QMessageBox::Critical); msg.addButton(QMessageBox::Ok); msg.setModal(true); msg.exec(); m_launcher->abort(); emitFailed("Profiler startup failed!"); }); profilerInstance->beginProfiling(m_launcher); } void LaunchController::onSucceeded() { emitSucceeded(); } void LaunchController::onFailed(QString reason) { if (m_instance->settings()->get("ShowConsoleOnError").toBool()) { APPLICATION->showInstanceWindow(m_instance, "console"); } emitFailed(reason); } void LaunchController::onProgressRequested(Task* task) { ProgressDialog progDialog(m_parentWidget); progDialog.setSkipButton(true, tr("Abort")); m_launcher->proceed(); progDialog.execWithTask(task); } bool LaunchController::abort() { if (!m_launcher) { return true; } if (!m_launcher->canAbort()) { return false; } auto response = CustomMessageBox::selectable(m_parentWidget, tr("Kill Minecraft?"), tr("This can cause the instance to get corrupted and should only be used if Minecraft " "is frozen for some reason"), QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) ->exec(); if (response == QMessageBox::Yes) { return m_launcher->abort(); } return false; } PrismLauncher-10.0.5/launcher/InstancePageProvider.h0000644000175100017510000000504515144136756022025 0ustar runnerrunner#pragma once #include #include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" #include "ui/pages/BasePageProvider.h" #include "ui/pages/instance/InstanceSettingsPage.h" #include "ui/pages/instance/LogPage.h" #include "ui/pages/instance/ManagedPackPage.h" #include "ui/pages/instance/ModFolderPage.h" #include "ui/pages/instance/NotesPage.h" #include "ui/pages/instance/OtherLogsPage.h" #include "ui/pages/instance/ResourcePackPage.h" #include "ui/pages/instance/ScreenshotsPage.h" #include "ui/pages/instance/ServersPage.h" #include "ui/pages/instance/ShaderPackPage.h" #include "ui/pages/instance/TexturePackPage.h" #include "ui/pages/instance/VersionPage.h" #include "ui/pages/instance/WorldListPage.h" class InstancePageProvider : protected QObject, public BasePageProvider { Q_OBJECT public: explicit InstancePageProvider(InstancePtr parent) { inst = parent; } virtual ~InstancePageProvider() = default; virtual QList getPages() override { QList values; values.append(new LogPage(inst)); std::shared_ptr onesix = std::dynamic_pointer_cast(inst); values.append(new VersionPage(onesix.get())); values.append(ManagedPackPage::createPage(onesix.get())); auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList()); modsPage->setFilter("%1 (*.zip *.jar *.litemod *.nilmod)"); values.append(modsPage); values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); values.append(new NilModFolderPage(onesix.get(), onesix->nilModList())); values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); values.append(new GlobalDataPackPage(onesix.get())); values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); values.append(new NotesPage(onesix.get())); values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix)); values.append(new OtherLogsPage("logs", tr("Other Logs"), "Other-Logs", inst)); return values; } virtual QString dialogTitle() override { return tr("Edit Instance (%1)").arg(inst->name()); } protected: InstancePtr inst; }; PrismLauncher-10.0.5/launcher/BaseInstance.cpp0000644000175100017510000003516715144136756020653 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "BaseInstance.h" #include #include #include #include #include #include "Application.h" #include "Json.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" #include "BuildConfig.h" #include "Commandline.h" #include "FileSystem.h" int getConsoleMaxLines(SettingsObjectPtr settings) { auto lineSetting = settings->getSetting("ConsoleMaxLines"); bool conversionOk = false; int maxLines = lineSetting->get().toInt(&conversionOk); if (!conversionOk) { maxLines = lineSetting->defValue().toInt(); qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; } return maxLines; } bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings) { return settings->get("ConsoleOverflowStop").toBool(); } BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : QObject() { m_settings = settings; m_global_settings = globalSettings; m_rootDir = rootDir; m_settings->registerSetting("name", "Unnamed Instance"); m_settings->registerSetting("iconKey", "default"); m_settings->registerSetting("notes", ""); m_settings->registerSetting("lastLaunchTime", 0); m_settings->registerSetting("totalTimePlayed", 0); if (m_settings->get("totalTimePlayed").toLongLong() < 0) m_settings->reset("totalTimePlayed"); m_settings->registerSetting("lastTimePlayed", 0); m_settings->registerSetting("linkedInstances", "[]"); m_settings->registerSetting("shortcuts", QString()); // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride); // NOTE: Sometimees InstanceType is already registered, as it was used to identify the type of // a locally stored instance if (!m_settings->getSetting("InstanceType")) m_settings->registerSetting("InstanceType", ""); // Custom Commands auto commandSetting = m_settings->registerSetting({ "OverrideCommands", "OverrideLaunchCmd" }, false); m_settings->registerOverride(globalSettings->getSetting("PreLaunchCommand"), commandSetting); m_settings->registerOverride(globalSettings->getSetting("WrapperCommand"), commandSetting); m_settings->registerOverride(globalSettings->getSetting("PostExitCommand"), commandSetting); // Console auto consoleSetting = m_settings->registerSetting("OverrideConsole", false); m_settings->registerOverride(globalSettings->getSetting("ShowConsole"), consoleSetting); m_settings->registerOverride(globalSettings->getSetting("AutoCloseConsole"), consoleSetting); m_settings->registerOverride(globalSettings->getSetting("ShowConsoleOnError"), consoleSetting); m_settings->registerOverride(globalSettings->getSetting("LogPrePostOutput"), consoleSetting); m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr); m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr); // Managed Packs m_settings->registerSetting("ManagedPack", false); m_settings->registerSetting("ManagedPackType", ""); m_settings->registerSetting("ManagedPackID", ""); m_settings->registerSetting("ManagedPackName", ""); m_settings->registerSetting("ManagedPackVersionID", ""); m_settings->registerSetting("ManagedPackVersionName", ""); m_settings->registerSetting("Profiler", ""); } QString BaseInstance::getPreLaunchCommand() { return settings()->get("PreLaunchCommand").toString(); } QString BaseInstance::getWrapperCommand() { return settings()->get("WrapperCommand").toString(); } QString BaseInstance::getPostExitCommand() { return settings()->get("PostExitCommand").toString(); } bool BaseInstance::isManagedPack() const { return m_settings->get("ManagedPack").toBool(); } QString BaseInstance::getManagedPackType() const { return m_settings->get("ManagedPackType").toString(); } QString BaseInstance::getManagedPackID() const { return m_settings->get("ManagedPackID").toString(); } QString BaseInstance::getManagedPackName() const { return m_settings->get("ManagedPackName").toString(); } QString BaseInstance::getManagedPackVersionID() const { return m_settings->get("ManagedPackVersionID").toString(); } QString BaseInstance::getManagedPackVersionName() const { return m_settings->get("ManagedPackVersionName").toString(); } void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version) { m_settings->set("ManagedPack", true); m_settings->set("ManagedPackType", type); m_settings->set("ManagedPackID", id); m_settings->set("ManagedPackName", name); m_settings->set("ManagedPackVersionID", versionId); m_settings->set("ManagedPackVersionName", version); } void BaseInstance::copyManagedPack(BaseInstance& other) { m_settings->set("ManagedPack", other.isManagedPack()); m_settings->set("ManagedPackType", other.getManagedPackType()); m_settings->set("ManagedPackID", other.getManagedPackID()); m_settings->set("ManagedPackName", other.getManagedPackName()); m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() && m_settings->get("OverrideJavaLocation").toBool()) { m_settings->set("OverrideJavaLocation", false); m_settings->set("JavaPath", ""); } } QStringList BaseInstance::getLinkedInstances() const { auto setting = m_settings->get("linkedInstances").toString(); return Json::toStringList(setting); } void BaseInstance::setLinkedInstances(const QStringList& list) { m_settings->set("linkedInstances", Json::fromStringList(list)); } void BaseInstance::addLinkedInstanceId(const QString& id) { auto linkedInstances = getLinkedInstances(); linkedInstances.append(id); setLinkedInstances(linkedInstances); } bool BaseInstance::removeLinkedInstanceId(const QString& id) { auto linkedInstances = getLinkedInstances(); int numRemoved = linkedInstances.removeAll(id); setLinkedInstances(linkedInstances); return numRemoved > 0; } bool BaseInstance::isLinkedToInstanceId(const QString& id) const { auto linkedInstances = getLinkedInstances(); return linkedInstances.contains(id); } void BaseInstance::iconUpdated(QString key) { if (iconKey() == key) { emit propertiesChanged(this); } } void BaseInstance::invalidate() { changeStatus(Status::Gone); qDebug() << "Instance" << id() << "has been invalidated."; } void BaseInstance::changeStatus(BaseInstance::Status newStatus) { Status status = currentStatus(); if (status != newStatus) { m_status = newStatus; emit statusChanged(status, newStatus); } } BaseInstance::Status BaseInstance::currentStatus() const { return m_status; } QString BaseInstance::id() const { return QFileInfo(instanceRoot()).fileName(); } bool BaseInstance::isRunning() const { return m_isRunning; } void BaseInstance::setRunning(bool running) { if (running == m_isRunning) return; m_isRunning = running; emit runningStatusChanged(running); } void BaseInstance::setMinecraftRunning(bool running) { if (!settings()->get("RecordGameTime").toBool()) { return; } if (running) { m_timeStarted = QDateTime::currentDateTime(); setLastLaunch(m_timeStarted.toMSecsSinceEpoch()); } else { QDateTime timeEnded = QDateTime::currentDateTime(); qint64 current = settings()->get("totalTimePlayed").toLongLong(); settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded)); settings()->set("lastTimePlayed", m_timeStarted.secsTo(timeEnded)); emit propertiesChanged(this); } } int64_t BaseInstance::totalTimePlayed() const { qint64 current = m_settings->get("totalTimePlayed").toLongLong(); if (m_isRunning) { QDateTime timeNow = QDateTime::currentDateTime(); return current + m_timeStarted.secsTo(timeNow); } return current; } int64_t BaseInstance::lastTimePlayed() const { if (m_isRunning) { QDateTime timeNow = QDateTime::currentDateTime(); return m_timeStarted.secsTo(timeNow); } return m_settings->get("lastTimePlayed").toLongLong(); } void BaseInstance::resetTimePlayed() { settings()->reset("totalTimePlayed"); settings()->reset("lastTimePlayed"); } QString BaseInstance::instanceType() const { return m_settings->get("InstanceType").toString(); } QString BaseInstance::instanceRoot() const { return m_rootDir; } SettingsObjectPtr BaseInstance::settings() { loadSpecificSettings(); return m_settings; } bool BaseInstance::canLaunch() const { return (!hasVersionBroken() && !isRunning()); } bool BaseInstance::reloadSettings() { return m_settings->reload(); } qint64 BaseInstance::lastLaunch() const { return m_settings->get("lastLaunchTime").value(); } void BaseInstance::setLastLaunch(qint64 val) { // FIXME: if no change, do not set. setting involves saving a file. m_settings->set("lastLaunchTime", val); emit propertiesChanged(this); } void BaseInstance::setNotes(QString val) { // FIXME: if no change, do not set. setting involves saving a file. m_settings->set("notes", val); } QString BaseInstance::notes() const { return m_settings->get("notes").toString(); } void BaseInstance::setIconKey(QString val) { // FIXME: if no change, do not set. setting involves saving a file. m_settings->set("iconKey", val); emit propertiesChanged(this); } QString BaseInstance::iconKey() const { return m_settings->get("iconKey").toString(); } void BaseInstance::setName(QString val) { // FIXME: if no change, do not set. setting involves saving a file. m_settings->set("name", val); emit propertiesChanged(this); } bool BaseInstance::syncInstanceDirName(const QString& newRoot) const { auto oldRoot = instanceRoot(); return oldRoot == newRoot || QFile::rename(oldRoot, newRoot); } void BaseInstance::registerShortcut(const ShortcutData& data) { auto currentShortcuts = shortcuts(); currentShortcuts.append(data); qDebug() << "Registering shortcut for instance" << id() << "with name" << data.name << "and path" << data.filePath; setShortcuts(currentShortcuts); } void BaseInstance::setShortcuts(const QList& shortcuts) { // FIXME: if no change, do not set. setting involves saving a file. QJsonArray array; for (const auto& elem : shortcuts) { array.append(QJsonObject{ { "name", elem.name }, { "filePath", elem.filePath }, { "target", static_cast(elem.target) } }); } QJsonDocument document; document.setArray(array); m_settings->set("shortcuts", QString::fromUtf8(document.toJson(QJsonDocument::Compact))); } QList BaseInstance::shortcuts() const { auto data = m_settings->get("shortcuts").toString().toUtf8(); QJsonParseError parseError; auto document = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError || !document.isArray()) return {}; QList results; for (const auto& elem : document.array()) { if (!elem.isObject()) continue; auto dict = elem.toObject(); if (!dict.contains("name") || !dict.contains("filePath") || !dict.contains("target")) continue; int value = dict["target"].toInt(-1); if (!dict["name"].isString() || !dict["filePath"].isString() || value < 0 || value >= 3) continue; QString shortcutName = dict["name"].toString(); QString filePath = dict["filePath"].toString(); if (!QDir(filePath).exists()) { qWarning() << "Shortcut" << shortcutName << "for instance" << name() << "have non-existent path" << filePath; continue; } results.append({ shortcutName, filePath, static_cast(value) }); } return results; } QString BaseInstance::name() const { return m_settings->get("name").toString(); } QString BaseInstance::windowTitle() const { return BuildConfig.LAUNCHER_DISPLAYNAME + ": " + name(); } // FIXME: why is this here? move it to MinecraftInstance!!! QStringList BaseInstance::extraArguments() { return Commandline::splitArgs(settings()->get("JvmArgs").toString()); } shared_qobject_ptr BaseInstance::getLaunchTask() { return m_launchProcess; } void BaseInstance::updateRuntimeContext() { // NOOP } bool BaseInstance::isLegacy() { return traits().contains("legacyLaunch") || traits().contains("alphaLaunch"); } PrismLauncher-10.0.5/launcher/RWStorage.h0000644000175100017510000000240115144136756017617 0ustar runnerrunner#pragma once #include #include #include #include template class RWStorage { public: void add(K key, V value) { QWriteLocker l(&lock); cache[key] = value; stale_entries.remove(key); } V get(K key) { QReadLocker l(&lock); if (cache.contains(key)) { return cache[key]; } else return V(); } bool get(K key, V& value) { QReadLocker l(&lock); if (cache.contains(key)) { value = cache[key]; return true; } else return false; } bool has(K key) { QReadLocker l(&lock); return cache.contains(key); } bool stale(K key) { QReadLocker l(&lock); if (!cache.contains(key)) return true; return stale_entries.contains(key); } void setStale(K key) { QWriteLocker l(&lock); if (cache.contains(key)) { stale_entries.insert(key); } } void clear() { QWriteLocker l(&lock); cache.clear(); stale_entries.clear(); } private: QReadWriteLock lock; QMap cache; QSet stale_entries; }; PrismLauncher-10.0.5/launcher/FileIgnoreProxy.cpp0000644000175100017510000002363515144136756021376 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "FileIgnoreProxy.h" #include #include #include #include #include "FileSystem.h" #include "SeparatorPrefixTree.h" #include "StringUtils.h" FileIgnoreProxy::FileIgnoreProxy(QString root, QObject* parent) : QSortFilterProxyModel(parent), m_root(root) {} // NOTE: Sadly, we have to do sorting ourselves. bool FileIgnoreProxy::lessThan(const QModelIndex& left, const QModelIndex& right) const { QFileSystemModel* fsm = qobject_cast(sourceModel()); if (!fsm) { return QSortFilterProxyModel::lessThan(left, right); } bool asc = sortOrder() == Qt::AscendingOrder ? true : false; QFileInfo leftFileInfo = fsm->fileInfo(left); QFileInfo rightFileInfo = fsm->fileInfo(right); if (!leftFileInfo.isDir() && rightFileInfo.isDir()) { return !asc; } if (leftFileInfo.isDir() && !rightFileInfo.isDir()) { return asc; } // sort and proxy model breaks the original model... if (sortColumn() == 0) { return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0; } if (sortColumn() == 1) { auto leftSize = leftFileInfo.size(); auto rightSize = rightFileInfo.size(); if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) { return StringUtils::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), Qt::CaseInsensitive) < 0 ? asc : !asc; } return leftSize < rightSize; } return QSortFilterProxyModel::lessThan(left, right); } Qt::ItemFlags FileIgnoreProxy::flags(const QModelIndex& index) const { if (!index.isValid()) return Qt::NoItemFlags; auto sourceIndex = mapToSource(index); Qt::ItemFlags flags = sourceIndex.flags(); if (index.column() == 0) { flags |= Qt::ItemIsUserCheckable; if (sourceIndex.model()->hasChildren(sourceIndex)) { flags |= Qt::ItemIsAutoTristate; } } return flags; } QVariant FileIgnoreProxy::data(const QModelIndex& index, int role) const { QModelIndex sourceIndex = mapToSource(index); if (index.column() == 0 && role == Qt::CheckStateRole) { QFileSystemModel* fsm = qobject_cast(sourceModel()); auto blockedPath = relPath(fsm->filePath(sourceIndex)); auto cover = m_blocked.cover(blockedPath); if (!cover.isNull()) { return QVariant(Qt::Unchecked); } else if (m_blocked.exists(blockedPath)) { return QVariant(Qt::PartiallyChecked); } else { return QVariant(Qt::Checked); } } return sourceIndex.data(role); } bool FileIgnoreProxy::setData(const QModelIndex& index, const QVariant& value, int role) { if (index.column() == 0 && role == Qt::CheckStateRole) { Qt::CheckState state = static_cast(value.toInt()); return setFilterState(index, state); } QModelIndex sourceIndex = mapToSource(index); return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role); } QString FileIgnoreProxy::relPath(const QString& path) const { return QDir(m_root).relativeFilePath(path); } bool FileIgnoreProxy::setFilterState(QModelIndex index, Qt::CheckState state) { QFileSystemModel* fsm = qobject_cast(sourceModel()); if (!fsm) { return false; } QModelIndex sourceIndex = mapToSource(index); auto blockedPath = relPath(fsm->filePath(sourceIndex)); bool changed = false; if (state == Qt::Unchecked) { // blocking a path auto& node = m_blocked.insert(blockedPath); // get rid of all blocked nodes below node.clear(); changed = true; } else if (state == Qt::Checked || state == Qt::PartiallyChecked) { if (!m_blocked.remove(blockedPath)) { auto cover = m_blocked.cover(blockedPath); qDebug() << "Blocked by cover" << cover; // uncover m_blocked.remove(cover); // block all contents, except for any cover QModelIndex rootIndex = fsm->index(FS::PathCombine(m_root, cover)); QModelIndex doing = rootIndex; int row = 0; QStack todo; while (1) { auto node = fsm->index(row, 0, doing); if (!node.isValid()) { if (!todo.size()) { break; } else { doing = todo.pop(); row = 0; continue; } } auto relpath = relPath(fsm->filePath(node)); if (blockedPath.startsWith(relpath)) // cover found? { // continue processing cover later todo.push(node); } else { // or just block this one. m_blocked.insert(relpath); } row++; } } changed = true; } if (changed) { // update the thing emit dataChanged(index, index, { Qt::CheckStateRole }); // update everything above index QModelIndex up = index.parent(); while (1) { if (!up.isValid()) break; emit dataChanged(up, up, { Qt::CheckStateRole }); up = up.parent(); } // and everything below the index QModelIndex doing = index; int row = 0; QStack todo; while (1) { auto node = this->index(row, 0, doing); if (!node.isValid()) { if (!todo.size()) { break; } else { doing = todo.pop(); row = 0; continue; } } emit dataChanged(node, node, { Qt::CheckStateRole }); todo.push(node); row++; } // siblings and unrelated nodes are ignored } return true; } bool FileIgnoreProxy::shouldExpand(QModelIndex index) { QModelIndex sourceIndex = mapToSource(index); QFileSystemModel* fsm = qobject_cast(sourceModel()); if (!fsm) { return false; } auto blockedPath = relPath(fsm->filePath(sourceIndex)); auto found = m_blocked.find(blockedPath); if (found) { return !found->leaf(); } return false; } void FileIgnoreProxy::setBlockedPaths(QStringList paths) { beginResetModel(); m_blocked.clear(); m_blocked.insert(paths); endResetModel(); } bool FileIgnoreProxy::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const { Q_UNUSED(source_parent) // adjust the columns you want to filter out here // return false for those that will be hidden if (source_column == 2 || source_column == 3) return false; return true; } bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); QFileSystemModel* fsm = qobject_cast(sourceModel()); auto fileInfo = fsm->fileInfo(index); return !ignoreFile(fileInfo); } bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const { if (m_ignoreFiles.contains(fileInfo.fileName())) { return true; } for (const auto& suffix : m_ignoreFilesSuffixes) { if (fileInfo.fileName().endsWith(suffix)) { return true; } } if (m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath()))) { return true; } return false; } bool FileIgnoreProxy::filterFile(const QFileInfo& file) const { return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file); } void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) { QFile ignoreFile(fileName); if (!ignoreFile.open(QIODevice::ReadOnly)) { return; } auto ignoreData = ignoreFile.readAll(); auto string = QString::fromUtf8(ignoreData); setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); } void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName) { auto ignoreData = blockedPaths().toStringList().join('\n').toUtf8(); try { FS::write(fileName, ignoreData); } catch (const Exception& e) { qWarning() << e.cause(); } } PrismLauncher-10.0.5/launcher/MMCTime.cpp0000644000175100017510000000604615144136756017541 0ustar runnerrunner/* * Copyright 2015 Petr Mrazek * Copyright 2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include QString Time::prettifyDuration(int64_t duration, bool noDays) { int seconds = (int)(duration % 60); duration /= 60; int minutes = (int)(duration % 60); duration /= 60; int hours = (int)(noDays ? duration : (duration % 24)); int days = (int)(noDays ? 0 : (duration / 24)); if ((hours == 0) && (days == 0)) { return QObject::tr("%1min %2s").arg(minutes).arg(seconds); } if (days == 0) { return QObject::tr("%1h %2min").arg(hours).arg(minutes); } return QObject::tr("%1d %2h %3min").arg(days).arg(hours).arg(minutes); } QString Time::humanReadableDuration(double duration, int precision) { using days = std::chrono::duration>; QString outStr; QTextStream os(&outStr); bool neg = false; if (duration < 0) { neg = true; // flag duration *= -1; // invert } auto std_duration = std::chrono::duration(duration); auto d = std::chrono::duration_cast(std_duration); std_duration -= d; auto h = std::chrono::duration_cast(std_duration); std_duration -= h; auto m = std::chrono::duration_cast(std_duration); std_duration -= m; auto s = std::chrono::duration_cast(std_duration); std_duration -= s; auto ms = std::chrono::duration_cast(std_duration); auto dc = d.count(); auto hc = h.count(); auto mc = m.count(); auto sc = s.count(); auto msc = ms.count(); if (neg) { os << '-'; } if (dc) { os << dc << QObject::tr("days"); } if (hc) { if (dc) os << " "; os << qSetFieldWidth(2) << hc << QObject::tr("h"); // hours } if (mc) { if (dc || hc) os << " "; os << qSetFieldWidth(2) << mc << QObject::tr("m"); // minutes } if (dc || hc || mc || sc) { if (dc || hc || mc) os << " "; os << qSetFieldWidth(2) << sc << QObject::tr("s"); // seconds } if ((msc && (precision > 0)) || !(dc || hc || mc || sc)) { if (dc || hc || mc || sc) os << " "; os << qSetFieldWidth(0) << qSetRealNumberPrecision(precision) << msc << QObject::tr("ms"); // miliseconds } os.flush(); return outStr; } PrismLauncher-10.0.5/launcher/SeparatorPrefixTree.h0000644000175100017510000001573715144136756021720 0ustar runnerrunner#pragma once #include #include #include template class SeparatorPrefixTree { public: SeparatorPrefixTree(QStringList paths) { insert(paths); } SeparatorPrefixTree(bool contained = false) { m_contained = contained; } void insert(QStringList paths) { for (auto& path : paths) { insert(path); } } /// insert an exact path into the tree SeparatorPrefixTree& insert(QString path) { auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { children[path] = SeparatorPrefixTree(true); return children[path]; } else { auto prefix = path.left(sepIndex); if (!children.contains(prefix)) { children[prefix] = SeparatorPrefixTree(false); } return children[prefix].insert(path.mid(sepIndex + 1)); } } /// is the path fully contained in the tree? bool contains(QString path) const { auto node = find(path); return node != nullptr; } /// does the tree cover a path? That means the prefix of the path is contained in the tree bool covers(QString path) const { // if we found some valid node, it's good enough. the tree covers the path if (m_contained) { return true; } auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { auto found = children.find(path); if (found == children.end()) { return false; } return (*found).covers(QString()); } else { auto prefix = path.left(sepIndex); auto found = children.find(prefix); if (found == children.end()) { return false; } return (*found).covers(path.mid(sepIndex + 1)); } } /// return the contained path that covers the path specified QString cover(QString path) const { // if we found some valid node, it's good enough. the tree covers the path if (m_contained) { return QString(""); } auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { auto found = children.find(path); if (found == children.end()) { return QString(); } auto nested = (*found).cover(QString()); if (nested.isNull()) { return nested; } if (nested.isEmpty()) return path; return path + Tseparator + nested; } else { auto prefix = path.left(sepIndex); auto found = children.find(prefix); if (found == children.end()) { return QString(); } auto nested = (*found).cover(path.mid(sepIndex + 1)); if (nested.isNull()) { return nested; } if (nested.isEmpty()) return prefix; return prefix + Tseparator + nested; } } /// Does the path-specified node exist in the tree? It does not have to be contained. bool exists(QString path) const { auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { auto found = children.find(path); if (found == children.end()) { return false; } return true; } else { auto prefix = path.left(sepIndex); auto found = children.find(prefix); if (found == children.end()) { return false; } return (*found).exists(path.mid(sepIndex + 1)); } } /// find a node in the tree by name const SeparatorPrefixTree* find(QString path) const { auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { auto found = children.find(path); if (found == children.end()) { return nullptr; } return &(*found); } else { auto prefix = path.left(sepIndex); auto found = children.find(prefix); if (found == children.end()) { return nullptr; } return (*found).find(path.mid(sepIndex + 1)); } } /// is this a leaf node? bool leaf() const { return children.isEmpty(); } /// is this node actually contained in the tree, or is it purely structural? bool contained() const { return m_contained; } /// Remove a path from the tree bool remove(QString path) { return removeInternal(path) != Failed; } /// Clear all children of this node tree node void clear() { children.clear(); } QStringList toStringList() const { QStringList collected; // collecting these is more expensive. auto iter = children.begin(); while (iter != children.end()) { QStringList list = iter.value().toStringList(); for (int i = 0; i < list.size(); i++) { list[i] = iter.key() + Tseparator + list[i]; } collected.append(list); if ((*iter).m_contained) { collected.append(iter.key()); } iter++; } return collected; } private: enum Removal { Failed, Succeeded, HasChildren }; Removal removeInternal(QString path = QString()) { if (path.isEmpty()) { if (!m_contained) { // remove all children - we are removing a prefix clear(); return Succeeded; } m_contained = false; if (children.size()) { return HasChildren; } return Succeeded; } Removal remStatus = Failed; QString childToRemove; auto sepIndex = path.indexOf(Tseparator); if (sepIndex == -1) { childToRemove = path; auto found = children.find(childToRemove); if (found == children.end()) { return Failed; } remStatus = (*found).removeInternal(); } else { childToRemove = path.left(sepIndex); auto found = children.find(childToRemove); if (found == children.end()) { return Failed; } remStatus = (*found).removeInternal(path.mid(sepIndex + 1)); } switch (remStatus) { case Failed: case HasChildren: { return remStatus; } case Succeeded: { children.remove(childToRemove); if (m_contained) { return HasChildren; } if (children.size()) { return HasChildren; } return Succeeded; } } return Failed; } private: QMap> children; bool m_contained = false; }; PrismLauncher-10.0.5/launcher/FastFileIconProvider.h0000644000175100017510000000161715144136756021773 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include class FastFileIconProvider : public QFileIconProvider { public: QIcon icon(const QFileInfo& info) const override; }; PrismLauncher-10.0.5/launcher/ResourceDownloadTask.cpp0000644000175100017510000001177115144136756022411 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022-2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ResourceDownloadTask.h" #include "Application.h" #include "FileSystem.h" #include "minecraft/mod/ResourceFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" #include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, const std::shared_ptr packs, bool is_indexed) : m_pack(std::move(pack)), m_pack_version(std::move(version)), m_pack_model(packs) { if (is_indexed) { m_update_task.reset(new LocalResourceUpdateTask(m_pack_model->indexDir(), *m_pack, m_pack_version)); connect(m_update_task.get(), &LocalResourceUpdateTask::hasOldResource, this, &ResourceDownloadTask::hasOldResource); addTask(m_update_task); } m_filesNetJob.reset(new NetJob(tr("Resource download"), APPLICATION->network())); m_filesNetJob->setStatus(tr("Downloading resource:\n%1").arg(m_pack_version.downloadUrl)); auto action = Net::ApiDownload::makeFile(m_pack_version.downloadUrl, m_pack_model->dir().absoluteFilePath(getFilename())); if (!m_pack_version.hash_type.isEmpty() && !m_pack_version.hash.isEmpty()) { switch (Hashing::algorithmFromString(m_pack_version.hash_type)) { case Hashing::Algorithm::Md4: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Md4, m_pack_version.hash)); break; case Hashing::Algorithm::Md5: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Md5, m_pack_version.hash)); break; case Hashing::Algorithm::Sha1: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha1, m_pack_version.hash)); break; case Hashing::Algorithm::Sha256: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_pack_version.hash)); break; case Hashing::Algorithm::Sha512: action->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha512, m_pack_version.hash)); break; default: break; } } m_filesNetJob->addNetAction(action); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &ResourceDownloadTask::propagateStepProgress); connect(m_filesNetJob.get(), &NetJob::failed, this, &ResourceDownloadTask::downloadFailed); addTask(m_filesNetJob); } void ResourceDownloadTask::downloadSucceeded() { m_filesNetJob.reset(); auto oldName = std::get<0>(to_delete); auto oldFilename = std::get<1>(to_delete); if (oldName.isEmpty() || oldFilename == m_pack_version.fileName) return; m_pack_model->uninstallResource(oldFilename, true); // also rename the shader config file if (dynamic_cast(m_pack_model.get()) != nullptr) { QFileInfo oldConfig(m_pack_model->dir(), oldFilename + ".txt"); QFileInfo newConfig(m_pack_model->dir(), getFilename() + ".txt"); if (oldConfig.exists() && !newConfig.exists()) { bool success = FS::move(oldConfig.filePath(), newConfig.filePath()); if (!success) emit logWarning(tr("Failed to rename shader config from '%1' to '%2'").arg(oldConfig.fileName(), newConfig.fileName())); } } } void ResourceDownloadTask::downloadFailed(QString reason) { m_filesNetJob.reset(); emitFailed(reason); } void ResourceDownloadTask::downloadProgressChanged(qint64 current, qint64 total) { emit progress(current, total); } // This indirection is done so that we don't delete a mod before being sure it was // downloaded successfully! void ResourceDownloadTask::hasOldResource(QString name, QString filename) { to_delete = { name, filename }; } PrismLauncher-10.0.5/launcher/InstanceCopyTask.cpp0000644000175100017510000001734715144136756021536 0ustar runnerrunner#include "InstanceCopyTask.h" #include #include #include #include "FileSystem.h" #include "Filter.h" #include "NullInstance.h" #include "settings/INISettingsObject.h" #include "tasks/Task.h" InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs) { m_origInstance = origInstance; m_keepPlaytime = prefs.isKeepPlaytimeEnabled(); m_useLinks = prefs.isUseSymLinksEnabled(); m_linkRecursively = prefs.isLinkRecursivelyEnabled(); m_useHardLinks = prefs.isLinkRecursivelyEnabled() && prefs.isUseHardLinksEnabled(); m_copySaves = prefs.isLinkRecursivelyEnabled() && prefs.isDontLinkSavesEnabled() && prefs.isCopySavesEnabled(); m_useClone = prefs.isUseCloneEnabled(); QString filters = prefs.getSelectedFiltersAsRegex(); if (m_useLinks || m_useHardLinks) { if (!filters.isEmpty()) filters += "|"; filters += "instance.cfg"; } qDebug() << "CopyFilters:" << filters; if (!filters.isEmpty()) { // Set regex filter: // FIXME: get this from the original instance type... QRegularExpression regexp(filters, QRegularExpression::CaseInsensitiveOption); m_matcher = Filters::regexp(regexp); } } void InstanceCopyTask::executeTask() { setStatus(tr("Copying instance %1").arg(m_origInstance->name())); m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { if (m_useClone) { FS::clone folderClone(m_origInstance->instanceRoot(), m_stagingPath); folderClone.matcher(m_matcher); folderClone(true); setProgress(0, folderClone.totalCloned()); connect(&folderClone, &FS::clone::fileCloned, [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); return folderClone(); } if (m_useLinks || m_useHardLinks) { std::unique_ptr savesCopy; if (m_copySaves) { QFileInfo mcDir(FS::PathCombine(m_stagingPath, "minecraft")); QFileInfo dotMCDir(FS::PathCombine(m_stagingPath, ".minecraft")); QString staging_mc_dir; if (dotMCDir.exists() && !mcDir.exists()) staging_mc_dir = dotMCDir.filePath(); else staging_mc_dir = mcDir.filePath(); savesCopy = std::make_unique(FS::PathCombine(m_origInstance->gameRoot(), "saves"), FS::PathCombine(staging_mc_dir, "saves")); (*savesCopy)(true); setProgress(0, savesCopy->totalCopied()); connect(savesCopy.get(), &FS::copy::fileCopied, [this](QString src) { setProgress(m_progress + 1, m_progressTotal); }); } FS::create_link folderLink(m_origInstance->instanceRoot(), m_stagingPath); int depth = m_linkRecursively ? -1 : 0; // we need to at least link the top level instead of the instance folder folderLink.linkRecursively(true).setMaxDepth(depth).useHardLinks(m_useHardLinks).matcher(m_matcher); folderLink(true); setProgress(0, m_progressTotal + folderLink.totalToLink()); connect(&folderLink, &FS::create_link::fileLinked, [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); bool there_were_errors = false; if (!folderLink()) { #if defined Q_OS_WIN32 if (!m_useHardLinks) { setProgress(0, m_progressTotal); qDebug() << "EXPECTED: Link failure, Windows requires permissions for symlinks"; qDebug() << "attempting to run with privelage"; QEventLoop loop; bool got_priv_results = false; connect(&folderLink, &FS::create_link::finishedPrivileged, this, [&got_priv_results, &loop](bool gotResults) { if (!gotResults) { qDebug() << "Privileged run exited without results!"; } got_priv_results = gotResults; loop.quit(); }); folderLink.runPrivileged(); loop.exec(); // wait for the finished signal for (auto result : folderLink.getResults()) { if (result.err_value != 0) { there_were_errors = true; } } if (savesCopy) { there_were_errors |= !(*savesCopy)(); } return got_priv_results && !there_were_errors; } #else qDebug() << "Link Failed!" << folderLink.getOSError().value() << folderLink.getOSError().message().c_str(); #endif return false; } if (savesCopy) { there_were_errors |= !(*savesCopy)(); } return !there_were_errors; } FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); folderCopy.matcher(m_matcher); folderCopy(true); setProgress(0, folderCopy.totalCopied()); connect(&folderCopy, &FS::copy::fileCopied, [this]() { setProgress(m_progress + 1, m_progressTotal); }); return folderCopy(); }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &InstanceCopyTask::copyAborted); m_copyFutureWatcher.setFuture(m_copyFuture); } void InstanceCopyTask::copyFinished() { auto successful = m_copyFuture.result(); if (!successful) { emitFailed(tr("Instance folder copy failed.")); return; } // FIXME: shouldn't this be able to report errors? auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); inst->setName(name()); inst->setIconKey(m_instIcon); if (!m_keepPlaytime) { inst->resetTimePlayed(); } if (m_useLinks) { inst->addLinkedInstanceId(m_origInstance->id()); auto allowed_symlinks_file = QFileInfo(FS::PathCombine(inst->gameRoot(), "allowed_symlinks.txt")); QByteArray allowed_symlinks; if (allowed_symlinks_file.exists()) { allowed_symlinks.append(FS::read(allowed_symlinks_file.filePath())); if (allowed_symlinks.right(1) != "\n") allowed_symlinks.append("\n"); // we want to be on a new line } allowed_symlinks.append(m_origInstance->gameRoot().toUtf8()); allowed_symlinks.append("\n"); if (allowed_symlinks_file.isSymLink()) FS::deletePath( allowed_symlinks_file .filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link. try { FS::write(allowed_symlinks_file.filePath(), allowed_symlinks); } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write symlink :" << e.cause(); } } emitSucceeded(); } void InstanceCopyTask::copyAborted() { emitFailed(tr("Instance folder copy has been aborted.")); return; } bool InstanceCopyTask::abort() { if (m_copyFutureWatcher.isRunning()) { m_copyFutureWatcher.cancel(); // NOTE: Here we don't do `emitAborted()` because it will be done when `m_copyFutureWatcher` actually cancels, which may not occur // immediately. return true; } return false; } PrismLauncher-10.0.5/launcher/meta/0000755000175100017510000000000015144136756016522 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/meta/JsonFormat.h0000644000175100017510000000372415144136756020763 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "Exception.h" namespace Meta { class Index; class Version; class VersionList; enum class MetadataVersion { Invalid = -1, InitialRelease = 1 }; class ParseException : public Exception { public: using Exception::Exception; }; struct Require { bool operator==(const Require& rhs) const { return uid == rhs.uid; } bool operator<(const Require& rhs) const { return uid < rhs.uid; } bool deepEquals(const Require& rhs) const { return uid == rhs.uid && equalsVersion == rhs.equalsVersion && suggests == rhs.suggests; } QString uid; QString equalsVersion; QString suggests; }; using RequireSet = std::set; void parseIndex(const QJsonObject& obj, Index* ptr); void parseVersion(const QJsonObject& obj, Version* ptr); void parseVersionList(const QJsonObject& obj, VersionList* ptr); MetadataVersion parseFormatVersion(const QJsonObject& obj, bool required = true); void serializeFormatVersion(QJsonObject& obj, MetadataVersion version); // FIXME: this has a different shape than the others...FIX IT!? void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char* keyName = "requires"); void serializeRequires(QJsonObject& objOut, RequireSet* ptr, const char* keyName = "requires"); MetadataVersion currentFormatVersion(); } // namespace Meta Q_DECLARE_METATYPE(std::set) PrismLauncher-10.0.5/launcher/meta/Index.cpp0000644000175100017510000001136215144136756020300 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Index.h" #include "JsonFormat.h" #include "QObjectPtr.h" #include "VersionList.h" #include "meta/BaseEntity.h" #include "tasks/SequentialTask.h" namespace Meta { Index::Index(QObject* parent) : QAbstractListModel(parent) {} Index::Index(const QList& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) { for (int i = 0; i < m_lists.size(); ++i) { m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); connectVersionList(i, m_lists.at(i)); } } QVariant Index::data(const QModelIndex& index, int role) const { if (index.parent().isValid() || index.row() < 0 || index.row() >= m_lists.size()) { return QVariant(); } VersionList::Ptr list = m_lists.at(index.row()); switch (role) { case Qt::DisplayRole: if (index.column() == 0) { return list->humanReadable(); } else { break; } case UidRole: return list->uid(); case NameRole: return list->name(); case ListPtrRole: return QVariant::fromValue(list); } return QVariant(); } int Index::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_lists.size(); } int Index::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) { return tr("Name"); } else { return QVariant(); } } bool Index::hasUid(const QString& uid) const { return m_uids.contains(uid); } VersionList::Ptr Index::get(const QString& uid) { VersionList::Ptr out = m_uids.value(uid, nullptr); if (!out) { out = std::make_shared(uid); m_uids[uid] = out; m_lists.append(out); } return out; } Version::Ptr Index::get(const QString& uid, const QString& version) { auto list = get(uid); return list->getVersion(version); } void Index::parse(const QJsonObject& obj) { parseIndex(obj, this); } void Index::merge(const std::shared_ptr& other) { const QList lists = other->m_lists; // initial load, no need to merge if (m_lists.isEmpty()) { beginResetModel(); m_lists = lists; for (int i = 0; i < lists.size(); ++i) { m_uids.insert(lists.at(i)->uid(), lists.at(i)); connectVersionList(i, lists.at(i)); } endResetModel(); } else { for (const VersionList::Ptr& list : lists) { if (m_uids.contains(list->uid())) { m_uids[list->uid()]->mergeFromIndex(list); } else { beginInsertRows(QModelIndex(), m_lists.size(), m_lists.size()); connectVersionList(m_lists.size(), list); m_lists.append(list); m_uids.insert(list->uid(), list); endInsertRows(); } } } } void Index::connectVersionList(const int row, const VersionList::Ptr& list) { connect(list.get(), &VersionList::nameChanged, this, [this, row] { emit dataChanged(index(row), index(row), { Qt::DisplayRole }); }); } Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force) { if (mode == Net::Mode::Offline) { return get(uid, version)->loadTask(mode); } auto versionList = get(uid); auto loadTask = makeShared(tr("Load meta for %1:%2", "This is for the task name that loads the meta index.").arg(uid, version)); if (status() != BaseEntity::LoadStatus::Remote || force) { loadTask->addTask(this->loadTask(mode)); } loadTask->addTask(versionList->loadTask(mode)); loadTask->addTask(versionList->getVersion(version)->loadTask(mode)); return loadTask; } Version::Ptr Index::getLoadedVersion(const QString& uid, const QString& version) { QEventLoop ev; auto task = loadVersion(uid, version); connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); return get(uid, version); } } // namespace Meta PrismLauncher-10.0.5/launcher/meta/VersionList.cpp0000644000175100017510000002364015144136756021514 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "VersionList.h" #include #include #include "Application.h" #include "Index.h" #include "JsonFormat.h" #include "Version.h" #include "meta/BaseEntity.h" #include "net/Mode.h" #include "tasks/SequentialTask.h" namespace Meta { VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(parent), m_uid(uid) { setObjectName("Version list: " + uid); } Task::Ptr VersionList::getLoadTask() { auto loadTask = makeShared(tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online)); loadTask->addTask(this->loadTask(Net::Mode::Online)); return loadTask; } bool VersionList::isLoaded() { return BaseEntity::isLoaded(); } const BaseVersion::Ptr VersionList::at(int i) const { return m_versions.at(i); } int VersionList::count() const { return m_versions.size(); } void VersionList::sortVersions() { beginResetModel(); std::sort(m_versions.begin(), m_versions.end(), [](const Version::Ptr& a, const Version::Ptr& b) { return *a.get() < *b.get(); }); endResetModel(); } QVariant VersionList::data(const QModelIndex& index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_versions.size() || index.parent().isValid()) { return QVariant(); } Version::Ptr version = m_versions.at(index.row()); switch (role) { case VersionPointerRole: return QVariant::fromValue(std::dynamic_pointer_cast(version)); case VersionRole: case VersionIdRole: return version->version(); case ParentVersionRole: { // FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent // uid'. auto& reqs = version->requiredSet(); auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require& req) { return req.uid == "net.minecraft"; }); if (iter != reqs.end()) { return (*iter).equalsVersion; } return QVariant(); } case TypeRole: return version->type(); case UidRole: return version->uid(); case TimeRole: return version->time(); case RequiresRole: return QVariant::fromValue(version->requiredSet()); case SortRole: return version->rawTime(); case VersionPtrRole: return QVariant::fromValue(version); case RecommendedRole: return version->isRecommended() || m_externalRecommendsVersions.contains(version->version()); case JavaMajorRole: { auto major = version->version(); if (major.startsWith("java")) { major = "Java " + major.mid(4); } return major; } // FIXME: this should be determined in whatever view/proxy is used... // case LatestRole: return version == getLatestStable(); default: return QVariant(); } } BaseVersionList::RoleList VersionList::providesRoles() const { return m_provided_roles; } void VersionList::setProvidedRoles(RoleList roles) { m_provided_roles = roles; }; QHash VersionList::roleNames() const { QHash roles = BaseVersionList::roleNames(); roles.insert(UidRole, "uid"); roles.insert(TimeRole, "time"); roles.insert(SortRole, "sort"); roles.insert(RequiresRole, "requires"); return roles; } QString VersionList::localFilename() const { return m_uid + "/index.json"; } QString VersionList::humanReadable() const { return m_name.isEmpty() ? m_uid : m_name; } Version::Ptr VersionList::getVersion(const QString& version) { Version::Ptr out = m_lookup.value(version, nullptr); if (!out) { out = std::make_shared(m_uid, version); m_lookup[version] = out; setupAddedVersion(m_versions.size(), out); m_versions.append(out); } return out; } bool VersionList::hasVersion(QString version) const { auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [version](Meta::Version::Ptr const& a) { return a->version() == version; }); return (ver != m_versions.constEnd()); } void VersionList::setName(const QString& name) { m_name = name; emit nameChanged(name); } void VersionList::setVersions(const QList& versions) { beginResetModel(); m_versions = versions; std::sort(m_versions.begin(), m_versions.end(), [](const Version::Ptr& a, const Version::Ptr& b) { return a->rawTime() > b->rawTime(); }); for (int i = 0; i < m_versions.size(); ++i) { m_lookup.insert(m_versions.at(i)->version(), m_versions.at(i)); setupAddedVersion(i, m_versions.at(i)); } // FIXME: this is dumb, we have 'recommended' as part of the metadata already... auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const Version::Ptr& ptr) { return ptr->type() == "release"; }); m_recommended = recommendedIt == m_versions.constEnd() ? nullptr : *recommendedIt; endResetModel(); } void VersionList::parse(const QJsonObject& obj) { parseVersionList(obj, this); } void VersionList::addExternalRecommends(const QStringList& recommends) { m_externalRecommendsVersions.append(recommends); } void VersionList::clearExternalRecommends() { m_externalRecommendsVersions.clear(); } // FIXME: this is dumb, we have 'recommended' as part of the metadata already... static const Meta::Version::Ptr& getBetterVersion(const Meta::Version::Ptr& a, const Meta::Version::Ptr& b) { if (!a) return b; if (!b) return a; if (a->type() == b->type()) { // newer of same type wins return (a->rawTime() > b->rawTime() ? a : b); } // 'release' type wins return (a->type() == "release" ? a : b); } void VersionList::mergeFromIndex(const VersionList::Ptr& other) { if (m_name != other->m_name) { setName(other->m_name); } if (!other->m_sha256.isEmpty()) { m_sha256 = other->m_sha256; } } void VersionList::merge(const VersionList::Ptr& other) { if (m_name != other->m_name) { setName(other->m_name); } if (!other->m_sha256.isEmpty()) { m_sha256 = other->m_sha256; } // TODO: do not reset the whole model. maybe? beginResetModel(); if (other->m_versions.isEmpty()) { qWarning() << "Empty list loaded ..."; } for (auto version : other->m_versions) { // we already have the version. merge the contents if (m_lookup.contains(version->version())) { auto existing = m_lookup.value(version->version()); existing->mergeFromList(version); version = existing; } else { m_lookup.insert(version->version(), version); // connect it. setupAddedVersion(m_versions.size(), version); m_versions.append(version); } m_recommended = getBetterVersion(m_recommended, version); } endResetModel(); } void VersionList::setupAddedVersion(const int row, const Version::Ptr& version) { disconnect(version.get(), &Version::requiresChanged, this, nullptr); disconnect(version.get(), &Version::timeChanged, this, nullptr); disconnect(version.get(), &Version::typeChanged, this, nullptr); connect(version.get(), &Version::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QList() << RequiresRole); }); connect(version.get(), &Version::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); }); connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); } BaseVersion::Ptr VersionList::getRecommended() const { return m_recommended; } void VersionList::waitToLoad() { if (isLoaded()) return; QEventLoop ev; auto task = getLoadTask(); connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); } Version::Ptr VersionList::getRecommendedForParent(const QString& uid, const QString& version) { auto foundExplicit = std::find_if(m_versions.begin(), m_versions.end(), [uid, version](Version::Ptr ver) -> bool { auto& reqs = ver->requiredSet(); auto parentReq = std::find_if(reqs.begin(), reqs.end(), [uid, version](const Require& req) -> bool { return req.uid == uid && req.equalsVersion == version; }); return parentReq != reqs.end() && ver->isRecommended(); }); if (foundExplicit != m_versions.end()) { return *foundExplicit; } return nullptr; } Version::Ptr VersionList::getLatestForParent(const QString& uid, const QString& version) { Version::Ptr latestCompat = nullptr; for (auto ver : m_versions) { auto& reqs = ver->requiredSet(); auto parentReq = std::find_if(reqs.begin(), reqs.end(), [uid, version](const Require& req) -> bool { return req.uid == uid && req.equalsVersion == version; }); if (parentReq != reqs.end()) { latestCompat = getBetterVersion(latestCompat, ver); } } return latestCompat; } } // namespace Meta PrismLauncher-10.0.5/launcher/meta/Index.h0000644000175100017510000000433215144136756017744 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "BaseEntity.h" #include "meta/VersionList.h" #include "net/Mode.h" class Task; namespace Meta { class Index : public QAbstractListModel, public BaseEntity { Q_OBJECT public: explicit Index(QObject* parent = nullptr); explicit Index(const QList& lists, QObject* parent = nullptr); virtual ~Index() = default; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; QVariant data(const QModelIndex& index, int role) const override; int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; QString localFilename() const override { return "index.json"; } // queries VersionList::Ptr get(const QString& uid); Version::Ptr get(const QString& uid, const QString& version); bool hasUid(const QString& uid) const; QList lists() const { return m_lists; } Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); // this blocks until the version is loaded Version::Ptr getLoadedVersion(const QString& uid, const QString& version); public: // for usage by parsers only void merge(const std::shared_ptr& other); protected: void parse(const QJsonObject& obj) override; private: QList m_lists; QHash m_uids; void connectVersionList(int row, const VersionList::Ptr& list); }; } // namespace Meta PrismLauncher-10.0.5/launcher/meta/VersionList.h0000644000175100017510000000644115144136756021161 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "BaseEntity.h" #include "BaseVersionList.h" #include "meta/Version.h" namespace Meta { class VersionList : public BaseVersionList, public BaseEntity { Q_OBJECT Q_PROPERTY(QString uid READ uid CONSTANT) Q_PROPERTY(QString name READ name NOTIFY nameChanged) public: explicit VersionList(const QString& uid, QObject* parent = nullptr); virtual ~VersionList() = default; using Ptr = std::shared_ptr; enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole }; bool isLoaded() override; Task::Ptr getLoadTask() override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; BaseVersion::Ptr getRecommended() const override; Version::Ptr getRecommendedForParent(const QString& uid, const QString& version); Version::Ptr getLatestForParent(const QString& uid, const QString& version); QVariant data(const QModelIndex& index, int role) const override; RoleList providesRoles() const override; QHash roleNames() const override; void setProvidedRoles(RoleList roles); QString localFilename() const override; QString uid() const { return m_uid; } QString name() const { return m_name; } QString humanReadable() const; Version::Ptr getVersion(const QString& version); bool hasVersion(QString version) const; QList versions() const { return m_versions; } // this blocks until the version list is loaded void waitToLoad(); public: // for usage only by parsers void setName(const QString& name); void setVersions(const QList& versions); void merge(const VersionList::Ptr& other); void mergeFromIndex(const VersionList::Ptr& other); void parse(const QJsonObject& obj) override; void addExternalRecommends(const QStringList& recommends); void clearExternalRecommends(); signals: void nameChanged(const QString& name); protected slots: void updateListData(QList) override {} private: QList m_versions; QStringList m_externalRecommendsVersions; QHash m_lookup; QString m_uid; QString m_name; Version::Ptr m_recommended; RoleList m_provided_roles = { VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, TypeRole, UidRole, TimeRole, RequiresRole, SortRole, RecommendedRole, LatestRole, VersionPtrRole }; void setupAddedVersion(int row, const Version::Ptr& version); }; } // namespace Meta Q_DECLARE_METATYPE(Meta::VersionList::Ptr) PrismLauncher-10.0.5/launcher/meta/Version.cpp0000644000175100017510000000567515144136756020670 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Version.h" #include #include "JsonFormat.h" Meta::Version::Version(const QString& uid, const QString& version) : BaseVersion(), m_uid(uid), m_version(version) {} QString Meta::Version::descriptor() const { return m_version; } QString Meta::Version::name() const { if (m_data) return m_data->name; return m_uid; } QString Meta::Version::typeString() const { return m_type; } QDateTime Meta::Version::time() const { return QDateTime::fromMSecsSinceEpoch(m_time * 1000, Qt::UTC); } void Meta::Version::parse(const QJsonObject& obj) { parseVersion(obj, this); } void Meta::Version::mergeFromList(const Meta::Version::Ptr& other) { if (other->m_providesRecommendations) { if (m_recommended != other->m_recommended) { setRecommended(other->m_recommended); } } if (m_type != other->m_type) { setType(other->m_type); } if (m_time != other->m_time) { setTime(other->m_time); } if (m_requires != other->m_requires) { m_requires = other->m_requires; } if (m_conflicts != other->m_conflicts) { m_conflicts = other->m_conflicts; } if (m_volatile != other->m_volatile) { setVolatile(other->m_volatile); } if (!other->m_sha256.isEmpty()) { m_sha256 = other->m_sha256; } } void Meta::Version::merge(const Version::Ptr& other) { mergeFromList(other); if (other->m_data) { setData(other->m_data); } } QString Meta::Version::localFilename() const { return m_uid + '/' + m_version + ".json"; } ::Version Meta::Version::toComparableVersion() const { return { descriptor() }; } void Meta::Version::setType(const QString& type) { m_type = type; emit typeChanged(); } void Meta::Version::setTime(const qint64 time) { m_time = time; emit timeChanged(); } void Meta::Version::setRequires(const Meta::RequireSet& reqs, const Meta::RequireSet& conflicts) { m_requires = reqs; m_conflicts = conflicts; emit requiresChanged(); } void Meta::Version::setVolatile(bool volatile_) { m_volatile = volatile_; } void Meta::Version::setData(const VersionFilePtr& data) { m_data = data; } void Meta::Version::setProvidesRecommendations() { m_providesRecommendations = true; } void Meta::Version::setRecommended(bool recommended) { m_recommended = recommended; } PrismLauncher-10.0.5/launcher/meta/BaseEntity.h0000644000175100017510000000363115144136756020745 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "net/Mode.h" #include "net/NetJob.h" #include "tasks/Task.h" namespace Meta { class BaseEntityLoadTask; class BaseEntity { friend BaseEntityLoadTask; public: /* types */ using Ptr = std::shared_ptr; enum class LoadStatus { NotLoaded, Local, Remote }; public: virtual ~BaseEntity() = default; virtual QString localFilename() const = 0; virtual QUrl url() const; bool isLoaded() const; LoadStatus status() const; /* for parsers */ void setSha256(QString sha256); virtual void parse(const QJsonObject& obj) = 0; [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online); protected: QString m_sha256; // the expected sha256 QString m_file_sha256; // the file sha256 private: LoadStatus m_load_status = LoadStatus::NotLoaded; Task::Ptr m_task; }; class BaseEntityLoadTask : public Task { Q_OBJECT public: explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode); ~BaseEntityLoadTask() override = default; virtual void executeTask() override; virtual bool canAbort() const override; virtual bool abort() override; private: BaseEntity* m_entity; Net::Mode m_mode; NetJob::Ptr m_task; }; } // namespace Meta PrismLauncher-10.0.5/launcher/meta/BaseEntity.cpp0000644000175100017510000001534715144136756021307 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "BaseEntity.h" #include "Exception.h" #include "FileSystem.h" #include "Json.h" #include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" #include "net/HttpMetaCache.h" #include "net/Mode.h" #include "net/NetJob.h" #include "Application.h" #include "BuildConfig.h" #include "tasks/Task.h" namespace Meta { class ParsingValidator : public Net::Validator { public: /* con/des */ ParsingValidator(BaseEntity* entity) : m_entity(entity) {}; virtual ~ParsingValidator() = default; public: /* methods */ bool init(QNetworkRequest&) override { m_data.clear(); return true; } bool write(QByteArray& data) override { this->m_data.append(data); return true; } bool abort() override { m_data.clear(); return true; } bool validate(QNetworkReply&) override { auto fname = m_entity->localFilename(); try { auto doc = Json::requireDocument(m_data, fname); auto obj = Json::requireObject(doc, fname); m_entity->parse(obj); return true; } catch (const Exception& e) { qWarning() << "Unable to parse response:" << e.cause(); return false; } } private: /* data */ QByteArray m_data; BaseEntity* m_entity; }; QUrl BaseEntity::url() const { auto s = APPLICATION->settings(); QString metaOverride = s->get("MetaURLOverride").toString(); if (metaOverride.isEmpty()) { return QUrl(BuildConfig.META_URL).resolved(localFilename()); } return QUrl(metaOverride).resolved(localFilename()); } Task::Ptr BaseEntity::loadTask(Net::Mode mode) { if (m_task && m_task->isRunning()) { return m_task; } m_task.reset(new BaseEntityLoadTask(this, mode)); return m_task; } bool BaseEntity::isLoaded() const { // consider it loaded only if the main hash is either empty and was remote loadded or the hashes match and was loaded return m_sha256.isEmpty() ? m_load_status == LoadStatus::Remote : m_load_status != LoadStatus::NotLoaded && m_sha256 == m_file_sha256; } void BaseEntity::setSha256(QString sha256) { m_sha256 = sha256; } BaseEntity::LoadStatus BaseEntity::status() const { return m_load_status; } BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {} void BaseEntityLoadTask::executeTask() { const QString fname = QDir("meta").absoluteFilePath(m_entity->localFilename()); auto hashMatches = false; // the file exists on disk try to load it if (QFile::exists(fname)) { try { QByteArray fileData; // read local file if nothing is loaded yet if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded || m_entity->m_file_sha256.isEmpty()) { setStatus(tr("Loading local file")); fileData = FS::read(fname); m_entity->m_file_sha256 = Hashing::hash(fileData, Hashing::Algorithm::Sha256); } // on online the hash needs to match hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256; if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) { throw Exception("mismatched checksum"); } // load local file if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded) { auto doc = Json::requireDocument(fileData, fname); auto obj = Json::requireObject(doc, fname); m_entity->parse(obj); m_entity->m_load_status = BaseEntity::LoadStatus::Local; } } catch (const Exception& e) { qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); // just make sure it's gone and we never consider it again. FS::deletePath(fname); m_entity->m_load_status = BaseEntity::LoadStatus::NotLoaded; } } // if we need remote update, run the update task auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline; // if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches; if (wasLoadedOffline || wasLoadedRemote) { emitSucceeded(); return; } m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network())); auto url = m_entity->url(); auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename()); entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); /* * The validator parses the file and loads it into the object. * If that fails, the file is not written to storage. */ if (!m_entity->m_sha256.isEmpty()) dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_entity->m_sha256)); dl->addValidator(new ParsingValidator(m_entity)); m_task->addNetAction(dl); m_task->setAskRetry(false); connect(m_task.get(), &Task::failed, this, &BaseEntityLoadTask::emitFailed); connect(m_task.get(), &Task::succeeded, this, &BaseEntityLoadTask::emitSucceeded); connect(m_task.get(), &Task::succeeded, this, [this]() { m_entity->m_load_status = BaseEntity::LoadStatus::Remote; m_entity->m_file_sha256 = m_entity->m_sha256; }); connect(m_task.get(), &Task::progress, this, &Task::setProgress); connect(m_task.get(), &Task::stepProgress, this, &BaseEntityLoadTask::propagateStepProgress); connect(m_task.get(), &Task::status, this, &Task::setStatus); connect(m_task.get(), &Task::details, this, &Task::setDetails); m_task->start(); } bool BaseEntityLoadTask::canAbort() const { return m_task ? m_task->canAbort() : false; } bool BaseEntityLoadTask::abort() { if (m_task) { Task::abort(); return m_task->abort(); } return Task::abort(); } } // namespace Meta PrismLauncher-10.0.5/launcher/meta/JsonFormat.cpp0000644000175100017510000001447615144136756021324 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "JsonFormat.h" // FIXME: remove this from here... somehow #include "Json.h" #include "minecraft/OneSixVersionFormat.h" #include "Index.h" #include "Version.h" #include "VersionList.h" using namespace Json; namespace Meta { MetadataVersion currentFormatVersion() { return MetadataVersion::InitialRelease; } // Index static std::shared_ptr parseIndexInternal(const QJsonObject& obj) { const QList objects = requireIsArrayOf(obj, "packages"); QList lists; lists.reserve(objects.size()); std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { VersionList::Ptr list = std::make_shared(requireString(obj, "uid")); list->setName(obj["name"].toString()); list->setSha256(obj["sha256"].toString()); return list; }); return std::make_shared(lists); } // Version static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& obj) { Version::Ptr version = std::make_shared(uid, requireString(obj, "version")); version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000); version->setType(obj["type"].toString()); version->setRecommended(obj["recommended"].toBool()); version->setVolatile(obj["volatile"].toBool()); RequireSet reqs, conflicts; parseRequires(obj, &reqs, "requires"); parseRequires(obj, &conflicts, "conflicts"); version->setRequires(reqs, conflicts); if (auto sha256 = obj["sha256"].toString(); !sha256.isEmpty()) { version->setSha256(sha256); } return version; } static Version::Ptr parseVersionInternal(const QJsonObject& obj) { Version::Ptr version = parseCommonVersion(requireString(obj, "uid"), obj); version->setData(OneSixVersionFormat::versionFileFromJson( QJsonDocument(obj), QString("%1/%2.json").arg(version->uid(), version->version()), obj.contains("order"))); return version; } // Version list / package static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj) { const QString uid = requireString(obj, "uid"); const QList versionsRaw = requireIsArrayOf(obj, "versions"); QList versions; versions.reserve(versionsRaw.size()); std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) { auto version = parseCommonVersion(uid, vObj); version->setProvidesRecommendations(); return version; }); VersionList::Ptr list = std::make_shared(uid); list->setName(obj["name"].toString()); list->setVersions(versions); return list; } MetadataVersion parseFormatVersion(const QJsonObject& obj, bool required) { if (!obj.contains("formatVersion")) { if (required) { return MetadataVersion::Invalid; } return MetadataVersion::InitialRelease; } if (!obj.value("formatVersion").isDouble()) { return MetadataVersion::Invalid; } switch (obj.value("formatVersion").toInt()) { case 0: case 1: return MetadataVersion::InitialRelease; default: return MetadataVersion::Invalid; } } void serializeFormatVersion(QJsonObject& obj, Meta::MetadataVersion version) { if (version == MetadataVersion::Invalid) { return; } obj.insert("formatVersion", int(version)); } void parseIndex(const QJsonObject& obj, Index* ptr) { const MetadataVersion version = parseFormatVersion(obj); switch (version) { case MetadataVersion::InitialRelease: ptr->merge(parseIndexInternal(obj)); break; case MetadataVersion::Invalid: throw ParseException(QObject::tr("Unknown format version!")); } } void parseVersionList(const QJsonObject& obj, VersionList* ptr) { const MetadataVersion version = parseFormatVersion(obj); switch (version) { case MetadataVersion::InitialRelease: ptr->merge(parseVersionListInternal(obj)); break; case MetadataVersion::Invalid: throw ParseException(QObject::tr("Unknown format version!")); } } void parseVersion(const QJsonObject& obj, Version* ptr) { const MetadataVersion version = parseFormatVersion(obj); switch (version) { case MetadataVersion::InitialRelease: ptr->merge(parseVersionInternal(obj)); break; case MetadataVersion::Invalid: throw ParseException(QObject::tr("Unknown format version!")); } } /* [ {"uid":"foo", "equals":"version"} ] */ void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char* keyName) { if (obj.contains(keyName)) { auto reqArray = requireArray(obj, keyName); auto iter = reqArray.begin(); while (iter != reqArray.end()) { auto reqObject = requireObject(*iter); auto uid = requireString(reqObject, "uid"); auto equals = reqObject["equals"].toString(); auto suggests = reqObject["suggests"].toString(); ptr->insert({ uid, equals, suggests }); iter++; } } } void serializeRequires(QJsonObject& obj, RequireSet* ptr, const char* keyName) { if (!ptr || ptr->empty()) { return; } QJsonArray arrOut; for (auto& iter : *ptr) { QJsonObject reqOut; reqOut.insert("uid", iter.uid); if (!iter.equalsVersion.isEmpty()) { reqOut.insert("equals", iter.equalsVersion); } if (!iter.suggests.isEmpty()) { reqOut.insert("suggests", iter.suggests); } arrOut.append(reqOut); } obj.insert(keyName, arrOut); } } // namespace Meta PrismLauncher-10.0.5/launcher/meta/Version.h0000644000175100017510000000531215144136756020321 0ustar runnerrunner/* Copyright 2015-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "../Version.h" #include "BaseVersion.h" #include #include #include #include #include "minecraft/VersionFile.h" #include "BaseEntity.h" #include "JsonFormat.h" namespace Meta { class Version : public QObject, public BaseVersion, public BaseEntity { Q_OBJECT public: using Ptr = std::shared_ptr; explicit Version(const QString& uid, const QString& version); virtual ~Version() = default; QString descriptor() const override; QString name() const override; QString typeString() const override; QString uid() const { return m_uid; } QString version() const { return m_version; } QString type() const { return m_type; } QDateTime time() const; qint64 rawTime() const { return m_time; } const Meta::RequireSet& requiredSet() const { return m_requires; } VersionFilePtr data() const { return m_data; } bool isRecommended() const { return m_recommended; } bool isLoaded() const { return m_data != nullptr && BaseEntity::isLoaded(); } void merge(const Version::Ptr& other); void mergeFromList(const Version::Ptr& other); void parse(const QJsonObject& obj) override; QString localFilename() const override; ::Version toComparableVersion() const; public: // for usage by format parsers only void setType(const QString& type); void setTime(qint64 time); void setRequires(const Meta::RequireSet& reqs, const Meta::RequireSet& conflicts); void setVolatile(bool volatile_); void setRecommended(bool recommended); void setProvidesRecommendations(); void setData(const VersionFilePtr& data); signals: void typeChanged(); void timeChanged(); void requiresChanged(); private: bool m_providesRecommendations = false; bool m_recommended = false; QString m_name; QString m_uid; QString m_version; QString m_type; qint64 m_time = 0; Meta::RequireSet m_requires; Meta::RequireSet m_conflicts; bool m_volatile = false; VersionFilePtr m_data; }; } // namespace Meta Q_DECLARE_METATYPE(Meta::Version::Ptr) PrismLauncher-10.0.5/launcher/RecursiveFileSystemWatcher.cpp0000644000175100017510000000517215144136756023577 0ustar runnerrunner#include "RecursiveFileSystemWatcher.h" #include RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this)) { connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RecursiveFileSystemWatcher::fileChange); connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &RecursiveFileSystemWatcher::directoryChange); } void RecursiveFileSystemWatcher::setRootDir(const QDir& root) { bool wasEnabled = m_isEnabled; disable(); m_root = root; setFiles(scanRecursive(m_root)); if (wasEnabled) { enable(); } } void RecursiveFileSystemWatcher::setWatchFiles(const bool watchFiles) { bool wasEnabled = m_isEnabled; disable(); m_watchFiles = watchFiles; if (wasEnabled) { enable(); } } void RecursiveFileSystemWatcher::enable() { if (m_isEnabled) { return; } Q_ASSERT(m_root != QDir::root()); addFilesToWatcherRecursive(m_root); m_isEnabled = true; } void RecursiveFileSystemWatcher::disable() { if (!m_isEnabled) { return; } m_isEnabled = false; m_watcher->removePaths(m_watcher->files()); m_watcher->removePaths(m_watcher->directories()); } void RecursiveFileSystemWatcher::setFiles(const QStringList& files) { if (files != m_files) { m_files = files; emit filesChanged(); } } void RecursiveFileSystemWatcher::addFilesToWatcherRecursive(const QDir& dir) { m_watcher->addPath(dir.absolutePath()); for (const QString& directory : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { addFilesToWatcherRecursive(dir.absoluteFilePath(directory)); } if (m_watchFiles) { for (const QFileInfo& info : dir.entryInfoList(QDir::Files)) { m_watcher->addPath(info.absoluteFilePath()); } } } QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir& directory) { QStringList ret; if (!m_matcher) { return {}; } for (const QString& dir : directory.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden)) { ret.append(scanRecursive(directory.absoluteFilePath(dir))); } for (const QString& file : directory.entryList(QDir::Files | QDir::Hidden)) { auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); if (m_matcher(relPath)) { ret.append(relPath); } } return ret; } void RecursiveFileSystemWatcher::fileChange(const QString& path) { emit fileChanged(path); } void RecursiveFileSystemWatcher::directoryChange([[maybe_unused]] const QString& path) { setFiles(scanRecursive(m_root)); } PrismLauncher-10.0.5/launcher/SysInfo.cpp0000644000175100017510000000425115144136756017674 0ustar runnerrunner#include #include #include "sys.h" #ifdef Q_OS_MACOS #include #endif #include #include #include #include #ifdef Q_OS_MACOS bool rosettaDetect() { int ret = 0; size_t size = sizeof(ret); if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { return false; } return ret == 1; } #endif namespace SysInfo { QString currentSystem() { #if defined(Q_OS_LINUX) return "linux"; #elif defined(Q_OS_MACOS) return "osx"; #elif defined(Q_OS_WINDOWS) return "windows"; #elif defined(Q_OS_FREEBSD) return "freebsd"; #elif defined(Q_OS_OPENBSD) return "openbsd"; #else return "unknown"; #endif } QString useQTForArch() { #if defined(Q_OS_MACOS) && !defined(Q_PROCESSOR_ARM) if (rosettaDetect()) { return "arm64"; } else { return "x86_64"; } #endif return QSysInfo::currentCpuArchitecture(); } int suitableMaxMem() { float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; int maxMemoryAlloc; // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB if (totalRAM < (4096 * 1.5)) maxMemoryAlloc = (int)(totalRAM / 1.5); else maxMemoryAlloc = 4096; return maxMemoryAlloc; } QString getSupportedJavaArchitecture() { auto sys = currentSystem(); auto arch = useQTForArch(); if (sys == "windows") { if (arch == "x86_64") return "windows-x64"; if (arch == "i386") return "windows-x86"; // Unknown, maybe arm, appending arch return "windows-" + arch; } if (sys == "osx") { if (arch == "arm64") return "mac-os-arm64"; if (arch.contains("64")) return "mac-os-x64"; if (arch.contains("86")) return "mac-os-x86"; // Unknown, maybe something new, appending arch return "mac-os-" + arch; } else if (sys == "linux") { if (arch == "x86_64") return "linux-x64"; if (arch == "i386") return "linux-x86"; // will work for arm32 arm(64) return "linux-" + arch; } return {}; } } // namespace SysInfo PrismLauncher-10.0.5/launcher/Launcher.in0000755000175100017510000000163015144136756017670 0ustar runnerrunner#!/usr/bin/env bash # Basic start script for running the launcher with the libs packaged with it. function printerror { printf "$1" if which zenity >/dev/null; then zenity --error --text="$1" &>/dev/null; elif which kdialog >/dev/null; then kdialog --error "$1" &>/dev/null; fi } if [[ $EUID -eq 0 ]]; then printerror "This program should not be run using sudo or as the root user!\n" exit 1 fi LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" # Makes the launcher use portals for file picking export QT_QPA_PLATFORMTHEME=xdgdesktopportal # Just to be sure... chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") if [ -f portable.txt ]; then ARGS+=("-d" "${LAUNCHER_DIR}") fi ARGS+=("$@") # Run the launcher exec -a "${ARGS[@]}" PrismLauncher-10.0.5/launcher/LaunchController.h0000644000175100017510000000654415144136756021234 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "minecraft/auth/MinecraftAccount.h" #include "minecraft/launch/MinecraftTarget.h" class InstanceWindow; class LaunchController : public Task { Q_OBJECT public: void executeTask() override; LaunchController(); virtual ~LaunchController() = default; void setInstance(InstancePtr instance) { m_instance = instance; } InstancePtr instance() { return m_instance; } void setOnline(bool online) { m_online = online; } void setOfflineName(const QString& offlineName) { m_offlineName = offlineName; } void setDemo(bool demo) { m_demo = demo; } void setProfiler(BaseProfilerFactory* profiler) { m_profiler = profiler; } void setParentWidget(QWidget* widget) { m_parentWidget = widget; } void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) { m_targetToJoin = std::move(targetToJoin); } void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); } QString id() { return m_instance->id(); } bool abort() override; private: void login(); void launchInstance(); void decideAccount(); bool askPlayDemo(); QString askOfflineName(QString playerName, bool demo, bool* ok = nullptr); bool reauthenticateAccount(MinecraftAccountPtr account); private slots: void readyForLaunch(); void onSucceeded(); void onFailed(QString reason); void onProgressRequested(Task* task); private: BaseProfilerFactory* m_profiler = nullptr; bool m_online = true; QString m_offlineName; bool m_demo = false; InstancePtr m_instance; QWidget* m_parentWidget = nullptr; InstanceWindow* m_console = nullptr; MinecraftAccountPtr m_accountToUse = nullptr; AuthSessionPtr m_session; shared_qobject_ptr m_launcher; MinecraftTarget::Ptr m_targetToJoin; }; PrismLauncher-10.0.5/launcher/ProblemProvider.h0000644000175100017510000000171615144136756021065 0ustar runnerrunner#pragma once #include #include enum class ProblemSeverity { None, Warning, Error }; struct PatchProblem { ProblemSeverity m_severity; QString m_description; }; class ProblemProvider { public: virtual ~ProblemProvider() {} virtual const QList getProblems() const = 0; virtual ProblemSeverity getProblemSeverity() const = 0; }; class ProblemContainer : public ProblemProvider { public: const QList getProblems() const override { return m_problems; } ProblemSeverity getProblemSeverity() const override { return m_problemSeverity; } virtual void addProblem(ProblemSeverity severity, const QString& description) { if (severity > m_problemSeverity) { m_problemSeverity = severity; } m_problems.append({ severity, description }); } private: QList m_problems; ProblemSeverity m_problemSeverity = ProblemSeverity::None; }; PrismLauncher-10.0.5/launcher/VersionProxyModel.h0000644000175100017510000000454515144136756021425 0ustar runnerrunner#pragma once #include #include "BaseVersionList.h" #include class VersionFilterModel; class VersionProxyModel : public QAbstractProxyModel { Q_OBJECT public: enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName, JavaMajor }; using FilterMap = QHash; public: VersionProxyModel(QObject* parent = 0); virtual ~VersionProxyModel() {}; virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; virtual QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; virtual QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; virtual QModelIndex parent(const QModelIndex& child) const override; virtual void setSourceModel(QAbstractItemModel* sourceModel) override; const FilterMap& filters() const; const QString& search() const; void setFilter(BaseVersionList::ModelRoles column, Filter filter); void setSearch(const QString& search); void clearFilters(); QModelIndex getRecommended() const; QModelIndex getVersion(const QString& version) const; void setCurrentVersion(const QString& version); private slots: void sourceDataChanged(const QModelIndex& source_top_left, const QModelIndex& source_bottom_right); void sourceAboutToBeReset(); void sourceReset(); void sourceRowsAboutToBeInserted(const QModelIndex& parent, int first, int last); void sourceRowsInserted(const QModelIndex& parent, int first, int last); void sourceRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); void sourceRowsRemoved(const QModelIndex& parent, int first, int last); private: QList m_columns; FilterMap m_filters; QString m_search; BaseVersionList::RoleList roles; VersionFilterModel* filterModel; bool hasRecommended = false; bool hasLatest = false; QString m_currentVersion; }; PrismLauncher-10.0.5/launcher/VersionProxyModel.cpp0000644000175100017510000003565515144136756021766 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "VersionProxyModel.h" #include #include #include #include #include class VersionFilterModel : public QSortFilterProxyModel { Q_OBJECT public: VersionFilterModel(VersionProxyModel* parent) : QSortFilterProxyModel(parent) { m_parent = parent; setSortRole(BaseVersionList::SortRole); sort(0, Qt::DescendingOrder); } bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { const auto& filters = m_parent->filters(); const QString& search = m_parent->search(); const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); if (!search.isEmpty() && !sourceModel()->data(idx, BaseVersionList::VersionRole).toString().contains(search, Qt::CaseInsensitive)) return false; for (auto it = filters.begin(); it != filters.end(); ++it) { auto data = sourceModel()->data(idx, it.key()); auto match = data.toString(); if (!it.value()(match)) { return false; } } return true; } void filterChanged() { invalidateFilter(); } private: VersionProxyModel* m_parent; }; VersionProxyModel::VersionProxyModel(QObject* parent) : QAbstractProxyModel(parent) { filterModel = new VersionFilterModel(this); connect(filterModel, &QAbstractItemModel::dataChanged, this, &VersionProxyModel::sourceDataChanged); connect(filterModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &VersionProxyModel::sourceRowsAboutToBeInserted); connect(filterModel, &QAbstractItemModel::rowsInserted, this, &VersionProxyModel::sourceRowsInserted); connect(filterModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &VersionProxyModel::sourceRowsAboutToBeRemoved); connect(filterModel, &QAbstractItemModel::rowsRemoved, this, &VersionProxyModel::sourceRowsRemoved); // FIXME: implement when needed /* connect(replacing, &QAbstractItemModel::rowsAboutToBeMoved, this, &VersionProxyModel::sourceRowsAboutToBeMoved); connect(replacing, &QAbstractItemModel::rowsMoved, this, &VersionProxyModel::sourceRowsMoved); connect(replacing, &QAbstractItemModel::layoutAboutToBeChanged, this, &VersionProxyModel::sourceLayoutAboutToBeChanged); connect(replacing, &QAbstractItemModel::layoutChanged, this, &VersionProxyModel::sourceLayoutChanged); */ connect(filterModel, &QAbstractItemModel::modelAboutToBeReset, this, &VersionProxyModel::sourceAboutToBeReset); connect(filterModel, &QAbstractItemModel::modelReset, this, &VersionProxyModel::sourceReset); QAbstractProxyModel::setSourceModel(filterModel); } QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, int role) const { if (section < 0 || section >= m_columns.size()) return QVariant(); if (orientation != Qt::Horizontal) return QVariant(); auto column = m_columns[section]; if (role == Qt::DisplayRole) { switch (column) { case Name: return tr("Version"); case ParentVersion: return tr("Minecraft"); // FIXME: this should come from metadata case Branch: return tr("Branch"); case Type: return tr("Type"); case CPUArchitecture: return tr("Architecture"); case Path: return tr("Path"); case JavaName: return tr("Java Name"); case JavaMajor: return tr("Major Version"); case Time: return tr("Released"); } } else if (role == Qt::ToolTipRole) { switch (column) { case Name: return tr("The name of the version."); case ParentVersion: return tr("Minecraft version"); // FIXME: this should come from metadata case Branch: return tr("The version's branch"); case Type: return tr("The version's type"); case CPUArchitecture: return tr("CPU Architecture"); case Path: return tr("Filesystem path to this version"); case JavaName: return tr("The alternative name of the Java version"); case JavaMajor: return tr("The Java major version"); case Time: return tr("Release date of this version"); } } return QVariant(); } QVariant VersionProxyModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } auto column = m_columns[index.column()]; auto parentIndex = mapToSource(index); switch (role) { case Qt::DisplayRole: { switch (column) { case Name: { QString version = sourceModel()->data(parentIndex, BaseVersionList::VersionRole).toString(); if (version == m_currentVersion) { return tr("%1 (installed)").arg(version); } return version; } case ParentVersion: return sourceModel()->data(parentIndex, BaseVersionList::ParentVersionRole); case Branch: return sourceModel()->data(parentIndex, BaseVersionList::BranchRole); case Type: return sourceModel()->data(parentIndex, BaseVersionList::TypeRole); case CPUArchitecture: return sourceModel()->data(parentIndex, BaseVersionList::CPUArchitectureRole); case Path: return sourceModel()->data(parentIndex, BaseVersionList::PathRole); case JavaName: return sourceModel()->data(parentIndex, BaseVersionList::JavaNameRole); case JavaMajor: return sourceModel()->data(parentIndex, BaseVersionList::JavaMajorRole); case Time: return sourceModel()->data(parentIndex, Meta::VersionList::TimeRole).toDate(); default: return QVariant(); } } case Qt::ToolTipRole: { if (column == Name && hasRecommended) { auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); if (value.toBool()) { return tr("Recommended"); } else if (hasLatest) { auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); if (latest.toBool()) { return tr("Latest"); } } } else { return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); } } case Qt::DecorationRole: { if (column == Name && hasRecommended) { auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); if (recommenced.toBool()) { return QIcon::fromTheme("star"); } else if (hasLatest) { auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); if (latest.toBool()) { return QIcon::fromTheme("bug"); } } QPixmap pixmap; QPixmapCache::find("placeholder", &pixmap); if (!pixmap) { QPixmap px(16, 16); px.fill(Qt::transparent); QPixmapCache::insert("placeholder", px); return px; } return pixmap; } return QVariant(); } default: { if (roles.contains((BaseVersionList::ModelRoles)role)) { return sourceModel()->data(parentIndex, role); } return QVariant(); } } } QModelIndex VersionProxyModel::parent([[maybe_unused]] const QModelIndex& child) const { return QModelIndex(); } QModelIndex VersionProxyModel::mapFromSource(const QModelIndex& sourceIndex) const { if (sourceIndex.isValid()) { return index(sourceIndex.row(), 0); } return QModelIndex(); } QModelIndex VersionProxyModel::mapToSource(const QModelIndex& proxyIndex) const { if (proxyIndex.isValid()) { return sourceModel()->index(proxyIndex.row(), 0); } return QModelIndex(); } QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex& parent) const { // no trees here... shoo if (parent.isValid()) { return QModelIndex(); } if (row < 0 || row >= sourceModel()->rowCount()) return QModelIndex(); if (column < 0 || column >= columnCount()) return QModelIndex(); return QAbstractItemModel::createIndex(row, column); } int VersionProxyModel::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_columns.size(); } int VersionProxyModel::rowCount(const QModelIndex& parent) const { if (sourceModel()) { return sourceModel()->rowCount(parent); } return 0; } void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, const QModelIndex& source_bottom_right) { if (source_top_left.parent() != source_bottom_right.parent()) return; // whole row is getting changed auto topLeft = createIndex(source_top_left.row(), 0); auto bottomRight = createIndex(source_bottom_right.row(), columnCount() - 1); emit dataChanged(topLeft, bottomRight); } void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) { auto replacing = dynamic_cast(replacingRaw); m_columns.clear(); if (!replacing) { roles.clear(); filterModel->setSourceModel(replacing); return; } roles = replacing->providesRoles(); if (roles.contains(BaseVersionList::VersionRole)) { m_columns.push_back(Name); } /* if(roles.contains(BaseVersionList::ParentVersionRole)) { m_columns.push_back(ParentVersion); } */ if (roles.contains(BaseVersionList::CPUArchitectureRole)) { m_columns.push_back(CPUArchitecture); } if (roles.contains(BaseVersionList::PathRole)) { m_columns.push_back(Path); } if (roles.contains(BaseVersionList::JavaNameRole)) { m_columns.push_back(JavaName); } if (roles.contains(BaseVersionList::JavaMajorRole)) { m_columns.push_back(JavaMajor); } if (roles.contains(Meta::VersionList::TimeRole)) { m_columns.push_back(Time); } if (roles.contains(BaseVersionList::BranchRole)) { m_columns.push_back(Branch); } if (roles.contains(BaseVersionList::TypeRole)) { m_columns.push_back(Type); } if (roles.contains(BaseVersionList::RecommendedRole)) { hasRecommended = true; } if (roles.contains(BaseVersionList::LatestRole)) { hasLatest = true; } filterModel->setSourceModel(replacing); } QModelIndex VersionProxyModel::getRecommended() const { if (!roles.contains(BaseVersionList::RecommendedRole)) { return index(0, 0); } int recommended = 0; for (int i = 0; i < rowCount(); i++) { auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::RecommendedRole); if (value.toBool()) { recommended = i; } } return index(recommended, 0); } QModelIndex VersionProxyModel::getVersion(const QString& version) const { int found = -1; for (int i = 0; i < rowCount(); i++) { auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::VersionRole); if (value.toString() == version) { found = i; } } if (found == -1) { return QModelIndex(); } return index(found, 0); } void VersionProxyModel::clearFilters() { m_filters.clear(); m_search.clear(); filterModel->filterChanged(); } void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter f) { m_filters[column] = std::move(f); filterModel->filterChanged(); } void VersionProxyModel::setSearch(const QString& search) { m_search = search; filterModel->filterChanged(); } const VersionProxyModel::FilterMap& VersionProxyModel::filters() const { return m_filters; } const QString& VersionProxyModel::search() const { return m_search; } void VersionProxyModel::sourceAboutToBeReset() { beginResetModel(); } void VersionProxyModel::sourceReset() { endResetModel(); } void VersionProxyModel::sourceRowsAboutToBeInserted(const QModelIndex& parent, int first, int last) { beginInsertRows(parent, first, last); } void VersionProxyModel::sourceRowsInserted([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int first, [[maybe_unused]] int last) { endInsertRows(); } void VersionProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { beginRemoveRows(parent, first, last); } void VersionProxyModel::sourceRowsRemoved([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int first, [[maybe_unused]] int last) { endRemoveRows(); } void VersionProxyModel::setCurrentVersion(const QString& version) { m_currentVersion = version; } #include "VersionProxyModel.moc" PrismLauncher-10.0.5/launcher/DesktopServices.cpp0000644000175100017510000000520115144136756021413 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 dada513 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2022 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "DesktopServices.h" #include #include #include #include #include "FileSystem.h" namespace DesktopServices { bool openPath(const QFileInfo& path, bool ensureFolderPathExists) { qDebug() << "Opening path" << path; if (ensureFolderPathExists) { FS::ensureFolderPathExists(path); } return openUrl(QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath())); } bool openPath(const QString& path, bool ensureFolderPathExists) { return openPath(QFileInfo(path), ensureFolderPathExists); } bool run(const QString& application, const QStringList& args, const QString& workingDirectory, qint64* pid) { qDebug() << "Running" << application << "with args" << args.join(' '); return QProcess::startDetached(application, args, workingDirectory, pid); } bool openUrl(const QUrl& url) { qDebug() << "Opening URL" << url.toString(); return QDesktopServices::openUrl(url); } bool isFlatpak() { #ifdef Q_OS_LINUX return QFile::exists("/.flatpak-info"); #else return false; #endif } bool isSnap() { #ifdef Q_OS_LINUX return getenv("SNAP"); #else return false; #endif } } // namespace DesktopServices PrismLauncher-10.0.5/launcher/Commandline.h0000644000175100017510000000205415144136756020174 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include /** * @file libutil/include/cmdutils.h * @brief commandline parsing and processing utilities */ namespace Commandline { /** * @brief split a string into argv items like a shell would do * @param args the argument string * @return a QStringList containing all arguments */ QStringList splitArgs(QString args); } // namespace Commandline PrismLauncher-10.0.5/launcher/InstanceCreationTask.h0000644000175100017510000000236715144136756022031 0ustar runnerrunner#pragma once #include "BaseVersion.h" #include "InstanceTask.h" class InstanceCreationTask : public InstanceTask { Q_OBJECT public: InstanceCreationTask() = default; virtual ~InstanceCreationTask() = default; protected: void executeTask() final override; /** * Tries to update an already existing instance. * * This can be implemented by subclasses to provide a way of updating an already existing * instance, according to that implementation's concept of 'identity' (i.e. instances that * are updates / downgrades of one another). * * If this returns true, createInstance() will not run, so you should do all update steps in here. * Otherwise, createInstance() is run as normal. */ virtual bool updateInstance() { return false; }; /** * Creates a new instance. * * Returns whether the instance creation was successful (true) or not (false). */ virtual bool createInstance() { return false; }; QString getError() const { return m_error_message; } protected: void setError(const QString& message) { m_error_message = message; }; protected: bool m_abort = false; QStringList m_files_to_remove; private: QString m_error_message; }; PrismLauncher-10.0.5/launcher/StringUtils.h0000644000175100017510000000571115144136756020240 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include namespace StringUtils { #if defined Q_OS_WIN32 using string = std::wstring; inline string toStdString(QString s) { return s.toStdWString(); } inline QString fromStdString(string s) { return QString::fromStdWString(s); } #else using string = std::string; inline string toStdString(QString s) { return s.toStdString(); } inline QString fromStdString(string s) { return QString::fromStdString(s); } #endif int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs); /** * @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path * @param url Url to truncate * @param max_len max length of url in characters * @param hard_limit if truncating the path can't get the url short enough, truncate it normally. */ QString truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit = false); QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1); QString getRandomAlphaNumeric(); QPair splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); QPair splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); QPair splitFirst(const QString& s, const QRegularExpression& re); QString htmlListPatch(QString htmlStr); } // namespace StringUtils PrismLauncher-10.0.5/launcher/GZip.h0000644000175100017510000000052615144136756016621 0ustar runnerrunner#pragma once #include #include namespace GZip { bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); QString readGzFileByBlocks(QFile* source, std::function handleBlock); } // namespace GZip PrismLauncher-10.0.5/launcher/KonamiCode.h0000644000175100017510000000035715144136756017763 0ustar runnerrunner#pragma once #include class KonamiCode : public QObject { Q_OBJECT public: KonamiCode(QObject* parent = 0); void input(QEvent* event); signals: void triggered(); private: int m_progress = 0; }; PrismLauncher-10.0.5/launcher/archive/0000755000175100017510000000000015144136756017215 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/archive/ExtractZipTask.cpp0000644000175100017510000001200115144136756022633 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExtractZipTask.h" #include #include "FileSystem.h" #include "archive/ArchiveReader.h" #include "archive/ArchiveWriter.h" namespace MMCZip { void ExtractZipTask::executeTask() { m_zipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); connect(&m_zipWatcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); m_zipWatcher.setFuture(m_zipFuture); } auto ExtractZipTask::extractZip() -> ZipResult { auto target = m_outputDir.absolutePath(); auto target_top_dir = QUrl::fromLocalFile(target); QStringList extracted; qDebug() << "Extracting subdir" << m_subdirectory << "from" << m_input.getZipName() << "to" << target; if (!m_input.collectFiles()) { return ZipResult(tr("Failed to enumerate files in archive")); } if (m_input.getFiles().isEmpty()) { logWarning(tr("Extracting empty archives seems odd...")); return ZipResult(); } auto extPtr = ArchiveWriter::createDiskWriter(); auto ext = extPtr.get(); setStatus("Extracting files..."); setProgress(0, m_input.getFiles().count()); ZipResult result; auto fileName = m_input.getZipName(); if (!m_input.parse([this, &result, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { if (m_zipFuture.isCanceled()) return false; setProgress(m_progress + 1, m_progressTotal); QString file_name = f->filename(); if (!file_name.startsWith(m_subdirectory)) { f->skip(); return true; } auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(m_subdirectory.size())); auto original_name = relative_file_name; setStatus("Unpacking: " + relative_file_name); // Fix subdirs/files ending with a / getting transformed into absolute paths if (relative_file_name.startsWith('/')) relative_file_name = relative_file_name.mid(1); // Fix weird "folders with a single file get squashed" thing QString sub_path; if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { sub_path = relative_file_name.section('/', 0, -2) + '/'; FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); relative_file_name = relative_file_name.split('/').last(); } QString target_file_path; if (relative_file_name.isEmpty()) { target_file_path = target + '/'; } else { target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) target_file_path += '/'; } if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { result = ZipResult(tr("Extracting %1 was cancelled, because it was effectively outside of the target path %2") .arg(relative_file_name, target)); return false; } if (!f->writeFile(ext, target_file_path)) { result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); return false; } extracted.append(target_file_path); qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; return true; })) { FS::removeFiles(extracted); return result.has_value() ? result : ZipResult(tr("Failed to parse file %1").arg(fileName)); } return ZipResult(); } void ExtractZipTask::finish() { if (m_zipFuture.isCanceled()) { emitAborted(); } else if (auto result = m_zipFuture.result(); result.has_value()) { emitFailed(result.value()); } else { emitSucceeded(); } } bool ExtractZipTask::abort() { if (m_zipFuture.isRunning()) { m_zipFuture.cancel(); // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur // immediately. return true; } return false; } } // namespace MMCZip PrismLauncher-10.0.5/launcher/archive/ArchiveWriter.h0000644000175100017510000000260315144136756022145 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "archive/ArchiveReader.h" struct archive; namespace MMCZip { class ArchiveWriter { public: ArchiveWriter(const QString& archiveName); virtual ~ArchiveWriter(); bool open(); bool close(); bool addFile(const QString& fileName, const QString& fileDest); bool addFile(const QString& fileDest, const QByteArray& data); bool addFile(ArchiveReader::File* f); static std::unique_ptr createDiskWriter(); private: struct archive* m_archive = nullptr; QString m_filename; QString m_format = "zip"; }; } // namespace MMCZip PrismLauncher-10.0.5/launcher/archive/ExtractZipTask.h0000644000175100017510000000300415144136756022303 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include "archive/ArchiveReader.h" #include "tasks/Task.h" namespace MMCZip { class ExtractZipTask : public Task { Q_OBJECT public: ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") : m_input(input), m_outputDir(outputDir), m_subdirectory(subdirectory) {} virtual ~ExtractZipTask() = default; using ZipResult = std::optional; protected: virtual void executeTask() override; bool abort() override; ZipResult extractZip(); void finish(); private: ArchiveReader m_input; QDir m_outputDir; QString m_subdirectory; QFuture m_zipFuture; QFutureWatcher m_zipWatcher; }; } // namespace MMCZip PrismLauncher-10.0.5/launcher/archive/ArchiveReader.cpp0000644000175100017510000001630115144136756022426 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only AND LicenseRef-PublicDomain /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Additional note: Portions of this file are released into the public domain * under LicenseRef-PublicDomain. */ #include "ArchiveReader.h" #include #include #include #include #include namespace MMCZip { QStringList ArchiveReader::getFiles() { return m_fileNames; } bool ArchiveReader::collectFiles(bool onlyFiles) { return parse([this, onlyFiles](File* f) { if (!onlyFiles || f->isFile()) m_fileNames << f->filename(); return f->skip(); }); } QString ArchiveReader::File::filename() { return QString::fromUtf8(archive_entry_pathname_utf8(m_entry)); } QByteArray ArchiveReader::File::readAll(int* outStatus) { QByteArray data; const void* buff; size_t size; la_int64_t offset; int status; while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) { data.append(static_cast(buff), static_cast(size)); } if (status != ARCHIVE_EOF && status != ARCHIVE_OK) { qWarning() << "libarchive read error:" << archive_error_string(m_archive.get()); } if (outStatus) { *outStatus = status; } return data; } QDateTime ArchiveReader::File::dateTime() { auto mtime = archive_entry_mtime(m_entry); auto mtime_nsec = archive_entry_mtime_nsec(m_entry); auto dt = QDateTime::fromSecsSinceEpoch(mtime); return dt.addMSecs(mtime_nsec / 1e6); } int ArchiveReader::File::readNextHeader() { return archive_read_next_header(m_archive.get(), &m_entry); } auto ArchiveReader::goToFile(QString filename) -> std::unique_ptr { auto f = std::make_unique(); auto a = f->m_archive.get(); archive_read_support_format_all(a); archive_read_support_filter_all(a); auto fileName = m_archivePath.toStdWString(); if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) { qCritical() << "Failed to open archive file:" << m_archivePath << "-" << archive_error_string(a); return nullptr; } while (f->readNextHeader() == ARCHIVE_OK) { if (f->filename() == filename) { return f; } f->skip(); } archive_read_close(a); return nullptr; } static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false) { int r; const void* buff; size_t size; la_int64_t offset; for (;;) { r = archive_read_data_block(ar, &buff, &size, &offset); if (r == ARCHIVE_EOF) return (ARCHIVE_OK); if (r < ARCHIVE_OK) { qCritical() << "Failed reading data block:" << archive_error_string(ar); return (r); } if (notBlock) { r = archive_write_data(aw, buff, size); } else { r = archive_write_data_block(aw, buff, size, offset); } if (r < ARCHIVE_OK) { qCritical() << "Failed writing data block:" << archive_error_string(aw); return (r); } } } bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool notBlock) { auto entry = m_entry; std::unique_ptr entryClone(nullptr, &archive_entry_free); if (!targetFileName.isEmpty()) { entryClone.reset(archive_entry_clone(m_entry)); entry = entryClone.get(); auto nameUtf8 = targetFileName.toUtf8(); archive_entry_set_pathname_utf8(entry, nameUtf8.constData()); } if (archive_write_header(out, entry) < ARCHIVE_OK) { qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out); return false; } else if (archive_entry_size(m_entry) > 0) { auto r = copy_data(m_archive.get(), out, notBlock); if (r < ARCHIVE_OK) qCritical() << "Failed reading data block:" << archive_error_string(out); if (r < ARCHIVE_WARN) return false; } auto r = archive_write_finish_entry(out); if (r < ARCHIVE_OK) qCritical() << "Failed to finish writing entry:" << archive_error_string(out); return (r >= ARCHIVE_WARN); } bool ArchiveReader::parse(std::function doStuff) { auto f = std::make_unique(); auto a = f->m_archive.get(); archive_read_support_format_all(a); archive_read_support_filter_all(a); auto fileName = m_archivePath.toStdWString(); if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) { qCritical() << "Failed to open archive file:" << m_archivePath << "-" << f->error(); return false; } bool breakControl = false; while (f->readNextHeader() == ARCHIVE_OK) { if (f && !doStuff(f.get(), breakControl)) { qCritical() << "Failed to parse file:" << f->filename() << "-" << f->error(); return false; } if (breakControl) { break; } } archive_read_close(a); return true; } bool ArchiveReader::parse(std::function doStuff) { return parse([doStuff](File* f, bool&) { return doStuff(f); }); } bool ArchiveReader::File::isFile() { return (archive_entry_filetype(m_entry) & AE_IFMT) == AE_IFREG; } bool ArchiveReader::File::skip() { return archive_read_data_skip(m_archive.get()) == ARCHIVE_OK; } const char* ArchiveReader::File::error() { return archive_error_string(m_archive.get()); } QString ArchiveReader::getZipName() { return m_archivePath; } bool ArchiveReader::exists(const QString& filePath) const { if (filePath == QLatin1String("/") || filePath.isEmpty()) return true; // Normalize input path (remove trailing slash, if any) QString normalizedPath = QDir::cleanPath(filePath); if (normalizedPath.startsWith('/')) normalizedPath.remove(0, 1); if (normalizedPath == QLatin1String(".")) return true; if (normalizedPath == QLatin1String("..")) return false; // root only // Check for exact file match if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) return true; // Check for directory existence by seeing if any file starts with that path QString dirPath = normalizedPath + QLatin1Char('/'); for (const QString& f : m_fileNames) { if (f.startsWith(dirPath, Qt::CaseInsensitive)) return true; } return false; } ArchiveReader::File::File() : m_archive(ArchivePtr(archive_read_new(), archive_read_free)) {} } // namespace MMCZip PrismLauncher-10.0.5/launcher/archive/ExportToZipTask.cpp0000644000175100017510000000624715144136756023024 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExportToZipTask.h" #include #include "FileSystem.h" namespace MMCZip { void ExportToZipTask::executeTask() { setStatus("Adding files..."); setProgress(0, m_files.length()); m_buildZipFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return exportZip(); }); connect(&m_buildZipWatcher, &QFutureWatcher::finished, this, &ExportToZipTask::finish); m_buildZipWatcher.setFuture(m_buildZipFuture); } auto ExportToZipTask::exportZip() -> ZipResult { if (!m_dir.exists()) { return ZipResult(tr("Folder doesn't exist")); } if (!m_output.open()) { return ZipResult(tr("Could not create file")); } for (auto fileName : m_extraFiles.keys()) { if (m_buildZipFuture.isCanceled()) return ZipResult(); if (!m_output.addFile(fileName, m_extraFiles[fileName])) { return ZipResult(tr("Could not add:") + fileName); } } for (const QFileInfo& file : m_files) { if (m_buildZipFuture.isCanceled()) return ZipResult(); auto absolute = file.absoluteFilePath(); auto relative = m_dir.relativeFilePath(absolute); setStatus("Compressing: " + relative); setProgress(m_progress + 1, m_progressTotal); if (m_followSymlinks) { if (file.isSymLink()) absolute = file.symLinkTarget(); else absolute = file.canonicalFilePath(); } if (!m_excludeFiles.contains(relative) && !m_output.addFile(absolute, m_destinationPrefix + relative)) { return ZipResult(tr("Could not read and compress %1").arg(relative)); } } if (!m_output.close()) { return ZipResult(tr("A zip error occurred")); } return ZipResult(); } void ExportToZipTask::finish() { if (m_buildZipFuture.isCanceled()) { FS::deletePath(m_outputPath); emitAborted(); } else if (auto result = m_buildZipFuture.result(); result.has_value()) { FS::deletePath(m_outputPath); emitFailed(result.value()); } else { emitSucceeded(); } } bool ExportToZipTask::abort() { if (m_buildZipFuture.isRunning()) { m_buildZipFuture.cancel(); // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not occur // immediately. return true; } return false; } } // namespace MMCZip PrismLauncher-10.0.5/launcher/archive/ExportToZipTask.h0000644000175100017510000000447315144136756022470 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include "archive/ArchiveWriter.h" #include "tasks/Task.h" namespace MMCZip { class ExportToZipTask : public Task { Q_OBJECT public: ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) : m_outputPath(outputPath) , m_output(outputPath) , m_dir(dir) , m_files(files) , m_destinationPrefix(destinationPrefix) , m_followSymlinks(followSymlinks) { setAbortable(true); }; ExportToZipTask(QString outputPath, QString dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks) {}; virtual ~ExportToZipTask() = default; void setExcludeFiles(QStringList excludeFiles) { m_excludeFiles = excludeFiles; } void addExtraFile(QString fileName, QByteArray data) { m_extraFiles.insert(fileName, data); } using ZipResult = std::optional; protected: virtual void executeTask() override; bool abort() override; ZipResult exportZip(); void finish(); private: QString m_outputPath; ArchiveWriter m_output; QDir m_dir; QFileInfoList m_files; QString m_destinationPrefix; bool m_followSymlinks; QStringList m_excludeFiles; QHash m_extraFiles; QFuture m_buildZipFuture; QFutureWatcher m_buildZipWatcher; }; } // namespace MMCZip PrismLauncher-10.0.5/launcher/archive/ArchiveReader.h0000644000175100017510000000403515144136756022074 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include struct archive; struct archive_entry; namespace MMCZip { class ArchiveReader { public: using ArchivePtr = std::unique_ptr; ArchiveReader(QString fileName) : m_archivePath(fileName) {} virtual ~ArchiveReader() = default; QStringList getFiles(); QString getZipName(); bool collectFiles(bool onlyFiles = true); bool exists(const QString& filePath) const; class File { public: File(); virtual ~File() = default; QString filename(); bool isFile(); QDateTime dateTime(); const char* error(); QByteArray readAll(int* outStatus = nullptr); bool skip(); bool writeFile(archive* out, QString targetFileName = "", bool notBlock = false); private: int readNextHeader(); private: friend ArchiveReader; ArchivePtr m_archive; archive_entry* m_entry; }; std::unique_ptr goToFile(QString filename); bool parse(std::function); bool parse(std::function); private: QString m_archivePath; size_t m_blockSize = 10240; QStringList m_fileNames = {}; }; } // namespace MMCZip PrismLauncher-10.0.5/launcher/archive/ArchiveWriter.cpp0000644000175100017510000002023415144136756022500 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ArchiveWriter.h" #include #include #include #include #include #include #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif // clang-format off #include #include // clang-format on #endif namespace MMCZip { ArchiveWriter::ArchiveWriter(const QString& archiveName) : m_filename(archiveName) {} ArchiveWriter::~ArchiveWriter() { close(); } bool ArchiveWriter::open() { if (m_filename.isEmpty()) { qCritical() << "Archive m_filename not set."; return false; } m_archive = archive_write_new(); if (!m_archive) { qCritical() << "Archive not initialized."; return false; } auto format = m_format.toUtf8(); archive_write_set_format_by_name(m_archive, format.constData()); if (archive_write_set_options(m_archive, "hdrcharset=UTF-8") != ARCHIVE_OK) { qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); return false; } auto archiveNameW = m_filename.toStdWString(); if (archive_write_open_filename_w(m_archive, archiveNameW.data()) != ARCHIVE_OK) { qCritical() << "Failed to open archive file:" << m_filename << "-" << archive_error_string(m_archive); return false; } return true; } bool ArchiveWriter::close() { bool success = true; if (m_archive) { if (archive_write_close(m_archive) != ARCHIVE_OK) { qCritical() << "Failed to close archive" << m_filename << "-" << archive_error_string(m_archive); success = false; } if (archive_write_free(m_archive) != ARCHIVE_OK) { qCritical() << "Failed to free archive" << m_filename << "-" << archive_error_string(m_archive); success = false; } m_archive = nullptr; } return success; } bool ArchiveWriter::addFile(const QString& fileName, const QString& fileDest) { QFileInfo fileInfo(fileName); if (!fileInfo.exists()) { qCritical() << "File does not exist:" << fileInfo.filePath(); return false; } std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); auto entry = entry_ptr.get(); if (!entry) { qCritical() << "Failed to create archive entry"; return false; } auto fileDestUtf8 = fileDest.toUtf8(); archive_entry_set_pathname_utf8(entry, fileDestUtf8.constData()); #if defined Q_OS_WIN32 { // Windows needs to use this method, thanks I hate it. auto widePath = fileInfo.absoluteFilePath().toStdWString(); HANDLE file_handle = CreateFileW(widePath.data(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (file_handle == INVALID_HANDLE_VALUE) { qCritical() << "Failed to stat file:" << fileInfo.filePath(); return false; } BY_HANDLE_FILE_INFORMATION file_info; if (!GetFileInformationByHandle(file_handle, &file_info)) { qCritical() << "Failed to stat file:" << fileInfo.filePath(); CloseHandle(file_handle); return false; } archive_entry_copy_bhfi(entry, &file_info); CloseHandle(file_handle); } #else { // this only works for multibyte encoded filenames if the local is properly set, // a wide character version doesn't seem to exist: here's hoping... QByteArray utf8 = fileInfo.absoluteFilePath().toUtf8(); const char* cpath = utf8.constData(); struct stat st; if (stat(cpath, &st) != 0) { qCritical() << "Failed to stat file:" << fileInfo.filePath(); return false; } // This should handle the copying of most attributes archive_entry_copy_stat(entry, &st); } #endif // However: // "The [filetype] constants used by stat(2) may have different numeric values from the corresponding [libarchive constants]." // - `archive_entry_stat(3)` if (fileInfo.isSymLink()) { archive_entry_set_filetype(entry, AE_IFLNK); // We also need to manually copy some attributes from the link itself, as `stat` above operates on its target auto target = fileInfo.symLinkTarget().toUtf8(); archive_entry_set_symlink_utf8(entry, target.constData()); archive_entry_set_size(entry, 0); archive_entry_set_perm(entry, fileInfo.permissions()); } else if (fileInfo.isFile()) { archive_entry_set_filetype(entry, AE_IFREG); } else { qCritical() << "Unsupported file type:" << fileInfo.filePath(); return false; } if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { qCritical() << "Failed to write header for:" << fileDest << "-" << archive_error_string(m_archive); return false; } if (fileInfo.isFile() && !fileInfo.isSymLink()) { QFile file(fileInfo.absoluteFilePath()); if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file:" << fileInfo.filePath(); return false; } constexpr qint64 chunkSize = 8192; QByteArray buffer; buffer.resize(chunkSize); while (!file.atEnd()) { auto bytesRead = file.read(buffer.data(), chunkSize); if (bytesRead < 0) { qCritical() << "Read error in file:" << fileInfo.filePath(); return false; } if (archive_write_data(m_archive, buffer.constData(), bytesRead) < 0) { qCritical() << "Write error in archive for:" << fileDest; return false; } } } return true; } bool ArchiveWriter::addFile(const QString& fileDest, const QByteArray& data) { std::unique_ptr entry_ptr(archive_entry_new(), archive_entry_free); auto entry = entry_ptr.get(); if (!entry) { qCritical() << "Failed to create archive entry"; return false; } auto fileDestUtf8 = fileDest.toUtf8(); archive_entry_set_pathname_utf8(entry, fileDestUtf8.constData()); archive_entry_set_perm(entry, 0644); archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_size(entry, data.size()); if (archive_write_header(m_archive, entry) != ARCHIVE_OK) { qCritical() << "Failed to write header for:" << fileDest << "-" << archive_error_string(m_archive); return false; } if (archive_write_data(m_archive, data.constData(), data.size()) < 0) { qCritical() << "Write error in archive for:" << fileDest << "-" << archive_error_string(m_archive); return false; } return true; } bool ArchiveWriter::addFile(ArchiveReader::File* f) { return f->writeFile(m_archive, "", true); } std::unique_ptr ArchiveWriter::createDiskWriter() { int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS | ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS; std::unique_ptr extPtr(archive_write_disk_new(), [](archive* a) { if (a) { archive_write_close(a); archive_write_free(a); } }); archive* ext = extPtr.get(); archive_write_disk_set_options(ext, flags); archive_write_disk_set_standard_lookup(ext); return extPtr; } } // namespace MMCZip PrismLauncher-10.0.5/launcher/qtlogging.ini0000644000175100017510000000111615144136756020267 0ustar runnerrunner[Rules] *.debug=true # prevent log spam and strange bugs # qt.qpa.drawing in particular causes theme artifacts on MacOS qt.*.debug=false # supress image format noise kf.imageformats.plugins.hdr=false kf.imageformats.plugins.xcf=false # don't log credentials by default launcher.auth.credentials.debug=false # remove the debug lines, other log levels still get through launcher.task.net.download.debug=false # enable or disable whole catageries launcher.task.net=true launcher.task=false launcher.task.net.upload=true launcher.task.net.metacache=false launcher.task.net.metacache.http=true PrismLauncher-10.0.5/launcher/MTPixmapCache.h0000644000175100017510000001616615144136756020402 0ustar runnerrunner#pragma once #include #include #include #include #include #include #define GET_TYPE() \ Qt::ConnectionType type; \ if (QThread::currentThread() != QCoreApplication::instance()->thread()) \ type = Qt::BlockingQueuedConnection; \ else \ type = Qt::DirectConnection; #define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ static RET_TYPE NAME() \ { \ RET_TYPE ret; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ return ret; \ } #define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1) \ { \ RET_TYPE ret; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \ return ret; \ } #define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ { \ RET_TYPE ret; \ GET_TYPE() \ QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \ Q_ARG(PARAM_2_TYPE, p2)); \ return ret; \ } /** A wrapper around QPixmapCache with thread affinity with the main thread. */ class PixmapCache final : public QObject { Q_OBJECT public: PixmapCache(QObject* parent) : QObject(parent) {} ~PixmapCache() override = default; static PixmapCache& instance() { return *s_instance; } static void setInstance(PixmapCache* i) { s_instance = i; } public: DEFINE_FUNC_NO_PARAM(cacheLimit, int) DEFINE_FUNC_NO_PARAM(clear, bool) DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*) DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*) DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&) DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&) DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&) DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) DEFINE_FUNC_NO_PARAM(markCacheMissByEviciton, bool) DEFINE_FUNC_ONE_PARAM(setFastEvictionThreshold, bool, int) // NOTE: Every function returns something non-void to simplify the macros. private slots: int _cacheLimit() { return QPixmapCache::cacheLimit(); } bool _clear() { QPixmapCache::clear(); return true; } bool _find(const QString& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); } bool _find(const QPixmapCache::Key& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); } bool _insert(const QString& key, const QPixmap& pixmap) { return QPixmapCache::insert(key, pixmap); } QPixmapCache::Key _insert(const QPixmap& pixmap) { return QPixmapCache::insert(pixmap); } bool _remove(const QString& key) { QPixmapCache::remove(key); return true; } bool _remove(const QPixmapCache::Key& key) { QPixmapCache::remove(key); return true; } bool _replace(const QPixmapCache::Key& key, const QPixmap& pixmap) { return QPixmapCache::replace(key, pixmap); } bool _setCacheLimit(int n) { QPixmapCache::setCacheLimit(n); return true; } /** * Mark that a cache miss occurred because of a eviction if too many of these occur too fast the cache size is increased * @return if the cache size was increased */ bool _markCacheMissByEviciton() { static constexpr uint maxCache = static_cast(std::numeric_limits::max()) / 4; static constexpr uint step = 10240; static constexpr int oneSecond = 1000; auto now = QTime::currentTime(); if (!m_last_cache_miss_by_eviciton.isNull()) { auto diff = m_last_cache_miss_by_eviciton.msecsTo(now); if (diff < oneSecond) { // less than a second ago ++m_consecutive_fast_evicitons; } else { m_consecutive_fast_evicitons = 0; } } m_last_cache_miss_by_eviciton = now; if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) { // increase the cache size uint newSize = _cacheLimit() + step; if (newSize >= maxCache) { // increase it until you overflow :D newSize = maxCache; qDebug() << m_consecutive_fast_evicitons << tr("pixmap cache misses by eviction happened too fast, doing nothing as the cache size reached it's limit"); } else { qDebug() << m_consecutive_fast_evicitons << tr("pixmap cache misses by eviction happened too fast, increasing cache size to") << static_cast(newSize); } _setCacheLimit(static_cast(newSize)); m_consecutive_fast_evicitons = 0; return true; } return false; } bool _setFastEvictionThreshold(int threshold) { m_consecutive_fast_evicitons_threshold = threshold; return true; } private: static PixmapCache* s_instance; QTime m_last_cache_miss_by_eviciton; int m_consecutive_fast_evicitons = 0; int m_consecutive_fast_evicitons_threshold = 15; }; PrismLauncher-10.0.5/launcher/include/0000755000175100017510000000000015144136756017217 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/include/qtcore.pch.hpp0000644000175100017510000000173515144136756022004 0ustar runnerrunner#pragma once #ifndef PRISM_PRECOMPILED_QTCORE_HEADERS_H #define PRISM_PRECOMPILED_QTCORE_HEADERS_H #include #include #include #include #include #include // collections #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #endif // PRISM_PRECOMPILED_QTCORE_HEADERS_H PrismLauncher-10.0.5/launcher/include/qtgui.pch.hpp0000644000175100017510000000140715144136756021634 0ustar runnerrunner#pragma once #ifndef PRISM_PRECOMPILED_QTGUI_HEADERS_H #define PRISM_PRECOMPILED_QTGUI_HEADERS_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #endif // PRISM_PRECOMPILED_GUI_HEADERS_H PrismLauncher-10.0.5/launcher/include/base.pch.hpp0000644000175100017510000000053315144136756021414 0ustar runnerrunner#pragma once #ifndef PRISM_PRECOMPILED_BASE_HEADERS_H #define PRISM_PRECOMPILED_BASE_HEADERS_H #include #include #include #include #include #include #include #include #include #include #endif // PRISM_PRECOMPILED_BASE_HEADERS_H PrismLauncher-10.0.5/launcher/KonamiCode.cpp0000644000175100017510000000147315144136756020316 0ustar runnerrunner#include "KonamiCode.h" #include #include namespace { const std::array konamiCode = { { Qt::Key_Up, Qt::Key_Up, Qt::Key_Down, Qt::Key_Down, Qt::Key_Left, Qt::Key_Right, Qt::Key_Left, Qt::Key_Right, Qt::Key_B, Qt::Key_A } }; } KonamiCode::KonamiCode(QObject* parent) : QObject(parent) {} void KonamiCode::input(QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); auto key = Qt::Key(keyEvent->key()); if (key == konamiCode[m_progress]) { m_progress++; } else { m_progress = 0; } if (m_progress == static_cast(konamiCode.size())) { m_progress = 0; emit triggered(); } } } PrismLauncher-10.0.5/launcher/GZip.cpp0000644000175100017510000001465115144136756017160 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "GZip.h" #include #include #include #include bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes) { if (compressedBytes.size() == 0) { uncompressedBytes = compressedBytes; return true; } unsigned uncompLength = compressedBytes.size(); uncompressedBytes.clear(); uncompressedBytes.resize(uncompLength); z_stream strm; memset(&strm, 0, sizeof(strm)); strm.next_in = (Bytef*)compressedBytes.data(); strm.avail_in = compressedBytes.size(); bool done = false; if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK) { return false; } int err = Z_OK; while (!done) { // If our output buffer is too small if (strm.total_out >= uncompLength) { uncompressedBytes.resize(uncompLength * 2); uncompLength *= 2; } strm.next_out = reinterpret_cast((uncompressedBytes.data() + strm.total_out)); strm.avail_out = uncompLength - strm.total_out; // Inflate another chunk. err = inflate(&strm, Z_SYNC_FLUSH); if (err == Z_STREAM_END) done = true; else if (err != Z_OK) { break; } } if (inflateEnd(&strm) != Z_OK || !done) { return false; } uncompressedBytes.resize(strm.total_out); return true; } bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes) { if (uncompressedBytes.size() == 0) { compressedBytes = uncompressedBytes; return true; } unsigned compLength = qMin(uncompressedBytes.size(), 16); compressedBytes.clear(); compressedBytes.resize(compLength); z_stream zs; memset(&zs, 0, sizeof(zs)); if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (16 + MAX_WBITS), 8, Z_DEFAULT_STRATEGY) != Z_OK) { return false; } zs.next_in = (Bytef*)uncompressedBytes.data(); zs.avail_in = uncompressedBytes.size(); int ret; compressedBytes.resize(uncompressedBytes.size()); unsigned offset = 0; unsigned temp = 0; do { auto remaining = compressedBytes.size() - offset; if (remaining < 1) { compressedBytes.resize(compressedBytes.size() * 2); } zs.next_out = reinterpret_cast((compressedBytes.data() + offset)); temp = zs.avail_out = compressedBytes.size() - offset; ret = deflate(&zs, Z_FINISH); offset += temp - zs.avail_out; } while (ret == Z_OK); compressedBytes.resize(offset); if (deflateEnd(&zs) != Z_OK) { return false; } if (ret != Z_STREAM_END) { return false; } return true; } int inf(QFile* source, std::function handleBlock) { constexpr auto CHUNK = 16384; int ret; unsigned have; z_stream strm; memset(&strm, 0, sizeof(strm)); char in[CHUNK]; unsigned char out[CHUNK]; ret = inflateInit2(&strm, (16 + MAX_WBITS)); if (ret != Z_OK) return ret; /* decompress until deflate stream ends or end of file */ do { strm.avail_in = source->read(in, CHUNK); if (source->error()) { (void)inflateEnd(&strm); return Z_ERRNO; } if (strm.avail_in == 0) break; strm.next_in = reinterpret_cast(in); /* run inflate() on input until output buffer not full */ do { strm.avail_out = CHUNK; strm.next_out = out; ret = inflate(&strm, Z_NO_FLUSH); assert(ret != Z_STREAM_ERROR); /* state not clobbered */ switch (ret) { case Z_NEED_DICT: ret = Z_DATA_ERROR; /* and fall through */ case Z_DATA_ERROR: case Z_MEM_ERROR: (void)inflateEnd(&strm); return ret; } have = CHUNK - strm.avail_out; if (!handleBlock(QByteArray(reinterpret_cast(out), have))) { (void)inflateEnd(&strm); return Z_OK; } } while (strm.avail_out == 0); /* done when inflate() says it's done */ } while (ret != Z_STREAM_END); /* clean up and return */ (void)inflateEnd(&strm); return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; } QString zerr(int ret) { switch (ret) { case Z_ERRNO: return QObject::tr("error handling file"); case Z_STREAM_ERROR: return QObject::tr("invalid compression level"); case Z_DATA_ERROR: return QObject::tr("invalid or incomplete deflate data"); case Z_MEM_ERROR: return QObject::tr("out of memory"); case Z_VERSION_ERROR: return QObject::tr("zlib version mismatch!"); } return {}; } QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock) { auto ret = inf(source, handleBlock); return zerr(ret); } PrismLauncher-10.0.5/launcher/InstanceCopyPrefs.cpp0000644000175100017510000000740715144136756021707 0ustar runnerrunner// // Created by marcelohdez on 10/22/22. // #include "InstanceCopyPrefs.h" bool InstanceCopyPrefs::allTrue() const { return copySaves && keepPlaytime && copyGameOptions && copyResourcePacks && copyShaderPacks && copyServers && copyMods && copyScreenshots; } // Returns a single RegEx string of the selected folders/files to filter out (ex: ".minecraft/saves|.minecraft/server.dat") QString InstanceCopyPrefs::getSelectedFiltersAsRegex() const { return getSelectedFiltersAsRegex({}); } QString InstanceCopyPrefs::getSelectedFiltersAsRegex(const QStringList& additionalFilters) const { QStringList filters; if (!copySaves) filters << "saves"; if (!copyGameOptions) filters << "options.txt"; if (!copyResourcePacks) filters << "resourcepacks" << "texturepacks"; if (!copyShaderPacks) filters << "shaderpacks"; if (!copyServers) filters << "servers.dat" << "servers.dat_old" << "server-resource-packs"; if (!copyMods) filters << "coremods" << "mods" << "config"; if (!copyScreenshots) filters << "screenshots"; for (auto filter : additionalFilters) { filters << filter; } // If we have any filters to add, join them as a single regex string to return: if (!filters.isEmpty()) { const QString MC_ROOT = "[.]?minecraft/"; // Ensure first filter starts with root, then join other filters with OR regex before root (ex: ".minecraft/saves|.minecraft/mods"): return MC_ROOT + filters.join("|" + MC_ROOT); } return {}; } // ======= Getters ======= bool InstanceCopyPrefs::isCopySavesEnabled() const { return copySaves; } bool InstanceCopyPrefs::isKeepPlaytimeEnabled() const { return keepPlaytime; } bool InstanceCopyPrefs::isCopyGameOptionsEnabled() const { return copyGameOptions; } bool InstanceCopyPrefs::isCopyResourcePacksEnabled() const { return copyResourcePacks; } bool InstanceCopyPrefs::isCopyShaderPacksEnabled() const { return copyShaderPacks; } bool InstanceCopyPrefs::isCopyServersEnabled() const { return copyServers; } bool InstanceCopyPrefs::isCopyModsEnabled() const { return copyMods; } bool InstanceCopyPrefs::isCopyScreenshotsEnabled() const { return copyScreenshots; } bool InstanceCopyPrefs::isUseSymLinksEnabled() const { return useSymLinks; } bool InstanceCopyPrefs::isUseHardLinksEnabled() const { return useHardLinks; } bool InstanceCopyPrefs::isLinkRecursivelyEnabled() const { return linkRecursively; } bool InstanceCopyPrefs::isDontLinkSavesEnabled() const { return dontLinkSaves; } bool InstanceCopyPrefs::isUseCloneEnabled() const { return useClone; } // ======= Setters ======= void InstanceCopyPrefs::enableCopySaves(bool b) { copySaves = b; } void InstanceCopyPrefs::enableKeepPlaytime(bool b) { keepPlaytime = b; } void InstanceCopyPrefs::enableCopyGameOptions(bool b) { copyGameOptions = b; } void InstanceCopyPrefs::enableCopyResourcePacks(bool b) { copyResourcePacks = b; } void InstanceCopyPrefs::enableCopyShaderPacks(bool b) { copyShaderPacks = b; } void InstanceCopyPrefs::enableCopyServers(bool b) { copyServers = b; } void InstanceCopyPrefs::enableCopyMods(bool b) { copyMods = b; } void InstanceCopyPrefs::enableCopyScreenshots(bool b) { copyScreenshots = b; } void InstanceCopyPrefs::enableUseSymLinks(bool b) { useSymLinks = b; } void InstanceCopyPrefs::enableLinkRecursively(bool b) { linkRecursively = b; } void InstanceCopyPrefs::enableUseHardLinks(bool b) { useHardLinks = b; } void InstanceCopyPrefs::enableDontLinkSaves(bool b) { dontLinkSaves = b; } void InstanceCopyPrefs::enableUseClone(bool b) { useClone = b; } PrismLauncher-10.0.5/launcher/Markdown.h0000644000175100017510000000151315144136756017527 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Joshua Goins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include QString markdownToHTML(const QString& markdown); PrismLauncher-10.0.5/launcher/DataMigrationTask.h0000644000175100017510000000165115144136756021316 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "FileSystem.h" #include "Filter.h" #include "tasks/Task.h" #include #include /* * Migrate existing data from other MMC-like launchers. */ class DataMigrationTask : public Task { Q_OBJECT public: explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathmatcher); ~DataMigrationTask() override = default; protected: virtual void executeTask() override; protected slots: void dryRunFinished(); void dryRunAborted(); void copyFinished(); void copyAborted(); private: const QString& m_sourcePath; const QString& m_targetPath; const Filter m_pathMatcher; FS::copy m_copy; int m_toCopy = 0; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; }; PrismLauncher-10.0.5/launcher/CMakeLists.txt0000644000175100017510000014314315144136756020342 0ustar runnerrunnerproject(application) ################################ FILES ################################ ######## Sources and headers ######## set(CORE_SOURCES # LOGIC - Base classes and infrastructure BaseInstaller.h BaseInstaller.cpp BaseVersionList.h BaseVersionList.cpp InstanceList.h InstanceList.cpp InstanceTask.h InstanceTask.cpp LoggedProcess.h LoggedProcess.cpp MessageLevel.cpp MessageLevel.h BaseVersion.h BaseInstance.h BaseInstance.cpp InstanceDirUpdate.h InstanceDirUpdate.cpp NullInstance.h MMCZip.h MMCZip.cpp archive/ArchiveReader.cpp archive/ArchiveReader.h archive/ArchiveWriter.cpp archive/ArchiveWriter.h archive/ExportToZipTask.cpp archive/ExportToZipTask.h archive/ExtractZipTask.cpp archive/ExtractZipTask.h StringUtils.h StringUtils.cpp QVariantUtils.h RuntimeContext.h PSaveFile.h # Basic instance manipulation tasks (derived from InstanceTask) InstanceCreationTask.h InstanceCreationTask.cpp InstanceCopyPrefs.h InstanceCopyPrefs.cpp InstanceCopyTask.h InstanceCopyTask.cpp InstanceImportTask.h InstanceImportTask.cpp # Resource downloading task ResourceDownloadTask.h ResourceDownloadTask.cpp # Use tracking separate from memory management Usable.h # Prefix tree where node names are strings between separators SeparatorPrefixTree.h # String filters Filter.h # JSON parsing helpers Json.h Json.cpp FileSystem.h FileSystem.cpp Exception.h # RW lock protected map RWStorage.h # A variable that has an implicit default value and keeps track of changes DefaultVariable.h # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms QObjectPtr.h # Compression support GZip.h GZip.cpp # Command line parameter parsing Commandline.h Commandline.cpp # Version number string support Version.h Version.cpp # A Recursive file system watcher RecursiveFileSystemWatcher.h RecursiveFileSystemWatcher.cpp # Time MMCTime.h MMCTime.cpp MTPixmapCache.h # Assertion helper AssertHelpers.h ) if (UNIX AND NOT CYGWIN AND NOT APPLE) set(CORE_SOURCES ${CORE_SOURCES} # MangoHud MangoHud.h MangoHud.cpp ) endif() set(NET_SOURCES # network stuffs net/ByteArraySink.h net/ChecksumValidator.h net/Download.cpp net/Download.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp net/HttpMetaCache.h net/MetaCacheSink.cpp net/MetaCacheSink.h net/Logging.h net/Logging.cpp net/NetJob.cpp net/NetJob.h net/NetUtils.h net/PasteUpload.cpp net/PasteUpload.h net/Sink.h net/Validator.h net/Upload.cpp net/Upload.h net/HeaderProxy.h net/RawHeaderProxy.h net/ApiHeaderProxy.h net/ApiDownload.h net/ApiDownload.cpp net/ApiUpload.cpp net/ApiUpload.h net/NetRequest.cpp net/NetRequest.h ) # Game launch logic set(LAUNCH_SOURCES launch/steps/CheckJava.cpp launch/steps/CheckJava.h launch/steps/LookupServerAddress.cpp launch/steps/LookupServerAddress.h launch/steps/PostLaunchCommand.cpp launch/steps/PostLaunchCommand.h launch/steps/PreLaunchCommand.cpp launch/steps/PreLaunchCommand.h launch/steps/TextPrint.cpp launch/steps/TextPrint.h launch/steps/QuitAfterGameStop.cpp launch/steps/QuitAfterGameStop.h launch/steps/PrintServers.cpp launch/steps/PrintServers.h launch/LaunchStep.cpp launch/LaunchStep.h launch/LaunchTask.cpp launch/LaunchTask.h launch/LogModel.cpp launch/LogModel.h launch/TaskStepWrapper.cpp launch/TaskStepWrapper.h logs/LogParser.cpp logs/LogParser.h ) # Old update system set(UPDATE_SOURCES updater/ExternalUpdater.h ) set(MAC_UPDATE_SOURCES updater/MacSparkleUpdater.h updater/MacSparkleUpdater.mm ) set(PRISM_UPDATE_SOURCES updater/PrismExternalUpdater.h updater/PrismExternalUpdater.cpp ) # Backend for the news bar... there's usually no news. set(NEWS_SOURCES # News System news/NewsChecker.h news/NewsChecker.cpp news/NewsEntry.h news/NewsEntry.cpp ) # Icon interface set(ICONS_SOURCES # Icons System and related code icons/IconUtils.h icons/IconUtils.cpp ) # Support for Minecraft instances and launch set(MINECRAFT_SOURCES # Logging minecraft/Logging.h minecraft/Logging.cpp # Minecraft support minecraft/auth/AccountData.cpp minecraft/auth/AccountData.h minecraft/auth/AccountList.cpp minecraft/auth/AccountList.h minecraft/auth/AuthSession.cpp minecraft/auth/AuthSession.h minecraft/auth/AuthStep.h minecraft/auth/MinecraftAccount.cpp minecraft/auth/MinecraftAccount.h minecraft/auth/Parsers.cpp minecraft/auth/Parsers.h minecraft/auth/AuthFlow.cpp minecraft/auth/AuthFlow.h minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h minecraft/auth/steps/GetSkinStep.cpp minecraft/auth/steps/GetSkinStep.h minecraft/auth/steps/LauncherLoginStep.cpp minecraft/auth/steps/LauncherLoginStep.h minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.h minecraft/auth/steps/MSADeviceCodeStep.cpp minecraft/auth/steps/MSADeviceCodeStep.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp minecraft/auth/steps/XboxAuthorizationStep.h minecraft/auth/steps/XboxProfileStep.cpp minecraft/auth/steps/XboxProfileStep.h minecraft/auth/steps/XboxUserStep.cpp minecraft/auth/steps/XboxUserStep.h minecraft/update/AssetUpdateTask.h minecraft/update/AssetUpdateTask.cpp minecraft/update/FMLLibrariesTask.cpp minecraft/update/FMLLibrariesTask.h minecraft/update/FoldersTask.cpp minecraft/update/FoldersTask.h minecraft/update/LibrariesTask.cpp minecraft/update/LibrariesTask.h minecraft/launch/ClaimAccount.cpp minecraft/launch/ClaimAccount.h minecraft/launch/CreateGameFolders.cpp minecraft/launch/CreateGameFolders.h minecraft/launch/EnsureOfflineLibraries.cpp minecraft/launch/EnsureOfflineLibraries.h minecraft/launch/ModMinecraftJar.cpp minecraft/launch/ModMinecraftJar.h minecraft/launch/ExtractNatives.cpp minecraft/launch/ExtractNatives.h minecraft/launch/LauncherPartLaunch.cpp minecraft/launch/LauncherPartLaunch.h minecraft/launch/MinecraftTarget.cpp minecraft/launch/MinecraftTarget.h minecraft/launch/PrintInstanceInfo.cpp minecraft/launch/PrintInstanceInfo.h minecraft/launch/ReconstructAssets.cpp minecraft/launch/ReconstructAssets.h minecraft/launch/ScanModFolders.cpp minecraft/launch/ScanModFolders.h minecraft/launch/VerifyJavaInstall.cpp minecraft/launch/VerifyJavaInstall.h minecraft/launch/AutoInstallJava.cpp minecraft/launch/AutoInstallJava.h minecraft/GradleSpecifier.h minecraft/MinecraftInstance.cpp minecraft/MinecraftInstance.h minecraft/LaunchProfile.cpp minecraft/LaunchProfile.h minecraft/Component.cpp minecraft/Component.h minecraft/PackProfile.cpp minecraft/PackProfile.h minecraft/ComponentUpdateTask.cpp minecraft/ComponentUpdateTask.h minecraft/MinecraftLoadAndCheck.h minecraft/MinecraftLoadAndCheck.cpp minecraft/MojangVersionFormat.cpp minecraft/MojangVersionFormat.h minecraft/Rule.cpp minecraft/Rule.h minecraft/OneSixVersionFormat.cpp minecraft/OneSixVersionFormat.h minecraft/ParseUtils.cpp minecraft/ParseUtils.h minecraft/ProfileUtils.cpp minecraft/ProfileUtils.h minecraft/ShortcutUtils.cpp minecraft/ShortcutUtils.h minecraft/Library.cpp minecraft/Library.h minecraft/MojangDownloadInfo.h minecraft/VanillaInstanceCreationTask.cpp minecraft/VanillaInstanceCreationTask.h minecraft/VersionFile.cpp minecraft/VersionFile.h minecraft/VersionFilterData.h minecraft/VersionFilterData.cpp minecraft/World.h minecraft/World.cpp minecraft/WorldList.h minecraft/WorldList.cpp minecraft/mod/MetadataHandler.h minecraft/mod/Mod.h minecraft/mod/Mod.cpp minecraft/mod/ModDetails.h minecraft/mod/ModFolderModel.h minecraft/mod/ModFolderModel.cpp minecraft/mod/Resource.h minecraft/mod/Resource.cpp minecraft/mod/ResourceFolderModel.h minecraft/mod/ResourceFolderModel.cpp minecraft/mod/DataPack.h minecraft/mod/DataPack.cpp minecraft/mod/DataPackFolderModel.h minecraft/mod/DataPackFolderModel.cpp minecraft/mod/ResourcePack.h minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePackFolderModel.h minecraft/mod/ResourcePackFolderModel.cpp minecraft/mod/TexturePack.h minecraft/mod/TexturePack.cpp minecraft/mod/ShaderPack.h minecraft/mod/ShaderPack.cpp minecraft/mod/WorldSave.h minecraft/mod/WorldSave.cpp minecraft/mod/TexturePackFolderModel.h minecraft/mod/TexturePackFolderModel.cpp minecraft/mod/ShaderPackFolderModel.h minecraft/mod/ShaderPackFolderModel.cpp minecraft/mod/tasks/ResourceFolderLoadTask.h minecraft/mod/tasks/ResourceFolderLoadTask.cpp minecraft/mod/tasks/LocalModParseTask.h minecraft/mod/tasks/LocalModParseTask.cpp minecraft/mod/tasks/LocalResourceUpdateTask.h minecraft/mod/tasks/LocalResourceUpdateTask.cpp minecraft/mod/tasks/LocalDataPackParseTask.h minecraft/mod/tasks/LocalDataPackParseTask.cpp minecraft/mod/tasks/LocalTexturePackParseTask.h minecraft/mod/tasks/LocalTexturePackParseTask.cpp minecraft/mod/tasks/LocalShaderPackParseTask.h minecraft/mod/tasks/LocalShaderPackParseTask.cpp minecraft/mod/tasks/LocalWorldSaveParseTask.h minecraft/mod/tasks/LocalWorldSaveParseTask.cpp minecraft/mod/tasks/LocalResourceParse.h minecraft/mod/tasks/LocalResourceParse.cpp minecraft/mod/tasks/GetModDependenciesTask.h minecraft/mod/tasks/GetModDependenciesTask.cpp # Assets minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp # Minecraft skins minecraft/skins/CapeChange.cpp minecraft/skins/CapeChange.h minecraft/skins/SkinUpload.cpp minecraft/skins/SkinUpload.h minecraft/skins/SkinDelete.cpp minecraft/skins/SkinDelete.h minecraft/skins/SkinModel.cpp minecraft/skins/SkinModel.h minecraft/skins/SkinList.cpp minecraft/skins/SkinList.h minecraft/Agent.h) # the screenshots feature set(SCREENSHOTS_SOURCES screenshots/Screenshot.h screenshots/ImgurUpload.h screenshots/ImgurUpload.cpp screenshots/ImgurAlbumCreation.h screenshots/ImgurAlbumCreation.cpp ) set(TASKS_SOURCES # Tasks tasks/Task.h tasks/Task.cpp tasks/ConcurrentTask.h tasks/ConcurrentTask.cpp tasks/SequentialTask.h tasks/SequentialTask.cpp tasks/MultipleOptionsTask.h tasks/MultipleOptionsTask.cpp ) set(SETTINGS_SOURCES # Settings settings/INIFile.cpp settings/INIFile.h settings/INISettingsObject.cpp settings/INISettingsObject.h settings/OverrideSetting.cpp settings/OverrideSetting.h settings/PassthroughSetting.cpp settings/PassthroughSetting.h settings/Setting.cpp settings/Setting.h settings/SettingsObject.cpp settings/SettingsObject.h ) set(JAVA_SOURCES java/JavaChecker.h java/JavaChecker.cpp java/JavaInstall.h java/JavaInstall.cpp java/JavaInstallList.h java/JavaInstallList.cpp java/JavaUtils.h java/JavaUtils.cpp java/JavaVersion.h java/JavaVersion.cpp java/JavaMetadata.h java/JavaMetadata.cpp java/download/ArchiveDownloadTask.cpp java/download/ArchiveDownloadTask.h java/download/ManifestDownloadTask.cpp java/download/ManifestDownloadTask.h java/download/SymlinkTask.cpp java/download/SymlinkTask.h ui/java/InstallJavaDialog.h ui/java/InstallJavaDialog.cpp ui/java/VersionList.h ui/java/VersionList.cpp ) set(TRANSLATIONS_SOURCES translations/TranslationsModel.h translations/TranslationsModel.cpp translations/POTranslator.h translations/POTranslator.cpp ) set(TOOLS_SOURCES # Tools tools/BaseExternalTool.cpp tools/BaseExternalTool.h tools/BaseProfiler.cpp tools/BaseProfiler.h tools/JProfiler.cpp tools/JProfiler.h tools/JVisualVM.cpp tools/JVisualVM.h tools/MCEditTool.cpp tools/MCEditTool.h tools/GenericProfiler.cpp tools/GenericProfiler.h ) set(META_SOURCES # Metadata sources meta/JsonFormat.cpp meta/JsonFormat.h meta/BaseEntity.cpp meta/BaseEntity.h meta/VersionList.cpp meta/VersionList.h meta/Version.cpp meta/Version.h meta/Index.cpp meta/Index.h ) set(API_SOURCES modplatform/ModIndex.h modplatform/ModIndex.cpp modplatform/ResourceType.h modplatform/ResourceType.cpp modplatform/ResourceAPI.h modplatform/ResourceAPI.cpp modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.cpp modplatform/CheckUpdateTask.h modplatform/flame/FlameAPI.h modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.cpp modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.cpp modplatform/helpers/OverrideUtils.h modplatform/helpers/OverrideUtils.cpp modplatform/helpers/ExportToModList.h modplatform/helpers/ExportToModList.cpp ) set(FTB_SOURCES modplatform/legacy_ftb/PackFetchTask.h modplatform/legacy_ftb/PackFetchTask.cpp modplatform/legacy_ftb/PackInstallTask.h modplatform/legacy_ftb/PackInstallTask.cpp modplatform/legacy_ftb/PrivatePackManager.h modplatform/legacy_ftb/PrivatePackManager.cpp modplatform/legacy_ftb/PackHelpers.h modplatform/import_ftb/PackInstallTask.h modplatform/import_ftb/PackInstallTask.cpp modplatform/import_ftb/PackHelpers.h modplatform/import_ftb/PackHelpers.cpp ) set(FLAME_SOURCES # Flame modplatform/flame/FlameModIndex.cpp modplatform/flame/FlameModIndex.h modplatform/flame/PackManifest.h modplatform/flame/PackManifest.cpp modplatform/flame/FileResolvingTask.h modplatform/flame/FileResolvingTask.cpp modplatform/flame/FlameCheckUpdate.cpp modplatform/flame/FlameCheckUpdate.h modplatform/flame/FlameInstanceCreationTask.h modplatform/flame/FlameInstanceCreationTask.cpp modplatform/flame/FlamePackExportTask.h modplatform/flame/FlamePackExportTask.cpp ) set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.cpp modplatform/modrinth/ModrinthPackIndex.h modplatform/modrinth/ModrinthCheckUpdate.cpp modplatform/modrinth/ModrinthCheckUpdate.h modplatform/modrinth/ModrinthInstanceCreationTask.cpp modplatform/modrinth/ModrinthInstanceCreationTask.h modplatform/modrinth/ModrinthPackExportTask.cpp modplatform/modrinth/ModrinthPackExportTask.h ) set(PACKWIZ_SOURCES modplatform/packwiz/Packwiz.h modplatform/packwiz/Packwiz.cpp ) set(TECHNIC_SOURCES modplatform/technic/SingleZipPackInstallTask.h modplatform/technic/SingleZipPackInstallTask.cpp modplatform/technic/SolderPackInstallTask.h modplatform/technic/SolderPackInstallTask.cpp modplatform/technic/SolderPackManifest.h modplatform/technic/SolderPackManifest.cpp modplatform/technic/TechnicPackProcessor.h modplatform/technic/TechnicPackProcessor.cpp ) set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLPackIndex.cpp modplatform/atlauncher/ATLPackIndex.h modplatform/atlauncher/ATLPackInstallTask.cpp modplatform/atlauncher/ATLPackInstallTask.h modplatform/atlauncher/ATLPackManifest.cpp modplatform/atlauncher/ATLPackManifest.h modplatform/atlauncher/ATLShareCode.cpp modplatform/atlauncher/ATLShareCode.h ) set(LINKEXE_SOURCES console/WindowsConsole.h console/WindowsConsole.cpp filelink/FileLink.h filelink/FileLink.cpp FileSystem.h FileSystem.cpp Exception.h StringUtils.h StringUtils.cpp DesktopServices.h DesktopServices.cpp ) set(PRISMUPDATER_SOURCES updater/prismupdater/PrismUpdater.h updater/prismupdater/PrismUpdater.cpp updater/prismupdater/UpdaterDialogs.h updater/prismupdater/UpdaterDialogs.cpp updater/prismupdater/GitHubRelease.h updater/prismupdater/GitHubRelease.cpp Json.h Json.cpp FileSystem.h FileSystem.cpp StringUtils.h StringUtils.cpp DesktopServices.h DesktopServices.cpp Version.h Version.cpp Markdown.h Markdown.cpp # Zip MMCZip.h MMCZip.cpp archive/ArchiveReader.cpp archive/ArchiveReader.h archive/ArchiveWriter.cpp archive/ArchiveWriter.h # Time MMCTime.h MMCTime.cpp net/ByteArraySink.h net/ChecksumValidator.h net/Download.cpp net/Download.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp net/HttpMetaCache.h net/Logging.h net/Logging.cpp net/NetRequest.cpp net/NetRequest.h net/NetJob.cpp net/NetJob.h net/NetUtils.h net/Sink.h net/Validator.h net/HeaderProxy.h net/RawHeaderProxy.h ui/dialogs/ProgressDialog.cpp ui/dialogs/ProgressDialog.h ui/widgets/SubTaskProgressBar.h ui/widgets/SubTaskProgressBar.cpp ) if(WIN32) set(PRISMUPDATER_SOURCES console/WindowsConsole.h console/WindowsConsole.cpp ${PRISMUPDATER_SOURCES} ) endif() ######## Logging categories ######## ecm_qt_declare_logging_category(CORE_SOURCES HEADER Logging.h IDENTIFIER authCredentials CATEGORY_NAME "launcher.auth.credentials" DEFAULT_SEVERITY Warning DESCRIPTION "Secrets and credentials for debugging purposes" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER instanceProfileC CATEGORY_NAME "launcher.instance.profile" DEFAULT_SEVERITY Debug DESCRIPTION "Profile actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER instanceProfileResolveC CATEGORY_NAME "launcher.instance.profile.resolve" DEFAULT_SEVERITY Debug DESCRIPTION "Profile component resolusion actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskLogC CATEGORY_NAME "launcher.task" DEFAULT_SEVERITY Debug DESCRIPTION "Task actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskNetLogC CATEGORY_NAME "launcher.task.net" DEFAULT_SEVERITY Debug DESCRIPTION "Task network action" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskDownloadLogC CATEGORY_NAME "launcher.task.net.download" DEFAULT_SEVERITY Debug DESCRIPTION "Task network download actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskUploadLogC CATEGORY_NAME "launcher.task.net.upload" DEFAULT_SEVERITY Debug DESCRIPTION "Task network upload actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskMetaCacheLogC CATEGORY_NAME "launcher.task.net.metacache" DEFAULT_SEVERITY Debug DESCRIPTION "task network meta-cache actions" EXPORT "${Launcher_Name}" ) ecm_qt_export_logging_category( IDENTIFIER taskHttpMetaCacheLogC CATEGORY_NAME "launcher.task.net.metacache.http" DEFAULT_SEVERITY Debug DESCRIPTION "task network http meta-cache actions" EXPORT "${Launcher_Name}" ) if(KDE_INSTALL_LOGGINGCATEGORIESDIR) # only install if there is a standard path for this ecm_qt_install_logging_categories( EXPORT "${Launcher_Name}" DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}" ) endif() ################################ COMPILE ################################ set(LOGIC_SOURCES ${CORE_SOURCES} ${NET_SOURCES} ${LAUNCH_SOURCES} ${UPDATE_SOURCES} ${NEWS_SOURCES} ${MINECRAFT_SOURCES} ${SCREENSHOTS_SOURCES} ${TASKS_SOURCES} ${SETTINGS_SOURCES} ${JAVA_SOURCES} ${TRANSLATIONS_SOURCES} ${TOOLS_SOURCES} ${META_SOURCES} ${ICONS_SOURCES} ${API_SOURCES} ${FTB_SOURCES} ${FLAME_SOURCES} ${MODRINTH_SOURCES} ${PACKWIZ_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} ) if(APPLE AND Launcher_ENABLE_UPDATER) set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES}) else() set (LOGIC_SOURCES ${LOGIC_SOURCES} ${PRISM_UPDATE_SOURCES}) endif() SET(LAUNCHER_SOURCES # Application base Application.h Application.cpp DataMigrationTask.h DataMigrationTask.cpp ApplicationMessage.h ApplicationMessage.cpp SysInfo.h SysInfo.cpp # console utils console/Console.h # GUI - general utilities DesktopServices.h DesktopServices.cpp VersionProxyModel.h VersionProxyModel.cpp Markdown.h Markdown.cpp # Super secret! KonamiCode.h KonamiCode.cpp # Bundled resources resources/backgrounds/backgrounds.qrc resources/multimc/multimc.qrc resources/pe_dark/pe_dark.qrc resources/pe_light/pe_light.qrc resources/pe_colored/pe_colored.qrc resources/pe_blue/pe_blue.qrc resources/breeze_dark/breeze_dark.qrc resources/breeze_light/breeze_light.qrc resources/OSX/OSX.qrc resources/iOS/iOS.qrc resources/flat/flat.qrc resources/flat_white/flat_white.qrc resources/documents/documents.qrc resources/shaders/shaders.qrc "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" # Icons icons/MMCIcon.h icons/MMCIcon.cpp icons/IconList.h icons/IconList.cpp # log utils logs/AnonymizeLog.cpp logs/AnonymizeLog.h # GUI - windows ui/GuiUtil.h ui/GuiUtil.cpp ui/MainWindow.h ui/MainWindow.cpp ui/InstanceWindow.h ui/InstanceWindow.cpp ui/ViewLogWindow.h ui/ViewLogWindow.cpp ui/ToolTipFilter.h ui/ToolTipFilter.cpp # FIXME: maybe find a better home for this. FileIgnoreProxy.cpp FileIgnoreProxy.h FastFileIconProvider.cpp FastFileIconProvider.h # GUI - setup wizard ui/setupwizard/SetupWizard.h ui/setupwizard/SetupWizard.cpp ui/setupwizard/BaseWizardPage.h ui/setupwizard/JavaWizardPage.cpp ui/setupwizard/JavaWizardPage.h ui/setupwizard/LanguageWizardPage.cpp ui/setupwizard/LanguageWizardPage.h ui/setupwizard/PasteWizardPage.cpp ui/setupwizard/PasteWizardPage.h ui/setupwizard/ThemeWizardPage.h ui/setupwizard/AutoJavaWizardPage.cpp ui/setupwizard/AutoJavaWizardPage.h ui/setupwizard/LoginWizardPage.cpp ui/setupwizard/LoginWizardPage.h # GUI - themes ui/themes/FusionTheme.cpp ui/themes/FusionTheme.h ui/themes/BrightTheme.cpp ui/themes/BrightTheme.h ui/themes/CustomTheme.cpp ui/themes/CustomTheme.h ui/themes/DarkTheme.cpp ui/themes/DarkTheme.h ui/themes/ITheme.cpp ui/themes/ITheme.h ui/themes/HintOverrideProxyStyle.cpp ui/themes/HintOverrideProxyStyle.h ui/themes/SystemTheme.cpp ui/themes/SystemTheme.h ui/themes/IconTheme.cpp ui/themes/IconTheme.h ui/themes/ThemeManager.cpp ui/themes/ThemeManager.h ui/themes/CatPack.cpp ui/themes/CatPack.h ui/themes/CatPainter.cpp ui/themes/CatPainter.h # Processes LaunchController.h LaunchController.cpp # page provider for instances InstancePageProvider.h # Common java checking UI JavaCommon.h JavaCommon.cpp # GUI - paged dialog base ui/pages/BasePage.h ui/pages/BasePageContainer.h ui/pages/BasePageProvider.h # GUI - instance pages ui/pages/instance/ExternalResourcesPage.cpp ui/pages/instance/ExternalResourcesPage.h ui/pages/instance/VersionPage.cpp ui/pages/instance/VersionPage.h ui/pages/instance/ManagedPackPage.cpp ui/pages/instance/ManagedPackPage.h ui/pages/instance/DataPackPage.h ui/pages/instance/DataPackPage.cpp ui/pages/instance/TexturePackPage.h ui/pages/instance/TexturePackPage.cpp ui/pages/instance/ResourcePackPage.h ui/pages/instance/ResourcePackPage.cpp ui/pages/instance/ShaderPackPage.h ui/pages/instance/ShaderPackPage.cpp ui/pages/instance/ModFolderPage.cpp ui/pages/instance/ModFolderPage.h ui/pages/instance/NotesPage.cpp ui/pages/instance/NotesPage.h ui/pages/instance/LogPage.cpp ui/pages/instance/LogPage.h ui/pages/instance/InstanceSettingsPage.h ui/pages/instance/ScreenshotsPage.cpp ui/pages/instance/ScreenshotsPage.h ui/pages/instance/OtherLogsPage.cpp ui/pages/instance/OtherLogsPage.h ui/pages/instance/ServersPage.cpp ui/pages/instance/ServersPage.h ui/pages/instance/WorldListPage.cpp ui/pages/instance/WorldListPage.h ui/pages/instance/McClient.cpp ui/pages/instance/McClient.h ui/pages/instance/McResolver.cpp ui/pages/instance/McResolver.h ui/pages/instance/ServerPingTask.cpp ui/pages/instance/ServerPingTask.h # GUI - global settings pages ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.h ui/pages/global/ExternalToolsPage.cpp ui/pages/global/ExternalToolsPage.h ui/pages/global/JavaPage.cpp ui/pages/global/JavaPage.h ui/pages/global/LanguagePage.cpp ui/pages/global/LanguagePage.h ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h ui/pages/global/APIPage.cpp ui/pages/global/APIPage.h # GUI - platform pages ui/pages/modplatform/CustomPage.cpp ui/pages/modplatform/CustomPage.h ui/pages/modplatform/ResourcePage.cpp ui/pages/modplatform/ResourcePage.h ui/pages/modplatform/ResourceModel.cpp ui/pages/modplatform/ResourceModel.h ui/pages/modplatform/ModPage.cpp ui/pages/modplatform/ModPage.h ui/pages/modplatform/ModModel.cpp ui/pages/modplatform/ModModel.h ui/pages/modplatform/ResourcePackPage.cpp ui/pages/modplatform/ResourcePackModel.cpp # Needed for MOC to find them without a corresponding .cpp ui/pages/modplatform/TexturePackPage.h ui/pages/modplatform/TexturePackModel.cpp ui/pages/modplatform/ShaderPackPage.cpp ui/pages/modplatform/ShaderPackModel.cpp ui/pages/modplatform/DataPackPage.cpp ui/pages/modplatform/DataPackModel.cpp ui/pages/modplatform/ModpackProviderBasePage.h ui/pages/modplatform/atlauncher/AtlFilterModel.cpp ui/pages/modplatform/atlauncher/AtlFilterModel.h ui/pages/modplatform/atlauncher/AtlListModel.cpp ui/pages/modplatform/atlauncher/AtlListModel.h ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h ui/pages/modplatform/atlauncher/AtlPage.cpp ui/pages/modplatform/atlauncher/AtlPage.h ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h ui/pages/modplatform/legacy_ftb/Page.cpp ui/pages/modplatform/legacy_ftb/Page.h ui/pages/modplatform/legacy_ftb/ListModel.h ui/pages/modplatform/legacy_ftb/ListModel.cpp ui/pages/modplatform/import_ftb/ImportFTBPage.cpp ui/pages/modplatform/import_ftb/ImportFTBPage.h ui/pages/modplatform/import_ftb/ListModel.h ui/pages/modplatform/import_ftb/ListModel.cpp ui/pages/modplatform/flame/FlameModel.cpp ui/pages/modplatform/flame/FlameModel.h ui/pages/modplatform/flame/FlamePage.cpp ui/pages/modplatform/flame/FlamePage.h ui/pages/modplatform/flame/FlameResourceModels.cpp ui/pages/modplatform/flame/FlameResourceModels.h ui/pages/modplatform/flame/FlameResourcePages.cpp ui/pages/modplatform/flame/FlameResourcePages.h ui/pages/modplatform/modrinth/ModrinthPage.cpp ui/pages/modplatform/modrinth/ModrinthPage.h ui/pages/modplatform/modrinth/ModrinthModel.cpp ui/pages/modplatform/modrinth/ModrinthModel.h ui/pages/modplatform/technic/TechnicModel.cpp ui/pages/modplatform/technic/TechnicModel.h ui/pages/modplatform/technic/TechnicPage.cpp ui/pages/modplatform/technic/TechnicPage.h ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.h ui/pages/modplatform/OptionalModDialog.cpp ui/pages/modplatform/OptionalModDialog.h ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp ui/pages/modplatform/modrinth/ModrinthResourcePages.h # GUI - dialogs ui/dialogs/AboutDialog.cpp ui/dialogs/AboutDialog.h ui/dialogs/ProfileSelectDialog.cpp ui/dialogs/ProfileSelectDialog.h ui/dialogs/ProfileSetupDialog.cpp ui/dialogs/ProfileSetupDialog.h ui/dialogs/CopyInstanceDialog.cpp ui/dialogs/CopyInstanceDialog.h ui/dialogs/CreateShortcutDialog.cpp ui/dialogs/CreateShortcutDialog.h ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.h ui/dialogs/ExportInstanceDialog.cpp ui/dialogs/ExportInstanceDialog.h ui/dialogs/ExportPackDialog.cpp ui/dialogs/ExportPackDialog.h ui/dialogs/ExportToModListDialog.cpp ui/dialogs/ExportToModListDialog.h ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.h ui/dialogs/ImportResourceDialog.cpp ui/dialogs/ImportResourceDialog.h ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.h ui/dialogs/NewComponentDialog.cpp ui/dialogs/NewComponentDialog.h ui/dialogs/NewInstanceDialog.cpp ui/dialogs/NewInstanceDialog.h ui/dialogs/NewsDialog.cpp ui/dialogs/NewsDialog.h ui/pagedialog/PageDialog.cpp ui/pagedialog/PageDialog.h ui/dialogs/ProgressDialog.cpp ui/dialogs/ProgressDialog.h ui/dialogs/ReviewMessageBox.cpp ui/dialogs/ReviewMessageBox.h ui/dialogs/VersionSelectDialog.cpp ui/dialogs/VersionSelectDialog.h ui/dialogs/ResourceDownloadDialog.cpp ui/dialogs/ResourceDownloadDialog.h ui/dialogs/ScrollMessageBox.cpp ui/dialogs/ScrollMessageBox.h ui/dialogs/BlockedModsDialog.cpp ui/dialogs/BlockedModsDialog.h ui/dialogs/ChooseProviderDialog.h ui/dialogs/ChooseProviderDialog.cpp ui/dialogs/ResourceUpdateDialog.cpp ui/dialogs/ResourceUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h ui/dialogs/ChooseOfflineNameDialog.cpp ui/dialogs/ChooseOfflineNameDialog.h ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h ui/dialogs/skins/draw/SkinOpenGLWindow.h ui/dialogs/skins/draw/SkinOpenGLWindow.cpp ui/dialogs/skins/draw/Scene.h ui/dialogs/skins/draw/Scene.cpp ui/dialogs/skins/draw/BoxGeometry.h ui/dialogs/skins/draw/BoxGeometry.cpp # GUI - widgets ui/widgets/CheckComboBox.cpp ui/widgets/CheckComboBox.h ui/widgets/Common.cpp ui/widgets/Common.h ui/widgets/CustomCommands.cpp ui/widgets/CustomCommands.h ui/widgets/EnvironmentVariables.cpp ui/widgets/EnvironmentVariables.h ui/widgets/IconLabel.cpp ui/widgets/IconLabel.h ui/widgets/JavaWizardWidget.cpp ui/widgets/JavaWizardWidget.h ui/widgets/LabeledToolButton.cpp ui/widgets/LabeledToolButton.h ui/widgets/LanguageSelectionWidget.cpp ui/widgets/LanguageSelectionWidget.h ui/widgets/LogView.cpp ui/widgets/LogView.h ui/widgets/InfoFrame.cpp ui/widgets/InfoFrame.h ui/widgets/ModFilterWidget.cpp ui/widgets/ModFilterWidget.h ui/widgets/ModListView.cpp ui/widgets/ModListView.h ui/widgets/PageContainer.cpp ui/widgets/PageContainer.h ui/widgets/PageContainer_p.h ui/widgets/ProjectDescriptionPage.h ui/widgets/ProjectDescriptionPage.cpp ui/widgets/VariableSizedImageObject.h ui/widgets/VariableSizedImageObject.cpp ui/widgets/ProjectItem.h ui/widgets/ProjectItem.cpp ui/widgets/SubTaskProgressBar.h ui/widgets/SubTaskProgressBar.cpp ui/widgets/VersionListView.cpp ui/widgets/VersionListView.h ui/widgets/VersionSelectWidget.cpp ui/widgets/VersionSelectWidget.h ui/widgets/ProgressWidget.h ui/widgets/ProgressWidget.cpp ui/widgets/WideBar.h ui/widgets/WideBar.cpp ui/widgets/AppearanceWidget.h ui/widgets/AppearanceWidget.cpp ui/widgets/MinecraftSettingsWidget.h ui/widgets/MinecraftSettingsWidget.cpp ui/widgets/JavaSettingsWidget.h ui/widgets/JavaSettingsWidget.cpp # GUI - instance group view ui/instanceview/InstanceProxyModel.cpp ui/instanceview/InstanceProxyModel.h ui/instanceview/AccessibleInstanceView.cpp ui/instanceview/AccessibleInstanceView.h ui/instanceview/AccessibleInstanceView_p.h ui/instanceview/InstanceView.cpp ui/instanceview/InstanceView.h ui/instanceview/InstanceDelegate.cpp ui/instanceview/InstanceDelegate.h ui/instanceview/VisualGroup.cpp ui/instanceview/VisualGroup.h ) if (APPLE) set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} ui/themes/ThemeManager.mm ) endif() if (NOT Apple) set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} ui/dialogs/UpdateAvailableDialog.h ui/dialogs/UpdateAvailableDialog.cpp ) endif() if (APPLE) set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} macsandbox/SecurityBookmarkFileAccess.h macsandbox/SecurityBookmarkFileAccess.mm ) endif() if(WIN32) set(LAUNCHER_SOURCES console/WindowsConsole.h console/WindowsConsole.cpp ${LAUNCHER_SOURCES} ) endif() qt_wrap_ui(LAUNCHER_UI ui/MainWindow.ui ui/setupwizard/PasteWizardPage.ui ui/setupwizard/AutoJavaWizardPage.ui ui/setupwizard/LoginWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/ExternalToolsPage.ui ui/pages/instance/ExternalResourcesPage.ui ui/pages/instance/NotesPage.ui ui/pages/instance/LogPage.ui ui/pages/instance/ServersPage.ui ui/pages/instance/OtherLogsPage.ui ui/pages/instance/VersionPage.ui ui/pages/instance/ManagedPackPage.ui ui/pages/instance/WorldListPage.ui ui/pages/instance/ScreenshotsPage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui ui/pages/modplatform/atlauncher/AtlPage.ui ui/pages/modplatform/CustomPage.ui ui/pages/modplatform/ResourcePage.ui ui/pages/modplatform/flame/FlamePage.ui ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/import_ftb/ImportFTBPage.ui ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/OptionalModDialog.ui ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui ui/widgets/CustomCommands.ui ui/widgets/EnvironmentVariables.ui ui/widgets/InfoFrame.ui ui/widgets/ModFilterWidget.ui ui/widgets/SubTaskProgressBar.ui ui/widgets/AppearanceWidget.ui ui/widgets/MinecraftSettingsWidget.ui ui/widgets/JavaSettingsWidget.ui ui/dialogs/CopyInstanceDialog.ui ui/dialogs/CreateShortcutDialog.ui ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui ui/dialogs/NewInstanceDialog.ui ui/dialogs/NewComponentDialog.ui ui/dialogs/NewsDialog.ui ui/dialogs/ProfileSelectDialog.ui ui/dialogs/ExportInstanceDialog.ui ui/dialogs/ExportPackDialog.ui ui/dialogs/ExportToModListDialog.ui ui/dialogs/IconPickerDialog.ui ui/dialogs/ImportResourceDialog.ui ui/dialogs/MSALoginDialog.ui ui/dialogs/AboutDialog.ui ui/dialogs/ReviewMessageBox.ui ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui ui/dialogs/skins/SkinManageDialog.ui ui/dialogs/ChooseOfflineNameDialog.ui ) qt_wrap_ui(PRISM_UPDATE_UI ui/dialogs/UpdateAvailableDialog.ui ) if (NOT Apple) set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI}) endif() qt_add_resources(LAUNCHER_RESOURCES resources/backgrounds/backgrounds.qrc resources/multimc/multimc.qrc resources/pe_dark/pe_dark.qrc resources/pe_light/pe_light.qrc resources/pe_colored/pe_colored.qrc resources/pe_blue/pe_blue.qrc resources/breeze_dark/breeze_dark.qrc resources/breeze_light/breeze_light.qrc resources/OSX/OSX.qrc resources/iOS/iOS.qrc resources/flat/flat.qrc resources/documents/documents.qrc resources/shaders/shaders.qrc "${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_LogoQRC}" ) qt_wrap_ui(PRISMUPDATER_UI updater/prismupdater/SelectReleaseDialog.ui ui/widgets/SubTaskProgressBar.ui ui/dialogs/ProgressDialog.ui ) ######## Windows resource files ######## if(WIN32) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) endif() include(CompilerWarnings) ######## Precompiled Headers ########### if(${Launcher_USE_PCH}) message(STATUS "Using precompiled headers for applicable launcher targets") set(PRECOMPILED_HEADERS include/base.pch.hpp include/qtcore.pch.hpp include/qtgui.pch.hpp ) endif() ####### Targets ######## # Add executable add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) set_project_warnings(Launcher_logic "${Launcher_MSVC_WARNINGS}" "${Launcher_CLANG_WARNINGS}" "${Launcher_GCC_WARNINGS}") target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) if(${Launcher_USE_PCH}) target_precompile_headers(Launcher_logic PRIVATE ${PRECOMPILED_HEADERS}) endif() target_link_libraries(Launcher_logic systeminfo Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} qdcss BuildConfig Qt${QT_VERSION_MAJOR}::Widgets ) if(TARGET PkgConfig::libqrencode) target_link_libraries(Launcher_logic PkgConfig::libqrencode) else() target_include_directories(Launcher_logic PRIVATE ${LIBQRENCODE_INCLUDE_DIR}) target_link_libraries(Launcher_logic ${LIBQRENCODE_LIBRARIES}) endif() if(TARGET PkgConfig::tomlplusplus) target_link_libraries(Launcher_logic PkgConfig::tomlplusplus) else() target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus) endif() if(TARGET PkgConfig::libarchive) target_link_libraries(Launcher_logic PkgConfig::libarchive) else() target_link_libraries(Launcher_logic LibArchive::LibArchive) endif() if (CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_libraries(Launcher_logic gamemode ) endif() target_link_libraries(Launcher_logic Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Xml Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::NetworkAuth Qt${QT_VERSION_MAJOR}::OpenGL ${Launcher_QT_DBUS} ${Launcher_QT_LIBS} ) target_link_libraries(Launcher_logic cmark::cmark LocalPeer Launcher_rainbow ) if (TARGET ${Launcher_QT_DBUS}) add_compile_definitions(WITH_QTDBUS) endif() if(APPLE) set(CMAKE_MACOSX_RPATH 1) set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") if(Launcher_ENABLE_UPDATER) file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") add_compile_definitions(SPARKLE_ENABLED) endif() target_link_libraries(Launcher_logic "-framework AppKit" "-framework Carbon" "-framework Foundation" "-framework ApplicationServices" ) if(Launcher_ENABLE_UPDATER) target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) endif() endif() target_link_libraries(Launcher_logic) add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) if(${Launcher_USE_PCH}) target_precompile_headers(${Launcher_Name} REUSE_FROM Launcher_logic) endif() target_link_libraries(${Launcher_Name} Launcher_logic) if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties(${Launcher_Name} PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}") endif() if(DEFINED Launcher_BINARY_RPATH) SET_TARGET_PROPERTIES(${Launcher_Name} PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") endif() if(DEFINED Launcher_APP_BINARY_DEFS) target_compile_definitions(${Launcher_Name} PRIVATE ${Launcher_APP_BINARY_DEFS}) target_compile_definitions(Launcher_logic PRIVATE ${Launcher_APP_BINARY_DEFS}) endif() install(TARGETS ${Launcher_Name} RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) # Deploy PDBs if(CMAKE_CXX_LINKER_SUPPORTS_PDB) install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() if(Launcher_BUILD_UPDATER) # Updater add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) if(${Launcher_USE_PCH}) target_precompile_headers(prism_updater_logic PRIVATE ${PRECOMPILED_HEADERS}) endif() target_link_libraries(prism_updater_logic ${ZLIB_LIBRARIES} systeminfo BuildConfig Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network ${Launcher_QT_LIBS} cmark::cmark ) if(TARGET PkgConfig::libarchive) target_link_libraries(prism_updater_logic PkgConfig::libarchive) else() target_link_libraries(prism_updater_logic LibArchive::LibArchive) endif() add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) if(${Launcher_USE_PCH}) target_precompile_headers("${Launcher_Name}_updater" REUSE_FROM prism_updater_logic) endif() if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater") endif() if(DEFINED Launcher_BINARY_RPATH) SET_TARGET_PROPERTIES("${Launcher_Name}_updater" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") endif() install(TARGETS "${Launcher_Name}_updater" BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) # Deploy PDBs if(CMAKE_CXX_LINKER_SUPPORTS_PDB) install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() endif() if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) # File link add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) set_project_warnings(filelink_logic "${Launcher_MSVC_WARNINGS}" "${Launcher_CLANG_WARNINGS}" "${Launcher_GCC_WARNINGS}") target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) if(${Launcher_USE_PCH}) target_precompile_headers(filelink_logic PRIVATE ${PRECOMPILED_HEADERS}) endif() target_link_libraries(filelink_logic systeminfo BuildConfig Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network # Qt${QT_VERSION_MAJOR}::Concurrent ${Launcher_QT_LIBS} ) add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp) target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) if(${Launcher_USE_PCH}) target_precompile_headers("${Launcher_Name}_filelink" REUSE_FROM filelink_logic) endif() # HACK: Fix manifest issues with Ninja in release mode (and only release mode) and MSVC # I have no idea why this works or why it's needed. UPDATE THIS IF YOU EDIT THE MANIFEST!!! -@getchoo # Thank you 2018 CMake mailing list thread https://cmake.cmake.narkive.com/LnotZXus/conflicting-msvc-manifests if(MSVC) set_property(TARGET "${Launcher_Name}_filelink" PROPERTY LINK_FLAGS "/MANIFESTUAC:level='requireAdministrator'") endif() target_link_libraries("${Launcher_Name}_filelink" filelink_logic) if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_filelink" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_filelink") endif() if(DEFINED Launcher_BINARY_RPATH) SET_TARGET_PROPERTIES("${Launcher_Name}_filelink" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") endif() install(TARGETS "${Launcher_Name}_filelink" BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) # Deploy PDBs if(CMAKE_CXX_LINKER_SUPPORTS_PDB) install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() endif() if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) # Add Sparkle updater # It has to be copied here instead of just allowing fixup_bundle to install it, otherwise essential parts of # the framework aren't installed install(DIRECTORY ${MACOSX_SPARKLE_DIR}/Sparkle.framework DESTINATION ${FRAMEWORK_DEST_DIR} USE_SOURCE_PERMISSIONS) endif() #### The bundle mess! #### # Bundle utilities are used to complete packages for different platforms - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. if(WIN32 OR (UNIX AND APPLE)) if(WIN32) set(QT_DEPLOY_TOOL_OPTIONS "--no-opengl-sw --no-quick-import --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types generic,networkinformation") endif() qt_generate_deploy_script( TARGET ${Launcher_Name} OUTPUT_SCRIPT QT_DEPLOY_SCRIPT CONTENT " qt_deploy_runtime_dependencies( EXECUTABLE ${BINARY_DEST_DIR}/$ BIN_DIR ${BINARY_DEST_DIR} LIBEXEC_DIR ${LIBRARY_DEST_DIR} LIB_DIR ${LIBRARY_DEST_DIR} PLUGINS_DIR ${PLUGIN_DEST_DIR} NO_OVERWRITE NO_TRANSLATIONS NO_COMPILER_RUNTIME DEPLOY_TOOL_OPTIONS ${QT_DEPLOY_TOOL_OPTIONS} )" ) # Bundle our linked dependencies install( RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET COMPONENT bundle DIRECTORIES ${CMAKE_SYSTEM_LIBRARY_PATH} ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} PRE_EXCLUDE_REGEXES "^(api-ms-win|ext-ms)-.*\\.dll$" # FIXME: Why aren't these caught by the below regex??? "^azure.*\\.dll$" "^vcruntime.*\\.dll$" POST_EXCLUDE_REGEXES "system32" LIBRARY DESTINATION ${LIBRARY_DEST_DIR} RUNTIME DESTINATION ${BINARY_DEST_DIR} FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} ) # Deploy Qt plugins install( SCRIPT ${QT_DEPLOY_SCRIPT} COMPONENT bundle ) # FIXME: remove this crap once we stop using msys2 if(MINGW) # i've not found a solution better than injecting the config vars like this... # with install(CODE" for everything everything just breaks install(CODE " set(QT_PLUGINS_DIR \"${QT_PLUGINS_DIR}\") set(QT_LIBS_DIR \"${QT_LIBS_DIR}\") set(QT_LIBEXECS_DIR \"${QT_LIBEXECS_DIR}\") set(CMAKE_SYSTEM_LIBRARY_PATH \"${CMAKE_SYSTEM_LIBRARY_PATH}\") set(CMAKE_INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX}\") " COMPONENT bundle) install(CODE [[ file(GLOB QT_IMAGEFORMAT_DLLS "${QT_PLUGINS_DIR}/imageformats/*.dll") set(CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL objdump) file(GET_RUNTIME_DEPENDENCIES RESOLVED_DEPENDENCIES_VAR imageformatdeps LIBRARIES ${QT_IMAGEFORMAT_DLLS} DIRECTORIES ${CMAKE_SYSTEM_LIBRARY_PATH} ${QT_PLUGINS_DIR} ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} PRE_EXCLUDE_REGEXES "^(api-ms-win|ext-ms)-.*\\.dll$" # FIXME: Why aren't these caught by the below regex??? "^azure.*\\.dll$" "^vcruntime.*\\.dll$" POST_EXCLUDE_REGEXES "system32" ) foreach(_lib ${imageformatdeps}) file(INSTALL DESTINATION ${CMAKE_INSTALL_PREFIX} TYPE SHARED_LIBRARY FOLLOW_SYMLINK_CHAIN FILES ${_lib} ) endforeach() ]] COMPONENT bundle) endif() # Add qt.conf - this makes Qt stop looking for things outside the bundle install( CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" COMPONENT bundle ) # Add qtlogging.ini as a config file install( FILES "qtlogging.ini" DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} COMPONENT bundle ) endif() find_program(CLANG_FORMAT clang-format OPTIONAL) if(CLANG_FORMAT) message(STATUS "Creating clang-format target") add_custom_target( clang-format COMMAND ${CLANG_FORMAT} -i --style=file:${CMAKE_SOURCE_DIR}/.clang-format ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${PRISMUPDATER_SOURCES} ${LINKEXE_SOURCES} ${PRECOMPILED_HEADERS} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} ) else() message(WARNING "Unable to find `clang-format`. Not creating custom target") endif() PrismLauncher-10.0.5/launcher/InstanceCreationTask.cpp0000644000175100017510000000326615144136756022363 0ustar runnerrunner#include "InstanceCreationTask.h" #include #include #include "FileSystem.h" void InstanceCreationTask::executeTask() { setAbortable(true); if (updateInstance()) { emitSucceeded(); return; } // When the user aborted in the update stage. if (m_abort) { emitAborted(); return; } if (!createInstance()) { if (m_abort) return; qWarning() << "Instance creation failed!"; if (!m_error_message.isEmpty()) { qWarning() << "Reason:" << m_error_message; emitFailed(tr("Error while creating new instance:\n%1").arg(m_error_message)); } else { emitFailed(tr("Error while creating new instance.")); } return; } // If this is set, it means we're updating an instance. So, we now need to remove the // files scheduled to, and we'd better not let the user abort in the middle of it, since it'd // put the instance in an invalid state. if (shouldOverride()) { bool deleteFailed = false; setAbortable(false); setStatus(tr("Removing old conflicting files...")); qDebug() << "Removing old files"; for (const QString& path : m_files_to_remove) { if (!QFile::exists(path)) continue; qDebug() << "Removing" << path; if (!QFile::remove(path)) { qCritical() << "Could not remove" << path; deleteFailed = true; } } if (deleteFailed) { emitFailed(tr("Failed to remove old conflicting files.")); return; } } if (!m_abort) emitSucceeded(); } PrismLauncher-10.0.5/launcher/Exception.h0000644000175100017510000000402615144136756017705 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include class Exception : public std::exception { public: Exception(const QString& message) : std::exception(), m_message(message.toUtf8()) { qCritical() << "Exception:" << message; } Exception(const Exception& other) : std::exception(), m_message(other.m_message) {} virtual ~Exception() noexcept {} const char* what() const noexcept { return m_message.constData(); } QString cause() const { return QString::fromUtf8(m_message); } private: QByteArray m_message; }; PrismLauncher-10.0.5/launcher/MangoHud.h0000644000175100017510000000162715144136756017455 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PrismLauncher - Minecraft Launcher * Copyright (C) 2022 Jan Drögehoff * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include namespace MangoHud { QString getLibraryString(); QString findLibrary(QString libName); } // namespace MangoHud PrismLauncher-10.0.5/launcher/SysInfo.h0000644000175100017510000000027415144136756017342 0ustar runnerrunner#pragma once #include namespace SysInfo { QString currentSystem(); QString useQTForArch(); QString getSupportedJavaArchitecture(); int suitableMaxMem(); } // namespace SysInfo PrismLauncher-10.0.5/launcher/DesktopServices.h0000644000175100017510000000206215144136756021062 0ustar runnerrunner#pragma once #include #include class QFileInfo; /** * This wraps around QDesktopServices and adds workarounds where needed * Use this instead of QDesktopServices! */ namespace DesktopServices { /** * Open a path in whatever application is applicable. * @param ensureFolderPathExists Make sure the path exists */ bool openPath(const QFileInfo& path, bool ensureFolderPathExists = false); /** * Open a path in whatever application is applicable. * @param ensureFolderPathExists Make sure the path exists */ bool openPath(const QString& path, bool ensureFolderPathExists = false); /** * Run an application */ bool run(const QString& application, const QStringList& args, const QString& workingDirectory = QString(), qint64* pid = 0); /** * Open the URL, most likely in a browser. Maybe. */ bool openUrl(const QUrl& url); /** * Determine whether the launcher is running in a Flatpak environment */ bool isFlatpak(); /** * Determine whether the launcher is running in a Snap environment */ bool isSnap(); } // namespace DesktopServices PrismLauncher-10.0.5/launcher/InstanceImportTask.h0000644000175100017510000000514015144136756021527 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "InstanceTask.h" class InstanceImportTask : public InstanceTask { Q_OBJECT public: explicit InstanceImportTask(const QUrl& sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {}); virtual ~InstanceImportTask() = default; bool abort() override; protected: //! Entry point for tasks. virtual void executeTask() override; private: void processMultiMC(); void processTechnic(); void processFlame(); void processModrinth(); private slots: void processZipPack(); void extractFinished(); private: /* data */ QUrl m_sourceUrl; QString m_archivePath; Task::Ptr m_task; enum class ModpackType { Unknown, MultiMC, Technic, Flame, Modrinth, } m_modpackType = ModpackType::Unknown; // Extra info we might need, that's available before, but can't be derived from // the source URL / the resource it points to alone. QMap m_extra_info; // FIXME: nuke QWidget* m_parent; void downloadFromUrl(); }; PrismLauncher-10.0.5/launcher/updater/0000755000175100017510000000000015144136757017241 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/updater/MacSparkleUpdater.h0000644000175100017510000001061015144136757022757 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Kenneth Chew * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef LAUNCHER_MACSPARKLEUPDATER_H #define LAUNCHER_MACSPARKLEUPDATER_H #include #include #include "ExternalUpdater.h" /*! * An implementation for the updater on macOS that uses the Sparkle framework. */ class MacSparkleUpdater : public ExternalUpdater { Q_OBJECT public: /*! * Start the Sparkle updater, which automatically checks for updates if necessary. */ MacSparkleUpdater(); ~MacSparkleUpdater() override; /*! * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ void checkForUpdates() override; /*! * Indicates whether or not to check for updates automatically. */ bool getAutomaticallyChecksForUpdates() override; /*! * Indicates the current automatic update check interval in seconds. */ double getUpdateCheckInterval() override; /*! * Indicates the set of Sparkle channels the updater is allowed to find new updates from. */ QSet getAllowedChannels(); /*! * Indicates whether or not beta updates should be checked for in addition to regular releases. */ bool getBetaAllowed() override; /*! * Set whether or not to check for updates automatically. * * As per Sparkle documentation, "By default, Sparkle asks users on second launch for permission if they want * automatic update checks enabled and sets this property based on their response. If SUEnableAutomaticChecks is * set in the Info.plist, this permission request is not performed however. * * Setting this property will persist in the host bundle’s user defaults. Only set this property if you need * dynamic behavior (e.g. user preferences). * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately." */ void setAutomaticallyChecksForUpdates(bool check) override; /*! * Set the current automatic update check interval in seconds. * * As per Sparkle documentation, "Setting this property will persist in the host bundle’s user defaults. For this * reason, only set this property if you need dynamic behavior (eg user preferences). Otherwise prefer to set * SUScheduledCheckInterval directly in your Info.plist. * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately." */ void setUpdateCheckInterval(double seconds) override; /*! * Clears all allowed Sparkle channels, returning to the default updater channel behavior. */ void clearAllowedChannels(); /*! * Set a single Sparkle channel the updater is allowed to find new updates from. * * Items in the default channel can always be found, regardless of this setting. If an empty string is passed, * return to the default behavior. */ void setAllowedChannel(const QString& channel); /*! * Set a set of Sparkle channels the updater is allowed to find new updates from. * * Items in the default channel can always be found, regardless of this setting. If an empty set is passed, * return to the default behavior. */ void setAllowedChannels(const QSet& channels); /*! * Set whether or not beta updates should be checked for in addition to regular releases. */ void setBetaAllowed(bool allowed) override; private: class Private; Private* priv; }; #endif // LAUNCHER_MACSPARKLEUPDATER_H PrismLauncher-10.0.5/launcher/updater/ExternalUpdater.h0000644000175100017510000000602515144136757022524 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Kenneth Chew * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef LAUNCHER_EXTERNALUPDATER_H #define LAUNCHER_EXTERNALUPDATER_H #include /*! * A base class for an updater that uses an external library. * This class contains basic functions to control the updater. * * To implement the updater on a new platform, create a new class that inherits from this class and * implement the pure virtual functions. * * The initializer of the new class should have the side effect of starting the automatic updater. That is, * once the class is initialized, the program should automatically check for updates if necessary. */ class ExternalUpdater : public QObject { Q_OBJECT public: /*! * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ virtual void checkForUpdates() = 0; /*! * Indicates whether or not to check for updates automatically. */ virtual bool getAutomaticallyChecksForUpdates() = 0; /*! * Indicates the current automatic update check interval in seconds. */ virtual double getUpdateCheckInterval() = 0; /*! * Indicates whether or not beta updates should be checked for in addition to regular releases. */ virtual bool getBetaAllowed() = 0; /*! * Set whether or not to check for updates automatically. */ virtual void setAutomaticallyChecksForUpdates(bool check) = 0; /*! * Set the current automatic update check interval in seconds. */ virtual void setUpdateCheckInterval(double seconds) = 0; /*! * Set whether or not beta updates should be checked for in addition to regular releases. */ virtual void setBetaAllowed(bool allowed) = 0; signals: /*! * Emits whenever the user's ability to check for updates changes. * * As per Sparkle documentation, "An update check can be made by the user when an update session isn’t in progress, * or when an update or its progress is being shown to the user. A user cannot check for updates when data (such * as the feed or an update) is still being downloaded automatically in the background. * * This property is suitable to use for menu item validation for seeing if checkForUpdates can be invoked." */ void canCheckForUpdatesChanged(bool canCheck); }; #endif // LAUNCHER_EXTERNALUPDATER_H PrismLauncher-10.0.5/launcher/updater/PrismExternalUpdater.h0000644000175100017510000000600615144136757023536 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include "ExternalUpdater.h" /*! * An implementation for the updater on windows and linux that uses out external updater. */ class PrismExternalUpdater : public ExternalUpdater { Q_OBJECT public: PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir); ~PrismExternalUpdater() override; /*! * Check for updates manually, showing the user a progress bar and an alert if no updates are found. */ void checkForUpdates() override; void checkForUpdates(bool triggeredByUser); /*! * Indicates whether or not to check for updates automatically. */ bool getAutomaticallyChecksForUpdates() override; /*! * Indicates the current automatic update check interval in seconds. */ double getUpdateCheckInterval() override; /*! * Indicates whether or not beta updates should be checked for in addition to regular releases. */ bool getBetaAllowed() override; /*! * Set whether or not to check for updates automatically. * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately." */ void setAutomaticallyChecksForUpdates(bool check) override; /*! * Set the current automatic update check interval in seconds. * * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow * reverting this property without kicking off a schedule change immediately." */ void setUpdateCheckInterval(double seconds) override; /*! * Set whether or not beta updates should be checked for in addition to regular releases. */ void setBetaAllowed(bool allowed) override; void resetAutoCheckTimer(); void disconnectTimer(); void connectTimer(); void offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes); void performUpdate(const QString& version_tag); public slots: void autoCheckTimerFired(); private: class Private; Private* priv; }; PrismLauncher-10.0.5/launcher/updater/prismupdater/0000755000175100017510000000000015144136757021760 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/updater/prismupdater/updater.exe.manifest0000644000175100017510000000173615144136757025743 0ustar runnerrunner PrismLauncher-10.0.5/launcher/updater/prismupdater/SelectReleaseDialog.ui0000644000175100017510000000412415144136757026160 0ustar runnerrunner SelectReleaseDialog 0 0 468 385 Select Release to Install true Please select the release you wish to update to. true 1 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() SelectReleaseDialog accept() 248 254 157 274 buttonBox rejected() SelectReleaseDialog reject() 316 260 286 274 PrismLauncher-10.0.5/launcher/updater/prismupdater/UpdaterDialogs.cpp0000644000175100017510000001361115144136757025375 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "UpdaterDialogs.h" #include "ui_SelectReleaseDialog.h" #include #include #include "Markdown.h" #include "StringUtils.h" SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList& releases, QWidget* parent) : QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog) { ui->setupUi(this); ui->changelogTextBrowser->setOpenExternalLinks(true); ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); ui->versionsTree->setColumnCount(2); ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") }); ui->versionsTree->header()->setStretchLastSection(false); ui->eplainLabel->setText(tr("Select a version to install.\n" "\n" "Currently installed version: %1") .arg(m_currentVersion.toString())); loadReleases(); connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseDialog::selectionChanged); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseDialog::reject); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } SelectReleaseDialog::~SelectReleaseDialog() { delete ui; } void SelectReleaseDialog::loadReleases() { for (auto rls : m_releases) { appendRelease(rls); } } void SelectReleaseDialog::appendRelease(GitHubRelease const& release) { auto rls_item = new QTreeWidgetItem(ui->versionsTree); rls_item->setText(0, release.tag_name); rls_item->setExpanded(true); rls_item->setText(1, release.published_at.toString()); rls_item->setData(0, Qt::UserRole, QVariant(release.id)); ui->versionsTree->addTopLevelItem(rls_item); } GitHubRelease SelectReleaseDialog::getRelease(QTreeWidgetItem* item) { int id = item->data(0, Qt::UserRole).toInt(); GitHubRelease release; for (auto rls : m_releases) { if (rls.id == id) release = rls; } return release; } void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) { GitHubRelease release = getRelease(current); QString body = markdownToHTML(release.body.toUtf8()); m_selectedRelease = release; ui->changelogTextBrowser->setHtml(StringUtils::htmlListPatch(body)); } SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList& assets, QWidget* parent) : QDialog(parent), m_assets(assets), ui(new Ui::SelectReleaseDialog) { ui->setupUi(this); ui->changelogTextBrowser->setOpenExternalLinks(true); ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); ui->versionsTree->setColumnCount(2); ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") }); ui->versionsTree->header()->setStretchLastSection(false); ui->eplainLabel->setText(tr("Select a version to install.")); ui->changelogTextBrowser->setHidden(true); loadAssets(); connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseAssetDialog::selectionChanged); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseAssetDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseAssetDialog::reject); } SelectReleaseAssetDialog::~SelectReleaseAssetDialog() { delete ui; } void SelectReleaseAssetDialog::loadAssets() { for (auto rls : m_assets) { appendAsset(rls); } } void SelectReleaseAssetDialog::appendAsset(GitHubReleaseAsset const& asset) { auto rls_item = new QTreeWidgetItem(ui->versionsTree); rls_item->setText(0, asset.name); rls_item->setExpanded(true); rls_item->setText(1, asset.updated_at.toString()); rls_item->setData(0, Qt::UserRole, QVariant(asset.id)); ui->versionsTree->addTopLevelItem(rls_item); } GitHubReleaseAsset SelectReleaseAssetDialog::getAsset(QTreeWidgetItem* item) { int id = item->data(0, Qt::UserRole).toInt(); GitHubReleaseAsset selected_asset; for (auto asset : m_assets) { if (asset.id == id) selected_asset = asset; } return selected_asset; } void SelectReleaseAssetDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) { GitHubReleaseAsset asset = getAsset(current); m_selectedAsset = asset; } PrismLauncher-10.0.5/launcher/updater/prismupdater/updater_main.cpp0000644000175100017510000000240515144136757025135 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "PrismUpdater.h" int main(int argc, char* argv[]) { PrismUpdaterApp wUpApp(argc, argv); switch (wUpApp.status()) { case PrismUpdaterApp::Starting: case PrismUpdaterApp::Initialized: { return wUpApp.exec(); } case PrismUpdaterApp::Failed: return 1; case PrismUpdaterApp::Succeeded: return 0; default: return -1; } } PrismLauncher-10.0.5/launcher/updater/prismupdater/PrismUpdater.h0000644000175100017510000000776715144136757024571 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include "QObjectPtr.h" #include "net/Download.h" #define PRISM_EXTERNAL_EXE #include "FileSystem.h" #include "GitHubRelease.h" class PrismUpdaterApp : public QApplication { // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: enum Status { Starting, Failed, Succeeded, Initialized, Aborted }; PrismUpdaterApp(int& argc, char** argv); virtual ~PrismUpdaterApp(); void loadReleaseList(); void run(); Status status() const { return m_status; } private: void fail(const QString& reason); void abort(const QString& reason); void showFatalErrorMessage(const QString& title, const QString& content); bool loadPrismVersionFromExe(const QString& exe_path); void downloadReleasePage(const QString& api_url, int page); int parseReleasePage(const QByteArray* response); bool needUpdate(const GitHubRelease& release); GitHubRelease getLatestRelease(); GitHubRelease selectRelease(); QList newerReleases(); QList nonDraftReleases(); void printReleases(); QList validReleaseArtifacts(const GitHubRelease& release); GitHubReleaseAsset selectAsset(const QList& assets); void performUpdate(const GitHubRelease& release); void performInstall(QFileInfo file); void unpackAndInstall(QFileInfo file); void backupAppDir(); std::optional unpackArchive(QFileInfo file); QFileInfo downloadAsset(const GitHubReleaseAsset& asset); bool callAppImageUpdate(); void moveAndFinishUpdate(QDir target); public slots: void downloadError(QString reason); private: const QString& root() { return m_rootPath; } bool isPortable() { return m_isPortable; } void clearUpdateLog(); void logUpdate(const QString& msg); QString m_rootPath; QString m_dataPath; bool m_isPortable = false; bool m_isAppimage = false; bool m_isFlatpak = false; QString m_appimagePath; QString m_prismExecutable; QUrl m_prismRepoUrl; Version m_userSelectedVersion; bool m_checkOnly; bool m_forceUpdate; bool m_printOnly; bool m_selectUI; bool m_allowDowngrade; bool m_allowPreRelease; QString m_updateLogPath; QString m_prismBinaryName; QString m_prismVersion; int m_prismVersionMajor = -1; int m_prismVersionMinor = -1; int m_prismVersionPatch = -1; QString m_prsimVersionChannel; QString m_prismGitCommit; GitHubRelease m_install_release; Status m_status = Status::Starting; shared_qobject_ptr m_network; QString m_current_url; Task::Ptr m_current_task; QList m_releases; public: std::unique_ptr logFile; bool logToConsole = false; #if defined Q_OS_WIN32 // used on Windows to attach the standard IO streams bool consoleAttached = false; #endif }; PrismLauncher-10.0.5/launcher/updater/prismupdater/GitHubRelease.h0000644000175100017510000000314315144136757024615 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include #include #include "Version.h" struct GitHubReleaseAsset { int id = -1; QString name; QString label; QString content_type; int size; QDateTime created_at; QDateTime updated_at; QString browser_download_url; bool isValid() { return id > 0; } }; struct GitHubRelease { int id = -1; QString name; QString tag_name; QDateTime created_at; QDateTime published_at; bool prerelease; bool draft; QString body; QList assets; Version version; bool isValid() const { return id > 0; } }; QDebug operator<<(QDebug debug, const GitHubReleaseAsset& rls); QDebug operator<<(QDebug debug, const GitHubRelease& rls); PrismLauncher-10.0.5/launcher/updater/prismupdater/GitHubRelease.cpp0000644000175100017510000000575515144136757025163 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "GitHubRelease.h" QDebug operator<<(QDebug debug, const GitHubReleaseAsset& asset) { QDebugStateSaver saver(debug); debug.nospace() << "GitHubReleaseAsset( " "id: " << asset.id << ", " "name " << asset.name << ", " "label: " << asset.label << ", " "content_type: " << asset.content_type << ", " "size: " << asset.size << ", " "created_at: " << asset.created_at << ", " "updated_at: " << asset.updated_at << ", " "browser_download_url: " << asset.browser_download_url << " " ")"; return debug; } QDebug operator<<(QDebug debug, const GitHubRelease& rls) { QDebugStateSaver saver(debug); debug.nospace() << "GitHubRelease( " "id: " << rls.id << ", " "name " << rls.name << ", " "tag_name: " << rls.tag_name << ", " "created_at: " << rls.created_at << ", " "published_at: " << rls.published_at << ", " "prerelease: " << rls.prerelease << ", " "draft: " << rls.draft << ", " "version" << rls.version << ", " "body: " << rls.body << ", " "assets: " << rls.assets << " " ")"; return debug; } PrismLauncher-10.0.5/launcher/updater/prismupdater/PrismUpdater.cpp0000644000175100017510000013672015144136757025114 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "PrismUpdater.h" #include "BuildConfig.h" #include "ui/dialogs/ProgressDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include "console/WindowsConsole.h" #endif #include namespace fs = std::filesystem; #include "DesktopServices.h" #include "updater/prismupdater/UpdaterDialogs.h" #include "FileSystem.h" #include "Json.h" #include "StringUtils.h" #include "net/Download.h" #include "net/RawHeaderProxy.h" #include "MMCZip.h" /** output to the log file */ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { static std::mutex loggerMutex; const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe QString out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; PrismUpdaterApp* app = static_cast(QCoreApplication::instance()); app->logFile->write(out.toUtf8()); app->logFile->flush(); if (app->logToConsole) { QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } } PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 // attach the parent console if stdout not already captured if (AttachWindowsConsole()) { consoleAttached = true; } #endif setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME + "Updater"); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); // Command line parsing QCommandLineParser parser; parser.setApplicationDescription(QObject::tr("An auto-updater for Prism Launcher")); parser.addOptions( { { { "d", "dir" }, tr("Use a custom path as application root (use '.' for current directory)."), tr("directory") }, { { "V", "prism-version" }, tr("Use this version as the installed launcher version. (provided because stdout can not be reliably captured on windows)"), tr("installed launcher version") }, { { "I", "install-version" }, "Install a specific version.", tr("version name") }, { { "U", "update-url" }, tr("Update from the specified repo."), tr("github repo url") }, { { "c", "check-only" }, tr("Only check if an update is needed. Exit status 100 if true, 0 if false (or non 0 if there was an error).") }, { { "p", "pre-release" }, tr("Allow updating to pre-release releases") }, { { "F", "force" }, tr("Force an update, even if one is not needed.") }, { { "l", "list" }, tr("List available releases.") }, { "debug", tr("Log debug to console.") }, { { "S", "select-ui" }, tr("Select the version to install with a GUI.") }, { { "D", "allow-downgrade" }, tr("Allow the updater to downgrade to previous versions.") } }); parser.addHelpOption(); parser.addVersionOption(); parser.process(arguments()); logToConsole = parser.isSet("debug"); QString origCwdPath = QDir::currentPath(); QString binPath = applicationDirPath(); { // find data director // Root path is used for updates and portable data #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr m_rootPath = foo.absolutePath(); #elif defined(Q_OS_WIN32) m_rootPath = binPath; #elif defined(Q_OS_MAC) QDir foo(FS::PathCombine(binPath, "../..")); m_rootPath = foo.absolutePath(); // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) FS::updateTimestamp(m_rootPath); #endif } QString adjustedBy; // change folder QString dirParam = parser.value("dir"); if (!dirParam.isEmpty()) { // the dir param. it makes prism launcher data path point to whatever the user specified // on command line adjustedBy = "Command line"; m_dataPath = dirParam; #ifndef Q_OS_MACOS if (QDir(FS::PathCombine(m_rootPath, "UserData")).exists()) { m_isPortable = true; } if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { m_isPortable = true; } #endif } else if (auto dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); !dataDirEnv.isEmpty()) { adjustedBy = "System environment"; m_dataPath = dataDirEnv; #ifndef Q_OS_MACOS if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { m_isPortable = true; } #endif } else { QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); m_dataPath = foo.absolutePath(); adjustedBy = "Persistent data path"; #ifndef Q_OS_MACOS if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) { m_dataPath = portableUserData; adjustedBy = "Portable user data path"; m_isPortable = true; } else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { m_dataPath = m_rootPath; adjustedBy = "Portable data path"; m_isPortable = true; } #endif } m_updateLogPath = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log"); { // setup logging FS::ensureFolderPathExists(FS::PathCombine(m_dataPath, "logs")); static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log"; static const QString logBase = FS::PathCombine(m_dataPath, "logs", baseLogFile); if (FS::ensureFolderPathExists("logs")) { // enough history to track both launches of the updater during a portable install FS::move(logBase.arg(1), logBase.arg(2)); FS::move(logBase.arg(0), logBase.arg(1)); } logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { showFatalErrorMessage(tr("The launcher data folder is not writable!"), tr("The updater couldn't create a log file - the data folder is not writable.\n" "\n" "Make sure you have write permissions to the data folder.\n" "(%1)\n" "\n" "The updater cannot continue until you fix this problem.") .arg(m_dataPath)); return; } qInstallMessageHandler(appDebugOutput); qSetMessagePattern( "%{time process}" " " "%{if-debug}D%{endif}" "%{if-info}I%{endif}" "%{if-warning}W%{endif}" "%{if-critical}C%{endif}" "%{if-fatal}F%{endif}" " " "|" " " "%{if-category}[%{category}]: %{endif}" "%{message}"); bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesPath = FS::PathCombine(m_dataPath, logRulesFile); qDebug() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); // search the dataPath() // seach app data standard path if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { qDebug() << "Found" << logRulesPath << "..."; foundLoggingRules = true; } } // seach root path if (!foundLoggingRules) { logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); qDebug() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } if (foundLoggingRules) { // load and set logging rules qDebug() << "Loading logging rules from:" << logRulesPath; QSettings loggingRules(logRulesPath, QSettings::IniFormat); loggingRules.beginGroup("Rules"); QStringList rule_names = loggingRules.childKeys(); QStringList rules; qDebug() << "Setting log rules:"; for (auto rule_name : rule_names) { auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); rules.append(rule); qDebug() << " " << rule; } auto rules_str = rules.join("\n"); QLoggingCategory::setFilterRules(rules_str); } qDebug() << "<> Log initialized."; } { // log debug program info qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + " Updater, " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); qDebug() << "Version :" << BuildConfig.printableVersionString(); qDebug() << "Git commit :" << BuildConfig.GIT_COMMIT; qDebug() << "Git refspec :" << BuildConfig.GIT_REFSPEC; qDebug() << "Compiled for :" << BuildConfig.systemID(); qDebug() << "Compiled by :" << BuildConfig.compilerID(); qDebug() << "Build Artifact :" << BuildConfig.BUILD_ARTIFACT; if (adjustedBy.size()) { qDebug() << "Data dir before adjustment :" << origCwdPath; qDebug() << "Data dir after adjustment :" << m_dataPath; qDebug() << "Adjusted by :" << adjustedBy; } else { qDebug() << "Data dir :" << QDir::currentPath(); } qDebug() << "Work dir :" << QDir::currentPath(); qDebug() << "Binary path :" << binPath; qDebug() << "Application root path :" << m_rootPath; qDebug() << "Portable install :" << m_isPortable; qDebug() << "<> Paths set."; } { // network m_network = makeShared(new QNetworkAccessManager()); qDebug() << "Detecting proxy settings..."; QNetworkProxy proxy = QNetworkProxy::applicationProxy(); m_network->setProxy(proxy); } #ifdef Q_OS_MACOS showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); #endif if (binPath.startsWith("/tmp/.mount_")) { m_isAppimage = true; m_appimagePath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); if (m_appimagePath.isEmpty()) { showFatalErrorMessage(tr("Unsupported Installation"), tr("Updater is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); } } m_isFlatpak = DesktopServices::isFlatpak(); QString prism_executable = FS::PathCombine(binPath, BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 prism_executable.append(".exe"); #endif if (!QFileInfo(prism_executable).isFile()) { showFatalErrorMessage(tr("Unsupported Installation"), tr("The updater can not find the main executable.")); } m_prismExecutable = prism_executable; auto prism_update_url = parser.value("update-url"); if (prism_update_url.isEmpty()) prism_update_url = BuildConfig.UPDATER_GITHUB_REPO; m_prismRepoUrl = QUrl::fromUserInput(prism_update_url); m_checkOnly = parser.isSet("check-only"); m_forceUpdate = parser.isSet("force"); m_printOnly = parser.isSet("list"); auto user_version = parser.value("install-version"); if (!user_version.isEmpty()) { m_userSelectedVersion = Version(user_version); } m_selectUI = parser.isSet("select-ui"); m_allowDowngrade = parser.isSet("allow-downgrade"); auto version = parser.value("prism-version"); if (!version.isEmpty()) { if (version.contains('-')) { auto index = version.indexOf('-'); m_prsimVersionChannel = version.mid(index + 1); version = version.left(index); } else { m_prsimVersionChannel = "stable"; } auto version_parts = version.split('.'); m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); if (!version_parts.isEmpty()) m_prismVersionPatch = version_parts.takeFirst().toInt(); else m_prismVersionPatch = 0; } m_allowPreRelease = parser.isSet("pre-release"); auto marker_file_path = QDir(m_rootPath).absoluteFilePath(".prism_launcher_updater_unpack.marker"); auto marker_file = QFileInfo(marker_file_path); if (marker_file.exists()) { auto target_dir = QString(FS::read(marker_file_path)).trimmed(); if (target_dir.isEmpty()) { qWarning() << "Empty updater marker file contains no install target. making best guess of parent dir"; target_dir = QDir(m_rootPath).absoluteFilePath(".."); } QMetaObject::invokeMethod(this, [this, target_dir]() { moveAndFinishUpdate(target_dir); }, Qt::QueuedConnection); } else { QMetaObject::invokeMethod(this, &PrismUpdaterApp::loadReleaseList, Qt::QueuedConnection); } } PrismUpdaterApp::~PrismUpdaterApp() { qDebug() << "updater shutting down"; // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); #if defined Q_OS_WIN32 // Detach from Windows console if (consoleAttached) { fclose(stdout); fclose(stdin); fclose(stderr); FreeConsole(); } #endif } void PrismUpdaterApp::fail(const QString& reason) { qCritical() << qPrintable(reason); m_status = Failed; exit(1); } void PrismUpdaterApp::abort(const QString& reason) { qCritical() << qPrintable(reason); m_status = Aborted; exit(2); } void PrismUpdaterApp::showFatalErrorMessage(const QString& title, const QString& content) { m_status = Failed; auto msgBox = new QMessageBox(); msgBox->setWindowTitle(title); msgBox->setText(content); msgBox->setStandardButtons(QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); msgBox->setIcon(QMessageBox::Critical); msgBox->setMinimumWidth(460); msgBox->adjustSize(); msgBox->exec(); exit(1); } void PrismUpdaterApp::run() { qDebug() << "found" << m_releases.length() << "releases on github"; qDebug() << "loading exe at" << m_prismExecutable; if (m_printOnly) { printReleases(); m_status = Succeeded; return exit(0); } if (!loadPrismVersionFromExe(m_prismExecutable)) { m_prismVersion = BuildConfig.printableVersionString(); m_prismVersionMajor = BuildConfig.VERSION_MAJOR; m_prismVersionMinor = BuildConfig.VERSION_MINOR; m_prismVersionPatch = BuildConfig.VERSION_PATCH; m_prsimVersionChannel = BuildConfig.VERSION_CHANNEL; m_prismGitCommit = BuildConfig.GIT_COMMIT; } m_status = Succeeded; qDebug() << "Executable reports as:" << m_prismBinaryName << "version:" << m_prismVersion; qDebug() << "Version major:" << m_prismVersionMajor; qDebug() << "Version minor:" << m_prismVersionMinor; qDebug() << "Version minor:" << m_prismVersionPatch; qDebug() << "Version channel:" << m_prsimVersionChannel; qDebug() << "Git Commit:" << m_prismGitCommit; auto latest = getLatestRelease(); qDebug() << "Latest release" << latest.version; auto need_update = needUpdate(latest); if (m_checkOnly) { if (need_update) { QTextStream stdOutStream(stdout); stdOutStream << "Name: " << latest.name << "\n"; stdOutStream << "Version: " << latest.tag_name << "\n"; stdOutStream << "TimeStamp: " << latest.created_at.toString(Qt::ISODate) << "\n"; stdOutStream << latest.body << "\n"; stdOutStream.flush(); return exit(100); } else { return exit(0); } } if (m_isFlatpak) { showFatalErrorMessage(tr("Updating flatpack not supported"), tr("Actions outside of checking if an update is available are not " "supported when running the flatpak version of Prism Launcher.")); return; } if (m_isAppimage) { bool result = true; if (need_update) result = callAppImageUpdate(); return exit(result ? 0 : 1); } if (need_update || m_forceUpdate || !m_userSelectedVersion.isEmpty()) { GitHubRelease update_release = latest; if (!m_userSelectedVersion.isEmpty()) { bool found = false; for (auto rls : m_releases) { if (rls.version == m_userSelectedVersion) { found = true; update_release = rls; break; } } if (!found) { showFatalErrorMessage( "No release for version!", QString("Can not find a github release for specified version %1").arg(m_userSelectedVersion.toString())); return; } } else if (m_selectUI) { update_release = selectRelease(); if (!update_release.isValid()) { showFatalErrorMessage("No version selected.", "No version was selected."); return; } } performUpdate(update_release); } exit(0); } void PrismUpdaterApp::moveAndFinishUpdate(QDir target) { logUpdate("Finishing update process"); logUpdate("Waiting 2 seconds for resources to free"); this->thread()->sleep(2); auto manifest_path = FS::PathCombine(m_rootPath, "manifest.txt"); QFileInfo manifest(manifest_path); auto app_dir = QDir(m_rootPath); QStringList file_list; if (manifest.isFile()) { // load manifest from file logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath())); try { auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath())); auto files = contents.split('\n'); for (auto file : files) { file_list.append(file.trimmed()); } } catch (FS::FileSystemException&) { } } if (file_list.isEmpty()) { logUpdate(tr("Manifest empty, making best guess of the directory contents of %1").arg(m_rootPath)); auto entries = target.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); for (auto entry : entries) { file_list.append(entry.fileName()); } } logUpdate(tr("Installing the following to %1 :\n %2").arg(target.absolutePath()).arg(file_list.join(",\n "))); bool error = false; QProgressDialog progress(tr("Installing from %1").arg(m_rootPath), "", 0, file_list.length()); progress.setCancelButton(nullptr); progress.setMinimumWidth(400); progress.adjustSize(); progress.show(); QCoreApplication::processEvents(); logUpdate(tr("Installing from %1").arg(m_rootPath)); auto copy = [this, app_dir, target](QString to_install_file) { auto rel_path = app_dir.relativeFilePath(to_install_file); auto install_path = FS::PathCombine(target.absolutePath(), rel_path); logUpdate(tr("Installing %1 from %2").arg(install_path).arg(to_install_file)); FS::ensureFilePathExists(install_path); auto result = FS::copy(to_install_file, install_path).overwrite(true)(); if (!result) { logUpdate(tr("Failed copy %1 to %2").arg(to_install_file).arg(install_path)); return true; } return false; }; int i = 0; for (auto glob : file_list) { QDirIterator iter(m_rootPath, QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); progress.setValue(i); QCoreApplication::processEvents(); if (!iter.hasNext() && !glob.isEmpty()) { if (auto file_info = QFileInfo(FS::PathCombine(m_rootPath, glob)); file_info.exists()) { error |= copy(file_info.absoluteFilePath()); } else { logUpdate(tr("File doesn't exist, ignoring: %1").arg(FS::PathCombine(m_rootPath, glob))); } } else { while (iter.hasNext()) { error |= copy(iter.next()); } } i++; } progress.setValue(i); QCoreApplication::processEvents(); if (error) { logUpdate(tr("There were errors installing the update.")); auto fail_marker = FS::PathCombine(m_dataPath, ".prism_launcher_update.fail"); FS::copy(m_updateLogPath, fail_marker).overwrite(true)(); } else { logUpdate(tr("Update succeed.")); auto success_marker = FS::PathCombine(m_dataPath, ".prism_launcher_update.success"); FS::copy(m_updateLogPath, success_marker).overwrite(true)(); } auto update_lock_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.lock"); FS::deletePath(update_lock_path); QProcess proc; auto app_exe_name = BuildConfig.LAUNCHER_APP_BINARY_NAME; #if defined Q_OS_WIN32 app_exe_name.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else app_exe_name.prepend("bin/"); #endif auto app_exe_path = target.absoluteFilePath(app_exe_name); proc.startDetached(app_exe_path); exit(error ? 1 : 0); } void PrismUpdaterApp::printReleases() { for (auto release : m_releases) { std::cout << release.name.toStdString() << " Version: " << release.tag_name.toStdString() << std::endl; } } QList PrismUpdaterApp::nonDraftReleases() { QList nonDraft; for (auto rls : m_releases) { if (rls.isValid() && !rls.draft) nonDraft.append(rls); } return nonDraft; } QList PrismUpdaterApp::newerReleases() { QList newer; for (auto rls : nonDraftReleases()) { if (rls.version > m_prismVersion) newer.append(rls); } return newer; } GitHubRelease PrismUpdaterApp::selectRelease() { QList releases; if (m_allowDowngrade) { releases = nonDraftReleases(); } else { releases = newerReleases(); } if (releases.isEmpty()) return {}; SelectReleaseDialog dlg(Version(m_prismVersion), releases); auto result = dlg.exec(); if (result == QDialog::Rejected) { return {}; } GitHubRelease release = dlg.selectedRelease(); return release; } QList PrismUpdaterApp::validReleaseArtifacts(const GitHubRelease& release) { QList valid; qDebug() << "Selecting best asset from" << release.tag_name << "for platform" << BuildConfig.BUILD_ARTIFACT << "portable:" << m_isPortable; if (BuildConfig.BUILD_ARTIFACT.isEmpty()) qWarning() << "Build platform is not set!"; for (auto asset : release.assets) { if (asset.name.endsWith(".zsync")) { qDebug() << "Rejecting zsync file" << asset.name; continue; } if (!m_isAppimage && asset.name.toLower().endsWith("appimage")) { qDebug() << "Rejecting" << asset.name << "because it is an AppImage"; continue; } else if (m_isAppimage && !asset.name.toLower().endsWith("appimage")) { qDebug() << "Rejecting" << asset.name << "because it is not an AppImage"; continue; } auto asset_name = asset.name.toLower(); auto [platform, platform_qt_ver] = StringUtils::splitFirst(BuildConfig.BUILD_ARTIFACT.toLower(), "-qt"); auto system_is_arm = QSysInfo::buildCpuArchitecture().contains("arm64"); auto asset_is_arm = asset_name.contains("arm64"); auto asset_is_archive = asset_name.endsWith(".zip") || asset_name.endsWith(".tar.gz"); bool for_platform = !platform.isEmpty() && asset_name.contains(platform); if (!for_platform) { qDebug() << "Rejecting" << asset.name << "because platforms do not match"; } bool for_portable = asset_name.contains("portable"); if (for_platform && asset_name.contains("legacy") && !platform.contains("legacy")) { qDebug() << "Rejecting" << asset.name << "because platforms do not match"; for_platform = false; } if (for_platform && ((asset_is_arm && !system_is_arm) || (!asset_is_arm && system_is_arm))) { qDebug() << "Rejecting" << asset.name << "because architecture does not match"; for_platform = false; } if (for_platform && platform.contains("windows") && !m_isPortable && asset_is_archive) { qDebug() << "Rejecting" << asset.name << "because it is not an installer"; for_platform = false; } static const QRegularExpression s_qtPattern("-qt(\\d+)"); auto qt_match = s_qtPattern.match(asset_name); if (for_platform && qt_match.hasMatch()) { if (platform_qt_ver.isEmpty() || platform_qt_ver.toInt() != qt_match.captured(1).toInt()) { qDebug() << "Rejecting" << asset.name << "because it is not for the correct qt version" << platform_qt_ver.toInt() << "vs" << qt_match.captured(1).toInt(); for_platform = false; } } if (((m_isPortable && for_portable) || (!m_isPortable && !for_portable)) && for_platform) { qDebug() << "Accepting" << asset.name; valid.append(asset); } } return valid; } GitHubReleaseAsset PrismUpdaterApp::selectAsset(const QList& assets) { SelectReleaseAssetDialog dlg(assets); auto result = dlg.exec(); if (result == QDialog::Rejected) { return {}; } GitHubReleaseAsset asset = dlg.selectedAsset(); return asset; } void PrismUpdaterApp::performUpdate(const GitHubRelease& release) { m_install_release = release; qDebug() << "Updating to" << release.tag_name; auto valid_assets = validReleaseArtifacts(release); qDebug() << "valid release assets:" << valid_assets; GitHubReleaseAsset selected_asset; if (valid_assets.isEmpty()) { return showFatalErrorMessage( tr("No Valid Release Assets"), tr("Github release %1 has no valid assets for this platform: %2") .arg(release.tag_name) .arg(tr("%1 portable: %2").arg(BuildConfig.BUILD_ARTIFACT).arg(m_isPortable ? tr("yes") : tr("no")))); } else if (valid_assets.length() > 1) { selected_asset = selectAsset(valid_assets); } else { selected_asset = valid_assets.takeFirst(); } if (!selected_asset.isValid()) { return showFatalErrorMessage(tr("No version selected."), tr("No version was selected.")); } qDebug() << "will install" << selected_asset; auto file = downloadAsset(selected_asset); if (!file.exists()) { return showFatalErrorMessage(tr("Failed to Download"), tr("Failed to download the selected asset.")); } performInstall(file); } QFileInfo PrismUpdaterApp::downloadAsset(const GitHubReleaseAsset& asset) { auto temp_dir = QDir::tempPath(); auto file_url = QUrl(asset.browser_download_url); auto out_file_path = FS::PathCombine(temp_dir, file_url.fileName()); qDebug() << "downloading" << file_url << "to" << out_file_path; auto download = Net::Download::makeFile(file_url, out_file_path); download->setNetwork(m_network); auto progress_dialog = ProgressDialog(); progress_dialog.adjustSize(); progress_dialog.execWithTask(download.get()); qDebug() << "download complete"; QFileInfo out_file(out_file_path); return out_file; } bool PrismUpdaterApp::callAppImageUpdate() { auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); QProcess proc = QProcess(); qDebug() << "Calling: AppImageUpdate" << appimage_path; proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate.AppImage")); proc.setArguments({ appimage_path }); auto result = proc.startDetached(); if (!result) qDebug() << "Failed to start AppImageUpdate reason:" << proc.errorString(); return result; } void PrismUpdaterApp::clearUpdateLog() { FS::deletePath(m_updateLogPath); } void PrismUpdaterApp::logUpdate(const QString& msg) { qDebug() << qUtf8Printable(msg); FS::append(m_updateLogPath, QStringLiteral("%1\n").arg(msg).toUtf8()); } std::tuple read_lock_File(const QString& path) { auto contents = QString(FS::read(path)); auto lines = contents.split('\n'); QDateTime timestamp; QString from, to, target, data_path; for (auto line : lines) { auto index = line.indexOf("="); if (index < 0) continue; auto left = line.left(index); auto right = line.mid(index + 1); if (left.toLower() == "timestamp") { timestamp = QDateTime::fromString(right, Qt::ISODate); } else if (left.toLower() == "from") { from = right; } else if (left.toLower() == "to") { to = right; } else if (left.toLower() == "target") { target = right; } else if (left.toLower() == "data_path") { data_path = right; } } return std::make_tuple(timestamp, from, to, target, data_path); } bool write_lock_file(const QString& path, QDateTime timestamp, QString from, QString to, QString target, QString data_path) { try { FS::write(path, QStringLiteral("TIMESTAMP=%1\nFROM=%2\nTO=%3\nTARGET=%4\nDATA_PATH=%5\n") .arg(timestamp.toString(Qt::ISODate)) .arg(from) .arg(to) .arg(target) .arg(data_path) .toUtf8()); } catch (FS::FileSystemException& err) { qWarning() << "Error writing lockfile:" << err.what() << "\n" << err.cause(); return false; } return true; } void PrismUpdaterApp::performInstall(QFileInfo file) { qDebug() << "starting install"; auto update_lock_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.lock"); QFileInfo update_lock(update_lock_path); if (update_lock.exists()) { auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock_path); auto msg = tr("Update already in progress\n"); auto infoMsg = tr("This installation has a update lock file present at: %1\n" "\n" "Timestamp: %2\n" "Updating from version %3 to %4\n" "Target install path: %5\n" "Data Path: %6" "\n" "This likely means that a previous update attempt failed. Please ensure your installation is in working order before " "proceeding.\n" "Check the Prism Launcher updater log at: \n" "%7\n" "for details on the last update attempt.\n" "\n" "To overwrite this lock and proceed with this update anyway, select \"Ignore\" below.") .arg(update_lock_path) .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path) .arg(m_updateLogPath); QMessageBox msgBox; msgBox.setText(msg); msgBox.setInformativeText(infoMsg); msgBox.setStandardButtons(QMessageBox::Ignore | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Cancel); msgBox.setMinimumWidth(460); msgBox.adjustSize(); switch (msgBox.exec()) { case QMessageBox::AcceptRole: break; case QMessageBox::RejectRole: [[fallthrough]]; default: return showFatalErrorMessage(tr("Update Aborted"), tr("The update attempt was aborted")); } } clearUpdateLog(); auto changelog_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.changelog"); FS::write(changelog_path, m_install_release.body.toUtf8()); logUpdate(tr("Updating from %1 to %2").arg(m_prismVersion).arg(m_install_release.tag_name)); if (m_isPortable || file.fileName().endsWith(".zip") || file.fileName().endsWith(".tar.gz")) { write_lock_file(update_lock_path, QDateTime::currentDateTime(), m_prismVersion, m_install_release.tag_name, m_rootPath, m_dataPath); logUpdate(tr("Updating portable install at %1").arg(m_rootPath)); unpackAndInstall(file); } else { logUpdate(tr("Running installer file at %1").arg(file.absoluteFilePath())); QProcess proc = QProcess(); #if defined Q_OS_WIN auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #endif proc.setProgram(file.absoluteFilePath()); bool result = proc.startDetached(); logUpdate(tr("Process start result: %1").arg(result ? tr("yes") : tr("no"))); exit(result ? 0 : 1); } } void PrismUpdaterApp::unpackAndInstall(QFileInfo archive) { logUpdate(tr("Backing up install")); backupAppDir(); if (auto loc = unpackArchive(archive)) { auto marker_file_path = loc.value().absoluteFilePath(".prism_launcher_updater_unpack.marker"); FS::write(marker_file_path, m_rootPath.toUtf8()); QProcess proc = QProcess(); auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exe_name.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else exe_name.prepend("bin/"); #endif auto new_updater_path = loc.value().absoluteFilePath(exe_name); logUpdate(tr("Starting new updater at '%1'").arg(new_updater_path)); if (!proc.startDetached(new_updater_path, { "-d", m_dataPath }, loc.value().absolutePath())) { logUpdate(tr("Failed to launch '%1' %2").arg(new_updater_path).arg(proc.errorString())); return exit(10); } return exit(); // up to the new updater now } return exit(1); // unpack failure } void PrismUpdaterApp::backupAppDir() { auto manifest_path = FS::PathCombine(m_rootPath, "manifest.txt"); QFileInfo manifest(manifest_path); QStringList file_list; if (manifest.isFile()) { // load manifest from file logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath())); try { auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath())); auto files = contents.split('\n'); for (auto file : files) { file_list.append(file.trimmed()); } } catch (FS::FileSystemException&) { } } if (file_list.isEmpty()) { // best guess if (BuildConfig.BUILD_ARTIFACT.toLower().contains("linux")) { file_list.append({ "PrismLauncher", "bin", "share", "lib" }); } else { // windows by process of elimination file_list.append({ "jars", "prismlauncher.exe", "prismlauncher_filelink.exe", "prismlauncher_updater.exe", "qtlogging.ini", "imageformats", "iconengines", "platforms", "styles", "tls", "qt.conf", "Qt*.dll", }); } logUpdate("manifest.txt empty or missing. making best guess at files to back up."); } logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n "))); static const QRegularExpression s_replaceRegex("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"); auto app_dir = QDir(m_rootPath); auto backup_dir = FS::PathCombine(app_dir.absolutePath(), QStringLiteral("backup_") + QString(m_prismVersion).replace(s_replaceRegex, QString("_")) + "-" + m_prismGitCommit); FS::ensureFolderPathExists(backup_dir); auto backup_marker_path = FS::PathCombine(m_dataPath, ".prism_launcher_update_backup_path.txt"); FS::write(backup_marker_path, backup_dir.toUtf8()); QProgressDialog progress(tr("Backing up install at %1").arg(m_rootPath), "", 0, file_list.length()); progress.setCancelButton(nullptr); progress.setMinimumWidth(400); progress.adjustSize(); progress.show(); QCoreApplication::processEvents(); logUpdate(tr("Backing up install at %1").arg(m_rootPath)); auto copy = [this, app_dir, backup_dir](QString to_bak_file) { auto rel_path = app_dir.relativeFilePath(to_bak_file); auto bak_path = FS::PathCombine(backup_dir, rel_path); logUpdate(tr("Backing up and then removing %1").arg(to_bak_file)); FS::ensureFilePathExists(bak_path); auto result = FS::copy(to_bak_file, bak_path).overwrite(true)(); if (!result) { logUpdate(tr("Failed to backup %1 to %2").arg(to_bak_file).arg(bak_path)); } else { if (!FS::deletePath(to_bak_file)) logUpdate(tr("Failed to remove %1").arg(to_bak_file)); } }; int i = 0; for (auto glob : file_list) { QDirIterator iter(app_dir.absolutePath(), QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); progress.setValue(i); QCoreApplication::processEvents(); if (!iter.hasNext() && !glob.isEmpty()) { if (auto file_info = QFileInfo(FS::PathCombine(app_dir.absolutePath(), glob)); file_info.exists()) { copy(file_info.absoluteFilePath()); } else { logUpdate(tr("File doesn't exist, ignoring: %1").arg(FS::PathCombine(app_dir.absolutePath(), glob))); } } else { while (iter.hasNext()) { copy(iter.next()); } } i++; } progress.setValue(i); QCoreApplication::processEvents(); } std::optional PrismUpdaterApp::unpackArchive(QFileInfo archive) { auto temp_extract_path = FS::PathCombine(m_dataPath, "prism_launcher_update_release"); FS::ensureFolderPathExists(temp_extract_path); auto tmp_extract_dir = QDir(temp_extract_path); auto result = MMCZip::extractDir(archive.absoluteFilePath(), tmp_extract_dir.absolutePath()); if (result) { logUpdate(tr("Extracted the following to \"%1\":\n %2").arg(tmp_extract_dir.absolutePath()).arg(result->join("\n "))); } else { logUpdate(tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); showFatalErrorMessage("Failed to extract archive", tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); return std::nullopt; } return tmp_extract_dir; } bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path) { QProcess proc = QProcess(); proc.setProcessChannelMode(QProcess::MergedChannels); proc.setReadChannel(QProcess::StandardOutput); proc.start(exe_path, { "--version" }); if (!proc.waitForStarted(5000)) { showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launch child process to read version.")); return false; } // wait 5 seconds to start if (!proc.waitForFinished(5000)) { showFatalErrorMessage(tr("Failed to Check Version"), tr("Child launcher process failed.")); return false; } auto out = proc.readAllStandardOutput(); auto lines = out.split('\n'); lines.removeAll(""); if (lines.length() < 2) return false; else if (lines.length() > 2) { auto line1 = lines.takeLast(); auto line2 = lines.takeLast(); lines = { line2, line1 }; } auto first = lines.takeFirst(); auto first_parts = first.split(' '); if (first_parts.length() < 2) return false; m_prismBinaryName = first_parts.takeFirst(); auto version = first_parts.takeFirst().trimmed(); m_prismVersion = version; if (version.contains('-')) { auto index = version.indexOf('-'); m_prsimVersionChannel = version.mid(index + 1); version = version.left(index); } else { m_prsimVersionChannel = "stable"; } auto version_parts = version.split('.'); if (version_parts.length() < 2) return false; m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); if (!version_parts.isEmpty()) m_prismVersionPatch = version_parts.takeFirst().toInt(); else m_prismVersionPatch = 0; m_prismGitCommit = lines.takeFirst().simplified(); return true; } void PrismUpdaterApp::loadReleaseList() { auto github_repo = m_prismRepoUrl; if (github_repo.host() != "github.com") return fail("updating from a non github url is not supported"); auto path_parts = github_repo.path().split('/'); path_parts.removeFirst(); // empty segment from leading / auto repo_owner = path_parts.takeFirst(); auto repo_name = path_parts.takeFirst(); auto api_url = QString("https://api.github.com/repos/%1/%2/releases").arg(repo_owner, repo_name); qDebug() << "Fetching release list from" << api_url; downloadReleasePage(api_url, 1); } void PrismUpdaterApp::downloadReleasePage(const QString& api_url, int page) { int per_page = 30; auto page_url = QString("%1?per_page=%2&page=%3").arg(api_url).arg(QString::number(per_page)).arg(QString::number(page)); auto response = std::make_shared(); auto download = Net::Download::makeByteArray(page_url, response); download->setNetwork(m_network); m_current_url = page_url; auto github_api_headers = new Net::RawHeaderProxy(); github_api_headers->addHeaders({ { "Accept", "application/vnd.github+json" }, { "X-GitHub-Api-Version", "2022-11-28" }, }); download->addHeaderProxy(github_api_headers); connect(download.get(), &Net::Download::succeeded, this, [this, response, per_page, api_url, page]() { int num_found = parseReleasePage(response.get()); if (!(num_found < per_page)) { // there may be more, fetch next page downloadReleasePage(api_url, page + 1); } else { run(); } }); connect(download.get(), &Net::Download::failed, this, &PrismUpdaterApp::downloadError); m_current_task.reset(download); connect(download.get(), &Net::Download::finished, this, [this]() { qDebug() << "Download" << m_current_task->getUid().toString() << "finished"; m_current_task.reset(); m_current_url = ""; }); QCoreApplication::processEvents(); QMetaObject::invokeMethod(download.get(), &Task::start, Qt::QueuedConnection); } int PrismUpdaterApp::parseReleasePage(const QByteArray* response) { if (response->isEmpty()) // empty page return 0; int num_releases = 0; try { auto doc = Json::requireDocument(*response); auto release_list = Json::requireArray(doc); for (auto release_json : release_list) { auto release_obj = Json::requireObject(release_json); GitHubRelease release = {}; release.id = Json::requireInteger(release_obj, "id"); release.name = release_obj["name"].toString(); release.tag_name = Json::requireString(release_obj, "tag_name"); release.created_at = QDateTime::fromString(Json::requireString(release_obj, "created_at"), Qt::ISODate); release.published_at = QDateTime::fromString(release_obj["published_at"].toString(), Qt::ISODate); release.draft = Json::requireBoolean(release_obj, "draft"); release.prerelease = Json::requireBoolean(release_obj, "prerelease"); release.body = release_obj["body"].toString(); release.version = Version(release.tag_name); auto release_assets_obj = Json::requireArray(release_obj, "assets"); for (auto asset_json : release_assets_obj) { auto asset_obj = Json::requireObject(asset_json); GitHubReleaseAsset asset = {}; asset.id = Json::requireInteger(asset_obj, "id"); asset.name = Json::requireString(asset_obj, "name"); asset.label = asset_obj["label"].toString(); asset.content_type = Json::requireString(asset_obj, "content_type"); asset.size = Json::requireInteger(asset_obj, "size"); asset.created_at = QDateTime::fromString(Json::requireString(asset_obj, "created_at"), Qt::ISODate); asset.updated_at = QDateTime::fromString(Json::requireString(asset_obj, "updated_at"), Qt::ISODate); asset.browser_download_url = Json::requireString(asset_obj, "browser_download_url"); release.assets.append(asset); } m_releases.append(release); num_releases++; } } catch (Json::JsonException& e) { auto err_msg = QString("Failed to parse releases from github: %1\n%2").arg(e.what()).arg(QString::fromStdString(response->toStdString())); fail(err_msg); } return num_releases; } GitHubRelease PrismUpdaterApp::getLatestRelease() { GitHubRelease latest; for (auto release : m_releases) { if (release.draft) continue; if (release.prerelease && !m_allowPreRelease) continue; if (!latest.isValid() || (release.version > latest.version)) { latest = release; } } return latest; } bool PrismUpdaterApp::needUpdate(const GitHubRelease& release) { auto current_ver = Version(QString("%1.%2.%3").arg(m_prismVersionMajor).arg(m_prismVersionMinor).arg(m_prismVersionPatch)); return current_ver < release.version; } void PrismUpdaterApp::downloadError(QString reason) { fail(QString("Network request Failed: %1 with reason %2").arg(m_current_url).arg(reason)); } PrismLauncher-10.0.5/launcher/updater/prismupdater/UpdaterDialogs.h0000644000175100017510000000442015144136757025040 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include "GitHubRelease.h" #include "Version.h" namespace Ui { class SelectReleaseDialog; } class SelectReleaseDialog : public QDialog { Q_OBJECT public: explicit SelectReleaseDialog(const Version& cur_version, const QList& releases, QWidget* parent = 0); ~SelectReleaseDialog(); void loadReleases(); void appendRelease(GitHubRelease const& release); GitHubRelease selectedRelease() { return m_selectedRelease; } private slots: GitHubRelease getRelease(QTreeWidgetItem* item); void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); protected: QList m_releases; GitHubRelease m_selectedRelease; Version m_currentVersion; Ui::SelectReleaseDialog* ui; }; class SelectReleaseAssetDialog : public QDialog { Q_OBJECT public: explicit SelectReleaseAssetDialog(const QList& assets, QWidget* parent = 0); ~SelectReleaseAssetDialog(); void loadAssets(); void appendAsset(GitHubReleaseAsset const& asset); GitHubReleaseAsset selectedAsset() { return m_selectedAsset; } private slots: GitHubReleaseAsset getAsset(QTreeWidgetItem* item); void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); protected: QList m_assets; GitHubReleaseAsset m_selectedAsset; Ui::SelectReleaseDialog* ui; }; PrismLauncher-10.0.5/launcher/updater/PrismExternalUpdater.cpp0000644000175100017510000003055215144136757024074 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "PrismExternalUpdater.h" #include #include #include #include #include #include #include #include #include #include #include "StringUtils.h" #include "BuildConfig.h" #include "ui/dialogs/UpdateAvailableDialog.h" class PrismExternalUpdater::Private { public: QDir appDir; QDir dataDir; QTimer updateTimer; bool allowBeta; bool autoCheck; double updateInterval; QDateTime lastCheck; std::unique_ptr settings; QWidget* parent; }; PrismExternalUpdater::PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir) { priv = new PrismExternalUpdater::Private(); priv->appDir = QDir(appDir); priv->dataDir = QDir(dataDir); auto settings_file = priv->dataDir.absoluteFilePath("prismlauncher_update.cfg"); priv->settings = std::make_unique(settings_file, QSettings::Format::IniFormat); priv->allowBeta = priv->settings->value("allow_beta", false).toBool(); priv->autoCheck = priv->settings->value("auto_check", false).toBool(); bool interval_ok; // default once per day priv->updateInterval = priv->settings->value("update_interval", 86400).toInt(&interval_ok); if (!interval_ok) priv->updateInterval = 86400; auto last_check = priv->settings->value("last_check"); if (!last_check.isNull() && last_check.isValid()) { priv->lastCheck = QDateTime::fromString(last_check.toString(), Qt::ISODate); } priv->parent = parent; connectTimer(); resetAutoCheckTimer(); } PrismExternalUpdater::~PrismExternalUpdater() { if (priv->updateTimer.isActive()) priv->updateTimer.stop(); disconnectTimer(); priv->settings->sync(); delete priv; } void PrismExternalUpdater::checkForUpdates() { checkForUpdates(true); } void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) { QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent); progress.setCancelButton(nullptr); progress.adjustSize(); progress.show(); QCoreApplication::processEvents(); QProcess proc; auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exe_name.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else exe_name = QString("bin/%1").arg(exe_name); #endif QStringList args = { "--check-only", "--dir", priv->dataDir.absolutePath(), "--debug" }; if (priv->allowBeta) args.append("--pre-release"); proc.start(priv->appDir.absoluteFilePath(exe_name), args); auto result_start = proc.waitForStarted(5000); if (!result_start) { auto err = proc.error(); qDebug() << "Failed to start updater after 5 seconds." << "reason:" << err << proc.errorString(); auto msgBox = QMessageBox(QMessageBox::Information, tr("Update Check Failed"), tr("Failed to start after 5 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); priv->lastCheck = QDateTime::currentDateTime(); priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); priv->settings->sync(); resetAutoCheckTimer(); return; } QCoreApplication::processEvents(); auto result_finished = proc.waitForFinished(60000); if (!result_finished) { proc.kill(); auto err = proc.error(); auto output = proc.readAll(); qDebug() << "Updater failed to close after 60 seconds." << "reason:" << err << proc.errorString(); auto msgBox = QMessageBox(QMessageBox::Information, tr("Update Check Failed"), tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); msgBox.setDetailedText(output); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); priv->lastCheck = QDateTime::currentDateTime(); priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); priv->settings->sync(); resetAutoCheckTimer(); return; } auto exit_code = proc.exitCode(); auto std_output = proc.readAllStandardOutput(); auto std_error = proc.readAllStandardError(); progress.hide(); QCoreApplication::processEvents(); switch (exit_code) { case 0: // no update available if (triggeredByUser) { qDebug() << "No update available"; auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("You are running the latest version."), QMessageBox::Ok, priv->parent); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); } break; case 1: // there was an error { qDebug() << "Updater subprocess error" << qPrintable(std_error); auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Check Error"), tr("There was an error running the update check."), QMessageBox::Ok, priv->parent); msgBox.setDetailedText(QString(std_error)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); } break; case 100: // update available { auto [first_line, remainder1] = StringUtils::splitFirst(std_output, '\n'); auto [second_line, remainder2] = StringUtils::splitFirst(remainder1, '\n'); auto [third_line, release_notes] = StringUtils::splitFirst(remainder2, '\n'); auto version_name = StringUtils::splitFirst(first_line, ": ").second.trimmed(); auto version_tag = StringUtils::splitFirst(second_line, ": ").second.trimmed(); auto release_timestamp = QDateTime::fromString(StringUtils::splitFirst(third_line, ": ").second.trimmed(), Qt::ISODate); qDebug() << "Update available:" << version_name << version_tag << release_timestamp; qDebug() << "Update release notes:" << release_notes; offerUpdate(version_name, version_tag, release_notes); } break; default: // unknown error code { qDebug() << "Updater exited with unknown code" << exit_code; auto msgBox = QMessageBox(QMessageBox::Information, tr("Unknown Update Error"), tr("The updater exited with an unknown condition.\nExit Code: %1").arg(QString::number(exit_code)), QMessageBox::Ok, priv->parent); auto detail_txt = tr("StdOut: %1\nStdErr: %2").arg(QString(std_output)).arg(QString(std_error)); msgBox.setDetailedText(detail_txt); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); } } priv->lastCheck = QDateTime::currentDateTime(); priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); priv->settings->sync(); resetAutoCheckTimer(); } bool PrismExternalUpdater::getAutomaticallyChecksForUpdates() { return priv->autoCheck; } double PrismExternalUpdater::getUpdateCheckInterval() { return priv->updateInterval; } bool PrismExternalUpdater::getBetaAllowed() { return priv->allowBeta; } void PrismExternalUpdater::setAutomaticallyChecksForUpdates(bool check) { priv->autoCheck = check; priv->settings->setValue("auto_check", check); priv->settings->sync(); resetAutoCheckTimer(); } void PrismExternalUpdater::setUpdateCheckInterval(double seconds) { priv->updateInterval = seconds; priv->settings->setValue("update_interval", seconds); priv->settings->sync(); resetAutoCheckTimer(); } void PrismExternalUpdater::setBetaAllowed(bool allowed) { priv->allowBeta = allowed; priv->settings->setValue("auto_beta", allowed); priv->settings->sync(); } void PrismExternalUpdater::resetAutoCheckTimer() { if (priv->autoCheck && priv->updateInterval > 0) { int timeoutDuration = 0; auto now = QDateTime::currentDateTime(); if (priv->lastCheck.isValid()) { auto diff = priv->lastCheck.secsTo(now); auto secs_left = priv->updateInterval - diff; if (secs_left < 0) secs_left = 0; timeoutDuration = secs_left * 1000; // to msec } qDebug() << "Auto update timer starting," << timeoutDuration / 1000 << "seconds left"; priv->updateTimer.start(timeoutDuration); } else { if (priv->updateTimer.isActive()) priv->updateTimer.stop(); } } void PrismExternalUpdater::connectTimer() { connect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired); } void PrismExternalUpdater::disconnectTimer() { disconnect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired); } void PrismExternalUpdater::autoCheckTimerFired() { qDebug() << "Auto update Timer fired"; checkForUpdates(false); } void PrismExternalUpdater::offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes) { priv->settings->beginGroup("skip"); auto should_skip = priv->settings->value(version_tag, false).toBool(); priv->settings->endGroup(); if (should_skip) { auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("There are no new updates available."), QMessageBox::Ok, priv->parent); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); return; } UpdateAvailableDialog dlg(BuildConfig.printableVersionString(), version_name, release_notes); auto result = dlg.exec(); qDebug() << "offer dlg result" << result; switch (result) { case UpdateAvailableDialog::Install: { performUpdate(version_tag); return; } case UpdateAvailableDialog::Skip: { priv->settings->beginGroup("skip"); priv->settings->setValue(version_tag, true); priv->settings->endGroup(); priv->settings->sync(); return; } case UpdateAvailableDialog::DontInstall: { return; } } } void PrismExternalUpdater::performUpdate(const QString& version_tag) { QProcess proc; auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exe_name.append(".exe"); auto env = QProcessEnvironment::systemEnvironment(); env.insert("__COMPAT_LAYER", "RUNASINVOKER"); proc.setProcessEnvironment(env); #else exe_name = QString("bin/%1").arg(exe_name); #endif QStringList args = { "--dir", priv->dataDir.absolutePath(), "--install-version", version_tag }; if (priv->allowBeta) args.append("--pre-release"); auto result = proc.startDetached(priv->appDir.absoluteFilePath(exe_name), args); if (!result) { qDebug() << "Failed to start updater:" << proc.error() << proc.errorString(); } QCoreApplication::exit(); } PrismLauncher-10.0.5/launcher/updater/MacSparkleUpdater.mm0000644000175100017510000001340415144136757023145 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Kenneth Chew * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "MacSparkleUpdater.h" #include "Application.h" #include #include @interface UpdaterObserver : NSObject @property(nonatomic, readonly) SPUUpdater* updater; /// A callback to run when the state of `canCheckForUpdates` for the `updater` changes. @property(nonatomic, copy) void (^callback)(bool); - (id)initWithUpdater:(SPUUpdater*)updater; @end @implementation UpdaterObserver - (id)initWithUpdater:(SPUUpdater*)updater { self = [super init]; _updater = updater; [self addObserver:self forKeyPath:@"updater.canCheckForUpdates" options:NSKeyValueObservingOptionNew context:nil]; return self; } - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { if ([keyPath isEqualToString:@"updater.canCheckForUpdates"]) { bool canCheck = [change[NSKeyValueChangeNewKey] boolValue]; self.callback(canCheck); } } @end @interface UpdaterDelegate : NSObject @property(nonatomic, copy) NSSet* allowedChannels; @end @implementation UpdaterDelegate - (NSSet*)allowedChannelsForUpdater:(SPUUpdater*)updater { return _allowedChannels; } @end class MacSparkleUpdater::Private { public: SPUStandardUpdaterController* updaterController; UpdaterObserver* updaterObserver; UpdaterDelegate* updaterDelegate; NSAutoreleasePool* autoReleasePool; }; MacSparkleUpdater::MacSparkleUpdater() { priv = new MacSparkleUpdater::Private(); // Enable Cocoa's memory management. NSApplicationLoad(); priv->autoReleasePool = [[NSAutoreleasePool alloc] init]; // Delegate is used for setting/getting allowed update channels. priv->updaterDelegate = [[UpdaterDelegate alloc] init]; // Controller is the interface for actually doing the updates. priv->updaterController = [[SPUStandardUpdaterController alloc] initWithStartingUpdater:true updaterDelegate:priv->updaterDelegate userDriverDelegate:nil]; priv->updaterObserver = [[UpdaterObserver alloc] initWithUpdater:priv->updaterController.updater]; // Use KVO to run a callback that emits a Qt signal when `canCheckForUpdates` changes, so the UI can respond accordingly. priv->updaterObserver.callback = ^(bool canCheck) { emit canCheckForUpdatesChanged(canCheck); }; } MacSparkleUpdater::~MacSparkleUpdater() { [priv->updaterObserver removeObserver:priv->updaterObserver forKeyPath:@"updater.canCheckForUpdates"]; [priv->updaterController release]; [priv->updaterObserver release]; [priv->updaterDelegate release]; [priv->autoReleasePool release]; delete priv; } void MacSparkleUpdater::checkForUpdates() { [priv->updaterController checkForUpdates:nil]; } bool MacSparkleUpdater::getAutomaticallyChecksForUpdates() { return priv->updaterController.updater.automaticallyChecksForUpdates; } double MacSparkleUpdater::getUpdateCheckInterval() { return priv->updaterController.updater.updateCheckInterval; } QSet MacSparkleUpdater::getAllowedChannels() { // Convert NSSet -> QSet __block QSet channels; [priv->updaterDelegate.allowedChannels enumerateObjectsUsingBlock:^(NSString* channel, BOOL* stop) { channels.insert(QString::fromNSString(channel)); }]; return channels; } bool MacSparkleUpdater::getBetaAllowed() { return getAllowedChannels().contains("beta"); } void MacSparkleUpdater::setAutomaticallyChecksForUpdates(bool check) { priv->updaterController.updater.automaticallyChecksForUpdates = check ? YES : NO; // make clang-tidy happy } void MacSparkleUpdater::setUpdateCheckInterval(double seconds) { priv->updaterController.updater.updateCheckInterval = seconds; } void MacSparkleUpdater::clearAllowedChannels() { priv->updaterDelegate.allowedChannels = [NSSet set]; } void MacSparkleUpdater::setAllowedChannel(const QString& channel) { if (channel.isEmpty()) { clearAllowedChannels(); return; } NSSet* nsChannels = [NSSet setWithObject:channel.toNSString()]; priv->updaterDelegate.allowedChannels = nsChannels; } void MacSparkleUpdater::setAllowedChannels(const QSet& channels) { if (channels.isEmpty()) { clearAllowedChannels(); return; } QString channelsConfig = ""; // Convert QSet -> NSSet NSMutableSet* nsChannels = [NSMutableSet setWithCapacity:channels.count()]; for (const QString& channel : channels) { [nsChannels addObject:channel.toNSString()]; channelsConfig += channel + " "; } priv->updaterDelegate.allowedChannels = nsChannels; } void MacSparkleUpdater::setBetaAllowed(bool allowed) { if (allowed) { setAllowedChannel("beta"); } else { clearAllowedChannels(); } } PrismLauncher-10.0.5/launcher/translations/0000755000175100017510000000000015144136756020315 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/translations/POTranslator.cpp0000644000175100017510000002364315144136756023421 0ustar runnerrunner#include "POTranslator.h" #include #include "FileSystem.h" struct POEntry { QString text; bool fuzzy; }; struct POTranslatorPrivate { QString filename; QHash mapping; QHash mapping_disambiguatrion; bool loaded = false; void reload(); }; class ParserArray : public QByteArray { public: ParserArray(const QByteArray& in) : QByteArray(in) {} bool chomp(const char* data, int length) { if (startsWith(data)) { remove(0, length); return true; } return false; } bool chompString(QByteArray& appendHere) { QByteArray msg; bool escape = false; if (size() < 2) { qDebug() << "String fragment is too short"; return false; } if (!startsWith('"')) { qDebug() << "String fragment does not start with \""; return false; } if (!endsWith('"')) { qDebug() << "String fragment does not end with \", instead, there is" << at(size() - 1); return false; } for (int i = 1; i < size() - 1; i++) { char c = operator[](i); if (escape) { switch (c) { case 'r': msg += '\r'; break; case 'n': msg += '\n'; break; case 't': msg += '\t'; break; case 'v': msg += '\v'; break; case 'a': msg += '\a'; break; case 'b': msg += '\b'; break; case 'f': msg += '\f'; break; case '"': msg += '"'; break; case '\\': msg.append('\\'); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': { int octal_start = i; while ((c = operator[](i)) >= '0' && c <= '7') { i++; if (i == length() - 1) { qDebug() << "Something went bad while parsing an octal escape string..."; return false; } } msg += mid(octal_start, i - octal_start).toUInt(0, 8); break; } case 'x': { // chomp the 'x' i++; int hex_start = i; while (isxdigit(operator[](i))) { i++; if (i == length() - 1) { qDebug() << "Something went bad while parsing a hex escape string..."; return false; } } msg += mid(hex_start, i - hex_start).toUInt(0, 16); break; } default: { qDebug() << "Invalid escape sequence character:" << c; return false; } } escape = false; } else if (c == '\\') { escape = true; } else { msg += c; } } if (escape) { qDebug() << "Unterminated escape sequence..."; return false; } appendHere += msg; return true; } }; void POTranslatorPrivate::reload() { QFile file(filename); if (!file.open(QFile::OpenMode::enum_type::ReadOnly | QFile::OpenMode::enum_type::Text)) { qDebug() << "Failed to open PO file:" << filename; return; } QByteArray context; QByteArray disambiguation; QByteArray id; QByteArray str; bool fuzzy = false; bool nextFuzzy = false; enum class Mode { First, MessageContext, MessageId, MessageString } mode = Mode::First; int lineNumber = 0; QHash newMapping; QHash newMapping_disambiguation; auto endEntry = [&]() { auto strStr = QString::fromUtf8(str); // NOTE: PO header has empty id. We skip it. if (!id.isEmpty()) { auto normalKey = context + "|" + id; newMapping.insert(normalKey, { strStr, fuzzy }); if (!disambiguation.isEmpty()) { auto disambiguationKey = context + "|" + id + "@" + disambiguation; newMapping_disambiguation.insert(disambiguationKey, { strStr, fuzzy }); } } context.clear(); disambiguation.clear(); id.clear(); str.clear(); fuzzy = nextFuzzy; nextFuzzy = false; }; while (!file.atEnd()) { ParserArray line = file.readLine(); if (line.endsWith('\n')) { line.resize(line.size() - 1); } if (line.endsWith('\r')) { line.resize(line.size() - 1); } if (!line.size()) { // NIL } else if (line[0] == '#') { if (line.contains(", fuzzy")) { nextFuzzy = true; } } else if (line.startsWith('"')) { QByteArray temp; QByteArray* out = &temp; switch (mode) { case Mode::First: qDebug() << "Unexpected escaped string during initial state... line:" << lineNumber; return; case Mode::MessageString: out = &str; break; case Mode::MessageContext: out = &context; break; case Mode::MessageId: out = &id; break; } if (!line.chompString(*out)) { qDebug() << "Badly formatted string on line:" << lineNumber; return; } } else if (line.chomp("msgctxt ", 8)) { switch (mode) { case Mode::First: break; case Mode::MessageString: endEntry(); break; case Mode::MessageContext: case Mode::MessageId: qDebug() << "Unexpected msgctxt line:" << lineNumber; return; } if (line.chompString(context)) { auto parts = context.split('|'); context = parts[0]; if (parts.size() > 1 && !parts[1].isEmpty()) { disambiguation = parts[1]; } mode = Mode::MessageContext; } } else if (line.chomp("msgid ", 6)) { switch (mode) { case Mode::MessageContext: case Mode::First: break; case Mode::MessageString: endEntry(); break; case Mode::MessageId: qDebug() << "Unexpected msgid line:" << lineNumber; return; } if (line.chompString(id)) { mode = Mode::MessageId; } } else if (line.chomp("msgstr ", 7)) { switch (mode) { case Mode::First: case Mode::MessageString: case Mode::MessageContext: qDebug() << "Unexpected msgstr line:" << lineNumber; return; case Mode::MessageId: break; } if (line.chompString(str)) { mode = Mode::MessageString; } } else { qDebug() << "I did not understand line:" << lineNumber << ":" << QString::fromUtf8(line); } lineNumber++; } endEntry(); mapping = std::move(newMapping); mapping_disambiguatrion = std::move(newMapping_disambiguation); loaded = true; } POTranslator::POTranslator(const QString& filename, QObject* parent) : QTranslator(parent) { d = new POTranslatorPrivate; d->filename = filename; d->reload(); } POTranslator::~POTranslator() { delete d; } QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, [[maybe_unused]] int n) const { if (disambiguation) { auto disambiguationKey = QByteArray(context) + "|" + QByteArray(sourceText) + "@" + QByteArray(disambiguation); auto iter = d->mapping_disambiguatrion.find(disambiguationKey); if (iter != d->mapping_disambiguatrion.end()) { auto& entry = *iter; if (entry.text.isEmpty()) { qDebug() << "Translation entry has no content:" << disambiguationKey; } if (entry.fuzzy) { qDebug() << "Translation entry is fuzzy:" << disambiguationKey << "->" << entry.text; } return entry.text; } } auto key = QByteArray(context) + "|" + QByteArray(sourceText); auto iter = d->mapping.find(key); if (iter != d->mapping.end()) { auto& entry = *iter; if (entry.text.isEmpty()) { qDebug() << "Translation entry has no content:" << key; } if (entry.fuzzy) { qDebug() << "Translation entry is fuzzy:" << key << "->" << entry.text; } return entry.text; } return QString(); } bool POTranslator::isEmpty() const { return !d->loaded; } PrismLauncher-10.0.5/launcher/translations/TranslationsModel.h0000644000175100017510000000411115144136756024125 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include struct Language; class TranslationsModel : public QAbstractListModel { Q_OBJECT public: explicit TranslationsModel(QString path, QObject* parent = 0); virtual ~TranslationsModel(); QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent) const override; bool selectLanguage(QString key); void updateLanguage(QString key); QModelIndex selectedIndex(); QString selectedLanguage(); void downloadIndex(); void setUseSystemLocale(bool useSystemLocale); private: QList::Iterator findLanguage(const QString& key); std::optional findLanguageAsOptional(const QString& key); void reloadLocalFiles(); void downloadTranslation(QString key); void downloadNext(); // hide copy constructor TranslationsModel(const TranslationsModel&) = delete; // hide assign op TranslationsModel& operator=(const TranslationsModel&) = delete; private slots: void indexReceived(); void indexFailed(QString reason); void dlFailed(QString reason); void dlGood(); void translationDirChanged(const QString& path); private: /* data */ struct Private; std::unique_ptr d; }; PrismLauncher-10.0.5/launcher/translations/POTranslator.h0000644000175100017510000000066615144136756023066 0ustar runnerrunner#pragma once #include struct POTranslatorPrivate; class POTranslator : public QTranslator { Q_OBJECT public: explicit POTranslator(const QString& filename, QObject* parent = nullptr); virtual ~POTranslator(); QString translate(const char* context, const char* sourceText, const char* disambiguation, int n) const override; bool isEmpty() const override; private: POTranslatorPrivate* d; }; PrismLauncher-10.0.5/launcher/translations/TranslationsModel.cpp0000644000175100017510000005140115144136756024464 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "TranslationsModel.h" #include #include #include #include #include #include #include #include "BuildConfig.h" #include "FileSystem.h" #include "Json.h" #include "net/ChecksumValidator.h" #include "net/NetJob.h" #include "POTranslator.h" #include "Application.h" const static QLatin1String defaultLangCode("en_US"); enum class FileType { NONE, QM, PO }; struct Language { Language() { updated = true; } Language(const QString& _key) { key = _key; locale = QLocale(key); updated = (key == defaultLangCode); } QString languageName() const { QString result; if (key == "ja_KANJI") { result = locale.nativeLanguageName() + u8" (漢字)"; } else if (key == "es_UY") { result = u8"Español de Latinoamérica"; } else if (key == "en_NZ") { result = u8"New Zealand English"; // No idea why qt translates this to just english and not to New Zealand English } else if (key == "en@pirate") { result = u8"Tongue of the High Seas"; } else if (key == "en@uwu") { result = u8"Cute Engwish"; } else if (key == "tok") { result = u8"toki pona"; } else if (key == "nan") { result = u8"é–©å—語"; // Using traditional Chinese script. Not sure if we should use simplified instead? } else { result = locale.nativeLanguageName(); } if (result.isEmpty()) { result = key; } return result; } float percentTranslated() const { if (total == 0) { return 100.0f; } return 100.0f * float(translated) / float(total); } void setTranslationStats(unsigned _translated, unsigned _untranslated, unsigned _fuzzy) { translated = _translated; untranslated = _untranslated; fuzzy = _fuzzy; total = translated + untranslated + fuzzy; } bool isOfSameNameAs(const Language& other) const { return key == other.key; } bool isIdenticalTo(const Language& other) const { return (key == other.key && file_name == other.file_name && file_size == other.file_size && file_sha1 == other.file_sha1 && translated == other.translated && fuzzy == other.fuzzy && total == other.fuzzy && localFileType == other.localFileType); } Language& apply(Language& other) { if (!isOfSameNameAs(other)) { return *this; } file_name = other.file_name; file_size = other.file_size; file_sha1 = other.file_sha1; translated = other.translated; fuzzy = other.fuzzy; total = other.total; localFileType = other.localFileType; return *this; } QString key; QLocale locale; bool updated; QString file_name = QString(); std::size_t file_size = 0; QString file_sha1 = QString(); unsigned translated = 0; unsigned untranslated = 0; unsigned fuzzy = 0; unsigned total = 0; FileType localFileType = FileType::NONE; }; struct TranslationsModel::Private { QDir m_dir; // initial state is just english QList m_languages = { Language(defaultLangCode) }; QString m_selectedLanguage = defaultLangCode; std::unique_ptr m_qt_translator; std::unique_ptr m_app_translator; Net::Download* m_index_task; QString m_downloadingTranslation; NetJob::Ptr m_dl_job; NetJob::Ptr m_index_job; QString m_nextDownload; std::unique_ptr m_po_translator; QFileSystemWatcher* watcher; const QString m_system_locale = QLocale::system().name(); const QString m_system_language = m_system_locale.split('_').front(); bool no_language_set = false; }; TranslationsModel::TranslationsModel(QString path, QObject* parent) : QAbstractListModel(parent) { d.reset(new Private); d->m_dir.setPath(path); FS::ensureFolderPathExists(path); reloadLocalFiles(); d->watcher = new QFileSystemWatcher(this); connect(d->watcher, &QFileSystemWatcher::directoryChanged, this, &TranslationsModel::translationDirChanged); d->watcher->addPath(d->m_dir.canonicalPath()); } TranslationsModel::~TranslationsModel() {} void TranslationsModel::translationDirChanged(const QString& path) { qDebug() << "Dir changed:" << path; if (!d->no_language_set) { reloadLocalFiles(); } selectLanguage(selectedLanguage()); } void TranslationsModel::indexReceived() { qDebug() << "Got translations index!"; d->m_index_job.reset(); if (d->no_language_set) { reloadLocalFiles(); auto language = d->m_system_locale; if (!findLanguageAsOptional(language).has_value()) { language = d->m_system_language; } selectLanguage(language); if (selectedLanguage() != defaultLangCode) { updateLanguage(selectedLanguage()); } APPLICATION->settings()->set("Language", selectedLanguage()); d->no_language_set = false; } else if (d->m_selectedLanguage != defaultLangCode) { downloadTranslation(d->m_selectedLanguage); } } namespace { void readIndex(const QString& path, QMap& languages) { QByteArray data; try { data = FS::read(path); } catch ([[maybe_unused]] const Exception& e) { qCritical() << "Translations Download Failed: index file not readable"; return; } try { auto toplevel_doc = Json::requireDocument(data); auto doc = Json::requireObject(toplevel_doc); auto file_type = Json::requireString(doc, "file_type"); if (file_type != "MMC-TRANSLATION-INDEX") { qCritical() << "Translations Download Failed: index file is of unknown file type" << file_type; return; } auto version = Json::requireInteger(doc, "version"); if (version > 2) { qCritical() << "Translations Download Failed: index file is of unknown format version" << file_type; return; } auto langObjs = Json::requireObject(doc, "languages"); for (auto iter = langObjs.begin(); iter != langObjs.end(); iter++) { Language lang(iter.key()); auto langObj = Json::requireObject(iter.value()); lang.setTranslationStats(langObj["translated"].toInt(), langObj["untranslated"].toInt(), langObj["fuzzy"].toInt()); lang.file_name = Json::requireString(langObj, "file"); lang.file_sha1 = Json::requireString(langObj, "sha1"); lang.file_size = Json::requireInteger(langObj, "size"); languages.insert(lang.key, lang); } } catch ([[maybe_unused]] Json::JsonException& e) { qCritical() << "Translations Download Failed: index file could not be parsed as json"; } } } // namespace void TranslationsModel::reloadLocalFiles() { QMap languages = { { defaultLangCode, Language(defaultLangCode) } }; readIndex(d->m_dir.absoluteFilePath("index_v2.json"), languages); auto entries = d->m_dir.entryInfoList({ "mmc_*.qm", "*.po" }, QDir::Files | QDir::NoDotAndDotDot); for (auto& entry : entries) { auto completeSuffix = entry.completeSuffix(); QString langCode; FileType fileType = FileType::NONE; if (completeSuffix == "qm") { langCode = entry.baseName().remove(0, 4); fileType = FileType::QM; } else if (completeSuffix == "po") { langCode = entry.baseName(); fileType = FileType::PO; } else { continue; } auto langIter = languages.find(langCode); if (langIter != languages.end()) { auto& language = *langIter; if (int(fileType) > int(language.localFileType)) { language.localFileType = fileType; } } else { if (fileType == FileType::PO) { Language localFound(langCode); localFound.localFileType = FileType::PO; languages.insert(langCode, localFound); } } } // changed and removed languages for (auto iter = d->m_languages.begin(); iter != d->m_languages.end();) { auto& language = *iter; auto row = iter - d->m_languages.begin(); auto updatedLanguageIter = languages.find(language.key); if (updatedLanguageIter != languages.end()) { if (language.isIdenticalTo(*updatedLanguageIter)) { languages.remove(language.key); } else { language.apply(*updatedLanguageIter); emit dataChanged(index(row), index(row)); languages.remove(language.key); } iter++; } else { beginRemoveRows(QModelIndex(), row, row); iter = d->m_languages.erase(iter); endRemoveRows(); } } // added languages if (languages.isEmpty()) { return; } beginInsertRows(QModelIndex(), 0, d->m_languages.size() + languages.size() - 1); for (auto& language : languages) { d->m_languages.append(language); } std::sort(d->m_languages.begin(), d->m_languages.end(), [this](const Language& a, const Language& b) { if (a.key != b.key) { if (a.key == d->m_system_locale || a.key == d->m_system_language) { return true; } if (b.key == d->m_system_locale || b.key == d->m_system_language) { return false; } } return a.languageName().toLower() < b.languageName().toLower(); }); endInsertRows(); } namespace { enum class Column { Language, Completeness }; } QVariant TranslationsModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); int row = index.row(); auto column = static_cast(index.column()); if (row < 0 || row >= d->m_languages.size()) return QVariant(); auto& lang = d->m_languages[row]; switch (role) { case Qt::DisplayRole: { switch (column) { case Column::Language: { return lang.languageName(); } case Column::Completeness: { return QString("%1%").arg(lang.percentTranslated(), 3, 'f', 1); } } qWarning("TranslationModel::data not implemented when role is DisplayRole"); } case Qt::ToolTipRole: { return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total") .arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total)); } case Qt::UserRole: return lang.key; default: return QVariant(); } } QVariant TranslationsModel::headerData(int section, Qt::Orientation orientation, int role) const { auto column = static_cast(section); if (role == Qt::DisplayRole) { switch (column) { case Column::Language: { return tr("Language"); } case Column::Completeness: { return tr("Completeness"); } } } else if (role == Qt::ToolTipRole) { switch (column) { case Column::Language: { return tr("The native language name."); } case Column::Completeness: { return tr("Completeness is the percentage of fully translated strings, not counting automatically guessed ones."); } } } return QAbstractListModel::headerData(section, orientation, role); } int TranslationsModel::rowCount([[maybe_unused]] const QModelIndex& parent) const { return d->m_languages.size(); } int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) const { return 2; } QList::Iterator TranslationsModel::findLanguage(const QString& key) { return std::find_if(d->m_languages.begin(), d->m_languages.end(), [key](Language& lang) { return lang.key == key; }); } std::optional TranslationsModel::findLanguageAsOptional(const QString& key) { auto found = findLanguage(key); if (found != d->m_languages.end()) return *found; return {}; } void TranslationsModel::setUseSystemLocale(bool useSystemLocale) { APPLICATION->settings()->set("UseSystemLocale", useSystemLocale); QLocale::setDefault(QLocale(useSystemLocale ? QString::fromStdString(std::locale().name()) : defaultLangCode)); } bool TranslationsModel::selectLanguage(QString key) { QString& langCode = key; auto langPtr = findLanguageAsOptional(key); if (langCode.isEmpty()) { d->no_language_set = true; } if (!langPtr.has_value()) { qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; langCode = defaultLangCode; } else { langCode = langPtr->key; } // uninstall existing translators if there are any if (d->m_app_translator) { QCoreApplication::removeTranslator(d->m_app_translator.get()); d->m_app_translator.reset(); } if (d->m_qt_translator) { QCoreApplication::removeTranslator(d->m_qt_translator.get()); d->m_qt_translator.reset(); } /* * FIXME: potential source of crashes: * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. * This function is not reentrant. */ QLocale::setDefault( QLocale(APPLICATION->settings()->get("UseSystemLocale").toBool() ? QString::fromStdString(std::locale().name()) : langCode)); // if it's the default UI language, finish if (langCode == defaultLangCode) { d->m_selectedLanguage = langCode; return true; } // otherwise install new translations bool successful = false; // FIXME: this is likely never present. FIX IT. d->m_qt_translator.reset(new QTranslator()); if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "..."; if (!QCoreApplication::installTranslator(d->m_qt_translator.get())) { qCritical() << "Loading Qt Language File failed."; d->m_qt_translator.reset(); } else { successful = true; } } else { d->m_qt_translator.reset(); } if (langPtr->localFileType == FileType::PO) { qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po")); if (!poTranslator->isEmpty()) { if (!QCoreApplication::installTranslator(poTranslator)) { delete poTranslator; qCritical() << "Installing Application Language File failed."; } else { d->m_app_translator.reset(poTranslator); successful = true; } } else { qCritical() << "Loading Application Language File failed."; d->m_app_translator.reset(); } } else if (langPtr->localFileType == FileType::QM) { d->m_app_translator.reset(new QTranslator()); if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path())) { qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; if (!QCoreApplication::installTranslator(d->m_app_translator.get())) { qCritical() << "Installing Application Language File failed."; d->m_app_translator.reset(); } else { successful = true; } } else { d->m_app_translator.reset(); } } else { d->m_app_translator.reset(); } d->m_selectedLanguage = langCode; return successful; } QModelIndex TranslationsModel::selectedIndex() { auto found = findLanguage(d->m_selectedLanguage); if (found != d->m_languages.end()) { return index(std::distance(d->m_languages.begin(), found), 0, QModelIndex()); } return QModelIndex(); } QString TranslationsModel::selectedLanguage() { return d->m_selectedLanguage; } void TranslationsModel::downloadIndex() { if (d->m_index_job || d->m_dl_job) { return; } qDebug() << "Downloading Translations Index..."; d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network())); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); entry->setStale(true); auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + "index_v2.json"), entry); d->m_index_task = task.get(); d->m_index_job->addNetAction(task); d->m_index_job->setAskRetry(false); connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); d->m_index_job->start(); } void TranslationsModel::updateLanguage(QString key) { if (key == defaultLangCode) { qWarning() << "Cannot update builtin language" << key; return; } auto found = findLanguageAsOptional(key); if (!found.has_value()) { qWarning() << "Cannot update invalid language" << key; return; } if (!found->updated) { downloadTranslation(key); } } void TranslationsModel::downloadTranslation(QString key) { if (d->m_dl_job) { d->m_nextDownload = key; return; } auto lang = findLanguageAsOptional(key); if (!lang.has_value()) { qWarning() << "Will not download an unknown translation" << key; return; } d->m_downloadingTranslation = key; MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); entry->setStale(true); auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->file_name), entry); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->file_sha1)); dl->setProgress(dl->getProgress(), lang->file_size); d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network())); d->m_dl_job->addNetAction(dl); d->m_dl_job->setAskRetry(false); connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed); d->m_dl_job->start(); } void TranslationsModel::downloadNext() { if (!d->m_nextDownload.isEmpty()) { downloadTranslation(d->m_nextDownload); d->m_nextDownload.clear(); } } void TranslationsModel::dlFailed(QString reason) { qCritical() << "Translations Download Failed:" << reason; d->m_dl_job.reset(); downloadNext(); } void TranslationsModel::dlGood() { qDebug() << "Got translation:" << d->m_downloadingTranslation; if (d->m_downloadingTranslation == d->m_selectedLanguage) { selectLanguage(d->m_selectedLanguage); } d->m_dl_job.reset(); downloadNext(); } void TranslationsModel::indexFailed(QString reason) { qCritical() << "Translations Index Download Failed:" << reason; d->m_index_job.reset(); } PrismLauncher-10.0.5/launcher/NullInstance.h0000644000175100017510000000650015144136756020345 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "BaseInstance.h" #include "launch/LaunchTask.h" class NullInstance : public BaseInstance { Q_OBJECT public: NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : BaseInstance(globalSettings, settings, rootDir) { setVersionBroken(true); } virtual ~NullInstance() = default; void saveNow() override {} void loadSpecificSettings() override { setSpecificSettingsLoaded(true); } QString getStatusbarDescription() override { return tr("Unknown instance type"); }; QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } QList createUpdateTask() override { return {}; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QMap getVariables() override { return QMap(); } QStringList getLogFileSearchPaths() override { return {}; } QString typeName() const override { return "Null"; } bool canExport() const override { return false; } bool canEdit() const override { return false; } bool canLaunch() const override { return false; } void populateLaunchMenu(QMenu* menu) override {} QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override { QStringList out; out << "Null instance - placeholder."; return out; } QString modsRoot() const override { return QString(); } void updateRuntimeContext() override { // NOOP } }; PrismLauncher-10.0.5/launcher/BaseInstaller.h0000644000175100017510000000235015144136756020475 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "BaseVersion.h" class MinecraftInstance; class QDir; class QString; class QObject; class Task; class BaseVersion; class BaseInstaller { public: BaseInstaller(); virtual ~BaseInstaller() {}; bool isApplied(MinecraftInstance* on); virtual bool add(MinecraftInstance* to); virtual bool remove(MinecraftInstance* from); virtual Task* createInstallTask(MinecraftInstance* instance, BaseVersion::Ptr version, QObject* parent) = 0; protected: virtual QString id() const = 0; QString filename(const QString& root) const; QDir patchesDir(const QString& root) const; }; PrismLauncher-10.0.5/launcher/FileSystem.h0000644000175100017510000004116315144136756020036 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "Exception.h" #include "Filter.h" #include #include #include #include #include #include #include namespace FS { class FileSystemException : public ::Exception { public: FileSystemException(const QString& message) : Exception(message) {} }; /** * write data to a file safely */ void write(const QString& filename, const QByteArray& data); /** * append data to a file safely */ void appendSafe(const QString& filename, const QByteArray& data); /** * append data to a file */ void append(const QString& filename, const QByteArray& data); /** * read data from a file safely */ QByteArray read(const QString& filename); /** * Update the last changed timestamp of an existing file */ bool updateTimestamp(const QString& filename); /** * Creates all the folders in a path for the specified path * last segment of the path is treated as a file name and is ignored! */ bool ensureFilePathExists(QString filenamepath); /** * Creates all the folders in a path for the specified path * last segment of the path is treated as a folder name and is created! */ bool ensureFolderPathExists(const QFileInfo folderPath); /** * Creates all the folders in a path for the specified path * last segment of the path is treated as a folder name and is created! */ bool ensureFolderPathExists(const QString folderPathName); /** * @brief Copies a directory and it's contents from src to dest */ class copy : public QObject { Q_OBJECT public: copy(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent) { m_src.setPath(src); m_dst.setPath(dst); } copy& followSymlinks(const bool follow) { m_followSymlinks = follow; return *this; } copy& matcher(Filter filter) { m_matcher = std::move(filter); return *this; } copy& whitelist(bool whitelist) { m_whitelist = whitelist; return *this; } copy& overwrite(const bool overwrite) { m_overwrite = overwrite; return *this; } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } qsizetype totalCopied() { return m_copied; } qsizetype totalFailed() { return m_failedPaths.length(); } QStringList failed() { return m_failedPaths; } signals: void fileCopied(const QString& relativeName); void copyFailed(const QString& relativeName); // TODO: maybe add a "shouldCopy" signal in the future? private: bool operator()(const QString& offset, bool dryRun = false); private: bool m_followSymlinks = true; Filter m_matcher = nullptr; bool m_whitelist = false; bool m_overwrite = false; QDir m_src; QDir m_dst; qsizetype m_copied; QStringList m_failedPaths; }; struct LinkPair { QString src; QString dst; }; struct LinkResult { QString src; QString dst; QString err_msg; int err_value; }; class ExternalLinkFileProcess : public QThread { Q_OBJECT public: ExternalLinkFileProcess(QString server, bool useHardLinks, QObject* parent = nullptr) : QThread(parent), m_useHardLinks(useHardLinks), m_server(server) {} void run() override { runLinkFile(); emit processExited(); } signals: void processExited(); private: void runLinkFile(); bool m_useHardLinks = false; QString m_server; }; /** * @brief links (a file / a directory and it's contents) from src to dest */ class create_link : public QObject { Q_OBJECT public: create_link(const QList path_pairs, QObject* parent = nullptr) : QObject(parent) { m_path_pairs.append(path_pairs); } create_link(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent) { LinkPair pair = { src, dst }; m_path_pairs.append(pair); } create_link& useHardLinks(const bool useHard) { m_useHardLinks = useHard; return *this; } create_link& matcher(Filter filter) { m_matcher = std::move(filter); return *this; } create_link& whitelist(bool whitelist) { m_whitelist = whitelist; return *this; } create_link& linkRecursively(bool recursive) { m_recursive = recursive; return *this; } create_link& setMaxDepth(int depth) { m_max_depth = depth; return *this; } create_link& debug(bool d) { m_debug = d; return *this; } std::error_code getOSError() { return m_os_err; } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } int totalLinked() { return m_linked; } int totalToLink() { return static_cast(m_links_to_make.size()); } void runPrivileged() { runPrivileged(QString()); } void runPrivileged(const QString& offset); QList getResults() { return m_path_results; } signals: void fileLinked(const QString& srcName, const QString& dstName); void linkFailed(const QString& srcName, const QString& dstName, const QString& err_msg, int err_value); void finished(); void finishedPrivileged(bool gotResults); private: bool operator()(const QString& offset, bool dryRun = false); void make_link_list(const QString& offset); bool make_links(); private: bool m_useHardLinks = false; Filter m_matcher = nullptr; bool m_whitelist = false; bool m_recursive = true; /// @brief >= -1 = infinite, 0 = link files at src/* to dest/*, 1 = link files at src/*/* to dest/*/*, etc. int m_max_depth = -1; QList m_path_pairs; QList m_path_results; QList m_links_to_make; int m_linked; bool m_debug = false; std::error_code m_os_err; QLocalServer m_linkServer; }; /** * @brief moves a file by renaming it * @param source source file path * @param dest destination filepath * */ bool move(const QString& source, const QString& dest); /** * Delete a folder recursively */ bool deletePath(QString path); bool removeFiles(QStringList listFile); /** * Trash a folder / file */ bool trash(QString path, QString* pathInTrash = nullptr); QString PathCombine(const QString& path1, const QString& path2); QString PathCombine(const QString& path1, const QString& path2, const QString& path3); QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4); QString AbsolutePath(const QString& path); /** * @brief depth of path. "foo.txt" -> 0 , "bar/foo.txt" -> 1, /baz/bar/foo.txt -> 2, etc. * * @param path path to measure * @return int number of components before base path */ int pathDepth(const QString& path); /** * @brief cut off segments of path until it is a max of length depth * * @param path path to truncate * @param depth max depth of new path * @return QString truncated path */ QString pathTruncate(const QString& path, int depth); /** * Resolve an executable * * Will resolve: * single executable (by name) * relative path * absolute path * * @return absolute path to executable or null string */ QString ResolveExecutable(QString path); /** * Normalize path * * Any paths inside the current directory will be normalized to relative paths (to current) * Other paths will be made absolute * * Returns false if the path logic somehow filed (and normalizedPath in invalid) */ QString NormalizePath(QString path); QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); QString RemoveInvalidPathChars(QString string, QChar replaceWith = '-'); QString DirNameFromString(QString string, QString inDir = "."); /// Checks if the a given Path contains "!" bool checkProblemticPathJava(QDir folder); // Get the Directory representing the User's Desktop QString getDesktopDir(); // Get the Directory representing the User's Applications directory QString getApplicationsDir(); // Overrides one folder with the contents of another, preserving items exclusive to the first folder // Equivalent to doing QDir::rename, but allowing for overrides bool overrideFolder(QString overwritten_path, QString override_path); /** * Creates a shortcut to the specified target file at the specified destination path. * Returns null QString if creation failed; otherwise returns the path to the created shortcut. */ QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); enum class FilesystemType { FAT, NTFS, REFS, EXT, EXT_2_OLD, EXT_2_3_4, XFS, BTRFS, NFS, ZFS, APFS, HFS, HFSPLUS, HFSX, FUSEBLK, F2FS, BCACHEFS, UNKNOWN }; /** * @brief Ordered Mapping of enum types to reported filesystem names * this mapping is non exsaustive, it just attempts to capture the filesystems which could be reasonalbly be in use . * all string values are in uppercase, use `QString.toUpper()` or equivalent during lookup. * * QMap is ordered * */ static const QMap s_filesystem_type_names = { { FilesystemType::FAT, { "FAT" } }, { FilesystemType::NTFS, { "NTFS" } }, { FilesystemType::REFS, { "REFS" } }, { FilesystemType::EXT_2_OLD, { "EXT_2_OLD", "EXT2_OLD" } }, { FilesystemType::EXT_2_3_4, { "EXT2/3/4", "EXT_2_3_4", "EXT2", "EXT3", "EXT4" } }, { FilesystemType::EXT, { "EXT" } }, { FilesystemType::XFS, { "XFS" } }, { FilesystemType::BTRFS, { "BTRFS" } }, { FilesystemType::NFS, { "NFS" } }, { FilesystemType::ZFS, { "ZFS" } }, { FilesystemType::APFS, { "APFS" } }, { FilesystemType::HFS, { "HFS" } }, { FilesystemType::HFSPLUS, { "HFSPLUS" } }, { FilesystemType::HFSX, { "HFSX" } }, { FilesystemType::FUSEBLK, { "FUSEBLK" } }, { FilesystemType::F2FS, { "F2FS" } }, { FilesystemType::BCACHEFS, { "BCACHEFS" } }, { FilesystemType::UNKNOWN, { "UNKNOWN" } } }; /** * @brief Get the string name of Filesystem enum object * * @param type * @return QString */ QString getFilesystemTypeName(FilesystemType type); /** * @brief Get the Filesystem enum object from a name * Does a lookup of the type name and returns an exact match * * @param name * @return FilesystemType */ FilesystemType getFilesystemType(const QString& name); /** * @brief Get the Filesystem enum object from a name * Does a fuzzy lookup of the type name and returns an apropreate match * * @param name * @return FilesystemType */ FilesystemType getFilesystemTypeFuzzy(const QString& name); struct FilesystemInfo { FilesystemType fsType = FilesystemType::UNKNOWN; QString fsTypeName; int blockSize; qint64 bytesAvailable; qint64 bytesFree; qint64 bytesTotal; QString name; QString rootPath; }; /** * @brief path to the near ancestor that exists * */ QString nearestExistentAncestor(const QString& path); /** * @brief colect information about the filesystem under a file * */ FilesystemInfo statFS(const QString& path); static const QList s_clone_filesystems = { FilesystemType::BTRFS, FilesystemType::APFS, FilesystemType::ZFS, FilesystemType::XFS, FilesystemType::REFS, FilesystemType::BCACHEFS }; /** * @brief if the Filesystem is reflink/clone capable * */ bool canCloneOnFS(const QString& path); bool canCloneOnFS(const FilesystemInfo& info); bool canCloneOnFS(FilesystemType type); /** * @brief if the Filesystems are reflink/clone capable and both are on the same device * */ bool canClone(const QString& src, const QString& dst); /** * @brief Copies a directory and it's contents from src to dest */ class clone : public QObject { Q_OBJECT public: clone(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent) { m_src.setPath(src); m_dst.setPath(dst); } clone& matcher(Filter filter) { m_matcher = std::move(filter); return *this; } clone& whitelist(bool whitelist) { m_whitelist = whitelist; return *this; } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } qsizetype totalCloned() { return m_cloned; } qsizetype totalFailed() { return m_failedClones.length(); } QList> failed() { return m_failedClones; } signals: void fileCloned(const QString& src, const QString& dst); void cloneFailed(const QString& src, const QString& dst); private: bool operator()(const QString& offset, bool dryRun = false); private: Filter m_matcher = nullptr; bool m_whitelist = false; QDir m_src; QDir m_dst; qsizetype m_cloned; QList> m_failedClones; }; /** * @brief clone/reflink file from src to dst * */ bool clone_file(const QString& src, const QString& dst, std::error_code& ec); #if defined(Q_OS_WIN) bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec); #elif defined(Q_OS_LINUX) bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec); #elif defined(Q_OS_MACOS) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec); #endif static const QList s_non_link_filesystems = { FilesystemType::FAT, }; /** * @brief if the Filesystem is symlink capable * */ bool canLinkOnFS(const QString& path); bool canLinkOnFS(const FilesystemInfo& info); bool canLinkOnFS(FilesystemType type); /** * @brief if the Filesystem is symlink capable on both ends * */ bool canLink(const QString& src, const QString& dst); uintmax_t hardLinkCount(const QString& path); #ifdef Q_OS_WIN QString getPathNameInLocal8bit(const QString& file); #endif QString getUniqueResourceName(const QString& filePath); } // namespace FS PrismLauncher-10.0.5/launcher/AssertHelpers.h0000644000175100017510000000160315144136756020531 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2025 Octol1ttle * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #if defined(ASSERT_NEVER) #error ASSERT_NEVER already defined #else #define ASSERT_NEVER(cond) (Q_ASSERT((cond) == false), (cond)) #endif PrismLauncher-10.0.5/launcher/InstanceList.h0000644000175100017510000001616215144136756020353 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include "BaseInstance.h" class QFileSystemWatcher; class InstanceTask; struct InstanceName; using InstanceId = QString; using GroupId = QString; using InstanceLocator = std::pair; enum class InstCreateError { NoCreateError = 0, NoSuchVersion, UnknownCreateError, InstExists, CantCreateDir }; enum class GroupsState { NotLoaded, Steady, Dirty }; struct TrashShortcutItem { ShortcutData data; QString trashPath; }; struct TrashHistoryItem { QString id; QString path; QString trashPath; QString groupName; QList shortcuts; }; class InstanceList : public QAbstractListModel { Q_OBJECT public: explicit InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent = 0); virtual ~InstanceList(); public: QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; enum AdditionalRoles { GroupRole = Qt::UserRole, InstancePointerRole = 0x34B1CB48, ///< Return pointer to real instance InstanceIDRole = 0x34B1CB49 ///< Return id if the instance }; /*! * \brief Error codes returned by functions in the InstanceList class. * NoError Indicates that no error occurred. * UnknownError indicates that an unspecified error occurred. */ enum InstListError { NoError = 0, UnknownError }; InstancePtr at(int i) const { return m_instances.at(i); } int count() const { return m_instances.count(); } InstListError loadList(); void saveNow(); /* O(n) */ InstancePtr getInstanceById(QString id) const; /* O(n) */ InstancePtr getInstanceByManagedName(const QString& managed_name) const; QModelIndex getInstanceIndexById(const QString& id) const; QStringList getGroups(); bool isGroupCollapsed(const QString& groupName); GroupId getInstanceGroup(const InstanceId& id) const; void setInstanceGroup(const InstanceId& id, GroupId name); void deleteGroup(const GroupId& name); void renameGroup(const GroupId& src, const GroupId& dst); bool trashInstance(const InstanceId& id); bool trashedSomething() const; bool undoTrashInstance(); void deleteInstance(const InstanceId& id); // Wrap an instance creation task in some more task machinery and make it ready to be used Task* wrapInstanceTask(InstanceTask* task); /** * Create a new empty staging area for instance creation and @return a path/key top commit it later. * Used by instance manipulation tasks. */ QString getStagedInstancePath(); /** * Commit the staging area given by @keyPath to the provider - used when creation succeeds. * Used by instance manipulation tasks. * should_override is used when another similar instance already exists, and we want to override it * - for instance, when updating it. */ bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, QString groupName, const InstanceTask&); /** * Destroy a previously created staging area given by @keyPath - used when creation fails. * Used by instance manipulation tasks. */ bool destroyStagingPath(const QString& keyPath); int getTotalPlayTime(); Qt::DropActions supportedDragActions() const override; Qt::DropActions supportedDropActions() const override; bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; QStringList mimeTypes() const override; QMimeData* mimeData(const QModelIndexList& indexes) const override; QStringList getLinkedInstancesById(const QString& id) const; signals: void dataIsInvalid(); void instancesChanged(); void instanceSelectRequest(QString instanceId); void groupsChanged(QSet groups); public slots: void on_InstFolderChanged(const Setting& setting, QVariant value); void on_GroupStateChanged(const QString& group, bool collapsed); private slots: void propertiesChanged(BaseInstance* inst); void providerUpdated(); void instanceDirContentsChanged(const QString& path); private: int getInstIndex(BaseInstance* inst) const; void updateTotalPlayTime(); void suspendWatch(); void resumeWatch(); void add(const QList& list); void loadGroupList(); void saveGroupList(); QList discoverInstances(); InstancePtr loadInstance(const InstanceId& id); void increaseGroupCount(const QString& group); void decreaseGroupCount(const QString& group); private: int m_watchLevel = 0; int totalPlayTime = 0; bool m_dirty = false; QList m_instances; // id -> refs QMap m_groupNameCache; SettingsObjectPtr m_globalSettings; QString m_instDir; QFileSystemWatcher* m_watcher; // FIXME: this is so inefficient that looking at it is almost painful. QSet m_collapsedGroups; QMap m_instanceGroupIndex; QSet instanceSet; bool m_groupsLoaded = false; bool m_instancesProbed = false; QStack m_trashHistory; }; PrismLauncher-10.0.5/launcher/Json.cpp0000644000175100017510000002271315144136756017216 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Json.h" #include #include #include "FileSystem.h" namespace Json { void write(const QJsonDocument& doc, const QString& filename) { FS::write(filename, doc.toJson()); } void write(const QJsonObject& object, const QString& filename) { write(QJsonDocument(object), filename); } void write(const QJsonArray& array, const QString& filename) { write(QJsonDocument(array), filename); } QByteArray toText(const QJsonObject& obj) { return QJsonDocument(obj).toJson(QJsonDocument::Compact); } QByteArray toText(const QJsonArray& array) { return QJsonDocument(array).toJson(QJsonDocument::Compact); } static bool isBinaryJson(const QByteArray& data) { decltype(QJsonDocument::BinaryFormatTag) tag = QJsonDocument::BinaryFormatTag; return memcmp(data.constData(), &tag, sizeof(QJsonDocument::BinaryFormatTag)) == 0; } QJsonDocument requireDocument(const QByteArray& data, const QString& what) { if (isBinaryJson(data)) { // FIXME: Is this needed? throw JsonException(what + ": Invalid JSON. Binary JSON unsupported"); } else { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { throw JsonException(what + ": Error parsing JSON: " + error.errorString()); } return doc; } } QJsonDocument requireDocument(const QString& filename, const QString& what) { return requireDocument(FS::read(filename), what); } QJsonObject requireObject(const QJsonDocument& doc, const QString& what) { if (!doc.isObject()) { throw JsonException(what + " is not an object"); } return doc.object(); } QJsonArray requireArray(const QJsonDocument& doc, const QString& what) { if (!doc.isArray()) { throw JsonException(what + " is not an array"); } return doc.array(); } QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error, QString* garbage) { auto doc = QJsonDocument::fromJson(json, error); if (error->error == QJsonParseError::GarbageAtEnd) { qsizetype offset = error->offset; QByteArray validJson = json.left(offset); doc = QJsonDocument::fromJson(validJson, error); if (garbage) *garbage = json.right(json.size() - offset); } return doc; } void writeString(QJsonObject& to, const QString& key, const QString& value) { if (!value.isEmpty()) { to.insert(key, value); } } void writeStringList(QJsonObject& to, const QString& key, const QStringList& values) { if (!values.isEmpty()) { QJsonArray array; for (auto value : values) { array.append(value); } to.insert(key, array); } } template <> QJsonValue toJson(const QUrl& url) { return QJsonValue(url.toString(QUrl::FullyEncoded)); } template <> QJsonValue toJson(const QByteArray& data) { return QJsonValue(QString::fromLatin1(data.toHex())); } template <> QJsonValue toJson(const QDateTime& datetime) { return QJsonValue(datetime.toString(Qt::ISODate)); } template <> QJsonValue toJson(const QDir& dir) { return QDir::current().relativeFilePath(dir.absolutePath()); } template <> QJsonValue toJson(const QUuid& uuid) { return uuid.toString(); } template <> QJsonValue toJson(const QVariant& variant) { return QJsonValue::fromVariant(variant); } template <> QByteArray requireIsType(const QJsonValue& value, const QString& what) { const QString string = value.toString(what); // ensure that the string can be safely cast to Latin1 if (string != QString::fromLatin1(string.toLatin1())) { throw JsonException(what + " is not encodable as Latin1"); } return QByteArray::fromHex(string.toLatin1()); } template <> QJsonArray requireIsType(const QJsonValue& value, const QString& what) { if (!value.isArray()) { throw JsonException(what + " is not an array"); } return value.toArray(); } template <> QString requireIsType(const QJsonValue& value, const QString& what) { if (!value.isString()) { throw JsonException(what + " is not a string"); } return value.toString(); } template <> bool requireIsType(const QJsonValue& value, const QString& what) { if (!value.isBool()) { throw JsonException(what + " is not a bool"); } return value.toBool(); } template <> double requireIsType(const QJsonValue& value, const QString& what) { if (!value.isDouble()) { throw JsonException(what + " is not a double"); } return value.toDouble(); } template <> int requireIsType(const QJsonValue& value, const QString& what) { const double doubl = requireIsType(value, what); if (fmod(doubl, 1) != 0) { throw JsonException(what + " is not an integer"); } return int(doubl); } template <> QDateTime requireIsType(const QJsonValue& value, const QString& what) { const QString string = requireIsType(value, what); const QDateTime datetime = QDateTime::fromString(string, Qt::ISODate); if (!datetime.isValid()) { throw JsonException(what + " is not a ISO formatted date/time value"); } return datetime; } template <> QUrl requireIsType(const QJsonValue& value, const QString& what) { const QString string = value.toString(what); if (string.isEmpty()) { return QUrl(); } const QUrl url = QUrl(string, QUrl::StrictMode); if (!url.isValid()) { throw JsonException(what + " is not a correctly formatted URL"); } return url; } template <> QDir requireIsType(const QJsonValue& value, const QString& what) { const QString string = requireIsType(value, what); // FIXME: does not handle invalid characters! return QDir::current().absoluteFilePath(string); } template <> QUuid requireIsType(const QJsonValue& value, const QString& what) { const QString string = requireIsType(value, what); const QUuid uuid = QUuid(string); if (uuid.toString() != string) // converts back => valid { throw JsonException(what + " is not a valid UUID"); } return uuid; } template <> QJsonObject requireIsType(const QJsonValue& value, const QString& what) { if (!value.isObject()) { throw JsonException(what + " is not an object"); } return value.toObject(); } template <> QVariant requireIsType(const QJsonValue& value, const QString& what) { if (value.isNull() || value.isUndefined()) { throw JsonException(what + " is null or undefined"); } return value.toVariant(); } template <> QJsonValue requireIsType(const QJsonValue& value, const QString& what) { if (value.isNull() || value.isUndefined()) { throw JsonException(what + " is null or undefined"); } return value; } QStringList toStringList(const QString& jsonString) { QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError || !doc.isArray()) return {}; try { return requireIsArrayOf(doc); } catch (Json::JsonException& e) { return {}; } } QString fromStringList(const QStringList& list) { QJsonArray array; for (const QString& str : list) { array.append(str); } QJsonDocument doc(toJsonArray(list)); return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); } QVariantMap toMap(const QString& jsonString) { QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError || !doc.isObject()) return {}; QJsonObject obj = doc.object(); return obj.toVariantMap(); } QString fromMap(const QVariantMap& map) { QJsonObject obj = QJsonObject::fromVariantMap(map); QJsonDocument doc(obj); return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); } } // namespace Json PrismLauncher-10.0.5/launcher/BaseVersionList.h0000644000175100017510000001044215144136756021022 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "BaseVersion.h" #include "QObjectPtr.h" #include "tasks/Task.h" /*! * \brief Class that each instance type's version list derives from. * Version lists are the lists that keep track of the available game versions * for that instance. This list will not be loaded on startup. It will be loaded * when the list's load function is called. Before using the version list, you * should check to see if it has been loaded yet and if not, load the list. * * Note that this class also inherits from QAbstractListModel. Methods from that * class determine how this version list shows up in a list view. Said methods * all have a default implementation, but they can be overridden by plugins to * change the behavior of the list. */ class BaseVersionList : public QAbstractListModel { Q_OBJECT public: enum ModelRoles { VersionPointerRole = Qt::UserRole, VersionRole, VersionIdRole, ParentVersionRole, RecommendedRole, LatestRole, TypeRole, BranchRole, PathRole, JavaNameRole, JavaMajorRole, CPUArchitectureRole, SortRole }; using RoleList = QList; explicit BaseVersionList(QObject* parent = 0); /*! * \brief Gets a task that will reload the version list. * Simply execute the task to load the list. * The task returned by this function should reset the model when it's done. * \return A pointer to a task that reloads the version list. */ virtual Task::Ptr getLoadTask() = 0; //! Checks whether or not the list is loaded. If this returns false, the list should be // loaded. virtual bool isLoaded() = 0; //! Gets the version at the given index. virtual const BaseVersion::Ptr at(int i) const = 0; //! Returns the number of versions in the list. virtual int count() const = 0; //////// List Model Functions //////// QVariant data(const QModelIndex& index, int role) const override; int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QHash roleNames() const override; //! which roles are provided by this version list? virtual RoleList providesRoles() const; /*! * \brief Finds a version by its descriptor. * \param descriptor The descriptor of the version to find. * \return A const pointer to the version with the given descriptor. NULL if * one doesn't exist. */ virtual BaseVersion::Ptr findVersion(const QString& descriptor); /*! * \brief Gets the recommended version from this list * If the list doesn't support recommended versions, this works exactly as getLatestStable */ virtual BaseVersion::Ptr getRecommended() const; /*! * Sorts the version list. */ virtual void sortVersions() = 0; protected slots: /*! * Updates this list with the given list of versions. * This is done by copying each version in the given list and inserting it * into this one. * We need to do this so that we can set the parents of the versions are set to this * version list. This can't be done in the load task, because the versions the load * task creates are on the load task's thread and Qt won't allow their parents * to be set to something created on another thread. * To get around that problem, we invoke this method on the GUI thread, which * then copies the versions and sets their parents correctly. * \param versions List of versions whose parents should be set. */ virtual void updateListData(QList versions) = 0; }; PrismLauncher-10.0.5/launcher/JavaCommon.h0000644000175100017510000000260615144136756020003 0ustar runnerrunner#pragma once #include class QWidget; /** * Common UI bits for the java pages to use. */ namespace JavaCommon { bool checkJVMArgs(QString args, QWidget* parent); // Show a dialog saying that the Java binary was usable void javaWasOk(QWidget* parent, const JavaChecker::Result& result); // Show a dialog saying that the Java binary was not usable because of bad options void javaArgsWereBad(QWidget* parent, const JavaChecker::Result& result); // Show a dialog saying that the Java binary was not usable void javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& result); // Show a dialog if we couldn't find Java Checker void javaCheckNotFound(QWidget* parent); class TestCheck : public QObject { Q_OBJECT public: TestCheck(QWidget* parent, QString path, QString args, int minMem, int maxMem, int permGen) : m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen) {} virtual ~TestCheck() = default; void run(); signals: void finished(); private slots: void checkFinished(const JavaChecker::Result& result); void checkFinishedWithArgs(const JavaChecker::Result& result); private: JavaChecker::Ptr checker; QWidget* m_parent = nullptr; QString m_path; QString m_args; int m_minMem = 0; int m_maxMem = 0; int m_permGen = 64; }; } // namespace JavaCommon PrismLauncher-10.0.5/launcher/Markdown.cpp0000644000175100017510000000206015144136756020060 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 Joshua Goins * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "Markdown.h" QString markdownToHTML(const QString& markdown) { const QByteArray markdownData = markdown.toUtf8(); char* buffer = cmark_markdown_to_html(markdownData.constData(), markdownData.length(), CMARK_OPT_NOBREAKS | CMARK_OPT_UNSAFE); QString htmlStr(buffer); free(buffer); return htmlStr; } PrismLauncher-10.0.5/launcher/FastFileIconProvider.cpp0000644000175100017510000000271015144136756022321 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "FastFileIconProvider.h" #include #include QIcon FastFileIconProvider::icon(const QFileInfo& info) const { #if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0) bool link = info.isSymbolicLink() || info.isAlias() || info.isShortcut(); #else // in versions prior to 6.4 we don't have access to isAlias bool link = info.isSymLink(); #endif QStyle::StandardPixmap icon; if (info.isDir()) { if (link) icon = QStyle::SP_DirLinkIcon; else icon = QStyle::SP_DirIcon; } else { if (link) icon = QStyle::SP_FileLinkIcon; else icon = QStyle::SP_FileIcon; } return QApplication::style()->standardIcon(icon); } PrismLauncher-10.0.5/launcher/LoggedProcess.cpp0000644000175100017510000001352015144136756021041 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022,2023 Sefa Eyeoglu * Copyright (c) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LoggedProcess.h" #include #include #include "MessageLevel.h" LoggedProcess::LoggedProcess(const QStringConverter::Encoding output_codec, QObject* parent) : QProcess(parent), m_err_decoder(output_codec), m_out_decoder(output_codec) { // QProcess has a strange interface... let's map a lot of those into a few. connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); connect(this, &QProcess::finished, this, &LoggedProcess::on_exit); connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error); connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); } LoggedProcess::~LoggedProcess() { if (m_is_detachable) { setProcessState(QProcess::NotRunning); } } QStringList LoggedProcess::reprocess(const QByteArray& data, QStringDecoder& decoder) { QString str = decoder(data); if (!m_leftover_line.isEmpty()) { str.prepend(m_leftover_line); m_leftover_line = ""; } auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed); m_leftover_line = lines.takeLast(); return lines; } void LoggedProcess::on_stdErr() { auto lines = reprocess(readAllStandardError(), m_err_decoder); emit log(lines, MessageLevel::StdErr); } void LoggedProcess::on_stdOut() { auto lines = reprocess(readAllStandardOutput(), m_out_decoder); emit log(lines, MessageLevel::StdOut); } void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status) { // save the exit code m_exit_code = exit_code; // based on state, send signals if (!m_is_aborting) { if (status == QProcess::NormalExit) { //: Message displayed on instance exit emit log({ tr("Process exited with code %1.").arg(exit_code) }, MessageLevel::Launcher); changeState(LoggedProcess::Finished); } else { //: Message displayed on instance crashed if (exit_code == -1) emit log({ tr("Process crashed.") }, MessageLevel::Launcher); else emit log({ tr("Process crashed with exitcode %1.").arg(exit_code) }, MessageLevel::Launcher); changeState(LoggedProcess::Crashed); } } else { //: Message displayed after the instance exits due to kill request emit log({ tr("Process was killed by user.") }, MessageLevel::Error); changeState(LoggedProcess::Aborted); } } void LoggedProcess::on_error(QProcess::ProcessError error) { switch (error) { case QProcess::FailedToStart: { emit log({ tr("The process failed to start.") }, MessageLevel::Fatal); changeState(LoggedProcess::FailedToStart); break; } // we'll just ignore those... never needed them case QProcess::Crashed: case QProcess::ReadError: case QProcess::Timedout: case QProcess::UnknownError: case QProcess::WriteError: break; } } void LoggedProcess::kill() { m_is_aborting = true; QProcess::kill(); } int LoggedProcess::exitCode() const { return m_exit_code; } void LoggedProcess::changeState(LoggedProcess::State state) { if (state == m_state) return; m_state = state; emit stateChanged(m_state); } LoggedProcess::State LoggedProcess::state() const { return m_state; } void LoggedProcess::on_stateChange(QProcess::ProcessState state) { switch (state) { case QProcess::NotRunning: break; // let's not - there are too many that handle this already. case QProcess::Starting: { if (m_state != LoggedProcess::NotRunning) { qWarning() << "Wrong state change for process from state" << m_state << "to" << (int)LoggedProcess::Starting; } changeState(LoggedProcess::Starting); return; } case QProcess::Running: { if (m_state != LoggedProcess::Starting) { qWarning() << "Wrong state change for process from state" << m_state << "to" << (int)LoggedProcess::Running; } changeState(LoggedProcess::Running); return; } } } void LoggedProcess::setDetachable(bool detachable) { m_is_detachable = detachable; } PrismLauncher-10.0.5/launcher/ApplicationMessage.cpp0000644000175100017510000000452215144136756022053 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ApplicationMessage.h" #include #include #include "Json.h" void ApplicationMessage::parse(const QByteArray& input) { auto doc = Json::requireDocument(input, "ApplicationMessage"); auto root = Json::requireObject(doc, "ApplicationMessage"); command = root.value("command").toString(); args.clear(); auto parsedArgs = root.value("args").toObject(); for (auto iter = parsedArgs.constBegin(); iter != parsedArgs.constEnd(); iter++) { args.insert(iter.key(), iter.value().toString()); } } QByteArray ApplicationMessage::serialize() { QJsonObject root; root.insert("command", command); QJsonObject outArgs; for (auto iter = args.constBegin(); iter != args.constEnd(); iter++) { outArgs.insert(iter.key(), iter.value()); } root.insert("args", outArgs); return Json::toText(root); } PrismLauncher-10.0.5/launcher/InstanceCopyTask.h0000644000175100017510000000167515144136756021200 0ustar runnerrunner#pragma once #include #include #include #include "BaseInstance.h" #include "BaseVersion.h" #include "Filter.h" #include "InstanceCopyPrefs.h" #include "InstanceTask.h" #include "net/NetJob.h" #include "settings/SettingsObject.h" #include "tasks/Task.h" class InstanceCopyTask : public InstanceTask { Q_OBJECT public: explicit InstanceCopyTask(InstancePtr origInstance, const InstanceCopyPrefs& prefs); protected: //! Entry point for tasks. virtual void executeTask() override; bool abort() override; void copyFinished(); void copyAborted(); private: /* data */ InstancePtr m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; Filter m_matcher; bool m_keepPlaytime; bool m_useLinks = false; bool m_useHardLinks = false; bool m_copySaves = false; bool m_linkRecursively = false; bool m_useClone = false; }; PrismLauncher-10.0.5/launcher/launch/0000755000175100017510000000000015144136756017046 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/launch/LogModel.cpp0000644000175100017510000000775115144136756021266 0ustar runnerrunner#include "LogModel.h" LogModel::LogModel(QObject* parent) : QAbstractListModel(parent) { m_content.resize(m_maxLines); } int LogModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_numLines; } QVariant LogModel::data(const QModelIndex& index, int role) const { if (index.row() < 0 || index.row() >= m_numLines) return QVariant(); auto row = index.row(); auto realRow = (row + m_firstLine) % m_maxLines; if (role == Qt::DisplayRole || role == Qt::EditRole) { return m_content[realRow].line; } if (role == LevelRole) { return static_cast(m_content[realRow].level); } return QVariant(); } void LogModel::append(MessageLevel level, QString line) { if (m_suspended) { return; } int lineNum = (m_firstLine + m_numLines) % m_maxLines; // overflow if (m_numLines == m_maxLines) { if (m_stopOnOverflow) { // nothing more to do, the buffer is full return; } beginRemoveRows(QModelIndex(), 0, 0); m_firstLine = (m_firstLine + 1) % m_maxLines; m_numLines--; endRemoveRows(); } else if (m_numLines == m_maxLines - 1 && m_stopOnOverflow) { level = MessageLevel::Fatal; line = m_overflowMessage; } beginInsertRows(QModelIndex(), m_numLines, m_numLines); m_numLines++; m_content[lineNum].level = level; m_content[lineNum].line = line; endInsertRows(); } void LogModel::suspend(bool suspend) { m_suspended = suspend; } bool LogModel::suspended() { return m_suspended; } void LogModel::clear() { beginResetModel(); m_firstLine = 0; m_numLines = 0; endResetModel(); } QString LogModel::toPlainText() { QString out; out.reserve(m_numLines * 80); for (int i = 0; i < m_numLines; i++) { QString& line = m_content[(m_firstLine + i) % m_maxLines].line; out.append(line + '\n'); } out.squeeze(); return out; } void LogModel::setMaxLines(int maxLines) { // no-op if (maxLines == m_maxLines) { return; } // if it all still fits in the buffer, just resize it if (m_firstLine + m_numLines < m_maxLines) { m_maxLines = maxLines; m_content.resize(maxLines); return; } // otherwise, we need to reorganize the data because it crosses the wrap boundary QList newContent; newContent.resize(maxLines); if (m_numLines <= maxLines) { // if it all fits in the new buffer, just copy it over for (int i = 0; i < m_numLines; i++) { newContent[i] = m_content[(m_firstLine + i) % m_maxLines]; } m_content.swap(newContent); } else { // if it doesn't fit, part of the data needs to be thrown away (the oldest log messages) int lead = m_numLines - maxLines; beginRemoveRows(QModelIndex(), 0, lead - 1); for (int i = 0; i < maxLines; i++) { newContent[i] = m_content[(m_firstLine + lead + i) % m_maxLines]; } m_numLines = m_maxLines; m_content.swap(newContent); endRemoveRows(); } m_firstLine = 0; m_maxLines = maxLines; } int LogModel::getMaxLines() { return m_maxLines; } void LogModel::setStopOnOverflow(bool stop) { m_stopOnOverflow = stop; } void LogModel::setOverflowMessage(const QString& overflowMessage) { m_overflowMessage = overflowMessage; } void LogModel::setLineWrap(bool state) { if (m_lineWrap != state) { m_lineWrap = state; } } bool LogModel::wrapLines() const { return m_lineWrap; } void LogModel::setColorLines(bool state) { if (m_colorLines != state) { m_colorLines = state; } } bool LogModel::colorLines() const { return m_colorLines; } bool LogModel::isOverFlow() { return m_numLines >= m_maxLines && m_stopOnOverflow; } MessageLevel LogModel::previousLevel() { if (m_numLines > 0) { return m_content[m_numLines - 1].level; } return MessageLevel::Unknown; } PrismLauncher-10.0.5/launcher/launch/LogModel.h0000644000175100017510000000263015144136756020722 0ustar runnerrunner#pragma once #include #include #include "MessageLevel.h" class LogModel : public QAbstractListModel { Q_OBJECT public: explicit LogModel(QObject* parent = 0); int rowCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex& index, int role) const; void append(MessageLevel, QString line); void clear(); void suspend(bool suspend); bool suspended(); QString toPlainText(); int getMaxLines(); void setMaxLines(int maxLines); void setStopOnOverflow(bool stop); void setOverflowMessage(const QString& overflowMessage); bool isOverFlow(); void setLineWrap(bool state); bool wrapLines() const; void setColorLines(bool state); bool colorLines() const; MessageLevel previousLevel(); enum Roles { LevelRole = Qt::UserRole }; private /* types */: struct entry { MessageLevel level = MessageLevel::Unknown; QString line; }; private: /* data */ QList m_content; int m_maxLines = 1000; // first line in the circular buffer int m_firstLine = 0; // number of lines occupied in the circular buffer int m_numLines = 0; bool m_stopOnOverflow = false; QString m_overflowMessage = "OVERFLOW"; bool m_suspended = false; bool m_lineWrap = true; bool m_colorLines = true; private: Q_DISABLE_COPY(LogModel) }; PrismLauncher-10.0.5/launcher/launch/TaskStepWrapper.h0000644000175100017510000000225515144136756022322 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include class TaskStepWrapper : public LaunchStep { Q_OBJECT public: explicit TaskStepWrapper(LaunchTask* parent, Task::Ptr task) : LaunchStep(parent), m_task(task) {}; virtual ~TaskStepWrapper() = default; void executeTask() override; bool canAbort() const override; void proceed() override; public slots: bool abort() override; private slots: void updateFinished(); private: Task::Ptr m_task; }; PrismLauncher-10.0.5/launcher/launch/steps/0000755000175100017510000000000015144136756020204 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/launch/steps/TextPrint.cpp0000644000175100017510000000105215144136756022647 0ustar runnerrunner#include "TextPrint.h" TextPrint::TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel level) : LaunchStep(parent) { m_lines = lines; m_level = level; } TextPrint::TextPrint(LaunchTask* parent, const QString& line, MessageLevel level) : LaunchStep(parent) { m_lines.append(line); m_level = level; } void TextPrint::executeTask() { emit logLines(m_lines, m_level); emitSucceeded(); } bool TextPrint::canAbort() const { return true; } bool TextPrint::abort() { emitFailed("Aborted."); return true; } PrismLauncher-10.0.5/launcher/launch/steps/LookupServerAddress.h0000644000175100017510000000252315144136756024325 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "minecraft/launch/MinecraftTarget.h" class LookupServerAddress : public LaunchStep { Q_OBJECT public: explicit LookupServerAddress(LaunchTask* parent); virtual ~LookupServerAddress() = default; virtual void executeTask(); virtual bool abort(); virtual bool canAbort() const { return true; } void setLookupAddress(const QString& lookupAddress); void setOutputAddressPtr(MinecraftTarget::Ptr output); private slots: void on_dnsLookupFinished(); private: void resolve(const QString& address, quint16 port); QDnsLookup* m_dnsLookup; QString m_lookupAddress; MinecraftTarget::Ptr m_output; }; PrismLauncher-10.0.5/launcher/launch/steps/CheckJava.h0000644000175100017510000000251315144136756022175 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include class CheckJava : public LaunchStep { Q_OBJECT public: explicit CheckJava(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~CheckJava() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } private slots: void checkJavaFinished(const JavaChecker::Result& result); private: void printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString& vendor); void printSystemInfo(bool javaIsKnown, bool javaIs64bit); private: QString m_javaPath; QString m_javaSignature; JavaChecker::Ptr m_JavaChecker; }; PrismLauncher-10.0.5/launcher/launch/steps/PostLaunchCommand.cpp0000644000175100017510000000667315144136756024303 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PostLaunchCommand.h" #include PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent) { auto instance = m_parent->instance(); m_command = instance->getPostExitCommand(); m_process.setProcessEnvironment(instance->createEnvironment()); connect(&m_process, &LoggedProcess::log, this, &PostLaunchCommand::logLines); connect(&m_process, &LoggedProcess::stateChanged, this, &PostLaunchCommand::on_state); } void PostLaunchCommand::executeTask() { auto cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher); auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); } void PostLaunchCommand::on_state(LoggedProcess::State state) { auto getError = [this]() { return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: case LoggedProcess::FailedToStart: { auto error = getError(); emit logLine(error, MessageLevel::Fatal); emitFailed(error); return; } case LoggedProcess::Finished: { if (m_process.exitCode() != 0) { auto error = getError(); emit logLine(error, MessageLevel::Fatal); emitFailed(error); } else { emit logLine(tr("Post-Launch command ran successfully.\n\n"), MessageLevel::Launcher); emitSucceeded(); } } default: break; } } void PostLaunchCommand::setWorkingDirectory(const QString& wd) { m_process.setWorkingDirectory(wd); } bool PostLaunchCommand::abort() { auto state = m_process.state(); if (state == LoggedProcess::Running || state == LoggedProcess::Starting) { m_process.kill(); } return true; } PrismLauncher-10.0.5/launcher/launch/steps/PrintServers.cpp0000644000175100017510000000366615144136756023371 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Leia uwu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "PrintServers.h" #include "QHostInfo" PrintServers::PrintServers(LaunchTask* parent, const QStringList& servers) : LaunchStep(parent) { m_servers = servers; } void PrintServers::executeTask() { for (QString server : m_servers) { QHostInfo::lookupHost(server, this, &PrintServers::resolveServer); } } void PrintServers::resolveServer(const QHostInfo& host_info) { QString server = host_info.hostName(); QString addresses = server + " resolves to:\n ["; if (!host_info.addresses().isEmpty()) { for (QHostAddress address : host_info.addresses()) { addresses += address.toString(); if (!host_info.addresses().endsWith(address)) { addresses += ", "; } } } else { addresses += "N/A"; } addresses += "]\n\n"; m_server_to_address.insert(server, addresses); // print server info in order once all servers are resolved if (m_server_to_address.size() >= m_servers.size()) { for (QString serv : m_servers) { emit logLine(m_server_to_address.value(serv), MessageLevel::Launcher); } emitSucceeded(); } } bool PrintServers::canAbort() const { return true; } PrismLauncher-10.0.5/launcher/launch/steps/PreLaunchCommand.h0000644000175100017510000000215015144136756023533 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "LoggedProcess.h" #include "launch/LaunchStep.h" class PreLaunchCommand : public LaunchStep { Q_OBJECT public: explicit PreLaunchCommand(LaunchTask* parent); virtual ~PreLaunchCommand() {}; virtual void executeTask(); virtual bool abort(); virtual bool canAbort() const { return true; } void setWorkingDirectory(const QString& wd); private slots: void on_state(LoggedProcess::State state); private: LoggedProcess m_process; QString m_command; }; PrismLauncher-10.0.5/launcher/launch/steps/TextPrint.h0000644000175100017510000000222715144136756022321 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include /* * FIXME: maybe do not export */ class TextPrint : public LaunchStep { Q_OBJECT public: explicit TextPrint(LaunchTask* parent, const QStringList& lines, MessageLevel level); explicit TextPrint(LaunchTask* parent, const QString& line, MessageLevel level); virtual ~TextPrint() {}; virtual void executeTask(); virtual bool canAbort() const; virtual bool abort(); private: QStringList m_lines; MessageLevel m_level; }; PrismLauncher-10.0.5/launcher/launch/steps/PostLaunchCommand.h0000644000175100017510000000215315144136756023735 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include class PostLaunchCommand : public LaunchStep { Q_OBJECT public: explicit PostLaunchCommand(LaunchTask* parent); virtual ~PostLaunchCommand() {}; virtual void executeTask(); virtual bool abort(); virtual bool canAbort() const { return true; } void setWorkingDirectory(const QString& wd); private slots: void on_state(LoggedProcess::State state); private: LoggedProcess m_process; QString m_command; }; PrismLauncher-10.0.5/launcher/launch/steps/PreLaunchCommand.cpp0000644000175100017510000000665715144136756024106 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PreLaunchCommand.h" #include PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent) { auto instance = m_parent->instance(); m_command = instance->getPreLaunchCommand(); m_process.setProcessEnvironment(instance->createEnvironment()); connect(&m_process, &LoggedProcess::log, this, &PreLaunchCommand::logLines); connect(&m_process, &LoggedProcess::stateChanged, this, &PreLaunchCommand::on_state); } void PreLaunchCommand::executeTask() { auto cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher); auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); } void PreLaunchCommand::on_state(LoggedProcess::State state) { auto getError = [this]() { return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); }; switch (state) { case LoggedProcess::Aborted: case LoggedProcess::Crashed: case LoggedProcess::FailedToStart: { auto error = getError(); emit logLine(error, MessageLevel::Fatal); emitFailed(error); return; } case LoggedProcess::Finished: { if (m_process.exitCode() != 0) { auto error = getError(); emit logLine(error, MessageLevel::Fatal); emitFailed(error); } else { emit logLine(tr("Pre-Launch command ran successfully.\n\n"), MessageLevel::Launcher); emitSucceeded(); } } default: break; } } void PreLaunchCommand::setWorkingDirectory(const QString& wd) { m_process.setWorkingDirectory(wd); } bool PreLaunchCommand::abort() { auto state = m_process.state(); if (state == LoggedProcess::Running || state == LoggedProcess::Starting) { m_process.kill(); } return true; } PrismLauncher-10.0.5/launcher/launch/steps/CheckJava.cpp0000644000175100017510000001555715144136756022544 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "CheckJava.h" #include #include #include #include #include #include #include "java/JavaUtils.h" void CheckJava::executeTask() { auto instance = m_parent->instance(); auto settings = instance->settings(); QString javaPathSetting = settings->get("JavaPath").toString(); m_javaPath = FS::ResolveExecutable(javaPathSetting); bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool(); auto realJavaPath = QStandardPaths::findExecutable(m_javaPath); if (realJavaPath.isEmpty()) { if (perInstance) { emit logLine(QString("The Java binary \"%1\" couldn't be found. Please fix the Java path " "override in the instance's settings or disable it.") .arg(javaPathSetting), MessageLevel::Warning); } else { emit logLine(QString("The Java binary \"%1\" couldn't be found. Please set up Java in " "the settings.") .arg(javaPathSetting), MessageLevel::Warning); } emitFailed(QString("Java path is not valid.")); return; } else { emit logLine("Java path is:\n" + m_javaPath + "\n\n", MessageLevel::Launcher); } if (JavaUtils::getJavaCheckPath().isEmpty()) { const char* reason = QT_TR_NOOP("Java checker library could not be found. Please check your installation."); emit logLine(tr(reason), MessageLevel::Fatal); emitFailed(tr(reason)); return; } QFileInfo javaInfo(realJavaPath); qint64 javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); auto storedSignature = settings->get("JavaSignature").toString(); auto storedArchitecture = settings->get("JavaArchitecture").toString(); auto storedRealArchitecture = settings->get("JavaRealArchitecture").toString(); auto storedVersion = settings->get("JavaVersion").toString(); auto storedVendor = settings->get("JavaVendor").toString(); QCryptographicHash hash(QCryptographicHash::Sha1); hash.addData(QByteArray::number(javaUnixTime)); hash.addData(m_javaPath.toUtf8()); m_javaSignature = hash.result().toHex(); // if timestamps are not the same, or something is missing, check! if (m_javaSignature != storedSignature || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedVendor.size() == 0) { m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0)); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); m_JavaChecker->start(); return; } else { auto verString = instance->settings()->get("JavaVersion").toString(); auto archString = instance->settings()->get("JavaArchitecture").toString(); auto realArchString = settings->get("JavaRealArchitecture").toString(); auto vendorString = instance->settings()->get("JavaVendor").toString(); printJavaInfo(verString, archString, realArchString, vendorString); } m_parent->instance()->updateRuntimeContext(); emitSucceeded(); } void CheckJava::checkJavaFinished(const JavaChecker::Result& result) { switch (result.validity) { case JavaChecker::Result::Validity::Errored: { // Error message displayed if java can't start emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error); emit logLine(QString("\nCheck your Java settings."), MessageLevel::Launcher); emitFailed(QString("Could not start java!")); return; } case JavaChecker::Result::Validity::ReturnedInvalidData: { emit logLine(QString("Java checker returned some invalid data we don't understand:"), MessageLevel::Error); emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); m_parent->instance()->updateRuntimeContext(); emitSucceeded(); return; } case JavaChecker::Result::Validity::Valid: { auto instance = m_parent->instance(); printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.realPlatform, result.javaVendor); instance->settings()->set("JavaVersion", result.javaVersion.toString()); instance->settings()->set("JavaArchitecture", result.mojangPlatform); instance->settings()->set("JavaRealArchitecture", result.realPlatform); instance->settings()->set("JavaVendor", result.javaVendor); instance->settings()->set("JavaSignature", m_javaSignature); m_parent->instance()->updateRuntimeContext(); emitSucceeded(); return; } } } void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString& vendor) { emit logLine( QString("Java is version %1, using %2 (%3) architecture, from %4.\n\n").arg(version, architecture, realArchitecture, vendor), MessageLevel::Launcher); } PrismLauncher-10.0.5/launcher/launch/steps/QuitAfterGameStop.cpp0000644000175100017510000000157715144136756024266 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 dada513 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "QuitAfterGameStop.h" #include #include "Application.h" void QuitAfterGameStop::executeTask() { APPLICATION->quit(); } PrismLauncher-10.0.5/launcher/launch/steps/QuitAfterGameStop.h0000644000175100017510000000205015144136756023716 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 dada513 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include class QuitAfterGameStop : public LaunchStep { Q_OBJECT public: explicit QuitAfterGameStop(LaunchTask* parent) : LaunchStep(parent) {}; virtual ~QuitAfterGameStop() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } }; PrismLauncher-10.0.5/launcher/launch/steps/PrintServers.h0000644000175100017510000000226115144136756023024 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Leia uwu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include class PrintServers : public LaunchStep { Q_OBJECT public: PrintServers(LaunchTask* parent, const QStringList& servers); virtual void executeTask(); virtual bool canAbort() const; private: void resolveServer(const QHostInfo& host_info); QMap m_server_to_address; QStringList m_servers; }; PrismLauncher-10.0.5/launcher/launch/steps/LookupServerAddress.cpp0000644000175100017510000000606515144136756024665 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LookupServerAddress.h" #include LookupServerAddress::LookupServerAddress(LaunchTask* parent) : LaunchStep(parent), m_dnsLookup(new QDnsLookup(this)) { connect(m_dnsLookup, &QDnsLookup::finished, this, &LookupServerAddress::on_dnsLookupFinished); m_dnsLookup->setType(QDnsLookup::SRV); } void LookupServerAddress::setLookupAddress(const QString& lookupAddress) { m_lookupAddress = lookupAddress; m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress)); } void LookupServerAddress::setOutputAddressPtr(MinecraftTarget::Ptr output) { m_output = std::move(output); } bool LookupServerAddress::abort() { m_dnsLookup->abort(); emitFailed("Aborted"); return true; } void LookupServerAddress::executeTask() { m_dnsLookup->lookup(); } void LookupServerAddress::on_dnsLookupFinished() { if (isFinished()) { // Aborted return; } if (m_dnsLookup->error() != QDnsLookup::NoError) { emit logLine(QString("Failed to resolve server address (this is NOT an error!) %1: %2\n") .arg(m_dnsLookup->name(), m_dnsLookup->errorString()), MessageLevel::Launcher); resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch // and leave it up to minecraft to fail (or maybe not) when connecting return; } const auto records = m_dnsLookup->serviceRecords(); if (records.empty()) { emit logLine(QString("Failed to resolve server address %1: the DNS lookup succeeded, but no records were returned.\n") .arg(m_dnsLookup->name()), MessageLevel::Warning); resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch // and leave it up to minecraft to fail (or maybe not) when connecting return; } const auto& firstRecord = records.at(0); quint16 port = firstRecord.port(); emit logLine( QString("Resolved server address %1 to %2 with port %3\n").arg(m_dnsLookup->name(), firstRecord.target(), QString::number(port)), MessageLevel::Launcher); resolve(firstRecord.target(), port); } void LookupServerAddress::resolve(const QString& address, quint16 port) { m_output->address = address; m_output->port = port; m_dnsLookup->deleteLater(); emitSucceeded(); } PrismLauncher-10.0.5/launcher/launch/TaskStepWrapper.cpp0000644000175100017510000000371015144136756022652 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "TaskStepWrapper.h" #include "tasks/Task.h" void TaskStepWrapper::executeTask() { if (m_state == Task::State::AbortedByUser) { emitFailed(tr("Task aborted.")); return; } connect(m_task.get(), &Task::finished, this, &TaskStepWrapper::updateFinished); connect(m_task.get(), &Task::progress, this, &TaskStepWrapper::setProgress); connect(m_task.get(), &Task::stepProgress, this, &TaskStepWrapper::propagateStepProgress); connect(m_task.get(), &Task::status, this, &TaskStepWrapper::setStatus); connect(m_task.get(), &Task::details, this, &TaskStepWrapper::setDetails); emit progressReportingRequest(); } void TaskStepWrapper::proceed() { m_task->start(); } void TaskStepWrapper::updateFinished() { if (m_task->wasSuccessful()) { m_task.reset(); emitSucceeded(); } else { QString reason = tr("Instance update failed because: %1\n\n").arg(m_task->failReason()); m_task.reset(); emit logLine(reason, MessageLevel::Fatal); emitFailed(reason); } } bool TaskStepWrapper::canAbort() const { if (m_task) { return m_task->canAbort(); } return true; } bool TaskStepWrapper::abort() { if (m_task && m_task->canAbort()) { auto status = m_task->abort(); emitFailed("Aborted."); return status; } return Task::abort(); } PrismLauncher-10.0.5/launcher/launch/LaunchTask.h0000644000175100017510000000775215144136756021267 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "BaseInstance.h" #include "LaunchStep.h" #include "LogModel.h" #include "MessageLevel.h" #include "logs/LogParser.h" class LaunchTask : public Task { Q_OBJECT protected: explicit LaunchTask(MinecraftInstancePtr instance); void init(); public: enum State { NotStarted, Running, Waiting, Failed, Aborted, Finished }; public: /* methods */ static shared_qobject_ptr create(MinecraftInstancePtr inst); virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); void setCensorFilter(QMap filter); MinecraftInstancePtr instance() { return m_instance; } void setPid(qint64 pid) { m_pid = pid; } qint64 pid() { return m_pid; } /** * @brief prepare the process for launch (for multi-stage launch) */ virtual void executeTask() override; /** * @brief launch the armed instance */ void proceed(); /** * @brief abort launch */ bool abort() override; bool canAbort() const override; shared_qobject_ptr getLogModel(); public: QString substituteVariables(QString& cmd, bool isLaunch = false) const; QString censorPrivateInfo(QString in); protected: /* methods */ virtual void emitFailed(QString reason) override; virtual void emitSucceeded() override; signals: /** * @brief emitted when the launch preparations are done */ void readyForLaunch(); void requestProgress(Task* task); void requestLogging(); public slots: void onLogLines(const QStringList& lines, MessageLevel defaultLevel = MessageLevel::Launcher); void onLogLine(QString line, MessageLevel defaultLevel = MessageLevel::Launcher); void onReadyForLaunch(); void onStepFinished(); void onProgressReportingRequested(); private: /*methods */ void finalizeSteps(bool successful, const QString& error); protected: bool parseXmlLogs(QString const& line, MessageLevel level); protected: /* data */ MinecraftInstancePtr m_instance; shared_qobject_ptr m_logModel; QList> m_steps; QMap m_censorFilter; int currentStep = -1; State state = NotStarted; qint64 m_pid = -1; LogParser m_stdoutParser; LogParser m_stderrParser; }; PrismLauncher-10.0.5/launcher/launch/LaunchStep.h0000644000175100017510000000244215144136756021267 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "MessageLevel.h" #include "tasks/Task.h" #include class LaunchTask; class LaunchStep : public Task { Q_OBJECT public: /* methods */ explicit LaunchStep(LaunchTask* parent); virtual ~LaunchStep() = default; signals: void logLines(QStringList lines, MessageLevel level); void logLine(QString line, MessageLevel level); void readyForLaunch(); void progressReportingRequest(); public slots: virtual void proceed() {}; // called in the opposite order than the Task launch(), used to clean up or otherwise undo things after the launch ends virtual void finalize() {}; protected: /* data */ LaunchTask* m_parent; }; PrismLauncher-10.0.5/launcher/launch/LaunchStep.cpp0000644000175100017510000000220115144136756021613 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "LaunchStep.h" #include "LaunchTask.h" LaunchStep::LaunchStep(LaunchTask* parent) : Task(), m_parent(parent) { connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch); connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine); connect(this, &LaunchStep::logLines, parent, &LaunchTask::onLogLines); connect(this, &LaunchStep::finished, parent, &LaunchTask::onStepFinished); connect(this, &LaunchStep::progressReportingRequest, parent, &LaunchTask::onProgressReportingRequested); } PrismLauncher-10.0.5/launcher/launch/LaunchTask.cpp0000644000175100017510000002447315144136756021621 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Authors: Orochimarufan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "launch/LaunchTask.h" #include #include #include #include #include #include #include #include "MessageLevel.h" #include "tasks/Task.h" void LaunchTask::init() { m_instance->setRunning(true); } shared_qobject_ptr LaunchTask::create(MinecraftInstancePtr inst) { shared_qobject_ptr proc(new LaunchTask(inst)); proc->init(); return proc; } LaunchTask::LaunchTask(MinecraftInstancePtr instance) : m_instance(instance) {} void LaunchTask::appendStep(shared_qobject_ptr step) { m_steps.append(step); } void LaunchTask::prependStep(shared_qobject_ptr step) { m_steps.prepend(step); } void LaunchTask::executeTask() { m_instance->setCrashed(false); if (!m_steps.size()) { state = LaunchTask::Finished; emitSucceeded(); return; } state = LaunchTask::Running; onStepFinished(); } void LaunchTask::onReadyForLaunch() { state = LaunchTask::Waiting; emit readyForLaunch(); } void LaunchTask::onStepFinished() { // initial -> just start the first step if (currentStep == -1) { currentStep++; m_steps[currentStep]->start(); return; } auto step = m_steps[currentStep]; if (step->wasSuccessful()) { // end? if (currentStep == m_steps.size() - 1) { finalizeSteps(true, QString()); } else { currentStep++; step = m_steps[currentStep]; step->start(); } } else { finalizeSteps(false, step->failReason()); } } void LaunchTask::finalizeSteps(bool successful, const QString& error) { for (auto step = currentStep; step >= 0; step--) { m_steps[step]->finalize(); } if (successful) { emitSucceeded(); } else { emitFailed(error); } } void LaunchTask::onProgressReportingRequested() { state = LaunchTask::Waiting; emit requestProgress(m_steps[currentStep].get()); } void LaunchTask::setCensorFilter(QMap filter) { m_censorFilter = filter; } QString LaunchTask::censorPrivateInfo(QString in) { auto iter = m_censorFilter.begin(); while (iter != m_censorFilter.end()) { in.replace(iter.key(), iter.value()); iter++; } return in; } void LaunchTask::proceed() { if (state != LaunchTask::Waiting) { return; } m_steps[currentStep]->proceed(); } bool LaunchTask::canAbort() const { switch (state) { case LaunchTask::Aborted: case LaunchTask::Failed: case LaunchTask::Finished: return false; case LaunchTask::NotStarted: return true; case LaunchTask::Running: case LaunchTask::Waiting: { auto step = m_steps[currentStep]; return step->canAbort(); } } return false; } bool LaunchTask::abort() { switch (state) { case LaunchTask::Aborted: case LaunchTask::Failed: case LaunchTask::Finished: return true; case LaunchTask::NotStarted: { state = LaunchTask::Aborted; emitFailed("Aborted"); return true; } case LaunchTask::Running: case LaunchTask::Waiting: { auto step = m_steps[currentStep]; if (!step->canAbort()) { return false; } if (step->abort()) { state = LaunchTask::Aborted; return true; } } default: break; } return false; } shared_qobject_ptr LaunchTask::getLogModel() { if (!m_logModel) { m_logModel.reset(new LogModel()); m_logModel->setMaxLines(getConsoleMaxLines(m_instance->settings())); m_logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); // FIXME: should this really be here? m_logModel->setOverflowMessage(tr("Stopped watching the game log because the log length surpassed %1 lines.\n" "You may have to fix your mods because the game is still logging to files and" " likely wasting harddrive space at an alarming rate!") .arg(m_logModel->getMaxLines())); } return m_logModel; } bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel level) { LogParser* parser; switch (static_cast(level)) { case MessageLevel::StdErr: parser = &m_stderrParser; break; case MessageLevel::StdOut: parser = &m_stdoutParser; break; default: return false; } parser->appendLine(line); auto items = parser->parseAvailable(); if (auto err = parser->getError(); err.has_value()) { auto& model = *getLogModel(); model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage)); return false; } if (items.isEmpty()) return true; auto model = getLogModel(); for (auto const& item : items) { if (std::holds_alternative(item)) { auto entry = std::get(item); auto msg = QString("[%1] [%2/%3] [%4]: %5") .arg(entry.timestamp.toString("HH:mm:ss")) .arg(entry.thread) .arg(entry.levelText) .arg(entry.logger) .arg(entry.message); msg = censorPrivateInfo(msg); model->append(entry.level, msg); } else if (std::holds_alternative(item)) { auto msg = std::get(item).message; MessageLevel newLevel = MessageLevel::takeFromLine(msg); if (newLevel == MessageLevel::Unknown) newLevel = LogParser::guessLevel(line, model->previousLevel()); msg = censorPrivateInfo(msg); model->append(newLevel, msg); } } return true; } void LaunchTask::onLogLines(const QStringList& lines, MessageLevel defaultLevel) { for (auto& line : lines) { onLogLine(line, defaultLevel); } } void LaunchTask::onLogLine(QString line, MessageLevel level) { if (parseXmlLogs(line, level)) { return; } // censor private user info line = censorPrivateInfo(line); getLogModel()->append(level, line); } void LaunchTask::emitSucceeded() { m_instance->setRunning(false); Task::emitSucceeded(); } void LaunchTask::emitFailed(QString reason) { m_instance->setRunning(false); m_instance->setCrashed(true); Task::emitFailed(reason); } QString expandVariables(const QString& input, QProcessEnvironment dict) { QString result = input; enum { base, maybeBrace, variable, brace } state = base; int startIdx = -1; for (int i = 0; i < result.length();) { QChar c = result.at(i++); switch (state) { case base: if (c == '$') state = maybeBrace; break; case maybeBrace: if (c == '{') { state = brace; startIdx = i; } else if (c.isLetterOrNumber() || c == '_') { state = variable; startIdx = i - 1; } else { state = base; } break; case brace: if (c == '}') { const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), ""); if (!res.isEmpty()) { result.replace(startIdx - 2, i - startIdx + 2, res); i = startIdx - 2 + res.length(); } state = base; } break; case variable: if (!c.isLetterOrNumber() && c != '_') { const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), ""); if (!res.isEmpty()) { result.replace(startIdx - 1, i - startIdx, res); i = startIdx - 1 + res.length(); } state = base; } break; } } if (state == variable) { if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty()) result.replace(startIdx - 1, result.length() - startIdx + 1, res); } return result; } QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const { return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment()); } PrismLauncher-10.0.5/launcher/MMCZip.cpp0000644000175100017510000002746515144136756017415 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MMCZip.h" #include #include "FileSystem.h" #include "archive/ArchiveReader.h" #include "archive/ArchiveWriter.h" #include #include #include #include #include namespace MMCZip { // ours using FilterFunction = std::function; #if defined(LAUNCHER_APPLICATION) bool mergeZipFiles(ArchiveWriter& into, QFileInfo from, QSet& contained, const FilterFunction& filter = nullptr) { ArchiveReader r(from.absoluteFilePath()); return r.parse([&into, &contained, &filter, from](ArchiveReader::File* f) { auto filename = f->filename(); if (filter && !filter(filename)) { qDebug() << "Skipping file" << filename << "from" << from.fileName() << "- filtered"; f->skip(); return true; } if (contained.contains(filename)) { qDebug() << "Skipping already contained file" << filename << "from" << from.fileName(); f->skip(); return true; } contained.insert(filename); if (!into.addFile(f)) { qCritical() << "Failed to copy data of" << filename << "into the jar"; return false; } return true; }); } bool compressDirFiles(ArchiveWriter& zip, QString dir, QFileInfoList files) { QDir directory(dir); if (!directory.exists()) return false; for (auto e : files) { auto filePath = directory.relativeFilePath(e.absoluteFilePath()); auto srcPath = e.absoluteFilePath(); if (!zip.addFile(srcPath, filePath)) return false; } return true; } // ours bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { ArchiveWriter zipOut(targetJarPath); if (!zipOut.open()) { FS::deletePath(targetJarPath); qCritical() << "Failed to open the minecraft.jar for modding"; return false; } // Files already added to the jar. // These files will be skipped. QSet addedFiles; // Modify the jar // This needs to be done in reverse-order to ensure we respect the loading order of components for (auto i = mods.crbegin(); i != mods.crend(); i++) { const auto* mod = *i; // do not merge disabled mods. if (!mod->enabled()) continue; if (mod->type() == ResourceType::ZIPFILE) { if (!mergeZipFiles(zipOut, mod->fileinfo(), addedFiles)) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } } else if (mod->type() == ResourceType::SINGLEFILE) { // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); if (!zipOut.addFile(filename.absoluteFilePath(), filename.fileName())) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } addedFiles.insert(filename.fileName()); } else if (mod->type() == ResourceType::FOLDER) { // untested, but seems to be unused / not possible to reach // FIXME: buggy - does not work with addedFiles auto filename = mod->fileinfo(); QString what_to_zip = filename.absoluteFilePath(); QDir dir(what_to_zip); dir.cdUp(); QString parent_dir = dir.absolutePath(); auto files = QFileInfoList(); collectFileListRecursively(what_to_zip, nullptr, &files, nullptr); for (auto e : files) { if (addedFiles.contains(e.filePath())) files.removeAll(e); } if (!compressDirFiles(zipOut, parent_dir, files)) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar."; return false; } qDebug() << "Adding folder" << filename.fileName() << "from" << filename.absoluteFilePath(); } else { // Make sure we do not continue launching when something is missing or undefined... zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar."; return false; } } if (!mergeZipFiles(zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key) { return !key.contains("META-INF"); })) { zipOut.close(); FS::deletePath(targetJarPath); qCritical() << "Failed to insert minecraft.jar contents."; return false; } // Recompress the jar if (!zipOut.close()) { FS::deletePath(targetJarPath); qCritical() << "Failed to finalize minecraft.jar!"; return false; } return true; } #endif // ours std::optional extractSubDir(ArchiveReader* zip, const QString& subdir, const QString& target) { auto target_top_dir = QUrl::fromLocalFile(target); QStringList extracted; qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; if (!zip->collectFiles()) { qWarning() << "Failed to enumerate files in archive"; return std::nullopt; } if (zip->getFiles().isEmpty()) { qDebug() << "Extracting empty archives seems odd..."; return extracted; } auto extPtr = ArchiveWriter::createDiskWriter(); auto ext = extPtr.get(); if (!zip->parse([&subdir, &target, &target_top_dir, ext, &extracted](ArchiveReader::File* f) { QString file_name = f->filename(); file_name = FS::RemoveInvalidPathChars(file_name); if (!file_name.startsWith(subdir)) { f->skip(); return true; } auto relative_file_name = QDir::fromNativeSeparators(file_name.mid(subdir.size())); auto original_name = relative_file_name; // Fix subdirs/files ending with a / getting transformed into absolute paths if (relative_file_name.startsWith('/')) relative_file_name = relative_file_name.mid(1); // Fix weird "folders with a single file get squashed" thing QString sub_path; if (relative_file_name.contains('/') && !relative_file_name.endsWith('/')) { sub_path = relative_file_name.section('/', 0, -2) + '/'; FS::ensureFolderPathExists(FS::PathCombine(target, sub_path)); relative_file_name = relative_file_name.split('/').last(); } QString target_file_path; if (relative_file_name.isEmpty()) { target_file_path = target + '/'; } else { target_file_path = FS::PathCombine(target_top_dir.toLocalFile(), sub_path, relative_file_name); if (relative_file_name.endsWith('/') && !target_file_path.endsWith('/')) target_file_path += '/'; } if (!target_top_dir.isParentOf(QUrl::fromLocalFile(target_file_path))) { qWarning() << "Extracting" << relative_file_name << "was cancelled, because it was effectively outside of the target path" << target; return false; } if (!f->writeFile(ext, target_file_path)) { qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; return false; } extracted.append(target_file_path); qDebug() << "Extracted file" << relative_file_name << "to" << target_file_path; return true; })) { qWarning() << "Failed to parse file" << zip->getZipName(); FS::removeFiles(extracted); return std::nullopt; } return extracted; } // ours std::optional extractDir(QString fileCompressed, QString dir) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); if (fileInfo.size() == 22) { return QStringList(); } ArchiveReader zip(fileCompressed); return extractSubDir(&zip, "", dir); } // ours std::optional extractDir(QString fileCompressed, QString subdir, QString dir) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); if (fileInfo.size() == 22) { return QStringList(); } ArchiveReader zip(fileCompressed); return extractSubDir(&zip, subdir, dir); } // ours bool extractFile(QString fileCompressed, QString file, QString target) { // check if this is a minimum size empty zip file... QFileInfo fileInfo(fileCompressed); if (fileInfo.size() == 22) { return true; } ArchiveReader zip(fileCompressed); auto f = zip.goToFile(file); if (!f) { return false; } auto extPtr = ArchiveWriter::createDiskWriter(); auto ext = extPtr.get(); return f->writeFile(ext, target); } bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter) { QDir rootDirectory(rootDir); if (!rootDirectory.exists()) return false; QDir directory; if (subDir == nullptr) directory = rootDirectory; else directory = QDir(subDir); if (!directory.exists()) return false; // shouldn't ever happen // recurse directories QFileInfoList entries = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); for (const auto& e : entries) { if (!collectFileListRecursively(rootDir, e.filePath(), files, excludeFilter)) return false; } // collect files entries = directory.entryInfoList(QDir::Files); for (const auto& e : entries) { if (excludeFilter && excludeFilter(e)) { QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); qDebug() << "Skipping file" << relativeFilePath; continue; } files->append(e); // we want the original paths for compressDirFiles } return true; } } // namespace MMCZip PrismLauncher-10.0.5/launcher/Version.cpp0000644000175100017510000001167415144136756017736 0ustar runnerrunner#include "Version.h" #include #include #include Version::Version(QString str) : m_string(std::move(str)) { parse(); } #define VERSION_OPERATOR(return_on_different) \ bool exclude_our_sections = false; \ bool exclude_their_sections = false; \ \ const auto size = qMax(m_sections.size(), other.m_sections.size()); \ for (int i = 0; i < size; ++i) { \ Section sec1 = (i >= m_sections.size()) ? Section() : m_sections.at(i); \ Section sec2 = (i >= other.m_sections.size()) ? Section() : other.m_sections.at(i); \ \ { /* Don't include appendixes in the comparison */ \ if (sec1.isAppendix()) \ exclude_our_sections = true; \ if (sec2.isAppendix()) \ exclude_their_sections = true; \ \ if (exclude_our_sections) { \ sec1 = Section(); \ if (sec2.m_isNull) \ break; \ } \ \ if (exclude_their_sections) { \ sec2 = Section(); \ if (sec1.m_isNull) \ break; \ } \ } \ \ if (sec1 != sec2) \ return return_on_different; \ } bool Version::operator<(const Version& other) const { VERSION_OPERATOR(sec1 < sec2) return false; } bool Version::operator==(const Version& other) const { VERSION_OPERATOR(false) return true; } bool Version::operator!=(const Version& other) const { return !operator==(other); } bool Version::operator<=(const Version& other) const { return *this < other || *this == other; } bool Version::operator>(const Version& other) const { return !(*this <= other); } bool Version::operator>=(const Version& other) const { return !(*this < other); } void Version::parse() { m_sections.clear(); QString currentSection; if (m_string.isEmpty()) return; auto classChange = [¤tSection](QChar lastChar, QChar currentChar) { if (lastChar.isNull()) return false; if (lastChar.isDigit() != currentChar.isDigit()) return true; const QList s_separators{ '.', '-', '+' }; if (s_separators.contains(currentChar) && currentSection.at(0) != currentChar) return true; return false; }; currentSection += m_string.at(0); for (int i = 1; i < m_string.size(); ++i) { const auto& current_char = m_string.at(i); if (classChange(m_string.at(i - 1), current_char)) { if (!currentSection.isEmpty()) m_sections.append(Section(currentSection)); currentSection = ""; } currentSection += current_char; } if (!currentSection.isEmpty()) m_sections.append(Section(currentSection)); } /// qDebug print support for the Version class QDebug operator<<(QDebug debug, const Version& v) { QDebugStateSaver saver(debug); debug.nospace() << "Version{ string: " << v.toString() << ", sections: [ "; bool first = true; for (auto s : v.m_sections) { if (!first) debug.nospace() << ", "; debug.nospace() << s.m_fullString; first = false; } debug.nospace() << " ]" << " }"; return debug; } PrismLauncher-10.0.5/launcher/ApplicationMessage.h0000644000175100017510000000034515144136756021517 0ustar runnerrunner#pragma once #include #include #include struct ApplicationMessage { QString command; QHash args; QByteArray serialize(); void parse(const QByteArray& input); }; PrismLauncher-10.0.5/launcher/BaseVersion.h0000644000175100017510000000303515144136756020166 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include /*! * An abstract base class for versions. */ class BaseVersion { public: using Ptr = std::shared_ptr; virtual ~BaseVersion() {} /*! * A string used to identify this version in config files. * This should be unique within the version list or shenanigans will occur. */ virtual QString descriptor() const = 0; /*! * The name of this version as it is displayed to the user. * For example: "1.5.1" */ virtual QString name() const = 0; /*! * This should return a string that describes * the kind of version this is (Stable, Beta, Snapshot, whatever) */ virtual QString typeString() const = 0; virtual bool operator<(BaseVersion& a) const { return name() < a.name(); } virtual bool operator>(BaseVersion& a) const { return name() > a.name(); } }; Q_DECLARE_METATYPE(BaseVersion::Ptr) PrismLauncher-10.0.5/launcher/BaseInstaller.cpp0000644000175100017510000000271115144136756021031 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include "BaseInstaller.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" BaseInstaller::BaseInstaller() {} bool BaseInstaller::isApplied(MinecraftInstance* on) { return QFile::exists(filename(on->instanceRoot())); } bool BaseInstaller::add(MinecraftInstance* to) { if (!patchesDir(to->instanceRoot()).exists()) { QDir(to->instanceRoot()).mkdir("patches"); } if (isApplied(to)) { if (!remove(to)) { return false; } } return true; } bool BaseInstaller::remove(MinecraftInstance* from) { return FS::deletePath(filename(from->instanceRoot())); } QString BaseInstaller::filename(const QString& root) const { return patchesDir(root).absoluteFilePath(id() + ".json"); } QDir BaseInstaller::patchesDir(const QString& root) const { return QDir(root + "/patches/"); } PrismLauncher-10.0.5/launcher/InstanceTask.cpp0000644000175100017510000000563015144136756020673 0ustar runnerrunner#include "InstanceTask.h" #include "Application.h" #include "settings/SettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" #include InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name) { auto dialog = CustomMessageBox::selectable(parent, QObject::tr("Change instance name"), QObject::tr("The instance's name seems to include the old version. Would you like to update it?\n\n" "Old name: %1\n" "New name: %2") .arg(old_name, new_name), QMessageBox::Question, QMessageBox::No | QMessageBox::Yes); auto result = dialog->exec(); if (result == QMessageBox::Yes) return InstanceNameChange::ShouldChange; return InstanceNameChange::ShouldKeep; } ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name) { if (APPLICATION->settings()->get("SkipModpackUpdatePrompt").toBool()) return ShouldUpdate::SkipUpdating; auto info = CustomMessageBox::selectable( parent, QObject::tr("Similar modpack was found!"), QObject::tr( "One or more of your instances are from this same modpack%1. Do you want to create a " "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") .arg(original_version_name), QMessageBox::Information, QMessageBox::Cancel); QAbstractButton* update = info->addButton(QObject::tr("Update existing instance"), QMessageBox::AcceptRole); QAbstractButton* skip = info->addButton(QObject::tr("Create new instance"), QMessageBox::ResetRole); info->exec(); if (info->clickedButton() == update) return ShouldUpdate::Update; if (info->clickedButton() == skip) return ShouldUpdate::SkipUpdating; return ShouldUpdate::Cancel; } QString InstanceName::name() const { if (!m_modified_name.isEmpty()) return modifiedName(); if (!m_original_version.isEmpty()) return QString("%1 %2").arg(m_original_name, m_original_version); return m_original_name; } QString InstanceName::originalName() const { return m_original_name; } QString InstanceName::modifiedName() const { if (!m_modified_name.isEmpty()) return m_modified_name; return m_original_name; } QString InstanceName::version() const { return m_original_version; } void InstanceName::setName(InstanceName& other) { m_original_name = other.m_original_name; m_original_version = other.m_original_version; m_modified_name = other.m_modified_name; } InstanceTask::InstanceTask() : Task(), InstanceName() {} PrismLauncher-10.0.5/launcher/MessageLevel.h0000644000175100017510000000315415144136756020324 0ustar runnerrunner#pragma once #include #include /** * @brief the MessageLevel Enum * defines what level a log message is */ struct MessageLevel { enum class Enum { Unknown, /**< No idea what this is or where it came from */ StdOut, /**< Undetermined stderr messages */ StdErr, /**< Undetermined stdout messages */ Launcher, /**< Launcher Messages */ Trace, /**< Trace Messages */ Debug, /**< Debug Messages */ Info, /**< Info Messages */ Message, /**< Standard Messages */ Warning, /**< Warnings */ Error, /**< Errors */ Fatal, /**< Fatal Errors */ }; using enum Enum; constexpr MessageLevel(Enum e = Unknown) : m_type(e) {} static MessageLevel fromName(const QString& type); static MessageLevel fromQtMsgType(const QtMsgType& type); static MessageLevel fromLine(const QString& line); inline bool isValid() const { return m_type != Unknown; } std::strong_ordering operator<=>(const MessageLevel& other) const = default; std::strong_ordering operator<=>(const MessageLevel::Enum& other) const { return m_type <=> other; } explicit operator int() const { return static_cast(m_type); } explicit operator MessageLevel::Enum() { return m_type; } /* Get message level from a line. Line is modified if it was successful. */ static MessageLevel takeFromLine(QString& line); /* Get message level from a line from the launcher log. Line is modified if it was successful. */ static MessageLevel takeFromLauncherLine(QString& line); private: Enum m_type; }; PrismLauncher-10.0.5/launcher/filelink/0000755000175100017510000000000015144136756017371 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/filelink/filelink_main.cpp0000644000175100017510000000234515144136756022702 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "FileLink.h" int main(int argc, char* argv[]) { FileLinkApp ldh(argc, argv); switch (ldh.status()) { case FileLinkApp::Starting: case FileLinkApp::Initialized: { return ldh.exec(); } case FileLinkApp::Failed: return 1; case FileLinkApp::Succeeded: return 0; default: return -1; } } PrismLauncher-10.0.5/launcher/filelink/FileLink.h0000644000175100017510000000361615144136756021245 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #pragma once #include #include #include #include #include #include #include #include #include #include #define PRISM_EXTERNAL_EXE #include "FileSystem.h" class FileLinkApp : public QCoreApplication { // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: enum Status { Starting, Failed, Succeeded, Initialized }; FileLinkApp(int& argc, char** argv); virtual ~FileLinkApp(); Status status() const { return m_status; } private: void joinServer(QString server); void readPathPairs(); void runLink(); void sendResults(); Status m_status = Status::Starting; bool m_useHardLinks = false; QDateTime m_startTime; QLocalSocket socket; QDataStream in; quint32 blockSize; QList m_links_to_make; QList m_path_results; #if defined Q_OS_WIN32 // used on Windows to attach the standard IO streams bool consoleAttached = false; #endif }; PrismLauncher-10.0.5/launcher/filelink/FileLink.cpp0000644000175100017510000001764315144136756021605 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "FileLink.h" #include "BuildConfig.h" #include "StringUtils.h" #include #include #include #include #include #include #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include "console/WindowsConsole.h" #endif #include namespace fs = std::filesystem; FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), socket(new QLocalSocket(this)) { #if defined Q_OS_WIN32 // attach the parent console if (AttachWindowsConsole()) { consoleAttached = true; } #endif setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME + "FileLink"); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); // Commandline parsing QCommandLineParser parser; parser.setApplicationDescription(QObject::tr("a batch MKLINK program for windows to be used with prismlauncher")); parser.addOptions({ { { "s", "server" }, "Join the specified server on launch", "pipe name" }, { { "H", "hard" }, "use hard links instead of symbolic", "true/false" } }); parser.addHelpOption(); parser.addVersionOption(); parser.process(arguments()); QString serverToJoin = parser.value("server"); m_useHardLinks = QVariant(parser.value("hard")).toBool(); qDebug() << "link program launched"; if (!serverToJoin.isEmpty()) { qDebug() << "joining server" << serverToJoin; joinServer(serverToJoin); } else { qDebug() << "no server to join"; m_status = Failed; exit(); } } void FileLinkApp::joinServer(QString server) { blockSize = 0; in.setDevice(&socket); connect(&socket, &QLocalSocket::connected, this, []() { qDebug() << "connected to server"; }); connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); connect(&socket, &QLocalSocket::errorOccurred, this, [this](QLocalSocket::LocalSocketError socketError) { m_status = Failed; switch (socketError) { case QLocalSocket::ServerNotFoundError: qDebug() << ("The host was not found. Please make sure " "that the server is running and that the " "server name is correct."); break; case QLocalSocket::ConnectionRefusedError: qDebug() << ("The connection was refused by the peer. " "Make sure the server is running, " "and check that the server name " "is correct."); break; case QLocalSocket::PeerClosedError: qDebug() << ("The connection was closed by the peer. "); break; default: qDebug() << "The following error occurred:" << socket.errorString(); } }); connect(&socket, &QLocalSocket::disconnected, this, [this]() { qDebug() << "disconnected from server, should exit"; m_status = Succeeded; exit(); }); socket.connectToServer(server); } void FileLinkApp::runLink() { std::error_code os_err; qDebug() << "creating links"; for (auto link : m_links_to_make) { QString src_path = link.src; QString dst_path = link.dst; FS::ensureFilePathExists(dst_path); if (m_useHardLinks) { qDebug() << "making hard link:" << src_path << "to" << dst_path; fs::create_hard_link(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); } else if (fs::is_directory(StringUtils::toStdString(src_path))) { qDebug() << "making directory_symlink:" << src_path << "to" << dst_path; fs::create_directory_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); } else { qDebug() << "making symlink:" << src_path << "to" << dst_path; fs::create_symlink(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), os_err); } if (os_err) { qWarning() << "Failed to link files:" << QString::fromStdString(os_err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; qDebug() << "Error category:" << os_err.category().name(); qDebug() << "Error code:" << os_err.value(); FS::LinkResult result = { src_path, dst_path, QString::fromStdString(os_err.message()), os_err.value() }; m_path_results.append(result); } else { FS::LinkResult result = { src_path, dst_path, "", 0 }; m_path_results.append(result); } } sendResults(); qDebug() << "done, should exit soon"; } void FileLinkApp::sendResults() { // construct block of data to send QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); qint32 blocksize = quint32(sizeof(quint32)); for (auto result : m_path_results) { blocksize += quint32(result.src.size()); blocksize += quint32(result.dst.size()); blocksize += quint32(result.err_msg.size()); blocksize += quint32(sizeof(quint32)); } qDebug() << "About to write block of size:" << blocksize; out << blocksize; out << quint32(m_path_results.length()); for (auto result : m_path_results) { out << result.src; out << result.dst; out << result.err_msg; out << quint32(result.err_value); } qint64 byteswritten = socket.write(block); bool bytesflushed = socket.flush(); qDebug() << "block flushed" << byteswritten << bytesflushed; } void FileLinkApp::readPathPairs() { m_links_to_make.clear(); qDebug() << "Reading path pairs from server"; qDebug() << "bytes available" << socket.bytesAvailable(); if (blockSize == 0) { // Relies on the fact that QDataStream serializes a quint32 into // sizeof(quint32) bytes if (socket.bytesAvailable() < (int)sizeof(quint32)) return; qDebug() << "reading block size"; in >> blockSize; } qDebug() << "blocksize is" << blockSize; qDebug() << "bytes available" << socket.bytesAvailable(); if (socket.bytesAvailable() < blockSize || in.atEnd()) return; quint32 numLinks; in >> numLinks; qDebug() << "numLinks" << numLinks; for (quint32 i = 0; i < numLinks; i++) { FS::LinkPair pair; in >> pair.src; in >> pair.dst; qDebug() << "link" << pair.src << "to" << pair.dst; m_links_to_make.append(pair); } runLink(); } FileLinkApp::~FileLinkApp() { qDebug() << "link program shutting down"; // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); #if defined Q_OS_WIN32 // Detach from Windows console if (consoleAttached) { fclose(stdout); fclose(stdin); fclose(stderr); } #endif } PrismLauncher-10.0.5/launcher/filelink/filelink.exe.manifest0000644000175100017510000000177515144136756023510 0ustar runnerrunner PrismLauncher-10.0.5/launcher/InstanceImportTask.cpp0000644000175100017510000004134315144136756022067 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "InstanceImportTask.h" #include "Application.h" #include "FileSystem.h" #include "NullInstance.h" #include "QObjectPtr.h" #include "archive/ArchiveReader.h" #include "archive/ExtractZipTask.h" #include "icons/IconList.h" #include "icons/IconUtils.h" #include "modplatform/flame/FlameInstanceCreationTask.h" #include "modplatform/modrinth/ModrinthInstanceCreationTask.h" #include "modplatform/technic/TechnicPackProcessor.h" #include "settings/INISettingsObject.h" #include "tasks/Task.h" #include "net/ApiDownload.h" #include #include #include InstanceImportTask::InstanceImportTask(const QUrl& sourceUrl, QWidget* parent, QMap&& extra_info) : m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent) {} bool InstanceImportTask::abort() { if (!canAbort()) return false; bool wasAborted = false; if (m_task) wasAborted = m_task->abort(); return wasAborted; } void InstanceImportTask::executeTask() { setAbortable(true); if (m_sourceUrl.isLocalFile()) { m_archivePath = m_sourceUrl.toLocalFile(); processZipPack(); } else { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); downloadFromUrl(); } } void InstanceImportTask::downloadFromUrl() { const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path()); auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); m_archivePath = entry->getFullPath(); auto filesNetJob = makeShared(tr("Modpack download"), APPLICATION->network()); filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); connect(filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::processZipPack); connect(filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::setProgress); connect(filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::emitFailed); connect(filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::emitAborted); m_task.reset(filesNetJob); filesNetJob->start(); } QString cleanPath(QString path) { if (path == ".") return QString(); QString result = path; if (result.startsWith("./")) result = result.mid(2); return result; } void InstanceImportTask::processZipPack() { setStatus(tr("Attempting to determine instance type")); QDir extractDir(m_stagingPath); qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it MMCZip::ArchiveReader packZip(m_archivePath); qDebug() << "Attempting to determine instance type"; QString root; // NOTE: Prioritize modpack platforms that aren't searched for recursively. // Especially Flame has a very common filename for its manifest, which may appear inside overrides for example // https://docs.modrinth.com/docs/modpacks/format_definition/#storage auto detectInstance = [this, &extractDir, &root](MMCZip::ArchiveReader::File* f, bool& stop) { if (!isRunning()) { stop = true; return true; } auto fileName = f->filename(); if (fileName == "modrinth.index.json") { // process as Modrinth pack qDebug() << "Modrinth:" << true; m_modpackType = ModpackType::Modrinth; stop = true; } else if (fileName == "bin/modpack.jar" || fileName == "bin/version.json") { // process as Technic pack qDebug() << "Technic:" << true; extractDir.mkpath("minecraft"); extractDir.cd("minecraft"); m_modpackType = ModpackType::Technic; stop = true; } else if (fileName == "manifest.json") { qDebug() << "Flame:" << true; m_modpackType = ModpackType::Flame; stop = true; } else if (QFileInfo fileInfo(fileName); fileInfo.fileName() == "instance.cfg") { qDebug() << "MultiMC:" << true; m_modpackType = ModpackType::MultiMC; root = cleanPath(fileInfo.path()); stop = true; } QCoreApplication::processEvents(); return true; }; if (!packZip.parse(detectInstance)) { emitFailed(tr("Unable to open supplied modpack zip file.")); return; } if (m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); return; } setStatus(tr("Extracting modpack")); // make sure we extract just the pack auto zipTask = makeShared(m_archivePath, extractDir, root); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(zipTask.get(), &Task::succeeded, this, &InstanceImportTask::extractFinished, Qt::QueuedConnection); connect(zipTask.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(zipTask.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(zipTask); zipTask->start(); } void InstanceImportTask::extractFinished() { setAbortable(false); QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; QDirIterator it(extractDir, QDirIterator::Subdirectories); while (it.hasNext()) { auto filepath = it.next(); QFileInfo file(filepath); auto permissions = QFile::permissions(filepath); auto origPermissions = permissions; if (file.isDir()) { // Folder +rwx for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; } else { // File +rw for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; } if (origPermissions != permissions) { if (!QFile::setPermissions(filepath, permissions)) { logWarning(tr("Could not fix permissions for %1").arg(filepath)); } else { qDebug() << "Fixed" << filepath; } } } switch (m_modpackType) { case ModpackType::MultiMC: processMultiMC(); return; case ModpackType::Technic: processTechnic(); return; case ModpackType::Flame: processFlame(); return; case ModpackType::Modrinth: processModrinth(); return; case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; } } bool installIcon(QString root, QString instIconKey) { auto importIconPath = IconUtils::findBestIconIn(root, instIconKey); if (importIconPath.isNull() || !QFile::exists(importIconPath)) importIconPath = IconUtils::findBestIconIn(root, "icon.png"); if (importIconPath.isNull() || !QFile::exists(importIconPath)) importIconPath = IconUtils::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png"); if (!importIconPath.isNull() && QFile::exists(importIconPath)) { // import icon auto iconList = APPLICATION->icons(); if (iconList->iconFileExists(instIconKey)) { iconList->deleteIcon(instIconKey); } iconList->installIcon(importIconPath, instIconKey + "." + QFileInfo(importIconPath).suffix()); return true; } return false; } void InstanceImportTask::processFlame() { shared_qobject_ptr inst_creation_task = nullptr; if (!m_extra_info.isEmpty()) { auto pack_id_it = m_extra_info.constFind("pack_id"); Q_ASSERT(pack_id_it != m_extra_info.constEnd()); auto pack_id = pack_id_it.value(); auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); Q_ASSERT(pack_version_id_it != m_extra_info.constEnd()); auto pack_version_id = pack_version_id_it.value(); QString original_instance_id; auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); if (original_instance_id_it != m_extra_info.constEnd()) original_instance_id = original_instance_id_it.value(); inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); } else { // FIXME: Find a way to get IDs in directly imported ZIPs inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, QString(), QString()); } inst_creation_task->setName(*this); // if the icon was specified by user, use that. otherwise pull icon from the pack if (m_instIcon == "default") { auto iconKey = QString("Flame_%1_Icon").arg(name()); if (installIcon(m_stagingPath, iconKey)) { m_instIcon = iconKey; } } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); auto weak = inst_creation_task.toWeakRef(); connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { if (auto sp = weak.lock()) { setOverride(sp->shouldOverride(), sp->originalInstanceID()); } emitSucceeded(); }); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(inst_creation_task); setAbortable(true); m_task->start(); } void InstanceImportTask::processTechnic() { shared_qobject_ptr packProcessor{ new Technic::TechnicPackProcessor }; connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath); } void InstanceImportTask::processMultiMC() { QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath); // reset time played on import... because packs. instance.resetTimePlayed(); // set a new nice name instance.setName(name()); // if the icon was specified by user, use that. otherwise pull icon from the pack if (m_instIcon != "default") { instance.setIconKey(m_instIcon); } else { m_instIcon = instance.iconKey(); installIcon(instance.instanceRoot(), m_instIcon); } emitSucceeded(); } void InstanceImportTask::processModrinth() { shared_qobject_ptr inst_creation_task = nullptr; if (!m_extra_info.isEmpty()) { auto pack_id_it = m_extra_info.constFind("pack_id"); Q_ASSERT(pack_id_it != m_extra_info.constEnd()); auto pack_id = pack_id_it.value(); QString pack_version_id; auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); if (pack_version_id_it != m_extra_info.constEnd()) pack_version_id = pack_version_id_it.value(); QString original_instance_id; auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); if (original_instance_id_it != m_extra_info.constEnd()) original_instance_id = original_instance_id_it.value(); inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); } else { QString pack_id; if (!m_sourceUrl.isEmpty()) { static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)"); pack_id = s_regex.match(m_sourceUrl.toString()).captured(1); } // FIXME: Find a way to get the ID in directly imported ZIPs inst_creation_task = makeShared(m_stagingPath, m_globalSettings, m_parent, pack_id); } inst_creation_task->setName(*this); // if the icon was specified by user, use that. otherwise pull icon from the pack if (m_instIcon == "default") { auto iconKey = QString("Modrinth_%1_Icon").arg(name()); if (installIcon(m_stagingPath, iconKey)) { m_instIcon = iconKey; } } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); auto weak = inst_creation_task.toWeakRef(); connect(inst_creation_task.get(), &Task::succeeded, this, [this, weak] { if (auto sp = weak.lock()) { setOverride(sp->shouldOverride(), sp->originalInstanceID()); } emitSucceeded(); }); connect(inst_creation_task.get(), &Task::failed, this, &InstanceImportTask::emitFailed); connect(inst_creation_task.get(), &Task::progress, this, &InstanceImportTask::setProgress); connect(inst_creation_task.get(), &Task::stepProgress, this, &InstanceImportTask::propagateStepProgress); connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(inst_creation_task); setAbortable(true); m_task->start(); } PrismLauncher-10.0.5/launcher/QVariantUtils.h0000644000175100017510000000415515144136756020520 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include namespace QVariantUtils { template inline QList toList(QVariant src) { QVariantList variantList = src.toList(); QList list_t; list_t.reserve(variantList.size()); for (const QVariant& v : variantList) { list_t.append(v.value()); } return list_t; } template inline QVariant fromList(QList val) { QVariantList variantList; variantList.reserve(val.size()); for (const T& v : val) { variantList.append(v); } return variantList; } } // namespace QVariantUtils PrismLauncher-10.0.5/launcher/BaseVersionList.cpp0000644000175100017510000000735715144136756021370 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "BaseVersionList.h" #include "BaseVersion.h" BaseVersionList::BaseVersionList(QObject* parent) : QAbstractListModel(parent) {} BaseVersion::Ptr BaseVersionList::findVersion(const QString& descriptor) { for (int i = 0; i < count(); i++) { if (at(i)->descriptor() == descriptor) return at(i); } return nullptr; } BaseVersion::Ptr BaseVersionList::getRecommended() const { if (count() <= 0) return nullptr; else return at(0); } QVariant BaseVersionList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() > count()) return QVariant(); BaseVersion::Ptr version = at(index.row()); switch (role) { case VersionPointerRole: return QVariant::fromValue(version); case VersionRole: return version->name(); case VersionIdRole: return version->descriptor(); case TypeRole: return version->typeString(); case JavaMajorRole: { auto major = version->name(); if (major.startsWith("java")) { major = "Java " + major.mid(4); } return major; } default: return QVariant(); } } BaseVersionList::RoleList BaseVersionList::providesRoles() const { return { VersionPointerRole, VersionRole, VersionIdRole, TypeRole }; } int BaseVersionList::rowCount(const QModelIndex& parent) const { // Return count return parent.isValid() ? 0 : count(); } int BaseVersionList::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } QHash BaseVersionList::roleNames() const { QHash roles = QAbstractListModel::roleNames(); roles.insert(VersionRole, "version"); roles.insert(VersionIdRole, "versionId"); roles.insert(ParentVersionRole, "parentGameVersion"); roles.insert(RecommendedRole, "recommended"); roles.insert(LatestRole, "latest"); roles.insert(TypeRole, "type"); roles.insert(BranchRole, "branch"); roles.insert(PathRole, "path"); roles.insert(JavaNameRole, "javaName"); roles.insert(CPUArchitectureRole, "architecture"); roles.insert(JavaMajorRole, "javaMajor"); return roles; } PrismLauncher-10.0.5/launcher/tasks/0000755000175100017510000000000015144136756016721 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/tasks/Task.cpp0000644000175100017510000001322015144136756020325 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Task.h" #include #include "AssertHelpers.h" Q_LOGGING_CATEGORY(taskLogC, "launcher.task") Task::Task(bool show_debug) : m_show_debug(show_debug) { m_uid = QUuid::createUuid(); setAutoDelete(false); } void Task::setStatus(const QString& new_status) { if (m_status != new_status) { m_status = new_status; emit status(m_status); } } void Task::setDetails(const QString& new_details) { if (m_details != new_details) { m_details = new_details; emit details(m_details); } } void Task::setProgress(qint64 current, qint64 total) { if ((m_progress != current) || (m_progressTotal != total)) { m_progress = current; m_progressTotal = total; emit progress(m_progress, m_progressTotal); } } void Task::start() { switch (m_state) { case State::Inactive: { if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "starting for the first time"; break; } case State::AbortedByUser: { if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "restarting for after being aborted by user"; break; } case State::Failed: { if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "restarting for after failing at first"; break; } case State::Succeeded: { if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "restarting for after succeeding at first"; break; } case State::Running: { if (ASSERT_NEVER(isRunning()) && m_show_debug) qCWarning(taskLogC) << "The launcher tried to start task" << describe() << "while it was already running!"; return; } } // NOTE: only fall through to here in end states m_state = State::Running; emit started(); executeTask(); } void Task::emitFailed(QString reason) { // Don't fail twice. if (ASSERT_NEVER(!isRunning())) { qCCritical(taskLogC) << "Task" << describe() << "failed while not running!!!!:" << reason; return; } m_state = State::Failed; m_failReason = reason; qCCritical(taskLogC) << "Task" << describe() << "failed:" << reason; emit failed(reason); emit finished(); } void Task::emitAborted() { // Don't abort twice. if (ASSERT_NEVER(!isRunning())) { qCCritical(taskLogC) << "Task" << describe() << "aborted while not running!!!!"; return; } m_state = State::AbortedByUser; m_failReason = "Aborted."; if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "aborted."; emit aborted(); emit finished(); } void Task::emitSucceeded() { // Don't succeed twice. if (ASSERT_NEVER(!isRunning())) { qCCritical(taskLogC) << "Task" << describe() << "succeeded while not running!!!!"; return; } m_state = State::Succeeded; if (m_show_debug) qCDebug(taskLogC) << "Task" << describe() << "succeeded"; emit succeeded(); emit finished(); } void Task::propagateStepProgress(TaskStepProgress const& task_progress) { emit stepProgress(task_progress); } QString Task::describe() { QString outStr; QTextStream out(&outStr); out << metaObject()->className() << QChar('('); auto name = objectName(); if (name.isEmpty()) { out << QString("0x%1").arg(reinterpret_cast(this), 0, 16); } else { out << name; } out << " ID: " << m_uid.toString(QUuid::WithoutBraces); out << QChar(')'); out.flush(); return outStr; } bool Task::isRunning() const { return m_state == State::Running; } bool Task::isFinished() const { return m_state != State::Running && m_state != State::Inactive; } bool Task::wasSuccessful() const { return m_state == State::Succeeded; } QString Task::failReason() const { return m_failReason; } void Task::logWarning(const QString& line) { qWarning() << line; m_Warnings.append(line); emit warningLogged(line); } QStringList Task::warnings() const { return m_Warnings; } PrismLauncher-10.0.5/launcher/tasks/SequentialTask.cpp0000644000175100017510000000410315144136756022360 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "SequentialTask.h" #include #include "tasks/ConcurrentTask.h" SequentialTask::SequentialTask(QString task_name) : ConcurrentTask(task_name, 1) {} void SequentialTask::subTaskFailed(Task::Ptr task, const QString& msg) { qWarning() << msg; ConcurrentTask::subTaskFailed(task, msg); emitFailed(msg); } void SequentialTask::updateState() { setProgress(m_done.count(), totalSize()); setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(totalSize()))); } PrismLauncher-10.0.5/launcher/tasks/ConcurrentTask.cpp0000644000175100017510000001752715144136756022406 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ConcurrentTask.h" #include #include "tasks/Task.h" ConcurrentTask::ConcurrentTask(QString task_name, int max_concurrent) : Task(), m_total_max_size(max_concurrent) { setObjectName(task_name); } ConcurrentTask::~ConcurrentTask() { for (auto task : m_doing) { if (task) task->disconnect(this); } } auto ConcurrentTask::getStepProgress() const -> TaskStepProgressList { return m_task_progress.values(); } void ConcurrentTask::addTask(Task::Ptr task) { m_queue.append(task); } void ConcurrentTask::executeTask() { for (auto i = 0; i < m_total_max_size; i++) QMetaObject::invokeMethod(this, &ConcurrentTask::executeNextSubTask, Qt::QueuedConnection); } bool ConcurrentTask::abort() { m_queue.clear(); if (m_doing.isEmpty()) { // Don't call emitAborted() here, we want to bypass the 'is the task running' check emit aborted(); emit finished(); return true; } bool suceedeed = true; QMutableHashIterator doing_iter(m_doing); while (doing_iter.hasNext()) { auto task = doing_iter.next(); disconnect(task->get(), &Task::aborted, this, 0); suceedeed &= (task.value())->abort(); } if (suceedeed) emitAborted(); else emitFailed(tr("Failed to abort all running tasks.")); return suceedeed; } void ConcurrentTask::clear() { Q_ASSERT(!isRunning()); m_done.clear(); m_failed.clear(); m_queue.clear(); m_task_progress.clear(); m_progress = 0; } void ConcurrentTask::executeNextSubTask() { if (!isRunning()) { return; } if (m_doing.count() >= m_total_max_size) { return; } if (m_queue.isEmpty()) { if (m_doing.isEmpty()) { if (m_failed.isEmpty()) { emitSucceeded(); } else if (m_failed.count() == 1) { auto task = m_failed.keys().first(); auto reason = task->failReason(); if (reason.isEmpty()) { // clearly a bug somewhere reason = tr("Task failed"); } emitFailed(reason); } else { QStringList failReason; for (auto t : m_failed) { auto reason = t->failReason(); if (!reason.isEmpty()) { failReason << reason; } } if (failReason.isEmpty()) { emitFailed(tr("Multiple subtasks failed")); } else { emitFailed(tr("Multiple subtasks failed\n%1").arg(failReason.join("\n"))); } } } return; } startSubTask(m_queue.dequeue()); } void ConcurrentTask::startSubTask(Task::Ptr next) { connect(next.get(), &Task::succeeded, this, [this, next]() { subTaskSucceeded(next); }); connect(next.get(), &Task::failed, this, [this, next](QString msg) { subTaskFailed(next, msg); }); // this should never happen but if it does, it's better to fail the task than get stuck connect(next.get(), &Task::aborted, this, [this, next] { subTaskFailed(next, "Aborted"); }); connect(next.get(), &Task::status, this, [this, next](QString msg) { subTaskStatus(next, msg); }); connect(next.get(), &Task::details, this, [this, next](QString msg) { subTaskDetails(next, msg); }); connect(next.get(), &Task::stepProgress, this, &ConcurrentTask::stepProgress); connect(next.get(), &Task::progress, this, [this, next](qint64 current, qint64 total) { subTaskProgress(next, current, total); }); m_doing.insert(next.get(), next); auto task_progress = std::make_shared(next->getUid()); m_task_progress.insert(next->getUid(), task_progress); updateState(); QMetaObject::invokeMethod(next.get(), &Task::start, Qt::QueuedConnection); } void ConcurrentTask::subTaskFinished(Task::Ptr task, TaskStepState state) { m_done.insert(task.get(), task); (state == TaskStepState::Succeeded ? m_succeeded : m_failed).insert(task.get(), task); m_doing.remove(task.get()); auto task_progress = *m_task_progress.value(task->getUid()); task_progress.state = state; m_task_progress.remove(task->getUid()); disconnect(task.get(), 0, this, 0); emit stepProgress(task_progress); updateState(); QMetaObject::invokeMethod(this, &ConcurrentTask::executeNextSubTask, Qt::QueuedConnection); } void ConcurrentTask::subTaskSucceeded(Task::Ptr task) { subTaskFinished(task, TaskStepState::Succeeded); } void ConcurrentTask::subTaskFailed(Task::Ptr task, [[maybe_unused]] const QString& msg) { subTaskFinished(task, TaskStepState::Failed); } void ConcurrentTask::subTaskStatus(Task::Ptr task, const QString& msg) { auto task_progress = m_task_progress.value(task->getUid()); task_progress->status = msg; task_progress->state = TaskStepState::Running; emit stepProgress(*task_progress); if (totalSize() == 1) { setStatus(msg); } } void ConcurrentTask::subTaskDetails(Task::Ptr task, const QString& msg) { auto task_progress = m_task_progress.value(task->getUid()); task_progress->details = msg; task_progress->state = TaskStepState::Running; emit stepProgress(*task_progress); if (totalSize() == 1) { setDetails(msg); } } void ConcurrentTask::subTaskProgress(Task::Ptr task, qint64 current, qint64 total) { auto task_progress = m_task_progress.value(task->getUid()); task_progress->update(current, total); emit stepProgress(*task_progress); updateState(); if (totalSize() == 1) { setProgress(task_progress->current, task_progress->total); } } void ConcurrentTask::updateState() { if (totalSize() > 1) { setProgress(m_done.count(), totalSize()); setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } else { QString status = tr("Please wait..."); if (m_queue.size() > 0) { status = tr("Waiting for a task to start..."); } else if (m_doing.size() > 0) { status = tr("Executing 1 task:"); } else if (m_done.size() > 0) { status = tr("Task finished."); } setStatus(status); } } PrismLauncher-10.0.5/launcher/tasks/ConcurrentTask.h0000644000175100017510000000707615144136756022051 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include "tasks/Task.h" /*! * Runs a list of tasks concurrently (according to `max_concurrent` parameter). * Behaviour is the same as regular Task (e.g. starts using start()) */ class ConcurrentTask : public Task { Q_OBJECT public: using Ptr = shared_qobject_ptr; explicit ConcurrentTask(QString task_name = "", int max_concurrent = 6); ~ConcurrentTask() override; // safe to call before starting the task void setMaxConcurrent(int max_concurrent) { m_total_max_size = max_concurrent; } bool canAbort() const override { return true; } inline auto isMultiStep() const -> bool override { return totalSize() > 1; } auto getStepProgress() const -> TaskStepProgressList override; //! Adds a task to execute in this ConcurrentTask void addTask(Task::Ptr task); public slots: bool abort() override; /** Resets the internal state of the task. * This allows the same task to be re-used. */ void clear(); protected slots: void executeTask() override; virtual void executeNextSubTask(); void subTaskSucceeded(Task::Ptr); virtual void subTaskFailed(Task::Ptr, const QString& msg); void subTaskFinished(Task::Ptr, TaskStepState); void subTaskStatus(Task::Ptr task, const QString& msg); void subTaskDetails(Task::Ptr task, const QString& msg); void subTaskProgress(Task::Ptr task, qint64 current, qint64 total); protected: // NOTE: This is not thread-safe. unsigned int totalSize() const { return static_cast(m_queue.size() + m_doing.size() + m_done.size()); } virtual void updateState(); void startSubTask(Task::Ptr task); protected: QQueue m_queue; QHash m_doing; QHash m_done; QHash m_failed; QHash m_succeeded; QHash> m_task_progress; int m_total_max_size; }; PrismLauncher-10.0.5/launcher/tasks/Task.h0000644000175100017510000001504215144136756017776 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PrismLauncher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include "QObjectPtr.h" Q_DECLARE_LOGGING_CATEGORY(taskLogC) enum class TaskStepState { Waiting, Running, Failed, Succeeded }; Q_DECLARE_METATYPE(TaskStepState) struct TaskStepProgress { QUuid uid; qint64 current = 0; qint64 total = -1; qint64 old_current = 0; qint64 old_total = -1; QString status = ""; QString details = ""; TaskStepState state = TaskStepState::Waiting; TaskStepProgress() { this->uid = QUuid::createUuid(); } TaskStepProgress(QUuid uid_) : uid(uid_) {} bool isDone() const { return (state == TaskStepState::Failed) || (state == TaskStepState::Succeeded); } void update(qint64 new_current, qint64 new_total) { this->old_current = this->current; this->old_total = this->total; this->current = new_current; this->total = new_total; this->state = TaskStepState::Running; } }; Q_DECLARE_METATYPE(TaskStepProgress) using TaskStepProgressList = QList>; /*! * Represents a task that has to be done. * To create a task, you need to subclass this class, implement the executeTask() method and call * emitSucceeded() or emitFailed() when the task is done. * the caller needs to call start() to start the task. */ class Task : public QObject, public QRunnable { Q_OBJECT public: using Ptr = shared_qobject_ptr; enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser }; public: explicit Task(bool show_debug_log = true); virtual ~Task() = default; bool isRunning() const; bool isFinished() const; bool wasSuccessful() const; /*! * MultiStep tasks are combinations of multiple tasks into a single logical task. * The main usage of this is in SequencialTask. */ virtual auto isMultiStep() const -> bool { return false; } /*! * Returns the string that was passed to emitFailed as the error message when the task failed. * If the task hasn't failed, returns an empty string. */ QString failReason() const; virtual QStringList warnings() const; virtual bool canAbort() const { return m_can_abort; } auto getState() const -> State { return m_state; } QString getStatus() { return m_status; } QString getDetails() { return m_details; } qint64 getProgress() { return m_progress; } qint64 getTotalProgress() { return m_progressTotal; } virtual auto getStepProgress() const -> TaskStepProgressList { return {}; } QUuid getUid() { return m_uid; } protected: void logWarning(const QString& line); private: QString describe(); signals: void started(); void progress(qint64 current, qint64 total); //! called when a task has either succeeded, aborted or failed. void finished(); //! called when a task has succeeded void succeeded(); //! called when a task has been aborted by calling abort() void aborted(); void failed(QString reason); void status(QString status); void details(QString details); void warningLogged(const QString& warning); void stepProgress(TaskStepProgress const& task_progress); //! Emitted when the canAbort() status has changed. */ void abortStatusChanged(bool can_abort); public slots: // QRunnable's interface void run() override { start(); } //! used by the task caller to start the task virtual void start(); //! used by external code to ask the task to abort virtual bool abort() { if (canAbort()) emitAborted(); return canAbort(); } void setAbortable(bool can_abort) { m_can_abort = can_abort; emit abortStatusChanged(can_abort); } protected: //! The task subclass must implement this method. This method is called to start to run the task. //! The task is not finished when this method returns. the subclass must manually call emitSucceeded() or emitFailed() instead. virtual void executeTask() = 0; protected slots: //! The Task subclass must call this method when the task has succeeded virtual void emitSucceeded(); //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead. virtual void emitAborted(); //! The Task subclass must call this method when the task has failed virtual void emitFailed(QString reason = ""); virtual void propagateStepProgress(TaskStepProgress const& task_progress); public slots: void setStatus(const QString& status); void setDetails(const QString& details); void setProgress(qint64 current, qint64 total); protected: State m_state = State::Inactive; QStringList m_Warnings; QString m_failReason = ""; QString m_status; QString m_details; int m_progress = 0; int m_progressTotal = 100; // TODO: Nuke in favor of QLoggingCategory bool m_show_debug = true; private: // Change using setAbortStatus bool m_can_abort = false; QUuid m_uid; }; PrismLauncher-10.0.5/launcher/tasks/MultipleOptionsTask.h0000644000175100017510000000367215144136756023074 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "ConcurrentTask.h" /* This task type will attempt to do run each of it's subtasks in sequence, * until one of them succeeds. When that happens, the remaining tasks will not run. * */ class MultipleOptionsTask : public ConcurrentTask { Q_OBJECT public: explicit MultipleOptionsTask(const QString& task_name = ""); ~MultipleOptionsTask() override = default; private slots: void executeNextSubTask() override; void updateState() override; }; PrismLauncher-10.0.5/launcher/tasks/MultipleOptionsTask.cpp0000644000175100017510000000424115144136756023420 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MultipleOptionsTask.h" #include MultipleOptionsTask::MultipleOptionsTask(const QString& task_name) : ConcurrentTask(task_name, 1) {} void MultipleOptionsTask::executeNextSubTask() { if (m_done.size() != m_failed.size()) { emitSucceeded(); return; } if (m_queue.isEmpty()) { emitFailed(tr("All attempts have failed!")); qWarning() << "All attempts have failed!"; return; } ConcurrentTask::executeNextSubTask(); } void MultipleOptionsTask::updateState() { setProgress(m_done.count(), totalSize()); setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(totalSize()))); } PrismLauncher-10.0.5/launcher/tasks/SequentialTask.h0000644000175100017510000000424615144136756022035 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "ConcurrentTask.h" /** A concurrent task that only allows one concurrent task :) * * This should be used when there's a need to maintain a strict ordering of task executions, and * the starting of a task is contingent on the success of the previous one. * * See MultipleOptionsTask if that's not the case. */ class SequentialTask : public ConcurrentTask { Q_OBJECT public: explicit SequentialTask(QString task_name = ""); ~SequentialTask() override = default; protected slots: virtual void subTaskFailed(Task::Ptr, const QString& msg) override; protected: void updateState() override; }; PrismLauncher-10.0.5/launcher/icons/0000755000175100017510000000000015144136756016707 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/icons/MMCIcon.cpp0000644000175100017510000000730115144136756020641 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MMCIcon.h" #include #include IconType operator--(IconType& t, int) { IconType temp = t; switch (t) { case IconType::Builtin: t = IconType::ToBeDeleted; break; case IconType::Transient: t = IconType::Builtin; break; case IconType::FileBased: t = IconType::Transient; break; default: break; } return temp; } IconType MMCIcon::type() const { return m_current_type; } QString MMCIcon::name() const { if (m_name.size()) return m_name; return m_key; } bool MMCIcon::has(IconType _type) const { return m_images[_type].present(); } QIcon MMCIcon::icon() const { if (m_current_type == IconType::ToBeDeleted) return QIcon(); auto& icon = m_images[m_current_type].icon; if (!icon.isNull()) return icon; // FIXME: inject this. return QIcon::fromTheme(m_images[m_current_type].key); } void MMCIcon::remove(IconType rm_type) { m_images[rm_type].filename = QString(); m_images[rm_type].icon = QIcon(); for (auto iter = rm_type; iter != IconType::ToBeDeleted; iter--) { if (m_images[iter].present()) { m_current_type = iter; return; } } m_current_type = IconType::ToBeDeleted; } void MMCIcon::replace(IconType new_type, QIcon icon, QString path) { if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted) { m_current_type = new_type; } m_images[new_type].icon = icon; m_images[new_type].filename = path; m_images[new_type].key = QString(); } void MMCIcon::replace(IconType new_type, const QString& key) { if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted) { m_current_type = new_type; } m_images[new_type].icon = QIcon(); m_images[new_type].filename = QString(); m_images[new_type].key = key; } QString MMCIcon::getFilePath() const { if (m_current_type == IconType::ToBeDeleted) { return QString(); } return m_images[m_current_type].filename; } bool MMCIcon::isBuiltIn() const { return m_current_type == IconType::Builtin; } PrismLauncher-10.0.5/launcher/icons/IconList.h0000644000175100017510000000751315144136756020612 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include "MMCIcon.h" #include "settings/Setting.h" #include "QObjectPtr.h" class QFileSystemWatcher; class IconList : public QAbstractListModel { Q_OBJECT public: explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0); virtual ~IconList() {}; QIcon getIcon(const QString& key) const; int getIconIndex(const QString& key) const; QString getDirectory() const; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; virtual QStringList mimeTypes() const override; virtual Qt::DropActions supportedDropActions() const override; virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; virtual Qt::ItemFlags flags(const QModelIndex& index) const override; bool addThemeIcon(const QString& key); bool addIcon(const QString& key, const QString& name, const QString& path, IconType type); void saveIcon(const QString& key, const QString& path, const char* format) const; bool deleteIcon(const QString& key); bool trashIcon(const QString& key); bool iconFileExists(const QString& key) const; QString iconDirectory(const QString& key) const; void installIcons(const QStringList& iconFiles); void installIcon(const QString& file, const QString& name); const MMCIcon* icon(const QString& key) const; void startWatching(); void stopWatching(); signals: void iconUpdated(QString key); private: // hide copy constructor IconList(const IconList&) = delete; // hide assign op IconList& operator=(const IconList&) = delete; void reindex(); void sortIconList(); bool addPathRecursively(const QString& path); QStringList getIconFilePaths() const; public slots: void directoryChanged(const QString& path); protected slots: void fileChanged(const QString& path); void SettingChanged(const Setting& setting, const QVariant& value); private: shared_qobject_ptr m_watcher; bool m_isWatching; QMap m_nameIndex; QList m_icons; QDir m_dir; }; PrismLauncher-10.0.5/launcher/icons/IconUtils.cpp0000644000175100017510000000452615144136756021333 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "IconUtils.h" #include #include "FileSystem.h" namespace { static const QStringList validIconExtensions = { { "svg", "png", "ico", "gif", "jpg", "jpeg", "webp" } }; } namespace IconUtils { QString findBestIconIn(const QString& folder, const QString& iconKey) { QString best_filename; QDirIterator it(folder, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::NoIteratorFlags); while (it.hasNext()) { it.next(); auto fileInfo = it.fileInfo(); if ((fileInfo.completeBaseName() == iconKey || fileInfo.fileName() == iconKey) && isIconSuffix(fileInfo.suffix())) return fileInfo.absoluteFilePath(); } return {}; } QString getIconFilter() { return "(*." + validIconExtensions.join(" *.") + ")"; } bool isIconSuffix(QString suffix) { return validIconExtensions.contains(suffix); } } // namespace IconUtils PrismLauncher-10.0.5/launcher/icons/MMCIcon.h0000644000175100017510000000435015144136756020307 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include enum IconType : unsigned { Builtin, Transient, FileBased, ICONS_TOTAL, ToBeDeleted }; struct MMCImage { QIcon icon; QString key; QString filename; bool present() const { return !icon.isNull() || !key.isEmpty(); } }; struct MMCIcon { QString m_key; QString m_name; MMCImage m_images[ICONS_TOTAL]; IconType m_current_type = ToBeDeleted; IconType type() const; QString name() const; bool has(IconType _type) const; QIcon icon() const; void remove(IconType rm_type); void replace(IconType new_type, QIcon icon, QString path = QString()); void replace(IconType new_type, const QString& key); bool isBuiltIn() const; QString getFilePath() const; }; PrismLauncher-10.0.5/launcher/icons/IconUtils.h0000644000175100017510000000352215144136756020773 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include namespace IconUtils { // Given a folder and an icon key, find 'best' of the icons with the given key in there and return its path QString findBestIconIn(const QString& folder, const QString& iconKey); // Get icon file type filter for file browser dialogs QString getIconFilter(); bool isIconSuffix(QString suffix); } // namespace IconUtils PrismLauncher-10.0.5/launcher/icons/IconList.cpp0000644000175100017510000003461515144136756021150 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "IconList.h" #include #include #include #include #include #include #include #include #include "icons/IconUtils.h" #define MAX_SIZE 1024 IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent) { QSet builtinNames; // add builtin icons for (const auto& builtinPath : builtinPaths) { QDir instanceIcons(builtinPath); auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name); for (const auto& fileInfo : fileInfoList) { builtinNames.insert(fileInfo.completeBaseName()); } } for (const auto& builtinName : builtinNames) { addThemeIcon(builtinName); } m_watcher.reset(new QFileSystemWatcher()); m_isWatching = false; connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged); connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged); directoryChanged(path); // Forces the UI to update, so that lengthy icon names are shown properly from the start emit iconUpdated({}); } void IconList::sortIconList() { qDebug() << "Sorting icon list..."; std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) { bool aIsSubdir = a.m_key.contains(QDir::separator()); bool bIsSubdir = b.m_key.contains(QDir::separator()); if (aIsSubdir != bIsSubdir) { return !aIsSubdir; // root-level icons come first } return a.m_key.localeAwareCompare(b.m_key) < 0; }); reindex(); } // Helper function to add directories recursively bool IconList::addPathRecursively(const QString& path) { QDir dir(path); if (!dir.exists()) return false; // Add the directory itself bool watching = m_watcher->addPath(path); // Add all subdirectories QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QFileInfo& entry : entries) { if (addPathRecursively(entry.absoluteFilePath())) { watching = true; } } return watching; } QStringList IconList::getIconFilePaths() const { QStringList iconFiles{}; QStringList directories{ m_dir.absolutePath() }; while (!directories.isEmpty()) { QString first = directories.takeFirst(); QDir dir(first); for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) { if (fileInfo.isDir()) directories.push_back(fileInfo.absoluteFilePath()); else iconFiles.push_back(fileInfo.absoluteFilePath()); } } return iconFiles; } QString formatName(const QDir& iconsDir, const QFileInfo& iconFile) { if (iconFile.dir() == iconsDir) return iconFile.completeBaseName(); constexpr auto delimiter = " » "; QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.completeBaseName(); return relativePathWithoutExtension.replace(QDir::separator(), delimiter); } /// Split into a separate function because the preprocessing impedes readability QSet toStringSet(const QList& list) { QSet set(list.begin(), list.end()); return set; } void IconList::directoryChanged(const QString& path) { QDir newDir(path); if (m_dir.absolutePath() != newDir.absolutePath()) { m_dir.setPath(path); m_dir.refresh(); if (m_isWatching) stopWatching(); startWatching(); } if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath())) return; m_dir.refresh(); const QStringList newFileNamesList = getIconFilePaths(); const QSet newSet = toStringSet(newFileNamesList); QSet currentSet; for (const MMCIcon& it : m_icons) { if (!it.has(IconType::FileBased)) continue; QFileInfo icon(it.getFilePath()); currentSet.insert(icon.absoluteFilePath()); } QSet toRemove = currentSet - newSet; QSet toAdd = newSet - currentSet; for (const QString& removedPath : toRemove) { qDebug() << "Removing icon" << removedPath; QFileInfo removedFile(removedPath); QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath()); QString key = QFileInfo(relativePath).completeBaseName(); int idx = getIconIndex(key); if (idx == -1) continue; m_icons[idx].remove(FileBased); if (m_icons[idx].type() == ToBeDeleted) { beginRemoveRows(QModelIndex(), idx, idx); m_icons.remove(idx); reindex(); endRemoveRows(); } else { dataChanged(index(idx), index(idx)); } m_watcher->removePath(removedPath); emit iconUpdated(key); } for (const QString& addedPath : toAdd) { qDebug() << "Adding icon" << addedPath; QFileInfo addfile(addedPath); QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath()); QString key = QFileInfo(relativePath).completeBaseName(); QString name = formatName(m_dir, addfile); if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) { m_watcher->addPath(addedPath); emit iconUpdated(key); } } sortIconList(); } void IconList::fileChanged(const QString& path) { qDebug() << "Checking icon" << path; QFileInfo checkfile(path); if (!checkfile.exists()) return; QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath()); int idx = getIconIndex(key); if (idx == -1) return; QIcon icon; // special handling for jpg and jpeg to go through pixmap to keep the size constant if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { icon.addPixmap(QPixmap(path)); } else { icon.addFile(path); } if (icon.availableSizes().empty()) return; m_icons[idx].m_images[IconType::FileBased].icon = icon; dataChanged(index(idx), index(idx)); emit iconUpdated(key); } void IconList::SettingChanged(const Setting& setting, const QVariant& value) { if (setting.id() != "IconsDir") return; directoryChanged(value.toString()); } void IconList::startWatching() { auto abs_path = m_dir.absolutePath(); FS::ensureFolderPathExists(abs_path); m_isWatching = addPathRecursively(abs_path); if (m_isWatching) { qDebug() << "Started watching" << abs_path; } else { qDebug() << "Failed to start watching" << abs_path; } } void IconList::stopWatching() { m_watcher->removePaths(m_watcher->files()); m_watcher->removePaths(m_watcher->directories()); m_isWatching = false; } QStringList IconList::mimeTypes() const { QStringList types; types << "text/uri-list"; return types; } Qt::DropActions IconList::supportedDropActions() const { return Qt::CopyAction; } bool IconList::dropMimeData(const QMimeData* data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) { if (action == Qt::IgnoreAction) return true; // check if the action is supported if (!data || !(action & supportedDropActions())) return false; // files dropped from outside? if (data->hasUrls()) { auto urls = data->urls(); QStringList iconFiles; for (const auto& url : urls) { // only local files may be dropped... if (!url.isLocalFile()) continue; iconFiles += url.toLocalFile(); } installIcons(iconFiles); return true; } return false; } Qt::ItemFlags IconList::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); return Qt::ItemIsDropEnabled | defaultFlags; } QVariant IconList::data(const QModelIndex& index, int role) const { if (!index.isValid()) return {}; int row = index.row(); if (row < 0 || row >= m_icons.size()) return {}; switch (role) { case Qt::DecorationRole: return m_icons[row].icon(); case Qt::DisplayRole: return m_icons[row].name(); case Qt::UserRole: return m_icons[row].m_key; default: return {}; } } int IconList::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_icons.size(); } void IconList::installIcons(const QStringList& iconFiles) { for (const QString& file : iconFiles) installIcon(file, {}); } void IconList::installIcon(const QString& file, const QString& name) { QFileInfo fileinfo(file); if (!fileinfo.isReadable() || !fileinfo.isFile()) return; if (!IconUtils::isIconSuffix(fileinfo.suffix())) return; QString target = FS::PathCombine(getDirectory(), name.isEmpty() ? fileinfo.fileName() : name); QFile::copy(file, target); } bool IconList::iconFileExists(const QString& key) const { auto iconEntry = icon(key); return iconEntry && iconEntry->has(IconType::FileBased); } /// Returns the icon with the given key or nullptr if it doesn't exist. const MMCIcon* IconList::icon(const QString& key) const { int iconIdx = getIconIndex(key); if (iconIdx == -1) return nullptr; return &m_icons[iconIdx]; } bool IconList::deleteIcon(const QString& key) { return iconFileExists(key) && FS::deletePath(icon(key)->getFilePath()); } bool IconList::trashIcon(const QString& key) { return iconFileExists(key) && FS::trash(icon(key)->getFilePath(), nullptr); } bool IconList::addThemeIcon(const QString& key) { auto iter = m_nameIndex.find(key); if (iter != m_nameIndex.end()) { auto& oldOne = m_icons[*iter]; oldOne.replace(Builtin, key); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = key; mmc_icon.m_key = key; mmc_icon.replace(Builtin, key); m_icons.push_back(mmc_icon); m_nameIndex[key] = m_icons.size() - 1; } endInsertRows(); return true; } bool IconList::addIcon(const QString& key, const QString& name, const QString& path, const IconType type) { // replace the icon even? is the input valid? QIcon icon; // special handling for jpg and jpeg to go through pixmap to keep the size constant if (path.endsWith(".jpg") || path.endsWith(".jpeg")) { icon.addPixmap(QPixmap(path)); } else { icon.addFile(path); } if (icon.isNull()) return false; auto iter = m_nameIndex.find(key); if (iter != m_nameIndex.end()) { auto& oldOne = m_icons[*iter]; oldOne.replace(type, icon, path); dataChanged(index(*iter), index(*iter)); return true; } // add a new icon beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size()); { MMCIcon mmc_icon; mmc_icon.m_name = name; mmc_icon.m_key = key; mmc_icon.replace(type, icon, path); m_icons.push_back(mmc_icon); m_nameIndex[key] = m_icons.size() - 1; } endInsertRows(); return true; } void IconList::saveIcon(const QString& key, const QString& path, const char* format) const { auto icon = getIcon(key); auto pixmap = icon.pixmap(128, 128); pixmap.save(path, format); } void IconList::reindex() { m_nameIndex.clear(); for (int i = 0; i < m_icons.size(); i++) { m_nameIndex[m_icons[i].m_key] = i; emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model } } QIcon IconList::getIcon(const QString& key) const { int iconIndex = getIconIndex(key); if (iconIndex != -1) return m_icons[iconIndex].icon(); // Fallback for icons that don't exist.b iconIndex = getIconIndex("grass"); if (iconIndex != -1) return m_icons[iconIndex].icon(); return {}; } int IconList::getIconIndex(const QString& key) const { auto iter = m_nameIndex.find(key == "default" ? "grass" : key); if (iter != m_nameIndex.end()) return *iter; return -1; } QString IconList::getDirectory() const { return m_dir.absolutePath(); } /// Returns the directory of the icon with the given key or the default directory if it's a builtin icon. QString IconList::iconDirectory(const QString& key) const { for (const auto& mmcIcon : m_icons) { if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) { QFileInfo iconFile(mmcIcon.getFilePath()); return iconFile.dir().path(); } } return getDirectory(); } PrismLauncher-10.0.5/launcher/InstanceList.cpp0000644000175100017510000010241515144136756020703 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "BaseInstance.h" #include "ExponentialSeries.h" #include "FileSystem.h" #include "InstanceList.h" #include "InstanceTask.h" #include "NullInstance.h" #include "WatchLock.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/ShortcutUtils.h" #include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 #include #endif const static int GROUP_FILE_FORMAT_VERSION = 1; InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent) : QAbstractListModel(parent), m_globalSettings(settings) { resumeWatch(); // Create aand normalize path if (!QDir::current().exists(instDir)) { QDir::current().mkpath(instDir); } connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated); // NOTE: canonicalPath requires the path to exist. Do not move this above the creation block! m_instDir = QDir(instDir).canonicalPath(); m_watcher = new QFileSystemWatcher(this); connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged); m_watcher->addPath(m_instDir); } InstanceList::~InstanceList() {} Qt::DropActions InstanceList::supportedDragActions() const { return Qt::MoveAction; } Qt::DropActions InstanceList::supportedDropActions() const { return Qt::MoveAction; } bool InstanceList::canDropMimeData(const QMimeData* data, [[maybe_unused]] Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) const { if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; } bool InstanceList::dropMimeData(const QMimeData* data, [[maybe_unused]] Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex& parent) { if (data && data->hasFormat("application/x-instanceid")) { return true; } return false; } QStringList InstanceList::mimeTypes() const { auto types = QAbstractListModel::mimeTypes(); types.push_back("application/x-instanceid"); return types; } QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const { auto mimeData = QAbstractListModel::mimeData(indexes); if (indexes.size() == 1) { auto instanceId = data(indexes[0], InstanceIDRole).toString(); mimeData->setData("application/x-instanceid", instanceId.toUtf8()); } return mimeData; } QStringList InstanceList::getLinkedInstancesById(const QString& id) const { QStringList linkedInstances; for (auto inst : m_instances) { if (inst->isLinkedToInstanceId(id)) linkedInstances.append(inst->id()); } return linkedInstances; } int InstanceList::rowCount(const QModelIndex& parent) const { Q_UNUSED(parent); return m_instances.count(); } QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const { Q_UNUSED(parent); if (row < 0 || row >= m_instances.size()) return QModelIndex(); return createIndex(row, column, (void*)m_instances.at(row).get()); } QVariant InstanceList::data(const QModelIndex& index, int role) const { if (!index.isValid()) { return QVariant(); } BaseInstance* pdata = static_cast(index.internalPointer()); switch (role) { case InstancePointerRole: { QVariant v = QVariant::fromValue((void*)pdata); return v; } case InstanceIDRole: { return pdata->id(); } case Qt::EditRole: case Qt::DisplayRole: { return pdata->name(); } case Qt::AccessibleTextRole: { return tr("%1 Instance").arg(pdata->name()); } case Qt::ToolTipRole: { return pdata->instanceRoot(); } case Qt::DecorationRole: { return pdata->iconKey(); } // HACK: see InstanceView.h in gui! case GroupRole: { return getInstanceGroup(pdata->id()); } default: break; } return QVariant(); } bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) { if (!index.isValid()) { return false; } if (role != Qt::EditRole) { return false; } BaseInstance* pdata = static_cast(index.internalPointer()); auto newName = value.toString(); if (pdata->name() == newName) { return true; } pdata->setName(newName); return true; } Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const { Qt::ItemFlags f; if (index.isValid()) { f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } return f; } GroupId InstanceList::getInstanceGroup(const InstanceId& id) const { auto inst = getInstanceById(id); if (!inst) { return GroupId(); } auto iter = m_instanceGroupIndex.find(inst->id()); if (iter != m_instanceGroupIndex.end()) { return *iter; } return GroupId(); } void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name) { if (name.isEmpty() && !name.isNull()) name = QString(); auto inst = getInstanceById(id); if (!inst) { qDebug() << "Attempt to set a null instance's group"; return; } bool changed = false; auto iter = m_instanceGroupIndex.find(inst->id()); if (iter != m_instanceGroupIndex.end()) { if (*iter != name) { decreaseGroupCount(*iter); *iter = name; changed = true; } } else { changed = true; m_instanceGroupIndex[id] = name; } if (changed) { increaseGroupCount(name); auto idx = getInstIndex(inst.get()); emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); } } QStringList InstanceList::getGroups() { return m_groupNameCache.keys(); } void InstanceList::deleteGroup(const GroupId& name) { m_groupNameCache.remove(name); m_collapsedGroups.remove(name); bool removed = false; qDebug() << "Delete group" << name; for (auto& instance : m_instances) { const QString& instID = instance->id(); const QString instGroupName = getInstanceGroup(instID); if (instGroupName == name) { m_instanceGroupIndex.remove(instID); qDebug() << "Remove" << instID << "from group" << name; removed = true; auto idx = getInstIndex(instance.get()); if (idx >= 0) emit dataChanged(index(idx), index(idx), { GroupRole }); } } if (removed) saveGroupList(); } void InstanceList::renameGroup(const QString& src, const QString& dst) { m_groupNameCache.remove(src); if (m_collapsedGroups.remove(src)) m_collapsedGroups.insert(dst); bool modified = false; qDebug() << "Rename group" << src << "to" << dst; for (auto& instance : m_instances) { const QString& instID = instance->id(); const QString instGroupName = getInstanceGroup(instID); if (instGroupName == src) { m_instanceGroupIndex[instID] = dst; increaseGroupCount(dst); qDebug() << "Set" << instID << "group to" << dst; modified = true; auto idx = getInstIndex(instance.get()); if (idx >= 0) emit dataChanged(index(idx), index(idx), { GroupRole }); } } if (modified) saveGroupList(); } bool InstanceList::isGroupCollapsed(const QString& group) { return m_collapsedGroups.contains(group); } bool InstanceList::trashInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { qWarning() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; return false; } QString cachedGroupId = m_instanceGroupIndex[id]; qDebug() << "Will trash instance" << id; QString trashedLoc; if (m_instanceGroupIndex.remove(id)) { decreaseGroupCount(cachedGroupId); saveGroupList(); } if (!FS::trash(inst->instanceRoot(), &trashedLoc)) { qWarning() << "Trash of instance" << id << "has not been completely successful..."; return false; } qDebug() << "Instance" << id << "has been trashed by the launcher."; m_trashHistory.push({ id, inst->instanceRoot(), trashedLoc, cachedGroupId }); // Also trash all of its shortcuts; we remove the shortcuts if trash fails since it is invalid anyway for (const auto& [name, filePath, target] : inst->shortcuts()) { if (!FS::trash(filePath, &trashedLoc)) { qWarning() << "Trash of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful, trying to delete it instead..."; if (!FS::deletePath(filePath)) { qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful, given up..."; } else { qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; } continue; } qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been trashed by the launcher."; m_trashHistory.top().shortcuts.append({ { name, filePath, target }, trashedLoc }); } return true; } bool InstanceList::trashedSomething() const { return !m_trashHistory.empty(); } bool InstanceList::undoTrashInstance() { if (m_trashHistory.empty()) { qWarning() << "Nothing to recover from trash."; return true; } auto top = m_trashHistory.pop(); while (QDir(top.path).exists()) { top.id += "1"; top.path += "1"; } if (!QFile(top.trashPath).rename(top.path)) { qWarning() << "Moving" << top.trashPath << "back to" << top.path << "failed!"; return false; } qDebug() << "Moving" << top.trashPath << "back to" << top.path; bool ok = true; for (const auto& [data, trashPath] : top.shortcuts) { if (QDir(data.filePath).exists()) { // Don't try to append 1 here as the shortcut may have suffixes like .app, just warn and skip it qWarning() << "Shortcut" << trashPath << "original directory" << data.filePath << "already exists!"; ok = false; continue; } if (!QFile(trashPath).rename(data.filePath)) { qWarning() << "Moving shortcut from" << trashPath << "back to" << data.filePath << "failed!"; ok = false; continue; } qDebug() << "Moving shortcut from" << trashPath << "back to" << data.filePath; } m_instanceGroupIndex[top.id] = top.groupName; increaseGroupCount(top.groupName); saveGroupList(); emit instancesChanged(); return ok; } void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { qWarning() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } QString cachedGroupId = m_instanceGroupIndex[id]; if (m_instanceGroupIndex.remove(id)) { decreaseGroupCount(cachedGroupId); saveGroupList(); } qDebug() << "Will delete instance" << id; if (!FS::deletePath(inst->instanceRoot())) { qWarning() << "Deletion of instance" << id << "has not been completely successful..."; return; } qDebug() << "Instance" << id << "has been deleted by the launcher."; for (const auto& [name, filePath, target] : inst->shortcuts()) { if (!FS::deletePath(filePath)) { qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful..."; continue; } qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; } } static QMap getIdMapping(const QList& list) { QMap out; int i = 0; for (auto& item : list) { auto id = item->id(); if (out.contains(id)) { qWarning() << "Duplicate ID" << id << "in instance list"; } out[id] = std::make_pair(item, i); i++; } return out; } QList InstanceList::discoverInstances() { qInfo() << "Discovering instances in" << m_instDir; QList out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (iter.hasNext()) { QString subDir = iter.next(); QFileInfo dirInfo(subDir); if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) continue; // if it is a symlink, ignore it if it goes to the instance folder if (dirInfo.isSymLink()) { QFileInfo targetInfo(dirInfo.symLinkTarget()); QFileInfo instDirInfo(m_instDir); if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) { qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; continue; } } auto id = dirInfo.fileName(); out.append(id); qInfo() << "Found instance ID" << id; } instanceSet = QSet(out.begin(), out.end()); m_instancesProbed = true; return out; } InstanceList::InstListError InstanceList::loadList() { auto existingIds = getIdMapping(m_instances); QList newList; for (auto& id : discoverInstances()) { if (existingIds.contains(id)) { auto instPair = existingIds[id]; existingIds.remove(id); qInfo() << "Should keep and soft-reload" << id; } else { InstancePtr instPtr = loadInstance(id); if (instPtr) { newList.append(instPtr); } } } // TODO: looks like a general algorithm with a few specifics inserted. Do something about it. if (!existingIds.isEmpty()) { // get the list of removed instances and sort it by their original index, from last to first auto deadList = existingIds.values(); auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; }; std::sort(deadList.begin(), deadList.end(), orderSortPredicate); // remove the contiguous ranges of rows int front_bookmark = -1; int back_bookmark = -1; int currentItem = -1; auto removeNow = [this, &front_bookmark, &back_bookmark, ¤tItem]() { beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); endRemoveRows(); front_bookmark = -1; back_bookmark = currentItem; }; for (auto& removedItem : deadList) { auto instPtr = removedItem.first; instPtr->invalidate(); currentItem = removedItem.second; if (back_bookmark == -1) { // no bookmark yet back_bookmark = currentItem; } else if (currentItem == front_bookmark - 1) { // part of contiguous sequence, continue } else { // seam between previous and current item removeNow(); } front_bookmark = currentItem; } if (back_bookmark != -1) { removeNow(); } } if (newList.size()) { add(newList); } m_dirty = false; updateTotalPlayTime(); return NoError; } void InstanceList::updateTotalPlayTime() { totalPlayTime = 0; for (auto const& itr : m_instances) { totalPlayTime += itr.get()->totalTimePlayed(); } } void InstanceList::saveNow() { for (auto& item : m_instances) { item->saveNow(); } } void InstanceList::add(const QList& t) { beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); m_instances.append(t); for (auto& ptr : t) { connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); } endInsertRows(); } void InstanceList::resumeWatch() { if (m_watchLevel > 0) { qWarning() << "Bad suspend level resume in instance list"; return; } m_watchLevel++; if (m_watchLevel > 0 && m_dirty) { loadList(); } } void InstanceList::suspendWatch() { m_watchLevel--; } void InstanceList::providerUpdated() { m_dirty = true; if (m_watchLevel == 1) { loadList(); } } InstancePtr InstanceList::getInstanceById(QString instId) const { if (instId.isEmpty()) return InstancePtr(); for (auto& inst : m_instances) { if (inst->id() == instId) { return inst; } } return InstancePtr(); } InstancePtr InstanceList::getInstanceByManagedName(const QString& managed_name) const { if (managed_name.isEmpty()) return {}; for (auto instance : m_instances) { if (instance->getManagedPackName() == managed_name) return instance; } return {}; } QModelIndex InstanceList::getInstanceIndexById(const QString& id) const { return index(getInstIndex(getInstanceById(id).get())); } int InstanceList::getInstIndex(BaseInstance* inst) const { int count = m_instances.count(); for (int i = 0; i < count; i++) { if (inst == m_instances[i].get()) { return i; } } return -1; } void InstanceList::propertiesChanged(BaseInstance* inst) { int i = getInstIndex(inst); if (i != -1) { emit dataChanged(index(i), index(i)); updateTotalPlayTime(); } } InstancePtr InstanceList::loadInstance(const InstanceId& id) { if (!m_groupsLoaded) { loadGroupList(); } auto instanceRoot = FS::PathCombine(m_instDir, id); auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; instanceSettings->registerSetting("InstanceType", ""); QString inst_type = instanceSettings->get("InstanceType").toString(); // NOTE: Some launcher versions didn't save the InstanceType properly. We will just bank on the probability that this is probably a // OneSix instance if (inst_type == "OneSix" || inst_type.isEmpty()) { inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); } else { inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); } qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); auto shortcut = inst->shortcuts(); if (!shortcut.isEmpty()) qDebug() << "Loaded" << shortcut.size() << "shortcut(s) for instance" << inst->name(); return inst; } void InstanceList::increaseGroupCount(const QString& group) { if (group.isEmpty()) return; ++m_groupNameCache[group]; } void InstanceList::decreaseGroupCount(const QString& group) { if (group.isEmpty()) return; if (--m_groupNameCache[group] < 1) { m_groupNameCache.remove(group); m_collapsedGroups.remove(group); } } void InstanceList::saveGroupList() { qDebug() << "Will save group list now."; if (!m_instancesProbed) { qDebug() << "Group saving prevented because we don't know the full list of instances yet."; return; } WatchLock foo(m_watcher, m_instDir); QString groupFileName = m_instDir + "/instgroups.json"; QMap> reverseGroupMap; for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { const QString& id = iter.key(); QString group = iter.value(); if (group.isEmpty()) continue; if (!instanceSet.contains(id)) { qDebug() << "Skipping saving missing instance" << id << "to groups list."; continue; } if (!reverseGroupMap.count(group)) { QSet set; set.insert(id); reverseGroupMap[group] = set; } else { QSet& set = reverseGroupMap[group]; set.insert(id); } } QJsonObject toplevel; toplevel.insert("formatVersion", QJsonValue(QString("1"))); QJsonObject groupsArr; for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) { auto list = iter.value(); auto name = iter.key(); QJsonObject groupObj; QJsonArray instanceArr; groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); for (auto item : list) { instanceArr.append(QJsonValue(item)); } groupObj.insert("instances", instanceArr); groupsArr.insert(name, groupObj); } toplevel.insert("groups", groupsArr); // empty string represents ungrouped "group" if (m_collapsedGroups.contains("")) { QJsonObject ungrouped; ungrouped.insert("hidden", QJsonValue(true)); toplevel.insert("ungrouped", ungrouped); } QJsonDocument doc(toplevel); try { FS::write(groupFileName, doc.toJson()); qDebug() << "Group list saved."; } catch (const FS::FileSystemException& e) { qCritical() << "Failed to write instance group file :" << e.cause(); } } void InstanceList::loadGroupList() { qDebug() << "Will load group list now."; QString groupFileName = m_instDir + "/instgroups.json"; // if there's no group file, fail if (!QFileInfo(groupFileName).exists()) return; QByteArray jsonData; try { jsonData = FS::read(groupFileName); } catch (const FS::FileSystemException& e) { qCritical() << "Failed to read instance group file :" << e.cause(); return; } QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); // if the json was bad, fail if (error.error != QJsonParseError::NoError) { qCritical() << QString("Failed to parse instance group file: %1 at offset %2") .arg(error.errorString(), QString::number(error.offset)) .toUtf8(); return; } // if the root of the json wasn't an object, fail if (!jsonDoc.isObject()) { qWarning() << "Invalid group file. Root entry should be an object."; return; } QJsonObject rootObj = jsonDoc.object(); // Make sure the format version matches, otherwise fail. if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION) return; // Get the groups. if it's not an object, fail if (!rootObj.value("groups").isObject()) { qWarning() << "Invalid group list JSON: 'groups' should be an object."; return; } m_instanceGroupIndex.clear(); m_groupNameCache.clear(); // Iterate through all the groups. QJsonObject groupMapping = rootObj.value("groups").toObject(); for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) { QString groupName = iter.key(); if (iter.key().isEmpty()) { qWarning() << "Redundant empty group found"; continue; } // If not an object, complain and skip to the next one. if (!iter.value().isObject()) { qWarning() << QString("Group '%1' in the group list should be an object").arg(groupName).toUtf8(); continue; } QJsonObject groupObj = iter.value().toObject(); if (!groupObj.value("instances").isArray()) { qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.") .arg(groupName) .toUtf8(); continue; } auto hidden = groupObj.value("hidden").toBool(false); if (hidden) m_collapsedGroups.insert(groupName); // Iterate through the list of instances in the group. QJsonArray instancesArray = groupObj.value("instances").toArray(); for (auto value : instancesArray) { m_instanceGroupIndex[value.toString()] = groupName; increaseGroupCount(groupName); } } bool ungroupedHidden = false; if (rootObj.value("ungrouped").isObject()) { QJsonObject ungrouped = rootObj.value("ungrouped").toObject(); ungroupedHidden = ungrouped.value("hidden").toBool(false); } if (ungroupedHidden) { // empty string represents ungrouped "group" m_collapsedGroups.insert(""); } m_groupsLoaded = true; qDebug() << "Group list loaded."; } void InstanceList::instanceDirContentsChanged(const QString& path) { Q_UNUSED(path); emit instancesChanged(); } void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting, QVariant value) { QString newInstDir = QDir(value.toString()).canonicalPath(); if (newInstDir != m_instDir) { if (m_groupsLoaded) { saveGroupList(); } m_instDir = newInstDir; m_groupsLoaded = false; beginRemoveRows(QModelIndex(), 0, count()); m_instances.erase(m_instances.begin(), m_instances.end()); endRemoveRows(); emit instancesChanged(); } } void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) { qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded"); if (collapsed) { m_collapsedGroups.insert(group); } else { m_collapsedGroups.remove(group); } saveGroupList(); } class InstanceStaging : public Task { Q_OBJECT const unsigned minBackoff = 1; const unsigned maxBackoff = 16; public: InstanceStaging(InstanceList* parent, InstanceTask* child, SettingsObjectPtr settings) : m_parent(parent), backoff(minBackoff, maxBackoff) { m_stagingPath = parent->getStagedInstancePath(); m_child.reset(child); m_child->setStagingPath(m_stagingPath); m_child->setParentSettings(std::move(settings)); connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::aborted, this, &InstanceStaging::childAborted); connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable); connect(child, &Task::status, this, &InstanceStaging::setStatus); connect(child, &Task::details, this, &InstanceStaging::setDetails); connect(child, &Task::progress, this, &InstanceStaging::setProgress); connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress); connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); } virtual ~InstanceStaging() {} // FIXME/TODO: add ability to abort during instance commit retries bool abort() override { if (!canAbort()) return false; m_child->abort(); return Task::abort(); } bool canAbort() const override { return (m_child && m_child->canAbort()); } protected: virtual void executeTask() override { if (m_stagingPath.isNull()) { emitFailed(tr("Could not create staging folder")); return; } m_child->start(); } QStringList warnings() const override { return m_child->warnings(); } private slots: void childSucceeded() { unsigned sleepTime = backoff(); if (m_parent->commitStagedInstance(m_stagingPath, *m_child.get(), m_child->group(), *m_child.get())) { emitSucceeded(); return; } // we actually failed, retry? if (sleepTime == maxBackoff) { emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); return; } qDebug() << "Failed to commit instance" << m_child->name() << "Initiating backoff:" << sleepTime; m_backoffTimer.start(sleepTime * 500); } void childFailed(const QString& reason) { m_parent->destroyStagingPath(m_stagingPath); emitFailed(reason); } void childAborted() { m_parent->destroyStagingPath(m_stagingPath); emitAborted(); } private: InstanceList* m_parent; /* * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. * Basically, it starts messing things up while the launcher is extracting/creating instances * and causes that horrible failure that is NTFS to lock files in place because they are open. */ ExponentialSeries backoff; QString m_stagingPath; unique_qobject_ptr m_child; QTimer m_backoffTimer; }; Task* InstanceList::wrapInstanceTask(InstanceTask* task) { return new InstanceStaging(this, task, m_globalSettings); } QString InstanceList::getStagedInstancePath() { const QString tempRoot = FS::PathCombine(m_instDir, ".tmp"); QString result; int tries = 0; do { if (++tries > 256) return {}; const QString key = QUuid::createUuid().toString(QUuid::Id128).left(6); result = FS::PathCombine(tempRoot, key); } while (QFileInfo::exists(result)); if (!QDir::current().mkpath(result)) return {}; #ifdef Q_OS_WIN32 SetFileAttributesA(tempRoot.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); #endif return result; } bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, QString groupName, InstanceTask const& commiting) { if (groupName.isEmpty() && !groupName.isNull()) groupName = QString(); QString instID; InstancePtr inst; auto should_override = commiting.shouldOverride(); if (should_override) { instID = commiting.originalInstanceID(); } else { instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir); } Q_ASSERT(!instID.isEmpty()); { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); if (should_override) { if (!FS::overrideFolder(destination, path)) { qWarning() << "Failed to override" << path << "to" << destination; return false; } } else { if (!FS::move(path, destination)) { qWarning() << "Failed to move" << path << "to" << destination; return false; } m_instanceGroupIndex[instID] = groupName; increaseGroupCount(groupName); } instanceSet.insert(instID); emit instancesChanged(); emit instanceSelectRequest(instID); } saveGroupList(); return true; } bool InstanceList::destroyStagingPath(const QString& keyPath) { return FS::deletePath(keyPath); } int InstanceList::getTotalPlayTime() { updateTotalPlayTime(); return totalPlayTime; } #include "InstanceList.moc" PrismLauncher-10.0.5/launcher/ExponentialSeries.h0000644000175100017510000000142315144136756021406 0ustar runnerrunner #pragma once template inline void clamp(T& current, T min, T max) { if (current < min) { current = min; } else if (current > max) { current = max; } } // List of numbers from min to max. Next is exponent times bigger than previous. class ExponentialSeries { public: ExponentialSeries(unsigned min, unsigned max, unsigned exponent = 2) { m_current = m_min = min; m_max = max; m_exponent = exponent; } void reset() { m_current = m_min; } unsigned operator()() { unsigned retval = m_current; m_current *= m_exponent; clamp(m_current, m_min, m_max); return retval; } unsigned m_current; unsigned m_min; unsigned m_max; unsigned m_exponent; }; PrismLauncher-10.0.5/launcher/Application.cpp0000644000175100017510000023307115144136756020551 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Lenny McLennington * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Application.h" #include "BuildConfig.h" #include "DataMigrationTask.h" #include "java/JavaInstallList.h" #include "net/PasteUpload.h" #include "tasks/Task.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" #include "ui/ToolTipFilter.h" #include "ui/ViewLogWindow.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/AccessibleInstanceView.h" #include "ui/pages/BasePageProvider.h" #include "ui/pages/global/APIPage.h" #include "ui/pages/global/AccountListPage.h" #include "ui/pages/global/AppearancePage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" #include "ui/pages/global/LauncherPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" #include "ui/setupwizard/AutoJavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" #include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/LoginWizardPage.h" #include "ui/setupwizard/PasteWizardPage.h" #include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/ThemeWizardPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/pagedialog/PageDialog.h" #include "ui/themes/ThemeManager.h" #include "ApplicationMessage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "InstanceList.h" #include "MTPixmapCache.h" #include #include "icons/IconList.h" #include "net/HttpMetaCache.h" #include "updater/ExternalUpdater.h" #include "tools/JProfiler.h" #include "tools/JVisualVM.h" #include "tools/MCEditTool.h" #include "settings/INISettingsObject.h" #include "settings/Setting.h" #include "meta/Index.h" #include "translations/TranslationsModel.h" #include #include #include #include #include #include "SysInfo.h" #ifdef Q_OS_LINUX #include #include "MangoHud.h" #include "gamemode_client.h" #endif #if defined(Q_OS_LINUX) #include #endif #if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) #include #include #endif #if defined(Q_OS_MAC) #if defined(SPARKLE_ENABLED) #include "updater/MacSparkleUpdater.h" #endif #else #include "updater/PrismExternalUpdater.h" #endif #if defined Q_OS_WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include #include "console/WindowsConsole.h" #endif #include "console/Console.h" #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) static const QLatin1String liveCheckFile("live.check"); PixmapCache* PixmapCache::s_instance = nullptr; static bool isANSIColorConsole; static QString defaultLogFormat = QStringLiteral( "%{time process}" " " "%{if-debug}Debug:%{endif}" "%{if-info}Info:%{endif}" "%{if-warning}Warning:%{endif}" "%{if-critical}Critical:%{endif}" "%{if-fatal}Fatal:%{endif}" " " "%{if-category}[%{category}] %{endif}" "%{message}" " " "(%{function}:%{line})"); #define ansi_reset "\x1b[0m" #define ansi_bold "\x1b[1m" #define ansi_reset_bold "\x1b[22m" #define ansi_faint "\x1b[2m" #define ansi_italic "\x1b[3m" #define ansi_red_fg "\x1b[31m" #define ansi_green_fg "\x1b[32m" #define ansi_yellow_fg "\x1b[33m" #define ansi_blue_fg "\x1b[34m" #define ansi_purple_fg "\x1b[35m" #define ansi_inverse "\x1b[7m" // clang-format off static QString ansiLogFormat = QStringLiteral( ansi_faint "%{time process}" ansi_reset " " "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}" "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}" "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}" "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}" "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}" " " "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}" "%{message}" " " ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset ); // clang-format on #undef ansi_inverse #undef ansi_purple_fg #undef ansi_blue_fg #undef ansi_yellow_fg #undef ansi_green_fg #undef ansi_red_fg #undef ansi_italic #undef ansi_faint #undef ansi_bold #undef ansi_reset_bold #undef ansi_reset namespace { /** This is used so that we can output to the log file in addition to the CLI. */ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { static std::mutex loggerMutex; const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe if (isANSIColorConsole) { // ensure default is set for log file qSetMessagePattern(defaultLogFormat); } QString out = qFormatLogMessage(type, context, msg); if (APPLICATION->logModel) { APPLICATION->logModel->append(MessageLevel::fromQtMsgType(type), out); } out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); if (isANSIColorConsole) { // format ansi for console; qSetMessagePattern(ansiLogFormat); out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; } QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } } // namespace std::tuple read_lock_File(const QString& path) { auto contents = QString(FS::read(path)); auto lines = contents.split('\n'); QDateTime timestamp; QString from, to, target, data_path; for (auto line : lines) { auto index = line.indexOf("="); if (index < 0) continue; auto left = line.left(index); auto right = line.mid(index + 1); if (left.toLower() == "timestamp") { timestamp = QDateTime::fromString(right, Qt::ISODate); } else if (left.toLower() == "from") { from = right; } else if (left.toLower() == "to") { to = right; } else if (left.toLower() == "target") { target = right; } else if (left.toLower() == "data_path") { data_path = right; } } return std::make_tuple(timestamp, from, to, target, data_path); } Application::Application(int& argc, char** argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 // attach the parent console if stdout not already captured if (AttachWindowsConsole()) { consoleAttached = true; if (auto err = EnableAnsiSupport(); !err) { isANSIColorConsole = true; } else { std::cout << "Error setting up ansi console" << err.message() << std::endl; } } #else if (console::isConsole()) { isANSIColorConsole = true; } #endif setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); setDesktopFileName(BuildConfig.LAUNCHER_APPID); m_startTime = QDateTime::currentDateTime(); // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); this->setQuitLockEnabled(false); // Commandline parsing QCommandLineParser parser; parser.setApplicationDescription(BuildConfig.LAUNCHER_DISPLAYNAME); parser.addOptions( { { { "d", "dir" }, "Use a custom path as application root (use '.' for current directory)", "directory" }, { { "l", "launch" }, "Launch the specified instance (by instance ID)", "instance" }, { { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" }, { { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" }, { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, { { "o", "offline" }, "Launch offline, with given player name (only valid in combination with --launch)", "offline" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, { "show", "Opens the window for the specified instance (by instance ID)", "show" } }); // Has to be positional for some OS to handle that properly parser.addPositionalArgument("URL", "Import the resource(s) at the given URL(s) (same as -I / --import)", "[URL...]"); parser.addHelpOption(); parser.addVersionOption(); parser.process(arguments()); m_instanceIdToLaunch = parser.value("launch"); m_serverToJoin = parser.value("server"); m_worldToJoin = parser.value("world"); m_profileToUse = parser.value("profile"); if (parser.isSet("offline")) { m_offline = true; m_offlineName = parser.value("offline"); } m_liveCheck = parser.isSet("alive"); m_instanceIdToShowWindowOf = parser.value("show"); for (auto url : parser.values("import")) { m_urlsToImport.append(normalizeImportUrl(url)); } // treat unspecified positional arguments as import urls for (auto url : parser.positionalArguments()) { m_urlsToImport.append(normalizeImportUrl(url)); } // error if --launch is missing with --server or --profile if ((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty() || !m_profileToUse.isEmpty() || m_offline) && m_instanceIdToLaunch.isEmpty()) { std::cerr << "--server, --profile and --offline can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; return; } QString origcwdPath = QDir::currentPath(); QString binPath = applicationDirPath(); { // Root path is used for updates and portable data #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr m_rootPath = foo.absolutePath(); #elif defined(Q_OS_WIN32) m_rootPath = binPath; #elif defined(Q_OS_MAC) QDir foo(FS::PathCombine(binPath, "../..")); m_rootPath = foo.absolutePath(); // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) FS::updateTimestamp(m_rootPath); #endif } QString adjustedBy; QString dataPath; // change folder QString dataDirEnv; QString dirParam = parser.value("dir"); if (!dirParam.isEmpty()) { // the dir param. it makes multimc data path point to whatever the user specified // on command line adjustedBy = "Command line"; dataPath = dirParam; } else if (dataDirEnv = QProcessEnvironment::systemEnvironment().value(QString("%1_DATA_DIR").arg(BuildConfig.LAUNCHER_NAME.toUpper())); !dataDirEnv.isEmpty()) { adjustedBy = "System environment"; dataPath = dataDirEnv; } else { QDir foo; if (DesktopServices::isSnap()) { foo = QDir(getenv("SNAP_USER_COMMON")); } else { foo = QDir(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); } dataPath = foo.absolutePath(); adjustedBy = "Persistent data path"; #ifndef Q_OS_MACOS if (auto portableUserData = FS::PathCombine(m_rootPath, "UserData"); QDir(portableUserData).exists()) { dataPath = portableUserData; adjustedBy = "Portable user data path"; m_portable = true; } else if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { dataPath = m_rootPath; adjustedBy = "Portable data path"; m_portable = true; } #endif } if (!FS::ensureFolderPathExists(dataPath)) { showFatalErrorMessage( "The launcher data folder could not be created.", QString("The launcher data folder could not be created.\n" "\n" "Make sure you have the right permissions to the launcher data folder and any folder needed to access it.\n" "(%1)\n" "\n" "The launcher cannot continue until you fix this problem.") .arg(dataPath)); return; } if (!QDir::setCurrent(dataPath)) { showFatalErrorMessage("The launcher data folder could not be opened.", QString("The launcher data folder could not be opened.\n" "\n" "Make sure you have the right permissions to the launcher data folder.\n" "(%1)\n" "\n" "The launcher cannot continue until you fix this problem.") .arg(dataPath)); return; } m_dataPath = dataPath; /* * Establish the mechanism for communication with an already running PrismLauncher that uses the same data path. * If there is one, tell it what the user actually wanted to do and exit. * We want to initialize this before logging to avoid messing with the log of a potential already running copy. */ auto appID = ApplicationId::fromPathAndVersion(QDir::currentPath(), BuildConfig.printableVersionString()); { // FIXME: you can run the same binaries with multiple data dirs and they won't clash. This could cause issues for updates. m_peerInstance = new LocalPeer(this, appID); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); if (m_peerInstance->isClient()) { bool sentMessage = false; int timeout = 2000; if (m_instanceIdToLaunch.isEmpty()) { ApplicationMessage activate; activate.command = "activate"; sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout); if (!m_urlsToImport.isEmpty()) { for (auto url : m_urlsToImport) { ApplicationMessage import; import.command = "import"; import.args.insert("url", url.toString()); sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout); } } } else { ApplicationMessage launch; launch.command = "launch"; launch.args["id"] = m_instanceIdToLaunch; if (!m_serverToJoin.isEmpty()) { launch.args["server"] = m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { launch.args["world"] = m_worldToJoin; } if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; } if (m_offline) { launch.args["offline_enabled"] = "true"; launch.args["offline_name"] = m_offlineName; } sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout); } if (sentMessage) { m_status = Application::Succeeded; return; } else { std::cerr << "Unable to redirect command to already running instance\n"; // C function not Qt function - event loop not started yet ::exit(1); } } } // init the logger { static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "-%0.log"; static const QString logBase = FS::PathCombine("logs", baseLogFile); if (FS::ensureFolderPathExists("logs")) { // if this did not fail for (auto i = 0; i <= 4; i++) if (auto oldName = baseLogFile.arg(i); QFile::exists(oldName)) // do not pointlessly delete new files if the old ones are not there FS::move(oldName, logBase.arg(i)); } for (auto i = 4; i > 0; i--) FS::move(logBase.arg(i - 1), logBase.arg(i)); logFile = std::unique_ptr(new QFile(logBase.arg(0))); if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { showFatalErrorMessage("The launcher data folder is not writable!", QString("The launcher couldn't create a log file - the data folder is not writable.\n" "\n" "Make sure you have write permissions to the data folder.\n" "(%1)\n" "\n" "The launcher cannot continue until you fix this problem.") .arg(dataPath)); return; } qInstallMessageHandler(appDebugOutput); qSetMessagePattern(defaultLogFormat); logModel.reset(new LogModel(this)); bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesPath = FS::PathCombine(dataPath, logRulesFile); qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); // search the dataPath() // seach app data standard path if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { qInfo() << "Found" << logRulesPath << "..."; foundLoggingRules = true; } } // seach root path if (!foundLoggingRules) { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) logRulesPath = FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME, logRulesFile); #else logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); #endif qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } if (foundLoggingRules) { // load and set logging rules qInfo() << "Loading logging rules from:" << logRulesPath; QSettings loggingRules(logRulesPath, QSettings::IniFormat); loggingRules.beginGroup("Rules"); QStringList rule_names = loggingRules.childKeys(); QStringList rules; qInfo() << "Setting log rules:"; for (auto rule_name : rule_names) { auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); rules.append(rule); qInfo() << " " << rule; } auto rules_str = rules.join("\n"); QLoggingCategory::setFilterRules(rules_str); } qInfo() << "<> Log initialized."; } { bool migrated = false; if (!migrated) migrated = handleDataMigration( dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", "polymc.cfg"); if (!migrated) migrated = handleDataMigration( dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", "multimc.cfg"); } { qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); qInfo() << "Version :" << BuildConfig.printableVersionString(); qInfo() << "Platform :" << BuildConfig.BUILD_PLATFORM; qInfo() << "Git commit :" << BuildConfig.GIT_COMMIT; qInfo() << "Git refspec :" << BuildConfig.GIT_REFSPEC; qInfo() << "Compiled for :" << BuildConfig.systemID(); qInfo() << "Compiled by :" << BuildConfig.compilerID(); qInfo() << "Build Artifact :" << BuildConfig.BUILD_ARTIFACT; qInfo() << "Updates Enabled :" << (updaterEnabled() ? "Yes" : "No"); if (adjustedBy.size()) { qInfo() << "Work dir before adjustment :" << origcwdPath; qInfo() << "Work dir after adjustment :" << QDir::currentPath(); qInfo() << "Adjusted by :" << adjustedBy; } else { qInfo() << "Work dir :" << QDir::currentPath(); } qInfo() << "Binary path :" << binPath; qInfo() << "Application root path :" << m_rootPath; if (!m_instanceIdToLaunch.isEmpty()) { qInfo() << "ID of instance to launch :" << m_instanceIdToLaunch; } if (!m_serverToJoin.isEmpty()) { qInfo() << "Address of server to join :" << m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { qInfo() << "Name of the world to join :" << m_worldToJoin; } qInfo() << "<> Paths set."; } if (m_liveCheck) { QFile check(liveCheckFile); if (check.open(QIODevice::WriteOnly | QIODevice::Truncate)) { auto payload = appID.toString().toUtf8(); if (check.write(payload) == payload.size()) { check.close(); } else { qWarning() << "Could not write into" << liveCheckFile << "!"; check.remove(); // also closes file! } } else { qWarning() << "Could not open" << liveCheckFile << "for writing!"; } } // Initialize application settings { // Provide a fallback for migration from PolyMC m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this)); // Theming m_settings->registerSetting("IconTheme", QString()); m_settings->registerSetting("ApplicationTheme", QString()); m_settings->registerSetting("BackgroundCat", QString("kitteh")); // Remembered state m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); m_settings->registerSetting("MenuBarInsteadOfToolBar", false); m_settings->registerSetting("NumberOfConcurrentTasks", 10); m_settings->registerSetting("NumberOfConcurrentDownloads", 6); m_settings->registerSetting("NumberOfManualRetries", 1); m_settings->registerSetting("RequestTimeout", 60); QString defaultMonospace; int defaultSize = 11; #ifdef Q_OS_WIN32 defaultMonospace = "Courier"; defaultSize = 10; #elif defined(Q_OS_MAC) defaultMonospace = "Menlo"; #else defaultMonospace = "Monospace"; #endif // resolve the font so the default actually matches QFont consoleFont; consoleFont.setFamily(defaultMonospace); consoleFont.setStyleHint(QFont::Monospace); consoleFont.setFixedPitch(true); QFontInfo consoleFontInfo(consoleFont); QString resolvedDefaultMonospace = consoleFontInfo.family(); QFont resolvedFont(resolvedDefaultMonospace); qDebug().nospace() << "Detected default console font: " << resolvedDefaultMonospace << ", substitutions: " << resolvedFont.substitutions().join(','); m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace); m_settings->registerSetting("ConsoleFontSize", defaultSize); m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); logModel->setMaxLines(getConsoleMaxLines(settings())); logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(settings())); logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); // Folders m_settings->registerSetting("InstanceDir", "instances"); m_settings->registerSetting({ "CentralModsDir", "ModsDir" }, "mods"); m_settings->registerSetting("IconsDir", "icons"); m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDirWatchRecursive", false); m_settings->registerSetting("MoveModsFromDownloadsDir", false); m_settings->registerSetting("SkinsDir", "skins"); m_settings->registerSetting("JavaDir", "java"); #ifdef Q_OS_MACOS // Folder security-scoped bookmarks m_settings->registerSetting("InstanceDirBookmark", ""); m_settings->registerSetting("CentralModsDirBookmark", ""); m_settings->registerSetting("IconsDirBookmark", ""); m_settings->registerSetting("DownloadsDirBookmark", ""); m_settings->registerSetting("SkinsDirBookmark", ""); m_settings->registerSetting("JavaDirBookmark", ""); #endif // Editors m_settings->registerSetting("JsonEditor", QString()); // Language m_settings->registerSetting("Language", QString()); m_settings->registerSetting("UseSystemLocale", false); // Console m_settings->registerSetting("ShowConsole", false); m_settings->registerSetting("AutoCloseConsole", false); m_settings->registerSetting("ShowConsoleOnError", true); m_settings->registerSetting("LogPrePostOutput", true); // Window Size m_settings->registerSetting({ "LaunchMaximized", "MCWindowMaximize" }, false); m_settings->registerSetting({ "MinecraftWinWidth", "MCWindowWidth" }, 854); m_settings->registerSetting({ "MinecraftWinHeight", "MCWindowHeight" }, 480); // Proxy Settings m_settings->registerSetting("ProxyType", "None"); m_settings->registerSetting({ "ProxyAddr", "ProxyHostName" }, "127.0.0.1"); m_settings->registerSetting("ProxyPort", 8080); m_settings->registerSetting({ "ProxyUser", "ProxyUsername" }, ""); m_settings->registerSetting({ "ProxyPass", "ProxyPassword" }, ""); // Memory m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::suitableMaxMem()); m_settings->registerSetting("PermGen", 128); // Java Settings m_settings->registerSetting("JavaPath", ""); m_settings->registerSetting("JavaSignature", ""); m_settings->registerSetting("JavaArchitecture", ""); m_settings->registerSetting("JavaRealArchitecture", ""); m_settings->registerSetting("JavaVersion", ""); m_settings->registerSetting("JavaVendor", ""); m_settings->registerSetting("LastHostname", ""); m_settings->registerSetting("JvmArgs", ""); m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); auto defaultEnableAutoJava = m_settings->get("JavaPath").toString().isEmpty(); m_settings->registerSetting("AutomaticJavaSwitch", defaultEnableAutoJava); m_settings->registerSetting("AutomaticJavaDownload", defaultEnableAutoJava); m_settings->registerSetting("UserAskedAboutAutomaticJavaDownload", false); // Legacy settings m_settings->registerSetting("OnlineFixes", false); // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("CustomOpenALPath", ""); m_settings->registerSetting("UseNativeGLFW", false); m_settings->registerSetting("CustomGLFWPath", ""); // Performance related options m_settings->registerSetting("EnableFeralGamemode", false); m_settings->registerSetting("EnableMangoHud", false); m_settings->registerSetting("UseDiscreteGpu", false); m_settings->registerSetting("UseZink", false); // Game time m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGlobalGameTime", true); m_settings->registerSetting("RecordGameTime", true); m_settings->registerSetting("ShowGameTimeWithoutDays", false); // Minecraft mods m_settings->registerSetting("ModMetadataDisabled", false); m_settings->registerSetting("ModDependenciesDisabled", false); m_settings->registerSetting("SkipModpackUpdatePrompt", false); // Minecraft offline player name m_settings->registerSetting("LastOfflinePlayerName", ""); // Wrapper command for launch m_settings->registerSetting("WrapperCommand", ""); // Custom Commands m_settings->registerSetting({ "PreLaunchCommand", "PreLaunchCmd" }, ""); m_settings->registerSetting({ "PostExitCommand", "PostExitCmd" }, ""); // The cat m_settings->registerSetting("TheCat", false); m_settings->registerSetting("CatOpacity", 100); m_settings->registerSetting("CatFit", "fit"); m_settings->registerSetting("StatusBarVisible", true); m_settings->registerSetting("ToolbarsLocked", false); // Instance m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("InstRenamingMode", "AskEverytime"); m_settings->registerSetting("SelectedInstance", QString()); // Window state and geometry m_settings->registerSetting("MainWindowState", ""); m_settings->registerSetting("MainWindowGeometry", ""); m_settings->registerSetting("ConsoleWindowState", ""); m_settings->registerSetting("ConsoleWindowGeometry", ""); m_settings->registerSetting("SettingsGeometry", ""); m_settings->registerSetting("PagedGeometry", ""); m_settings->registerSetting("NewInstanceGeometry", ""); m_settings->registerSetting("UpdateDialogGeometry", ""); m_settings->registerSetting("ModDownloadGeometry", ""); m_settings->registerSetting("RPDownloadGeometry", ""); m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); m_settings->registerSetting("DataPackDownloadGeometry", ""); // data pack window // in future, more pages may be added - so this name is chosen to avoid needing migration m_settings->registerSetting("WorldManagementGeometry", ""); // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); m_settings->registerSetting("PastebinCustomAPIBase", ""); QString pastebinURL = m_settings->get("PastebinURL").toString(); bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; if (!pastebinURL.isEmpty() && !userHadDefaultPastebin) { m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); m_settings->set("PastebinCustomAPIBase", pastebinURL); m_settings->reset("PastebinURL"); } bool ok; int pasteType = m_settings->get("PastebinType").toInt(&ok); // If PastebinType is invalid then reset the related settings. if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last)) { m_settings->reset("PastebinType"); m_settings->reset("PastebinCustomAPIBase"); } } { // Meta URL m_settings->registerSetting("MetaURLOverride", ""); QUrl metaUrl(m_settings->get("MetaURLOverride").toString()); // get rid of invalid meta urls if (!metaUrl.isValid() || (metaUrl.scheme() != "http" && metaUrl.scheme() != "https")) m_settings->reset("MetaURLOverride"); // Resource URL m_settings->registerSetting("ResourceURL", BuildConfig.DEFAULT_RESOURCE_BASE); QUrl resourceUrl(m_settings->get("ResourceURL").toString()); // get rid of invalid resource urls if (!resourceUrl.isValid() || (resourceUrl.scheme() != "http" && resourceUrl.scheme() != "https")) m_settings->reset("ResourceURL"); } m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); m_settings->registerSetting("Env", "{}"); // Custom Microsoft Authentication Client ID m_settings->registerSetting("MSAClientIDOverride", ""); // Custom Flame API Key { m_settings->registerSetting("CFKeyOverride", ""); m_settings->registerSetting("FlameKeyOverride", ""); QString flameKey = m_settings->get("CFKeyOverride").toString(); if (!flameKey.isEmpty()) m_settings->set("FlameKeyOverride", flameKey); m_settings->reset("CFKeyOverride"); } m_settings->registerSetting("ModrinthToken", ""); m_settings->registerSetting("UserAgentOverride", ""); // FTBApp instances m_settings->registerSetting("FTBAppInstancesPath", ""); // Custom Technic Client ID m_settings->registerSetting("TechnicClientID", ""); // Init page provider { m_globalSettingsProvider = std::make_shared(tr("Settings")); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); } PixmapCache::setInstance(new PixmapCache(this)); qInfo() << "<> Settings loaded."; } #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(groupViewAccessibleFactory); #endif /* !QT_NO_ACCESSIBILITY */ // initialize network access and proxy setup { m_network.reset(new QNetworkAccessManager()); QString proxyTypeStr = settings()->get("ProxyType").toString(); QString addr = settings()->get("ProxyAddr").toString(); int port = settings()->get("ProxyPort").value(); QString user = settings()->get("ProxyUser").toString(); QString pass = settings()->get("ProxyPass").toString(); updateProxySettings(proxyTypeStr, addr, port, user, pass); qInfo() << "<> Network done."; } // load translations { m_translations.reset(new TranslationsModel("translations")); auto bcp47Name = m_settings->get("Language").toString(); m_translations->selectLanguage(bcp47Name); qInfo() << "Your language is" << bcp47Name; qInfo() << "<> Translations loaded."; } // Instance icons { auto setting = APPLICATION->settings()->getSetting("IconsDir"); QStringList instFolders = { ":/icons/multimc/32x32/instances/", ":/icons/multimc/50x50/instances/", ":/icons/multimc/128x128/instances/", ":/icons/multimc/scalable/instances/" }; m_icons.reset(new IconList(instFolders, setting->get().toString())); connect(setting.get(), &Setting::SettingChanged, [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); qInfo() << "<> Instance icons initialized."; } // Themes m_themeManager = std::make_unique(); #ifdef Q_OS_MACOS // for macOS: getting directory settings will generate URL security-scoped bookmarks if needed and not present // this facilitates a smooth transition from a non-sandboxed version of the launcher, that likely can access the directory, // and a sandboxed version that can't access the directory without a bookmark // this section can likely be removed once the sandboxed version has been released for a while and migrations aren't done anymore { m_settings->get("InstanceDir"); m_settings->get("CentralModsDir"); m_settings->get("IconsDir"); m_settings->get("DownloadsDir"); m_settings->get("SkinsDir"); m_settings->get("JavaDir"); } #endif // initialize and load all instances { auto InstDirSetting = m_settings->getSetting("InstanceDir"); // instance path: check for problems with '!' in instance path and warn the user in the log // and remember that we have to show him a dialog when the gui starts (if it does so) QString instDir = m_settings->get("InstanceDir").toString(); qInfo() << "Instance path :" << instDir; if (FS::checkProblemticPathJava(QDir(instDir))) { qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; } m_instances.reset(new InstanceList(m_settings, instDir, this)); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); qInfo() << "Loading Instances..."; m_instances->loadList(); qInfo() << "<> Instances loaded."; } // and accounts { m_accounts.reset(new AccountList(this)); qInfo() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); m_accounts->fillQueue(); qInfo() << "<> Accounts loaded."; } // init the http meta cache { m_metacache.reset(new HttpMetaCache("metacache")); m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath()); m_metacache->addBase("libraries", QDir("libraries").absolutePath()); m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath()); m_metacache->addBase("general", QDir("cache").absolutePath()); m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath()); m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath()); m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); m_metacache->addBase("ModrinthModpacks", QDir("cache/ModrinthModpacks").absolutePath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("java", QDir("cache/java").absolutePath()); m_metacache->Load(); qInfo() << "<> Cache initialized."; } // now we have network, download translation updates m_translations->downloadIndex(); // FIXME: what to do with these? m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory())); m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory())); m_profilers.insert("generic", std::shared_ptr(new GenericProfilerFactory())); for (auto profiler : m_profilers.values()) { profiler->registerSettings(m_settings); } // Create the MCEdit thing... why is this here? { m_mcedit.reset(new MCEditTool(m_settings)); } #ifdef Q_OS_MACOS connect(this, &Application::clickedOnDock, [this]() { this->showMainWindow(); }); #endif connect(this, &Application::aboutToQuit, [this]() { if (m_instances) { // save any remaining instance state m_instances->saveNow(); } if (logFile) { logFile->flush(); logFile->close(); } }); updateCapabilities(); detectLibraries(); // check update locks { auto update_log_path = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log"); auto update_lock = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.lock")); if (update_lock.exists()) { auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock.absoluteFilePath()); auto infoMsg = tr("This installation has a update lock file present at: %1\n" "\n" "Timestamp: %2\n" "Updating from version %3 to %4\n" "Target install path: %5\n" "Data Path: %6" "\n" "This likely means that a update attempt failed. Please ensure your installation is in working order before " "proceeding.\n" "Check the Prism Launcher updater log at: \n" "%7\n" "for details on the last update attempt.\n" "\n" "To delete this lock and proceed select \"Ignore\" below.") .arg(update_lock.absoluteFilePath()) .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path) .arg(update_log_path); auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update In Progress"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort); msgBox.setDefaultButton(QMessageBox::Abort); msgBox.setModal(true); msgBox.setDetailedText(FS::read(update_log_path)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); auto res = msgBox.exec(); switch (res) { case QMessageBox::Ignore: { FS::deletePath(update_lock.absoluteFilePath()); break; } case QMessageBox::Abort: [[fallthrough]]; default: { qDebug() << "Exiting because update lockfile is present"; QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); return; } } } auto update_fail_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.fail")); if (update_fail_marker.exists()) { auto infoMsg = tr("An update attempt failed\n" "\n" "Please ensure your installation is in working order before " "proceeding.\n" "Check the Prism Launcher updater log at: \n" "%1\n" "for details on the last update attempt.") .arg(update_log_path); auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Failed"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort); msgBox.setDefaultButton(QMessageBox::Abort); msgBox.setModal(true); msgBox.setDetailedText(FS::read(update_log_path)); msgBox.setMinimumWidth(460); msgBox.adjustSize(); auto res = msgBox.exec(); switch (res) { case QMessageBox::Ignore: { FS::deletePath(update_fail_marker.absoluteFilePath()); break; } case QMessageBox::Abort: [[fallthrough]]; default: { qDebug() << "Exiting because update lockfile is present"; QMetaObject::invokeMethod(this, []() { exit(1); }, Qt::QueuedConnection); return; } } } auto update_success_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.success")); if (update_success_marker.exists()) { auto infoMsg = tr("Update succeeded\n" "\n" "You are now running %1 .\n" "Check the Prism Launcher updater log at: \n" "%2\n" "for details.") .arg(BuildConfig.printableVersionString()) .arg(update_log_path); auto msgBox = new QMessageBox(QMessageBox::Information, tr("Update Succeeded"), infoMsg, QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setDetailedText(FS::read(update_log_path)); msgBox->setAttribute(Qt::WA_DeleteOnClose); msgBox->setMinimumWidth(460); msgBox->adjustSize(); msgBox->open(); FS::deletePath(update_success_marker.absoluteFilePath()); } } // notify user if /tmp is mounted with `noexec` (#1693) QString jvmArgs = m_settings->get("JvmArgs").toString(); if (jvmArgs.indexOf("java.io.tmpdir") == -1) { /* java.io.tmpdir is a valid workaround, so don't annoy */ bool is_tmp_noexec = false; #if defined(Q_OS_LINUX) struct statvfs tmp_stat; statvfs("/tmp", &tmp_stat); is_tmp_noexec = tmp_stat.f_flag & ST_NOEXEC; #elif defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) struct statfs tmp_stat; statfs("/tmp", &tmp_stat); is_tmp_noexec = tmp_stat.f_flags & MNT_NOEXEC; #endif if (is_tmp_noexec) { auto infoMsg = tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n" "Some versions of Minecraft may not launch.\n" "\n" "You may solve this issue by remounting /tmp as 'exec' or setting " "the java.io.tmpdir JVM argument to a writeable directory in a " "filesystem where the 'exec' flag is set (e.g., /home/user/.local/tmp)\n"); auto msgBox = new QMessageBox(QMessageBox::Information, tr("Incompatible system configuration"), infoMsg, QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setAttribute(Qt::WA_DeleteOnClose); msgBox->setMinimumWidth(460); msgBox->adjustSize(); msgBox->open(); } } if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { installEventFilter(new ToolTipFilter); } if (createSetupWizard()) { return; } m_themeManager->applyCurrentlySelectedTheme(true); performMainStartupAction(); } bool Application::createSetupWizard() { bool javaRequired = [this]() { if (BuildConfig.JAVA_DOWNLOADER_ENABLED && settings()->get("AutomaticJavaDownload").toBool()) { return false; } bool ignoreJavaWizard = settings()->get("IgnoreJavaWizard").toBool(); if (ignoreJavaWizard) { return false; } QString currentHostName = QHostInfo::localHostName(); QString oldHostName = settings()->get("LastHostname").toString(); if (currentHostName != oldHostName) { settings()->set("LastHostname", currentHostName); return true; } QString currentJavaPath = settings()->get("JavaPath").toString(); QString actualPath = FS::ResolveExecutable(currentJavaPath); return actualPath.isNull(); }(); bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !settings()->get("AutomaticJavaDownload").toBool() && !settings()->get("AutomaticJavaSwitch").toBool() && !settings()->get("UserAskedAboutAutomaticJavaDownload").toBool(); bool languageRequired = settings()->get("Language").toString().isEmpty(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString()); bool validIcons = m_themeManager->isValidIconTheme(settings()->get("IconTheme").toString()); bool login = !m_accounts->anyAccountIsValid() && capabilities() & Application::SupportsMSA; bool themeInterventionRequired = !validWidgets || !validIcons; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired || askjava || login; if (wizardRequired) { // set default theme after going into theme wizard if (!validIcons) settings()->set("IconTheme", QString("pe_colored")); if (!validWidgets) { #if defined(Q_OS_WIN32) && QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) const QString style = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark ? QStringLiteral("dark") : QStringLiteral("bright"); #else const QString style = QStringLiteral("system"); #endif settings()->set("ApplicationTheme", style); } m_themeManager->applyCurrentlySelectedTheme(true); m_setupWizard = new SetupWizard(nullptr); if (languageRequired) { m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard)); } if (javaRequired) { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); } else if (askjava) { m_setupWizard->addPage(new AutoJavaWizardPage(m_setupWizard)); } if (pasteInterventionRequired) { m_setupWizard->addPage(new PasteWizardPage(m_setupWizard)); } if (themeInterventionRequired) { m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard)); } if (login) { m_setupWizard->addPage(new LoginWizardPage(m_setupWizard)); } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); } return wizardRequired || login; } bool Application::updaterEnabled() { #if defined(Q_OS_MAC) return BuildConfig.UPDATER_ENABLED; #else return BuildConfig.UPDATER_ENABLED && QFileInfo(FS::PathCombine(m_rootPath, updaterBinaryName())).isFile(); #endif } QString Application::updaterBinaryName() { auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exe_name.append(".exe"); #else exe_name.prepend("bin/"); #endif return exe_name; } bool Application::event(QEvent* event) { #ifdef Q_OS_MACOS if (event->type() == QEvent::ApplicationStateChange) { auto ev = static_cast(event); if (m_prevAppState == Qt::ApplicationActive && ev->applicationState() == Qt::ApplicationActive) { emit clickedOnDock(); } m_prevAppState = ev->applicationState(); } #endif if (event->type() == QEvent::FileOpen) { if (!m_mainWindow) { showMainWindow(false); } auto ev = static_cast(event); m_mainWindow->processURLs({ ev->url() }); } return QApplication::event(event); } void Application::setupWizardFinished(int status) { qDebug() << "Wizard result =" << status; performMainStartupAction(); } void Application::performMainStartupAction() { m_status = Application::Initialized; if (!m_instanceIdToLaunch.isEmpty()) { auto inst = instances()->getInstanceById(m_instanceIdToLaunch); if (inst) { MinecraftTarget::Ptr targetToJoin = nullptr; MinecraftAccountPtr accountToUse = nullptr; qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; if (!m_serverToJoin.isEmpty()) { // FIXME: validate the server string targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_serverToJoin, false))); qDebug() << " Launching with server" << m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_worldToJoin, true))); qDebug() << " Launching with world" << m_worldToJoin; } if (!m_profileToUse.isEmpty()) { accountToUse = accounts()->getAccountByProfileName(m_profileToUse); if (!accountToUse) { return; } qDebug() << " Launching with account" << m_profileToUse; } launch(inst, !m_offline, false, targetToJoin, accountToUse, m_offlineName); return; } } if (!m_instanceIdToShowWindowOf.isEmpty()) { auto inst = instances()->getInstanceById(m_instanceIdToShowWindowOf); if (inst) { qDebug() << "<> Showing window of instance " << m_instanceIdToShowWindowOf; showInstanceWindow(inst); return; } } if (!m_mainWindow) { // normal main window showMainWindow(false); qDebug() << "<> Main window shown."; } // initialize the updater if (updaterEnabled()) { qDebug() << "Initializing updater"; #ifdef Q_OS_MAC #if defined(SPARKLE_ENABLED) m_updater.reset(new MacSparkleUpdater()); #endif #else m_updater.reset(new PrismExternalUpdater(m_mainWindow, m_rootPath, m_dataPath)); #endif qDebug() << "<> Updater started."; } { // delete instances tmp dirctory auto instDir = m_settings->get("InstanceDir").toString(); const QString tempRoot = FS::PathCombine(instDir, ".tmp"); FS::deletePath(tempRoot); } if (!m_urlsToImport.isEmpty()) { qDebug() << "<> Importing from url:" << m_urlsToImport; m_mainWindow->processURLs(m_urlsToImport); } } void Application::showFatalErrorMessage(const QString& title, const QString& content) { m_status = Application::Failed; auto dialog = CustomMessageBox::selectable(nullptr, title, content, QMessageBox::Critical); dialog->exec(); } Application::~Application() { // Shut down logger by setting the logger function to nothing qInstallMessageHandler(nullptr); #if defined Q_OS_WIN32 // Detach from Windows console if (consoleAttached) { fclose(stdout); fclose(stdin); fclose(stderr); FreeConsole(); } #endif } void Application::messageReceived(const QByteArray& message) { ApplicationMessage received; received.parse(message); auto& command = received.command; if (status() != Initialized) { bool isLoginAtempt = false; if (command == "import") { QString url = received.args["url"]; isLoginAtempt = !url.isEmpty() && normalizeImportUrl(url).scheme() == BuildConfig.LAUNCHER_APP_BINARY_NAME; } if (!isLoginAtempt) { qDebug() << "Received message" << message << "while still initializing. It will be ignored."; return; } } if (command == "activate") { showMainWindow(); } else if (command == "import") { QString url = received.args["url"]; if (url.isEmpty()) { qWarning() << "Received" << command << "message without a zip path/URL."; return; } if (!m_mainWindow) { showMainWindow(false); } m_mainWindow->processURLs({ normalizeImportUrl(url) }); } else if (command == "launch") { QString id = received.args["id"]; QString server = received.args["server"]; QString world = received.args["world"]; QString profile = received.args["profile"]; bool offline = received.args["offline_enabled"] == "true"; QString offlineName = received.args["offline_name"]; InstancePtr instance; if (!id.isEmpty()) { instance = instances()->getInstanceById(id); if (!instance) { qWarning() << "Launch command requires an valid instance ID. " << id << "resolves to nothing."; return; } } else { qWarning() << "Launch command called without an instance ID..."; return; } MinecraftTarget::Ptr serverObject = nullptr; if (!server.isEmpty()) { serverObject = std::make_shared(MinecraftTarget::parse(server, false)); } else if (!world.isEmpty()) { serverObject = std::make_shared(MinecraftTarget::parse(world, true)); } MinecraftAccountPtr accountObject; if (!profile.isEmpty()) { accountObject = accounts()->getAccountByProfileName(profile); if (!accountObject) { qWarning() << "Launch command requires the specified profile to be valid. " << profile << "does not resolve to any account."; return; } } launch(instance, !offline, false, serverObject, accountObject, offlineName); } else { qWarning() << "Received invalid message" << message; } } std::shared_ptr Application::translations() { return m_translations; } std::shared_ptr Application::javalist() { if (!m_javalist) { m_javalist.reset(new JavaInstallList()); } return m_javalist; } QIcon Application::logo() { return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); } bool Application::openJsonEditor(const QString& filename) { const QString file = QDir::current().absoluteFilePath(filename); if (m_settings->get("JsonEditor").toString().isEmpty()) { return DesktopServices::openUrl(QUrl::fromLocalFile(file)); } else { // return DesktopServices::openFile(m_settings->get("JsonEditor").toString(), file); return DesktopServices::run(m_settings->get("JsonEditor").toString(), { file }); } } bool Application::launch(InstancePtr instance, bool online, bool demo, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse, const QString& offlineName) { if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; } else if (instance->canLaunch()) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; auto window = extras.window; if (window) { if (!window->saveAll()) { return false; } } auto& controller = extras.controller; controller.reset(new LaunchController()); controller->setInstance(instance); controller->setOnline(online); controller->setDemo(demo); controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); controller->setOfflineName(offlineName); if (window) { controller->setParentWidget(window); } else if (m_mainWindow) { controller->setParentWidget(m_mainWindow); } connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded); connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); connect(controller.get(), &LaunchController::aborted, this, [this] { controllerFailed(tr("Aborted")); }); addRunningInstance(); QMetaObject::invokeMethod(controller.get(), &Task::start, Qt::QueuedConnection); return true; } else if (instance->isRunning()) { showInstanceWindow(instance, "console"); return true; } else if (instance->canEdit()) { showInstanceWindow(instance); return true; } return false; } bool Application::kill(InstancePtr instance) { if (!instance->isRunning()) { qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; return false; } QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instance->id()]; // NOTE: copy of the shared pointer keeps it alive auto controller = extras.controller; locker.unlock(); if (controller) { return controller->abort(); } return true; } void Application::closeCurrentWindow() { if (focusWindow()) focusWindow()->close(); } void Application::addRunningInstance() { m_runningInstances++; if (m_runningInstances == 1) { emit updateAllowedChanged(false); } } void Application::subRunningInstance() { if (m_runningInstances == 0) { qCritical() << "Something went really wrong and we now have less than 0 running instances... WTF"; return; } m_runningInstances--; if (m_runningInstances == 0) { emit updateAllowedChanged(true); } } bool Application::shouldExitNow() const { return m_runningInstances == 0 && m_openWindows == 0; } bool Application::updatesAreAllowed() { return m_runningInstances == 0; } void Application::updateIsRunning(bool running) { m_updateRunning = running; } void Application::controllerSucceeded() { auto controller = qobject_cast(sender()); if (!controller) return; auto id = controller->id(); QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; // on success, do... if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) { if (extras.window) { QMetaObject::invokeMethod(extras.window, &QWidget::close, Qt::QueuedConnection); } } extras.controller.reset(); subRunningInstance(); // quit when there are no more windows. if (shouldExitNow()) { m_status = Status::Succeeded; exit(0); } } void Application::controllerFailed(const QString& error) { Q_UNUSED(error); auto controller = qobject_cast(sender()); if (!controller) return; auto id = controller->id(); QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; // on failure, do... nothing extras.controller.reset(); subRunningInstance(); // quit when there are no more windows. if (shouldExitNow()) { m_status = Status::Failed; exit(1); } } void Application::ShowGlobalSettings(class QWidget* parent, QString open_page) { if (!m_globalSettingsProvider) { return; } emit globalSettingsAboutToOpen(); { SettingsObject::Lock lock(APPLICATION->settings()); PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent); connect(&dlg, &PageDialog::applied, this, &Application::globalSettingsApplied); dlg.exec(); } } MainWindow* Application::showMainWindow(bool minimized) { if (m_mainWindow) { m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized); m_mainWindow->raise(); m_mainWindow->activateWindow(); } else { m_mainWindow = new MainWindow(); m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toString().toUtf8())); m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toString().toUtf8())); if (minimized) { m_mainWindow->showMinimized(); } else { m_mainWindow->show(); } m_mainWindow->checkInstancePathForProblems(); connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged); connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose); m_openWindows++; } return m_mainWindow; } ViewLogWindow* Application::showLogWindow() { if (m_viewLogWindow) { m_viewLogWindow->setWindowState(m_viewLogWindow->windowState() & ~Qt::WindowMinimized); m_viewLogWindow->raise(); m_viewLogWindow->activateWindow(); } else { m_viewLogWindow = new ViewLogWindow(); connect(m_viewLogWindow, &ViewLogWindow::isClosing, this, &Application::on_windowClose); m_openWindows++; } return m_viewLogWindow; } InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) { if (!instance) return nullptr; auto id = instance->id(); QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[id]; auto& window = extras.window; if (window) { // If the window is minimized on macOS or Windows, activate and bring it up #ifdef Q_OS_MACOS if (window->isMinimized()) { window->setWindowState(window->windowState() & ~Qt::WindowMinimized); } #elif defined(Q_OS_WIN) if (window->isMinimized()) { window->showNormal(); } #endif window->raise(); window->activateWindow(); } else { window = new InstanceWindow(instance); m_openWindows++; connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose); } if (!page.isEmpty()) { window->selectPage(page); } if (extras.controller) { extras.controller->setParentWidget(window); } return window; } void Application::on_windowClose() { m_openWindows--; auto instWindow = qobject_cast(sender()); if (instWindow) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instWindow->instanceId()]; extras.window = nullptr; if (extras.controller) { extras.controller->setParentWidget(m_mainWindow); } } auto mainWindow = qobject_cast(sender()); if (mainWindow) { m_mainWindow = nullptr; } auto logWindow = qobject_cast(sender()); if (logWindow) { m_viewLogWindow = nullptr; } // quit when there are no more windows. if (shouldExitNow()) { exit(0); } } void Application::updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password) { // Set the application proxy settings. if (proxyTypeStr == "SOCKS5") { QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, addr, port, user, password)); } else if (proxyTypeStr == "HTTP") { QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::HttpProxy, addr, port, user, password)); } else if (proxyTypeStr == "None") { // If we have no proxy set, set no proxy and return. QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::NoProxy)); } else { // If we have "Default" selected, set Qt to use the system proxy settings. QNetworkProxyFactory::setUseSystemConfiguration(true); } qDebug() << "Detecting proxy settings..."; QNetworkProxy proxy = QNetworkProxy::applicationProxy(); m_network->setProxy(proxy); QString proxyDesc; if (proxy.type() == QNetworkProxy::NoProxy) { qDebug() << "Using no proxy is an option!"; return; } switch (proxy.type()) { case QNetworkProxy::DefaultProxy: proxyDesc = "Default proxy: "; break; case QNetworkProxy::Socks5Proxy: proxyDesc = "Socks5 proxy: "; break; case QNetworkProxy::HttpProxy: proxyDesc = "HTTP proxy: "; break; case QNetworkProxy::HttpCachingProxy: proxyDesc = "HTTP caching: "; break; case QNetworkProxy::FtpCachingProxy: proxyDesc = "FTP caching: "; break; default: proxyDesc = "DERP proxy: "; break; } proxyDesc += QString("%1:%2").arg(proxy.hostName()).arg(proxy.port()); qDebug() << proxyDesc; } shared_qobject_ptr Application::metacache() { return m_metacache; } shared_qobject_ptr Application::network() { return m_network; } shared_qobject_ptr Application::metadataIndex() { if (!m_metadataIndex) { m_metadataIndex.reset(new Meta::Index()); } return m_metadataIndex; } void Application::updateCapabilities() { m_capabilities = None; if (!getMSAClientID().isEmpty()) m_capabilities |= SupportsMSA; if (!getFlameAPIKey().isEmpty()) m_capabilities |= SupportsFlame; #ifdef Q_OS_LINUX if (gamemode_query_status() >= 0) m_capabilities |= SupportsGameMode; if (!MangoHud::getLibraryString().isEmpty()) m_capabilities |= SupportsMangoHud; #endif } void Application::detectLibraries() { #ifdef Q_OS_LINUX m_detectedGLFWPath = MangoHud::findLibrary(BuildConfig.GLFW_LIBRARY_NAME); m_detectedOpenALPath = MangoHud::findLibrary(BuildConfig.OPENAL_LIBRARY_NAME); qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath; #endif } QString Application::getJarPath(QString jarFile) { QStringList potentialPaths = { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME), #endif FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"), FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging }; for (QString p : potentialPaths) { QString jarPath = FS::PathCombine(p, jarFile); if (QFileInfo(jarPath).isFile()) return jarPath; } return {}; } QString Application::getMSAClientID() { QString clientIDOverride = m_settings->get("MSAClientIDOverride").toString(); if (!clientIDOverride.isEmpty()) { return clientIDOverride; } return BuildConfig.MSA_CLIENT_ID; } QString Application::getFlameAPIKey() { QString keyOverride = m_settings->get("FlameKeyOverride").toString(); if (!keyOverride.isEmpty()) { return keyOverride; } return BuildConfig.FLAME_API_KEY; } QString Application::getModrinthAPIToken() { QString tokenOverride = m_settings->get("ModrinthToken").toString(); if (!tokenOverride.isEmpty()) return tokenOverride; return QString(); } QString Application::getUserAgent() { QString uaOverride = m_settings->get("UserAgentOverride").toString(); if (!uaOverride.isEmpty()) { return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); } return BuildConfig.USER_AGENT; } bool Application::handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, const QString& configFile) const { QString nomigratePath = FS::PathCombine(currentData, name + "_nomigrate.txt"); QStringList configPaths = { FS::PathCombine(oldData, configFile), FS::PathCombine(oldData, BuildConfig.LAUNCHER_CONFIGFILE) }; QLocale locale; // Is there a valid config at the old location? bool configExists = false; for (QString configPath : configPaths) { configExists |= QFileInfo::exists(configPath); } if (!configExists || QFileInfo::exists(nomigratePath)) { qDebug() << "<> No migration needed from" << name; return false; } QString message; bool currentExists = QFileInfo::exists(FS::PathCombine(currentData, BuildConfig.LAUNCHER_CONFIGFILE)); if (currentExists) { message = tr("Old data from %1 was found, but you already have existing data for %2. Sadly you will need to migrate yourself. Do " "you want to be reminded of the pending data migration next time you start %2?") .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME); } else { message = tr("It looks like you used %1 before. Do you want to migrate your data to the new location of %2?") .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME); QFileInfo logInfo(FS::PathCombine(oldData, name + "-0.log")); if (logInfo.exists()) { QString lastModified = logInfo.lastModified().toString(locale.dateFormat()); message = tr("It looks like you used %1 on %2 before. Do you want to migrate your data to the new location of %3?") .arg(name, lastModified, BuildConfig.LAUNCHER_DISPLAYNAME); } } QMessageBox::StandardButton askMoveDialogue = QMessageBox::question(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); auto setDoNotMigrate = [&nomigratePath] { QFile file(nomigratePath); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "setDoNotMigrate failed; Failed to open file '" << file.fileName() << "' for writing!"; } }; // create no-migrate file if user doesn't want to migrate if (askMoveDialogue != QMessageBox::Yes) { qDebug() << "<> Migration declined for" << name; setDoNotMigrate(); return currentExists; // cancel further migrations, if we already have a data directory } if (!currentExists) { // Migrate! using namespace Filters; QList filters; filters.append(equals(configFile)); filters.append(equals(BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before filters.append(startsWith("logs/")); filters.append(equals("accounts.json")); filters.append(startsWith("accounts/")); filters.append(startsWith("assets/")); filters.append(startsWith("icons/")); filters.append(startsWith("instances/")); filters.append(startsWith("libraries/")); filters.append(startsWith("mods/")); filters.append(startsWith("themes/")); ProgressDialog diag; DataMigrationTask task(oldData, currentData, any(std::move(filters))); if (diag.execWithTask(&task)) { qDebug() << "<> Migration succeeded"; setDoNotMigrate(); } else { QString reason = task.failReason(); QMessageBox::critical(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, tr("Migration failed! Reason: %1").arg(reason)); } } else { qWarning() << "<> Migration was skipped, due to existing data"; } return true; } void Application::triggerUpdateCheck() { if (m_updater) { qDebug() << "Checking for updates."; m_updater->setBetaAllowed(false); // There are no other channels than stable m_updater->checkForUpdates(); } else { qDebug() << "Updater not available."; } } QUrl Application::normalizeImportUrl(QString const& url) { auto local_file = QFileInfo(url); if (local_file.exists()) { return QUrl::fromLocalFile(local_file.absoluteFilePath()); } else { return QUrl::fromUserInput(url); } } const QString Application::javaPath() { return m_settings->get("JavaDir").toString(); } void Application::addQSavePath(QString path) { QMutexLocker locker(&m_qsaveResourcesMutex); m_qsaveResources[path] = m_qsaveResources.value(path, 0) + 1; } void Application::removeQSavePath(QString path) { QMutexLocker locker(&m_qsaveResourcesMutex); auto count = m_qsaveResources.value(path, 0) - 1; if (count <= 0) { m_qsaveResources.remove(path); } else { m_qsaveResources[path] = count; } } bool Application::checkQSavePath(QString path) { QMutexLocker locker(&m_qsaveResourcesMutex); for (auto partialPath : m_qsaveResources.keys()) { if (path.startsWith(partialPath) && m_qsaveResources.value(partialPath, 0) > 0) { return true; } } return false; } PrismLauncher-10.0.5/launcher/FileIgnoreProxy.h0000644000175100017510000000671615144136756021044 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "SeparatorPrefixTree.h" class FileIgnoreProxy : public QSortFilterProxyModel { Q_OBJECT public: FileIgnoreProxy(QString root, QObject* parent); // NOTE: Sadly, we have to do sorting ourselves. bool lessThan(const QModelIndex& left, const QModelIndex& right) const; virtual Qt::ItemFlags flags(const QModelIndex& index) const; virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); QString relPath(const QString& path) const; bool setFilterState(QModelIndex index, Qt::CheckState state); bool shouldExpand(QModelIndex index); void setBlockedPaths(QStringList paths); inline const SeparatorPrefixTree<'/'>& blockedPaths() const { return m_blocked; } inline SeparatorPrefixTree<'/'>& blockedPaths() { return m_blocked; } // list of file names that need to be removed completely from model inline QStringList& ignoreFilesWithName() { return m_ignoreFiles; } inline QStringList& ignoreFilesWithSuffix() { return m_ignoreFilesSuffixes; } // list of relative paths that need to be removed completely from model inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } bool filterFile(const QFileInfo& fileName) const; void loadBlockedPathsFromFile(const QString& fileName); void saveBlockedPathsToFile(const QString& fileName); protected: bool filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const; bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; bool ignoreFile(QFileInfo file) const; private: const QString m_root; SeparatorPrefixTree<'/'> m_blocked; QStringList m_ignoreFiles; QStringList m_ignoreFilesSuffixes; SeparatorPrefixTree<'/'> m_ignoreFilePaths; }; PrismLauncher-10.0.5/launcher/FileSystem.cpp0000644000175100017510000015710615144136756020376 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "FileSystem.h" #include #include "BuildConfig.h" #include #include #include #include #include #include #include #include #include #include #include #include "DesktopServices.h" #include "PSaveFile.h" #include "StringUtils.h" #if defined Q_OS_WIN32 #define NOMINMAX #define WIN32_LEAN_AND_MEAN #include #include #include #include #include #include #include #include // for ShellExecute #include #include #include #else #include #endif #include namespace fs = std::filesystem; // clone #if defined(Q_OS_LINUX) #include #include /* Definition of FICLONE* constants */ #include #include #include #elif defined(Q_OS_MACOS) #include #include #elif defined(Q_OS_WIN) // winbtrfs clone vs rundll32 shellbtrfs.dll,ReflinkCopy #include #include #include #include // refs #include #if defined(__MINGW32__) #include #endif #endif #if defined(Q_OS_WIN) #if defined(__MINGW32__) // Avoid re-defining structs retroactively added to MinGW // https://github.com/mingw-w64/mingw-w64/issues/90#issuecomment-2829284729 #if __MINGW64_VERSION_MAJOR < 13 struct _DUPLICATE_EXTENTS_DATA { HANDLE FileHandle; LARGE_INTEGER SourceFileOffset; LARGE_INTEGER TargetFileOffset; LARGE_INTEGER ByteCount; }; using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA; using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*; #endif struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 WORD Reserved; // Must be 0 DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx DWORD ChecksumChunkSizeInBytes; DWORD ClusterSizeInBytes; }; using FSCTL_GET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER; using PFSCTL_GET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER*; struct _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER { WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 WORD Reserved; // Must be 0 DWORD Flags; // FSCTL_INTEGRITY_FLAG_xxx }; using FSCTL_SET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER; using PFSCTL_SET_INTEGRITY_INFORMATION_BUFFER = _FSCTL_SET_INTEGRITY_INFORMATION_BUFFER*; #endif #ifndef FSCTL_DUPLICATE_EXTENTS_TO_FILE #define FSCTL_DUPLICATE_EXTENTS_TO_FILE CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 209, METHOD_BUFFERED, FILE_WRITE_DATA) #endif #ifndef FSCTL_GET_INTEGRITY_INFORMATION #define FSCTL_GET_INTEGRITY_INFORMATION \ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 159, METHOD_BUFFERED, FILE_ANY_ACCESS) // FSCTL_GET_INTEGRITY_INFORMATION_BUFFER #endif #ifndef FSCTL_SET_INTEGRITY_INFORMATION #define FSCTL_SET_INTEGRITY_INFORMATION \ CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 160, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA) // FSCTL_SET_INTEGRITY_INFORMATION_BUFFER #endif #ifndef ERROR_NOT_CAPABLE #define ERROR_NOT_CAPABLE 775L #endif #ifndef ERROR_BLOCK_TOO_MANY_REFERENCES #define ERROR_BLOCK_TOO_MANY_REFERENCES 347L #endif #endif namespace FS { void ensureExists(const QDir& dir) { if (!QDir().mkpath(dir.absolutePath())) { throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")"); } } void write(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); PSaveFile file(filename); if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (data.size() != file.write(data)) { throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } if (!file.commit()) { throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); } } void appendSafe(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); QByteArray buffer; try { buffer = read(filename); } catch (FileSystemException&) { buffer = QByteArray(); } buffer.append(data); PSaveFile file(filename); if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (buffer.size() != file.write(buffer)) { throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } if (!file.commit()) { throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); } } void append(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); QFile file(filename); if (!file.open(QFile::Append)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (data.size() != file.write(data)) { throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); } } QByteArray read(const QString& filename) { QFile file(filename); if (!file.open(QFile::ReadOnly)) { throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString()); } const qint64 size = file.size(); QByteArray data(int(size), 0); const qint64 ret = file.read(data.data(), size); if (ret == -1 || ret != size) { throw FileSystemException("Error reading data from " + filename + ": " + file.errorString()); } return data; } bool updateTimestamp(const QString& filename) { #ifdef Q_OS_WIN32 std::wstring filename_utf_16 = filename.toStdWString(); return (_wutime64(filename_utf_16.c_str(), nullptr) == 0); #else QByteArray filenameBA = QFile::encodeName(filename); return (utime(filenameBA.data(), nullptr) == 0); #endif } bool ensureFilePathExists(QString filenamepath) { QFileInfo a(filenamepath); QDir dir; QString ensuredPath = a.path(); bool success = dir.mkpath(ensuredPath); return success; } bool ensureFolderPathExists(const QFileInfo folderPath) { QDir dir; QString ensuredPath = folderPath.filePath(); if (folderPath.exists()) return true; bool success = dir.mkpath(ensuredPath); return success; } bool ensureFolderPathExists(const QString folderPathName) { return ensureFolderPathExists(QFileInfo(folderPathName)); } bool copyFileAttributes(QString src, QString dst) { #ifdef Q_OS_WIN32 auto attrs = GetFileAttributesW(src.toStdWString().c_str()); if (attrs == INVALID_FILE_ATTRIBUTES) return false; return SetFileAttributesW(dst.toStdWString().c_str(), attrs); #endif return true; } // needs folders to exists void copyFolderAttributes(QString src, QString dst, QString relative) { auto path = PathCombine(src, relative); QDir dsrc(src); while ((path = QFileInfo(path).path()).length() >= src.length()) { auto dst_path = PathCombine(dst, dsrc.relativeFilePath(path)); copyFileAttributes(path, dst_path); } } /** * @brief Copies a directory and it's contents from src to dest * @param offset subdirectory form src to copy to dest * @return if there was an error during the filecopy */ bool copy::operator()(const QString& offset, bool dryRun) { using copy_opts = fs::copy_options; m_copied = 0; // reset counter m_failedPaths.clear(); // NOTE always deep copy on windows. the alternatives are too messy. #if defined Q_OS_WIN32 m_followSymlinks = true; #endif auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); std::error_code err; fs::copy_options opt = copy_opts::none; // The default behavior is to follow symlinks if (!m_followSymlinks) opt |= copy_opts::copy_symlinks; if (m_overwrite) opt |= copy_opts::overwrite_existing; // Function that'll do the actual copying auto copy_file = [this, dryRun, src, dst, opt, &err](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); if (!dryRun) { ensureFilePathExists(dst_path); #ifdef Q_OS_WIN32 copyFolderAttributes(src, dst, relative_dst_path); #endif fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err); } if (err) { qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; m_failedPaths.append(dst_path); emit copyFailed(relative_dst_path); return; } m_copied++; emit fileCopied(relative_dst_path); }; // We can't use copy_opts::recursive because we need to take into account the // blacklisted paths, so we iterate over the source directory, and if there's no blacklist // match, we copy the file. QDir src_dir(src); QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); while (source_it.hasNext()) { auto src_path = source_it.next(); auto relative_path = src_dir.relativeFilePath(src_path); copy_file(src_path, relative_path); } // If the root src is not a directory, the previous iterator won't run. if (!fs::is_directory(StringUtils::toStdString(src))) copy_file(src, ""); return err.value() == 0; } /// qDebug print support for the LinkPair struct QDebug operator<<(QDebug debug, const LinkPair& lp) { QDebugStateSaver saver(debug); debug.nospace() << "LinkPair{ src: " << lp.src << " , dst: " << lp.dst << " }"; return debug; } bool create_link::operator()(const QString& offset, bool dryRun) { m_linked = 0; // reset counter m_path_results.clear(); m_links_to_make.clear(); m_path_results.clear(); make_link_list(offset); if (!dryRun) return make_links(); return true; } /** * @brief Make a list of all the links to make * @param offset subdirectory of src to link to dest */ void create_link::make_link_list(const QString& offset) { for (auto pair : m_path_pairs) { const QString& srcPath = pair.src; const QString& dstPath = pair.dst; auto src = PathCombine(QDir(srcPath).absolutePath(), offset); auto dst = PathCombine(QDir(dstPath).absolutePath(), offset); // you can't hard link a directory so make sure if we deal with a directory we do so recursively if (m_useHardLinks) m_recursive = true; // Function that'll do the actual linking auto link_file = [this, dst](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) { qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; return; } auto dst_path = PathCombine(dst, relative_dst_path); LinkPair link = { src_path, dst_path }; m_links_to_make.append(link); }; if ((!m_recursive) || !fs::is_directory(StringUtils::toStdString(src))) { if (m_debug) qDebug() << "linking single file or dir:" << src << "to" << dst; link_file(src, ""); } else { if (m_debug) qDebug().nospace() << "linking recursively: " << src << " to " << dst << ", max_depth: " << m_max_depth; QDir src_dir(src); QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); QStringList linkedPaths; while (source_it.hasNext()) { auto src_path = source_it.next(); auto relative_path = src_dir.relativeFilePath(src_path); if (m_max_depth >= 0 && pathDepth(relative_path) > m_max_depth) { relative_path = pathTruncate(relative_path, m_max_depth); src_path = src_dir.filePath(relative_path); if (linkedPaths.contains(src_path)) { continue; } } linkedPaths.append(src_path); link_file(src_path, relative_path); } } } } bool create_link::make_links() { for (auto link : m_links_to_make) { QString src_path = link.src; QString dst_path = link.dst; auto src_path_std = StringUtils::toStdString(link.src); auto dst_path_std = StringUtils::toStdString(link.dst); ensureFilePathExists(dst_path); if (m_useHardLinks) { if (m_debug) qDebug() << "making hard link:" << src_path << "to" << dst_path; fs::create_hard_link(src_path_std, dst_path_std, m_os_err); } else if (fs::is_directory(src_path_std)) { if (m_debug) qDebug() << "making directory_symlink:" << src_path << "to" << dst_path; fs::create_directory_symlink(src_path_std, dst_path_std, m_os_err); } else { if (m_debug) qDebug() << "making symlink:" << src_path << "to" << dst_path; fs::create_symlink(src_path_std, dst_path_std, m_os_err); } if (m_os_err) { qWarning() << "Failed to link files:" << QString::fromStdString(m_os_err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; qDebug() << "Error category:" << m_os_err.category().name(); qDebug() << "Error code:" << m_os_err.value(); emit linkFailed(src_path, dst_path, QString::fromStdString(m_os_err.message()), m_os_err.value()); } else { m_linked++; emit fileLinked(src_path, dst_path); } if (m_os_err) return false; } return true; } void create_link::runPrivileged(const QString& offset) { m_linked = 0; // reset counter m_path_results.clear(); m_links_to_make.clear(); bool gotResults = false; make_link_list(offset); QString serverName = BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink_server" + StringUtils::getRandomAlphaNumeric(); connect(&m_linkServer, &QLocalServer::newConnection, this, [this, &gotResults]() { qDebug() << "Client connected, sending out pairs"; // construct block of data to send QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); qint32 blocksize = quint32(sizeof(quint32)); for (auto link : m_links_to_make) { blocksize += quint32(link.src.size()); blocksize += quint32(link.dst.size()); } qDebug() << "About to write block of size:" << blocksize; out << blocksize; out << quint32(m_links_to_make.length()); for (auto link : m_links_to_make) { out << link.src; out << link.dst; } QLocalSocket* clientConnection = m_linkServer.nextPendingConnection(); connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater); connect(clientConnection, &QLocalSocket::readyRead, this, [&, clientConnection]() { QDataStream in; quint32 blockSize = 0; in.setDevice(clientConnection); qDebug() << "Reading path results from client"; qDebug() << "bytes available" << clientConnection->bytesAvailable(); // Relies on the fact that QDataStream serializes a quint32 into // sizeof(quint32) bytes if (clientConnection->bytesAvailable() < (int)sizeof(quint32)) return; qDebug() << "reading block size"; in >> blockSize; qDebug() << "blocksize is" << blockSize; qDebug() << "bytes available" << clientConnection->bytesAvailable(); if (clientConnection->bytesAvailable() < blockSize || in.atEnd()) return; quint32 numResults; in >> numResults; qDebug() << "numResults" << numResults; for (quint32 i = 0; i < numResults; i++) { FS::LinkResult result; in >> result.src; in >> result.dst; in >> result.err_msg; qint32 err_value; in >> err_value; result.err_value = err_value; if (result.err_value) { qDebug() << "privileged link fail" << result.src << "to" << result.dst << "code" << result.err_value << result.err_msg; emit linkFailed(result.src, result.dst, result.err_msg, result.err_value); } else { qDebug() << "privileged link success" << result.src << "to" << result.dst; m_linked++; emit fileLinked(result.src, result.dst); } m_path_results.append(result); } gotResults = true; qDebug() << "results received, closing connection"; clientConnection->close(); }); qint64 byteswritten = clientConnection->write(block); bool bytesflushed = clientConnection->flush(); qDebug() << "block flushed" << byteswritten << bytesflushed; }); qDebug() << "Listening on pipe" << serverName; if (!m_linkServer.listen(serverName)) { qDebug() << "Unable to start local pipe server on" << serverName << ":" << m_linkServer.errorString(); return; } ExternalLinkFileProcess* linkFileProcess = new ExternalLinkFileProcess(serverName, m_useHardLinks, this); connect(linkFileProcess, &ExternalLinkFileProcess::processExited, this, [this, &gotResults]() { emit finishedPrivileged(gotResults); }); connect(linkFileProcess, &ExternalLinkFileProcess::finished, linkFileProcess, &QObject::deleteLater); linkFileProcess->start(); } void ExternalLinkFileProcess::runLinkFile() { QString fileLinkExe = PathCombine(QCoreApplication::instance()->applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME + "_filelink"); QString params = "-s " + m_server; params += " -H " + QVariant(m_useHardLinks).toString(); #if defined Q_OS_WIN32 SHELLEXECUTEINFO ShExecInfo; fileLinkExe = fileLinkExe + ".exe"; qDebug() << "Running: runas" << fileLinkExe << params; LPCWSTR programNameWin = (const wchar_t*)fileLinkExe.utf16(); LPCWSTR paramsWin = (const wchar_t*)params.utf16(); // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa ShExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); ShExecInfo.fMask = SEE_MASK_NOCLOSEPROCESS; ShExecInfo.hwnd = NULL; // Optional. A handle to the owner window, used to display and position any UI that the system might produce // while executing this function. ShExecInfo.lpVerb = L"runas"; // elevate to admin, show UAC ShExecInfo.lpFile = programNameWin; ShExecInfo.lpParameters = paramsWin; ShExecInfo.lpDirectory = NULL; ShExecInfo.nShow = SW_HIDE; ShExecInfo.hInstApp = NULL; ShellExecuteEx(&ShExecInfo); WaitForSingleObject(ShExecInfo.hProcess, INFINITE); CloseHandle(ShExecInfo.hProcess); #endif qDebug() << "Process exited"; } bool moveByCopy(const QString& source, const QString& dest) { if (!copy(source, dest)()) { // copy qDebug() << "Copy of" << source << "to" << dest << "failed!"; return false; } if (!deletePath(source)) { // remove original qDebug() << "Deletion of" << source << "failed!"; return false; }; return true; } bool move(const QString& source, const QString& dest) { std::error_code err; ensureFilePathExists(dest); fs::rename(StringUtils::toStdString(source), StringUtils::toStdString(dest), err); if (err.value() != 0) { if (moveByCopy(source, dest)) return true; qDebug() << "Move of" << source << "to" << dest << "failed!"; qWarning() << "Failed to move file:" << QString::fromStdString(err.message()) << QString::number(err.value()); return false; } return true; } bool deletePath(QString path) { std::error_code err; fs::remove_all(StringUtils::toStdString(path), err); if (err) { qWarning() << "Failed to remove files:" << QString::fromStdString(err.message()); } return err.value() == 0; } bool trash(QString path, QString* pathInTrash) { // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal if (DesktopServices::isFlatpak()) return false; #if defined Q_OS_WIN32 if (IsWindowsServer()) return false; #endif return QFile::moveToTrash(path, pathInTrash); } QString PathCombine(const QString& path1, const QString& path2) { if (!path1.size()) return path2; if (!path2.size()) return path1; return QDir::cleanPath(path1 + QDir::separator() + path2); } QString PathCombine(const QString& path1, const QString& path2, const QString& path3) { return PathCombine(PathCombine(path1, path2), path3); } QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4) { return PathCombine(PathCombine(path1, path2, path3), path4); } QString AbsolutePath(const QString& path) { return QFileInfo(path).absolutePath(); } int pathDepth(const QString& path) { if (path.isEmpty()) return 0; QFileInfo info(path); auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); int numParts = parts.length(); numParts -= parts.count("."); numParts -= parts.count("..") * 2; return numParts; } QString pathTruncate(const QString& path, int depth) { if (path.isEmpty() || (depth < 0)) return ""; QString trunc = QFileInfo(path).path(); if (pathDepth(trunc) > depth) { return pathTruncate(trunc, depth); } auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts); if (parts.startsWith(".") && !path.startsWith(".")) { parts.removeFirst(); } if (QDir::toNativeSeparators(path).startsWith(QDir::separator())) { parts.prepend(""); } trunc = parts.join(QDir::separator()); return trunc; } QString ResolveExecutable(QString path) { if (path.isEmpty()) { return QString(); } if (!path.contains('/')) { path = QStandardPaths::findExecutable(path); } QFileInfo pathInfo(path); if (!pathInfo.exists() || !pathInfo.isExecutable()) { return QString(); } return pathInfo.absoluteFilePath(); } /** * Normalize path * * Any paths inside the current folder will be normalized to relative paths (to current) * Other paths will be made absolute */ QString NormalizePath(QString path) { QDir a = QDir::currentPath(); QString currentAbsolute = a.absolutePath(); QDir b(path); QString newAbsolute = b.absolutePath(); if (newAbsolute.startsWith(currentAbsolute)) { return a.relativeFilePath(newAbsolute); } else { return newAbsolute; } } static const QString BAD_WIN_CHARS = "<>:\"|?*\r\n"; static const QString BAD_NTFS_CHARS = "<>:\"|?*"; static const QString BAD_HFS_CHARS = ":"; static const QString BAD_FILENAME_CHARS = BAD_WIN_CHARS + "\\/"; QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) { for (int i = 0; i < string.length(); i++) if (string.at(i) < ' ' || BAD_FILENAME_CHARS.contains(string.at(i))) string[i] = replaceWith; return string; } QString RemoveInvalidPathChars(QString path, QChar replaceWith) { QString invalidChars; #ifdef Q_OS_WIN invalidChars = BAD_WIN_CHARS; #endif // the null character is ignored in this check as it was not a problem until now switch (statFS(path).fsType) { case FilesystemType::FAT: // similar to NTFS /* fallthrough */ case FilesystemType::NTFS: /* fallthrough */ case FilesystemType::REFS: // similar to NTFS(should be available only on windows) invalidChars += BAD_NTFS_CHARS; break; // case FilesystemType::EXT: // case FilesystemType::EXT_2_OLD: // case FilesystemType::EXT_2_3_4: // case FilesystemType::XFS: // case FilesystemType::BTRFS: // case FilesystemType::NFS: // case FilesystemType::ZFS: case FilesystemType::APFS: /* fallthrough */ case FilesystemType::HFS: /* fallthrough */ case FilesystemType::HFSPLUS: /* fallthrough */ case FilesystemType::HFSX: invalidChars += BAD_HFS_CHARS; break; // case FilesystemType::FUSEBLK: // case FilesystemType::F2FS: // case FilesystemType::UNKNOWN: default: break; } if (invalidChars.size() != 0) { for (int i = 0; i < path.length(); i++) { if (path.at(i) < ' ' || invalidChars.contains(path.at(i))) { path[i] = replaceWith; } } } return path; } QString DirNameFromString(QString string, QString inDir) { int num = 0; QString baseName = RemoveInvalidFilenameChars(string, '-'); QString dirName; do { if (num == 0) { dirName = baseName; } else { dirName = baseName + "(" + QString::number(num) + ")"; } // If it's over 9000 if (num > 9000) return ""; num++; } while (QFileInfo(PathCombine(inDir, dirName)).exists()); return dirName; } // Does the folder path contain any '!'? If yes, return true, otherwise false. // (This is a problem for Java) bool checkProblemticPathJava(QDir folder) { QString pathfoldername = folder.absolutePath(); return pathfoldername.contains("!", Qt::CaseInsensitive); } QString getDesktopDir() { return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); } QString getApplicationsDir() { return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); } QString quoteArgs(const QStringList& args, const QString& wrap, const QString& escapeChar, bool wrapOnlyIfNeeded = false) { QString result; auto size = args.size(); for (int i = 0; i < size; ++i) { QString arg = args[i]; arg.replace(wrap, escapeChar); bool needsWrapping = !wrapOnlyIfNeeded || arg.contains(' ') || arg.contains('\t') || arg.contains(wrap); if (needsWrapping) result += wrap + arg + wrap; else result += arg; if (i < size - 1) result += ' '; } return result; } // Cross-platform Shortcut creation QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { if (destination.isEmpty()) { destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); } if (!ensureFilePathExists(destination)) { qWarning() << "Destination path can't be created!"; return QString(); } #if defined(Q_OS_MACOS) QDir application = destination + ".app/"; if (application.exists()) { qWarning() << "Application already exists!"; return QString(); } if (!application.mkpath(".")) { qWarning() << "Couldn't create application"; return QString(); } QDir content = application.path() + "/Contents/"; QDir resources = content.path() + "/Resources/"; QDir binaryDir = content.path() + "/MacOS/"; QFile info(content.path() + "/Info.plist"); if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { qWarning() << "Couldn't create directories within application"; return QString(); } info.open(QIODevice::WriteOnly | QIODevice::Text); QFile(icon).rename(resources.path() + "/Icon.icns"); // Create the Command file QString exec = binaryDir.path() + "/Run.command"; QFile f(exec); f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); auto argstring = quoteArgs(args, "\"", "\\\""); stream << "#!/bin/bash" << "\n"; stream << "\"" << target << "\" " << argstring << "\n"; stream.flush(); f.close(); f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); // Generate the Info.plist QTextStream infoStream(&info); infoStream << " \n" "" "\n" "\n" " CFBundleExecutable\n" " Run.command\n" // The path to the executable " CFBundleIconFile\n" " Icon.icns\n" " CFBundleName\n" " " << name << "\n" // Name of the application " CFBundlePackageType\n" " APPL\n" " CFBundleShortVersionString\n" " 1.0\n" " CFBundleVersion\n" " 1.0\n" "\n" ""; return application.path(); #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated destination += ".desktop"; QFile f(destination); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "Failed to open file '" << f.fileName() << "' for writing!"; return QString(); } QTextStream stream(&f); auto argstring = quoteArgs(args, "'", "'\\''"); stream << "[Desktop Entry]" << "\n"; stream << "Type=Application" << "\n"; stream << "Categories=Game;ActionGame;AdventureGame;Simulation" << "\n"; stream << "Exec=\"" << target.toLocal8Bit() << "\" " << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; if (!icon.isEmpty()) { stream << "Icon=" << icon.toLocal8Bit() << "\n"; } stream.flush(); f.close(); f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); return destination; #elif defined(Q_OS_WIN) QFileInfo targetInfo(target); if (!targetInfo.exists()) { qWarning() << "Target file does not exist!"; return QString(); } target = targetInfo.absoluteFilePath(); if (target.length() >= MAX_PATH) { qWarning() << "Target file path is too long!"; return QString(); } if (!icon.isEmpty() && icon.length() >= MAX_PATH) { qWarning() << "Icon path is too long!"; return QString(); } destination += ".lnk"; if (destination.length() >= MAX_PATH) { qWarning() << "Destination path is too long!"; return QString(); } auto argStr = quoteArgs(args, "\"", "\\\"", true); if (argStr.length() >= MAX_PATH) { qWarning() << "Arguments string is too long!"; return QString(); } HRESULT hres; // ...yes, you need to initialize the entire COM stack just to make a shortcut hres = CoInitialize(nullptr); if (FAILED(hres)) { qWarning() << "Failed to initialize COM!"; return QString(); } WCHAR wsz[MAX_PATH]; IShellLink* psl; // create an IShellLink instance - this stores the shortcut's attributes hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl); if (SUCCEEDED(hres)) { wmemset(wsz, 0, MAX_PATH); target.toWCharArray(wsz); psl->SetPath(wsz); wmemset(wsz, 0, MAX_PATH); argStr.toWCharArray(wsz); psl->SetArguments(wsz); wmemset(wsz, 0, MAX_PATH); targetInfo.absolutePath().toWCharArray(wsz); psl->SetWorkingDirectory(wsz); // "Starts in" attribute if (!icon.isEmpty()) { wmemset(wsz, 0, MAX_PATH); icon.toWCharArray(wsz); psl->SetIconLocation(wsz, 0); } // query an IPersistFile interface from our IShellLink instance // this is the interface that will actually let us save the shortcut to disk! IPersistFile* ppf; hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf); if (SUCCEEDED(hres)) { wmemset(wsz, 0, MAX_PATH); destination.toWCharArray(wsz); hres = ppf->Save(wsz, TRUE); if (FAILED(hres)) { qWarning() << "IPresistFile->Save() failed"; qWarning() << "hres =" << hres; } ppf->Release(); } else { qWarning() << "Failed to query IPersistFile interface from IShellLink instance"; qWarning() << "hres =" << hres; } psl->Release(); } else { qWarning() << "Failed to create IShellLink instance"; qWarning() << "hres =" << hres; } // go away COM, nobody likes you CoUninitialize(); if (SUCCEEDED(hres)) return destination; return QString(); #else qWarning("Desktop Shortcuts not supported on your platform!"); return QString(); #endif } bool overrideFolder(QString overwritten_path, QString override_path) { using copy_opts = fs::copy_options; if (!FS::ensureFolderPathExists(overwritten_path)) return false; std::error_code err; fs::copy_options opt = copy_opts::recursive | copy_opts::overwrite_existing; // FIXME: hello traveller! Apparently std::copy does NOT overwrite existing files on GNU libstdc++ on Windows? fs::copy(StringUtils::toStdString(override_path), StringUtils::toStdString(overwritten_path), opt, err); if (err) { qCritical() << QString("Failed to apply override from %1 to %2").arg(override_path, overwritten_path); qCritical() << "Reason:" << QString::fromStdString(err.message()); } return err.value() == 0; } QString getFilesystemTypeName(FilesystemType type) { auto iter = s_filesystem_type_names.constFind(type); if (iter != s_filesystem_type_names.constEnd()) { return iter.value().constFirst(); } return getFilesystemTypeName(FilesystemType::UNKNOWN); } FilesystemType getFilesystemTypeFuzzy(const QString& name) { for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) { auto fs_names = iter.value(); for (auto fs_name : fs_names) { if (name.toUpper().contains(fs_name.toUpper())) return iter.key(); } } return FilesystemType::UNKNOWN; } FilesystemType getFilesystemType(const QString& name) { for (auto iter = s_filesystem_type_names.constBegin(); iter != s_filesystem_type_names.constEnd(); ++iter) { auto fs_names = iter.value(); if (fs_names.contains(name.toUpper())) return iter.key(); } return FilesystemType::UNKNOWN; } /** * @brief path to the near ancestor that exists * */ QString nearestExistentAncestor(const QString& path) { if (QFileInfo::exists(path)) return path; QDir dir(path); if (!dir.makeAbsolute()) return {}; do { dir.setPath(QDir::cleanPath(dir.filePath(QStringLiteral("..")))); } while (!dir.exists() && !dir.isRoot()); return dir.exists() ? dir.path() : QString(); } /** * @brief colect information about the filesystem under a file * */ FilesystemInfo statFS(const QString& path) { FilesystemInfo info; QStorageInfo storage_info(nearestExistentAncestor(path)); info.fsTypeName = storage_info.fileSystemType(); info.fsType = getFilesystemTypeFuzzy(info.fsTypeName); info.blockSize = storage_info.blockSize(); info.bytesAvailable = storage_info.bytesAvailable(); info.bytesFree = storage_info.bytesFree(); info.bytesTotal = storage_info.bytesTotal(); info.name = storage_info.name(); info.rootPath = storage_info.rootPath(); return info; } /** * @brief if the Filesystem is reflink/clone capable * */ bool canCloneOnFS(const QString& path) { FilesystemInfo info = statFS(path); return canCloneOnFS(info); } bool canCloneOnFS(const FilesystemInfo& info) { return canCloneOnFS(info.fsType); } bool canCloneOnFS(FilesystemType type) { return s_clone_filesystems.contains(type); } /** * @brief if the Filesystem is reflink/clone capable and both paths are on the same device * */ bool canClone(const QString& src, const QString& dst) { auto srcVInfo = statFS(src); auto dstVInfo = statFS(dst); bool sameDevice = srcVInfo.rootPath == dstVInfo.rootPath; return sameDevice && canCloneOnFS(srcVInfo) && canCloneOnFS(dstVInfo); } /** * @brief reflink/clones a directory and it's contents from src to dest * @param offset subdirectory form src to copy to dest * @return if there was an error during the filecopy */ bool clone::operator()(const QString& offset, bool dryRun) { if (!canClone(m_src.absolutePath(), m_dst.absolutePath())) { qWarning() << "Can not clone: not same device or not clone/reflink filesystem"; qDebug() << "Source path:" << m_src.absolutePath(); qDebug() << "Destination path:" << m_dst.absolutePath(); emit cloneFailed(m_src.absolutePath(), m_dst.absolutePath()); return false; } m_cloned = 0; // reset counter m_failedClones.clear(); auto src = PathCombine(m_src.absolutePath(), offset); auto dst = PathCombine(m_dst.absolutePath(), offset); std::error_code err; // Function that'll do the actual cloneing auto cloneFile = [this, dryRun, dst, &err](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); if (!dryRun) { ensureFilePathExists(dst_path); clone_file(src_path, dst_path, err); } if (err) { qDebug() << "Failed to clone files: error" << err.value() << "message" << QString::fromStdString(err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; m_failedClones.append(qMakePair(src_path, dst_path)); emit cloneFailed(src_path, dst_path); return; } m_cloned++; emit fileCloned(src_path, dst_path); }; // We can't use copy_opts::recursive because we need to take into account the // blacklisted paths, so we iterate over the source directory, and if there's no blacklist // match, we copy the file. QDir src_dir(src); QDirIterator source_it(src, QDir::Filter::Files | QDir::Filter::Hidden, QDirIterator::Subdirectories); while (source_it.hasNext()) { auto src_path = source_it.next(); auto relative_path = src_dir.relativeFilePath(src_path); cloneFile(src_path, relative_path); } // If the root src is not a directory, the previous iterator won't run. if (!fs::is_directory(StringUtils::toStdString(src))) cloneFile(src, ""); return err.value() == 0; } /** * @brief clone/reflink file from src to dst * */ bool clone_file(const QString& src, const QString& dst, std::error_code& ec) { auto src_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(src).absoluteFilePath())); auto dst_path = StringUtils::toStdString(QDir::toNativeSeparators(QFileInfo(dst).absoluteFilePath())); FilesystemInfo srcinfo = statFS(src); FilesystemInfo dstinfo = statFS(dst); if ((srcinfo.rootPath != dstinfo.rootPath) || (srcinfo.fsType != dstinfo.fsType)) { ec = std::make_error_code(std::errc::not_supported); qWarning() << "reflink/clone must be to the same device and filesystem! src and dst root filesystems do not match."; return false; } #if defined(Q_OS_WIN) if (!win_ioctl_clone(src_path, dst_path, ec)) { qDebug() << "failed win_ioctl_clone"; qWarning() << "clone/reflink not supported on windows outside of btrfs or ReFS!"; qWarning() << "check out https://github.com/maharmstone/btrfs for btrfs support!"; return false; } #elif defined(Q_OS_LINUX) if (!linux_ficlone(src_path, dst_path, ec)) { qDebug() << "failed linux_ficlone:"; return false; } #elif defined(Q_OS_MACOS) if (!macos_bsd_clonefile(src_path, dst_path, ec)) { qDebug() << "failed macos_bsd_clonefile:"; return false; } #else qWarning() << "clone/reflink not supported! unknown OS"; ec = std::make_error_code(std::errc::not_supported); return false; #endif return true; } #if defined(Q_OS_WIN) static long RoundUpToPowerOf2(long originalValue, long roundingMultiplePowerOf2) { long mask = roundingMultiplePowerOf2 - 1; return (originalValue + mask) & ~mask; } bool win_ioctl_clone(const std::wstring& src_path, const std::wstring& dst_path, std::error_code& ec) { /** * This algorithm inspired from https://github.com/0xbadfca11/reflink * LICENSE MIT * * Additional references * https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ni-winioctl-fsctl_duplicate_extents_to_file * https://github.com/microsoft/CopyOnWrite/blob/main/lib/Windows/WindowsCopyOnWriteFilesystem.cs#L94 */ HANDLE hSourceFile = CreateFileW(src_path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); if (hSourceFile == INVALID_HANDLE_VALUE) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to open source file" << src_path.c_str(); return false; } ULONG fs_flags; if (!GetVolumeInformationByHandleW(hSourceFile, nullptr, 0, nullptr, nullptr, &fs_flags, nullptr, 0)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to get Filesystem information for" << src_path.c_str(); CloseHandle(hSourceFile); return false; } if (!(fs_flags & FILE_SUPPORTS_BLOCK_REFCOUNTING)) { SetLastError(ERROR_NOT_CAPABLE); ec = std::error_code(GetLastError(), std::system_category()); qWarning() << "Filesystem at" << src_path.c_str() << "does not support reflink"; CloseHandle(hSourceFile); return false; } FILE_END_OF_FILE_INFO sourceFileLength; if (!GetFileSizeEx(hSourceFile, &sourceFileLength.EndOfFile)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to size of source file" << src_path.c_str(); CloseHandle(hSourceFile); return false; } FILE_BASIC_INFO sourceFileBasicInfo; if (!GetFileInformationByHandleEx(hSourceFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to source file info" << src_path.c_str(); CloseHandle(hSourceFile); return false; } ULONG junk; FSCTL_GET_INTEGRITY_INFORMATION_BUFFER sourceFileIntegrity; if (!DeviceIoControl(hSourceFile, FSCTL_GET_INTEGRITY_INFORMATION, nullptr, 0, &sourceFileIntegrity, sizeof(sourceFileIntegrity), &junk, nullptr)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to source file integrity info" << src_path.c_str(); CloseHandle(hSourceFile); return false; } HANDLE hDestFile = CreateFileW(dst_path.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, 0, nullptr, CREATE_NEW, 0, hSourceFile); if (hDestFile == INVALID_HANDLE_VALUE) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to open dest file" << dst_path.c_str(); CloseHandle(hSourceFile); return false; } FILE_DISPOSITION_INFO destFileDispose = { TRUE }; if (!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose))) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to set dest file info" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, nullptr, 0, nullptr, 0, &junk, nullptr)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to set dest sparseness" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } FSCTL_SET_INTEGRITY_INFORMATION_BUFFER setDestFileintegrity = { sourceFileIntegrity.ChecksumAlgorithm, sourceFileIntegrity.Reserved, sourceFileIntegrity.Flags }; if (!DeviceIoControl(hDestFile, FSCTL_SET_INTEGRITY_INFORMATION, &setDestFileintegrity, sizeof(setDestFileintegrity), nullptr, 0, nullptr, nullptr)) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to set dest file integrity info" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } if (!SetFileInformationByHandle(hDestFile, FileEndOfFileInfo, &sourceFileLength, sizeof(sourceFileLength))) { ec = std::error_code(GetLastError(), std::system_category()); qDebug() << "Failed to set dest file size" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } const LONG64 splitThreshold = (1LL << 32) - sourceFileIntegrity.ClusterSizeInBytes; DUPLICATE_EXTENTS_DATA dupExtent; dupExtent.FileHandle = hSourceFile; for (LONG64 offset = 0, remain = RoundUpToPowerOf2(sourceFileLength.EndOfFile.QuadPart, sourceFileIntegrity.ClusterSizeInBytes); remain > 0; offset += splitThreshold, remain -= splitThreshold) { dupExtent.SourceFileOffset.QuadPart = dupExtent.TargetFileOffset.QuadPart = offset; dupExtent.ByteCount.QuadPart = std::min(splitThreshold, remain); if (!DeviceIoControl(hDestFile, FSCTL_DUPLICATE_EXTENTS_TO_FILE, &dupExtent, sizeof(dupExtent), nullptr, 0, &junk, nullptr)) { DWORD err = GetLastError(); QString additionalMessage; if (err == ERROR_BLOCK_TOO_MANY_REFERENCES) { static const int MaxClonesPerFile = 8175; additionalMessage = QString( " This is ERROR_BLOCK_TOO_MANY_REFERENCES and may mean you have surpassed the maximum " "allowed %1 references for a single file. " "See " "https://docs.microsoft.com/en-us/windows-server/storage/refs/block-cloning#functionality-restrictions-and-remarks") .arg(MaxClonesPerFile); } ec = std::error_code(err, std::system_category()); qDebug() << "Failed copy-on-write cloning of" << src_path.c_str() << "to" << dst_path.c_str() << "with error" << err << additionalMessage; CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } } if (!(sourceFileBasicInfo.FileAttributes & FILE_ATTRIBUTE_SPARSE_FILE)) { FILE_SET_SPARSE_BUFFER setDestSparse = { FALSE }; if (!DeviceIoControl(hDestFile, FSCTL_SET_SPARSE, &setDestSparse, sizeof(setDestSparse), nullptr, 0, &junk, nullptr)) { qDebug() << "Failed to set dest file sparseness" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } } sourceFileBasicInfo.CreationTime.QuadPart = 0; if (!SetFileInformationByHandle(hDestFile, FileBasicInfo, &sourceFileBasicInfo, sizeof(sourceFileBasicInfo))) { qDebug() << "Failed to set dest file creation time" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } if (!FlushFileBuffers(hDestFile)) { qDebug() << "Failed to flush dest file buffer" << dst_path.c_str(); CloseHandle(hSourceFile); CloseHandle(hDestFile); return false; } destFileDispose = { FALSE }; bool result = !!SetFileInformationByHandle(hDestFile, FileDispositionInfo, &destFileDispose, sizeof(destFileDispose)); CloseHandle(hSourceFile); CloseHandle(hDestFile); return result; } #elif defined(Q_OS_LINUX) bool linux_ficlone(const std::string& src_path, const std::string& dst_path, std::error_code& ec) { // https://man7.org/linux/man-pages/man2/ioctl_ficlone.2.html int src_fd = open(src_path.c_str(), O_RDONLY); if (src_fd == -1) { qDebug() << "Failed to open file:" << src_path.c_str(); qDebug() << "Error:" << strerror(errno); ec = std::make_error_code(static_cast(errno)); return false; } int dst_fd = open(dst_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); if (dst_fd == -1) { qDebug() << "Failed to open file:" << dst_path.c_str(); qDebug() << "Error:" << strerror(errno); ec = std::make_error_code(static_cast(errno)); close(src_fd); return false; } // attempt to clone if (ioctl(dst_fd, FICLONE, src_fd) == -1) { qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str(); qDebug() << "Error:" << strerror(errno); ec = std::make_error_code(static_cast(errno)); close(src_fd); close(dst_fd); return false; } if (close(src_fd)) { qDebug() << "Failed to close file:" << src_path.c_str(); qDebug() << "Error:" << strerror(errno); } if (close(dst_fd)) { qDebug() << "Failed to close file:" << dst_path.c_str(); qDebug() << "Error:" << strerror(errno); } return true; } #elif defined(Q_OS_MACOS) bool macos_bsd_clonefile(const std::string& src_path, const std::string& dst_path, std::error_code& ec) { // clonefile(const char * src, const char * dst, int flags); // https://www.manpagez.com/man/2/clonefile/ qDebug() << "attempting file clone via clonefile" << src_path.c_str() << "to" << dst_path.c_str(); if (clonefile(src_path.c_str(), dst_path.c_str(), 0) == -1) { qDebug() << "Failed to clone file:" << src_path.c_str() << "to" << dst_path.c_str(); qDebug() << "Error:" << strerror(errno); ec = std::make_error_code(static_cast(errno)); return false; } return true; } #endif /** * @brief if the Filesystem is symlink capable * */ bool canLinkOnFS(const QString& path) { FilesystemInfo info = statFS(path); return canLinkOnFS(info); } bool canLinkOnFS(const FilesystemInfo& info) { return canLinkOnFS(info.fsType); } bool canLinkOnFS(FilesystemType type) { return !s_non_link_filesystems.contains(type); } /** * @brief if the Filesystem is symlink capable on both ends * */ bool canLink(const QString& src, const QString& dst) { return canLinkOnFS(src) && canLinkOnFS(dst); } uintmax_t hardLinkCount(const QString& path) { std::error_code err; int count = fs::hard_link_count(StringUtils::toStdString(path), err); if (err) { qWarning() << "Failed to count hard links for" << path << ":" << QString::fromStdString(err.message()); count = 0; } return count; } #ifdef Q_OS_WIN // returns 8.3 file format from long path QString shortPathName(const QString& file) { auto input = file.toStdWString(); std::wstring output; long length = GetShortPathNameW(input.c_str(), NULL, 0); if (length == 0) return {}; // NOTE: this resizing might seem weird... // when GetShortPathNameW fails, it returns length including null character // when it succeeds, it returns length excluding null character // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx output.resize(length); if (GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length) == 0) return {}; output.resize(length - 1); QString ret = QString::fromStdWString(output); return ret; } // if the string survives roundtrip through local 8bit encoding... bool fitsInLocal8bit(const QString& string) { return string == QString::fromLocal8Bit(string.toLocal8Bit()); } QString getPathNameInLocal8bit(const QString& file) { if (!fitsInLocal8bit(file)) { auto path = shortPathName(file); if (!path.isEmpty()) { return path; } // in case shortPathName fails just return the path as is } return file; } #endif QString getUniqueResourceName(const QString& filePath) { auto newFileName = filePath; if (!newFileName.endsWith(".disabled")) { return newFileName; // prioritize enabled mods } newFileName.chop(9); if (!QFile::exists(newFileName)) { return filePath; } QFileInfo fileInfo(filePath); auto baseName = fileInfo.completeBaseName(); auto path = fileInfo.absolutePath(); int counter = 1; do { if (counter == 1) { newFileName = FS::PathCombine(path, baseName + ".duplicate"); } else { newFileName = FS::PathCombine(path, baseName + ".duplicate" + QString::number(counter)); } counter++; } while (QFile::exists(newFileName)); return newFileName; } bool removeFiles(QStringList listFile) { bool ret = true; // For each file for (int i = 0; i < listFile.count(); i++) { // Remove ret = ret && QFile::remove(listFile.at(i)); } return ret; } } // namespace FS PrismLauncher-10.0.5/launcher/InstanceTask.h0000644000175100017510000000427015144136756020337 0ustar runnerrunner#pragma once #include "settings/SettingsObject.h" #include "tasks/Task.h" /* Helpers */ enum class InstanceNameChange { ShouldChange, ShouldKeep }; [[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name); enum class ShouldUpdate { Update, SkipUpdating, Cancel }; [[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name); struct InstanceName { public: InstanceName() = default; InstanceName(QString name, QString version) : m_original_name(std::move(name)), m_original_version(std::move(version)) {} QString modifiedName() const; QString originalName() const; QString name() const; QString version() const; void setName(QString name) { m_modified_name = name; } void setName(InstanceName& other); protected: QString m_original_name; QString m_original_version; QString m_modified_name; }; class InstanceTask : public Task, public InstanceName { Q_OBJECT public: InstanceTask(); ~InstanceTask() override = default; void setParentSettings(SettingsObjectPtr settings) { m_globalSettings = settings; } void setStagingPath(const QString& stagingPath) { m_stagingPath = stagingPath; } void setIcon(const QString& icon) { m_instIcon = icon; } void setGroup(const QString& group) { m_instGroup = group; } QString group() const { return m_instGroup; } bool shouldConfirmUpdate() const { return m_confirm_update; } void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; } bool shouldOverride() const { return m_override_existing; } QString originalInstanceID() const { return m_original_instance_id; }; protected: void setOverride(bool override, QString instance_id_to_override = {}) { m_override_existing = override; if (!instance_id_to_override.isEmpty()) m_original_instance_id = instance_id_to_override; } protected: /* data */ SettingsObjectPtr m_globalSettings; QString m_instIcon; QString m_instGroup; QString m_stagingPath; bool m_override_existing = false; bool m_confirm_update = true; QString m_original_instance_id; }; PrismLauncher-10.0.5/launcher/Filter.h0000644000175100017510000000263615144136756017201 0ustar runnerrunner#pragma once #include #include using Filter = std::function; namespace Filters { inline Filter inverse(Filter filter) { return [filter = std::move(filter)](const QString& src) { return !filter(src); }; } inline Filter any(QList filters) { return [filters = std::move(filters)](const QString& src) { for (auto& filter : filters) if (filter(src)) return true; return false; }; } inline Filter equals(QString pattern) { return [pattern = std::move(pattern)](const QString& src) { return src == pattern; }; } inline Filter equalsAny(QStringList patterns = {}) { return [patterns = std::move(patterns)](const QString& src) { return patterns.isEmpty() || patterns.contains(src); }; } inline Filter equalsOrEmpty(QString pattern) { return [pattern = std::move(pattern)](const QString& src) { return src.isEmpty() || src == pattern; }; } inline Filter contains(QString pattern) { return [pattern = std::move(pattern)](const QString& src) { return src.contains(pattern); }; } inline Filter startsWith(QString pattern) { return [pattern = std::move(pattern)](const QString& src) { return src.startsWith(pattern); }; } inline Filter regexp(QRegularExpression pattern) { return [pattern = std::move(pattern)](const QString& src) { return pattern.match(src).hasMatch(); }; } } // namespace Filters PrismLauncher-10.0.5/launcher/StringUtils.cpp0000644000175100017510000002106015144136756020566 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "StringUtils.h" #include #include #include /// If you're wondering where these came from exactly, then know you're not the only one =D /// TAKEN FROM Qt, because it doesn't expose it intelligently static inline QChar getNextChar(const QString& s, int location) { return (location < s.length()) ? s.at(location) : QChar(); } /// TAKEN FROM Qt, because it doesn't expose it intelligently int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { int l1 = 0, l2 = 0; while (l1 <= s1.size() && l2 <= s2.size()) { // skip spaces, tabs and 0's QChar c1 = getNextChar(s1, l1); while (c1.isSpace()) c1 = getNextChar(s1, ++l1); QChar c2 = getNextChar(s2, l2); while (c2.isSpace()) c2 = getNextChar(s2, ++l2); if (c1.isDigit() && c2.isDigit()) { while (c1.digitValue() == 0) c1 = getNextChar(s1, ++l1); while (c2.digitValue() == 0) c2 = getNextChar(s2, ++l2); int lookAheadLocation1 = l1; int lookAheadLocation2 = l2; int currentReturnValue = 0; // find the last digit, setting currentReturnValue as we go if it isn't equal for (QChar lookAhead1 = c1, lookAhead2 = c2; (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); lookAhead1 = getNextChar(s1, ++lookAheadLocation1), lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) { bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); if (!is1ADigit && !is2ADigit) break; if (!is1ADigit) return -1; if (!is2ADigit) return 1; if (currentReturnValue == 0) { if (lookAhead1 < lookAhead2) { currentReturnValue = -1; } else if (lookAhead1 > lookAhead2) { currentReturnValue = 1; } } } if (currentReturnValue != 0) return currentReturnValue; } if (cs == Qt::CaseInsensitive) { if (!c1.isLower()) c1 = c1.toLower(); if (!c2.isLower()) c2 = c2.toLower(); } int r = QString::localeAwareCompare(c1, c2); if (r < 0) return -1; if (r > 0) return 1; l1 += 1; l2 += 1; } // The two strings are the same (02 == 2) so fall back to the normal sort return QString::compare(s1, s2, cs); } QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit) { auto display_options = QUrl::RemoveUserInfo | QUrl::RemoveFragment | QUrl::NormalizePathSegments; auto str_url = url.toDisplayString(display_options); if (str_url.length() <= max_len) return str_url; auto url_path_parts = url.path().split('/'); QString last_path_segment = url_path_parts.takeLast(); if (url_path_parts.size() >= 1 && url_path_parts.first().isEmpty()) url_path_parts.removeFirst(); // drop empty first segment (from leading / ) if (url_path_parts.size() >= 1) url_path_parts.removeLast(); // drop the next to last path segment auto url_template = QStringLiteral("%1://%2/%3%4"); auto url_compact = url_path_parts.isEmpty() ? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query()) : url_template.arg(url.scheme(), url.host(), QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query()); // remove url parts one by one if it's still too long while (url_compact.length() > max_len && url_path_parts.size() >= 1) { url_path_parts.removeLast(); // drop the next to last path segment url_compact = url_path_parts.isEmpty() ? url_template.arg(url.scheme(), url.host(), QStringList({ "...", last_path_segment }).join('/'), url.query()) : url_template.arg(url.scheme(), url.host(), QStringList({ url_path_parts.join('/'), "...", last_path_segment }).join('/'), url.query()); } if ((url_compact.length() >= max_len) && hard_limit) { // still too long, truncate normally url_compact = QString(str_url); auto to_remove = url_compact.length() - max_len + 3; url_compact.remove(url_compact.length() - to_remove - 1, to_remove); url_compact.append("..."); } return url_compact; } static const QStringList s_units_si{ "KB", "MB", "GB", "TB" }; static const QStringList s_units_kibi{ "KiB", "MiB", "GiB", "TiB" }; QString StringUtils::humanReadableFileSize(double bytes, bool use_si, int decimal_points) { const QStringList units = use_si ? s_units_si : s_units_kibi; const int scale = use_si ? 1000 : 1024; int u = -1; double r = pow(10, decimal_points); do { bytes /= scale; u++; } while (round(abs(bytes) * r) / r >= scale && u < units.length() - 1); return QString::number(bytes, 'f', 2) + " " + units[u]; } QString StringUtils::getRandomAlphaNumeric() { return QUuid::createUuid().toString(QUuid::Id128); } QPair StringUtils::splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs) { QString left, right; auto index = s.indexOf(sep, 0, cs); left = s.mid(0, index); right = s.mid(index + sep.length()); return qMakePair(left, right); } QPair StringUtils::splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs) { QString left, right; auto index = s.indexOf(sep, 0, cs); left = s.mid(0, index); right = s.mid(left.length() + 1); return qMakePair(left, right); } QPair StringUtils::splitFirst(const QString& s, const QRegularExpression& re) { QString left, right; QRegularExpressionMatch match; auto index = s.indexOf(re, 0, &match); left = s.mid(0, index); auto end = match.hasMatch() ? left.length() + match.capturedLength() : left.length() + 1; right = s.mid(end); return qMakePair(left, right); } QString StringUtils::htmlListPatch(QString htmlStr) { static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>"); int pos = htmlStr.indexOf(s_ulMatcher); int imgPos; while (pos != -1) { pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index imgPos = htmlStr.indexOf(""); pos = htmlStr.indexOf(s_ulMatcher, pos); } return htmlStr; } PrismLauncher-10.0.5/launcher/DefaultVariable.h0000644000175100017510000000116115144136756020776 0ustar runnerrunner#pragma once template class DefaultVariable { public: DefaultVariable(const T& value) { defaultValue = value; } DefaultVariable& operator=(const T& value) { currentValue = value; is_default = currentValue == defaultValue; is_explicit = true; return *this; } operator const T&() const { return is_default ? defaultValue : currentValue; } bool isDefault() const { return is_default; } bool isExplicit() const { return is_explicit; } private: T currentValue; T defaultValue; bool is_default = true; bool is_explicit = false; }; PrismLauncher-10.0.5/launcher/settings/0000755000175100017510000000000015144136756017434 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/settings/INISettingsObject.h0000644000175100017510000000350615144136756023100 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "settings/INIFile.h" #include "settings/SettingsObject.h" /*! * \brief A settings object that stores its settings in an INIFile. */ class INISettingsObject : public SettingsObject { Q_OBJECT public: /** 'paths' is a list of INI files to try, in order, for fallback support. */ explicit INISettingsObject(QStringList paths, QObject* parent = nullptr); explicit INISettingsObject(QString path, QObject* parent = nullptr); /*! * \brief Gets the path to the INI file. * \return The path to the INI file. */ virtual QString filePath() const { return m_filePath; } /*! * \brief Sets the path to the INI file and reloads it. * \param filePath The INI file's new path. */ virtual void setFilePath(const QString& filePath); bool reload() override; void suspendSave() override; void resumeSave() override; protected slots: virtual void changeSetting(const Setting& setting, QVariant value) override; virtual void resetSetting(const Setting& setting) override; protected: virtual QVariant retrieveValue(const Setting& setting) override; void doSave(); protected: INIFile m_ini; QString m_filePath; }; PrismLauncher-10.0.5/launcher/settings/SettingsObject.cpp0000644000175100017510000001762115144136756023076 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "settings/SettingsObject.h" #include #include "PassthroughSetting.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" #include #include #include #ifdef Q_OS_MACOS #include "macsandbox/SecurityBookmarkFileAccess.h" #endif SettingsObject::SettingsObject(QObject* parent) : QObject(parent) {} SettingsObject::~SettingsObject() { m_settings.clear(); } std::shared_ptr SettingsObject::registerOverride(std::shared_ptr original, std::shared_ptr gate) { if (contains(original->id())) { qCritical() << QString("Failed to register setting %1. ID already exists.").arg(original->id()); return nullptr; // Fail } auto override = std::make_shared(original, gate); override->m_storage = this; connectSignals(*override); m_settings.insert(override->id(), override); return override; } std::shared_ptr SettingsObject::registerPassthrough(std::shared_ptr original, std::shared_ptr gate) { if (contains(original->id())) { qCritical() << QString("Failed to register setting %1. ID already exists.").arg(original->id()); return nullptr; // Fail } auto passthrough = std::make_shared(original, gate); passthrough->m_storage = this; connectSignals(*passthrough); m_settings.insert(passthrough->id(), passthrough); return passthrough; } std::shared_ptr SettingsObject::registerSetting(QStringList synonyms, QVariant defVal) { if (synonyms.empty()) return nullptr; if (contains(synonyms.first())) { qCritical() << QString("Failed to register setting %1. ID already exists.").arg(synonyms.first()); return nullptr; // Fail } auto setting = std::make_shared(synonyms, defVal); setting->m_storage = this; connectSignals(*setting); m_settings.insert(setting->id(), setting); return setting; } std::shared_ptr SettingsObject::getSetting(const QString& id) const { // Make sure there is a setting with the given ID. if (!m_settings.contains(id)) return NULL; return m_settings[id]; } QVariant SettingsObject::get(const QString& id) { auto setting = getSetting(id); #ifdef Q_OS_MACOS // for macOS, use a security scoped bookmark for the paths if (id.endsWith("Dir")) { return { getPathFromBookmark(id) }; } #endif return (setting ? setting->get() : QVariant()); } bool SettingsObject::set(const QString& id, QVariant value) { auto setting = getSetting(id); if (!setting) { qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); return false; } #ifdef Q_OS_MACOS // for macOS, keep a security scoped bookmark for the paths if (value.userType() == QMetaType::QString && id.endsWith("Dir")) { setPathWithBookmark(id, value.toString()); } #endif setting->set(std::move(value)); return true; } #ifdef Q_OS_MACOS QString SettingsObject::getPathFromBookmark(const QString& id) { auto setting = getSetting(id); if (!setting) { qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); return ""; } // there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access) if (setting->get() == setting->defValue() || QDir(setting->get().toString()).absolutePath().startsWith(QDir::current().absolutePath())) { return setting->get().toString(); } auto bookmarkId = id + "Bookmark"; auto bookmarkSetting = getSetting(bookmarkId); if (!bookmarkSetting) { qCritical() << QString("Error changing setting %1. Bookmark setting doesn't exist.").arg(id); return ""; } QByteArray bookmark = bookmarkSetting->get().toByteArray(); if (bookmark.isEmpty()) { qDebug() << "Creating bookmark for" << id << "at" << setting->get().toString(); setPathWithBookmark(id, setting->get().toString()); return setting->get().toString(); } bool stale; QUrl url = m_sandboxedFileAccess.securityScopedBookmarkToURL(bookmark, stale); if (url.isValid()) { if (stale) { setting->set(url.path()); bookmarkSetting->set(bookmark); } m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bookmark, stale); // already did a stale check, no need to do it again // convert to relative path to current directory if `url` is a descendant of the current directory QDir currentDir = QDir::current().absolutePath(); return url.path().startsWith(currentDir.absolutePath()) ? currentDir.relativeFilePath(url.path()) : url.path(); } return setting->get().toString(); } bool SettingsObject::setPathWithBookmark(const QString& id, const QString& path) { auto setting = getSetting(id); if (!setting) { qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); return false; } QDir dir(path); if (!dir.exists()) { qCritical() << QString("Error changing setting %1. Path doesn't exist.").arg(id); return false; } QString absolutePath = dir.absolutePath(); QString bookmarkId = id + "Bookmark"; std::shared_ptr bookmarkSetting = getSetting(bookmarkId); // there is no need to use bookmarks if the default value is used or the directory is within the data directory (already can access) if (path == setting->defValue().toString() || absolutePath.startsWith(QDir::current().absolutePath())) { bookmarkSetting->reset(); return true; } QByteArray bytes = m_sandboxedFileAccess.pathToSecurityScopedBookmark(absolutePath); if (bytes.isEmpty()) { qCritical() << QString("Failed to create bookmark for %1 - no access?").arg(id); // TODO: show an alert to the user asking them to reselect the directory return false; } auto oldBookmark = bookmarkSetting->get().toByteArray(); m_sandboxedFileAccess.stopUsingSecurityScopedBookmark(oldBookmark); if (!bytes.isEmpty() && bookmarkSetting) { bookmarkSetting->set(bytes); bool stale; m_sandboxedFileAccess.startUsingSecurityScopedBookmark(bytes, stale); // just created the bookmark, it shouldn't be stale } setting->set(path); return true; } #endif void SettingsObject::reset(const QString& id) const { auto setting = getSetting(id); if (setting) setting->reset(); } bool SettingsObject::contains(const QString& id) { return m_settings.contains(id); } bool SettingsObject::reload() { for (auto setting : m_settings.values()) { setting->set(setting->get()); } return true; } void SettingsObject::connectSignals(const Setting& setting) { connect(&setting, &Setting::SettingChanged, this, &SettingsObject::changeSetting); connect(&setting, &Setting::SettingChanged, this, &SettingsObject::SettingChanged); connect(&setting, &Setting::settingReset, this, &SettingsObject::resetSetting); connect(&setting, &Setting::settingReset, this, &SettingsObject::settingReset); } std::shared_ptr SettingsObject::getOrRegisterSetting(const QString& id, QVariant defVal) { return contains(id) ? getSetting(id) : registerSetting(id, defVal); } PrismLauncher-10.0.5/launcher/settings/PassthroughSetting.cpp0000644000175100017510000000276315144136756024015 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PassthroughSetting.h" PassthroughSetting::PassthroughSetting(std::shared_ptr other, std::shared_ptr gate) : Setting(other->configKeys(), QVariant()) { Q_ASSERT(other); m_other = other; m_gate = gate; } bool PassthroughSetting::isOverriding() const { if (!m_gate) { return false; } return m_gate->get().toBool(); } QVariant PassthroughSetting::defValue() const { if (isOverriding()) { return m_other->get(); } return m_other->defValue(); } QVariant PassthroughSetting::get() const { if (isOverriding()) { return Setting::get(); } return m_other->get(); } void PassthroughSetting::reset() { if (isOverriding()) { Setting::reset(); } m_other->reset(); } void PassthroughSetting::set(QVariant value) { if (isOverriding()) { Setting::set(value); } m_other->set(value); } PrismLauncher-10.0.5/launcher/settings/INIFile.h0000644000175100017510000000373715144136756021036 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include // Sectionless INI parser (for instance config files) class INIFile : public QMap { public: explicit INIFile(); bool loadFile(QString fileName); bool loadFile(QByteArray data); bool saveFile(QString fileName); QVariant get(QString key, QVariant def) const; void set(QString key, QVariant val); }; PrismLauncher-10.0.5/launcher/settings/OverrideSetting.cpp0000644000175100017510000000240115144136756023252 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "OverrideSetting.h" OverrideSetting::OverrideSetting(std::shared_ptr other, std::shared_ptr gate) : Setting(other->configKeys(), QVariant()) { Q_ASSERT(other); Q_ASSERT(gate); m_other = other; m_gate = gate; } bool OverrideSetting::isOverriding() const { return m_gate->get().toBool(); } QVariant OverrideSetting::defValue() const { return m_other->get(); } QVariant OverrideSetting::get() const { if (isOverriding()) { return Setting::get(); } return m_other->get(); } void OverrideSetting::reset() { Setting::reset(); } void OverrideSetting::set(QVariant value) { Setting::set(value); } PrismLauncher-10.0.5/launcher/settings/Setting.h0000644000175100017510000000676515144136756021240 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include class SettingsObject; /*! * */ class Setting : public QObject { Q_OBJECT public: /** * Construct a Setting * * Synonyms are all the possible names used in the settings object, in order of preference. * First synonym is the ID, which identifies the setting in Prism Launcher. * * defVal is the default value that will be returned when the settings object * doesn't have any value for this setting. */ explicit Setting(QStringList synonyms, QVariant defVal = QVariant()); /*! * \brief Gets this setting's ID. * This is used to refer to the setting within the application. * \warning Changing the ID while the setting is registered with a SettingsObject results in * undefined behavior. * \return The ID of the setting. */ virtual QString id() const { return m_synonyms.first(); } /*! * \brief Gets this setting's config file key. * This is used to store the setting's value in the config file. It is usually * the same as the setting's ID, but it can be different. * \return The setting's config file key. */ virtual QStringList configKeys() const { return m_synonyms; } /*! * \brief Gets this setting's value as a QVariant. * This is done by calling the SettingsObject's retrieveValue() function. * If this Setting doesn't have a SettingsObject, this returns an invalid QVariant. * \return QVariant containing this setting's value. * \sa value() */ virtual QVariant get() const; /*! * \brief Gets this setting's default value. * \return The default value of this setting. */ virtual QVariant defValue() const; signals: /*! * \brief Signal emitted when this Setting object's value changes. * \param setting A reference to the Setting that changed. * \param value This Setting object's new value. */ void SettingChanged(const Setting& setting, QVariant value); /*! * \brief Signal emitted when this Setting object's value resets to default. * \param setting A reference to the Setting that changed. */ void settingReset(const Setting& setting); public slots: /*! * \brief Changes the setting's value. * This is done by emitting the SettingChanged() signal which will then be * handled by the SettingsObject object and cause the setting to change. * \param value The new value. */ virtual void set(QVariant value); /*! * \brief Reset the setting to default * This is done by emitting the settingReset() signal which will then be * handled by the SettingsObject object and cause the setting to change. */ virtual void reset(); protected: friend class SettingsObject; SettingsObject* m_storage; QStringList m_synonyms; QVariant m_defVal; }; PrismLauncher-10.0.5/launcher/settings/PassthroughSetting.h0000644000175100017510000000247315144136756023460 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "Setting.h" /*! * \brief A setting that 'overrides another.' based on the value of a 'gate' setting * If 'gate' evaluates to true, the override stores and returns data * If 'gate' evaluates to false, the original does, */ class PassthroughSetting : public Setting { Q_OBJECT public: explicit PassthroughSetting(std::shared_ptr overridden, std::shared_ptr gate); virtual QVariant defValue() const; virtual QVariant get() const; virtual void set(QVariant value); virtual void reset(); private: bool isOverriding() const; protected: std::shared_ptr m_other; std::shared_ptr m_gate; }; PrismLauncher-10.0.5/launcher/settings/SettingsObject.h0000644000175100017510000002145315144136756022541 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #ifdef Q_OS_MACOS #include "macsandbox/SecurityBookmarkFileAccess.h" #endif class Setting; class SettingsObject; using SettingsObjectPtr = std::shared_ptr; using SettingsObjectWeakPtr = std::weak_ptr; /*! * \brief The SettingsObject handles communicating settings between the application and a *settings file. * The class keeps a list of Setting objects. Each Setting object represents one * of the application's settings. These Setting objects are registered with * a SettingsObject and can be managed similarly to the way a list works. * * \author Andrew Okin * \date 2/22/2013 * * \sa Setting */ class SettingsObject : public QObject { Q_OBJECT public: class Lock { public: Lock(SettingsObjectPtr locked) : m_locked(locked) { m_locked->suspendSave(); } ~Lock() { m_locked->resumeSave(); } private: SettingsObjectPtr m_locked; }; public: explicit SettingsObject(QObject* parent = 0); virtual ~SettingsObject(); /*! * Registers an override setting for the given original setting in this settings object * gate decides if the passthrough (true) or the original (false) is used for value * * This will fail if there is already a setting with the same ID as * the one that is being registered. * \return A valid Setting shared pointer if successful. */ std::shared_ptr registerOverride(std::shared_ptr original, std::shared_ptr gate); /*! * Registers a passthorugh setting for the given original setting in this settings object * gate decides if the passthrough (true) or the original (false) is used for value * * This will fail if there is already a setting with the same ID as * the one that is being registered. * \return A valid Setting shared pointer if successful. */ std::shared_ptr registerPassthrough(std::shared_ptr original, std::shared_ptr gate); /*! * Registers the given setting with this SettingsObject and connects the necessary signals. * * This will fail if there is already a setting with the same ID as * the one that is being registered. * \return A valid Setting shared pointer if successful. */ std::shared_ptr registerSetting(QStringList synonyms, QVariant defVal = QVariant()); /*! * Registers the given setting with this SettingsObject and connects the necessary signals. * * This will fail if there is already a setting with the same ID as * the one that is being registered. * \return A valid Setting shared pointer if successful. */ std::shared_ptr registerSetting(QString id, QVariant defVal = QVariant()) { return registerSetting(QStringList(id), defVal); } /*! * \brief Gets the setting with the given ID. * \param id The ID of the setting to get. * \return A pointer to the setting with the given ID. * Returns null if there is no setting with the given ID. * \sa operator []() */ std::shared_ptr getSetting(const QString& id) const; /*! * \brief Gets the setting with the given ID. * \brief if is not registered yet it does that * \param id The ID of the setting to get. * \return A pointer to the setting with the given ID. * Returns null if there is no setting with the given ID. * \sa operator []() */ std::shared_ptr getOrRegisterSetting(const QString& id, QVariant defVal = QVariant()); /*! * \brief Gets the value of the setting with the given ID. * \param id The ID of the setting to get. * \return The setting's value as a QVariant. * If no setting with the given ID exists, returns an invalid QVariant. */ QVariant get(const QString& id); #ifdef Q_OS_MACOS /*! * \brief Get the path to the file or directory represented by the bookmark stored in the associated setting. * \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end. * \return A path to the file or directory represented by the bookmark. * If a bookmark is not valid or stored, use default logic (directly return the stored path). * This can attempt to create a bookmark if the path is accessible and the bookmark is not valid. */ QString getPathFromBookmark(const QString& id); /*! * \brief Set a security-scoped bookmark to the provided path for the associated setting. * \param id The setting ID of the relevant directory - this should not include "Bookmark" at the end. * \param path The new desired path. * \return A boolean indicating whether a bookmark was successfully set. * The path needs to be accessible to the launcher before calling this function. For example, * it could come from a user selection in an open panel. */ bool setPathWithBookmark(const QString& id, const QString& path); #endif /*! * \brief Sets the value of the setting with the given ID. * If no setting with the given ID exists, returns false * \param id The ID of the setting to change. * \param value The new value of the setting. * \return True if successful, false if it failed. */ bool set(const QString& id, QVariant value); /*! * \brief Reverts the setting with the given ID to default. * \param id The ID of the setting to reset. */ void reset(const QString& id) const; /*! * \brief Checks if this SettingsObject contains a setting with the given ID. * \param id The ID to check for. * \return True if the SettingsObject has a setting with the given ID. */ bool contains(const QString& id); /*! * \brief Reloads the settings and emit signals for changed settings * \return True if reloading was successful */ virtual bool reload(); virtual void suspendSave() = 0; virtual void resumeSave() = 0; signals: /*! * \brief Signal emitted when one of this SettingsObject object's settings changes. * This is usually just connected directly to each Setting object's * SettingChanged() signals. * \param setting A reference to the Setting object that changed. * \param value The Setting object's new value. */ void SettingChanged(const Setting& setting, QVariant value); /*! * \brief Signal emitted when one of this SettingsObject object's settings resets. * This is usually just connected directly to each Setting object's * settingReset() signals. * \param setting A reference to the Setting object that changed. */ void settingReset(const Setting& setting); protected slots: /*! * \brief Changes a setting. * This slot is usually connected to each Setting object's * SettingChanged() signal. The signal is emitted, causing this slot * to update the setting's value in the config file. * \param setting A reference to the Setting object that changed. * \param value The setting's new value. */ virtual void changeSetting(const Setting& setting, QVariant value) = 0; /*! * \brief Resets a setting. * This slot is usually connected to each Setting object's * settingReset() signal. The signal is emitted, causing this slot * to update the setting's value in the config file. * \param setting A reference to the Setting object that changed. */ virtual void resetSetting(const Setting& setting) = 0; protected: /*! * \brief Connects the necessary signals to the given Setting. * \param setting The setting to connect. */ void connectSignals(const Setting& setting); /*! * \brief Function used by Setting objects to get their values from the SettingsObject. * \param setting The * \return */ virtual QVariant retrieveValue(const Setting& setting) = 0; friend class Setting; private: QMap> m_settings; #ifdef Q_OS_MACOS SecurityBookmarkFileAccess m_sandboxedFileAccess; #endif protected: bool m_suspendSave = false; bool m_doSave = false; }; PrismLauncher-10.0.5/launcher/settings/INISettingsObject.cpp0000644000175100017510000000611315144136756023430 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "INISettingsObject.h" #include "Setting.h" #include #include INISettingsObject::INISettingsObject(QStringList paths, QObject* parent) : SettingsObject(parent) { auto first_path = paths.constFirst(); for (auto path : paths) { if (!QFile::exists(path)) continue; if (path != first_path && QFile::exists(path)) { // Copy the fallback to the preferred path. QFile::copy(path, first_path); qDebug() << "Copied settings from" << path << "to" << first_path; break; } } m_filePath = first_path; m_ini.loadFile(first_path); } INISettingsObject::INISettingsObject(QString path, QObject* parent) : SettingsObject(parent) { m_filePath = path; m_ini.loadFile(path); } void INISettingsObject::setFilePath(const QString& filePath) { m_filePath = filePath; } bool INISettingsObject::reload() { return m_ini.loadFile(m_filePath) && SettingsObject::reload(); } void INISettingsObject::suspendSave() { m_suspendSave = true; } void INISettingsObject::resumeSave() { m_suspendSave = false; if (m_doSave) { m_ini.saveFile(m_filePath); } } void INISettingsObject::changeSetting(const Setting& setting, QVariant value) { if (contains(setting.id())) { // valid value -> set the main config, remove all the sysnonyms if (value.isValid()) { auto list = setting.configKeys(); m_ini.set(list.takeFirst(), value); for (auto iter : list) m_ini.remove(iter); } // invalid -> remove all (just like resetSetting) else { for (auto iter : setting.configKeys()) m_ini.remove(iter); } doSave(); } } void INISettingsObject::doSave() { if (m_suspendSave) { m_doSave = true; } else { m_ini.saveFile(m_filePath); } } void INISettingsObject::resetSetting(const Setting& setting) { // if we have the setting, remove all the synonyms. ALL OF THEM if (contains(setting.id())) { for (auto iter : setting.configKeys()) m_ini.remove(iter); doSave(); } } QVariant INISettingsObject::retrieveValue(const Setting& setting) { // if we have the setting, return value of the first matching synonym if (contains(setting.id())) { for (auto iter : setting.configKeys()) { if (m_ini.contains(iter)) return m_ini[iter]; } } return QVariant(); } PrismLauncher-10.0.5/launcher/settings/OverrideSetting.h0000644000175100017510000000251615144136756022726 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include "Setting.h" /*! * \brief A setting that 'overrides another.' * This means that the setting's default value will be the value of another setting. * The other setting can be (and usually is) a part of a different SettingsObject * than this one. */ class OverrideSetting : public Setting { Q_OBJECT public: explicit OverrideSetting(std::shared_ptr overridden, std::shared_ptr gate); virtual QVariant defValue() const; virtual QVariant get() const; virtual void set(QVariant value); virtual void reset(); private: bool isOverriding() const; protected: std::shared_ptr m_other; std::shared_ptr m_gate; }; PrismLauncher-10.0.5/launcher/settings/INIFile.cpp0000644000175100017510000001777015144136756021373 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2023 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "settings/INIFile.h" #include #include #include #include #include #include #include #include "Json.h" INIFile::INIFile() {} bool INIFile::saveFile(QString fileName) { if (!contains("ConfigVersion")) insert("ConfigVersion", "1.3"); QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; _settings_obj.setFallbacksEnabled(false); _settings_obj.clear(); for (Iterator iter = begin(); iter != end(); iter++) _settings_obj.setValue(iter.key(), iter.value()); _settings_obj.sync(); if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { // Shouldn't be possible! Q_ASSERT(status != QSettings::Status::FormatError); if (status == QSettings::Status::AccessError) qCritical() << "An access error occurred (e.g. trying to write to a read-only file)."; return false; } return true; } QString unescape(QString orig) { QString out; QChar prev = QChar::Null; for (auto c : orig) { if (prev == '\\') { if (c == 'n') out += '\n'; else if (c == 't') out += '\t'; else if (c == '#') out += '#'; else out += c; prev = QChar::Null; } else { if (c == '\\') { prev = c; continue; } out += c; prev = QChar::Null; } } return out; } QString unquote(QString str) { if ((str.contains(QChar(';')) || str.contains(QChar('=')) || str.contains(QChar(','))) && str.endsWith("\"") && str.startsWith("\"")) { #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) str = str.remove(0, 1); str = str.remove(str.size() - 1, 1); #else str = str.removeFirst().removeLast(); #endif } return str; } bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map) { QTextStream in(device.readAll()); #if QT_VERSION <= QT_VERSION_CHECK(6, 0, 0) in.setCodec("UTF-8"); #endif QStringList lines = in.readAll().split('\n'); for (int i = 0; i < lines.count(); i++) { QString& lineRaw = lines[i]; // Ignore comments. int commentIndex = 0; QString line = lineRaw; // Search for comments until no more escaped # are available while ((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) { if (commentIndex > 0 && line.at(commentIndex - 1) == '\\') { continue; } line = line.left(lineRaw.indexOf('#')).trimmed(); } int eqPos = line.indexOf('='); if (eqPos == -1) continue; QString key = line.left(eqPos).trimmed(); QString valueStr = line.right(line.length() - eqPos - 1).trimmed(); valueStr = unquote(unescape(valueStr)); QVariant value(valueStr); map.insert(key, value); } return true; } QVariant migrateQByteArrayToBase64(QString key, QVariant value) { static const QStringList otherByteArrays = { "MainWindowState", "MainWindowGeometry", "ConsoleWindowState", "ConsoleWindowGeometry", "PagedGeometry", "NewInstanceGeometry", "ModDownloadGeometry", "RPDownloadGeometry", "TPDownloadGeometry", "ShaderDownloadGeometry" }; if (key.startsWith("WideBarVisibility_") || (key.startsWith("UI/") && key.endsWith("_Page/Columns"))) { return QString::fromUtf8(value.toByteArray().toBase64()); } if (otherByteArrays.contains(key)) { return QString::fromUtf8(value.toByteArray()); } if (key == "linkedInstances") { return Json::fromStringList(value.toStringList()); } if (key == "Env") { return Json::fromMap(value.toMap()); } return value; } bool INIFile::loadFile(QString fileName) { QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; _settings_obj.setFallbacksEnabled(false); if (auto status = _settings_obj.status(); status != QSettings::Status::NoError) { if (status == QSettings::Status::AccessError) qCritical() << "An access error occurred (e.g. trying to write to a read-only file)."; if (status == QSettings::Status::FormatError) qCritical() << "A format error occurred (e.g. loading a malformed INI file)."; return false; } if (!_settings_obj.value("ConfigVersion").isValid()) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) return false; QSettings::SettingsMap map; parseOldFileFormat(file, map); file.close(); for (auto&& key : map.keys()) { auto value = migrateQByteArrayToBase64(key, map.value(key)); insert(key, value); } insert("ConfigVersion", "1.3"); } else if (_settings_obj.value("ConfigVersion").toString() == "1.1") { for (auto&& key : _settings_obj.allKeys()) { auto value = migrateQByteArrayToBase64(key, _settings_obj.value(key)); if (auto valueStr = value.toString(); (valueStr.contains(QChar(';')) || valueStr.contains(QChar('=')) || valueStr.contains(QChar(','))) && valueStr.endsWith("\"") && valueStr.startsWith("\"")) { insert(key, unquote(valueStr)); } else { insert(key, value); } } insert("ConfigVersion", "1.3"); } else if (_settings_obj.value("ConfigVersion").toString() == "1.2") { for (auto&& key : _settings_obj.allKeys()) { auto value = migrateQByteArrayToBase64(key, _settings_obj.value(key)); insert(key, value); } insert("ConfigVersion", "1.3"); } else { for (auto&& key : _settings_obj.allKeys()) { insert(key, _settings_obj.value(key)); } } return true; } bool INIFile::loadFile(QByteArray data) { QTemporaryFile file; if (!file.open()) return false; file.write(data); file.flush(); file.close(); auto loaded = loadFile(file.fileName()); file.remove(); return loaded; } QVariant INIFile::get(QString key, QVariant def) const { if (!this->contains(key)) return def; else return this->operator[](key); } void INIFile::set(QString key, QVariant val) { this->operator[](key) = val; } PrismLauncher-10.0.5/launcher/settings/Setting.cpp0000644000175100017510000000233515144136756021560 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "Setting.h" #include "settings/SettingsObject.h" Setting::Setting(QStringList synonyms, QVariant defVal) : QObject(), m_synonyms(synonyms), m_defVal(defVal) {} QVariant Setting::get() const { SettingsObject* sbase = m_storage; if (!sbase) { return defValue(); } else { QVariant test = sbase->retrieveValue(*this); if (!test.isValid()) return defValue(); return test; } } QVariant Setting::defValue() const { return m_defVal; } void Setting::set(QVariant value) { emit SettingChanged(*this, value); } void Setting::reset() { emit settingReset(*this); } PrismLauncher-10.0.5/launcher/QObjectPtr.h0000644000175100017510000000315115144136756017762 0ustar runnerrunner#pragma once #include #include #include #include /** * A unique pointer class with unique pointer semantics intended for derivates of QObject * Calls deleteLater() instead of destroying the contained object immediately */ template using unique_qobject_ptr = QScopedPointer; /** * A shared pointer class with shared pointer semantics intended for derivates of QObject * Calls deleteLater() instead of destroying the contained object immediately */ template class shared_qobject_ptr : public QSharedPointer { public: constexpr explicit shared_qobject_ptr() : QSharedPointer() {} constexpr explicit shared_qobject_ptr(T* ptr) : QSharedPointer(ptr, &QObject::deleteLater) {} constexpr shared_qobject_ptr(std::nullptr_t null_ptr) : QSharedPointer(null_ptr, &QObject::deleteLater) {} template constexpr shared_qobject_ptr(const shared_qobject_ptr& other) : QSharedPointer(other) {} template constexpr shared_qobject_ptr(const QSharedPointer& other) : QSharedPointer(other) {} void reset() { QSharedPointer::reset(); } void reset(T* other) { shared_qobject_ptr t(other); this->swap(t); } void reset(const shared_qobject_ptr& other) { shared_qobject_ptr t(other); this->swap(t); } }; template shared_qobject_ptr makeShared(Args... args) { auto obj = new T(args...); return shared_qobject_ptr(obj); } PrismLauncher-10.0.5/launcher/ResourceDownloadTask.h0000644000175100017510000000441015144136756022046 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022-2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "net/NetJob.h" #include "tasks/SequentialTask.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/ModIndex.h" class ResourceFolderModel; class ResourceDownloadTask : public SequentialTask { Q_OBJECT public: explicit ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, std::shared_ptr packs, bool is_indexed = true); const QString& getFilename() const { return m_pack_version.fileName; } const QVariant& getVersionID() const { return m_pack_version.fileId; } const ModPlatform::IndexedVersion& getVersion() const { return m_pack_version; } const ModPlatform::ResourceProvider& getProvider() const { return m_pack->provider; } const QString& getName() const { return m_pack->name; } ModPlatform::IndexedPack::Ptr getPack() { return m_pack; } private: ModPlatform::IndexedPack::Ptr m_pack; ModPlatform::IndexedVersion m_pack_version; const std::shared_ptr m_pack_model; NetJob::Ptr m_filesNetJob; LocalResourceUpdateTask::Ptr m_update_task; void downloadProgressChanged(qint64 current, qint64 total); void downloadFailed(QString reason); void downloadSucceeded(); std::tuple to_delete{ "", "" }; private slots: void hasOldResource(QString name, QString filename); }; PrismLauncher-10.0.5/launcher/InstanceDirUpdate.cpp0000644000175100017510000001316715144136756021656 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "InstanceDirUpdate.h" #include #include "Application.h" #include "FileSystem.h" #include "InstanceList.h" #include "ui/dialogs/CustomMessageBox.h" QString askToUpdateInstanceDirName(InstancePtr instance, const QString& oldName, const QString& newName, QWidget* parent) { if (oldName == newName) return QString(); QString renamingMode = APPLICATION->settings()->get("InstRenamingMode").toString(); if (renamingMode == "MetadataOnly") return QString(); auto oldRoot = instance->instanceRoot(); auto newDirName = FS::DirNameFromString(newName, QFileInfo(oldRoot).dir().absolutePath()); auto newRoot = FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newDirName); if (oldRoot == newRoot) return QString(); if (oldRoot == FS::PathCombine(QFileInfo(oldRoot).dir().absolutePath(), newName)) return QString(); // Check for conflict if (QDir(newRoot).exists()) { QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), QObject::tr("New instance root (%1) already exists.
    Only the metadata will be renamed.").arg(newRoot)); return QString(); } // Ask if we should rename if (renamingMode == "AskEverytime") { auto checkBox = new QCheckBox(QObject::tr("&Remember my choice"), parent); auto dialog = CustomMessageBox::selectable(parent, QObject::tr("Rename instance folder"), QObject::tr("Would you also like to rename the instance folder?\n\n" "Old name: %1\n" "New name: %2") .arg(oldName, newName), QMessageBox::Question, QMessageBox::No | QMessageBox::Yes, QMessageBox::NoButton, checkBox); auto res = dialog->exec(); if (checkBox->isChecked()) { if (res == QMessageBox::Yes) APPLICATION->settings()->set("InstRenamingMode", "PhysicalDir"); else APPLICATION->settings()->set("InstRenamingMode", "MetadataOnly"); } if (res == QMessageBox::No) return QString(); } // Check for linked instances if (!checkLinkedInstances(instance->id(), parent, QObject::tr("Renaming"))) return QString(); // Now we can confirm that a renaming is happening if (!instance->syncInstanceDirName(newRoot)) { QMessageBox::warning(parent, QObject::tr("Cannot rename instance"), QObject::tr("An error occurred when performing the following renaming operation:
    " " - Old instance root: %1
    " " - New instance root: %2
    " "Only the metadata is renamed.") .arg(oldRoot, newRoot)); return QString(); } return newRoot; } bool checkLinkedInstances(const QString& id, QWidget* parent, const QString& verb) { auto linkedInstances = APPLICATION->instances()->getLinkedInstancesById(id); if (!linkedInstances.empty()) { auto response = CustomMessageBox::selectable(parent, QObject::tr("There are linked instances"), QObject::tr("The following instance(s) might reference files in this instance:\n\n" "%1\n\n" "%2 it could break the other instance(s), \n\n" "Do you wish to proceed?", nullptr, linkedInstances.count()) .arg(linkedInstances.join("\n")) .arg(verb), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); if (response != QMessageBox::Yes) return false; } return true; } PrismLauncher-10.0.5/launcher/news/0000755000175100017510000000000015144136756016550 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/news/NewsEntry.cpp0000644000175100017510000000345715144136756021223 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NewsEntry.h" #include #include NewsEntry::NewsEntry(QObject* parent) : QObject(parent) { this->title = tr("Untitled"); this->content = tr("No content."); this->link = ""; } NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, QObject* parent) : QObject(parent) { this->title = title; this->content = content; this->link = link; } /*! * Gets the text content of the given child element as a QVariant. */ inline QString childValue(const QDomElement& element, const QString& childName, QString defaultVal = "") { QDomNodeList nodes = element.elementsByTagName(childName); if (nodes.count() > 0) { QDomElement elem = nodes.at(0).toElement(); return elem.text(); } else { return defaultVal; } } bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, [[maybe_unused]] QString* errorMsg) { QString title = childValue(element, "title", tr("Untitled")); QString content = childValue(element, "content", tr("No content.")); QString link = childValue(element, "id"); entry->title = title; entry->content = content; entry->link = link; return true; } PrismLauncher-10.0.5/launcher/news/NewsChecker.h0000644000175100017510000000540615144136756021127 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include "NewsEntry.h" class NewsChecker : public QObject { Q_OBJECT public: /*! * Constructs a news reader to read from the given RSS feed URL. */ NewsChecker(shared_qobject_ptr network, const QString& feedUrl); /*! * Returns the error message for the last time the news was loaded. * Empty string if the last load was successful. */ QString getLastLoadErrorMsg() const; /*! * Returns true if the news has been loaded successfully. */ bool isNewsLoaded() const; //! True if the news is currently loading. If true, reloadNews() will do nothing. bool isLoadingNews() const; /*! * Returns a list of news entries. */ QList getNewsEntries() const; /*! * Reloads the news from the website's RSS feed. * If the news is already loading, this does nothing. */ void Q_SLOT reloadNews(); signals: /*! * Signal fired after the news has finished loading. */ void newsLoaded(); /*! * Signal fired after the news fails to load. */ void newsLoadingFailed(QString errorMsg); protected slots: void rssDownloadFinished(); void rssDownloadFailed(QString reason); protected: /* data */ //! The URL for the RSS feed to fetch. QString m_feedUrl; //! List of news entries. QList m_newsEntries; //! The network job to use to load the news. NetJob::Ptr m_newsNetJob; //! True if news has been loaded. bool m_loadedNews; std::shared_ptr newsData = std::make_shared(); /*! * Gets the error message that was given last time the news was loaded. * If the last news load succeeded, this will be an empty string. */ QString m_lastLoadError; shared_qobject_ptr m_network; protected slots: /// Emits newsLoaded() and sets m_lastLoadError to empty string. void succeed(); /// Emits newsLoadingFailed() and sets m_lastLoadError to the given message. void fail(const QString& errorMsg); }; PrismLauncher-10.0.5/launcher/news/NewsChecker.cpp0000644000175100017510000001072315144136756021460 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "NewsChecker.h" #include #include #include NewsChecker::NewsChecker(shared_qobject_ptr network, const QString& feedUrl) { m_network = network; m_feedUrl = feedUrl; } void NewsChecker::reloadNews() { // Start a netjob to download the RSS feed and call rssDownloadFinished() when it's done. if (isLoadingNews()) { qDebug() << "Ignored request to reload news. Currently reloading already."; return; } qDebug() << "Reloading news."; NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData)); job->setAskRetry(false); connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); job->start(); } void NewsChecker::rssDownloadFinished() { // Parse the XML file and process the RSS feed entries. qDebug() << "Finished loading RSS feed."; m_newsNetJob.reset(); QDomDocument doc; { // Stuff to store error info in. QString errorMsg = "Unknown error."; int errorLine = -1; int errorCol = -1; // Parse the XML. if (!doc.setContent(*newsData, false, &errorMsg, &errorLine, &errorCol)) { QString fullErrorMsg = QString("Error parsing RSS feed XML. %1 at %2:%3.").arg(errorMsg).arg(errorLine).arg(errorCol); fail(fullErrorMsg); newsData->clear(); return; } newsData->clear(); } // If the parsing succeeded, read it. QDomNodeList items = doc.elementsByTagName("entry"); m_newsEntries.clear(); for (int i = 0; i < items.length(); i++) { QDomElement element = items.at(i).toElement(); NewsEntryPtr entry; entry.reset(new NewsEntry()); QString errorMsg = "An unknown error occurred."; if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) { qDebug() << "Loaded news entry" << entry->title; m_newsEntries.append(entry); } else { qWarning() << "Failed to load news entry at index" << i << ":" << errorMsg; } } succeed(); } void NewsChecker::rssDownloadFailed(QString reason) { // Set an error message and fail. fail(tr("Failed to load news RSS feed:\n%1").arg(reason)); } QList NewsChecker::getNewsEntries() const { return m_newsEntries; } bool NewsChecker::isLoadingNews() const { return m_newsNetJob.get() != nullptr; } QString NewsChecker::getLastLoadErrorMsg() const { return m_lastLoadError; } void NewsChecker::succeed() { m_lastLoadError = ""; qDebug() << "News loading succeeded."; m_newsNetJob.reset(); emit newsLoaded(); } void NewsChecker::fail(const QString& errorMsg) { m_lastLoadError = errorMsg; qDebug() << "Failed to load news:" << errorMsg; m_newsNetJob.reset(); emit newsLoadingFailed(errorMsg); } PrismLauncher-10.0.5/launcher/news/NewsEntry.h0000644000175100017510000000306615144136756020664 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include class NewsEntry : public QObject { Q_OBJECT public: /*! * Constructs an empty news entry. */ explicit NewsEntry(QObject* parent = 0); /*! * Constructs a new news entry. * Note that content may contain HTML. */ NewsEntry(const QString& title, const QString& content, const QString& link, QObject* parent = 0); /*! * Attempts to load information from the given XML element into the given news entry pointer. * If this fails, the function will return false and store an error message in the errorMsg pointer. */ static bool fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg = 0); //! The post title. QString title; //! The post's content. May contain HTML. QString content; //! URL to the post. QString link; }; using NewsEntryPtr = std::shared_ptr; PrismLauncher-10.0.5/launcher/modplatform/0000755000175100017510000000000015144136756020120 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/import_ftb/0000755000175100017510000000000015144136756022265 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/import_ftb/PackHelpers.cpp0000644000175100017510000001401115144136756025167 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "modplatform/import_ftb/PackHelpers.h" #include #include #include #include #include "FileSystem.h" #include "Json.h" namespace FTBImportAPP { QIcon loadFTBIcon(const QString& imagePath) { // Map of type byte to image type string static const QHash imageTypeMap = { { 0x00, "png" }, { 0x01, "jpg" }, { 0x02, "gif" }, { 0x03, "webp" } }; QFile file(imagePath); if (!file.exists() || !file.open(QIODevice::ReadOnly)) { return QIcon(); } char type; if (!file.getChar(&type)) { qDebug() << "Missing FTB image type header at" << imagePath; return QIcon(); } if (!imageTypeMap.contains(type)) { qDebug().nospace().noquote() << "Don't recognize FTB image type 0x" << QString::number(type, 16); return QIcon(); } auto imageType = imageTypeMap[type]; // Extract actual image data beyond the first byte QImageReader reader(&file, imageType); auto pixmap = QPixmap::fromImageReader(&reader); if (pixmap.isNull()) { qDebug() << "The FTB image at" << imagePath << "is not valid"; return QIcon(); } return QIcon(pixmap); } Modpack parseDirectory(QString path) { Modpack modpack{ path }; auto instanceFile = QFileInfo(FS::PathCombine(path, "instance.json")); if (!instanceFile.exists() || !instanceFile.isFile()) return {}; try { auto doc = Json::requireDocument(instanceFile.absoluteFilePath(), "FTB_APP instance JSON file"); const auto root = doc.object(); modpack.uuid = Json::requireString(root, "uuid", "uuid"); modpack.id = Json::requireInteger(root, "id", "id"); modpack.versionId = Json::requireInteger(root, "versionId", "versionId"); modpack.name = Json::requireString(root, "name", "name"); modpack.version = Json::requireString(root, "version", "version"); modpack.mcVersion = Json::requireString(root, "mcVersion", "mcVersion"); modpack.jvmArgs = root["jvmArgs"].toVariant(); modpack.totalPlayTime = Json::requireInteger(root, "totalPlayTime", "totalPlayTime"); auto modLoader = Json::requireString(root, "modLoader", "modLoader"); if (!modLoader.isEmpty()) { const auto parts = modLoader.split('-', Qt::KeepEmptyParts); if (parts.size() >= 2) { const auto loader = parts.first().toLower(); modpack.loaderVersion = parts.at(1).trimmed(); if (loader == "neoforge") { modpack.loaderType = ModPlatform::NeoForge; } else if (loader == "forge") { modpack.loaderType = ModPlatform::Forge; } else if (loader == "fabric") { modpack.loaderType = ModPlatform::Fabric; } else if (loader == "quilt") { modpack.loaderType = ModPlatform::Quilt; } } } } catch (const Exception& e) { qDebug() << "Couldn't load ftb instance json:" << e.cause(); return {}; } if (!modpack.loaderType.has_value()) { legacyInstanceParsing(path, &modpack.loaderType, &modpack.loaderVersion); } auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg")); if (iconFile.exists() && iconFile.isFile()) { modpack.icon = QIcon(iconFile.absoluteFilePath()); } else { // the logo is a file that the first bit denotes the image tipe followed by the actual image data modpack.icon = loadFTBIcon(FS::PathCombine(path, ".ftbapp", "logo")); } return modpack; } void legacyInstanceParsing(QString path, std::optional* loaderType, QString* loaderVersion) { auto versionsFile = QFileInfo(FS::PathCombine(path, ".ftbapp", "version.json")); if (!versionsFile.exists() || !versionsFile.isFile()) { versionsFile = QFileInfo(FS::PathCombine(path, "version.json")); } if (!versionsFile.exists() || !versionsFile.isFile()) { qDebug() << "Couldn't find ftb version json"; return; } try { auto doc = Json::requireDocument(versionsFile.absoluteFilePath(), "FTB_APP version JSON file"); const auto root = doc.object(); auto targets = Json::requireArray(root, "targets", "targets"); for (auto target : targets) { auto obj = Json::requireObject(target, "target"); auto name = Json::requireString(obj, "name", "name"); auto version = Json::requireString(obj, "version", "version"); if (name == "neoforge") { *loaderType = ModPlatform::NeoForge; *loaderVersion = version; break; } else if (name == "forge") { *loaderType = ModPlatform::Forge; *loaderVersion = version; break; } else if (name == "fabric") { *loaderType = ModPlatform::Fabric; *loaderVersion = version; break; } else if (name == "quilt") { *loaderType = ModPlatform::Quilt; *loaderVersion = version; break; } } } catch (const Exception& e) { qDebug() << "Couldn't load ftb version json:" << e.cause(); return; } } } // namespace FTBImportAPP PrismLauncher-10.0.5/launcher/modplatform/import_ftb/PackInstallTask.cpp0000644000175100017510000001006615144136756026024 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "PackInstallTask.h" #include #include "BaseInstance.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/ResourceAPI.h" #include "modplatform/import_ftb/PackHelpers.h" #include "settings/INISettingsObject.h" namespace FTBImportAPP { void PackInstallTask::executeTask() { setStatus(tr("Copying files...")); setAbortable(false); progress(1, 2); m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { FS::copy folderCopy(m_pack.path, FS::PathCombine(m_stagingPath, "minecraft")); return folderCopy(); }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::copySettings); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); m_copyFutureWatcher.setFuture(m_copyFuture); } void PackInstallTask::copySettings() { setStatus(tr("Copying settings...")); progress(2, 2); QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); instanceSettings->suspendSave(); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); instance.settings()->set("InstanceType", "OneSix"); instance.settings()->set("totalTimePlayed", m_pack.totalPlayTime / 1000); if (m_pack.jvmArgs.isValid() && !m_pack.jvmArgs.toString().isEmpty()) { instance.settings()->set("OverrideJavaArgs", true); instance.settings()->set("JvmArgs", m_pack.jvmArgs.toString()); } auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); auto modloader = m_pack.loaderType; if (modloader.has_value()) switch (modloader.value()) { case ModPlatform::NeoForge: { components->setComponentVersion("net.neoforged", m_pack.loaderVersion, true); break; } case ModPlatform::Forge: { components->setComponentVersion("net.minecraftforge", m_pack.loaderVersion, true); break; } case ModPlatform::Fabric: { components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.loaderVersion, true); break; } case ModPlatform::Quilt: { components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.loaderVersion, true); break; } case ModPlatform::Cauldron: break; case ModPlatform::LiteLoader: break; case ModPlatform::DataPack: break; case ModPlatform::Babric: break; case ModPlatform::BTA: break; case ModPlatform::LegacyFabric: break; case ModPlatform::Ornithe: break; case ModPlatform::Rift: break; } components->saveNow(); instance.setName(name()); if (m_instIcon == "default") m_instIcon = "ftb_logo"; instance.setIconKey(m_instIcon); instanceSettings->resumeSave(); emitSucceeded(); } } // namespace FTBImportAPP PrismLauncher-10.0.5/launcher/modplatform/import_ftb/PackHelpers.h0000644000175100017510000000304715144136756024643 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include "modplatform/ResourceAPI.h" namespace FTBImportAPP { struct Modpack { QString path; // json data QString uuid; int id; int versionId; QString name; QString version; QString mcVersion; int totalPlayTime; // not needed for instance creation QVariant jvmArgs; std::optional loaderType; QString loaderVersion; QIcon icon; }; using ModpackList = QList; Modpack parseDirectory(QString path); void legacyInstanceParsing(QString path, std::optional* loaderType, QString* loaderVersion); } // namespace FTBImportAPP // We need it for the proxy model Q_DECLARE_METATYPE(FTBImportAPP::Modpack) PrismLauncher-10.0.5/launcher/modplatform/import_ftb/PackInstallTask.h0000644000175100017510000000244415144136756025472 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "InstanceTask.h" #include "PackHelpers.h" namespace FTBImportAPP { class PackInstallTask : public InstanceTask { Q_OBJECT public: explicit PackInstallTask(const Modpack& pack) : m_pack(pack) {} virtual ~PackInstallTask() = default; protected: virtual void executeTask() override; private slots: void copySettings(); private: QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; const Modpack m_pack; }; } // namespace FTBImportAPP PrismLauncher-10.0.5/launcher/modplatform/legacy_ftb/0000755000175100017510000000000015144136756022217 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/legacy_ftb/PackInstallTask.cpp0000644000175100017510000001634115144136756025760 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PackInstallTask.h" #include #include "BaseInstance.h" #include "FileSystem.h" #include "MMCZip.h" #include "minecraft/GradleSpecifier.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "settings/INISettingsObject.h" #include "Application.h" #include "BuildConfig.h" #include "net/ApiDownload.h" namespace LegacyFTB { PackInstallTask::PackInstallTask(shared_qobject_ptr network, const Modpack& pack, QString version) { m_pack = pack; m_version = version; m_network = network; } void PackInstallTask::executeTask() { downloadPack(); } void PackInstallTask::downloadPack() { setStatus(tr("Downloading zip for %1").arg(m_pack.name)); setProgress(1, 4); setAbortable(false); auto path = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", path); entry->setStale(true); archivePath = entry->getFullPath(); netJobContainer.reset(new NetJob("Download FTB Pack", m_network)); QString url; if (m_pack.type == PackType::Private) { url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(path); } else { url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(path); } netJobContainer->addNetAction(Net::ApiDownload::makeCached(url, entry)); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed); connect(netJobContainer.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); connect(netJobContainer.get(), &NetJob::aborted, this, &PackInstallTask::emitAborted); netJobContainer->start(); setAbortable(true); progress(1, 4); } void PackInstallTask::unzip() { setStatus(tr("Extracting modpack")); setAbortable(false); progress(2, 4); QDir extractDir(m_stagingPath); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::onUnzipCanceled); m_extractFutureWatcher.setFuture(m_extractFuture); } void PackInstallTask::onUnzipFinished() { install(); } void PackInstallTask::onUnzipCanceled() { emitAborted(); } void PackInstallTask::install() { setStatus(tr("Installing modpack")); progress(3, 4); QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); if (unzipMcDir.exists()) { // ok, found minecraft dir, move contents to instance dir if (!FS::move(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/minecraft")) { emitFailed(tr("Failed to move unpacked Minecraft!")); return; } } QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); instanceSettings->suspendSave(); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); bool fallback = true; // handle different versions QFile packJson(m_stagingPath + "/minecraft/pack.json"); QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); if (packJson.exists()) { if (packJson.open(QIODevice::ReadOnly | QIODevice::Text)) { QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); packJson.close(); // we only care about the libs QJsonArray libs = doc.object().value("libraries").toArray(); for (const auto& value : libs) { QString nameValue = value.toObject().value("name").toString(); if (!nameValue.startsWith("net.minecraftforge")) { continue; } GradleSpecifier forgeVersion(nameValue); components->setComponentVersion("net.minecraftforge", forgeVersion.version().replace(m_pack.mcVersion, "").replace("-", "")); packJson.remove(); fallback = false; break; } } else { qWarning() << "Failed to open file '" << packJson.fileName() << "' for reading!"; } } if (jarmodDir.exists()) { qDebug() << "Found jarmods, installing..."; QStringList jarmods; for (auto info : jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { qDebug() << "Jarmod:" << info.fileName(); jarmods.push_back(info.absoluteFilePath()); } components->installJarMods(jarmods); fallback = false; } // just nuke unzip directory, it s not needed anymore FS::deletePath(m_stagingPath + "/unzip"); if (fallback) { // TODO: Some fallback mechanism... or just keep failing! emitFailed(tr("No installation method found!")); return; } components->saveNow(); progress(4, 4); instance.setName(name()); if (m_instIcon == "default") { m_instIcon = "ftb_logo"; } instance.setIconKey(m_instIcon); instanceSettings->resumeSave(); emitSucceeded(); } bool PackInstallTask::abort() { if (!canAbort()) { return false; } netJobContainer->abort(); return InstanceTask::abort(); } } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp0000644000175100017510000000441615144136756026434 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PrivatePackManager.h" #include #include "FileSystem.h" namespace LegacyFTB { void PrivatePackManager::load() { try { auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts); currentPacks = QSet(foo.begin(), foo.end()); dirty = false; } catch (...) { currentPacks = {}; qWarning() << "Failed to read third party FTB pack codes from" << m_filename; } } void PrivatePackManager::save() const { if (!dirty) { return; } try { QStringList list = currentPacks.values(); FS::write(m_filename, list.join('\n').toUtf8()); dirty = false; } catch (...) { qWarning() << "Failed to write third party FTB pack codes to" << m_filename; } } } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/modplatform/legacy_ftb/PackHelpers.h0000644000175100017510000000134315144136756024572 0ustar runnerrunner#pragma once #include #include #include #include namespace LegacyFTB { // Header for structs etc... enum class PackType { Public, ThirdParty, Private }; struct Modpack { QString name; QString description; QString author; QStringList oldVersions; QString currentVersion; QString mcVersion; QString mods; QString logo; // Technical data QString dir; QString file; //<- Url in the xml, but doesn't make much sense bool bugged = false; bool broken = false; PackType type; QString packCode; }; using ModpackList = QList; } // namespace LegacyFTB // We need it for the proxy model Q_DECLARE_METATYPE(LegacyFTB::Modpack) PrismLauncher-10.0.5/launcher/modplatform/legacy_ftb/PackFetchTask.h0000644000175100017510000000244715144136756025052 0ustar runnerrunner#pragma once #include #include #include #include #include "PackHelpers.h" #include "net/NetJob.h" namespace LegacyFTB { class PackFetchTask : public QObject { Q_OBJECT public: PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network) {}; virtual ~PackFetchTask() = default; void fetch(); void fetchPrivate(const QStringList& toFetch); private: shared_qobject_ptr m_network; NetJob::Ptr jobPtr; std::shared_ptr publicModpacksXmlFileData = std::make_shared(); std::shared_ptr thirdPartyModpacksXmlFileData = std::make_shared(); bool parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list); ModpackList publicPacks; ModpackList thirdPartyPacks; protected slots: void fileDownloadFinished(); void fileDownloadFailed(QString reason); void fileDownloadAborted(); signals: void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); void failed(QString reason); void aborted(); void privateFileDownloadFinished(const Modpack& modpack); void privateFileDownloadFailed(QString reason, QString packCode); }; } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/modplatform/legacy_ftb/PrivatePackManager.h0000644000175100017510000000127615144136756026102 0ustar runnerrunner#pragma once #include #include #include namespace LegacyFTB { class PrivatePackManager { public: ~PrivatePackManager() { save(); } void load(); void save() const; bool empty() const { return currentPacks.empty(); } const QSet& getCurrentPackCodes() const { return currentPacks; } void add(const QString& code) { currentPacks.insert(code); dirty = true; } void remove(const QString& code) { currentPacks.remove(code); dirty = true; } private: QSet currentPacks; QString m_filename = "private_packs.txt"; mutable bool dirty = false; }; } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/modplatform/legacy_ftb/PackFetchTask.cpp0000644000175100017510000001562115144136756025403 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "PackFetchTask.h" #include "PrivatePackManager.h" #include #include "Application.h" #include "BuildConfig.h" #include "net/ApiDownload.h" namespace LegacyFTB { void PackFetchTask::fetch() { publicPacks.clear(); thirdPartyPacks.clear(); jobPtr.reset(new NetJob("LegacyFTB::ModpackFetch", m_network)); QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); jobPtr->addNetAction(Net::ApiDownload::makeByteArray(publicPacksUrl, publicModpacksXmlFileData)); QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData)); connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); jobPtr->start(); } void PackFetchTask::fetchPrivate(const QStringList& toFetch) { QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml"; for (auto& packCode : toFetch) { auto data = std::make_shared(); NetJob* job = new NetJob("Fetching private pack", m_network); job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data)); job->setAskRetry(false); connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; parseAndAddPacks(*data, PackType::Private, packs); for (auto& currentPack : packs) { currentPack.packCode = packCode; emit privateFileDownloadFinished(currentPack); } job->deleteLater(); data->clear(); }); connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) { emit privateFileDownloadFailed(reason, packCode); job->deleteLater(); data->clear(); }); connect(job, &NetJob::aborted, this, [this, job, data] { job->deleteLater(); data->clear(); emit aborted(); }); job->start(); } } void PackFetchTask::fileDownloadFinished() { jobPtr.reset(); QStringList failedLists; if (!parseAndAddPacks(*publicModpacksXmlFileData, PackType::Public, publicPacks)) { failedLists.append(tr("Public Packs")); } if (!parseAndAddPacks(*thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) { failedLists.append(tr("Third Party Packs")); } if (failedLists.size() > 0) { emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- "))); } else { emit finished(publicPacks, thirdPartyPacks); } } bool PackFetchTask::parseAndAddPacks(QByteArray& data, PackType packType, ModpackList& list) { QDomDocument doc; QString errorMsg = "Unknown error."; int errorLine = -1; int errorCol = -1; if (!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) { auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:%3!").arg(errorMsg).arg(errorLine).arg(errorCol); qWarning() << fullErrMsg; data.clear(); return false; } QDomNodeList nodes = doc.elementsByTagName("modpack"); for (int i = 0; i < nodes.length(); i++) { QDomElement element = nodes.at(i).toElement(); Modpack modpack; modpack.name = element.attribute("name"); modpack.currentVersion = element.attribute("version"); modpack.mcVersion = element.attribute("mcVersion"); modpack.description = element.attribute("description"); modpack.mods = element.attribute("mods"); modpack.logo = element.attribute("logo"); modpack.oldVersions = element.attribute("oldVersions").split(";"); modpack.broken = false; modpack.bugged = false; // remove empty if the xml is bugged for (QString curr : modpack.oldVersions) { if (curr.isNull() || curr.isEmpty()) { modpack.oldVersions.removeAll(curr); modpack.bugged = true; qWarning() << "Removed some empty versions from" << modpack.name; } } if (modpack.oldVersions.size() < 1) { if (!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) { modpack.oldVersions.append(modpack.currentVersion); qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")"; } else { modpack.broken = true; qWarning() << "Broken pack:" << modpack.name << "=> No valid version!"; } } modpack.author = element.attribute("author"); modpack.dir = element.attribute("dir"); modpack.file = element.attribute("url"); modpack.type = packType; list.append(modpack); } return true; } void PackFetchTask::fileDownloadFailed(QString reason) { qWarning() << "Fetching FTBPacks failed:" << reason; emit failed(reason); } void PackFetchTask::fileDownloadAborted() { emit aborted(); } } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/modplatform/legacy_ftb/PackInstallTask.h0000644000175100017510000000214515144136756025422 0ustar runnerrunner#pragma once #include "InstanceTask.h" #include "PackHelpers.h" #include "meta/Index.h" #include "meta/Version.h" #include "meta/VersionList.h" #include "net/NetJob.h" #include namespace LegacyFTB { class PackInstallTask : public InstanceTask { Q_OBJECT public: explicit PackInstallTask(shared_qobject_ptr network, const Modpack& pack, QString version); virtual ~PackInstallTask() {} bool canAbort() const override { return true; } bool abort() override; protected: //! Entry point for tasks. virtual void executeTask() override; private: void downloadPack(); void unzip(); void install(); private slots: void onUnzipFinished(); void onUnzipCanceled(); private: /* data */ shared_qobject_ptr m_network; bool abortable = false; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; NetJob::Ptr netJobContainer; QString archivePath; Modpack m_pack; QString m_version; }; } // namespace LegacyFTB PrismLauncher-10.0.5/launcher/modplatform/ResourceType.cpp0000644000175100017510000000356715144136756023270 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ResourceType.h" namespace ModPlatform { static const QMap s_packedTypeNames = { { ResourceType::ResourcePack, QObject::tr("resource pack") }, { ResourceType::TexturePack, QObject::tr("texture pack") }, { ResourceType::DataPack, QObject::tr("data pack") }, { ResourceType::ShaderPack, QObject::tr("shader pack") }, { ResourceType::World, QObject::tr("world save") }, { ResourceType::Mod, QObject::tr("mod") }, { ResourceType::Unknown, QObject::tr("unknown") } }; namespace ResourceTypeUtils { QString getName(ResourceType type) { return s_packedTypeNames.constFind(type).value(); } } // namespace ResourceTypeUtils } // namespace ModPlatform PrismLauncher-10.0.5/launcher/modplatform/EnsureMetadataTask.h0000644000175100017510000000366115144136756024024 0ustar runnerrunner#pragma once #include "ModIndex.h" #include "net/NetJob.h" #include "modplatform/helpers/HashUtils.h" #include "minecraft/mod/Resource.h" #include "tasks/ConcurrentTask.h" class Mod; class QDir; class EnsureMetadataTask : public Task { Q_OBJECT public: EnsureMetadataTask(Resource*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(QHash&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); ~EnsureMetadataTask() = default; Task::Ptr getHashingTask() { return m_hashingTask; } public slots: bool abort() override; protected slots: void executeTask() override; private: // FIXME: Move to their own namespace Task::Ptr modrinthVersionsTask(); Task::Ptr modrinthProjectsTask(); Task::Ptr flameVersionsTask(); Task::Ptr flameProjectsTask(); // Helpers enum class RemoveFromList { Yes, No }; void emitReady(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); void emitFail(Resource*, QString key = {}, RemoveFromList = RemoveFromList::Yes); // Hashes and stuff Hashing::Hasher::Ptr createNewHash(Resource*); QString getExistingHash(Resource*); private slots: void updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource*); void updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource); signals: void metadataReady(Resource*); void metadataFailed(Resource*); private: QHash m_resources; QDir m_indexDir; ModPlatform::ResourceProvider m_provider; QHash m_tempVersions; Task::Ptr m_hashingTask; Task::Ptr m_currentTask; QHash m_updateMetadataTasks; }; PrismLauncher-10.0.5/launcher/modplatform/ResourceAPI.h0000644000175100017510000001360615144136756022420 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2023-2025 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include "../Version.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceType.h" #include "tasks/Task.h" /* Simple class with a common interface for interacting with APIs */ class ResourceAPI { public: virtual ~ResourceAPI() = default; struct SortingMethod { // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods. // Used by Flame in the API request. unsigned int index; // The real name of the sorting, as used in the respective API specification. // Used by Modrinth in the API request. QString name; // The human-readable name of the sorting, used for display in the UI. QString readable_name; }; template struct Callback { std::function on_succeed; std::function on_fail; std::function on_abort; }; struct SearchArgs { ModPlatform::ResourceType type{}; int offset = 0; std::optional search; std::optional sorting; std::optional loaders; std::optional> versions; std::optional side; std::optional categoryIds; bool openSource; }; struct VersionSearchArgs { ModPlatform::IndexedPack::Ptr pack; std::optional> mcVersions; std::optional loaders; ModPlatform::ResourceType resourceType; }; struct ProjectInfoArgs { ModPlatform::IndexedPack::Ptr pack; }; struct DependencySearchArgs { ModPlatform::Dependency dependency; Version mcVersion; ModPlatform::ModLoaderTypes loader; }; public: /** Gets a list of available sorting methods for this API. */ virtual auto getSortingMethods() const -> QList = 0; public slots: virtual Task::Ptr searchProjects(SearchArgs&&, Callback>&&) const; virtual Task::Ptr getProject(QString addonId, std::shared_ptr response) const; virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const = 0; virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback&&) const; Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const; virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback&&) const; protected: inline QString debugName() const { return "External resource API"; } QString mapMCVersionToModrinth(Version v) const; QString getGameVersionsString(std::list mcVersions) const; public: virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; virtual auto getInfoURL(QString const& id) const -> std::optional = 0; virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; /** Functions to load data into a pack. * * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. */ virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) const = 0; virtual ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const = 0; /** Converts a JSON document to a common array format. * * This is needed so that different providers, with different JSON structures, can be parsed * uniformally. You NEED to re-implement this if you intend on using the default callbacks. */ virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0; /** Functions to load data into a pack. * * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. */ virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) const = 0; }; PrismLauncher-10.0.5/launcher/modplatform/ModIndex.h0000644000175100017510000001425715144136756022011 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include #include #include class QIODevice; namespace ModPlatform { enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5, DataPack = 1 << 6, Babric = 1 << 7, BTA = 1 << 8, LegacyFabric = 1 << 9, Ornithe = 1 << 10, Rift = 1 << 11 }; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) QList modLoaderTypesToList(ModLoaderTypes flags); enum class ResourceProvider { MODRINTH, FLAME }; enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; enum class Side { NoSide = 0, ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; namespace SideUtils { QString toString(Side side); Side fromString(QString side); } // namespace SideUtils namespace ProviderCapabilities { const char* name(ResourceProvider); QString readableName(ResourceProvider); QStringList hashType(ResourceProvider); } // namespace ProviderCapabilities struct ModpackAuthor { QString name; QString url; }; struct DonationData { QString id; QString platform; QString url; }; struct IndexedVersionType { enum class Enum { Unknown, Release = 1, Beta, Alpha }; using enum Enum; constexpr IndexedVersionType(Enum e = Unknown) : m_type(e) {} static IndexedVersionType fromString(const QString& type); inline bool isValid() const { return m_type != Unknown; } std::strong_ordering operator<=>(const IndexedVersionType& other) const = default; std::strong_ordering operator<=>(const IndexedVersionType::Enum& other) const { return m_type <=> other; } QString toString() const; explicit operator int() const { return static_cast(m_type); } explicit operator IndexedVersionType::Enum() { return m_type; } private: Enum m_type; }; struct Dependency { QVariant addonId; DependencyType type; QString version; }; struct IndexedVersion { QVariant addonId; QVariant fileId; QString version; QString version_number = {}; IndexedVersionType version_type; QStringList mcVersion; QString downloadUrl; QString date; QString fileName; ModLoaderTypes loaders = {}; QString hash_type; QString hash; bool is_preferred = true; QString changelog; QList dependencies; Side side; // this is for flame API // For internal use, not provided by APIs bool is_currently_selected = false; QString getVersionDisplayString() const { auto release_type = version_type.isValid() ? QString(" [%1]").arg(version_type.toString()) : ""; auto versionStr = !version.contains(version_number) ? version_number : ""; QString gameVersion = ""; for (auto v : mcVersion) { if (version.contains(v)) { gameVersion = ""; break; } if (gameVersion.isEmpty()) { gameVersion = QObject::tr(" for %1").arg(v); } } return QString("%1%2 — %3%4").arg(version, gameVersion, versionStr, release_type); } }; struct ExtraPackData { QList donate; QString issuesUrl; QString sourceUrl; QString wikiUrl; QString discordUrl; QString status; QString body; }; struct IndexedPack { using Ptr = std::shared_ptr; QVariant addonId; ResourceProvider provider; QString name; QString slug; QString description; QList authors; QString logoName; QString logoUrl; QString websiteUrl; Side side; bool versionsLoaded = false; QList versions; // Don't load by default, since some modplatform don't have that info bool extraDataLoaded = true; ExtraPackData extraData; // For internal use, not provided by APIs bool isVersionSelected(int index) const { if (!versionsLoaded) return false; return versions.at(index).is_currently_selected; } bool isAnyVersionSelected() const { if (!versionsLoaded) return false; return std::any_of(versions.constBegin(), versions.constEnd(), [](auto const& v) { return v.is_currently_selected; }); } }; struct OverrideDep { QString quilt; QString fabric; QString slug; ModPlatform::ResourceProvider provider; }; inline auto getOverrideDeps() -> QList { return { { "634179", "306612", "API", ModPlatform::ResourceProvider::FLAME }, { "720410", "308769", "KotlinLibraries", ModPlatform::ResourceProvider::FLAME }, { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH }, { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } }; } QString getMetaURL(ResourceProvider provider, QVariant projectID); auto getModLoaderAsString(ModLoaderType type) -> const QString; auto getModLoaderFromString(QString type) -> ModLoaderType; constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept { auto x = static_cast(l); return x && !(x & (x - 1)); } struct Category { QString name; QString id; }; } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) Q_DECLARE_METATYPE(ModPlatform::IndexedPack::Ptr) Q_DECLARE_METATYPE(ModPlatform::ResourceProvider) PrismLauncher-10.0.5/launcher/modplatform/modrinth/0000755000175100017510000000000015144136756021744 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthAPI.cpp0000644000175100017510000001420215144136756024565 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "ModrinthAPI.h" #include "Application.h" #include "Json.h" #include "net/ApiDownload.h" #include "net/ApiUpload.h" #include "net/NetJob.h" #include "net/Upload.h" Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); return netJob; } Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetCurrentVersions"), APPLICATION->network()); QJsonObject body_obj; Json::writeStringList(body_obj, "hashes", hashes); Json::writeString(body_obj, "algorithm", hash_format); QJsonDocument body(body_obj); auto body_raw = body.toJson(); netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); netJob->setAskRetry(false); return netJob; } Task::Ptr ModrinthAPI::latestVersion(QString hash, QString hash_format, std::optional> mcVersions, std::optional loaders, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); QJsonObject body_obj; if (loaders.has_value()) Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); if (mcVersions.has_value()) { QStringList game_versions; for (auto& ver : mcVersions.value()) { game_versions.append(mapMCVersionToModrinth(ver)); } Json::writeStringList(body_obj, "game_versions", game_versions); } QJsonDocument body(body_obj); auto body_raw = body.toJson(); netJob->addNetAction(Net::ApiUpload::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); return netJob; } Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, std::optional loaders, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); QJsonObject body_obj; Json::writeStringList(body_obj, "hashes", hashes); Json::writeString(body_obj, "algorithm", hash_format); if (loaders.has_value()) Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders.value())); if (mcVersions.has_value()) { QStringList game_versions; for (auto& ver : mcVersions.value()) { game_versions.append(mapMCVersionToModrinth(ver)); } Json::writeStringList(body_obj, "game_versions", game_versions); } QJsonDocument body(body_obj); auto body_raw = body.toJson(); netJob->addNetAction( Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); return netJob; } Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); return netJob; } QList ModrinthAPI::getSortingMethods() const { // https://docs.modrinth.com/api-spec/#tag/projects/operation/searchProjects return { { 1, "relevance", QObject::tr("Sort by Relevance") }, { 2, "downloads", QObject::tr("Sort by Downloads") }, { 3, "follows", QObject::tr("Sort by Follows") }, { 4, "newest", QObject::tr("Sort by Newest") }, { 5, "updated", QObject::tr("Sort by Last Updated") } }; } Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetCategories"), APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(BuildConfig.MODRINTH_PROD_URL + "/tag/category"), response)); QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Modrinth failed to get categories:" << msg; }); return netJob; } QList ModrinthAPI::loadCategories(std::shared_ptr response, QString projectType) { QList categories; QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from categories at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return categories; } try { auto arr = Json::requireArray(doc); for (auto val : arr) { auto cat = Json::requireObject(val); auto name = Json::requireString(cat, "name"); if (cat["project_type"].toString() == projectType) categories.push_back({ name, name }); } } catch (Json::JsonException& e) { qCritical() << "Failed to parse response from a version request."; qCritical() << e.what(); qDebug() << doc; } return categories; } QList ModrinthAPI::loadModCategories(std::shared_ptr response) { return loadCategories(response, "mod"); }; PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthPackIndex.h0000644000175100017510000000207615144136756025475 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "modplatform/ModIndex.h" #include "BaseInstance.h" namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; } // namespace Modrinth PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthPackExportTask.h0000644000175100017510000000442415144136756026531 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "BaseInstance.h" #include "MMCZip.h" #include "minecraft/MinecraftInstance.h" #include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/Task.h" class ModrinthPackExportTask : public Task { Q_OBJECT public: ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFileFunction filter); protected: void executeTask() override; bool abort() override; private: struct ResolvedFile { QString sha1, sha512, url; qint64 size; ModPlatform::Side side; }; static const QStringList PREFIXES; static const QStringList FILE_EXTENSIONS; // inputs const QString name, version, summary; const bool optionalFiles; const InstancePtr instance; MinecraftInstance* mcInstance; const QDir gameRoot; const QString output; const MMCZip::FilterFileFunction filter; ModrinthAPI api; QFileInfoList files; QMap pendingHashes; QMap resolvedFiles; Task::Ptr task; void collectFiles(); void collectHashes(); void makeApiRequest(); void parseApiResponse(std::shared_ptr response); void buildZip(); QByteArray generateIndex(); }; PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp0000644000175100017510000002664015144136756027070 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ModrinthPackExportTask.h" #include #include #include #include #include #include "Json.h" #include "MMCZip.h" #include "archive/ExportToZipTask.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/Task.h" const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" }); const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" }); ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFileFunction filter) : name(name) , version(version) , summary(summary) , optionalFiles(optionalFiles) , instance(instance) , mcInstance(dynamic_cast(instance.get())) , gameRoot(instance->gameRoot()) , output(output) , filter(filter) {} void ModrinthPackExportTask::executeTask() { setStatus(tr("Searching for files...")); setProgress(0, 0); collectFiles(); } bool ModrinthPackExportTask::abort() { if (task) { task->abort(); return true; } return false; } void ModrinthPackExportTask::collectFiles() { setAbortable(false); QCoreApplication::processEvents(); files.clear(); if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) { emitFailed(tr("Could not search for files")); return; } pendingHashes.clear(); resolvedFiles.clear(); if (mcInstance) { mcInstance->loaderModList()->update(); connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &ModrinthPackExportTask::collectHashes); } else collectHashes(); } void ModrinthPackExportTask::collectHashes() { setStatus(tr("Finding file hashes...")); for (const QFileInfo& file : files) { QCoreApplication::processEvents(); const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); // require sensible file types if (!std::any_of(PREFIXES.begin(), PREFIXES.end(), [&relative](const QString& prefix) { return relative.startsWith(prefix); })) continue; if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); })) continue; QFile openFile(file.absoluteFilePath()); if (!openFile.open(QFile::ReadOnly)) { qWarning() << "Could not open" << file << "for hashing"; continue; } const QByteArray data = openFile.readAll(); if (openFile.error() != QFileDevice::NoError) { qWarning() << "Could not read" << file; continue; } auto sha512 = Hashing::hash(data, Hashing::Algorithm::Sha512); auto allMods = mcInstance->loaderModList()->allMods(); if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); modIter != allMods.end()) { const Mod* mod = *modIter; if (mod->metadata() != nullptr) { const QUrl& url = mod->metadata()->url; // ensure the url is permitted on modrinth.com if (!url.isEmpty() && BuildConfig.MODRINTH_MRPACK_HOSTS.contains(url.host())) { qDebug() << "Resolving" << relative << "from index"; auto sha1 = Hashing::hash(data, Hashing::Algorithm::Sha1); ResolvedFile resolvedFile{ sha1, sha512, url.toEncoded(), openFile.size(), mod->metadata()->side }; resolvedFiles[relative] = resolvedFile; // nice! we've managed to resolve based on local metadata! // no need to enqueue it continue; } } } qDebug() << "Enqueueing" << relative << "for Modrinth query"; pendingHashes[relative] = sha512; } setAbortable(true); makeApiRequest(); } void ModrinthPackExportTask::makeApiRequest() { if (pendingHashes.isEmpty()) buildZip(); else { setStatus(tr("Finding versions for hashes...")); auto response = std::make_shared(); task = api.currentVersions(pendingHashes.values(), "sha512", response); connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); task->start(); } } void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr response) { task = nullptr; try { const QJsonDocument doc = Json::requireDocument(*response); QMapIterator iterator(pendingHashes); while (iterator.hasNext()) { iterator.next(); const QJsonObject obj = doc[iterator.value()].toObject(); if (obj.isEmpty()) continue; const QJsonArray files_array = obj["files"].toArray(); if (auto fileIter = std::find_if(files_array.begin(), files_array.end(), [&iterator](const QJsonValue& file) { return file["hashes"]["sha512"] == iterator.value(); }); fileIter != files_array.end()) { // map the file to the url resolvedFiles[iterator.key()] = ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(), iterator.value(), fileIter->toObject()["url"].toString(), fileIter->toObject()["size"].toInt() }; } } } catch (const Json::JsonException& e) { emitFailed(tr("Failed to parse versions response: %1").arg(e.what())); return; } pendingHashes.clear(); buildZip(); } void ModrinthPackExportTask::buildZip() { setStatus(tr("Adding files...")); auto zipTask = makeShared(output, gameRoot, files, "overrides/", true); zipTask->addExtraFile("modrinth.index.json", generateIndex()); zipTask->setExcludeFiles(resolvedFiles.keys()); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(zipTask.get(), &Task::succeeded, this, &ModrinthPackExportTask::emitSucceeded); connect(zipTask.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(zipTask.get(), &Task::stepProgress, this, &ModrinthPackExportTask::propagateStepProgress); connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); task.reset(zipTask); zipTask->start(); } QByteArray ModrinthPackExportTask::generateIndex() { QJsonObject out; out["formatVersion"] = 1; out["game"] = "minecraft"; out["name"] = name; out["versionId"] = version; if (!summary.isEmpty()) out["summary"] = summary; if (mcInstance) { auto profile = mcInstance->getPackProfile(); // collect all supported components const ComponentPtr minecraft = profile->getComponent("net.minecraft"); const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); const ComponentPtr forge = profile->getComponent("net.minecraftforge"); const ComponentPtr neoForge = profile->getComponent("net.neoforged"); // convert all available components to mrpack dependencies QJsonObject dependencies; if (minecraft != nullptr) dependencies["minecraft"] = minecraft->m_version; if (quilt != nullptr) dependencies["quilt-loader"] = quilt->m_version; if (fabric != nullptr) dependencies["fabric-loader"] = fabric->m_version; if (forge != nullptr) dependencies["forge"] = forge->m_version; if (neoForge != nullptr) dependencies["neoforge"] = neoForge->m_version; out["dependencies"] = dependencies; } QJsonArray filesOut; for (auto iterator = resolvedFiles.constBegin(); iterator != resolvedFiles.constEnd(); iterator++) { QJsonObject fileOut; QString path = iterator.key(); const ResolvedFile& value = iterator.value(); QJsonObject env; // detect disabled mod const QFileInfo pathInfo(path); if (optionalFiles && pathInfo.suffix() == "disabled") { // rename it path = pathInfo.dir().filePath(pathInfo.completeBaseName()); env["client"] = "optional"; env["server"] = "optional"; } else { env["client"] = "required"; env["server"] = "required"; } // a server side mod does not imply that the mod does not work on the client // however, if a mrpack mod is marked as server-only it will not install on the client if (iterator->side == ModPlatform::Side::ClientSide) env["server"] = "unsupported"; fileOut["env"] = env; fileOut["path"] = path; fileOut["downloads"] = QJsonArray{ iterator->url }; QJsonObject hashes; hashes["sha1"] = value.sha1; hashes["sha512"] = value.sha512; fileOut["hashes"] = hashes; fileOut["fileSize"] = value.size; filesOut << fileOut; } out["files"] = filesOut; return QJsonDocument(out).toJson(QJsonDocument::Compact); } PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h0000644000175100017510000000321515144136756027677 0ustar runnerrunner#pragma once #include #include #include #include #include #include #include #include "BaseInstance.h" #include "InstanceCreationTask.h" class ModrinthCreationTask final : public InstanceCreationTask { Q_OBJECT struct File { QString path; QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; QQueue downloads; bool required = true; }; public: ModrinthCreationTask(QString staging_path, SettingsObjectPtr global_settings, QWidget* parent, QString id, QString version_id = {}, QString original_instance_id = {}) : InstanceCreationTask(), m_parent(parent), m_managed_id(std::move(id)), m_managed_version_id(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); m_original_instance_id = std::move(original_instance_id); } bool abort() override; bool updateInstance() override; bool createInstance() override; private: bool parseManifest(const QString&, std::vector&, bool set_internal_data = true, bool show_optional_dialog = true); private: QWidget* m_parent = nullptr; QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version; QString m_managed_id, m_managed_version_id, m_managed_name; std::vector m_files; Task::Ptr m_task; std::optional m_instance; QString m_root_path = "minecraft"; }; PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthPackIndex.cpp0000644000175100017510000002054515144136756026031 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ModrinthPackIndex.h" #include "FileSystem.h" #include "ModrinthAPI.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/ModIndex.h" static ModrinthAPI api; bool shouldDownloadOnSide(QString side) { return side == "required" || side == "optional"; } // https://docs.modrinth.com/api/operations/getproject/ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = obj["project_id"].toString(); if (pack.addonId.toString().isEmpty()) pack.addonId = Json::requireString(obj, "id"); pack.provider = ModPlatform::ResourceProvider::MODRINTH; pack.name = Json::requireString(obj, "title"); pack.slug = obj["slug"].toString(""); if (!pack.slug.isEmpty()) pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug; else pack.websiteUrl = ""; pack.description = obj["description"].toString(""); pack.logoUrl = obj["icon_url"].toString(""); pack.logoName = QString("%1.%2").arg(obj["slug"].toString(), QFileInfo(QUrl(pack.logoUrl).fileName()).suffix()); if (obj.contains("author")) { ModPlatform::ModpackAuthor modAuthor; modAuthor.name = obj["author"].toString(); modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors = { modAuthor }; } auto client = shouldDownloadOnSide(obj["client_side"].toString()); auto server = shouldDownloadOnSide(obj["server_side"].toString()); if (server && client) { pack.side = ModPlatform::Side::UniversalSide; } else if (server) { pack.side = ModPlatform::Side::ServerSide; } else if (client) { pack.side = ModPlatform::Side::ClientSide; } // Modrinth can have more data than what's provided by the basic search :) pack.extraDataLoaded = false; } void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.extraData.issuesUrl = obj["issues_url"].toString(); if (pack.extraData.issuesUrl.endsWith('/')) pack.extraData.issuesUrl.chop(1); pack.extraData.sourceUrl = obj["source_url"].toString(); if (pack.extraData.sourceUrl.endsWith('/')) pack.extraData.sourceUrl.chop(1); pack.extraData.wikiUrl = obj["wiki_url"].toString(); if (pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); pack.extraData.discordUrl = obj["discord_url"].toString(); if (pack.extraData.discordUrl.endsWith('/')) pack.extraData.discordUrl.chop(1); auto donate_arr = obj["donation_urls"].toArray(); for (auto d : donate_arr) { auto d_obj = Json::requireObject(d); ModPlatform::DonationData donate; donate.id = d_obj["id"].toString(); donate.platform = d_obj["platform"].toString(); donate.url = d_obj["url"].toString(); pack.extraData.donate.append(donate); } pack.extraData.status = obj["status"].toString(); pack.extraData.body = obj["body"].toString().remove("
    "); pack.extraDataLoaded = true; } ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name) { ModPlatform::IndexedVersion file; file.addonId = Json::requireString(obj, "project_id"); file.fileId = Json::requireString(obj, "id"); file.date = Json::requireString(obj, "date_published"); auto versionArray = Json::requireArray(obj, "game_versions"); if (versionArray.empty()) { return {}; } for (auto mcVer : versionArray) { file.mcVersion.append({ ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString()), mcVer.toString() }); // double this so we can check both strings when filtering } auto loaders = Json::requireArray(obj, "loaders"); for (auto loader : loaders) { if (loader == "neoforge") file.loaders |= ModPlatform::NeoForge; else if (loader == "forge") file.loaders |= ModPlatform::Forge; else if (loader == "cauldron") file.loaders |= ModPlatform::Cauldron; else if (loader == "liteloader") file.loaders |= ModPlatform::LiteLoader; else if (loader == "fabric") file.loaders |= ModPlatform::Fabric; else if (loader == "quilt") file.loaders |= ModPlatform::Quilt; } file.version = Json::requireString(obj, "name"); file.version_number = Json::requireString(obj, "version_number"); file.version_type = ModPlatform::IndexedVersionType::fromString(Json::requireString(obj, "version_type")); file.changelog = Json::requireString(obj, "changelog"); auto dependencies = obj["dependencies"].toArray(); for (auto d : dependencies) { auto dep = d.toObject(); ModPlatform::Dependency dependency; dependency.addonId = dep["project_id"].toString(); dependency.version = dep["version_id"].toString(); auto depType = Json::requireString(dep, "dependency_type"); if (depType == "required") dependency.type = ModPlatform::DependencyType::REQUIRED; else if (depType == "optional") dependency.type = ModPlatform::DependencyType::OPTIONAL; else if (depType == "incompatible") dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; else if (depType == "embedded") dependency.type = ModPlatform::DependencyType::EMBEDDED; else dependency.type = ModPlatform::DependencyType::UNKNOWN; file.dependencies.append(dependency); } auto files = Json::requireArray(obj, "files"); int i = 0; if (files.empty()) { // This should not happen normally, but check just in case qWarning() << "Modrinth returned an unexpected empty list of files:" << obj; return {}; } // Find correct file (needed in cases where one version may have multiple files) // Will default to the last one if there's no primary (though I think Modrinth requires that // at least one file is primary, idk) // NOTE: files.count() is 1-indexed, so we need to subtract 1 to become 0-indexed while (i < files.count() - 1) { auto parent = files[i].toObject(); auto fileName = Json::requireString(parent, "filename"); if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) { file.is_preferred = true; break; } // Grab the primary file, if available if (Json::requireBoolean(parent, "primary")) break; i++; } auto parent = files[i].toObject(); if (parent.contains("url")) { file.downloadUrl = Json::requireString(parent, "url"); file.fileName = Json::requireString(parent, "filename"); file.fileName = FS::RemoveInvalidPathChars(file.fileName); file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1); auto hash_list = Json::requireObject(parent, "hashes"); if (hash_list.contains(preferred_hash_type)) { file.hash = Json::requireString(hash_list, preferred_hash_type); file.hash_type = preferred_hash_type; } else { auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH); for (auto& hash_type : hash_types) { if (hash_list.contains(hash_type)) { file.hash = Json::requireString(hash_list, hash_type); file.hash_type = hash_type; break; } } } return file; } return {}; } PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp0000644000175100017510000002206515144136756026342 0ustar runnerrunner#include "ModrinthCheckUpdate.h" #include "Application.h" #include "ModrinthAPI.h" #include "ModrinthPackIndex.h" #include "Json.h" #include "QObjectPtr.h" #include "ResourceDownloadTask.h" #include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" static ModrinthAPI api; ModrinthCheckUpdate::ModrinthCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, std::shared_ptr resourceModel) : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) , m_hashType(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) { if (!m_loadersList.isEmpty()) { // this is for mods so append all the other posible loaders to the initial list m_initialSize = m_loadersList.length(); ModPlatform::ModLoaderTypes modLoaders; for (auto m : resources) { modLoaders |= m->metadata()->loaders; } for (auto l : m_loadersList) { modLoaders &= ~l; } m_loadersList.append(ModPlatform::modLoaderTypesToList(modLoaders)); } } bool ModrinthCheckUpdate::abort() { if (m_job) return m_job->abort(); return true; } /* Check for update: * - Get latest version available * - Compare hash of the latest version with the current hash * - If equal, no updates, else, there's updates, so add to the list * */ void ModrinthCheckUpdate::executeTask() { setStatus(tr("Preparing resources for Modrinth...")); setProgress(0, (m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2 + 1); auto hashing_task = makeShared("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); bool startHasing = false; for (auto* resource : m_resources) { auto hash = resource->metadata()->hash; // Sadly the API can only handle one hash type per call, se we // need to generate a new hash if the current one is innadequate // (though it will rarely happen, if at all) if (resource->metadata()->hash_format != m_hashType) { auto hash_task = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_mappings.insert(hash, resource); }); connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); hashing_task->addTask(hash_task); startHasing = true; } else { m_mappings.insert(hash, resource); } } if (startHasing) { connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); m_job = hashing_task; hashing_task->start(); } else { checkNextLoader(); } } void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional loader, bool forceModLoaderCheck) { m_loaderIdx++; setStatus(tr("Waiting for the API response from Modrinth...")); setProgress(m_progress + 1, m_progressTotal); auto response = std::make_shared(); QStringList hashes; if (forceModLoaderCheck && loader.has_value()) { for (auto hash : m_mappings.keys()) { if (m_mappings.value(hash)->metadata()->loaders & loader.value()) { hashes.append(hash); } } } else { hashes = m_mappings.keys(); } if (hashes.isEmpty()) { checkNextLoader(); return; } auto job = api.latestVersions(hashes, m_hashType, m_gameVersions, loader, response); connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response, loader); }); connect(job.get(), &Task::failed, this, &ModrinthCheckUpdate::checkNextLoader); m_job = job; job->start(); } void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr response, std::optional loader) { setStatus(tr("Parsing the API response from Modrinth...")); setProgress(m_progress + 1, m_progressTotal); QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; emitFailed(parse_error.errorString()); return; } try { auto iter = m_mappings.begin(); while (iter != m_mappings.end()) { const QString hash = iter.key(); Resource* resource = iter.value(); auto project_obj = doc[hash].toObject(); // If the returned project is empty, but we have Modrinth metadata, // it means this specific version is not available if (project_obj.isEmpty()) { qDebug() << "Mod" << m_mappings.find(hash).value()->name() << "got an empty response. Hash:" << hash; ++iter; continue; } // Sometimes a version may have multiple files, one with "forge" and one with "fabric", // so we may want to filter it QString loader_filter; if (loader.has_value()) { for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) { loader_filter = ModPlatform::getModLoaderAsString(flag); break; } } // Currently, we rely on a couple heuristics to determine whether an update is actually available or not: // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the // loader_filter // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) // Such is the pain of having arbitrary files for a given version .-. auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hashType, loader_filter); if (project_ver.downloadUrl.isEmpty()) { qCritical() << "Modrinth mod without download url!" << project_ver.fileName; ++iter; continue; } // Fake pack with the necessary info to pass to the download task :) auto pack = std::make_shared(); pack->name = resource->name(); pack->slug = resource->metadata()->slug; pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::MODRINTH; if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { auto download_task = makeShared(pack, project_ver, m_resourceModel); QString old_version = resource->metadata()->version_number; if (old_version.isEmpty()) { if (resource->status() == ResourceStatus::NOT_INSTALLED) old_version = tr("Not installed"); else old_version = tr("Unknown"); } m_updates.emplace_back(pack->name, hash, old_version, project_ver.version_number, project_ver.version_type, project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task, resource->enabled()); } m_deps.append(std::make_shared(pack, project_ver)); iter = m_mappings.erase(iter); } } catch (Json::JsonException& e) { emitFailed(e.cause() + ": " + e.what()); return; } checkNextLoader(); } void ModrinthCheckUpdate::checkNextLoader() { if (m_mappings.isEmpty()) { emitSucceeded(); return; } if (m_loaderIdx < m_loadersList.size()) { // this are mods so check with loades getUpdateModsForLoader(m_loadersList.at(m_loaderIdx), m_loaderIdx > m_initialSize); return; } else if (m_loadersList.isEmpty() && m_loaderIdx == 0) { // this are other resources no need to check more than once with empty loader getUpdateModsForLoader(); return; } for (auto resource : m_mappings) { QString reason; if (dynamic_cast(resource) != nullptr) reason = tr("No valid version found for this resource. It's probably unavailable for the current game " "version / mod loader."); else reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); emit checkFailed(resource, reason); } emitSucceeded(); }PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthAPI.h0000644000175100017510000002333415144136756024240 0ustar runnerrunner// SPDX-FileCopyrightText: 2022-2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "BuildConfig.h" #include "Json.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" #include class ModrinthAPI : public ResourceAPI { public: Task::Ptr currentVersion(QString hash, QString hash_format, std::shared_ptr response); Task::Ptr currentVersions(const QStringList& hashes, QString hash_format, std::shared_ptr response); Task::Ptr latestVersion(QString hash, QString hash_format, std::optional> mcVersions, std::optional loaders, std::shared_ptr response); Task::Ptr latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, std::optional loaders, std::shared_ptr response); Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; static Task::Ptr getModCategories(std::shared_ptr response); static QList loadCategories(std::shared_ptr response, QString projectType); static QList loadModCategories(std::shared_ptr response); public: auto getSortingMethods() const -> QList override; inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList { QStringList l; for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader, ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA, ModPlatform::LegacyFabric, ModPlatform::Ornithe, ModPlatform::Rift }) { if (types & loader) { l << getModLoaderAsString(loader); } } return l; } static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString { QStringList l; for (auto loader : getModLoaderStrings(types)) { l << QString("\"categories:%1\"").arg(loader); } return l.join(','); } static auto getCategoriesFilters(QStringList categories) -> const QString { QStringList l; for (auto cat : categories) { l << QString("\"categories:%1\"").arg(cat); } return l.join(','); } static QString getSideFilters(ModPlatform::Side side) { switch (side) { case ModPlatform::Side::ClientSide: return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\""); case ModPlatform::Side::ServerSide: return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\""); case ModPlatform::Side::UniversalSide: return QString("\"client_side:required\"],[\"server_side:required\""); case ModPlatform::Side::NoSide: // fallthrough default: return {}; } } static inline QString mapMCVersionFromModrinth(QString v) { static const QString preString = " Pre-Release "; bool pre = false; if (v.contains("-pre")) { pre = true; v.replace("-pre", preString); } v.replace("-", " "); if (pre) { v.replace(" Pre Release ", preString); } return v; } private: static QString resourceTypeParameter(ModPlatform::ResourceType type) { switch (type) { case ModPlatform::ResourceType::Mod: return "mod"; case ModPlatform::ResourceType::ResourcePack: return "resourcepack"; case ModPlatform::ResourceType::ShaderPack: return "shader"; case ModPlatform::ResourceType::DataPack: return "datapack"; case ModPlatform::ResourceType::Modpack: return "modpack"; default: qWarning() << "Invalid resource type for Modrinth API!"; break; } return ""; } QString createFacets(SearchArgs const& args) const { QStringList facets_list; if (args.loaders.has_value() && args.loaders.value() != 0) facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); if (args.versions.has_value() && !args.versions.value().empty()) facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); if (args.side.has_value()) { auto side = getSideFilters(args.side.value()); if (!side.isEmpty()) facets_list.append(QString("[%1]").arg(side)); } if (args.categoryIds.has_value() && !args.categoryIds->empty()) facets_list.append(QString("[%1]").arg(getCategoriesFilters(args.categoryIds.value()))); if (args.openSource) facets_list.append("[\"open_source:true\"]"); facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); return QString("[%1]").arg(facets_list.join(',')); } public: inline auto getSearchURL(SearchArgs const& args) const -> std::optional override { if (args.loaders.has_value() && args.loaders.value() != 0) { if (!validateModLoaders(args.loaders.value())) { qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!"; return {}; } } QStringList get_arguments; get_arguments.append(QString("offset=%1").arg(args.offset)); get_arguments.append(QString("limit=25")); if (args.search.has_value()) get_arguments.append(QString("query=%1").arg(args.search.value())); if (args.sorting.has_value()) get_arguments.append(QString("index=%1").arg(args.sorting.value().name)); get_arguments.append(QString("facets=%1").arg(createFacets(args))); return BuildConfig.MODRINTH_PROD_URL + "/search?" + get_arguments.join('&'); }; inline auto getInfoURL(QString const& id) const -> std::optional override { return BuildConfig.MODRINTH_PROD_URL + "/project/" + id; }; inline auto getMultipleModInfoURL(QStringList ids) const -> QString { return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\"")); }; inline auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional override { QStringList get_arguments; if (args.mcVersions.has_value()) get_arguments.append(QString("game_versions=[%1]").arg(getGameVersionsString(args.mcVersions.value()))); if (args.loaders.has_value()) get_arguments.append(QString("loaders=[\"%1\"]").arg(getModLoaderStrings(args.loaders.value()).join("\",\""))); return QString("%1/project/%2/version%3%4") .arg(BuildConfig.MODRINTH_PROD_URL, args.pack->addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; QString getGameVersionsArray(std::list mcVersions) const { QString s; for (auto& ver : mcVersions) { s += QString("\"versions:%1\",").arg(mapMCVersionToModrinth(ver)); } s.remove(s.length() - 1, 1); // remove last comma return s.isEmpty() ? QString() : s; } static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric | ModPlatform::Ornithe | ModPlatform::Rift); } std::optional getDependencyURL(DependencySearchArgs const& args) const override { return args.dependency.version.length() != 0 ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version) : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]") .arg(BuildConfig.MODRINTH_PROD_URL) .arg(args.dependency.addonId.toString()) .arg(mapMCVersionToModrinth(args.mcVersion)) .arg(getModLoaderStrings(args.loader).join("\",\"")); }; QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object().value("hits").toArray(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadIndexedPack(m, obj); } ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const override { return Modrinth::loadIndexedPackVersion(obj); }; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadExtraPackData(m, obj); } }; PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthCheckUpdate.h0000644000175100017510000000163715144136756026011 0ustar runnerrunner#pragma once #include "modplatform/CheckUpdateTask.h" class ModrinthCheckUpdate : public CheckUpdateTask { Q_OBJECT public: ModrinthCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, std::shared_ptr resourceModel); public slots: bool abort() override; protected slots: void executeTask() override; void getUpdateModsForLoader(std::optional loader = {}, bool forceModLoaderCheck = false); void checkVersionsResponse(std::shared_ptr response, std::optional loader); void checkNextLoader(); private: Task::Ptr m_job = nullptr; QHash m_mappings; QString m_hashType; int m_loaderIdx = 0; int m_initialSize = 0; }; PrismLauncher-10.0.5/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp0000644000175100017510000004714215144136756030241 0ustar runnerrunner#include "ModrinthInstanceCreationTask.h" #include "Application.h" #include "FileSystem.h" #include "InstanceList.h" #include "Json.h" #include "QObjectPtr.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/Mod.h" #include "modplatform/EnsureMetadataTask.h" #include "modplatform/helpers/OverrideUtils.h" #include "net/ChecksumValidator.h" #include "net/ApiDownload.h" #include "net/NetJob.h" #include "settings/INISettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/pages/modplatform/OptionalModDialog.h" #include #include #include #include bool ModrinthCreationTask::abort() { if (!canAbort()) return false; m_abort = true; if (m_task) m_task->abort(); return Task::abort(); } bool ModrinthCreationTask::updateInstance() { auto instance_list = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? InstancePtr inst; if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { inst = instance_list->getInstanceById(original_id); Q_ASSERT(inst); } else { inst = instance_list->getInstanceByManagedName(originalName()); if (!inst) { inst = instance_list->getInstanceById(originalName()); if (!inst) return false; } } QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); if (!parseManifest(index_path, m_files, true, false)) return false; auto version_name = inst->getManagedPackVersionName(); m_root_path = QFileInfo(inst->gameRoot()).fileName(); auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; if (shouldConfirmUpdate()) { auto should_update = askIfShouldUpdate(m_parent, version_str); if (should_update == ShouldUpdate::SkipUpdating) return false; if (should_update == ShouldUpdate::Cancel) { m_abort = true; return false; } } // Remove repeated files, we don't need to download them! QDir old_inst_dir(inst->instanceRoot()); QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "mrpack")); QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json")); QFileInfo old_index_file(old_index_path); if (old_index_file.exists()) { std::vector old_files; parseManifest(old_index_path, old_files, false, false); // Let's remove all duplicated, identical resources! auto files_iterator = m_files.begin(); begin: while (files_iterator != m_files.end()) { auto const& file = *files_iterator; auto old_files_iterator = old_files.begin(); while (old_files_iterator != old_files.end()) { auto const& old_file = *old_files_iterator; if (old_file.hash == file.hash) { qDebug() << "Removed file at" << file.path << "from list of downloads"; files_iterator = m_files.erase(files_iterator); old_files_iterator = old_files.erase(old_files_iterator); goto begin; // Sorry :c } old_files_iterator++; } files_iterator++; } QDir old_minecraft_dir(inst->gameRoot()); // Some files were removed from the old version, and some will be downloaded in an updated version, // so we're fine removing them! if (!old_files.empty()) { for (auto const& file : old_files) { if (file.path.isEmpty()) continue; qDebug() << "Scheduling" << file.path << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path)); if (file.path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path.chopped(9))); } else { m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(file.path + ".disabled")); } } } // We will remove all the previous overrides, to prevent duplicate files! // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? // FIXME: We may want to do something about disabled mods. auto old_overrides = Override::readOverrides("overrides", old_index_folder); for (const auto& entry : old_overrides) { if (entry.isEmpty()) continue; qDebug() << "Scheduling" << entry << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); } auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder); for (const auto& entry : old_client_overrides) { if (entry.isEmpty()) continue; qDebug() << "Scheduling" << entry << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); } } else { // We don't have an old index file, so we may duplicate stuff! auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), tr("We couldn't find a suitable index file for the older version. This may cause some " "of the files to be duplicated. Do you want to continue?"), QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); if (dialog->exec() == QDialog::DialogCode::Rejected) { m_abort = true; return false; } } setOverride(true, inst->id()); qDebug() << "Will override instance!"; m_instance = inst; // We let it go through the createInstance() stage, just with a couple modifications for updating return false; } // https://docs.modrinth.com/docs/modpacks/format_definition/ bool ModrinthCreationTask::createInstance() { QEventLoop loop; QString parent_folder(FS::PathCombine(m_stagingPath, "mrpack")); QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); if (m_files.empty() && !parseManifest(index_path, m_files, true, true)) return false; // Keep index file in case we need it some other time (like when changing versions) QString new_index_place(FS::PathCombine(parent_folder, "modrinth.index.json")); FS::ensureFilePathExists(new_index_place); FS::move(index_path, new_index_place); auto mcPath = FS::PathCombine(m_stagingPath, m_root_path); auto override_path = FS::PathCombine(m_stagingPath, "overrides"); if (QFile::exists(override_path)) { // Create a list of overrides in "overrides.txt" inside mrpack/ Override::createOverrides("overrides", parent_folder, override_path); // Apply the overrides if (!FS::move(override_path, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + "overrides"); return false; } } // Do client overrides auto client_override_path = FS::PathCombine(m_stagingPath, "client-overrides"); if (QFile::exists(client_override_path)) { // Create a list of overrides in "client-overrides.txt" inside mrpack/ Override::createOverrides("client-overrides", parent_folder, client_override_path); // Apply the overrides if (!FS::overrideFolder(mcPath, client_override_path)) { setError(tr("Could not rename the client overrides folder:\n") + "client overrides"); return false; } } QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", m_minecraft_version, true); if (!m_fabric_version.isEmpty()) components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version); if (!m_quilt_version.isEmpty()) components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version); if (!m_forge_version.isEmpty()) components->setComponentVersion("net.minecraftforge", m_forge_version); if (!m_neoForge_version.isEmpty()) components->setComponentVersion("net.neoforged", m_neoForge_version); if (m_instIcon != "default") { instance.setIconKey(m_instIcon); } else if (!m_managed_id.isEmpty()) { instance.setIconKey("modrinth"); } // Don't add managed info to packs without an ID (most likely imported from ZIP) if (!m_managed_id.isEmpty()) instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); else instance.setManagedPack("modrinth", "", name(), "", ""); instance.setName(name()); instance.saveNow(); auto downloadMods = makeShared(tr("Mod Download Modrinth"), APPLICATION->network()); auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); // TODO make this work with other sorts of resource QHash resources; for (auto& file : m_files) { auto fileName = file.path; fileName = FS::RemoveInvalidPathChars(fileName); auto file_path = FS::PathCombine(root_modpack_path, fileName); if (!root_modpack_url.isParentOf(QUrl::fromLocalFile(file_path))) { // This means we somehow got out of the root folder, so abort here to prevent exploits setError(tr("One of the files has a path that leads to an arbitrary location (%1). This is a security risk and isn't allowed.") .arg(fileName)); return false; } if (fileName.startsWith("mods/")) { auto mod = new Mod(file_path); ModDetails d; d.mod_id = file_path; mod->setDetails(d); resources[file.hash.toHex()] = mod; } if (file.downloads.empty()) { setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName)); return false; } qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); downloadMods->addNetAction(dl); if (!file.downloads.empty()) { // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) auto param = dl.toWeakRef(); connect(dl.get(), &Task::failed, [&file, file_path, param, downloadMods] { auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); downloadMods->addNetAction(ndl); if (auto shared = param.lock()) shared->succeeded(); }); } } bool ended_well = false; connect(downloadMods.get(), &NetJob::succeeded, this, [&ended_well]() { ended_well = true; }); connect(downloadMods.get(), &NetJob::failed, [this, &ended_well](const QString& reason) { ended_well = false; setError(reason); }); connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); connect(downloadMods.get(), &NetJob::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); downloadMods->start(); m_task = downloadMods; loop.exec(); if (!ended_well) { for (auto resource : resources) { delete resource; } return ended_well; } QEventLoop ensureMetaLoop; QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); auto ensureMetadataTask = makeShared(resources, folder, ModPlatform::ResourceProvider::MODRINTH); connect(ensureMetadataTask.get(), &Task::succeeded, this, [&ended_well]() { ended_well = true; }); connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); connect(ensureMetadataTask.get(), &Task::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); connect(ensureMetadataTask.get(), &Task::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); ensureMetadataTask->start(); m_task = ensureMetadataTask; ensureMetaLoop.exec(); for (auto resource : resources) { delete resource; } resources.clear(); // Update information of the already installed instance, if any. if (m_instance && ended_well) { setAbortable(false); auto inst = m_instance.value(); // Only change the name if it didn't use a custom name, so that the previous custom name // is preserved, but if we're using the original one, we update the version string. // NOTE: This needs to come before the copyManagedPack call! if (inst->name().contains(inst->getManagedPackVersionName()) && inst->name() != instance.name()) { if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange) inst->setName(instance.name()); } inst->copyManagedPack(instance); } return ended_well; } bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector& files, bool set_internal_data, bool show_optional_dialog) { try { auto doc = Json::requireDocument(index_path); auto obj = Json::requireObject(doc, "modrinth.index.json"); int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); if (formatVersion == 1) { auto game = Json::requireString(obj, "game", "modrinth.index.json"); if (game != "minecraft") { throw JSONValidationError("Unknown game: " + game); } if (set_internal_data) { if (m_managed_version_id.isEmpty()) m_managed_version_id = obj["versionId"].toString(); m_managed_name = obj["name"].toString(); } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); std::vector optionalFiles; for (const auto& modInfo : jsonFiles) { File file; file.path = Json::requireString(modInfo, "path").replace("\\", "/"); auto env = modInfo["env"].toObject(); // 'env' field is optional if (!env.isEmpty()) { QString support = env["client"].toString("unsupported"); if (support == "unsupported") { continue; } else if (support == "optional") { file.required = false; } } QJsonObject hashes = Json::requireObject(modInfo, "hashes"); file.hash = QByteArray::fromHex(Json::requireString(hashes, "sha512").toLatin1()); file.hashAlgorithm = QCryptographicHash::Sha512; // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) auto download_arr = modInfo["downloads"].toArray(); for (auto download : download_arr) { qWarning() << download.toString(); bool is_last = download.toString() == download_arr.last().toString(); auto download_url = QUrl(download.toString()); if (!download_url.isValid()) { qDebug() << QString("Download URL (%1) for %2 is not a correctly formatted URL").arg(download_url.toString(), file.path); if (is_last && file.downloads.isEmpty()) throw JSONValidationError(tr("Download URL for %1 is not a correctly formatted URL").arg(file.path)); } else { file.downloads.push_back(download_url); } } (file.required ? files : optionalFiles).push_back(file); } if (!optionalFiles.empty()) { if (show_optional_dialog) { QStringList oFiles; for (auto file : optionalFiles) oFiles.push_back(file.path); OptionalModDialog optionalModDialog(m_parent, oFiles); if (optionalModDialog.exec() == QDialog::Rejected) { emitAborted(); return false; } auto selectedMods = optionalModDialog.getResult(); for (auto file : optionalFiles) { if (selectedMods.contains(file.path)) { file.required = true; } else { file.path += ".disabled"; } files.push_back(file); } } else { for (auto file : optionalFiles) { file.path += ".disabled"; files.push_back(file); } } } if (set_internal_data) { auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); if (name == "minecraft") { m_minecraft_version = Json::requireString(*it, "Minecraft version"); } else if (name == "fabric-loader") { m_fabric_version = Json::requireString(*it, "Fabric Loader version"); } else if (name == "quilt-loader") { m_quilt_version = Json::requireString(*it, "Quilt Loader version"); } else if (name == "forge") { m_forge_version = Json::requireString(*it, "Forge version"); } else if (name == "neoforge") { m_neoForge_version = Json::requireString(*it, "NeoForge version"); } else { throw JSONValidationError("Unknown dependency type: " + name); } } } } else { throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); } } catch (const JSONValidationError& e) { setError(tr("Could not understand pack index:\n") + e.cause()); return false; } return true; } PrismLauncher-10.0.5/launcher/modplatform/technic/0000755000175100017510000000000015144136756021535 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/technic/TechnicPackProcessor.cpp0000644000175100017510000002143015144136756026315 0ustar runnerrunner/* Copyright 2020-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "TechnicPackProcessor.h" #include #include #include #include #include #include #include "archive/ArchiveReader.h" void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString& instName, const QString& instIcon, const QString& stagingPath, const QString& minecraftVersion, [[maybe_unused]] const bool isSolder) { QString minecraftPath = FS::PathCombine(stagingPath, "minecraft"); QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); instance.setName(instName); if (instIcon != "default") { instance.setIconKey(instIcon); } auto components = instance.getPackProfile(); components->buildingFromScratch(); QByteArray data; QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar"); QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json"); QString fmlMinecraftVersion; if (QFile::exists(modpackJar)) { MMCZip::ArchiveReader zipFile(modpackJar); if (!zipFile.collectFiles()) { emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); return; } if (zipFile.exists("/version.json")) { if (zipFile.exists("/fmlversion.properties")) { auto file = zipFile.goToFile("fmlversion.properties"); if (!file) { emit failed(tr("Unable to open \"fmlversion.properties\"!")); return; } QByteArray fmlVersionData = file->readAll(); INIFile iniFile; iniFile.loadFile(fmlVersionData); // If not present, this evaluates to a null string fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString(); } auto file = zipFile.goToFile("version.json"); if (!file) { emit failed(tr("Unable to open \"version.json\"!")); return; } data = file->readAll(); } else { if (minecraftVersion.isEmpty()) { emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but Minecraft version is unknown")); return; } components->setComponentVersion("net.minecraft", minecraftVersion, true); components->installJarMods({ modpackJar }); // Forge for 1.4.7 and for 1.5.2 require extra libraries. // Figure out the forge version and add it as a component // (the code still comes from the jar mod installed above) if (zipFile.exists("/forgeversion.properties")) { auto file = zipFile.goToFile("forgeversion.properties"); if (!file) { // Really shouldn't happen, but error handling shall not be forgotten emit failed(tr("Unable to open \"forgeversion.properties\"")); return; } auto forgeVersionData = file->readAll(); INIFile iniFile; iniFile.loadFile(forgeVersionData); QString major, minor, revision, build; major = iniFile["forge.major.number"].toString(); minor = iniFile["forge.minor.number"].toString(); revision = iniFile["forge.revision.number"].toString(); build = iniFile["forge.build.number"].toString(); if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty()) { emit failed(tr("Invalid \"forgeversion.properties\"!")); return; } components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build); } components->saveNow(); emit succeeded(); return; } } else if (QFile::exists(versionJson)) { QFile file(versionJson); if (!file.open(QIODevice::ReadOnly)) { emit failed(tr("Unable to open \"version.json\"!")); return; } data = file.readAll(); file.close(); } else { // This is the "Vanilla" modpack, excluded by the search code components->setComponentVersion("net.minecraft", minecraftVersion, true); components->saveNow(); emit succeeded(); return; } try { QJsonDocument doc = Json::requireDocument(data); QJsonObject root = Json::requireObject(doc, "version.json"); QString packMinecraftVersion = root["inheritsFrom"].toString(); if (packMinecraftVersion.isEmpty()) { if (fmlMinecraftVersion.isEmpty()) { emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); return; } packMinecraftVersion = fmlMinecraftVersion; } components->setComponentVersion("net.minecraft", packMinecraftVersion, true); for (auto library : root["libraries"].toArray()) { if (!library.isObject()) { continue; } auto libraryObject = library.toObject(); auto libraryName = libraryObject["name"].toString(); if (libraryName.startsWith("net.neoforged.fancymodloader:")) { // it is neoforge // no easy way to get the version from the libs so use the arguments auto arguments = root["arguments"].toObject(); bool isVersionArg = false; QString neoforgeVersion; for (auto arg : arguments["game"].toArray()) { auto argument = arg.toString(""); if (isVersionArg) { neoforgeVersion = argument; break; } else { isVersionArg = "--fml.neoForgeVersion" == argument || "--fml.forgeVersion" == argument; } } if (!neoforgeVersion.isEmpty()) { components->setComponentVersion("net.neoforged", neoforgeVersion); } break; } else if ((libraryName.startsWith("net.minecraftforge:forge:") || libraryName.startsWith("net.minecraftforge:fmlloader:")) && libraryName.contains('-')) { QString libraryVersion = libraryName.section(':', 2); if (!libraryVersion.startsWith("1.7.10-")) { components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1)); } else { // 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); } break; } else { // -> static QMap loaderMap{ { "net.minecraftforge:minecraftforge:", "net.minecraftforge" }, { "net.fabricmc:fabric-loader:", "net.fabricmc.fabric-loader" }, { "org.quiltmc:quilt-loader:", "org.quiltmc.quilt-loader" } }; for (const auto& loader : loaderMap.keys()) { if (libraryName.startsWith(loader)) { components->setComponentVersion(loaderMap.value(loader), libraryName.section(':', 2)); break; } } } } } catch (const JSONValidationError& e) { emit failed(tr("Could not understand \"version.json\":\n") + e.cause()); return; } components->saveNow(); emit succeeded(); } PrismLauncher-10.0.5/launcher/modplatform/technic/SolderPackManifest.h0000644000175100017510000000233115144136756025423 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include namespace TechnicSolder { struct Pack { QString recommended; QString latest; QList builds; }; void loadPack(Pack& v, QJsonObject& obj); struct PackBuildMod { QString name; QString version; QString md5; QString url; }; struct PackBuild { QString minecraft; QList mods; }; void loadPackBuild(PackBuild& v, QJsonObject& obj); } // namespace TechnicSolder PrismLauncher-10.0.5/launcher/modplatform/technic/SolderPackManifest.cpp0000644000175100017510000000341215144136756025757 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SolderPackManifest.h" #include "Json.h" namespace TechnicSolder { void loadPack(Pack& v, QJsonObject& obj) { v.recommended = Json::requireString(obj, "recommended"); v.latest = Json::requireString(obj, "latest"); auto builds = Json::requireArray(obj, "builds"); for (const auto buildRaw : builds) { auto build = Json::requireString(buildRaw); v.builds.append(build); } } static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) { b.name = Json::requireString(obj, "name"); b.version = obj["version"].toString(""); b.md5 = Json::requireString(obj, "md5"); b.url = Json::requireString(obj, "url"); } void loadPackBuild(PackBuild& v, QJsonObject& obj) { v.minecraft = Json::requireString(obj, "minecraft"); auto mods = Json::requireArray(obj, "mods"); for (const auto modRaw : mods) { auto modObj = Json::requireObject(modRaw); PackBuildMod mod; loadPackBuildMod(mod, modObj); v.mods.append(mod); } } } // namespace TechnicSolder PrismLauncher-10.0.5/launcher/modplatform/technic/SolderPackInstallTask.h0000644000175100017510000000561315144136756026114 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021-2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include namespace Technic { class SolderPackInstallTask : public InstanceTask { Q_OBJECT public: explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl& solderUrl, const QString& pack, const QString& version, const QString& minecraftVersion); bool canAbort() const override { return true; } bool abort() override; protected: //! Entry point for tasks. virtual void executeTask() override; private slots: void fileListSucceeded(); void downloadSucceeded(); void downloadFailed(QString reason); void downloadProgressChanged(qint64 current, qint64 total); void downloadAborted(); void extractFinished(); void extractAborted(); private: bool m_abortable = false; shared_qobject_ptr m_network; NetJob::Ptr m_filesNetJob; QUrl m_solderUrl; QString m_pack; QString m_version; QString m_minecraftVersion; std::shared_ptr m_response = std::make_shared(); QTemporaryDir m_outputDir; int m_modCount; QFuture m_extractFuture; QFutureWatcher m_extractFutureWatcher; }; } // namespace Technic PrismLauncher-10.0.5/launcher/modplatform/technic/SingleZipPackInstallTask.cpp0000644000175100017510000001176315144136756027126 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "SingleZipPackInstallTask.h" #include #include "FileSystem.h" #include "MMCZip.h" #include "TechnicPackProcessor.h" #include "Application.h" #include "net/ApiDownload.h" Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl& sourceUrl, const QString& minecraftVersion) { m_sourceUrl = sourceUrl; m_minecraftVersion = minecraftVersion; } bool Technic::SingleZipPackInstallTask::abort() { if (m_abortable) { return m_filesNetJob->abort(); } return false; } void Technic::SingleZipPackInstallTask::executeTask() { setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); m_archivePath = entry->getFullPath(); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged); connect(job, &NetJob::stepProgress, this, &Technic::SingleZipPackInstallTask::propagateStepProgress); connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed); m_filesNetJob->start(); } void Technic::SingleZipPackInstallTask::downloadSucceeded() { m_abortable = false; setStatus(tr("Extracting modpack")); QDir extractDir(FS::PathCombine(m_stagingPath, "minecraft")); qDebug() << "Attempting to create instance from" << m_archivePath; // open the zip and find relevant files in it m_packZip.reset(new MMCZip::ArchiveReader(m_archivePath)); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath()); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted); m_extractFutureWatcher.setFuture(m_extractFuture); m_filesNetJob.reset(); } void Technic::SingleZipPackInstallTask::downloadFailed(QString reason) { m_abortable = false; m_filesNetJob.reset(); emitFailed(reason); } void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) { m_abortable = true; setProgress(current / 2, total); } void Technic::SingleZipPackInstallTask::extractFinished() { m_packZip.reset(); if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); return; } QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; QDirIterator it(extractDir, QDirIterator::Subdirectories); while (it.hasNext()) { auto filepath = it.next(); QFileInfo file(filepath); auto permissions = QFile::permissions(filepath); auto origPermissions = permissions; if (file.isDir()) { // Folder +rwx for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; } else { // File +rw for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; } if (origPermissions != permissions) { if (!QFile::setPermissions(filepath, permissions)) { logWarning(tr("Could not fix permissions for %1").arg(filepath)); } else { qDebug() << "Fixed" << filepath; } } } auto packProcessor = makeShared(); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion); } void Technic::SingleZipPackInstallTask::extractAborted() { emitFailed(tr("Instance import has been aborted.")); } PrismLauncher-10.0.5/launcher/modplatform/technic/SolderPackInstallTask.cpp0000644000175100017510000002004515144136756026443 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2021-2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "SolderPackInstallTask.h" #include #include #include #include #include "SolderPackManifest.h" #include "TechnicPackProcessor.h" #include "net/ApiDownload.h" #include "net/ChecksumValidator.h" Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr network, const QUrl& solderUrl, const QString& pack, const QString& version, const QString& minecraftVersion) { m_solderUrl = solderUrl; m_pack = pack; m_version = version; m_network = network; m_minecraftVersion = minecraftVersion; } bool Technic::SolderPackInstallTask::abort() { if (m_abortable) { return m_filesNetJob->abort(); } return false; } void Technic::SolderPackInstallTask::executeTask() { setStatus(tr("Resolving modpack files")); m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network)); auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); m_filesNetJob->addNetAction(Net::ApiDownload::makeByteArray(sourceUrl, m_response)); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); connect(job, &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); m_filesNetJob->start(); } void Technic::SolderPackInstallTask::fileListSucceeded() { setStatus(tr("Downloading modpack")); QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*m_response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Solder at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *m_response; return; } auto obj = doc.object(); TechnicSolder::PackBuild build; try { TechnicSolder::loadPackBuild(build, obj); } catch (const JSONValidationError& e) { m_filesNetJob.reset(); emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); return; } if (!build.minecraft.isEmpty()) m_minecraftVersion = build.minecraft; m_filesNetJob.reset(new NetJob(tr("Downloading modpack"), m_network)); int i = 0; for (const auto& mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); auto dl = Net::ApiDownload::makeFile(mod.url, path); if (!mod.md5.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } m_filesNetJob->addNetAction(dl); i++; } m_modCount = build.mods.size(); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &Technic::SolderPackInstallTask::propagateStepProgress); connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); connect(m_filesNetJob.get(), &NetJob::aborted, this, &Technic::SolderPackInstallTask::downloadAborted); m_filesNetJob->start(); } void Technic::SolderPackInstallTask::downloadSucceeded() { m_abortable = false; setStatus(tr("Extracting modpack")); m_filesNetJob.reset(); m_extractFuture = QtConcurrent::run([this]() { int i = 0; QString extractDir = FS::PathCombine(m_stagingPath, "minecraft"); FS::ensureFolderPathExists(extractDir); while (m_modCount > i) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); if (!MMCZip::extractDir(path, extractDir)) { return false; } i++; } return true; }); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SolderPackInstallTask::extractFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &Technic::SolderPackInstallTask::extractAborted); m_extractFutureWatcher.setFuture(m_extractFuture); } void Technic::SolderPackInstallTask::downloadFailed(QString reason) { m_abortable = false; m_filesNetJob.reset(); emitFailed(reason); } void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) { m_abortable = true; setProgress(current / 2, total); } void Technic::SolderPackInstallTask::downloadAborted() { m_filesNetJob.reset(); emitAborted(); } void Technic::SolderPackInstallTask::extractFinished() { if (!m_extractFuture.result()) { emitFailed(tr("Failed to extract modpack")); return; } QDir extractDir(m_stagingPath); qDebug() << "Fixing permissions for extracted pack files..."; QDirIterator it(extractDir, QDirIterator::Subdirectories); while (it.hasNext()) { auto filepath = it.next(); QFileInfo file(filepath); auto permissions = QFile::permissions(filepath); auto origPermissions = permissions; if (file.isDir()) { // Folder +rwx for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; } else { // File +rw for current user permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; } if (origPermissions != permissions) { if (!QFile::setPermissions(filepath, permissions)) { logWarning(tr("Could not fix permissions for %1").arg(filepath)); } else { qDebug() << "Fixed" << filepath; } } } auto packProcessor = makeShared(); connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded); connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed); packProcessor->run(m_globalSettings, name(), m_instIcon, m_stagingPath, m_minecraftVersion, true); } void Technic::SolderPackInstallTask::extractAborted() { emitFailed(tr("Instance import has been aborted.")); } PrismLauncher-10.0.5/launcher/modplatform/technic/SingleZipPackInstallTask.h0000644000175100017510000000322415144136756026564 0ustar runnerrunner/* Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "InstanceTask.h" #include "archive/ArchiveReader.h" #include "net/NetJob.h" #include #include #include #include namespace Technic { class SingleZipPackInstallTask : public InstanceTask { Q_OBJECT public: SingleZipPackInstallTask(const QUrl& sourceUrl, const QString& minecraftVersion); bool canAbort() const override { return true; } bool abort() override; protected: void executeTask() override; private slots: void downloadSucceeded(); void downloadFailed(QString reason); void downloadProgressChanged(qint64 current, qint64 total); void extractFinished(); void extractAborted(); private: bool m_abortable = false; QUrl m_sourceUrl; QString m_minecraftVersion; QString m_archivePath; NetJob::Ptr m_filesNetJob; std::unique_ptr m_packZip; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; }; } // namespace Technic PrismLauncher-10.0.5/launcher/modplatform/technic/TechnicPackProcessor.h0000644000175100017510000000231415144136756025762 0ustar runnerrunner/* Copyright 2020-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "settings/SettingsObject.h" namespace Technic { // not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask class TechnicPackProcessor : public QObject { Q_OBJECT signals: void succeeded(); void failed(QString reason); public: void run(SettingsObjectPtr globalSettings, const QString& instName, const QString& instIcon, const QString& stagingPath, const QString& minecraftVersion = QString(), bool isSolder = false); }; } // namespace Technic PrismLauncher-10.0.5/launcher/modplatform/atlauncher/0000755000175100017510000000000015144136756022246 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/atlauncher/ATLPackIndex.cpp0000644000175100017510000000347515144136756025172 0ustar runnerrunner/* * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Petr Mrazek * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ATLPackIndex.h" #include #include "Json.h" static void loadIndexedVersion(ATLauncher::IndexedVersion& v, QJsonObject& obj) { v.version = Json::requireString(obj, "version"); v.minecraft = Json::requireString(obj, "minecraft"); } void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj) { m.id = Json::requireInteger(obj, "id"); m.position = Json::requireInteger(obj, "position"); m.name = Json::requireString(obj, "name"); m.type = Json::requireString(obj, "type") == "private" ? ATLauncher::PackType::Private : ATLauncher::PackType::Public; auto versionsArr = Json::requireArray(obj, "versions"); for (const auto versionRaw : versionsArr) { auto versionObj = Json::requireObject(versionRaw); ATLauncher::IndexedVersion version; loadIndexedVersion(version, versionObj); m.versions.append(version); } m.system = obj["system"].toBool(); m.description = obj["description"].toString(""); static const QRegularExpression s_regex("[^A-Za-z0-9]"); m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png"; } PrismLauncher-10.0.5/launcher/modplatform/atlauncher/ATLShareCode.h0000644000175100017510000000226015144136756024615 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include namespace ATLauncher { struct ShareCodeMod { bool selected; QString name; }; struct ShareCode { QString pack; QString version; QList mods; }; struct ShareCodeResponse { bool error; int code; QString message; ShareCode data; }; void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj); } // namespace ATLauncher PrismLauncher-10.0.5/launcher/modplatform/atlauncher/ATLShareCode.cpp0000644000175100017510000000355415144136756025157 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ATLShareCode.h" #include "Json.h" namespace ATLauncher { static void loadShareCodeMod(ShareCodeMod& m, QJsonObject& obj) { m.selected = Json::requireBoolean(obj, "selected"); m.name = Json::requireString(obj, "name"); } static void loadShareCode(ShareCode& c, QJsonObject& obj) { c.pack = Json::requireString(obj, "pack"); c.version = Json::requireString(obj, "version"); auto mods = Json::requireObject(obj, "mods"); auto optional = Json::requireArray(mods, "optional"); for (const auto modRaw : optional) { auto modObj = Json::requireObject(modRaw); ShareCodeMod mod; loadShareCodeMod(mod, modObj); c.mods.append(mod); } } void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj) { r.error = Json::requireBoolean(obj, "error"); r.code = Json::requireInteger(obj, "code"); if (obj.contains("message") && !obj.value("message").isNull()) r.message = Json::requireString(obj, "message"); if (!r.error) { auto dataRaw = Json::requireObject(obj, "data"); loadShareCode(r.data, dataRaw); } } } // namespace ATLauncher PrismLauncher-10.0.5/launcher/modplatform/atlauncher/ATLPackManifest.cpp0000644000175100017510000002735015144136756025667 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Petr Mrazek * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ATLPackManifest.h" #include "Json.h" static ATLauncher::DownloadType parseDownloadType(QString rawType) { if (rawType == QString("server")) { return ATLauncher::DownloadType::Server; } else if (rawType == QString("browser")) { return ATLauncher::DownloadType::Browser; } else if (rawType == QString("direct")) { return ATLauncher::DownloadType::Direct; } return ATLauncher::DownloadType::Unknown; } static ATLauncher::ModType parseModType(QString rawType) { // See https://wiki.atlauncher.com/mod_types if (rawType == QString("root")) { return ATLauncher::ModType::Root; } else if (rawType == QString("forge")) { return ATLauncher::ModType::Forge; } else if (rawType == QString("jar")) { return ATLauncher::ModType::Jar; } else if (rawType == QString("mods")) { return ATLauncher::ModType::Mods; } else if (rawType == QString("flan")) { return ATLauncher::ModType::Flan; } else if (rawType == QString("dependency") || rawType == QString("depandency")) { return ATLauncher::ModType::Dependency; } else if (rawType == QString("ic2lib")) { return ATLauncher::ModType::Ic2Lib; } else if (rawType == QString("denlib")) { return ATLauncher::ModType::DenLib; } else if (rawType == QString("coremods")) { return ATLauncher::ModType::Coremods; } else if (rawType == QString("mcpc")) { return ATLauncher::ModType::MCPC; } else if (rawType == QString("plugins")) { return ATLauncher::ModType::Plugins; } else if (rawType == QString("extract")) { return ATLauncher::ModType::Extract; } else if (rawType == QString("decomp")) { return ATLauncher::ModType::Decomp; } else if (rawType == QString("texturepack")) { return ATLauncher::ModType::TexturePack; } else if (rawType == QString("resourcepack")) { return ATLauncher::ModType::ResourcePack; } else if (rawType == QString("shaderpack")) { return ATLauncher::ModType::ShaderPack; } else if (rawType == QString("texturepackextract")) { return ATLauncher::ModType::TexturePackExtract; } else if (rawType == QString("resourcepackextract")) { return ATLauncher::ModType::ResourcePackExtract; } else if (rawType == QString("millenaire")) { return ATLauncher::ModType::Millenaire; } return ATLauncher::ModType::Unknown; } static void loadVersionLoader(ATLauncher::VersionLoader& p, QJsonObject& obj) { p.type = Json::requireString(obj, "type"); p.choose = obj["choose"].toBool(); auto metadata = Json::requireObject(obj, "metadata"); p.latest = metadata["latest"].toBool(); p.recommended = metadata["recommended"].toBool(); // Minecraft Forge if (p.type == "forge" || p.type == "neoforge") { p.version = metadata["version"].toString(""); } // Fabric Loader if (p.type == "fabric") { p.version = metadata["loader"].toString(""); } } static void loadVersionLibrary(ATLauncher::VersionLibrary& p, QJsonObject& obj) { p.url = Json::requireString(obj, "url"); p.file = Json::requireString(obj, "file"); p.md5 = Json::requireString(obj, "md5"); p.download_raw = Json::requireString(obj, "download"); p.download = parseDownloadType(p.download_raw); p.server = obj["server"].toString(""); } static void loadVersionConfigs(ATLauncher::VersionConfigs& p, QJsonObject& obj) { p.filesize = Json::requireInteger(obj, "filesize"); p.sha1 = Json::requireString(obj, "sha1"); } static void loadVersionMod(ATLauncher::VersionMod& p, QJsonObject& obj) { p.name = Json::requireString(obj, "name"); p.version = Json::requireString(obj, "version"); p.url = Json::requireString(obj, "url"); p.file = Json::requireString(obj, "file"); p.md5 = obj["md5"].toString(""); p.download_raw = Json::requireString(obj, "download"); p.download = parseDownloadType(p.download_raw); p.type_raw = Json::requireString(obj, "type"); p.type = parseModType(p.type_raw); // This contributes to the Minecraft Forge detection, where we rely on mod.type being "Forge" // when the mod represents Forge. As there is little difference between "Jar" and "Forge, some // packs regretfully use "Jar". This will correct the type to "Forge" in these cases (as best // it can). if (p.name == QString("Minecraft Forge") && p.type == ATLauncher::ModType::Jar) { p.type_raw = "forge"; p.type = ATLauncher::ModType::Forge; } if (obj.contains("extractTo")) { p.extractTo_raw = Json::requireString(obj, "extractTo"); p.extractTo = parseModType(p.extractTo_raw); p.extractFolder = obj["extractFolder"].toString("").replace("%s%", "/"); } if (obj.contains("decompType")) { p.decompType_raw = Json::requireString(obj, "decompType"); p.decompType = parseModType(p.decompType_raw); p.decompFile = Json::requireString(obj, "decompFile"); } p.description = obj["description"].toString(""); p.optional = obj["optional"].toBool(); p.recommended = obj["recommended"].toBool(); p.selected = obj["selected"].toBool(); p.hidden = obj["hidden"].toBool(); p.library = obj["library"].toBool(); p.group = obj["group"].toString(""); if (obj.contains("depends")) { auto dependsArr = Json::requireArray(obj, "depends"); for (const auto depends : dependsArr) { p.depends.append(Json::requireString(depends)); } } p.colour = obj["colour"].toString(""); p.warning = obj["warning"].toString(""); p.client = obj["client"].toBool(); // computed p.effectively_hidden = p.hidden || p.library; } static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj) { m.install = obj["install"].toString(""); m.update = obj["update"].toString(""); } static void loadVersionMainClass(ATLauncher::PackVersionMainClass& m, QJsonObject& obj) { m.mainClass = obj["mainClass"].toString(""); m.depends = obj["depends"].toString(""); } static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments& a, QJsonObject& obj) { a.arguments = obj["arguments"].toString(""); a.depends = obj["depends"].toString(""); } static void loadVersionKeep(ATLauncher::VersionKeep& k, QJsonObject& obj) { k.base = Json::requireString(obj, "base"); k.target = Json::requireString(obj, "target"); } static void loadVersionKeeps(ATLauncher::VersionKeeps& k, QJsonObject& obj) { if (obj.contains("files")) { auto files = Json::requireArray(obj, "files"); for (const auto keepRaw : files) { auto keepObj = Json::requireObject(keepRaw); ATLauncher::VersionKeep keep; loadVersionKeep(keep, keepObj); k.files.append(keep); } } if (obj.contains("folders")) { auto folders = Json::requireArray(obj, "folders"); for (const auto keepRaw : folders) { auto keepObj = Json::requireObject(keepRaw); ATLauncher::VersionKeep keep; loadVersionKeep(keep, keepObj); k.folders.append(keep); } } } static void loadVersionDelete(ATLauncher::VersionDelete& d, QJsonObject& obj) { d.base = Json::requireString(obj, "base"); d.target = Json::requireString(obj, "target"); } static void loadVersionDeletes(ATLauncher::VersionDeletes& d, QJsonObject& obj) { if (obj.contains("files")) { auto files = Json::requireArray(obj, "files"); for (const auto deleteRaw : files) { auto deleteObj = Json::requireObject(deleteRaw); ATLauncher::VersionDelete versionDelete; loadVersionDelete(versionDelete, deleteObj); d.files.append(versionDelete); } } if (obj.contains("folders")) { auto folders = Json::requireArray(obj, "folders"); for (const auto deleteRaw : folders) { auto deleteObj = Json::requireObject(deleteRaw); ATLauncher::VersionDelete versionDelete; loadVersionDelete(versionDelete, deleteObj); d.folders.append(versionDelete); } } } void ATLauncher::loadVersion(PackVersion& v, QJsonObject& obj) { v.version = Json::requireString(obj, "version"); v.minecraft = Json::requireString(obj, "minecraft"); v.noConfigs = obj["noConfigs"].toBool(); if (obj.contains("mainClass")) { auto main = Json::requireObject(obj, "mainClass"); loadVersionMainClass(v.mainClass, main); } if (obj.contains("extraArguments")) { auto arguments = Json::requireObject(obj, "extraArguments"); loadVersionExtraArguments(v.extraArguments, arguments); } if (obj.contains("loader")) { auto loader = Json::requireObject(obj, "loader"); loadVersionLoader(v.loader, loader); } if (obj.contains("libraries")) { auto libraries = Json::requireArray(obj, "libraries"); for (const auto libraryRaw : libraries) { auto libraryObj = Json::requireObject(libraryRaw); ATLauncher::VersionLibrary target; loadVersionLibrary(target, libraryObj); v.libraries.append(target); } } if (obj.contains("mods")) { auto mods = Json::requireArray(obj, "mods"); for (const auto modRaw : mods) { auto modObj = Json::requireObject(modRaw); ATLauncher::VersionMod mod; loadVersionMod(mod, modObj); v.mods.append(mod); } } if (obj.contains("configs")) { auto configsObj = Json::requireObject(obj, "configs"); loadVersionConfigs(v.configs, configsObj); } auto colourObj = obj["colours"].toObject(); for (const auto& key : colourObj.keys()) { v.colours[key] = Json::requireString(colourObj.value(key), "colour"); } auto warningsObj = obj["warnings"].toObject(); for (const auto& key : warningsObj.keys()) { v.warnings[key] = Json::requireString(warningsObj.value(key), "warning"); } auto messages = obj["messages"].toObject(); loadVersionMessages(v.messages, messages); auto keeps = obj["keeps"].toObject(); loadVersionKeeps(v.keeps, keeps); auto deletes = obj["deletes"].toObject(); loadVersionDeletes(v.deletes, deletes); } PrismLauncher-10.0.5/launcher/modplatform/atlauncher/ATLPackManifest.h0000644000175100017510000000776115144136756025340 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include namespace ATLauncher { enum class PackType { Public, Private }; enum class ModType { Root, Forge, Jar, Mods, Flan, Dependency, Ic2Lib, DenLib, Coremods, MCPC, Plugins, Extract, Decomp, TexturePack, ResourcePack, ShaderPack, TexturePackExtract, ResourcePackExtract, Millenaire, Unknown }; enum class DownloadType { Server, Browser, Direct, Unknown }; struct VersionLoader { QString type; bool latest; bool recommended; bool choose; QString version; }; struct VersionLibrary { QString url; QString file; QString server; QString md5; DownloadType download; QString download_raw; }; struct VersionMod { QString name; QString version; QString url; QString file; QString md5; DownloadType download; QString download_raw; ModType type; QString type_raw; ModType extractTo; QString extractTo_raw; QString extractFolder; ModType decompType; QString decompType_raw; QString decompFile; QString description; bool optional; bool recommended; bool selected; bool hidden; bool library; QString group; QStringList depends; QString colour; QString warning; bool client; // computed bool effectively_hidden; }; struct VersionConfigs { int filesize; QString sha1; }; struct VersionMessages { QString install; QString update; }; struct VersionKeep { QString base; QString target; }; struct VersionKeeps { QList files; QList folders; }; struct VersionDelete { QString base; QString target; }; struct VersionDeletes { QList files; QList folders; }; struct PackVersionMainClass { QString mainClass; QString depends; }; struct PackVersionExtraArguments { QString arguments; QString depends; }; struct PackVersion { QString version; QString minecraft; bool noConfigs; PackVersionMainClass mainClass; PackVersionExtraArguments extraArguments; VersionLoader loader; QList libraries; QList mods; VersionConfigs configs; QMap colours; QMap warnings; VersionMessages messages; VersionKeeps keeps; VersionDeletes deletes; }; void loadVersion(PackVersion& v, QJsonObject& obj); } // namespace ATLauncher PrismLauncher-10.0.5/launcher/modplatform/atlauncher/ATLPackInstallTask.h0000644000175100017510000001144115144136756026011 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Petr Mrazek * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include "ATLPackManifest.h" #include "InstanceTask.h" #include "meta/Version.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/NetJob.h" #include "settings/INISettingsObject.h" #include #include namespace ATLauncher { enum class InstallMode { Install, Reinstall, Update, }; class UserInteractionSupport { public: /** * Requests a user interaction to select which optional mods should be installed. */ virtual std::optional> chooseOptionalMods(const PackVersion& version, QList mods) = 0; /** * Requests a user interaction to select a component version from a given version list * and constrained to a given Minecraft version. */ virtual QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) = 0; /** * Requests a user interaction to display a message. */ virtual void displayMessage(QString message) = 0; virtual ~UserInteractionSupport() = default; }; class PackInstallTask : public InstanceTask { Q_OBJECT public: explicit PackInstallTask(UserInteractionSupport* support, QString packName, QString version, InstallMode installMode = InstallMode::Install); virtual ~PackInstallTask() { delete m_support; } bool canAbort() const override { return true; } bool abort() override; protected: virtual void executeTask() override; private slots: void onDownloadSucceeded(); void onDownloadFailed(QString reason); void onDownloadAborted(); void onModsDownloaded(); void onModsExtracted(); private: QString getDirForModType(ModType type, QString raw); QString getVersionForLoader(QString uid); QString detectLibrary(const VersionLibrary& library); bool createLibrariesComponent(QString instanceRoot, std::shared_ptr profile); bool createPackComponent(QString instanceRoot, std::shared_ptr profile); void deleteExistingFiles(); void installConfigs(); void extractConfigs(); void downloadMods(); bool extractMods(const QMap& toExtract, const QMap& toDecomp, const QMap& toCopy); void install(); private: UserInteractionSupport* m_support; bool abortable = false; NetJob::Ptr jobPtr; std::shared_ptr response = std::make_shared(); InstallMode m_install_mode; QString m_pack_name; QString m_pack_safe_name; QString m_version_name; PackVersion m_version; QMap modsToExtract; QMap modsToDecomp; QMap modsToCopy; QString archivePath; QStringList jarmods; Meta::Version::Ptr minecraftVersion; QMap componentsToInstall; QFuture> m_extractFuture; QFutureWatcher> m_extractFutureWatcher; QFuture m_modExtractFuture; QFutureWatcher m_modExtractFutureWatcher; }; } // namespace ATLauncher PrismLauncher-10.0.5/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp0000644000175100017510000011470415144136756026352 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2020-2021 Jamie Mansfield * Copyright 2021 Petr Mrazek * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "ATLPackInstallTask.h" #include #include #include "FileSystem.h" #include "Json.h" #include "MMCZip.h" #include "Version.h" #include "meta/Index.h" #include "meta/Version.h" #include "meta/VersionList.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/PackProfile.h" #include "modplatform/atlauncher/ATLPackManifest.h" #include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" #include "net/ApiDownload.h" #include "Application.h" #include "BuildConfig.h" #include "ui/dialogs/BlockedModsDialog.h" namespace ATLauncher { static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version); PackInstallTask::PackInstallTask(UserInteractionSupport* support, QString packName, QString version, InstallMode installMode) { m_support = support; m_pack_name = packName; static const QRegularExpression s_regex("[^A-Za-z0-9]"); m_pack_safe_name = packName.replace(s_regex, ""); m_version_name = version; m_install_mode = installMode; } bool PackInstallTask::abort() { if (abortable) { return jobPtr->abort(); } return false; } void PackInstallTask::executeTask() { qDebug() << "PackInstallTask::executeTask:" << QThread::currentThreadId(); NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) }; auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json").arg(m_pack_safe_name).arg(m_version_name); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); jobPtr = netJob; jobPtr->start(); } void PackInstallTask::onDownloadSucceeded() { qDebug() << "PackInstallTask::onDownloadSucceeded:" << QThread::currentThreadId(); jobPtr.reset(); QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from ATLauncher at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response.get(); return; } auto obj = doc.object(); ATLauncher::PackVersion version; try { ATLauncher::loadVersion(version, obj); } catch (const JSONValidationError& e) { emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); return; } m_version = version; // Derived from the installation mode QString message; bool resetDirectory; switch (m_install_mode) { case InstallMode::Reinstall: case InstallMode::Update: message = m_version.messages.update; resetDirectory = true; break; case InstallMode::Install: message = m_version.messages.install; resetDirectory = false; break; default: emitFailed(tr("Unsupported installation mode")); return; } // Display message if one exists if (!message.isEmpty()) m_support->displayMessage(message); auto ver = getComponentVersion("net.minecraft", m_version.minecraft); if (!ver) { emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft)); return; } minecraftVersion = ver; if (resetDirectory) { deleteExistingFiles(); } if (m_version.noConfigs) { downloadMods(); } else { installConfigs(); } } void PackInstallTask::onDownloadFailed(QString reason) { qDebug() << "PackInstallTask::onDownloadFailed:" << QThread::currentThreadId(); jobPtr.reset(); emitFailed(reason); } void PackInstallTask::onDownloadAborted() { jobPtr.reset(); emitAborted(); } void PackInstallTask::deleteExistingFiles() { setStatus(tr("Deleting existing files...")); // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/delete VersionDeletes deletes; deletes.folders.append(VersionDelete{ "root", "mods%s%" }); deletes.folders.append(VersionDelete{ "root", "configs%s%" }); deletes.folders.append(VersionDelete{ "root", "bin%s%" }); // Setup defaults, as per https://wiki.atlauncher.com/pack-admin/xml/keep VersionKeeps keeps; keeps.files.append(VersionKeep{ "root", "mods%s%PortalGunSounds.pak" }); keeps.folders.append(VersionKeep{ "root", "mods%s%rei_minimap%s%" }); keeps.folders.append(VersionKeep{ "root", "mods%s%VoxelMods%s%" }); keeps.files.append(VersionKeep{ "root", "config%s%NEI.cfg" }); keeps.files.append(VersionKeep{ "root", "options.txt" }); keeps.files.append(VersionKeep{ "root", "servers.dat" }); // Merge with version deletes and keeps for (const auto& item : m_version.deletes.files) deletes.files.append(item); for (const auto& item : m_version.deletes.folders) deletes.folders.append(item); for (const auto& item : m_version.keeps.files) keeps.files.append(item); for (const auto& item : m_version.keeps.folders) keeps.folders.append(item); auto getPathForBase = [this](const QString& base) { auto minecraftPath = FS::PathCombine(m_stagingPath, "minecraft"); if (base == "root") { return minecraftPath; } else if (base == "config") { return FS::PathCombine(minecraftPath, "config"); } else { qWarning() << "Unrecognised base path" << base; return minecraftPath; } }; auto convertToSystemPath = [](const QString& path) { auto t = path; t.replace("%s%", QDir::separator()); return t; }; auto shouldKeep = [keeps, getPathForBase, convertToSystemPath](const QString& fullPath) { for (const auto& item : keeps.files) { auto basePath = getPathForBase(item.base); auto targetPath = convertToSystemPath(item.target); auto path = FS::PathCombine(basePath, targetPath); if (fullPath == path) { return true; } } for (const auto& item : keeps.folders) { auto basePath = getPathForBase(item.base); auto targetPath = convertToSystemPath(item.target); auto path = FS::PathCombine(basePath, targetPath); if (fullPath.startsWith(path)) { return true; } } return false; }; // Keep track of files to delete QSet filesToDelete; for (const auto& item : deletes.files) { auto basePath = getPathForBase(item.base); auto targetPath = convertToSystemPath(item.target); auto fullPath = FS::PathCombine(basePath, targetPath); if (shouldKeep(fullPath)) continue; filesToDelete.insert(fullPath); } for (const auto& item : deletes.folders) { auto basePath = getPathForBase(item.base); auto targetPath = convertToSystemPath(item.target); auto fullPath = FS::PathCombine(basePath, targetPath); QDirIterator it(fullPath, QDirIterator::Subdirectories); while (it.hasNext()) { auto path = it.next(); if (shouldKeep(path)) continue; filesToDelete.insert(path); } } // Delete the files for (const auto& item : filesToDelete) { FS::deletePath(item); } } QString PackInstallTask::getDirForModType(ModType type, QString raw) { switch (type) { // Mod types that can either be ignored at this stage, or ignored // completely. case ModType::Root: case ModType::Extract: case ModType::Decomp: case ModType::TexturePackExtract: case ModType::ResourcePackExtract: case ModType::MCPC: return Q_NULLPTR; case ModType::Forge: // Forge detection happens later on, if it cannot be detected it will // install a jarmod component. case ModType::Jar: return "jarmods"; case ModType::Mods: return "mods"; case ModType::Flan: return "Flan"; case ModType::Dependency: return FS::PathCombine("mods", m_version.minecraft); case ModType::Ic2Lib: return FS::PathCombine("mods", "ic2"); case ModType::DenLib: return FS::PathCombine("mods", "denlib"); case ModType::Coremods: return "coremods"; case ModType::Plugins: return "plugins"; case ModType::TexturePack: return "texturepacks"; case ModType::ResourcePack: return "resourcepacks"; case ModType::ShaderPack: return "shaderpacks"; case ModType::Millenaire: qWarning() << "Unsupported mod type: " + raw; return Q_NULLPTR; case ModType::Unknown: emitFailed(tr("Unknown mod type: %1").arg(raw)); return Q_NULLPTR; } return Q_NULLPTR; } QString PackInstallTask::getVersionForLoader(QString uid) { if (m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) { auto vlist = APPLICATION->metadataIndex()->get(uid); if (!vlist) { emitFailed(tr("Failed to get local metadata index for %1").arg(uid)); return Q_NULLPTR; } vlist->waitToLoad(); if (m_version.loader.recommended || m_version.loader.latest) { for (int i = 0; i < vlist->versions().size(); i++) { auto version = vlist->versions().at(i); auto reqs = version->requiredSet(); // filter by minecraft version, if the loader depends on a certain version. // not all mod loaders depend on a given Minecraft version, so we won't do this // filtering for those loaders. if (m_version.loader.type != "fabric") { auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require& req) { return req.uid == "net.minecraft"; }); if (iter == reqs.end()) continue; if (iter->equalsVersion != m_version.minecraft) continue; } if (m_version.loader.recommended) { // first recommended build we find, we use. if (!version->isRecommended()) continue; } return version->descriptor(); } emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); return Q_NULLPTR; } else if (m_version.loader.choose) { // Fabric Loader doesn't depend on a given Minecraft version. if (m_version.loader.type == "fabric") { return m_support->chooseVersion(vlist, Q_NULLPTR); } return m_support->chooseVersion(vlist, m_version.minecraft); } } if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) { emitFailed(tr("No loader version set for modpack!")); return Q_NULLPTR; } return m_version.loader.version; } QString PackInstallTask::detectLibrary(const VersionLibrary& library) { // Try to detect what the library is if (!library.server.isEmpty() && library.server.split("/").length() >= 3) { auto lastSlash = library.server.lastIndexOf("/"); auto locationAndVersion = library.server.mid(0, lastSlash); auto fileName = library.server.mid(lastSlash + 1); lastSlash = locationAndVersion.lastIndexOf("/"); auto location = locationAndVersion.mid(0, lastSlash); auto version = locationAndVersion.mid(lastSlash + 1); lastSlash = location.lastIndexOf("/"); auto group = location.mid(0, lastSlash).replace("/", "."); auto artefact = location.mid(lastSlash + 1); return group + ":" + artefact + ":" + version; } if (library.file.contains("-")) { auto lastSlash = library.file.lastIndexOf("-"); auto name = library.file.mid(0, lastSlash); auto version = library.file.mid(lastSlash + 1).remove(".jar"); if (name == QString("guava")) { return "com.google.guava:guava:" + version; } else if (name == QString("commons-lang3")) { return "org.apache.commons:commons-lang3:" + version; } } return "org.multimc.atlauncher:" + library.md5 + ":1"; } bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr profile) { if (m_version.libraries.isEmpty()) { return true; } QList exempt; for (const auto& componentUid : componentsToInstall.keys()) { auto componentVersion = componentsToInstall.value(componentUid); if (componentVersion->data()) { for (const auto& library : componentVersion->data()->libraries) { GradleSpecifier lib(library->rawName()); exempt.append(lib); } } } if (minecraftVersion->data()) { for (const auto& library : minecraftVersion->data()->libraries) { GradleSpecifier lib(library->rawName()); exempt.append(lib); } } auto uuid = QUuid::createUuid(); auto id = uuid.toString().remove('{').remove('}'); auto target_id = "org.multimc.atlauncher." + id; auto patchDir = FS::PathCombine(instanceRoot, "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); auto f = std::make_shared(); f->name = m_pack_name + " " + m_version_name + " (libraries)"; const static QMap liteLoaderMap = { { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" }, { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" }, { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" }, { "55785ccc82c07ff0ba038fe24be63ea2", "1.7.10_01" }, { "63ada46e033d0cb6782bada09ad5ca4e", "1.7.10_04" }, { "7983e4b28217c9ae8569074388409c86", "1.7.10_03" }, { "c09882458d74fe0697c7681b8993097e", "1.7.10_02" }, { "db7235aefd407ac1fde09a7baba50839", "1.7.10_00" }, { "6e9028816027f53957bd8fcdfabae064", "1.8" }, { "5e732dc446f9fe2abe5f9decaec40cde", "1.10-SNAPSHOT" }, { "3a98b5ed95810bf164e71c1a53be568d", "1.11.2-SNAPSHOT" }, { "ba8e6285966d7d988a96496f48cbddaa", "1.8.9-SNAPSHOT" }, { "8524af3ac3325a82444cc75ae6e9112f", "1.11-SNAPSHOT" }, { "53639d52340479ccf206a04f5e16606f", "1.5.2_01" }, { "1fcdcf66ce0a0806b7ad8686afdce3f7", "1.6.4_00" }, { "531c116f71ae2b11033f9a11a0f8e668", "1.6.4_01" }, { "4009eeb99c9068f608d3483a6439af88", "1.7.2_03" }, { "66f343354b8417abce1a10d557d2c6e9", "1.7.2_04" }, { "ab554c21f28fbc4ae9b098bcb5f4cceb", "1.7.2_05" }, { "e1d76a05a3723920e2f80a5e66c45f16", "1.7.2_02" }, { "00318cb0c787934d523f63cdfe8ddde4", "1.9-SNAPSHOT" }, { "986fd1ee9525cb0dcab7609401cef754", "1.9.4-SNAPSHOT" }, { "571ad5e6edd5ff40259570c9be588bb5", "1.9.4" }, { "1cdd72f7232e45551f16cc8ffd27ccf3", "1.10.2-SNAPSHOT" }, { "8a7c21f32d77ee08b393dd3921ced8eb", "1.10.2" }, { "b9bef8abc8dc309069aeba6fbbe58980", "1.12.1-SNAPSHOT" } }; for (const auto& lib : m_version.libraries) { // If the library is LiteLoader, we need to ignore it and handle it separately. if (liteLoaderMap.contains(lib.md5)) { auto ver = getComponentVersion("com.mumfrey.liteloader", liteLoaderMap.value(lib.md5)); if (ver) { componentsToInstall.insert("com.mumfrey.liteloader", ver); continue; } } auto libName = detectLibrary(lib); GradleSpecifier libSpecifier(libName); bool libExempt = false; for (const auto& existingLib : exempt) { if (libSpecifier.matchName(existingLib)) { // If the pack specifies a newer version of the lib, use that! libExempt = Version(libSpecifier.version()) >= Version(existingLib.version()); } } if (libExempt) continue; auto library = std::make_shared(); library->setRawName(libName); switch (lib.download) { case DownloadType::Server: library->setAbsoluteUrl(BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url); break; case DownloadType::Direct: library->setAbsoluteUrl(lib.url); break; case DownloadType::Browser: case DownloadType::Unknown: emitFailed(tr("Unknown or unsupported download type: %1").arg(lib.download_raw)); return false; } f->libraries.append(library); } if (f->libraries.isEmpty()) { return true; } QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); return true; } bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr profile) { if (m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) { return true; } auto mainClass = m_version.mainClass.mainClass; auto extraArguments = m_version.extraArguments.arguments; auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty(); auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty(); if (hasMainClassDepends || hasExtraArgumentsDepends) { QSet mods; for (const auto& item : m_version.mods) { mods.insert(item.name); } if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) { mainClass = ""; } if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends)) { extraArguments = ""; } } if (mainClass.isEmpty() && extraArguments.isEmpty()) { return true; } auto uuid = QUuid::createUuid(); auto id = uuid.toString().remove('{').remove('}'); auto target_id = "org.multimc.atlauncher." + id; auto patchDir = FS::PathCombine(instanceRoot, "patches"); if (!FS::ensureFolderPathExists(patchDir)) { return false; } auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); QStringList mainClasses; QStringList tweakers; for (const auto& componentUid : componentsToInstall.keys()) { auto componentVersion = componentsToInstall.value(componentUid); if (componentVersion->data()) { if (componentVersion->data()->mainClass != QString("")) { mainClasses.append(componentVersion->data()->mainClass); } tweakers.append(componentVersion->data()->addTweakers); } } auto f = std::make_shared(); f->name = m_pack_name + " " + m_version_name; if (!mainClass.isEmpty() && !mainClasses.contains(mainClass)) { f->mainClass = mainClass; } // Parse out tweakers auto args = extraArguments.split(" "); QString previous; for (auto arg : args) { if (arg.startsWith("--tweakClass=") || previous == "--tweakClass") { auto tweakClass = arg.remove("--tweakClass="); if (tweakers.contains(tweakClass)) continue; f->addTweakers.append(tweakClass); } previous = arg; } if (f->mainClass == QString() && f->addTweakers.isEmpty()) { return true; } QFile file(patchFileName); if (!file.open(QFile::WriteOnly)) { qCritical() << "Error opening" << file.fileName() << "for reading:" << file.errorString(); return false; } file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); profile->appendComponent(ComponentPtr{ new Component(profile.get(), target_id, f) }); return true; } void PackInstallTask::installConfigs() { qDebug() << "PackInstallTask::installConfigs:" << QThread::currentThreadId(); setStatus(tr("Downloading configs...")); jobPtr.reset(new NetJob(tr("Config download"), APPLICATION->network())); auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip").arg(m_pack_safe_name).arg(m_version_name); auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path); entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); if (!m_version.configs.sha1.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, m_version.configs.sha1)); } jobPtr->addNetAction(dl); archivePath = entry->getFullPath(); connect(jobPtr.get(), &NetJob::succeeded, this, [this]() { abortable = false; jobPtr.reset(); extractConfigs(); }); connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { abortable = false; jobPtr.reset(); emitFailed(reason); }); connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { abortable = true; setProgress(current, total); }); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); connect(jobPtr.get(), &NetJob::aborted, [this] { abortable = false; jobPtr.reset(); emitAborted(); }); jobPtr->start(); } void PackInstallTask::extractConfigs() { qDebug() << "PackInstallTask::extractConfigs:" << QThread::currentThreadId(); setStatus(tr("Extracting configs...")); QDir extractDir(m_stagingPath); m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/minecraft"); connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [this]() { downloadMods(); }); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [this]() { emitAborted(); }); m_extractFutureWatcher.setFuture(m_extractFuture); } void PackInstallTask::downloadMods() { qDebug() << "PackInstallTask::installMods:" << QThread::currentThreadId(); QList optionalMods; for (const auto& mod : m_version.mods) { if (mod.optional) { optionalMods.push_back(mod); } } // Select optional mods, if pack contains any QList selectedMods; if (!optionalMods.isEmpty()) { setStatus(tr("Selecting optional mods...")); auto mods = m_support->chooseOptionalMods(m_version, optionalMods); if (!mods.has_value()) { emitAborted(); return; } selectedMods = mods.value(); } setStatus(tr("Downloading mods...")); jarmods.clear(); jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network())); QList blocked_mods; for (const auto& mod : m_version.mods) { // skip non-client mods if (!mod.client) continue; // skip optional mods that were not selected if (mod.optional && !selectedMods.contains(mod.name)) continue; QString url; switch (mod.download) { case DownloadType::Server: url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; break; case DownloadType::Browser: { blocked_mods.append(mod); continue; } case DownloadType::Direct: url = mod.url; break; case DownloadType::Unknown: emitFailed(tr("Unknown download type: %1").arg(mod.download_raw)); return; } QFileInfo fileName(mod.file); auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); modsToExtract.insert(entry->getFullPath(), mod); auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); } else if (mod.type == ModType::Decomp) { auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); modsToDecomp.insert(entry->getFullPath(), mod); auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); } else { auto relpath = getDirForModType(mod.type, mod.type_raw); if (relpath == Q_NULLPTR) continue; auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); if (mod.type == ModType::Forge) { auto ver = getComponentVersion("net.minecraftforge", mod.version); if (ver) { componentsToInstall.insert("net.minecraftforge", ver); continue; } qDebug() << "Jarmod: " + path; jarmods.push_back(path); } if (mod.type == ModType::Jar) { qDebug() << "Jarmod: " + path; jarmods.push_back(path); } // Download after Forge handling, to avoid downloading Forge twice. qDebug() << "Will download" << url << "to" << path; modsToCopy[entry->getFullPath()] = path; } } if (!blocked_mods.isEmpty()) { QList mods; for (auto mod : blocked_mods) { BlockedMod blocked_mod; blocked_mod.name = mod.file; blocked_mod.websiteUrl = mod.url; blocked_mod.hash = mod.md5; blocked_mod.matched = false; blocked_mod.localPath = ""; mods.append(blocked_mod); } qWarning() << "Blocked mods found, displaying mod list"; BlockedModsDialog message_dialog(nullptr, tr("Blocked mods found"), tr("The following files are not available for download in third party launchers.
    " "You will need to manually download them and add them to the instance."), mods, "md5"); message_dialog.setModal(true); if (message_dialog.exec()) { qDebug() << "Post dialog blocked mods list:" << mods; for (auto blocked : mods) { if (!blocked.matched) { qDebug() << blocked.name << "was not matched to a local file, skipping copy"; continue; } auto modIter = std::find_if(blocked_mods.begin(), blocked_mods.end(), [blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; }); if (modIter == blocked_mods.end()) continue; auto mod = *modIter; if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { modsToExtract.insert(blocked.localPath, mod); } else if (mod.type == ModType::Decomp) { modsToDecomp.insert(blocked.localPath, mod); } else { auto relpath = getDirForModType(mod.type, mod.type_raw); if (relpath == Q_NULLPTR) continue; auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); if (mod.type == ModType::Forge) { auto ver = getComponentVersion("net.minecraftforge", mod.version); if (ver) { componentsToInstall.insert("net.minecraftforge", ver); continue; } qDebug() << "Jarmod: " + path; jarmods.push_back(path); } if (mod.type == ModType::Jar) { qDebug() << "Jarmod: " + path; jarmods.push_back(path); } modsToCopy[blocked.localPath] = path; } } } else { emitFailed(tr("Unknown download type: %1").arg("browser")); return; } } connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); abortable = true; setProgress(current, total); }); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); connect(jobPtr.get(), &NetJob::aborted, &PackInstallTask::emitAborted); connect(jobPtr.get(), &NetJob::failed, &PackInstallTask::emitFailed); jobPtr->start(); } void PackInstallTask::onModsDownloaded() { abortable = false; qDebug() << "PackInstallTask::onModsDownloaded:" << QThread::currentThreadId(); jobPtr.reset(); if (!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), &PackInstallTask::extractMods, this, modsToExtract, modsToDecomp, modsToCopy); connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); m_modExtractFutureWatcher.setFuture(m_modExtractFuture); } else { install(); } } void PackInstallTask::onModsExtracted() { qDebug() << "PackInstallTask::onModsExtracted:" << QThread::currentThreadId(); if (m_modExtractFuture.result()) { install(); } else { emitFailed(tr("Failed to extract mods...")); } } bool PackInstallTask::extractMods(const QMap& toExtract, const QMap& toDecomp, const QMap& toCopy) { qDebug() << "PackInstallTask::extractMods:" << QThread::currentThreadId(); setStatus(tr("Extracting mods...")); for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) { auto& modPath = iter.key(); auto& mod = iter.value(); QString extractToDir; if (mod.type == ModType::Extract) { extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw); } else if (mod.type == ModType::TexturePackExtract) { extractToDir = FS::PathCombine("texturepacks", "extracted"); } else if (mod.type == ModType::ResourcePackExtract) { extractToDir = FS::PathCombine("resourcepacks", "extracted"); } QDir extractDir(m_stagingPath); auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir); QString folderToExtract = ""; if (mod.type == ModType::Extract) { folderToExtract = mod.extractFolder; static const QRegularExpression s_regex("^/"); folderToExtract.remove(s_regex); } qDebug() << "Extracting " + mod.file + " to " + extractToDir; if (!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) { // assume error return false; } } for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) { auto& modPath = iter.key(); auto& mod = iter.value(); auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); QDir extractDir(m_stagingPath); auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; if (!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) { qWarning() << "Failed to extract" << mod.decompFile; return false; } } for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { auto& from = iter.key(); auto& to = iter.value(); // If the file already exists, assume the mod is the correct copy - and remove // the copy from the Configs.zip QFileInfo fileInfo(to); if (fileInfo.exists()) { if (!FS::deletePath(to)) { qWarning() << "Failed to delete" << to; return false; } } FS::copy fileCopyOperation(from, to); if (!fileCopyOperation()) { qWarning() << "Failed to copy" << from << "to" << to; return false; } } return true; } void PackInstallTask::install() { qDebug() << "PackInstallTask::install:" << QThread::currentThreadId(); setStatus(tr("Installing modpack")); auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(instanceConfigPath); instanceSettings->suspendSave(); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); // Use a component to add libraries BEFORE Minecraft if (!createLibrariesComponent(instance.instanceRoot(), components)) { emitFailed(tr("Failed to create libraries component")); return; } // Minecraft components->setComponentVersion("net.minecraft", m_version.minecraft, true); // Loader if (m_version.loader.type == QString("forge")) { auto version = getVersionForLoader("net.minecraftforge"); if (version == Q_NULLPTR) return; components->setComponentVersion("net.minecraftforge", version); } else if (m_version.loader.type == QString("neoforge")) { auto version = getVersionForLoader("net.neoforged"); if (version == Q_NULLPTR) return; components->setComponentVersion("net.neoforged", version); } else if (m_version.loader.type == QString("fabric")) { auto version = getVersionForLoader("net.fabricmc.fabric-loader"); if (version == Q_NULLPTR) return; components->setComponentVersion("net.fabricmc.fabric-loader", version); } else if (m_version.loader.type != QString()) { emitFailed(tr("Unknown loader type: ") + m_version.loader.type); return; } for (const auto& componentUid : componentsToInstall.keys()) { auto version = componentsToInstall.value(componentUid); components->setComponentVersion(componentUid, version->version()); } components->installJarMods(jarmods); // Use a component to fill in the rest of the data // todo: use more detection if (!createPackComponent(instance.instanceRoot(), components)) { emitFailed(tr("Failed to create pack component")); return; } components->saveNow(); instance.setName(name()); instance.setIconKey(m_instIcon); instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); instanceSettings->resumeSave(); jarmods.clear(); emitSucceeded(); } static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version) { return APPLICATION->metadataIndex()->getLoadedVersion(uid, version); } } // namespace ATLauncher PrismLauncher-10.0.5/launcher/modplatform/atlauncher/ATLPackIndex.h0000644000175100017510000000226415144136756024632 0ustar runnerrunner/* * Copyright 2020-2021 Jamie Mansfield * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "ATLPackManifest.h" #include #include #include namespace ATLauncher { struct IndexedVersion { QString version; QString minecraft; }; struct IndexedPack { int id; int position; QString name; PackType type; QList versions; bool system; QString description; QString safeName; }; void loadIndexedPack(IndexedPack& m, QJsonObject& obj); } // namespace ATLauncher Q_DECLARE_METATYPE(ATLauncher::IndexedPack) Q_DECLARE_METATYPE(QList) PrismLauncher-10.0.5/launcher/modplatform/CheckUpdateTask.h0000644000175100017510000000473315144136756023303 0ustar runnerrunner#pragma once #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "tasks/Task.h" class ResourceDownloadTask; class ModFolderModel; class CheckUpdateTask : public Task { Q_OBJECT public: CheckUpdateTask(QList& resources, std::list& mcVersions, QList loadersList, std::shared_ptr resourceModel) : Task() , m_resources(resources) , m_gameVersions(mcVersions) , m_loadersList(std::move(loadersList)) , m_resourceModel(std::move(resourceModel)) {} struct Update { QString name; QString old_hash; QString old_version; QString new_version; std::optional new_version_type; QString changelog; ModPlatform::ResourceProvider provider; shared_qobject_ptr download; bool enabled = true; public: Update(QString name, QString old_h, QString old_v, QString new_v, std::optional new_v_type, QString changelog, ModPlatform::ResourceProvider p, shared_qobject_ptr t, bool enabled = true) : name(std::move(name)) , old_hash(std::move(old_h)) , old_version(std::move(old_v)) , new_version(std::move(new_v)) , new_version_type(std::move(new_v_type)) , changelog(std::move(changelog)) , provider(p) , download(std::move(t)) , enabled(enabled) {} }; auto getUpdates() -> std::vector&& { return std::move(m_updates); } auto getDependencies() -> QList>&& { return std::move(m_deps); } public slots: bool abort() override = 0; protected slots: void executeTask() override = 0; signals: void checkFailed(Resource* failed, QString reason, QUrl recover_url = {}); protected: QList& m_resources; std::list& m_gameVersions; QList m_loadersList; std::shared_ptr m_resourceModel; std::vector m_updates; QList> m_deps; }; PrismLauncher-10.0.5/launcher/modplatform/ResourceAPI.cpp0000644000175100017510000002766115144136756022761 0ustar runnerrunner#include "modplatform/ResourceAPI.h" #include "Application.h" #include "Json.h" #include "net/NetJob.h" #include "modplatform/ModIndex.h" #include "net/ApiDownload.h" Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback>&& callbacks) const { auto search_url_optional = getSearchURL(args); if (!search_url_optional.has_value()) { callbacks.on_fail("Failed to create search URL", -1); return nullptr; } auto search_url = search_url_optional.value(); auto response = std::make_shared(); auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(search_url), response)); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from" << debugName() << "at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; callbacks.on_fail(parse_error.errorString(), -1); return; } QList newList; auto packs = documentToArray(doc); for (auto packRaw : packs) { auto packObj = packRaw.toObject(); ModPlatform::IndexedPack::Ptr pack = std::make_shared(); try { loadIndexedPack(*pack, packObj); newList << pack; } catch (const JSONValidationError& e) { qWarning().nospace() << "Error while loading resource from " << debugName() << ": " << e.cause(); continue; } } callbacks.on_succeed(newList); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. // This prevents the lambda from extending the lifetime of the shared resource, // as it only temporarily locks the resource when needed. auto weak = netJob.toWeakRef(); QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; if (auto netJob = weak.lock()) { if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) network_error_code = failed_action->replyStatusCode(); } callbacks.on_fail(reason, network_error_code); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { if (callbacks.on_abort != nullptr) callbacks.on_abort(); }); return netJob; } Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const { auto versions_url_optional = getVersionsURL(args); if (!versions_url_optional.has_value()) return nullptr; auto versions_url = versions_url_optional.value(); auto netJob = makeShared(QString("%1::Versions").arg(args.pack->name), APPLICATION->network()); auto response = std::make_shared(); netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response for getting versions at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } QVector unsortedVersions; try { auto arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array(); for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj, args.resourceType); if (!file.addonId.isValid()) file.addonId = args.pack->addonId; if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format return a.date > b.date; }; std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading" << debugName() << "resource version:" << e.cause(); } callbacks.on_succeed(unsortedVersions); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. // This prevents the lambda from extending the lifetime of the shared resource, // as it only temporarily locks the resource when needed. auto weak = netJob.toWeakRef(); QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; if (auto netJob = weak.lock()) { if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) network_error_code = failed_action->replyStatusCode(); } callbacks.on_fail(reason, network_error_code); }); QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { if (callbacks.on_abort != nullptr) callbacks.on_abort(); }); return netJob; } Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback&& callbacks) const { auto response = std::make_shared(); auto job = getProject(args.pack->addonId.toString(), response); QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] { auto pack = args.pack; QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response for mod info at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } try { auto obj = Json::requireObject(doc); if (obj.contains("data")) obj = Json::requireObject(obj, "data"); loadIndexedPack(*pack, obj); loadExtraPackInfo(*pack, obj); } catch (const JSONValidationError& e) { qDebug() << doc; qWarning() << "Error while reading" << debugName() << "resource info:" << e.cause(); } callbacks.on_succeed(pack); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. // This prevents the lambda from extending the lifetime of the shared resource, // as it only temporarily locks the resource when needed. auto weak = job.toWeakRef(); QObject::connect(job.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; if (auto job = weak.lock()) { if (auto netJob = qSharedPointerDynamicCast(job)) { if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) { network_error_code = failed_action->replyStatusCode(); } } } callbacks.on_fail(reason, network_error_code); }); QObject::connect(job.get(), &NetJob::aborted, [callbacks] { if (callbacks.on_abort != nullptr) callbacks.on_abort(); }); return job; } Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callback&& callbacks) const { auto versions_url_optional = getDependencyURL(args); if (!versions_url_optional.has_value()) return nullptr; auto versions_url = versions_url_optional.value(); auto netJob = makeShared(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network()); auto response = std::make_shared(); netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response for getting dependency version at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } QJsonArray arr; if (args.dependency.version.length() != 0 && doc.isObject()) { arr.append(doc.object()); } else { arr = doc.isObject() ? doc.object()["data"].toArray() : doc.array(); } QVector versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod); if (!file.addonId.isValid()) file.addonId = args.dependency.addonId; if (file.fileId.isValid() && (!file.loaders || args.loader & file.loaders)) // Heuristic to check if the returned value is valid versions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format return a.date > b.date; }; std::sort(versions.begin(), versions.end(), orderSortPredicate); auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion(); callbacks.on_succeed(bestMatch); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. // This prevents the lambda from extending the lifetime of the shared resource, // as it only temporarily locks the resource when needed. auto weak = netJob.toWeakRef(); QObject::connect(netJob.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { int network_error_code = -1; if (auto netJob = weak.lock()) { if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) network_error_code = failed_action->replyStatusCode(); } callbacks.on_fail(reason, network_error_code); }); return netJob; } QString ResourceAPI::getGameVersionsString(std::list mcVersions) const { QString s; for (auto& ver : mcVersions) { s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver)); } s.remove(s.length() - 1, 1); // remove last comma return s; } QString ResourceAPI::mapMCVersionToModrinth(Version v) const { static const QString preString = " Pre-Release "; auto verStr = v.toString(); if (verStr.contains(preString)) { verStr.replace(preString, "-pre"); } verStr.replace(" ", "-"); return verStr; } Task::Ptr ResourceAPI::getProject(QString addonId, std::shared_ptr response) const { auto project_url_optional = getInfoURL(addonId); if (!project_url_optional.has_value()) return nullptr; auto project_url = project_url_optional.value(); auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response)); return netJob; } PrismLauncher-10.0.5/launcher/modplatform/ModIndex.cpp0000644000175100017510000001176115144136756022341 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "modplatform/ModIndex.h" #include #include #include namespace ModPlatform { static const QMap s_indexed_version_type_names = { { "release", IndexedVersionType::Release }, { "beta", IndexedVersionType::Beta }, { "alpha", IndexedVersionType::Alpha } }; static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA, LegacyFabric, Ornithe, Rift }; QList modLoaderTypesToList(ModLoaderTypes flags) { QList flagList; for (auto flag : loaderList) { if (flags.testFlag(flag)) { flagList.append(flag); } } return flagList; } QString IndexedVersionType::toString() const { return s_indexed_version_type_names.key(m_type, "unknown"); } IndexedVersionType IndexedVersionType::fromString(const QString& type) { return s_indexed_version_type_names.value(type, IndexedVersionType::Unknown); } const char* ProviderCapabilities::name(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: return "modrinth"; case ResourceProvider::FLAME: return "curseforge"; } return {}; } QString ProviderCapabilities::readableName(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: return "Modrinth"; case ResourceProvider::FLAME: return "CurseForge"; } return {}; } QStringList ProviderCapabilities::hashType(ResourceProvider p) { switch (p) { case ResourceProvider::MODRINTH: return { "sha512", "sha1" }; case ResourceProvider::FLAME: // Try newer formats first, fall back to old format return { "sha1", "md5", "murmur2" }; } return {}; } QString getMetaURL(ResourceProvider provider, QVariant projectID) { return ((provider == ModPlatform::ResourceProvider::FLAME) ? "https://www.curseforge.com/projects/" : "https://modrinth.com/mod/") + projectID.toString(); } auto getModLoaderAsString(ModLoaderType type) -> const QString { switch (type) { case NeoForge: return "neoforge"; case Forge: return "forge"; case Cauldron: return "cauldron"; case LiteLoader: return "liteloader"; case Fabric: return "fabric"; case Quilt: return "quilt"; case DataPack: return "datapack"; case Babric: return "babric"; case BTA: return "bta-babric"; case LegacyFabric: return "legacy-fabric"; case Ornithe: return "ornithe"; case Rift: return "rift"; default: break; } return ""; } auto getModLoaderFromString(QString type) -> ModLoaderType { if (type == "neoforge") return NeoForge; if (type == "forge") return Forge; if (type == "cauldron") return Cauldron; if (type == "liteloader") return LiteLoader; if (type == "fabric") return Fabric; if (type == "quilt") return Quilt; if (type == "babric") return Babric; if (type == "bta-babric") return BTA; if (type == "legacy-fabric") return LegacyFabric; if (type == "ornithe") return Ornithe; if (type == "rift") return Rift; return {}; } QString SideUtils::toString(Side side) { switch (side) { case Side::ClientSide: return "client"; case Side::ServerSide: return "server"; case Side::UniversalSide: return "both"; case Side::NoSide: break; } return {}; } Side SideUtils::fromString(QString side) { if (side == "client") return Side::ClientSide; if (side == "server") return Side::ServerSide; if (side == "both") return Side::UniversalSide; return Side::UniversalSide; } } // namespace ModPlatform PrismLauncher-10.0.5/launcher/modplatform/flame/0000755000175100017510000000000015144136756021204 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/flame/FileResolvingTask.cpp0000644000175100017510000002562215144136756025312 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "FileResolvingTask.h" #include #include "Json.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" #include "net/NetJob.h" #include "tasks/Task.h" static const FlameAPI flameAPI; static ModrinthAPI modrinthAPI; Flame::FileResolvingTask::FileResolvingTask(Flame::Manifest& toProcess) : m_manifest(toProcess) {} bool Flame::FileResolvingTask::abort() { bool aborted = true; if (m_task) { aborted = m_task->abort(); } return aborted ? Task::abort() : false; } void Flame::FileResolvingTask::executeTask() { if (m_manifest.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately emitSucceeded(); return; } setStatus(tr("Resolving mod IDs...")); setProgress(0, 3); m_result.reset(new QByteArray()); QStringList fileIds; for (auto file : m_manifest.files) { fileIds.push_back(QString::number(file.fileId)); } m_task = flameAPI.getFiles(fileIds, m_result); auto step_progress = std::make_shared(); connect(m_task.get(), &Task::finished, this, [this, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); netJobFinished(); }); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); emitFailed(reason); }); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); m_task->start(); } ModPlatform::ResourceType getResourceType(int classId) { switch (classId) { case 17: // Worlds return ModPlatform::ResourceType::World; case 6: // Mods return ModPlatform::ResourceType::Mod; case 12: // Resource Packs // return ModPlatform::ResourceType::ResourcePack; // not really a resourcepack /* fallthrough */ case 4546: // Customization // return ModPlatform::ResourceType::ShaderPack; // not really a shaderPack /* fallthrough */ case 4471: // Modpacks /* fallthrough */ case 5: // Bukkit Plugins /* fallthrough */ case 4559: // Addons /* fallthrough */ default: return ModPlatform::ResourceType::Unknown; } } void Flame::FileResolvingTask::netJobFinished() { setProgress(1, 3); // job to check modrinth for blocked projects QJsonDocument doc; QJsonArray array; try { doc = Json::requireDocument(*m_result); array = Json::requireArray(doc.object()["data"]); } catch (Json::JsonException& e) { qCritical() << "Non-JSON data returned from the CF API"; qCritical() << e.cause(); emitFailed(tr("Invalid data returned from the API.")); return; } QStringList hashes; for (QJsonValueRef file : array) { try { auto obj = Json::requireObject(file); auto version = FlameMod::loadIndexedPackVersion(obj); auto fileid = version.fileId.toInt(); Q_ASSERT(fileid != 0); Q_ASSERT(m_manifest.files.contains(fileid)); m_manifest.files[fileid].version = version; auto url = QUrl(version.downloadUrl, QUrl::TolerantMode); if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) { hashes.push_back(version.hash); } } catch (Json::JsonException& e) { qCritical() << "Non-JSON data returned from the CF API"; qCritical() << e.cause(); emitFailed(tr("Invalid data returned from the API.")); return; } } if (hashes.isEmpty()) { getFlameProjects(); return; } m_result.reset(new QByteArray()); m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result); (dynamic_cast(m_task.get()))->setAskRetry(false); auto step_progress = std::make_shared(); connect(m_task.get(), &Task::finished, this, [this, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *m_result; getFlameProjects(); return; } try { auto entries = Json::requireObject(doc); for (auto& out : m_manifest.files) { auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode); if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) { try { auto entry = Json::requireObject(entries, out.version.hash); auto file = Modrinth::loadIndexedPackVersion(entry); // If there's more than one mod loader for this version, we can't know for sure // which file is relative to each loader, so it's best to not use any one and // let the user download it manually. if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { out.version.downloadUrl = file.downloadUrl; qDebug() << "Found alternative on modrinth" << out.version.fileName; } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; } } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } getFlameProjects(); }); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); }); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); m_task->start(); } void Flame::FileResolvingTask::getFlameProjects() { setProgress(2, 3); m_result.reset(new QByteArray()); QStringList addonIds; for (auto file : m_manifest.files) { addonIds.push_back(QString::number(file.projectId)); } m_task = flameAPI.getProjects(addonIds, m_result); auto step_progress = std::make_shared(); connect(m_task.get(), &Task::succeeded, this, [this, step_progress] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*m_result, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth projects task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *m_result; return; } try { QJsonArray entries; entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entry_obj = Json::requireObject(entry); auto id = Json::requireInteger(entry_obj, "id"); auto file = std::find_if(m_manifest.files.begin(), m_manifest.files.end(), [id](const Flame::File& file) { return file.projectId == id; }); if (file == m_manifest.files.end()) { continue; } setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName)); FlameMod::loadIndexedPack(file->pack, entry_obj); file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId")); if (file->resourceType == ModPlatform::ResourceType::World) { file->targetFolder = "saves"; } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); emitSucceeded(); }); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); emitFailed(reason); }); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); m_task->start(); } PrismLauncher-10.0.5/launcher/modplatform/flame/FlameInstanceCreationTask.cpp0000644000175100017510000006757115144136756026751 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "FlameInstanceCreationTask.h" #include "QObjectPtr.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "modplatform/flame/PackManifest.h" #include "Application.h" #include "FileSystem.h" #include "InstanceList.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/helpers/OverrideUtils.h" #include "settings/INISettingsObject.h" #include "sys.h" #include "tasks/ConcurrentTask.h" #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include #include #include "meta/Index.h" #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.h" #include "net/ApiDownload.h" #include "ui/pages/modplatform/OptionalModDialog.h" static const FlameAPI api; bool FlameCreationTask::abort() { if (!canAbort()) return false; m_abort = true; if (m_processUpdateFileInfoJob) m_processUpdateFileInfoJob->abort(); if (m_filesJob) m_filesJob->abort(); if (m_modIdResolver) m_modIdResolver->abort(); return Task::abort(); } bool FlameCreationTask::updateInstance() { auto instance_list = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? InstancePtr inst; if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { inst = instance_list->getInstanceById(original_id); Q_ASSERT(inst); } else { inst = instance_list->getInstanceByManagedName(originalName()); if (!inst) { inst = instance_list->getInstanceById(originalName()); if (!inst) return false; } } QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); try { Flame::loadManifest(m_pack, index_path); } catch (const JSONValidationError& e) { setError(tr("Could not understand pack manifest:\n") + e.cause()); return false; } auto version_id = inst->getManagedPackVersionName(); auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : ""; if (shouldConfirmUpdate()) { auto should_update = askIfShouldUpdate(m_parent, version_str); if (should_update == ShouldUpdate::SkipUpdating) return false; if (should_update == ShouldUpdate::Cancel) { m_abort = true; return false; } } QDir old_inst_dir(inst->instanceRoot()); QString old_index_folder(FS::PathCombine(old_inst_dir.absolutePath(), "flame")); QString old_index_path(FS::PathCombine(old_index_folder, "manifest.json")); QFileInfo old_index_file(old_index_path); if (old_index_file.exists()) { Flame::Manifest old_pack; Flame::loadManifest(old_pack, old_index_path); auto& old_files = old_pack.files; auto& files = m_pack.files; // Remove repeated files, we don't need to download them! auto files_iterator = files.begin(); while (files_iterator != files.end()) { auto const& file = files_iterator; auto old_file = old_files.find(file.key()); if (old_file != old_files.end()) { // We found a match, but is it a different version? if (old_file->fileId == file->fileId) { qDebug() << "Removed file at" << file->targetFolder << "with id" << file->fileId << "from list of downloads"; old_files.remove(file.key()); files_iterator = files.erase(files_iterator); if (files_iterator != files.begin()) files_iterator--; } } files_iterator++; } QDir old_minecraft_dir(inst->gameRoot()); // We will remove all the previous overrides, to prevent duplicate files! // TODO: Currently 'overrides' will always override the stuff on update. How do we preserve unchanged overrides? // FIXME: We may want to do something about disabled mods. auto old_overrides = Override::readOverrides("overrides", old_index_folder); for (const auto& entry : old_overrides) { if (entry.isEmpty()) continue; qDebug() << "Scheduling" << entry << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(entry)); } // Remove remaining old files (we need to do an API request to know which ids are which files...) QStringList fileIds; for (auto& file : old_files) { fileIds.append(QString::number(file.fileId)); } auto raw_response = std::make_shared(); auto job = api.getFiles(fileIds, raw_response); QEventLoop loop; connect(job.get(), &Task::succeeded, this, [this, raw_response, fileIds, old_inst_dir, &old_files, old_minecraft_dir] { // Parse the API response QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*raw_response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame files task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *raw_response; return; } try { QJsonArray entries; if (fileIds.size() == 1) entries = { Json::requireObject(Json::requireObject(doc), "data") }; else entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entry_obj = Json::requireObject(entry); Flame::File file; // We don't care about blocked mods, we just need local data to delete the file file.version = FlameMod::loadIndexedPackVersion(entry_obj); auto id = Json::requireInteger(entry_obj, "id"); old_files.insert(id, file); } } catch (Json::JsonException& e) { qCritical() << e.cause() << e.what(); } // Delete the files for (auto& file : old_files) { if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty()) continue; QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName)); qDebug() << "Scheduling" << relative_path << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); if (relative_path.endsWith(".disabled")) { // remove it if it was enabled/disabled by user m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path.chopped(9))); } else { m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path + ".disabled")); } } }); connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files:" << reason; }); connect(job.get(), &Task::finished, &loop, &QEventLoop::quit); m_processUpdateFileInfoJob = job; job->start(); loop.exec(); m_processUpdateFileInfoJob = nullptr; } else { // We don't have an old index file, so we may duplicate stuff! auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), tr("We couldn't find a suitable index file for the older version. This may cause some " "of the files to be duplicated. Do you want to continue?"), QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); if (dialog->exec() == QDialog::DialogCode::Rejected) { m_abort = true; return false; } } setOverride(true, inst->id()); qDebug() << "Will override instance!"; m_instance = inst; // We let it go through the createInstance() stage, just with a couple modifications for updating return false; } QString FlameCreationTask::getVersionForLoader(QString uid, QString loaderType, QString loaderVersion, QString mcVersion) { if (loaderVersion == "recommended") { auto vlist = APPLICATION->metadataIndex()->get(uid); if (!vlist) { setError(tr("Failed to get local metadata index for %1").arg(uid)); return {}; } if (!vlist->isLoaded()) { QEventLoop loadVersionLoop; auto task = vlist->getLoadTask(); connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); if (!task->isRunning()) task->start(); loadVersionLoop.exec(); } for (auto version : vlist->versions()) { // first recommended build we find, we use. if (!version->isRecommended()) continue; auto reqs = version->requiredSet(); // filter by minecraft version, if the loader depends on a certain version. // not all mod loaders depend on a given Minecraft version, so we won't do this // filtering for those loaders. if (loaderType == "forge" || loaderType == "neoforge") { auto iter = std::find_if(reqs.begin(), reqs.end(), [mcVersion](const Meta::Require& req) { return req.uid == "net.minecraft" && req.equalsVersion == mcVersion; }); if (iter == reqs.end()) continue; } return version->descriptor(); } setError(tr("Failed to find version for %1 loader").arg(loaderType)); return {}; } if (loaderVersion.isEmpty()) { emitFailed(tr("No loader version set for modpack!")); return {}; } return loaderVersion; } bool FlameCreationTask::createInstance() { QEventLoop loop; QString parent_folder(FS::PathCombine(m_stagingPath, "flame")); try { QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); if (!m_pack.is_loaded) Flame::loadManifest(m_pack, index_path); // Keep index file in case we need it some other time (like when changing versions) QString new_index_place(FS::PathCombine(parent_folder, "manifest.json")); FS::ensureFilePathExists(new_index_place); FS::move(index_path, new_index_place); } catch (const JSONValidationError& e) { setError(tr("Could not understand pack manifest:\n") + e.cause()); return false; } if (!m_pack.overrides.isEmpty()) { QString overridePath = FS::PathCombine(m_stagingPath, m_pack.overrides); if (QFile::exists(overridePath)) { // Create a list of overrides in "overrides.txt" inside flame/ Override::createOverrides("overrides", parent_folder, overridePath); QString mcPath = FS::PathCombine(m_stagingPath, "minecraft"); if (!FS::move(overridePath, mcPath)) { setError(tr("Could not rename the overrides folder:\n") + m_pack.overrides); return false; } } else { logWarning( tr("The specified overrides folder (%1) is missing. Maybe the modpack was already used before?").arg(m_pack.overrides)); } } QString loaderType; QString loaderUid; QString loaderVersion; for (auto& loader : m_pack.minecraft.modLoaders) { auto id = loader.id; if (id.startsWith("neoforge-")) { id.remove("neoforge-"); if (id.startsWith("1.20.1-")) id.remove("1.20.1-"); // this is a mess for curseforge loaderType = "neoforge"; loaderUid = "net.neoforged"; } else if (id.startsWith("forge-")) { id.remove("forge-"); loaderType = "forge"; loaderUid = "net.minecraftforge"; } else if (id.startsWith("fabric-")) { id.remove("fabric-"); loaderType = "fabric"; loaderUid = "net.fabricmc.fabric-loader"; } else if (id.startsWith("quilt-")) { id.remove("quilt-"); loaderType = "quilt"; loaderUid = "org.quiltmc.quilt-loader"; } else { logWarning(tr("Unknown mod loader in manifest: %1").arg(id)); continue; } loaderVersion = id; } QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto mcVersion = m_pack.minecraft.version; // Hack to correct some 'special sauce'... if (mcVersion.endsWith('.')) { static const QRegularExpression s_regex("[.]+$"); mcVersion.remove(s_regex); logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); } auto components = instance.getPackProfile(); components->buildingFromScratch(); components->setComponentVersion("net.minecraft", mcVersion, true); if (!loaderType.isEmpty()) { auto version = getVersionForLoader(loaderUid, loaderType, loaderVersion, mcVersion); if (version.isEmpty()) return false; components->setComponentVersion(loaderUid, version); } if (m_instIcon != "default") { instance.setIconKey(m_instIcon); } else { if (m_pack.name.contains("Direwolf20")) { instance.setIconKey("steve"); } else if (m_pack.name.contains("FTB") || m_pack.name.contains("Feed The Beast")) { instance.setIconKey("ftb_logo"); } else { instance.setIconKey("flame"); } } int recommendedRAM = m_pack.minecraft.recommendedRAM; // only set memory if this is a fresh instance if (m_instance == nullptr && recommendedRAM > 0) { const uint64_t sysMiB = Sys::getSystemRam() / Sys::mebibyte; const uint64_t max = sysMiB * 0.9; if (static_cast(recommendedRAM) > max) { logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 MiB to %2 MiB!") .arg(recommendedRAM) .arg(max)); recommendedRAM = max; } instance.settings()->set("OverrideMemory", true); instance.settings()->set("MaxMemAlloc", recommendedRAM); } QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); QFileInfo jarmodsInfo(jarmodsPath); if (jarmodsInfo.isDir()) { // install all the jar mods qDebug() << "Found jarmods:"; QDir jarmodsDir(jarmodsPath); QStringList jarMods; for (const auto& info : jarmodsDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) { qDebug() << info.fileName(); jarMods.push_back(info.absoluteFilePath()); } auto profile = instance.getPackProfile(); profile->installJarMods(jarMods); // nuke the original files FS::deletePath(jarmodsPath); } // Don't add managed info to packs without an ID (most likely imported from ZIP) if (!m_managedId.isEmpty()) instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version); else instance.setManagedPack("flame", "", name(), "", ""); instance.setName(name()); m_modIdResolver.reset(new Flame::FileResolvingTask(m_pack)); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) { m_modIdResolver.reset(); setError(tr("Unable to resolve mod IDs:\n") + reason); loop.quit(); }); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress); connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails); m_modIdResolver->start(); loop.exec(); bool did_succeed = getError().isEmpty(); // Update information of the already installed instance, if any. if (m_instance && did_succeed) { setAbortable(false); auto inst = m_instance.value(); inst->copyManagedPack(instance); } return did_succeed; } void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) { auto results = m_modIdResolver->getResults().files; QStringList optionalFiles; for (auto& result : results) { if (!result.required) { optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); } } if (!optionalFiles.empty()) { OptionalModDialog optionalModDialog(m_parent, optionalFiles); if (optionalModDialog.exec() == QDialog::Rejected) { emitAborted(); loop.quit(); return; } m_selectedOptionalMods = optionalModDialog.getResult(); } // first check for blocked mods QList blocked_mods; auto anyBlocked = false; for (const auto& result : results.values()) { if (result.resourceType != ModPlatform::ResourceType::Mod) { m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder)); } // skip optional mods that were not selected if (result.version.downloadUrl.isEmpty()) { BlockedMod blocked_mod; blocked_mod.name = result.version.fileName; blocked_mod.websiteUrl = QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId)); blocked_mod.hash = result.version.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; blocked_mod.targetFolder = result.targetFolder; auto fileName = result.version.fileName; fileName = FS::RemoveInvalidPathChars(fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName); blocked_mod.disabled = !result.required && !m_selectedOptionalMods.contains(relpath); blocked_mods.append(blocked_mod); anyBlocked = true; } } if (anyBlocked) { qWarning() << "Blocked mods found, displaying mod list"; BlockedModsDialog message_dialog(m_parent, tr("Blocked mods found"), tr("The following files are not available for download in third party launchers.
    " "You will need to manually download them and add them to the instance."), blocked_mods); message_dialog.setModal(true); if (message_dialog.exec()) { qDebug() << "Post dialog blocked mods list:" << blocked_mods; copyBlockedMods(blocked_mods); setupDownloadJob(loop); } else { m_modIdResolver.reset(); setError("Canceled"); loop.quit(); } } else { setupDownloadJob(loop); } } void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); auto results = m_modIdResolver->getResults().files; for (const auto& result : results) { auto fileName = result.version.fileName; fileName = FS::RemoveInvalidPathChars(fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName); if (!result.required && !m_selectedOptionalMods.contains(relpath)) { relpath += ".disabled"; } relpath = FS::PathCombine("minecraft", relpath); auto path = FS::PathCombine(m_stagingPath, relpath); if (!result.version.downloadUrl.isEmpty()) { qDebug() << "Will download" << result.version.downloadUrl << "to" << path; auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path); m_filesJob->addNetAction(dl); } } connect(m_filesJob.get(), &NetJob::finished, this, [this, &loop]() { m_filesJob.reset(); validateOtherResources(loop); }); connect(m_filesJob.get(), &NetJob::failed, [this](QString reason) { m_filesJob.reset(); setError(reason); }); connect(m_filesJob.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); m_filesJob->start(); } /// @brief copy the matched blocked mods to the instance staging area /// @param blocked_mods list of the blocked mods and their matched paths void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) { setStatus(tr("Copying Blocked Mods...")); setAbortable(false); int i = 0; int total = blocked_mods.length(); setProgress(i, total); for (auto const& mod : blocked_mods) { if (!mod.matched) { qDebug() << mod.name << "was not matched to a local file, skipping copy"; continue; } auto destPath = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); if (mod.disabled) destPath += ".disabled"; setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); qDebug() << "Will try to copy" << mod.localPath << "to" << destPath; if (mod.move) { if (!FS::move(mod.localPath, destPath)) { qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed"; } } else { if (!FS::copy(mod.localPath, destPath)()) { qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed"; } } i++; setProgress(i, total); } setAbortable(true); } void FlameCreationTask::validateOtherResources(QEventLoop& loop) { qDebug() << "Validating whether other resources are in the right place"; QStringList zipMods; for (auto [fileName, targetFolder] : m_otherResources) { qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); /// @brief check the target and move the the file /// @return path where file can now be found auto validatePath = [&localPath, this](QString fileName, QString targetFolder, QString realTarget) { if (targetFolder != realTarget) { qDebug() << "Target folder of" << fileName << "is incorrect, it belongs in" << realTarget; auto destPath = FS::PathCombine(m_stagingPath, "minecraft", realTarget, fileName); qDebug() << "Moving" << localPath << "to" << destPath; if (FS::move(localPath, destPath)) { return destPath; } } else { qDebug() << "Target folder of" << fileName << "is correct at" << targetFolder; } return localPath; }; auto installWorld = [this](QString worldPath) { qDebug() << "Installing World from" << worldPath; QFileInfo worldFileInfo(worldPath); World w(worldFileInfo); if (!w.isValid()) { qDebug() << "World at" << worldPath << "is not valid, skipping install."; } else { w.install(FS::PathCombine(m_stagingPath, "minecraft", "saves")); } }; QFileInfo localFileInfo(localPath); auto type = ResourceUtils::identify(localFileInfo); QString worldPath; switch (type) { case ModPlatform::ResourceType::Mod: validatePath(fileName, targetFolder, "mods"); zipMods.push_back(fileName); break; case ModPlatform::ResourceType::ResourcePack: validatePath(fileName, targetFolder, "resourcepacks"); break; case ModPlatform::ResourceType::TexturePack: validatePath(fileName, targetFolder, "texturepacks"); break; case ModPlatform::ResourceType::DataPack: validatePath(fileName, targetFolder, "datapacks"); break; case ModPlatform::ResourceType::ShaderPack: // in theory flame API can't do this but who knows, that *may* change ? // better to handle it if it *does* occur in the future validatePath(fileName, targetFolder, "shaderpacks"); break; case ModPlatform::ResourceType::World: worldPath = validatePath(fileName, targetFolder, "saves"); installWorld(worldPath); break; case ModPlatform::ResourceType::Unknown: /* fallthrough */ default: qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; break; } } // TODO make this work with other sorts of resource auto task = makeShared("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); auto results = m_modIdResolver->getResults().files; auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index"); for (auto file : results) { if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) { continue; } task->addTask(makeShared(folder, file.pack, file.version)); } connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); m_processUpdateFileInfoJob = task; task->start(); } PrismLauncher-10.0.5/launcher/modplatform/flame/FlameAPI.cpp0000644000175100017510000002420115144136756023265 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #include "FlameAPI.h" #include #include #include "BuildConfig.h" #include "FlameModIndex.h" #include "Application.h" #include "Json.h" #include "modplatform/ModIndex.h" #include "net/ApiDownload.h" #include "net/ApiUpload.h" #include "net/NetJob.h" Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shared_ptr response) { auto netJob = makeShared(QString("Flame::MatchFingerprints"), APPLICATION->network()); QJsonObject body_obj; QJsonArray fingerprints_arr; for (auto& fp : fingerprints) { fingerprints_arr.append(QString("%1").arg(fp)); } body_obj["fingerprints"] = fingerprints_arr; QJsonDocument body(body_obj); auto body_raw = body.toJson(); netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/fingerprints"), response, body_raw)); return netJob; } QString FlameAPI::getModFileChangelog(int modId, int fileId) { QEventLoop lock; QString changelog; auto netJob = makeShared(QString("Flame::FileChangelog"), APPLICATION->network()); auto response = std::make_shared(); netJob->addNetAction(Net::ApiDownload::makeByteArray( QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2/changelog") .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), response)); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &changelog] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame::FileChangelog at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; netJob->failed(parse_error.errorString()); return; } changelog = doc.object()["data"].toString(); }); QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); netJob->start(); lock.exec(); return changelog; } QString FlameAPI::getModDescription(int modId) { QEventLoop lock; QString description; auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); auto response = std::make_shared(); netJob->addNetAction(Net::ApiDownload::makeByteArray( QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/description").arg(QString::number(modId)), response)); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame::ModDescription at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; netJob->failed(parse_error.errorString()); return; } description = doc.object()["data"].toString(); }); QObject::connect(netJob.get(), &NetJob::finished, [&lock] { lock.quit(); }); netJob->start(); lock.exec(); return description; } Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Flame::GetProjects"), APPLICATION->network()); QJsonObject body_obj; QJsonArray addons_arr; for (auto& addonId : addonIds) { addons_arr.append(addonId); } body_obj["modIds"] = addons_arr; QJsonDocument body(body_obj); auto body_raw = body.toJson(); netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods"), response, body_raw)); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr response) const { auto netJob = makeShared(QString("Flame::GetFiles"), APPLICATION->network()); QJsonObject body_obj; QJsonArray files_arr; for (auto& fileId : fileIds) { files_arr.append(fileId); } body_obj["fileIds"] = files_arr; QJsonDocument body(body_obj); auto body_raw = body.toJson(); netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/files"), response, body_raw)); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const { auto netJob = makeShared(QString("Flame::GetFile"), APPLICATION->network()); netJob->addNetAction( Net::ApiDownload::makeByteArray(QUrl(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2").arg(addonId, fileId)), response)); QObject::connect(netJob.get(), &NetJob::failed, [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; }); return netJob; } QList FlameAPI::getSortingMethods() const { // https://docs.curseforge.com/?python#tocS_ModsSearchSortField return { { 1, "Featured", QObject::tr("Sort by Featured") }, { 2, "Popularity", QObject::tr("Sort by Popularity") }, { 3, "LastUpdated", QObject::tr("Sort by Last Updated") }, { 4, "Name", QObject::tr("Sort by Name") }, { 5, "Author", QObject::tr("Sort by Author") }, { 6, "TotalDownloads", QObject::tr("Sort by Downloads") }, { 7, "Category", QObject::tr("Sort by Category") }, { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; } Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatform::ResourceType type) { auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray( QUrl(QString(BuildConfig.FLAME_BASE_URL + "/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); return netJob; } Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) { return getCategories(response, ModPlatform::ResourceType::Mod); } QList FlameAPI::loadModCategories(std::shared_ptr response) { QList categories; QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from categories at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return categories; } try { auto obj = Json::requireObject(doc); auto arr = Json::requireArray(obj, "data"); for (auto val : arr) { auto cat = Json::requireObject(val); auto id = Json::requireInteger(cat, "id"); auto name = Json::requireString(cat, "name"); categories.push_back({ name, QString::number(id) }); } } catch (Json::JsonException& e) { qCritical() << "Failed to parse response from a version request."; qCritical() << e.what(); qDebug() << doc; } return categories; }; std::optional FlameAPI::getLatestVersion(QList versions, QList instanceLoaders, ModPlatform::ModLoaderTypes modLoaders, bool checkLoaders) { static const auto noLoader = ModPlatform::ModLoaderType(0); if (!checkLoaders) { std::optional ver; for (auto file_tmp : versions) { if (!ver.has_value() || file_tmp.date > ver->date) { ver = file_tmp; } } return ver; } QHash bestMatch; auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) { if (bestMatch.contains(loader)) { auto best = bestMatch.value(loader); if (version.date > best.date) { bestMatch[loader] = version; } } else { bestMatch[loader] = version; } }; for (auto file_tmp : versions) { auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders); if (loaders.isEmpty()) { checkVersion(file_tmp, noLoader); } else { for (auto loader : loaders) { checkVersion(file_tmp, loader); } } } // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders); currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader for (auto loader : currentLoaders) { if (bestMatch.contains(loader)) { auto bestForLoader = bestMatch.value(loader); // awkward case where the mod has only two loaders and one of them is not specified if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2) { auto bestForNoLoader = bestMatch.value(noLoader); if (bestForNoLoader.date > bestForLoader.date) { return bestForNoLoader; } } return bestForLoader; } } return {}; } PrismLauncher-10.0.5/launcher/modplatform/flame/PackManifest.cpp0000644000175100017510000000456615144136756024270 0ustar runnerrunner#include "PackManifest.h" #include "Json.h" static void loadFileV1(Flame::File& f, QJsonObject& file) { f.projectId = Json::requireInteger(file, "projectID"); f.fileId = Json::requireInteger(file, "fileID"); f.required = file["required"].toBool(true); } static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader) { m.id = Json::requireString(modLoader, "id"); m.primary = modLoader["primary"].toBool(); } static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) { m.version = Json::requireString(minecraft, "version"); // extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack // intended use is likely hardcoded in the 'Flame' client, the manifest says nothing m.libraries = minecraft["libraries"].toString(); auto arr = minecraft["modLoaders"].toArray(); for (QJsonValueRef item : arr) { auto obj = Json::requireObject(item); Flame::Modloader loader; loadModloaderV1(loader, obj); m.modLoaders.append(loader); } m.recommendedRAM = minecraft["recommendedRam"].toInt(); } static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) { auto mc = Json::requireObject(manifest, "minecraft"); loadMinecraftV1(pack.minecraft, mc); pack.name = manifest["name"].toString("Unnamed"); pack.version = manifest["version"].toString(); pack.author = manifest["author"].toString("Anonymous"); auto arr = manifest["files"].toArray(); for (auto item : arr) { auto obj = Json::requireObject(item); Flame::File file; loadFileV1(file, obj); Q_ASSERT(file.projectId != 0); pack.files.insert(file.fileId, file); } pack.overrides = manifest["overrides"].toString("overrides"); pack.is_loaded = true; } void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) { auto doc = Json::requireDocument(filepath); auto obj = Json::requireObject(doc); m.manifestType = Json::requireString(obj, "manifestType"); if (m.manifestType != "minecraftModpack") { throw JSONValidationError("Not a modpack manifest!"); } m.manifestVersion = Json::requireInteger(obj, "manifestVersion"); if (m.manifestVersion != 1) { throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion)); } loadManifestV1(m, obj); } PrismLauncher-10.0.5/launcher/modplatform/flame/FlameModIndex.h0000644000175100017510000000101015144136756024021 0ustar runnerrunner// // Created by timoreo on 16/01/2022. // #pragma once #include "modplatform/ModIndex.h" #include "BaseInstance.h" namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadBody(ModPlatform::IndexedPack& m); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr); ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false); } // namespace FlameMod PrismLauncher-10.0.5/launcher/modplatform/flame/PackManifest.h0000644000175100017510000000500015144136756023715 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include "modplatform/ModIndex.h" #include "modplatform/ResourceType.h" namespace Flame { struct File { int projectId = 0; int fileId = 0; // NOTE: the opposite to 'optional' bool required = true; ModPlatform::IndexedPack pack; ModPlatform::IndexedVersion version; // our QString targetFolder = QStringLiteral("mods"); ModPlatform::ResourceType resourceType; }; struct Modloader { QString id; bool primary = false; }; struct Minecraft { QString version; QString libraries; QList modLoaders; int recommendedRAM; }; struct Manifest { QString manifestType; int manifestVersion = 0; Flame::Minecraft minecraft; QString name; QString version; QString author; // File id -> File QMap files; QString overrides; bool is_loaded = false; }; void loadManifest(Flame::Manifest& m, const QString& filepath); } // namespace Flame PrismLauncher-10.0.5/launcher/modplatform/flame/FlameInstanceCreationTask.h0000644000175100017510000000626315144136756026405 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "InstanceCreationTask.h" #include #include "minecraft/MinecraftInstance.h" #include "modplatform/flame/FileResolvingTask.h" #include "net/NetJob.h" #include "ui/dialogs/BlockedModsDialog.h" class FlameCreationTask final : public InstanceCreationTask { Q_OBJECT public: FlameCreationTask(const QString& staging_path, SettingsObjectPtr global_settings, QWidget* parent, QString id, QString version_id, QString original_instance_id = {}) : InstanceCreationTask(), m_parent(parent), m_managedId(std::move(id)), m_managedVersionId(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); m_original_instance_id = std::move(original_instance_id); } bool abort() override; bool updateInstance() override; bool createInstance() override; private slots: void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); void copyBlockedMods(QList const& blocked_mods); void validateOtherResources(QEventLoop& loop); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); private: QWidget* m_parent = nullptr; shared_qobject_ptr m_modIdResolver; Flame::Manifest m_pack; // Handle to allow aborting Task::Ptr m_processUpdateFileInfoJob = nullptr; NetJob::Ptr m_filesJob = nullptr; QString m_managedId, m_managedVersionId; QList> m_otherResources; std::optional m_instance; QStringList m_selectedOptionalMods; }; PrismLauncher-10.0.5/launcher/modplatform/flame/FlameAPI.h0000644000175100017510000001727515144136756022747 0ustar runnerrunner// SPDX-FileCopyrightText: 2023 flowln // // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include "BuildConfig.h" #include "Json.h" #include "Version.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameModIndex.h" class FlameAPI : public ResourceAPI { public: QString getModFileChangelog(int modId, int fileId); QString getModDescription(int modId); std::optional getLatestVersion(QList versions, QList instanceLoaders, ModPlatform::ModLoaderTypes fallback, bool checkLoaders); Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; Task::Ptr matchFingerprints(const QList& fingerprints, std::shared_ptr response); Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const; static Task::Ptr getCategories(std::shared_ptr response, ModPlatform::ResourceType type); static Task::Ptr getModCategories(std::shared_ptr response); static QList loadModCategories(std::shared_ptr response); QList getSortingMethods() const override; static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders) { return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt); } private: static int getClassId(ModPlatform::ResourceType type) { switch (type) { default: case ModPlatform::ResourceType::Mod: return 6; case ModPlatform::ResourceType::ResourcePack: return 12; case ModPlatform::ResourceType::ShaderPack: return 6552; case ModPlatform::ResourceType::Modpack: return 4471; case ModPlatform::ResourceType::DataPack: return 6945; } } static int getMappedModLoader(ModPlatform::ModLoaderType loaders) { // https://docs.curseforge.com/?http#tocS_ModLoaderType switch (loaders) { case ModPlatform::Forge: return 1; case ModPlatform::Cauldron: return 2; case ModPlatform::LiteLoader: return 3; case ModPlatform::Fabric: return 4; case ModPlatform::Quilt: return 5; case ModPlatform::NeoForge: return 6; case ModPlatform::DataPack: case ModPlatform::Babric: case ModPlatform::BTA: case ModPlatform::LegacyFabric: case ModPlatform::Ornithe: case ModPlatform::Rift: break; // not supported } return 0; } static const QStringList getModLoaderStrings(const ModPlatform::ModLoaderTypes types) { QStringList l; for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) { if (types & loader) { l << QString::number(getMappedModLoader(loader)); } } return l; } static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; } public: std::optional getSearchURL(SearchArgs const& args) const override { QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); get_arguments.append(QString("index=%1").arg(args.offset)); get_arguments.append("pageSize=25"); if (args.search.has_value()) get_arguments.append(QString("searchFilter=%1").arg(args.search.value())); if (args.sorting.has_value()) get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); if (args.loaders.has_value()) { ModPlatform::ModLoaderTypes loaders = args.loaders.value(); loaders &= ~ModPlatform::ModLoaderType::DataPack; if (loaders != 0) get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(loaders))); } if (args.categoryIds.has_value() && !args.categoryIds->empty()) get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); if (args.versions.has_value() && !args.versions.value().empty()) get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString())); return BuildConfig.FLAME_BASE_URL + "/mods/search?gameId=432&" + get_arguments.join('&'); } std::optional getVersionsURL(VersionSearchArgs const& args) const override { auto addonId = args.pack->addonId.toString(); QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId); if (args.mcVersions.has_value()) url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loaders.value()))); url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; } QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object()["data"].toArray(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); } ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override { auto arr = FlameMod::loadIndexedPackVersion(obj); if (resourceType != ModPlatform::ResourceType::TexturePack) { return arr; } // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. auto const& mc_versions = arr.mcVersion; if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), [](auto const& mc_version) { return Version(mc_version) <= Version("1.6"); })) { return arr; } return {}; }; void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); } private: std::optional getInfoURL(QString const& id) const override { return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); } std::optional getDependencyURL(DependencySearchArgs const& args) const override { auto addonId = args.dependency.addonId.toString(); auto url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000&gameVersion=%2").arg(addonId, args.mcVersion.toString()); if (args.loader && ModPlatform::hasSingleModLoaderSelected(args.loader)) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loader))); url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; } }; PrismLauncher-10.0.5/launcher/modplatform/flame/FlamePackExportTask.cpp0000644000175100017510000004053515144136756025567 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "FlamePackExportTask.h" #include #include #include #include #include #include #include #include #include #include "Application.h" #include "Json.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/Task.h" #include "archive/ExportToZipTask.h" const QString FlamePackExportTask::TEMPLATE = "
  • {name}{authors}
  • \n"; const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); FlamePackExportTask::FlamePackExportTask(FlamePackExportOptions&& options) : m_options(std::move(options)), m_gameRoot(m_options.instance->gameRoot()) {} void FlamePackExportTask::executeTask() { setStatus(tr("Searching for files...")); setProgress(0, 5); collectFiles(); } bool FlamePackExportTask::abort() { if (task) { task->abort(); return true; } return false; } void FlamePackExportTask::collectFiles() { setAbortable(false); QCoreApplication::processEvents(); files.clear(); if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &files, m_options.filter)) { emitFailed(tr("Could not search for files")); return; } pendingHashes.clear(); resolvedFiles.clear(); m_options.instance->loaderModList()->update(); connect(m_options.instance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); } void FlamePackExportTask::collectHashes() { setAbortable(true); setStatus(tr("Finding file hashes...")); setProgress(1, 5); auto allMods = m_options.instance->loaderModList()->allMods(); ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); task.reset(hashingTask); for (const QFileInfo& file : files) { const QString relative = m_gameRoot.relativeFilePath(file.absoluteFilePath()); // require sensible file types if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); })) continue; if (relative.startsWith("resourcepacks/") && (relative.endsWith(".zip") || relative.endsWith(".zip.disabled"))) { // is resourcepack auto hashTask = Hashing::createHasher(file.absoluteFilePath(), ModPlatform::ResourceProvider::FLAME); connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, relative, file](QString hash) { if (m_state == Task::State::Running) { pendingHashes.insert(hash, { relative, file.absoluteFilePath(), relative.endsWith(".zip") }); } }); connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); hashingTask->addTask(hashTask); continue; } if (auto modIter = std::find_if(allMods.begin(), allMods.end(), [&file](Mod* mod) { return mod->fileinfo() == file; }); modIter != allMods.end()) { const Mod* mod = *modIter; if (!mod || mod->type() == ResourceType::FOLDER) { continue; } if (mod->metadata() && mod->metadata()->provider == ModPlatform::ResourceProvider::FLAME) { resolvedFiles.insert(mod->fileinfo().absoluteFilePath(), { mod->metadata()->project_id.toInt(), mod->metadata()->file_id.toInt(), mod->enabled(), true, mod->metadata()->name, mod->metadata()->slug, mod->authors().join(", ") }); continue; } auto hashTask = Hashing::createHasher(mod->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::FLAME); connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, mod](QString hash) { if (m_state == Task::State::Running) { pendingHashes.insert(hash, { mod->name(), mod->fileinfo().absoluteFilePath(), mod->enabled(), true }); } }); connect(hashTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); hashingTask->addTask(hashTask); } } auto progressStep = std::make_shared(); connect(hashingTask.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(hashingTask.get(), &Task::succeeded, this, &FlamePackExportTask::makeApiRequest); connect(hashingTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(hashingTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress); connect(hashingTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(hashingTask.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); hashingTask->start(); } void FlamePackExportTask::makeApiRequest() { if (pendingHashes.isEmpty()) { buildZip(); return; } setStatus(tr("Finding versions for hashes...")); setProgress(2, 5); auto response = std::make_shared(); QList fingerprints; for (auto& murmur : pendingHashes.keys()) { fingerprints.push_back(murmur.toUInt()); } task.reset(api.matchFingerprints(fingerprints, response)); connect(task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parseError{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from CurseForge::CurrentVersions at" << parseError.offset << "reason:" << parseError.errorString(); qWarning() << *response; emitFailed(parseError.errorString()); return; } try { auto docObj = Json::requireObject(doc); auto dataObj = Json::requireObject(docObj, "data"); auto dataArr = Json::requireArray(dataObj, "exactMatches"); if (dataArr.isEmpty()) { qWarning() << "No matches found for fingerprint search!"; getProjectsInfo(); return; } for (auto match : dataArr) { auto matchObj = match.toObject(); auto fileObj = matchObj["file"].toObject(); if (matchObj.isEmpty() || fileObj.isEmpty()) { qWarning() << "Fingerprint match is empty!"; return; } auto fingerprint = QString::number(fileObj["fileFingerprint"].toInteger()); auto mod = pendingHashes.find(fingerprint); if (mod == pendingHashes.end()) { qWarning() << "Invalid fingerprint from the API response."; continue; } setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name)); if (fileObj["isAvailable"].toBool()) resolvedFiles.insert(mod->path, { Json::requireInteger(fileObj, "modId"), Json::requireInteger(fileObj, "id"), mod->enabled, mod->isMod }); } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } pendingHashes.clear(); getProjectsInfo(); }); connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task->start(); } void FlamePackExportTask::getProjectsInfo() { setStatus(tr("Finding project info from CurseForge...")); setProgress(3, 5); QStringList addonIds; for (const auto& resolved : resolvedFiles) { if (resolved.slug.isEmpty()) { addonIds << QString::number(resolved.addonId); } } auto response = std::make_shared(); Task::Ptr projTask; if (addonIds.isEmpty()) { buildZip(); return; } else if (addonIds.size() == 1) { projTask = api.getProject(*addonIds.begin(), response); } else { projTask = api.getProjects(addonIds, response); } connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parseError{}; auto doc = QJsonDocument::fromJson(*response, &parseError); if (parseError.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from CurseForge projects task at" << parseError.offset << "reason:" << parseError.errorString(); qWarning() << *response; emitFailed(parseError.errorString()); return; } try { QJsonArray entries; if (addonIds.size() == 1) entries = { Json::requireObject(Json::requireObject(doc), "data") }; else entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entryObj = Json::requireObject(entry); try { setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(Json::requireString(entryObj, "name"))); ModPlatform::IndexedPack pack; FlameMod::loadIndexedPack(pack, entryObj); for (auto key : resolvedFiles.keys()) { auto val = resolvedFiles.value(key); if (val.addonId == pack.addonId) { val.name = pack.name; val.slug = pack.slug; QStringList authors; for (auto author : pack.authors) authors << author.name; val.authors = authors.join(", "); resolvedFiles[key] = val; } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } buildZip(); }); connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task.reset(projTask); task->start(); } void FlamePackExportTask::buildZip() { setStatus(tr("Adding files...")); setProgress(4, 5); auto zipTask = makeShared(m_options.output, m_gameRoot, files, "overrides/", true); zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("modlist.html", generateHTML()); QStringList exclude; std::transform(resolvedFiles.keyBegin(), resolvedFiles.keyEnd(), std::back_insert_iterator(exclude), [this](QString file) { return m_gameRoot.relativeFilePath(file); }); zipTask->setExcludeFiles(exclude); auto progressStep = std::make_shared(); connect(zipTask.get(), &Task::finished, this, [this, progressStep] { progressStep->state = TaskStepState::Succeeded; stepProgress(*progressStep); }); connect(zipTask.get(), &Task::succeeded, this, &FlamePackExportTask::emitSucceeded); connect(zipTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); connect(zipTask.get(), &Task::failed, this, [this, progressStep](QString reason) { progressStep->state = TaskStepState::Failed; stepProgress(*progressStep); emitFailed(reason); }); connect(zipTask.get(), &Task::stepProgress, this, &FlamePackExportTask::propagateStepProgress); connect(zipTask.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { progressStep->update(current, total); stepProgress(*progressStep); }); connect(zipTask.get(), &Task::status, this, [this, progressStep](QString status) { progressStep->status = status; stepProgress(*progressStep); }); task.reset(zipTask); zipTask->start(); } QByteArray FlamePackExportTask::generateIndex() { QJsonObject obj; obj["manifestType"] = "minecraftModpack"; obj["manifestVersion"] = 1; obj["name"] = m_options.name; obj["version"] = m_options.version; obj["author"] = m_options.author; obj["overrides"] = "overrides"; QJsonObject version; auto profile = m_options.instance->getPackProfile(); // collect all supported components const ComponentPtr minecraft = profile->getComponent("net.minecraft"); const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); const ComponentPtr forge = profile->getComponent("net.minecraftforge"); const ComponentPtr neoforge = profile->getComponent("net.neoforged"); // convert all available components to mrpack dependencies if (minecraft != nullptr) version["version"] = minecraft->m_version; QString id; if (quilt != nullptr) id = "quilt-" + quilt->m_version; else if (fabric != nullptr) id = "fabric-" + fabric->m_version; else if (forge != nullptr) id = "forge-" + forge->m_version; else if (neoforge != nullptr) { id = "neoforge-"; if (minecraft->m_version == "1.20.1") id += "1.20.1-"; id += neoforge->m_version; } version["modLoaders"] = QJsonArray(); if (!id.isEmpty()) { QJsonObject loader; loader["id"] = id; loader["primary"] = true; version["modLoaders"] = QJsonArray({ loader }); } if (m_options.recommendedRAM > 0) version["recommendedRam"] = m_options.recommendedRAM; obj["minecraft"] = version; QJsonArray files; for (auto mod : resolvedFiles) { QJsonObject file; file["projectID"] = mod.addonId; file["fileID"] = mod.version; file["required"] = mod.enabled || !m_options.optionalFiles; files << file; } obj["files"] = files; return QJsonDocument(obj).toJson(QJsonDocument::Compact); } QByteArray FlamePackExportTask::generateHTML() { QString content = ""; for (auto mod : resolvedFiles) { if (mod.isMod) { content += QString(TEMPLATE) .replace("{name}", mod.name.toHtmlEscaped()) .replace("{url}", ModPlatform::getMetaURL(ModPlatform::ResourceProvider::FLAME, mod.addonId).toHtmlEscaped()) .replace("{authors}", !mod.authors.isEmpty() ? QString(" (by %1)").arg(mod.authors).toHtmlEscaped() : ""); } } content = "
      " + content + "
    "; return content.toUtf8(); } PrismLauncher-10.0.5/launcher/modplatform/flame/FlameCheckUpdate.cpp0000644000175100017510000001756715144136756025055 0ustar runnerrunner#include "FlameCheckUpdate.h" #include "Application.h" #include "FlameAPI.h" #include "FlameModIndex.h" #include #include #include "Json.h" #include "QObjectPtr.h" #include "ResourceDownloadTask.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "net/ApiDownload.h" #include "net/NetJob.h" #include "tasks/Task.h" static FlameAPI api; bool FlameCheckUpdate::abort() { bool result = false; if (m_task && m_task->canAbort()) { result = m_task->abort(); } Task::abort(); return result; } /* Check for update: * - Get latest version available * - Compare hash of the latest version with the current hash * - If equal, no updates, else, there's updates, so add to the list * */ void FlameCheckUpdate::executeTask() { setStatus(tr("Preparing resources for CurseForge...")); auto netJob = new NetJob("Get latest versions", APPLICATION->network()); connect(netJob, &Task::finished, this, &FlameCheckUpdate::collectBlockedMods); connect(netJob, &Task::progress, this, &FlameCheckUpdate::setProgress); connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails); for (auto* resource : m_resources) { auto project = std::make_shared(); project->addonId = resource->metadata()->project_id.toString(); auto versionsUrlOptional = api.getVersionsURL({ project, m_gameVersions }); if (!versionsUrlOptional.has_value()) continue; auto response = std::make_shared(); auto task = Net::ApiDownload::makeByteArray(versionsUrlOptional.value(), response); connect(task.get(), &Task::succeeded, this, [this, resource, response] { getLatestVersionCallback(resource, response); }); netJob->addNetAction(task); } m_task.reset(netJob); m_task->start(); } void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ptr response) { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from latest mod version at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } // Fake pack with the necessary info to pass to the download task :) auto pack = std::make_shared(); pack->name = resource->name(); pack->slug = resource->metadata()->slug; pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::FLAME; try { auto obj = Json::requireObject(doc); auto arr = Json::requireArray(obj, "data"); FlameMod::loadIndexedPackVersions(*pack.get(), arr); } catch (Json::JsonException& e) { qCritical() << "Failed to parse response from a version request."; qCritical() << e.what(); qDebug() << doc; } auto latest_ver = api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty()); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); if (!latest_ver.has_value() || !latest_ver->addonId.isValid()) { QString reason; if (dynamic_cast(resource) != nullptr) reason = tr("No valid version found for this resource. It's probably unavailable for the current game " "version / mod loader."); else reason = tr("No valid version found for this resource. It's probably unavailable for the current game version."); emit checkFailed(resource, reason); return; } if (latest_ver->downloadUrl.isEmpty() && latest_ver->fileId != resource->metadata()->file_id) { m_blocked[resource] = latest_ver->fileId.toString(); return; } if (!latest_ver->hash.isEmpty() && (resource->metadata()->hash != latest_ver->hash || resource->status() == ResourceStatus::NOT_INSTALLED)) { auto old_version = resource->metadata()->version_number; if (old_version.isEmpty()) { if (resource->status() == ResourceStatus::NOT_INSTALLED) old_version = tr("Not installed"); else old_version = tr("Unknown"); } auto download_task = makeShared(pack, latest_ver.value(), m_resourceModel); m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type, api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()), ModPlatform::ResourceProvider::FLAME, download_task, resource->enabled()); } m_deps.append(std::make_shared(pack, latest_ver.value())); } void FlameCheckUpdate::collectBlockedMods() { QStringList addonIds; QHash quickSearch; for (auto const& resource : m_blocked.keys()) { auto addonId = resource->metadata()->project_id.toString(); addonIds.append(addonId); quickSearch[addonId] = resource; } auto response = std::make_shared(); Task::Ptr projTask; if (addonIds.isEmpty()) { emitSucceeded(); return; } else if (addonIds.size() == 1) { projTask = api.getProject(*addonIds.begin(), response); } else { projTask = api.getProjects(addonIds, response); } connect(projTask.get(), &Task::succeeded, this, [this, response, addonIds, quickSearch] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame projects task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } try { QJsonArray entries; if (addonIds.size() == 1) entries = { Json::requireObject(Json::requireObject(doc), "data") }; else entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entry_obj = Json::requireObject(entry); auto id = QString::number(Json::requireInteger(entry_obj, "id")); auto resource = quickSearch.find(id).value(); ModPlatform::IndexedPack pack; try { setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); FlameMod::loadIndexedPack(pack, entry_obj); auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, m_blocked[resource]); emit checkFailed(resource, tr("Resource has a new update available, but is not downloadable using CurseForge."), recover_url); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } }); connect(projTask.get(), &Task::finished, this, &FlameCheckUpdate::emitSucceeded); // do not care much about error connect(projTask.get(), &Task::progress, this, &FlameCheckUpdate::setProgress); connect(projTask.get(), &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); connect(projTask.get(), &Task::details, this, &FlameCheckUpdate::setDetails); m_task.reset(projTask); m_task->start(); }PrismLauncher-10.0.5/launcher/modplatform/flame/FileResolvingTask.h0000644000175100017510000000263515144136756024756 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "PackManifest.h" #include "tasks/Task.h" namespace Flame { class FileResolvingTask : public Task { Q_OBJECT public: explicit FileResolvingTask(Flame::Manifest& toProcess); virtual ~FileResolvingTask() = default; bool canAbort() const override { return true; } bool abort() override; const Flame::Manifest& getResults() const { return m_manifest; } protected: virtual void executeTask() override; protected slots: void netJobFinished(); private: void getFlameProjects(); private: /* data */ Flame::Manifest m_manifest; std::shared_ptr m_result; Task::Ptr m_task; }; } // namespace Flame PrismLauncher-10.0.5/launcher/modplatform/flame/FlameCheckUpdate.h0000644000175100017510000000145415144136756024506 0ustar runnerrunner#pragma once #include "modplatform/CheckUpdateTask.h" class FlameCheckUpdate : public CheckUpdateTask { Q_OBJECT public: FlameCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, std::shared_ptr resourceModel) : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) {} public slots: bool abort() override; protected slots: void executeTask() override; private slots: void getLatestVersionCallback(Resource* resource, std::shared_ptr response); void collectBlockedMods(); private: Task::Ptr m_task = nullptr; QHash m_blocked; }; PrismLauncher-10.0.5/launcher/modplatform/flame/FlamePackExportTask.h0000644000175100017510000000424015144136756025225 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 TheKodeToad * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "MMCZip.h" #include "minecraft/MinecraftInstance.h" #include "modplatform/flame/FlameAPI.h" #include "tasks/Task.h" struct FlamePackExportOptions { QString name; QString version; QString author; bool optionalFiles; MinecraftInstancePtr instance; QString output; MMCZip::FilterFileFunction filter; int recommendedRAM; }; class FlamePackExportTask : public Task { Q_OBJECT public: FlamePackExportTask(FlamePackExportOptions&& options); protected: void executeTask() override; bool abort() override; private: static const QString TEMPLATE; static const QStringList FILE_EXTENSIONS; // inputs struct ResolvedFile { int addonId; int version; bool enabled; bool isMod; QString name; QString slug; QString authors; }; struct HashInfo { QString name; QString path; bool enabled; bool isMod; }; FlamePackExportOptions m_options; QDir m_gameRoot; FlameAPI api; QFileInfoList files; QMap pendingHashes{}; QMap resolvedFiles{}; Task::Ptr task; void collectFiles(); void collectHashes(); void makeApiRequest(); void getProjectsInfo(); void buildZip(); QByteArray generateIndex(); QByteArray generateHTML(); }; PrismLauncher-10.0.5/launcher/modplatform/flame/FlameModIndex.cpp0000644000175100017510000001656115144136756024375 0ustar runnerrunner#include "FlameModIndex.h" #include "FileSystem.h" #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" static FlameAPI api; void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.provider = ModPlatform::ResourceProvider::FLAME; pack.name = Json::requireString(obj, "name"); pack.slug = Json::requireString(obj, "slug"); pack.websiteUrl = obj["links"].toObject()["websiteUrl"].toString(""); pack.description = obj["summary"].toString(""); QJsonObject logo = obj["logo"].toObject(); pack.logoName = logo["title"].toString(); pack.logoUrl = logo["thumbnailUrl"].toString(); if (pack.logoUrl.isEmpty()) { pack.logoUrl = logo["url"].toString(); } auto authors = obj["authors"].toArray(); if (!authors.isEmpty()) { pack.authors.clear(); for (auto authorIter : authors) { auto author = Json::requireObject(authorIter); ModPlatform::ModpackAuthor packAuthor; packAuthor.name = Json::requireString(author, "name"); packAuthor.url = Json::requireString(author, "url"); pack.authors.append(packAuthor); } } pack.extraDataLoaded = false; loadURLs(pack, obj); } void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) { auto links_obj = obj["links"].toObject(); pack.extraData.issuesUrl = links_obj["issuesUrl"].toString(); if (pack.extraData.issuesUrl.endsWith('/')) pack.extraData.issuesUrl.chop(1); pack.extraData.sourceUrl = links_obj["sourceUrl"].toString(); if (pack.extraData.sourceUrl.endsWith('/')) pack.extraData.sourceUrl.chop(1); pack.extraData.wikiUrl = links_obj["wikiUrl"].toString(); if (pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); if (!pack.extraData.body.isEmpty()) pack.extraDataLoaded = true; } void FlameMod::loadBody(ModPlatform::IndexedPack& pack) { pack.extraData.body = api.getModDescription(pack.addonId.toInt()); if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty()) pack.extraDataLoaded = true; } static QString enumToString(int hash_algorithm) { switch (hash_algorithm) { default: case 1: return "sha1"; case 2: return "md5"; } } void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) { QList unsortedVersions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); if (!file.addonId.isValid()) file.addonId = pack.addonId; if (file.fileId.isValid()) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { // dates are in RFC 3339 format return a.date > b.date; }; std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); pack.versions = unsortedVersions; pack.versionsLoaded = true; } auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion { auto versionArray = Json::requireArray(obj, "gameVersions"); ModPlatform::IndexedVersion file; for (auto mcVer : versionArray) { auto str = mcVer.toString(); if (str.contains('.')) file.mcVersion.append(str); file.side = ModPlatform::Side::NoSide; if (auto loader = str.toLower(); loader == "neoforge") file.loaders |= ModPlatform::NeoForge; else if (loader == "forge") file.loaders |= ModPlatform::Forge; else if (loader == "cauldron") file.loaders |= ModPlatform::Cauldron; else if (loader == "liteloader") file.loaders |= ModPlatform::LiteLoader; else if (loader == "fabric") file.loaders |= ModPlatform::Fabric; else if (loader == "quilt") file.loaders |= ModPlatform::Quilt; else if (loader == "server" || loader == "client") { if (file.side == ModPlatform::Side::NoSide) file.side = ModPlatform::SideUtils::fromString(loader); else if (file.side != ModPlatform::SideUtils::fromString(loader)) file.side = ModPlatform::Side::UniversalSide; } } file.addonId = Json::requireInteger(obj, "modId"); file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); file.downloadUrl = obj["downloadUrl"].toString(); file.fileName = Json::requireString(obj, "fileName"); file.fileName = FS::RemoveInvalidPathChars(file.fileName); ModPlatform::IndexedVersionType ver_type; switch (Json::requireInteger(obj, "releaseType")) { case 1: ver_type = ModPlatform::IndexedVersionType::Release; break; case 2: ver_type = ModPlatform::IndexedVersionType::Beta; break; case 3: ver_type = ModPlatform::IndexedVersionType::Alpha; break; default: ver_type = ModPlatform::IndexedVersionType::Unknown; break; } file.version_type = ver_type; auto hash_list = obj["hashes"].toArray(); for (auto h : hash_list) { auto hash_entry = h.toObject(); auto hash_types = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::FLAME); auto hash_algo = enumToString(hash_entry["algo"].toInt(1)); if (hash_types.contains(hash_algo)) { file.hash = Json::requireString(hash_entry, "value"); file.hash_type = hash_algo; break; } } auto dependencies = obj["dependencies"].toArray(); for (auto d : dependencies) { auto dep = d.toObject(); ModPlatform::Dependency dependency; dependency.addonId = Json::requireInteger(dep, "modId"); switch (Json::requireInteger(dep, "relationType")) { case 1: // EmbeddedLibrary dependency.type = ModPlatform::DependencyType::EMBEDDED; break; case 2: // OptionalDependency dependency.type = ModPlatform::DependencyType::OPTIONAL; break; case 3: // RequiredDependency dependency.type = ModPlatform::DependencyType::REQUIRED; break; case 4: // Tool dependency.type = ModPlatform::DependencyType::TOOL; break; case 5: // Incompatible dependency.type = ModPlatform::DependencyType::INCOMPATIBLE; break; case 6: // Include dependency.type = ModPlatform::DependencyType::INCLUDE; break; default: dependency.type = ModPlatform::DependencyType::UNKNOWN; break; } file.dependencies.append(dependency); } if (load_changelog) file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt()); return file; } PrismLauncher-10.0.5/launcher/modplatform/ResourceType.h0000644000175100017510000000271015144136756022722 0ustar runnerrunner// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> // // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include #include namespace ModPlatform { enum class ResourceType { Mod, ResourcePack, ShaderPack, Modpack, DataPack, World, Screenshots, TexturePack, Unknown }; namespace ResourceTypeUtils { static const std::set VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack, ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod }; QString getName(ResourceType type); } // namespace ResourceTypeUtils } // namespace ModPlatform PrismLauncher-10.0.5/launcher/modplatform/helpers/0000755000175100017510000000000015144136756021562 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/helpers/ExportToModList.cpp0000644000175100017510000001654415144136756025360 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExportToModList.h" #include #include #include namespace ExportToModList { QString toHTML(QList mods, OptionalData extraData) { QStringList lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name().toHtmlEscaped(); if (extraData & Url) { auto url = mod->homepage().toHtmlEscaped(); if (!url.isEmpty()) modName = QString("%2").arg(url, modName); } auto line = modName; if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); if (!ver.isEmpty()) line += QString(" [%1]").arg(ver.toHtmlEscaped()); } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + mod->authors().join(", ").toHtmlEscaped(); if (extraData & FileName) line += QString(" (%1)").arg(mod->fileinfo().fileName().toHtmlEscaped()); lines.append(QString("
  • %1
  • ").arg(line)); } return QString("
      \n\t%1\n
    ").arg(lines.join("\n\t")); } QString toMarkdownEscaped(QString src) { for (auto ch : "\\`*_{}[]<>()#+-.!|") src.replace(ch, QString("\\%1").arg(ch)); return src; } QString toMarkdown(QList mods, OptionalData extraData) { QStringList lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = toMarkdownEscaped(mod->name()); if (extraData & Url) { auto url = mod->homepage(); if (!url.isEmpty()) modName = QString("[%1](%2)").arg(modName, url); } auto line = modName; if (extraData & Version) { auto ver = toMarkdownEscaped(mod->version()); if (ver.isEmpty() && meta != nullptr) ver = toMarkdownEscaped(meta->version().toString()); if (!ver.isEmpty()) line += QString(" [%1]").arg(ver); } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + toMarkdownEscaped(mod->authors().join(", ")); if (extraData & FileName) line += QString(" (%1)").arg(toMarkdownEscaped(mod->fileinfo().fileName())); lines << "- " + line; } return lines.join("\n"); } QString toPlainTXT(QList mods, OptionalData extraData) { QStringList lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); auto line = modName; if (extraData & Url) { auto url = mod->homepage(); if (!url.isEmpty()) line += QString(" (%1)").arg(url); } if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); if (!ver.isEmpty()) line += QString(" [%1]").arg(ver); } if (extraData & Authors && !mod->authors().isEmpty()) line += " by " + mod->authors().join(", "); if (extraData & FileName) line += QString(" (%1)").arg(mod->fileinfo().fileName()); lines << line; } return lines.join("\n"); } QString toJSON(QList mods, OptionalData extraData) { QJsonArray lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); QJsonObject line; line["name"] = modName; if (extraData & Url) { auto url = mod->homepage(); if (!url.isEmpty()) line["url"] = url; } if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); if (!ver.isEmpty()) line["version"] = ver; } if (extraData & Authors && !mod->authors().isEmpty()) line["authors"] = QJsonArray::fromStringList(mod->authors()); if (extraData & FileName) line["filename"] = mod->fileinfo().fileName(); lines << line; } QJsonDocument doc; doc.setArray(lines); return doc.toJson(); } QString toCSV(QList mods, OptionalData extraData) { QStringList lines; for (auto mod : mods) { QStringList data; auto meta = mod->metadata(); auto modName = mod->name(); data << modName; if (extraData & Url) data << mod->homepage(); if (extraData & Version) { auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); data << ver; } if (extraData & Authors) { QString authors; if (mod->authors().length() == 1) authors = mod->authors().back(); else if (mod->authors().length() > 1) authors = QString("\"%1\"").arg(mod->authors().join(",")); data << authors; } if (extraData & FileName) data << mod->fileinfo().fileName(); lines << data.join(","); } return lines.join("\n"); } QString exportToModList(QList mods, Formats format, OptionalData extraData) { switch (format) { case HTML: return toHTML(mods, extraData); case MARKDOWN: return toMarkdown(mods, extraData); case PLAINTXT: return toPlainTXT(mods, extraData); case JSON: return toJSON(mods, extraData); case CSV: return toCSV(mods, extraData); default: { return QString("unknown format:%1").arg(format); } } } QString exportToModList(QList mods, QString lineTemplate) { QStringList lines; for (auto mod : mods) { auto meta = mod->metadata(); auto modName = mod->name(); auto modID = mod->mod_id(); auto url = mod->homepage(); auto ver = mod->version(); if (ver.isEmpty() && meta != nullptr) ver = meta->version().toString(); auto authors = mod->authors().join(", "); auto filename = mod->fileinfo().fileName(); lines << QString(lineTemplate) .replace("{name}", modName) .replace("{mod_id}", modID) .replace("{url}", url) .replace("{version}", ver) .replace("{authors}", authors) .replace("{filename}", filename); } return lines.join("\n"); } } // namespace ExportToModList PrismLauncher-10.0.5/launcher/modplatform/helpers/OverrideUtils.cpp0000644000175100017510000000331415144136756025067 0ustar runnerrunner#include "OverrideUtils.h" #include #include "FileSystem.h" namespace Override { void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path) { QString file_path(FS::PathCombine(parent_folder, name + ".txt")); if (QFile::exists(file_path)) FS::deletePath(file_path); FS::ensureFilePathExists(file_path); QFile file(file_path); if (!file.open(QFile::WriteOnly)) { qWarning() << "Failed to open file '" << file.fileName() << "' for writing!"; return; } QDirIterator override_iterator(override_path, QDirIterator::Subdirectories); while (override_iterator.hasNext()) { auto override_file_path = override_iterator.next(); QFileInfo info(override_file_path); if (info.isFile()) { // Absolute path with temp directory -> relative path override_file_path = override_file_path.split(name).last().remove(0, 1); file.write(override_file_path.toUtf8()); file.write("\n"); } } file.close(); } QStringList readOverrides(const QString& name, const QString& parent_folder) { QString file_path(FS::PathCombine(parent_folder, name + ".txt")); QFile file(file_path); if (!file.exists()) return {}; QStringList previous_overrides; if (!file.open(QFile::ReadOnly)) { qWarning() << "Failed to open file '" << file.fileName() << "' for reading!"; return previous_overrides; } QString entry; do { entry = file.readLine(); previous_overrides.append(entry.trimmed()); } while (!entry.isEmpty()); file.close(); return previous_overrides; } } // namespace Override PrismLauncher-10.0.5/launcher/modplatform/helpers/OverrideUtils.h0000644000175100017510000000115215144136756024532 0ustar runnerrunner#pragma once #include namespace Override { /** This creates a file in `parent_folder` that holds information about which * overrides are in `override_path`. * * If there's already an existing such file, it will be ovewritten. */ void createOverrides(const QString& name, const QString& parent_folder, const QString& override_path); /** This reads an existing overrides archive, returning a list of overrides. * * If there's no such file in `parent_folder`, it will return an empty list. */ QStringList readOverrides(const QString& name, const QString& parent_folder); } // namespace Override PrismLauncher-10.0.5/launcher/modplatform/helpers/HashUtils.cpp0000644000175100017510000001134615144136756024177 0ustar runnerrunner#include "HashUtils.h" #include #include #include #include #include namespace Hashing { Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider) { switch (provider) { case ModPlatform::ResourceProvider::MODRINTH: return makeShared(file_path, ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()); case ModPlatform::ResourceProvider::FLAME: return makeShared(file_path, Algorithm::Murmur2); default: qCritical() << "[Hashing]" << "Unrecognized mod platform!"; return nullptr; } } Hasher::Ptr createHasher(QString file_path, QString type) { return makeShared(file_path, type); } class QIODeviceReader : public Murmur2::Reader { public: QIODeviceReader(QIODevice* device) : m_device(device) {} virtual ~QIODeviceReader() = default; virtual int read(char* s, int n) { return m_device->read(s, n); } virtual bool eof() { return m_device->atEnd(); } virtual void goToBeginning() { m_device->seek(0); } virtual void close() { m_device->close(); } private: QIODevice* m_device; }; QString algorithmToString(Algorithm type) { switch (type) { case Algorithm::Md4: return "md4"; case Algorithm::Md5: return "md5"; case Algorithm::Sha1: return "sha1"; case Algorithm::Sha256: return "sha256"; case Algorithm::Sha512: return "sha512"; case Algorithm::Murmur2: return "murmur2"; // case Algorithm::Unknown: default: break; } return "unknown"; } Algorithm algorithmFromString(QString type) { if (type == "md4") return Algorithm::Md4; if (type == "md5") return Algorithm::Md5; if (type == "sha1") return Algorithm::Sha1; if (type == "sha256") return Algorithm::Sha256; if (type == "sha512") return Algorithm::Sha512; if (type == "murmur2") return Algorithm::Murmur2; return Algorithm::Unknown; } QString hash(QIODevice* device, Algorithm type) { if (!device->isOpen() && !device->open(QFile::ReadOnly)) return ""; QCryptographicHash::Algorithm alg = QCryptographicHash::Sha1; switch (type) { case Algorithm::Md4: alg = QCryptographicHash::Algorithm::Md4; break; case Algorithm::Md5: alg = QCryptographicHash::Algorithm::Md5; break; case Algorithm::Sha1: alg = QCryptographicHash::Algorithm::Sha1; break; case Algorithm::Sha256: alg = QCryptographicHash::Algorithm::Sha256; break; case Algorithm::Sha512: alg = QCryptographicHash::Algorithm::Sha512; break; case Algorithm::Murmur2: { // CF-specific auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); }; auto reader = std::make_unique(device); auto result = QString::number(Murmur2::hash(reader.get(), 4 * MiB, should_filter_out)); device->close(); return result; } case Algorithm::Unknown: device->close(); return ""; } QCryptographicHash hash(alg); if (!hash.addData(device)) qCritical() << "Failed to read JAR to create hash!"; Q_ASSERT(hash.result().length() == hash.hashLength(alg)); auto result = hash.result().toHex(); device->close(); return result; } QString hash(QString fileName, Algorithm type) { QFile file(fileName); return hash(&file, type); } QString hash(QByteArray data, Algorithm type) { QBuffer buff(&data); return hash(&buff, type); } void Hasher::executeTask() { m_future = QtConcurrent::run( QThreadPool::globalInstance(), [](QString fileName, Algorithm type) { return hash(fileName, type); }, m_path, m_alg); connect(&m_watcher, &QFutureWatcher::finished, this, [this] { if (m_future.isCanceled()) { emitAborted(); } else if (m_result = m_future.result(); m_result.isEmpty()) { emitFailed("Empty hash!"); } else { emit resultsReady(m_result); emitSucceeded(); } }); m_watcher.setFuture(m_future); } bool Hasher::abort() { if (m_future.isRunning()) { m_future.cancel(); // NOTE: Here we don't do `emitAborted()` because it will be done when `m_build_zip_future` actually cancels, which may not // occur immediately. return true; } return false; } } // namespace Hashing PrismLauncher-10.0.5/launcher/modplatform/helpers/ExportToModList.h0000644000175100017510000000224415144136756025015 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include #include "minecraft/mod/Mod.h" namespace ExportToModList { enum Formats { HTML, MARKDOWN, PLAINTXT, JSON, CSV, CUSTOM }; enum OptionalData { Authors = 1 << 0, Url = 1 << 1, Version = 1 << 2, FileName = 1 << 3 }; QString exportToModList(QList mods, Formats format, OptionalData extraData); QString exportToModList(QList mods, QString lineTemplate); } // namespace ExportToModList PrismLauncher-10.0.5/launcher/modplatform/helpers/HashUtils.h0000644000175100017510000000242515144136756023642 0ustar runnerrunner#pragma once #include #include #include #include #include "modplatform/ModIndex.h" #include "tasks/Task.h" namespace Hashing { enum class Algorithm { Md4, Md5, Sha1, Sha256, Sha512, Murmur2, Unknown }; QString algorithmToString(Algorithm type); Algorithm algorithmFromString(QString type); QString hash(QIODevice* device, Algorithm type); QString hash(QString fileName, Algorithm type); QString hash(QByteArray data, Algorithm type); class Hasher : public Task { Q_OBJECT public: using Ptr = shared_qobject_ptr; Hasher(QString file_path, Algorithm alg) : m_path(file_path), m_alg(alg) {} Hasher(QString file_path, QString alg) : Hasher(file_path, algorithmFromString(alg)) {} bool abort() override; void executeTask() override; QString getResult() const { return m_result; }; QString getPath() const { return m_path; }; signals: void resultsReady(QString hash); private: QString m_result; QString m_path; Algorithm m_alg; QFuture m_future; QFutureWatcher m_watcher; }; Hasher::Ptr createHasher(QString file_path, ModPlatform::ResourceProvider provider); Hasher::Ptr createHasher(QString file_path, QString type); } // namespace Hashing PrismLauncher-10.0.5/launcher/modplatform/packwiz/0000755000175100017510000000000015144136756021570 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/modplatform/packwiz/Packwiz.cpp0000644000175100017510000003032715144136756023711 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "Packwiz.h" #include #include #include #include #include #include "FileSystem.h" #include "StringUtils.h" #include "modplatform/ModIndex.h" #include namespace Packwiz { auto getRealIndexName(const QDir& index_dir, QString normalized_fname, bool should_find_match) -> QString { QFile index_file(index_dir.absoluteFilePath(normalized_fname)); QString real_fname = normalized_fname; if (!index_file.exists()) { // Tries to get similar entries for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { if (!QString::compare(normalized_fname, file_name, Qt::CaseInsensitive)) { real_fname = file_name; break; } } if (should_find_match && !QString::compare(normalized_fname, real_fname, Qt::CaseSensitive)) { qCritical() << "Could not find a match for a valid metadata file!"; qCritical() << "File:" << normalized_fname; return {}; } } return real_fname; } // Helpers static inline auto indexFileName(QString const& mod_slug) -> QString { if (mod_slug.endsWith(".pw.toml")) return mod_slug; return QString("%1.pw.toml").arg(mod_slug); } // Helper functions for extracting data from the TOML file auto stringEntry(toml::table table, QString entry_name) -> QString { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { qDebug() << "Failed to read str property '" + entry_name + "' in mod metadata."; return {}; } return node.value_or(""); } auto intEntry(toml::table table, QString entry_name) -> int { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { qDebug() << "Failed to read int property '" + entry_name + "' in mod metadata."; return {}; } return node.value_or(0); } auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod { Mod mod; mod.slug = mod_pack.slug; mod.name = mod_pack.name; mod.filename = mod_version.fileName; if (mod_pack.provider == ModPlatform::ResourceProvider::FLAME) { mod.mode = "metadata:curseforge"; } else { mod.mode = "url"; mod.url = mod_version.downloadUrl; } mod.hash_format = mod_version.hash_type; mod.hash = mod_version.hash; mod.provider = mod_pack.provider; mod.file_id = mod_version.fileId; mod.project_id = mod_pack.addonId; mod.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side; mod.loaders = mod_version.loaders; mod.mcVersions = mod_version.mcVersion; mod.mcVersions.sort(); mod.releaseType = mod_version.version_type; mod.version_number = mod_version.version_number; if (mod.version_number.isNull()) // on CurseForge, there is only a version name - not a version number mod.version_number = mod_version.version; return mod; } void V1::updateModIndex(const QDir& index_dir, Mod& mod) { if (!mod.isValid()) { qCritical() << QString("Tried to update metadata of an invalid mod!"); return; } // Ensure the corresponding mod's info exists, and create it if not auto normalized_fname = indexFileName(mod.slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); QFile index_file(index_dir.absoluteFilePath(real_fname)); if (real_fname != normalized_fname) index_file.rename(normalized_fname); // There's already data on there! // TODO: We should do more stuff here, as the user is likely trying to // override a file. In this case, check versions and ask the user what // they want to do! if (index_file.exists()) { index_file.remove(); } else { FS::ensureFilePathExists(index_file.fileName()); } toml::table update; switch (mod.provider) { case (ModPlatform::ResourceProvider::FLAME): if (mod.file_id.toInt() == 0 || mod.project_id.toInt() == 0) { qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); return; } update = toml::table{ { "file-id", mod.file_id.toInt() }, { "project-id", mod.project_id.toInt() }, }; break; case (ModPlatform::ResourceProvider::MODRINTH): if (mod.mod_id().toString().isEmpty() || mod.version().toString().isEmpty()) { qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); return; } update = toml::table{ { "mod-id", mod.mod_id().toString().toStdString() }, { "version", mod.version().toString().toStdString() }, }; break; } toml::array loaders; for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) { loaders.push_back(getModLoaderAsString(loader).toStdString()); } toml::array mcVersions; for (auto version : mod.mcVersions) { mcVersions.push_back(version.toStdString()); } if (!index_file.open(QIODevice::ReadWrite)) { qCritical() << QString("Could not open file %1!").arg(normalized_fname); return; } // Put TOML data into the file QTextStream in_stream(&index_file); { auto tbl = toml::table{ { "name", mod.name.toStdString() }, { "filename", mod.filename.toStdString() }, { "side", ModPlatform::SideUtils::toString(mod.side).toStdString() }, { "x-prismlauncher-loaders", loaders }, { "x-prismlauncher-mc-versions", mcVersions }, { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, { "x-prismlauncher-version-number", mod.version_number.toStdString() }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, { "url", mod.url.toString().toStdString() }, { "hash-format", mod.hash_format.toStdString() }, { "hash", mod.hash.toStdString() }, } }, { "update", toml::table{ { ModPlatform::ProviderCapabilities::name(mod.provider), update } } } }; std::stringstream ss; ss << tbl; in_stream << QString::fromStdString(ss.str()); } index_file.flush(); index_file.close(); } void V1::deleteModIndex(const QDir& index_dir, QString& mod_slug) { auto normalized_fname = indexFileName(mod_slug); auto real_fname = getRealIndexName(index_dir, normalized_fname); if (real_fname.isEmpty()) return; QFile index_file(index_dir.absoluteFilePath(real_fname)); if (!index_file.exists()) { qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug); return; } if (!index_file.remove()) { qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug); } } auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod { Mod mod; auto normalized_fname = indexFileName(slug); auto real_fname = getRealIndexName(index_dir, normalized_fname, true); if (real_fname.isEmpty()) return {}; toml::table table; #if TOML_EXCEPTIONS try { table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); } catch (const toml::parse_error& err) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason:" << QString(err.what()); return {}; } #else toml::parse_result result = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); if (!result) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); qWarning() << "Reason:" << result.error().description(); return {}; } table = result.table(); #endif // index_file.close(); mod.slug = slug; { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); mod.side = ModPlatform::SideUtils::fromString(stringEntry(table, "side")); mod.releaseType = ModPlatform::IndexedVersionType::fromString(table["x-prismlauncher-release-type"].value_or("")); if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) { for (auto&& loader : *loaders.as_array()) { if (loader.is_string()) { mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or(""))); } } } if (auto versions = table["x-prismlauncher-mc-versions"]; versions && versions.is_array()) { for (auto&& version : *versions.as_array()) { if (version.is_string()) { auto ver = QString::fromStdString(version.as_string()->value_or("")); if (!ver.isEmpty()) { mod.mcVersions << ver; } } } mod.mcVersions.sort(); } } mod.version_number = table["x-prismlauncher-version-number"].value_or(""); { // [download] info auto download_table = table["download"].as_table(); if (!download_table) { qCritical() << QString("No [download] section found on mod metadata!"); return {}; } mod.mode = stringEntry(*download_table, "mode"); mod.url = stringEntry(*download_table, "url"); mod.hash_format = stringEntry(*download_table, "hash-format"); mod.hash = stringEntry(*download_table, "hash"); } { // [update] info using Provider = ModPlatform::ResourceProvider; auto update_table = table["update"]; if (!update_table || !update_table.is_table()) { qCritical() << QString("No [update] section found on mod metadata!"); return {}; } toml::table* mod_provider_table = nullptr; if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::FLAME)].as_table())) { mod.provider = Provider::FLAME; mod.file_id = intEntry(*mod_provider_table, "file-id"); mod.project_id = intEntry(*mod_provider_table, "project-id"); } else if ((mod_provider_table = update_table[ModPlatform::ProviderCapabilities::name(Provider::MODRINTH)].as_table())) { mod.provider = Provider::MODRINTH; mod.mod_id() = stringEntry(*mod_provider_table, "mod-id"); mod.version() = stringEntry(*mod_provider_table, "version"); } else { qCritical() << QString("No mod provider on mod metadata!"); return {}; } } return mod; } auto V1::getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod { for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { auto mod = getIndexForMod(index_dir, file_name); if (mod.mod_id() == mod_id) return mod; } return {}; } } // namespace Packwiz PrismLauncher-10.0.5/launcher/modplatform/packwiz/Packwiz.h0000644000175100017510000000654115144136756023357 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 flowln * Copyright (c) 2023 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include "modplatform/ModIndex.h" #include #include #include class QDir; namespace Packwiz { auto getRealIndexName(const QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; class V1 { public: // can also represent other resources beside loader mods - but this is what packwiz calls it struct Mod { QString slug{}; QString name{}; QString filename{}; ModPlatform::Side side{ ModPlatform::Side::UniversalSide }; ModPlatform::ModLoaderTypes loaders; QStringList mcVersions; ModPlatform::IndexedVersionType releaseType; // [download] QString mode{}; QUrl url{}; QString hash_format{}; QString hash{}; // [update] ModPlatform::ResourceProvider provider{}; QVariant file_id{}; QVariant project_id{}; QString version_number{}; public: // This is a totally heuristic, but should work for now. auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); } // Different providers can use different names for the same thing // Modrinth-specific auto mod_id() -> QVariant& { return project_id; } auto version() -> QVariant& { return file_id; } }; /* Generates the object representing the information in a mod.pw.toml file via * its common representation in the launcher, when downloading mods. * */ static auto createModFormat(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; /* Updates the mod index for the provided mod. * This creates a new index if one does not exist already * TODO: Ask the user if they want to override, and delete the old mod's files, or keep the old one. * */ static void updateModIndex(const QDir& index_dir, Mod& mod); /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */ static void deleteModIndex(const QDir& index_dir, QString& mod_slug); /* Gets the metadata for a mod with a particular file name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(const QDir& index_dir, QString slug) -> Mod; /* Gets the metadata for a mod with a particular id. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod; }; } // namespace Packwiz PrismLauncher-10.0.5/launcher/modplatform/EnsureMetadataTask.cpp0000644000175100017510000004210115144136756024347 0ustar runnerrunner#include "EnsureMetadataTask.h" #include #include #include "Application.h" #include "Json.h" #include "QObjectPtr.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/tasks/LocalResourceUpdateTask.h" #include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" static ModrinthAPI modrinth_api; static FlameAPI flame_api; EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov) : Task(), m_indexDir(dir), m_provider(prov), m_hashingTask(nullptr), m_currentTask(nullptr) { auto hashTask = createNewHash(resource); if (!hashTask) return; connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); m_hashingTask = hashTask; } EnsureMetadataTask::EnsureMetadataTask(QList& resources, QDir dir, ModPlatform::ResourceProvider prov) : Task(), m_indexDir(dir), m_provider(prov), m_currentTask(nullptr) { auto hashTask = makeShared("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); m_hashingTask = hashTask; for (auto* resource : resources) { auto hash_task = createNewHash(resource); if (!hash_task) continue; connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); }); connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); }); hashTask->addTask(hash_task); } } EnsureMetadataTask::EnsureMetadataTask(QHash& resources, QDir dir, ModPlatform::ResourceProvider prov) : Task(), m_resources(resources), m_indexDir(dir), m_provider(prov), m_currentTask(nullptr) {} Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Resource* resource) { if (!resource || !resource->valid() || resource->type() == ResourceType::FOLDER) return nullptr; return Hashing::createHasher(resource->fileinfo().absoluteFilePath(), m_provider); } QString EnsureMetadataTask::getExistingHash(Resource* resource) { // Check for already computed hashes // (linear on the number of mods vs. linear on the size of the mod's JAR) auto it = m_resources.keyValueBegin(); while (it != m_resources.keyValueEnd()) { if ((*it).second == resource) break; it++; } // We already have the hash computed if (it != m_resources.keyValueEnd()) { return (*it).first; } // No existing hash return {}; } bool EnsureMetadataTask::abort() { // Prevent sending signals to a dead object disconnect(this, 0, 0, 0); if (m_currentTask) return m_currentTask->abort(); return true; } void EnsureMetadataTask::executeTask() { setStatus(tr("Checking if resources have metadata...")); for (auto* resource : m_resources) { if (!resource->valid()) { qDebug() << "Resource" << resource->name() << "is invalid!"; emitFail(resource); continue; } // They already have the right metadata :o if (resource->status() != ResourceStatus::NO_METADATA && resource->metadata() && resource->metadata()->provider == m_provider) { qDebug() << "Resource" << resource->name() << "already has metadata!"; emitReady(resource); continue; } // Folders don't have metadata if (resource->type() == ResourceType::FOLDER) { emitReady(resource); } } Task::Ptr version_task; switch (m_provider) { case (ModPlatform::ResourceProvider::MODRINTH): version_task = modrinthVersionsTask(); break; case (ModPlatform::ResourceProvider::FLAME): version_task = flameVersionsTask(); break; } auto invalidade_leftover = [this] { for (auto resource = m_resources.constBegin(); resource != m_resources.constEnd(); resource++) emitFail(resource.value(), resource.key(), RemoveFromList::No); m_resources.clear(); emitSucceeded(); }; connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] { Task::Ptr project_task; switch (m_provider) { case (ModPlatform::ResourceProvider::MODRINTH): project_task = modrinthProjectsTask(); break; case (ModPlatform::ResourceProvider::FLAME): project_task = flameProjectsTask(); break; } if (!project_task) { invalidade_leftover(); return; } connect(project_task.get(), &Task::finished, this, [this, invalidade_leftover, project_task] { invalidade_leftover(); project_task->deleteLater(); if (m_currentTask) m_currentTask.reset(); }); connect(project_task.get(), &Task::failed, this, &EnsureMetadataTask::emitFailed); m_currentTask = project_task; project_task->start(); }); if (m_resources.size() > 1) setStatus(tr("Requesting metadata information from %1...").arg(ModPlatform::ProviderCapabilities::readableName(m_provider))); else if (!m_resources.empty()) setStatus(tr("Requesting metadata information from %1 for '%2'...") .arg(ModPlatform::ProviderCapabilities::readableName(m_provider), m_resources.begin().value()->name())); m_currentTask = version_task; version_task->start(); } void EnsureMetadataTask::emitReady(Resource* resource, QString key, RemoveFromList remove) { if (!resource) { qCritical() << "Tried to mark a null resource as ready."; if (!key.isEmpty()) m_resources.remove(key); return; } qDebug() << QString("Generated metadata for %1").arg(resource->name()); emit metadataReady(resource); if (remove == RemoveFromList::Yes) { if (key.isEmpty()) key = getExistingHash(resource); m_resources.remove(key); } } void EnsureMetadataTask::emitFail(Resource* resource, QString key, RemoveFromList remove) { if (!resource) { qCritical() << "Tried to mark a null resource as failed."; if (!key.isEmpty()) m_resources.remove(key); return; } qDebug() << QString("Failed to generate metadata for %1").arg(resource->name()); emit metadataFailed(resource); if (remove == RemoveFromList::Yes) { if (key.isEmpty()) key = getExistingHash(resource); m_resources.remove(key); } } // Modrinth Task::Ptr EnsureMetadataTask::modrinthVersionsTask() { auto hash_type = ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first(); auto response = std::make_shared(); auto ver_task = modrinth_api.currentVersions(m_resources.keys(), hash_type, response); // Prevents unfortunate timings when aborting the task if (!ver_task) return Task::Ptr{ nullptr }; connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; failed(parse_error.errorString()); return; } try { auto entries = Json::requireObject(doc); for (auto& hash : m_resources.keys()) { auto resource = m_resources.find(hash).value(); try { auto entry = Json::requireObject(entries, hash); setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); qDebug() << "Getting version for" << resource->name() << "from Modrinth"; m_tempVersions.insert(hash, Modrinth::loadIndexedPackVersion(entry)); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; emitFail(resource); } } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } }); return ver_task; } Task::Ptr EnsureMetadataTask::modrinthProjectsTask() { QHash addonIds; for (auto const& data : m_tempVersions) addonIds.insert(data.addonId.toString(), data.hash); auto response = std::make_shared(); Task::Ptr proj_task; if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; } else if (addonIds.size() == 1) { proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response); } else { proj_task = modrinth_api.getProjects(addonIds.keys(), response); } // Prevents unfortunate timings when aborting the task if (!proj_task) return Task::Ptr{ nullptr }; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Modrinth projects task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } QJsonArray entries; try { if (addonIds.size() == 1) entries = { doc.object() }; else entries = Json::requireArray(doc); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } for (auto entry : entries) { ModPlatform::IndexedPack pack; try { auto entry_obj = Json::requireObject(entry); Modrinth::loadIndexedPack(pack, entry_obj); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; // Skip this entry, since it has problems continue; } auto hash = addonIds.find(pack.addonId.toString()).value(); auto resource_iter = m_resources.find(hash); if (resource_iter == m_resources.end()) { qWarning() << "Invalid project id from the API response."; continue; } auto* resource = resource_iter.value(); setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(resource->name())); updateMetadata(pack, m_tempVersions.find(hash).value(), resource); } }); return proj_task; } // Flame Task::Ptr EnsureMetadataTask::flameVersionsTask() { auto response = std::make_shared(); QList fingerprints; for (auto& murmur : m_resources.keys()) { fingerprints.push_back(murmur.toUInt()); } auto ver_task = flame_api.matchFingerprints(fingerprints, response); connect(ver_task.get(), &Task::succeeded, this, [this, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame::CurrentVersions at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; failed(parse_error.errorString()); return; } try { auto doc_obj = Json::requireObject(doc); auto data_obj = Json::requireObject(doc_obj, "data"); auto data_arr = Json::requireArray(data_obj, "exactMatches"); if (data_arr.isEmpty()) { qWarning() << "No matches found for fingerprint search!"; return; } for (auto match : data_arr) { auto match_obj = match.toObject(); auto file_obj = match_obj["file"].toObject(); if (match_obj.isEmpty() || file_obj.isEmpty()) { qWarning() << "Fingerprint match is empty!"; return; } auto fingerprint = QString::number(file_obj["fileFingerprint"].toInteger()); auto resource = m_resources.find(fingerprint); if (resource == m_resources.end()) { qWarning() << "Invalid fingerprint from the API response."; continue; } setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*resource)->name())); m_tempVersions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj)); } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } }); return ver_task; } Task::Ptr EnsureMetadataTask::flameProjectsTask() { QHash addonIds; for (auto const& hash : m_resources.keys()) { if (m_tempVersions.contains(hash)) { auto data = m_tempVersions.find(hash).value(); auto id_str = data.addonId.toString(); if (!id_str.isEmpty()) addonIds.insert(data.addonId.toString(), hash); } } auto response = std::make_shared(); Task::Ptr proj_task; if (addonIds.isEmpty()) { qWarning() << "No addonId found!"; } else if (addonIds.size() == 1) { proj_task = flame_api.getProject(*addonIds.keyBegin(), response); } else { proj_task = flame_api.getProjects(addonIds.keys(), response); } // Prevents unfortunate timings when aborting the task if (!proj_task) return Task::Ptr{ nullptr }; connect(proj_task.get(), &Task::succeeded, this, [this, response, addonIds] { QJsonParseError parse_error{}; auto doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from Flame projects task at" << parse_error.offset << "reason:" << parse_error.errorString(); qWarning() << *response; return; } try { QJsonArray entries; if (addonIds.size() == 1) entries = { Json::requireObject(Json::requireObject(doc), "data") }; else entries = Json::requireArray(Json::requireObject(doc), "data"); for (auto entry : entries) { auto entry_obj = Json::requireObject(entry); auto id = QString::number(Json::requireInteger(entry_obj, "id")); auto hash = addonIds.find(id).value(); auto resource = m_resources.find(hash).value(); ModPlatform::IndexedPack pack; try { setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(resource->name())); FlameMod::loadIndexedPack(pack, entry_obj); } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << entries; emitFail(resource); } updateMetadata(pack, m_tempVersions.find(hash).value(), resource); } } catch (Json::JsonException& e) { qDebug() << e.cause(); qDebug() << doc; } }); return proj_task; } void EnsureMetadataTask::updateMetadata(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Resource* resource) { try { // Prevent file name mismatch ver.fileName = resource->fileinfo().fileName(); if (ver.fileName.endsWith(".disabled")) ver.fileName.chop(9); auto task = makeShared(m_indexDir, pack, ver); connect(task.get(), &Task::finished, this, [this, &pack, resource] { updateMetadataCallback(pack, resource); }); m_updateMetadataTasks[ModPlatform::ProviderCapabilities::name(pack.provider) + pack.addonId.toString()] = task; task->start(); } catch (Json::JsonException& e) { qDebug() << e.cause(); emitFail(resource); } } void EnsureMetadataTask::updateMetadataCallback(ModPlatform::IndexedPack& pack, Resource* resource) { QDir tmpIndexDir(m_indexDir); auto metadata = Metadata::get(tmpIndexDir, pack.slug); if (!metadata.isValid()) { qCritical() << "Failed to generate metadata at last step!"; emitFail(resource); return; } resource->setMetadata(metadata); emitReady(resource); } PrismLauncher-10.0.5/launcher/Version.h0000644000175100017510000001210515144136756017371 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2023 flowln * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include class QUrl; class Version { public: Version(QString str); Version() = default; bool operator<(const Version& other) const; bool operator<=(const Version& other) const; bool operator>(const Version& other) const; bool operator>=(const Version& other) const; bool operator==(const Version& other) const; bool operator!=(const Version& other) const; QString toString() const { return m_string; } bool isEmpty() const { return m_string.isEmpty(); } friend QDebug operator<<(QDebug debug, const Version& v); private: struct Section { explicit Section(QString fullString) : m_fullString(std::move(fullString)) { qsizetype cutoff = m_fullString.size(); for (int i = 0; i < m_fullString.size(); i++) { if (!m_fullString[i].isDigit()) { cutoff = i; break; } } auto numPart = QStringView{ m_fullString }.left(cutoff); if (!numPart.isEmpty()) { m_isNull = false; m_numPart = numPart.toInt(); } auto stringPart = QStringView{ m_fullString }.mid(cutoff); if (!stringPart.isEmpty()) { m_isNull = false; m_stringPart = stringPart.toString(); } } explicit Section() = default; bool m_isNull = true; int m_numPart = 0; QString m_stringPart; QString m_fullString; inline bool isAppendix() const { return m_stringPart.startsWith('+'); } inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } inline bool operator==(const Section& other) const { if (m_isNull && !other.m_isNull) return false; if (!m_isNull && other.m_isNull) return false; if (!m_isNull && !other.m_isNull) { return (m_numPart == other.m_numPart) && (m_stringPart == other.m_stringPart); } return true; } inline bool operator<(const Section& other) const { static auto unequal_is_less = [](Section const& non_null) -> bool { if (non_null.m_stringPart.isEmpty()) return non_null.m_numPart == 0; return (non_null.m_stringPart != QLatin1Char('.')) && non_null.isPreRelease(); }; if (!m_isNull && other.m_isNull) return unequal_is_less(*this); if (m_isNull && !other.m_isNull) return !unequal_is_less(other); if (!m_isNull && !other.m_isNull) { if (m_numPart < other.m_numPart) return true; if (m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) return true; if (!m_stringPart.isEmpty() && other.m_stringPart.isEmpty()) return false; if (m_stringPart.isEmpty() && !other.m_stringPart.isEmpty()) return true; return false; } return m_fullString < other.m_fullString; } inline bool operator!=(const Section& other) const { return !(*this == other); } inline bool operator>(const Section& other) const { return !(*this < other || *this == other); } }; private: QString m_string; QList
    m_sections; void parse(); }; PrismLauncher-10.0.5/launcher/MangoHud.cpp0000644000175100017510000001213615144136756020005 0ustar runnerrunner// SPDX-License-Identifier: GPL-3.0-only /* * PrismLauncher - Minecraft Launcher * Copyright (C) 2022 Jan Drögehoff * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include "FileSystem.h" #include "Json.h" #include "MangoHud.h" #ifdef __GLIBC__ #ifndef _GNU_SOURCE #define _GNU_SOURCE #define UNDEF_GNU_SOURCE #endif #include #include #endif namespace MangoHud { QString getLibraryString() { /** * Guess MangoHud install location by searching for vulkan layers in this order: * * $VK_LAYER_PATH * $XDG_DATA_DIRS (/usr/local/share/:/usr/share/) * $XDG_DATA_HOME (~/.local/share) * /etc * $XDG_CONFIG_DIRS (/etc/xdg) * $XDG_CONFIG_HOME (~/.config) * * @returns Absolute path of libMangoHud.so if found and empty QString otherwise. */ QStringList vkLayerList; { QString home = QDir::homePath(); QString vkLayerPath = qEnvironmentVariable("VK_LAYER_PATH"); if (!vkLayerPath.isEmpty()) { vkLayerList << vkLayerPath; } QStringList xdgDataDirs = qEnvironmentVariable("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(QLatin1String(":")); for (QString dir : xdgDataDirs) { vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d"); } QString xdgDataHome = qEnvironmentVariable("XDG_DATA_HOME"); if (xdgDataHome.isEmpty()) { xdgDataHome = FS::PathCombine(home, ".local", "share"); } vkLayerList << FS::PathCombine(xdgDataHome, "vulkan", "implicit_layer.d"); vkLayerList << "/etc"; QStringList xdgConfigDirs = qEnvironmentVariable("XDG_CONFIG_DIRS", "/etc/xdg").split(QLatin1String(":")); for (QString dir : xdgConfigDirs) { vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d"); } QString xdgConfigHome = qEnvironmentVariable("XDG_CONFIG_HOME"); if (xdgConfigHome.isEmpty()) { xdgConfigHome = FS::PathCombine(home, ".config"); } vkLayerList << FS::PathCombine(xdgConfigHome, "vulkan", "implicit_layer.d"); } for (const QString& vkLayer : vkLayerList) { // prefer to use architecture specific vulkan layers QString currentArch = QSysInfo::currentCpuArchitecture(); if (currentArch == "arm64") { currentArch = "aarch64"; } QStringList manifestNames = { QString("MangoHud.%1.json").arg(currentArch), "MangoHud.json" }; QString filePath{}; for (const QString& manifestName : manifestNames) { QString tryPath = FS::PathCombine(vkLayer, manifestName); if (QFile::exists(tryPath)) { filePath = tryPath; break; } } if (filePath.isEmpty()) { continue; } try { auto conf = Json::requireDocument(filePath, vkLayer); auto confObject = Json::requireObject(conf, vkLayer); auto layer = confObject["layer"].toObject(); QString libraryName = layer["library_path"].toString(); if (libraryName.isEmpty()) { continue; } if (QFileInfo(libraryName).isAbsolute()) { return libraryName; } #ifdef __GLIBC__ // Check whether mangohud is usable on a glibc based system QString libraryPath = findLibrary(libraryName); if (!libraryPath.isEmpty()) { return libraryPath; } #else // Without glibc return recorded shared library as-is. return libraryName; #endif } catch (const Exception& e) { } } return {}; } QString findLibrary(QString libName) { #ifdef __GLIBC__ const char* library = libName.toLocal8Bit().constData(); void* handle = dlopen(library, RTLD_NOW); if (!handle) { qCritical() << "dlopen() failed:" << dlerror(); return {}; } char path[PATH_MAX]; if (dlinfo(handle, RTLD_DI_ORIGIN, path) == -1) { qCritical() << "dlinfo() failed:" << dlerror(); dlclose(handle); return {}; } auto fullPath = FS::PathCombine(QString(path), libName); dlclose(handle); return fullPath; #else qWarning() << "MangoHud::findLibrary is not implemented on this platform"; return {}; #endif } } // namespace MangoHud #ifdef UNDEF_GNU_SOURCE #undef _GNU_SOURCE #undef UNDEF_GNU_SOURCE #endif PrismLauncher-10.0.5/launcher/resources/0000755000175100017510000000000015144136756017606 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/breeze_light/0000755000175100017510000000000015144136756022251 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/breeze_light/breeze_light.qrc0000644000175100017510000000364315144136756025431 0ustar runnerrunner index.theme scalable/about.svg scalable/accounts.svg scalable/bug.svg scalable/centralmods.svg scalable/checkupdate.svg scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg scalable/jarmods.svg scalable/java.svg scalable/language.svg scalable/loadermods.svg scalable/log.svg scalable/minecraft.svg scalable/matrix.svg scalable/new.svg scalable/news.svg scalable/notes.svg scalable/proxy.svg scalable/reddit-alien.svg scalable/refresh.svg scalable/resourcepacks.svg scalable/shaderpacks.svg scalable/shortcut.svg scalable/screenshots.svg scalable/settings.svg scalable/status-bad.svg scalable/status-good.svg scalable/status-yellow.svg scalable/viewfolder.svg scalable/worlds.svg scalable/delete.svg scalable/tag.svg scalable/export.svg scalable/rename.svg scalable/launch.svg scalable/server.svg scalable/appearance.svg PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/0000755000175100017510000000000015144136756024017 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/appearance.svg0000644000175100017510000000160415144136756026640 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/jarmods.svg0000644000175100017510000000270215144136756026200 0ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/java.svg0000644000175100017510000000415515144136756025466 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/shaderpacks.svg0000644000175100017510000000120115144136756027022 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/status-bad.svg0000644000175100017510000000072715144136756026615 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/worlds.svg0000644000175100017510000005032015144136756026052 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/tag.svg0000644000175100017510000000114215144136756025311 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/status-yellow.svg0000644000175100017510000000113215144136756027371 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/proxy.svg0000644000175100017510000000127215144136756025723 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/launch.svg0000644000175100017510000000042315144136756026011 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/reddit-alien.svg0000644000175100017510000000132015144136756027075 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/delete.svg0000644000175100017510000000111515144136756026000 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/notes.svg0000644000175100017510000000122215144136756025665 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/instance-settings.svg0000644000175100017510000000103715144136756030203 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/custom-commands.svg0000644000175100017510000000107115144136756027650 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/minecraft.svg0000644000175100017510000000344115144136756026512 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/loadermods.svg0000644000175100017510000000060215144136756026667 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/bug.svg0000644000175100017510000000070515144136756025317 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/viewfolder.svg0000644000175100017510000000060715144136756026711 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/centralmods.svg0000644000175100017510000000042115144136756027050 0ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/checkupdate.svg0000644000175100017510000000110115144136756027011 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/externaltools.svg0000644000175100017510000000111015144136756027434 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/help.svg0000644000175100017510000000125015144136756025466 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/about.svg0000644000175100017510000000075715144136756025663 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/accounts.svg0000644000175100017510000000234215144136756026360 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/discord.svg0000644000175100017510000000173415144136756026174 0ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/server.svg0000644000175100017510000000075715144136756026057 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/screenshots.svg0000644000175100017510000000117515144136756027104 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/log.svg0000644000175100017510000000067515144136756025331 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/settings.svg0000644000175100017510000000133015144136756026375 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/coremods.svg0000644000175100017510000000057315144136756026360 0ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/matrix.svg0000644000175100017510000000265615144136756026055 0ustar runnerrunner Matrix (protocol) logo PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/shortcut.svg0000644000175100017510000000172015144136756026413 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/patreon.svg0000644000175100017510000000040515144136756026207 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/copy.svg0000644000175100017510000000103115144136756025505 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/resourcepacks.svg0000644000175100017510000000125315144136756027412 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/export.svg0000644000175100017510000000112315144136756026056 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/status-good.svg0000644000175100017510000000102615144136756027010 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/new.svg0000644000175100017510000000104215144136756025326 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/language.svg0000644000175100017510000000112115144136756026316 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/news.svg0000644000175100017510000000063715144136756025522 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/rename.svg0000644000175100017510000000156315144136756026014 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/datapacks.svg0000644000175100017510000000226715144136756026502 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/scalable/refresh.svg0000644000175100017510000000315115144136756026176 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/breeze_light/index.theme0000644000175100017510000000023115144136756024400 0ustar runnerrunner[Icon Theme] Name=Breeze Light Comment=Breeze Light Icons Inherits=multimc Directories=scalable [scalable] Size=48 Type=Scalable MinSize=16 MaxSize=256 PrismLauncher-10.0.5/launcher/resources/multimc/0000755000175100017510000000000015144136756021260 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/multimc/48x48/0000755000175100017510000000000015144136756022057 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/multimc/48x48/viewfolder.png0000644000175100017510000000233715144136756024740 0ustar runnerrunner‰PNG  IHDR00Wù‡¦IDATxíÓ5”,×Äñ}÷vÏì>3333&fŠÍìÔ‘ÙŽœçΔ›™CbfÈÄÌÒÃá¾_y÷ì`• ZðæªS=<è¾íAÒ'¿yìÈv`îH€¤Ì}ÙAqi^˜ölÄñbU‹¾\˜Iɽ¨rÄ]¡¬»†^V‚‚Û´üù¾ê}Àß íä"É@®€O{=|¾ò“žý„Ê ŸÒµõà”ìH°\Û¹`õØs‹üÕ´×{¿·”ß[ @ÂÞ-¡Ûg2>1‘^÷œ¾”xÌl韔à¼4)î^‡è%ýø À·,`ƒ€–f=XÄíy°ûŽòªgõùŸK7OXmüu cÄÝId&ÑÒŸ é…À  ²e‚4l†dh q ÷•xÕ³:·dànf +ø’ëZ\rý0}ø¾&Àl¾²nS@@¦Ù4³¹³„¶Tî„ØÙÀÀvæÝæN™{Ž -A@ÚÇ0dzW€q0à]pqüGšÍf0-͘Ø"·™lŽ_@‚LØ´£cZ m›Ñ[À€H›Ã™ŒJz› a›#bWÛ“fT"!-tûCè«o =ýñ/¡NÍfH“©QB& Ì×C¸ôºS¥¾à)OE¶ÌðÐNÏ@Œ‹ZG_‹C^ð”ǵú÷‹®øøz“Ï–4yÂ#º¿i–›5£bôëéJ·íɳ_Z.']\¬—}é/›IUvá–)$„#c°‰Z¬s¨Ccö®—?zïozÂêì¢Uì‰!b< èÀÐ-fËÍô¼ß\5¯“®œ»úd{È?/Û,—ë Û!)AëjY\}ãÙ§æmV§}9sã¹|Ò®V­¹@šqk¥°ºäê…kŸ®µ”›CRW;J;ÅHPJ±f?,pS„èjÅ6™Ñ2è@”¢8Ì^k=*0© Có®ëÈ " BÔz°@wx)è;TŠØ f¤D„ ºÂMµ«å ºBLz±}Q2”"Z:"D§rãa›Ò¦V´7 ƒ-C­ÒjcE!ÝXû®Ü¼’ÄþDÍ2#¬!°M_ƒ¡e”º'k_ËÉ–¢,Ð6HŒ°aÒ‰ÙŠô‡âIWN®R‡zi½‘)[L{PK”<È~ªîMê™ùÊK ôèª#i8Ìh(%b9ÔÓuZÏžžåbhì’ðÈØ44ºZÊìÄ´œ«zX7¿át›µÔCm ÃøÒNµ«eöˆ‡v³øâÛ§óqöðÉ4²ÀÛû£ºls ©™¿ü޽yýÚf~ø‰r¦¯¶C$16F«/â'êiÀàÕÏêO ™\zý,Ö›†„1f|lð“5åÙëOT€g<&ožìO¸þLÿ°´ˆ2Î'ü™Ï²5ýë~÷Ðåb1€±%¦{{«×¾ù­g%ÿ¬zÁ[X¬/£IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/status-bad.png0000644000175100017510000000274515144136756024644 0ustar runnerrunner‰PNG  IHDR00Wù‡¬IDATxÚÕ™Œ\Q…ÿ¤QÛ°Rl«ŠÕHÓ¨^ÛÆìÖ¶mÛ¶mNÖQO¿Ûœ&Ó×Ý›™ö­þä[^œs¥±0b»YçåfC—šM]d¶aÙ©yfOg›}ž Óøy ›ÄÿÆ‘¦‘´Åä±¶ŒufÝ7š¥ð}ïj³o+ÍbË` ,‚ù0fÁ ˜“a"ŒƒòÔ“·†2ª)ËZ+Ö›u¥Å#[̾lBÈzX «`…L,†…0æÀL˜S`L€Fˆ@-TSV9e–Q¶µT ºÓ³ŒÝfowPé6Ø al‡“pnÁCˆŠp ÎÂ^X`ꨃ¨‚rÊ.¥ŽaÔeaÆq³~‡Ì®ï£’=°K‚÷Ã%x¯“ä>‚9¦^€ (ƒêÊ£N #Žš >böæï…ƒp^‡À+8¡ùQ åP EÔ™OÝö?A‹?`öà vÐPx ¯Cæ…z·J½P …Ôû—ØMFWà>µüaë×-Ìeˆ@ A>ä$kb;]·÷»Ô*GáYèbýóc²z¡òÐ’•èpÚÂä7n•Ù‡Ô½¯[™çZr A½ðFÛ¿Tn`pëû­6O}Pæ¢ÉÌ ¥*ï+Oº{P­^È…l´y—X6¤ŒuÚœ6©ŸøÇp6ÃÍDzJin*Ï9•á3qIC)r Íî°+ÙHVkg½`~ñ4¹B·.]~M¼«^üOi"Ê3Pe<ò›`(kAG6µcs‹,'ÁJµÎËÄ÷‡!C†ÄªªªbýºwÕ{LH¼KãÒº<.¯+ÃoBykLÃÒÑjñÁ¹¥ûbÎ#˜ˆ-‡Ë ˆ ñÇ}üø1ÖØÐà„z"ÐòïÒº<.¯+c@&Až d¢uTü“cÊBÂ6Á+Ϙoy'àw|úôé/QïÒ*dB==õ7𿤙5SpÙ;_‡¬Óžn<›ßœ‰Kø  Wö9Ï<ÚišŽfsÁ¥£3|›£ãï£f2Ga³&Ÿ¿n^¬à¯pe¹2]Ù®Žh3î@® d Ù]ŠŒsúкx¬L`ù ´¦×DÒAiÕk ,Ç šŽv›b6ušnM•È» &g"ñþeX¬Mdg`ª¹{ê]ùÎ*Qˆ&Â/ÇÈ@»q½;5Q‡§«J¢‰ðÄ‹söÀ)ciz:^—mÏÑÁ³1%gBâ=ŸŸ[ê§nKûìÖ×ñð\‰% —¡š˜¬ÞÉ]¬¼Ñ$ë|&êÏVÇ—ˆfw‡4PC7Ôj(Üí C(;~U2ªõ2p©#Nâr–¢J]¦Ot€eôPp-1›Z¦7™­`#[ÜÈ ÙŽKô”1¥%"”p¢"w0ÒƒÒÝv~˜Ëæ\ä›í-Te{ÚùqZÃGÇiE®YJ¾nÿÛñ…¦²‚èžcö%O—çÃíðJy0~õ ^)]ðHŽ. 5mG—ú(TZ_—ú@¸§ þù6[[õ¶vô¬²5¾õ›|VQ0®22ä4.´õÖ4äÅ-N£5îÙŽ×Óå¶î¶áÓâ](:Þ§EEŠY¿T³7ir\ת/ÓBuFâÄg Éi³DƒIü#ÕuÔz{"|îÅ;-É~Z3ÚløX2§@ªöˆó­ þ”H¸Ž àý€Ãob îeÄØêÃ# [ 7^13˜Ñ05:&Eѯ}È·tÆç¾ø(ÿbV{—ϯühÏ/N_VÅ„A‘°*¾ó³r›\MµD2µMH£Aa`°|_5Æ JR-@püèjïW¿¸êþø@Ýé/€Üùuew*!E…*VÅþ ‰£‡'‡WÍÔ %ECkS•ÖèaRª–Ò\UT•U›> K#€p0¢T¾û¯íã(@Ýþ!ÏÜá¾¶¿Þ§aPµ,¢B ­˜GT"™>ŒÄÈf2 VX 1Ph‰Bªë°B©"1aFU9vøˆ3?û*ÀÊ&ÜŸg™g«bF6GzÑBB‰fveoß|°gÌæÞ7 "¢ŠM‘Ðe2ÌJ‰$lÜ# ›Û;rÜÉ«~a¤Êú`OŸ×ÚfòshÅî²˱{û»v/Ÿwùü/ž7úlŒ(!Œ ¨B!( QHµ4‰ Usøè1WNŸ´Àª÷Aö®\2ÏkÁNÑ7“îƒ*Âò>Ïöw/¹pæ*¼×íÝçî·u°.š?H)±@¶~+ÄFb=Çú`~.^uó>r›ÜU²ÖC 4Ì¡AÄ<ÏÖûWœ=u•·¼é¹¾ò“zx2 ñçÓ0K9Ú™üòg˾:{ò*°>ü-™‡BcYXê!éï4÷¼Ûœ9{EúÐ@Ca Y>%+@!þ°c­\õÓ¨¼ëSß¾ï=ï|ッýâÏߥºçÎî:uz×€Í-؞ؒ‚áO;V;‡|þ#ï‚zÔË?”ÛÝêVÆè‚`B‹¼¶vlaU¤JаíØbËR[ý–vØi¬c‹Ã>ñÁw-%´··gjŒD‹` LÈR^:Z†…Ù6 ÐQHPÈÆ¶µí¨Ä<¯¬Æ@È( ݶ´AA–¼þþŽl;:*4µÈƒ cFÄ’m—ÐÈ"…ŽñOp¶KZ¢ =40M¥÷¡£a 84±î–£/¦0Šñç8ŠCS™G£••£³‰–mÇ܇It « ‡ž¶Ât†R¶ïãýLH1Uˆ?쨈¦µöëæËH’îË⿇ÌrÛ›­mÛv8Ö­ZÛ¶m}¶Æži”|ïݯ§:&£×Þq«*uë᜛çPÙ¬±u{‡©œ XNƒDèk'V˜™Ÿz¤¯ˆAÁ?Ó!­ËHñj¬BSÛž2;¨€üøûŸ­¡ ÅëO_¥F¼ÓÛpjr‘jµC½Öc”>ý‘ÇZÃ¥¿yƒafu޳ït Dðã&ãeßÂÞàöÉa?±Æ0Ku4Aè}5œÖlÝÜ¢{óïta…|1ÇæV‹ÍíÝn²ûìt‡X«ÑJ£”àÇüò,‘xþøgÿ†³ïy‚¥ćô`÷6g$‚Rƃp<ŒŒý€  x@¥{õÞyáq b ·ž¹Âᙘɷ®ñÖw:„2†K¿ñC/p»:`iy’C‡¸{}IÆh 3EBœ:¿ÊÍ—n±y{‡·¾×9ŒNÐø±î–:<ú¶’š@€‘pdœð”€ Ù«!(mxîŸãNÌ`bËý­6‰º}lj3«LÎOPî:2…˜J£‡ÎFL¯ÏÓse-·®=àòÍ*Û›MV, Gž'ÿäy¼2’&ñ8öÁ%Èc\›=L+p¤îI¢Æ>cxîO_âßir­Go˜[œ ÊXº„™ùÄ1M1ñÂË÷(ÅŠÉ¥ô¦My§Eœ8ÚÇ™ùÃ$áÎs·xí‰KDZí#ñxLª+Ki•R§aY àÆ) *7·Y/(‚R´ºŽzgÀ‡}ÄyºÏd!ÂÅ÷ÿÄÜ.w9sr™ß(csî×ú\¿¼ÉÒlr¥M6CÜ‹›¼ÓùEÚGxù.3¿š‚Èc\%ûÄ•—”´Måþ·*cçeáÆ³W™ž/Ñ&4ºNžXfªïævsÈýú€6œ;»Ê‰‹;¾@6kX˜ŒðÆ23_dva‚‘´{#²Ùˆ¿ù»ë<õä âúµbŠÄé™ ÀïS0¶˜{]&µCNÆEh/?q•ã‡gh´‡8¥YZšäã>â wwºtŽ\.f$ðžïv„é©‚æ=ßý?ÿ[Ï“Œ‹Ë%z‰g§5Bk… ›[5ú­!xPîrüØ<;—“wž² cR ~L ·¥Ô`ãq·ICÇïS!2Ton²qn‰­jõ3\8¿N¥ÙØüÐy:ϵi‰çîV‹ßýË+,¯ÍЬwét½QÖœQ!ÖšƒÇWè5;Dq$C¿ùuÀG[¥™È(°€Y€ñw’28‘õ/˜£À%Žgž¼ÎòR‰åÕIînwX®÷˜ž+à:ý#/ä²–™\Lµ5äÕ›5r¥,wˬRHd0 Äù=rïN…s(£È³„‘S¥Ù‰°¯Ü®p¹ü ’ÖÿÈ>œ“ÖúéßiR7ûärY®ß©Sí%,¯Lq§Ã©ã‹ˆòD‘!“5Ä>0HÜCòÞgu>ÇíJ‰"‚ÖQêÑ7 Q ½&Š,ÊÀÂl‰­­¥é¢yö¯_x«]žÌÊÔ‚B<* öÙ;•†Ë8õ>ߺ]½ó:Ç+U29î•»h«v/*Éd¯Üh@d(Æ–x·¼q§Á¥û-´¢8¿W\B~"‡‰,ïvq‰ßú‹¶“Œä¬P>$û´ßÃŽRæ†1¬ÜÞ3zíK‹%^¿ÓÄæ3\»Û¢å!—øÁß~bÎà£s”[l”"[È â#´B4(‰ÂÐRœÌQŒ ··ÛX6›ÅZ³n}’$ âÓË„”<”ìÉáÔ.ɘZSÈEìTº 1C­Ùnö8rbžáÐSk™(•v¡±Ùèg,•^Ât1"k-å»M¢\„öÿH>ªí:2t{#òqž×n5°‘ç>‰íÙƒ‹œ;{ŠÄbÀ’2Õ!x¬w‚È(è DZq3ñÓ?ý…ù¶Q˜ÈíBç³¾åÏøòO{ë‹E~ë™;d#ÃK·›\«ô1Z³4™¡‘X : ö0J@9AG%rùµ[˜™Ì0»Yë°Z2 D)r*£^p°79"«Nð{$šZ߃ pAÐAxñF‹gùÆ_|/ÿ¤‹¬-Lð§o”¹Ýqhe‚Í!õ$"‹ÖåSØt‚(aÐîò êÝ Q»ƒuC”1W,J4 +àS-2x…’4(^C0·4…ø@l5C¥‰äãˆðí¿ý Ÿòž‡¹^í±º8ÁNßá%ŠÊ â=$àGkÁkÀZN*ñÆåm|§BÒ’órÑ‰ë‘  Æ€gЈ(àq Ѐ_D¡Ð{¿Ñsâðgpp9Ég"0JPIÌ+÷Û„lÄú\ɬ%FáÄ]£Ø(nÖ‡üÚ·çaI‚VQFvîÝkî4þê`_ãã¾õ7©]ïÀ Z™ Ç½Ù¨«0FÐ@¿¥ÐÚ N‘ `ØQôÛ j€ í·å]áï:AD[£"­ñÎ1莸±ÓãÎrŸs+%JV3 °œÓPÔ\)¨6zì”›˜]n‚‰3H²µíýÁ;WöûÏüýrI Ê©?½:HÄ•rJYƒóB¥9à÷.×xÇJ‘™¬`«ã;õÚ³6•§ìMhudð îºõÆ;¥ ùŸ?ûãßyöÌéÈ×Ï ïr`’÷8:Í{šâ#LPŒ4Þ >@¹3äÒN ç)ÒrÏa õê~õV«¼sx‘}aøŸò³þUßÝ휙¹q½òη*}î4‡rr©¨”Òt†ž—4)i(ˆExöRYFÕ¶â~Y2Æ7 4ùgBES_ÄÿbLn9ú%RÌV4U:¼qlãgv_f7´i×{´¶ä]ÿõævó§¶_yýûÿ#*œ_Zù¤O-­}ÆWÏú¼o™Þøô¯+-}ÄgG¥µcüâ –SxìÑûIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/patreon.png0000644000175100017510000000115615144136756024240 0ustar runnerrunner‰PNG  IHDR00Wù‡5IDATxÚ혬QEk[ŠjƵßÌvæ[Qm[ŠÚFj›aÕ¶¡ŠPÛ·»µµjû÷&'\¼ó¤Q¢xNËÉïpÈÕ·Û§ÔI°ÎË6ËlébÄÈ@t²š¸`]kƒoBúè·¿W @A°¤ƒd7X_Úà'¼éy›0¦D¨è¬-Az$¯ÀŠß†ôšMf(` Òg`…ÞÉ/ûØ>  @B`YÖµî ²­“Êù/@2¬ðÒ=hš]Ì7°ôúXó>A2טÄÚ`½ïgá?Ž ‰÷^€tX r ² {&JiôqŽŠ”lïXf€BºÃC½lƒ€yŽ˜˜Ê®ÐIë|\i†4Õ½i*X’À÷¬ƒCXì…ÀÄÖ{!0Ö!±Ö鞀Ìr/`Ä„&`d¨{æ±åÁò<È‚ÜYí<ÚJèþºÏMSÄ«­D¿f ùÞm%Œ)Ö ¼i3ÏÖÈVyhÞ¼(XOPø0É5ü9RRb#°Üñ±ð/@–åï¡>Æ"ç9ŸŽ’ƒ¹‚’[Î8Ï`´oÀ[Iõ@zؽ€\§t çjѾärj,×ÿ Öïuª3E‡z¹ëĹŒBŒ•ÒNÁ~p‚{j³œ2&­‚?·Ó.ã,ÿ m…Ié Ön Í„Ii¶Ù% ¸ˆkð D‘@$ D$ÓÁzòÛÈ¡Q¢üŸy ·X:¥YÓdIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/status-running.png0000644000175100017510000000330415144136756025566 0ustar runnerrunner‰PNG  IHDR00Wù‡‹IDATxÕšp[ÇÆ÷ef†0'CáÁ2333ŠÙf6•q¨(s- Ì0sd ‰åí×J½‘Ù–{g>áƒïw·wïöI4ôà‘¾jé*^"]YiK#ݾRÒïi"ýî2”ü¥&2–’±à72æg!÷%ÒçÍ}OX¼S7‡45)¤©®&m“¶‚IW•A¥Lú½Ð“¡*‚ P”ËdÊ©†RÈì›CÃh1MÃôn}€4uLšZ¨†I*ˆRhŸ@*ùBòY L¹P6”ÅdÈä{ ~½òVp.½³ßOï62½ÛÕ³@dÇçË2­maÚ|Œ)-Ä”Þ)’×›Û™ÖfZŽíË`:?Ü ÈùüdÝ1—b¯óIôÖÁ5ôNsˆÞib@pÂdZya2ƒ¦´¦Õ8^µ ä ‘e×jräÿŸ†ºÆKéíˆéíf&€i3Þ¯k#1ÑúCL޵ yÉá½’ï6_Ã{Ñò‘ZÞŠ´ÇXèÅU 0î…¿!v”“Å=f€æk`¾±\Ò-.-oD~oì“ÅU›[©£ôÂN‡«Ÿ=ñvå) ^Éqäº@˜`>¥3þæÕñ‘XãÞ0“u{i²O£>CS÷ $³Œ@Xš•A:ŒJ¢Pé…í€ØöIæ«n•iQæw@Ñú©¡žO°.çž8Δ«öž=wQ·á(=¤J\YY ´Øt¼çƒoie:ý<¦ÇS&®cÂæ‹ *ÈñÛÉÔ%´åo’\M« @¬8ÒûpÍ5×ð]wÝŧOœ‹ÞòÄbušFû Rƒ^ðþk˜@°,¬u}O•3f ÷Ýw¼aã&ž3w.ÿ{ÁÓr²XÈÕ<1OéOxŽ„®ô.ˆÁ±®;ö ;;›C¡û|>Öéõ|Í„)L÷.eJi-Ćj1ÙÜwR$ô{·bñÅa­‘Ž––þå—_ø‘Gá³ÆÎbzû‡ö”˜€<_‘Ö8±@¬:4(5êêê8ã½÷yáÂ…üŸY·3-+‹ ÄšzexŽŠw2/$Yòîa2 -4T‰ÎÎNÎÏÏg³ÕÆc&LfºÃΔÒ6ôiÕº3 Àdv- ÒP1±@8ªeá¨ÑÚÚÊ’V>ÊçŒÁôæ7CƒH.d† „*éC’j©˜iy0¦jÔ××óGÌ7ÜpÿoÖLK÷ `e•2Ü*$ X PpÄ@ I«Ä¤$ž2m&ÓÍz¦Mý?§h]“:]È/ ÄæöxH´µµqff&?÷ü |ᘩLÏ|4€ó¶¨eÈ ‚"µ#îjƒAþâ‹/%­þ?õz¦Äü~ d`?¡°n Dzç°¨QXXÈ‹–,á©ÓfôV𨴠§’{”dQT³@¤‚JN!K ,[ÚFá ¶ø=d°@¬?8ò§ÑõÑÓ¨Ùÿ!ı²v\È*ÃæåB¯b¹ \2 –ù e)aݱP g@ d èÅœmGÔbKR”kGˆ5#{9mZNKXvl‹’òGpA“£¤K š0À](Ïòm–”ÍjëG•”(¥P¶ Ké–Ñ9bŠzñ’$­/RŠz%,ž7åK¡„VÕö põÕWówÜÿÛ*«jÔÖWn«¨›Eø¢R6ª©´ùȸ±u”ɾS5ßÃ-òêVl Š2u°[‹©Ç˜²Ä|tî÷v×'؈# ɹ0+׆a•œ3)W5¹?¡>ãmÏ)dszÉæâZcØÌK¯'çE›Ï"ͧQ¿Âì¾ åG@}ꘈ›ä Ÿ˜ïÿQ{!Ž€ØqÐÕÕ2­Åeª\]#K…hóòӠ˜yÌÿ.*Hr€iCò46WX9VR¶VŒ3zÞM¦ÌK†ø3ë·'‘=s ̇ºY['9;¨<_ƒ}»‡p.—ú3ëЋƒúÅ|´ì8é" ôULëƒL[ŽJé'õ+„×ò¾“õ¼ P›Ü˜R%cMÎsÅï¯V×0€¸{¹º‘»¹TäØÃö‡k §@ÕÑý‡“}SäX',¤Wœ3Ðz/ÁL^ÿs¥xÝuˆðZ>Ãw² ¶•}bÐÚÇtF†)ˆÑJIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/cat.png0000644000175100017510000000361015144136756023334 0ustar runnerrunner‰PNG  IHDR00Wù‡OIDATxÚÌX´Gkg¯r¶mÛ¾ûýÿ±m'Å Û¶m'¥Ø¶Ñ™>λ7»ûªÞùv_÷ôt¿þHˆá–Š2;Ù8Æ)ûHáh«Þy1ÙR"};Õ íóÞØ, #œN žšYB1QwGé­Ùí¥tMÔío¤ßËrRJNÿËN.9€„Π‰(ý½.«ã¤ÆÜÞ»Rãs~е%ýe‰û+Ñéþ2Þ‘òþ(ˆÿµ ûH8çø£PR&Q€\ªÄ¯W¤V©ÿa¨j‡•„¤„PAƒë¯˜- áƒÆ|þ¢ÀÒ^ì8¶:~I´'ßžÙ V—zîûªŒÖcI¸`¬vÔ¢³¿ƒÂxÞ»þ…âö„¥.ºå@xVÏ|@1ŒTí 2j+šäù…lsvŸTÞz(èãåK”÷ûàúGO…Ìꈹïm†ˆ÷;‹ÇrH¨1Yw@ª+*õkµêÕ¦›òúòB[:5{›h-¦©`³D˜cîk-²‡”µ¿|æÒ"o5˜uöM‹GÆ9’{I(0Z³g¨9àS_\6P;à—x™D~ 6Æk÷î+Ÿ¹­,µètjÜü|œã8mËežz™Ù–üï3 ¶Kà9Þ_ñ·Qj‚)ÞÕW6ýPEZ$Eø@¥Ðø+y'¥,áÂHõîƒZòûÎN.§)MZ âÿ¡Ü/hYï-›~³ž• :•hÍ"ùG8i0úzwkÁàÔ«Ó>‹Ý"Pá‹|–}{¬Þ½·.»= NIàV¹¨~òjÖâ÷Tg¶A‚Ó ÙœßjîÏÐIîí¬Él‡X{ÒâEeÁ4VF,¾9¯Ÿ®z. +ñ¯Qr„zË¦îÆš_œª,(âé¬õXËS¾•o`ÊJ¨h¥L…ÏqÀ¨±€Ic¥\½æÿhUÐ(u¸q}3Q»¿‘…]hÅγá.‹ —*0£˜Y\Æ× •JdàŽN{ƒ°]Jε#7w)yaîi›Z®µ&­uÕÀ¬³ÍŸËÆÿP³v·?­cfUr z¥U.<è^³’ !,PŸÓý¢ÔOËÀq<^h Ëjåôv^hsÝCX 7©äm|[$fW"Hñ4V À1@‚=å&B!¢@N „7óJ!-6çÍ@­2¶SVü•ÀÓRlîk…ƒÐó"¢„'DðK¸Hˆ 'R©h”‘¼I“•ù< xÃZýt¬zçÜ ¸lî+­Dcµÿ_KYm;nta°¾Ý-ÉL‡ 3¿ÿ3ä6Ìœ¡Ã¶¥îÞ3O­Ub*³ƒæ³=ƃ ±ã ´M{ŒŽÍSÉÛ9§c“ñ2$J. ðø e× “ãÞ‚=QÀ`bÐdÒDø~!¿Úµ« !D^ $ÃKžŸ9¼üOøNG7Œê è!õÄ‚ ¶þú°]ž—&6/ï L"ç´ëøgÀÀ[À Ÿ9ÖÒ€°ÀƒãÐ\èß 7ÛÕr^ΪXS¿àÉ0ªPïÂ[NyxØÍ¥!T (Œ1¢ªC‡™ÐæتŠ-‘È%1ìéÕ^U¬È9qìçãýƒO‡ƒÙ²&Bƒ€"TfÄÇ€ð xèÊÉÀš¶”ÂñÙ!ÃÞˆÅx‡:Ö< óM)žéº–Íó+¿òóåŸzU0J€;îã^âÝ5'†¹ZƒSÁe‡¿¡—R†ªÓšöÕ\R¼|répÎÞü?/´iMwÕ”›HÇ ƒb¨¨bEb¬íduÄŸW/Ü,°4jÃ燗ÿøEèpœe¼3R1ܹ5÷ÌOÕ"ÌÀ÷AÿþlÆ{BóLÞ9äüýŽ<0 š!›WªŠ5f†·qÜoˆ³‰Ë%“JG›ZR»ÎM¶ïv$tR¿~~~ÿU„ ‰õÉ1ëðGQ3íƒO€-`ØsØ‚±ÃtE÷úŠö޼UðON,vV.h¾¬‰8œ‡Ž€¿Äf¬ËN:=ätå¹q?QWcšÆ‰'ASðÉõ1£B}`T(³DÙ+”Ñ&ÄñºàP$¡ÖPgh°Sa'†NgçŽ/“Ýðx³m¦[²\¯Éð' °²!ýª¢®„5…Üõú\··Ñ¡D ÌÑ#Îá8á¹ V°¬€%øRh¬ [rÛ²nOYå5dÀyÄÆ†ŒÔ!Ks’õ‘eÜDº>'à¹ãàà !ä€T*Š¡²ÂK ]µ&ùg¾ÄpÁ?(Å©ÔIQ™IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/bug.png0000644000175100017510000000517015144136756023345 0ustar runnerrunner‰PNG  IHDR00Wù‡ ?IDATxÚÕÙ¸óhÆñ_ÒKcamÛ¶mÛ¶mÛ¶-Œm›Ÿ5Gh“&Ù|ÛwæÊ¶{¦;gý\×]ãþç~^´©Ea­•…Pì,ÿáŠ×hü.¥Þ†wâA˜ÆpùXíÿwÁñZ<7Ã^©üÏTMîƒ[„ëüCQ»â è? ÐYÃØ#¨±ööûoDˆý'Û¯7ÀÚAB[UJïöûÏŒÝ`|||¯<ðv׺ֵžüØöxùW¦½î‹3žÿ…ûÌ”û|tÚ­o=nƒ%Câµ¶ÝUwë·¸Å[GGG´°° 4¤¼½çÔÔÔž¸å–´ÞÜcyS(Žˆ´uú´1èÜ¥Ü)¿jØåW+lÎ@­9ö޵§§y¼iÆ ¶nݪ¿¿ßÄÄ„2ás¼lá¢ä:yÒGÝÎ äÈ ŽMøãŠ‹jøU“?âlEV”µÚ‚Ù¹hÆWÕ|žç†‡‡í¶Ûn&''ÕëuÍfS#kc}õˆ };…ûý·àÕöÿþ®žúÚ ãÌ„Áw˜¿&öÃ.³V|UÍ7 %¼òèëëëDZêÛXëÚÆƒ‚ñªª€c±Á»xèçwñã§{&®ŽÉÒ`_¹.þ„·âö³V¼êât¹yHÓT Ë2«µÞ¢X-˜ûú÷ßÌËwpI+Àt@„ךŠíþˆoç„OÕ &Ý¿ÇÞx4î[Vg­¸cqyÞƒ÷)UÎ6¿,ûýM¥ÌÍÍ)[D™‚+«¥pnÊGæØÐâO+|l^0Þ­š¶bܰÏÞ=ác#<¿ ‚á`Z0uÕÅ¥£Ê™F˜mì±ÇõªI™ÎkQ­³Súª»¨ ìˆA®QwÝWyßÛÔZœ‰ 1‹y¤(b@Ôcù‡žGˆ3î3Ìt À“Ǻ['®Š×¾ý~µ' 8 'âRœ®EY1à`|ßÇ¡8V©rzܰçž{Ú}÷Ý•ƒ½k™h`ÀpÄÁ{ó¡~¿/š¬˜¯Šˆ:@î5ÈÝlÄ)Ø€Âí­hA 8oÇ{ñ!|-Ûæè½öÚK  V«¡wÝ0Jò<®Ñ߆xä(7Œ£Õ T¸Ôuÿ CvŽà\lB³s(°ŒópT:hhhè"€Þ {åI1S´âD¤UB%D޼ UPh ÎKyÑv°‰o,u÷qèÔ=èFèGŒb(Ú•b[°›®5¿cÝ@+™§wíV“?l0ÏÓ–å¼ÐI¢š¬o@« yùÐ\{vÚññyÎIº!nÞçÚ{Å®0X* y©l§n´éÒâÁç:xÇKÏY¿o²’Yˆ92GöŒòüvõ,}ýDÑúØžCñÝ÷Ù­649m!)!²BC¤YB¤%Dš+Õ†È Îê0|^‹ÎŒ @øN}n†k`7ôEeõ\‰³ÂeE¡ÿ€ÆòÞœß?v×Í^R¼vicþÎd«×Õã§ô§}׌òzž$²$µË®»œš6Ÿ¶,e…e‘F€)(åQ#LÆÜe0´XG«\¯ÏØ{cµ’!¾R€"¶#G<Ä)¢Mfü§…¦ÅF"M­$ÕJi©,mC L–ÉêOå›»òÆI~°+ý„öêPxl÷ØÔtä^xÞcúÊÈÍãrmý¦É›øô|Ës6.Ylî4 ’’è/!撖Ŭ°Ri§‚kÔ¹Ï ý¹ªLÐx츞ÇàæWºjì@†¬hë €ùœCšÒ"ù«ùTÚ’°’hv$Qýì¼¢B©$1©Ö~€`¾Þ¹úhâ©­B>T†½;2Û»hIÓTÒn›Ù 1QI¢ˆ$ñ³“ YEç·øs“•¼…]"›qNÁFÄ1„=‚×à•x¹R)/YŸ«e•î×ÏêÌÄŽÆqUª{¡ í¹ €+¾((|©°PµÍˆf8úB’¶“è‹y{H«û×:V÷˜ 9š…Ìb ¶ýÍV¢s/¤Ô6>–Qt@ZtC$]í Ba±[ ³SâƒÜ¬]b>ØÈÃ÷­ËÍ"GŠòÕ÷BA徿’;<VM£jüÒŒ,ñâ9~4Ÿh¦©fc5ˆÐNiX±D^Bì^ãyüs”»õUÚµ2$Èzî…¥‘O·º«îq~ØàÄ”m9ßYáœåÑL4’D³ Ô†\"«¦Œ ÍB¶.3‹,"E±ê^¨ª¥Ä²Âö`º¢¯j]Ô¢4¯4®P¸†ub¨„X¸|Ø €­¹¹Œ.ÃÒðƒ¦w}…æráƒÝÆ ·Ý¶ÀhÄk¤E€Æ»y’˜œ¶˜tCTÛõàÔ™XĦAo€Ìç[F¶äÝæÃµ;ÖyñàÃDH‹ÑhC4šI¨­Ë“˜l'±’‘V­ Ñ*8;séÉ™u؆u$ÿ‡ÂB÷Ö¯ÿs®;œ`Ôî7©Q5_…H*ÆÃmI€˜ítÅý£h¥ÞŸü$u"¶ã<\ŠåÊ/²žæß‚7Áº‚“rIZ…¨*_åºàw oØ‘øäŽ/5J 0>’+Ö 3!‰Bã¯{yË?ld‡/6㜆-hÄô4ÿfÌ’ûýŹ B7H·.ÎøEÂŽ‚SÓÜOšÉ-¤šÊÌ.»šÛúӅ摟¸lù‡g¥Ùñ8ÇâB¬„£ß ÐÛ<.hþ*wè–ÜúŠÉU!’œõ9ÕÚÐÊ•ÆI€ãC yÁ)ivfƒ“qPЙ˜/½gÐáÀ6ü ¬¤\ø£Ü7e~ÝÝ>´:ÚçšÃ·¨Ñ¸üèw@l_Z^>qv~=¶àœ€ó1WšoAO€0ó„»á!‰K° ç☔CPxúÅ…77 +k!ÏíãužÖÏ-k$%D©T#,vóI’üpý–c–³l;.ÂÅØþFÉ×r†¦Àp4Ž ‰Œ°[™ûÈo3÷X—ûe³§Ig*â:ÓÕ–Ëó©3W.ýØú­¿<}¥qJ9[„ Ö öHŽÄa8 Ñ‘.^‰?-<÷û™ïž³!_MIÞV£mÎmÿùrrðw—¿_(Š3p4ÁYX G~-ghzŸ!éx¾†wàÕ0ÊéÓlœˆ¥>,Œ¶È¶¶oŒlÙ˜+ý[ƶà|\n¯„k k«ðý'‹ÑÂ@P 0‹ù Y,u´Í œ Çáp@P Š‘£‰RdÈ;Zæ?p7Üû¢‰spNÂòÎVìlGøoÄÃ5°R\бù—Ö_hRßdr.›IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/status-good.png0000644000175100017510000000335115144136756025040 0ustar runnerrunner‰PNG  IHDR00Wù‡°IDATxÚÅ™ÕzãH…+$$Ù{˜ïf_ O± Wƒá 333y2fff¸^ð¼€sX¢Þóµë£U“œú¾?féuU««CQ‰Š5)ôjùçôbé zê.¤Ç˺é¡ÛK÷ÜStÇ5E7]^ºîꦫ®BºèºAœŸÓAüfQãýj-Ϥ÷Ëë¨`Ù,y– z¹TÐsð€wÿ¼fÏÀðâïƒ;àÄ_W!þ2¸Î9'ÓtÔqŽŽàØ–E%PíÊ\ªY1I•+•ƒˆ/ïA Õ«‚Ú’u% êO4ç§ÏÛñ^ƒ ßKÅÈØyÀ8N8µOÒa{.ý€sE5:Ö®¢æ•T¿RP-¨†ø Pã†0‚Àq Þx\˜½­ó(€c0qrLÐ÷*ŠJ´­I§ÖU>j^…+ñu éÑ“Âb"d T+H+ŒÊqˆ? ƒíÕÓ)¢¨_¹™WþFM ¨W½[ÅIãX@Å1‹0"'x‚ýöß`b3…5t¡zÐšçƒ ŸÐBšQv4Â(àq/Ø¢ äw: õ7B®KMH™ÎsËáú¸¤a ~؃‘È6JQ<¥Ë}r–©õnCÊĈaœó¢&F< ¨‰=©«ž* —OÈù½T.ÃbxåÇ ¯{0Çuÿ(ì;õ‰ù§ØwËsé=ßœŠ!¾_‰ðQ0úÀÁHS¢¿ö€]0“gÏûëY6Iþ´¦ÅN|'x ö‚j0h0ñÖÆiòõIÚjvÇ~á>G¯ Ü ]œ÷1ßn“PV+bÃÆ ‚¾&Aå` ÀÄ8¡ N#A9ú9ú$ž¬vaý2Õ¤&Ú’c.>//Ox<±é³Mæ&J¤ÈÓ§i[àðÑÒLz‹°÷ÎEÿñãGñ矊¢BÌ•ëdkDX1ÖÉåïchHYñÿ7Q ù·ïl\2êHÆ=4÷ܳ0á_þ$,–xs[¸ÈÇ@W<ê€ äê³tRˆn¹?GÃáo<^:ƒ›«G™±¨‹ŸßÀ88­úë ähŸšŒ²[º ÊÓ?Z@-èfa–ˆ7¦ó<™ Y¸A²O½Î-_ƒm~ñ üÖ€» ŒZ!Þ81¥Il@ŽB!¡½ëFŸêïWÛ’æ_¾"y‚ŒŒ ¡¬R ŒŽZ-ž©O nBoê¥K0pô&Ì/þk)^žèÇ”‚ Ì`Â*ñLg¼œJy¼„Æz &LÈ IÎÅË"¤ ‰g† ‘ `*"4ÕS0ÛÄÀ¨ßÄs˜šhc,onà”Ó+›ê³ 'áÓ«?“HUSÅ7ÄÌÌŒA‚‰‰‹,Ï)Ä5À)tÜÑM'y¦91”07±LÖˆ7/⣎B˜tª”yj h–‰7ŸF9oÈ]€c  yžY(h–‰7¿‘íÇíøoe\Öø‹™°H‚ئ§Ã„b#´?MPQ§J„Œbë° dë¾Ófþ† ü°™#0âQd—1¡/Ì Y_,š…K&ä9£<Ån×r!~ÒhDrfÛü#S›ˆ¦>^Î$@>Ç{þ+ýß9¦3Y8v¶fÁ?ºwìíê9˜™–Œpš1™8V†zN;fÅg‚:ڡβs2ÌÀo2ð[CkQãJ˜Ï!æF¦ÝÀ ¦/èæÏn¾+…øˆ¡YéIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/status-yellow.png0000644000175100017510000000272315144136756025425 0ustar runnerrunner‰PNG  IHDR00Wù‡šIDATxÚÕš5#W†ÿ*ÇfÆœ7²”2çšcå¡Ò–i´DÇ7Ǹ0ËÌÌÌÌíþK¯¦¤ºñÝ’t³Wõ¯hôº¿ë~Ð=‚ˆ\ŠF,¤Œ"4j!}8{ÈBç …õP=ìî¤wå#Ô•”˲{¡/Ï"0]„ðT†&Š cªÑdX¥2¨ê/€ô©zó!=ª®ßy Ùx Y¸ * Ða ZsqÒœ‹HC¾MÀZ9~_)ÃŽÈ¢j¡D+![ ‡DšÎ¦}…Z¼é/2Q D¤9;YøýòÊðÎj9RWË +ªåR}¬€ì<¥#׉jù!¤Û£ -„P5f#µLm_@~_uŸÎSØy¤FiürÅT›³!m )ÒûMéxÿ\t^Ó¥s™Ž rXEc‰ÕÆsH{¾Bt¾âÓFsü>s9¿vr\GÉÑ^-¤§È‚Ôk$˜N§˜+A*W™ÕÊõÓ¤LbRª»ÐD!K!2‘z*€™"üÎõ}Ž«LäÈc…If$ZóL²@ýþZ€i]ç§‹°Ãµû/9ÐÛÕÆ3H“¨ËÄŽÙ'¼t7pWVm<äþÐô]HTN&"ž£ÅŽâ„gš™ÒSä}’çCk~4 NNœt_ˆÀ3g™Í'ü¢¿4ÿ€nœ8€‘BxøUM•ûÏyŠÑn¹R›…€ 0AxØœ ×ø€šµ €ª&a`ÐÂÏî„àÉѯ»5î<ÚL ú-¤hÅ$ !㥼Ðßj€Q €Tie‡ !ýªEÛ;÷¸¡Ì>€t—AÚŠ)Ú -Úô^ 'n¹ó€iëT–z„`ÁᵄÍ?„üû+ä»/ _ù&!âØ´A[´IÛ¯ú³ôØ`ÒÑ›e!¶=vÞ#dì6äç äÏ?ÿ”’’’„Š6h‹6<6^Ä­D6:óÐÙmŠmVI^Ã7!?|Hð?Ú -ÚôàDŽ‹@'ÖYŸv«2¿ÕÇd`¹XW¡®$@›¦‹jvö®b µæÀfG€[/¼FoA~ $mÓmžj ]%„X}⽌NÚß~|òäƒw!¾—qlÚ -Ú<å2Ú’‹[„˜¾ãû˜™9J˜¬E·cV<ìô^£„{”4faÈ@˜‰ì/yO`s˜3aVÿ„˜³ý 0cǃ±Öœ¬þ;"U§ãw؉iʇLÝ…ìÔœï`ƶyKÁ+ŽïpµIÈÌ@DNT Ó¬ŽŒÝ‚,>‚¬?'ëWŠÎò½ègãzM«ë°«U„6þSƒªtÕqÇ…ˆ‡‰ó?s¯s8fÒìñ2 u:¬ò€yÜ¿Ë1’ücoU¥!E U¥#]m.Áú¸N™ç¶ù,Äk/ËîËÁ —¯IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/noaccount.png0000644000175100017510000000026715144136756024563 0ustar runnerrunner‰PNG  IHDR00ri¦[~IDATxÚå˵@AªÀݡޥ¸»DHH†»KN¡Àýh{ØÉÇâ…>8`…n¸ 3¸!‘fþã2333,,33333óÇÌÉ‹cÇìñðhÄÛ9åz?FU©f$]éöéîÓW烿{•wÁ–™››ûºÉÉÉO Ãðz½Þwï‚-Ë;yð/¢è·ƒ øøb±8›ÉdþÿÌÌÌg?ôÐC ðð{e'NœÈ7›Í/j4¿˜¦éçès¢V«áy£ÑÕÇüÊ+¯|b¿ßÏ–Ëåg€è½¢€ÕÕÕ/O’ä·Åô§åóù9YAÇ,Ùlß÷988@çfêõú‡är¹ÏQ©>ßaEœoú»áíÜ>S€¾^ÌÞèº.¥R Gìð*• ²“©¡µl—Õ^- ß üâ»EÝÝÝŒã8_¦ÉMà?¿ÛíÎÈLLL8b` 1Ììì¬íjb¶¶¶^{NŸVäp8œÒý¥">ãÊ•+Ùv»ý8¼K øÏ°ßWŸ¦É§4™Õäf—íím._¾Ìþþ>º†l…Ø·s{{{¦ŒúY`Ö:<Hûgêù}àÉ·ÚBŸû«¼Ù8Ôö™²Ç×ämŽØ·É5™ ´Z-¶¸¸h€äy;V|iiɾ_¼xÑÔÑ3ì~==Ë®I ÔðvNj¾ q?¤ã_‚·KB±ðùbø÷4Ág+Qæô`P­V¸zõªÙcee…;î¸Å¥Yãœ=sæŒ)õì³Ï¢ûí^5¹Ö8»W$ " ÙõÕŒ”øHpmî1ðØ[]À‰ó‹Ÿ\.»¿)k|¾˜šËÆ&G5¯4KhÜkÙVAD k`Ž˜6ÀÚ­É5Þ”šžž¶±²&m©P{¦æ1e5®qòäÉ›ŸŸÿDÍמy“¼ßçÞþKç¦~œ/ô†sšE$’ÿµ69ò®Vó½–M]7æÅ¸‹AûÔ»_62pg…‰UTˆ=S ZÁhc———mŒvûÞétØÚÜš)×J3w¶ùIÉA!ü÷u4fËÿùñB©ž§Ò,±0?ÏüÜ‚å·v=55…Øz­=žþy“_ŒYóJ%³Œ&6æ(ï[Qºf ­sV¼±óGkÏ׳Œ$©ëAì‘­&´÷»<û×›3ÿõG/~0ð-×°rËÔ§õvýéAËÇ-–I˜—/¿ÀææýÞÀ"RLêxÓÀ €}ƶ&0ÆT f«µµ5kNe»lej)ìE°xS©ÝîËæ¨5+ø™ÝýWéðï¿ù"OÿÝšˆÉwïæh˽¶’b6Í–SüQÈ ÿ¹Î ìP›-ðàûÝIÞUäíuL‰r¹b…¤Å¤l`ß/\¸`ÇR%‘1*Ÿ(6åþíßþÍ PQvßÿÿÿÿŸÿóþ)ðܳÏÑëöï±¾²õtŸ'z´Öû†aé†rÂù1ú™ßõÿ8Ú^åqËþûï»·V¤7>dã"<ú§ùÕûg¯Œ(éüÜêµB…8I¤€‹Ó…ÑnGM·# IŽlÍà: ÝvëIZAÄvk›‹/¬C9ÃìÇÌVb±Q¤qœ2r<ðêiƒKm³þâ.å© gn[æÔ¹ŠnÞ#%ZØŒõj¥Ê8ñäË3UŸåòÃ-žþ—5v.¶qÒ”æ²gì"MVÉf’aBÜ Eü=ap2DyÙ(NŽ/`«="C¦Ôä+žbó…CÒžCþl‰^Ø&[‘¤—ƤWC‚û24+.“µ kÛÛl¿Ô#RÉ®sêÆEægi·ºLMN3 ú$¹Öv‡Çÿâ"»Ï=Çp/¡çhÖÅòd†°£~Ê)ÔJQHF¾ÐÂi©f•zM—Î`LÅL7*Ç÷ÀdÑýò̓Á´jeiºÎ]7-Ðïøl®uÈ ÄüUèj@V–š¼{Š*N‰rIʯ-4=^xìU^#õºΨÐì$„‹ÿ¾Ç3½Íþ³:ÉÌbd#4«f „/Ž)L¬YÇ—†R=$õÊ•†='—Ëâ‘‘Ö¨¸Ö×)pç¹EFã+´ÇtÀñÖ˜dÛãæ[X¿Ü¦s0"wƱætŠzh1¦pó.ÁT@Y±z¸Õã•çÿ“ wž¤T.ðšÿ:4J9&¦\rN–„”ØIÈUò8‘C€/•%$.Éïe~U6ôéE>ŽT‰e·öÐ;ÞBnAÉQ-±yУÝ÷åw§13§´)X\®ãs œP:`ð=âSc–ï]d…yÚ×"˜Œ™;1CY ‰€­?ܧuyDu±@Ù) ,Œâ±€$8y¬1G òà´UÔ8%ÍêÐÏ2Žv Ò˜FY÷æsÈ>Ú#rÙì±XsTJ5Y™þȧÝ(*çhVS¢0d¯=Tƒf™Ê¸l?6¿äÑmdH»¥g38O%ôV=œÕŒ uq§Š”¦C »Y"Ì7kDµ˜Ñ¾‡[vIóZ”p¥‚«8öd©Hª*ÔœÀòykÞ¼,äàÍjÞpËp´í¶øaÄê\ƒn\aº,©‹¤ ·3l~rië-sÔ„©wúOôYû—ËJ‡z®Ä¨=Æ/‡ _â ¬öò€ÄeIŸðJd \›š¬Px¤LDšƒ¢ÀÖÜ"g—g˜nV Äü@„bÈS†ãàxz4¦Ä@Ÿ·kißõØ>(·c*ŠÇÉú ­îÃØc~¾†¿9TÄ•˜¼ýÊã-‚]ÙoÛǽ=Ïài¤®þ-ÃÊ${—º*xÄ„@.Ž˜.¸dªŽ7"…A^À –@µ’Èp/À!º×RHçj•âñ)taqúËå¯é‘°+Ð9å·Žp¼6nÉ¥¯‡Ýtb–{.,eq¹,œ”lu†LäQ-%dÇ9ú¯Œ˜›®1ô=v¶Šõyðú¬ÌUƒUZ;ËwOªö†> S5Î.Nb?™õG¦ož‰"›Ë”(Õ²¥ÐuŠ“”ª[Ð^´ÌmëA»=Ÿ®¥bN7g¹¸yÀ^gÀÝç—¶@I…5]‡­aØïÑlÔX9Ñ$*¦”ܬЏëQ ˜ªå)K*êšï3–DIš2Ó(³0Y3R{#Ù% IR?,B™Ò˜iCAßUÈñ ܲ:ûåj’iIÇÖAÏÞk0ñ°t t¾¥ zš`c¯Ëüd•ÛϨOÒÄB÷2öCÙÐÀ‘UzdsxŠ_}‡™Ê”åzùäu^êšT”X,æzvÅ)¥_ÎÁËV se‚Qçx²™ŒÌ©Ó—g̨©áqy³ð hµˆU˜ÍÖv:ÆÜç—U¤k¬‰<1ærjaÒ|Ý÷BUßWŸ…à”')rb4£{¾‰5Ðäé ÇÖHò¤MòÒú>O]Ú¶kJ ÀŠ8³4I ¬ívÐDÆ`³æRk›]NÍMÈ.¡šÏ³sÏ7€eÌv5GŒŸ/øÈmR17´w!gҗ≓%êíÑëdñ(O "Q m¥Ã±¸:×d¿;ÂRR´˜Y±³S5MšG–²ôŒIjj^Ùë˜jó5Ä`³X0Ë ê£l.o@õ,+Pd™me!­E®õ`WϦ¡Y8“F,èþzÕ=¾0{ØäË3 sq«e¬©Ñ¸°2MO,¾²q)\Uè½D>Íšj€|  ÃòtÃØf£²Ÿ³g°¥Ž§q‚$Û„ÆðL³l$(e ¤úÝ.KGf½¬ç0Y+›´È[€5àŠ€çr¶òÉ»¾ÎEq秦¹´}HEìTÔ`k›ûœ\žµ1A_ u.ü(2/ëXkFÙzÅ&DFäeGª‰ãDà+öнfÝ$c mNÐ5WÏÈàõ‚g}vlò¯ÅT^´d) †Å““1 íþX,ùÔj×bÒgu¦Nµ\â¹Ë»7Ÿ˜3æ^XÛãñW¶¸åÔ,"Ïò\“›"|/ìG6sd¯º¥Ì^»G¡1K&—ÓõSb)&5­XÑ9SˬvlŒ>tãÊ—ka™Þ:è |dÖ¥ |F6:w­i£€N˜%™>O!ì3Yr¬ˆíVŸšŠ=5?¡¾ÈŠ€Þ•Üý#khU5ödËuA³ˆ46Ép¢1Ià›šRàh…ÎٱƜ1CøîëbTrž%[Z=N Œ`}·‹6c3vr´ö÷( 7‰‘Íìa}cÞ\W"ù‰ÃÌD…¥© n©`@ ùœ1>Ý(‹õª±;Qs_ÛA˜àŒÕã>~œ²$+뙯]‰÷;C£·À°q¬wŸ]HûCÿÆZ¹P;­ô™iTL®öÀ³U TÌ›¢&KdÔ±Iü0äPj¥¡ØSÆ's7S {Œ}ZÃÀl™¦–ëD)¶ÚjÑ´èTêXÄÊ*xQl‹ 0p(bR)P°Ý/wdÇþñº€­»…—L½ÛÏ,ܹ×–ÕÆÖŽl•s ‡i1C/Ô§Ù‚‚€lìv-~gìúÄëÓ>Øc;„º'SRÌ[Oh½ÑžA»Í#ÛXê©ÿìºšÛ ÔâÚŠ’ä[´h~*ð·Ç)ðZ'ÿ©¼þiÙ&ç>%J^¬‘/•ØØn±/fê•"ëŠQ5—ýø)ö휫1Ä!­WéõHu‚8µWq*¯ËªÆ~EG¯XƒŽ}{ï± IÁ¾ Ù±¬ô]ªí“XÞÚ¿À?K×_L’è …;+'ïù9ÂÞšÚ‹•Ù&³ŠÁç/ïIö"j0RçZÃO±µß£-ûˆe[ëjö¬±Ø«»¼mlRQ·÷Ÿškl«°øPùÿmº÷s?ü·÷OLàß½±ÿÓ™ÉÞT5ÿÀ\ÞË“+Ø*<k;‡$œhTAUõO•VgT€Ç«›-äËtÙÀÞ·,ZÈ\^¶pÙ¥ÝX«ñ·…‘Yåï>oaËðÖm=àÛß?%éèUº­=MîþhŒ&5pjJtÞ½X «¥¢_œn˜¿Å*sU{mwÒ˜\¥;½‚Ôë¤Iòƒº~øV û®ú;qßš(‰~VR{u—ö¢Þ,)íuÛ·ÂFj¡ý®½. ­*ÜVÞQ‘uråfßÉ»ß͸#Æù ÿîúC÷øYàÕ€9½aÞ3U+Ëâ[|¬Èµæî |{§ÑªjÌ–>™0Š‚K³¥Oþ¿§þR?þFÉò+ŠX¯^)ݪ¤)ucEjÆÀ¶TDœÄÖZ‘nÁ©˜Ï~×î/ùg.Š9­?§)ô†Þ½jîŒ^ÌPv£†”ç?’¤|<ðç6þ½ô¿U<¤ˆŠøÕÞ0¨Êë“rÎïk±pcÜã¼ýM1&I£IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/star.png0000644000175100017510000000525115144136756023541 0ustar runnerrunner‰PNG  IHDR00Wù‡ pIDATxí—tÉÕ…¿[ÝCY¦Ø2-3󆙙™™™™™™™™™™c†e0³-–¦«î¯“bõ™•2É*œ|ç¼ónsW?¨.þÉüíùÞ"þNÔMì—À®îÆøwaç7k}»¿Ñ|Æà†§î\ÿ¤»¿Ù|ήoÖæÂßžœ¿1Û¿V©¹àA1‹/SmQŽ‚¢Ó‹‰LlûjõmÀØ¿ì¶|©H:7Á‹*½§äùò… ȶ‘µö­|ž“~ï”~DþFþ†]#z…Cïüæ©÷Ú#ØCï©÷–³¹sxE‹ù¢K?“ó7¢š+<‚ 7Ï9éÁ¡~äàñÍÛX÷nœüLð[€ñ©Ø:"EžzŽRcÅI ôGoêËŽ&›s’œxRŠSDÄß]øÑœ¿\áÎõ¤Eç?•ê‚&ÓÑh±ë¯‚˜Þ“ðS€af‰6}8g–((œ'ñÝž¥×œ³ðìÛ[2@0¥•kßšxxËF0·IN?³ ÇÌ–¹N<Í•JßÜãnaˆ™h1÷Ø3´õ×=.FŸ™Ä:`³@ß›1 r)ÜÝAž{Ì­+óO8œøóˆƒ—m`ÿ¦/DÛ²ÓG€ ®&Zõ®Œ«IȤ“‚ôŬ¹èØÃoðÆ:ÓfÝ`ËOß­ÖÀæÍ)¥;EX D®¹[üH6¹3šyàd¬×Ä,»ô”{"F(g³TÒ.i†Y|Ú=Øü‹7¯ÀÅû<ÍxµÌ`L´ó¢ß¿)c‚ªˆ¡…‚ã$]Køúç+¯4–žq_ú–® Ý1 ÀÐÞƒlûÝûIc2+~.ùW­ÈÆ»Œ‡ã)g”~ýú@‰¤:ùÎ1>8q4b.ÕæBæ,;‹G]K•j²ÝBH– \USÒžÒ\Ćö_ñ;nûc·ŠdcFWÚlV'û·ˆ)z/x¤œnúÕëí/½(G7n/q]KË-ò,Ïi,8‚ž…Çј»ŒÆ¼eT꽸Z¥T)ùN]ÆÓé ²>ŠV‹±íŒØÁè¾KÙ{1ÅØ(˜„Ùƒù ö7 üUáí@¡_¾:L¢k ½ÅçdÕ}KO¢wñ jÎ[îZs1Ž¢4²,,,LY—‹U% ÈWÕò0²1€B”7Ý=ÀÈdd†ö\ìm똔’/ròãcò÷õËW…9Rø|¨WovØY·ÓÜþS­T@1Vš€…K1ÓÊš’6Ý`P‡&TLµÉÐÁ-ÚüûÏ{âÀÞ?´ÌÍõ‹Wfs¾^ëë½îÉ׿?JµéJtUSÇv×4òÌÞ:-ul¡ï(6ýäõ m¿r%›é'/ ž˜ÄkŽ>û-쿎KMЗ¿8˜r4hkÚší¶6ŒÆîøê¤öþ’V¨2ÐÖßy—e?¥×æ)Ñ‚ô 9Üùò5«®Ó{½~jÕ£`e˜Â`•ûü49Ó9’ÎL÷õSÙƒ`Q4û¸øûoÅ…›ÂÿÏàúÁ‹@°u¾ÌWz4rÍÛ’…E(”I@è’Jbzº½|šò6¦ö¹oëø·íÜ+q'ð/€DJpâ C{F‡6­üŽ­ý8%ÛÉ8AÛÛÉ2H˜ä¶Ò”µJºóüò>·µÛç:Ûûçôsá¯?胛wŽ8ò$'ÿˆyš  à¯$«ßæ×\TûnõøÓon‚…mäR~(§½„;“E¥œ(]/„1 ©Öds虡·ŸKÖ~‘/.d^(¥/–þôígŠúìðT‹çvj8椛‚ë@]SjúŽäN_JrηÍj,劋¿Ç¥?û•ƒyµI¯R"‰NÀo"iþ«w>6¯ÿHG}Cáª)×çtõªRGêœÈ(i Ë¥`€u+›‡÷^¤Í>í‹W¡Ä{ ¥×é /ZföCzYP˜ѯ¶Ý·Ñû[úŸ`™BWõRi”ˆ TÀÐ.Òîßw­d_ž¸`µPáÏGûE3-|ôÕ'22ZúXV× ®yÇsè­?•:YG*…i&½N\2õ3<¼\NÚ¿ÆFqã5ñ›ß&ý›„ï%|9`¦!3aðV›'Å‚olúÍúþso´L¸n»£Í—ÿ‹ÔQݪª‡E1`Æ÷à¡KäÑ]v(0RÀ—nŒð~ì'‚¯L‰n5P&7`}d×¥cO¿þNjápP@È PdDž@“f‚G CÜ­Ý œ€È@”ÀĪØq98ñ¹½¦Û¢?/ ºÑÊ‚WÇ14²õwTâïEU¨Ê ‹ÜGÈ,eà ¨T"(!ž*pdäJP46h\xm‚1º§H7,£e©Üî…°(Cä˜ ÈÙTGØ%äXêNƇö e8%ˆ‘dº“ÇÝVÓ‚,"P.Ø Ô62 ååy ( PGA' ›ºoÖ¾.EšNˆ.ºP$[,¨Vµ-Lc(/ ¤RPÔÖ´Ï!t˜ ò`–¦H  y«E7BÊXQ©¡J0©ô'¡R$th9:Ô^J__28"r…©ºɪõŠ¡],I‰ hÍ*)‘§Hµ‰Cœ ¾ÁnL8TV“ômaÖÔµ¸¼d)pcŽœ"ý­H`¶]('yñäMÅ„Á¨c’Uy‰©*êRÈ¥ó;~÷ ¢Æc{‰#5`pVpA39ì`ì¶a€M)&rÜŽN¹|ŒK0qO¯\´˜— êÝ#0N7j6}õá”u,<È@Éb‚+6Š+/dX~4sTìW^.ý™Ö ˜#õ¿Å¬œÓâ^ù¥t‡ßeÁñ× œ|M1¿'Á¸!‚#$CŠ˜.èõ7¡K*¿¼æ}šGž¾l”X‡ÅEkÌ%¿NcìQà 6tb! 1F†™£CÆý0÷²8|ÅéâÄó+–™Z—ôòÝ· ã>¸”ÙðúÓxçí*øÝÏM_{É\¿ãޤ7Þ„‰7Þ”•o¾Ïyã9þ “çLn‹¿‚7Ý„ÚëoÄáoº)ž´ŸLÞsä-·ÁŸÖ¼ô‡¯^3½ëNÕ¯O>».è7]Ðá½×hðîqåæûŒõÈêô¯;—ö»ôÅEæ²—¾ŸXoÿô=Ò\ßùå/¯ziþùW¿¼àü³ŸœûàÉÌ0ßg¬GëÕz>äÎi¿äܧTÝ2ª¼Æró¨òú›G–›ï3Öcè÷¢çVË/—dÓoÌL®à­YÉÌ{sS|Ÿ±C¿Ÿ,ʤ¥Rª^+Mˆ’ê{M+¯uÒßHj¥Q_#ZI´>¥ä×ôÌfXïþFƒÖšPR¾ª$Q•¦P7P ’x(šœ"RN!9/‚A²®FÊõ°ÞU£Vaƒðð¢ŽoKá‘ÐizIJMt";nЕ%K¿äKo6mFUº'/Ž Œtøù°®6M2¢tSÛ’–oG” Éлp;ŽeÏÌíôXoo¶ï°‡xŒmÊ>¢GbÚAwüùmZÈ÷Þ(m åj¡¥b]‰i®5lR4­ê¦p‡KQv"±çCt­‡M£ï²qñ,:Åëˆj³nã(ÝQíP]N·øzW¿Fbã¿¢;o˜<2BñnWR´x2e‘rºÆ–QiD+¯#ã!‚¨o‘JvŒˆò(Œ$é+§»û9%u_R¼Ë¹àåÚ[0w¤¬òM:Ç*(‹Ö_1 ´̶3@²Y¢N§G\§}#UtŽ,¥Ï²—)Ýíld¼Lh‰=VºÏ•”Ö~F©»²X%E‘z¢Êí` bg@ÔçûѾÅšÏÐI—³¡3™­SOÐy³=)Üöß²>Ÿt2tÙt>à"6ÊŽbÃÌDº‰ÙD’A+‰vÐbÛ ©ý‹ØPâ,`“úçØ½üÌù>?aôÙv(e»œ d4ˆÂøBÚBÁƒè¶Û™lQXÁ¾³þÅé}ÙoñlSs=Rï%³Z-ÚH’׌Jí‰Ègày&î¸n'Z)‘Y¦åwÑ»îUJ"Ë)è½3±;÷‰vßï Âó1¶b|Á"ÀH "¨˜à˜—#W3ƒLù'¤-K? Y³ŒòÄÌé~%}A ­T­”"\Îû«¸qlzG‘#cäZˆ{ ü2} …‘jJ·ü±ÎÛ¤°@ZÓ>²µy‹‹ÖxÛž#lE˜ «7SIÓ‚qÔÍ}Ÿ7 ŽgQÁ„Š„@89o¨ŒhµBø üÚzÊd®‘êÏFP¶õ߉mNÅ”lü{R9…©_D OWÑØ!a?d"è)¡¤ 9Naoæ»;óÅKcœFpW ` Bˆ0ˆç¸9 Âkm>ƒðÒÁsÒá³YôÑ“Ìzç=ª»iáGKÂE,¬wÕ"¥%ÆT+‚@kªöºŽYŸU1mÔuà$ÁMÙÁ,;p ²ÙÖ8A B[LóûSà¦m¥jáÛ|ðèÍ,Û÷~d¼¨…•ÙÆz—rå¿|‰…­‰2ê~ó ¾2YSî7añRKð-Â Éæ Žaòo[†óÏK²¼|¯=“Ú½®Çé³mý-d=k‘”ÿÛ#b"ŠÔÚ¾ôØšìßžaê½O3ïíg`E;5$-ÏbC%­±Õb¯³à÷{– 5óqÞé4ít*ÎNÿiëôPD‘þÏîOh-Ú.â¶°É>ˆ=ÌØkbÉg£Á­÷i¯²b¿ŒOP-öLp­ññêí6ƒìÒ‹†ñÊ Ç‘Þâo˜§£Ôj|„‹X‹z Øéh ‰ðaMÈ~Mìo×ú!î!Yû¸Ë}j~^,ÆÖÚ•˜àœ©÷I!r ùTÜ©|šq·=AºO´ÿl¥äš<ë9  Á&£A)kׇ#Oaö”—Øþ×ÿ >Xnð= œ`lÐ:ÈUBf¸ÒGQ–×ÖRß” ~Òý k’õ¨$HAC€œGcÆ$³FzíRbïƒÓþ é@Ð9PY–ðW¢D ÿå5 ”"Á1—lýrŠöøåºÖ(éi¤ ¦L#€Ä‘uRõ©œ®OfuC*C{hrÆq »rsÁ™³ÏÇ,±T£OtÖb·}œ•H‹ ¬'Û5®õh½ZÏ€€œýñÔÝl:.q¤í!š®EÆ@¿ÒµäMê\hëè G›ÍÛ™šêg¤8BµSe¦2Þêv.ïd{eûÖÙæì—;Ë_þÉ5@P®‹y !/Çq <àà¶§Þ–¡!Šù"ù|ž|GDˆÂˆ0 ‰¢ˆ•ú 츀_øcv­ìݽ¼‡a> Ć߸Á“·ÜtË›?ûŽ”{j,ï"æô;œxî~ö=˜Þ8MWºT ëa8¦lËÌ,Ìðƒÿ€__ökÄc«xúJà¬nâ)/ºéK>yÓ7á£çä `Ë‘<Ã^Ë~U"½ÇÎä±·z<åñ2½5àš0œ&¬†|ão_çï;þŠúò÷8Ÿ<˜å‚AïÍxúª·ÜâíœSù?¼âûw~/ÁËÖÁ¦›ü0çŽÎíŸÏQ›b±³ÈµÉdq’¹¹9Þ÷«wÓrY“7÷þ  9y›zúòGœòHnêyú¯ŸŒ }ËaðBy ´ôǽtã‹îùR¢|L-¬‘¸äZM­[C}ån§ÞÝ »—[Ë6¾lŘNJð²Ÿü0Î<úl:q‡?ïþ=ÆÊí¬M®-Ô¦~ó¬co`xû³-s]³À"»Ã“øÕù?/þyçï¿*>r¿Ä)S§²ÜXf(?„UÁ`MR$-¾xÚQgØ{Ýú>Ì·æ¹ÎV±§¾‡[q[ntÌÅZXi®€@7ìbÁ²¦@®ž›HÑÇJã¥;ßâ,4X/&1ì™ÝÃâò".vX,ÆrùÅR‘£§&&^]\8ˆùÆ<(÷“¢Ð¡ƒEP1k øíø»& ¦î§‡°®p8‚æfæØrÙ¶lßB'ì¬yÒ¥B‰“Ž=‰ãO8ž‰É‰µR”ýhšÐ„xbQq‡(×ó÷OêÑ­ît›»ÑÐ)káQ3ÊO~ó.¼òB‹fÔäü­çsÁÎ 8zóÑÜàÌ06<¶vyM“ѺxX·Æ3 µøc¥ †&‡éD]Ö¢ä—h.4ùܯ?G‡dHºFØØ¨‡Cdb"?&ñ꽨Uv,í`÷_vs‹ë߂㦎[«|—N£‹Áô8H`¼“{z·‘œtÆíΦ֩³… À9çžÃ¿¶ü‹ Q¡yø]‹ÆŠ:09­€S¯-Þø"â!G{"Fó Iøó¥¦V9ýØÓ³Â‡*Ÿ&‹àŒá ¤â^21º‘ÂH‰0 9¾õ¹üÒËW•°#Iˆ½ ÞwÈëÏäçÀ€©Ë„&zwBî.%ñwÙ ulH4æÀÐ碅‹( •Ø4º Ü7à@E±Ö€UY%0i 'všññ#ÇNÐì4Y‹êb•¿^ðW2L@>ö"[í¨÷@àfø"½ œ¼­;~ÇîÈÒv1í#²‚çΞËm‡nKÞäWÏfŠbÅ€K- ¤H,O ‚òÈPÿô9þzîþò9c)I@nÜûê¢×:1-X.N Êöòâ^IS†0rîܹ„&$”^HgèzÑtvâÆ Þê×à>åaB¡¡r(vÎì¤u„!SÀ/˜­»[µGÊ: JÞ×» ñÉÍëG0Ä@&2ä yÚ´±bA¸p ‰@xbIN9…`ï9ñ飓eºa—CaÔpÅö+È$‡(±ñî}$å]%ùfg>~Hë&!;Å€D„7Át~šœË‘Ä I˜€’’®O*ë:gH ±0vBÒmÛØ8¢(âPÔªé \F [¶?.aˆÓÏ·f⇴ïѽŒ8aÌŒ1e¦È%9èBF¯ÞûV«Eu¥J½Rï縩a¬Øý±FÇ»XQ+k ,­,ÑÇk ´<ýëdrxôž3î}\ë.!áãFìÙH.ÊA;-.±`Ä`Œ¡Ú©R]®²¼´LÔŒ „@ I¬hì"2M8!I@®)Ðh7 ø`#Á©¢Æý‚urï{œúµÅ›ÕøÜÊß þáÑumv…;0‘ôbP}³ õzúŸƒÁK|Äž5Ä¡ÃÏëŸÉ\”­Î“„Žƒ°Úú±]–p9ëä¼s_ZÙÕykábIÀPS‘XZ1âaðK&ð°<0Vð€ÉÉßœò* ¾… Q…jµ‚ñ=Œ1Xc+ØÀ¢(.pà“JøŠ 1ŸHsíâ¨èNU¥²·Bì;È€˜¼éOÒ ®®Egô,à"®!.¯Öx #zDβUEñ0ĸʧq¾[% 9E÷(qßáš È#ñ®8ïŒ8uΩ ²/~/£I^xf-©l…’«œB –1ødÙ\%@ðÁ©¹3ðA®IÛ•r¯ìî®I£ÆRÕ„ª8iHS“BlU«š$½a”®è2^Ô¥»-ïy±§Öêüj‰Ð:FOÈÍ$÷ŒF¼Ûâj¢wLîtO*Ånd ¹!"§9§ ¡‹]äb?´I·'#’wÚu"—Ix¤D2(ûs=Hshc…>Ç)¹f`ãnòµéÇ1Gˆ)ÉrÜdrŠv\SÆñ¹7ÂýQî û 8pa ñ/–õë×+RžÛÚºìÍW\±»3‘HøBŒp8¢D"ÈÃ%F‘ëÈ1B„à8„ 49¢ˆ'¸¦ây>A¥‚aX|ç;?ª‘ß:dYæ!`nÑÇNvv®{Ïãÿ¼æ‡?ü2êê*ÿnÓ¹.1!å!Ûv΋–e!„‘ˆè‘ë6®ë*‰DŒ¦¦¦–òòê]ssSý@ ð% TÒõl6=!jÎf ˆn¸¡‡þþ3BDÁÐm<×Ã0 ‚h‚ "Bpž°"„}‰*®>¢$Z–B8šbÏ·rÍ5{¨ªª UTT/U€8‹0::êÕÕ5 ŠˆKŠE€™™ ^ýê÷aûÝ(‘JÐT|%!*s”2‰!‰Zp ªÁ9 øó< f,ššGسjjª•êjyà @dÑßu½³…B†bÑ ³³…êêkå¹»ðë# ˆ…ƒÑ <Ui ) Ã+p\°ž§"ŒŒL`Y%êëkä·ë«2%Î+*Qį=…BÎ×õ@@KKí­qð‹W!¦D• jµ FÕ ™‡BÂ+ËŸC×Å¢µÒ âS(K&“q€E ÿßNŸË %$SÐÚÚÊ’ÀÊÔ´`¡%þaˆG$FY@Ÿ#›-°uÛf"q=è”áB>@tˆÂ)@î!äá{ÚÎò­_Çcû86mÚ%«“ä‘b.•!—Ë.Ú€ðE èȧ …\,D€ïC}]•täL§Át ¤.X( ¹l2‰ ƒsyèMÑ0×OGs-¦áñÉd‚¾¾öÞñ»looïà$0 X!.²ˆ…Žär™¬ P•¦iQßÐ@ç²röÝ“…ÉT'¡"IŸ Ï«àù`Ú=Ð È%(ØÔy§xƵaÊʸsï1²¹"…b‰}wÝ„¤ì¼ã8}Àq`ð%À0ôiñ㨤ÔÊRÉTWÁŠe°‚l¹.AɆH!èÄá`C|>(í–%OòæîW]þöí?Ì{z@D[fžêê Ö®Ý;thoH&À¢œ={ÜܲeפÔ.±+;k!>X&¦ b‚¾  ‚ˆªÂ`Ù>ÌkØÌÚ5Û¹éÆ›ùî=˜]FmÔäƒÏ\φ [¸ýöƒ2o,‹—Jõ~é€_*•úòùÍëW÷òÚ'\ËìœÉ¿õ5¾uÒe°áQмb*Žç3–2 iPUU¡µµ-_TáûI€s&›Í¸ÒV@&ylÝØN‰ mj‚@H(¬ŽˆˆÂ½‡·m>Ç3ñî=Þ˧¿ý~=ÝH®óÑ„ÚY˜;i1†¦óxžÌ"hhhij€øý"À4õÞ‚i 9¶QÃQÖ.«°ø\HTÃóˆ ú;ì½¼ëÚr®¼üüöú[øòïnå`ÙÕxk!|ËpôyP *â3èÓǰl—D"Nyye%PD݉öî½~rÍš®Žíì¶l€ëZP¢ÃøZ$!ĽP”¸1ÆÃËïá%Ø-÷ªùê׿ÎÍ}3í—Ñ^VEÒ8D[e˜e+*i«ŽQŸP©KvP]¹ÃòˆÇ£ÒMå²z«r]'v¿\×UftÓ¤”+0<=Nêð]lÎâžäñh¶Îñ‚®¾úô¤¸ùæ¯ã+6O¹n+­ IZj“T—ÕÑØØ$zã(jÛõp\ÏUWÎeqFc’¶ @üE xË[¾pu"¡=ÜÅçÐoËÑ¿åÕ•J##èÅuuÌõžb²µ•ª¥KÁÿS>¯–Ö>+5|0vQ› ºðùeÑ^±ãšú¹[Ûy˧÷²yí*vìcO=uŠÒðűQòýýèé9Vä²÷í%-fã5D²IÝØØðdE÷`p¸ˆrÑ^ZYymèÞ½_Þä®î¼âo§¦k¹¡!J§O¡Ïïí%ß}è3³˜¶‡æBÂq0“I–¯XAç’NŽéÙcšÖï3™ÔÔÇ>ömGQÔ¥’»d÷®KUiO§ÓÇzznûÑ7þb°Ñ{£¯­ªJê†ùþ¶Dè%|áó«Ÿüt,!V8s]ãùǘË鯪#‰¡è%2ËØøâW‹€µ˜†Å™3gý¥é–æötYY¢!‘,«˜ß&5Lƒè|JÕ¦§gRããã7>süó¿þõ÷Ž:à_”€—'ËvÆ-ý“»woÝyù+^IÕÖ‡Ñû{1ÇFÄó‚ÁŠÇŽ1^(q°m%æ%—Q/Ä}½€iš,Ù¼ƒd"‰Xˆ²ŠJâÉ$ªF GQCQ-ŠIb;.z>…•ŸÅÈÏ‘ÍåŒÓ§OßÒ×wêÓ7ÜðÓ} BþY/DžÕæóÞ3Þð6Ó¦xê–7Ä2º/ˆßÏœfªd²¯c¡=à’«D}M­ÓpÙü¥²º–dy%ž¯àz*rÎäºÀ]wHO ¡˜çÀ.ÒÛ7`9rà+¿ûÝ>lú `ÿ]oŒÆÊ Óüòæöº'?êSŸ¤êÊnŒ‘AÌÁ~!?Œ!©ÒòRó–tØ)ÃæöÆNªŸð"žøÜ§ÓÑVaºø¨¨¡yÂ`Û' +ç8‚àX ÑYˆ„–åb¥‡H*ÄVüú׿¹óøñ‹s¹ô`üUoG*]ÛúÉuÛÖu_û™Ï^Õ%5|kd{bž¸˜·Îœ 3)ä÷ÖvxôÓY{é*åTT$YÚÙN²"ŠïƒiýÙž(๠).Ѱ‹cèä³Y<ÇÀ³udQÃþý‡øæ7¿ñË[nùå›Á¿ð:-´"ì:ßø£ölßõ±Oá%*1OÖZ·ÆG1¥æu¢ qwjŠ Ë㮦å$þd6]ÕMuy¦°-uPT*++¨©¯¡®±T Aðóþ·_F_£ªu×ùÙ×½iÓ§¿Œk˜X÷Æ£ˆï1çE ¢¤RLy¿‹×Ûéå¹råúp2B¡°$]‹©±1R33´/m“{*–¡ã¹.²$•l4‰$’åh¡r!X/:=ÊÊó´¶—“LFE€Š¤Xb±'NÌg°hÊ.h7À·Û½ë™Û¿üMÝÀé?ƒ#–±Æ…ø˜@j]Z/“fØp=Iãç¹âB82~ÉÖ+Ö®^µvå–-[Ë–.]Bmí|Í)xž·¥ó ‘‰‰DD ƒ´>à{>r_ì— ¼{fƒÊ…黚WÒøžj‡FkÖ¬†Þ®W%€¤ÁU·z&¿·ž¶ûtk5Hï#'ÑæöŸv’^-x@ƒÂ¸ä^/|HOgßá!3}F­6|-ש9³uÄOꉬÛÔ3&C-#Úø¾Ëí-àV¸ÁquHž;z jxW‡ý¸¿Ìi $/t}p㻺3ùJOÛñ¹Ú÷›=Äó° ùÚòãÍõFÑÚ2½zU!tÜÖZ¤ŸÊ¾pÀw{éÔ c\L˜Z.úJ-|.:KË,lG(6ãÙ?nJ™¾¬|Q×Å] “~;RÏçÞC›ï°>^Àkìá tJÐÑú¥þ-]qª°<"îM½6àøEsÛtMìù„•ïx&ȇ5Îâ|ï¯wÓn_ãóñƒ¾ßÇݦZؼbsãÛ>×À±Ùbî!?ì«ËžñM‹±ó8ïXpµ»PÛÈd§Ÿðp=„!è'Yy_2¯CÆf‰Ýꫪ—Óò)ÉÌNåŸ€ŽÆ”?ÿ p0yT•¦Ž©Ñôquê}Ÿ8¢B3&Ôi¦aÖ¤MS­I£*µpF«;îÔfñ žÇžG (cÁüi-|“vî|Ó?í ÉêlJÕ€ö,ÕWι&A㇗iÔ "ñ>¸;WÝ-éš4²BCzòÔ`mGVºóÇ -c¾µUp7â‚tGCІ÷/ÐÀŽ,X¨¦êx„ Œïù' ±*^]ÍibDYpX¿|M]¥þ홪*ŽÔP#=vh©¸OY®–ÚDÇšÀƈù„Å ÎR"Èö´f0Çîéê2a •qBäpØVŸä¯¬5¬¾°ÜèÁÅBX6·‹0<3!ž¬+á;ŒEæ‘4Èš²5¸+Gx£½>Ù_i ß)%îkå¦ý¤Ò¼Psw‘¡üŒ_”›þ³Ø¢ ¦·ª¢0\Ù©?ª0ë7õoËrçó\V¦qÃÊ”‘ô½ŠsƒUY!<ÊDó‚¬_•ûµ²R~0üè›\Ë"fÁ8÷´MQN`mB6b´»I Htr…öÙ–èXëO_+ÚðwÖ!$çLnäÙßm´ÍÜZ[mnÎ¥E 0³p¦h[<«]´!tìR€ê’(Hrw0m\Zë’ÈDÐŽ0' ý€U A(`mˆHÐÆXB¢¦4Zcþ/ÀB%ËBÊõ&ß¡Ü(ËßènNGká1_`9<à`WÁò lžÃEž8óÙ¹Ø>ÉÇâä[ŽAx&´xggB¤oø{?‹°˜óŒ¶TÞa1yB1ˆs#rÅünæà¬ÎwÙá80}¾ `Qb·B2XÏX¸Dqç~gœ'„ChA OÌœXO¬C–w Dˆ9^äÆã¾ €–%dX”g¬ÊY@ ;9‚v%ú=èë×–¡V2…SN0àÂ!x¡¾ `€›!ÂIJ,;b8¬I=z”†au,AÀ9€w™hæ#‚Ï×$fQˆrwâ!ˆpÂQž¶Þ9€eéc«e,¡B_ÿö,DPâÆ8ðA€? DÀƒ²Î¡†,N9ŒC=䟦YÉœšûµ²‹¿ëªª"Å-¬²”™üƒrÒ~²P gß§¢òÄkT¢–O%¶Í†ˆ2#5þ[•س(…ËUú‚"JŸVPîÁyO(µõ%±ÕÏÑî"©ñ!Âõ@g1O~`qKØjbÞ¬žJøQ¦S 1†BÜ"z ¸Í°ßZ ¸û±+tâ‰' œqÖ©/~t3íá@’bév#åìZ„„B޼@L%:çA€ÿÜ`ØÇ°é.`Ô bbT¦–µª-1dº¥3I‹GðB8 ©pÁ¬‰Õ ŒÓÅ†Ý ›üáÚŒ\NêOʳò›ÂŽ{avJrC¬Tÿ–2šÿÉYì£ØðϬï7ýøõêjJÒÌÑ ûù…ãt‚a;ÃFk%àÓðûQðò'·èÉ7®wñÊg·Òð`Kee×!lœ“›Ð¢½º$R)qß°ÿñ×ô¾:ëôbf¶êK¾f|.5ìmØÜ°ÁÚè´‹›§ÏGeD˜w :Nßðß>TLø§ÊÏøUýZS5yDž4»Tš•«q¢æ—[ ‡¶v~Á±N¥ko¹À#àü‹ÎÐu·]¨¯b (€ƒŒ$æ´¦vš9¡^$*[æ·_¾"-ì/Ío—æÖKsÊ|žæO^qò‰‡¾d\Ž5ì`ØØ!¿N.¸äLHć¿ÝPIŒÕ!–Ïë'zQN0òÒ¼#_!Í.ÐÒiizã…Û#ŒÇé†]ÙyœÐù3ðßK§œF˜ J$aù¶‘o2òUF¾Pš™©œ¤ZÃE½ãÞ7't"wgž}jŸàº“Rº&Fš[mä‹¥YÙÜ<߯ îßNú) Ð6ºJœ¬$±óßHÊrj£o>{E“G:I›£‘=³7ÙdNÜà ÛzâÞ/¼xîú*¶X’ûû¤‡ °$vê~NÜ¥s:EMaۥ攉¤Ö1×ÈßnkiØÞC~]ü–ñ"ú @WÀ„îÿÁÂmbgj« 3òÒÁvy«òì8›¬*îûôK¾%³;ÑWPÅ2? ø‰?J\6³@šB'³×÷…|Íê?¨oÆÎVÿæ° ¶Ö†ÃûBþOÿE79ÐXÔ…¡ ‡ó}!ÿgþ©Iz³á Ãi䛳ÛôüŸþÇûö4ìhØÂ[ßôÆŸÛðÍM ŽÕ×…<øùÒ–ÚüßO„IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/help.png0000644000175100017510000000670715144136756023527 0ustar runnerrunner‰PNG  IHDR00Wù‡ ŽIDATxÚíZ—9—½R«Ìl7Cº‡g2¼ÌÌÌŒ–é_,óXffÆp23ád:ÔhÆBIۑˉâ8ß—Í2è©÷>лOn‚ÿAÐhU?_àÑ*ðï4þ½h Y`=‡„]†ieӘРBBß ý|·n½ìt „Ä%X, YZ5ÓégÌXò3ž8`XvÕˆ%ÓTÓLA( 8cž; ýa3tWÙppÚï7;¤só;úØ&X(Ù…ùç´Lös¹ê§äË‹KÙr5žÊg¨J¢›p9AÈ"8X t†p{=1hÕƒÞÞV£×Ü; j/õ»—Ð>y€ @üG âˆ?ùBª¸øå™Êü̬>´P^^4R…„iÁá~(ö'G×ex!@§†F`1„Ã>µ]Þ¸z¹×ØÞ8<ìîü£ýpj@ðA@*±ùÕÏMåæ¿cní‰g—{ØJ+BGÏpƒ!àBÈ}w"` „%†N75dLÀ èl^çÛÏìv·/üY¯ví×Ñ;ý>÷þ@ÝßЇ×â‹+ßZ\xâ‡}ñã_{æi#LæQwºƒ2p>aB 8pý Œ 8GÏði Ùr‰ËåAìIÆØ’ {Ãí=þ¿í&xkyé»*+¿ûÉÿÄjéÀiZƒ`d]1F<Ú !ä AäcAÈ!"㉛äÄ(Ôúž€O¢T-ëµV˜¬:†µ‹ÞÖfDâ P`fÞ^]ûîêòÁï8øqŸPHÎ-b§¢ï²$ "àê~ì à#«CñQq!0ô9¸ní“(Pšsaß™s¸µ¡$> ›\úø¯)/>þ½O}Â'”³ Øî†pr¼\•}>òƒ ­€)¦ù¹Œš¨TöI:ç :yÏ—á7vð-ò÷i¹Ùµ{òc?q5»¸Lvú nÈA € ðãÇ¢´pD`*‰±'È"†‰J)GÃA°0ð<7luOýq¿(òëëéüzè™ùÄÅ'žÐj.0ôÆCÜ6sJ‘† €©ÄtŠ˜AahT‚ ŸšärL†0m ÅL¶: žË®²áÕK¼û%N–ŸýÊÊ'¾å±_H ´8ÚÃàÎLUD $ØbÊÄz%g–2x~5‹g÷×Çæ’X*ØHÙx”´Œ …„Šå@@dÞär)˜ ÉnsÏrÃØQ¸;õI/èSÃ*ûô²‘)~ñâÃæôT­n.n›J{¥”)Á>¿’ÁúLåT ¶IA©>¸ÑÃK§ëx÷rKþò±' €ÛG-%5¤íiáÆÚÇ:µOñÛ¸  óÑ<4ókŸ?³üä7¬?óŒÝELž8Pc]É„©á‹Ÿ­à›?iO-fPLš’”r¸>—DmC“ÞY«$ñø|JZøZÃÁÐgwY_»I€ŽH0¤Rqh,4»µm¸¡sn»@ÜË8X°“•O®.­fôD ½îq¯—#¦†¶‹‹;}œÛ`{ïø\VÜå¢g—3xd6…¥bßô‰óè:!þüøÚÃ0 ¥q2ãÖä\ ç äçfi*7÷̰±û¤‡ ؽh(ëñLþ¹Ò\•EÀ‚1zuFÀeE}ëbKÆõÕºƒK{C´>¼(Ö)!Ò¯kâ›>aŸôHåt Ÿw°„£WÚè9}0!¤W(p›$é¥b:ÜÜB±µwùyooå%`c€˜FÀ6õü#™âÌL"›ÇžÇ ¸Ra'= d—wûà‡˜sœ¤"J”¾ÇÐHZÖª ,âX.ű²?/ì ÀËÅ#œžÐP¨Võ«Fâ ²¹2Úµ1 u`)aZÆã¹R9Ó„0@&ì´JKÆ ¥úx^Àð¢×0.ä|¥î`§í,"cë ` 5|nïAÜP ™Ë’T:¿jkñy5wU;m˜‰•T.G©å•„“•Vcb“S¨ÅmˤœÖ÷' žÌ@(`#ëCñ|&`Ø6ìl!Ï©6so–Ö,«b'ãÄãò‹'ÁO-dÓV!”=€DLÃc³IÌçl@­ëáFËçŠõeOÉhJþº ;™Ži†9 6€•NÆìxÚˆYBð-ƒOP¬®j"¡*! Óñ±²øŠgQJ›p|†7Î7eH ¡XŸ`bÊÇFÆ"–mkš¡ ™Lbb37®é1‹êØ-I¬ÈãIõ U>O€š—„¥ãÅÕ,¾åçñÔBZVâWÏÖñ×'wÑWw‚;ó$ªÈ㔌4àzàу L“PBÅ-¥™ŒkÅúj‚ãøˆ¤¥ã“Éïƒ_دI™ä/Ÿ©ã×Þ¸Ž‹»C„lD’L³> zBN hš p:€Ñ}‡àÜØÉjžDÚèÙå4¾m¿B?<“”½ñ_ŸØÃï¼³‰‹;ø,’ä“Ö¤„ê‰q}¸ÌT€L¯ÄeNz.g Z ÓÁ+ɪ¸ BÙ2ÒGŸ°Œ‡ªIósr¿þæu|XÊüRã\µ:€ú%’ BAÀú€Æ§%1‡×ëžÛ \:%Jب ‰R&Hb,ðTâ2æ)%8~µƒ?<´…Ë{Cx&OL†’‘>ÒÀá;CÆC^6¼i…LÀ{¾7܆ÂÐJÆá!"Ð$òÄô¸?F °X°‘Ošè»!Þ<ß”1ï‡\i'„ÜùØ„62u°Ãaß oWi1'4Ü|çÃ^«ÅM" S ¨–ž^(à¡\§BÐw™”ç¶RÓLmt rç)dã`Ðn´5ÏÙàÝCÌ]†þú¹N­æ"ð–¡Ë»iyõdWÀ«Œ !““q‚WÎ4p£á`à3œÙìIRP­‹;CÓ<²¸AÐmµ…Ó©_uØð:~ö‡íÓíúöö ÕZKægÐv‚;{]¥ï¹é€qéBŽ7ö†´þ$%Š•1}?~ @34X”ãòæVèºý“h·v?†zpÁÉ5×7wWJjêô–fB¸^‚”w?b„"eéø¬'Šø¼§ÊRþÙ±¼y¡)œ‰Ÿf}5Ĥ‚õ{m46¯7ƒ^÷pµý‘œj:ƒüµŸ3»¾’ÉØ)8¾ ¨•!BÎÁ8ÔŠŠÙœ…/~»–ƒ#y}n«­–{§Å`¼W‘ø£H›[6E§~ã”ç÷ߟ¼7¥¸{ üAóÍöîõ“µk7DÖb:w^ÒÚ>“}nÔ ß}Ã@ n©N‚ÑžÞ%“ÕJ‹)Ö'ÈÆuð~›—ºÁ°õhŸ¸€}´žXÀÛuh¬”`Â|1_­ÚÔŠ£Ùà…\ÆyÈU¡GTðr„’A<¦Éfç¯NìIÑé4î} %Т8ªº¸©£§¸~ö<ÿðƒ£owk×~Þîuâ~®U_Ó[Œé¨‘x¨8S¥.'xcòª>¹û²Ûg\*Í£¼~®Ó›}Ùz`T‚—©$dèT3¼Ú.Î=´Óܾô³¬vè-îýÞ 8;+™L$³ÅRµHÜÒ ÷º"?$dˆ5ú>šÑ0¯“Ö§«N‰ì™í`€Þ9ìn]þà÷k»¿ ìÖîûf. ½øÑ öt&“Mä‹9ÙÞùÓI(•”bu…%ê>Ê*5Tšø¸püD¸ñþ‘Wº;×~þ‰sØÜN×½ Ûßä”ZŽÃËd3VqŸ„Ï ¥±¦QÁkHïÓ¬%¬©STÒ1¤©‹'N²‹'kîlü4:GCç@ƒ¾ßÖ®xœÅܾ»žN'íR) ¢éð.îM‚N€ï'c?Ó1“5‘`Cœ?vüá£ÚåŸ@íÝ—tˆ%‰ðF;ìËàþ°×[34#Y.¦I2a2ƹS¬/ANXÿ6pËÐdÈÌ$5ø=œ9rÜûðƒÃ¯7› Û‡^ÐQÀ?•D+ì:ç<ÆwNsÖ…T"¦•r)¤âæø4òˆr¦+€5ùC•=r)C%¥Ã ‡¸qá¢8säh}óüÉ?ìn]ù4޽  @ü{ÿJI¤þ…d¥ú•éòÂç׿J‹‹z¢P×cp]p 1?*†¶Aai@Œ0xýê[[bëÒ¥~sûÊñncûü­úßïmðÿ£'6êL¬ºú‚•È}¾™ùøtu~!]¬XñLšÚÉ$ Ë‚®ë#/†œ^Otë°±{£Ý©m¾çw[ÿ0èn¾‚îé‹zøÖ/õdDd¡€ÒÜà ;ó±â-;¹nÚ骳šiB€2ø®ë Ú~¯w=t»ø¢Ìë\ÿíkQ¸„ÿUÿ+A¢\Š I \@Ü/À4‹1ʨÒÀ8±°¿_GÃomjñÿÓó„(ëxˆ)ÿ±òÿã_´½”8—w;µIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/log.png0000644000175100017510000000324015144136756023345 0ustar runnerrunner‰PNG  IHDR00ý 1 gIDATx¥–…n,ËÕ…¿Ý8=0ãáÿ3Š’¹Âˆ+offA˜™a˜/“y|Æöxšj¥Ô’,Ëž> [­úöªjõlíU ‰à¯ýaöãÁ›£˜î¾cø÷l¼ïÍ?|M€HžÐÄF3‹˜¨:åG¶“Á§pàlΉs‚DÓ¯RÔr8ÒNÿ}ûŽO=@Lfz° ÂÓ©¶AVü¹|ùKè> ÄÃ4Ñg”2Né åÄ:¥K¤’˜žŽ€Ìk#ֈسà”ÚÇŠ…ŽÈ0fZTè9»Í2Û%c¤ÈæüÌ žQn]™éQëp¬Usž I|Lmžgõ2»Ž45f%P#o‘0O‡Ez4—!ë$$äô‰YÄ|9c»¤ô˜PyÂBR ˜US&@AÁ€ÐÇqt{=¡¤"Äa7î‡LÓÅ1JæÒV Є)‰rº<ÇKõ<ÄL3%W‰0srŒè(gÌ<ûZã ¨¥¶5XTÐl·Â"†JlAª-¤RhÂ$3‚†›5F  Af„µ% $D€k:ˆˆhô%Æ€hˆp¨í²ñ”8&ÚdUžT†´ÀÐÆóX7”„f[d¾Ô¡rÏMoX­®9NZIx~óÅT^:Ï9Ûf©2×|—1Ւͬà1sôxš rr ×Ù!Â2FTÌ1¥çuÝœòë<럜„’ü—Žx  ÙuRÆ\Ó˜‘©‹S.û:¥¢«ˆ„Z‡@ ?Ò;,q¢!û¬kÂÿ#µU`™"‹èÈl¡ù8²Ð÷À«u)˜=+"¥3•3qR= pÊì˜H2ײLø;ºœàXæúL‰™¶é‘5çö˜J 6Ùf‹†Ü%" hî¯ m»­±.þ?À+r‰ÿº‰.½i±HÈ*YL(3‡5÷?¾˜‹‰\úû%àòá©Í¢]ž"äŒ.ò\åß§ä”$ôy  hèÎø} m ´Îæ…Rk^"»T<€“Ì8hº×AX©ÒEVª6Se†„Aâ 4"™ç©b‹° ” µY$öø'91)Gd¬Ïã0J2nówR Ëÿ$ Ï1{D”„Üòs}îr‡Û³-(dQËäz9”êð:=Ç¥ìñZÅlËxÒ:Ï’h…Œ’®Œ æµJ‚Ô¶–(°S[HÌT‰9ÌïsÅVÓõó=2V’z}Líµ´oCÌÏu¨åÚ,B#ö%úŒµ‹x))ôú%Œ™hŽš¡¶B …œ{î+zžŠB1)kmçëëºe”ºm9"Õ¢õtË–¸®ÚBðŒ.œƒš Ñà|´ó7¥Õ0{‘¡ bJí#ŽHèÑ%ð hœkƒ o´XÄ<ÃKǧÑml?h-éˆm N “«ÃªÔN.\ûu-•*7˜rª33a Y;¹4×v] 1ड़7†Ô+nXö·:ü¶§…÷žÿçYÉ×Î-æSç×_.ï»`Çóü씤5£ Q™\.‡H×3C¢r¢(BNu³5} [hˆ r\b˜Z:ÓÛ©«r)' Ë,†ë1>>†—ªâ õOtÊåLÊÝÑlãS Cb—Ñšÿ49'O;KRñWÐhZ-ÙAÌÒä ÎÈq\!ð' —p , M¡¡5-´áz$—¼ê#“NÑ/qRlbO°\pžÉÈ¡C®a×ÊÊd™GÃTÛSHˆ¥ rœÝ«ùr]k±6¶’1€½~Ì«ÞøhžôÔÿÅhƒåIl#f¨ÄÎSJ/ ­˜ƒ"AL½é$tkq´A}A–„j' 1¹©lvÎrúä1Dv>ãÍÿC9‡F’œýñ[8qð»ŒcŠ€½H¢•Ay׿Øzæ;Å…E ­¸váeè.¦`!,`’ZÅ:ò¦:z8}ö,Ÿ}ç‹ÉUײýùÛî=Ì¡¾‹öc?`ë]ŸÏo¿—‰Þ>l[‚"!ATÔXŽ@ØL#ªh‚Ê„éE 9P’0€-™ÇË‚›cQ²ÄD´€p4_C´“áCOkÁKXBû†_âåX®Eª:Ö7mC„„È×§0JÏÞ…‚óß []‹•ßJÚ¶±$d]b" £8BÍE‘Œú\€¾ñ¦Ä“££Ô5ÝÆ@碂ÆIZè"djRø¥!Q )Ž”â¶° ‘–@ˆÊ.©ðjŒà:¨ _¥ïøþ»•¦~ÙNÜfzÛ[i^û/Ô/ÚoàçNý–ÓÇÊèP;'~ùE& ì¤EËpÈí‹)NLD_Õ$F0†ÒH m –‘Dš%‹–Pœœ$—žWÑJE, ¨ÈðÈ8AP¢®6ãØôžþ%ɤG80Ì¡C_ {t˜êæÅܺù;þkzöUº >ãØ¡ƒ[’¤ŒƒL††ö¾A¬”$BcKbëE®ß–Ž”ÂüúFŒ_¬x+QYðƒùÍu45VÓÑ5€1P,„,[¶€åÿ¹©‚ÏÙ¶öî9ÊùÖÿä¿]Ìw¡š‹Œœ™Âòn¿bECM Êø:- §®ì1‰Ö…h+Q9ˆ£Ò(ko²¯„³g»¨ªJaY ÏåðÑ XR°x~ )w„'> •E ïÇc¼áÝE¥(Y–ÞÖJÂ>Á^©„GU&è¦Á9ÁÉ„Ú` H)éïíÆF1Ñ:ʉb (.a­×êóêêæñÏÿ°ŽŸÿê0¥0¢­½Ÿ¢ï“Ïeð<‡ÿþ-8ŽB <í1#£ýü|ï>NïaÍêf~ýû]„A†@FpHh›`l€ºÆs¤ì.Nû,Æ´ÖL”JL„aÅ ™Ráì-ðäÇße4ÇŽwò¥¯ýšþaV­haã†e,]ÒHU6/(¥äB{+}ƒC'%é*‡© HC½M2=Ÿìþ-µõçµ ön#Þääp|çi´`"Jb%Ö@é,Q`[/m3s­îèuHk[?»÷§p ),þç?7±få"’iŒÀ¶-´±_võòã=ð‡jªHà,Ò¼¸†RÿÃÔÔŸ%á.ÄZ0ÊE: ql›¨4L¶~JýÏü,>­zZRŽ ¢hö·ÓÇO¶ÒÑ9HsSÍE7ÙÄê ©Ê§‰e[h£8×ÞÍvàH[ÝÇÇHDÃ…©xZkü®1|ÏÂmÌÆ¯ÚºŸ¢Ã±åÂHÈâ$Zp½û‘ÏÏ}T_¸6ˆ AÌŒؼy%«V4S3/O*•`bbÏMðëƒøíÂ>6²¨®O2èû$[ò±Ô)ƒ.)[Ò”ö˜š, E-K¾HOÛ}10±¢Ø–…í¹ØB«ý"gr&fÿHù_ÿ±‘MëVQWWß ¸®C"áñ‹={ùéoöÄ›7B l¥ nS“¶Ie“x™$‰l‚TMš|}6öÝl.Š–Rè0Éd©…å8ØRÆ©…Æ– e5•¨•ɤ=ZZV#¥A)C*]MÁŸÂurÌË/aÇ–À‚R’0ˆ°6Øh#ˆÂÛµÁÄ}â4ŠãƒÛáv‰´ò´,¤h¥°¥‡6°ss(r ‚ig!QÉ¥Ò«VÔq-yþ»a®PÄ÷Ç!×rõþQÑ,,P_·’¿âÍ—QvÒÁÜ¡<ˆç¢ü4 ܲénìßý´Òh£0Z¡.å•BiŽEa´Fiu%¯Z—÷UÄÏ—1êò¸¸¿Ñ—úNg´A]3GœGØUDW¹xÙÓÿåtWÇѵõ-c0Æ`ˆ3€‰“¸æªú8gÀ\éS–`†vc˜†`ÖA RÙFõö¾;¯|Ö½þå·®<¾kóŠssIÖ­ž ¸Ó¥#5¨™ƒâÝüÕàæ‡î› ÌqþÁÃ5>´þIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/48x48/copy.png0000644000175100017510000000235615144136756023545 0ustar runnerrunner‰PNG  IHDR00Wù‡µIDATxÌ“ajƒP„¿y-gér€ô·Gê é-Z) ‚ÆæÁ aó"…äƒeà±ÂÌ®»P˜>š30˜&ó €ãÔÁüÜÅ8NO€…Øs;®ø©k¦óIÀ‚ø7Œ¥n#‰²,ɲLã8²9@‚Bšqaø>r/®g²Šè°=€¤U‡aˆê'¿>õ_A"„ðb¿¾_àLÍïCßóu:Ѷ­7ký׳FQäyþv©LRÏ š¦ù>-Àúæ qò]×QUÕ¼ÿÝfÌÔ‰Hzâ6àL;UÌ=ßu ÿqð¦[ÞmÀ™wÄ8xþh1„aÒøÿ/-踚ƒ’i•]–ÜV¤ð”¸ê³(æ |ì¹qÛÍõˆà]ˆÐ[Þ^…ðøà\g¸Ok„Xaò˜#ô—.p½r!P@ ÿ!^òù˜á€ ňyð™–Lè@?ö ´d@Üt!"Žø¿ ìÐÏ‚/º€"ñàdÌã•]x^ëBfww×ðh;ÖÍÌ‘qps€„Pü®㥇;Àp+xÞº #¢Á'1¯tïŸbtFàU/N¢ÂËU÷‚´Q: óz¯Å4$æh؉(¼ÃÛûöV"28F"|^ÖÄAy"¼Þ—YöBŒ3 ÏèËì{!ÿ± Oˆ¾Ì>Bq'|Ofyiòïð¹+åùÉ9B.¼Ã.·ÍÈy®~ñ×¢C˜ð×Ì9³€uüxŸðktæxphþên”À+¿‰5ËÁ8ŠŸdzhmëÖÁÚx›}‚±mÛFéÝ¿Ù&ã/69×ø¿J˲à°"m6áÂ3%?#àð`Œo†é`÷†a09Ïíÿ’Þbtrz°»»k À:¿„oŒ ¿ZQT’ UÓ`èÌûŒÏ' ‹¡£¯¿?=ùM}·€.ð!x÷½Oä5–$ ««k%‘q 9{Ž—.^B8BS{GOFJJæâÂ|€RB8â({®{7ò¢(b~~Ó33ì³ñx7®_ÃÅ‹°´¼ŒªÚÚÞ´äÔ´Õ•åþ0=ÀåDŸäFŸ²_#KK˸{ç>|ÈÞ7 ³ss¨©­éÏJÏL&øV«.ÚwêL(¬iÏŸ;¾¡¡~úëç¯oÆÇƈÜsd>Ÿ·ýqè0~së†A&tfäÜù³p¶6àëÆ~ÿú󺣣³€´ßI¾Ib§løKôˆ~âp˦°`Z=¶pëæÍ²©«é}ÿîÛ¾¾þFò¾A%i¤fR%øª!u|K&Ö)¶H,ÆŽùWUUµdeçû 8pa ñ/–õë×+RžÛÚºìÍW\±»3‘HøBŒp8¢D"ÈÃ%F‘ëÈ1B„à8„ 49¢ˆ'¸¦ây>A¥‚aX|ç;?ª‘ß:dYæ!`nÑÇNvv®{Ïãÿ¼æ‡?ü2êê*ÿnÓ¹.1!å!Ûv΋–e!„‘ˆè‘ë6®ë*‰DŒ¦¦¦–òòê]ssSý@ ð% TÒõl6=!jÎf ˆn¸¡‡þþ3BDÁÐm<×Ã0 ‚h‚ "Bpž°"„}‰*®>¢$Z–B8šbÏ·rÍ5{¨ªª UTT/U€8‹0::êÕÕ5 ŠˆKŠE€™™ ^ýê÷aûÝ(‘JÐT|%!*s”2‰!‰Zp ªÁ9 øó< f,ššGسjjª•êjyà @dÑßu½³…B†bÑ ³³…êêkå¹»ðë# ˆ…ƒÑ <Ui ) Ã+p\°ž§"ŒŒL`Y%êëkä·ë«2%Î+*Qį=…BÎ×õ@@KKí­qð‹W!¦D• jµ FÕ ™‡BÂ+ËŸC×Å¢µÒ âS(K&“q€E ÿßNŸË %$SÐÚÚÊ’ÀÊÔ´`¡%þaˆG$FY@Ÿ#›-°uÛf"q=è”áB>@tˆÂ)@î!äá{ÚÎò­_Çcû86mÚ%«“ä‘b.•!—Ë.Ú€ðE èȧ …\,D€ïC}]•täL§Át ¤.X( ¹l2‰ ƒsyèMÑ0×OGs-¦áñÉd‚¾¾öÞñ»looïà$0 X!.²ˆ…Žär™¬ P•¦iQßÐ@ç²röÝ“…ÉT'¡"IŸ Ï«àù`Ú=Ð È%(ØÔy§xƵaÊʸsï1²¹"…b‰}wÝ„¤ì¼ã8}Àq`ð%À0ôiñ㨤ÔÊRÉTWÁŠe°‚l¹.AɆH!èÄá`C|>(í–%OòæîW]þöí?Ì{z@D[fžêê Ö®Ý;thoH&À¢œ={ÜܲeפÔ.±+;k!>X&¦ b‚¾  ‚ˆªÂ`Ù>ÌkØÌÚ5Û¹éÆ›ùî=˜]FmÔäƒÏ\φ [¸ýöƒ2o,‹—Jõ~é€_*•úòùÍëW÷òÚ'\ËìœÉ¿õ5¾uÒe°áQмb*Žç3–2 iPUU¡µµ-_TáûI€s&›Í¸ÒV@&ylÝØN‰ mj‚@H(¬ŽˆˆÂ½‡·m>Ç3ñî=Þ˧¿ý~=ÝH®óÑ„ÚY˜;i1†¦óxžÌ"hhhij€øý"À4õÞ‚i 9¶QÃQÖ.«°ø\HTÃóˆ ú;ì½¼ëÚr®¼üüöú[øòïnå`ÙÕxk!|ËpôyP *â3èÓǰl—D"Nyye%PD݉öî½~rÍš®Žíì¶l€ëZP¢ÃøZ$!ĽP”¸1ÆÃËïá%Ø-÷ªùê׿ÎÍ}3í—Ñ^VEÒ8D[e˜e+*i«ŽQŸP©KvP]¹ÃòˆÇ£ÒMå²z«r]'v¿\×UftÓ¤”+0<=Nêð]lÎâžäñh¶Îñ‚®¾úô¤¸ùæ¯ã+6O¹n+­ IZj“T—ÕÑØØ$zã(jÛõp\ÏUWÎeqFc’¶ @üE xË[¾pu"¡=ÜÅçÐoËÑ¿åÕ•J##èÅuuÌõžb²µ•ª¥KÁÿS>¯–Ö>+5|0vQ› ºðùeÑ^±ãšú¹[Ûy˧÷²yí*vìcO=uŠÒðűQòýýèé9Vä²÷í%-fã5D²IÝØØðdE÷`p¸ˆrÑ^ZYymèÞ½_Þä®î¼âo§¦k¹¡!J§O¡Ïïí%ß}è3³˜¶‡æBÂq0“I–¯XAç’NŽéÙcšÖï3™ÔÔÇ>ömGQÔ¥’»d÷®KUiO§ÓÇzznûÑ7þb°Ñ{£¯­ªJê†ùþ¶Dè%|áó«Ÿüt,!V8s]ãùǘË鯪#‰¡è%2ËØøâW‹€µ˜†Å™3gý¥é–æötYY¢!‘,«˜ß&5Lƒè|JÕ¦§gRããã7>süó¿þõ÷Ž:à_”€—'ËvÆ-ý“»woÝyù+^IÕÖ‡Ñû{1ÇFÄó‚ÁŠÇŽ1^(q°m%æ%—Q/Ä}½€iš,Ù¼ƒd"‰Xˆ²ŠJâÉ$ªF GQCQ-ŠIb;.z>…•ŸÅÈÏ‘ÍåŒÓ§OßÒ×wêÓ7ÜðÓ} BþY/DžÕæóÞ3Þð6Ó¦xê–7Ä2º/ˆßÏœfªd²¯c¡=à’«D}M­ÓpÙü¥²º–dy%ž¯àz*rÎäºÀ]wHO ¡˜çÀ.ÒÛ7`9rà+¿ûÝ>lú `ÿ]oŒÆÊ Óüòæöº'?êSŸ¤êÊnŒ‘AÌÁ~!?Œ!©ÒòRó–tØ)ÃæöÆNªŸð"žøÜ§ÓÑVaºø¨¨¡yÂ`Û' +ç8‚àX ÑYˆ„–åb¥‡H*ÄVüú׿¹óøñ‹s¹ô`üUoG*]ÛúÉuÛÖu_û™Ï^Õ%5|kd{bž¸˜·Îœ 3)ä÷ÖvxôÓY{é*åTT$YÚÙN²"ŠïƒiýÙž(๠).Ѱ‹cèä³Y<ÇÀ³udQÃþý‡øæ7¿ñË[nùå›Á¿ð:-´"ì:ßø£ölßõ±Oá%*1OÖZ·ÆG1¥æu¢ qwjŠ Ë㮦å$þd6]ÕMuy¦°-uPT*++¨©¯¡®±T Aðóþ·_F_£ªu×ùÙ×½iÓ§¿Œk˜X÷Æ£ˆï1çE ¢¤RLy¿‹×Ûéå¹råúp2B¡°$]‹©±1R33´/m“{*–¡ã¹.²$•l4‰$’åh¡r!X/:=ÊÊó´¶—“LFE€Š¤Xb±'NÌg°hÊ.h7À·Û½ë™Û¿üMÝÀé?ƒ#–±Æ…ø˜@j]Z/“fØp=Iãç¹âB82~ÉÖ+Ö®^µvå–-[Ë–.]Bmí|Í)xž·¥ó ‘‰‰DD ƒ´>à{>r_ì— ¼çÍc“Ó^;Ì9îéÍß:øÑÕOØçΕ ÿ»\µ\ùàG×úòW¹¶Ô/Þ¯üMû[õió3>ÿíi¼ïƒ«LÇŸÀÂ?@¨•îYªRl 5ÑŠZhi‡Ñ ‚ŽÚäôâ}â 4¿½ÿ»;Ùܤ͎hc$äwžåŸ_oߺΌÅÈ#×¹ÞÔE',°^ß³³WæíU¬âD%I‰*}‹4–Zd½··Jv™P˜„jîw¿C¿ûƒ Gš‘Š;RIÉ`êôGíýís‹ï0çRk¸ü—mviÄ*6Bžpá%t®Û!E›9ŠLz.t‰Oß †W¯{{+èFÐÅ’ÕGŒ²Ÿ f¢*DdrÜr¨ŽThæDZ8T†Í| ‰`HPÎ@IÔìB é’/]3Y/¢Y‹£ÌŒ¢LœŒëœM´ DÄ`ÈDå ¬«Q/öÂKÚÚÄ3UgÕëHMx<ëàÀ°i#@±PDZ}3Dsf°ø~óçÛtöd æz»-æ©ÍO¶Ø)#ÄcBº/æ´mÄ ÇQ(Xl¥ÂbŠ%ƒ+²« —©º@ù¼Ö;䔹ksÑmKÞ̫γ&å˜ígbŦ®6KK@r‡óGhh$F½VàJ™(Q %Á¶6k,¸îÜGo¼ÈÆ¿ñ¤õ'£-FàI—SÑ8«t,O(ÖêÌAG9J à—ߢäý³¾=¹²HeùêbÕ•ÃþèKÿkÿmê]¾¶Á^oùò‘«ÅN2BFHiA’#î'mêHrÓË7c€ÑM‰¿ðhóãϦ¹>@1íÙ êcRøÐy¾…P‘6Ȫ e‚#03Æ£ ¦› …È‘•¬˜#§ÂÈ; •7äàg縢 Œ•F&aØÈaÌ2CÉ(ÏWYÌšP(V7™}®âüåyÂ/ü÷:¯µ˜ô³œwõKWúríeç¬[# ¶E(èøS]Î÷ÿ0,ðÎs¿>5ߪ³o2gmöÒë׿w¬ûÈà• ÔØrÎ˱ÈÌzÙ¶~ÄRóõÙsõÚf0  ã68Æ&aÅ š5/å*ï|OO<öý‹Ã—ÿ°!C_Ïb•­ â‚ ²7ž¹ú¹sõ)B·`·gÌYW·™½öy«ýÃäO¤®»¬â"•M7¹p£'õ [Úý#Î>üÅÇwS>^rü Ê4”`W©":±gªË퀻¨{WfØäæ[)Ìm窉•$.Á‹T®Z6 ù{f ×/ƒËV#o 9ùºõ+*= ?Îh,áñÚœ÷ñ#•6Êù·H˜ÝÞpŽ™¨XóÌZœ¦¹ä!ØÁkͳèOþF+l“çïïŸB~%4@øx'’„IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/status-bad.png0000644000175100017510000000117015144136756024617 0ustar runnerrunner‰PNG  IHDRàw=ø?IDATxµ•ŒA…OX/¶½±Ê8y±kÛ¶mÛ¶mÛvn´1Ë¿ß)ofÍäÛÌ»8gîüR–e¥²UÊm–òë¤I+¤£‹¥Ây0‹õ4bÉ §¦,ƒk¤û¤É»¤/˜d›`'ƒ°fÂdGÍHÌzÒS!ƒ³RÁ1é±….ÁsøŸ>ÀØ£a=ƒé-Óà°Ôñ/§ ?µP¹Sa½ýÑ(Ñ`îû(¸èݹ±’¼…0¾a’g¾ðó}húèIJ1t̹‰É胖¯I4˜¼‘å£D`´nÞ<Ñ´i½v̹É®BÀ`’µµJÊ­d¬“ÉÎÝØ¹mÛ¬¨¨(›5mZ6ò‰ñÚ1ç\³#™d=ôFrZ$å—x <ºwgÿ}ýúÕ‚ÞµñÚ1§2׸6Þw §@[seQ2¦×Q(šÄX4Nûü9&ÍŽ®q"!EŒëxtiÿ ­ÉRáîHMÒIÒGñÈ ­ñüÛQ›£camÑPiÒèš¾ÈáYÐ)?„wkø6íý÷6e‘Ãíˆ|ÐÖB¯¿›°Éä~¯ÔÀ«Â}â«Âÿüb‚Ç£øù¦/;÷Z£Wú²3¥‚îŒ5?˜T÷,üs4=’×u4éÛ…‚aþšUBü2Œô±X¼´Nœc”­„Ûe‘s«- ?†2>™Ÿ&“á dÝa¬øÃèõ'Þš®•ùèG:H¹NR¾3p ׎9çš²4~gNoÄïØYIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/patreon.png0000644000175100017510000000057115144136756024224 0ustar runnerrunner‰PNG  IHDRàw=ø@IDATxíÔDQà%¢’H( ®B"ÎjíLk¯îÍ I€!@Â!áH@N÷çy;ûÑðã}o훉~¥ÊÈµŠ°ìz{kµAÐz ’-°ncÅeذÜEHº?öÄñ(XAúV|¼ô©L Õ>‚Þ<ƒe©€´9 –'°ÂÒ×þ…ìK¬¨˜Ž @êæ@Ò €ÔeåËX9µíp@;åÉe0@zoô áÀ•å'ï¬Gå@’Oƒô½úí¥‡dmÁ6$Ç/èÜ>hõúXn*þ€zc¼Ú.ʲ1°^Xž&–S†]äYë,×`í~9ô¤· ·ƒ(°oSO!ÏG@:‹¤9ßÿ:Okà©pàp› maÙ‹þR}Ôëo¡›IIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/status-running.png0000644000175100017510000000135515144136756025556 0ustar runnerrunner‰PNG  IHDRàw=ø´IDATxµUC˜lg¬8Ù3ö:ÞÄÜ+¶Í¶íîØXe÷lmÛ¶í™óÎýÇÓÔ÷Uë?]uïÑED(»îg lO„ªµ ê¦ih˜µmÐV'B[a€®ô~ÄïFŸ‚| Š>‚¢›`àwÇ Á=Lð𻳗`n&h+ ºr‚¾8Æ‚§º²q d£ËP°˜k’ðçá? Í¿æ Þ6&+lò3,Õ×"$^¢«ðÝÐ1ÈF8x‚…—ƒ#Ùê †"fþXR¯‘﾿9-+‹ÿ»»ø:ÿ]&8›Ù €`ÌûÛ êzŠ>Þ.®H&ºã3±×°IÁ”÷VAW@ÑQuWP¾ï~ÇI_)4tËû?‰4Äœ.c3·LhCÓü· Á=|ï»Nêëë£Òò zó3]';›‰¯C2 è³fƒÔœ»?ÃHXXX Sé±7¾&xZ¢ÜÅœTf¶ êÓal‘ Ö1<e¢ž’Ù6!øpi®ö(”ñ0 9/&(ÒL Ž"ÁåE‚N†~W¹Ùy@‰ÅêD[àk±Š‹›hÙîmx¬KŽ¿¤ ,).ÿoªn[Í,@£ä°.GÐSõ·—±mÛz€ìÃm°Jž*¯’elÛÎÌt÷÷×µyÚ<%e^zzýÃ7c_Å©CÆ J-kgÝâ­7f#èÇkËž}xx± ªdIŽjg¿hˆÀÔ³¶YRyÏl8{òòé$Ê&-ƒªZ-m³J2ÒøÈŶ|Þš'Ì–+ö^ ]8e­UÔâ0¾N¢ÔæÖçߘU‰¾ÿx¦Æ›'/[ !¢`ó¥-m#D&´Eói›GácazÌl”@À|&È©Œtu'•LAƺ%¦èwyk5UÇlQTÜ<wÑSuóSB#HHEÐXèËriäÝQ¥’Ôú§d*B pªü)ÿvE']fP †@ƒ†$MAßv¯âà ÷ùÖ&ÍÎjZGD1<æ  QçhüûÝðEñ%¹©ÈS߆®¡T‡j#ëöÈj°U Kšä›Z”«i7‘X@+Ä´¥Æ_O)á}EŸöÚõ ä1µvm¤¡áRÁgЦ@jX$˜ðbàAJ¦á›ŠüoÒh%£èÇ -©@½IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/loadermods.png0000644000175100017510000000210415144136756024677 0ustar runnerrunner‰PNG  IHDRàw=ø IDATxb Ä7_Jj½8»bÚõ'‹ïüiYpûSvÏ•Ó@ñlÿò3Œ¸ô1c¸UÚQ u ;X>¥4ZY*Ú]š9ÊUŠ'7DÞDO…o ;S%EJ©`á ¼<Û¿YÛ¶mÛ¶1¨ÍQ5ªÝQmÛ¶ûì÷ÂÛÛîÞ$ÓÌË«wf'{ß%ß·¸‹N‚ ÁµU. §06©adBÛ8QÖý/áŸ_ÿôÇîð»Çßè§ Ò0!M„V€Þûj”¾ûì•»—ßéÖiÛT—ßéæúeWXý¹¶ÆŠ4”¤š´ï©Æ…3kæ­³B³ã»ŠA£¢„7µQ¿öÇÐ=§,6¤µ R$Þ¾á$þõ×îmK³V>p‰Íw;õËkO\VÅ @L¦Â%s`rY[ö dˆ„4Ö,BAlÔW?MÀܶ…7U ™ìó=ב¾’¡!‡1‹j²{ŒI59&±$dß®xüWUš1º¶ÒW åK&BÊdòTL¤„„Ý>×"ÀY‘K(£UªÀõª›êj<˜´Y—ÒóX*ÉZg×i¶§‘cvŸ«GDHÁ`&0oíý®ew=NÇcmƒß?Ÿv4WØ2µrKH’eÞ†)#gÜŠ‚ë(ò±€‰yßvCGÊ«iY¤jZ^yØŽ Nêl©T÷=;‡øòämB’òÙ…(ïµ’þKØ@¢î¡DýÒAcOõ5®m™”»feYvxÙY¶Ù ©T!Yÿ•¥R]8<渕‰Á8â|ÏÃdrb`$ŸûBŠ6=qÙ$™ê¦y ¼ª¿yÞíwŽ÷|øþàwÏ‘bÕ®5yæ« Vn­ã“`³ö=%Ó+ý•ð÷¿…ðñÛO>Ý2ÃÚ›++Þ~쪣»?¹ÿa©è\fçó–ßüìóùü;Ý}£éâ¹-U«Pìõ”u—¡ª*h®÷éÇžP½ðìc/}óü…›å/4Ö´.±eemûliÛÆûÞx…Ù¿ ¶¢@1ÖêýO¾ý྽g?tÙ¾oqÄ}O6Õy›!ø×öå·}ðþý{/ö Š#Ú3>mÆ”[”Ç’H±2·¢¶£ô/2˜ ¼3b)sÆKXAx Olÿk%2¨{ûúÁõk©ÿ!‚Å5e1c:ÉU)([Kë¢ÍÏgÎü㲕±r'ôÿ8/N@°jv/‹‹öoÆìáヌÇ3@8IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/status-good.png0000644000175100017510000000143215144136756025022 0ustar runnerrunner‰PNG  IHDRàw=øáIDATxÚ•ŒQ…ÏZõ¡B„Æ5^ܰ¶½æ¼Ú¶mÛ¶›/m¬)Nïû—³œn’3Â÷åþW Ùip"#ÇR8˜î`wÚ lK acJkSn`e²ƒZ;€b-½+FÇ÷#3ƒ8›á* q$8açuâŒFì1‰5ÉDP¥ÊvQj9˜…_<Ȉ›Yßp%‹¸c/¢‰w Þ{#ÏF‡‰r%/²¾a‰5°K®eÎÃÍlwÒ8N@¾r+šp b©éb1¯CN§ Teqq×&ÞEx¾ò:‚Ø’D,R’y--i©ùÉŒo¸J¼íü”Œ"©Ô‰¹æ7é“AzGÒ‰§±ÿÚÃl∷*7¢‰&1Ëp„½験æâ²Þ#xßá}ùàáŽ;†8Ý(Ù™@Ì1\ÌÑÒ­©lO%žF÷þáÓ’d]]H¤%w£ˆ¹F¸`}²ƒ-©RÃvu}å ÞN ÿ.¬V³r·é¼SÇ¥²×Ð^ó÷”H¾«Ñ‚P³1„cIíàGNáûOï&’wþá’í aA¨¶C8,ù fJ wïÛÍ¿ÿ’¤ÀDrÛ?\²­IPfÝÀ£eúŸƒü¤~nâtð°ÁþáR¢¤Æ-³”[ÍÉùt{I}}½o¸<›ßÔÉ –*Áý(yÙ‰Ä/\"ÃtNã0•ɰÀt±+A^v"ñ÷vðìÆ‰FÊÄ|“¸Ý©äÇÏ~àÂhœdNËZ¤&ÌÒ¿¡D#^E´—œ ʹ ¸ü+ŒÙºg±“`JÒ@Ì0\¬Nl/y'³ZÎ]Â×&RJ3³ÍrÝ"Ñæaªîb‰F\•rù˵h¢Xf®‚w²áxZ2Eÿ¦DÄæxâNT§CQÞmƒ‰™êŸ™I}múÒ'SŒ ¹*Ät•²DbS<%åIU% ¿s1Mó»é{ƒ‰Z:&U:G‰n¨„$r­9òN}Óã@ê}‘º HIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/status-yellow.png0000644000175100017510000000121015144136756025377 0ustar runnerrunner‰PNG  IHDRàw=øOIDATxÚ­Uƒ®œa=psߣ‘6®ÃÆETEí] k®mÛÆêÿkNgêoÍä¬çœ@DyqåFÝ©Omðî[±¶c¾e†¾fÂÚªÞE#ž®6£®GÞ.]xrá@üÔ:aœyA×QÐ%ãÀ Z7ƒVŒ Å^Äçºñ¤d„õ)ŒqH‹>‚¾OƒhFÅ÷)ÐûaÐQ´Ð šëq‚m \»aHºqö1ñ•­´lÍtã`¢†œâùµzT¼Ãòð•mv} Ia®,3Œ×Þ|䥋,šAã]0*v<9¶ƒ> Ê«Cº£èMtü*õK–° ~ë 0_1ƒ±Ë9¿$ÁVîîv/ºzÃpxóÍÃá­{\·å/ÛþIl‰ìñÚÎ;•9ï<$PVÆê•j9¦œËhjΠ¬ɹ—>ùÎÜf¹Á«ä× ¼ÚÚª^‰J…3!«^šñ™¼ç•ÂBØ6¢ÍÀjO7}¿âŽ‚ÓÛËoVÆXÌ:†±^ZÓŒ½b±Æµ c•?-ë×OM±®úáZkµ¬»ñÆI~b2]!Ä 5§»žÏç?Ü;ß§1 _?6véKåò£Œ>&™¼îƆ†ë“š¦u;޳A*¥ó‹Rx*׳çóÖåÇÅh(—C"Ïvv~qûØØ\ûõJåÁ¿-ë/b^“Óö„aÜÃõXªëÂÑsše 71Œm\€èle!r¾‹}ß]k­÷T*¶"ÖœÏÃéê³Ö=vçιWB期¤5_Ö huÄã×Sx«Ìº¼‘"(}fšw.DÀk1r]å2Éò™G^ýY(³UÒmçIEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/noaccount.png0000644000175100017510000000027215144136756024543 0ustar runnerrunner‰PNG  IHDRÅb$IDATxœÉ! aÐw‡Û?Ó7fú¦ÇëŠÞ7E2A¯4}‚Þ‹éšÞhŠ °1ã§×ïÕW È4xó¥¤É@ô*¸ó‰Æ‚Š ®lC±$ñ¢Å'„bÍŒ CG6¬B1§K›qæŠ)%{jp æŠ1™ÿnƒE™Á¤™ìùO9$Õ8.IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/minecraft.png0000644000175100017510000000262115144136756024522 0ustar runnerrunner‰PNG  IHDRàw=øXIDATx¥–5`ûÚ…?¡E1½0>.333Nº•™÷¹ÌÚ©°”™™™9;Nì˜D«×’ùß9Gp¯Êmž÷{ÞOUÕ,..*[[[Ï~Ámþ/àÅ{äC$Iz›¢(œN§xžG¿ßÿùp8ü¡eYo~r‡¯ÿÜS*ËòÛt]„aÄqŒã8Õúøøáâüã²,<›Íæ Ý&€Püpà=“Éäâf„JD!Æã1H£Ñ`ii €<Ï)ŠâabýÃ$I~¥iÚk€üGÀ¿ø´Ç~ø~Û¶ï"Äv‰iš¸®;†ííí ØívY^^f."Ë2àþa~muíOk7,¿øÎ%àEzìW%Ezò]ïu ã aÇv¨µ¹Bvww™Ç´³³Sç€ÕÕÕÊÑÞÞ^%榛nFÕ%Œ†z÷ÞéÙ·;?Ÿ~xtèìNîác‡.õKëmâ ƒ*uÍf³R*Þ ÎÎΪí9äüüœ»Üz¬ºITúLú‡ßòÇïž0ì÷½t \ä„#ŸÐQñ{1£•€Æ²A½í°¼º …D§ÓA¼EìððP8ñ¹û=ïÂÒfƒpœð·ïœóÃÏýÿ<§ÖÔihfq H~Z¢Ý°€ùà”áFzÌò¯£ÝJˆ¶SV×—¨7èuÏðŸÖu î÷°‡0ê»üðSäÏß;Å=Œh^·ˆjy„ÃR¨q h6âC_ŽÈ 0ð( -þ²ß!¼gƽt”šÄhz?Žøþ'þÀß…êý?t1ë56nY¡t ô~ 3ÓQ¯ybÙ 7”mþÔõrR)ƹ¯…=4ùÓ/éþ}ÀöÝ–ñ/ ~ûµý½ Š-ÝA­)LŽÇÄ}…º¬c7À¨éW€ÍåïÈG «µEÂæ˜n/d÷k‡ìÜeëD¢¿6bò³€“ï1eƒ¥›[xûŠPœ'y”áè&Š^ ÊEY^&AÌZÓ¢µ¼Bωýާ3ýõ”ÞŠ((¶„-Ù$Ô<ð±J•BË)“œÆ‚ÍbËdäEx³C»&"M•™øE¿‡!KÔZMZÛ&á˩ FSÃxÒ«ÇXqöÓNW ÌJ#?0IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/resourcepacks.png0000644000175100017510000000314315144136756025423 0ustar runnerrunner‰PNG  IHDRàw=ø*IDATxÚ}•p+k†Ÿo£Æ±ÚÛ¼îµmÛ¶mÛ¶mëØìé±Ê4i°›ì³3éÌõ3óÎ?ïû £gÂÿs0°#Ðà÷ò 6köŒ¬­š€‡€VþaÔïüuˆÜŽ eQͳÿÔÔªíë¶ÙKïÛÑj©5s´ë—Ùïwó0‡¿p ýQªX·œy•_ŒðE½è©û˜5Ó†Sådú¦&¿ª¼U|mm*Ø'Ù'üC¯9¾ï  Ô&½pþ”@Ñ'Sât€ò°Ù{Âcj»OütvvÒÜܬLÓÄãñ(Û¶º[làªGÕDEø@3ð.H“¸šB`?#²C^Ò+ ¼2yò©3F>­´´MÓ$—ˉã8®ðz½`†ì¹÷î;EJ‚— ¢ç%Àp?ážÿL Ôª*¯×÷ÒèQ“—GR©”[=ét˲Èf³¸IòI™4iápXÛýÒ1ÓrHå°›ÿ4¢ùt1ÔÃóçVìu‘Öó·Y¶Š¹Á”ÂÉÄÖ47 ¶m»ITmm-cÆŒQn¢ºñþ¢^û¨ÌúO¬Ÿ£–ù¤€ð>BG#LÛië49ëÑÈq?üHG,&q /•±B!Œ=ÕÕ©íO9EFäƒëº.J)¤õ]Õ°|þÚçïJ=à»ÃÂû*ØÝ\ýí×ꀮäs;ÅéYž×z}1d—ôÊÆ`Ùo±ØºqãÆîóù$¿pqÁS™õ"éd³9QÙ¤4µeðz Qbç46¤'Êosf?º|ù5F×mëpiKgL0.\øÉ—_~ùù{ìQŸ78‚Á .ÒãBôö¯ÐSt#ƒG5Ó´¥‘hÎG"²=Ûbû—_~¹ ptþFÉ«2 Ñѱب¹MM‹FŽy‚aZ×Ù¢@L²í¿`Û9Ç¢µÓ‡Ù÷RZ’¥êå—_vƒ? óO~-ïbËF+Û«ººzŒkÛnÛ¶Å2j¡ý±3í’ñOÇSs»li3Ò/¼øÂÅùÎ呂ÁŸ˜¼ð¬Ê’ÚŠÊDϔϯëÉÀ¦Mn¢Ö…ß}wÝ AƒÊ»hÔuÔP(„‹ œŽ¥Å‰«Z~ÿù÷YŸ|òñ9ÀÏü ¹u+-½|ØI'ÕUU•‰ã r9Tþ›³m?ÿ|ݬ_}íóÑ£²qäeùN(++sGåžpvΜÙó>úèã‡ÞhkkKŠ?óÌþÔg¦é|_S£fš»ÿþάúzõCÞš?õû;ayê»wïvæôéÓŸÚ¥~—çÆŽ{{49Ø Ø˜Œjj hrTu‡Ã#áðð¼ßžêêt›×Ï¿¿‰ë¶¶.nvœ5€è€VPªp…vAÀ*(÷}í¤±Ó,IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/24x24/log.png0000644000175100017510000000110615144136756023330 0ustar runnerrunner‰PNG  IHDRJ~õs IDATxÚlÓ…=ÅñÿK²·§Ÿ»Œ3NMUàîÒ %à#¸;gI~°ûÅ“`ÿ?›®Ä´ø C®w·°›µ+Á­_»Ãðò…†Ûï±NªfX‘JP`N©2¦ÿÜvýG×Ì|E^IòVÑICÃoTý9t™%I`ˆþæS+ˆJ˜’ ü£zà¼8²ŠÞ1Q— ­*b¦ÄXS~áw$”þß6sb²ŸI†¹ìLžªëÌŒ4v!kG›Ïž¢B]æLhÉ #^3“™’hè0ˆE‹PCÔ7ô}ªU‰‘OéªCdäFÏI²…ÝîR( U|‡ð€€. ‰Ì´|{U*†e Þ)LˆLè‘Ù§ðœJ¢b¼A 4¥Ð•yû)þZè’e ª®ÏÔ8R5v‹%OÕºÒ õl¬àBý  jN–IHÏhùSÆ tc Lã$ƒ„Á*Î’3 ‹*­¶`Þp‰–BKà ¦K¦Ç+~dFaÈk:¬@P⌱"o•¨Ìˆ“‰Zk!øg‹¢äÿ £ř:½ ""-™.ÃÚÖ¦Õ*X)I@€ÿÕ.MˆþEúÂuy¨Y=æ…×_Áƒé½É c›ªÅWÕ¦bÙÆª¾þìÁWßモ+Ðpd7c°±þ˜á/ˆ~KˆáD9IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/8x8/0000755000175100017510000000000015144136756021707 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/multimc/8x8/noaccount.png0000644000175100017510000000021215144136756024401 0ustar runnerrunner‰PNG  IHDRádáWQIDATxc“‘—“‘f‘‘QÒÒ’•d™8}ñôÍÊ :zk¦•”0LøŸÜ7-ùCa×_ß©%Q ™ÙjÉÉši± ¾ÞjjššñaïªÞ©ËZ¾IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/scalable/0000755000175100017510000000000015144136756023026 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/multimc/scalable/appearance.svg0000644000175100017510000046502615144136756025663 0ustar runnerrunner image/svg+xml OK 22% OK 22% PrismLauncher-10.0.5/launcher/resources/multimc/scalable/new.svg.license0000644000175100017510000000012715144136756025761 0ustar runnerrunnerSPDX-FileCopyrightText: 2007 KDE Community SPDX-License-Identifier: LGPL-3.0-or-later PrismLauncher-10.0.5/launcher/resources/multimc/scalable/atlauncher.svg0000644000175100017510000000207315144136756025677 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/java.svg0000644000175100017510000010764615144136756024506 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/status-bad.svg0000644000175100017510000001200715144136756025616 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/tag.svg0000644000175100017510000005517615144136756024340 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/status-yellow.svg0000644000175100017510000001316415144136756026410 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/proxy.svg0000644000175100017510000002317215144136756024735 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/launch.svg0000644000175100017510000000563015144136756025025 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/reddit-alien.svg0000644000175100017510000002114315144136756026111 0ustar runnerrunner image/svg+xmlPrismLauncher-10.0.5/launcher/resources/multimc/scalable/delete.svg0000644000175100017510000004044615144136756025021 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/custom-commands.svg0000644000175100017510000034246715144136756026700 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/bug.svg0000644000175100017510000003221115144136756024323 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/viewfolder.svg0000644000175100017510000003167415144136756025730 0ustar runnerrunner unsorted Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons image/svg+xml en PrismLauncher-10.0.5/launcher/resources/multimc/scalable/centralmods.svg0000644000175100017510000027064215144136756026075 0ustar runnerrunner unsorted Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons image/svg+xml en PrismLauncher-10.0.5/launcher/resources/multimc/scalable/checkupdate.svg0000644000175100017510000016226115144136756026037 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/about.svg.license0000644000175100017510000000012715144136756026302 0ustar runnerrunnerSPDX-FileCopyrightText: 2007 KDE Community SPDX-License-Identifier: LGPL-3.0-or-later PrismLauncher-10.0.5/launcher/resources/multimc/scalable/atlauncher-placeholder.png0000644000175100017510000002444015144136756030146 0ustar runnerrunner‰PNG  IHDR,–ë9"…(çIDATxÚìÖÝO“wÀñzá&Æ»]l3fÉœÅ1yU™¼*´…¶(Ph)¥´¼Í)°›Iuà Aª T„ :Ñ) Ê[ë…òR´Ë†þ';íiÔdÙâæR'ßœœ^õê“ßÃÛ÷†æà`èMý;/ã‹õéÍð»I9)2„,†ð?ý㊓ӕâ¾.ÂýÁáuÁág‚BCø¯óã&îM@~au¸ÝBCøšÇ4ØtÓÖù“í*W`õ£ãÑõ‘ÑõQÄK4‡&‡Æß eY ákãG!ÅSÈS]$ð{‰CÎà€"CÈbÿáÔ›¿o±7µØWæ/‘À.[ljéJ¨z¦Š‹0É™4; àQé–øÂ))”{WIJ¿1­  4yŽZ{5Ø£ˆ"òC®í,g‚¯~œ‡+Õ¤˜!d1„1·g®wÛº0¢Xk×,Š0=ø¥6ó)€‡³Æ“åcBL5)!~¢{1îbBCøŠ9|6ŽøQ—ç…O$*È©ì×¼Ô7?*Í(‘»ùÁ·l4YÐM©èšp†Åzø5Ä6¸Khˆéžuó«\)¨\QÁ† Ÿ¤!¿c6U6© ~ÒÆ4 ÊF’c{Â( Hü¢OG¸ ®‰`Yo5Â.k‡îG5 ¤†$´Ç­¼íåæC÷83»[„ö¨´óóüPlO(ò£²‡_üàæGå¶f4ß?DzÞ:„×­ƒŽ;QL®?BEâW~—r D~ЇB (?hxþ>ìÑ3PD~9f!…_×lBÖ[PÑ#Öµ®:VǾ^(„.,ÖrZcãóÄo#\òÞ”Â~tk©“‹PbI›Ä¹­Y\„wx<èêL;áeå¸D9&‘ ÁuÌP^rZ˲| a„a |'„üP l ºøi 8ˆ"ò£¢¼W¨~ž4nŠW$N~îŒb¤Hü¸uδ!?ØQ~Ç eN„5E !Ëç‚À\gÁÊ]AJÿ—¢²%9ÆEX1SP1­ª˜.¨Ÿ¯s˜!hy:jsÍÜìWà‹Õ«®)jS“À”ÛBÁŠí‹!~ÜòÇÓ¡_Å;~•ï?È…½„,Ÿû…7 Îß…)ü‰·J'?Ìí)Ú¼fÎ6÷â9ð[?×”`òPŒ}%ÅM.~¾„ !ËFíZƒPé Žâ¶íÎÜAü´S2µY  Ká%$„U]Õ„vßã¨iÕ¨L*¨èrñ{úÇʤÃâÊÊHF~ÂÁæöÍØÆŽÄoÏ©!ß ?Bèì\yXICÈò5„_/“—Êàs”"BÌ?ó°ç]•E üpãÕý\öÜ]rê5ò{è°Ò†6µm"Tз»ä…°lßWÁ¾¿M¹•!dù B,,?Âm [·dú} ~ø÷Éuž>† O–ˆp?aD~0×!Ü’á{ƒhä"„» '}yiyiíè¯é³ Y$ñ£ä0æÌÁCµ †åë/a…!•®sb<'æEü(¢¨¿ªÏ6dS¯¤˜q6ƒø]ã0 zû¡3 †å룢t%Ú£eafI!>† JÒÅqªzÒ­9yƼì†ì5²FíCWFÚÖ9Ì1¥"?o„À/@9b„pOÍÇAú !Ë×j‹‹  ¸¡¬D*+IKÑò7ˆyø9ºa3ssZ!Tm/„§Bæ/Ö¡H„µóNK Œ2‘â×G¸'c÷^Bàáº}û»Fï †B H ¬œu€RUÛa¥aE ¸b¼Ø9^Â%Ô#äU2™§_BV¶,,,ÌÏÏóvt3„ß¿'ö=oÝõÍÏšŽ€"Bl " ¿Šˆ)„ªí½„ƒ£K „²Êh‰¡s´ŨaÄ ÚU6]hDy0~(!ù•?Ò"O¤–üœ ¡j»#tæGàP.aâ !PÜ€p¤$ÕlqQ8p!yÙ ó…öé"" ÇÝ+ÞžX“acsþòÝ_܈Щã½+²³šÀ ëËoÄ^SUÛ átd*EØ#B¬:ä€.¹„X1Râ^¬p/8*@1¹„8íóŽ…B8DàÇz7"l<ÓðNü¢@œá³ÇžÉvd€ã²óëËo÷ê;ß½¦ª¶'B¶´´ä ÖË%d§ÃŽååå³gÏ‚2 D5 ÷¢ƒ¹„@ˆ$Bý¾X¼wäµ!ðcR NðãBf;2%BÞ‹V͸¼‹ðÿöÚe…Pµ.'¯Å¥…Âq‰ÿòÅx)f“ˆ¤C ”?†²ÏçƒêúóOŽ£á×ü:„»ÑÓó4ùz=Þdà÷¶æP-¡j»!¬¨¬ À`SWÅX±c\ ¬;À—k²Ì5Qêš,Å {r Á-ƒ™yÃ&ýÖ¯VÔÆËÁé—Pò3"äw¸¿%/N.!›ZQUÛ ayEy·À;àN"ÄM)NÇXq…¦/…K˜D˜{&'¯Ó‚=B”­ƒYÁϽZ^CéÍž¬ý®=;6»žÛû,ÿ B60ôhÔ»îj¼]©ª¶Â|o>Ź¥YéÐ1VD‡ö‘¢Â–ÂÚþZ"DØC8BfëÌ?Ž¡e@ d°'"šø1J3^F„7šÁ)„ªm»„,²Š,…ç—æäJ„…-8ËýEÒan‹ˆÓ|&;2ŠÌ„å2òs'V]´ƒÐ‰°ùë#$?ÿ»­H!Tm„Cp1̳b#BfõZP¹¿7¥š@"Ì Ï†Â³Aœ…#fð³%r §b#Þ‡.¤!ÌýšŸ~æ)òcÞ‡ ¡j;"äUàI ä"üQ(« ”¡Ã€Y<–ÒžbŽ¡¹%‡QéD~ùxቑ\ÞŽNƆnį½¿N?€T”D¸'ﹿÀ¯4üÛü…@œü'xb.ü +„ªí†PR„=-áYõ¸³¬½Œw¤È:C-¹„‘9°l¢E…oůBà[ñk7b×ÁO"ü ü^8øüKeÀŸÌ³šà§ª¶-B^ ÞRÇùcdzØâIGˆ:Bg"x(Ã%Ï¿²ñœà‡èFr ëo:Mõ›ò¼ÜŒ!#Bww•]»BÕ€PRô4h3X&²ÖYŸkЙBN8L¼¢˜‰!b q‚Ÿ˜AÑuð«½V²ë¥ñ«n>õZìøJ~ ¡êÉBÈ«p0×>Z$œx…È€°u¶"ìÑ!’ÿÑÄìÖã¶ž\Ž!g°ý•–ŠH¹&3ôü.¬žcÁø0’÷žØó’ë þ5BÕUŽ•qƒ)„>Ô<ëi‹6w„ZJ„9mÙ9­&[w®à-¯ˆÚ%ºä>{ô›øÊ(ù1ðcGœ‡À)„*…0—øÍæÑÌ®`;J„-³>߬q¶F|ò3Ùm& µf ,bO-¡'ƒßÛÞg@8t¿³þ†3£þøeœ>†B•B(*Í£Y)6¡¨±7ÜŠµ%7'V_.·Mäfœ9Ì&Æ„ÀWW΂ŸèmgNm6ø)„*…0} Í#¢¶¨¯-ÒDŠà‡HûÂ8s:ër í®E‡m2é"òs†üº]ã ¡J!L\¥£ù¥£ÅÃ6ýB ¬l´Ð7#—°C8ŒtZ¦3¬Ó™–‰L.¡çΩ¦Õ* 9ƒsf`¾[Òè€À®€B¨RS ÊÆD’ú©Í!úxæ š(r 2òcØ@„ófÖÖãˆüNß+Á ,°êXÕƒR…Põ¤/!â¬+™ˆÆ#Pü¢È‡S d¦þs˜Áä“W’c‰%´ö™‡ÃCà÷àგÛVØcDØ­!Ìv›À)„*…°@ +Bœˆ ‡f_vÓt="‰Ð6hò¯v¡þ— K¸’@hí7ç¸rÐHxüP:ÂvZjÌ®;% ¡J!W¡¯@8-pJ 0°)‘¢Ȉ0Ö³wµ«æœ“½+§€°ö²£¤/üÌ®œ/ï8|ù€!îHÁ)„*…0uø àgÅØIÞŽ¡¬mÖ[4dáí(ø źÍÙÞ}åü£—(‚_qŸ Kúl;Îî`F„ÇŽ<¢ªBÃ6!Ê·5[‘QÞŽ"Ž!²Cðc¯iY‰&Ð:c›6oŠüa땤ªžx„tØ\„ üÊ(P·„çpZƒ9h šm#Ø3"l½$ø)„*…Ð^М/f0 a3ø™8†„r ÏexŽ! LCXtÅ~F„ÖÚÜúɺú‰º…P¥ò²µXòZ­ ÈÛQ–Âa6ÎÖ ôH„9㦣G(™@¸¼£ìjqÙµ’—7Ahê:L |S!T)„@ØjeE'ŠÛ‹ˆ3‡Ã3a"Åœ ÓùGË(…Ð „f¶÷ânðCpxB·„äǰ!V_.SU ¡uŽ-‡KÚ‹ä ÂaKD ä© \ôKhÞwq÷¾‹Ï „W…@œE\ÂKÇ-mYä'""¬¾R†B•BhÍK.!²¬¦ :D-á$¢%¹„ûg_ @œr Â+6ðCF„uuK±Ùú«•à§9TUO6Âçóžƒ@T¼!"E"dˆ°„à·FDr Ììß»)Bð[ŠÍ,­Î„WB•B¨!d@X ,„¾Ì„C_Fs´¾%â•QfuJ„ˆÁí¤fw ~ÎEH$Â7B•B¨C¸ÛöÜóÏí¯Üãê±ë—}‘:8E²j2Y½¯“Ÿ,µ„¹mYäÇÀO.áéË¥cñ>¤ªB²Lï1 Ä ~̩ߡ„gD8ïGF„˱ÙÁ»à§ªBû »ó“{¹÷tï%EØãú¢éó.›Ð¦K/ƒŸ!øå æ¢Á» ¡J!WMØå»@Q¿„@ˆH±qªF:”ó®~›"ÜÚ‰N^*2"ÌÈ?…P¥n¸Ü‚êc¿K"¤@qæÖ˜PÃdusD t 9*‡›"ܹ(ø±“º%$¿ÜÉìB?vR`ßB¨zâÖ„Q%š†Ž"P”‰Ð ‡Zà·) ;™áÐJ;ùÑ!gû‡„À~…P¥ÒaUP <®ež>v¨&…!“‹/Ù¤ÀKX~C+8å " ¼ýðáñ¡>…P¥⪠9!±„ÇýG Ф!D¤H~ÜC—!ø!"LE·r½÷NB D~·Öß¹¥!Ô–PÝŽª“q —0'éÏHsjë—Ðë*ºd5"üÆâβÛF„ Í&í«6‹·Ößf ¡J!Ü€ÐrÖ…\}‘.ÛH¶~ sBDŠYþÕ.¦GH~yÃ&ÛiS„Þüab ›†š†ÔO™TO6Bw á) dÈ(aó†³m漑lÂŽÈéÈúìÖûkÀO!T)„DXÁ%ì×â,µ"‰0ëÏ ì ´uŽ· D8œBØó€Ÿ!>æ ËÌlð»B•B(.s0¹Ãκð©~Œ" !³’?:ìkë ´2"ÄÉ%´ß³¡MV¯•‚RU ¡a(ÁaÎdÆ@´»Os{¡©9 É%äöαÖÎqáKȼ׫í÷òôéüªâÅ(…pÈíÕÜlSUj E9øÙ¾±-(’_ ¡/‹'ø1ð“Ÿçúi¦G~(º>u üÖJªâ%8õÁÏ¿Òá_iWU !—yCîâ1[ !óeJ„Ü@œ-Ã>)Ð+Ú$Bð›y("?±„DèÏ<Ö{°ç–BÕ“ŽðpÍ‹æa&²’1ÛŸCØ:ìcÈ(p,>0õ`—¹‡àǺoJ„ ¡êÉFè~‘!ê6…ݶ‘ã¦3©ÛÑÀZŸ¥?³u¸™¹„2ð÷# Äa•†ðhË£­(KêvT¥&b_:½oOÞnŽ!2 dp~–þ,"lÙ¸„ÕWÊ«/—›Z?:?¦GØ­!´M™-“Ù ¡êÉEØvë J„¬)¼ápÂ!"¹„Lã‡RÇĎάOáD°ÇôóBfËD6RUO"BòëzØŽ^‚@·èaym)Ò#DDÈ1äž»¿ôÊêY#B4¾6~Œ(žxÍRôºÅ̱LB B¨zòÞýéõÇŸ­v>l¿ÕGF„v8DLj½²rüЫ1Õ$oG‡cÝë]µ‰P ¬ö»ÀYƒˆ1TUOBò»öî§+è1ûl!yfj$BD¤¸ß±g¿}O aÊ<;G8%  üXH×Ǧ Qòk øPÑE‰0‡›¯Ö)„ªí0þÙ­Ÿß^ûü¶†ð>)¾ùþùÆ;nßL6F«­ÓÇÐ ÏS O ”_a\ dà—?iEy“–áX"BðBܸÂkÓ2ëtöàjÇàJÇ zE¡ÚÞ×4~ˆøù!#BV)*wCàæ–3ˆV»(õK(øˆÚˆP·„%W­=÷ÎhÛÕ+ ÕvEøÆ¯_¹ü›WѽÏÞ"?t÷·×.}´|éWË8‰0£éȦ™=aŸ(¦C8‘@øfüðùiðcäÇÀ¯¡Ø¡ª¶IF~iÝÿôùé{ã£å ï†Ï„F„ÈÜ“†°æMû`¬{áT !ø]‹_AF„/ߟ;·:~%Z=÷Ï ¬´«%TmO„âÕB°æò' ~W~óêʯ®Þ×zãWg¥ÀÞêû>l™ïHæà!=BG!"Eð]!BÔ%oG«£U§£UàwÕ€ð啹s±y„\B"Ä v¯‹×$|N«ª¶Bæ Ö¬|t•å‰?ôö}Ð(£ a–ÿfúiéð„Èês q¹„àW=€ðêF„¦šãYÕ™°ÇÑ›ÖåÍŸ¾þG= ¡j› lJ~ó³.Té:IýÛç?üñçÿÌü6m\ÂÃà‡8óC¦ü°IގʰŒk–«ìSeàÇNGª(§w¶>«:‹‘Ÿ\»¿¸*ŸÐ¾úãl ù}ùÕ—2…PµåÊꕨ6TYtüûïÞ#?Ù{ÿû½¼p–-œeDˆH1·Ñœ¶„@hŸ,?V3#âäíhÏ·==ßöºfË$B)O†‘Ÿ®«ïŸ|ØK{²?þé£o*„ª­ŠpWÛ.ùåO,!ÒÚsŽ"P$¿þþ2ëT&2ò£Ãú¨"‚_éV¤GH8ÖàÇ\3a&øññ,OR”/*e¤H~úBÕÖC¸»mE"d5A²ò±ÂùÃ÷CYˆ{!,!²úˆ»>RÃÀ‘#¿†" ìÕ-á•OÎãáùéß”àÉPëšW°ávÍ…Bç>¦!|ÿ³ÿøè«_(„ª­†µîXè® 9kÓ á'(R`6¡Èê?®CHb¡r ä)>J,!ùé[ü®ƒŸŒÉïÂGá ‡ñ:÷a0Éï¿?þê@ˆ?B¨ÚÉgÿ|7"ÞŽJ„Òá;ko§-!2L"ø!:¤@ÞŽ:Âå öˆõjKØõ-ñíðåŸÍÐÞÆ7%oÒ^Ú›Ô ±…ߎ¿èýö—÷hýúO¿Äÿ|F!Tm „2ðc=síŽ@‘@ØŸXBž7â×oÄßzgír )F†ë P‡0 ØCØ@!K(#?Ñ·x ŠàÇ·#LRìýPðëû@&(’ßô½8ù(’RU[äÁLÿ³»úžÅ "!p¾»¾‹í"œäG‡H‡oܽT3W F„ ·£à‰O {0µ„ä·ò?×ßMþ2öÒ^<|ÿ·ütoJ~ôÙ€P6ò³Ö©ÿìEÓDÈ~×{àâ^ØSU[ !"y;J„È>V”ç±Ú×—p˜Û–“Ûš3¸ÐÓ3ׯ%D(?¤!|Ù3çvºpÐâÏ’9†i|qG>óÔ¿÷“<ÿð§?èßû±óß9G~ú7%ïýþ{“š=œ£?쪭rŽW„¯+„ª-ˆP DæÖ8Dù6.á×AxùÁëZÑXDl`ÂûŸ½x2R¼÷é5ÚK{ñ0röÒû¯ßÿ„üôýã'~ˆ1ƒHÝŽª¶ÂC ä)‚"ö.t÷-ta^¡üâWåíh}¼!ËŠf¢ÑÈêxÞûô-Øã‹þDCÿ#)ùâAœ‡%¿Ÿi/؇ÿ÷SòûÁ'ǦBàH•~ ÷«%TmE„ì9 !ç»{ç»àpji‚g0¾¾Úr× ~)„ë9†™‘Œ¾w}½ï6!ÚK{ñÀ_f¤…-È䋇¿ú%_úñä‹ùp(‰ðÔi !øáÆ)„ª­‡ð Ò–.Ø$B!p^Œáäâø”(Æ×c-wÜ­wj[îÔè WÖ/Ö†k¿GH ¿Ì˜®Ñ¿÷còGRòÅú¹OòÓ½uèùXøqòÅùÉ%<­¡s®üBÕÖ^B",·áäB ¢@R¿V áBòCDȰ‡@ÈÜÁ ØKõQâÅ~™ÁIÉS<ù|Ÿü$Bñäóé—Ÿ&*"”?—nŠ7Æ ¡j äÊ%¤Ã“sÖ’ ö±pK©¥B&¶Åê`¯."NÄ”KÈjCNü8Ã=]A~i/HQÿâaò'½¤(_‚Ÿ!ù!…Pµ5ž‰4ž« C<9oÅ „ùÓÙÉO´¤[»5à‡ˆP'ùí+Û½¯|76ñ{áÉlÃ&Ú“ýëÿ„g-?é¡@>ìA !øÅÄÃ!”Žpç¹o\ypöBÕ–Bm„CQÔÃ{Q",˜ÎfáH8 c?vÜòtÜöP \Bž°'ÛÕšø‘¿Ž€0ñ•Ô~ñ#)¾÷Ó÷Ú‡s£ï‰÷~ üŒ)§DX9ç”oJR# ¡j‹ ”‹Ç¬ˆ{˜?m’ÑF"᎛ÜáíÒ#ä÷QA÷¢2"decü‘”|ñÀg-iÑ„ë+ú%ä›_¼Zÿš„áQ­B¨Ú›µ lŽ66aÀ„LœÊΟ2!Ç'2 l'øÊgà+8øBöõ¿Tdï}ù=ùIw¡H"䋇§4~Œ3È7%à‡Œ“oJ| ¡j t‡]Â!Šj „'FÌ…£f.¡ŒüHüö– l›"ÄßDˆ€_ ǹ)‘ àG„äÇÀéây âó¡/"<ªåãY¦ª¶BwÄEаW4–BÈ‚sÓ¼ G;o{Ÿ~æ)b3"”S DH‡ü6œ!Ê÷ï¼ÏÜ!ìi¹‘!ø¥žÐᣠoÿÜ©Rý•´aMØ%ZüYÅiGÍÁùi8DXÂÎ[^ÛaH~.m ‰PþH I„tH~F„òÅ2‰°õ^nü€B¨Ú ÁyZ†²,þLA1 j!"Eê2"4‚$B:´¶ç‚_¿!³%$E„|ÖbDˆù yhDȦF„|S¢ÞþÿöÍæEŽ" ãbüXTVÍfÝÙM̳‚BF ăJDÅw‡€N„œ"O""B@¼xõèÕcþº<ÝÏÎo;ý“fz§Ó$U<4=Õ5ÝÕ³õÛ§¾ÞEjùå#þ[ÿÏ™g?~ޙɤ«Õ27þ}£§LS'^-ð³Làê—G×wŽ+4Þ^þaOø•î!0 !ûÂOhS¸´}ý3œµ%u|}§$ðv BEHy¤·1ᇌ¼R¢¡)3´ýy¡¦d¦õÉ.P¿ü¿Éßzûç×2„óCŠîŽ–2„ç/‰=h«r!¬Ié¸ZFHYŽÌÀ ­ñdÇBˆZ¯@hü$ÏÐℬ”¨ÂôÍíµZä‹I‹äèj†IøÙ a©ÑÒÊ“OÂ(BÝòð™$„É )9aBK‘ÖtÜN„Õø ÉNȃØKBèÊd;Óůð›«_Ú¼)g„!idˆê(%£Hã^>]‡¯Ç­á‚3ô¾p¥ËEÔëƒ)„^x{È6(å„éBËÝQ¯”¨>Â.H oþ÷f†°„1E¥„Ü- !û«Nøy©¯Ëý¨BQ ? öj!õéߘn1~^)ñBeÒ™}ž!\t3ýàÊK>Q?³%„qÌ©«m(wûõN½(GùñR¬¹ÊøMT¸CZúºª„ÒÖ»[Iiô³¿NpRRá„e¸°äMáÒö­O⪃$ü¤BXÈøÁ[„Ð)C¸ Ñì˜õ‰z§í! Øl!œG3Mæ0Õ‰š¾ò¹•0à)ô¨ã+é»Ùð)ƒÆ¿à’Ò¹¯^Ômc­x#Ý*@Ø 7‡ÂPj¦þ8 HJûà™á°}Ç(*NjûÖE­¼Û _5>'´xáá oçÂ…ŠÆç_˜&~rôlKaC·z,$8l!9@–qL9$‡ªÆwñ? x‡X+¿Ñ<FâÇÖðwÆ[ûNÖÌáÆ48C(Š@ 'ÔÚƒñ‹!•„ðÈÒãåI†°#Ñ ­ž·‡IWS­«Ð~løÔš GQÇÅ’žÂGjN5T@ŠïZúº @»Í–ZqU/@¸°„Ú §ãiEfLpÂbS¸P\ß{õj™„žn‰jáACÐȘ­/¹y3„ ’ÀýhñÕBhQ³æY¸ns!0vP11 @UŠ5'dž.¬Ó¦Väð­N!@8,T@h‡‚øŒ›_Üœñ×û>!µöÊ g3Ö™„ ÎÐÑ}Q;¡#3t<óöf’1áç“„à—!|€ÁBÑ6(É hâ-!T†Â½²ùF?QœÐ*ñ“ŽIŽÌÂ’CAx†74{œG}ó რž’Ø"[BÈ8KýŸeí¾o3ø¡ð¨Qtp†tþÚ{Ñ IBðËö$x‚æ[[ûn !—èÙîìh\'œB‹qgGc²êXn éxþ»ý ) Œkñµ±_†°ŠüÄ%õæÊ÷Ôd[[Ø1ccŒ²TU² „ÍwÌ40þ}„E Ãå„‚ÐzîäRrC øeûãÂ)AH‚< }g¼~Hs@ˆ¥Ç 4‡ª’¨C!$2£pÂk£O% tù a?—ééƒ!ÌD» „Æ_’«$ù¡€ýsGQ¤s!3WLÀDÏ!ų3œÐe2„¬ÿ››G ñ„s©:)šTï ŒAR8¡ó3„Y½î{ÔܻԵ£”IEND®B`‚PrismLauncher-10.0.5/launcher/resources/multimc/scalable/status-running.svg0000644000175100017510000001371615144136756026560 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/0000755000175100017510000000000015144136756025015 5ustar runnerrunnerPrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/steve.svg0000644000175100017510000001437015144136756026671 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/grass.svg0000644000175100017510000001370515144136756026663 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/herobrine.svg0000644000175100017510000001155115144136756027516 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/ftb_logo.svg0000644000175100017510000001013115144136756027325 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/squarecreeper.svg0000644000175100017510000000674615144136756030421 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/tnt.svg0000644000175100017510000001234015144136756026343 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/enderpearl.svg0000644000175100017510000001023315144136756027656 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/skeleton.svg0000644000175100017510000001171415144136756027366 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/fabricmc.svg0000644000175100017510000001514415144136756027311 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/meat.svg0000644000175100017510000001213415144136756026465 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/fox_legacy.svg0000644000175100017510000004406415144136756027666 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/bee_legacy.svg0000644000175100017510000004212315144136756027617 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/modrinth.svg0000644000175100017510000001103115144136756027356 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/stone.svg0000644000175100017510000000556015144136756026674 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/flame.svg0000644000175100017510000000611415144136756026624 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/brick.svg0000644000175100017510000000565615144136756026644 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/fox.svg0000644000175100017510000001534515144136756026342 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/gold.svg0000644000175100017510000000526015144136756026466 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/planks.svg0000644000175100017510000000770115144136756027033 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/magitech.svg0000644000175100017510000001347715144136756027333 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/quiltmc.svg0000644000175100017510000001576315144136756027230 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/bee.svg0000644000175100017510000001254515144136756026300 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/netherstar.svg0000644000175100017510000001350115144136756027715 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/creeper.svg0000644000175100017510000000601515144136756027165 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/chicken.svg0000644000175100017510000001211415144136756027141 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/prismlauncher.svg0000644000175100017510000000572715144136756030425 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/dirt.svg0000644000175100017510000001062015144136756026477 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/neoforged.svg0000644000175100017510000000521115144136756027505 0ustar runnerrunner Sefa Eyeoglu <contact@scrumplex.net> PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/diamond.svg0000644000175100017510000000521115144136756027150 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/iron.svg0000644000175100017510000001630215144136756026507 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/enderman.svg0000644000175100017510000000726715144136756027343 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/instances/gear.svg0000644000175100017510000001011715144136756026454 0ustar runnerrunner Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher PrismLauncher-10.0.5/launcher/resources/multimc/scalable/about.svg0000644000175100017510000032133615144136756024671 0ustar runnerrunner image/svg+xmlimage/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/adoptium.svg0000644000175100017510000001537415144136756025403 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/discord.svg0000644000175100017510000000354215144136756025202 0ustar runnerrunner PrismLauncher-10.0.5/launcher/resources/multimc/scalable/technic.svg0000644000175100017510000000512515144136756025167 0ustar runnerrunner image/svg+xml PrismLauncher-10.0.5/launcher/resources/multimc/scalable/server.svg0000644000175100017510000360147515144136756025075 0ustar runnerrunner image/svg+xml eJzsvXd+6kzWPzgb0B5wwAkbSwIEOCvhnHGOGLCNjQETnu6n/5j1zD5mY3MqSVVCEiL0r/t95159 zLWRVPFbp06u+NTZ5Ypeab5VV1JJOSbF42a7Wuo222sx/G1sv17vdbpt9NXCxWJMUZMyPKTv517o g9fVdqfWbKzhW/hmAb29YJX+qlVi17WPRrNRW4wtXO/vnpye7Mcs/XrfWoTHirVuvQoPfjcbv71q u9lWlWSptsiaAGVapS48oOZW5PyKKstaTEmtyVl4wGj2GpVa48No/nMttpLS4CcbU7QM/KTh9l7t otrxPJNJZtCTSfwM/kwlM/Cs1Sz3fqqN7lm7Wa52Omaz3mx31mLm36VG7Lj0AXdKsbtqvd78R8yo l8rfEvQ981Ko1avQzZ9SN5ZDfdb3FfXF6NXqlZPez1sV+q/KCvo69YJLvOpAUVAq+h19nX3Z/4Fv LqvdLjQR6kPjdrFrmDDWzR/8GHyHr4WHi+pHDaag+bRIS2w3Wz+l9jd6DfUKfShqDn9mUafQQ8Xq T6sO44e7j3uMOuz+Rp+CfpABUtU0fOS1WCqjxtIZ2nh3dKp/1ar/WIudNBtVMgR6u3tZ+1cVzXsO /ZBvL3r1avuqUetC4zT0VZ4MwHGzUq3Ds867hXoJ9xtfivtJHiiW2h/VLsxis97rYnTlZHoLBvio 9HcVTZJCKjhtVRvF5jVu34qWlqH5eShMSUPDcko2pmq4aC2WTdF6FPwFbQx6Hb3MSkUAO4PJOW3X PmqNNdqm7Mtuu1ZxJyylAoToJ254Msf95NkPaSF0ttutNmiLASrmMTf1cvL4Euq0GxWz+YNGu4Mg DpPeAFDWmx/knvM7vgOv91qk/fjvF5iYs3atgcqUTvCd3MtZvQe3dtvNXmu/8d6UFsiSPit1PwHM 1UalA+sSPbRSa9Dl/t5Dixmt1wL5LQZrqPRTK39WYXG2FkMLLrZLZag/dvr2VS13oYyLWrlULzfh N5j/Xr0Za5MvBhd1WUYj1o4Z7V7nM1ZsNutcU8+qjQZaktC2d9LgKvxKegCPXXbbGLTNWMt9cCab s3Xtv7hiwEgb5gbfRMOP/sRD+F4HuhRlyP7+eWvWa50f1JIa/t1tGfl7cCFnuPeN0wYBid8gkAed HrbIG/7dL7VR92X1v7hioEWDKz1zy+0CLa5hTP9PrNYs1eu1j3ap9Vkr+9fsFFqmj75DtQNQXi6h +jPpsPpRX99rjQqsrcterYtKwjtorNz8aTU7sM2yCiitqNZLb8126V94lXVnspqih1ZAOmhV36FU dx2xbu03/oK9pNwWuuJ+CRNSBzSVIgC1XmqU2jH8ff+E1brdXrvEiGYJPeD5yqkh8wJUnafbKytS NmY0OKq+2y5VajDUwCVdNeD1aiX2Qb+KKYuS35ewGaVjRkV6kDakgvjPxpdVMAsGXHohX8jBpeEr DZcKl1KQ4SEbfiy4TNuAS7fzds7O2lnJ1uyMnbZTtmortmwV4Bn0z4RLt/JWDq6spVkZK22lLNVS 4JLNgmmjB0wDrjxcObiypmZmzLRkpk3VVEzZlKFJtmHBM4ahG3m4ckYWLs3IGGkjZahwKYasF3Rb t3RTh4f0vJ7Ts7qmZ+BK6yld1RVJl/PQr7ydt6AqVI6ez+dz+WxegysNVyqvwqXkZei7jRoMzdFz +Rz6l81puQxc6Vwqp+YUuGQpW8iirltZE5oD1WXz2VwW/dOymWwarlRWzSpwyZqtQd81U4NGazrU B49ompaBK62lNBUuRZMlTc7AIGag+xkDtTyTR1Vmsui5TCaThgtYMLhkNCdpOw2DmTbTMAppHXUA 1ZnW0GPpdDqVTklpFS45VYALZiZlpWC4UjAcqXwK+pGCBqa0FJSZSsHTKnyqcMloslWYRxXmSYVJ QAOswhCqeQkGCLoP7c2iZqDS0Rsq+qeoilIA1hamH02uYuILZkbR4crjC42Ghq8MXCklJcGHii8Z XXIBXza+THoZ+NKdK0evvJxll7SFIK3CmwSeCJwImDlAZRp6ImNEIiTmAItZjEHURIQ/0wTMYNxl zBQBHQacLQHmDMCJDnjTMNIQ1hDOChhnOsUZQ5kKjVMwwEyAGIJXDgNMy8M8wuyoEkYYoAt+EL4M D740jC8VI0wG7rSAwYXgpQPAELgQvFxoqRLCFqCrAPhCCEPYMgBdOYwwDSMsjRGmEIQBwAoYYrAO McgMDLI8BhnATMpkOJylYJJk/IPwZgHeENZMwJoBWMthvOUcvCHEYbilFRdyEmDOTpkUdwbgTsRe FrBHEJhOpRj+ABcyQiH82IBEy0EiDL9EgMjhEK2ejAeNKUCUqsrwKQMiCxiX8NmHTfiRKEB1ClC0 thlMswBSTUljsKbx/ymK15Si4P/JPwe78GlLADYEYouC2MIXgbJFwWxiIJPf83AxaOfdC0MaDSUM pnftop88Hkf2kxV+NO6HjDD5LSvRXzP4JpsE9xv3YY1ODPvJOr8jwpLBn/C/hClHmlIQ8X+Xpri0 RcE/aH4VPL+E0hSc/y2JTjaadBNPugXTTsgQ+STfMKJkABJcPGTxZw7+yuK/AB0SJlUEJBoAhAAF N4YCJu0ARoANJWcMNg54JIwdy6FuJkCHUDnyk3eoXY5+Zh3Kx6ifCyz4lAR8pTG6GFVUMM5kB2UK wphAJxnQCMww4CQHbwRrpoM1A/+Wx//nPYjL4R90IZqSc4mrrEmyhq+sjGgB+T2Nf8e0gV7o9xS+ 3G9Tzl24MKTx7Bl4xrJ0StJ0/5Cd4S4442vScSUjmcMLlI0i3kckOnZkzFJ01GQyXmRFcjuKRReh 6QwGPwi4+5Lbb/xDLrGrpGvkUvGPeyneS0INmeQ/abLF/SlwjGIQpOF/BcMgjVdHFi8lHaMMrcgC RqOK13YGL/4cJhIGJSQ2ojOU/iD6hElWDtM1A1M7G5FKvC2qmKhmMIXO4Y3AgM3VwptsAe+8KlDf NOzHGuYF0R4Nu7UEmzbavgsYxQre2tOw0Wt418/B/q8DJ2ACT4B4gwLgHfELKeAcEOepAS+BOAod cxcm8Bm2pBXwElGAxKYwP5IBjjYL/Amw05hbMTHnYmcLiJPBRDkFvE0GczlZ4HeALwLeB3FAiM+2 JcQUwaUAXUhhZjwDfBMqDfFQwGYBP4W4Kgu4qwLeKxXEB8B2mMbcl4bYb+DF8rqO+TJTAgbNAoEA mDVY6YhAq5iBSyOeB9g5JEDksDChG+ifCcyeBVwfSD9AMNHMoK0G8YNp4Auh1xIwiFlgE5GYghhG w0T/kPBiAxtZwORXwTsXmg/EmSMmS0OjgIUf1HhoEaoFC0WWBNIRkqRA6sLUCe0viOtGuz3MFZoJ xNUhCQNzsjngaPMgcOlY8DKAxzWxIGbZNuZ6C5JdINtCAVNRsqkWMCeGZTh0ZfClOVfWuXLsIlSa YM9Fn8Khz8WfHwJdDBIUIgzmJcw0mj44ZEjUHCQyLFoUiwyNBI8awaNEIWkwSMJVwIgkmExRTBJU 5igqOVx6kJmWOHDmKDgdeKKBwegk+ExhfLoI5TFqYowCSiUKUwXD1AWqhoHqQtXASEVYtTFWRbSm MVoxXiXUIAxYBlkTQ5aBth+2GLgUtzxyDYJcCUGIA68LXx7A/RDmQEwxTFEsURi7QHah7ILZC2ce 0DykbQxpdAmwZsBm0HbBzeDtBbmGIY1xgyiZS8sQdBzwcCSNAciFkAsiBiNTwjhiSJKxTgBhCaMJ 6woInFxqxyDFQGU7RA/BCouHjPZlsFLCiy6GL4owhxIyWohAxsFMcpDGY42gjeJNQByPOYI6Ricp pZQo8hj2RPTx+Et5MCii0MGhZJocGeXR6OLRJagEkS4mCSoFXEoOND3gpESWxyePUB6jDKUYpxIF KoOqCFYRrrZdEPDaj1jArBQKW82XNnsoNKXS6Vj8xWhLclJWs8gqB7+A1J5Po1+UDIBPAi4nmQcu G+gv/ALEGLHy7Bv6+obEcfrsyjtKFna56hfDuUznomNLOi1RjkehMkyKXhl8MQEoS4UkInsblBsy sXhOJolwRcAXSZg1YgJbhopzRM7LY32A4exWsF9h1QESMZGomeL2LCIEw74lwUJDsrSzccFksI3L 3bpylH1yd64C3rkUrG1IYTUJ5aEkWO46VqeITBRjo9DGRZiofN9+xe9WDgcl0a2KkBdGWggD5e5P JtmbOL6J7UqEZhh0NwI6IdF9KI2JA9AEwrnmfS7dc+U54RFdrk7OVWxQ+VOiwij6kwqoVAxjAqzK AUOhQluawiNNVS0uRDSJis9oCHQHLwwtBhYTkVTuyOhIhMSipIo5atUDm4yENUZIN4A0BkhzgLQI poMfE6shCIYYz82UGCmMI4akHOGAJMwCESzxeGKsUFpAFGWHBEzx/FA6k5IwrLIYWHnKEpkeZKlY aexl0NG2RrBlUXQh9bMqOeAi8NLJ1Kd9rpQosAsCPS//as43TB2AdQQZR3DWHEVCltPXMhmbydyG QFIYrgimbInCi0MTU7LRS8F4IsIVjylGdIhawMGVRBXPSH2Qo5o+lwblqfrGcFWDAj2yKMIKLsIk B2IqViRlKMzQlcW6yayDtDzVUxkcvTKofstimJOITgyDTk5RmkdVbWkq+Lnw0wB+WU4HSPSCTNOK tmBLopC0qD62wOTVif77U+B/psA+TRPVQOE1K3GLGS9UYiPJ9V1eNiPLsSAuY+IyK4x9wfyMhO2C 6CIrlDJAlB2yqKXQpEwTYaDy1DSD7YaU48pg42HaTknYgqhiK6JC7TYFXA6zJiIGT8c2xTy2KWYx P5jBlkViW6TWRWJflKiJ0cRyjy6YGbGhEa4U5lpdWyOxNvrbG9MSNgMpjsnRNTrq2BzEDI9pZnjE 1M7f7ogNQ8QsRMyOxPBowt5ODENZanhkZkeZGoVEiyOxNxJro6wVJLwDmNTUmNOoeUzzuTJ9V1q4 UuJ0UzswviQ26QHTblEO2eibfP/pz0gEAfjqh4AIAi8M/ICQkjxYKOAGETD0w4HYnRkgUhgQIiQs CWPCRUUOo4LiwrVEC8AwKTCIRVq0SauIK3Ot0gQfuoMPgpA0FgsxQBzLtMlBJOdABEAiOSihOAla 9UFrPxsKEQoLaRA2HAHICw1bgIZDF6QAdAj4oIKaFyBeiGCQSB6UiDgRkYKxwkPFDywSRosXL17E 8JgRCYmB1TYcMZEwbjKOXdklKSJ2XPQQwuLihxAXB0ASpTAOhlwR8UFSkMNhOi+r8D+S/vAvShZE QuKgqGDhEH5Jx9RYKpdMA6MUy6lJFUQOVMalFH8Zqwyj4wiqMSaoxpigil8nYmmMCaruN2pM05KI yY3l8tAAOccaNLkCJ9K6fDopw/KbXOtYgbh1moZKQ6ObzaVTqFQV3svkcal54LfRLRkIBZqVdA6e Rg9rUFxGjgHj7DZr7JKgPUALmZsSc0SK6pvk75yEvZNU5p1E2RvXosLsKTYWJVVqSQ== ce0olteGIhpQJMpUI10AYZ6Z3toWrCaOxYRTVLvmkqwjjVlSxqZ66bRgIWHCfYEzjvQbRphZxFUY ZiRfy4ireFY55aDGmUbygmLQVQsqEmchSTuK52CFoKgMFNWBWCEoOfpAVyPoVUb7q6NVRx1IFIIZ rA7MSdBmoom2qe0khVV8WazSQ8q8LRcKrnktzMAmmthccLjqJBDamD5A8zWz+RnZHLD4GNlyUghi tD7zWsFjXmNKac6WITkI8jew6RRHpqCAVnhjhmhokyieTKZ15owZqqBydhXOokGDIoupmyUBWaJR I88ZNVzzG0GTq1jmlMoAHGDSEVpSGBk5QCQCRQFDIo11vflBUBDBwNtbRYurwVtcJYFgiGAgMnmB 0wAxSIiWV5sHheSiQgCFFxYiMEKUhhIzdwkGWJ7E8PpD0d7ltXhhoiP5GCp4q1deMFK4YAmEi8Th pc9AEWABE+1fHAECqKhIvMpQ4ywiK0BOMBlBpAPbX//3qDD85HYmrhNwZ6naDIZR6kO4S+w4Qsft ggTSOlYz2RjIbPtD8DWRax5SeyNShlSTOlFHAkxTeK/LAzAtgKQCYETaa6K5trFWEW1qOcCXmbUB TCkAUTanS4AZG1qbAmxkAQ4mAECBaUfqZh0muAA0IA0zi+bVhgWv4hnNw2JH9F+ltD8Pu4hl2UCx UxKsCWSE0YETt7E1JQXkExlCdGQmKhBtFvZeJp7LxG85i4WWNJZLmM8ycQll3sp+rsomlhRARpA8 fsrMS9mk8qLjoUxlRK97ck5wTQaWXuI8k3XOM9lRD3DKAV41kOb0AoJWQApVCviqBLwaAeoamqKO x9gr1KJux3nqcpyhbqAKdv+0qauxjl0/iZsxcfkk7p421n8bxL9Toh6Eaaw5V7Cjn01dOHWsD81i h7w01p0q2O/Lxsp84utFfLwyWH+L3bkkbEywsIZYx1rkLNY3p7HzlUIdcf5A4Q8U/kDhDxR4KDgq ENkJKFWwqJlBfzCh2O8eFrthQHNqCoVuZuQ8koyz6O8MDUuVfcTryG+MJ0arvmK0isTonCBGM3aZ l5pEp0TCQvQzEBmOhXDEaJnnvxSZiOkq/clQxjxDjYE5+pN3/I2J661FbXoOMyPhjy3ksTDc4o2w dK2shJauSxZsrCxkoU4s2CnraDvTjlJTcVSYthD0pFMtZVZClUPJiutOEeqo6QoLvKQQIDziFlPL JnFuznAijUYN40zOZT/MO50aK4kLBZpRZ3gjkrEoRAy1kDffuGp7qo91rTZ+NhuPxUYcSD/pmboD pgS1C6d4Ic6LKerXr1FXEWbsNwS3RlfAg/lyBkcIJBMJcyBZxrU6WmPXwKDTMfVamxzjgthhxwuN Sf28zG9QXT6GFmHkMzTcw1VRIJMv60jfbiHalUijHU21RafZY/ISG0hlSaxuII2xMIW3qWmZVOzE MaEYOfSUo+kG3IgFYkkUSaFMV8ErKniPy3SQXwv1BKaOLZLjBMwUELajgGDevxoVLl2/X+bzSxwn M46zWt7UJeYtSbpbwEuXLd+0s4DdRYwXMtoaAxxcdbyZUjRLjjKIQNpVBjliPtqmsciVwls3wkMe i16GK9Q70rxGeAAYSHcyxmYDRC5AwmzAGEyAlweQGBNAoOJvW9W5lagJ5g8+ptNdkw6zJDkrQAzr JOsgLwTepbD+TLCeYQWKENcpseg7cTsx6Wbl3UzSdNNi25Z3I8mz+FlCRCVq3/KjoQbdz1wKKtJP au+m5JySH4nRH5cTgkVvca6WiuC17vVZJ86+xJuSd1Xn3dQ1iTpL5ql3JPGLtB1PSOYBydweXVdH 5taYx0On4x0Z2iURh030PvuHhxsmzMAkwvWeJQTCVUih2UcEAuGB6MIReSCKyixVTxJ9U8FQJKqV JBpJosw2qQJboSprDesocthf1SK6CqyaJC6pWDmJdRYmmlbiAI2GBmku0NT++9RVTBFK+DqqHKJc nepwCYw7wDpwygukKGeXdfSdhLOjPkcp7Oim/R/Ss4kqMNUJqxIVYUwNZniU/inJE1QDfA/hAYS4 ZSc80A0X9YQuc8FnbogoF7wsCYGhxKcs7YSbYe9GLijPjb9zw8tyjmscFlEk6m7HQsZkKrg6IeMs qNcNGs85xFkIG3dItCXEjWeJuCZxYeMKFzhuOiSbEW1GtgnhZqSbEW9Gvh0pjtFwoOKElRhSAB4k /0ocpxVB/B0s/Ur9+94A+VCRk7IWIiOS+1hOVIiQh77AtluZ5P1hht88sqw676mpZCbrSowjvIvr TEFPMug9hf6C39Pw86gAYmym76WySS3j1jnCu7jONOzZKFdSElmKszn6Xob1IIva7LQ1gzIeOVUO /yqpEVmg0TioMPOKxl6j1vMsar/TUE1NZhSuxqFfxTUC2YFGcXZx8hqZBnhPmI+sknQhMsKrRMvA zO0ZfjYU5iKQZvb5LH6QvJ3LJ1PchI5eBLQAu3ZgJKCmK0pWw9DAzh2kHBg/gJ0ziV61x+gFjKcF yftqQfJ/nAn+OBP8cSb440zwx5ngjzPBH2eCP84Ef5wJ/pgN/1iQ/0DhDxT+QOG/3Zkgm0+qwAQN 6VDA3hpLnPb1KVCFvKGY13CZZq9LAe9QQNgIMbtRn5gUZC7nJaRUqLHcIxpJnKlYlIxEkTrAxsrM m268vSSYJrmAe16MIKIEi7ZkokSORjlzWmtOwuRzQjmChSQkhzLdoGNnCFOOgOEOpc5kTk7upGMq OZkAveKn5cgargiac8QN0x1o0ZgpOePtCqN4yEXHEAUvGtVxDeFDyzMBTiI550dn2dkkvB5NGh5u 0U/iP2IzDpZLOKE4MeB8HHiKxoCnURS46EKRddT6efp7znGmQHTCdBwrLOpYQSLC0Y+JEtVJNDkg l2USh4jL2Ab7Z/PgNg9+M3VHhB8TZ1S4cXFHhh8bZ3QkboAcjx9hjNxRcsfJHSl3rOhoSZw7hOsM 4Ubr8QZqPsIzMPRX8on9xUPY75LU75TEuyUxxyRFcEzyuCZJTgwli5/MOHGTYlBtxKhqyYmQDA6l 9Q+NDAiMdAfYExnJG6kHWnl4bJleM73LmNDky6aEORPCm7h5cVWsIgkzoKUdFsVjPZNwFlzCqWSF LLiKkHSZGc90mrKV5VzN0CyqKcrAKJJv3mXRgskSL7v2S9eCSWyYtmPBpJlKiRmTT3LL0tw6OTRZ NmYu8ajq6hPcWF7JSfTjZmdmuWDEHDH85U0lxHKBoLzNPrmHhAuvEMo4MtaRMY/MDSVNGEjKQpL5 MfD+laespGvfRPtcSmIzhFPrsgTFrpGTJSh20xO780RS2LrJifF0SThZLUkiowszRhLRujbnDM03 y+WadbYNZnuGT8kxQVtchtm8M4M65hLYLGZpPlmNMhIatU6zC2qSuIl1U6Vw2Yv51CpcMieWVdZN ZGyR/KmSLwBMmtnYmykqKLUQl3zVPxeRNyEy2Vmd3dZyssWyDdnmhpFt14ZjymefLEdvTnIS9TI+ gM/+zBaHu0jY3yxXb9r5pJl7mb9mQOJePoEvTU1bcLxh+xL5kpRKbuJod7D5BNLst7wz7Gywxaw7 9C+JS8nD1hn6nSkJyd+acGW4dL8Zmv3HYeUk+rU3eVBayAPMLm8qIf4eyxPsm5tITCQsM3dE28uv c+y6f4Ysrc+TUMiR5WbJ8qQUFfwJFWqDYB6F/dlEcQZ5ZrvyZmUMziVq+6RjJKnvYBeTfNOJirkY xVR3bqI7N80dl+RO4nIxBmdjdPIxBqS341IySlxaCP+8jJm+DBP9qe64lHfBySv4fKWR0sUOMhtw GTglx8iU9jEy+WXf5HMfOklj3ayHkpB8k5vswOn2y77JzbnkTLoz7T45DvszHIo5DnPu/Et9OQ69 WQ7dPIcOFDgw8HBQWG4gb6pOLzCCIOILGP8UNt48n84sBduS/WbLnS9PtlSpb85Gz1KJ50/iFi0/ g3QOQzJV+s8k9b8UJlPIWNk/n6Ezahek4EmNPKfCDEqBKz9s9QekvoxKFfgElzmPy0eq39iFs7oF uHxgpYqjpJJEpw/qFe6qUFxrrZ8BX4xmwCoqiUsQ6ef00a+j6t/1BBWV1Lfl+bnOB2SEZEE6pke9 ojqqFNcMzjvN8LnMHXWVRNlezfE25QZV8JIQdYC8WZzXBSqSr7ZKzC2tePxsOF8bZ+iJ170z+Brn d2NiRbaQQzE4hWKKpkPU6IEBeaoT5cOdTJrIsOCMp+KwiIo7qhJVnbIf5nbiehuYdKgtpnOiyQhV OtwqDXwhY56VBEVh3pFNDDoFThwMFWgKNGmqmHkwg3+wOCThtINZmm4wz6kU2aebBvNPzkH2zz9L n+phe4UkfWPqCEUVoTS+jlBUEUrj6whFFeFIYSosq12G04vJjkbMFOIr+rSGVPllCkEVKErQiano 1xnmhZETx67/gDJPukBvQEte0LSK48iPJD+WeDQl31SBeTEPXFAauCxFVkbUJXKaRKZv5TWumie8 j6oPJTqE/rEpGVFtKATHBaRilAbkYtQ9Wffc7Gl87jTLTcgo+Sbew6n3uLyMIXnTfGx5uXRSUzLp EGsfe4I4EzMP3JSGvJGRKyz5gk+ulUGJuJCjclZVMhrKAYbO7M3CV30pzyZU3lgZz6CPeS2ZTuez 4yc748r6X5XnLOVnS00tYnvt/1Fb6mAulTBNPm6mfrZU0T05jE/14VIDbamR+FTGqSK5nepjJC68 k6limIxO5HP/0E4d+4Wa5PQWx2E4hYwdJACOD35joW8Kje3CQW804o3EvDkRb1y8G411k5wwt7wb 4Iaj2/jINsonBJmRGTR0qgz0NyOn/c3IjnzD4ceVczTBjGzwPCE9rQ0DSqKpp7Ncsmn+5BcSF8vQ 5XqtuvZ6gjPCmKtimvMc9g/sP4OIJDln1ntXOnJxxyJj4QEJh8YyN1XRKz6Dd0aCPbRfYuzhkxgJ 9jIYfVmMPBNHD9pEHUiCB4lTMoGdhYFXAMylsAtiRowcxDsXczDVaNygTTyQJRisDHZCzgGyTLSF wlaqAp7QJuuJFXQt6ETGYKwiU8VmKTx4uzkDCZM78KfknEBIcUO0zjR/usJhiAkihOxkHGsCM4tT gU/CufiZfdzgUOZKgcwkbjvSoOwRUlIUg2mUk59ILBrNmq45oXosdzo71U93sqi7fg/sHEDLEWmw PabAfK6pfGMzGYceDqE4Cf5l53gjmR4boTp/KTTAjf6qOMIRS9HObEIp55wA8j/zwOi/j38kSqaJ YYkcLaBRWUv7Pxh9+b+zQMX5Hx++x8tcKmdu4M0VfecZcAss51hMsPlENG8xtxTLsYwWOK0JW2Ru Cg83soT6oUg0+ijrHHXhZvNAy8r+44L4xwXRxxt1PAeadHQHGhcghegONC5W9AEONH4CccbfgcYn wYOLHwFBfvnzOaFY8knvoHFoUvsisHUOUzyqqPOIxHmP5DlsZTzeI2LkdY7DmIMygg== MwkDjc+a4WIt5cRc9/kO6Q4gxIRGbhaK/pRGXC4KifMbygh+Q+JpDJaP3C9K/hQ4EpeaQkxOIWbH 8fcY8kmkLnkUAf2qgP5E6u75DGIifow0SVAJuAlF3IT8LCW/izt2ord7ZLyLP1uiKUYIIWO6GTfR iKuhoefIU8rGzvp2UUlP/JYooWMHfzunymOIeqLY/7ORvKO4RY8uYmf9ROwsErEdd2VL4Sx07umG Kc5a51rsmNVO42x3Hgue5DkEMdCKF2iH9RzrKdniuZ7MNuefPYUZ1sTcKXz2FEOi8qV7cbbaoH/E hs4OFHSPFGRXSjhckIhDxD8i68QBk3hNNxbYkLBZ3XPcIDWw84HBTlCwc+ggHxHMbLjYncKm5lvX nYIYbYm5Fj/HHclK5p7MdQbHg2VxTJiOpS98BCsR6wfZzLM4mQuZpv9FIYPB/1Rq1srSzDI28CZU I6VhBtWk9tKUY9rzC3v+/5UU4znxW/HV7LkpeogMkKXaPZK2RibHlJIML8xFNUUzvajUXZPkeylg ZtZ23FaJe6RB3VcZEwKXhP1Ys6REypEQn9YUzQpDuRPqQUk8XG3MqRBexSQLFJ+EiI/yJYwL8Xtl jpXM+zVNzypTaRIZhfp4FRx/WLIgTcrdwCU5zrHE8dJ1kdXogXtpwQGTOMv+EYoiC0W8LwSldu6R OezQHNty/5nc6TniCTp5uhWyjVGTHD9x/jCdlLPfAgUm2YH8TtpyDlbysfCIAgN/rhJm6CSfY5WY GY2OLM2BhxZVaEKGkHQMQbkYgnJTcgFHJDWm/1He/ht+hC1f8uz5rjrZSZfGT3bwhh/xnycwQfJI F312RM7+Kh7/5R7ulHfclLIkfSo744k/6ck97ck98Yk/98k9+8m5yHj3u7BxnozceVTek6jcp3Xe 9in5GT8F4zETmMWYiQy3ElLuSrBkliVUjJ8wKeuiO3EUxDRKpCMmITEpiRw6RVwFsNIwRTzSqe5B x07nyNFcxZ7RBvYW17DDsoJ9jk3sGqwBWVb/PVvhv6dAkg7Xxu7weadLKnKppp3KC67JKSG/muPm /m9r38QLpBwBC8wIuORhL4n9ohZGvmz+ksQ/B1xW2EVOah/e+y3UrChF8H/LCllj+42KOd6oKPlY FXG6WJaCiLMppgWDomNOZFk0/4tk92SOWcU1FdnwqbyOznNjjg1ZIW9dxOdJ2ShkGQUxZ7PMXYEm KCPnxyVzJPUcTf7GVTLkiyNnMxtzFCPoMl5Omo2zdq3RrTU+pJOW8yj//cpK+At5ckdvdy+7f9er HWn1sNH8RwP/EVuTFh6s6nupV+8+LcZWT0o/1diytHpZ+2nVq+wROXbq0dPcluCLiwjx6be6JLt/ /A1/HMAvX/DVP2Lp2HHs4UmOVeDb2wsJl1uRVs9K0PzYuhRbhYbB/7gL0Ge3AwNH4axUr3a7Vdzo s7cIzVx4uID3Ot1282kRd/L2XxL/nXRWHsPRBEZr4aTX/KsZK5d+WrVmoxorN+vNdjWm4JLPDKdL rOXDzOgRNJKfxr4Rww+MAh1FJrcu//55a9ZRMf8X/RoK8nwZUDhtq9Us934A21apW4IFs8r+BnSh v2rlLgxLqf03+fv2+OikWan63lyPLfzzp96A2yulbrdde+t1q51FAC08qrfbpf8zRUygfO6p8met XmlXG+QZNba6DyPj3EUf3b9bVXJ3Ya7Refmr1O6sw3q9hNIbH+Kjf5XqPfYs+r4T8BzSgpLHaEs6 wl//Q0enAasrwsDUm+XvaiXKyLAnl/+z/XqrNSrQUCVC3wAcl9XuCe7E4P7xT09o+sccCSV0JCL1 v1Z6q1ejAH/grP5PWuhrf0Ve6ujR/zCiUffKvU63+fOfpWT/PhyudUqIjUJ7HiyxqHD8t68LaMt/ UVP+N6zSzvs//ot34//RG7ocW72oluohgxppQP/T1G5gN/6O0o2//9PdWFW0TApEJwVknf7n+P50 6rVydd+K0ivn0f9s3xQ1N2iSPqu1j88oxNN58r++S/+oVbqfUXpEH/wv2FcxXv7TOxhuxP/wvesP bf1voq1/6E9U+vMf7FBs1Wg2Q7rz1uyCQHNUfe+etmsftUaUnvW/899CZC+bvXa5ajR7jcp/XHoB Hvs/3YSfardUAWFu3Hbkx2zHTIVqT6Ogi3tYQLC+ryixs3a1U23/VY0Vq//sxuxKrVt6q9Vr3b8d yoobip91VLYgtPVasaNS46NX+qjGzpqtXqu/cDn2XgfgVhvVdqzFqmn+VW23kH64E/5CuV5rxcpN JD3/M9aufsD67jhN8n2j2evWa41qDOnrv6sRH+5Cr2lDZK6v/KPtUqdbba/8VS13m+3YW6leapQZ M+yhPCmNEbtWqVLxtOCn1Pn29LnTanY9T5XqNdr0LCNElVYtSb5K02/KzXrbmV59P6b3us3YBW5n 7V8MIw/H1UqtFGvXOs1671/I5vDEocVvQgtOnzEqujFXJygLoGGAin03muVvGMvYR7vJIBDwKGpG qVuNgcyP9Zh0KDLO3q3v52PH1c6n05ESGtrYRRW1H/1K3kjJwhunvW4L6g9/h2tSPtYqtaCHndpP r15yH1Gdyc/Huu1So9MqwWot/w09q1Xg6X9VRQqCniu1u2/NUrtCbDnkPorcyAU+E1Oh/2wVD3z0 o11lBGPgs22modYymVQm+EmFa8DAR7kGDHzWacACOrsK/Yspapb+ygHv8nr3xWw3W0bzn7d39A3g EpJyyIM37s4PpZMnY7L/K9e16j+GKXuP45McnHzr+2elNqAfQNU5swomECIAC08ZGWCER28+a+XP s3bzvVavHlb/5igLQazw8GXvDRZZoQm08AIh0XkBjbXmV7z981at9BXv22jUYljEHQfg66gN5Wa7 Uq30067Y6kmzK9xW+CXTaLp0O1ZrYLrc7NS6kUmNGgMqFUJe+F0AP2si8m9S8n/Bk/++R08pKb8M ovvCU0WH4GddOJNHCA1ZvSaE3uAJvUCmyNPMVh8rNSqUboVRKvLSEWqC3iYv4R3X+5LiadSpZ7+M Qtbts8uog00ejjba5NmBwy0+5j/e5JnIA04eH3bEyVtDDTltu2fMV40aWyb6pbm/n8tYsFQqyE8i nt4+nXtIbNxszq+XbpcPUnOnK8ZOe/fnc+2jMXVQmFpemDdrpWRnVrvas7WZtZ2r3a3j9Pba0eP8 8U67V84WbPU4F1fS6RlZ7lhf1seyPLuz/pxc2tlYbnV2OofqqhTfWT+aarOHDrrGx9750c5Gunpp 1ja3ylYyOf/RV9VR5Q7qy1qF+Fr2frdrfT0Z6fuVZf2nedTR9y+7n4ktbaZXsNKzN8ZXff5Gilvv 8sGbb2Gz2fx79vr84VEvmsnr4Er559aedja+C087a53kT8JajvcKC7uVdymOB6vw+nLas96fbrJG fad+u/ZufHbNz+y9IgzH65xVVo5+dza2529IOdDkjvn88dyE3+Z+rf3K/pSxkvua1S9XZhqkDbel Sk+K578WEmW7nDlfMD/TL+sbejw1lzBOll8TO+b8VcGs9pa2rg9mPtfL5dI3+q2WsN+PPknNirxa yrZrs69rteeDilGPb8+vtBOPPf3ocu4XtX9xZ/3gMyXFtfXrpx29UZ7/SWwer69mfx43a9nsauc9 pbfL+0rie01xSixbB51rGLbsfDV7k5IrazVztQTzqxxvLqwsV4169uyH9ODuKL5j7m/M3NjL+UwH 5mX/QZvZyprN58TGdeVhTX2becLFbjXi0KEtbWkGTcmDdqOdN9A4bRnfi9oKheZ15UhWnmaOrdXS xlxhKnHfRrVo6MYzLgU/IsXlt+n9NP49sVXYoL9t3NiH5HFz2X4lhal36j5A91ZObG3Zy6q1/bFJ y7nZ3FivfJ0845l0GgzlnRoZWgs8ZBw4DXhyG6AsbF6gh6pp/F1myrBe8FBb1c52WrvXvsp60fpK WO+rh792qTQ/a2hvV+cbe9bLll78LHf1s9nysV5UUzD7evbpbgbeqdzbt6/bPWeICGoFmD5/u4Xl 6sn2Hpuwu6b1fmlX8HhCsaXFxNbM+g2ZIVSyFLdflMS1kb49KOy0259X6bXjm208Q7lMra3B5C0t J4xm/tk7lGLH+XFn40QmFhUlxdcTm72FglVXDPkgswYfu3KNlLOlvTV31ovdab140O31D6VnJrlx ZxN/255C3xWBjv0ma7p3nHoX+WphYa61aH5qF/f2m7yxZFXb7WW5era57jSEDIczGEe7+suBgtG2 sfJaQCv1MGntf2VLZO2TCc1d/v4c6qfPxnHBfD/IycrhW6VgVn7uMPH0mYNdo65t3rhla92jzXtj rziz6WmDFIdWVE+t3e94Fao630AUJiW/r900+1vrfa4Mvy13d1vTb/l8Yj194hmR9f1O892sdWoa opYrD2fq4sz+ntur9era3A+s5ItFBK/jxObB/QGr9OMR6FgK7saPlna7L5Uf/fL1wFqtpadtUsD7 /J6mF4+bHzvXxf1Swc6d30rxteOEyhWB5iDDtpaLQuFlceqTvi2/ynYZxEdMKDeW77Z39ezzVEeX t3pZ9zn7bfVhVj9b3n4gLUSEWYpj0szfX6kfbyzP1G71YnFumdt9FPmtZ883zh6ceflJ2Ne3e2xD WZqz3vVUlaP8zl2YfXrf/rQO2kuv/W8vP2k3ycOs9V5sa9bB4u3lbnz3IC0XHndS6G4T9q6PXkEv P7zDAq/8wiOndwCfbe4u9GVNv5jGXxQW8tqivbL8nraM47kFh0itbsy/3HxmL54qeQDxziZ8FEz4 MNHiKujow2B/ZtEjZgF9nKMbW+i3S6CWwpPo28Kp8+Ql+rhgf+Ii8MPcjUvnxrZQvXnmfBi4FtIA XSiM/IkfN53qTfZiwfApe8upWRer13Et3oZuOYVZ7E9y12TlkIaYTp/PhBHbtIS+FEzPiBV2nHYb rD34O1LsjjNY5+JEuKO447TGecS8ILU4JZKmbIgvbgsl7qC7m+i7TfRI9sS5oQuDwSOC1CLOWyTY DAUaby2BM7jl9j+wyT4YwsNGkOze4ibUnaEdsZwLp0mXAkD4ifAbMRcn4dgh3zkY64NSIJBILS6U juTo68D9k3TI6B9AUrPh6Qv+DU8jmUt3xM7ECsRecQvJ7YHzYZ5xtbj3/cASOL9nzmu7TtdM1iu6 zNwR8yw0Muiogk1nvRRclDh3STl+xIwbaLcWt2XZHaE9wePpvubi5cKpgEMOh2SHkIYDrVV02q07 Ay2SaO4GbpfppTAnhHHa/uz0yC7dPdo/3Wl34xf6ZS8+LW5vTZA7Ogva/Y/9g1i2g9XPZlyTD+xf E21LS1bZNH9AQDC+gYepx2sZThhS8iAMHRraPLCVi+ccC1U86CX4DZp/7gpY24y/RIdYO7TvE+Zu 857bkR1ZDTUqgQUWJLguA4txvyF0SJ7ZuZ5fMKxK/eipYGXuVU8t2fX7owJw45md1Q== K2tvoTGjH95dNoT7pUetfb53ubOxkp22DhIzGUE+BVEYsYM8ywYiLD8sxtuHVV2yQbIQ+sqxRgkQ n4q/+tn+4qP1BiKzTxFEWHf5eyz7ZTqNC4Pwwd1W8jyxfV36leKM886VJsJ5o6IugVM6Nbv2fSX+ ob5ubFuAMczyIP5/43WjYI0vSjj4xF3DyIF5OVX2Hq1SY/cG8eDfBVsBufVSy6uy0pyTs58zVRgT TVtyuDmvHOQWpp8eaUZhw35MOlxWEouCUjxQGIwqCqLCstmrp+qh9X7euVytnT0YdNGgPqfUaZCS wqXbSLLtzVeKIuIq/k0VJoI4I8U3Eq3118LC+2FSlzeK9+ri7PMGY0/5cbI+Vha3CUDOU79N/fDm dhaWT+K3b9j6BSApzotAXFNAVinoVtVZ3Rd0NvB43myuuAsg/109NEGUqCQKry9becs4uvv2NBNq oULVx0Nh8ay+jtevq5NYO1BXFjzFutKPKPus0kp1+cGu5MrP8sF3aU99XV86IxKfvFb5qctALZUD TLj8VhZbDMXszvrh3TRIYJ9TzuyvIRXMqX65W/kCjG221Z2rqTxZHsuzqzD7mws9tQrC4Nw7ueXA HUv0yvKCmReFVK/Evi5vr78a9ca1Kb/nzX2q6OjdwRppGXtGumfIUlxZvuo4q/YpCXTz4VnPrx+u ODeKTIzees1mi0tV+WBveh3GO5lYf1v7zEaoGY1Yf92j10wB6VEIAYVR779nzM/HqbXE1u7Li1B2 8tD4Pk7MJrbO3tPiHDwb3+rmlHvD0dKoVnXq/MSoVw3FrE09zANBNc5hVcqzn7pd/v6dw7OR/2rn C4Xnd3u+oJ/vA4HfPSdSmaxq+hylzrcHBshsb/nV44tHHZWdcvWS/XjRFSnOnmTrVyiMV0eErN69 FWcH9F29WDtK12++Xmi+R1Fg8A3p00gou+Zh3VMe0OSzvfo1v9BWjZr5+WDCOteOX/uL7Z65qzex dfe0RB/Z+gUW4/hjaud3u1xlkzjf03NvrQ8pnlT2rpcREopIq3BnvSenMmRENw/aHXn/YXfD2aLs tYPSW5JQ0C3tRIaqiom9zce0yrEd6t7jvF40Ty+t5EVlZWft5LsGSHZ2LBd5RPs7t3dxi0T4B3u5 edzT86v1JbcwqrJEmoaF00YR7devsEu9XUHZZdXduRmSFaMJk3OZerUOD5vr5uu3kQZ2Qju19suJ c/iuqFBegFa/bH58FlaAPVuYzl2ut+7tt1X5Az7ua2snH+s1+6049ysyMhpZlQ+54tzsZeFlZu6i 8HKmd5Fm/c2/8R/xb5i//DTS1e5BiXbGqGfPExxzQzYjbWNhp72S7+nnS9Y7cH0ra9WWWy1R9yq5 qZPC4t1ZF7gnpeLcOEpsHp5UrMpPftmtGfq3sADbxNwNLOGNtHBj/nP1s/r8yngYbn/FBGdvdwpW ZeV559dQ1oEgpWbslamk5u2a8NzW2rt+eOj3CPCW7KF97Xd1Xe17qH4ZL7x8w1o8ezh4tMvbcxnr cH/qMn8W/yrsdA6OvvBzlML0Y8isTc/CvNBluGZDK4zWsgcdzKSy+rlz/X5loElu8dwhLSyx+5Rd XNUze0/7IoeqUR1sLlczSvZzQb3Sz9dv4hwTTKcxv2AddM4asLq15G589/FFb+xelQobu8qCpzAK uWRlJ//5qNdhCVvH+kVxB3hLnvOmLVsF5vVoSc8+rps7aze/texNKlXVi3qzD3Jq5vvXyCxq93pj b+m3sFEodziobG1qKUcDjx5nDOYR+ngUcLK97w8QbWYa2lD92NGas7fGebyprTSWiw4ztZnSi9fb 3zsbW60ToGNXq4cb9tt8JvCha9gIFjtoN9QdcoSGcm+mYOpPH/Cx8lKwzo7V/gI6yzvNleIe1HK+ tP7pXRZOX709dUwYfDkP1iPwFJkTGOitT3eagCYfFs3PTPcM8ZavrSXjq/tT48u+2UzDHnHRsRNL 2jvPnMPHSuvFeN65nu12heX62sufHdw/cd1FJDgVL/8Akt2hdrT/R5jg6pfdh5r1PrdXz2Xa6zfY eLRefT779MGLhjawfSA4S3MF08gvIUbtBLg6vW2XXh8W+FpktbzTsz5mHu9A/Fgo22VtcUOXtw5+ PIBdr16pZevg+OoWaOneCmD6YVdcPmtYaUwYrPLtyTvW9O52gbe8fbUONlOq/Xz69mS9FxtJt1hk uNnGwiVsBGsH1FQHokI/jXzN66fNcit/knk5gClpXACraReRlSd/8SGuxS/CBsFvnw6LhYqY+dZT 3SVLv2gcW/bb+8tmfy3wSHohfwo7iXxul292cniFCSMmd5bvtfuLpRIwMvO3nj0C90qbLhzPoTkw rYOpd9m3lsx979RTgEuToYitq7StZ7bfjwoLh7t5TnIKWamRYM/sL9RUcF81a5vbOWyf4e1rK1Pf ztQuARPRWbH295H8smrUrerG87R+dn5/AlyRccxveXn9B1iDG+CUqChBTaf3+uVb650YsNTN6pXw Dqes2FYL68vTjlIj77CNZFiy1/WjIlL2z9ovLWRHxqPDbwqEazirIfvEIxKZFeCjDovQ3etNz77A j8ObFgfwvS4W5h7mL0Be2tWs/Z96Cr/Bz77wTq6++3TfnF87eX77hZ39aL5vLeLWgPSm55avfpDN 4tvHGKWxednS7Dlrv564LOi9j7bncYdTZDOtnQP1Oi4sYtYgsX2TeM5evM+X1aV270Rd1L62VHtj r6C+Tq3oauFJv1QLxirQMfU1lTfV0uHyKblPbhXXL1TrRzawlKQWDjPn+E/Vqizp5Dlr72dLTcjX 635tWK/my9fUHAwrFXGwCB0u5jEN2T+9+QD+8OMFP6ltFh43sVUbmTSRio2ZNIUljGpJzdnlaQ0a Wp9qF+zcxR6GBb9esr97F0fYhrv+/jM3BxV8aX67uMO8HVvveTsJLGJyMXTHvZHiduJ0PgHz+7wS 9uQWEgEOZ6HmQjK05pudjXphcQBv+eCSQi83g/Z488TOv+rNz9OlxFbnPu/uZ7hDmwvni3fG+dnO yWptZm3DVZOQtZ8qvNRXNeCjtfpO9vbD1nO7nS5yoFgpLBysXJvK1U/HtWHyNfvJE5StJEIF3tQo d0Eli/PSHpCCprz+3qu2qe+Es0M2l6xVbiMU9mZN+1Xyl7AWLwuFnY+dOvDqxQodFu30GuuUlnpI G3SxtvIxleKYZaTII8Q1Ln8/INFtAbbO1ozWPexuQu+Tq9zDtedlQKXVWcquPScuMYfuDjnjlFKL jRRQ9HRJT8/PN/PKzeEqP2z7pdudzuLttHV48PRLyKOwd6WBkXlvwcbzuKqtvZ3Oed6V4qFvw853 tLdXeOk+rRR25qtF/aLe/OVZPyb0MleQs/ts8aXX0C9nzE3rI6NNbyz3HkEW07Z+l2wva+hsGc5u wfYXurV0qHR3NpfY3MlOW9ghBbXVzF5Wmhu7rdnnFVedK8XXKpVycU0tLZ0Z9RWluHt7f9uAaeoa rg6APAJ0s1d4fep9IheeeUzlKJvnmX25W9JzK8lp2H1WbgAHV8DD6I23j45rW+dLfFZ/sEGYc9wR ir3WLzPPPZjkuzgwb+c98e6NXrzKFvHYAdeHRi//vfN1BD3/uREU32iUX2+UnfWW3NHljZMWLwqj wTLmN4rWUm21vLF8d2jAHrCPZmOuRhauwyxyml7PyLPJcYTwj4ZR1k8P90pYYkcjoQpNOp+2y3fV h/XSTfPLWrW/4gU736zZz7efVyC/FOdW7nwef9WuTh++CO6KR4XHwLILi931JmIRM8bx4mIt29g/ T3jcyfAuRjaF+d14YfbeXrnLfRjpm9QKN/vOktN+D0vbm49WFQSkzoYi1ueUsrS5/rb2vWk91e5m OE6JKwfzBbBj/wQVkTo3P3cqwMPUzRu9qb8v8HN1/jgDfISR08/ul5tu70VZDNCB/C7m7XJvugbS 3VMGS/QuoRRqvs8BJd5DvhErTaY1uc/v3Jwn5jDr7sqfhCavAWf+taw3tqfOCq8LTypaDI+Ye+Sa R8t+eYaddO9CW7u9eoO+VAvA4Ty97sYBaqiodaDi8gfSsS5CBY8rPAcra2szT5H81TL5l4R9nMjD uksswxoqduzy1h4UdjXXms1f2Q+z+Y+bNrRrL4043WfifQUt/L+3WESCosYOmm+x0xbysuzELBwy E83rGLtirroBT6tfzbdk57vWequXGt9iVIr3sXb1r2q7U0Xltb2hNN5nW6WParvU+KiGF1lu1lHY AhdtgYNmkCM09HC/8d6MoS5R59yzdrVSfa81at1mTK8036qxM6uQhHKaZCS8XtH8ILkOujk1neLd gPmnLp1IhBGGEpUGDXq57JYalVK70udrKz4cKR7Db+BYPcV2qdVicQFhDaI+7fuNcr2HXNbPmvVa mfq3L8CgXTVqyOHWby5ZESQQxGw2KjXUuP1KtdGtvdcYEMJqhwXTrTVw9/iKA3o2IIwo+ktCoFKE 94JDosI6R8YFbsN40GFGSBthhLlhEgKD/KpWyKySoC+93G6+lbpHpb9hdXoidv3eM+rVagVFQ0Z9 1sDxk6FjwRVc48jIgIIv3NgQNz7M72k3lgOHcgweG+qfX+RCjaI2qthsDe6q48POrfHAh00Uw2M1 /9Eg6Wq8Szwd+vJxs9F0393/AbqqvzX/ovhQ1Uz0ivtezmiDXi4Cnl2aGPb4brv0NxcVc1hrVAa/ hOvwfSu8bagysWlhQ3hR/Tgutb/Z2kiiMQsGQbFd+0GP33CRS1rYC6fv7wic7eaP3u7+o9n+5mE9 ROfPeyU3/ik9ABLlT6iv6jt0YXW6r7qwcN8cCGKj1A6nic7s+BU/6J2gsQgfRPIODH0Eign4RzxF FNoKa7vaRjgoOoHD4YtN7LV3raVC6ZvftHjJxHAY5Gl2WM0M7Z1RKnE3keHec0lsKOkTRzQorihs a9yvA8PTbZe6zTaKvYa91yy1SAR2reqEfF24wV6xHxT+9TRwx+Y3GbhPgsCibdsF4Pduqm8okDIi jfBbS+qANeEJpwxdeoj9ASa8+Nn7eWuUavVOX7iXl3GNGhYcyDZddap4iQERd4KVF857wAJVYzUU /dgtofmoxTqwMmK/vWq9Xo1VarEKycEK3wCL3Yx14IFS/a9SrNeIIdYrxk044uWqToA0LzDErjoo VPrXpzpabqvawI9AjT/NCvC5ZeAhYo1e868SKgxeqpP6ag2+ymX2fgXWPTwFg1Gv/etfpXa9iZ7s NeaBYQfGsESwBqXXS3+j+G7c1H1k36x9NGJNVkx9npbQhNoaJTQEVVrx//v/xDplGJtOudbrNpOR Wcxyu9Zyp8hJaovawnVkwAJAUlu1LHKq2QDaGDHWP4zHphtlA0mf40gvfVz6UGXQZMV/D/2iR3Ry ZF24R+iG6QxnJNHTd1WWW612kk9KI6Sdjj6JvoQclV1DG1rybYAogJ6ka5Q+lAsvsA== zsshvioCzGa/0RQp4f3nUtj4lYYeIQkxBxX02cRbFk0OkE+H96EthLgHtY0v0n9GOt06bV+rFcKR oUF5q3V/SojxEShuNq8GtfRTjMPvKxS3EWdmCHmm3k4COaRYyYcPSpft8cF9hV6i4upuOhVNCewA N7d+wEPlUeA5gzfoOcLjhbWy9fHzTacE0YwQaKEHqw2UcjNk5srtSrJTbtVDhVH0ULP9kQwDMqqt gzKP96VA6K8Oto2wskh1KPC7FL51o9kvYyLcRYlVQuQAUm+50QnDGzzzXq+1Ppvtf0UYDJpN12+P QY/gdAkDW+8kWXirl8ohuk5UYpdjTAYO718DkU6g1uAZRB+Yo2l9QzoIoBUCqQ1BJ1CDbq1bj15u PVzzIz7srOJAGLbJtjgAPGQhtQeWhiuO3Lp2OE0jlSLm4c2RW4Okf/wsEAakOhy0jaBHkX67FipK vje6yUq91X5vNsIWC3nMJav+GzCq8geJaV5kBNXc6b11wmQzscT2AFWH+LQDigjPhu/vrU6yUf0A 7PwVxkX+s5UUeMWAkupKGQs+oQ8BBeviDPZ0Pnza30kijrVR7YTLQfBc57NUqbarIVsmrtGTKibg OWAOxHoDnnvD2TJo0wJGovpXtR5GOdqVdsfLCvkNe7NV6YUMFS6GsfdhzcZFdUKmDz9QDllNpKpe oxy8kqAMtj9h1f+Awgbu1m554fYH9BzsZGEaDXjkw8us+WuE4Mm298ngnREaVy+1Qrf3nyRLDtfs fjJuZ2CJoTsaHr5So9EM29/dess/f38HKy3gQbQ31hohIwzPuARyQIoez2A2QnUyjnzRdNm7BQub GKmIXopV2kCd2gE1IM7UrSLYHDq4GRUs+3Pb6ILdg/2yGtu/PI2ZzVIXpL/C6e6FrmaDBDo05M1w PtXpcKlDRAhHGuUsrzp7nrO9OimILq93b0seCwl8h2wzRT5jrPN0sfrTcm283Cs43RZRIYnwQPca IFU7DJjs3rCKllOQUwVK0HcI7BW5k3afxqfzFPuzi8Kts493304cNcsiL+PWghRISFbutEpetQV9 mdipunzDuXH4Z/e0cVZirEXKffWsXS3XhFxiXpnfPaVJPOgHzRo5AAgVz+5JWLfHf+ObXCmxlbG+ 09unr1tyZe5yB/+5ld+b/XJvpIxcKqfdaz/3ONkI9rDgXtt7qZrt9lapsVu//nozXo/PdX25oTyt byvXOWum0rOluGXuPT4pS3q2kZkyF8/TndQsijF6W5VXE8dqeuN8bT21Xewa1nt+93tv5mKzZL3L d1vOXTWxeaF9Ts23ir9Ty59fe1OJcjI+tfxSeZhaVguXUwt7HegL+uIumTrKTyW2NltxWstnN7XV WtxoQZMPfmiTSxcG/e31YBv3JbnayfzCb5etvkegf51coVFUF/J3s1IcxknB3Th1W9Z+7HzkoOZc L7G1OzWX7qidY1Zsbjd1sz7zDn/u1uHdO4t1/KjTbq937ttP62en8mr6cgG3FVcKtZBqd5XH9O3n 0bJvpU8fxl5gpZr6tToVVOlb+3k1cQO1iNXSSk+MxZmrVv3Ir9LOzLNmBFW6t3WebVx7KkW14GrT i7eJ9fLpsV+l7d7r2sJSfGPq1a9SuSBvbwRUqs3M5tbf8xjJPn1N3z3KhaJx7tvT6UJrLX5aO77w rXR3pnnkqZSuF1zt/MFR+jhogK/aj2X1AFW62De8u9M3qXimOw+vpZt9c7o2C7NPqz2bn/fMarqY 3a3jSmE1vdlipU/tp+u3i4BK114yV5WPVbdSKc5V+7xSPAmsNLv6fjnvX+nG1GK7s5bo+Fd6ln2G Wih++/ramd+6UwIqzXwuzhvVXf9K04tPiY31H66nOEmcO6ulzMyv1jv2q1QuHJ4bAZVqM/FMLrMd UOndC3IlbBaLvn2d3p3dnDuqfl75Vrp7nrsOGt69uZWp5Cep1H78LgDG+AGeXuzM7yzjAV7qq3Tv 5Sfzu9SSodJsy1vp0f7JE630bmXB01Mpns0kk/dutUJf7w356Oc861/p/nQvd3RXzvlWelqv7rmV wryI1R4mf1vLAZU+JOTLo9+ef6WHqeejQmF7yq9SmJfifm0/sK+Xx1rxNahSS76Wn/P+lR6tzBQr L0vruFIp7u3r9fNmJ7DS64XqSyuo0mP5ZmXb8KtUikO1duJmJ9c2fQf4frn4HFjp13TxyAio9FGT n0qvCVwpwpinrydXte+1+Mmyb6XPz7cvgZU2q9sLH36VSnFU7bH8ah9Z/gNcuFKmb7vn+36Vttsn KzO00rfUomfRJPJLdgpXKsWV0nR3V6RKa+1eypRRpct9lZ5uLvw+27c7UOlm21NpYq15t0wr/c4v uZUCTUbVxt8fpslWrhp3yr5IIC5k++RhF1W60r+nnqzMNlLrZ1Cp2fUOr21/JXGlaF4W7GUPKZyv JChVSs2umYciKawmNnMnD6jS1f5Ktfj05t3eHlR6MOVWCrVgdko1Tj9JX7fXzpOeAf5qbtrfpNLt q6MjcXhhYr8+m3hPBRbqzOTvqt2fKXWt8Ua5i/77vbmpVOWn5X83vQiLZitTDbrbBjAc19y7IrVM 32/KRzspFd/vJ+H32/LRxXY66K4uH1UONb+7GMn3pnw8dZMLetuWT8v7F0F3X+RL87sbcPdhWb68 npmiI+ZzPykXd44Xgu5m5Kup36T/3Y0pud2bytK7/ftL+mFPvr6a2iT3vQsp/XAgX3+sbAfdPZJv ZvO63108Yg8n8o1mm0Fvn8n3M9mnoLtf8tPt/VLA3ces/PT1vsxGrP9+Xn6+Wk8F3d2RX7SXfMDd l7aSXDrMuHc9I1ZaUPKv6nHA228zijH3XAi6e6kcnk8fBI5YuamcfKu1gLcri8rd1+Gc/93Mc6O4 vtG997+rds6m5hcPTuiIqfNbC3vifX0qubezRe56aZvarU9tpA++uLvmytIFL2EtVIoJo9k5YRSG SGDp5QtEdwxAXtXwEy6p+LitxHdWu/NmobidvbPuC3dF697eSMJ3csE0kmXTNFYPE7x89hqvk76k CCF16k5szaTmsbCH6RiSZB5c2rZ6nGvMy6ubtz20Nh6A/L1vOJLozGpt820R1tC03cmdr515eMv2 tDq/ebZCNgokyXB0nK8084kkmaZ/pem7W79KYRfD1U4XKiId5yvFkkxApcC8giRTCqr0FVfqIlno 6/TuSp6rtDI3N+NWivl7p9KUZ3gRd7/OKt2t40rRvJABnl7g+5q+nHUrBYlPSQZWivn7gEq1GcTd P7q8Ja3W6etDYKUwvD9qYKWYu/dUiiU+Wi3i7ytBlVaDK82dnN8GV4p4Bo639A4w4hpegio974PS zNoyrR7/RnG+5jP7/k+uRyox/XgU+JwUF55cPCNPEnqhHmmi3kdYuIUeyNLzD6argsE8GiEubGRT jMLAizvK8pWy7Hw88OI4jDca1Ba/ml4TDVTEhdOGcxDI5pqocaar/3KrR6noMnHygSb0hmdzaQWn TjcsJN0V8CMe7dJWfvfqDP6ci9OP0i3HEZPZP3eXLjz+YCn2U2eXI3Bck7fsOP1YPm6S0SF8OSPH bg8AgSYZQG723ZE3Vj+rVhx9ACS35X23UX5Nch4JaNKyjTWK8B8bUcVP4YcHvRc46O6Q4w/aPyw3 s/45NJn08GyKh4h///DHxYD5Az742J0/pIXzm8GX3hJrPJFa/PrXdCZ5wPxJ8YEzuJ4YcrD6i0J9 oYUtRygsEth371qecWc6paGRJb8r03eDVg4bd6yDDRn5QTCNtnLwjrwujzFYIulJNvtIz91yC9YL X4HT/yFnw0bd2OUK4JT5MIASozuvi3MYl/5jd7fcDW8Npuz4g44dVoz6rkr78XrKj3AHr0oiuvh0 bVs5PBJWpdi5aF1bmYkw0HL1LDmHrQVUQ9I3yrtnP4N7NUd65Q/2Z1uudr5vqO7Cb7YizlVL3CFS /jjfPZwjGLvwJS72o7U61MAEDMtLm9Gx/oHZkavdq1WKHQfJRELxLeytE1gUzP7AwsR1p7T61t2b +hveZynquisgNdF+4LpDNGseeL3jZYcmLbiw4LWj8HipoD71jMNBE4o+aOOJorkfG9A/lwYSmiwU tmws0EbxLUsEALZUkN/N5ENAJ0/PMfuFUzfyXFjYlKR/+6bkO9cOp7R4xilv6Ud83MehV7edIIYg Asd4yu1iu1666t2YwuZXGMXKrlKand8TOFgf/ikS9/Sdnxo0WJtvC7hJvI3Pr1GqcZ8/8N8rndYM bhLi+r7zM4GNcveNaPP3Pes3f4Tnx2R2JnwGufkTWboI88dRfu9gydXSy9VkwIDo2Ff1ejKFuRuK pyigycMX9j7/ezuxERvA1w03Yu9rC/cTGjEPRRt2xKhei+lhulu/HsFVNW6fA4kQ8JZDcMcfe9FF QWGD5ujYHrKh7EVksH05BejQ9/SEVuUesnIcDiMoLwZi7GMvNbt+eDT86PCtYTTC4WBHGJ2XZFRR IaQvg4hCpIbIrpQ0UGoJbMgAKsAaEsJboqaokcWn4IbAoqfeHRFkQ6hq7dd/1+xudUJ2O0CJ1C8b YsOj6EbE3lnBTXLV2dCar32gAiU7HIuSUIGg1uDG7qnTiKDUcInHomcAqacKbpSH8RgkDwc3qTsz QN6PSgD2hyIAhIfx9tDtX3QCENY/Ka4+dRPz4ww6h4MgTsHZX7yoDeDltxesrrJkfBWG6p8UD5jB z+gLMkwmB4ln9/FXlPjGGKzQfd3RXUQbrPAlHggGSpPFJZ7sX+I/B+ISDxDSArRLvIYkNbs2PT2m LuHnQNR1pVyPu2GFim3lcC6KIsDRMiIKE7CUoGvK/NhdS21fF485G1+wgiNwC0rN5n/lCL2SwhUc B1DLdXPsDiEOVtxVh9f7bCsHPZGee4dFijowmeE0j6K+hqP8B6EaGz9SIGgaRFZ6e+2s67LSVGud 2r6KR1AmDmalW4d+OjNxvQweu+0rJVDXFbwN+moUD70boT8ieE44sGt4GxxzvWxfbS5GwLkUD0N6 69C7/Y2A87XzKaSB53e+0ToUT0fS9YUSgEO8341DAPC+DwPj2ep8B2YwL4tGR9zoApEciZedh5pn 5jlTLPxmwncLvspuTuKLuO5uL8PtE36IcHgYd853z3789rtRVGioKKRtHbxeIuhboTCRBx2JJqNF o4yjiyfzguZtcWFcnT4uZcmvFEqThyknMXxrBG9bVs7yRHq14pYiypXRdj6xsGQk7iKapWJxsZ/f vC1GtTCA/DJI7wWFjbvduHIlatnui69zRrCNL3AoK3PqaqR54c3cYRQNRJfMooeiwXdrHBvn9R8b iqJdRaVoUjzEigds5WQoGhoxVNiELEhQlB9FG5JTQnrp1ATWPjLE+9Kioda+x/chDGMDy1mJ2hok I4eUE4V8DG7NqseLQDC9fDYTmwxF/ryAOGGJQAkaa6440+GqL5dCHb3oUkESz5LXqQ== C74LJuXD+VyZcGM/AhHycmYe2+vt9TiGXG9RDpENt+9H4OpRYYL3g19R0kBngd27Fpamx2GlifS6 LLo1jVAOLiWQsZfiw5UT0QXCUwqvhyHlhIrUUVvjcTzysfCGseTewiIqsqRIu2Fpejfh3Q1L00cR ZJoZzo4cuBveDM/f94MUaRQnxt8//gbuhSLGouyGj7/h/L0fQMQYKzyrynQUncsAel+aPp7ILgbl RN59wnYxKGdsDyJcihzJF25wOUr4XijFI++GMGErYQ4SIXshk169u+HawnLfbri2EGUi/PbCPk4J mvLQjeSH5bphBPZ+bUHxWBL5lgmMhbsgg6xvICqEr21pmNUNhY3G6fppFV7akVZ3BLysLWghIzaU cAkzeT2Ay5QG209vb0NV11E88zhNL25UsIoiAqvp2ZZU4/ZuBW9LQi1e34mRhbQ7z7bk8R2lvmuh nmunQrs+A9XQ3FCKXmqBYDGTA2Sj6N62qLBIW0uQQ71YlDYpDYlq3E3PRZhJbt8P8kJEcxnJ+ZbA IlBGfut4vaWCEBGtSX1qYQ7JEZZXi18W8Ju8WnjxhTOdAZJs5fRTiu/Wr0svOL9KIT+781wobp3Z w8fQhUfQ0ajnsWPoWOP9I+jYiI0bQxceQUejnseOofOvlEXQuRLfeDF04RF0NFpw7Bi68Ag6R34Z M4YuERpB50QLjhlDFzi8OIIuOFpwuBi68OdQDO8kYujCI+h8I7lGiKEL94fm7JVjxdB5HJI9+7VH D/NgRXJ24fi64DigX88uFr1R3iYN8J8CXof59Q2ImBJVTKP5r2JL4oMl8r+DxylItj2b9/Ul5608 UcdJVDEFj5M7Sr42catvo+9zLZoRRK6wIDzkrCPo+UfHUzMcmpJvDwP61xc5F7V/ok4JNSoiOAc2 iTflED3/qIM+SHMVul6iBM0NYpsFuAZ62z7bI2oP3fqg6addMbo2mjOIzxzYEWxzUkS18bM9vL7K 7RqLFbUfH2fG8XmgwW4BriCiJ+TgYLfRXEE8MYnW2AYXewDPH31gQlxB/MWQECRHiNAKkWkEFyy0 Kt/UpoeJGKwsjkYe3tTeIA1J1DghaNLLAAdKKWqQaSHUlj1Acdan6UVaSDWamiRAcSaqDRGnu0y8 O1z9/S4aGHtsdoKEpjmLiyGZV3NGjwNDcXzdQZzgEHF8z73wbXKYOL5BvNcSr7QKblLVGwTDy/sR ggu5JoX4wJ9eeAI0B8XxDfKBHyaOL9iYPNT67Ndg+vKW0QsbEGDjKQpre4ILW51MJ5HmavN6QJaA YTrpa70YdcQGePsPN2KpcTrpKn4Jb0mSQAquTh97g6JnIvGye1jnGxox5C72oIiwAdFyrIBgu9je YIKDV7cykDp/7KHIlMkIe93t4LXNWRMWAlWoqIi5qKJ+EIXpbnXkAclRnJkOGpPwQDkpHmmSPZaR EIknwCMChbgFmxujjedWJ1yEkyINx8DQ2KVwAxalMKhD6agdGuTKv2C1PVwkQnLnozuE7BsY6WMH h8D3ITk8/Gg4CdphIL221/3JqHdwk9z17sOPDbPiYZyiLFfO/hISZjeUeseH0SYc7P6w6p0hI+M8 0U+DGsWaNJRGJt10cnb5NGo0PPU3yZtTZfRxGhz3GtIoUSOz2fVqZFBIUgSNDIexIBXiz8HYGhkp nppdW5gNl1QjcugHgRoZj+4igkbm52ACHkTQtfTC2GFoHo3MSPmUcBjaMBoZ/4hUFIY2goOwd5oc jUywFi7SwEQMznH9lILCc2B0/MJzhgrO4TS9q/3McutwUBxqJGb5EK39cT33ttfOB3huSZGlltT2 VTZC6KivSse7Ix9G8J0d3LVFT9cEv4toCD0cPm+YX05IFFcX7LoQPa7OJbI+uaGix9VF9mUMlpIO I4aQBISOCmOD/JODlaADg+s8jnpAI+e9dlT4bnGQj2I0W9mgeLiI+frGjIcTbUksIm7S8XAjY2yo eLgQD9UJxsNNwEM1QjxclIjU8ePhuGjBKNEsI8bD+VPLScfDuVFpw4VqDBcPF56HZFLxcGxexIi4 4PkdLR6O1uKJiBvZrLN73ZxAZD3a3gb02Y+P8OctUWGRyEcEn0goSp2Ed/qSfhnJYhXKqKLQwzHT kGL5BZczdloLXIooPAfFiQ8uZ5QYe28k11V0U95AiuZmAPbPpjW8xzMw4ltL3kAc+9FsDfIiiLYM 76IkhZUixDINyJoSgZ1nNgsoLNgWOqRr8nyftDgCN24O79jgx42jkMHxswHjUugiHEd6xeX4L8Oh cnfgcsZLdYFLwRibDGuPm/Tmm0wxOBNFiF4a9a/Pqwh9FyRQ83nhokSklqYvIoQvDKJjN5OLSL2Z ZETqzWQiUh9/JxKRqkyvTCAiFUqZSEQqKmcSEamonPEjUlH0migeihLf4FhuzwIJSprqcTIKiubw LsOXdv8yfGlH1WsN8LadUCics4uxYLh/SyjcGLmghwiFkwYL7hMIheNGTJuMXOkXChcmVwYQrhFC 4ZyzBf0bNaFQOCIlccFwXC0DQ+EiMoZvHehLMGkaLiM8CnwSHSREy8jwcXXf4WKR12KDuXFfmw0q 7Dfc/BPVJ9DE9v1ueKq3qDzMnTcReYAtKUIeWxzCFiG/mpAm2Cca3UyuhphwhtscoEko07fgdTOS D6rLsBs4s4dPfY7HHT7EcuFiNovOBL+cAn7paGolt/IytXS4rkwtWxc3U8vXz5foWPDi1FJR19Bv Z+g5c2rl8CUjr95+Z+lmtNn85hu8N/tFdUpisNtCSLDb2arMj7EQ7NaZn23yB5yKEXaZz4W32Yvv gLizxcewYLfnlcBK5YKhnQqekGI0lnC0mDfY7Tks2G0q41cpi7Db3W3fOX31RmOFxJ1tTD2FBICd 71wInJIn2G29fnUSUGnmc2nnZqkVFAB2FxJhBwP8wc+qN9jtaS84wi7+vXr9FlRpKTTCbldeC6y0 3fk4mAmsdOpxRrsKPI9vajnsaLz9Oc+souW6gqvHv7FIvF7F7zlMk8UnT2eqUUqcPt2civBcu/fy HRfy9KJe97GiTFkMby8veLbTMJGKd7kNyqXm4WDP5n68bvJefWqYz8aAc6zEMJdRDv2iUhI0auc3 aqNCmxR8Rouvz9VYJ8n5cb8+2RvGPElOmDp6jlzwSXkRx2l+ejifq5Bz0QaeOCL49YWe+zb+IXJO Ud4j5Pq48ag4GHzYCNc/4qsQ2KiB7uHRmhTlnJEBg+40KdgvfMj1Ir8fpR8jNYnXebZErm8S0XSB 1oSJRtP5cdtEbznJaDo/hZefpne8aDq/WLrgzJOjRtP5OYAE+CiOEU0ndIjG0g3KdTN8NN3wWutR oulCkDzBaDq/WDqsVZhoNJ3fDDgUZmLRdH5ys28E91jRdP3tOg6zvY4YTecXSzcgZmSEaLo+5oY7 IXdy0XR+s9sn748dTccPFuOig85LGj2azkUJr0+edDSd3/y5PiSTiqbzi6Xz4ZTGjKbzK8rJCTmx aLoQC+8Eo+n8YunGGrGBwTnDjNhw0XQDRmxC0XR+sXR4F5toNJ3fuuJOyptQNF2wl9oko+n8Yul8 /JTGjKbzi/3ykV7HjKbzi6XzsyWNF03nN0NeX+vxo+n8YulC5MrA4YgcfhNkeZ9ANJ1fLF2UHERB 5gPUpGgCoNcPVvSoXOv0Be8sWANZjP4wOj8p6Wt/0DGVUQOfGLWIyF0McV6dH8MThbsY7rw6P/cJ /jy+gdxFtHHyOdnW6wcbcZw+B27fAgQC85Cgc+8CTqIduklenj8SLn2bFHoErRgpPKhRQ4XGLgZS GNSoYFeK4cbJm6sz2g4iSkSp2fz3qigRHXizi/TbK6OpwUY85s4zYv4H3Y3AkovH3EU8Z2TMY+7C dRf0oLtxlDH4mLvx/ZOjHHMXwT8ZBdKNecwdojADD7qLODDBYUoBJ00EuCmMesyds1eGHXTnBFWF H3MXMS8c7Awz44LhcIJxFq3DoSKLsEYxKJp57XzsWJ5DvHUG+ipED6SL4Hk4yNcazfnYEbA4ijOa 92/YwkXH04l75ShRafhsugHEOsyJTDgjFUYnO4YTmSeeaMEVlLlzrC4Dt7qhNjqkmvcGdo3inQ4N TQ/QDUf3hsI8/6S8oXD0/7ie4ZeBjlBDRj6Oc8akI+/jcsaNqcWl+Gx5IrWMWk7YCox+TuKEDpok RQGvOphaRg2s/aou9AXWflUH2pKiav1QYQOCV6QhAr++qkuRiBmnLvKcAMIP5mUjUowyL0iGelBf N4klUfShvm5OJDeA6fqNjBWZcjXJEwyvJnmC4dX4CQLwSXk+/PsIkY+J4QMZ+r06UTljG3dJKeNn CSDlRGTiifwSXE6Iy9AQvqr0nMSooQzRAhnuWp5FSPmxCS3DoBPuhouvHPWEu4DYt0mdW01PuBuT G494wl2UyMfxT7ijJ+WNvwxDT7gb9qS80WTpvpPyBnnuDYwP4YpyUuSEZjmLGFg7+IQ7X6+biPET j79R3ZvC6BgwdwNC0qOyOST2LZLkGCWwVpn2PSV9yLOeYbyrEWIvIkQ+TiCw9sajzRrR5wqXE120 DtSN43LGD6y9GZAXbsj49r5E1aKP4ggnevU716BQqavgSGApPsQyfGmPGMTku4vdBoYxjRDENL/5 NiWFBwtF9bBBhX0GR6hzWc2jCO5Q2HeEVRmotxRHLFR6H0KufGljAT6yXBlAUNcWkhGCmHCW5ghh TNCoEPl7EGOI6JiXNXzrs7ji78ZgDD0n5VV6QVM77HGPZpIjBX2WkWFjXN86ESw2zBduUIwrFDZG aipPnqu3zqSOezST2ZARGy7G1bh9iXI2hRQe43o3fIxrSA4i1KiI5xiEbQ7u6t3KHDfc+gQfEjoH R512W+3O0iC9g/wdiu0roo+dqUQ5eYDC+kwc25dMHeVnnamb8zSO/vbSnsZr34mdUuJtfn2K57BN q2v5gGPupucC4+HavdfkiiiJiwfdbagfwSfOhZytl76791RKZt8J2PpSAiuVC+8nl4GVzikHL+Wg SitSPOwcNuOSq1QMTevMfFaD4uFye1u/sz9OT5HWWoxy9A3DYwO8GXbiXF4OivzTYMRmL37U56Aw vJCAw+lCJx1c6e7y67VbKVr7QrXz1YXsZ1CUYzKs0qP5wEqleLtztTUV2NepnaeVojCr1TyrHv9G J2LRvK/8BD6H6Rh78rn30xhYojbz+2zfng58LvNJcQebJFn7KEjmUfewnUwjs/DTt3UWeu1gcuRs dO5u13eigcitcvYgRkEfrOHdJoNOMTubLwSomHw1JCEH0CGeN/QUs6infA10xJTiEUxdMGsF33w8 UTldjoexxnatcsfJ41gV5gkZPk4+rlWBBsEBUWlLgeLVsFFpA3w0h8BToJfWkL49qH8DfdL7++cn WaAQtzA/rWGaxEXXjjfoEby0oq6X9ZBT6Ulr+n1jvRlCfNJhPtsT0THb/WemjKCDvQ== W+5GUG5FOvsJGnod7gozQPzlKIw9ASvP3YqvQ81w+jF7RL2WRz+GogDHNlmjGEBRk+JDLSNFAUZK UuoMi+9eaU8uZaNNc92MUZjoNKJ4bTso6u53sLU6CoUpFSYnI7+lpsdRBgvnJJamjyeW7AoGy6vn HzbzkEen8p1reRwIPApG/9NLo0e3BfNtUXJEiKGAg1ziB3mtOnbk79yAPAdDhEW9TXmYN+/5laI2 Nji6LTjPQSRuXGjUZ6Cz/nDH6mHfUYHejxHKGXjmjZsTMnIoZ9sTqBMBDEER3KiwQWJK9HahDCHh h24PUVgg4kcZsYGxPMOM2Ig5R/xHbOAZx9ELCzzcTYgUZoX58od+UYBRYwCl+GD+MDgKMGoMID5V duQowKgxgETPP2oU4Gj65GGjAKPGAPqfwB5QxMgn6rneUKNEAfbNUEAMIOepMkIUYNTxdPbKkaIA o8YABsnI0aIAB4ujwda38CjA0yv/XoUdysfikf+9h/L5Izk8amv4Q9R8JYuJH8oXqoWLGDg8+FA+ KfI4jXMoH8f1/RsP5RuohZvIoXyhMSMTO5TPJ6v5v+FQvoB84yHjVA1cvTukNYNzQ418rl+E3FAT ONcv/FS/4XJDBZ/rN3xuqFHO9evvGn+q34h+Sn3n+oVrhYLOrxz2XL+gqLvU+LmhDqI6TA2Kr5xM LASNSRz7XD/nDd9T/dCITeJcv0mcLzb4XL9wnYMnkmvkc/28XRPF+9HOr+w/128EveUI5/r145M/ 1S/8dIbo5/qN7KE61Ll+4af6DfCFi3yuX3jEjIPkMc/1GxQxNJlz/aJHpU0o3N3nVL8gPX+EhDfC uX6je0EPc65f+Kl+EzqPb2nQ7E/mXL/I5/GNda6fU4rvqX59FqsRz/ULd3Pznpsw6rl+4ZJasK/1 cOf6hYanFCOfZDTgXD8ylEGn+vVbEkc7148F7vmf6uerhwl1uPY/12+EqLQRzvULCgVTovGWEc/1 m8Daj3CuXzgb4J7HN37cQ/CpfsOfxzdKSgG/8/jGj3vwnurntfGNeq6fr+3KsXBKUXn5Aef6BXWc LEJ3FxvvXL8oUWnjn+vnxG/5rpxQOjbEuX4jcOMjnOvnAzTuVL+xz+OLdLhmhPP4xs7sQc/jm8C5 fuH6Ly4yZaxz/UbKqDP0uX7hArU3w+Go5/qFn+rnI72OdK5fOJsjTehcv4FZmyZyrl/4qX7Dnsc3 mjar/zy+cemv36l+o/hc+ZzrNyAYHmNsAuf6hXvnOGcMjXmuX6hey8QW3gmc6+eEj/lKonQXG/tc v3CxHc/LBM71CxfbqfwyqZingFP9RpEr/c71C5YrgzTwo5zrF36qX9Rs89ECYoNO9RsUKxr1XL/w gFjiETH+uX7hAbH+/Njw5/oFB8SiU/2iaBQjBcSGnuo3HA8TfK5fuHHBLzvQKOf69a1P4VS/Af6W kc/1C0cEyaAbMVdKtc/OiL8L3hzovsDOSwpW7b5fra16VbvwXYgnq4/jvXC+mCdwUcBTy6PDgsk5 M91lLyi8Eg1+CO5WFrhIYeRt2zDpiLLC6IsvVbPd3rqo7ax2Nw50JXdzqc5vTVv4ERRPtZconpXa U/HHxNwUUgRNzT/vfU4lN7/0xPrWL4rk2li/uEsUa99N2ba/VmX7a2VNLhyeW3KhWTuSd8/z/19x 36Ed1bFt+wX9D02QkZDUqhzICoAwIoNAYIKQRDhGElbA94w33v32t+ZctbtbARtf+75zzjDqvXrv 2hVWmCtU9axZuXP/rVnZ/vDRPPiy9ck8ueereXrn86p5tv/pg1k1B1/M6purh+b57OqkeTnzbMq8 fn/2gXnz5sUH8/axPxDN/86/uGDe3Zt8tLe3d3Nub/+X3ct7h3Znde/wbZncv5B/AiB4fICdnRdm P28uP1i+Xz/cWH398uOZiz9NvHh4vlz+sjDx8Mntn3/69OvE2bN17v7kuS8bE3dC/enB+3+9WLpy sf0e397huzOHM1/D/XdcEt32Nn/r6dMJc35rU2gPd0/VIW1duLt0fx/bSVfOzC6shbGfgNQddtOX v1xa+M5kXQoyHYe/mXdX3kzt7d2fnf6DkYapF9NX/IUb5tbC3QVz68PGz+b2g/vb++fepPfcX1nP tN8tvPbbzemr5f4rM3fr7RnsSXxkbq7mt/yZPzP3YGvmmDQdFZ8jP9n3bnJ7FG1FhGQ4uJH9GZ+J g5/OTH3+cu3MjLu1cmZ6Y/bRmemXk0tnzi+VB9iA+3P7Sc15//DM7NyF+/jiJebuBX5X8+aZmXsv lnsTZy7+dk5Gvf5bafytP6XppqFBz9uLi/by4uf1OYvxvb1xeeXMHpfp1k13r8inJ1/tzMeP1+TT 6m908M3cr2cHfNafr9sHxkwO5lQnn4d6nGyf/rV1UZ5ZntL3fbjw2zQup9vlpclZXM62y5Uwp4+J Pji4ufHrb9XMxTtmfnt3ZX/+7vPnr0UfHIi8tI5ey1Ojr8ZHcO3q9NgX6+cXr3ZfLM6OvnALL59d 7764Mzf6QjDYx/nRWx7Y4VdvZPEmvpi521cvjmjjb769ODP2xdibb98ZyGzPXBSj9su0tHJuyr3e /9chVv/2Yyuf3xt//dmZQzP38Mao7beEi0K7NYNnLgre3lqgXoFSLKJVV4oopgeysA8fzCHGPEMT LJfPLJ/g4uAtD1/5uXtPfvXy9FOZ/nNXzuH7aXnBYNvMra2MJuZd95Z3svDp5ezS3PqVny593Lnw 7NaVm+FfY8pTVevtF4+G3uuRgH1nh1dqpzz/Yountce45UrtNCxbnPq5PKrl9q0bF7aeLt3ZbD9T KaN6YTv+feqml2YmDm9NLt+546a+vD3X2GttPQwHvk724oJhxt4/mMUUzci05h25fDbXGP/9C2Pf r9yZkk+vrOLfufdv3LV6x8lavV/3/HTUAi6OmzzhofHanjGp/KsK4JehArgO2ZdPX8LKMQUgsg8V 0BTA8d/SnfsJg5xsnoUo6zagB1sXKYtieJ9c5DxgX+8XnGWO3/mMhg34689vrt96O/UKFREi9gOI 86T+5ObW/q8odl6+2KTbnp1zS7OvpkWmr8506wLZF+ggtMUBoMoL9arln6mGmOOtuRGs6h3JB4lu 6JgF8Y4u6CEyGyY5KrM5OXmpk+NLHMakvfnm0pWOdoNyBXTx+De5vDUgM/QmVEGIVP668OXszi07 83XRLP18Zdec1AfihOsiilKwggygI+9Pc/3G0RrRCrDscInh8V39OoIYFJV4PMR2XDY67KHA4uqT m5tmc2JnaW/v6sbR34UFKLly93nbLcjb3eLT92EExUjDD/3e65p4N3G8icP3D38TM1gO9Wecf3rp fh6z16Sdubq2zWooQof9Z0cOQwD/hrWH403E3Vsj8wczqXvjqRSFGe47BOnBFsvT+DQY0uYY51eq sN/ME1myg8k9lWSxPy/HzG035k8XJ66ceTd2PkHDI/pbzr+IYgq35Qs7GA2cPMZd7YoQ3ozBgCtL +086BMCjEOq5U37XuPvNWZnU/bO3NuemR7/lPDzloFV0a0Ji+DPQwxMPXh09UuLi2IBe7fn33YAu HTla4l347Viv2289nzjb4uDab7rmh+9u+OnRDzDLZO2O/QSbCMBg1MDwDAgcx1DPtoFvPLjXo744 e+J4iOl7g+/0my9tCzE1ef3V69MWQgZ043A0oCN47IcXAqdIdk38Mt7A5e3HwwZejBogtjzWBA+O /BvMoBHv8T4M1+U7vTjZxNu9vzQMNnAkNt4OOfo7w/h0eFoD9F+2j/7i8i/fmbFjbPjw7XGGHY3q yH1b+0MNo8ettDsF+F95eOTO3TPfbfEHBgmpfLh3dviq7bFXLd9b2zjiIP4y5Ky1X8a71H5t/Guz hQfXp6HZ00g7NZ1stg5uFArfyKCMUIqgx+1faFrFOq1FNcYLL97SYs02Mxl/2cflXGe+ly8MP6Hu AhYbdvjiUTtMszzTLhcHNNqD4YNz0zdXXyyjN7/466tfFzpbGSbHgPjQsGJdxLSO4e2j9nUMb1+4 vnKt+6JZ2mZnD9/c6L5YGYP7o9cTJ1+aoo0YN+q3b0yP4e2xN9++NQK0b7jxVGgrcw3c3H5ggGGm /flLAHS3nzmCZUZHGwIUxLw4TUTd8N/DO7OdXTy82GzXw5WBgu4LV1+IcD18bKAyAec3cflCm/XX n+6oq+Smzg+mehMNR67d4VpOjzsQaw8GfItfKL5c3noz9WHxU7r1YOFLOVfGUAEXFohSmzqexh3l XqXFx3P/TItde8/MqL3eRHp29dLi/LOfLr9b/JTvfp1/Or/zXBH6wquLb5R/pzZefO5cyqd+yF7v xtnr/crMGBBXr+T94wGRZ28CO9inFI2vn11VXD5CpuLJ+WWgqzVhpSt3ul+dv7czwtvEGc3VnS8X GthQjn+429WQCDqe7gTtzb66uJ/PPvxp6OLud17pQCPGzT+LYYoFPtLNG7i8NN0hQRGzoTTJE6uG si/Shpg3KrAHxK329pvBSxW+0+Oky4Yx0WHm8qfui7uTlGM7Y3bedXN8l+ddkLo48X5InWm0ZxYS f3cwpmtmPl6d6/DtijHx/V2klu7asVtuL/yWhFU+PgC+FV2ytjsgUjo3pacDjcIfu/99rVesy/3i YuzPPT78srX3YO/zx887/Zne5d7c/B1rn+1s7t7a29p6uvVfB0u7G4fbWzsH/Uv9ufkni3fulLi0 tbG7udWfOfJbSWMxzSMROHJiepm2X16ZfXdrNz/3ZvNkfG595/aX1X+9X3h379H8/MyOfX35ul0t S+c2D8VDWlpc/uU1Bf2UCCsl5LJI8cHC0od6+9flc4+vri99MGvXxuSH2PbC16e/wfFaxiFo4na9 3XwFp+3Jmcnl/ZkeCWtqKRT5H01GIMoyCnseS/IRrF9LE08mbtXzZ58uvv753PJ0jV8fLry7m5/d +PD06+Wbzxfu/dzD94tv55fTu/vzy/7To6Xr00+eyle/rd6YO7i4JF+cec8wzlHhmLTTv2YcfrX2 pmOYezujQMRIaMVAfmkrP9NZobW9ptIOJsFabw+anhUx6T7ZScQ/JpsyjZmSi+OA73ylQ8hLkZBX e/QFhxIp7t/QVzTjHHvh0zBMdXdq/It6sD78Ynr8i5XpjeEXs6MvEBd/k7eGX82NP/N18ePwi2PS cFEFYYz2bHIobPdnxr/4GDZ7E8OvBlS2oiFuGNVIy7O3gIfvWxXQ5euPcflovO33H6cxs4+aDG+c z5ea3hbvWQPUG7NXLeXl0Rz9D7tx/Q6m7VFrduPRMzbrx6I/y2/3Jpplmg2IxT2dHY+TXL9xecj7 jxe+DD5enH+48WFF5OXunTNPRszJpR0qqBPnnnaqYcFNLd+6clqLp7XXKm3/rMVHK1dH8hkPHp7f uv36Zfk4/+Tw/OebLzcfGozKjvh3HG2svfEiL12IKIwNfel6HjLLc41PuqVHi3Dlng== z3Vs/1zafv8aZbLPbWdMnjOIs4tPXj8dr4U5LTLcKYChVP5VBbB0uH55uTdBFbAw9WXv4Y25/e1r Sy8Xt199RwEMdc2PWceRbcSu+v8t6ziyjb2J/z3rOLKNI6n8563jyDYerYD+o4jgiLFPVDyekLX7 Y4bn/M03cSwG8+hEDMbtz47HYDbK3RNhnIPJ/WEDD08GcS7eHoTleUR6WrBo9+LJeNPsvbFo02Rd 80eiTencmYmF80vHAlYd74tDpJFIWQYivsuz/qfLr6zYiKUZsIAmQ0kDswyU9nr/racj1Mnx24Mj 6b5zF74bwei8cMQwvhfBuPA3IxjTOz/oua7N7PxxxEjjRTw/f8xvbwP65ehpoNP3pn8sYpT3fsRR 1+3o0rnehDYxihjdeH5xXdZq+hbPV6W+GM7Jl+O/XXmy32/GQ0niinMZ5C0nF2Luby7E1HgMDUHB Ua7yh8KCunPkL/fh2P7dqZm/E5rU/SKnNMB1+eEm/qdRudEv/U6Z7zRx5ATY5XczP8iGM2PT0pv4 wxaHnRfZyI+n7ncnsn40R++z3+8hBiny8mccM+NGr1I+11fdvrt45L65i2Nd2vGXH3Zd2nbDOoXJ zaeiuG/sK1QbHqeq+una7eearxgzKGOxjAdbO82wvvh11r1d3pxBxmhWcRvyMYrGBy0j8+wsTc+F Lnsmn1C/RjusoHtohxV0a/Ij3pjVy9f7W4rG/fkrV97Rgzcf3tz4OGYrxzt6zLSOfXHU9xz74pnd HH5xzM5+GHvLONyfPX/n0xBAazbZ3l45O0T698eRvtiFIXi7Pzv+xddLYNz7c8Pq1HjfKFheTitY 7vuaGLPv30wM2340rbe8/2ohfI9mhzrpvLtwbRKFWI8GDXKnRbTyyFBliobZWH5AQmt24+Ur20V/ bk0PE7KKPdxUujScmKcDfYuZOl+M+/Jk4uZMnRLXzKyeP4IKJre73xZTUHqi5HBYGvN07kdbPK29 3mktmrEW7dy5/SvTzy7vpWvPwv35/HpzsiH0l0/OdNnWV26UZh6xl5s63PyVA6f/8nxmHIgvX4XE Px80DP7yjlXGX9p40IKiS1+eufbp8NUbYit3c2r9bVd5tJKPJYa63ULi7GqdgkjEPCXwYscWT762 W0TQOgc3TI4c3KFPekGTKVv/2mJlw5SmflDooJGe6SEWFEG7ceZ1k6dFopmHMwxYzX1+cTBQ3Pp5 5tI0tYU/f/nmz6ftz/DXV+/fVR7CP71uv8Xk5rOG5Y/VRdDhPlYSQa/72uKsZj+PFkIM8a1IRl5d Xnz367x4r+OxutuXpsAs9zi3olyKLgkc4KIgWGHs/H9f613uTSDe8/bmzuZ4rKc3MSGUJ1sHh19x Q3y7sPXx887K+r+39nq2r/838n/8m2vfutJHMMn0I6gr73uT+98+Wm/yVH9FdPnbufm9g6XPGwef d3fW9/7dvwTSi3srz+4s9S/1R/de7k9Kb8xbuVu+mkKAiffdR7SJD53SyHe/lNb+a/vLjnw9u35w sPf5/eHB1n5rdH5vb/3EXRufPn/Z3Nva0Xtcf+7OzsHoW/xz8O+vW/rt5KeDg6+X5uZ+//33we9+ sLv3cc4Z8UBkLFP9uSfyup2PR5/9tv7lsHsY9P1L3793Z3273dq6126e+fvDsn84rD/v+pPV22+f fvq8f/PLFjjlRwZw4hHS5b+e6c/Lfy9+7x3Knwc9M8jOxlT7ZpBCNpEfTC7By4dY5bssH2wF7ZQP L9alFTNwZE1p9t9y+bN8+Jfw6e/90L/Xf/Xa9Dfxxsc9HwfWW9+3Ng1CDam/DZLxxZKUQ7R9HwbV +SKEPDBRbl7rCSmZUkkqNfW9H9RgglyXQQwiC0KQ75VQgin9jZ53A+czmqnyzlD63g68qZYE453r exlecNpIMlaesXYQbI59a9KgmAJBK9JR6Zp8SkHuMH1XBtnqv9IDko18UevAVQgnpJK34BOf4Yc0 cCknIcoIZL77SV5asyMhOWv72WDYnvfnQfTG97Md5FgL75HhlX52g+iCDNOhV14eknE79txFWcYa QYloNQxKkg9ymU3ltQm8PwYb+4vygJc5dXwgRIfu+oGNMiQheJnt/moPM5MMptXIZNYc+lW+qwbD CBgb/mz0oqxGlFfjKsh7ZJXks8y28E7/xIov9j4ow8QRx8j6xupxkzBD9hlsEWS1bCGpRHkX1ryk onxiXAJbyOByUrZwKZIvvLeWhGyMLLEb5CTtKeOIsiRf2Jo9l91UQ76ImQwoBOcDCCbF2lopkdMr zFQzGcN64UALxjBJZrqtcd8JAyQZefeXX2BpqnCDLx7q2419LZ8bd2RyhwPfyboW+QvmcEavk3wv vFGs9Ah3J2FWeVB4w2MUcu2y53UpDrwi8+VkwoVXnA2h8YYnMzhIC5dtIEIjlylj1uXSBaPfpyF3 2ApmcAORHSy4fIghgmBzyY07vINAGSvdNxncEWIkKyl3eOWOVDg23w/CftID+SzrHxJl/ejCd9zh jzCHSbhFGvLSyDbWPcmLhCTjiIG8YUJV3vDRR/CGExURle+SF3UCVnDWkVCicJ8Qgi+RhBpkcZU3 YmyqBzMlrFACdYQQsFJgFlU0SdlSpldejlmzJg5Clje4fhFhlM7JJ97hsOrFSr/0r9Kd8oYVOe1D YtPwLnxuvJHAG1bnTxY2Z/BGCMHzWuajgDmGXBqbTIsuKUXvCbKk4A4XnCPBUU2IpjMlt4cgKhli wLWRSbUyCUIQ0VFC8PpMLI09hHWS13ax8hD3GPje6l1jDoOJM2IoguiBCr2EWXJOecMpbwRLdeLA G6bGrOq5yriOr/uQNcYUB/qWfKGEiAqHPQFvZelSksk0wlErPcpxFfaQ2Y4xt75QvFRIqUGj8Iew Aa8CmV0VQPQ0L6I25Vr4I4kdWiSnQK+DD4wRFpTXSFdCreQDiqQHH9QqM+d1OT1MjpfvR3/xBaSk ihUovNO60df43BghUkl4q9LbmZDgqbmPTQJm6lT4ZxoMAd7rb+xuf9093Nns739a/7rV3xYAMQYS bH/+a880xNnmWl4ZOcOWM2KlvxU8L39p6prZFU0j3DO8JeOpjZ6oh6T36i1HGKFrxpJH7fDW9s6N 3sL7nmldWfgknZt8tgO0s9n/uLe++RmJUjul30NOwc/jaFke+tgjjBHjJNLb/Z2NVMLyhoVtctfC AtDR154+7LqBm0EQ80dra2Rht8neAdyZoWa86oMUCq2n3K9wxojYgFBsLUQXYtOwkjmCBNYW9SJW DryKLq+RVaMRICFra6MoxhXlZ8huNqqBlSQGyqlYgzGFECMMhbClS/0TPV7EBJ42a15nbbaakxMm ZBmD92JhZ4XzKxZDZg7/szQI2q0A/dR/sd2b5Zde1M1scSHLA+WPHiyCwQbC5TL5G3/tXQt/+V3d dMqb2jKHxp1YSVkLLKoXtIPmsXTOyuICcpALRQXFHGm8c4VetWK/irxcFIGgEdpAUZLBVlmdIGtR YGXFpkXwiHRPlHKlZpNVgXoMwgE+K0kQtwVuSoLf2B1q3RP92/izJbTllDWcBTQCtGhz4kYfpOFa 8lj3BORxCFjKYnMj834xCCcfLA6oMgKk6aMB8/s/eePCD75xOP/DBzdGYjumqqIImhc7ETP+YmmH 6jqfVNdpqK7TUF1HVdf2qLom6A1j6jqourZj6jp26jofV9f1uLq2nbo+1t+VH1B3bqpPGZCu80OU hsgCMMG2wFk6xgnJ6kvarI596L5JYmkT0Sh6MVKJh212O31oZBKz2FEvywE4s90TiyrwTRZKDF6A bwBsJVMhgmmKwHBh6gCAA31nITYy+wW4UtwDWthEybV8d4KkyTAEivWrCJqIuTxBSmkUvcGJZ4NL OB68tu1a3L7FHv1IIkBwqVicJPKYIk2OIoxSRaPCBpkCu45uiaRbqPEKSBwTgY3gpMq5gRfJwYOA wdcELKCkoBSrd+ADp6d0hLH5ojaW2RcPEjpAMLARbp8V7ZYxJOgW6bn7rqVzzdRhUQbihQOSzMKj Kt2HLOPCp5qS+L4y76JniIKFQ4DMkjCo8ESgWsQoRdGlRA1aqtgN+ULWK2HUAE4JENGOccSfBZe+ rh98cjKqt+7t6QGmSUM3qNaxUMbCwvzGxuH2492DdTTboivjL+nP3d89eLy1sbu3KROCrzn+3Ebf DT50Qz994HOPt9a/3FuXt/4Xgi+Ti/N3bre5ffphd29bvxoGUuY3d99vvZ2/UzGAJwf//rL1dtQn vSs3XIWYyxOgqafD2E6LuNj+i81Oqk9daPcnC33CNssqpP7kVP/Fc2GSpR7UbGnfi4wWYVd4XLBL IgQwb0mUjEMIQoTUQhqFHVNGCMIgFmEYt/Byg4hFKA6cLa6BqNsAHEsPQDSXWC25ttKCsK6DkRIp zjCCBp6BiFYlLpL3VxFLyHcSGYiIcgDpgNPg4YpwGg9UJAIrYq2OZSnaYQg4fWDgcSEIhHGqLj3d D+MHmLYiklOEUuEiVf6t6GaB5wB8Zbz6QYuYD9EerTWYciFkhAxAgOyfmLDFH8GcVrWwuEUe6DL4 HKEkraxvMNKYGDSofrBlokl0yVUYNJEnmZVZL98LYoOXlSw0mCwLoKrMLyMQXpYM8R76RqLoI7Sb zIVMo6ylhXzC1vxVwcz+f10wdejHRv6H4/5Pi2Vb51MF8c/W+Q+E8+IhvdY4kk3T/0ibEWETECqA 975NUnaijRFZQmhwhaTqLUlOjA8oRaOZRaCCiXoTWFpmVZwoL8ayL+AkakDCQjsGCLSsqEavQqKh ExKhA0gRoZNSBKUgdGYQchKfvCCIifipYbwNRrsUjTjwnmDYeeerCg2FGIqCTjulCmZbKBpdAyVU 6pImaB5BkipeD4UTyMjAXXEA0YVBSI08FoQ9SKLTw2EJ6AcpaDQDd3FWSKKWwF1ifZOSvA6NgMtq WzkimAiSY+vilDJOiedCSEoaxrd88EVJorItwxoFEXI2JegikyTG1utdAob1riBLKUhZuseJA+8Y eADFKgAiKciwCvrrdXiEqCQUfb04kA6TaRvCkA6VwmdSIoYFsGLcqyBIo604BAilN7CFRV+eXIID iXYEJfWrAJJcvVICQhxCScHbdo/wV23QTUcF5kE8k0MqwkG4KlGW9Rsmwsrig1LFh4VG5eKgBad9 Q1hF7mUcxrT5tN6yTZkw2yYdKk2Y2Cedc1lusUfQ7ikZpxRBkdT3zhPMF4xWFL1QGPwkpRjhwMrY j8dUCRtVXBfRpKvarMx7BUYVY4yJ85HXwjZZp5JMVSE+qehUwrMTgnhkVgkBwSbMrYMYk0SQB1IG dNW7EIXDoohHiJayQE/X2M3kBA7MMk8lcP5Srk5vYvgdswP2IaXUxNkRmNPuiUXHFaphOxgheil/ ncNcZNjFyKeS15Yh5HpLZkghi1RCgOVVgJF6C8ExXmUjbXzUtwjjfGMLISgFXCHXTA== OGAF8PY1NiALDwraYYtMMFQGVyHlAoT0raLxileCaDWGiozwo1KsB86X1orhQ7DcmKekIUwEuiEn FSkPkQ55syytEeGsfJatWPTbkWKpWhE6QUJGKNHpu22bSYEmNVuleOAMRKeSLhuMAJ4SVWtLo1Rr GVP0YCxSLPSUUJwKF0ORIusV8Q4XlSKGT55qoi2Uou6iUBg2EorMm5F3VEhaTnpPsZhz0VXi5DFs xCCcE80o4/2Gdp0rpGSrIdCa2Lcg/LOG71OwpAgbedwRZdI4L7lrgbknofgiksl3iNHDtS9YV4Qd RVXSRYpUw9JTseAVsyKwFj6wcDAEJ6h6geMV9C0Z4aqNnlCC1Rv4QFCcVhnPr/Q3EgaKBmLQB6x4 lKTUdgcyYlgai1wQPBV4e+gDblgBxSGOLY8kUdqgCPiMhc3WTPlNyHiQLQSkFCUYJYjy4DNQsfK0 zFhJlFN4y0gxCiULWyjFlkAesJVWKIqGiDHp2omcRpFpeTWMKqV0rQcKwHdBnBxPCPYWqYBdL1xs xAJgdAoyD5GUpN0WMOCp4+QWgYmwqNGQZYQQo2f+oesIUokisDCLIcejFOtpWOXVTsYGhV0M1bJQ MkI4HWrmgMiwJajXSwqVeIG4UPMlBCo9oXUzl0IpXp4XpV5LbZNnoGVL8wNIcR4+hixXoUrDGhio bEROTWyUEgjsYwrtqWSrIpBEa42QA+IDQhFL4AGRabgAsMAQ33AHlxBgCooZix+kfbmOVcXDafBW KAkefSLA4zVN0CoXn65H1jnD0iOHVlqmQK4DRoplFa0kNjtZNZQFCyyzg34hgygILELF8a0xy4JW 08WmAO8j4BRTh0V7GhnckIlBalYoTCqKuiB6SE2ga0sBsFVx6fkeIwiTnhfuAD8VjA0OD/graQR7 EQ6iQ7K0IPltKLbVA49mNaprEHSmuoQSgmmin43lfIiIN4oDtMIMIf++AtktIbNdcRKpEAMDtt2r U1B9gRcLRsTwgsbKsBDIj0lnS+RaFm8BJRLy2cKHQJMi79om01a4J9juLXA6hCISZJXiRNGQRyzN CxqGagQfxSFFdCF5zarahaRE8qPzrt3DFDSwX0mNwkgo72mKRQSGfJ5MW1PhlNJXENRUS8jgWDFg WaWlJjEBuHa5SZR0jjiPeQJKnTjkfMbA9SVFrB5lTJioSSahJdzgyt5EBJ4zHWZjTZP5hEAPgDCy onJdQ+YYPUzGN0p8LQR6go9CH/JuMoEUBG6NNxSsEeoqKLjSSBCV2AdMcrUpMdHKBRSkz3EJ+AAY gzVaxSMGGlgoYrcc7sgsVKhYDzgH0INoLNfhaJDtR3lEZYoRl8DkGcIi07fKR8BHGVEsWTAoziqi kIumieWaWV1cA3dsQLWKkpcnMuJGvIP5J1zDuq/hjoAJEgpzh9INYaIqoiYUa7igEdnW2ic0UgL8 Lc9mZVq0W0hi0ENBv4WHce3hSiyi36YWUizmOMqd0TGYUsAyawgUi9PHso6I4psIhm5FHEgKr+IO rR0BHpEpjrR1CShJtI/VNlIUJJXpTETcUaDSgJFi1iYIgIRgnSxwRM5Y9AASUILt0ETU8phslIM5 UqsZSOHKivkjKBQoBO+f0ydIoPIJF5uNiajuyYgUoBso5eGlgLCKS/FDWJMgTgTLTFBuYFgEkxlQ QueR4BKcTwMLaIuJqEzMCHvSg8xelxDsity+TG4QVK0mWEwZs0pVRL2fAO7AaaIvHLJxME0G2XMx p/IOyKcHk0QF36uQTxmex4qL4lbtX4mFhWPVfAR9ng4rjUHVFggqYAsK0ujAOQjEA6DoNTXlIkGM Q2IvgteJlEK7g0xPTUg9jgXPRNXARrCxYKNqO91YrXIaMTQI7Bl4QVVy1MQjKN41CqAV+tK0IMIe nIzcaXFrIbtxaA1QHiTaOI+0MgqURMQwBbmz07lgRAlMbjVVgBFBaoTLVtXaOwqfbTFD2rrMKh+r YI/aFRPtPdEgC0KQWI2Gd0SmMjLUuo0NTYfC5ffNPCQmEjBLkZ58yohqJPJQyY1CbZhDp9Bg72BB MEI4aXgGKApvRowBiUYWRkR4kfDZU1EfB5SKRAIyfoZMQtRGQJ0gZ7mFYnANH17G61GowjsK51X0 SGGSEfdgbTP8NUZMmEMWlZBL59CAUqhamV9ILWEB19Nqs1WzNdCcGRMMBscSF9UM9KQCqmiKUU2J yWJXAWaoTSBe1IGlcFYhkOxo561ZhQNCSaa5PiUiUQ73zbV+elT1YTWzIoY6TJRXr9iyUDll6NzQ CFFAA0UHhgNj4WilP8g8rcEPK+hDpsik5uGZSh4gC0PXIGAGYUKdnXi1LR5BLnFM7wsAVAawCYsF Sk4sjLLJsFUUWqWi1Xq+jVkTfA4gJTQvEQASyq4qI2UUEFILyzhqx2yBaUFHFI4MIm8RXO1Du8Wi YQw2hsaPyhkoaVC0wog7tbWwbm76ICcWEAiELyqVdMiEEm3zlaw1Zbx/gONw+6U3OTYAwyQRRmVL 83sYhcac4F7q7ECzgNrINbXUXhfHdnYCAL7jPBJiVT/TArjSimarJRDG50YhEEVysCgE0HJG0UDS tRiUSaFuCxU3LBg8Iyhw8Thp4qLaAFZ+0oI5JLhgA8TVoY2jDSqwcN/4PZLC8IQqDBrcSsM7vGJh gohETOMyFxnumLitMH7R0jGU9xGTytNWfHi8v4jbwNbwzCodRSTOASaoP5CKhT1DRKRUxfFE5RFZ 2aEbi+oVvE+DGUzTGfaFGc625sBTST0wUmiE8FTzdSGBlfewmha6TdRrTBqtWAW7GUQHsGiIH6cW 44koZk1cD64i4EBGsBcP0JZHZJDYYkUZSoT9capxiJ4iIG8XCUke02hZYoVL+Jmx+TUMSNAFEAqD h8r7coeIN8JLuC5oUqREfGxUGhmNIguFATUGPYR9QtXMErshIhey8o0qqSKKIKAGxTQKIxIBPjFl Jged0QAlYpVAAxbgAoZ2Cx3WAJCjFCTOpAtC4YIDOgjOxnUyZICMBLgwWDDK49CkgmDQKh3cNcQF ufAoStNIOMExSkayRjrkWgSmsvsyYqUg8BKQVjctTGiN4RwwN61xSoT1ZV75lxTnMZNe0Yg8g+Rv 9FpIt8pWETMDBT2SnhWveNohis6QqahwiqINCE1YZhQJpOQdjLqGDKxlNc7ICHYcZtdxib9wsKBL N/AAHVq4ZVm9DfFtUDcnopTAU7BvqIilU5a4MqXpC6/+CCnkO7rJNDHA/rGgzhJG1nLewdu49lyY 3Jg+qK8IfMLaXOTCabexmIYlC7ExjVMRAwNyRrPXsHQ2KmlkkQCfFBYulayGJZKrDQMMhFeG2o+r mxsEge6DT7PG+C7CK4Q4to2OfgNdYNM4wnBOEPtq4XD4SnDFoQjhhCLIDSuEpWYOhmVGMLRV0x9e q7OgmWF0NAOTSKA7At+xUgBLlwBgGBAS6Yr6lkhB0MpEestcXFyXGDQrG72Wtgmwo+dLbUgzVKx6 woFGyCR9QqQP9eWgICkM9hCVjWvqMGVBIjSkURJdT4aF8Q7PwmWE4xF2yEhKFrqrskjsF7PbFLlI i+kUPgJlIVXFAunYuINimpGe8p6uJhwwoPBIvZ2bLQEggskDU/J+VjJRmLjvARSU8pdmr/FO2CzN PxBsydjUy2JSo7CVYDWTg1pKLQdPjsyAiEjQQm0WkAklaRkbK3xZEYvkIarh8VTRxc6qzBDyRgCU lJIUNWTY8BWk/8TFIrKoGg5HQrA4rg/jjqRUzoPRGnqkHqHeO55ZRebRQ3ChlYFya9PbwDbwhRAS R8EGjU1CRThiXKgq7iB8depTgJkN1U516tOlrEVBK6BQacLgWEZXGBI3yp3CpqAgQOyq7rygn4TC YK00sEhv1s7BQbotIOyGQDGUABFeZFScMf5MRuUT6mUIW2QDNxRZCZbjw2WJTNI6KldsvwiMXxv1 UsmdLIXtHHhW5yMVgugk7/BAHkiNIUKFFa5EiszRFAVrluqtikrIOhSWHJJC2G21sGoFO1ayV20V US6Ea4bWKxgXLFqr1rgjqwA3i8XcQByAZgldAcUP/R5UniJZSa0hnczcpYHkLKvLggYgNctbqMCK yhuzvGQKBExiy8KiPAP+lY0sjUVGM3h1uTRnwuQZsthUWijlN1AtvOb2j9We1p/DeYcL1RJ1QB4E Gt62MThVSy5ZLVn3SJxhYqBBtBVfMwvrc8i2y6Xy2tIYYAjal9r5X7prhr2pKWgu26O+H33JQJmk IJeADiOcDEJClpzPZK10pMjTvSssIzZO45ogcdcD7kEQExWGNeic+2ZGWrxQywQKnSqrAsekeXK6 iSixDM6qpQevZsd6OGNb2CvQPnFILR4hnJDbwEUr0GfiBp0VfbvVgIP4lZlLblPW0FfkwDvZCyg0 0zFonEoIpSiBYAMEGHMdeMTz8M5y0fpRsg1jPEanT6CxFl2nmlqtEGOeIDG9LoPT8IlRXkwtytGC cKxLwJ3sCbkeRTaZURub6Fpy3ej5tLSUTi9tPGJBxeqMF1bnQLN7lhpFzUhBadekTI0ATPIkmVJa QwINmD838HDxmIB7Emps70eILjEdw+057f3wlZE16UjyAFxshLK7gpKolgw1Jq69D0tpeVfRRC3q O1jTAFtSiGRYJ2I1BB+cFqajmgRIHPF1h7pKEjQjhIz6N70lKSnnoLdkZnsAVUrhWHKbFTBL6Mpe qCQLlqARkC8EPqi6p0PmlsuKgHXuZjsiN1taXYmSEjLOaAcKS1vKqNxmwN/XRjpSirPyve0TP1wb 1aqLPvYCkHnSFBPyids91Kqz3CMr5FnpBVSbM9OBfCAqw+HQMynlkZEVCuvJQcGIUSrOXKJcQ+Gs 4Q7RM5oIDAyyhKR1AlgKwQdwdQT+6lKBFb+hH5qgwOoXVnwK02sbiACsnej7Pzgt2Egjpod1bAQp 20oCukBMFK4RCIr0Ge9d5R26NwJ5PCgjELBhj0FG7qdwtN+pbV5b1GfAtiCltoVHwUSrVlk72Zl/ cqCmalaTUU9Be9tkSgUiTndsssgL8s2MpVX1HYfBUaZDVL4ZiWCeMzbB1dyf1agxCBw+THyi2Yoa nUQOEpWf8mqG5VIrRMDgreoIZstR34wZQxQczhMCaW3is96jBdQgFM5hqr5dM4jjNaCmEy+ASO8p NnGvi4Z14JTB1KHuz1iuHuslFjlbdGAxRu5xMy2VhSVHsOXEfH53q9P/bK24WTTCrwuVa1U1bYHw TOHWENHlhTmTtkfZ6O48pl2q7iGWN8unGHTLCdqIWQkRLssab/HA1rFtilshqbjCuwi7MV/MusC5 xl8QIpQygjxIAut8demqZK3qN5BiI7GAriqbxchKtxOj3PgHtR1qaSqT0tRQ26QgyU2ALvMY4N0W 1sbI0idu+gNHVRotLCfiINBYRNrCN4EpEVbcsOAjeE3N18SQ0iJeweBDBQaM3EnDag== f1zDIwxw+VFAVHQpN7D1R0uVkBEy7FSyRYFfqcSsuKWhxVSYOkY/UtWbWmFdcBqS0DI/Qa1CSNXU ITpExKYavYEROw6v1Kzgy6NOyhs1UbUVf2LvN4wpqqYMc0Yuafi3thItFmUbVrEwKYwtnShYRE0P ak74BFmv+hbexT4dAA54TpkhVdfySaUq8BQKdp5pjQHjayvYNCMzykpQ3ZDpNCZW2v6GVWzEDSUE 3Qudqu6/Rr0HHCcaEZ8UbiPjaxms8m2XNaJLGv/3GcWLgElMZPmsLiRCFi7BZcQj2LeJ2A/qu33V RAEDDgShgU6kFoQzKI+gXKvkiATjwStSh4OKygKwDVLSKHbHcFfBSCxVFwrdZfCutXRQGcQ8zt3/ pOBUlApaGGGGErexM4MAM6CYkghMTDpza6GoC7oCsy9DYUyPm/JD0Ix9wEajGJWNqSVD7SLSQmH+ FYXkgUVuARvHZdqj1eifbmCDBYjD4jSPXjBGS+xEAksiEcfViJNHYF8Dt5wtUiySgKAUIh0fldcj zA/jlD7orvVodfckKRn7eRDeRWyRFKBKvpyRS5wGIb4lm4lEdb7B4OjUTVsBZ6JGCoPSGKn3GraT dmNm3TTYO3MmWLlBAvxAIQR9sVVvF6vDNoHjbeYNmRVZrmpINkK90e92rQ4mqutHgjg4zMO37cjY sS9dQupeq0kcN/biEYYAHHeHcHipMFyN3dZAYthKj6iqc1rZifICT3+vO8wBSW5shgIQ8JqtQOng GuQ9YnNtrAp7HQAhxlIY819DPzlhVZ1sHFNRC/MfdO4cImCi4nA/wLoomaJpr9jgzPgd3WX7GgiG DzDoFDPzyjJ3AS4lqiIySywwv4LNuFcE2XF5IKJkAYPC3sg1bQJRHaPGYIW9QFITlWYw0NKGVRBZ s85d1Zg8UtMwyzh/AfksFBBlWmNQbGFEWVxG7tG22O2EeAUcT96RMDnIiAcmW71pGf5WAI8jI3JJ Gr9IpQ3GB0ZYgzAcTxvIuhOVKU2eUJBqYHiAaSPldI1DMlZPgqkauxRBz8rGnV/PUnycahBs1kJO S53cCqgZh6i8g4XUCGpV7Rpi8Lrj32F3GIeTIUmpZaPI+5oARYSwvbjibAeGFcnHvlUTYMdzZYgJ Z30wHIjAL30rULDvEr609U14vU3M25bKIhmPPIoWRfCMAh0kfCMUPSAM7K1iArjW0XgdQkasBhuP cJqGroiPmmemX+tb7T12laIaQhYxMsVcbFZW0o1KOB7CNlZibhkVPtZ2kspIABkxqSHNLjFb0LQc T2FI7Am3nZPiWV0ET1nNL5RC0NoKwV4Qz5A95zIl+iA41SN6hrVZ8OdSK4HACTwsRqeVN7rH31FJ aLzZaZyMKoA1XmjUMUHlcGqIbkmnn0CKbVUyDQdge1zV+E4rTbZA2I7xkhYnddxURw5iwZOlY8gb cGyGOAgo6wpaIg6L6ZCMLpwlqtY1NMo0Wh4WDlqyGvMbTFjKM8IyrBzgwTrOtqR11RisDBBRWc/a A6qoFVC43wvPRFW/TjdGaACf64NdIwg9YY8bwyLYtMYqq9ol+3B4BauqoCvIPM42tDA0DtirLp4H KAx0kpItEERLMmt/YtFqPR0UcreFwAXukXMajC5OixdX2UYL/zApi/5j+yvzM7WNmqFNhD40SI0t dqh0RlADBgy8YmorzGOFtmO6mckRGnunlTzIcwRVeUnP6chVjwkgqzNGiSyFDc22BaA95DGARKBI URXKOwwys1SURZ9BaxAgo9MYXWz6mmXlzCTSs3YAV1oBKH98ew+KsgtYnelNl7UMBTtQNJrtUAqA WlZMC9kHKrnNtU/0fHCaExJ6pcUe1fQGrdvMoZPDrCXL2TNS6ZKamq5gGrwhSIyhoDS0rJDv0rZD cJUTYAGqVRFatQSyWlSKO8jtuqpRdQy53fvMzU8W0AUb2pE5R/wuYWNQUQ8bCTFAa/HtijolhWfq JNzB3U/cGFW4iAZJDybNgkJECCpCBFjo7Jp4V6tLryclySImxdoVO1K+UTIBzHWzi8cdES44uYml VBR3zbw2jIZDC1h8207UWkFvWViBvngNGDa3jKHZiB2MhNswsgA12IPaagthp2W82LkDt9SrJlyB ViloXSi0KnxL9C4rRaOuUXuLCpFI+2KjKkb4v841SsEmPVaaKINZPWxI2jFw2jHmgiIPp9gQ+gvy CoAJRMbN5xbnoIGCVcE15iI6TdrhuiiGY5GAblfHoVhEtplbZyPSfQDVGnqBlKJoDZUPjqoXfhty JyhcQHYJ4gM3CqUNVTGFKNwEF4A1A7CLNRDeJ2PaiViQpdAOmRLbX3CkRkhaNUjQEbEHGpQAsIPY l0hNgD9ImcUzIlDiTDSQi0dQlh7anjlSGHaAA9yED3xceXAOs/U+avLcN1iyBo+B7gtcCc3Je+wc g9NYNDDkq8b7fEuByDNV/XEcuqSQPBgttIRoq5OKWgtAFlA8uSS0bQidiaSLwypHuMFA5cHpnkTI NtYGZ20g0SPX+HqRfhM0uENYItD1o3+Lg7KQMaZnRZMnFNZN07Pihhksa2LFDU80EmHnqUyWwQ5W lhPvM33GduHCZ6gfthJQchpB4QSq44r4DRA5ysNIoRpzRXOHfAqOPgBzYaQWriDiLFSu6j4C8Gce ZYE0IMLMKGgDFqSFDq0kCQwRKeUIiwAHe27MJsuw6Nc3fbNGlxRVBAChCMqFrPvU/LAGL7TNY143 WQeesyCdSBrJ5Q2YUN8OcGM/WTrkUSYZ6GxhtygYCQpyrRcRO5MuB+6RBqvznC+MmEApGo2mhqRC vQIKvWocaoBNYCsn/O1/MOILDW6otHLJiPfCHqHqAb5p9C1nz6x7pyxKy8NFq9ujWCVDVxBVUNhI ijoIeG1wZ+EDsy7C00OnolrkRkpwENxmOFl4B4vMDWwyAQXTnaHZXz7B0kYoD68bOjlJ4BjrdLdm gjZCCMJrsYEn8AtZi7xJ0GIlZGm1agCRn0hWoJ4hhTFORCnikMKwADVOaa4Bxgq4vMpdvWCnaBU0 4g7Uk9CnZoyHpe9OC2M22F5T3zz9D3ud2oyzbmJNF0XL5dt6H1umfzTkH7S8hDVMGvFHlb4WErnI fCK3NIOQomZHmRRDiTWyl5qEjEWdUFEvSZOQ3AmDXJ7VzCXTdij6EUvXpTpD57haxhjpQKcWIWT2 VjfPEchrfpfRUZR/+6z5e2aKmJwPscuPY5Lg3BYGNXDYpdNzC1uhG0oF4NZhh1DW2CorMXB6nfFM BRqN+4Hi2/5Yandu9ms7YWEh2f0aW+9YN4liLx6Yhe3asJksRo+6y1sLB+DOa+YBiXgsNuvBou4q 93gaOYPYErosTOq2iC1yicS+aw5Iexda8WBLyjD/EpmRSS23hIQ6sh2sT2f+M+hGstTW6jgr/IMR Q6ZIgQGQ8zc8FREkrbVBzMYFPXeG6RRgR6eFDFnDYKkokmZCNmsJBc3XWkvaspZGa4xIYGFX7qof UDmK0EXWMIhmelkjk9reGuaQwYxZg5Ka1KI885aiSa2o5VtgJB81hUWPpDsH6JvmwsTR4vl3Ner5 ejwpEPe0/LixWo5KDbOh+fFW0uq905UocO7H2bqlwkFCcT/P9tQtFowZ6kyQGyB0iPwxo1+U/+iv rTGdF1DanaLaNhI4W1FLHL/xHoIfCF6I2k4rHaT88BqIBARglo2Wu8Y+RqxG0ESOHRJqbu+u3nAm mNvgQ17r+oSd6Zvj3TDxCYmFrO8ihtX6atvaYXoLhSmmau3AMf76BzUktiMzJMfNGXp6lIdBoSag /11R/JRYIuQjzwXgXu7CoXIzH7IdRleYpQKrfAZxpJR0oLVtPeFc8JyJYy/+RxPa2PWvR/byXJtt JUH/dOV7zBVaPZnVWIpIbYf0Ot1doRk9WlKSXEvfISzPfVA4Y8xoVS6uPQ/pYcIRGT0ezRo141hT K1fkkZ4gRF7n2r1GNxp53TYAQneeIbc3aCrWR91tlfQhAPXEVyfNbAJQZbZik3KWx0ZPFEG2NLBp p9chBoWYI+s2Cqvb9Gg9Hr2lW96qD+0Rp8Ey6hjeAfSO+GC2mi/vtlWxNklP4CIH5S7J2R1aAKGM +kxA1AJFSqElT7NuyOVpTLSOeTgcbn8BgSdOcC9dy1BjL2xwwzpO7b9tsbzQSZNWTlr1EY4QwLOr 7Z623Y3HHGPhq+4Q8bXVzVXVFrgnqZstS52q7lyhJ6gJXz0K2mgYVe/KNM9VM85KotJAPRiWCNlk LWpCahIJSQtnXreAYN/dIksDAvZGICiJwmrmqJGz5L6ZrIQjXP9PZuMpLYp1GUzWbDxBLQJs1Xaj 18NG2vQzQQ+YgfNJrNHyBbjzQXeyxqQiRNCAzboaPGVdATQocGGpuh70ekAAkP2m1Qn4jlXLSYXG ceOtHsqNlWiJS3pqKmdaImW706YqbCvPX8naE54fwjLmHNsjrAjHPZlOLwUc7gqiK6Xjg24zcYSU khAa7kdO+8T0/UNLwzVwLLsS/i9tVbh9i4cFtFqVVjZR2rmZjeR1U3+2bbYYCYIPEa1OzXjL3z2y NB85/S6K+dTDQN3wb8tge+4DQPwgM7CCo5lq0c6CSwyA7yxj1dhkPYsd61B1CF1Zp8cCxiry82K7 J0oxI2iIM72kl6c/OJszKzq8nirJAs6FDRZSIbiIBwx0+Y+8c2EbCVTUcGFQGVHUUx5DMY1pR7gl cafwGCpvkPjhLhjsrT7tdazbZe1C0eOYFn5wjCjsYTRDnxQ2H53B+Q/w18Vnf7OVPz8v8K9wzH/y TLLTDiLLP3Yg4Oj0YxyGip2X6kHyCPNtpZh23pZlCgTJ5ty8MNYQhrZ9GqcRJYYcuZOvtp11iwjU IOCCcn0UxSK4YrVukUUiCGY0FWAjT+9GJjipTdZNHa7BiXaIMTQ2a6yDZt0WuQUBNVHcI4XzZawq OZyU5DKripgxRFog81huIsnM8laLM19Q8FK0qIXnCvFQM9b1B/q/xbaz+eE04shASEAZui+CRY/P HtXSLDI8PDofETixNrMWG5oCD7oTacLpIn9wZOBIfQmW0JNYHWwuStNNUrasSDLNFu7cxglfPncV XbLYOJccmfJZlFzjRDO4MDjUw3GrKLOEPKWkaMHAn5wPOIlT+sQjrvlvSxdCd0dG88dj+U+f+Xf6 un3vCMAfEb3gtH4WPoYlbCHF+yGFlTHcuIGaBB/0PHRso6tauIy6k1hZoceNtos9nntsNZoH4OWt Oj0op0NwNCrXB6O5oRbArl5/4sSiOiIhnYUfCdGMPU/rRRJNDyBGLrTw5w4YgudRbmJNkL5HCJBn oYSkJ9zWosf6Fbj7SBPgQEskslidVLWIAikoZB64N5cYHRjRawDLIRWF0Rl1MFn+cHzi2iG5AmRp Oh3q+cV0Mj5teMa3TAUOpf2OnNUjMKGyHATNUNy8L0CNswh2ibTPOsHAuqEfyqrtjg== lqVFZhg7mnE4OX7FAPEOZMtQg4d4LCKh6HvWWpb/f3KGAR0dzp8M5j8uaact3KmCVn9M0J716olP Kwv6w1k3dzb5s1mzs7j2+kNai3u7X/d7h/tbe5sykv4cvtjZBfXe+t6v+/1fd3Z/3+nv7B70/8/4 TyTh+M4l+e9xD95Pwf5zfDBEWBnXcXj0+OSrx/Km/YO93ddT7PiLl0cPJu9+GgnngZ74caRZgX5Q lyiHnxXRaxcrvSc9/J2t/IYfoUH4xWjsh/o8fzxo2IBedS1YPqZNWBRzn2hCVPNYGxZxr7E2rCtj rbSrP2injUWbGY5Fn2ujaRenjGf06f/2P3/QdZR11VWcmHi4/nHr6d765y9be72P++vftvrrOzuQ oa2v8o2w09b+we7eVn//0+7voMgj3e0TEzcf3Or9P39xpbo=