>>=y,p-=y),p<15&&(d+=z[n++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(d&(1<>>=y,p-=y,(y=s-a)>3,d&=(1<<(p-=w<<3))-1,e.next_in=n,e.next_out=s,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=P,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new I.Buf32(n),t.distcode=t.distdyn=new I.Buf32(i),t.sane=1,t.back=-1,N):U}function o(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,a(e)):U}function h(e,t){var r,n;return e&&e.state?(n=e.state,t<0?(r=0,t=-t):(r=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=s.wsize?(I.arraySet(s.window,t,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(n<(i=s.wsize-s.wnext)&&(i=n),I.arraySet(s.window,t,r-n,i,s.wnext),(n-=i)?(I.arraySet(s.window,t,r-n,n,0),s.wnext=n,s.whave=s.wsize):(s.wnext+=i,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){e.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){e.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){e.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break e;o--,u+=n[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(d=r.length)&&(d=o),d&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,n,s,d,k)),512&r.flags&&(r.check=B(r.check,n,d,s)),o-=d,s+=d,r.length-=d),r.length))break e;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break e;for(d=0;k=n[s+d++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&d>9&1,r.head.done=!0),e.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break e;o--,u+=n[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break e;o--,u+=n[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==t)break;u>>>=2,l-=2;break e;case 2:r.mode=17;break;case 3:e.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break e;o--,u+=n[s++]<>>16^65535)){e.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===t)break e;case 15:r.mode=16;case 16:if(d=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){e.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],d=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+d>r.nlen+r.ndist){e.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){e.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){e.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){e.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===t)break e;case 20:r.mode=21;case 21:if(6<=o&&258<=h){e.next_out=a,e.avail_out=h,e.next_in=s,e.avail_in=o,r.hold=u,r.bits=l,R(e,c),a=e.next_out,i=e.output,h=e.avail_out,s=e.next_in,n=e.input,o=e.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){e.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break e;o--,u+=n[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){e.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){e.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break e;if(d=c-h,r.offset>d){if((d=r.offset-d)>r.whave&&r.sane){e.msg="invalid distance too far back",r.mode=30;break}p=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=i,p=a-r.offset,d=r.length;for(hd?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=t[r+a[v]]}if(k>>7)]}function U(e,t){e.pending_buf[e.pending++]=255&t,e.pending_buf[e.pending++]=t>>>8&255}function P(e,t,r){e.bi_valid>d-r?(e.bi_buf|=t<>d-e.bi_valid,e.bi_valid+=r-d):(e.bi_buf|=t<>>=1,r<<=1,0<--t;);return r>>>1}function Z(e,t,r){var n,i,s=new Array(g+1),a=0;for(n=1;n<=g;n++)s[n]=a=a+r[n-1]<<1;for(i=0;i<=t;i++){var o=e[2*i+1];0!==o&&(e[2*i]=j(s[o]++,o))}}function W(e){var t;for(t=0;t>1;1<=r;r--)G(e,s,r);for(i=h;r=e.heap[1],e.heap[1]=e.heap[e.heap_len--],G(e,s,1),n=e.heap[1],e.heap[--e.heap_max]=r,e.heap[--e.heap_max]=n,s[2*i]=s[2*r]+s[2*n],e.depth[i]=(e.depth[r]>=e.depth[n]?e.depth[r]:e.depth[n])+1,s[2*r+1]=s[2*n+1]=i,e.heap[1]=i++,G(e,s,1),2<=e.heap_len;);e.heap[--e.heap_max]=e.heap[1],function(e,t){var r,n,i,s,a,o,h=t.dyn_tree,u=t.max_code,l=t.stat_desc.static_tree,f=t.stat_desc.has_stree,c=t.stat_desc.extra_bits,d=t.stat_desc.extra_base,p=t.stat_desc.max_length,m=0;for(s=0;s<=g;s++)e.bl_count[s]=0;for(h[2*e.heap[e.heap_max]+1]=0,r=e.heap_max+1;r<_;r++)p<(s=h[2*h[2*(n=e.heap[r])+1]+1]+1)&&(s=p,m++),h[2*n+1]=s,u>=7;n>>=1)if(1&r&&0!==e.dyn_ltree[2*t])return o;if(0!==e.dyn_ltree[18]||0!==e.dyn_ltree[20]||0!==e.dyn_ltree[26])return h;for(t=32;t>>3,(s=e.static_len+3+7>>>3)<=i&&(i=s)):i=s=r+5,r+4<=i&&-1!==t?J(e,t,r,n):4===e.strategy||s===i?(P(e,2+(n?1:0),3),K(e,z,C)):(P(e,4+(n?1:0),3),function(e,t,r,n){var i;for(P(e,t-257,5),P(e,r-1,5),P(e,n-4,4),i=0;i>>8&255,e.pending_buf[e.d_buf+2*e.last_lit+1]=255&t,e.pending_buf[e.l_buf+e.last_lit]=255&r,e.last_lit++,0===t?e.dyn_ltree[2*r]++:(e.matches++,t--,e.dyn_ltree[2*(A[r]+u+1)]++,e.dyn_dtree[2*N(t)]++),e.last_lit===e.lit_bufsize-1},r._tr_align=function(e){P(e,2,3),L(e,m,z),function(e){16===e.bi_valid?(U(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}(e)}},{"../utils/common":41}],53:[function(e,t,r){"use strict";t.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(e,t,r){(function(e){!function(r,n){"use strict";if(!r.setImmediate){var i,s,t,a,o=1,h={},u=!1,l=r.document,e=Object.getPrototypeOf&&Object.getPrototypeOf(r);e=e&&e.setTimeout?e:r,i="[object process]"==={}.toString.call(r.process)?function(e){process.nextTick(function(){c(e)})}:function(){if(r.postMessage&&!r.importScripts){var e=!0,t=r.onmessage;return r.onmessage=function(){e=!1},r.postMessage("","*"),r.onmessage=t,e}}()?(a="setImmediate$"+Math.random()+"$",r.addEventListener?r.addEventListener("message",d,!1):r.attachEvent("onmessage",d),function(e){r.postMessage(a+e,"*")}):r.MessageChannel?((t=new MessageChannel).port1.onmessage=function(e){c(e.data)},function(e){t.port2.postMessage(e)}):l&&"onreadystatechange"in l.createElement("script")?(s=l.documentElement,function(e){var t=l.createElement("script");t.onreadystatechange=function(){c(e),t.onreadystatechange=null,s.removeChild(t),t=null},s.appendChild(t)}):function(e){setTimeout(c,0,e)},e.setImmediate=function(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),r=0;r | MIT License
5 | */
6 |
7 | const basePath = window.location.href.split("?")[0];
8 | const usageMsg = `[*] USAGE: ${basePath}?channel=&binaryType=&version=
9 |
10 | Binary Types:
11 | * WindowsPlayer
12 | * WindowsStudio64
13 | * MacPlayer
14 | * MacStudio
15 |
16 | Extra Notes:
17 | * If \`channel\` isn't provided, it will default to "LIVE" (the production channel)
18 |
19 | You can also use an extra query argument we provide, \`blobDir\`, for specifying
20 | where RDD should fetch deployment files from. This is useful for using different
21 | relative directories than normal for a certain client type, such as for fetching
22 | stuff from /mac/arm64/ instead of /mac/
23 |
24 | Blob Directories (Examples):
25 | * "/" (Default for WindowsPlayer/WindowsStudio64)
26 | * "/mac/" (Default for MacPlayer/MacStudio)
27 | * "/mac/arm64/"
28 | ..
29 | `;
30 |
31 | const hostPath = "https://setup-aws.rbxcdn.com"; // Only the AWS mirror has proper CORS cfg
32 |
33 | // Root extract locations for the Win manifests
34 | const extractRoots = {
35 | player: {
36 | "RobloxApp.zip": "",
37 | "redist.zip": "",
38 | "shaders.zip": "shaders/",
39 | "ssl.zip": "ssl/",
40 |
41 | "WebView2.zip": "",
42 | "WebView2RuntimeInstaller.zip": "WebView2RuntimeInstaller/",
43 |
44 | "content-avatar.zip": "content/avatar/",
45 | "content-configs.zip": "content/configs/",
46 | "content-fonts.zip": "content/fonts/",
47 | "content-sky.zip": "content/sky/",
48 | "content-sounds.zip": "content/sounds/",
49 | "content-textures2.zip": "content/textures/",
50 | "content-models.zip": "content/models/",
51 |
52 | "content-platform-fonts.zip": "PlatformContent/pc/fonts/",
53 | "content-platform-dictionaries.zip": "PlatformContent/pc/shared_compression_dictionaries/",
54 | "content-terrain.zip": "PlatformContent/pc/terrain/",
55 | "content-textures3.zip": "PlatformContent/pc/textures/",
56 |
57 | "extracontent-luapackages.zip": "ExtraContent/LuaPackages/",
58 | "extracontent-translations.zip": "ExtraContent/translations/",
59 | "extracontent-models.zip": "ExtraContent/models/",
60 | "extracontent-textures.zip": "ExtraContent/textures/",
61 | "extracontent-places.zip": "ExtraContent/places/"
62 | },
63 |
64 | studio: {
65 | "RobloxStudio.zip": "",
66 | "RibbonConfig.zip": "RibbonConfig/",
67 | "redist.zip": "",
68 | "Libraries.zip": "",
69 | "LibrariesQt5.zip": "",
70 |
71 | "WebView2.zip": "",
72 | "WebView2RuntimeInstaller.zip": "",
73 |
74 | "shaders.zip": "shaders/",
75 | "ssl.zip": "ssl/",
76 |
77 | "Qml.zip": "Qml/",
78 | "Plugins.zip": "Plugins/",
79 | "StudioFonts.zip": "StudioFonts/",
80 | "BuiltInPlugins.zip": "BuiltInPlugins/",
81 | "ApplicationConfig.zip": "ApplicationConfig/",
82 | "BuiltInStandalonePlugins.zip": "BuiltInStandalonePlugins/",
83 |
84 | "content-qt_translations.zip": "content/qt_translations/",
85 | "content-sky.zip": "content/sky/",
86 | "content-fonts.zip": "content/fonts/",
87 | "content-avatar.zip": "content/avatar/",
88 | "content-models.zip": "content/models/",
89 | "content-sounds.zip": "content/sounds/",
90 | "content-configs.zip": "content/configs/",
91 | "content-api-docs.zip": "content/api_docs/",
92 | "content-textures2.zip": "content/textures/",
93 | "content-studio_svg_textures.zip": "content/studio_svg_textures/",
94 |
95 | "content-platform-fonts.zip": "PlatformContent/pc/fonts/",
96 | "content-platform-dictionaries.zip": "PlatformContent/pc/shared_compression_dictionaries/",
97 | "content-terrain.zip": "PlatformContent/pc/terrain/",
98 | "content-textures3.zip": "PlatformContent/pc/textures/",
99 |
100 | "extracontent-translations.zip": "ExtraContent/translations/",
101 | "extracontent-luapackages.zip": "ExtraContent/LuaPackages/",
102 | "extracontent-textures.zip": "ExtraContent/textures/",
103 | "extracontent-scripts.zip": "ExtraContent/scripts/",
104 | "extracontent-models.zip": "ExtraContent/models/"
105 | }
106 | };
107 |
108 | const binaryTypes = {
109 | WindowsPlayer: {
110 | //versionFile: "/version",
111 | blobDir: "/"
112 | },
113 | WindowsStudio64: {
114 | //versionFile: "/versionQTStudio",
115 | blobDir: "/"
116 | },
117 | MacPlayer: {
118 | //versionFile: "/mac/version",
119 | blobDir: "/mac/"
120 | },
121 | MacStudio: {
122 | //versionFile: "/mac/versionStudio",
123 | blobDir: "/mac/"
124 | },
125 | }
126 |
127 | const urlParams = new URLSearchParams(window.location.search);
128 |
129 | const consoleText = document.getElementById("consoleText");
130 | const downloadForm = document.getElementById("downloadForm");
131 | const downloadFormDiv = document.getElementById("downloadFormDiv");
132 |
133 | function getPermLink() {
134 | const channelName = downloadForm.channel.value.trim() || downloadForm.channel.placeholder;
135 | let queryString = `?channel=${encodeURIComponent(channelName)}&binaryType=${encodeURIComponent(downloadForm.binaryType.value)}`;
136 |
137 | const versionHash = downloadForm.version.value.trim();
138 | if (versionHash !== "") {
139 | queryString += `&version=${encodeURIComponent(versionHash)}`;
140 | }
141 |
142 | const compressZip = downloadForm.compressZip.checked;
143 | const compressionLevel = downloadForm.compressionLevel.value;
144 | if (compressZip === true) {
145 | queryString += `&compressZip=true&compressionLevel=${compressionLevel}`;
146 | }
147 |
148 | return basePath + queryString;
149 | };
150 |
151 | function downloadFromForm() {
152 | window.open(getPermLink(), "_self");
153 | };
154 |
155 | function copyPermLink() {
156 | navigator.clipboard.writeText(getPermLink());
157 | };
158 |
159 | function scrollToBottom() {
160 | window.scrollTo({
161 | top: document.body.scrollHeight
162 | });
163 | };
164 |
165 | function escHtml(originalText) {
166 | return originalText
167 | .replace(/&/g, "&")
168 | .replace(//g, ">")
170 | .replace(/"/g, """)
171 | .replace(/'/g, "'")
172 | .replace(/ /g, " ")
173 | .replace(/\n/g, "
");
174 | };
175 |
176 | function log(msg = "", end = "\n", autoScroll = true) {
177 | consoleText.append(msg + end);
178 | if (autoScroll) {
179 | scrollToBottom();
180 | }
181 | };
182 |
183 | // Prompt download
184 | function downloadBinaryFile(fileName, data, mimeType = "application/zip") {
185 | const blob = new Blob([data], { type: mimeType });
186 |
187 | let link = document.createElement("a");
188 | link.href = URL.createObjectURL(blob);
189 | link.download = fileName;
190 |
191 | let button = document.createElement("button");
192 | button.innerText = `${fileName}`;
193 | link.appendChild(button);
194 |
195 | document.body.appendChild(link);
196 | scrollToBottom();
197 |
198 | button.click();
199 | };
200 |
201 | function requestBinary(url, callback) {
202 | const httpRequest = new XMLHttpRequest();
203 |
204 | httpRequest.open("GET", url, true);
205 | httpRequest.responseType = "arraybuffer";
206 |
207 | // When the request is done later..
208 | httpRequest.onload = function() {
209 | // Handle req issues, and don't call-back
210 | const statusCode = httpRequest.status;
211 | if (statusCode != 200) {
212 | log(`[!] Binary request error (${statusCode}) @ ${url}`);
213 | return;
214 | }
215 |
216 | const arrayBuffer = httpRequest.response;
217 | if (! arrayBuffer) {
218 | log(`[!] Binary request error (${statusCode}) @ ${url} - Failed to get binary ArrayBuffer from response`);
219 | return;
220 | }
221 |
222 | callback(arrayBuffer, statusCode);
223 | };
224 |
225 | httpRequest.onerror = function(e) {
226 | log(`[!] Binary request error @ ${url} - ${e}`);
227 | };
228 |
229 | httpRequest.send();
230 | };
231 |
232 | function getQuery(queryString) {
233 | if (! urlParams.has(queryString)) {
234 | return null;
235 | }
236 |
237 | return urlParams.get(queryString) || null;
238 | };
239 |
240 | let channel = getQuery("channel");
241 | let version = getQuery("version") || getQuery("guid");
242 | let binaryType = getQuery("binaryType");
243 | let blobDir = getQuery("blobDir");
244 |
245 | let compressZip = getQuery("compressZip");
246 | let compressionLevel = getQuery("compressionLevel");
247 |
248 | let channelPath;
249 | let versionPath;
250 |
251 | let binExtractRoots;
252 | let zip;
253 |
254 | main();
255 |
256 | function main() {
257 | if (window.location.search === "") {
258 | downloadFormDiv.hidden = false;
259 | log(usageMsg, "\n", false);
260 | return;
261 | }
262 |
263 | // Query params
264 |
265 | if (channel !== null) {
266 | if (channel !== "LIVE") {
267 | channel = channel.toLowerCase();
268 | }
269 | } else {
270 | channel = "LIVE";
271 | }
272 |
273 | if (channel === "LIVE") {
274 | channelPath = `${hostPath}`;
275 | } else {
276 | channelPath = `${hostPath}/channel/${channel}`;
277 | }
278 |
279 | if (version !== null) {
280 | version = version.toLowerCase();
281 | if (! version.startsWith("version-")) { // Only the version GUID is actually necessary
282 | version = "version-" + version;
283 | }
284 | }
285 |
286 | // We're also checking to make sure blobDir hasn't been included too for the compatibility warning later
287 | if (version && ! binaryType) {
288 | log("[!] Error: If you provide a specific `version`, you need to set the `binaryType` aswell! See the usage doc below for examples of various `binaryType` inputs:", "\n\n");
289 | log(usageMsg, "\n", false);
290 | return;
291 | }
292 |
293 | if (blobDir !== null && blobDir !== "") {
294 | if (blobDir.slice(0) !== "/") {
295 | blobDir = "/" + blobDir;
296 | }
297 | if (blobDir.slice(-1) !== "/") {
298 | blobDir += "/";
299 | }
300 | }
301 |
302 | if (compressZip !== null) {
303 | if (compressZip !== "true" && compressZip !== "false") {
304 | log(`[!] Error: The \`compressZip\` query must be "true" or "false", got "${compressZip}"`);
305 | }
306 |
307 | compressZip = (compressZip === "true");
308 | } else {
309 | compressZip = downloadForm.compressZip.checked;
310 | }
311 |
312 | if (compressionLevel !== null) {
313 | try {
314 | compressionLevel = parseInt(compressionLevel);
315 | } catch (err) {
316 | log(`[!] Error: Failed to parse \`compressionLevel\` query: ${err}`, "\n\n");
317 | log(usageMsg, "\n", false);
318 | return;
319 | }
320 |
321 | if (compressionLevel > 9 || compressionLevel < 1) {
322 | log(`[!] Error: The \`compressionLevel\` query must be a value between 1 and 9, got ${compressionLevel}`, "\n\n");
323 | log(usageMsg, "\n", false);
324 | return;
325 | }
326 | } else {
327 | compressionLevel = downloadForm.compressionLevel.value; // Only applies to when `compressZip` is true aswell
328 | }
329 |
330 | // At this point, we expect `binaryType` to be defined
331 | if (! binaryType) {
332 | log("[!] Error: Missing required \`binaryType\` query, are you using an old perm link for a specific version?", "\n\n");
333 | log(usageMsg, "\n", false);
334 | return;
335 | }
336 |
337 | if (binaryType in binaryTypes) {
338 | const binaryTypeObject = binaryTypes[binaryType];
339 |
340 | // If `blobDir` has already been defined by the user, we don't want to override it here
341 | if (! blobDir) {
342 | blobDir = binaryTypeObject.blobDir;
343 | }
344 | } else {
345 | log(`[!] Error: \`binaryType\` "${binaryType}" not supported. See below for supported \`binaryType\` inputs:`, "\n\n");
346 | log(usageMsg);
347 | return;
348 | }
349 |
350 | if (version) {
351 | fetchManifest();
352 | } else {
353 | const binaryTypeEncoded = escHtml(binaryType);
354 | const channelNameEncoded = escHtml(channel);
355 |
356 | const clientSettingsUrl = `https://clientsettings.roblox.com/v2/client-version/${binaryTypeEncoded}/channel/${channelNameEncoded}`;
357 | log("Copy the version hash (the area with \"version-xxxxxxxxxxxxxxxx\" in double-quotes) from the page in the link below (we can't because of CORS), and paste it in the field named \"Version Hash\" in the form above\n");
358 | consoleText.innerHTML += `${clientSettingsUrl}
`;
359 |
360 | // Same options as may have been input from the page before
361 | downloadForm.channel.value = channelNameEncoded;
362 | downloadForm.binaryType.value = binaryTypeEncoded;
363 | downloadForm.compressZip.checked = compressZip;
364 | downloadForm.compressionLevel.value = compressionLevel;
365 |
366 | downloadFormDiv.hidden = false;
367 |
368 | return;
369 | }
370 | };
371 |
372 | async function fetchManifest() {
373 | versionPath = `${channelPath}${blobDir}${version}-`;
374 |
375 | if (binaryType === "MacPlayer" || binaryType === "MacStudio") {
376 | const zipFileName = (binaryType == "MacPlayer" && "RobloxPlayer.zip") || (binaryType == "MacStudio" && "RobloxStudioApp.zip");
377 | log(`[+] Fetching zip archive for BinaryType "${binaryType}" (${zipFileName})`);
378 |
379 | const outputFileName = `${channel}-${binaryType}-${version}.zip`;
380 | log(`[+] (Please wait!) Downloading ${outputFileName}..`, "");
381 |
382 | requestBinary(versionPath + zipFileName, function(zipData) {
383 | log("done!");
384 | downloadBinaryFile(outputFileName, zipData);
385 | });
386 | } else {
387 | // Now, we're only dealing with Windows bin logic
388 | log(`[+] Fetching rbxPkgManifest for ${version}@${channel}..`);
389 |
390 | // TODO: This is terrible, just a temp fix so we don't get 5 billion issue reports for not supporting /channel/common/
391 | var manifestBody = "";
392 | {
393 | var resp = await fetch(versionPath + "rbxPkgManifest.txt");
394 | if (! resp.ok) {
395 | channelPath = `${hostPath}/channel/common`;
396 | versionPath = `${channelPath}${blobDir}${version}-`;
397 |
398 | resp = await fetch(versionPath + "rbxPkgManifest.txt");
399 | }
400 |
401 | if (! resp.ok) {
402 | log(`[!] Failed to fetch rbxPkgManifest: (status: ${resp.status}, err: ${(await resp.text()) || ""})`);
403 | return;
404 | }
405 |
406 | manifestBody = await resp.text();
407 | }
408 |
409 | downloadZipsFromManifest(manifestBody);
410 | }
411 | };
412 |
413 | async function downloadZipsFromManifest(manifestBody) {
414 | const pkgManifestLines = manifestBody.split("\n").map(line => line.trim());
415 |
416 | if (pkgManifestLines[0] !== "v0") {
417 | log(`[!] Error: unknown rbxPkgManifest format version; expected "v0", got "${pkgManifestLines[0]}"`);
418 | return;
419 | }
420 |
421 | if (pkgManifestLines.includes("RobloxApp.zip")) {
422 | binExtractRoots = extractRoots.player;
423 |
424 | if (binaryType === "WindowsStudio64") {
425 | log(`[!] Error: BinaryType \`${binaryType}\` given, but "RobloxApp.zip" was found in the manifest!`);
426 | return;
427 | }
428 | } else if (pkgManifestLines.includes("RobloxStudio.zip")) {
429 | binExtractRoots = extractRoots.studio;
430 |
431 | if (binaryType === "WindowsPlayer") {
432 | log(`[!] Error: BinaryType \`${binaryType}\` given, but "RobloxStudio.zip" was found in the manifest!`);
433 | return;
434 | }
435 | } else {
436 | log("[!] Error: Bad/unrecognized rbxPkgManifest, aborting");
437 | return;
438 | }
439 |
440 | log(`[+] Fetching blobs for BinaryType \`${binaryType}\`..`);
441 |
442 | zip = new JSZip();
443 |
444 | // For both WindowsPlayer and WindowsStudio64
445 | zip.file("AppSettings.xml", `
446 |
447 | \tcontent
448 | \thttp://www.roblox.com
449 |
450 | `);
451 |
452 | let threadsLeft = 0;
453 |
454 | function doneCallback() {
455 | threadsLeft -= 1;
456 | };
457 |
458 | function getThreadsLeft() {
459 | return threadsLeft - 1;
460 | };
461 |
462 | for (const index in pkgManifestLines) {
463 | const pkgManifestLine = pkgManifestLines[index];
464 | if (! pkgManifestLine.endsWith(".zip")) {
465 | // Not a package in the manifest. Should I be using the checksum? Yes.. I'll do it later. Maybe.
466 | continue;
467 | }
468 |
469 | threadsLeft += 1;
470 | downloadPackage(pkgManifestLine, doneCallback, getThreadsLeft);
471 | }
472 |
473 | function checkIfNoThreadsLeft() {
474 | if (threadsLeft > 0) {
475 | setTimeout(checkIfNoThreadsLeft, 250);
476 | return;
477 | }
478 |
479 | // Now, we can export and download the complete zip
480 | const outputFileName = `${channel}-${binaryType}-${version}.zip`;
481 | log();
482 | if (compressZip) {
483 | log(`[!] NOTE: Compressing final zip (with a compression level of ${compressionLevel}/9), this may take a minute`);
484 | }
485 |
486 | log(`[+] Exporting assembled zip file "${outputFileName}".. `, "");
487 |
488 | zip.generateAsync({
489 | type: "arraybuffer",
490 | compression: compressZip ? "DEFLATE" : "STORE",
491 | compressionOptions: {
492 | level: compressionLevel
493 | }
494 | }).then(function(outputZipData) {
495 | zip = null;
496 | log("done!");
497 | downloadBinaryFile(outputFileName, outputZipData);
498 | });
499 | };
500 |
501 | checkIfNoThreadsLeft();
502 | };
503 |
504 | async function downloadPackage(packageName, doneCallback, getThreadsLeft) {
505 | log(`[+] Fetching "${packageName}"..`);
506 | const blobUrl = versionPath + packageName;
507 |
508 | requestBinary(blobUrl, async function(blobData) {
509 | if (packageName in binExtractRoots == false) {
510 | log(`[*] Package name "${packageName}" not defined in extraction roots for BinaryType \`${binaryType}\`, skipping extraction! (THIS MAY MAKE THE ZIP OUTPUT INCOMPLETE, BE AWARE!)`);
511 | zip.file(packageName, blobData);
512 | log(`[+] Moved package "${packageName}" directly to the root folder`);
513 | doneCallback();
514 | return;
515 | }
516 |
517 | log(`[+] Extracting "${packageName}"..`);
518 | const extractRootFolder = binExtractRoots[packageName];
519 |
520 | await JSZip.loadAsync(blobData).then(async function(packageZip) {
521 | blobData = null;
522 | let fileGetPromises = [];
523 |
524 | packageZip.forEach(function(path, object) {
525 | if (path.endsWith("\\")) {
526 | // If it's a directory, skip
527 | return;
528 | }
529 |
530 | const fixedPath = path.replace(/\\/g, "/");
531 | const fileGetPromise = object.async("arraybuffer").then(function(data) {
532 | zip.file(extractRootFolder + fixedPath, data);
533 | });
534 |
535 | fileGetPromises.push(fileGetPromise);
536 | });
537 |
538 | await Promise.all(fileGetPromises);
539 | packageZip = null;
540 | });
541 |
542 | log(`[+] Extracted "${packageName}"! (Packages left: ${getThreadsLeft()})`);
543 | doneCallback();
544 | });
545 | };
546 |
--------------------------------------------------------------------------------