app: add a function for direct6 downloads

This commit is contained in:
Adam
2024-06-27 02:42:45 +01:00
parent 18f6fed386
commit 6fa0f0600a
7 changed files with 185 additions and 47 deletions

View File

Binary file not shown.

View File

File diff suppressed because one or more lines are too long

2
app/dist/index.html vendored
View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bolt Launcher</title>
<script type="module" crossorigin src="/assets/index-DtKEzot0.js"></script>
<script type="module" crossorigin src="/assets/index-B75594vm.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B5crSLm9.css">
</head>
<body

View File

@@ -1,39 +1,39 @@
{
"name": "vite_app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite build --watch --mode development",
"watch": "vite build --watch --mode development",
"build": "vite build",
"minify": "vite build --minify",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"test": "vitest",
"eslint": "eslint .",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.38",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.5.13",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"tailwindcss": "^3.4.4",
"tslib": "^2.6.2",
"typescript": "^5.4.3",
"typescript-eslint": "^7.4.0",
"vite": "^5.2.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.0"
}
"name": "vite_app",
"version": "0.0.0",
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.38",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.5.13",
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"tailwindcss": "^3.4.4",
"tslib": "^2.6.2",
"typescript": "^5.4.3",
"typescript-eslint": "^7.4.0",
"vite": "^5.2.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.0"
},
"private": true,
"scripts": {
"dev": "vite build --watch --mode development",
"watch": "vite build --watch --mode development",
"build": "vite build",
"minify": "vite build --minify",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"test": "vitest",
"eslint": "eslint .",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"type": "module"
}

View File

@@ -6,6 +6,7 @@ import {
type Auth,
type Character,
type GameClient,
type Direct6Token,
unwrap
} from '$lib/Util/interfaces';
import {
@@ -805,3 +806,130 @@ export function savePluginConfig(): void {
};
xml.send(JSON.stringify(get(pluginList)));
}
// download and attempt to launch an official windows or mac client
// (not the official linux client - that comes from a different url, see launchRS3Linux)
export function launchOfficialClient(
windows: boolean,
osrs: boolean,
jx_session_id: string,
jx_character_id: string,
jx_display_name: string
) {
saveConfig();
const launchPath: string = `/launch-${osrs ? 'osrs' : 'rs3'}-${windows ? 'exe' : 'macapp'}?`;
const metaPath: string = `${osrs ? 'osrs' : atob(boltSub.provider)}-${windows ? 'win' : 'mac'}`;
const primaryUrl: string = `${atob(boltSub.direct6_url)}${metaPath}/${metaPath}.json`
const launch = (hash?: string, exe?: Blob) => {
const xml = new XMLHttpRequest();
const params: Record<string, string> = {};
if (hash) params.hash = hash;
if (jx_session_id) params.jx_session_id = jx_session_id;
if (jx_character_id) params.jx_character_id = jx_character_id;
if (jx_display_name) params.jx_display_name = jx_display_name;
xml.open('POST', launchPath.concat(new URLSearchParams(params).toString()), true);
xml.setRequestHeader('Content-Type', 'application/octet-stream');
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
msg(`Game launch status: '${xml.responseText.trim()}'`);
if (xml.status == 200 && hash) {
// TODO: change installed version to `hash`
}
}
};
if (exe !== null) {
exe?.arrayBuffer().then((x) => xml.send(x));
} else {
xml.send();
}
};
// yes, there is way too much nesting happening here
const xml = new XMLHttpRequest();
xml.open('GET', primaryUrl, true);
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
if (xml.status == 200) {
const metaToken: Direct6Token = JSON.parse(atob(xml.responseText.split('.')[1])).environments.production;
// TODO: check here if metaToken.id matches the already-installed version, if it does, just launch straight away
msg(`Downloading client version ${metaToken.version}...`);
const catalogUrl: string = `${atob(boltSub.direct6_url)}${metaPath}/catalog/${metaToken.id}/catalog.json`;
const xml2 = new XMLHttpRequest();
xml2.open('GET', catalogUrl, true);
xml2.onreadystatechange = () => {
if (xml2.readyState == 4) {
if (xml2.status == 200) {
const catalog = JSON.parse(atob(xml2.responseText.split('.')[1]));
const metafileUrl = catalog.metafile.replace("http:", "https:");
const xml3 = new XMLHttpRequest();
xml3.open('GET', metafileUrl, true);
xml3.onreadystatechange = () => {
if (xml3.readyState == 4) {
if (xml3.status == 200) {
const metafile = JSON.parse(atob(xml3.responseText.split('.')[1]));
const chunk_promises: Promise<Blob>[] = metafile.pieces.digests.map((x: string) => {
const hex_chunk: string = atob(x).split("").map(c => c.charCodeAt(0).toString(16).padStart(2, "0")).join("");
const chunk_url: string = catalog.config.remote.baseUrl.replace("http:", "https:").concat(catalog.config.remote.pieceFormat.replace("{SubString:0,2,{TargetDigest}}", hex_chunk.substring(0, 2)).replace("{TargetDigest}", hex_chunk));
return new Promise((resolve, reject) => {
const xml = new XMLHttpRequest();
xml.open('GET', chunk_url, true);
xml.responseType = 'blob';
xml.onreadystatechange = () => {
if (xml.readyState == 4) {
if (xml.status == 200) {
const ds = new DecompressionStream("gzip");
new Response(xml.response.slice(6).stream().pipeThrough(ds)).blob().then(resolve);
} else {
reject(`Error from ${chunk_url}: ${xml.status}: ${xml.responseText}`);
}
}
}
xml.send();
});
});
// while all that stuff is downloading, let's read the file list and find the exe's location in the data blob
var exeOffset: number = 0;
var exeSize: number | null = null;
for (let i = 0; i < metafile.files.length; i += 1) {
const isTargetExe: boolean = windows ? metafile.files[i].name.endsWith(".exe") : metafile.files[i].name.includes(".app/Contents/MacOS/");
if (isTargetExe) {
if (exeSize !== null) {
err(`Error parsing ${metafileUrl}: file list has multiple possibilities for main exe`, false);
return;
} else {
exeSize = metafile.files[i].size;
}
} else if (exeSize === null) {
exeOffset += metafile.files[i].size;
}
}
if (exeSize === null) {
err(`Error parsing ${metafileUrl}: file list has no possibilities for main exe`, false);
return;
}
Promise.all(chunk_promises).then(x => {
const exeFile = new Blob(x).slice(exeOffset, exeOffset + <number>exeSize);
launch(metafile.id, exeFile);
});
} else {
err(`Error from ${metafileUrl}: ${xml3.status}: ${xml3.responseText}`, false);
}
}
};
xml3.send();
} else {
err(`Error from ${catalogUrl}: ${xml2.status}: ${xml2.responseText}`, false);
}
}
};
xml2.send();
} else {
err(`Error from ${primaryUrl}: ${xml.status}: ${xml.responseText}`, false);
}
}
};
xml.send();
}

