Compare commits

..

33 Commits

Author SHA1 Message Date
Alex
8ec6d3fd95 Merge branch 'amiayweb:main' into test-mac 2026-01-19 07:44:40 +07:00
sanasol
115e76e461 Update macOS build target architectures to include x64 and arm64 2026-01-19 01:44:11 +01:00
AMIAY
f0b2342c71 Merge pull request #47 from GreenKod/refactor-split-file
refactor: split main file into smaller modules for better maintainabi…
2026-01-18 21:06:09 +01:00
greenkod
7dbc900338 refactor: split main file into smaller modules for better maintainability 2026-01-18 21:29:37 +03:00
AMIAY
bc31f58c9c Update print statement from 'Hello' to 'Goodbye' 2026-01-18 16:07:31 +01:00
AMIAY
7e5a1577a3 Update and rename styles.css to style.css 2026-01-18 16:07:19 +01:00
AMIAY
84f0c0ba71 Delete GUI/style.css 2026-01-18 16:07:10 +01:00
AMIAY
7ede6c2f27 Add files via upload 2026-01-18 15:42:24 +01:00
AMIAY
be1a24a992 Add files via upload 2026-01-18 15:42:23 +01:00
AMIAY
9a751958b0 Add files via upload 2026-01-18 15:42:22 +01:00
AMIAY
9fcf603e08 Add files via upload 2026-01-18 15:42:20 +01:00
AMIAY
4bc1661587 Update package.json with new configuration 2026-01-18 14:41:51 +01:00
AMIAY
512a53aee7 Enhance README with additional resources and links
Updated instructions to include links to Releases and SERVER.md.
2026-01-18 14:41:22 +01:00
AMIAY
cabb5a57d2 Add Hytale F2P Server Setup Guide
Added a comprehensive setup guide for the Hytale F2P server, including server file setup, network configuration with Radmin VPN, RAM allocation recommendations, and tips for optimal performance.
2026-01-18 14:40:21 +01:00
AMIAY
c0dc65c59a Update dependencies and fix tar version to avoid workflow & electron-builder problem 2026-01-18 14:07:50 +01:00
AMIAY
b748e7316d Update package-lock.json 2026-01-18 13:59:41 +01:00
AMIAY
1c7f24c67c Update package-lock.json 2026-01-18 13:58:23 +01:00
AMIAY
b0c8c6affa Add npm install step to release workflow
Added npm install step before npm ci for all platforms.
2026-01-18 13:55:39 +01:00
AMIAY
472e55668a Update 'tar' dependency version
Updated 'tar' dependency version from 7.0.0 to 7.5.3.
2026-01-18 13:52:50 +01:00
AMIAY
6d09bba996 Merge pull request #34 from fazrigading/fix/tar-7.5.3
fix: update tar to 7.5.3 and add override for GHSA-8qq5-rm4j-mr97
2026-01-18 13:51:11 +01:00
AMIAY
6f3ae4aed7 Add README1.md to the .github directory 2026-01-18 13:45:48 +01:00
AMIAY
ee40bab9c3 Merge pull request #33 from sanasol/feature/build-releases
Github action to build binaries
2026-01-18 13:44:38 +01:00
AMIAY
84dc63b13e Change VPN software reference in server setup
Updated instructions to use Radmin VPN instead of Hamachi.
2026-01-18 13:44:00 +01:00
Fazri Gading
022a1bfde1 Update .gitignore 2026-01-18 20:36:03 +08:00
sanasol
2896ca862b Add README for GitHub Actions build and release workflow detailing triggers, platforms, and artifact generation 2026-01-18 13:27:36 +01:00
Fazri Gading
651cc16485 add override to ensure
nested dependencies in electron-builder are also patched against
arbitrary file overwrite exploits
2026-01-18 20:26:42 +08:00
Fazri Gading
5d986768d9 Delete package-lock.json 2026-01-18 20:18:09 +08:00
Fazri Gading
0f0e360cad Merge branch 'fix/tar-7.5.3' of https://github.com/fazrigading/Hytale-F2P into fix/tar-7.5.3 2026-01-18 20:16:10 +08:00
Fazri Gading
9c95bbb174 removed override for tar>=7.5.3 dep 2026-01-18 20:11:59 +08:00
Fazri Gading
23e32b3688 ignore package lock to avoid conflicts 2026-01-18 20:11:09 +08:00
Fazri Gading
f138ada0a6 Merge branch 'amiayweb:main' into fix/tar-7.5.3 2026-01-18 20:09:57 +08:00
sanasol
b5adf4aa6c Add GitHub Actions workflow for building and releasing artifacts across Linux, Windows, and macOS platforms 2026-01-18 13:08:51 +01:00
Fazri Gading
224f3f77fb chore: pin tar >=7.5.3 and add overrides to address vuln 2026-01-18 19:02:57 +08:00
34 changed files with 12445 additions and 11349 deletions

61
.github/README1.md vendored Normal file
View File

@@ -0,0 +1,61 @@
# GitHub Actions
## Build and Release Workflow
The `release.yml` workflow automatically builds the launcher for all platforms.
### Triggers
| Trigger | Builds | Creates Release |
|---------|--------|-----------------|
| Push to `main` | Yes | No |
| Push tag `v*` | Yes | Yes |
| Manual dispatch | Yes | No |
### Platforms
All builds run in parallel:
- **Linux** (ubuntu-latest): AppImage, deb
- **Windows** (windows-latest): NSIS installer, portable exe
- **macOS** (macos-latest): Universal DMG (Intel + Apple Silicon)
### Creating a Release
1. Update version in `package.json`
2. Commit and push to `main`
3. Create and push a version tag:
```bash
git tag v2.0.1
git push origin v2.0.1
```
The workflow will:
1. Build all platforms in parallel
2. Upload artifacts to GitHub Release
3. Generate release notes automatically
### Build Artifacts
After each build, artifacts are available in the Actions tab for 90 days:
- `linux-builds`: `.AppImage`, `.deb`
- `windows-builds`: `.exe`
- `macos-builds`: `.dmg`, `.zip`, `latest-mac.yml`
### Local Development
Build locally for your platform:
```bash
npm run build:linux
npm run build:win
npm run build:mac
```
Or build all platforms (requires appropriate OS):
```bash
npm run build:all
```

View File

