Write some info in README.md, design titlebar, make downloader and very basic instance checker
This commit is contained in:
parent
202e97ac5a
commit
daae340d1e
@ -1,3 +1,12 @@
|
|||||||
# OpenModLauncher-Base
|
# OpenModLauncher-Base
|
||||||
|
|
||||||
A small Minecraft launcher codebase
|
A small Minecraft launcher codebase
|
||||||
|
|
||||||
|
This is currently a work in progress for another project of mine but it can be used in other projects too as I try to be the least project-specific as I can be.
|
||||||
|
|
||||||
|
Please note I reused and rearranged a lot of code from a launcher I've programmed earlier this year.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
At the time of writing this README, this launcher can only download a Minecraft version specified in meta.json into a given directory.
|
||||||
|
Launching Minecraft, installing a mod loader on top of many versions and downloading custom files are the main planned features.
|
||||||
|
10
lib/checker.js
Normal file
10
lib/checker.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const path = require("node:path");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"check_install": function (directory) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
resolve(fs.existsSync(path.join(directory, "versions")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
146
lib/downloader.js
Normal file
146
lib/downloader.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
const path = require("node:path");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const fsPromises = require("node:fs/promises");
|
||||||
|
|
||||||
|
const httpsWrapper = require("./util/https.js");
|
||||||
|
const utilities = require("./util/functions.js");
|
||||||
|
|
||||||
|
let maxConcurrentDownloads = 16;
|
||||||
|
let downloadQueue = [];
|
||||||
|
let totalDownloads = 0;
|
||||||
|
let isDownloading = false;
|
||||||
|
let ongoingDownloads = 0;
|
||||||
|
|
||||||
|
function waitUntilDownloadsFinish() {
|
||||||
|
return new Promise(async function (resolve, reject) {
|
||||||
|
var downloadCountChecker = setInterval(function () {
|
||||||
|
if (ongoingDownloads < 1 && downloadQueue.length < 1 && !isDownloading) {
|
||||||
|
resolve(clearInterval(downloadCountChecker));
|
||||||
|
}
|
||||||
|
}, 256);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"download_file": function (url, filePath) {
|
||||||
|
return new Promise(async function (resolve, reject) {
|
||||||
|
var directory = path.dirname(filePath);
|
||||||
|
if (!(await fs.existsSync(directory))) await fsPromises.mkdir(directory, {"recursive": true});
|
||||||
|
|
||||||
|
var writeStream = fs.createWriteStream(filePath);
|
||||||
|
writeStream.on("finish", resolve);
|
||||||
|
|
||||||
|
httpsWrapper.getStream(url).then(function (response) {
|
||||||
|
response.pipe(writeStream);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
"process_queue": function (progressCB) {
|
||||||
|
return new Promise(async function (resolve, reject) {
|
||||||
|
totalDownloads = downloadQueue.length;
|
||||||
|
if (downloadQueue.length > 0) isDownloading = true;
|
||||||
|
|
||||||
|
if (progressCB) {
|
||||||
|
progressCB(0.0, 0, downloadQueue.length, totalDownloads);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (downloadQueue.length > 0) {
|
||||||
|
if (ongoingDownloads < maxConcurrentDownloads) {
|
||||||
|
setImmediate(function (downloadData) {
|
||||||
|
module.exports.download_file(downloadData.url, downloadData.path).then(function () {
|
||||||
|
ongoingDownloads--;
|
||||||
|
if (downloadQueue.length < 1) {
|
||||||
|
isDownloading = false;
|
||||||
|
}
|
||||||
|
}).catch(function (error) {
|
||||||
|
ongoingDownloads--;
|
||||||
|
downloadQueue.push(downloadData);
|
||||||
|
});
|
||||||
|
}, downloadQueue.shift());
|
||||||
|
ongoingDownloads++;
|
||||||
|
let progress = (totalDownloads - (downloadQueue.length + ongoingDownloads)) / totalDownloads;
|
||||||
|
if (progressCB) {
|
||||||
|
progressCB(progress, ongoingDownloads, downloadQueue.length, totalDownloads);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await utilities.sleep(256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitUntilDownloadsFinish().then(function () {
|
||||||
|
if (progressCB) {
|
||||||
|
progressCB(1.0, 0, 0, totalDownloads);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
"download_minecraft": function (directory, meta, manifestSource, progressCB) {
|
||||||
|
return new Promise(async function (resolve, reject) {
|
||||||
|
let gameManifest = JSON.parse(await httpsWrapper.get(manifestSource["versions_manifest"]));
|
||||||
|
|
||||||
|
let versionInManifest = gameManifest.versions.find(ver => ver.id == meta["version"]);
|
||||||
|
|
||||||
|
if (!versionInManifest) {
|
||||||
|
return reject(new Error("Minecraft version \"" + meta["version"] + "\" could not be found in given source. "));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(directory)) await fsPromises.mkdir(directory, {"recursive": true});
|
||||||
|
|
||||||
|
let versionDirectory = path.join(directory, "versions", meta["version"]);
|
||||||
|
|
||||||
|
if (!fs.existsSync(versionDirectory)) await fsPromises.mkdir(versionDirectory, {"recursive": true});
|
||||||
|
let versionManifest = JSON.parse(await httpsWrapper.get(versionInManifest.url));
|
||||||
|
fs.writeFileSync(path.join(versionDirectory, meta["version"] + ".json"), JSON.stringify(versionManifest, null, "\t"));
|
||||||
|
|
||||||
|
let librariesDirectory = path.join(directory, "libraries");
|
||||||
|
|
||||||
|
for (let l = 0; l < versionManifest.libraries.length; l++) {
|
||||||
|
let library = versionManifest.libraries[l];
|
||||||
|
|
||||||
|
if ("classifiers" in library.downloads) continue; // Attempt at skipping platform-specific code, hoping the game still starts.
|
||||||
|
|
||||||
|
let libraryFilePath = path.join(librariesDirectory, ...(library.downloads.artifact.path.split("/")));
|
||||||
|
|
||||||
|
downloadQueue.push({
|
||||||
|
"url": library.downloads.artifact.url,
|
||||||
|
"path": libraryFilePath
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let assetsDirectory = path.join(directory, "assets");
|
||||||
|
|
||||||
|
let assetIndexesDirectory = path.join(assetsDirectory, "indexes");
|
||||||
|
if (!fs.existsSync(assetIndexesDirectory)) await fsPromises.mkdir(assetIndexesDirectory, {"recursive": true});
|
||||||
|
|
||||||
|
let assetIndexPath = path.join(assetIndexesDirectory, versionManifest.assetIndex.id + ".json");
|
||||||
|
let assetIndex = JSON.parse(await httpsWrapper.get(versionManifest.assetIndex.url));
|
||||||
|
fs.writeFileSync(assetIndexPath, JSON.stringify(assetIndex, null, "\t"));
|
||||||
|
|
||||||
|
let assetObjectsDirectory = path.join(assetsDirectory, "objects");
|
||||||
|
for (const assetLocation in assetIndex.objects) {
|
||||||
|
if (assetIndex.objects.hasOwnProperty(assetLocation)) {
|
||||||
|
let asset = assetIndex.objects[assetLocation];
|
||||||
|
|
||||||
|
let assetPath = [
|
||||||
|
asset.hash.substring(0, 2), asset.hash
|
||||||
|
];
|
||||||
|
|
||||||
|
downloadQueue.push({
|
||||||
|
"url": manifestSource["asset_base"] + assetPath.join("/"),
|
||||||
|
"path": path.join(assetObjectsDirectory, ...assetPath)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadQueue.push({
|
||||||
|
"url": versionManifest.downloads.client.url,
|
||||||
|
"path": path.join(versionDirectory, meta["version"] + ".jar")
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDownloading) await module.exports.process_queue(progressCB);
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
7
lib/util/functions.js
Normal file
7
lib/util/functions.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
"sleep": function (delay) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
setTimeout(resolve, delay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
35
lib/util/https.js
Normal file
35
lib/util/https.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"get": function (url) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var request = https.get(url, function (response) {
|
||||||
|
if (response.statusCode != 200) return reject(new Error("Response status code wasn't 200. "));
|
||||||
|
|
||||||
|
response.setEncoding("utf-8");
|
||||||
|
let body = "";
|
||||||
|
|
||||||
|
response.on("data", function (chunk) {
|
||||||
|
body += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
response.on("end", function () {
|
||||||
|
resolve(body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", reject);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
"getStream": function (url) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var request = https.get(url, function (response) {
|
||||||
|
if (response.statusCode != 200) return reject(new Error("Response status code wasn't 200"));
|
||||||
|
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
60
main.js
60
main.js
@ -1,8 +1,49 @@
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const child_process = require("node:child_process");
|
const fs = require("node:fs");
|
||||||
|
const os = require("node:os");
|
||||||
|
|
||||||
const electron = require("electron");
|
const electron = require("electron");
|
||||||
|
|
||||||
|
const downloader = require("./lib/downloader.js");
|
||||||
|
const checker = require("./lib/checker.js");
|
||||||
|
|
||||||
|
let meta = JSON.parse(fs.readFileSync(path.join(__dirname, "meta.json"), "utf-8"));
|
||||||
|
let sources = JSON.parse(fs.readFileSync(path.join(__dirname, "sources.json"), "utf-8"));
|
||||||
|
|
||||||
|
let osPlatform = os.platform();
|
||||||
|
let userHome = os.homedir();
|
||||||
|
|
||||||
|
let gameDirectory;
|
||||||
|
switch (osPlatform) {
|
||||||
|
case "win32":
|
||||||
|
gameDirectory = path.join(userHome, "AppData", "Roaming", ...(meta["install_directory"]).split("/"));
|
||||||
|
break;
|
||||||
|
case "darwin":
|
||||||
|
gameDirectory = path.join(userHome, "Library", "Application Support", ...(meta["install_directory"]).split("/"));
|
||||||
|
break;
|
||||||
|
case "linux":
|
||||||
|
gameDirectory = path.join(userHome, ...(meta["install_directory"]).split("/"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
gameDirectory = path.join(userHome, ...(meta["install_directory"]).split("/"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let window;
|
||||||
|
|
||||||
|
function showProgress(progress, ongoing, remaining, total) {
|
||||||
|
window.setProgressBar(progress);
|
||||||
|
|
||||||
|
window.webContents.send("event", {
|
||||||
|
"type": "download-progress",
|
||||||
|
"data": {
|
||||||
|
"ongoing": ongoing,
|
||||||
|
"remaining": remaining,
|
||||||
|
"total": total
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
electron.app.on("ready", function () {
|
electron.app.on("ready", function () {
|
||||||
window = new electron.BrowserWindow({
|
window = new electron.BrowserWindow({
|
||||||
"width": 960,
|
"width": 960,
|
||||||
@ -14,7 +55,7 @@ electron.app.on("ready", function () {
|
|||||||
},
|
},
|
||||||
"title": "OpenModLauncher",
|
"title": "OpenModLauncher",
|
||||||
"icon": path.join(__dirname, "res", "img", "logo.png"),
|
"icon": path.join(__dirname, "res", "img", "logo.png"),
|
||||||
"frame": true,
|
"frame": false,
|
||||||
"transparent": true,
|
"transparent": true,
|
||||||
"resizable": false,
|
"resizable": false,
|
||||||
"show": false
|
"show": false
|
||||||
@ -25,6 +66,21 @@ electron.app.on("ready", function () {
|
|||||||
window.on("ready-to-show", window.show);
|
window.on("ready-to-show", window.show);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
electron.ipcMain.handle("minimize", function (event) {
|
||||||
|
return window.minimize();
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.ipcMain.handle("close", function (event) {
|
||||||
|
return window.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.ipcMain.handle("is-installed", function (event) {
|
||||||
|
return checker.check_install(gameDirectory);
|
||||||
|
});
|
||||||
|
electron.ipcMain.handle("install", function (event) {
|
||||||
|
return downloader.download_minecraft(gameDirectory, meta["minecraft"], sources["minecraft"], showProgress);
|
||||||
|
});
|
||||||
|
|
||||||
electron.app.on("window-all-closed", function () {
|
electron.app.on("window-all-closed", function () {
|
||||||
electron.app.quit();
|
electron.app.quit();
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"install_directory": "games/.openmod",
|
||||||
"minecraft": {
|
"minecraft": {
|
||||||
"version": "${MINECRAFT_VERSION}"
|
"version": "1.7.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
preload.js
18
preload.js
@ -0,0 +1,18 @@
|
|||||||
|
const electron = require("electron");
|
||||||
|
|
||||||
|
electron.contextBridge.exposeInMainWorld("minimizeWindow", function () {
|
||||||
|
return electron.ipcRenderer.invoke("minimize");
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.contextBridge.exposeInMainWorld("closeWindow", function () {
|
||||||
|
return electron.ipcRenderer.invoke("close");
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.contextBridge.exposeInMainWorld("game", {
|
||||||
|
"isInstalled": function () {
|
||||||
|
return electron.ipcRenderer.invoke("is-installed");
|
||||||
|
},
|
||||||
|
"install": function () {
|
||||||
|
return electron.ipcRenderer.invoke("install");
|
||||||
|
}
|
||||||
|
});
|
47
res/css/main.css
Normal file
47
res/css/main.css
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
:root {
|
||||||
|
color: rgba(248, 248, 248, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#titlebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
background-color: rgba(16, 16, 16, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#window-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-action {
|
||||||
|
background-color: rgba(24, 24, 32, 1.0);
|
||||||
|
width: 32px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-action:hover {
|
||||||
|
background-color: rgba(48, 48, 64, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#window-close {
|
||||||
|
background-color: rgba(128, 0, 0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#window-close:hover {
|
||||||
|
background-color: rgba(248, 0, 0, 1.0);
|
||||||
|
}
|
@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"minecraft": {
|
"minecraft": {
|
||||||
"versions_manifest": "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
|
"versions_manifest": "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json",
|
||||||
|
"asset_base": "https://resources.download.minecraft.net/"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"files_manifest": "${CUSTOM_MANIFEST_URL}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,20 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>OpenModLauncher</title>
|
<title>OpenModLauncher</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../res/css/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="titlebar">
|
<div id="titlebar">
|
||||||
|
<span>OpenModLauncher</span>
|
||||||
|
<div id="window-actions">
|
||||||
|
<div class="window-action" id="window-minimize" onclick="minimizeWindow();">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="window-action" id="window-close" onclick="closeWindow();">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user