const {
app,
clipboard,
dialog,
ipcMain,
protocol,
shell,
BrowserWindow,
nativeImage,
Menu,
} = require('electron');
const shortcut = require('electron-localshortcut');
const Store = require('electron-store');
Store.initRenderer();
const config = new Store();
const path = require('path');
const { autoUpdate } = require('./features');
const https = require('https');
const log = require('electron-log');
const fse = require('fs-extra');
const md5File = require('md5-file');
const fs = require('fs');
const { checkFileExists } = require('./features/const');
Menu.setApplicationMenu(null);
const launcherMode = config.get('launcherMode', true);
const performanceMode = config.get('performanceMode', false);
const gamePreload = path.join(__dirname, 'preload', 'global.js');
const settingsPreload = path.join(__dirname, 'preload', 'settings.js');
const launcherPreload = path.join(__dirname, 'preload', 'launcher.js');
let JSZip, pluginLoader;
if (!performanceMode || launcherMode) {
JSZip = require('jszip');
pluginLoader = require('./features/plugins').pluginLoader;
}
process.env.ELECTRON_ENABLE_LOGGING = '1';
log.info(`
------------------------------------------
Starting KirkaClient ${app.getVersion()}.
Epoch Time: ${Date.now()} | ${(new Date()).toString()}
User: ${config.get('user')}
UserID: ${config.get('userID')}
Directory: ${__dirname}
Electron Version: ${process.versions.electron}
Chromium Version: ${process.versions.chrome}
`);
let mainWindow;
let settingsWindow;
let launcherWindow;
let launchMainClient = false;
let CtrlW = false;
const allowedScripts = [];
const installedPlugins = [];
const scriptCol = [];
const pluginIdentifier = {};
const pluginIdentifier2 = {};
let pluginsLoaded = false;
const icons = {
linux: path.join(__dirname, 'media', 'icon.png'),
win32: path.join(__dirname, 'media', 'icon.ico'),
darwin: path.join(__dirname, 'media', 'icon.icns')
};
const icon = icons[process.platform];
protocol.registerSchemesAsPrivileged([{
scheme: 'kirkaclient',
privileges: { secure: true, corsEnabled: true },
}]);
if (config.get('unlimitedFPS', false) && !launcherMode) {
app.commandLine.appendSwitch('disable-frame-rate-limit');
app.commandLine.appendSwitch('disable-gpu-vsync');
}
app.commandLine.appendSwitch('ignore-gpu-blacklist');
app.allowRendererProcessReuse = true;
async function askUserToUpdate() {
const options = {
type: 'info',
title: 'Update Available',
message: 'KirkaClient has been completely rewritten, and is a lot faster and better. Please download the new ' +
'version from https://client.kirka.io. Click Ok to continue to the download page.',
buttons: ['Ok']
};
await dialog.showMessageBox(options);
await shell.openExternal('https://client.kirka.io');
app.quit();
}
async function createWindow() {
log.info('Creating main window');
mainWindow = new BrowserWindow({
width: 1280,
height: 720,
backgroundColor: '#000000',
titleBarStyle: 'hidden',
show: true,
title: `KirkaClient v${app.getVersion()}`,
acceptFirstMouse: true,
icon: nativeImage.createFromPath(icon),
webPreferences: {
preload: gamePreload,
devTools: !app.isPackaged
},
});
createShortcutKeys();
await initAutoUpdater(mainWindow.webContents);
mainWindow.on('close', function(e) {
if (CtrlW) {
e.preventDefault();
CtrlW = false;
return;
}
app.quit();
});
if (config.get('fullScreenStart', true))
mainWindow.setFullScreen(true);
mainWindow.webContents.on('new-window', (e, url) => {
e.preventDefault();
mainWindow.loadURL(url);
});
await mainWindow.loadURL('https://kirka.io/');
}
function createShortcutKeys() {
const contents = mainWindow.webContents;
shortcut.register(mainWindow, 'Escape', () => contents.executeJavaScript('document.exitPointerLock()', true));
shortcut.register(mainWindow, 'F4', () => clipboard.writeText(contents.getURL()));
shortcut.register(mainWindow, 'F5', () => contents.reload());
shortcut.register(mainWindow, 'Shift+F5', () => contents.reloadIgnoringCache());
shortcut.register(mainWindow, 'F6', () => joinByURL());
shortcut.register(mainWindow, 'F8', () => mainWindow.loadURL('https://kirka.io/'));
shortcut.register(mainWindow, 'F11', () => mainWindow.setFullScreen(!mainWindow.isFullScreen()));
// electronLocalshortcut.register(win, 'Control+Alt+C', () => clearCache());
if (config.get('controlW', true))
shortcut.register(mainWindow, 'Control+W', () => { CtrlW = true; });
}
async function createLauncherWindow() {
log.info('creating launcher window');
log.info('launcher preload', launcherPreload);
log.info('icon', icon);
launcherWindow = new BrowserWindow({
width: 1280,
height: 720,
backgroundColor: '#000000',
show: true,
title: 'KirkaClient Launcher',
icon: nativeImage.createFromPath(icon),
webPreferences: {
preload: launcherPreload,
devTools: !app.isPackaged
},
});
await launcherWindow.loadFile(path.join(__dirname, 'launcher/launcher.html'));
launcherWindow.webContents.openDevTools();
await initPlugins(launcherWindow.webContents);
await initAutoUpdater(launcherWindow.webContents);
ipcMain.on('launchClient', () => {
launchMainClient = true;
app.quit();
});
ipcMain.on('launchSettings', createSettings);
const req = https.get('https://client.kirka.io/changelogs', (res) => {
res.setEncoding('utf8');
let rawData = '';
res.on('data', (chunk) => {
rawData += chunk;
});
res.on('end', async() => {
try {
const changelog = JSON.parse(rawData);
launcherWindow.webContents.send('changeLogs', changelog);
} catch (e) {
log.error(e.message);
}
});
});
req.on('error', (e) => {
log.error(e.message);
});
req.end();
}
ipcMain.on('joinLink', joinByURL);
async function joinByURL() {
const urld = clipboard.readText();
if (urld.startsWith('https://kirka.io/games/'))
await mainWindow.loadURL(urld);
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin')
app.quit();
});
async function initAutoUpdater(webContents) {
const req = https.get('https://client.kirka.io/api/v4', (res) => {
res.setEncoding('utf8');
let rawData = '';
res.on('data', (chunk) => {
rawData += chunk;
});
res.on('end', async() => {
try {
const updateContent = JSON.parse(rawData);
const didUpdate = await autoUpdate(webContents, updateContent);
log.info(didUpdate);
if (didUpdate) {
config.set('update', true);
const options = {
buttons: ['Ok'],
message: 'Update Complete! Client will now restart.'
};
await dialog.showMessageBox(options);
rebootClient();
}
} catch (e) {
log.error(e.message);
}
});
});
req.on('error', (e) => {
log.error(e.message);
});
req.end();
}
ipcMain.on('show-settings', async function() {
if (settingsWindow) {
settingsWindow.focus();
return;
}
await createSettings();
});
ipcMain.on('reboot', () => {
rebootClient();
});
ipcMain.on('installedPlugins', (ev) => {
ev.returnValue = JSON.stringify(installedPlugins);
});
ipcMain.handle('allowedScripts', () => {
return JSON.stringify(pluginIdentifier);
});
ipcMain.handle('scriptPath', () => {
return path.join(app.getPath('appData'), '/kirkaclient/plugins');
});
ipcMain.handle('ensureIntegrity', async function() {
await ensureIntegrity();
return JSON.stringify(allowedScripts);
});
ipcMain.handle('canLoadPlugins', () => {
return pluginsLoaded;
});
ipcMain.handle('downloadPlugin', async(ev, uuid) => {
log.info('[PLUGINS] Need to download', uuid);
return await downloadPlugin(uuid);
});
ipcMain.handle('uninstallPlugin', async(ev, uuid) => {
log.info('[PLUGINS] Need to remove', uuid);
if (!pluginIdentifier[uuid])
return { success: false };
const scriptPath = pluginIdentifier[uuid][1];
await fse.remove(scriptPath);
installedPlugins.splice(installedPlugins.indexOf(uuid), 1);
return { success: true };
});
ipcMain.handle('ask-confirmation', async(ev, title, message, details) => {
const response = await dialog.showMessageBox({
title: title,
message: message,
detail: details,
type: 'question',
buttons: ['Ok', 'Cancel'],
defaultId: 0,
cancelId: 1
});
return response.response;
});
ipcMain.handle('getDirectories', async(ev, source) => {
return await getDirectories(source);
});
async function installUpdate(pluginPath, uuid) {
try {
await fse.remove(pluginPath);
} catch (e) {
log.info(e);
}
await downloadPlugin(uuid);
}
async function unzipFile(zip) {
const fileBuffer = await fse.readFile(zip);
const pluginPath = path.join(app.getPath('appData'), '/kirkaclient/plugins');
const newZip = new JSZip();
const contents = await newZip.loadAsync(fileBuffer);
for (const filename of Object.keys(contents.files)) {
const content = await newZip.file(filename).async('nodebuffer');
const dest = path.join(pluginPath, filename);
await fse.ensureDir(path.dirname(dest));
await fse.writeFile(dest, content);
}
}
async function downloadPlugin(uuid) {
return await new Promise(resolve => {
const req = https.get(`https://client.kirka.io/api/v4/plugins/download/${uuid}?token=${encodeURIComponent(config.get('devToken'))}`, (res) => {
res.setEncoding('binary');
let chunks = '';
log.info(`[PLUGINS] Update GET code: ${res.statusCode}`);
if (res.statusCode !== 200)
return resolve(false);
const filename = res.headers['filename'];
res.on('data', (chunk) => {
chunks += chunk;
});
res.on('end', async() => {
try {
const pluginsDir = path.join(app.getPath('appData'), '/kirkaclient/plugins/', filename);
await fse.writeFile(pluginsDir, chunks, 'binary');
await unzipFile(pluginsDir);
await fse.remove(pluginsDir);
log.info(`[PLUGINS] File ${filename} downloaded`);
resolve(true);
} catch (e) {
log.error(e);
resolve(false);
}
});
});
req.on('error', error => {
log.error(`[PLUGINS] Download Error: ${error}`);
resolve(false);
});
req.end();
});
}
function ensureScriptIntegrity(filePath, scriptUUID) {
if (!app.isPackaged)
return { success: true };
return new Promise((resolve, reject) => {
const hash = md5File.sync(filePath);
const data = { hash: hash, uuid: scriptUUID };
const request = {
method: 'POST',
hostname: 'client.kirka.io',
path: '/api/v4/plugins/updates',
headers: {
'Content-Type': 'application/json'
},
};
const req = https.request(request, res => {
res.setEncoding('utf-8');
let chunks = '';
log.info(`[PLUGINS] Integrity check POST: ${res.statusCode} with payload ${JSON.stringify(data)}`);
if (res.statusCode !== 200) {
if (!app.isPackaged)
resolve({ success: false });
else
reject();
} else {
res.on('data', (chunk) => {
chunks += chunk;
});
res.on('end', () => {
const response = JSON.parse(chunks);
const success = response.success;
log.info(`Response on ${scriptUUID}: ${JSON.stringify(response, null, 2)}`);
if (!success)
reject();
resolve(response);
});
}
});
req.on('error', error => {
log.error(`POST Error: ${error}`);
reject();
});
req.write(JSON.stringify(data));
req.end();
});
}
async function ensureIntegrity() {
const oldAllowed = allowedScripts;
allowedScripts.length = 0;
const fileDir = path.join(app.getPath('appData'), 'kirkaclient', 'plugins');
await fse.ensureDir(fileDir);
for (const scriptPath in oldAllowed) {
try {
const scriptUUID = pluginIdentifier2[scriptPath];
await ensureScriptIntegrity(scriptPath, scriptUUID);
allowedScripts.push(scriptPath);
log.info(`Ensured script: ${scriptPath}`);
} catch (err) {
log.info(err);
}
}
}
async function copyFolder(from, to, webContents) {
let files;
try {
await fse.ensureDir(to);
} catch (err) {
log.info('[Copy Folder Error]:', err);
log.info('from:', from, 'to:', to);
return;
}
try {
files = await fs.promises.readdir(from);
} catch (err) {
log.info(err);
log.info(from, to);
return;
}
for (const file of files) {
const fromPath = path.join(from, file);
const toPath = path.join(to, file);
const stat = await fse.stat(fromPath);
if (stat.isDirectory())
await copyFolder(fromPath, toPath, webContents);
else {
try {
await fse.copyFile(fromPath, toPath);
} catch (err) {
log.info(err);
}
}
}
}
async function copyNodeModules(srcDir, node_modules, incomplete_init, webContents) {
// if (!app.isPackaged)
// return;
try {
await fse.remove(node_modules);
} catch (err) {
log.info(err);
}
await fse.mkdir(node_modules, { recursive: true });
await fse.writeFile(incomplete_init, 'DO NOT DELETE THIS!');
log.info('copying from', srcDir, 'to', node_modules);
webContents.send('copying');
await copyFolder(srcDir, node_modules, webContents);
log.info('copying done');
webContents.send('copyProgress');
await fse.unlink(incomplete_init);
}
async function getDirectories(source) {
return (await fse.readdir(source, { withFileTypes: true }))
.filter(dirent => dirent.isDirectory() && dirent.name !== 'node_modules')
.map(dirent => dirent.name);
}
async function initPlugins(webContents) {
const fileDir = path.join(app.getPath('appData'), 'kirkaclient', 'plugins');
log.info('fileDir', fileDir);
const node_modules = path.join(fileDir, 'node_modules');
const srcDir = path.join(__dirname, '../node_modules');
const incomplete_init = path.join(fileDir, 'node_modules.lock');
try {
await fse.mkdir(fileDir);
} catch (err) {
log.info(err);
}
if (!await checkFileExists(node_modules) || await checkFileExists(incomplete_init)) {
webContents.send('message', 'Configuring Plugins...');
await copyNodeModules(srcDir, node_modules, incomplete_init, webContents);
}
log.info('node_modules stuff done.');
log.info(await fse.readdir(fileDir));
const filenames = [];
// get all directories inside a direcotry
const dirs = await getDirectories(fileDir);
log.info(dirs);
for (const dir of dirs) {
log.info(dir);
const packageFile = path.join(fileDir, dir, 'package.json');
if (await checkFileExists(packageFile)) {
const packageJson = JSON.parse((await fse.readFile(packageFile)).toString());
filenames.push([dir, packageJson]);
} else
log.info('No package.json');
}
log.info('filenames', filenames);
if (filenames.length === 0)
webContents.send('pluginProgress', 0, 0, 0);
let count = 0;
for (const [dir, packageJson] of filenames) {
try {
count += 1;
const pluginName = packageJson.name;
const pluginPath = path.join(fileDir, dir);
const scriptPath = path.join(pluginPath, packageJson.main);
const pluginUUID = packageJson.uuid;
const pluginVer = packageJson.version;
webContents.send('message', `Loading ${pluginName} v${pluginVer} (${count}/${filenames.length})`);
log.info('scriptPath:', scriptPath);
const data = await ensureScriptIntegrity(scriptPath, pluginUUID);
log.debug(data);
if (data) {
if (data.update) {
webContents.send('message', 'Updating Plugin');
await installUpdate(pluginPath, pluginUUID);
webContents.send('message', `Reloading Plugin: ${count}/${filenames.length}`);
}
}
log.debug(packageJson);
let script = await pluginLoader(pluginUUID, dir, packageJson);
if (Array.isArray(script)) {
webContents.send('message', 'Cache corrupted. Rebuilding...');
await copyNodeModules(srcDir, node_modules, incomplete_init, webContents);
script = await pluginLoader(pluginUUID, dir, packageJson, false, true);
}
if (Array.isArray(script))
continue;
if (!script.isPlatformMatching())
log.info(`Script ignored, platform not matching: ${script.scriptName}`);
else {
allowedScripts.push(scriptPath);
installedPlugins.push(script.scriptUUID);
pluginIdentifier[script.scriptUUID] = [script.scriptName, pluginPath];
pluginIdentifier2[script.scriptName] = script.scriptUUID;
scriptCol.push(script);
try {
log.debug('[PLUGIN]:', script.scriptName, 'launching main');
script.launchMain(mainWindow);
} catch (err) {
log.info(err);
dialog.showErrorBox(`Error in ${script.scriptName} Plugin. Uninstall it to skip this dialog.`, err);
}
log.info(`Loaded script: ${script.scriptName}- v${script.version}`);
webContents.send('pluginProgress', filenames.length, count, ((count / filenames.length) * 100).toFixed(0));
}
} catch (err) {
log.info(err);
}
}
pluginsLoaded = true;
}
async function createSettings() {
settingsWindow = new BrowserWindow({
width: 1280,
height: 720,
show: true,
frame: true,
icon: nativeImage.createFromPath(icon),
title: 'KirkaClient Settings',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: settingsPreload,
devTools: !app.isPackaged,
}
});
settingsWindow.removeMenu();
settingsWindow.webContents.openDevTools();
settingsWindow.on('close', () => {
settingsWindow = null;
});
await settingsWindow.loadFile(path.join(__dirname, 'settings/settings.html'));
}
function rebootClient() {
app.relaunch();
app.quit();
}
app.once('ready', async function() {
if (!config.has('terms')) {
const res = await dialog.showMessageBox({
type: 'info',
title: 'Terms of Service',
message: 'By using this client, you agree to our terms and services.\nThey can be found at https://client.kirka.io/terms',
buttons: ['I agree', 'I disagree'],
});
if (res.response === 1)
app.quit();
else
config.set('terms', true);
}
if (process.versions.electron !== '10.4.7' && process.platform === 'win32')
return askUserToUpdate();
if (launcherMode) {
log.info('Launcher mode');
await createLauncherWindow();
} else {
log.info('Client mode');
await createWindow();
}
});
app.on('will-quit', function() {
if (launcherMode && launchMainClient) {
config.set('launcherMode', false);
log.info('Rebooting');
rebootClient();
} else {
config.set('launcherMode', true);
log.info('Quitting');
app.quit();
}
});
// https://github.com/nodejs/node-gyp/issues/2673#issuecomment-1239619438