View File

@@ -50,6 +50,7 @@ export interface Bolt {
profile_api: string;
shield_url: string;
content_url: string;
direct6_url: string;
default_config_uri: string;
games: Array<string>;
}
@@ -142,6 +143,14 @@ export interface SelectedPlay {
client?: Client;
}
// response token from the official "Direct6" URL
export interface Direct6Token {
id?: string;
version?: string;
promoteTime?: number;
scanTime?: number;
}
// connected game client for plugin management purposes
export interface GameClient {
uid: string;

View File

@@ -125,6 +125,7 @@ bool Browser::App::Execute(const CefString&, CefRefPtr<CefV8Value>, const CefV8V
retval->SetValue("shield_url", CefV8Value::CreateString("aHR0cHM6Ly9hdXRoLmphZ2V4LmNvbS9zaGllbGQvb2F1dGgvdG9rZW4"), V8_PROPERTY_ATTRIBUTE_READONLY);
retval->SetValue("content_url", CefV8Value::CreateString("aHR0cHM6Ly9jb250ZW50LnJ1bmVzY2FwZS5jb20vZG93bmxvYWRzL3VidW50dS8"), V8_PROPERTY_ATTRIBUTE_READONLY);
retval->SetValue("default_config_uri", CefV8Value::CreateString("aHR0cHM6Ly93d3cucnVuZXNjYXBlLmNvbS9rPTUvbD0wL2phdl9jb25maWcud3M"), V8_PROPERTY_ATTRIBUTE_READONLY);
retval->SetValue("direct6_url", CefV8Value::CreateString("aHR0cHM6Ly9qYWdleC5ha2FtYWl6ZWQubmV0L2RpcmVjdDYv"), V8_PROPERTY_ATTRIBUTE_READONLY);
CefRefPtr<CefV8Value> games = CefV8Value::CreateArray(2);
games->SetValue(0, CefV8Value::CreateString("UnVuZVNjYXBl"));