Commit fb4b940e authored by insert's avatar insert

Initial Commit

parents
bundle
dist
instances
node_modules
launcher.json
\ No newline at end of file
- Auth
- Interface
- Forge Installer
\ No newline at end of file
{
"name": "launcher",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron .",
"dev": "concurrently --kill-others \"tsc-watch\" \"webpack --watch\""
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.4.5",
"@types/deepmerge": "^2.2.0",
"@types/fs-extra": "^5.1.0",
"@types/got": "^9.4.4",
"@types/node": "^12.0.2",
"@types/yauzl": "^2.9.1",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-preset-jsx": "^1.0.0",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^2.0.2",
"concurrently": "^4.1.0",
"css-loader": "^2.1.1",
"electron": "^5.0.1",
"electron-reload": "^1.4.0",
"extract-loader": "^3.1.0",
"file-loader": "^3.0.1",
"html-loader": "^0.5.5",
"node-sass": "^4.12.0",
"resolve-url-loader": "^3.1.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"typescript": "^3.4.5",
"vue": "^2.6.10",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.31.0",
"webpack-cli": "^3.3.2"
},
"dependencies": {
"@mdi/font": "^3.6.95",
"deepmerge": "^3.2.0",
"electron-debug": "^3.0.0",
"find-java-home": "^0.2.0",
"fix-path": "^2.1.0",
"fs-extra": "^8.0.0",
"got": "^9.6.0",
"libminecraft": "^1.0.3",
"ts-loader": "^6.0.0"
}
}
import { readJSON, writeJSON } from 'fs-extra';
import deepMerge from 'deepmerge';
import { win } from '.';
import { Config, ConfigKeys } from './interfaces';
export const defaults: Config = {
java: {
path: 'java'
},
instances: {},
accounts: [],
selectedAccount: -1,
selectedInstance: ''
}
export function GetDefaults() { return Object.assign({}, defaults) }
export const CONFIG_FILE = 'launcher.json';
export class ConfigHandle {
config?: Config;
getConfig() {
return <Config> this.config;
}
async load() {
try {
this.config = await readJSON(CONFIG_FILE);
await this.populateMissing();
} catch (e) {
this.config = GetDefaults();
}
await this.save();
}
async populateMissing() {
this.config = deepMerge(GetDefaults(), this.getConfig());
}
async save() {
await writeJSON(CONFIG_FILE, this.config);
}
async set(keys: ConfigKeys) {
this.config = <Config> deepMerge(this.getConfig(), keys);
await this.save();
}
delete(key: any) {
// @ts-ignore
delete this.config[key];
}
get(key: string) {
let seg = key.split('.'), root = this.config;
// @ts-ignore
seg.forEach(key => root = root[key]);
return <any> root;
}
}
export const config = new ConfigHandle();
export function SyncConfig(e?: any) {
let args = ['config', 'sync', config.getConfig()];
if (e) {
e.reply(...args);
} else {
win && win.webContents.send(args.shift() as string, ...args);
}
}
\ No newline at end of file
import { app, BrowserWindow, ipcMain } from 'electron';
require('electron-reload')(__dirname);
require('electron-debug')({
showDevTools: false
});
import { LoadInstances, SyncInstances } from './instance';
import { config, SyncConfig } from './config';
import './windows/settings';
import './windows/account';
import './windows/create';
import './installer/versions';
require('fix-path')();
export let win: BrowserWindow | null;
app.on('ready', async () => {
await config.load();
LoadInstances();
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
additionalArguments: [
'main'
]
},
title: 'launcher alpha'
});
win.setMenu(null);
win.loadFile('dist/bundle/index.html');
win.on('closed', () => {
win = null;
});
});
ipcMain.on('sync', async (e: any) => {
SyncConfig(e);
SyncInstances(e);
e.reply('sync');
});
\ No newline at end of file
import { ipcMain } from 'electron';
import { GetVersionManifest, VersionManifest, ForgePage, GetForgeVersions } from 'libminecraft';
import { readdir } from 'fs-extra';
import { RESOURCE_PATH } from '../instance';
var manifestCache: VersionManifest | undefined;
var forgeCache: { [key: string]: ForgePage } = {};
async function GetVersionsList() {
return await readdir(RESOURCE_PATH.resolve('versions'));
}
ipcMain.on('version', async (e: any, ...args: any[]) => {
switch (args.shift()) {
case 'manifest':
{
if (!manifestCache) {
manifestCache = await GetVersionManifest();
}
let versions = await GetVersionsList();
manifestCache.versions.map(v => {
if (versions.indexOf(v.id) > -1) {
(<any> v).downloaded = true;
} else {
(<any> v).downloaded = false;
}
return v;
});
e.reply('version', 'manifest', manifestCache);
}
break;
case 'forge':
{
let target = args.shift();
if (!forgeCache[target]) {
try {
forgeCache[target] = await GetForgeVersions(target);
} catch (e) {
console.log(e);
forgeCache[target] = <any> {versions:[]};
}
}
let versions = await GetVersionsList();
forgeCache[target].versions.map(v => {
if (versions.indexOf(`${v.mcver}-forge-${v.id}`) > -1) {
(<any> v).downloaded = true;
} else {
(<any> v).downloaded = false;
}
return v;
});
e.reply('version', 'forge', target, forgeCache[target]);
}
break;
}
});
\ No newline at end of file
import { ipcMain } from 'electron';
import { MinecraftFolder, GetVersionMeta, Launch } from 'libminecraft';
import { config, SyncConfig } from './config';
import { win } from '.';
import { Instance, InstanceMap, ConfigInstance } from './interfaces';
export const RESOURCE_PATH = new MinecraftFolder('..', 'minecraft', '.minecraft');
export var instances: InstanceMap<Instance> = {};
export function LoadInstances() {
let cinstances: InstanceMap<ConfigInstance> = config.get('instances'), keys = Object.keys(cinstances);
for (let i=0;i<keys.length;i++) {
let id = keys[i];
let { name, icon, game_directory, version } = cinstances[keys[i]];
let instance: Instance = {
name, icon, version,
gameDirectory: new MinecraftFolder(game_directory),
isRunning: false
};
instances[id] = instance;
}
}
export function SaveInstances() {
let cinstances: InstanceMap<ConfigInstance> = {}, keys = Object.keys(instances);
for (let i=0;i<keys.length;i++) {
let id = keys[i];
let { name, icon, gameDirectory, version } = instances[id];
let instance: ConfigInstance = {
name, icon, version,
game_directory: gameDirectory.getRoot()
};
cinstances[id] = instance;
}
config.delete('instances');
config.set({
instances: cinstances
});
}
export function SyncInstances(e?: any) {
let args = ['instance', 'sync', instances];
if (e) {
e.reply(...args);
} else {
win && win.webContents.send(args.shift() as string, ...args);
}
}
let attemptStart: boolean;
ipcMain.on('instance', async (e: any, ...args: any[]) => {
switch (args.shift()) {
case 'select':
config.set({
selectedInstance: args.shift() as string
});
SyncConfig(e);
break;
case 'delete':
console.log('delete ' + args[0]);
config.set({
selectedInstance: ''
});
delete instances[args.shift()];
SaveInstances();
SyncConfig(e);
SyncInstances();
break;
case 'stop':
let handle = instances[args.shift()].handle;
if (handle) {
handle.kill();
}
break;
case 'start':
{
if (attemptStart) return;
attemptStart = true;
let id = args.shift(), instance = <Instance> instances[id as any];
if (!instance) throw new Error(`ERR: ${id} does not exist!`);
attemptStart = false;
if (instance.isRunning) return;
let version = await GetVersionMeta(RESOURCE_PATH, instance.version);
let launch = await Launch({
resourcePath: RESOURCE_PATH,
gameData: instance.gameDirectory,
java: config.get('java.path'),
version
});
instance.isRunning = true;
instance.handle = launch;
e.reply('instance', 'sync', instances);
if (launch.stdout) launch.stdout.on('data', (d: any) => console.log(d.toString()));
if (launch.stderr) launch.stderr.on('data', (d: any) => console.log(d.toString()));
launch.on('exit', () => {
instance.isRunning = false;
delete instance.handle;
e.reply('instance', 'sync', instances);
});
}
break;
}
});
\ No newline at end of file
import { ChildProcessWithoutNullStreams } from 'child_process';
import { MinecraftFolder } from 'libminecraft/dist/util/folder';
import { Account } from 'libminecraft/dist/auth/yggdrasil';
export { MinecraftFolder };
export interface ConfigInstance {
name: string
icon: string
game_directory: string
version: string
}
export interface Instance {
name: string
icon: string
gameDirectory: MinecraftFolder
version: string
isRunning: boolean
handle?: ChildProcessWithoutNullStreams
};
export type InstanceMap<T> = { [key: string]: T };
export interface Config {
java: {
path: string
}
instances: InstanceMap<ConfigInstance>
accounts: Account[]
selectedAccount: number
selectedInstance: string
}
export interface ConfigKeys {
java?: {
path?: string
}
instances?: InstanceMap<ConfigInstance>,
accounts?: Account[],
selectedAccount?: number
selectedInstance?: string
}
\ No newline at end of file
import { BrowserWindow, ipcMain, dialog } from 'electron';
import { win } from '..';
import { Authenticate } from 'libminecraft';
import { config, SyncConfig } from '../config';
export var settingsWin: BrowserWindow | null;
export function OpenAccountWindow() {
if (settingsWin) {
settingsWin.focus();
return;
}
settingsWin = new BrowserWindow({
width: 500,
height: 450,
webPreferences: {
nodeIntegration: true,
additionalArguments: [
'account'
]
},
title: 'accounts',
parent: win as BrowserWindow
});
settingsWin.setMenu(null);
settingsWin.loadFile('dist/bundle/account.html');
settingsWin.on('closed', () => {
settingsWin = null;
});
}
ipcMain.on('window', (e: any, ...args: any[]) => {
if (args.shift() === 'account')
OpenAccountWindow();
});
let req: boolean;
ipcMain.on('account', async (e: any, ...args: any[]) => {
switch (args.shift()) {
case 'add':
let username = args.shift().trim(), exists = false;
config.getConfig().accounts.forEach(account => {
if (account.username.toLowerCase() === username.toLowerCase())
exists = true;
});
if (exists) return;
if (req) return;
req = true;
let account = await Authenticate(username, args.shift().trim());
config.getConfig().accounts.push(account);
config.save();
req = false;
break;
case 'select':
config.set({
selectedAccount: parseInt(args.shift())
});
break;
case 'edit':
dialog.showMessageBox({
title: 'Not available.',
message: 'This feature is currently WIP.'
});
break;
case 'delete':
config.getConfig().accounts.splice(parseInt(args.shift()), 1);
config.save();
break;
}
SyncConfig(e);
SyncConfig();
});
\ No newline at end of file
import { BrowserWindow, ipcMain } from 'electron';
import { win } from '..';
export var createWin: BrowserWindow | null;
export function OpenCreationWindow() {
if (createWin) {
createWin.focus();
return;
}
createWin = new BrowserWindow({
width: 500,
height: 600,
webPreferences: {
nodeIntegration: true,
additionalArguments: [
'create'
]
},
title: 'Create an instance',
parent: win as BrowserWindow,
modal: true
});
createWin.setMenu(null);
createWin.loadFile('dist/bundle/create.html');
createWin.on('closed', () => {
createWin = null;
});
}
ipcMain.on('window', (e: any, ...args: any[]) => {
if (args.shift() === 'create')
OpenCreationWindow();
});
\ No newline at end of file
import { BrowserWindow, ipcMain } from 'electron';
import { instances } from '../instance';
export var windows: {
[key: string]: BrowserWindow
} = {};
export function OpenSettingsMenu(instance: string) {
if (windows[instance]) {
windows[instance].focus();
return;
}
let win: BrowserWindow | null = new BrowserWindow({
width: 300,
height: 600,
webPreferences: {
nodeIntegration: true,
additionalArguments: [
instance,
'settings'
]
},
title: 'settings for ' + instances[instance].name
});
windows[instance] = win;
win.setMenu(null);
win.loadFile('dist/bundle/settings.html');
win.on('closed', () => {
win = null;
delete windows[instance];
});
}
ipcMain.on('window', (e: any, ...args: any[]) => {
if (args.shift() === 'settings')
OpenSettingsMenu(args.shift());
});
\ No newline at end of file
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"exclude": [
"bundle"
]
}
\ No newline at end of file
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
let mode = 'development';
module.exports = {
target: 'electron-renderer',
plugins: mode === 'development' ? [] : [
new CleanWebpackPlugin(),
],
entry: {
'bundle': './bundle/index.ts',
'index': './bundle/index.html',
'settings': './bundle/windows/settings.html',
'account': './bundle/windows/account.html',
'create': './bundle/windows/create.html'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
'scss': 'vue-style-loader!css-loader!sass-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
}
}
},
{
test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@vue/babel-preset-jsx']
}
},
{
loader: 'ts-loader',
options: {
configFile: './tsconfig.json'
}
}
],
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]',
outputPath: 'assets'
}
},
{
test: /\.(html)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]'
}
},
'extract-loader',
{
loader: 'html-loader',
options: {
attrs: ['img:src', 'link:href']
}
}
]
},
{
test: /\.scss$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].css'
}
},
'extract-loader',
'css-loader',
'resolve-url-loader',
{
loader: 'sass-loader',
options: {
includePaths: [
path.resolve(__dirname, 'bundle')
],
sourceMap: true
}
}
]
},
{
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'file-loader',
options: {
useRelativePath: true,
name: '[name].[ext]',
outputPath: 'fonts/'
}
}]
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
output: {
path: path.resolve(__dirname, 'dist', 'bundle')
},
mode
};
\ No newline at end of file
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment