diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdde09d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/* +node_modules/* \ No newline at end of file diff --git a/backend/launcher.js b/backend/launcher.js index cc522cf..bb0d76f 100644 --- a/backend/launcher.js +++ b/backend/launcher.js @@ -10,6 +10,7 @@ const crypto = require('crypto'); const execAsync = promisify(exec); const execFileAsync = promisify(execFile); +const JAVA_EXECUTABLE = 'java' + (process.platform === 'win32' ? '.exe' : ''); function getAppDir() { const home = os.homedir(); @@ -29,6 +30,182 @@ const GAME_DIR = path.join(APP_DIR, 'release', 'package', 'game', 'latest'); const JRE_DIR = path.join(APP_DIR, 'release', 'package', 'jre', 'latest'); const CONFIG_FILE = path.join(APP_DIR, 'config.json'); +function expandHome(inputPath) { + if (!inputPath) { + return inputPath; + } + if (inputPath === '~') { + return os.homedir(); + } + if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) { + return path.join(os.homedir(), inputPath.slice(2)); + } + return inputPath; +} + +function loadConfig() { + try { + if (fs.existsSync(CONFIG_FILE)) { + return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); + } + } catch (err) { + console.log('Notice: could not load config:', err.message); + } + return {}; +} + +function saveConfig(update) { + try { + createFolders(); + const config = loadConfig(); + const next = { ...config, ...update }; + fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8'); + } catch (err) { + console.log('Notice: could not save config:', err.message); + } +} + +async function findJavaOnPath(commandName = 'java') { + const lookupCmd = process.platform === 'win32' ? 'where' : 'which'; + try { + const { stdout } = await execFileAsync(lookupCmd, [commandName]); + const line = stdout.split(/\r?\n/).map(lineItem => lineItem.trim()).find(Boolean); + return line || null; + } catch (err) { + return null; + } +} + +async function getMacJavaHome() { + if (process.platform !== 'darwin') { + return null; + } + try { + const { stdout } = await execFileAsync('/usr/libexec/java_home'); + const home = stdout.trim(); + if (!home) { + return null; + } + return path.join(home, 'bin', JAVA_EXECUTABLE); + } catch (err) { + return null; + } +} + +async function resolveJavaPath(inputPath) { + const trimmed = (inputPath || '').trim(); + if (!trimmed) { + return null; + } + + const expanded = expandHome(trimmed); + if (fs.existsSync(expanded)) { + const stat = fs.statSync(expanded); + if (stat.isDirectory()) { + const candidate = path.join(expanded, 'bin', JAVA_EXECUTABLE); + return fs.existsSync(candidate) ? candidate : null; + } + return expanded; + } + + if (!path.isAbsolute(expanded)) { + return await findJavaOnPath(trimmed); + } + + return null; +} + +async function detectSystemJava() { + const envHome = process.env.JAVA_HOME; + if (envHome) { + const envJava = path.join(envHome, 'bin', JAVA_EXECUTABLE); + if (fs.existsSync(envJava)) { + return envJava; + } + } + + const macJava = await getMacJavaHome(); + if (macJava && fs.existsSync(macJava)) { + return macJava; + } + + const pathJava = await findJavaOnPath('java'); + if (pathJava && fs.existsSync(pathJava)) { + return pathJava; + } + + return null; +} + +async function getJavaDetection() { + const candidates = []; + const bundledJava = getBundledJavaPath() || path.join(JRE_DIR, 'bin', JAVA_EXECUTABLE); + + candidates.push({ + label: 'Bundled JRE', + path: bundledJava, + exists: fs.existsSync(bundledJava) + }); + + const javaHomeEnv = process.env.JAVA_HOME; + if (javaHomeEnv) { + const envJava = path.join(javaHomeEnv, 'bin', JAVA_EXECUTABLE); + candidates.push({ + label: 'JAVA_HOME', + path: envJava, + exists: fs.existsSync(envJava), + note: fs.existsSync(envJava) ? '' : 'Not found' + }); + } else { + candidates.push({ + label: 'JAVA_HOME', + path: '', + exists: false, + note: 'Not set' + }); + } + + if (process.platform === 'darwin') { + const macJava = await getMacJavaHome(); + if (macJava) { + candidates.push({ + label: 'java_home', + path: macJava, + exists: fs.existsSync(macJava), + note: fs.existsSync(macJava) ? '' : 'Not found' + }); + } else { + candidates.push({ + label: 'java_home', + path: '', + exists: false, + note: 'Not found' + }); + } + } + + const pathJava = await findJavaOnPath('java'); + if (pathJava) { + candidates.push({ + label: 'PATH', + path: pathJava, + exists: true + }); + } else { + candidates.push({ + label: 'PATH', + path: '', + exists: false, + note: 'java not found' + }); + } + + return { + javaPath: loadJavaPath(), + candidates + }; +} + function getOS() { if (process.platform === 'win32') return 'windows'; if (process.platform === 'darwin') return 'darwin'; @@ -105,20 +282,40 @@ async function installButler() { return butlerPath; } - let url; + let urls = []; const osName = getOS(); + const arch = getArch(); if (osName === 'windows') { - url = 'https://broth.itch.zone/butler/windows-amd64/LATEST/archive/default'; + urls = ['https://broth.itch.zone/butler/windows-amd64/LATEST/archive/default']; } else if (osName === 'darwin') { - url = 'https://broth.itch.zone/butler/darwin-amd64/LATEST/archive/default'; + if (arch === 'arm64') { + urls = [ + 'https://broth.itch.zone/butler/darwin-arm64/LATEST/archive/default', + 'https://broth.itch.zone/butler/darwin-amd64/LATEST/archive/default' + ]; + } else { + urls = ['https://broth.itch.zone/butler/darwin-amd64/LATEST/archive/default']; + } } else if (osName === 'linux') { - url = 'https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default'; + urls = ['https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default']; } else { throw new Error('Operating system not supported'); } console.log('Fetching Butler tool...'); - await downloadFile(url, zipPath); + let lastError = null; + for (const url of urls) { + try { + await downloadFile(url, zipPath); + lastError = null; + break; + } catch (error) { + lastError = error; + } + } + if (lastError) { + throw lastError; + } console.log('Unpacking Butler...'); const zip = new AdmZip(zipPath); @@ -163,10 +360,9 @@ async function applyPWR(pwrFile, progressCallback) { const gameLatest = GAME_DIR; const stagingDir = path.join(gameLatest, 'staging-temp'); - const gameClient = process.platform === 'win32' ? 'HytaleClient.exe' : 'HytaleClient'; - const clientPath = path.join(gameLatest, 'Client', gameClient); + const clientPath = findClientPath(gameLatest); - if (fs.existsSync(clientPath)) { + if (clientPath) { console.log('Game files detected, skipping patch installation.'); return; } @@ -235,8 +431,8 @@ async function downloadJRE(progressCallback) { const osName = getOS(); const arch = getArch(); - const javaBin = path.join(JRE_DIR, 'bin', 'java' + (process.platform === 'win32' ? '.exe' : '')); - if (fs.existsSync(javaBin)) { + const bundledJava = getBundledJavaPath(); + if (bundledJava) { console.log('Java runtime found, skipping download'); return; } @@ -294,9 +490,14 @@ async function downloadJRE(progressCallback) { await extractJRE(cacheFile, JRE_DIR); if (process.platform !== 'win32') { - const java = path.join(JRE_DIR, 'bin', 'java'); - if (fs.existsSync(java)) { - fs.chmodSync(java, 0o755); + const javaCandidates = [ + path.join(JRE_DIR, 'bin', JAVA_EXECUTABLE), + path.join(JRE_DIR, 'Contents', 'Home', 'bin', JAVA_EXECUTABLE) + ]; + for (const javaPath of javaCandidates) { + if (fs.existsSync(javaPath)) { + fs.chmodSync(javaPath, 0o755); + } } } @@ -383,31 +584,93 @@ function flattenJREDir(jreLatest) { } } -function getJavaExec() { - const javaBin = path.join(JRE_DIR, 'bin', 'java' + (process.platform === 'win32' ? '.exe' : '')); +function getBundledJavaPath() { + const candidates = [ + path.join(JRE_DIR, 'bin', JAVA_EXECUTABLE) + ]; - if (fs.existsSync(javaBin)) { - return javaBin; + if (process.platform === 'darwin') { + candidates.push(path.join(JRE_DIR, 'Contents', 'Home', 'bin', JAVA_EXECUTABLE)); + } + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + + return null; +} + +function getJavaExec() { + const bundledJava = getBundledJavaPath(); + if (bundledJava) { + return bundledJava; } console.log('Notice: Java runtime not found, using system default'); return 'java'; } -async function launchGame(playerName = 'Player', progressCallback) { +function getClientCandidates(gameLatest) { + const candidates = []; + if (process.platform === 'win32') { + candidates.push(path.join(gameLatest, 'Client', 'HytaleClient.exe')); + } else if (process.platform === 'darwin') { + candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'MacOS', 'HytaleClient')); + candidates.push(path.join(gameLatest, 'Client', 'HytaleClient')); + } else { + candidates.push(path.join(gameLatest, 'Client', 'HytaleClient')); + } + return candidates; +} + +function findClientPath(gameLatest) { + const candidates = getClientCandidates(gameLatest); + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; +} + +async function launchGame(playerName = 'Player', progressCallback, javaPathOverride) { createFolders(); saveUsername(playerName); - await downloadJRE(progressCallback); + const configuredJava = (javaPathOverride !== undefined && javaPathOverride !== null + ? javaPathOverride + : loadJavaPath() || '').trim(); + let javaBin = null; - const javaBin = getJavaExec(); + if (configuredJava) { + javaBin = await resolveJavaPath(configuredJava); + if (!javaBin) { + throw new Error(`Configured Java path not found: ${configuredJava}`); + } + } else { + try { + await downloadJRE(progressCallback); + } catch (error) { + const fallback = await detectSystemJava(); + if (fallback) { + javaBin = fallback; + } else { + throw error; + } + } + + if (!javaBin) { + javaBin = getJavaExec(); + } + } const gameLatest = GAME_DIR; - const gameClient = process.platform === 'win32' ? 'HytaleClient.exe' : 'HytaleClient'; - const clientPath = path.join(gameLatest, 'Client', gameClient); + let clientPath = findClientPath(gameLatest); - if (!fs.existsSync(clientPath)) { + if (!clientPath) { if (progressCallback) { progressCallback('Fetching game files...', null, null, null, null); } @@ -416,8 +679,10 @@ async function launchGame(playerName = 'Player', progressCallback) { await applyPWR(pwrFile, progressCallback); } - if (!fs.existsSync(clientPath)) { - throw new Error(`Game client missing at path: ${clientPath}`); + clientPath = findClientPath(gameLatest); + if (!clientPath) { + const attempted = getClientCandidates(gameLatest).join(', '); + throw new Error(`Game client missing. Tried: ${attempted}`); } const uuid = uuidv4(); @@ -444,29 +709,29 @@ async function launchGame(playerName = 'Player', progressCallback) { } function saveUsername(username) { - try { - createFolders(); - const config = { username: username || 'Player' }; - fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); - } catch (err) { - console.log('Notice: could not save username:', err.message); - } + saveConfig({ username: username || 'Player' }); } function loadUsername() { - try { - if (fs.existsSync(CONFIG_FILE)) { - const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); - return config.username || 'Player'; - } - } catch (err) { - console.log('Notice: could not load username:', err.message); - } - return 'Player'; + const config = loadConfig(); + return config.username || 'Player'; +} + +function saveJavaPath(javaPath) { + const trimmed = (javaPath || '').trim(); + saveConfig({ javaPath: trimmed }); +} + +function loadJavaPath() { + const config = loadConfig(); + return config.javaPath || ''; } module.exports = { launchGame, saveUsername, - loadUsername + loadUsername, + saveJavaPath, + loadJavaPath, + getJavaDetection }; diff --git a/index.html b/index.html index 7d92b6a..bed7e5b 100644 --- a/index.html +++ b/index.html @@ -242,19 +242,31 @@

F2P LAUNCHER

-
- - + + -
- + placeholder="Enter your name" + > + + +
+ + +
+