@@ -17,10 +17,9 @@ jobs:
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npm run build:linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx electron-builder --linux --publish never
- uses: actions/upload-artifact@v4
with:
name: linux-builds
@@ -36,10 +35,9 @@ jobs:
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npm run build:win
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx electron-builder --win --publish never
- uses: actions/upload-artifact@v4
with:
name: windows-builds
@@ -54,10 +52,9 @@ jobs:
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npm run build:mac
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx electron-builder --mac --publish never
- uses: actions/upload-artifact@v4
with:
name: macos-builds

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
dist/*
node_modules/*
node_modules/*
package-lock.json

View File

@@ -7,7 +7,7 @@
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style.css">
</head>
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
<div class="absolute inset-0 z-0">
@@ -313,6 +313,18 @@
</p>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="openGameLocationBtn" class="settings-action-btn" onclick="openGameLocation()">
<i class="fas fa-folder-open"></i>
<div class="btn-content">
<div class="btn-title">Open Game Location</div>
<div class="btn-description">Open the game installation folder</div>
</div>
</button>
</div>
</div>
</div>
</div>
</div>
@@ -417,7 +429,20 @@
</div>
</footer>
<script type="module" src="js/script.js"></script>
<script type="module" src="js/script.js"></script> <!-- Discord Notification -->
<div id="discordNotification" class="discord-notification">
<div class="notification-content">
<i class="fab fa-discord"></i>
<span class="notification-text">Join our Discord community!</span>
<button class="notification-action" onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
Join Discord
</button>
</div>
<button class="notification-close" onclick="closeDiscordNotification()">
<i class="fas fa-times"></i>
</button>
</div>
<script type="module" src="js/update.js"></script>
</body>
</html>
</html>

View File

@@ -30,6 +30,24 @@ export function setupInstallation() {
if (installPlayerName) {
installPlayerName.addEventListener('change', savePlayerName);
}
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => {
if (window.LauncherUI) {
window.LauncherUI.showProgress();
window.LauncherUI.updateProgress(data);
}
});
}
if (window.electronAPI && window.electronAPI.onProgressComplete) {
window.electronAPI.onProgressComplete(() => {
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetInstallButton();
});
}
}
export async function installGame() {

View File

@@ -31,6 +31,15 @@ export function setupLauncher() {
}
});
}
if (window.electronAPI && window.electronAPI.onProgressComplete) {
window.electronAPI.onProgressComplete(() => {
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetPlayButton();
});
}
}
export async function launch() {

View File

@@ -1,9 +1,42 @@
import './ui.js';
import './install.js';
import './launcher.js';
import './news.js';
import './mods.js';
import './players.js';
import './chat.js';
import './settings.js';
import './ui.js';
import './install.js';
import './launcher.js';
import './news.js';
import './mods.js';
import './players.js';
import './chat.js';
import './settings.js';
// Discord notification functions
window.closeDiscordNotification = function() {
const notification = document.getElementById('discordNotification');
if (notification) {
notification.classList.add('hidden');
setTimeout(() => {
notification.style.display = 'none';
}, 300);
}
};
// Show notification after a delay
document.addEventListener('DOMContentLoaded', () => {
const notification = document.getElementById('discordNotification');
if (notification) {
// Check if user has previously dismissed the notification
const dismissed = localStorage.getItem('discordNotificationDismissed');
if (!dismissed) {
setTimeout(() => {
notification.style.display = 'flex';
}, 3000); // Show after 3 seconds
} else {
notification.style.display = 'none';
}
}
});
// Remember when user closes notification
const originalClose = window.closeDiscordNotification;
window.closeDiscordNotification = function() {
localStorage.setItem('discordNotificationDismissed', 'true');
originalClose();
};

View File

@@ -119,6 +119,15 @@ async function loadAllSettings() {
await loadPlayerName();
}
async function openGameLocation() {
try {
if (window.electronAPI && window.electronAPI.openGameLocation) {
await window.electronAPI.openGameLocation();
}
} catch (error) {
console.error('Error opening game location:', error);
}
}
export function getCurrentJavaPath() {
if (customJavaCheck && customJavaCheck.checked && customJavaPath) {
@@ -135,6 +144,9 @@ export function getCurrentPlayerName() {
return 'Player';
}
// Make openGameLocation globally available
window.openGameLocation = openGameLocation;
document.addEventListener('DOMContentLoaded', initSettings);
window.SettingsAPI = {

View File

@@ -196,8 +196,26 @@ function setupFirstLaunchHandlers() {
updateProgress(data);
});
let lockButtonTimeout = null;
window.electronAPI.onLockPlayButton((locked) => {
lockPlayButton(locked);
if (locked) {
if (lockButtonTimeout) {
clearTimeout(lockButtonTimeout);
}
lockButtonTimeout = setTimeout(() => {
console.warn('Play button has been locked for too long, forcing unlock');
lockPlayButton(false);
lockButtonTimeout = null;
}, 20000);
} else {
if (lockButtonTimeout) {
clearTimeout(lockButtonTimeout);
lockButtonTimeout = null;
}
}
});
}
@@ -448,6 +466,17 @@ function setupUI() {
lockPlayButton(true);
setTimeout(() => {
const playButton = document.getElementById('homePlayBtn');
if (playButton && playButton.getAttribute('data-locked') === 'true') {
const spanElement = playButton.querySelector('span');
if (spanElement && spanElement.textContent === 'CHECKING...') {
console.warn('Play button still locked after startup timeout, forcing unlock');
lockPlayButton(false);
}
}
}, 25000);
handleNavigation();
setupWindowControls();
setupSidebarLogo();

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,9 @@
<div align="center">
![Version](https://img.shields.io/badge/Version-2.0.0-green?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-2.0.1-green?style=for-the-badge)
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
[![Join Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white)](https://discord.gg/MHkEjepMQ7)
**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (windows users & non-premium only)**
@@ -59,23 +58,14 @@
3. Launch from desktop or start menu
#### Linux
See [BUILD.md](BUILD.md) for detailed build instructions.
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
#### macOS
See [BUILD.md](BUILD.md) for detailed build instructions.
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
#### 🖥️ How to create server (Windows Only)?
1. Download the server files directly from: `http://3.10.208.30:3002/server`
2. Replace the existing files in your `HytaleF2P` installation folder
3. Run the server launcher (.bat) to start hosting your own Hytale server
4. You will need a third party software like Hamachi (check on youtube how to use hamachi).
See [SERVER.md](SERVER.md)
### 🎮 Usage
1. **Enter your player name**
2. **Click "PLAY"**
3. **Automatic setup** - The launcher handles everything automatically
4. **Game launches** - Enjoy playing Hytale!
---
@@ -87,7 +77,17 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
## 📋 Changelog
### 🆕 v2.0.0 *(Latest)*
### 🆕 v2.0.1 *(Latest)*
- 📊 **Advanced Logging System** - Complete logging with timestamps, file rotation, and session tracking
- 🔧 **Play Button Fix** - Resolved issue where play button could get stuck in "CHECKING..." state
- 💬 **Discord Integration** - Added closable Discord notification for community engagement
- 📁 **Game Location Access** - New "Open Game Location" button in settings for easy file access
- 🎯 **UI Polish** - Removed bounce animation from player counter for smoother experience
- 🛡️ **Stability Improvements** - Enhanced error handling and process lifecycle management
-**Performance Optimizations** - Faster startup times and better resource management
- 🔄 **Timeout Protection** - Added safety timeouts to prevent launcher freezing
### 🔄 v2.0.0
-**Automatic Game Update System** - Smart version checking and seamless updates
-**Partial Automatic Launcher Update System** - This will inform you when I release a new update.
- 🛡️ **UserData Preservation** - Intelligent backup/restore of game saves during updates
@@ -187,11 +187,6 @@ This launcher is created for **educational purposes only**.
---
## 📬 Contact
[![Discord](https://img.shields.io/badge/Discord-amiay3-5865F2?logo=discord&logoColor=white)](https://discord.com/users/1433515183606599873)
<div align="center">
**⭐ Star this project if you found it helpful! ⭐**

87
SERVER.md Normal file
View File

@@ -0,0 +1,87 @@
# Hytale F2P Server Setup Guide
## Server File Setup
**Download server file:**
```
http://3.10.208.30:3002/server
```
**Replace the file here:**
`<your_path>\HytaleF2P\release\package\game\latest\Server`
If you don't have any custom installation path:
1. Press **WIN + R**
2. Type: `%localappdata%\HytaleF2P\release\package\game\latest\Server`
3. Press **Enter**
You will be redirected to the correct folder automatically.
## Network Setup - Radmin VPN Required
**Important:** The server only supports third-party software for LAN-style connections. You must use **Radmin VPN** to connect players together.
1. **Download and install [Radmin VPN](https://www.radmin-vpn.com/)**
2. **Create or join a network** in Radmin VPN
3. **All players must be connected** to the same Radmin network
4. **Use the Radmin VPN IP address** to connect to the server
This creates a virtual LAN environment that allows the Hytale server to work properly with multiple players.
## RAM Allocation Guide (Windows)
When you start a Hytale server using `start-server.bat`, Java will use very little memory by default.
This can cause slow startup, crashes, or the server not launching at all.
**You should always allocate RAM in your launch command.**
Edit your `start-server.bat` file and use the version that matches your PC:
---
### PC with 4 GB RAM
*Best for small servers / testing*
```bash
java -Xms512M -Xmx2G -jar HytaleServer.jar --assets ..\Assets.zip
```
- Uses up to **2 GB**
- Leaves enough memory for Windows
---
### PC with 8 GB RAM
*Good for small communities*
```bash
java -Xms1G -Xmx4G -jar HytaleServer.jar --assets ..\Assets.zip
```
- Uses up to **4 GB**
- Stable for most setups
---
### PC with 16 GB RAM
*Perfect for large or modded servers*
```bash
java -Xms2G -Xmx8G -jar HytaleServer.jar --assets ..\Assets.zip
```
- Uses up to **8 GB**
- Ideal for heavy worlds and plugins
---
## Tips
- `-Xms` = minimum RAM allocation
- `-Xmx` = maximum RAM allocation
- **Never allocate all your system RAM** — Windows still needs memory to run
- **Test your configuration** with a small world first
- **Monitor server performance** and adjust RAM as needed

163
backend/core/config.js Normal file
View File

@@ -0,0 +1,163 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
function getAppDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleF2P');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleF2P');
} else {
return path.join(home, '.hytalef2p');
}
}
const CONFIG_FILE = path.join(getAppDir(), 'config.json');
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 {
const configDir = path.dirname(CONFIG_FILE);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
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);
}
}
function saveUsername(username) {
saveConfig({ username: username || 'Player' });
}
function loadUsername() {
const config = loadConfig();
return config.username || 'Player';
}
function saveChatUsername(chatUsername) {
saveConfig({ chatUsername: chatUsername || '' });
}
function loadChatUsername() {
const config = loadConfig();
return config.chatUsername || '';
}
function getUuidForUser(username) {
const { v4: uuidv4 } = require('uuid');
const config = loadConfig();
const userUuids = config.userUuids || {};
if (userUuids[username]) {
return userUuids[username];
}
const newUuid = uuidv4();
userUuids[username] = newUuid;
saveConfig({ userUuids });
return newUuid;
}
function saveJavaPath(javaPath) {
const trimmed = (javaPath || '').trim();
saveConfig({ javaPath: trimmed });
}
function loadJavaPath() {
const config = loadConfig();
return config.javaPath || '';
}
function saveInstallPath(installPath) {
const trimmed = (installPath || '').trim();
saveConfig({ installPath: trimmed });
}
function loadInstallPath() {
const config = loadConfig();
return config.installPath || '';
}
function saveModsToConfig(mods) {
try {
let config = loadConfig();
config.installedMods = mods;
const configDir = path.dirname(CONFIG_FILE);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
console.log('Mods saved to config.json');
} catch (error) {
console.error('Error saving mods to config:', error);
}
}
function loadModsFromConfig() {
try {
const config = loadConfig();
return config.installedMods || [];
} catch (error) {
console.error('Error loading mods from config:', error);
return [];
}
}
function isFirstLaunch() {
const config = loadConfig();
if ('hasLaunchedBefore' in config) {
return !config.hasLaunchedBefore;
}
const hasUserData = config.installPath || config.username || config.javaPath ||
config.chatUsername || config.userUuids ||
Object.keys(config).length > 0;
if (!hasUserData) {
return true;
}
return true;
}
function markAsLaunched() {
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
}
module.exports = {
loadConfig,
saveConfig,
saveUsername,
loadUsername,
saveChatUsername,
loadChatUsername,
getUuidForUser,
saveJavaPath,
loadJavaPath,
saveInstallPath,
loadInstallPath,
saveModsToConfig,
loadModsFromConfig,
isFirstLaunch,
markAsLaunched,
CONFIG_FILE
};

197
backend/core/paths.js Normal file
View File

@@ -0,0 +1,197 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
function getAppDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleF2P');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleF2P');
} else {
return path.join(home, '.hytalef2p');
}
}
const DEFAULT_APP_DIR = getAppDir();
function getResolvedAppDir(customPath) {
if (customPath && customPath.trim()) {
return path.join(customPath.trim(), 'HytaleF2P');
}
try {
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
if (config.installPath && config.installPath.trim()) {
return path.join(config.installPath.trim(), 'HytaleF2P');
}
}
} catch (err) {
}
return DEFAULT_APP_DIR;
}
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;
}
const APP_DIR = DEFAULT_APP_DIR;
const CACHE_DIR = path.join(APP_DIR, 'cache');
const TOOLS_DIR = path.join(APP_DIR, 'butler');
const GAME_DIR = path.join(APP_DIR, 'release', 'package', 'game', 'latest');
const JRE_DIR = path.join(APP_DIR, 'release', 'package', 'jre', 'latest');
const PLAYER_ID_FILE = path.join(APP_DIR, 'player_id.json');
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;
}
function findUserDataPath(gameLatest) {
const candidates = [];
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'UserData'));
candidates.push(path.join(gameLatest, 'Hytale.app', 'Contents', 'UserData'));
candidates.push(path.join(gameLatest, 'UserData'));
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
let defaultPath;
if (process.platform === 'darwin') {
defaultPath = path.join(gameLatest, 'Client', 'UserData');
} else {
defaultPath = path.join(gameLatest, 'Client', 'UserData');
}
if (!fs.existsSync(defaultPath)) {
fs.mkdirSync(defaultPath, { recursive: true });
}
return defaultPath;
}
function findUserDataRecursive(gameLatest) {
function searchDirectory(dir) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory()) {
const fullPath = path.join(dir, item.name);
if (item.name === 'UserData') {
return fullPath;
}
const found = searchDirectory(fullPath);
if (found) {
return found;
}
}
}
} catch (error) {
}
return null;
}
if (!fs.existsSync(gameLatest)) {
return null;
}
const found = searchDirectory(gameLatest);
return found;
}
async function getModsPath(customInstallPath = null) {
try {
let installPath = customInstallPath;
if (!installPath) {
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
installPath = config.installPath || '';
}
}
if (!installPath) {
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
installPath = path.join(localAppData, 'HytaleF2P');
} else {
installPath = path.join(installPath, 'HytaleF2P');
}
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
const userDataPath = findUserDataPath(gameLatest);
const modsPath = path.join(userDataPath, 'Mods');
const disabledModsPath = path.join(userDataPath, 'DisabledMods');
if (!fs.existsSync(modsPath)) {
fs.mkdirSync(modsPath, { recursive: true });
}
if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
}
return modsPath;
} catch (error) {
console.error('Error getting mods path:', error);
throw error;
}
}
module.exports = {
getAppDir,
getResolvedAppDir,
expandHome,
APP_DIR,
CACHE_DIR,
TOOLS_DIR,
GAME_DIR,
JRE_DIR,
PLAYER_ID_FILE,
getClientCandidates,
findClientPath,
findUserDataPath,
findUserDataRecursive,
getModsPath
};

File diff suppressed because it is too large Load Diff

213
backend/logger.js Normal file
View File

@@ -0,0 +1,213 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
class Logger {
constructor() {
this.logDir = null;
this.logFile = null;
this.maxLogSize = 10 * 1024 * 1024; // 10MB
this.maxLogFiles = 5;
this.originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info
};
this.initializeLogDirectory();
}
getAppDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleF2P');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleF2P');
} else {
return path.join(home, '.hytalef2p');
}
}
getInstallPath() {
try {
const configFile = path.join(this.getAppDir(), 'config.json');
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
if (config.installPath && config.installPath.trim()) {
return path.join(config.installPath.trim(), 'HytaleF2P');
}
}
} catch (err) {
}
return this.getAppDir();
}
initializeLogDirectory() {
try {
const installPath = this.getInstallPath();
this.logDir = path.join(installPath, 'logs');
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
const today = new Date();
const dateString = today.toISOString().split('T')[0]; // YYYY-MM-DD
const timeString = today.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-'); // HH-MM-SS
this.logFile = path.join(this.logDir, `launcher-${dateString}-${timeString}.log`);
this.writeToFile(`\n=== NEW LAUNCHER SESSION - ${today.toISOString()} ===\n`);
} catch (error) {
this.logDir = path.join(os.tmpdir(), 'HytaleF2P-logs');
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
const today = new Date();
const dateString = today.toISOString().split('T')[0];
const timeString = today.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-');
this.logFile = path.join(this.logDir, `launcher-${dateString}-${timeString}.log`);
this.writeToFile(`\n=== FALLBACK SESSION IN TEMP - ${today.toISOString()} ===\n`);
}
}
writeToFile(message) {
if (!this.logFile) return;
try {
if (fs.existsSync(this.logFile)) {
const stats = fs.statSync(this.logFile);
if (stats.size > this.maxLogSize) {
this.rotateLogFile();
}
}
fs.appendFileSync(this.logFile, message, 'utf8');
} catch (error) {
this.originalConsole.error('Impossible d\'écrire dans le fichier de log:', error.message);
}
}
rotateLogFile() {
try {
const today = new Date();
const dateString = today.toISOString().split('T')[0];
const timeString = today.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-');
const rotatedFile = path.join(this.logDir, `launcher-${dateString}-${timeString}.log`);
fs.renameSync(this.logFile, rotatedFile);
this.cleanupOldLogs();
const newToday = new Date();
const newDateString = newToday.toISOString().split('T')[0];
const newTimeString = newToday.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-');
this.logFile = path.join(this.logDir, `launcher-${newDateString}-${newTimeString}.log`);
this.writeToFile(`\n=== LOG ROTATION - ${newToday.toISOString()} ===\n`);
} catch (error) {
this.originalConsole.error('Erreur lors de la rotation des logs:', error.message);
}
}
cleanupOldLogs() {
try {
const files = fs.readdirSync(this.logDir)
.filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
.map(file => ({
name: file,
path: path.join(this.logDir, file),
mtime: fs.statSync(path.join(this.logDir, file)).mtime
}))
.sort((a, b) => b.mtime - a.mtime);
if (files.length > this.maxLogFiles) {
const filesToDelete = files.slice(this.maxLogFiles);
filesToDelete.forEach(file => {
try {
fs.unlinkSync(file.path);
} catch (err) {
this.originalConsole.error(`Impossible de supprimer le fichier de log ${file.name}:`, err.message);
}
});
}
} catch (error) {
this.originalConsole.error('Erreur lors du nettoyage des logs:', error.message);
}
}
formatLogMessage(level, ...args) {
const timestamp = new Date().toISOString();
const message = args.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2);
} catch (e) {
return String(arg);
}
}
return String(arg);
}).join(' ');
return `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
}
log(...args) {
const logMessage = this.formatLogMessage('info', ...args);
this.writeToFile(logMessage);
this.originalConsole.log(...args);
}
error(...args) {
const logMessage = this.formatLogMessage('error', ...args);
this.writeToFile(logMessage);
this.originalConsole.error(...args);
}
warn(...args) {
const logMessage = this.formatLogMessage('warn', ...args);
this.writeToFile(logMessage);
this.originalConsole.warn(...args);
}
info(...args) {
const logMessage = this.formatLogMessage('info', ...args);
this.writeToFile(logMessage);
this.originalConsole.info(...args);
}
interceptConsole() {
console.log = (...args) => this.log(...args);
console.error = (...args) => this.error(...args);
console.warn = (...args) => this.warn(...args);
console.info = (...args) => this.info(...args);
process.on('uncaughtException', (error) => {
this.error('Uncaught exception:', error.stack || error.message);
});
process.on('unhandledRejection', (reason, promise) => {
this.error('Unhandled rejection at', promise, 'reason:', reason);
});
}
restoreConsole() {
console.log = this.originalConsole.log;
console.error = this.originalConsole.error;
console.warn = this.originalConsole.warn;
console.info = this.originalConsole.info;
}
getLogDirectory() {
return this.logDir;
}
updateInstallPath() {
this.initializeLogDirectory();
}
}
const logger = new Logger();
module.exports = logger;

View File

@@ -0,0 +1,75 @@
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');
const { TOOLS_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile } = require('../utils/fileManager');
async function installButler(toolsDir = TOOLS_DIR) {
if (!fs.existsSync(toolsDir)) {
fs.mkdirSync(toolsDir, { recursive: true });
}
const butlerName = process.platform === 'win32' ? 'butler.exe' : 'butler';
const butlerPath = path.join(toolsDir, butlerName);
const zipPath = path.join(toolsDir, 'butler.zip');
if (fs.existsSync(butlerPath)) {
return butlerPath;
}
let urls = [];
const osName = getOS();
const arch = getArch();
if (osName === 'windows') {
urls = ['https://broth.itch.zone/butler/windows-amd64/LATEST/archive/default'];
} else if (osName === 'darwin') {
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') {
urls = ['https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default'];
} else {
throw new Error('Operating system not supported');
}
console.log('Fetching Butler tool...');
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);
zip.extractAllTo(toolsDir, true);
if (process.platform !== 'win32') {
fs.chmodSync(butlerPath, 0o755);
}
try {
fs.unlinkSync(zipPath);
} catch (err) {
console.log('Notice: could not delete butler.zip');
}
return butlerPath;
}
module.exports = {
installButler
};

View File

@@ -0,0 +1,272 @@
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const { promisify } = require('util');
const { spawn } = require('child_process');
const { getResolvedAppDir, findClientPath } = require('../core/paths');
const { setupWaylandEnvironment } = require('../utils/platformUtils');
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser } = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager');
const execAsync = promisify(exec);
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
const gameLatest = customGameDir;
let clientPath = findClientPath(gameLatest);
if (!clientPath) {
throw new Error('Game is not installed. Please install the game first.');
}
saveUsername(playerName);
if (installPathOverride) {
saveInstallPath(installPathOverride);
}
const configuredJava = (javaPathOverride !== undefined && javaPathOverride !== null
? javaPathOverride
: loadJavaPath() || '').trim();
let javaBin = null;
if (configuredJava) {
javaBin = await resolveJavaPath(configuredJava);
if (!javaBin) {
throw new Error(`Configured Java path not found: ${configuredJava}`);
}
} else {
javaBin = getJavaExec(customJreDir);
if (!getBundledJavaPath(customJreDir)) {
const fallback = await detectSystemJava();
if (fallback) {
javaBin = fallback;
} else {
throw new Error('Java runtime not found. Please install the game first or configure Java path.');
}
}
}
if (process.platform === 'darwin') {
try {
const appBundle = path.join(gameLatest, 'Client', 'Hytale.app');
const serverDir = path.join(gameLatest, 'Server');
const signPath = async (targetPath, deep = false) => {
await execAsync(`xattr -cr "${targetPath}"`).catch(() => {});
const deepFlag = deep ? '--deep ' : '';
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => {});
};
if (fs.existsSync(appBundle)) {
await signPath(appBundle, true);
console.log('Signed macOS app bundle');
} else {
await signPath(path.dirname(clientPath), true);
console.log('Signed macOS client binary');
}
if (javaBin && fs.existsSync(javaBin)) {
let jreRoot = path.dirname(path.dirname(javaBin));
if (jreRoot.endsWith('Home')) {
jreRoot = path.dirname(path.dirname(jreRoot));
}
await signPath(jreRoot, true);
await signPath(javaBin, false);
console.log('Signed Java runtime');
}
if (fs.existsSync(serverDir)) {
await execAsync(`xattr -cr "${serverDir}"`).catch(() => {});
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => {});
console.log('Signed server binaries');
}
if (javaBin && fs.existsSync(javaBin)) {
const javaWrapperPath = path.join(path.dirname(javaBin), 'java-wrapper');
const wrapperScript = `#!/bin/bash
# Java wrapper for macOS - adds --disable-sentry to fix Sentry hang issue
REAL_JAVA="${javaBin}"
ARGS=("$@")
for i in "\${!ARGS[@]}"; do
if [[ "\${ARGS[$i]}" == *"HytaleServer.jar"* ]]; then
ARGS=("\${ARGS[@]:0:$((i+1))}" "--disable-sentry" "\${ARGS[@]:$((i+1))}")
break
fi
done
exec "$REAL_JAVA" "\${ARGS[@]}"
`;
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
await signPath(javaWrapperPath, false);
console.log('Created java wrapper with --disable-sentry fix');
javaBin = javaWrapperPath;
}
} catch (signError) {
console.log('Notice: macOS signing step failed:', signError.message);
console.log('The game may still launch if Gatekeeper allows it');
}
}
const uuid = getUuidForUser(playerName);
const args = [
'--app-dir', gameLatest,
'--java-exec', javaBin,
'--auth-mode', 'offline',
'--uuid', uuid,
'--name', playerName,
'--user-dir', userDataDir
];
if (progressCallback) {
progressCallback('Starting game...', null, null, null, null);
}
console.log('Starting game...');
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
const env = { ...process.env };
const waylandEnv = setupWaylandEnvironment();
Object.assign(env, waylandEnv);
try {
let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],
detached: true,
env: env
};
if (process.platform === 'win32') {
spawnOptions.shell = false;
spawnOptions.windowsHide = true;
}
const child = spawn(clientPath, args, spawnOptions);
console.log(`Game process started with PID: ${child.pid}`);
let hasExited = false;
let outputReceived = false;
child.stdout.on('data', (data) => {
outputReceived = true;
console.log(`Game output: ${data.toString().trim()}`);
});
child.stderr.on('data', (data) => {
outputReceived = true;
console.error(`Game error: ${data.toString().trim()}`);
});
child.on('error', (error) => {
hasExited = true;
console.error(`Failed to start game process: ${error.message}`);
if (progressCallback) {
progressCallback(`Failed to start game: ${error.message}`, -1, null, null, null);
}
});
child.on('exit', (code, signal) => {
hasExited = true;
if (code !== null) {
console.log(`Game process exited with code ${code}`);
if (code !== 0 && progressCallback) {
progressCallback(`Game exited with error code ${code}`, -1, null, null, null);
}
} else if (signal) {
console.log(`Game process terminated by signal ${signal}`);
}
});
setTimeout(() => {
if (!hasExited) {
console.log('Game appears to be running successfully');
child.unref();
if (progressCallback) {
progressCallback('Game launched successfully', 100, null, null, null);
}
} else if (!outputReceived) {
console.warn('Game process exited immediately with no output - possible issue with game files or dependencies');
}
}, 3000);
return { success: true, installed: true, launched: true, pid: child.pid };
} catch (spawnError) {
console.error(`Error spawning game process: ${spawnError.message}`);
if (progressCallback) {
progressCallback(`Error launching game: ${spawnError.message}`, -1, null, null, null);
}
throw spawnError;
}
}
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
try {
if (progressCallback) {
progressCallback('Checking for updates...', 0, null, null, null);
}
const [installedVersion, latestVersion] = await Promise.all([
getInstalledClientVersion(),
getLatestClientVersion()
]);
console.log(`Installed version: ${installedVersion}, Latest version: ${latestVersion}`);
let needsUpdate = false;
if (installedVersion && latestVersion && installedVersion !== latestVersion) {
needsUpdate = true;
console.log('Version mismatch detected, update required');
}
if (needsUpdate) {
if (progressCallback) {
progressCallback('Game update required, starting update process...', 10, null, null, null);
}
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customToolsDir = path.join(customAppDir, 'butler');
const customCacheDir = path.join(customAppDir, 'cache');
try {
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir);
console.log('Game updated successfully, waiting before launch...');
if (progressCallback) {
progressCallback('Preparing game launch...', 90, null, null, null);
}
await new Promise(resolve => setTimeout(resolve, 3000));
} catch (updateError) {
console.error('Update failed:', updateError);
if (progressCallback) {
progressCallback(`Update failed: ${updateError.message}`, -1, null, null, null);
}
throw updateError;
}
}
if (progressCallback) {
progressCallback('Launching game...', 80, null, null, null);
}
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride);
} catch (error) {
console.error('Error in version check and launch:', error);
if (progressCallback) {
progressCallback(`Error: ${error.message}`, -1, null, null, null);
}
throw error;
}
}
module.exports = {
launchGame,
launchGameWithVersionCheck
};

View File

@@ -0,0 +1,406 @@
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
const { installButler } = require('./butlerManager');
const { checkAndInstallMultiClient } = require('./multiClientManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config');
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
async function downloadPWR(version = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR) {
const osName = getOS();
const arch = getArch();
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`;
const dest = path.join(cacheDir, fileName);
if (fs.existsSync(dest)) {
console.log('PWR file found in cache:', dest);
return dest;
}
console.log('Fetching PWR patch file:', url);
await downloadFile(url, dest, progressCallback);
console.log('PWR saved to:', dest);
return dest;
}
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR) {
const butlerPath = await installButler(toolsDir);
const gameLatest = gameDir;
const stagingDir = path.join(gameLatest, 'staging-temp');
const clientPath = findClientPath(gameLatest);
if (clientPath) {
console.log('Game files detected, skipping patch installation.');
return;
}
if (!fs.existsSync(gameLatest)) {
fs.mkdirSync(gameLatest, { recursive: true });
}
if (!fs.existsSync(stagingDir)) {
fs.mkdirSync(stagingDir, { recursive: true });
}
if (progressCallback) {
progressCallback('Installing game patch...', null, null, null, null);
}
console.log('Installing game patch...');
if (!fs.existsSync(butlerPath)) {
throw new Error(`Butler tool not found at: ${butlerPath}`);
}
if (!fs.existsSync(pwrFile)) {
throw new Error(`PWR file not found at: ${pwrFile}`);
}
const args = [
'apply',
'--staging-dir',
stagingDir,
pwrFile,
gameLatest
];
try {
await new Promise((resolve, reject) => {
const child = execFile(butlerPath, args, {
maxBuffer: 1024 * 1024 * 10,
timeout: 600000
}, (error, stdout, stderr) => {
if (error) {
console.error('Butler stderr:', stderr);
console.error('Butler stdout:', stdout);
reject(new Error(`Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`));
} else {
resolve();
}
});
});
} catch (error) {
throw error;
}
if (fs.existsSync(stagingDir)) {
fs.rmSync(stagingDir, { recursive: true, force: true });
}
if (progressCallback) {
progressCallback('Installation complete', null, null, null, null);
}
console.log('Installation complete');
}
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR) {
let tempUpdateDir;
try {
if (progressCallback) {
progressCallback('Updating game files...', 0, null, null, null);
}
console.log(`Updating game files to version: ${newVersion}`);
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
if (fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
}
fs.mkdirSync(tempUpdateDir, { recursive: true });
if (progressCallback) {
progressCallback('Downloading new game version...', 10, null, null, null);
}
const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir);
if (progressCallback) {
progressCallback('Extracting new files...', 50, null, null, null);
}
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null);
}
let userDataBackup = null;
const userDataPath = findUserDataRecursive(gameDir);
if (userDataPath && fs.existsSync(userDataPath)) {
userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now());
console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`);
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const files = fs.readdirSync(src);
for (const file of files) {
copyRecursive(path.join(src, file), path.join(dest, file));
}
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataPath, userDataBackup);
} else {
console.log('No UserData folder found in game directory');
}
if (fs.existsSync(gameDir)) {
console.log('Removing old game files...');
fs.rmSync(gameDir, { recursive: true, force: true });
}
fs.renameSync(tempUpdateDir, gameDir);
const multiResult = await checkAndInstallMultiClient(gameDir, progressCallback);
console.log('Multiplayer-client check result after update:', multiResult);
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
console.log('HomePage.ui update result after update:', homeUIResult);
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
console.log('Logo@2x.png update result after update:', logoResult);
if (userDataBackup && fs.existsSync(userDataBackup)) {
const newUserDataPath = findUserDataPath(gameDir);
const userDataParent = path.dirname(newUserDataPath);
if (!fs.existsSync(userDataParent)) {
fs.mkdirSync(userDataParent, { recursive: true });
}
console.log(`Restoring UserData to: ${newUserDataPath}`);
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const files = fs.readdirSync(src);
for (const file of files) {
copyRecursive(path.join(src, file), path.join(dest, file));
}
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataBackup, newUserDataPath);
}
console.log(`Game files updated successfully to version: ${newVersion}`);
if (userDataBackup && fs.existsSync(userDataBackup)) {
try {
fs.rmSync(userDataBackup, { recursive: true, force: true });
console.log('UserData backup cleaned up');
} catch (cleanupError) {
console.warn('Could not clean up UserData backup:', cleanupError.message);
}
}
console.log('Waiting for file system sync...');
await new Promise(resolve => setTimeout(resolve, 2000));
if (progressCallback) {
progressCallback('Game update completed', 100, null, null, null);
}
return { success: true, updated: true, version: newVersion };
} catch (error) {
console.error('Error updating game files:', error);
if (userDataBackup && fs.existsSync(userDataBackup)) {
try {
fs.rmSync(userDataBackup, { recursive: true, force: true });
console.log('UserData backup cleaned up after error');
} catch (cleanupError) {
console.warn('Could not clean up UserData backup:', cleanupError.message);
}
}
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
}
throw new Error(`Failed to update game files: ${error.message}`);
}
}
function isGameInstalled() {
const appDir = getResolvedAppDir();
const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
const clientPath = findClientPath(gameDir);
return clientPath !== null;
}
async function installGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
const customAppDir = getResolvedAppDir(installPathOverride);
const customCacheDir = path.join(customAppDir, 'cache');
const customToolsDir = path.join(customAppDir, 'butler');
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
if (!fs.existsSync(userDataDir)) {
fs.mkdirSync(userDataDir, { recursive: true });
}
saveUsername(playerName);
if (installPathOverride) {
saveInstallPath(installPathOverride);
}
const gameLatest = customGameDir;
let clientPath = findClientPath(gameLatest);
if (clientPath) {
if (progressCallback) {
progressCallback('Game already installed', 100, null, null, null);
}
console.log('Game is already installed');
return { success: true, alreadyInstalled: true };
}
const configuredJava = (javaPathOverride !== undefined && javaPathOverride !== null
? javaPathOverride
: loadJavaPath() || '').trim();
let javaBin = null;
if (configuredJava) {
javaBin = await resolveJavaPath(configuredJava);
if (!javaBin) {
throw new Error(`Configured Java path not found: ${configuredJava}`);
}
} else {
try {
await downloadJRE(progressCallback, customCacheDir, customJreDir);
} catch (error) {
const fallback = await detectSystemJava();
if (fallback) {
javaBin = fallback;
} else {
throw error;
}
}
if (!javaBin) {
javaBin = getJavaExec(customJreDir);
}
}
if (progressCallback) {
progressCallback('Fetching game files...', null, null, null, null);
}
console.log('Installing game files...');
const latestVersion = await getLatestClientVersion();
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
const multiResult = await checkAndInstallMultiClient(customGameDir, progressCallback);
console.log('Multiplayer check result:', multiResult);
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
console.log('HomePage.ui update result after installation:', homeUIResult);
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
console.log('Logo@2x.png update result after installation:', logoResult);
if (progressCallback) {
progressCallback('Installation complete', 100, null, null, null);
}
console.log('Game installation completed successfully');
return {
success: true,
installed: true,
multiClient: multiResult
};
}
async function uninstallGame() {
const appDir = getResolvedAppDir();
if (!fs.existsSync(appDir)) {
throw new Error('Game is not installed');
}
try {
fs.rmSync(appDir, { recursive: true, force: true });
console.log('Game uninstalled successfully - removed entire HytaleF2P folder');
if (fs.existsSync(CONFIG_FILE)) {
const config = loadConfig();
delete config.installPath;
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
}
} catch (error) {
throw new Error(`Failed to uninstall game: ${error.message}`);
}
}
function checkExistingGameInstallation() {
try {
const config = loadConfig();
if (!config.installPath || !config.installPath.trim()) {
return null;
}
const installPath = config.installPath.trim();
const gameDir = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest');
if (!fs.existsSync(gameDir)) {
return null;
}
const clientPath = findClientPath(gameDir);
if (!clientPath) {
return null;
}
const userDataPath = findUserDataRecursive(gameDir);
return {
gameDir: gameDir,
clientPath: clientPath,
userDataPath: userDataPath,
installPath: installPath,
hasUserData: userDataPath && fs.existsSync(userDataPath)
};
} catch (error) {
console.error('Error checking existing game installation:', error);
return null;
}
}
module.exports = {
downloadPWR,
applyPWR,
updateGameFiles,
isGameInstalled,
installGame,
uninstallGame,
checkExistingGameInstallation
};

View File

@@ -0,0 +1,363 @@
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const { promisify } = require('util');
const axios = require('axios');
const AdmZip = require('adm-zip');
const crypto = require('crypto');
const tar = require('tar');
const { expandHome, JRE_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils');
const { loadConfig } = require('../core/config');
const { downloadFile } = require('../utils/fileManager');
const execFileAsync = promisify(execFile);
const JAVA_EXECUTABLE = 'java' + (process.platform === 'win32' ? '.exe' : '');
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;
}
function loadJavaPath() {
const config = loadConfig();
return config.javaPath || '';
}
function getBundledJavaPath(jreDir = JRE_DIR) {
const candidates = [
path.join(jreDir, 'bin', JAVA_EXECUTABLE)
];
if (process.platform === 'darwin') {
candidates.push(path.join(jreDir, 'Contents', 'Home', 'bin', JAVA_EXECUTABLE));
}
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
function getJavaExec(jreDir = JRE_DIR) {
const bundledJava = getBundledJavaPath(jreDir);
if (bundledJava) {
return bundledJava;
}
console.log('Notice: Java runtime not found, using system default');
return 'java';
}
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
};
}
async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) {
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
const osName = getOS();
const arch = getArch();
const bundledJava = getBundledJavaPath(jreDir);
if (bundledJava) {
console.log('Java runtime found, skipping download');
return;
}
console.log('Requesting Java runtime information...');
const response = await axios.get('https://launcher.hytale.com/version/release/jre.json', {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9'
}
});
const jreData = response.data;
const osData = jreData.download_url[osName];
if (!osData) {
throw new Error(`Java runtime unavailable for platform: ${osName}`);
}
const platform = osData[arch];
if (!platform) {
throw new Error(`Java runtime unavailable for architecture ${arch} on ${osName}`);
}
const fileName = path.basename(platform.url);
const cacheFile = path.join(cacheDir, fileName);
if (!fs.existsSync(cacheFile)) {
if (progressCallback) {
progressCallback('Fetching Java runtime...', null, null, null, null);
}
console.log('Fetching Java runtime...');
await downloadFile(platform.url, cacheFile, progressCallback);
console.log('Download finished');
}
if (progressCallback) {
progressCallback('Validating files...', null, null, null, null);
}
console.log('Validating files...');
const fileBuffer = fs.readFileSync(cacheFile);
const hashSum = crypto.createHash('sha256');
hashSum.update(fileBuffer);
const hex = hashSum.digest('hex');
if (hex !== platform.sha256) {
fs.unlinkSync(cacheFile);
throw new Error(`File validation failed: expected ${platform.sha256} but got ${hex}`);
}
if (progressCallback) {
progressCallback('Unpacking Java runtime...', null, null, null, null);
}
console.log('Unpacking Java runtime...');
await extractJRE(cacheFile, jreDir);
if (process.platform !== 'win32') {
const javaCandidates = [
path.join(jreDir, 'bin', JAVA_EXECUTABLE),
path.join(jreDir, 'Contents', 'Home', 'bin', JAVA_EXECUTABLE)
];
for (const javaPath of javaCandidates) {
if (fs.existsSync(javaPath)) {
fs.chmodSync(javaPath, 0o755);
}
}
}
flattenJREDir(jreDir);
try {
fs.unlinkSync(cacheFile);
} catch (err) {
console.log('Notice: could not delete cached Java files:', err.message);
}
console.log('Java runtime ready');
}
async function extractJRE(archivePath, destDir) {
if (fs.existsSync(destDir)) {
fs.rmSync(destDir, { recursive: true, force: true });
}
fs.mkdirSync(destDir, { recursive: true });
if (archivePath.endsWith('.zip')) {
return extractZip(archivePath, destDir);
} else if (archivePath.endsWith('.tar.gz')) {
return extractTarGz(archivePath, destDir);
} else {
throw new Error(`Archive type not supported: ${archivePath}`);
}
}
function extractZip(zipPath, dest) {
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
for (const entry of entries) {
const entryPath = path.join(dest, entry.entryName);
const resolvedPath = path.resolve(entryPath);
const resolvedDest = path.resolve(dest);
if (!resolvedPath.startsWith(resolvedDest)) {
throw new Error(`Invalid file path detected: ${entryPath}`);
}
if (entry.isDirectory) {
fs.mkdirSync(entryPath, { recursive: true });
} else {
fs.mkdirSync(path.dirname(entryPath), { recursive: true });
fs.writeFileSync(entryPath, entry.getData());
if (process.platform !== 'win32') {
fs.chmodSync(entryPath, entry.header.attr >>> 16);
}
}
}
}
function extractTarGz(tarGzPath, dest) {
return tar.extract({
file: tarGzPath,
cwd: dest,
strip: 0
});
}
function flattenJREDir(jreLatest) {
try {
const entries = fs.readdirSync(jreLatest, { withFileTypes: true });
if (entries.length !== 1 || !entries[0].isDirectory()) {
return;
}
const nested = path.join(jreLatest, entries[0].name);
const files = fs.readdirSync(nested, { withFileTypes: true });
for (const file of files) {
const oldPath = path.join(nested, file.name);
const newPath = path.join(jreLatest, file.name);
fs.renameSync(oldPath, newPath);
}
fs.rmSync(nested, { recursive: true, force: true });
} catch (err) {
console.log('Notice: could not restructure Java directory:', err.message);
}
}
module.exports = {
findJavaOnPath,
getMacJavaHome,
resolveJavaPath,
detectSystemJava,
loadJavaPath,
getBundledJavaPath,
getJavaExec,
getJavaDetection,
downloadJRE,
extractJRE,
JAVA_EXECUTABLE
};

View File

@@ -0,0 +1,276 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const { getModsPath } = require('../core/paths');
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
function generateModId(filename) {
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
}
function extractModName(filename) {
let name = path.parse(filename).name;
name = name.replace(/-v?\d+\.[\d\.]+.*$/i, '');
name = name.replace(/-\d+\.[\d\.]+.*$/i, '');
name = name.replace(/[-_]/g, ' ');
name = name.replace(/\b\w/g, l => l.toUpperCase());
return name || 'Unknown Mod';
}
function extractVersion(filename) {
const versionMatch = filename.match(/v?(\d+\.[\d\.]+)/);
return versionMatch ? versionMatch[1] : null;
}
async function loadInstalledMods(modsPath) {
try {
const configMods = loadModsFromConfig();
const modsMap = new Map();
configMods.forEach(mod => {
modsMap.set(mod.fileName, mod);
});
if (fs.existsSync(modsPath)) {
const files = fs.readdirSync(modsPath);
for (const file of files) {
const filePath = path.join(modsPath, file);
const stats = fs.statSync(filePath);
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
const configMod = modsMap.get(file);
const modInfo = {
id: configMod?.id || generateModId(file),
name: configMod?.name || extractModName(file),
version: configMod?.version || extractVersion(file) || '1.0.0',
description: configMod?.description || 'Installed mod',
author: configMod?.author || 'Unknown',
enabled: true,
filePath: filePath,
fileName: file,
fileSize: configMod?.fileSize || stats.size,
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
curseForgeId: configMod?.curseForgeId,
curseForgeFileId: configMod?.curseForgeFileId
};
modsMap.set(file, modInfo);
}
}
}
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (fs.existsSync(disabledModsPath)) {
const files = fs.readdirSync(disabledModsPath);
for (const file of files) {
const filePath = path.join(disabledModsPath, file);
const stats = fs.statSync(filePath);
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
const configMod = modsMap.get(file);
const modInfo = {
id: configMod?.id || generateModId(file),
name: configMod?.name || extractModName(file),
version: configMod?.version || extractVersion(file) || '1.0.0',
description: configMod?.description || 'Disabled mod',
author: configMod?.author || 'Unknown',
enabled: false,
filePath: filePath,
fileName: file,
fileSize: configMod?.fileSize || stats.size,
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
curseForgeId: configMod?.curseForgeId,
curseForgeFileId: configMod?.curseForgeFileId
};
modsMap.set(file, modInfo);
}
}
}
return Array.from(modsMap.values());
} catch (error) {
console.error('Error loading installed mods:', error);
return [];
}
}
async function downloadMod(modInfo) {
try {
const modsPath = await getModsPath();
if (!modInfo.downloadUrl && !modInfo.fileId) {
throw new Error('No download URL or file ID provided');
}
let downloadUrl = modInfo.downloadUrl;
if (!downloadUrl && modInfo.fileId && modInfo.modId) {
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
headers: {
'x-api-key': modInfo.apiKey,
'Accept': 'application/json'
}
});
downloadUrl = response.data.data.downloadUrl;
}
if (!downloadUrl) {
throw new Error('Could not determine download URL');
}
const fileName = modInfo.fileName || `mod-${modInfo.modId}.jar`;
const filePath = path.join(modsPath, fileName);
const response = await axios({
method: 'get',
url: downloadUrl,
responseType: 'stream'
});
const writer = fs.createWriteStream(filePath);
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', () => {
const configMods = loadModsFromConfig();
const newMod = {
id: modInfo.id || generateModId(fileName),
name: modInfo.name || extractModName(fileName),
version: modInfo.version || '1.0.0',
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
author: modInfo.author || 'Unknown',
enabled: true,
fileName: fileName,
fileSize: fs.statSync(filePath).size,
dateInstalled: new Date().toISOString(),
curseForgeId: modInfo.modId,
curseForgeFileId: modInfo.fileId
};
configMods.push(newMod);
saveModsToConfig(configMods);
resolve({
success: true,
filePath: filePath,
fileName: fileName,
modInfo: newMod
});
});
writer.on('error', reject);
});
} catch (error) {
console.error('Error downloading mod:', error);
return {
success: false,
error: error.message
};
}
}
async function uninstallMod(modId, modsPath) {
try {
const configMods = loadModsFromConfig();
const mod = configMods.find(m => m.id === modId);
if (!mod) {
throw new Error('Mod not found in config');
}
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
const enabledPath = path.join(modsPath, mod.fileName);
const disabledPath = path.join(disabledModsPath, mod.fileName);
let fileRemoved = false;
if (fs.existsSync(enabledPath)) {
fs.unlinkSync(enabledPath);
fileRemoved = true;
console.log('Removed mod from Mods folder:', enabledPath);
} else if (fs.existsSync(disabledPath)) {
fs.unlinkSync(disabledPath);
fileRemoved = true;
console.log('Removed mod from DisabledMods folder:', disabledPath);
}
if (!fileRemoved) {
console.warn('Mod file not found on filesystem, removing from config anyway');
}
const updatedMods = configMods.filter(m => m.id !== modId);
saveModsToConfig(updatedMods);
console.log('Mod removed from config.json');
return { success: true };
} catch (error) {
console.error('Error uninstalling mod:', error);
return {
success: false,
error: error.message
};
}
}
async function toggleMod(modId, modsPath) {
try {
const mods = await loadInstalledMods(modsPath);
const mod = mods.find(m => m.id === modId);
if (!mod) {
throw new Error('Mod not found');
}
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
}
const currentPath = mod.filePath;
let newPath, newEnabled;
if (mod.enabled) {
newPath = path.join(disabledModsPath, path.basename(currentPath));
newEnabled = false;
} else {
newPath = path.join(modsPath, path.basename(currentPath));
newEnabled = true;
}
fs.renameSync(currentPath, newPath);
const configMods = loadModsFromConfig();
const configModIndex = configMods.findIndex(m => m.id === modId);
if (configModIndex !== -1) {
configMods[configModIndex].enabled = newEnabled;
saveModsToConfig(configMods);
}
return { success: true, enabled: newEnabled };
} catch (error) {
console.error('Error toggling mod:', error);
return {
success: false,
error: error.message
};
}
}
module.exports = {
loadInstalledMods,
downloadMod,
uninstallMod,
toggleMod,
generateModId,
extractModName,
extractVersion
};

View File

@@ -0,0 +1,86 @@
const fs = require('fs');
const path = require('path');
const { findClientPath } = require('../core/paths');
const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion, getMultiClientVersion } = require('../services/versionManager');
async function downloadMultiClient(gameDir, progressCallback) {
try {
if (process.platform !== 'win32') {
console.log('Multiplayer-client is only available for Windows');
return { success: false, reason: 'Platform not supported' };
}
const clientPath = findClientPath(gameDir);
if (!clientPath) {
throw new Error('Game client not found. Install game first.');
}
console.log('Downloading Multiplayer from server...');
if (progressCallback) {
progressCallback('Downloading Multiplayer...', null, null, null, null);
}
const clientUrl = 'http://3.10.208.30:3002/client';
const tempClientPath = path.join(path.dirname(clientPath), 'HytaleClient_temp.exe');
await downloadFile(clientUrl, tempClientPath, progressCallback);
const backupPath = path.join(path.dirname(clientPath), 'HytaleClient_original.exe');
if (!fs.existsSync(backupPath)) {
fs.copyFileSync(clientPath, backupPath);
console.log('Original client backed up');
}
fs.renameSync(tempClientPath, clientPath);
if (progressCallback) {
progressCallback('Multiplayer installed', 100, null, null, null);
}
console.log('Multiplayer installed successfully');
return { success: true, installed: true };
} catch (error) {
console.error('Error installing Multiplayer:', error);
throw new Error(`Failed to install Multiplayer: ${error.message}`);
}
}
async function checkAndInstallMultiClient(gameDir, progressCallback) {
try {
if (process.platform !== 'win32') {
console.log('Multiplayer check skipped (Windows only)');
return { success: true, skipped: true, reason: 'Windows only' };
}
console.log('Checking for Multiplayer availability...');
const [clientVersion, multiVersion] = await Promise.all([
getLatestClientVersion(),
getMultiClientVersion()
]);
if (!multiVersion) {
console.log('Multiplayer not available');
return { success: true, skipped: true, reason: 'Multiplayer not available' };
}
if (clientVersion === multiVersion) {
console.log(`Versions match (${clientVersion}), installing Multiplayer...`);
return await downloadMultiClient(gameDir, progressCallback);
} else {
console.log(`Version mismatch: client=${clientVersion}, multi=${multiVersion}`);
return { success: true, skipped: true, reason: 'Version mismatch' };
}
} catch (error) {
console.error('Error checking Multiplayer:', error);
return { success: false, error: error.message };
}
}
module.exports = {
downloadMultiClient,
checkAndInstallMultiClient
};

View File

@@ -0,0 +1,116 @@
const fs = require('fs');
const path = require('path');
const { downloadFile, findHomePageUIPath, findLogoPath } = require('../utils/fileManager');
async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
try {
console.log('Downloading HomePage.ui from server...');
if (progressCallback) {
progressCallback('Downloading HomePage.ui...', null, null, null, null);
}
const homeUIUrl = 'http://3.10.208.30:3002/api/HomeUI';
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
await downloadFile(homeUIUrl, tempHomePath);
const existingHomePath = findHomePageUIPath(gameDir);
if (existingHomePath && fs.existsSync(existingHomePath)) {
console.log('Found existing HomePage.ui at:', existingHomePath);
const backupPath = existingHomePath + '.backup';
if (!fs.existsSync(backupPath)) {
fs.copyFileSync(existingHomePath, backupPath);
console.log('Original HomePage.ui backed up');
}
fs.copyFileSync(tempHomePath, existingHomePath);
console.log('HomePage.ui replaced successfully');
} else {
console.log('No existing HomePage.ui found, skipping replacement');
}
if (fs.existsSync(tempHomePath)) {
fs.unlinkSync(tempHomePath);
}
if (progressCallback) {
progressCallback('HomePage.ui updated', null, null, null, null);
}
return { success: true, updated: true };
} catch (error) {
console.error('Error downloading/replacing HomePage.ui:', error);
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
if (fs.existsSync(tempHomePath)) {
fs.unlinkSync(tempHomePath);
}
console.log('HomePage.ui update failed, continuing...');
return { success: false, error: error.message };
}
}
async function downloadAndReplaceLogo(gameDir, progressCallback) {
try {
console.log('Downloading Logo@2x.png from server...');
if (progressCallback) {
progressCallback('Downloading Logo@2x.png...', null, null, null, null);
}
const logoUrl = 'http://3.10.208.30:3002/api/Logo';
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
await downloadFile(logoUrl, tempLogoPath);
const existingLogoPath = findLogoPath(gameDir);
if (existingLogoPath && fs.existsSync(existingLogoPath)) {
console.log('Found existing Logo@2x.png at:', existingLogoPath);
const backupPath = existingLogoPath + '.backup';
if (!fs.existsSync(backupPath)) {
fs.copyFileSync(existingLogoPath, backupPath);
console.log('Original Logo@2x.png backed up');
}
fs.copyFileSync(tempLogoPath, existingLogoPath);
console.log('Logo@2x.png replaced successfully');
} else {
console.log('No existing Logo@2x.png found, skipping replacement');
}
if (fs.existsSync(tempLogoPath)) {
fs.unlinkSync(tempLogoPath);
}
if (progressCallback) {
progressCallback('Logo@2x.png updated', null, null, null, null);
}
return { success: true, updated: true };
} catch (error) {
console.error('Error downloading/replacing Logo@2x.png:', error);
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
if (fs.existsSync(tempLogoPath)) {
fs.unlinkSync(tempLogoPath);
}
console.log('Logo@2x.png update failed, continuing...');
return { success: false, error: error.message };
}
}
module.exports = {
downloadAndReplaceHomePageUI,
findHomePageUIPath,
downloadAndReplaceLogo,
findLogoPath
};

View File

@@ -0,0 +1,105 @@
const path = require('path');
const fs = require('fs');
const { markAsLaunched, loadConfig } = require('../core/config');
const { checkExistingGameInstallation, updateGameFiles } = require('../managers/gameManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('./versionManager');
async function proposeGameUpdate(existingGame, progressCallback) {
try {
console.log('Proposing game update for existing installation...');
if (progressCallback) {
progressCallback('Checking for game updates...', 0, null, null, null);
}
const [installedVersion, latestVersion] = await Promise.all([
getInstalledClientVersion(),
getLatestClientVersion()
]);
console.log(`Existing installation - Installed: ${installedVersion}, Latest: ${latestVersion}`);
const customAppDir = path.join(existingGame.installPath, 'HytaleF2P');
const customCacheDir = path.join(customAppDir, 'cache');
const customToolsDir = path.join(customAppDir, 'butler');
[customCacheDir, customToolsDir].forEach(dir => {
const fs = require('fs');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
if (progressCallback) {
progressCallback('Updating existing game installation...', 20, null, null, null);
}
await updateGameFiles(latestVersion, progressCallback, existingGame.gameDir, customToolsDir, customCacheDir);
if (progressCallback) {
progressCallback('Game update completed successfully', 100, null, null, null);
}
console.log('Existing game installation updated successfully');
return { success: true, updated: true };
} catch (error) {
console.error('Error updating existing game:', error);
if (progressCallback) {
progressCallback(`Update failed: ${error.message}`, -1, null, null, null);
}
throw error;
}
}
async function handleFirstLaunchCheck(progressCallback) {
try {
const config = loadConfig();
if (config.hasLaunchedBefore === true) {
return { isFirstLaunch: false, needsUpdate: false };
}
console.log('First launch detected, checking for existing game installation...');
const existingGame = checkExistingGameInstallation();
if (!existingGame) {
console.log('No existing game installation found');
const hasUserData = config.installPath || config.username || config.javaPath ||
config.chatUsername || config.userUuids ||
Object.keys(config).length > 0;
if (hasUserData) {
console.log('Detected existing user data but no game, marking as launched');
markAsLaunched();
return { isFirstLaunch: false, needsUpdate: false };
} else {
markAsLaunched();
return { isFirstLaunch: true, needsUpdate: false, existingGame: null };
}
}
console.log('Existing game installation found:', {
gameDir: existingGame.gameDir,
hasUserData: existingGame.hasUserData
});
return {
isFirstLaunch: true,
needsUpdate: true,
existingGame: existingGame
};
} catch (error) {
console.error('Error in first launch check:', error);
markAsLaunched();
return { isFirstLaunch: true, needsUpdate: false, error: error.message };
}
}
module.exports = {
proposeGameUpdate,
handleFirstLaunchCheck
};

View File

@@ -0,0 +1,31 @@
const axios = require('axios');
async function getHytaleNews() {
try {
const response = await axios.get('https://launcher.hytale.com/launcher-feed/release/feed.json', {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
timeout: 10000
});
const articles = response.data.articles || [];
return articles.map(article => ({
title: article.title || '',
description: article.description || '',
destUrl: article.dest_url || '',
imageUrl: article.image_url ?
(article.image_url.startsWith('http') ?
article.image_url :
`https://launcher.hytale.com/launcher-feed/release/${article.image_url}`
) : ''
}));
} catch (error) {
console.error('Failed to fetch news:', error.message);
return [];
}
}
module.exports = {
getHytaleNews
};

View File

@@ -0,0 +1,34 @@
const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
function getOrCreatePlayerId() {
try {
if (!fs.existsSync(APP_DIR)) {
fs.mkdirSync(APP_DIR, { recursive: true });
}
if (fs.existsSync(PLAYER_ID_FILE)) {
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
if (data.playerId) {
return data.playerId;
}
}
const newPlayerId = uuidv4();
fs.writeFileSync(PLAYER_ID_FILE, JSON.stringify({
playerId: newPlayerId,
createdAt: new Date().toISOString()
}, null, 2));
return newPlayerId;
} catch (error) {
console.error('Error managing player ID:', error);
return uuidv4();
}
}
module.exports = {
getOrCreatePlayerId
};

View File

@@ -0,0 +1,82 @@
const axios = require('axios');
async function getLatestClientVersion() {
try {
console.log('Fetching latest client version from API...');
const response = await axios.get('http://3.10.208.30:3002/api/version_client', {
timeout: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.client_version) {
const version = response.data.client_version;
console.log(`Latest client version: ${version}`);
return version;
} else {
console.log('Warning: Invalid API response, falling back to default version');
return '4.pwr';
}
} catch (error) {
console.error('Error fetching client version:', error.message);
console.log('Warning: API unavailable, falling back to default version');
return '4.pwr';
}
}
async function getInstalledClientVersion() {
try {
console.log('Fetching installed client version from API...');
const response = await axios.get('http://3.10.208.30:3002/api/clientCheck', {
timeout: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.client_version) {
const version = response.data.client_version;
console.log(`Installed client version: ${version}`);
return version;
} else {
console.log('Warning: Invalid clientCheck API response');
return null;
}
} catch (error) {
console.error('Error fetching installed client version:', error.message);
console.log('Warning: clientCheck API unavailable');
return null;
}
}
async function getMultiClientVersion() {
try {
console.log('Fetching Multiplayer version from API...');
const response = await axios.get('http://3.10.208.30:3002/api/multi', {
timeout: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.multi_version) {
const version = response.data.multi_version;
console.log(`Multiplayer version: ${version}`);
return version;
} else {
console.log('Warning: Invalid multi API response');
return null;
}
} catch (error) {
console.error('Error fetching Multiplayer version:', error.message);
console.log('Multiplayer not available');
return null;
}
}
module.exports = {
getLatestClientVersion,
getInstalledClientVersion,
getMultiClientVersion
};

View File

@@ -1,7 +1,7 @@
const axios = require('axios');
const UPDATE_CHECK_URL = 'http://3.10.208.30:3002/api/version_launcher';
const CURRENT_VERSION = '2.0.0';
const CURRENT_VERSION = '2.0.1';
const GITHUB_DOWNLOAD_URL = 'https://github.com/amiayweb/Hytale-F2P/';
class UpdateManager {

View File

@@ -0,0 +1,103 @@
const fs = require('fs');
const path = require('path');
const axios = require('axios');
async function downloadFile(url, dest, progressCallback) {
const response = await axios({
method: 'GET',
url: url,
responseType: 'stream',
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://launcher.hytale.com/'
}
});
const totalSize = parseInt(response.headers['content-length'], 10);
let downloaded = 0;
const startTime = Date.now();
const writer = fs.createWriteStream(dest);
response.data.on('data', (chunk) => {
downloaded += chunk.length;
if (progressCallback && totalSize > 0) {
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
const elapsed = (Date.now() - startTime) / 1000;
const speed = elapsed > 0 ? downloaded / elapsed : 0;
progressCallback(null, percent, speed, downloaded, totalSize);
}
});
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
response.data.on('error', reject);
});
}
function findHomePageUIPath(gameLatest) {
function searchDirectory(dir) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isFile() && item.name === 'HomePage.ui') {
return path.join(dir, item.name);
} else if (item.isDirectory()) {
const found = searchDirectory(path.join(dir, item.name));
if (found) {
return found;
}
}
}
} catch (error) {
}
return null;
}
if (!fs.existsSync(gameLatest)) {
return null;
}
return searchDirectory(gameLatest);
}
function findLogoPath(gameLatest) {
function searchDirectory(dir) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isFile() && item.name === 'Logo@2x.png') {
return path.join(dir, item.name);
} else if (item.isDirectory()) {
const found = searchDirectory(path.join(dir, item.name));
if (found) {
return found;
}
}
}
} catch (error) {
}
return null;
}
if (!fs.existsSync(gameLatest)) {
return null;
}
return searchDirectory(gameLatest);
}
module.exports = {
downloadFile,
findHomePageUIPath,
findLogoPath
};

View File

@@ -0,0 +1,73 @@
const { execSync } = require('child_process');
function getOS() {
if (process.platform === 'win32') return 'windows';
if (process.platform === 'darwin') return 'darwin';
if (process.platform === 'linux') return 'linux';
return 'unknown';
}
function getArch() {
return process.arch === 'x64' ? 'amd64' : process.arch;
}
function isWaylandSession() {
if (process.platform !== 'linux') {
return false;
}
const sessionType = process.env.XDG_SESSION_TYPE;
if (sessionType && sessionType.toLowerCase() === 'wayland') {
return true;
}
if (process.env.WAYLAND_DISPLAY) {
return true;
}
try {
const sessionId = process.env.XDG_SESSION_ID;
if (sessionId) {
const output = execSync(`loginctl show-session ${sessionId} -p Type`, { encoding: 'utf8' });
if (output && output.toLowerCase().includes('wayland')) {
return true;
}
}
} catch (err) {
}
return false;
}
function setupWaylandEnvironment() {
if (process.platform !== 'linux') {
return {};
}
if (!isWaylandSession()) {
console.log('Detected X11 session, using default environment');
return {};
}
console.log('Detected Wayland session, configuring environment...');
const envVars = {
SDL_VIDEODRIVER: 'wayland',
GDK_BACKEND: 'wayland',
QT_QPA_PLATFORM: 'wayland',
MOZ_ENABLE_WAYLAND: '1',
_JAVA_AWT_WM_NONREPARENTING: '1'
};
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
console.log('Wayland environment variables:', envVars);
return envVars;
}
module.exports = {
getOS,
getArch,
isWaylandSession,
setupWaylandEnvironment
};

141
main.js
View File

@@ -3,6 +3,9 @@ const path = require('path');
const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, isGameInstalled, uninstallGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
const UpdateManager = require('./backend/updateManager');
const logger = require('./backend/logger');
logger.interceptConsole();
let mainWindow;
let updateManager;
@@ -66,7 +69,7 @@ function createWindow() {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
devTools: false,
devTools: true,
webSecurity: true
}
});
@@ -116,9 +119,30 @@ function createWindow() {
}
app.whenReady().then(async () => {
console.log('=== HYTALE F2P LAUNCHER STARTED ===');
console.log('Platform:', process.platform);
console.log('Architecture:', process.arch);
console.log('Electron version:', process.versions.electron);
console.log('Node.js version:', process.versions.node);
console.log('Log directory:', logger.getLogDirectory());
createWindow();
setTimeout(async () => {
let timeoutReached = false;
const unlockPlayButton = () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('lock-play-button', false);
}
};
const timeoutId = setTimeout(() => {
timeoutReached = true;
console.warn('First launch check timeout reached, unlocking play button');
unlockPlayButton();
}, 15000);
try {
console.log('Starting first launch check...');
@@ -132,7 +156,19 @@ app.whenReady().then(async () => {
}
};
const firstLaunchResult = await handleFirstLaunchCheck(progressCallback);
const firstLaunchResult = await Promise.race([
handleFirstLaunchCheck(progressCallback),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('First launch check timeout')), 12000);
})
]);
clearTimeout(timeoutId);
if (timeoutReached) {
console.log('Timeout already reached, skipping result processing');
return;
}
console.log('First launch check result:', firstLaunchResult);
@@ -141,32 +177,39 @@ app.whenReady().then(async () => {
console.log('Sending show-first-launch-update event...');
setTimeout(() => {
mainWindow.webContents.send('show-first-launch-update', {
existingGame: firstLaunchResult.existingGame,
isFirstLaunch: firstLaunchResult.isFirstLaunch
});
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('show-first-launch-update', {
existingGame: firstLaunchResult.existingGame,
isFirstLaunch: firstLaunchResult.isFirstLaunch
});
}
}, 1000);
} else if (firstLaunchResult.isFirstLaunch && !firstLaunchResult.existingGame) {
console.log('Sending show-first-launch-welcome event...');
setTimeout(() => {
mainWindow.webContents.send('show-first-launch-welcome');
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('show-first-launch-welcome');
}
}, 1000);
} else {
mainWindow.webContents.send('lock-play-button', false);
unlockPlayButton();
}
}
} catch (error) {
clearTimeout(timeoutId);
console.error('Error during first launch check:', error);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('lock-play-button', false);
if (!timeoutReached) {
unlockPlayButton();
}
}
}, 3000);
});
app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ===');
// Clean up Discord RPC connection
if (discordRPC) {
try {
@@ -198,6 +241,12 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) =
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
if (mainWindow && !mainWindow.isDestroyed()) {
setTimeout(() => {
mainWindow.webContents.send('progress-complete');
}, 2000);
}
return result;
} catch (error) {
console.error('Launch error:', error);
@@ -223,6 +272,12 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath)
const result = await installGame(playerName, progressCallback, javaPath, installPath);
if (mainWindow && !mainWindow.isDestroyed()) {
setTimeout(() => {
mainWindow.webContents.send('progress-complete');
}, 1000);
}
return result;
} catch (error) {
console.error('Install error:', error);
@@ -257,6 +312,7 @@ ipcMain.handle('load-java-path', () => {
ipcMain.handle('save-install-path', (event, installPath) => {
saveInstallPath(installPath);
logger.updateInstallPath();
return { success: true };
});
@@ -311,8 +367,16 @@ ipcMain.handle('mark-as-launched', async () => {
}
});
ipcMain.handle('is-game-installed', () => {
return isGameInstalled();
ipcMain.handle('is-game-installed', async () => {
try {
return await Promise.race([
Promise.resolve(isGameInstalled()),
new Promise((resolve) => setTimeout(() => resolve(false), 5000))
]);
} catch (error) {
console.error('Error checking game installation:', error);
return false;
}
});
ipcMain.handle('uninstall-game', async () => {
@@ -345,6 +409,23 @@ ipcMain.handle('open-external', async (event, url) => {
}
});
ipcMain.handle('open-game-location', async () => {
try {
const { getResolvedAppDir } = require('./backend/launcher');
const gameDir = path.join(getResolvedAppDir(), 'release', 'package', 'game');
if (fs.existsSync(gameDir)) {
await shell.openPath(gameDir);
return { success: true };
} else {
throw new Error('Game directory not found');
}
} catch (error) {
console.error('Failed to open game location:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('browse-java-path', async () => {
const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
@@ -392,7 +473,10 @@ ipcMain.handle('save-settings', async (event, settings) => {
try {
if (settings.playerName) saveUsername(settings.playerName);
if (settings.javaPath !== undefined) saveJavaPath(settings.javaPath);
if (settings.installPath !== undefined) saveInstallPath(settings.installPath);
if (settings.installPath !== undefined) {
saveInstallPath(settings.installPath);
logger.updateInstallPath();
}
return { success: true };
} catch (error) {
console.error('Save settings error:', error);
@@ -564,3 +648,34 @@ ipcMain.handle('window-minimize', () => {
}
});
ipcMain.handle('get-log-directory', () => {
return logger.getLogDirectory();
});
ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
try {
const logDir = logger.getLogDirectory();
if (!logDir) return null;
// Find the most recent log file
const files = fs.readdirSync(logDir)
.filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
.map(file => ({
name: file,
path: path.join(logDir, file),
mtime: fs.statSync(path.join(logDir, file)).mtime
}))
.sort((a, b) => b.mtime - a.mtime);
if (files.length === 0) return null;
const latestLogFile = files[0].path;
const content = fs.readFileSync(latestLogFile, 'utf8');
const lines = content.split('\n');
return lines.slice(-maxLines).join('\n');
} catch (error) {
console.error('Error reading logs:', error);
return null;
}
});

9869
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "hytale-f2p-launcher",
"version": "2.0.0",
"version": "2.0.1",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js",
@@ -37,9 +37,12 @@
"adm-zip": "^0.5.10",
"axios": "^1.6.0",
"discord-rpc": "^4.0.1",
"tar": "^7.0.0",
"tar": "^6.2.1",
"uuid": "^9.0.1"
},
"overrides": {
"tar": "$tar"
},
"build": {
"appId": "com.hytalef2p.launcher",
"productName": "Hytale F2P",
@@ -94,7 +97,8 @@
{
"target": "dmg",
"arch": [
"universal"
"x64",
"arm64"
]
},
{
@@ -115,3 +119,6 @@
}
}
}

View File

@@ -20,6 +20,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
openExternal: (url) => ipcRenderer.invoke('open-external', url),
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
openGameLocation: () => ipcRenderer.invoke('open-game-location'),
saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
loadSettings: () => ipcRenderer.invoke('load-settings'),
getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'),
@@ -33,6 +34,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
onProgressUpdate: (callback) => {
ipcRenderer.on('progress-update', (event, data) => callback(data));
},
onProgressComplete: (callback) => {
ipcRenderer.on('progress-complete', () => callback());
},
getUserId: () => ipcRenderer.invoke('get-user-id'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
@@ -54,5 +58,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
onLockPlayButton: (callback) => {
ipcRenderer.on('lock-play-button', (event, locked) => callback(locked));
}
},
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines)
});