Compare commits

..

3 Commits

49 changed files with 6239 additions and 13653 deletions

View File

@@ -1,2 +0,0 @@
CURSEFORGE_API_KEY=$1234asdxXXXXXXkQCXXXXXXXXXXASDb32
DISCORD_CLIENT_ID=561263XXXXXX

18
.github/README1.md vendored
View File

@@ -22,25 +22,13 @@ All builds run in parallel:
### Creating a Release ### Creating a Release
**⚠️ IMPORTANT: Semantic Versioning Required** 1. Update version in `package.json`
This project uses **strict semantic versioning with numerical versions only**:
-**Valid**: `2.0.1`, `2.0.11`, `2.1.0`, `3.0.0`
-**Invalid**: `2.0.2b`, `2.0.2a`, `2.0.1-beta`
**Format**: `MAJOR.MINOR.PATCH` (e.g., `2.0.11`)
The auto-update system requires semantic versioning for proper version comparison. Letter suffixes are not supported.
**Steps:**
1. Update version in `package.json` (use numerical format only, e.g., `2.0.11`)
2. Commit and push to `main` 2. Commit and push to `main`
3. Create and push a version tag: 3. Create and push a version tag:
```bash ```bash
git tag v2.0.11 git tag v2.0.1
git push origin v2.0.11 git push origin v2.0.1
``` ```
The workflow will: The workflow will:

View File

@@ -3,7 +3,7 @@ name: Build and Release
on: on:
push: push:
branches: branches:
- release - main
tags: tags:
- 'v*' - 'v*'
workflow_dispatch: workflow_dispatch:
@@ -13,31 +13,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# FIX Install bsdtar for Pacman builds
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y libarchive-tools
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
cache: 'npm' cache: 'npm'
- run: npm install
- run: npm ci - run: npm ci
- run: npx electron-builder --linux --publish never
- name: Build Linux Packages
run: |
npx electron-builder --linux --x64 --arm64 --publish never
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: linux-builds name: linux-builds
path: | path: |
dist/*.AppImage dist/*.AppImage
dist/*.AppImage.blockmap
dist/*.deb dist/*.deb
dist/*.rpm
dist/*.pacman
dist/latest-linux.yml
build-windows: build-windows:
runs-on: windows-latest runs-on: windows-latest
@@ -47,6 +35,7 @@ jobs:
with: with:
node-version: '22' node-version: '22'
cache: 'npm' cache: 'npm'
- run: npm install
- run: npm ci - run: npm ci
- run: npx electron-builder --win --publish never - run: npx electron-builder --win --publish never
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@@ -54,8 +43,6 @@ jobs:
name: windows-builds name: windows-builds
path: | path: |
dist/*.exe dist/*.exe
dist/*.exe.blockmap
dist/latest.yml
build-macos: build-macos:
runs-on: macos-latest runs-on: macos-latest
@@ -65,6 +52,7 @@ jobs:
with: with:
node-version: '22' node-version: '22'
cache: 'npm' cache: 'npm'
- run: npm install
- run: npm ci - run: npm ci
- run: npx electron-builder --mac --publish never - run: npx electron-builder --mac --publish never
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@@ -78,19 +66,12 @@ jobs:
release: release:
needs: [build-linux, build-windows, build-macos] needs: [build-linux, build-windows, build-macos]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: | if: startsWith(github.ref, 'refs/tags/v')
startsWith(github.ref, 'refs/tags/v') ||
github.ref == 'refs/heads/release' ||
github.event_name == 'workflow_dispatch'
permissions: permissions:
contents: write contents: write
steps: steps:
# FIX: './package.json' Module Not Found in `Get version` step
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -99,22 +80,13 @@ jobs:
- name: Display structure of downloaded files - name: Display structure of downloaded files
run: ls -R artifacts run: ls -R artifacts
- name: Get version from package.json
id: pkg_version
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
# If it's a tag, use the tag.
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
# If it's the 'release' branch, use 'v2.0.2-beta.r42'
name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}-beta.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
files: | files: |
artifacts/linux-builds/**/* artifacts/linux-builds/*
artifacts/windows-builds/**/* artifacts/windows-builds/*
artifacts/macos-builds/**/* artifacts/macos-builds/*
generate_release_notes: true generate_release_notes: true
draft: true draft: false
# DYNAMIC FLAGS: Mark as pre-release ONLY IF it's NOT a tag (meaning it's a branch push) prerelease: false
prerelease: ${{ github.ref_type != 'tag' }}

13
.gitignore vendored
View File

@@ -1,14 +1,3 @@
dist/* dist/*
node_modules/* node_modules/*
bun.lock package-lock.json
# Build artifacts
src/
pkg/
# Package files
*.tar.zst
*.zst.DS_Store
*.zst
bun.lockb
.env

View File

@@ -1,26 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hytale F2P Launcher</title> <title>Hytale F2P Launcher</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> <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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link <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">
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" <link rel="stylesheet" href="style.css">
rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head> </head>
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1"> <body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
<div class="absolute inset-0 z-0"> <div class="absolute inset-0 z-0">
<img src="https://assets.authbp.xyz/bg.png" alt="Background" class="w-full h-full object-cover" /> <img src="https://i.imgur.com/Visrk66.png" alt="Background" class="w-full h-full object-cover" />
<div class="absolute inset-0 bg-black/60"></div> <div class="absolute inset-0 bg-black/60"></div>
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox=" 0 0 256 256" <div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"%3E%3Cfilter id="noiseFilter"%3E%3CfeTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/%3E%3C/filter%3E%3Crect width="100%25" height="100%25" filter="url(%23noiseFilter)" opacity="0.1"/%3E%3C/svg%3E')] opacity-20"></div>
xmlns="http://www.w3.org/2000/svg" %3E%3Cfilter id="noiseFilter" %3E%3CfeTurbulence type="fractalNoise"
baseFrequency="0.65" numOctaves="3" stitchTiles="stitch" /%3E%3C/filter%3E%3Crect width="100%25"
height="100%25" filter="url(%23noiseFilter)" opacity="0.1" /%3E%3C/svg%3E')] opacity-20"></div>
</div> </div>
<div class="flex w-full h-screen relative z-10"> <div class="flex w-full h-screen relative z-10">
@@ -33,27 +26,27 @@
<div class="sidebar-nav"> <div class="sidebar-nav">
<div class="nav-item active" data-page="play"> <div class="nav-item active" data-page="play">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
<span class="nav-tooltip" data-i18n="nav.play">Play</span> <span class="nav-tooltip">Play</span>
</div> </div>
<div class="nav-item" data-page="mods"> <div class="nav-item" data-page="mods">
<i class="fas fa-box"></i> <i class="fas fa-box"></i>
<span class="nav-tooltip" data-i18n="nav.mods">Mods</span> <span class="nav-tooltip">Mods</span>
</div> </div>
<div class="nav-item" data-page="news"> <div class="nav-item" data-page="news">
<i class="fas fa-newspaper"></i> <i class="fas fa-newspaper"></i>
<span class="nav-tooltip" data-i18n="nav.news">News</span> <span class="nav-tooltip">News</span>
</div> </div>
<div class="nav-item" data-page="chat"> <div class="nav-item" data-page="chat">
<i class="fas fa-comments"></i> <i class="fas fa-comments"></i>
<span class="nav-tooltip" data-i18n="nav.chat">Players Chat</span> <span class="nav-tooltip">Players Chat</span>
</div> </div>
<div class="nav-item" data-page="settings"> <div class="nav-item" data-page="settings">
<i class="fas fa-cog"></i> <i class="fas fa-cog"></i>
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span> <span class="nav-tooltip">Settings</span>
</div> </div>
<div class="nav-item logs-nav-item" data-page="logs" id="openLogsBtn" onclick="openLogs()"> <div class="nav-item" data-page="skins">
<i class="fas fa-terminal"></i> <i class="fas fa-user"></i>
<span class="nav-tooltip">Logs</span> <span class="nav-tooltip">Skins</span>
</div> </div>
</div> </div>
@@ -64,35 +57,14 @@
<header class="header"> <header class="header">
<div id="playersOnlineCounter" class="players-counter"> <div id="playersOnlineCounter" class="players-counter">
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
<span class="counter-label" data-i18n="header.playersLabel">Players:</span> <span class="counter-label">Players:</span>
<span id="onlineCount" class="counter-value">0</span> <span id="onlineCount" class="counter-value">0</span>
</div> </div>
<div class="profile-selector" id="profileSelector">
<button class="profile-btn" onclick="toggleProfileDropdown()">
<i class="fas fa-user-circle"></i>
<span id="currentProfileName">Default</span>
<i class="fas fa-chevron-down"></i>
</button>
<div class="profile-dropdown" id="profileDropdown">
<div class="profile-list" id="profileList">
<!-- Profiles populated by JS -->
</div>
<div class="profile-divider"></div>
<div class="profile-action" onclick="openProfileManager()">
<i class="fas fa-cog"></i>
<span data-i18n="header.manageProfiles">Manage Profiles</span>
</div>
</div>
</div>
<div class="window-controls"> <div class="window-controls">
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()"> <button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
<i class="fas fa-minus"></i> <i class="fas fa-minus"></i>
</button> </button>
<button class="control-btn maximize" onclick="toggleMaximize()">
<i class="fas fa-square"></i>
</button>
<button class="control-btn close" onclick="window.electronAPI?.closeWindow()"> <button class="control-btn close" onclick="window.electronAPI?.closeWindow()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
@@ -103,6 +75,9 @@
<h1 class="game-title"> <h1 class="game-title">
HY<span class="title-accent">TALE</span> HY<span class="title-accent">TALE</span>
</h1> </h1>
<div class="game-tags">
<span class="tag">FREE TO PLAY</span>
</div>
</div> </div>
<div class="content-pages"> <div class="content-pages">
@@ -110,34 +85,28 @@
<div class="install-content"> <div class="install-content">
<div class="install-header"> <div class="install-header">
<h1 class="install-title"> <h1 class="install-title">
HY<span class="title-accent">TALE</span> HYTA<span class="title-accent">LE</span>
</h1> </h1>
<p class="install-subtitle" data-i18n="install.title">FREE TO PLAY LAUNCHER</p> <p class="install-subtitle">FREE TO PLAY LAUNCHER</p>
</div> </div>
<div class="install-form"> <div class="install-form">
<div class="form-group"> <div class="form-group">
<label class="form-label" data-i18n="install.playerName">Player Name</label> <label class="form-label">Player Name</label>
<input type="text" id="installPlayerName" <input type="text" id="installPlayerName" placeholder="Enter your name" class="form-input" value="Player" />
data-i18n-placeholder="install.playerNamePlaceholder" class="form-input"
value="Player" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-group"> <label class="checkbox-group">
<input type="checkbox" id="installCustomCheck" class="custom-checkbox"> <input type="checkbox" id="installCustomCheck" class="custom-checkbox">
<span class="checkbox-label" data-i18n="install.customInstallation">Custom <span class="checkbox-label">Custom Installation</span>
Installation</span>
</label> </label>
<div id="installCustomOptions" class="custom-options"> <div id="installCustomOptions" class="custom-options">
<div class="form-subgroup"> <div class="form-subgroup">
<label class="form-label" data-i18n="install.installationFolder">Installation <label class="form-label">Installation Folder</label>
Folder</label>
<div class="input-with-button"> <div class="input-with-button">
<input type="text" id="installPath" <input type="text" id="installPath" placeholder="Default location" class="form-input" readonly />
data-i18n-placeholder="install.pathPlaceholder" class="form-input"
readonly />
<button onclick="browseInstallPath()" class="browse-btn"> <button onclick="browseInstallPath()" class="browse-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
</button> </button>
@@ -148,7 +117,7 @@
<button id="installBtn" class="install-button" onclick="installGame()"> <button id="installBtn" class="install-button" onclick="installGame()">
<i class="fas fa-download mr-2"></i> <i class="fas fa-download mr-2"></i>
<span id="installText" data-i18n="install.installButton">INSTALL HYTALE</span> <span id="installText">INSTALL HYTALE</span>
</button> </button>
</div> </div>
</div> </div>
@@ -161,15 +130,14 @@
<div class="play-header"> <div class="play-header">
<h2 class="play-title"> <h2 class="play-title">
<i class="fas fa-play-circle mr-2"></i> <i class="fas fa-play-circle mr-2"></i>
<span data-i18n="play.ready">READY TO PLAY</span> READY TO PLAY
</h2> </h2>
<p class="play-subtitle" data-i18n="play.subtitle">Launch Hytale and enter the <p class="play-subtitle">Launch Hytale and enter the adventure</p>
adventure</p>
</div> </div>
<button id="homePlayBtn" class="home-play-button" onclick="launch()"> <button id="homePlayBtn" class="home-play-button" onclick="launch()">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
<span data-i18n="play.playButton">PLAY HYTALE</span> <span>PLAY HYTALE</span>
</button> </button>
</div> </div>
</div> </div>
@@ -178,11 +146,10 @@
<div class="news-header"> <div class="news-header">
<h2 class="news-title"> <h2 class="news-title">
<i class="fas fa-star mr-2"></i> <i class="fas fa-star mr-2"></i>
<span data-i18n="play.latestNews">LATEST NEWS</span> LATEST NEWS
</h2> </h2>
<button class="view-all-btn" onclick="navigateToPage('news')"> <button class="view-all-btn" onclick="navigateToPage('news')">
<span data-i18n="play.viewAll">VIEW ALL</span> <i VIEW ALL <i class="fas fa-arrow-right ml-1"></i>
class="fas fa-arrow-right ml-1"></i>
</button> </button>
</div> </div>
<div id="newsGrid" class="news-grid-horizontal"></div> <div id="newsGrid" class="news-grid-horizontal"></div>
@@ -193,13 +160,12 @@
<div class="mods-header"> <div class="mods-header">
<div class="mods-search-container"> <div class="mods-search-container">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
<input type="text" id="modsSearch" data-i18n-placeholder="mods.searchPlaceholder" <input type="text" id="modsSearch" placeholder="Search mods..." class="mods-search" />
class="mods-search" />
</div> </div>
<div class="mods-actions"> <div class="mods-actions">
<button id="myModsBtn" class="mods-btn-primary"> <button id="myModsBtn" class="mods-btn-primary">
<i class="fas fa-box"></i> <i class="fas fa-box"></i>
<span data-i18n="mods.myMods">MY MODS</span> MY MODS
</button> </button>
</div> </div>
</div> </div>
@@ -210,14 +176,13 @@
<div class="mods-pagination"> <div class="mods-pagination">
<button id="prevPage" class="pagination-btn"> <button id="prevPage" class="pagination-btn">
<i class="fas fa-chevron-left"></i> <i class="fas fa-chevron-left"></i>
<span data-i18n="mods.previous">PREVIOUS</span> PREVIOUS
</button> </button>
<span class="pagination-info"> <span class="pagination-info">
<span data-i18n="mods.page">Page</span> <span id="currentPage">1</span> <span Page <span id="currentPage">1</span> of <span id="totalPages">1</span>
data-i18n="mods.of">of</span> <span id="totalPages">1</span>
</span> </span>
<button id="nextPage" class="pagination-btn"> <button id="nextPage" class="pagination-btn">
<span data-i18n="mods.next">NEXT</span> NEXT
<i class="fas fa-chevron-right"></i> <i class="fas fa-chevron-right"></i>
</button> </button>
</div> </div>
@@ -227,7 +192,7 @@
<div class="news-header"> <div class="news-header">
<h2 class="news-title"> <h2 class="news-title">
<i class="fas fa-newspaper mr-2"></i> <i class="fas fa-newspaper mr-2"></i>
<span data-i18n="news.title">ALL NEWS</span> ALL NEWS
</h2> </h2>
</div> </div>
<div id="allNewsGrid" class="news-grid-full"></div> <div id="allNewsGrid" class="news-grid-full"></div>
@@ -238,17 +203,11 @@
<div class="chat-header"> <div class="chat-header">
<h2 class="chat-title"> <h2 class="chat-title">
<i class="fas fa-comments mr-2"></i> <i class="fas fa-comments mr-2"></i>
<span data-i18n="chat.title">PLAYERS CHAT</span> PLAYERS CHAT
</h2> </h2>
<div class="chat-header-actions">
<button id="chatColorBtn" class="chat-color-btn">
<i class="fas fa-palette"></i>
<span data-i18n="chat.pickColor">Color</span>
</button>
<div class="chat-online-badge"> <div class="chat-online-badge">
<i class="fas fa-circle"></i> <i class="fas fa-circle"></i>
<span id="chatOnlineCount">0</span> <span data-i18n="chat.online">online</span> <span id="chatOnlineCount">0</span> online
</div>
</div> </div>
</div> </div>
@@ -259,9 +218,13 @@
<div class="chat-footer"> <div class="chat-footer">
<div class="chat-input-container"> <div class="chat-input-container">
<textarea id="chatInput" class="chat-input" <textarea
data-i18n-placeholder="chat.inputPlaceholder" rows="1" id="chatInput"
maxlength="500"></textarea> class="chat-input"
placeholder="Type your message... (Links are automatically censored)"
rows="1"
maxlength="500"
></textarea>
<button id="chatSendBtn" class="chat-send-btn"> <button id="chatSendBtn" class="chat-send-btn">
<i class="fas fa-paper-plane"></i> <i class="fas fa-paper-plane"></i>
</button> </button>
@@ -270,7 +233,7 @@
<span class="chat-char-counter" id="chatCharCounter">0/500</span> <span class="chat-char-counter" id="chatCharCounter">0/500</span>
<span class="chat-warning-text"> <span class="chat-warning-text">
<i class="fas fa-shield-alt"></i> <i class="fas fa-shield-alt"></i>
<span data-i18n="chat.secureChat">Secure chat - Links are censored</span> Secure chat - Links are censored
</span> </span>
</div> </div>
</div> </div>
@@ -282,180 +245,15 @@
<div class="settings-header"> <div class="settings-header">
<h2 class="settings-title"> <h2 class="settings-title">
<i class="fas fa-cog mr-2"></i> <i class="fas fa-cog mr-2"></i>
<span data-i18n="settings.title">SETTINGS</span> SETTINGS
</h2> </h2>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-gamepad"></i>
<span data-i18n="settings.game">Game Options</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.playerName">Player
Name</label>
<input type="text" id="settingsPlayerName" class="settings-input"
data-i18n-placeholder="settings.playerNamePlaceholder" maxlength="16" />
<p class="settings-hint">
<i class="fas fa-user"></i>
<span data-i18n="settings.playerNameHint">This name will be used in-game
(1-16 characters)</span>
</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" data-i18n="settings.openGameLocation">Open
Game Location</div>
<div class="btn-description"
data-i18n="settings.openGameLocationDesc">Open the game
installation folder</div>
</div>
</button>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="repairGameBtn" class="settings-action-btn"
onclick="repairGame()">
<i class="fas fa-tools"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.repairGame">Repair Game
</div>
<div class="btn-description" data-i18n="settings.reinstallGame">
Reinstall game files (preserves data)
</div>
</div>
</button>
</div>
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU
Preference</label>
<div class="segmented-control">
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto"
checked>
<label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
<input type="radio" id="gpu-integrated" name="gpuPreference"
value="integrated">
<label for="gpu-integrated"
data-i18n="settings.gpuIntegrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference"
value="dedicated">
<label for="gpu-dedicated"
data-i18n="settings.gpuDedicated">Dedicated</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux:
affects DRI_PRIME)</span>
</p>
<div id="gpu-detection-info" class="gpu-detection-info"></div>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-fingerprint"></i>
<span data-i18n="settings.account">Player UUID Management</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.currentUUID">Current
UUID</label>
<div class="uuid-display-container">
<input type="text" id="currentUuid" class="settings-input uuid-input"
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.uuidHint">Your unique player identifier for
this username</span>
</p>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="manageUuidsBtn" class="settings-action-btn">
<i class="fas fa-list"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All
UUIDs</div>
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">
View and manage all player UUIDs</div>
</div>
</button>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
<span data-i18n="settings.discord">Discord Integration</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable
Discord Rich Presence</div>
<div class="checkbox-description"
data-i18n="settings.discordDescription">Show your launcher activity
on Discord
</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-window-close"></i>
<span data-i18n="settings.closeLauncher">Launcher Behavior</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="closeLauncherCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.closeOnStart">Close Launcher on game start</div>
<div class="checkbox-description" data-i18n="settings.closeOnStartDescription">
Automatically close the launcher after Hytale has launched
</div>
</div>
</label>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-coffee"></i> <i class="fas fa-coffee"></i>
<span data-i18n="settings.java">Java Runtime</span> Java Runtime
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
@@ -463,31 +261,31 @@
<input type="checkbox" id="customJavaCheck" /> <input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span> <span class="checkmark"></span>
<div class="checkbox-content"> <div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.useCustomJava">Use <div class="checkbox-title">Use Custom Java Path</div>
Custom Java Path</div> <div class="checkbox-description">Override the bundled Java runtime with your own installation</div>
<div class="checkbox-description" data-i18n="settings.javaDescription">
Override the bundled Java runtime with
your own installation</div>
</div> </div>
</label> </label>
</div> </div>
<div id="customJavaOptions" class="custom-java-options" style="display: none;"> <div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.javaPath">Java <label class="settings-input-label">Java Executable Path</label>
Executable Path</label>
<div class="settings-input-with-button"> <div class="settings-input-with-button">
<input type="text" id="customJavaPath" class="settings-input" <input
data-i18n-placeholder="settings.javaPathPlaceholder" readonly /> type="text"
id="customJavaPath"
class="settings-input"
placeholder="Select Java path..."
readonly
/>
<button id="browseJavaBtn" class="settings-browse-btn"> <button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
<span data-i18n="settings.javaBrowse">Browse</span> Browse
</button> </button>
</div> </div>
<p class="settings-hint"> <p class="settings-hint">
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
<span data-i18n="settings.javaHint">Select the Java installation folder Select the Java installation folder (supports Windows, Mac, Linux)
(supports Windows, Mac, Linux)</span>
</p> </p>
</div> </div>
</div> </div>
@@ -495,17 +293,36 @@
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-language"></i> <i class="fas fa-gamepad"></i>
<span data-i18n="settings.language">Language</span> Game Options
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label" <label class="settings-input-label">Player Name</label>
data-i18n="settings.selectLanguage">Select Language</label> <input
<select id="languageSelect" class="settings-input"> type="text"
<!-- Options populated by i18n.js --> id="settingsPlayerName"
</select> class="settings-input"
placeholder="Enter your player name"
maxlength="16"
/>
<p class="settings-hint">
<i class="fas fa-user"></i>
This name will be used in-game (1-16 characters)
</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> </div>
@@ -513,30 +330,11 @@
</div> </div>
</div> </div>
<div id="logs-page" class="page"> <div id="skins-page" class="page">
<div class="logs-container"> <div class="placeholder-content">
<div class="logs-header"> <i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
<h2 class="logs-title"> <h2>Skins</h2>
<i class="fas fa-terminal"></i> <p>Skin customization coming soon...</p>
<span data-i18n="settings.logs">SYSTEM LOGS</span>
</h2>
<div class="logs-actions">
<button class="logs-action-btn" onclick="copyLogs()">
<i class="fas fa-copy"></i> <span data-i18n="settings.logsCopy">Copy</span>
</button>
<button class="logs-action-btn" onclick="refreshLogs()">
<i class="fas fa-sync-alt"></i> <span
data-i18n="settings.logsRefresh">Refresh</span>
</button>
<button class="logs-action-btn" onclick="openLogsFolder()">
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
Folder</span>
</button>
</div>
</div>
<div id="logsTerminal" class="logs-terminal">
<div class="text-gray-500 text-center mt-10" data-i18n="settings.logsLoading">Loading
logs...</div>
</div> </div>
</div> </div>
</div> </div>
@@ -549,7 +347,7 @@
<div class="mods-modal-header"> <div class="mods-modal-header">
<h2 class="mods-modal-title"> <h2 class="mods-modal-title">
<i class="fas fa-box mr-2"></i> <i class="fas fa-box mr-2"></i>
<span data-i18n="mods.modalTitle">MY MODS</span> MY MODS
</h2> </h2>
<button id="closeMyModsModal" class="mods-modal-close"> <button id="closeMyModsModal" class="mods-modal-close">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@@ -565,7 +363,7 @@
<div id="progressOverlay" class="progress-overlay" style="display: none;"> <div id="progressOverlay" class="progress-overlay" style="display: none;">
<div class="progress-content"> <div class="progress-content">
<div class="progress-info"> <div class="progress-info">
<span id="progressText" data-i18n="progress.initializing">Initializing...</span> <span id="progressText">Initializing...</span>
<span id="progressPercent">0%</span> <span id="progressPercent">0%</span>
</div> </div>
<div class="progress-bar-container"> <div class="progress-bar-container">
@@ -578,171 +376,55 @@
</div> </div>
</div> </div>
<!-- Installation effects overlay -->
<div id="installationEffects" class="installation-effects" style="display: none;">
<div class="space-effects">
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
<div class="warp-line"></div>
</div>
</div>
<div id="chatUsernameModal" class="chat-username-modal" style="display: none;"> <div id="chatUsernameModal" class="chat-username-modal" style="display: none;">
<div class="chat-username-modal-content"> <div class="chat-username-modal-content">
<div class="chat-username-modal-header"> <div class="chat-username-modal-header">
<h2 class="chat-username-modal-title"> <h2 class="chat-username-modal-title">
<i class="fas fa-comments mr-2"></i> <i class="fas fa-comments mr-2"></i>
<span data-i18n="chat.joinChat">Join Chat</span> Join Chat
</h2> </h2>
</div> </div>
<div class="chat-username-modal-body"> <div class="chat-username-modal-body">
<p class="chat-username-modal-description" data-i18n="chat.chooseUsername"> <p class="chat-username-modal-description">
Choose a username to join the Players Chat Choose a username to join the Players Chat
</p> </p>
<div class="chat-username-input-group"> <div class="chat-username-input-group">
<label for="chatUsernameInput" class="chat-username-label" <label for="chatUsernameInput" class="chat-username-label">Username</label>
data-i18n="chat.username">Username</label> <input
<input type="text" id="chatUsernameInput" class="chat-username-input" type="text"
data-i18n-placeholder="chat.usernamePlaceholder" maxlength="20" autocomplete="off" /> id="chatUsernameInput"
<span class="chat-username-hint" data-i18n="chat.usernameHint">3-20 characters, letters, numbers, - class="chat-username-input"
and _ only</span> placeholder="Enter your username..."
maxlength="20"
autocomplete="off"
/>
<span class="chat-username-hint">3-20 characters, letters, numbers, - and _ only</span>
<span id="chatUsernameError" class="chat-username-error"></span> <span id="chatUsernameError" class="chat-username-error"></span>
</div> </div>
</div> </div>
<div class="chat-username-modal-footer"> <div class="chat-username-modal-footer">
<button id="chatUsernameCancel" class="chat-username-btn-cancel"> <button id="chatUsernameCancel" class="chat-username-btn-cancel">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
<span data-i18n="common.cancel">Cancel</span> Cancel
</button> </button>
<button id="chatUsernameSubmit" class="chat-username-btn-submit"> <button id="chatUsernameSubmit" class="chat-username-btn-submit">
<i class="fas fa-check"></i> <i class="fas fa-check"></i>
<span data-i18n="chat.joinButton">Join Chat</span> Join Chat
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- UUID Management Modal -->
<div id="uuidModal" class="uuid-modal" style="display: none;">
<div class="uuid-modal-content">
<div class="uuid-modal-header">
<h2 class="uuid-modal-title">
<i class="fas fa-fingerprint mr-2"></i>
<span data-i18n="uuid.modalTitle">UUID Management</span>
</h2>
<button id="uuidModalClose" class="modal-close-btn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="uuid-modal-body">
<div class="uuid-current-section">
<h3 class="uuid-section-title" data-i18n="uuid.currentUserUUID">Current User UUID</h3>
<div class="uuid-current-display">
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="modalRegenerateUuidBtn" class="uuid-action-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="uuid-list-section">
<div class="uuid-list-header">
<h3 class="uuid-section-title" data-i18n="uuid.allPlayerUUIDs">All Player UUIDs</h3>
<button id="generateNewUuidBtn" class="uuid-generate-btn">
<i class="fas fa-plus"></i>
<span data-i18n="uuid.generateNew">Generate New UUID</span>
</button>
</div>
<div id="uuidList" class="uuid-list">
<div class="uuid-loading">
<i class="fas fa-spinner fa-spin"></i>
<span data-i18n="uuid.loadingUUIDs">Loading UUIDs...</span>
</div>
</div>
</div>
<div class="uuid-custom-section">
<h3 class="uuid-section-title" data-i18n="uuid.setCustomUUID">Set Custom UUID</h3>
<div class="uuid-custom-form">
<input type="text" id="customUuidInput" class="uuid-input"
data-i18n-placeholder="uuid.customPlaceholder" maxlength="36" />
<button id="setCustomUuidBtn" class="uuid-set-btn">
<i class="fas fa-check"></i>
<span data-i18n="uuid.setUUID">Set UUID</span>
</button>
</div>
<p class="uuid-custom-hint">
<i class="fas fa-exclamation-triangle"></i>
<span data-i18n="uuid.warning">Warning: Setting a custom UUID will change your current player
identity</span>
</p>
</div>
</div>
</div>
</div>
<!-- Profile Manager Modal -->
<div id="profileManagerModal" class="profile-modal" style="display: none;">
<div class="profile-modal-content">
<div class="profile-modal-header">
<h2 class="profile-modal-title">
<i class="fas fa-users-cog mr-2"></i>
<span data-i18n="profiles.modalTitle">Manage Profiles</span>
</h2>
<button class="modal-close-btn" onclick="closeProfileManager()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="profile-modal-body">
<div class="profile-manager-list" id="managerProfileList">
<!-- Populated by JS -->
</div>
<div class="profile-create-section">
<input type="text" id="newProfileName" data-i18n-placeholder="profiles.newProfilePlaceholder"
class="profile-input" maxlength="20">
<button class="profile-create-btn" onclick="createNewProfile()">
<i class="fas fa-plus"></i> <span data-i18n="profiles.createProfile">Create Profile</span>
</button>
</div>
</div>
</div>
</div>
<div class="version-display-bottom">
<i class="fas fa-code-branch"></i>
<span id="launcherVersion">Loading...</span>
</div>
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2"> <footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
<div class="flex items-center justify-center text-xs text-gray-400"> <div class="flex items-center justify-center text-xs text-gray-400">
<span>Made by <a href="https://github.com/amiayweb" target="_blank" <span>Made by <a href="https://github.com/amiayweb" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a></span>
class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a> & <a
href="https://github.com/Relyz1993" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@Relyz</a></span>
<span class="mx-2">|</span> <span class="mx-2">|</span>
<span>Contributors: <span>Contributors:
<a href="https://github.com/chasem-dev" target="_blank" <a href="https://github.com/chasem-dev" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>,
class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>, <a href="https://github.com/crimera" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>,
<a href="https://github.com/crimera" target="_blank" <a href="https://github.com/sanasol" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>,
class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>, <a href="https://github.com/Terromur" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
<a href="https://github.com/sanasol" target="_blank" <a href="https://github.com/ericiskoolbeans" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>
class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>,
<a href="https://github.com/Terromur" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
<a href="https://github.com/ericiskoolbeans" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
<a href="https://github.com/fazrigading" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
</span> </span>
</div> </div>
</footer> </footer>
@@ -751,66 +433,16 @@
<div id="discordNotification" class="discord-notification"> <div id="discordNotification" class="discord-notification">
<div class="notification-content"> <div class="notification-content">
<i class="fab fa-discord"></i> <i class="fab fa-discord"></i>
<span class="notification-text" data-i18n="discord.notificationText">Join our Discord community!</span> <span class="notification-text">Join our Discord community!</span>
<button class="notification-action" <button class="notification-action" onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')"> Join Discord
<span data-i18n="discord.joinButton">Join Discord</span>
</button> </button>
</div> </div>
<button class="notification-close" onclick="closeDiscordNotification()"> <button class="notification-close" onclick="closeDiscordNotification()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<!-- Modal pour sélectionner la couleur du chat -->
<div id="chatColorModal" class="chat-color-modal" style="display: none;">
<div class="chat-color-modal-content">
<div class="chat-color-modal-header">
<h3 class="chat-color-modal-title">
<i class="fas fa-palette"></i>
<span data-i18n="chat.colorModal.title">Customize Username Color</span>
</h3>
<button class="modal-close-btn" onclick="closeChatColorModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="chat-color-modal-body">
<div id="solidColorSection" class="color-section">
<h4 data-i18n="chat.colorModal.chooseSolid">Choose a solid color:</h4>
<div class="predefined-colors">
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
<div class="color-option" data-color="#2ecc71" style="background: #2ecc71;"></div>
<div class="color-option" data-color="#f39c12" style="background: #f39c12;"></div>
<div class="color-option" data-color="#9b59b6" style="background: #9b59b6;"></div>
<div class="color-option" data-color="#1abc9c" style="background: #1abc9c;"></div>
<div class="color-option" data-color="#e91e63" style="background: #e91e63;"></div>
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
</div>
<div class="custom-color-input">
<label for="customColor" data-i18n="chat.colorModal.customColor">Custom color:</label>
<input type="color" id="customColor" value="#3498db">
</div>
</div>
<div class="color-preview">
<h4 data-i18n="chat.colorModal.preview">Preview:</h4>
<div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">
YourUsername</div>
</div>
</div>
<div class="chat-color-modal-footer">
<button class="btn-secondary" onclick="closeChatColorModal()"><span
data-i18n="common.cancel">Cancel</span></button>
<button class="btn-primary" onclick="applyChatColor()"><span data-i18n="chat.colorModal.apply">Apply
Color</span></button>
</div>
</div>
</div>
<script src="js/i18n.js"></script>
<script type="module" src="js/update.js"></script> <script type="module" src="js/update.js"></script>
</body> </body>
</html> </html>

View File

@@ -3,34 +3,20 @@ let socket = null;
let isAuthenticated = false; let isAuthenticated = false;
let messageQueue = []; let messageQueue = [];
let chatUsername = ''; let chatUsername = '';
let userColor = '#3498db'; const SOCKET_URL = 'http://3.10.208.30:3001';
let userBadge = null;
const SOCKET_URL = 'https://chat.hytalef2p.com';
const MAX_MESSAGE_LENGTH = 500; const MAX_MESSAGE_LENGTH = 500;
async function getOrCreatePlayerId() {
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
export async function initChat() { export async function initChat() {
if (window.electronAPI?.loadChatUsername) { if (window.electronAPI?.loadChatUsername) {
chatUsername = await window.electronAPI.loadChatUsername(); chatUsername = await window.electronAPI.loadChatUsername();
} }
if (window.electronAPI?.loadChatColor) {
const savedColor = await window.electronAPI.loadChatColor();
if (savedColor) {
userColor = savedColor;
}
}
if (!chatUsername || chatUsername.trim() === '') { if (!chatUsername || chatUsername.trim() === '') {
showUsernameModal(); showUsernameModal();
return; return;
} }
setupChatUI(); setupChatUI();
setupColorSelector();
await connectToChat(); await connectToChat();
} }
@@ -150,22 +136,13 @@ async function connectToChat() {
reconnectionDelay: 1000 reconnectionDelay: 1000
}); });
socket.on('connect', async () => { socket.on('connect', () => {
console.log('Connected to chat server'); console.log('Connected to chat server');
socket.emit('authenticate', { username: chatUsername, userId });
const uuid = await window.electronAPI?.getCurrentUuid();
socket.emit('authenticate', {
username: chatUsername,
userId,
uuid: uuid,
userColor: userColor
});
}); });
socket.on('authenticated', (data) => { socket.on('authenticated', (data) => {
isAuthenticated = true; isAuthenticated = true;
userBadge = data.badge;
addSystemMessage(`Connected as ${data.username}`); addSystemMessage(`Connected as ${data.username}`);
while (messageQueue.length > 0) { while (messageQueue.length > 0) {
@@ -178,7 +155,7 @@ async function connectToChat() {
if (data.type === 'system') { if (data.type === 'system') {
addSystemMessage(data.message); addSystemMessage(data.message);
} else if (data.type === 'user') { } else if (data.type === 'user') {
addUserMessage(data.username, data.message, data.timestamp, data.userColor, data.badge); addUserMessage(data.username, data.message, data.timestamp);
} }
}); });
@@ -249,7 +226,7 @@ function sendMessage() {
updateCharCounter(); updateCharCounter();
} }
function addUserMessage(username, message, timestamp, userColor = '#3498db', badge = null) { function addUserMessage(username, message, timestamp) {
const chatMessages = document.getElementById('chatMessages'); const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return; if (!chatMessages) return;
@@ -261,35 +238,14 @@ function addUserMessage(username, message, timestamp, userColor = '#3498db', bad
minute: '2-digit' minute: '2-digit'
}); });
let badgeHTML = '';
if (badge) {
let badgeStyle = '';
if (badge.style === 'rainbow') {
badgeStyle = `background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #ffeaa7, #fab1a0, #fd79a8); background-size: 400% 400%; animation: rainbow 3s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`;
} else if (badge.style === 'gradient') {
if (badge.badge === 'CONTRIBUTOR') {
badgeStyle = `background: linear-gradient(45deg, #22c55e, #16a34a); background-size: 200% 200%; animation: contributorGlow 2s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`;
} else {
badgeStyle = `color: ${badge.color}; font-weight: bold; display: inline;`;
}
}
badgeHTML = `<span class="user-badge" style="${badgeStyle}">[${badge.badge}]</span> `;
}
messageDiv.innerHTML = ` messageDiv.innerHTML = `
<div class="message-header"> <div class="message-header">
<span class="message-user-info">${badgeHTML}<span class="message-username" style="font-weight: bold;" data-username-color="${userColor}">${escapeHtml(username)}</span></span> <span class="message-username">${escapeHtml(username)}</span>
<span class="message-time">${time}</span> <span class="message-time">${time}</span>
</div> </div>
<div class="message-content">${message}</div> <div class="message-content">${message}</div>
`; `;
const usernameElement = messageDiv.querySelector('.message-username');
if (usernameElement) {
applyUserColorStyle(usernameElement, userColor);
}
chatMessages.appendChild(messageDiv); chatMessages.appendChild(messageDiv);
scrollToBottom(); scrollToBottom();
} }
@@ -396,104 +352,6 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
function setupColorSelector() {
const colorBtn = document.getElementById('chatColorBtn');
if (colorBtn) {
colorBtn.addEventListener('click', showChatColorModal);
}
const colorOptions = document.querySelectorAll('.color-option');
colorOptions.forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
option.classList.add('selected');
updateColorPreview();
});
});
const customColor = document.getElementById('customColor');
if (customColor) {
customColor.addEventListener('input', () => {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
updateColorPreview();
});
}
}
function showChatColorModal() {
const modal = document.getElementById('chatColorModal');
if (modal) {
modal.style.display = 'flex';
updateColorPreview();
}
}
window.closeChatColorModal = function() {
const modal = document.getElementById('chatColorModal');
if (modal) {
modal.style.display = 'none';
}
}
function updateColorPreview() {
const preview = document.getElementById('colorPreview');
if (!preview) return;
const selectedOption = document.querySelector('.color-option.selected');
let color = '#3498db';
if (selectedOption) {
color = selectedOption.dataset.color;
} else {
const customColor = document.getElementById('customColor');
if (customColor) color = customColor.value;
}
preview.style.color = color;
preview.style.background = 'transparent';
preview.style.webkitBackgroundClip = 'initial';
preview.style.webkitTextFillColor = 'initial';
}
window.applyChatColor = async function() {
let newColor;
const selectedOption = document.querySelector('.color-option.selected');
if (selectedOption) {
newColor = selectedOption.dataset.color;
} else {
const customColor = document.getElementById('customColor');
newColor = customColor ? customColor.value : '#3498db';
}
userColor = newColor;
if (window.electronAPI?.saveChatColor) {
await window.electronAPI.saveChatColor(newColor);
}
if (socket && isAuthenticated) {
const uuid = await window.electronAPI?.getCurrentUuid();
socket.emit('authenticate', {
username: chatUsername,
userId: await getOrCreatePlayerId(),
uuid: uuid,
userColor: userColor
});
addSystemMessage('Username color updated successfully', 'success');
}
closeChatColorModal();
}
function applyUserColorStyle(element, color) {
element.style.color = color;
element.style.background = 'transparent';
element.style.webkitBackgroundClip = 'initial';
element.style.webkitTextFillColor = 'initial';
}
window.ChatAPI = { window.ChatAPI = {
send: sendMessage, send: sendMessage,
disconnect: () => socket?.disconnect() disconnect: () => socket?.disconnect()

View File

@@ -1,89 +0,0 @@
// Minimal i18n system - optimized async loading
const i18n = (() => {
let currentLang = 'en';
let translations = {};
const availableLanguages = [
{ code: 'en', name: 'English' },
{ code: 'es', name: 'Español' },
{ code: 'pt-BR', name: 'Português (Brasil)' }
];
// Load single language file
async function loadLanguage(lang) {
if (translations[lang]) return true;
try {
const response = await fetch(`locales/${lang}.json`);
if (response.ok) {
translations[lang] = await response.json();
return true;
}
} catch (e) {
console.warn(`Failed to load language: ${lang}`);
}
return false;
}
// Get translation by key
function t(key) {
const keys = key.split('.');
let value = translations[currentLang];
for (const k of keys) {
if (value && value[k] !== undefined) {
value = value[k];
} else {
return key;
}
}
return value;
}
// Set language
async function setLanguage(lang) {
await loadLanguage(lang);
if (translations[lang]) {
currentLang = lang;
updateDOM();
window.electronAPI?.saveLanguage(lang);
}
}
// Update all elements with data-i18n attribute
function updateDOM() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
el.textContent = t(key);
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
el.placeholder = t(key);
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
el.title = t(key);
});
}
// Initialize - load saved language only
async function init(savedLang) {
const lang = savedLang || 'en';
await loadLanguage(lang);
currentLang = lang;
updateDOM();
}
return {
init,
t,
setLanguage,
getAvailableLanguages: () => availableLanguages,
getCurrentLanguage: () => currentLang
};
})();
// Make i18n globally available
window.i18n = i18n;

View File

@@ -33,23 +33,19 @@ export function setupInstallation() {
if (window.electronAPI && window.electronAPI.onProgressUpdate) { if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => { window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.showProgress();
window.LauncherUI.updateProgress(data); window.LauncherUI.updateProgress(data);
} }
}); });
} }
// Setup installation effects listeners if (window.electronAPI && window.electronAPI.onProgressComplete) {
if (window.electronAPI && window.electronAPI.onInstallationStart) { window.electronAPI.onProgressComplete(() => {
window.electronAPI.onInstallationStart(() => { if (window.LauncherUI) {
showInstallationEffects(); window.LauncherUI.hideProgress();
});
} }
resetInstallButton();
if (window.electronAPI && window.electronAPI.onInstallationEnd) {
window.electronAPI.onInstallationEnd(() => {
hideInstallationEffects();
}); });
} }
} }
@@ -64,7 +60,7 @@ export async function installGame() {
isDownloading = true; isDownloading = true;
if (installBtn) { if (installBtn) {
installBtn.disabled = true; installBtn.disabled = true;
installText.textContent = window.i18n ? window.i18n.t('install.installing') : 'INSTALLING...'; installText.textContent = 'INSTALLING...';
} }
try { try {
@@ -72,15 +68,13 @@ export async function installGame() {
const result = await window.electronAPI.installGame(playerName, '', installPath); const result = await window.electronAPI.installGame(playerName, '', installPath);
if (result.success) { if (result.success) {
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: successMsg }); window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true); window.LauncherUI.showLauncherOrInstall(true);
const playerNameInput = document.getElementById('playerName'); const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName; if (playerNameInput) playerNameInput.value = playerName;
resetInstallButton();
}, 2000); }, 2000);
} }
} else { } else {
@@ -90,20 +84,12 @@ export async function installGame() {
simulateInstallation(playerName); simulateInstallation(playerName);
} }
} catch (error) { } catch (error) {
const errorMsg = window.i18n ? window.i18n.t('progress.installationFailed').replace('{error}', error.message) : `Installation failed: ${error.message}`;
// Hide installation effects on error
if (window.hideInstallationEffects) {
window.hideInstallationEffects();
}
// Reset button state on error
resetInstallButton();
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: errorMsg }); window.LauncherUI.updateProgress({ message: `Installation failed: ${error.message}` });
// Don't hide progress bar, just update the message setTimeout(() => {
// User can see the error and close it manually window.LauncherUI.hideProgress();
resetInstallButton();
}, 3000);
} }
} }
} }
@@ -114,13 +100,10 @@ function simulateInstallation(playerName) {
progress += Math.random() * 3; progress += Math.random() * 3;
if (progress > 100) progress = 100; if (progress > 100) progress = 100;
const installingMsg = window.i18n ? window.i18n.t('progress.installingGameFiles') : 'Installing game files...';
const completeMsg = window.i18n ? window.i18n.t('progress.installComplete') : 'Installation complete!';
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ window.LauncherUI.updateProgress({
percent: progress, percent: progress,
message: progress < 100 ? installingMsg : completeMsg, message: progress < 100 ? 'Installing game files...' : 'Installation complete!',
speed: 1024 * 1024 * (5 + Math.random() * 10), speed: 1024 * 1024 * (5 + Math.random() * 10),
downloaded: progress * 1024 * 1024 * 20, downloaded: progress * 1024 * 1024 * 20,
total: 1024 * 1024 * 2000 total: 1024 * 1024 * 2000
@@ -129,10 +112,9 @@ function simulateInstallation(playerName) {
if (progress >= 100) { if (progress >= 100) {
clearInterval(interval); clearInterval(interval);
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
setTimeout(() => { setTimeout(() => {
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: successMsg }); window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true); window.LauncherUI.showLauncherOrInstall(true);
@@ -228,9 +210,3 @@ document.addEventListener('DOMContentLoaded', async () => {
setupInstallation(); setupInstallation();
await checkGameStatusAndShowInterface(); await checkGameStatusAndShowInterface();
}); });
window.browseInstallPath = browseInstallPath;
document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});

View File

@@ -25,171 +25,22 @@ export function setupLauncher() {
if (window.electronAPI && window.electronAPI.onProgressUpdate) { if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => { window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.showProgress();
window.LauncherUI.updateProgress(data); window.LauncherUI.updateProgress(data);
} }
}); });
} }
// Initial Profile Load if (window.electronAPI && window.electronAPI.onProgressComplete) {
loadProfiles(); window.electronAPI.onProgressComplete(() => {
// Close dropdown on outside click
document.addEventListener('click', (e) => {
const selector = document.getElementById('profileSelector');
if (selector && !selector.contains(e.target)) {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
}
});
}
// ==========================================
// PROFILE MANAGEMENT
// ==========================================
async function loadProfiles() {
try {
if (!window.electronAPI || !window.electronAPI.profile) return;
const profiles = await window.electronAPI.profile.list();
const activeProfile = await window.electronAPI.profile.getActive();
renderProfileList(profiles, activeProfile);
updateCurrentProfileUI(activeProfile);
} catch (error) {
console.error('Failed to load profiles:', error);
}
}
function renderProfileList(profiles, activeProfile) {
const list = document.getElementById('profileList');
const managerList = document.getElementById('managerProfileList');
if (!list) return;
// Dropdown List
list.innerHTML = profiles.map(p => `
<div class="profile-item ${p.id === activeProfile.id ? 'active' : ''}"
onclick="switchProfile('${p.id}')">
<span>${p.name}</span>
${p.id === activeProfile.id ? '<i class="fas fa-check ml-auto"></i>' : ''}
</div>
`).join('');
// Manager Modal List
if (managerList) {
managerList.innerHTML = profiles.map(p => `
<div class="profile-manager-item ${p.id === activeProfile.id ? 'active' : ''}">
<div class="flex items-center gap-3">
<i class="fas fa-user-circle text-xl text-gray-400"></i>
<div>
<div class="font-bold">${p.name}</div>
<div class="text-xs text-gray-500">ID: ${p.id.substring(0, 8)}...</div>
</div>
</div>
${p.id !== activeProfile.id ? `
<button class="profile-delete-btn" onclick="deleteProfile('${p.id}')" title="Delete Profile">
<i class="fas fa-trash"></i>
</button>
` : '<span class="text-xs text-green-500 font-bold px-2">ACTIVE</span>'}
</div>
`).join('');
}
}
function updateCurrentProfileUI(profile) {
const nameEl = document.getElementById('currentProfileName');
if (nameEl && profile) {
nameEl.textContent = profile.name;
}
}
window.toggleProfileDropdown = () => {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) {
dropdown.classList.toggle('show');
}
};
window.openProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) {
modal.style.display = 'flex';
// Refresh list
loadProfiles();
}
// Close dropdown
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
};
window.closeProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) modal.style.display = 'none';
};
window.createNewProfile = async () => {
const input = document.getElementById('newProfileName');
if (!input || !input.value.trim()) return;
try {
const name = input.value.trim();
await window.electronAPI.profile.create(name);
input.value = '';
await loadProfiles();
} catch (error) {
console.error('Failed to create profile:', error);
alert('Failed to create profile: ' + error.message);
}
};
window.deleteProfile = async (id) => {
if (!confirm('Are you sure you want to delete this profile? parameters and mods configuration will be lost.')) return;
try {
await window.electronAPI.profile.delete(id);
await loadProfiles();
} catch (error) {
console.error('Failed to delete profile:', error);
alert('Failed to delete profile: ' + error.message);
}
};
window.switchProfile = async (id) => {
try {
if (window.LauncherUI) window.LauncherUI.showProgress();
const switchingMsg = window.i18n ? window.i18n.t('progress.switchingProfile') : 'Switching Profile...';
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: switchingMsg });
await window.electronAPI.profile.activate(id);
// Refresh UI
await loadProfiles();
// Refresh Mods
if (window.modsManager) {
if (window.modsManager.loadInstalledMods) await window.modsManager.loadInstalledMods();
if (window.modsManager.loadBrowseMods) await window.modsManager.loadBrowseMods();
}
// Close dropdown
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
if (window.LauncherUI) { if (window.LauncherUI) {
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Profile Switched!'; window.LauncherUI.hideProgress();
window.LauncherUI.updateProgress({ message: switchedMsg });
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
} }
resetPlayButton();
} catch (error) { });
console.error('Failed to switch profile:', error);
alert('Failed to switch profile: ' + error.message);
if (window.LauncherUI) window.LauncherUI.hideProgress();
} }
}; }
export async function launch() { export async function launch() {
if (isDownloading || (playBtn && playBtn.disabled)) return; if (isDownloading || (playBtn && playBtn.disabled)) return;
@@ -206,15 +57,6 @@ export async function launch() {
javaPath = window.SettingsAPI.getCurrentJavaPath(); javaPath = window.SettingsAPI.getCurrentJavaPath();
} }
let gpuPreference = 'auto';
try {
if (window.electronAPI && window.electronAPI.loadGpuPreference) {
gpuPreference = await window.electronAPI.loadGpuPreference();
}
} catch (error) {
console.error('Error loading GPU preference:', error);
}
if (window.LauncherUI) window.LauncherUI.showProgress(); if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true; isDownloading = true;
if (playBtn) { if (playBtn) {
@@ -223,196 +65,51 @@ export async function launch() {
} }
try { try {
const startingMsg = window.i18n ? window.i18n.t('progress.startingGame') : 'Starting game...';
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
if (window.electronAPI && window.electronAPI.launchGame) { if (window.electronAPI && window.electronAPI.launchGame) {
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference); const result = await window.electronAPI.launchGame(playerName, javaPath, '');
isDownloading = false;
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetPlayButton();
if (result.success) { if (result.success) {
if (window.electronAPI.minimizeWindow) {
setTimeout(() => {
window.electronAPI.minimizeWindow();
}, 500);
}
} else {
console.error('Launch failed:', result.error);
}
} else {
isDownloading = false;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game started successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
if (window.electronAPI.minimizeWindow) {
window.electronAPI.minimizeWindow();
} }
}, 2000);
}
} else {
throw new Error(result.error || 'Launch failed');
}
} else {
setTimeout(() => {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game started successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
resetPlayButton(); resetPlayButton();
}, 2000);
}
}, 2000);
} }
} catch (error) { } catch (error) {
isDownloading = false;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Failed: ${error.message}` });
setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
}
resetPlayButton(); resetPlayButton();
console.error('Launch error:', error); }, 3000);
} }
}
function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
// Apply defaults with i18n support
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
const existingModal = document.querySelector('.custom-confirm-modal');
if (existingModal) {
existingModal.remove();
} }
const modal = document.createElement('div');
modal.className = 'custom-confirm-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.className = 'custom-confirm-dialog';
dialog.style.cssText = `
background: #1f2937;
border-radius: 12px;
padding: 0;
min-width: 400px;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(239, 68, 68, 0.3);
transform: scale(0.9);
transition: transform 0.3s ease;
`;
dialog.innerHTML = `
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</h3>
</div>
</div>
<div style="padding: 24px; color: #e5e7eb;">
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
</div>
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button class="custom-confirm-cancel" style="
background: transparent;
color: #9ca3af;
border: 1px solid rgba(156, 163, 175, 0.3);
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${cancelText}</button>
<button class="custom-confirm-action" style="
background: #ef4444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${confirmText}</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// Animate in
setTimeout(() => {
modal.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// Event handlers
const cancelBtn = dialog.querySelector('.custom-confirm-cancel');
const actionBtn = dialog.querySelector('.custom-confirm-action');
const closeModal = () => {
modal.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
modal.remove();
}, 300);
};
cancelBtn.onclick = () => {
closeModal();
if (onCancel) onCancel();
};
actionBtn.onclick = () => {
closeModal();
onConfirm();
};
modal.onclick = (e) => {
if (e.target === modal) {
closeModal();
if (onCancel) onCancel();
}
};
// Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeModal();
if (onCancel) onCancel();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
} }
export async function uninstallGame() { export async function uninstallGame() {
const message = window.i18n ? window.i18n.t('confirm.uninstallGameMessage') : 'Are you sure you want to uninstall Hytale? All game files will be deleted.'; if (!confirm('Are you sure you want to uninstall Hytale? All game files will be deleted.')) {
const title = window.i18n ? window.i18n.t('confirm.uninstallGameTitle') : 'Uninstall Game'; return;
const confirmBtn = window.i18n ? window.i18n.t('confirm.uninstallGameButton') : 'Uninstall'; }
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
showCustomConfirm(
message,
title,
async () => {
await performUninstall();
},
null,
confirmBtn,
cancelBtn
);
}
async function performUninstall() {
if (window.LauncherUI) window.LauncherUI.showProgress(); if (window.LauncherUI) window.LauncherUI.showProgress();
const uninstallingMsg = window.i18n ? window.i18n.t('progress.uninstallingGame') : 'Uninstalling game...'; if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' });
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: uninstallingMsg });
if (uninstallBtn) uninstallBtn.disabled = true; if (uninstallBtn) uninstallBtn.disabled = true;
try { try {
@@ -420,9 +117,8 @@ async function performUninstall() {
const result = await window.electronAPI.uninstallGame(); const result = await window.electronAPI.uninstallGame();
if (result.success) { if (result.success) {
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: successMsg }); window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false); window.LauncherUI.showLauncherOrInstall(false);
@@ -432,10 +128,9 @@ async function performUninstall() {
throw new Error(result.error || 'Uninstall failed'); throw new Error(result.error || 'Uninstall failed');
} }
} else { } else {
const successMsg = window.i18n ? window.i18n.t('progress.gameUninstalled') : 'Game uninstalled successfully!';
setTimeout(() => { setTimeout(() => {
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: successMsg }); window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false); window.LauncherUI.showLauncherOrInstall(false);
@@ -444,9 +139,8 @@ async function performUninstall() {
}, 2000); }, 2000);
} }
} catch (error) { } catch (error) {
const errorMsg = window.i18n ? window.i18n.t('progress.uninstallFailed').replace('{error}', error.message) : `Uninstall failed: ${error.message}`;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: errorMsg }); window.LauncherUI.updateProgress({ message: `Uninstall failed: ${error.message}` });
setTimeout(() => window.LauncherUI.hideProgress(), 3000); setTimeout(() => window.LauncherUI.hideProgress(), 3000);
} }
} finally { } finally {
@@ -454,54 +148,11 @@ async function performUninstall() {
} }
} }
export async function repairGame() {
showCustomConfirm(
'Are you sure you want to repair Hytale? This will reinstall the game files but keep your data (saves, screenshots, etc.).',
'Repair Game',
async () => {
await performRepair();
},
null,
'Repair',
'Cancel'
);
}
async function performRepair() {
if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Repairing game...' });
isDownloading = true;
try {
if (window.electronAPI && window.electronAPI.repairGame) {
const result = await window.electronAPI.repairGame();
if (result.success) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game repaired successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
}, 2000);
}
} else {
throw new Error(result.error || 'Repair failed');
}
}
} catch (error) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Repair failed: ${error.message}` });
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
}
} finally {
isDownloading = false;
}
}
function resetPlayButton() { function resetPlayButton() {
isDownloading = false; isDownloading = false;
if (playBtn) { if (playBtn) {
playBtn.disabled = false; playBtn.disabled = false;
playText.textContent = window.i18n ? window.i18n.t('play.play') : 'PLAY'; playText.textContent = 'PLAY';
} }
} }
@@ -589,59 +240,5 @@ async function loadCustomJavaPath() {
window.launch = launch; window.launch = launch;
window.uninstallGame = uninstallGame; window.uninstallGame = uninstallGame;
window.repairGame = repairGame;
window.openLogs = async () => {
if (window.LauncherUI) {
window.LauncherUI.showPage('logs-page');
window.LauncherUI.setActiveNav('logs');
}
await refreshLogs();
};
window.openLogsFolder = async () => {
try {
if (window.electronAPI && window.electronAPI.openLogsFolder) {
await window.electronAPI.openLogsFolder();
}
} catch (error) {
console.error('Failed to open logs folder:', error);
}
};
window.refreshLogs = async () => {
const terminal = document.getElementById('logsTerminal');
if (!terminal) return;
try {
if (window.electronAPI && window.electronAPI.getRecentLogs) {
// Fetch up to MAX_LOG_LINES lines
const logs = await window.electronAPI.getRecentLogs(MAX_LOG_LINES);
if (logs) {
// Formatting for colors could be done here if needed
terminal.textContent = logs;
terminal.scrollTop = terminal.scrollHeight;
} else {
terminal.textContent = 'No logs available.';
}
}
} catch (error) {
terminal.textContent = 'Error loading logs: ' + error.message;
}
};
window.copyLogs = () => {
const terminal = document.getElementById('logsTerminal');
if (terminal) {
navigator.clipboard.writeText(terminal.textContent)
.then(() => alert('Logs copied to clipboard!'))
.catch(err => console.error('Failed to copy logs:', err));
}
};
window.repairGame = repairGame;
// Constants
const MAX_LOG_LINES = 500;
document.addEventListener('DOMContentLoaded', setupLauncher); document.addEventListener('DOMContentLoaded', setupLauncher);

View File

@@ -1,96 +0,0 @@
// Logs Page Logic
async function loadLogs() {
const terminal = document.getElementById('logsTerminal');
if (!terminal) return;
terminal.innerHTML = '<div class="text-gray-500 text-center mt-10">Loading logs...</div>';
try {
const logs = await window.electronAPI.getRecentLogs(500); // Fetch last 500 lines
if (logs) {
// Escape HTML to prevent XSS and preserve format
const safeLogs = logs.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
terminal.innerHTML = `<pre class="logs-content">${safeLogs}</pre>`;
// Auto scroll to bottom
terminal.scrollTop = terminal.scrollHeight;
} else {
terminal.innerHTML = '<div class="text-gray-500 text-center mt-10">No logs found.</div>';
}
} catch (error) {
console.error('Failed to load logs:', error);
terminal.innerHTML = `<div class="text-red-500 text-center mt-10">Error loading logs: ${error.message}</div>`;
}
}
async function refreshLogs() {
const btn = document.querySelector('button[onclick="refreshLogs()"] i');
if (btn) btn.classList.add('fa-spin');
await loadLogs();
if (btn) setTimeout(() => btn.classList.remove('fa-spin'), 500);
}
async function copyLogs() {
const terminal = document.getElementById('logsTerminal');
if (!terminal) return;
const content = terminal.innerText;
if (!content) return;
try {
await navigator.clipboard.writeText(content);
const btn = document.querySelector('button[onclick="copyLogs()"]');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(() => {
btn.innerHTML = originalText;
}, 2000);
} catch (err) {
console.error('Failed to copy logs:', err);
}
}
async function openLogsFolder() {
await window.electronAPI.openLogsFolder();
}
function openLogs() {
// Navigation is handled by sidebar logic, but we can trigger a refresh
window.LauncherUI.showPage('logs-page');
window.LauncherUI.setActiveNav('logs');
refreshLogs();
}
// Expose functions globally
window.refreshLogs = refreshLogs;
window.copyLogs = copyLogs;
window.openLogsFolder = openLogsFolder;
window.openLogs = openLogs;
// Auto-load logs when the page becomes active
const logsObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.target.classList.contains('active') && mutation.target.id === 'logs-page') {
loadLogs();
}
});
});
document.addEventListener('DOMContentLoaded', () => {
const logsPage = document.getElementById('logs-page');
if (logsPage) {
logsObserver.observe(logsPage, { attributes: true, attributeFilter: ['class'] });
}
});

View File

@@ -1,5 +1,5 @@
let API_KEY = null; const API_KEY = '$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32';
const CURSEFORGE_API = 'https://api.curseforge.com/v1'; const CURSEFORGE_API = 'https://api.curseforge.com/v1';
const HYTALE_GAME_ID = 70216; const HYTALE_GAME_ID = 70216;
@@ -11,15 +11,6 @@ let modsPageSize = 20;
let modsTotalPages = 1; let modsTotalPages = 1;
export async function initModsManager() { export async function initModsManager() {
try {
if (window.electronAPI && window.electronAPI.getEnvVar) {
API_KEY = await window.electronAPI.getEnvVar('CURSEFORGE_API_KEY');
console.log('Loaded API Key:', API_KEY ? 'Yes' : 'No');
}
} catch (err) {
console.error('Failed to load API Key:', err);
}
setupModsEventListeners(); setupModsEventListeners();
await loadInstalledMods(); await loadInstalledMods();
await loadBrowseMods(); await loadBrowseMods();
@@ -122,15 +113,10 @@ function displayInstalledMods(mods) {
modsContainer.innerHTML = ` modsContainer.innerHTML = `
<div class=\"empty-installed-mods\"> <div class=\"empty-installed-mods\">
<i class=\"fas fa-box-open\"></i> <i class=\"fas fa-box-open\"></i>
<h4 data-i18n="mods.noModsInstalled">No Mods Installed</h4> <h4>No Mods Installed</h4>
<p data-i18n="mods.noModsInstalledDesc">Add mods from CurseForge or import local files</p> <p>Add mods from CurseForge or import local files</p>
</div> </div>
`; `;
if (window.i18n) {
const container = modsContainer.querySelector('.empty-installed-mods');
container.querySelector('h4').textContent = window.i18n.t('mods.noModsInstalled');
container.querySelector('p').textContent = window.i18n.t('mods.noModsInstalledDesc');
}
return; return;
} }
@@ -152,9 +138,9 @@ function displayInstalledMods(mods) {
function createInstalledModCard(mod) { function createInstalledModCard(mod) {
const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500'; const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500';
const statusText = mod.enabled ? (window.i18n ? window.i18n.t('mods.active') : 'ACTIVE') : (window.i18n ? window.i18n.t('mods.disabled') : 'DISABLED'); const statusText = mod.enabled ? 'ACTIVE' : 'DISABLED';
const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable'; const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable';
const toggleBtnText = mod.enabled ? (window.i18n ? window.i18n.t('mods.disable') : 'DISABLE') : (window.i18n ? window.i18n.t('mods.enable') : 'ENABLE'); const toggleBtnText = mod.enabled ? 'DISABLE' : 'ENABLE';
const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play'; const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play';
return ` return `
@@ -168,7 +154,7 @@ function createInstalledModCard(mod) {
<h4 class="installed-mod-name">${mod.name}</h4> <h4 class="installed-mod-name">${mod.name}</h4>
<span class="installed-mod-version">v${mod.version}</span> <span class="installed-mod-version">v${mod.version}</span>
</div> </div>
<p class="installed-mod-description">${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}</p> <p class="installed-mod-description">${mod.description || 'No description available'}</p>
</div> </div>
<div class="installed-mod-actions"> <div class="installed-mod-actions">
@@ -177,7 +163,7 @@ function createInstalledModCard(mod) {
${statusText} ${statusText}
</div> </div>
<div class="installed-mod-buttons"> <div class="installed-mod-buttons">
<button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="${window.i18n ? window.i18n.t('mods.delete') : 'Delete mod'}"> <button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="Delete mod">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}"> <button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
@@ -194,7 +180,7 @@ async function loadBrowseMods() {
const browseContainer = document.getElementById('browseModsList'); const browseContainer = document.getElementById('browseModsList');
if (!browseContainer) return; if (!browseContainer) return;
browseContainer.innerHTML = `<div class="loading-mods"><div class="loading-spinner"></div><span>${window.i18n ? window.i18n.t('mods.loadingMods') : 'Loading mods from CurseForge...'}</span></div>`; browseContainer.innerHTML = '<div class=\"loading-mods\"><div class=\"loading-spinner\"></div><span>Loading mods from CurseForge...</span></div>';
try { try {
if (!API_KEY || API_KEY.length < 10) { if (!API_KEY || API_KEY.length < 10) {
@@ -278,15 +264,10 @@ function displayBrowseMods(mods) {
browseContainer.innerHTML = ` browseContainer.innerHTML = `
<div class=\"empty-browse-mods\"> <div class=\"empty-browse-mods\">
<i class=\"fas fa-search\"></i> <i class=\"fas fa-search\"></i>
<h4 data-i18n="mods.noModsFound">No Mods Found</h4> <h4>No Mods Found</h4>
<p data-i18n="mods.noModsFoundDesc">Try adjusting your search</p> <p>Try adjusting your search</p>
</div> </div>
`; `;
if (window.i18n) {
const container = browseContainer.querySelector('.empty-browse-mods');
container.querySelector('h4').textContent = window.i18n.t('mods.noModsFound');
container.querySelector('p').textContent = window.i18n.t('mods.noModsFoundDesc');
}
return; return;
} }
@@ -301,17 +282,10 @@ function displayBrowseMods(mods) {
} }
function createBrowseModCard(mod) { function createBrowseModCard(mod) {
const isInstalled = installedMods.some(installed => { const isInstalled = installedMods.some(installed =>
// Check by CurseForge ID (most reliable) installed.name.toLowerCase().includes(mod.name.toLowerCase()) ||
if (installed.curseForgeId && installed.curseForgeId.toString() === mod.id.toString()) { installed.curseForgeId == mod.id
return true; );
}
// Check by exact name match for manually installed mods
if (installed.name.toLowerCase() === mod.name.toLowerCase()) {
return true;
}
return false;
});
return ` return `
<div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\"> <div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\">
@@ -343,16 +317,16 @@ function createBrowseModCard(mod) {
<div class=\"mod-actions\"> <div class=\"mod-actions\">
<button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\"> <button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\">
<i class=\"fas fa-external-link-alt\"></i> <i class=\"fas fa-external-link-alt\"></i>
${window.i18n ? window.i18n.t('mods.view') : 'VIEW'} VIEW
</button> </button>
${!isInstalled ? ${!isInstalled ?
`<button id="install-${mod.id}" class="mod-btn-toggle bg-primary text-black hover:bg-primary/80"> `<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
<i class="fas fa-download"></i> <i class=\"fas fa-download\"></i>
${window.i18n ? window.i18n.t('mods.install') : 'INSTALL'} INSTALL
</button>` : </button>` :
`<button class="mod-btn-toggle bg-white/10 text-white" disabled> `<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
<i class="fas fa-check"></i> <i class=\"fas fa-check\"></i>
${window.i18n ? window.i18n.t('mods.installed') : 'INSTALLED'} INSTALLED
</button>` </button>`
} }
</div> </div>
@@ -362,8 +336,7 @@ function createBrowseModCard(mod) {
async function downloadAndInstallMod(modInfo) { async function downloadAndInstallMod(modInfo) {
try { try {
const downloadMsg = window.i18n ? window.i18n.t('notifications.modsDownloading').replace('{name}', modInfo.name) : `Downloading ${modInfo.name}...`; window.LauncherUI?.showProgress(`Downloading ${modInfo.name}...`);
window.LauncherUI?.showProgress(downloadMsg);
const result = await window.electronAPI?.downloadMod(modInfo); const result = await window.electronAPI?.downloadMod(modInfo);
@@ -387,23 +360,20 @@ async function downloadAndInstallMod(modInfo) {
await loadInstalledMods(); await loadInstalledMods();
await loadBrowseMods(); await loadBrowseMods();
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
const successMsg = window.i18n ? window.i18n.t('notifications.modsInstalledSuccess').replace('{name}', modInfo.name) : `${modInfo.name} installed successfully! 🎉`; showNotification(`${modInfo.name} installed successfully! 🎉`, 'success');
showNotification(successMsg, 'success');
} else { } else {
throw new Error(result?.error || 'Failed to download mod'); throw new Error(result?.error || 'Failed to download mod');
} }
} catch (error) { } catch (error) {
console.error('Error downloading mod:', error); console.error('Error downloading mod:', error);
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
const errorMsg = window.i18n ? window.i18n.t('notifications.modsDownloadFailed').replace('{error}', error.message) : 'Failed to download mod: ' + error.message; showNotification('Failed to download mod: ' + error.message, 'error');
showNotification(errorMsg, 'error');
} }
} }
async function toggleMod(modId) { async function toggleMod(modId) {
try { try {
const toggleMsg = window.i18n ? window.i18n.t('notifications.modsTogglingMod') : 'Toggling mod...'; window.LauncherUI?.showProgress('Toggling mod...');
window.LauncherUI?.showProgress(toggleMsg);
const modsPath = await window.electronAPI?.getModsPath(); const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.toggleMod(modId, modsPath); const result = await window.electronAPI?.toggleMod(modId, modsPath);
@@ -417,8 +387,7 @@ async function toggleMod(modId) {
} catch (error) { } catch (error) {
console.error('Error toggling mod:', error); console.error('Error toggling mod:', error);
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
const errorMsg = window.i18n ? window.i18n.t('notifications.modsToggleFailed').replace('{error}', error.message) : 'Failed to toggle mod: ' + error.message; showNotification('Failed to toggle mod: ' + error.message, 'error');
showNotification(errorMsg, 'error');
} }
} }
@@ -426,16 +395,11 @@ async function deleteMod(modId) {
const mod = installedMods.find(m => m.id === modId); const mod = installedMods.find(m => m.id === modId);
if (!mod) return; if (!mod) return;
const confirmMsg = window.i18n ?
window.i18n.t('mods.confirmDelete').replace('{name}', mod.name) + ' ' + window.i18n.t('mods.confirmDeleteDesc') :
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`;
showConfirmModal( showConfirmModal(
confirmMsg, `Are you sure you want to delete "${mod.name}"? This action cannot be undone.`,
async () => { async () => {
try { try {
const deleteMsg = window.i18n ? window.i18n.t('notifications.modsDeletingMod') : 'Deleting mod...'; window.LauncherUI?.showProgress('Deleting mod...');
window.LauncherUI?.showProgress(deleteMsg);
const modsPath = await window.electronAPI?.getModsPath(); const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.uninstallMod(modId, modsPath); const result = await window.electronAPI?.uninstallMod(modId, modsPath);
@@ -444,16 +408,14 @@ async function deleteMod(modId) {
await loadInstalledMods(); await loadInstalledMods();
await loadBrowseMods(); await loadBrowseMods();
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
const successMsg = window.i18n ? window.i18n.t('notifications.modsDeletedSuccess').replace('{name}', mod.name) : `"${mod.name}" deleted successfully`; showNotification(`"${mod.name}" deleted successfully`, 'success');
showNotification(successMsg, 'success');
} else { } else {
throw new Error(result?.error || 'Failed to delete mod'); throw new Error(result?.error || 'Failed to delete mod');
} }
} catch (error) { } catch (error) {
console.error('Error deleting mod:', error); console.error('Error deleting mod:', error);
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
const errorMsg = window.i18n ? window.i18n.t('notifications.modsDeleteFailed').replace('{error}', error.message) : 'Failed to delete mod: ' + error.message; showNotification('Failed to delete mod: ' + error.message, 'error');
showNotification(errorMsg, 'error');
} }
} }
); );
@@ -560,6 +522,7 @@ function showNotification(message, type = 'info', duration = 4000) {
}, duration); }, duration);
} }
// Custom confirmation modal
function showConfirmModal(message, onConfirm, onCancel = null) { function showConfirmModal(message, onConfirm, onCancel = null) {
const existingModal = document.querySelector('.mod-confirm-modal'); const existingModal = document.querySelector('.mod-confirm-modal');
if (existingModal) { if (existingModal) {
@@ -602,7 +565,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);"> <div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;"> <div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i> <i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${window.i18n ? window.i18n.t('mods.confirmDeletion') : 'Confirm Deletion'}</h3> <h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Confirm Deletion</h3>
</div> </div>
</div> </div>
<div style="padding: 24px; color: #e5e7eb;"> <div style="padding: 24px; color: #e5e7eb;">
@@ -618,7 +581,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
cursor: pointer; cursor: pointer;
font-weight: 500; font-weight: 500;
transition: all 0.2s; transition: all 0.2s;
">${window.i18n ? window.i18n.t('common.cancel') : 'Cancel'}</button> ">Cancel</button>
<button class="mod-confirm-delete" style=" <button class="mod-confirm-delete" style="
background: #ef4444; background: #ef4444;
color: white; color: white;
@@ -628,7 +591,7 @@ function showConfirmModal(message, onConfirm, onCancel = null) {
cursor: pointer; cursor: pointer;
font-weight: 500; font-weight: 500;
transition: all 0.2s; transition: all 0.2s;
">${window.i18n ? window.i18n.t('common.delete') : 'Delete'}</button> ">Delete</button>
</div> </div>
`; `;
@@ -746,8 +709,7 @@ function viewModPage(modId) {
} }
} else { } else {
console.error('Mod not found with ID:', modId); console.error('Mod not found with ID:', modId);
const errorMsg = window.i18n ? window.i18n.t('notifications.modsModNotFound') : 'Mod information not found'; showNotification('Mod information not found', 'error');
showNotification(errorMsg, 'error');
} }
} }
@@ -756,9 +718,7 @@ window.modsManager = {
deleteMod, deleteMod,
openMyModsModal, openMyModsModal,
closeMyModsModal, closeMyModsModal,
viewModPage, viewModPage
loadInstalledMods,
loadBrowseMods
}; };
document.addEventListener('DOMContentLoaded', initModsManager); document.addEventListener('DOMContentLoaded', initModsManager);

View File

@@ -1,5 +1,5 @@
const API_URL = 'https://api.hytalef2p.com/api'; const API_URL = 'http://3.10.208.30/api';
let updateInterval = null; let updateInterval = null;
let currentUserId = null; let currentUserId = null;

View File

@@ -6,70 +6,8 @@ import './mods.js';
import './players.js'; import './players.js';
import './chat.js'; import './chat.js';
import './settings.js'; import './settings.js';
import './logs.js';
// Initialize i18n immediately (before DOMContentLoaded)
let i18nInitialized = false;
(async () => {
const savedLang = await window.electronAPI?.loadLanguage();
await i18n.init(savedLang);
i18nInitialized = true;
// Update language selector if DOM is already loaded
if (document.readyState === 'complete' || document.readyState === 'interactive') {
updateLanguageSelector();
}
})();
function updateLanguageSelector() {
const langSelect = document.getElementById('languageSelect');
if (langSelect) {
// Clear existing options
langSelect.innerHTML = '';
const languages = i18n.getAvailableLanguages();
const currentLang = i18n.getCurrentLanguage();
languages.forEach(lang => {
const option = document.createElement('option');
option.value = lang.code;
option.textContent = lang.name;
if (lang.code === currentLang) {
option.selected = true;
}
langSelect.appendChild(option);
});
// Handle language change (add listener only once)
if (!langSelect.hasAttribute('data-listener-added')) {
langSelect.addEventListener('change', async (e) => {
await i18n.setLanguage(e.target.value);
});
langSelect.setAttribute('data-listener-added', 'true');
}
}
}
document.addEventListener('DOMContentLoaded', () => {
// Populate language selector (wait for i18n if needed)
if (i18nInitialized) {
updateLanguageSelector();
}
// Discord notification
const notification = document.getElementById('discordNotification');
if (notification) {
const dismissed = localStorage.getItem('discordNotificationDismissed');
if (!dismissed) {
setTimeout(() => {
notification.style.display = 'flex';
}, 3000);
} else {
notification.style.display = 'none';
}
}
});
// Discord notification functions
window.closeDiscordNotification = function() { window.closeDiscordNotification = function() {
const notification = document.getElementById('discordNotification'); const notification = document.getElementById('discordNotification');
if (notification) { if (notification) {
@@ -78,5 +16,27 @@ window.closeDiscordNotification = function() {
notification.style.display = 'none'; notification.style.display = 'none';
}, 300); }, 300);
} }
localStorage.setItem('discordNotificationDismissed', 'true'); };
// 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

@@ -4,152 +4,6 @@ let customJavaOptions;
let customJavaPath; let customJavaPath;
let browseJavaBtn; let browseJavaBtn;
let settingsPlayerName; let settingsPlayerName;
let discordRPCCheck;
let closeLauncherCheck;
let gpuPreferenceRadios;
// UUID Management elements
let currentUuidDisplay;
let copyUuidBtn;
let regenerateUuidBtn;
let manageUuidsBtn;
let uuidModal;
let uuidModalClose;
let modalCurrentUuid;
let modalCopyUuidBtn;
let modalRegenerateUuidBtn;
let generateNewUuidBtn;
let uuidList;
let customUuidInput;
let setCustomUuidBtn;
function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
// Apply defaults with i18n support
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
const existingModal = document.querySelector('.custom-confirm-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.className = 'custom-confirm-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.className = 'custom-confirm-dialog';
dialog.style.cssText = `
background: #1f2937;
border-radius: 12px;
padding: 0;
min-width: 400px;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(147, 51, 234, 0.3);
transform: scale(0.9);
transition: transform 0.3s ease;
`;
dialog.innerHTML = `
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #9333ea;">
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</h3>
</div>
</div>
<div style="padding: 24px; color: #e5e7eb;">
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
</div>
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button class="custom-confirm-cancel" style="
background: transparent;
color: #9ca3af;
border: 1px solid rgba(156, 163, 175, 0.3);
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${cancelText}</button>
<button class="custom-confirm-action" style="
background: linear-gradient(135deg, #9333ea, #3b82f6);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${confirmText}</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// Animate in
setTimeout(() => {
modal.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// Event handlers
const cancelBtn = dialog.querySelector('.custom-confirm-cancel');
const actionBtn = dialog.querySelector('.custom-confirm-action');
const closeModal = () => {
modal.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
modal.remove();
}, 300);
};
cancelBtn.onclick = () => {
closeModal();
if (onCancel) onCancel();
};
actionBtn.onclick = () => {
closeModal();
onConfirm();
};
modal.onclick = (e) => {
if (e.target === modal) {
closeModal();
if (onCancel) onCancel();
}
};
// Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeModal();
if (onCancel) onCancel();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
export function initSettings() { export function initSettings() {
setupSettingsElements(); setupSettingsElements();
@@ -162,25 +16,6 @@ function setupSettingsElements() {
customJavaPath = document.getElementById('customJavaPath'); customJavaPath = document.getElementById('customJavaPath');
browseJavaBtn = document.getElementById('browseJavaBtn'); browseJavaBtn = document.getElementById('browseJavaBtn');
settingsPlayerName = document.getElementById('settingsPlayerName'); settingsPlayerName = document.getElementById('settingsPlayerName');
discordRPCCheck = document.getElementById('discordRPCCheck');
closeLauncherCheck = document.getElementById('closeLauncherCheck');
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
// UUID Management elements
currentUuidDisplay = document.getElementById('currentUuid');
copyUuidBtn = document.getElementById('copyUuidBtn');
regenerateUuidBtn = document.getElementById('regenerateUuidBtn');
manageUuidsBtn = document.getElementById('manageUuidsBtn');
uuidModal = document.getElementById('uuidModal');
uuidModalClose = document.getElementById('uuidModalClose');
modalCurrentUuid = document.getElementById('modalCurrentUuid');
modalCopyUuidBtn = document.getElementById('modalCopyUuidBtn');
modalRegenerateUuidBtn = document.getElementById('modalRegenerateUuidBtn');
generateNewUuidBtn = document.getElementById('generateNewUuidBtn');
uuidList = document.getElementById('uuidList');
customUuidInput = document.getElementById('customUuidInput');
setCustomUuidBtn = document.getElementById('setCustomUuidBtn');
if (customJavaCheck) { if (customJavaCheck) {
customJavaCheck.addEventListener('change', toggleCustomJava); customJavaCheck.addEventListener('change', toggleCustomJava);
@@ -193,65 +28,6 @@ function setupSettingsElements() {
if (settingsPlayerName) { if (settingsPlayerName) {
settingsPlayerName.addEventListener('change', savePlayerName); settingsPlayerName.addEventListener('change', savePlayerName);
} }
if (discordRPCCheck) {
discordRPCCheck.addEventListener('change', saveDiscordRPC);
}
if (closeLauncherCheck) {
closeLauncherCheck.addEventListener('change', saveCloseLauncher);
}
// UUID event listeners
if (copyUuidBtn) {
copyUuidBtn.addEventListener('click', copyCurrentUuid);
}
if (regenerateUuidBtn) {
regenerateUuidBtn.addEventListener('click', regenerateCurrentUuid);
}
if (manageUuidsBtn) {
manageUuidsBtn.addEventListener('click', openUuidModal);
}
if (uuidModalClose) {
uuidModalClose.addEventListener('click', closeUuidModal);
}
if (modalCopyUuidBtn) {
modalCopyUuidBtn.addEventListener('click', copyCurrentUuid);
}
if (modalRegenerateUuidBtn) {
modalRegenerateUuidBtn.addEventListener('click', regenerateCurrentUuid);
}
if (generateNewUuidBtn) {
generateNewUuidBtn.addEventListener('click', generateNewUuid);
}
if (setCustomUuidBtn) {
setCustomUuidBtn.addEventListener('click', setCustomUuid);
}
if (uuidModal) {
uuidModal.addEventListener('click', (e) => {
if (e.target === uuidModal) {
closeUuidModal();
}
});
}
if (gpuPreferenceRadios) {
gpuPreferenceRadios.forEach(radio => {
radio.addEventListener('change', async () => {
await saveGpuPreference();
await updateGpuLabel();
});
});
}
} }
function toggleCustomJava() { function toggleCustomJava() {
@@ -314,193 +90,35 @@ async function loadCustomJavaPath() {
} }
} }
async function saveDiscordRPC() {
try {
if (window.electronAPI && window.electronAPI.saveDiscordRPC && discordRPCCheck) {
const enabled = discordRPCCheck.checked;
console.log('Saving Discord RPC setting:', enabled);
const result = await window.electronAPI.saveDiscordRPC(enabled);
if (result && result.success) {
console.log('Discord RPC setting saved successfully:', enabled);
// Feedback visuel pour l'utilisateur
if (enabled) {
const msg = window.i18n ? window.i18n.t('notifications.discordEnabled') : 'Discord Rich Presence enabled';
showNotification(msg, 'success');
} else {
const msg = window.i18n ? window.i18n.t('notifications.discordDisabled') : 'Discord Rich Presence disabled';
showNotification(msg, 'success');
}
} else {
throw new Error('Failed to save Discord RPC setting');
}
}
} catch (error) {
console.error('Error saving Discord RPC setting:', error);
const msg = window.i18n ? window.i18n.t('notifications.discordSaveFailed') : 'Failed to save Discord setting';
showNotification(msg, 'error');
}
}
async function loadDiscordRPC() {
try {
if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
const enabled = await window.electronAPI.loadDiscordRPC();
if (discordRPCCheck) {
discordRPCCheck.checked = enabled;
}
}
} catch (error) {
console.error('Error loading Discord RPC setting:', error);
}
}
async function saveCloseLauncher() {
try {
if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) {
const enabled = closeLauncherCheck.checked;
await window.electronAPI.saveCloseLauncher(enabled);
}
} catch (error) {
console.error('Error saving close launcher setting:', error);
}
}
async function loadCloseLauncher() {
try {
if (window.electronAPI && window.electronAPI.loadCloseLauncher) {
const enabled = await window.electronAPI.loadCloseLauncher();
if (closeLauncherCheck) {
closeLauncherCheck.checked = enabled;
}
}
} catch (error) {
console.error('Error loading close launcher setting:', error);
}
}
async function savePlayerName() { async function savePlayerName() {
try { try {
if (!window.electronAPI || !settingsPlayerName) return; if (window.electronAPI && window.electronAPI.saveUsername && settingsPlayerName) {
const playerName = settingsPlayerName.value.trim() || 'Player';
const playerName = settingsPlayerName.value.trim();
if (!playerName) {
const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a valid player name';
showNotification(msg, 'error');
return;
}
await window.electronAPI.saveUsername(playerName); await window.electronAPI.saveUsername(playerName);
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully'; }
showNotification(successMsg, 'success');
} catch (error) { } catch (error) {
console.error('Error saving player name:', error); console.error('Error saving player name:', error);
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
showNotification(errorMsg, 'error');
} }
} }
async function loadPlayerName() { async function loadPlayerName() {
try { try {
if (!window.electronAPI || !settingsPlayerName) return; if (window.electronAPI && window.electronAPI.loadUsername && settingsPlayerName) {
const savedName = await window.electronAPI.loadUsername(); const savedName = await window.electronAPI.loadUsername();
if (savedName) { if (savedName) {
settingsPlayerName.value = savedName; settingsPlayerName.value = savedName;
} }
}
} catch (error) { } catch (error) {
console.error('Error loading player name:', error); console.error('Error loading player name:', error);
} }
} }
async function saveGpuPreference() {
try {
if (window.electronAPI && window.electronAPI.saveGpuPreference && gpuPreferenceRadios) {
const gpuPreference = Array.from(gpuPreferenceRadios).find(radio => radio.checked)?.value || 'auto';
await window.electronAPI.saveGpuPreference(gpuPreference);
}
} catch (error) {
console.error('Error saving GPU preference:', error);
}
}
async function updateGpuLabel() {
const detectionInfo = document.getElementById('gpu-detection-info');
if (!detectionInfo) return;
if (gpuPreferenceRadios) {
const checked = Array.from(gpuPreferenceRadios).find(radio => radio.checked);
if (checked) {
try {
if (window.electronAPI && window.electronAPI.getDetectedGpu) {
const detected = await window.electronAPI.getDetectedGpu();
if (checked.value === 'auto') {
if (detected.dedicatedName) {
detectionInfo.textContent = `dGPU detected, using ${detected.dedicatedName}`;
} else {
detectionInfo.textContent = `dGPU not detected, using iGPU (${detected.integratedName}) instead`;
}
detectionInfo.style.display = 'block';
} else if (checked.value === 'integrated') {
detectionInfo.textContent = `Detected: ${detected.integratedName}`;
detectionInfo.style.display = 'block';
} else if (checked.value === 'dedicated') {
if (detected.dedicatedName) {
detectionInfo.textContent = `Detected: ${detected.dedicatedName}`;
} else {
detectionInfo.textContent = `No dedicated GPU detected`;
}
detectionInfo.style.display = 'block';
} else {
detectionInfo.style.display = 'none';
}
}
} catch (error) {
console.error('Error getting detected GPU:', error);
detectionInfo.style.display = 'none';
}
} else {
detectionInfo.style.display = 'none';
}
} else {
detectionInfo.style.display = 'none';
}
}
async function loadGpuPreference() {
try {
if (window.electronAPI && window.electronAPI.loadGpuPreference && gpuPreferenceRadios) {
const savedPreference = await window.electronAPI.loadGpuPreference();
if (savedPreference) {
for (const radio of gpuPreferenceRadios) {
if (radio.value === savedPreference) {
radio.checked = true;
break;
}
}
await updateGpuLabel();
}
}
} catch (error) {
console.error('Error loading GPU preference:', error);
}
}
async function loadAllSettings() { async function loadAllSettings() {
await loadCustomJavaPath(); await loadCustomJavaPath();
await loadPlayerName(); await loadPlayerName();
await loadCurrentUuid();
await loadDiscordRPC();
await loadCloseLauncher();
await loadGpuPreference();
} }
async function openGameLocation() { async function openGameLocation() {
try { try {
if (window.electronAPI && window.electronAPI.openGameLocation) { if (window.electronAPI && window.electronAPI.openGameLocation) {
@@ -526,6 +144,7 @@ export function getCurrentPlayerName() {
return 'Player'; return 'Player';
} }
// Make openGameLocation globally available
window.openGameLocation = openGameLocation; window.openGameLocation = openGameLocation;
document.addEventListener('DOMContentLoaded', initSettings); document.addEventListener('DOMContentLoaded', initSettings);
@@ -534,361 +153,3 @@ window.SettingsAPI = {
getCurrentJavaPath, getCurrentJavaPath,
getCurrentPlayerName getCurrentPlayerName
}; };
async function loadCurrentUuid() {
try {
if (window.electronAPI && window.electronAPI.getCurrentUuid) {
const uuid = await window.electronAPI.getCurrentUuid();
if (uuid) {
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
}
}
} catch (error) {
console.error('Error loading current UUID:', error);
}
}
async function copyCurrentUuid() {
try {
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
if (uuid && navigator.clipboard) {
await navigator.clipboard.writeText(uuid);
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
showNotification(msg, 'success');
}
} catch (error) {
console.error('Error copying UUID:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
showNotification(msg, 'error');
}
}
async function regenerateCurrentUuid() {
try {
if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) {
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
showCustomConfirm(
message,
title,
async () => {
await performRegenerateUuid();
},
null,
confirmBtn,
cancelBtn
);
} else {
console.error('electronAPI.resetCurrentUserUuid not available');
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenNotAvailable') : 'UUID regeneration not available';
showNotification(msg, 'error');
}
} catch (error) {
console.error('Error in regenerateCurrentUuid:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed') : 'Failed to regenerate UUID';
showNotification(msg, 'error');
}
}
async function performRegenerateUuid() {
try {
const result = await window.electronAPI.resetCurrentUserUuid();
if (result.success && result.uuid) {
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerated') : 'New UUID generated successfully!';
showNotification(msg, 'success');
if (uuidModal && uuidModal.style.display !== 'none') {
await loadAllUuids();
}
} else {
throw new Error(result.error || 'Failed to generate new UUID');
}
} catch (error) {
console.error('Error regenerating UUID:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidRegenFailed').replace('{error}', error.message) : `Failed to regenerate UUID: ${error.message}`;
showNotification(msg, 'error');
}
}
async function openUuidModal() {
try {
if (uuidModal) {
uuidModal.style.display = 'flex';
uuidModal.classList.add('active');
await loadAllUuids();
}
} catch (error) {
console.error('Error opening UUID modal:', error);
}
}
function closeUuidModal() {
if (uuidModal) {
uuidModal.classList.remove('active');
setTimeout(() => {
uuidModal.style.display = 'none';
}, 300);
}
}
async function loadAllUuids() {
try {
if (!uuidList) return;
uuidList.innerHTML = `
<div class="uuid-loading">
<i class="fas fa-spinner fa-spin"></i>
Loading UUIDs...
</div>
`;
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
const mappings = await window.electronAPI.getAllUuidMappings();
if (mappings.length === 0) {
uuidList.innerHTML = `
<div class="uuid-loading">
<i class="fas fa-info-circle"></i>
No UUIDs found
</div>
`;
return;
}
uuidList.innerHTML = '';
for (const mapping of mappings) {
const item = document.createElement('div');
item.className = `uuid-list-item${mapping.isCurrent ? ' current' : ''}`;
item.innerHTML = `
<div class="uuid-item-info">
<div class="uuid-item-username">${escapeHtml(mapping.username)}</div>
<div class="uuid-item-uuid">${mapping.uuid}</div>
</div>
<div class="uuid-item-actions">
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
${!mapping.isCurrent ? `<button class="uuid-item-btn delete" onclick="deleteUuid('${escapeHtml(mapping.username)}')" title="Delete UUID">
<i class="fas fa-trash"></i>
</button>` : ''}
</div>
`;
uuidList.appendChild(item);
}
}
} catch (error) {
console.error('Error loading UUIDs:', error);
if (uuidList) {
uuidList.innerHTML = `
<div class="uuid-loading">
<i class="fas fa-exclamation-triangle"></i>
Error loading UUIDs
</div>
`;
}
}
}
async function generateNewUuid() {
try {
if (window.electronAPI && window.electronAPI.generateNewUuid) {
const newUuid = await window.electronAPI.generateNewUuid();
if (newUuid) {
if (customUuidInput) customUuidInput.value = newUuid;
const msg = window.i18n ? window.i18n.t('notifications.uuidGeneratedShort') : 'New UUID generated!';
showNotification(msg, 'success');
}
}
} catch (error) {
console.error('Error generating new UUID:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerateFailed') : 'Failed to generate new UUID';
showNotification(msg, 'error');
}
}
async function setCustomUuid() {
try {
if (!customUuidInput || !customUuidInput.value.trim()) {
const msg = window.i18n ? window.i18n.t('notifications.uuidRequired') : 'Please enter a UUID';
showNotification(msg, 'error');
return;
}
const uuid = customUuidInput.value.trim();
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(uuid)) {
const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
showNotification(msg, 'error');
return;
}
const message = window.i18n ? window.i18n.t('confirm.setCustomUuidMessage') : 'Are you sure you want to set this custom UUID? This will change your player identity.';
const title = window.i18n ? window.i18n.t('confirm.setCustomUuidTitle') : 'Set Custom UUID';
const confirmBtn = window.i18n ? window.i18n.t('confirm.setCustomUuidButton') : 'Set UUID';
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
showCustomConfirm(
message,
title,
async () => {
await performSetCustomUuid(uuid);
},
null,
confirmBtn,
cancelBtn
);
} catch (error) {
console.error('Error in setCustomUuid:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed') : 'Failed to set custom UUID';
showNotification(msg, 'error');
}
}
async function performSetCustomUuid(uuid) {
try {
if (window.electronAPI && window.electronAPI.setUuidForUser) {
const username = getCurrentPlayerName();
const result = await window.electronAPI.setUuidForUser(username, uuid);
if (result.success) {
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
if (customUuidInput) customUuidInput.value = '';
const msg = window.i18n ? window.i18n.t('notifications.uuidSetSuccess') : 'Custom UUID set successfully!';
showNotification(msg, 'success');
await loadAllUuids();
} else {
throw new Error(result.error || 'Failed to set custom UUID');
}
}
} catch (error) {
console.error('Error setting custom UUID:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed').replace('{error}', error.message) : `Failed to set custom UUID: ${error.message}`;
showNotification(msg, 'error');
}
}
window.copyUuid = async function(uuid) {
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(uuid);
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
showNotification(msg, 'success');
}
} catch (error) {
console.error('Error copying UUID:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidCopyFailed') : 'Failed to copy UUID';
showNotification(msg, 'error');
}
};
window.deleteUuid = async function(username) {
try {
const message = window.i18n ? window.i18n.t('confirm.deleteUuidMessage').replace('{username}', username) : `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`;
const title = window.i18n ? window.i18n.t('confirm.deleteUuidTitle') : 'Delete UUID';
const confirmBtn = window.i18n ? window.i18n.t('confirm.deleteUuidButton') : 'Delete';
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
showCustomConfirm(
message,
title,
async () => {
await performDeleteUuid(username);
},
null,
confirmBtn,
cancelBtn
);
} catch (error) {
console.error('Error in deleteUuid:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed') : 'Failed to delete UUID';
showNotification(msg, 'error');
}
};
async function performDeleteUuid(username) {
try {
if (window.electronAPI && window.electronAPI.deleteUuidForUser) {
const result = await window.electronAPI.deleteUuidForUser(username);
if (result.success) {
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
showNotification(msg, 'success');
await loadAllUuids();
} else {
throw new Error(result.error || 'Failed to delete UUID');
}
}
} catch (error) {
console.error('Error deleting UUID:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed').replace('{error}', error.message) : `Failed to delete UUID: ${error.message}`;
showNotification(msg, 'error');
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
color: white;
font-weight: 600;
z-index: 10000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
`;
if (type === 'success') {
notification.style.background = 'linear-gradient(135deg, #22c55e, #16a34a)';
} else if (type === 'error') {
notification.style.background = 'linear-gradient(135deg, #ef4444, #dc2626)';
} else {
notification.style.background = 'linear-gradient(135deg, #3b82f6, #2563eb)';
}
notification.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation-triangle' : 'info-circle'}"></i>
${message}
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}

View File

@@ -49,13 +49,6 @@ function setupWindowControls() {
const windowControls = document.querySelector('.window-controls'); const windowControls = document.querySelector('.window-controls');
const header = document.querySelector('.header'); const header = document.querySelector('.header');
const profileSelector = document.querySelector('.profile-selector');
if (profileSelector) {
profileSelector.style.pointerEvents = 'auto';
profileSelector.style.zIndex = '10000';
}
if (windowControls) { if (windowControls) {
windowControls.style.pointerEvents = 'auto'; windowControls.style.pointerEvents = 'auto';
windowControls.style.zIndex = '10000'; windowControls.style.zIndex = '10000';
@@ -66,9 +59,6 @@ function setupWindowControls() {
if (windowControls) { if (windowControls) {
windowControls.style.webkitAppRegion = 'no-drag'; windowControls.style.webkitAppRegion = 'no-drag';
} }
if (profileSelector) {
profileSelector.style.webkitAppRegion = 'no-drag';
}
} }
if (window.electronAPI) { if (window.electronAPI) {
@@ -366,7 +356,7 @@ function lockPlayButton(locked) {
if (!playButton.getAttribute('data-original-text')) { if (!playButton.getAttribute('data-original-text')) {
playButton.setAttribute('data-original-text', spanElement.textContent); playButton.setAttribute('data-original-text', spanElement.textContent);
} }
spanElement.textContent = window.i18n ? window.i18n.t('play.checking') : 'CHECKING...'; spanElement.textContent = 'CHECKING...';
} }
console.log('Play button locked'); console.log('Play button locked');
@@ -377,9 +367,9 @@ function lockPlayButton(locked) {
playButton.removeAttribute('data-locked'); playButton.removeAttribute('data-locked');
const spanElement = playButton.querySelector('span'); const spanElement = playButton.querySelector('span');
if (spanElement) { const originalText = playButton.getAttribute('data-original-text');
// Use i18n to get the current translation instead of restoring saved text if (spanElement && originalText) {
spanElement.textContent = window.i18n ? window.i18n.t('play.playButton') : 'PLAY HYTALE'; spanElement.textContent = originalText;
playButton.removeAttribute('data-original-text'); playButton.removeAttribute('data-original-text');
} }
@@ -393,8 +383,7 @@ async function acceptFirstLaunchUpdate() {
const existingGame = window.firstLaunchExistingGame; const existingGame = window.firstLaunchExistingGame;
if (!existingGame) { if (!existingGame) {
const errorMsg = window.i18n ? window.i18n.t('notifications.gameDataNotFound') : 'Error: Game data not found'; showNotification('Error: Game data not found', 'error');
showNotification(errorMsg, 'error');
return; return;
} }
@@ -411,8 +400,7 @@ async function acceptFirstLaunchUpdate() {
try { try {
showProgress(); showProgress();
const updateMsg = window.i18n ? window.i18n.t('progress.startingUpdate') : 'Starting mandatory game update...'; updateProgress({ message: 'Starting mandatory game update...', percent: 0 });
updateProgress({ message: updateMsg, percent: 0 });
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame); const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
@@ -426,12 +414,10 @@ async function acceptFirstLaunchUpdate() {
if (result.success) { if (result.success) {
hideProgress(); hideProgress();
const successMsg = window.i18n ? window.i18n.t('notifications.gameUpdatedSuccess') : 'Game updated successfully! 🎉'; showNotification('Game updated successfully! 🎉', 'success');
showNotification(successMsg, 'success');
} else { } else {
hideProgress(); hideProgress();
const errorMsg = window.i18n ? window.i18n.t('notifications.updateFailed').replace('{error}', result.error) : `Update failed: ${result.error}`; showNotification(`Update failed: ${result.error}`, 'error');
showNotification(errorMsg, 'error');
} }
} catch (error) { } catch (error) {
if (modal) { if (modal) {
@@ -439,8 +425,7 @@ async function acceptFirstLaunchUpdate() {
} }
lockPlayButton(false); lockPlayButton(false);
hideProgress(); hideProgress();
const errorMsg = window.i18n ? window.i18n.t('notifications.updateError').replace('{error}', error.message) : `Update error: ${error.message}`; showNotification(`Update error: ${error.message}`, 'error');
showNotification(errorMsg, 'error');
} }
} }
@@ -479,9 +464,6 @@ function setupUI() {
progressSpeed = document.getElementById('progressSpeed'); progressSpeed = document.getElementById('progressSpeed');
progressSize = document.getElementById('progressSize'); progressSize = document.getElementById('progressSize');
// Setup draggable progress bar
setupProgressDrag();
lockPlayButton(true); lockPlayButton(true);
setTimeout(() => { setTimeout(() => {
@@ -500,26 +482,10 @@ function setupUI() {
setupSidebarLogo(); setupSidebarLogo();
setupAnimations(); setupAnimations();
setupFirstLaunchHandlers(); setupFirstLaunchHandlers();
loadLauncherVersion();
document.body.focus(); document.body.focus();
} }
// Load launcher version from package.json
async function loadLauncherVersion() {
try {
if (window.electronAPI && window.electronAPI.getVersion) {
const version = await window.electronAPI.getVersion();
const versionElement = document.getElementById('launcherVersion');
if (versionElement) {
versionElement.textContent = `v${version}`;
}
}
} catch (error) {
console.error('Failed to load launcher version:', error);
}
}
window.LauncherUI = { window.LauncherUI = {
showPage, showPage,
setActiveNav, setActiveNav,
@@ -529,91 +495,4 @@ window.LauncherUI = {
updateProgress updateProgress
}; };
// Make installation effects globally available
window.showInstallationEffects = showInstallationEffects;
window.hideInstallationEffects = hideInstallationEffects;
// Draggable progress bar functionality
function setupProgressDrag() {
if (!progressOverlay) return;
let isDragging = false;
let offsetX;
let offsetY;
progressOverlay.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
// Only drag if clicking on the overlay itself, not on buttons or inputs
if (e.target.closest('.progress-bar-fill')) return;
if (e.target === progressOverlay || e.target.closest('.progress-content')) {
isDragging = true;
progressOverlay.classList.add('dragging');
// Get the current position of the progress overlay
const rect = progressOverlay.getBoundingClientRect();
offsetX = e.clientX - rect.left - progressOverlay.offsetWidth / 2;
offsetY = e.clientY - rect.top;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
// Calculate new position
const newX = e.clientX - offsetX - progressOverlay.offsetWidth / 2;
const newY = e.clientY - offsetY;
// Get window bounds
const maxX = window.innerWidth - progressOverlay.offsetWidth;
const maxY = window.innerHeight - progressOverlay.offsetHeight;
const minX = 0;
const minY = 0;
// Constrain to window bounds
const constrainedX = Math.max(minX, Math.min(newX, maxX));
const constrainedY = Math.max(minY, Math.min(newY, maxY));
progressOverlay.style.left = constrainedX + 'px';
progressOverlay.style.bottom = 'auto';
progressOverlay.style.top = constrainedY + 'px';
progressOverlay.style.transform = 'none';
}
}
function dragEnd() {
isDragging = false;
progressOverlay.classList.remove('dragging');
}
}
// Show/hide installation effects
function showInstallationEffects() {
const installationEffects = document.getElementById('installationEffects');
if (installationEffects) {
installationEffects.style.display = 'block';
}
}
function hideInstallationEffects() {
const installationEffects = document.getElementById('installationEffects');
if (installationEffects) {
installationEffects.style.display = 'none';
}
}
// Toggle maximize/restore window function
function toggleMaximize() {
if (window.electronAPI && window.electronAPI.maximizeWindow) {
window.electronAPI.maximizeWindow();
}
}
// Make toggleMaximize globally available
window.toggleMaximize = toggleMaximize;
document.addEventListener('DOMContentLoaded', setupUI); document.addEventListener('DOMContentLoaded', setupUI);

View File

@@ -10,23 +10,6 @@ class ClientUpdateManager {
this.showUpdatePopup(updateInfo); this.showUpdatePopup(updateInfo);
}); });
// Listen for electron-updater events
window.electronAPI.onUpdateAvailable((updateInfo) => {
this.showUpdatePopup(updateInfo);
});
window.electronAPI.onUpdateDownloadProgress((progress) => {
this.updateDownloadProgress(progress);
});
window.electronAPI.onUpdateDownloaded((updateInfo) => {
this.showUpdateDownloaded(updateInfo);
});
window.electronAPI.onUpdateError((errorInfo) => {
this.handleUpdateError(errorInfo);
});
this.checkForUpdatesOnDemand(); this.checkForUpdatesOnDemand();
} }
@@ -50,46 +33,23 @@ class ClientUpdateManager {
<div class="update-popup-versions"> <div class="update-popup-versions">
<div class="version-row"> <div class="version-row">
<span class="version-label">Current Version:</span> <span class="version-label">Current Version:</span>
<span class="version-current">${updateInfo.currentVersion || updateInfo.version || 'Unknown'}</span> <span class="version-current">${updateInfo.currentVersion}</span>
</div> </div>
<div class="version-row"> <div class="version-row">
<span class="version-label">New Version:</span> <span class="version-label">New Version:</span>
<span class="version-new">${updateInfo.newVersion || updateInfo.version || 'Unknown'}</span> <span class="version-new">${updateInfo.newVersion}</span>
</div> </div>
</div> </div>
<div class="update-popup-message"> <div class="update-popup-message">
A new version of Hytale F2P Launcher is available.<br> A new version of Hytale F2P Launcher is available.<br>
<span id="update-status-text">Downloading update automatically...</span> Please download the latest version to continue using the launcher.
<div id="update-error-message" style="display: none; margin-top: 0.75rem; padding: 0.75rem; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; color: #fca5a5; font-size: 0.875rem;">
<i class="fas fa-exclamation-triangle" style="margin-right: 0.5rem;"></i>
<span id="update-error-text"></span>
</div>
</div> </div>
<div id="update-progress-container" style="display: none; margin-bottom: 1rem;"> <button id="update-download-btn" class="update-download-btn">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-size: 0.75rem; color: #9ca3af;">
<span id="update-progress-percent">0%</span>
<span id="update-progress-speed">0 KB/s</span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<div id="update-progress-bar" style="width: 0%; height: 100%; background: linear-gradient(90deg, #3b82f6, #9333ea); transition: width 0.3s ease;"></div>
</div>
<div style="margin-top: 0.5rem; font-size: 0.75rem; color: #9ca3af; text-align: center;">
<span id="update-progress-size">0 MB / 0 MB</span>
</div>
</div>
<div id="update-buttons-container" style="display: none;">
<button id="update-install-btn" class="update-download-btn">
<i class="fas fa-check" style="margin-right: 0.5rem;"></i>
Install & Restart
</button>
<button id="update-download-btn" class="update-download-btn update-download-btn-secondary" style="margin-top: 0.75rem;">
<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i> <i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>
Manually Download Download Update
</button> </button>
</div>
<div class="update-popup-footer"> <div class="update-popup-footer">
This popup cannot be closed until you update the launcher This popup cannot be closed until you update the launcher
@@ -102,31 +62,6 @@ class ClientUpdateManager {
this.blockInterface(); this.blockInterface();
// Show progress container immediately (auto-download is enabled)
const progressContainer = document.getElementById('update-progress-container');
if (progressContainer) {
progressContainer.style.display = 'block';
}
const installBtn = document.getElementById('update-install-btn');
if (installBtn) {
installBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
installBtn.disabled = true;
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Installing...';
try {
await window.electronAPI.quitAndInstallUpdate();
} catch (error) {
console.error('❌ Error installing update:', error);
installBtn.disabled = false;
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
}
});
}
const downloadBtn = document.getElementById('update-download-btn'); const downloadBtn = document.getElementById('update-download-btn');
if (downloadBtn) { if (downloadBtn) {
downloadBtn.addEventListener('click', async (e) => { downloadBtn.addEventListener('click', async (e) => {
@@ -145,7 +80,7 @@ class ClientUpdateManager {
} catch (error) { } catch (error) {
console.error('❌ Error opening download page:', error); console.error('❌ Error opening download page:', error);
downloadBtn.disabled = false; downloadBtn.disabled = false;
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Manually Download'; downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update';
} }
}); });
} }
@@ -164,134 +99,6 @@ class ClientUpdateManager {
console.log('🔔 Update popup displayed with new style'); console.log('🔔 Update popup displayed with new style');
} }
updateDownloadProgress(progress) {
const progressBar = document.getElementById('update-progress-bar');
const progressPercent = document.getElementById('update-progress-percent');
const progressSpeed = document.getElementById('update-progress-speed');
const progressSize = document.getElementById('update-progress-size');
if (progressBar && progress) {
const percent = Math.round(progress.percent || 0);
progressBar.style.width = `${percent}%`;
if (progressPercent) {
progressPercent.textContent = `${percent}%`;
}
if (progressSpeed && progress.bytesPerSecond) {
const speedMBps = (progress.bytesPerSecond / 1024 / 1024).toFixed(2);
progressSpeed.textContent = `${speedMBps} MB/s`;
}
if (progressSize && progress.transferred && progress.total) {
const transferredMB = (progress.transferred / 1024 / 1024).toFixed(2);
const totalMB = (progress.total / 1024 / 1024).toFixed(2);
progressSize.textContent = `${transferredMB} MB / ${totalMB} MB`;
}
// Don't update status text here - it's already set and the progress bar shows the percentage
}
}
showUpdateDownloaded(updateInfo) {
const statusText = document.getElementById('update-status-text');
const progressContainer = document.getElementById('update-progress-container');
const buttonsContainer = document.getElementById('update-buttons-container');
if (statusText) {
statusText.textContent = 'Update downloaded! Ready to install.';
}
if (progressContainer) {
progressContainer.style.display = 'none';
}
if (buttonsContainer) {
buttonsContainer.style.display = 'block';
}
console.log('✅ Update downloaded, ready to install');
}
handleUpdateError(errorInfo) {
console.error('Update error:', errorInfo);
// If manual download is required, update the UI (this will handle status text)
if (errorInfo.requiresManualDownload) {
this.showManualDownloadRequired(errorInfo);
return; // Don't do anything else, showManualDownloadRequired handles everything
}
// For non-critical errors, just show error message without changing status
const errorMessage = document.getElementById('update-error-message');
const errorText = document.getElementById('update-error-text');
if (errorMessage && errorText) {
let message = errorInfo.message || 'An error occurred during the update process.';
if (errorInfo.isMacSigningError) {
message = 'Auto-update requires code signing. Please download manually.';
}
errorText.textContent = message;
errorMessage.style.display = 'block';
}
}
showManualDownloadRequired(errorInfo) {
const statusText = document.getElementById('update-status-text');
const progressContainer = document.getElementById('update-progress-container');
const buttonsContainer = document.getElementById('update-buttons-container');
const installBtn = document.getElementById('update-install-btn');
const downloadBtn = document.getElementById('update-download-btn');
const errorMessage = document.getElementById('update-error-message');
const errorText = document.getElementById('update-error-text');
// Hide progress and install button
if (progressContainer) {
progressContainer.style.display = 'none';
}
if (installBtn) {
installBtn.style.display = 'none';
}
// Update status message (only once, don't change it again)
if (statusText && !statusText.dataset.manualMode) {
statusText.textContent = 'Please download and install the update manually.';
statusText.dataset.manualMode = 'true'; // Mark that we've set manual mode
}
// Show error message with details
if (errorMessage && errorText) {
let message = 'Auto-update is not available. ';
if (errorInfo.isMacSigningError) {
message = 'This app requires code signing for automatic updates.';
} else if (errorInfo.isLinuxInstallError) {
message = 'Auto-installation requires root privileges. Please download and install the update manually using your package manager.';
} else if (errorInfo.message) {
message = errorInfo.message;
} else {
message = 'An error occurred during the update process.';
}
errorText.textContent = message;
errorMessage.style.display = 'block';
}
// Show and enable the manual download button (make it primary since it's the only option)
if (downloadBtn) {
downloadBtn.style.display = 'block';
downloadBtn.disabled = false;
downloadBtn.classList.remove('update-download-btn-secondary');
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update Manually';
}
// Show buttons container if not already visible
if (buttonsContainer) {
buttonsContainer.style.display = 'block';
}
console.log('⚠️ Manual download required due to update error');
}
blockInterface() { blockInterface() {
const mainContent = document.querySelector('.flex.w-full.h-screen'); const mainContent = document.querySelector('.flex.w-full.h-screen');
if (mainContent) { if (mainContent) {
@@ -337,12 +144,7 @@ class ClientUpdateManager {
async checkForUpdatesOnDemand() { async checkForUpdatesOnDemand() {
try { try {
const updateInfo = await window.electronAPI.checkForUpdates(); const updateInfo = await window.electronAPI.checkForUpdates();
if (updateInfo.updateAvailable) {
// Double-check that versions are actually different before showing popup
if (updateInfo.updateAvailable &&
updateInfo.newVersion &&
updateInfo.currentVersion &&
updateInfo.newVersion !== updateInfo.currentVersion) {
this.showUpdatePopup(updateInfo); this.showUpdatePopup(updateInfo);
} }
return updateInfo; return updateInfo;

View File

@@ -1,231 +0,0 @@
{
"nav": {
"play": "Play",
"mods": "Mods",
"news": "News",
"chat": "Players Chat",
"settings": "Settings"
},
"header": {
"playersLabel": "Players:",
"manageProfiles": "Manage Profiles",
"defaultProfile": "Default"
},
"install": {
"title": "FREE TO PLAY LAUNCHER",
"playerName": "Player Name",
"playerNamePlaceholder": "Enter your name",
"customInstallation": "Custom Installation",
"installationFolder": "Installation Folder",
"pathPlaceholder": "Default location",
"browse": "Browse",
"installButton": "INSTALL HYTALE",
"installing": "INSTALLING..."
},
"play": {
"ready": "READY TO PLAY",
"subtitle": "Launch Hytale and enter the adventure",
"playButton": "PLAY HYTALE",
"latestNews": "LATEST NEWS",
"viewAll": "VIEW ALL",
"checking": "CHECKING...",
"play": "PLAY"
},
"mods": {
"searchPlaceholder": "Search mods...",
"myMods": "MY MODS",
"previous": "PREVIOUS",
"next": "NEXT",
"page": "Page",
"of": "of",
"modalTitle": "MY MODS",
"noModsFound": "No Mods Found",
"noModsFoundDesc": "Try adjusting your search",
"noModsInstalled": "No Mods Installed",
"noModsInstalledDesc": "Add mods from CurseForge or import local files",
"view": "VIEW",
"install": "INSTALL",
"installed": "INSTALLED",
"enable": "ENABLE",
"disable": "DISABLE",
"active": "ACTIVE",
"disabled": "DISABLED",
"delete": "Delete mod",
"noDescription": "No description available",
"confirmDelete": "Are you sure you want to delete \"{name}\"?",
"confirmDeleteDesc": "This action cannot be undone.",
"confirmDeletion": "Confirm Deletion"
},
"news": {
"title": "ALL NEWS",
"readMore": "Read More"
},
"chat": {
"title": "PLAYERS CHAT",
"pickColor": "Color",
"inputPlaceholder": "Type your message...",
"send": "Send",
"online": "online",
"charCounter": "{current}/{max}",
"secureChat": "Secure chat - Links are censored",
"joinChat": "Join Chat",
"chooseUsername": "Choose a username to join the Players Chat",
"username": "Username",
"usernamePlaceholder": "Enter your username...",
"usernameHint": "3-20 characters, letters, numbers, - and _ only",
"joinButton": "Join Chat",
"colorModal": {
"title": "Customize Username Color",
"chooseSolid": "Choose a solid color:",
"customColor": "Custom color:",
"preview": "Preview:",
"previewUsername": "Username",
"apply": "Apply Color"
}
},
"settings": {
"title": "SETTINGS",
"java": "Java Runtime",
"useCustomJava": "Use Custom Java Path",
"javaDescription": "Override the bundled Java runtime with your own installation",
"javaPath": "Java Executable Path",
"javaPathPlaceholder": "Select Java path...",
"javaBrowse": "Browse",
"javaHint": "Select the Java installation folder (supports Windows, Mac, Linux)",
"discord": "Discord Integration",
"enableRPC": "Enable Discord Rich Presence",
"discordDescription": "Show your launcher activity on Discord",
"game": "Game Options",
"playerName": "Player Name",
"playerNamePlaceholder": "Enter your player name",
"playerNameHint": "This name will be used in-game (1-16 characters)",
"openGameLocation": "Open Game Location",
"openGameLocationDesc": "Open the game installation folder",
"account": "Player UUID Management",
"currentUUID": "Current UUID",
"uuidPlaceholder": "Loading UUID...",
"copyUUID": "Copy UUID",
"regenerateUUID": "Regenerate UUID",
"uuidHint": "Your unique player identifier for this username",
"manageUUIDs": "Manage All UUIDs",
"manageUUIDsDesc": "View and manage all player UUIDs",
"language": "Language",
"selectLanguage": "Select Language",
"repairGame": "Repair Game",
"reinstallGame": "Reinstall game files (preserves data)",
"gpuPreference": "GPU Preference",
"gpuHint": "Select your preferred GPU (Linux: affects DRI_PRIME)",
"gpuAuto": "Auto",
"gpuIntegrated": "Integrated",
"gpuDedicated": "Dedicated",
"logs": "SYSTEM LOGS",
"logsCopy": "Copy",
"logsRefresh": "Refresh",
"logsFolder": "Open Folder",
"logsLoading": "Loading logs...",
"closeLauncher": "Launcher Behavior",
"closeOnStart": "Close Launcher on game start",
"closeOnStartDescription": "Automatically close the launcher after Hytale has launched"
},
"uuid": {
"modalTitle": "UUID Management",
"currentUserUUID": "Current User UUID",
"allPlayerUUIDs": "All Player UUIDs",
"generateNew": "Generate New UUID",
"loadingUUIDs": "Loading UUIDs...",
"setCustomUUID": "Set Custom UUID",
"customPlaceholder": "Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Set UUID",
"warning": "Warning: Setting a custom UUID will change your current player identity",
"copyTooltip": "Copy UUID",
"regenerateTooltip": "Generate New UUID"
},
"profiles": {
"modalTitle": "Manage Profiles",
"newProfilePlaceholder": "New Profile Name",
"createProfile": "Create Profile"
},
"discord": {
"notificationText": "Join our Discord community!",
"joinButton": "Join Discord"
},
"common": {
"confirm": "Confirm",
"cancel": "Cancel",
"save": "Save",
"close": "Close",
"delete": "Delete",
"edit": "Edit",
"loading": "Loading...",
"apply": "Apply"
},
"notifications": {
"gameDataNotFound": "Error: Game data not found",
"gameUpdatedSuccess": "Game updated successfully! 🎉",
"updateFailed": "Update failed: {error}",
"updateError": "Update error: {error}",
"discordEnabled": "Discord Rich Presence enabled",
"discordDisabled": "Discord Rich Presence disabled",
"discordSaveFailed": "Failed to save Discord setting",
"playerNameRequired": "Please enter a valid player name",
"playerNameSaved": "Player name saved successfully",
"playerNameSaveFailed": "Failed to save player name",
"uuidCopied": "UUID copied to clipboard!",
"uuidCopyFailed": "Failed to copy UUID",
"uuidRegenNotAvailable": "UUID regeneration not available",
"uuidRegenFailed": "Failed to regenerate UUID",
"uuidGenerated": "New UUID generated successfully!",
"uuidGeneratedShort": "New UUID generated!",
"uuidGenerateFailed": "Failed to generate new UUID",
"uuidRequired": "Please enter a UUID",
"uuidInvalidFormat": "Invalid UUID format",
"uuidSetFailed": "Failed to set custom UUID",
"uuidSetSuccess": "Custom UUID set successfully!",
"uuidDeleteFailed": "Failed to delete UUID",
"uuidDeleteSuccess": "UUID deleted successfully!",
"modsDownloading": "Downloading {name}...",
"modsTogglingMod": "Toggling mod...",
"modsDeletingMod": "Deleting mod...",
"modsLoadingMods": "Loading mods from CurseForge...",
"modsInstalledSuccess": "{name} installed successfully! 🎉",
"modsDeletedSuccess": "{name} deleted successfully",
"modsDownloadFailed": "Failed to download mod: {error}",
"modsToggleFailed": "Failed to toggle mod: {error}",
"modsDeleteFailed": "Failed to delete mod: {error}",
"modsModNotFound": "Mod information not found"
},
"confirm": {
"defaultTitle": "Confirm action",
"regenerateUuidTitle": "Generate new UUID",
"regenerateUuidMessage": "Are you sure you want to generate a new UUID? This will change your player identity.",
"regenerateUuidButton": "Generate",
"setCustomUuidTitle": "Set custom UUID",
"setCustomUuidMessage": "Are you sure you want to set this custom UUID? This will change your player identity.",
"setCustomUuidButton": "Set UUID",
"deleteUuidTitle": "Delete UUID",
"deleteUuidMessage": "Are you sure you want to delete the UUID for \"{username}\"? This action cannot be undone.",
"deleteUuidButton": "Delete",
"uninstallGameTitle": "Uninstall game",
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
"uninstallGameButton": "Uninstall"
},
"progress": {
"initializing": "Initializing...",
"downloading": "Downloading...",
"installing": "Installing...",
"extracting": "Extracting...",
"verifying": "Verifying...",
"switchingProfile": "Switching profile...",
"profileSwitched": "Profile switched!",
"startingGame": "Starting game...",
"launching": "LAUNCHING...",
"uninstallingGame": "Uninstalling game...",
"gameUninstalled": "Game uninstalled successfully!",
"uninstallFailed": "Uninstall failed: {error}",
"startingUpdate": "Starting mandatory game update...",
"installationComplete": "Installation completed successfully!",
"installationFailed": "Installation failed: {error}",
"installingGameFiles": "Installing game files...",
"installComplete": "Installation complete!"
}
}

View File

@@ -1,231 +0,0 @@
{
"nav": {
"play": "Jugar",
"mods": "Mods",
"news": "Noticias",
"chat": "Chat de Jugadores",
"settings": "Configuración"
},
"header": {
"playersLabel": "Jugadores:",
"manageProfiles": "Gestionar Perfiles",
"defaultProfile": "Predeterminado"
},
"install": {
"title": "LAUNCHER GRATUITO",
"playerName": "Nombre del Jugador",
"playerNamePlaceholder": "Ingresa tu nombre",
"customInstallation": "Instalación Personalizada",
"installationFolder": "Carpeta de Instalación",
"pathPlaceholder": "Ubicación predeterminada",
"browse": "Examinar",
"installButton": "INSTALAR HYTALE",
"installing": "INSTALANDO..."
},
"play": {
"ready": "LISTO PARA JUGAR",
"subtitle": "Inicia Hytale y entra en la aventura",
"playButton": "JUGAR HYTALE",
"latestNews": "ÚLTIMAS NOTICIAS",
"viewAll": "VER TODO",
"checking": "VERIFICANDO...",
"play": "JUGAR"
},
"mods": {
"searchPlaceholder": "Buscar mods...",
"myMods": "MIS MODS",
"previous": "ANTERIOR",
"next": "SIGUIENTE",
"page": "Página",
"of": "de",
"modalTitle": "MIS MODS",
"noModsFound": "No se encontraron mods",
"noModsFoundDesc": "Intenta ajustar tu búsqueda",
"noModsInstalled": "No hay mods instalados",
"noModsInstalledDesc": "Añade mods desde CurseForge o importa archivos locales",
"view": "VER",
"install": "INSTALAR",
"installed": "INSTALADO",
"enable": "ACTIVAR",
"disable": "DESACTIVAR",
"active": "ACTIVO",
"disabled": "DESACTIVADO",
"delete": "Eliminar mod",
"noDescription": "Sin descripción disponible",
"confirmDelete": "¿Estás seguro de que quieres eliminar \"{name}\"?",
"confirmDeleteDesc": "Esta acción no se puede deshacer.",
"confirmDeletion": "Confirmar eliminación"
},
"news": {
"title": "TODAS LAS NOTICIAS",
"readMore": "Leer más"
},
"chat": {
"title": "CHAT DE JUGADORES",
"pickColor": "Color",
"inputPlaceholder": "Escribe tu mensaje...",
"send": "Enviar",
"online": "en línea",
"charCounter": "{current}/{max}",
"secureChat": "Chat seguro - Los enlaces están censurados",
"joinChat": "Unirse al chat",
"chooseUsername": "Elige un nombre de usuario para unirte al chat de jugadores",
"username": "Nombre de usuario",
"usernamePlaceholder": "Ingresa tu nombre de usuario...",
"usernameHint": "3-20 caracteres, letras, números, - y _ solamente",
"joinButton": "Unirse al Chat",
"colorModal": {
"title": "Personalizar color del nombre",
"chooseSolid": "Elige un color sólido:",
"customColor": "Color personalizado:",
"preview": "Vista previa:",
"previewUsername": "Nombre de usuario",
"apply": "Aplicar color"
}
},
"settings": {
"title": "CONFIGURACIÓN",
"java": "Entorno Java",
"useCustomJava": "Usar ruta de Java personalizada",
"javaDescription": "Reemplaza el entorno Java incluido con tu propia instalación",
"javaPath": "Ruta del ejecutable Java",
"javaPathPlaceholder": "Selecciona la ruta de Java...",
"javaBrowse": "Examinar",
"javaHint": "Selecciona la carpeta de instalación de Java (compatible con Windows, Mac, Linux)",
"discord": "Integración con Discord",
"enableRPC": "Habilitar Discord Rich Presence",
"discordDescription": "Muestra tu actividad del launcher en Discord",
"game": "Opciones del juego",
"playerName": "Nombre del jugador",
"playerNamePlaceholder": "Ingresa tu nombre de jugador",
"playerNameHint": "Este nombre se usará en el juego (1-16 caracteres)",
"openGameLocation": "Abrir ubicación del juego",
"openGameLocationDesc": "Abre la carpeta de instalación del juego",
"account": "Gestión de UUID del jugador",
"currentUUID": "UUID actual",
"uuidPlaceholder": "Cargando UUID...",
"copyUUID": "Copiar UUID",
"regenerateUUID": "Regenerar UUID",
"uuidHint": "Tu identificador único de jugador para este nombre de usuario",
"manageUUIDs": "Gestionar todos los UUIDs",
"manageUUIDsDesc": "Ver y gestionar todos los UUIDs de jugadores",
"language": "Idioma",
"selectLanguage": "Seleccionar idioma",
"repairGame": "Reparar juego",
"reinstallGame": "Reinstalar archivos del juego (conserva los datos)",
"gpuPreference": "Preferencia de GPU",
"gpuHint": "Selecciona tu GPU preferida (Linux: afecta DRI_PRIME)",
"gpuAuto": "Automático",
"gpuIntegrated": "Integrada",
"gpuDedicated": "Dedicada",
"logs": "REGISTROS DEL SISTEMA",
"logsCopy": "Copiar",
"logsRefresh": "Actualizar",
"logsFolder": "Abrir Carpeta",
"logsLoading": "Cargando registros...",
"closeLauncher": "Comportamiento del Launcher",
"closeOnStart": "Cerrar Launcher al iniciar el juego",
"closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado"
},
"uuid": {
"modalTitle": "Gestión de UUID",
"currentUserUUID": "UUID del usuario actual",
"allPlayerUUIDs": "Todos los UUIDs de jugadores",
"generateNew": "Generar nuevo UUID",
"loadingUUIDs": "Cargando UUIDs...",
"setCustomUUID": "Establecer UUID personalizado",
"customPlaceholder": "Ingresa un UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Establecer UUID",
"warning": "Advertencia: Establecer un UUID personalizado cambiará tu identidad de jugador actual",
"copyTooltip": "Copiar UUID",
"regenerateTooltip": "Generar nuevo UUID"
},
"profiles": {
"modalTitle": "Gestionar perfiles",
"newProfilePlaceholder": "Nombre del nuevo perfil",
"createProfile": "Crear perfil"
},
"discord": {
"notificationText": "¡Únete a nuestra comunidad de Discord!",
"joinButton": "Unirse a Discord"
},
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",
"save": "Guardar",
"close": "Cerrar",
"delete": "Eliminar",
"edit": "Editar",
"loading": "Cargando...",
"apply": "Aplicar"
},
"notifications": {
"gameDataNotFound": "Error: No se encontraron datos del juego",
"gameUpdatedSuccess": "¡Juego actualizado con éxito! 🎉",
"updateFailed": "Actualización fallida: {error}",
"updateError": "Error de actualización: {error}",
"discordEnabled": "Discord Rich Presence habilitado",
"discordDisabled": "Discord Rich Presence deshabilitado",
"discordSaveFailed": "Error al guardar la configuración de Discord",
"playerNameRequired": "Por favor ingresa un nombre de jugador válido",
"playerNameSaved": "Nombre de jugador guardado con éxito",
"playerNameSaveFailed": "Error al guardar el nombre de jugador",
"uuidCopied": "¡UUID copiado al portapapeles!",
"uuidCopyFailed": "Error al copiar UUID",
"uuidRegenNotAvailable": "Regeneración de UUID no disponible",
"uuidRegenFailed": "Error al regenerar UUID",
"uuidGenerated": "¡Nuevo UUID generado con éxito!",
"uuidGeneratedShort": "¡Nuevo UUID generado!",
"uuidGenerateFailed": "Error al generar nuevo UUID",
"uuidRequired": "Por favor ingresa un UUID",
"uuidInvalidFormat": "Formato de UUID inválido",
"uuidSetFailed": "Error al establecer UUID personalizado",
"uuidSetSuccess": "¡UUID personalizado establecido con éxito!",
"uuidDeleteFailed": "Error al eliminar UUID",
"uuidDeleteSuccess": "¡UUID eliminado con éxito!",
"modsDownloading": "Descargando {name}...",
"modsTogglingMod": "Alternando mod...",
"modsDeletingMod": "Eliminando mod...",
"modsLoadingMods": "Cargando mods desde CurseForge...",
"modsInstalledSuccess": "¡{name} instalado con éxito! 🎉",
"modsDeletedSuccess": "{name} eliminado con éxito",
"modsDownloadFailed": "Error al descargar mod: {error}",
"modsToggleFailed": "Error al alternar mod: {error}",
"modsDeleteFailed": "Error al eliminar mod: {error}",
"modsModNotFound": "Información del mod no encontrada"
},
"confirm": {
"defaultTitle": "Confirmar acción",
"regenerateUuidTitle": "Generar nuevo UUID",
"regenerateUuidMessage": "¿Estás seguro de que quieres generar un nuevo UUID? Esto cambiará tu identidad de jugador.",
"regenerateUuidButton": "Generar",
"setCustomUuidTitle": "Establecer UUID personalizado",
"setCustomUuidMessage": "¿Estás seguro de que quieres establecer este UUID personalizado? Esto cambiará tu identidad de jugador.",
"setCustomUuidButton": "Establecer UUID",
"deleteUuidTitle": "Eliminar UUID",
"deleteUuidMessage": "¿Estás seguro de que quieres eliminar el UUID de \"{username}\"? Esta acción no se puede deshacer.",
"deleteUuidButton": "Eliminar",
"uninstallGameTitle": "Desinstalar juego",
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
"uninstallGameButton": "Desinstalar"
},
"progress": {
"initializing": "Inicializando...",
"downloading": "Descargando...",
"installing": "Instalando...",
"extracting": "Extrayendo...",
"verifying": "Verificando...",
"switchingProfile": "Cambiando perfil...",
"profileSwitched": "¡Perfil cambiado!",
"startingGame": "Iniciando juego...",
"launching": "INICIANDO...",
"uninstallingGame": "Desinstalando juego...",
"gameUninstalled": "¡Juego desinstalado con éxito!",
"uninstallFailed": "Desinstalación fallida: {error}",
"startingUpdate": "Iniciando actualización obligatoria del juego...",
"installationComplete": "¡Instalación completada con éxito!",
"installationFailed": "Instalación fallida: {error}",
"installingGameFiles": "Instalando archivos del juego...",
"installComplete": "¡Instalación completa!"
}
}

View File

@@ -1,232 +0,0 @@
{
"nav": {
"play": "Jogar",
"mods": "Mods",
"news": "Notícias",
"chat": "Chat de Jogadores",
"settings": "Configurações"
},
"header": {
"playersLabel": "Jogadores:",
"manageProfiles": "Gerenciar Perfis",
"defaultProfile": "Padrão"
},
"install": {
"title": "LANÇADOR JOGO GRATUITO",
"playerName": "Nome do Jogador",
"playerNamePlaceholder": "Digite seu nome",
"customInstallation": "Instalação Personalizada",
"installationFolder": "Pasta de Instalação",
"pathPlaceholder": "Local padrão",
"browse": "Procurar",
"installButton": "INSTALAR HYTALE",
"installing": "INSTALANDO..."
},
"play": {
"ready": "PRONTO PARA JOGAR",
"subtitle": "Inicie Hytale e entre na aventura",
"playButton": "JOGAR HYTALE",
"latestNews": "ÚLTIMAS NOTÍCIAS",
"viewAll": "VER TUDO",
"checking": "VERIFICANDO...",
"play": "JOGAR"
},
"mods": {
"searchPlaceholder": "Pesquisar mods...",
"myMods": "MEUS MODS",
"previous": "ANTERIOR",
"next": "PRÓXIMO",
"page": "Página",
"of": "de",
"modalTitle": "MEUS MODS",
"noModsFound": "Nenhum mod encontrado",
"noModsFoundDesc": "Tente ajustar sua pesquisa",
"noModsInstalled": "Nenhum mod instalado",
"noModsInstalledDesc": "Adicione mods do CurseForge ou importe arquivos locais",
"view": "VER",
"install": "INSTALAR",
"installed": "INSTALADO",
"enable": "ATIVAR",
"disable": "DESATIVAR",
"active": "ATIVO",
"disabled": "DESATIVADO",
"delete": "Excluir mod",
"noDescription": "Nenhuma descrição disponível",
"confirmDelete": "Tem certeza de que deseja excluir \"{name}\"?",
"confirmDeleteDesc": "Esta ação não pode ser desfeita.",
"confirmDeletion": "Confirmar exclusão"
},
"news": {
"title": "TODAS AS NOTÍCIAS",
"readMore": "Leia mais"
},
"chat": {
"title": "CHAT DE JOGADORES",
"pickColor": "Cor",
"inputPlaceholder": "Digite sua mensagem...",
"send": "Enviar",
"online": "online",
"charCounter": "{current}/{max}",
"secureChat": "Chat seguro - Links são censurados",
"joinChat": "Entrar no chat",
"chooseUsername": "Escolha um nome de usuário para entrar no chat de jogadores",
"username": "Nome de usuário",
"usernamePlaceholder": "Digite seu nome de usuário...",
"usernameHint": "3-20 caracteres, letras, números, - e _ apenas",
"joinButton": "Entrar no Chat",
"colorModal": {
"title": "Personalizar cor do nome de usuário",
"chooseSolid": "Escolha uma cor sólida:",
"customColor": "Cor personalizada:",
"preview": "Visualização:",
"previewUsername": "Nome de usuário",
"apply": "Aplicar cor"
}
},
"settings": {
"title": "CONFIGURAÇÕES",
"java": "Tempo de execução Java",
"useCustomJava": "Usar caminho personalizado do Java",
"javaDescription": "Substitua o tempo de execução Java incluído pela sua própria instalação",
"javaPath": "Caminho do executável Java",
"javaPathPlaceholder": "Selecione o caminho do Java...",
"javaBrowse": "Procurar",
"javaHint": "Selecione a pasta de instalação do Java (suporta Windows, Mac, Linux)",
"discord": "Integração do Discord",
"enableRPC": "Ativar Discord Rich Presence",
"discordDescription": "Mostre sua atividade do lançador no Discord",
"game": "Opções do jogo",
"playerName": "Nome do jogador",
"playerNamePlaceholder": "Digite seu nome de jogador",
"playerNameHint": "Este nome será usado no jogo (1-16 caracteres)",
"openGameLocation": "Abrir local do jogo",
"openGameLocationDesc": "Abra a pasta de instalação do jogo",
"account": "Gerenciamento de UUID do jogador",
"currentUUID": "UUID atual",
"uuidPlaceholder": "Carregando UUID...",
"copyUUID": "Copiar UUID",
"regenerateUUID": "Regenerar UUID",
"uuidHint": "Seu identificador único de jogador para este nome de usuário",
"manageUUIDs": "Gerenciar todos os UUIDs",
"manageUUIDsDesc": "Ver e gerenciar todos os UUIDs de jogadores",
"language": "Idioma",
"selectLanguage": "Selecionar idioma",
"repairGame": "Reparar jogo",
"reinstallGame": "Reinstalar arquivos do jogo (mantém os dados)",
"gpuPreference": "Preferência de GPU",
"gpuHint": "Selecione sua GPU preferida (Linux: afeta o DRI_PRIME)",
"gpuAuto": "Automático",
"gpuIntegrated": "Integrada",
"gpuDedicated": "Dedicada",
"logs": "REGISTROS DO SISTEMA",
"logsCopy": "Copiar",
"logsRefresh": "Atualizar",
"logsFolder": "Abrir Pasta",
"logsLoading": "Carregando registros...",
"closeLauncher": "Comportamento do Lançador",
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
"closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado"
},
"uuid": {
"modalTitle": "Gerenciamento de UUID",
"currentUserUUID": "UUID do usuário atual",
"allPlayerUUIDs": "Todos os UUIDs de jogadores",
"generateNew": "Gerar novo UUID",
"loadingUUIDs": "Carregando UUIDs...",
"setCustomUUID": "Definir UUID personalizado",
"customPlaceholder": "Digite um UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Definir UUID",
"warning": "Aviso: Definir um UUID personalizado alterará sua identidade de jogador atual",
"copyTooltip": "Copiar UUID",
"regenerateTooltip": "Gerar novo UUID"
},
"profiles": {
"modalTitle": "Gerenciar perfis",
"newProfilePlaceholder": "Nome do novo perfil",
"createProfile": "Criar perfil"
},
"discord": {
"notificationText": "Junte-se à nossa comunidade do Discord!",
"joinButton": "Entrar no Discord"
},
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",
"save": "Salvar",
"close": "Fechar",
"delete": "Excluir",
"edit": "Editar",
"loading": "Carregando...",
"apply": "Aplicar"
},
"notifications": {
"gameDataNotFound": "Erro: Dados do jogo não encontrados",
"gameUpdatedSuccess": "Jogo atualizado com sucesso! 🎉",
"updateFailed": "Falha na atualização: {error}",
"updateError": "Erro de atualização: {error}",
"discordEnabled": "Discord Rich Presence ativado",
"discordDisabled": "Discord Rich Presence desativado",
"discordSaveFailed": "Falha ao salvar configuração do Discord",
"playerNameRequired": "Por favor, digite um nome de jogador válido",
"playerNameSaved": "Nome do jogador salvo com sucesso",
"playerNameSaveFailed": "Falha ao salvar o nome do jogador",
"uuidCopied": "UUID copiado para a área de transferência!",
"uuidCopyFailed": "Falha ao copiar UUID",
"uuidRegenNotAvailable": "Regeneração de UUID não disponível",
"uuidRegenFailed": "Falha ao regenerar UUID",
"uuidGenerated": "Novo UUID gerado com sucesso!",
"uuidGeneratedShort": "Novo UUID gerado!",
"uuidGenerateFailed": "Falha ao gerar novo UUID",
"uuidRequired": "Por favor, digite um UUID",
"uuidInvalidFormat": "Formato de UUID inválido",
"uuidSetFailed": "Falha ao definir UUID personalizado",
"uuidSetSuccess": "UUID personalizado definido com sucesso!",
"uuidDeleteFailed": "Falha ao excluir UUID",
"uuidDeleteSuccess": "UUID excluído com sucesso!",
"modsDownloading": "Baixando {name}...",
"modsTogglingMod": "Alternando mod...",
"modsDeletingMod": "Excluindo mod...",
"modsLoadingMods": "Carregando mods do CurseForge...",
"modsInstalledSuccess": "{name} instalado com sucesso! 🎉",
"modsDeletedSuccess": "{name} excluído com sucesso",
"modsDownloadFailed": "Falha ao baixar mod: {error}",
"modsToggleFailed": "Falha ao alternar mod: {error}",
"modsDeleteFailed": "Falha ao excluir mod: {error}",
"modsModNotFound": "Informações do mod não encontradas"
},
"confirm": {
"defaultTitle": "Confirmar ação",
"regenerateUuidTitle": "Gerar novo UUID",
"regenerateUuidMessage": "Tem certeza de que deseja gerar um novo UUID? Isso alterará sua identidade de jogador.",
"regenerateUuidButton": "Gerar",
"setCustomUuidTitle": "Definir UUID personalizado",
"setCustomUuidMessage": "Tem certeza de que deseja definir este UUID personalizado? Isso alterará sua identidade de jogador.",
"setCustomUuidButton": "Definir UUID",
"deleteUuidTitle": "Excluir UUID",
"deleteUuidMessage": "Tem certeza de que deseja excluir o UUID de \"{username}\"? Esta ação não pode ser desfeita.",
"deleteUuidButton": "Excluir",
"uninstallGameTitle": "Desinstalar jogo",
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
"uninstallGameButton": "Desinstalar"
},
"progress": {
"initializing": "Inicializando...",
"downloading": "Baixando...",
"installing": "Instalando...",
"extracting": "Extraindo...",
"verifying": "Verificando...",
"switchingProfile": "Alternando perfil...",
"profileSwitched": "Perfil alternado!",
"startingGame": "Iniciando jogo...",
"launching": "INICIANDO...",
"uninstallingGame": "Desinstalando jogo...",
"gameUninstalled": "Jogo desinstalado com sucesso!",
"uninstallFailed": "Falha na desinstalação: {error}",
"startingUpdate": "Iniciando atualização obrigatória do jogo...",
"installationComplete": "Instalação concluída com sucesso!",
"installationFailed": "Falha na instalação: {error}",
"installingGameFiles": "Instalando arquivos do jogo...",
"installComplete": "Instalação concluída!"
}
}

View File

@@ -1,178 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hytale F2P</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100%;
height: 100vh;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Space Grotesk', sans-serif;
overflow: hidden;
position: relative;
border-radius: 16px;
}
.background {
position: absolute;
inset: 0;
z-index: 0;
border-radius: 16px;
overflow: hidden;
}
.background img {
width: 100%;
height: 100%;
object-fit: cover;
}
.background::after {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.7);
}
.splash-container {
position: relative;
z-index: 10;
text-align: center;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.logo {
width: 120px;
height: 120px;
margin: 0 auto 2rem;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.logo img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 0 30px rgba(147, 51, 234, 0.5));
}
.title {
font-size: 3rem;
font-weight: 700;
color: white;
margin-bottom: 1rem;
letter-spacing: 0.1em;
}
.title-accent {
background: linear-gradient(135deg, #9333ea, #a855f7, #c084fc);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
font-size: 0.875rem;
color: #9ca3af;
margin-bottom: 2rem;
text-transform: uppercase;
letter-spacing: 0.2em;
}
.loader {
width: 200px;
height: 4px;
background: rgba(147, 51, 234, 0.2);
border-radius: 2px;
margin: 0 auto;
overflow: hidden;
position: relative;
}
.loader::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, #9333ea, #a855f7, #c084fc);
animation: loading 1.5s ease-in-out infinite;
box-shadow: 0 0 20px rgba(147, 51, 234, 0.6);
}
@keyframes loading {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
.loading-text {
margin-top: 1rem;
font-size: 0.75rem;
color: #6b7280;
animation: blink 1.5s ease-in-out infinite;
}
@keyframes blink {
0%, 100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
</style>
</head>
<body>
<div class="background">
<img src="https://assets.authbp.xyz/bg.png" alt="Background">
</div>
<div class="splash-container">
<div class="logo">
<img src="./icon.png" alt="Hytale Logo">
</div>
<h1 class="title">
HY<span class="title-accent">TALE</span>
</h1>
<p class="subtitle">FREE TO PLAY LAUNCHER</p>
<div class="loader"></div>
<p class="loading-text">Loading...</p>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
Type=Application Type=Application
Name=Hytale-F2P Name=Hytale-F2P
Comment=A modern, cross-platform launcher for Hytale with automatic updates and multi-client support Comment=A modern, cross-platform launcher for Hytale with automatic updates and multi-client support
Exec=/opt/Hytale-F2P/hytale-f2p-launcherv2 Exec=/opt/Hytale-F2P/hytale-f2p-launcher
Categories=Game; Categories=Game;
Icon=Hytale-F2P Icon=Hytale-F2P
Terminal=false Terminal=false

View File

@@ -1,20 +1,19 @@
# Maintainer: Terromur <terromuroz@proton.me> # Maintainer: Terromur <terromuroz@proton.me>
# Maintainer: Fazri Gading <fazrigading@gmail.com>
pkgname=Hytale-F2P-git pkgname=Hytale-F2P-git
_pkgname=Hytale-F2P _pkgname=Hytale-F2P
pkgver=2.0.2b.r120.gb05aeef pkgver=2.0.0.r47.gebcfdc4
pkgrel=1 pkgrel=1
pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support" pkgdesc="HyLauncher - unofficial Hytale Launcher for free to play gamers"
arch=('x86_64') arch=('x86_64')
url="https://github.com/amiayweb/Hytale-F2P" url="https://github.com/amiayweb/Hytale-F2P"
license=('custom') license=('custom')
makedepends=('npm' 'git' 'rpm-tools' 'libxcrypt-compat') makedepends=('npm')
source=("git+$url.git" "Hytale-F2P.desktop") source=("git+$url.git" "Hytale-F2P.desktop")
sha256sums=('SKIP' '8c78a6931fade2b0501122980dc238e042b9f6f0292b5ca74c391d7b3c1543c0') sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
pkgver() { pkgver() {
cd "$_pkgname" cd "$_pkgname"
printf "2.0.2b.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" printf "2.0.0.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
} }
build() { build() {
@@ -29,3 +28,4 @@ package() {
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop" install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
install -Dm644 "$_pkgname/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png" install -Dm644 "$_pkgname/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png"
} }

View File

@@ -1,19 +1,18 @@
# 🎮 Hytale F2P Launcher | Multiplayer Support [Windows, MacOS, Linux] # 🎮 Hytale F2P Launcher | Multiplayer Support
<div align="center"> <div align="center">
![Version](https://img.shields.io/badge/Version-2.0.2-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) ![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) ![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (all OS supported)** **A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (windows users & non-premium only)**
[![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers) [![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members) [![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members)
**If you find this project useful, please give it a star!** **If you find this project useful, please give it a star!**
🛑 **Found a problem? Open an issue! Im on Windows, so I cant test on macOS or Linux.** 🛑
🛑 **Found a problem? Join the Discord: https://discord.gg/gME8rUy3MB** 🛑
</div> </div>
@@ -37,7 +36,7 @@
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates - 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
- 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS - 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS
-**Java Management** - Automatic Java runtime detection and installation -**Java Management** - Automatic Java runtime detection and installation
- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows, macOS & Linux !) - 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows)
🛡️ **Advanced Features** 🛡️ **Advanced Features**
- 📁 **Custom Installation** - Choose your own installation directory - 📁 **Custom Installation** - Choose your own installation directory
@@ -64,7 +63,7 @@ See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https
#### macOS #### macOS
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section. See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
#### 🖥️ How to play online on F2P? #### 🖥️ How to create server (Windows Only)?
See [SERVER.md](SERVER.md) See [SERVER.md](SERVER.md)
@@ -76,50 +75,9 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
--- ---
## 📌 Versioning Policy
**⚠️ Important: Semantic Versioning Required**
This project follows **strict semantic versioning** with **numerical versions only**:
-**Valid**: `2.0.1`, `2.0.11`, `2.1.0`, `3.0.0`
-**Invalid**: `2.0.2b`, `2.0.2a`, `2.0.1-beta`, `v2.0.2b`
**Format**: `MAJOR.MINOR.PATCH` (e.g., `2.0.11`)
- **MAJOR**: Breaking changes
- **MINOR**: New features (backward compatible)
- **PATCH**: Bug fixes (backward compatible)
**Why?** The auto-update system requires semantic versioning for proper version comparison. Letter suffixes (like `2.0.2b`) are not supported and will cause update detection issues.
---
## 📋 Changelog ## 📋 Changelog
### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)* ### 🆕 v2.0.1 *(Latest)*
- 🌎 **Language Translation** — A big welcome for Spanish 🇪🇸 and Portuguese (Brazil) 🇧🇷 players! **Language setting can be found in the bottom part of Settings pane.**
- 💻 **Laptop/Hybrid GPU Performance Issue Fix** — Added automatic GPU detection system and options to choose which GPU will be used for the game, *specifically for Linux users*.
- 👨‍💻 **In-App Logging** — Reporting bugs and issues to `Github Issues` tab or `Open A Ticket` channel in our Discord Server has been made easier for players, no more finding logs file manually.
- 🛠️ **Repair Button** — Your game's broken? One button will fix them, go to Settings pane to Repair your game in one-click, **without losing any data**. If doing so did not fix your issue, please report it to us immediately!
- 🐛 **Fixed Bugs** — Fixed issue [#84](https://github.com/amiayweb/Hytale-F2P/issues/84) where mods disappearing when game starts in previous launcher (v2.0.2a).
### 🆕 v2.0.2a *(Minor Update)*
- 🧑‍🚀 **Profiles System** — Added proper profile management: create, switch, and delete profiles. Each profile now has its own **isolated mod list**.
- 🔒 **Mod Isolation** — Fixed ModManager so mods are **strictly scoped to the active profile**. Browsing and installing now only affects the selected profile.
- 🚨 **Critical Path Fix** — Resolved a macOS bug where mods were being saved to a Windows path (`~/AppData/Local`) instead of `~/Library/Application Support`. Mods now save to the **correct location** and load properly in-game.
- 🛡️ **Stability Improvements** — Added an **auto-sync step before every launch** to ensure the physical mods folder always matches the active profile.
- 🎨 **UI Enhancements** — Added a **profile selector dropdown** and a **profile management modal**.
### 🆕 v2.0.2
- 🎮 **Discord RPC Integration** - Added Discord Rich Presence with toggle in settings (enabled by default)
- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS
- 🎨 **Chat Improvements** - Simplified chat color system
- 🏆 **Badge System Expansion** - Added new FOUNDER UUID to the badge system
- 🔧 **Progress Bar Fix** - Resolved issue where download progress bar stayed active after game launch
- 🐛 **Bug Fixes**: General fixes
### 🔄 v2.0.1
- 📊 **Advanced Logging System** - Complete logging with timestamps, file rotation, and session tracking - 📊 **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 - 🔧 **Play Button Fix** - Resolved issue where play button could get stuck in "CHECKING..." state
- 💬 **Discord Integration** - Added closable Discord notification for community engagement - 💬 **Discord Integration** - Added closable Discord notification for community engagement
@@ -171,16 +129,14 @@ This project follows **strict semantic versioning** with **numerical versions on
### 🏆 Project Creator ### 🏆 Project Creator
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator* - [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
### 🌟 Contributors ### 🌟 Contributors
- [**@sanasol**](https://github.com/sanasol) - *Main Issues fixer | Multiplayer Patcher*
- [**@Terromur**](https://github.com/Terromur) - *Main Issues fixer | Beta tester*
- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester*
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer* - [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
- [**@crimera**](https://github.com/crimera) - *Issues fixer* - [**@crimera**](https://github.com/crimera) - *Issues fixer*
- [**@sanasol**](https://github.com/sanasol) - *Issues fixer*
- [**@Terromur**](https://github.com/Terromur) - *Issues fixer*
- [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer* - [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer*
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
--- ---
@@ -200,7 +156,10 @@ This project follows **strict semantic versioning** with **numerical versions on
<div align="center"> <div align="center">
**Need help?** Join us: https://discord.gg/gME8rUy3MB [![GitHub Issues](https://img.shields.io/badge/GitHub-Issues-red?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/issues)
[![Discussions](https://img.shields.io/badge/GitHub-Discussions-blue?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/discussions)
**Need help?** Open an [issue](https://github.com/amiayweb/Hytale-F2P/issues) or start a [discussion](https://github.com/amiayweb/Hytale-F2P/discussions)!
</div> </div>

479
SERVER.md
View File

@@ -1,444 +1,87 @@
# Hytale F2P Server Guide # Hytale F2P Server Setup Guide
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup. ## Server File Setup
DOWNLOAD SERVER FILES HERE: https://discord.gg/MEyWUxt77m **Download server file:**
```
https://files.hytalef2p.com/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:
--- ---
## Part 1: Playing with Friends (Online Play) ### PC with 4 GB RAM
*Best for small servers / testing*
The easiest way to play with friends - no manual server setup required! ```bash
java -Xms512M -Xmx2G -jar HytaleServer.jar --assets ..\Assets.zip
### How It Works
1. **Start the game** via F2P Launcher
2. **Click "Online Play"** in the main menu
3. **Share the invite code** with your friends
4. Friends enter your invite code to join
The game automatically handles networking using UPnP/STUN/NAT traversal.
### Network Requirements
For Online Play to work, you need:
- **UPnP enabled** on your router (most routers have this on by default)
- **Public IP address** from your ISP (not behind CGNAT)
### Common Issues
#### "NAT Type: Carrier-Grade NAT (CGNAT)" Warning
If you see this message:
```
Connected via UPnP
NAT Type: Carrier-Grade NAT (CGNAT)
Warning: Your network configuration may prevent other players from connecting.
``` ```
**What this means:** Your ISP doesn't give you a public IP address. Multiple customers share one public IP, which blocks incoming connections. - Uses up to **2 GB**
- Leaves enough memory for Windows
**Solutions:**
1. **Contact your ISP** - Request a public/static IP address (may cost extra)
2. **Use a VPN with port forwarding** - Services like Mullvad, PIA, or AirVPN offer this
3. **Use Radmin VPN or Playit.gg** - Create a virtual LAN with friends (see below)
4. **Have a friend with public IP host instead**
#### "UPnP Failed" or "Port Mapping Failed"
**Check your router:**
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`)
2. Find UPnP settings (often under "Advanced" or "NAT")
3. Enable UPnP if disabled
4. Restart your router
**If UPnP isn't available:**
- Manually forward **port 5520 UDP** to your computer's local IP
- See "Port Forwarding" section below
#### "Strict NAT" or "Symmetric NAT"
Some routers have restrictive NAT that blocks peer connections.
**Try:**
1. Enable "NAT Passthrough" or "NAT Filtering: Open" in router settings
2. Put your device in router's DMZ (temporary test only)
3. Use Radmin VPN as workaround
### Workarounds for NAT/CGNAT Issues
#### Option 1: playit.gg (Recommended)
Free tunneling service - only the host needs to install it:
1. **Download [playit.gg](https://playit.gg/)** and run it - Connect your account from the terminal (do not close it when playing on the server)
2. **Add a tunnel** - Select "UDP", tunnel description of "Hytale Server", port count `1`, and local port `5520`
3. **Start the tunnel** - You'll get a public address like `xx-xx.gl.at.ply.gg:5520`
4. **Share the address** - Friends connect directly using this address
Works with both Online Play and dedicated servers. No software needed for players joining.
#### Option 2: Radmin VPN
Creates a virtual LAN - all players need to install it:
1. **Download [Radmin VPN](https://www.radmin-vpn.com/)** - All players install it
2. **Create a network** - One person creates, others join with network name/password
3. **Host via Online Play** - Use your Radmin VPN IP instead
4. **Friends connect** - They'll see you on the virtual LAN
Both options bypass all NAT/CGNAT issues.
--- ---
## Part 2: Dedicated Server (Advanced) ### PC with 8 GB RAM
*Good for small communities*
For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
### Quick Start
#### Step 1: Get the Server JAR
The server scripts will automatically download the pre-patched server JAR if it's not present.
**Option A:** Let the scripts download automatically (requires `HYTALE_SERVER_URL` to be configured)
**Option B:** Manually place `HytaleServer.jar` (pre-patched for F2P) in the Server directory:
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
If you have a custom install path, the Server folder is inside your custom location under `HytaleF2P/release/package/game/latest/Server`.
#### Step 2: Run the Server
**Windows:**
```batch
cd scripts
run_server.bat
```
**macOS / Linux:**
```bash ```bash
cd scripts java -Xms1G -Xmx4G -jar HytaleServer.jar --assets ..\Assets.zip
./run_server.sh
``` ```
The scripts will: - Uses up to **4 GB**
1. Find your game installation automatically - Stable for most setups
2. Download the pre-patched server JAR if needed
3. Fetch session tokens from the auth server
4. Start the server
#### Step 3: Connect Players
Share your server IP address with players. They connect via the F2P Launcher's server browser or direct connect.
--- ---
## Network Setup (Dedicated Server) ### PC with 16 GB RAM
*Perfect for large or modded servers*
### Local Network (LAN) ```bash
java -Xms2G -Xmx8G -jar HytaleServer.jar --assets ..\Assets.zip
If all players are on the same network:
1. Find your local IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux)
2. Share this IP with players on your network
3. Default port is `5520`
### Port Forwarding (Internet Play)
To allow direct internet connections:
1. Forward **port 5520 (UDP)** in your router
2. Find your public IP at [whatismyip.com](https://whatismyip.com)
3. Share your public IP with players
**Windows Firewall:**
```powershell
# Run as Administrator
netsh advfirewall firewall add rule name="Hytale Server" dir=in action=allow protocol=UDP localport=5520
``` ```
- Uses up to **8 GB**
- Ideal for heavy worlds and plugins
--- ---
## Configuration ## Tips
### Environment Variables - `-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
Set these before running to customize your server:
| Variable | Default | Description |
|----------|---------|-------------|
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
| `HYTALE_AUTH_DOMAIN` | `sanasol.ws` | Auth server domain |
| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port |
| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) |
| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name |
| `HYTALE_GAME_PATH` | (auto-detected) | Override game location |
| `JVM_XMS` | `2G` | Minimum Java memory |
| `JVM_XMX` | `4G` | Maximum Java memory |
**Example (Windows):**
```batch
set HYTALE_SERVER_NAME=My Awesome Server
set JVM_XMX=8G
run_server.bat
```
**Example (Linux/macOS):**
```bash
HYTALE_SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
```
### Authentication Modes
| Mode | Description | Use Case |
|------|-------------|----------|
| `authenticated` | Players log in via F2P Launcher | Public servers |
| `unauthenticated` | No login required | LAN parties, testing |
| `singleplayer` | Local play only | Solo testing |
---
## RAM Allocation Guide
Adjust memory based on your system:
| System RAM | Players | JVM_XMS | JVM_XMX |
|------------|---------|---------|---------|
| 4 GB | 1-3 | `512M` | `2G` |
| 8 GB | 3-8 | `1G` | `4G` |
| 16 GB | 8-15 | `2G` | `8G` |
| 32 GB | 15+ | `4G` | `12G` |
**Example for large server:**
```bash
JVM_XMS=4G JVM_XMX=12G ./run_server.sh
```
**Tips:**
- `-Xms` = minimum RAM (allocated at startup)
- `-Xmx` = maximum RAM (upper limit)
- Never allocate all your system RAM - leave room for OS
- Start conservative and increase if needed
---
## Server Commands
Once running, use these commands in the console:
| Command | Description |
|---------|-------------|
| `help` | Show all commands |
| `stop` | Stop server gracefully |
| `save` | Force world save |
| `list` | List online players |
| `op <player>` | Give operator status |
| `deop <player>` | Remove operator status |
| `kick <player>` | Kick a player |
| `ban <player>` | Ban a player |
| `unban <player>` | Unban a player |
| `tp <player> <x> <y> <z>` | Teleport player |
---
## Command Line Options
Pass these when starting the server:
```bash
./run_server.sh [OPTIONS]
```
| Option | Description |
|--------|-------------|
| `--bind <ip:port>` | Server address (default: 0.0.0.0:5520) |
| `--auth-mode <mode>` | Authentication mode |
| `--universe <path>` | Path to world data |
| `--mods <path>` | Path to mods folder |
| `--backup` | Enable automatic backups |
| `--backup-dir <path>` | Backup directory |
| `--backup-frequency <mins>` | Backup interval |
| `--owner-name <name>` | Server owner username |
| `--allow-op` | Allow op commands |
| `--disable-sentry` | Disable crash reporting |
| `--help` | Show all options |
**Example:**
```bash
./run_server.sh --backup --backup-frequency 30 --allow-op
```
---
## File Structure
```
<game_path>/
├── Assets.zip # Game assets (required)
├── Client/ # Game client
└── Server/
├── HytaleServer.jar # Server executable (pre-patched)
├── HytaleServer.aot # AOT cache (faster startup)
├── universe/ # World data
│ ├── world/ # Default world
│ └── players/ # Player data
├── mods/ # Server mods (optional)
└── Licenses/ # License files
```
---
## Backups
### Automatic Backups
```bash
./run_server.sh --backup --backup-dir ./backups --backup-frequency 30
```
### Manual Backup
1. Use `save` command or stop the server
2. Copy the `universe/` folder
3. Store in a safe location
### Restore
1. Stop the server
2. Delete/rename current `universe/`
3. Copy backup to `universe/`
4. Restart server
---
## Troubleshooting
### "Java not found" or "Java version too old"
**Java 21 is REQUIRED** (the server uses class file version 65.0).
**Install Java 21:**
- **Windows:** `winget install EclipseAdoptium.Temurin.21.JDK`
- **macOS:** `brew install openjdk@21`
- **Ubuntu:** `sudo apt install openjdk-21-jdk`
- **Fedora:** `sudo dnf install java-21-openjdk`
- **Arch:** `sudo pacman -S jdk21-openjdk`
- **Download:** [adoptium.net/temurin/releases/?version=21](https://adoptium.net/temurin/releases/?version=21)
**macOS: Set Java 21 as default:**
```bash
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
export PATH="$JAVA_HOME/bin:$PATH"
```
Add these lines to `~/.zshrc` or `~/.bash_profile` to make permanent.
### "Game directory not found"
- Download game via F2P Launcher first
- Or set `HYTALE_GAME_PATH` environment variable
- Check custom install path in launcher settings
### "Assets.zip not found"
Game files incomplete. Re-download via the launcher.
### "Port already in use"
```bash
./run_server.sh --bind 0.0.0.0:5521
```
### "Out of memory"
Increase JVM_XMX:
```bash
JVM_XMX=6G ./run_server.sh
```
### Players can't connect
1. Server shows "Server Ready"?
2. Using F2P Launcher (not official)?
3. Port 5520 open in firewall?
4. Port forwarding configured (for internet)?
5. Try `--auth-mode unauthenticated` for testing
### "Authentication failed"
- Ensure players use F2P Launcher
- Auth server may be temporarily down
- Test with `--auth-mode unauthenticated`
---
## Docker Deployment (Advanced)
For production servers, use Docker:
```bash
docker run -d \
--name hytale-server \
-p 5520:5520/udp \
-v ./data:/data \
-e HYTALE_AUTH_DOMAIN=sanasol.ws \
-e HYTALE_SERVER_NAME="My Server" \
-e JVM_XMX=8G \
ghcr.io/hybrowse/hytale-server-docker:latest
```
See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for details.
---
## Server Settings Summary
### Minimal Setup
```bash
./run_server.sh
```
### Custom Memory
```bash
JVM_XMS=2G JVM_XMX=8G ./run_server.sh
```
### Custom Port
```bash
HYTALE_BIND=0.0.0.0:25565 ./run_server.sh
```
### LAN Party (No Auth)
```bash
./run_server.sh --auth-mode unauthenticated
```
### Full Custom Setup
```bash
HYTALE_SERVER_NAME="Epic Server" \
HYTALE_BIND=0.0.0.0:5520 \
JVM_XMS=2G \
JVM_XMX=8G \
./run_server.sh --backup --backup-frequency 15 --allow-op
```
---
## Getting Help
- Check server console logs for errors
- Test with `--auth-mode unauthenticated` first
- Ensure all players have F2P Launcher
- Join the community for support
---
## Credits
- Hytale F2P Project
- [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker)
- Auth Server: sanasol.ws

View File

@@ -1,379 +0,0 @@
const { autoUpdater } = require('electron-updater');
const { app } = require('electron');
const logger = require('./logger');
const fs = require('fs');
const path = require('path');
const os = require('os');
class AppUpdater {
constructor(mainWindow) {
this.mainWindow = mainWindow;
this.autoUpdateAvailable = true; // Track if auto-update is possible
this.updateAvailable = false; // Track if an update was detected
this.updateVersion = null; // Store the available update version
this.setupAutoUpdater();
}
setupAutoUpdater() {
// Enable dev mode for testing (reads dev-app-update.yml)
// Only enable in development, not in production builds
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
autoUpdater.forceDevUpdateConfig = true;
console.log('Dev update mode enabled - using dev-app-update.yml');
}
// Configure logger for electron-updater
// Create a compatible logger interface
autoUpdater.logger = {
info: (...args) => logger.info(...args),
warn: (...args) => logger.warn(...args),
error: (...args) => logger.error(...args),
debug: (...args) => logger.log(...args)
};
// Auto download updates
autoUpdater.autoDownload = true;
// Auto install on quit (after download)
autoUpdater.autoInstallOnAppQuit = true;
// Event handlers
autoUpdater.on('checking-for-update', () => {
console.log('Checking for updates...');
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-checking');
}
});
autoUpdater.on('update-available', (info) => {
console.log('Update available:', info.version);
const currentVersion = app.getVersion();
const newVersion = info.version;
// Only proceed if the new version is actually different from current
if (newVersion === currentVersion) {
console.log('Update version matches current version, ignoring update-available event');
return;
}
this.updateAvailable = true;
this.updateVersion = newVersion;
this.autoUpdateAvailable = true; // Reset flag when new update is available
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-available', {
version: newVersion,
newVersion: newVersion,
currentVersion: currentVersion,
releaseName: info.releaseName,
releaseNotes: info.releaseNotes
});
// Also send to the old popup handler for compatibility
this.mainWindow.webContents.send('show-update-popup', {
currentVersion: currentVersion,
newVersion: newVersion,
version: newVersion
});
}
});
autoUpdater.on('update-not-available', (info) => {
console.log('Update not available. Current version is latest.');
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-not-available', {
version: info.version
});
}
});
autoUpdater.on('error', (err) => {
console.error('Error in auto-updater:', err);
// Check if this is a network error (not critical, don't show UI)
const errorMessage = err.message?.toLowerCase() || '';
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
errorMessage.includes('network') ||
errorMessage.includes('connection') ||
errorMessage.includes('timeout') ||
errorMessage.includes('enotfound');
if (isNetworkError) {
console.warn('Network error in auto-updater - will retry later. Not showing error UI.');
return; // Don't show error UI for network issues
}
// Handle SHA512 checksum mismatch - this can happen during updates, just retry
const isChecksumError = err.code === 'ERR_CHECKSUM_MISMATCH' ||
errorMessage.includes('sha512') ||
errorMessage.includes('checksum') ||
errorMessage.includes('mismatch');
if (isChecksumError) {
console.warn('SHA512 checksum mismatch detected - clearing cache and will retry automatically. This is normal during updates.');
// Clear the update cache and let it re-download
this.clearUpdateCache();
// Don't show error UI - just log and let it retry automatically on next check
return;
}
// Determine if this is a critical error that prevents auto-update
const isCriticalError = this.isCriticalUpdateError(err);
if (isCriticalError) {
this.autoUpdateAvailable = false;
console.warn('Auto-update failed. Manual download required.');
}
// Handle missing metadata files (platform-specific builds)
if (err.code === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND') {
const platform = process.platform === 'darwin' ? 'macOS' :
process.platform === 'win32' ? 'Windows' : 'Linux';
const missingFile = process.platform === 'darwin' ? 'latest-mac.yml' :
process.platform === 'win32' ? 'latest.yml' : 'latest-linux.yml';
console.warn(`${platform} update metadata file (${missingFile}) not found in release.`);
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: `Update metadata file for ${platform} not found in release. Please download manually.`,
code: err.code,
requiresManualDownload: true,
updateVersion: this.updateVersion,
isMissingMetadata: true
});
}
return;
}
// Linux-specific: Handle installation permission errors
if (process.platform === 'linux') {
const errorMessage = err.message?.toLowerCase() || '';
const errorStack = err.stack?.toLowerCase() || '';
const isInstallError = errorMessage.includes('pkexec') ||
errorMessage.includes('gksudo') ||
errorMessage.includes('kdesudo') ||
errorMessage.includes('setuid root') ||
errorMessage.includes('exited with code 127') ||
errorStack.includes('pacmanupdater') ||
errorStack.includes('doinstall') ||
errorMessage.includes('installation failed');
if (isInstallError) {
console.warn('Linux installation error: Package installation requires root privileges. Manual installation required.');
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: 'Auto-installation requires root privileges. Please download and install the update manually.',
code: err.code || 'ERR_LINUX_INSTALL_PERMISSION',
isLinuxInstallError: true,
requiresManualDownload: true,
updateVersion: this.updateVersion
});
}
return;
}
}
// macOS-specific: Handle unsigned app errors gracefully
if (process.platform === 'darwin' && err.code === 2) {
console.warn('macOS update error: App may not be code-signed. Auto-update requires code signing.');
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: 'Auto-update requires code signing. Please download manually from GitHub.',
code: err.code,
isMacSigningError: true,
requiresManualDownload: true,
updateVersion: this.updateVersion
});
}
return;
}
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-error', {
message: err.message,
code: err.code,
requiresManualDownload: isCriticalError,
updateVersion: this.updateVersion
});
}
});
autoUpdater.on('download-progress', (progressObj) => {
const message = `Download speed: ${progressObj.bytesPerSecond} - Downloaded ${progressObj.percent}% (${progressObj.transferred}/${progressObj.total})`;
console.log(message);
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-download-progress', {
percent: progressObj.percent,
bytesPerSecond: progressObj.bytesPerSecond,
transferred: progressObj.transferred,
total: progressObj.total
});
}
});
autoUpdater.on('update-downloaded', (info) => {
console.log('Update downloaded:', info.version);
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send('update-downloaded', {
version: info.version,
releaseName: info.releaseName,
releaseNotes: info.releaseNotes
});
}
});
}
checkForUpdatesAndNotify() {
// Check for updates and notify if available
autoUpdater.checkForUpdatesAndNotify().catch(err => {
console.error('Failed to check for updates:', err);
// Network errors are not critical - just log and continue
const errorMessage = err.message?.toLowerCase() || '';
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
errorMessage.includes('network') ||
errorMessage.includes('connection') ||
errorMessage.includes('timeout') ||
errorMessage.includes('enotfound');
if (isNetworkError) {
console.warn('Network error checking for updates - will retry later. This is not critical.');
return; // Don't show error UI for network issues
}
const isCritical = this.isCriticalUpdateError(err);
if (this.mainWindow && !this.mainWindow.isDestroyed() && isCritical) {
this.mainWindow.webContents.send('update-error', {
message: err.message || 'Failed to check for updates',
code: err.code,
requiresManualDownload: true
});
}
});
}
checkForUpdates() {
// Manual check for updates (returns promise)
return autoUpdater.checkForUpdates().catch(err => {
console.error('Failed to check for updates:', err);
// Network errors are not critical - just return no update available
const errorMessage = err.message?.toLowerCase() || '';
const isNetworkError = errorMessage.includes('err_name_not_resolved') ||
errorMessage.includes('network') ||
errorMessage.includes('connection') ||
errorMessage.includes('timeout') ||
errorMessage.includes('enotfound');
if (isNetworkError) {
console.warn('Network error - update check unavailable');
return { updateInfo: null }; // Return empty result for network errors
}
const isCritical = this.isCriticalUpdateError(err);
if (isCritical) {
this.autoUpdateAvailable = false;
}
throw err;
});
}
quitAndInstall() {
// Quit and install the update
autoUpdater.quitAndInstall(false, true);
}
getUpdateInfo() {
return {
currentVersion: app.getVersion(),
updateAvailable: false
};
}
clearUpdateCache() {
try {
// Get the cache directory based on platform
const cacheDir = process.platform === 'darwin'
? path.join(os.homedir(), 'Library', 'Caches', `${app.getName()}-updater`)
: process.platform === 'win32'
? path.join(os.homedir(), 'AppData', 'Local', `${app.getName()}-updater`)
: path.join(os.homedir(), '.cache', `${app.getName()}-updater`);
if (fs.existsSync(cacheDir)) {
fs.rmSync(cacheDir, { recursive: true, force: true });
console.log('Update cache cleared successfully');
} else {
console.log('Update cache directory does not exist');
}
} catch (cacheError) {
console.warn('Could not clear update cache:', cacheError.message);
}
}
isCriticalUpdateError(err) {
// Check for errors that prevent auto-update
const errorMessage = err.message?.toLowerCase() || '';
const errorCode = err.code;
// Missing update metadata files (platform-specific)
if (errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND' ||
errorMessage.includes('cannot find latest') ||
errorMessage.includes('latest-linux.yml') ||
errorMessage.includes('latest-mac.yml') ||
errorMessage.includes('latest.yml')) {
return true;
}
// macOS code signing errors
if (process.platform === 'darwin' && (errorCode === 2 || errorMessage.includes('shipit'))) {
return true;
}
// Download failures
if (errorMessage.includes('download') && errorMessage.includes('fail')) {
return true;
}
// Network errors that prevent download (but we handle these separately as non-critical)
// Installation errors
if (errorMessage.includes('install') && errorMessage.includes('fail')) {
return true;
}
// Permission errors
if (errorMessage.includes('permission') || errorMessage.includes('access denied')) {
return true;
}
// Linux installation errors (pkexec, sudo issues)
if (process.platform === 'linux' && (
errorMessage.includes('pkexec') ||
errorMessage.includes('setuid root') ||
errorMessage.includes('exited with code 127') ||
errorMessage.includes('gksudo') ||
errorMessage.includes('kdesudo'))) {
return true;
}
// File system errors (but not "not found" for metadata files - handled above)
if (errorMessage.includes('enoent') || errorMessage.includes('cannot find')) {
// Only if it's not about metadata files
if (!errorMessage.includes('latest') && !errorMessage.includes('.yml')) {
return true;
}
}
// Generic critical error codes (but not checksum errors - those are handled separately)
if (errorCode && (errorCode >= 100 ||
errorCode === 'ERR_UPDATER_INVALID_RELEASE_FEED' ||
errorCode === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND')) {
// Don't treat checksum errors as critical - they're handled separately
if (errorCode === 'ERR_CHECKSUM_MISMATCH') {
return false;
}
return true;
}
return false;
}
}
module.exports = AppUpdater;

View File

@@ -2,7 +2,6 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const os = require('os'); const os = require('os');
// Default auth domain - can be overridden by env var or config // Default auth domain - can be overridden by env var or config
const DEFAULT_AUTH_DOMAIN = 'sanasol.ws'; const DEFAULT_AUTH_DOMAIN = 'sanasol.ws';
@@ -14,10 +13,6 @@ function getAuthDomain() {
} }
// Then check config file // Then check config file
const config = loadConfig(); const config = loadConfig();
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
// Allow profile to override auth domain if ever needed
// but for now stick to global or env
}
if (config.authDomain) { if (config.authDomain) {
return config.authDomain; return config.authDomain;
} }
@@ -115,16 +110,6 @@ function saveJavaPath(javaPath) {
function loadJavaPath() { function loadJavaPath() {
const config = loadConfig(); const config = loadConfig();
// Prefer Active Profile's Java Path
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
const profile = config.profiles[config.activeProfileId];
if (profile.javaPath && profile.javaPath.trim().length > 0) {
return profile.javaPath;
}
}
// Fallback to global setting
return config.javaPath || ''; return config.javaPath || '';
} }
@@ -138,48 +123,10 @@ function loadInstallPath() {
return config.installPath || ''; return config.installPath || '';
} }
function saveDiscordRPC(enabled) {
saveConfig({ discordRPC: !!enabled });
}
function loadDiscordRPC() {
const config = loadConfig();
return config.discordRPC !== undefined ? config.discordRPC : true;
}
function saveLanguage(language) {
saveConfig({ language: language || 'en' });
}
function loadLanguage() {
const config = loadConfig();
return config.language || 'en';
}
function saveCloseLauncherOnStart(enabled) {
saveConfig({ closeLauncherOnStart: !!enabled });
}
function loadCloseLauncherOnStart() {
const config = loadConfig();
return config.closeLauncherOnStart !== undefined ? config.closeLauncherOnStart : false;
}
function saveModsToConfig(mods) { function saveModsToConfig(mods) {
try { try {
const config = loadConfig(); let config = loadConfig();
// Config migration handles structure, but mod saves must go to the ACTIVE profile.
// Global installedMods is kept mainly for reference/migration.
// The profile is the source of truth for enabled mods.
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
config.profiles[config.activeProfileId].mods = mods;
} else {
// Fallback for legacy or no-profile state
config.installedMods = mods; config.installedMods = mods;
}
const configDir = path.dirname(CONFIG_FILE); const configDir = path.dirname(CONFIG_FILE);
if (!fs.existsSync(configDir)) { if (!fs.existsSync(configDir)) {
@@ -196,12 +143,6 @@ function saveModsToConfig(mods) {
function loadModsFromConfig() { function loadModsFromConfig() {
try { try {
const config = loadConfig(); const config = loadConfig();
// Prefer Active Profile
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
return config.profiles[config.activeProfileId].mods || [];
}
return config.installedMods || []; return config.installedMods || [];
} catch (error) { } catch (error) {
console.error('Error loading mods from config:', error); console.error('Error loading mods from config:', error);
@@ -231,79 +172,6 @@ function markAsLaunched() {
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() }); saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
} }
// UUID Management Functions
function getCurrentUuid() {
const username = loadUsername();
return getUuidForUser(username);
}
function getAllUuidMappings() {
const config = loadConfig();
return config.userUuids || {};
}
function setUuidForUser(username, uuid) {
const { v4: uuidv4, validate: validateUuid } = require('uuid');
// Validate UUID format
if (!validateUuid(uuid)) {
throw new Error('Invalid UUID format');
}
const config = loadConfig();
const userUuids = config.userUuids || {};
userUuids[username] = uuid;
saveConfig({ userUuids });
return uuid;
}
function generateNewUuid() {
const { v4: uuidv4 } = require('uuid');
return uuidv4();
}
function deleteUuidForUser(username) {
const config = loadConfig();
const userUuids = config.userUuids || {};
if (userUuids[username]) {
delete userUuids[username];
saveConfig({ userUuids });
return true;
}
return false;
}
function resetCurrentUserUuid() {
const username = loadUsername();
const { v4: uuidv4 } = require('uuid');
const newUuid = uuidv4();
return setUuidForUser(username, newUuid);
}
function saveChatColor(color) {
const config = loadConfig();
config.chatColor = color;
saveConfig(config);
}
function loadChatColor() {
const config = loadConfig();
return config.chatColor || '#3498db';
}
function saveGpuPreference(gpuPreference) {
saveConfig({ gpuPreference: gpuPreference || 'auto' });
}
function loadGpuPreference() {
const config = loadConfig();
return config.gpuPreference || 'auto';
}
module.exports = { module.exports = {
loadConfig, loadConfig,
saveConfig, saveConfig,
@@ -311,37 +179,19 @@ module.exports = {
loadUsername, loadUsername,
saveChatUsername, saveChatUsername,
loadChatUsername, loadChatUsername,
saveChatColor,
loadChatColor,
getUuidForUser, getUuidForUser,
saveJavaPath, saveJavaPath,
loadJavaPath, loadJavaPath,
saveInstallPath, saveInstallPath,
loadInstallPath, loadInstallPath,
saveDiscordRPC,
loadDiscordRPC,
saveLanguage,
loadLanguage,
saveModsToConfig, saveModsToConfig,
loadModsFromConfig, loadModsFromConfig,
isFirstLaunch, isFirstLaunch,
markAsLaunched, markAsLaunched,
CONFIG_FILE, CONFIG_FILE,
// Auth server exports // Auth domain config
getAuthServerUrl, DEFAULT_AUTH_DOMAIN,
getAuthDomain, getAuthDomain,
saveAuthDomain, getAuthServerUrl,
// UUID Management exports saveAuthDomain
getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid,
// GPU Preference exports
saveGpuPreference,
loadGpuPreference,
// Close Launcher export
saveCloseLauncherOnStart,
loadCloseLauncherOnStart
}; };

View File

@@ -152,8 +152,10 @@ async function getModsPath(customInstallPath = null) {
} }
if (!installPath) { if (!installPath) {
// Use the standard app directory logic which handles platforms correctly const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
installPath = getAppDir(); installPath = path.join(localAppData, 'HytaleF2P');
} else {
installPath = path.join(installPath, 'HytaleF2P');
} }
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest'); const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
@@ -162,18 +164,13 @@ async function getModsPath(customInstallPath = null) {
const modsPath = path.join(userDataPath, 'Mods'); const modsPath = path.join(userDataPath, 'Mods');
const disabledModsPath = path.join(userDataPath, 'DisabledMods'); const disabledModsPath = path.join(userDataPath, 'DisabledMods');
const profilesPath = path.join(userDataPath, 'Profiles');
if (!fs.existsSync(modsPath)) { if (!fs.existsSync(modsPath)) {
// Ensure the Mods directory exists
fs.mkdirSync(modsPath, { recursive: true }); fs.mkdirSync(modsPath, { recursive: true });
} }
if (!fs.existsSync(disabledModsPath)) { if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true }); fs.mkdirSync(disabledModsPath, { recursive: true });
} }
if (!fs.existsSync(profilesPath)) {
fs.mkdirSync(profilesPath, { recursive: true });
}
return modsPath; return modsPath;
} catch (error) { } catch (error) {
@@ -182,34 +179,6 @@ async function getModsPath(customInstallPath = null) {
} }
} }
function getProfilesDir(customInstallPath = null) {
try {
// get UserData path
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) installPath = getAppDir();
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
const userDataPath = findUserDataPath(gameLatest);
const profilesDir = path.join(userDataPath, 'Profiles');
if (!fs.existsSync(profilesDir)) {
fs.mkdirSync(profilesDir, { recursive: true });
}
return profilesDir;
} catch (err) {
console.error('Error getting profiles dir:', err);
return null;
}
}
module.exports = { module.exports = {
getAppDir, getAppDir,
getResolvedAppDir, getResolvedAppDir,
@@ -224,6 +193,5 @@ module.exports = {
findClientPath, findClientPath,
findUserDataPath, findUserDataPath,
findUserDataRecursive, findUserDataRecursive,
getModsPath, getModsPath
getProfilesDir
}; };

View File

@@ -7,33 +7,16 @@ const {
loadUsername, loadUsername,
saveChatUsername, saveChatUsername,
loadChatUsername, loadChatUsername,
saveChatColor,
loadChatColor,
saveJavaPath, saveJavaPath,
loadJavaPath, loadJavaPath,
saveInstallPath, saveInstallPath,
loadInstallPath, loadInstallPath,
saveDiscordRPC,
loadDiscordRPC,
saveLanguage,
loadLanguage,
saveCloseLauncherOnStart,
loadCloseLauncherOnStart,
saveModsToConfig, saveModsToConfig,
loadModsFromConfig, loadModsFromConfig,
getUuidForUser, getUuidForUser,
isFirstLaunch, isFirstLaunch,
markAsLaunched, markAsLaunched,
// UUID Management CONFIG_FILE
getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid,
// GPU Preference
saveGpuPreference,
loadGpuPreference
} = require('./core/config'); } = require('./core/config');
const { getResolvedAppDir, getModsPath } = require('./core/paths'); const { getResolvedAppDir, getModsPath } = require('./core/paths');
@@ -44,8 +27,7 @@ const {
installGame, installGame,
uninstallGame, uninstallGame,
updateGameFiles, updateGameFiles,
checkExistingGameInstallation, checkExistingGameInstallation
repairGame
} = require('./managers/gameManager'); } = require('./managers/gameManager');
const { const {
@@ -55,6 +37,8 @@ const {
const { getJavaDetection } = require('./managers/javaManager'); const { getJavaDetection } = require('./managers/javaManager');
const { checkAndInstallMultiClient } = require('./managers/multiClientManager');
const { const {
downloadAndReplaceHomePageUI, downloadAndReplaceHomePageUI,
findHomePageUIPath, findHomePageUIPath,
@@ -84,9 +68,6 @@ const {
handleFirstLaunchCheck handleFirstLaunchCheck
} = require('./services/firstLaunch'); } = require('./services/firstLaunch');
// Utils
const { detectGpu } = require('./utils/platformUtils');
// Re-export all functions to maintain backward compatibility // Re-export all functions to maintain backward compatibility
module.exports = { module.exports = {
// Game launch functions // Game launch functions
@@ -98,15 +79,12 @@ module.exports = {
isGameInstalled, isGameInstalled,
uninstallGame, uninstallGame,
updateGameFiles, updateGameFiles,
repairGame,
// User configuration functions // User configuration functions
saveUsername, saveUsername,
loadUsername, loadUsername,
saveChatUsername, saveChatUsername,
loadChatUsername, loadChatUsername,
saveChatColor,
loadChatColor,
getUuidForUser, getUuidForUser,
// Java configuration functions // Java configuration functions
@@ -118,23 +96,6 @@ module.exports = {
saveInstallPath, saveInstallPath,
loadInstallPath, loadInstallPath,
// Discord RPC functions
saveDiscordRPC,
loadDiscordRPC,
// Language functions
saveLanguage,
loadLanguage,
// Close Launcher functions
saveCloseLauncherOnStart,
loadCloseLauncherOnStart,
// GPU Preference functions
saveGpuPreference,
loadGpuPreference,
detectGpu,
// Version functions // Version functions
getInstalledClientVersion, getInstalledClientVersion,
getLatestClientVersion, getLatestClientVersion,
@@ -145,13 +106,8 @@ module.exports = {
// Player ID functions // Player ID functions
getOrCreatePlayerId, getOrCreatePlayerId,
// UUID Management functions // Multi-client functions
getCurrentUuid, checkAndInstallMultiClient,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid,
// Mod management functions // Mod management functions
getModsPath, getModsPath,

View File

@@ -6,12 +6,11 @@ const { promisify } = require('util');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const { getResolvedAppDir, findClientPath } = require('../core/paths'); const { getResolvedAppDir, findClientPath } = require('../core/paths');
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils'); const { setupWaylandEnvironment } = require('../utils/platformUtils');
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain } = require('../core/config'); const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain } = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager'); const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager'); const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager'); const { updateGameFiles } = require('./gameManager');
const { syncModsForCurrentProfile } = require('./modManager');
// Client patcher for custom auth server (sanasol.ws) // Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null; let clientPatcher = null;
@@ -101,7 +100,7 @@ function generateLocalTokens(uuid, name) {
}; };
} }
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') { async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
const customAppDir = getResolvedAppDir(installPathOverride); const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest'); const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest'); const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
@@ -193,9 +192,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
const serverDir = path.join(gameLatest, 'Server'); const serverDir = path.join(gameLatest, 'Server');
const signPath = async (targetPath, deep = false) => { const signPath = async (targetPath, deep = false) => {
await execAsync(`xattr -cr "${targetPath}"`).catch(() => { }); await execAsync(`xattr -cr "${targetPath}"`).catch(() => {});
const deepFlag = deep ? '--deep ' : ''; const deepFlag = deep ? '--deep ' : '';
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => { }); await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => {});
}; };
if (fs.existsSync(appBundle)) { if (fs.existsSync(appBundle)) {
@@ -217,8 +216,8 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
} }
if (fs.existsSync(serverDir)) { if (fs.existsSync(serverDir)) {
await execAsync(`xattr -cr "${serverDir}"`).catch(() => { }); await execAsync(`xattr -cr "${serverDir}"`).catch(() => {});
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => { }); await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => {});
console.log('Signed server binaries (after patching)'); console.log('Signed server binaries (after patching)');
} }
@@ -261,18 +260,6 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
if (progressCallback) { if (progressCallback) {
progressCallback('Starting game...', null, null, null, null); progressCallback('Starting game...', null, null, null, null);
} }
// Ensure mods are synced for the active profile before launching
try {
console.log('Syncing mods for active profile before launch...');
if (progressCallback) progressCallback('Syncing mods...', null, null, null, null);
await syncModsForCurrentProfile();
} catch (syncError) {
console.error('Failed to sync mods before launch:', syncError);
// Continue anyway? Or fail?
// Warn user but continue might be safer to avoid blocking play if sync is just glitchy
}
console.log('Starting game...'); console.log('Starting game...');
console.log(`Command: "${clientPath}" ${args.join(' ')}`); console.log(`Command: "${clientPath}" ${args.join(' ')}`);
@@ -281,9 +268,6 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
const waylandEnv = setupWaylandEnvironment(); const waylandEnv = setupWaylandEnvironment();
Object.assign(env, waylandEnv); Object.assign(env, waylandEnv);
const gpuEnv = setupGpuEnvironment(gpuPreference);
Object.assign(env, gpuEnv);
try { try {
let spawnOptions = { let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
@@ -355,7 +339,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
} }
} }
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') { async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
try { try {
if (progressCallback) { if (progressCallback) {
progressCallback('Checking for updates...', 0, null, null, null); progressCallback('Checking for updates...', 0, null, null, null);
@@ -406,7 +390,7 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
progressCallback('Launching game...', 80, null, null, null); progressCallback('Launching game...', 80, null, null, null);
} }
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference); return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride);
} catch (error) { } catch (error) {
console.error('Error in version check and launch:', error); console.error('Error in version check and launch:', error);
if (progressCallback) { if (progressCallback) {

View File

@@ -6,6 +6,7 @@ const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile } = require('../utils/fileManager'); const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager'); const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
const { installButler } = require('./butlerManager'); const { installButler } = require('./butlerManager');
const { checkAndInstallMultiClient } = require('./multiClientManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config'); const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config');
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager'); const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
@@ -164,6 +165,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
fs.renameSync(tempUpdateDir, gameDir); 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); const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
console.log('HomePage.ui update result after update:', homeUIResult); console.log('HomePage.ui update result after update:', homeUIResult);
@@ -314,6 +318,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir); const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir); await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
const multiResult = await checkAndInstallMultiClient(customGameDir, progressCallback);
console.log('Multiplayer check result:', multiResult);
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback); const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
console.log('HomePage.ui update result after installation:', homeUIResult); console.log('HomePage.ui update result after installation:', homeUIResult);
@@ -327,7 +334,8 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
return { return {
success: true, success: true,
installed: true installed: true,
multiClient: multiResult
}; };
} }
@@ -387,102 +395,6 @@ function checkExistingGameInstallation() {
} }
} }
async function repairGame(progressCallback) {
const appDir = getResolvedAppDir();
const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
// Check if game exists
if (!fs.existsSync(gameDir)) {
throw new Error('Game directory not found. Cannot repair.');
}
// Locate UserData
const userDataPath = findUserDataRecursive(gameDir);
let userDataBackup = null;
if (progressCallback) {
progressCallback('Backing up user data...', 10, null, null, null);
}
// Backup UserData
if (userDataPath && fs.existsSync(userDataPath)) {
userDataBackup = path.join(appDir, 'UserData_backup_repair_' + Date.now());
console.log(`Backing up UserData during repair from ${userDataPath} to ${userDataBackup}`);
// Copy function
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child)));
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataPath, userDataBackup);
}
if (progressCallback) {
progressCallback('Removing old game files...', 30, null, null, null);
}
// Delete Game and Cache Directory
console.log('Removing corrupted game files...');
fs.rmSync(gameDir, { recursive: true, force: true });
const cacheDir = path.join(appDir, 'cache');
if (fs.existsSync(cacheDir)) {
console.log('Clearing cache directory...');
fs.rmSync(cacheDir, { recursive: true, force: true });
}
console.log('Reinstalling game files...');
// Passing null/undefined for overrides to use defaults/saved configs
// installGame calls progressCallback internally
await installGame('Player', progressCallback);
// Restore UserData
if (userDataBackup && fs.existsSync(userDataBackup)) {
if (progressCallback) {
progressCallback('Restoring user data...', 90, null, null, null);
}
// installGame creates: path.join(customGameDir, 'Client', 'UserData')
const newGameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
const newUserDataPath = path.join(newGameDir, 'Client', 'UserData');
if (!fs.existsSync(newUserDataPath)) {
fs.mkdirSync(newUserDataPath, { 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 });
fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child)));
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataBackup, newUserDataPath);
// Cleanup Backup
console.log('Cleaning up repair backup...');
fs.rmSync(userDataBackup, { recursive: true, force: true });
}
if (progressCallback) {
progressCallback('Repair completed successfully!', 100, null, null, null);
}
return { success: true, repaired: true };
}
module.exports = { module.exports = {
downloadPWR, downloadPWR,
applyPWR, applyPWR,
@@ -490,9 +402,5 @@ module.exports = {
isGameInstalled, isGameInstalled,
installGame, installGame,
uninstallGame, uninstallGame,
isGameInstalled, checkExistingGameInstallation
installGame,
uninstallGame,
checkExistingGameInstallation,
repairGame
}; };

View File

@@ -2,29 +2,8 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const axios = require('axios'); const axios = require('axios');
const { getModsPath, getProfilesDir } = require('../core/paths'); const { getModsPath } = require('../core/paths');
const { saveModsToConfig, loadModsFromConfig } = require('../core/config'); const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
const profileManager = require('./profileManager');
const API_KEY = process.env.CURSEFORGE_API_KEY;
/**
* Get the physical mods path for a specific profile.
* Each profile now has its own 'mods' folder.
*/
function getProfileModsPath(profileId) {
const profilesDir = getProfilesDir();
if (!profilesDir) return null;
const profileDir = path.join(profilesDir, profileId);
const modsDir = path.join(profileDir, 'mods');
if (!fs.existsSync(modsDir)) {
fs.mkdirSync(modsDir, { recursive: true });
}
return modsDir;
}
function generateModId(filename) { function generateModId(filename) {
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8); return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
@@ -47,56 +26,77 @@ function extractVersion(filename) {
return versionMatch ? versionMatch[1] : null; return versionMatch ? versionMatch[1] : null;
} }
// Helper to get mods from active profile
function getProfileMods() {
const profile = profileManager.getActiveProfile();
return profile ? (profile.mods || []) : [];
}
async function loadInstalledMods(modsPath) { async function loadInstalledMods(modsPath) {
try { try {
// Sync first to ensure we detect any manually added mods and paths are correct const configMods = loadModsFromConfig();
await syncModsForCurrentProfile(); const modsMap = new Map();
const activeProfile = profileManager.getActiveProfile(); configMods.forEach(mod => {
if (!activeProfile) return []; modsMap.set(mod.fileName, mod);
const profileMods = activeProfile.mods || [];
// Use profile-specific paths
const profileModsPath = getProfileModsPath(activeProfile.id);
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
if (!fs.existsSync(profileModsPath)) fs.mkdirSync(profileModsPath, { recursive: true });
if (!fs.existsSync(profileDisabledModsPath)) fs.mkdirSync(profileDisabledModsPath, { recursive: true });
const validMods = [];
for (const modConfig of profileMods) {
// Check if file exists in either location
const inEnabled = fs.existsSync(path.join(profileModsPath, modConfig.fileName));
const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, modConfig.fileName));
if (inEnabled || inDisabled) {
validMods.push({
...modConfig,
// Set filePath based on physical location
filePath: inEnabled ? path.join(profileModsPath, modConfig.fileName) : path.join(profileDisabledModsPath, modConfig.fileName),
enabled: modConfig.enabled !== false // Default true
});
} else {
console.warn(`[ModManager] Mod ${modConfig.fileName} listed in profile but not found on disk.`);
// Include it so user can see it's missing or remove it
validMods.push({
...modConfig,
filePath: null,
missing: true,
enabled: modConfig.enabled !== false
}); });
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);
}
} }
} }
return validMods; 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) { } catch (error) {
console.error('Error loading installed mods:', error); console.error('Error loading installed mods:', error);
return []; return [];
@@ -105,11 +105,7 @@ async function loadInstalledMods(modsPath) {
async function downloadMod(modInfo) { async function downloadMod(modInfo) {
try { try {
const activeProfile = profileManager.getActiveProfile(); const modsPath = await getModsPath();
if (!activeProfile) throw new Error('No active profile to save mod to');
const modsPath = getProfileModsPath(activeProfile.id);
if (!modsPath) throw new Error('Could not determine profile mods path');
if (!modInfo.downloadUrl && !modInfo.fileId) { if (!modInfo.downloadUrl && !modInfo.fileId) {
throw new Error('No download URL or file ID provided'); throw new Error('No download URL or file ID provided');
@@ -118,9 +114,9 @@ async function downloadMod(modInfo) {
let downloadUrl = modInfo.downloadUrl; let downloadUrl = modInfo.downloadUrl;
if (!downloadUrl && modInfo.fileId && modInfo.modId) { if (!downloadUrl && modInfo.fileId && modInfo.modId) {
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId || modInfo.curseForgeId}/files/${modInfo.fileId || modInfo.curseForgeFileId}`, { const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
headers: { headers: {
'x-api-key': modInfo.apiKey || API_KEY, 'x-api-key': modInfo.apiKey,
'Accept': 'application/json' 'Accept': 'application/json'
} }
}); });
@@ -146,7 +142,7 @@ async function downloadMod(modInfo) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
writer.on('finish', () => { writer.on('finish', () => {
// Update Active Profile const configMods = loadModsFromConfig();
const newMod = { const newMod = {
id: modInfo.id || generateModId(fileName), id: modInfo.id || generateModId(fileName),
name: modInfo.name || extractModName(fileName), name: modInfo.name || extractModName(fileName),
@@ -161,8 +157,8 @@ async function downloadMod(modInfo) {
curseForgeFileId: modInfo.fileId curseForgeFileId: modInfo.fileId
}; };
const updatedMods = [...(activeProfile.mods || []), newMod]; configMods.push(newMod);
profileManager.updateProfile(activeProfile.id, { mods: updatedMods }); saveModsToConfig(configMods);
resolve({ resolve({
success: true, success: true,
@@ -185,41 +181,35 @@ async function downloadMod(modInfo) {
async function uninstallMod(modId, modsPath) { async function uninstallMod(modId, modsPath) {
try { try {
const activeProfile = profileManager.getActiveProfile(); const configMods = loadModsFromConfig();
if (!activeProfile) throw new Error('No active profile'); const mod = configMods.find(m => m.id === modId);
const profileMods = activeProfile.mods || [];
const mod = profileMods.find(m => m.id === modId);
if (!mod) { if (!mod) {
throw new Error('Mod not found in profile'); throw new Error('Mod not found in config');
} }
// Use profile paths const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
const profileModsPath = getProfileModsPath(activeProfile.id); const enabledPath = path.join(modsPath, mod.fileName);
const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
const enabledPath = path.join(profileModsPath, mod.fileName);
const disabledPath = path.join(disabledModsPath, mod.fileName); const disabledPath = path.join(disabledModsPath, mod.fileName);
let fileRemoved = false; let fileRemoved = false;
// Try to remove file from both locations to be safe
if (fs.existsSync(enabledPath)) { if (fs.existsSync(enabledPath)) {
fs.unlinkSync(enabledPath); fs.unlinkSync(enabledPath);
fileRemoved = true; fileRemoved = true;
} console.log('Removed mod from Mods folder:', enabledPath);
if (fs.existsSync(disabledPath)) { } else if (fs.existsSync(disabledPath)) {
try { fs.unlinkSync(disabledPath); fileRemoved = true; } catch (e) { } fs.unlinkSync(disabledPath);
fileRemoved = true;
console.log('Removed mod from DisabledMods folder:', disabledPath);
} }
if (!fileRemoved) { if (!fileRemoved) {
console.warn('Mod file not found on filesystem, removing from profile anyway'); console.warn('Mod file not found on filesystem, removing from config anyway');
} }
const updatedMods = profileMods.filter(m => m.id !== modId); const updatedMods = configMods.filter(m => m.id !== modId);
profileManager.updateProfile(activeProfile.id, { mods: updatedMods }); saveModsToConfig(updatedMods);
console.log('Mod removed from config.json');
console.log('Mod removed from profile');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@@ -233,45 +223,36 @@ async function uninstallMod(modId, modsPath) {
async function toggleMod(modId, modsPath) { async function toggleMod(modId, modsPath) {
try { try {
const activeProfile = profileManager.getActiveProfile(); const mods = await loadInstalledMods(modsPath);
if (!activeProfile) throw new Error('No active profile'); const mod = mods.find(m => m.id === modId);
const profileMods = activeProfile.mods || []; if (!mod) {
const modIndex = profileMods.findIndex(m => m.id === modId); throw new Error('Mod not found');
if (modIndex === -1) {
throw new Error('Mod not found in profile');
} }
const mod = profileMods[modIndex]; const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
const newEnabled = !mod.enabled; // Toggle if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
// Update Profile First
const updatedMods = [...profileMods];
updatedMods[modIndex] = { ...mod, enabled: newEnabled };
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
// Move file between Profile/Mods and Profile/DisabledMods
const profileModsPath = getProfileModsPath(activeProfile.id);
const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
const currentPath = mod.enabled ? path.join(profileModsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
const targetDir = newEnabled ? profileModsPath : disabledModsPath;
const targetPath = path.join(targetDir, mod.fileName);
if (fs.existsSync(currentPath)) {
fs.renameSync(currentPath, targetPath);
} else {
// Fallback: check if it's already in target?
if (fs.existsSync(targetPath)) {
console.log(`[ModManager] Mod ${mod.fileName} is already in the correct state`);
} else {
// Try finding it
const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(profileModsPath, mod.fileName);
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
} }
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 }; return { success: true, enabled: newEnabled };
@@ -284,200 +265,11 @@ async function toggleMod(modId, modsPath) {
} }
} }
async function syncModsForCurrentProfile() {
try {
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) {
console.warn('No active profile found during mod sync');
return;
}
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`);
// 1. Resolve Paths
// globalModsPath is the one the game uses (symlink target)
const globalModsPath = await getModsPath();
// profileModsPath is the real storage for this profile
const profileModsPath = getProfileModsPath(activeProfile.id);
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
if (!fs.existsSync(profileDisabledModsPath)) {
fs.mkdirSync(profileDisabledModsPath, { recursive: true });
}
// 2. Symlink / Migration Logic
let needsLink = false;
if (fs.existsSync(globalModsPath)) {
const stats = fs.lstatSync(globalModsPath);
if (stats.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(globalModsPath);
// Normalize paths for comparison
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
console.log(`[ModManager] Updating symlink from ${linkTarget} to ${profileModsPath}`);
fs.unlinkSync(globalModsPath);
needsLink = true;
}
} else if (stats.isDirectory()) {
// MIGRATION: It's a real directory. Move contents to profile.
console.log('[ModManager] Migrating global mods folder to profile folder...');
const files = fs.readdirSync(globalModsPath);
for (const file of files) {
const src = path.join(globalModsPath, file);
const dest = path.join(profileModsPath, file);
// Only move if dest doesn't exist to avoid overwriting
if (!fs.existsSync(dest)) {
fs.renameSync(src, dest);
}
}
// Also migrate DisabledMods if it exists globally
const globalDisabledPath = path.join(path.dirname(globalModsPath), 'DisabledMods');
if (fs.existsSync(globalDisabledPath) && fs.lstatSync(globalDisabledPath).isDirectory()) {
const dFiles = fs.readdirSync(globalDisabledPath);
for (const file of dFiles) {
const src = path.join(globalDisabledPath, file);
const dest = path.join(profileDisabledModsPath, file);
if (!fs.existsSync(dest)) {
fs.renameSync(src, dest);
}
}
// We can remove global DisabledMods now, as it's not used by game
try { fs.rmSync(globalDisabledPath, { recursive: true, force: true }); } catch(e) {}
}
// Remove the directory so we can link it
try {
fs.rmSync(globalModsPath, { recursive: true, force: true });
needsLink = true;
} catch (e) {
console.error('Failed to remove global mods dir:', e);
// Throw error to stop.
throw new Error('Failed to migrate mods directory. Please clear ' + globalModsPath);
}
}
} else {
needsLink = true;
}
if (needsLink) {
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
try {
// 'junction' is key for Windows without admin
fs.symlinkSync(profileModsPath, globalModsPath, 'junction');
} catch (err) {
// If we can't create the symlink, try creating the directory first
console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.');
console.error(err.message);
// Fallback: create a real directory so the game still works
if (!fs.existsSync(globalModsPath)) {
fs.mkdirSync(globalModsPath, { recursive: true });
}
}
}
// 3. Auto-Repair (Download missing mods)
const profileModsSnapshot = activeProfile.mods || [];
for (const mod of profileModsSnapshot) {
if (mod.enabled && !mod.manual) {
const inEnabled = fs.existsSync(path.join(profileModsPath, mod.fileName));
const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, mod.fileName));
if (!inEnabled && !inDisabled) {
if (mod.curseForgeId && (mod.curseForgeFileId || mod.fileId)) {
console.log(`[ModManager] Auto-repair: Re-downloading missing mod "${mod.name}"...`);
try {
await downloadMod({
...mod,
modId: mod.curseForgeId,
fileId: mod.curseForgeFileId || mod.fileId,
apiKey: API_KEY
});
} catch (err) {
console.error(`[ModManager] Auto-repair failed for "${mod.name}": ${err.message}`);
}
}
}
}
}
// 4. Auto-Import (Detect manual drops in the profile folder)
const enabledFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
let profileMods = activeProfile.mods || [];
let profileUpdated = false;
// Anything in this folder belongs to this profile.
for (const file of enabledFiles) {
const isKnown = profileMods.some(m => m.fileName === file);
if (!isKnown) {
console.log(`[ModManager] Auto-importing manual mod: ${file}`);
const newMod = {
id: generateModId(file),
name: extractModName(file),
version: 'Unknown',
description: 'Manually installed',
author: 'Local',
enabled: true,
fileName: file,
fileSize: 0,
dateInstalled: new Date().toISOString(),
manual: true
};
profileMods.push(newMod);
profileUpdated = true;
}
}
if (profileUpdated) {
profileManager.updateProfile(activeProfile.id, { mods: profileMods });
const updatedProfile = profileManager.getActiveProfile();
profileMods = updatedProfile ? (updatedProfile.mods || []) : profileMods;
}
// 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods)
// Note: Since Global/Mods IS Profile/Mods (via symlink), moving out of Profile/Mods disables it for the game.
const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
for (const fileName of allFiles) {
const modConfig = profileMods.find(m => m.fileName === fileName);
const shouldBeEnabled = modConfig && modConfig.enabled !== false;
const currentPath = enabledFiles.includes(fileName) ? path.join(profileModsPath, fileName) : path.join(profileDisabledModsPath, fileName);
const targetDir = shouldBeEnabled ? profileModsPath : profileDisabledModsPath;
const targetPath = path.join(targetDir, fileName);
if (path.dirname(currentPath) !== targetDir) {
console.log(`[Mod Sync] Moving ${fileName} to ${shouldBeEnabled ? 'Enabled' : 'Disabled'}`);
try {
fs.renameSync(currentPath, targetPath);
} catch (err) {
console.error(`Failed to move ${fileName}: ${err.message}`);
}
}
}
return { success: true };
} catch (error) {
console.error('[ModManager] Error syncing mods:', error);
return { success: false, error: error.message };
}
}
module.exports = { module.exports = {
loadInstalledMods, loadInstalledMods,
downloadMod, downloadMod,
uninstallMod, uninstallMod,
toggleMod, toggleMod,
syncModsForCurrentProfile,
generateModId, generateModId,
extractModName, extractModName,
extractVersion 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

@@ -1,206 +0,0 @@
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const {
loadConfig,
saveConfig,
getModsPath
} = require('../core/config');
// Lazy-load modManager to avoid circular deps, or keep imports structured.
// For now, access mod paths directly or use helper helpers.
class ProfileManager {
constructor() {
this.initialized = false;
}
init() {
if (this.initialized) return;
const config = loadConfig();
// Migration: specific check to see if we have profiles yet
if (!config.profiles || Object.keys(config.profiles).length === 0) {
this.migrateLegacyConfig(config);
}
this.initialized = true;
console.log('[ProfileManager] Initialized');
}
migrateLegacyConfig(config) {
console.log('[ProfileManager] Migrating legacy config to profile system...');
// Create a default profile with current settings
const defaultProfileId = 'default';
const defaultProfile = {
id: defaultProfileId,
name: 'Default',
created: new Date().toISOString(),
lastUsed: new Date().toISOString(),
// settings specific to this profile
// If global settings existed, we copy them here
mods: config.installedMods || [], // Legacy mods are now part of default profile
javaPath: config.javaPath || '',
gameOptions: {
minMemory: '1G',
maxMemory: '4G',
args: []
}
};
const updates = {
profiles: {
[defaultProfileId]: defaultProfile
},
activeProfileId: defaultProfileId,
// Mods are currently treated as files on disk.
// The profile's `mods` array is the source of truth for enabled/known mods per profile.
};
saveConfig(updates);
console.log('[ProfileManager] Migration complete. Created Default profile.');
}
createProfile(name) {
if (!name || typeof name !== 'string') {
throw new Error('Invalid profile name');
}
const config = loadConfig();
const id = uuidv4();
const newProfile = {
id,
name: name.trim(),
created: new Date().toISOString(),
lastUsed: null,
mods: [], // Start with no mods enabled
javaPath: '',
gameOptions: {
minMemory: '1G',
maxMemory: '4G',
args: []
}
};
const profiles = config.profiles || {};
profiles[id] = newProfile;
saveConfig({ profiles });
console.log(`[ProfileManager] Created new profile: "${name}" (${id})`);
return newProfile;
}
getProfiles() {
const config = loadConfig();
return Object.values(config.profiles || {});
}
getProfile(id) {
const config = loadConfig();
return (config.profiles && config.profiles[id]) || null;
}
getActiveProfile() {
const config = loadConfig();
const activeId = config.activeProfileId;
if (!activeId || !config.profiles || !config.profiles[activeId]) {
// Fallback if something is corrupted
return this.getProfiles()[0] || null;
}
return config.profiles[activeId];
}
async activateProfile(id) {
const config = loadConfig();
if (!config.profiles || !config.profiles[id]) {
throw new Error(`Profile not found: ${id}`);
}
if (config.activeProfileId === id) {
console.log(`[ProfileManager] Profile ${id} is already active.`);
return config.profiles[id];
}
console.log(`[ProfileManager] Switching to profile: ${config.profiles[id].name} (${id})`);
// 1. Update config first
config.profiles[id].lastUsed = new Date().toISOString();
saveConfig({
activeProfileId: id,
profiles: config.profiles
});
// 2. Trigger Mod Sync
// We need to require this here to ensure it uses the *newly saved* active profile ID
const { syncModsForCurrentProfile } = require('./modManager');
await syncModsForCurrentProfile();
return config.profiles[id];
}
deleteProfile(id) {
const config = loadConfig();
const profiles = config.profiles || {};
if (!profiles[id]) {
throw new Error('Profile not found');
}
if (config.activeProfileId === id) {
throw new Error('Cannot delete the active profile');
}
// Don't allow deleting the last profile
if (Object.keys(profiles).length <= 1) {
throw new Error('Cannot delete the only remaining profile');
}
delete profiles[id];
saveConfig({ profiles });
console.log(`[ProfileManager] Deleted profile: ${id}`);
return true;
}
updateProfile(id, updates) {
const config = loadConfig();
const profiles = config.profiles || {};
if (!profiles[id]) {
throw new Error('Profile not found');
}
// Safety checks on updates
const allowedFields = ['name', 'javaPath', 'gameOptions', 'mods'];
const sanitizedUpdates = {};
Object.keys(updates).forEach(key => {
if (allowedFields.includes(key)) {
sanitizedUpdates[key] = updates[key];
}
});
profiles[id] = { ...profiles[id], ...sanitizedUpdates };
saveConfig({ profiles });
console.log(`[ProfileManager] Updated profile: ${id}`);
// If we updated mods for the *active* profile, we might need to sync immediately
if (config.activeProfileId === id && updates.mods) {
// Optionally trigger sync?
// Sync is usually triggered when toggling a single mod.
// For bulk updates, the caller should decide when to sync.
}
return profiles[id];
}
}
module.exports = new ProfileManager();

View File

@@ -10,7 +10,7 @@ async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
progressCallback('Downloading HomePage.ui...', null, null, null, null); progressCallback('Downloading HomePage.ui...', null, null, null, null);
} }
const homeUIUrl = 'https://files.hytalef2p.com/api/HomeUI'; const homeUIUrl = 'http://3.10.208.30:3002/api/HomeUI';
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui'); const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
await downloadFile(homeUIUrl, tempHomePath); await downloadFile(homeUIUrl, tempHomePath);
@@ -63,7 +63,7 @@ async function downloadAndReplaceLogo(gameDir, progressCallback) {
progressCallback('Downloading Logo@2x.png...', null, null, null, null); progressCallback('Downloading Logo@2x.png...', null, null, null, null);
} }
const logoUrl = 'https://files.hytalef2p.com/api/Logo'; const logoUrl = 'http://3.10.208.30:3002/api/Logo';
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png'); const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
await downloadFile(logoUrl, tempLogoPath); await downloadFile(logoUrl, tempLogoPath);

View File

@@ -3,7 +3,7 @@ const axios = require('axios');
async function getLatestClientVersion() { async function getLatestClientVersion() {
try { try {
console.log('Fetching latest client version from API...'); console.log('Fetching latest client version from API...');
const response = await axios.get('https://files.hytalef2p.com/api/version_client', { const response = await axios.get('http://3.10.208.30:3002/api/version_client', {
timeout: 5000, timeout: 5000,
headers: { headers: {
'User-Agent': 'Hytale-F2P-Launcher' 'User-Agent': 'Hytale-F2P-Launcher'
@@ -28,7 +28,7 @@ async function getLatestClientVersion() {
async function getInstalledClientVersion() { async function getInstalledClientVersion() {
try { try {
console.log('Fetching installed client version from API...'); console.log('Fetching installed client version from API...');
const response = await axios.get('https://files.hytalef2p.com/api/clientCheck', { const response = await axios.get('http://3.10.208.30:3002/api/clientCheck', {
timeout: 5000, timeout: 5000,
headers: { headers: {
'User-Agent': 'Hytale-F2P-Launcher' 'User-Agent': 'Hytale-F2P-Launcher'
@@ -50,7 +50,33 @@ async function getInstalledClientVersion() {
} }
} }
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 = { module.exports = {
getLatestClientVersion, getLatestClientVersion,
getInstalledClientVersion getInstalledClientVersion,
getMultiClientVersion
}; };

View File

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

View File

@@ -6,18 +6,23 @@ const AdmZip = require('adm-zip');
// Domain configuration // Domain configuration
const ORIGINAL_DOMAIN = 'hytale.com'; const ORIGINAL_DOMAIN = 'hytale.com';
// Get target domain from config or environment
function getTargetDomain() { function getTargetDomain() {
// Check environment variable first
if (process.env.HYTALE_AUTH_DOMAIN) { if (process.env.HYTALE_AUTH_DOMAIN) {
return process.env.HYTALE_AUTH_DOMAIN; return process.env.HYTALE_AUTH_DOMAIN;
} }
// Try to load from config
try { try {
const { getAuthDomain } = require('../core/config'); const { getAuthDomain } = require('../core/config');
return getAuthDomain(); return getAuthDomain();
} catch (e) { } catch (e) {
// Config not available, use default
return 'sanasol.ws'; return 'sanasol.ws';
} }
} }
// Default domain - must be exactly 10 characters (same as hytale.com)
const DEFAULT_NEW_DOMAIN = 'sanasol.ws'; const DEFAULT_NEW_DOMAIN = 'sanasol.ws';
/** /**
@@ -34,6 +39,7 @@ class ClientPatcher {
*/ */
getNewDomain() { getNewDomain() {
const domain = getTargetDomain(); const domain = getTargetDomain();
// Validate domain length matches original
if (domain.length !== ORIGINAL_DOMAIN.length) { if (domain.length !== ORIGINAL_DOMAIN.length) {
console.warn(`Warning: Domain "${domain}" length (${domain.length}) doesn't match original "${ORIGINAL_DOMAIN}" (${ORIGINAL_DOMAIN.length})`); console.warn(`Warning: Domain "${domain}" length (${domain.length}) doesn't match original "${ORIGINAL_DOMAIN}" (${ORIGINAL_DOMAIN.length})`);
console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`); console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`);
@@ -86,9 +92,11 @@ class ClientPatcher {
const oldUtf8 = this.stringToUtf8(oldDomain); const oldUtf8 = this.stringToUtf8(oldDomain);
const newUtf8 = this.stringToUtf8(newDomain); const newUtf8 = this.stringToUtf8(newDomain);
// Find all occurrences of the domain
const positions = this.findAllOccurrences(result, oldUtf8); const positions = this.findAllOccurrences(result, oldUtf8);
for (const pos of positions) { for (const pos of positions) {
// Replace the domain
newUtf8.copy(result, pos); newUtf8.copy(result, pos);
count++; count++;
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`); console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
@@ -107,29 +115,39 @@ class ClientPatcher {
let count = 0; let count = 0;
const result = Buffer.from(data); const result = Buffer.from(data);
// Get UTF-16LE bytes without the last character
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1)); const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1)); const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
const oldLastChar = this.stringToUtf16LE(oldDomain.slice(-1)); const oldLastChar = this.stringToUtf16LE(oldDomain.slice(-1));
const newLastChar = this.stringToUtf16LE(newDomain.slice(-1)); const newLastChar = this.stringToUtf16LE(newDomain.slice(-1));
// ASCII code of last characters
const oldLastCharByte = oldDomain.charCodeAt(oldDomain.length - 1); const oldLastCharByte = oldDomain.charCodeAt(oldDomain.length - 1);
const newLastCharByte = newDomain.charCodeAt(newDomain.length - 1); const newLastCharByte = newDomain.charCodeAt(newDomain.length - 1);
// Find all occurrences of the domain without the last character
const positions = this.findAllOccurrences(result, oldUtf16NoLast); const positions = this.findAllOccurrences(result, oldUtf16NoLast);
for (const pos of positions) { for (const pos of positions) {
// Check if we have the last character following
const lastCharPos = pos + oldUtf16NoLast.length; const lastCharPos = pos + oldUtf16NoLast.length;
if (lastCharPos + 1 > result.length) continue; if (lastCharPos + 1 > result.length) continue;
// Read the byte at last char position
const lastCharFirstByte = result[lastCharPos]; const lastCharFirstByte = result[lastCharPos];
// Check if first byte matches the last character of old domain
if (lastCharFirstByte === oldLastCharByte) { if (lastCharFirstByte === oldLastCharByte) {
// Replace all but last character
newUtf16NoLast.copy(result, pos); newUtf16NoLast.copy(result, pos);
// Replace just the first byte of the last character (preserve metadata byte if any)
result[lastCharPos] = newLastCharByte; result[lastCharPos] = newLastCharByte;
// If there's a proper null byte (standard UTF-16LE), also check/preserve it
if (lastCharPos + 1 < result.length) { if (lastCharPos + 1 < result.length) {
const secondByte = result[lastCharPos + 1]; const secondByte = result[lastCharPos + 1];
// Log what type of occurrence this is
if (secondByte === 0x00) { if (secondByte === 0x00) {
console.log(` Patched UTF-16LE occurrence at offset 0x${pos.toString(16)}`); console.log(` Patched UTF-16LE occurrence at offset 0x${pos.toString(16)}`);
} else { } else {
@@ -143,29 +161,6 @@ class ClientPatcher {
return { buffer: result, count }; return { buffer: result, count };
} }
/**
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
*/
patchDiscordUrl(data) {
let count = 0;
const result = Buffer.from(data);
const oldUrl = '.gg/hytale';
const newUrl = '.gg/MHkEjepMQ7';
const oldUtf16 = this.stringToUtf16LE(oldUrl);
const newUtf16 = this.stringToUtf16LE(newUrl);
const positions = this.findAllOccurrences(result, oldUtf16);
for (const pos of positions) {
newUtf16.copy(result, pos);
count++;
}
return { buffer: result, count };
}
/** /**
* Check if the client binary has already been patched * Check if the client binary has already been patched
*/ */
@@ -175,10 +170,12 @@ class ClientPatcher {
if (fs.existsSync(patchFlagFile)) { if (fs.existsSync(patchFlagFile)) {
try { try {
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8')); const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
// Check if patched with same target domain
if (flagData.targetDomain === newDomain) { if (flagData.targetDomain === newDomain) {
return true; return true;
} }
} catch (e) { } catch (e) {
// Flag file corrupted, will re-patch
} }
} }
return false; return false;
@@ -243,12 +240,14 @@ class ClientPatcher {
console.log(`Target: ${clientPath}`); console.log(`Target: ${clientPath}`);
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`); console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
// Check if file exists
if (!fs.existsSync(clientPath)) { if (!fs.existsSync(clientPath)) {
const error = `Client binary not found: ${clientPath}`; const error = `Client binary not found: ${clientPath}`;
console.error(error); console.error(error);
return { success: false, error }; return { success: false, error };
} }
// Check if already patched
if (this.isPatchedAlready(clientPath)) { if (this.isPatchedAlready(clientPath)) {
console.log(`Client already patched for ${newDomain}, skipping`); console.log(`Client already patched for ${newDomain}, skipping`);
if (progressCallback) { if (progressCallback) {
@@ -261,6 +260,7 @@ class ClientPatcher {
progressCallback('Preparing to patch client...', 10); progressCallback('Preparing to patch client...', 10);
} }
// Create backup
console.log('Creating backup...'); console.log('Creating backup...');
this.backupClient(clientPath); this.backupClient(clientPath);
@@ -268,6 +268,7 @@ class ClientPatcher {
progressCallback('Reading client binary...', 20); progressCallback('Reading client binary...', 20);
} }
// Read the binary
console.log('Reading client binary...'); console.log('Reading client binary...');
const data = fs.readFileSync(clientPath); const data = fs.readFileSync(clientPath);
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`); console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
@@ -276,34 +277,34 @@ class ClientPatcher {
progressCallback('Patching domain references...', 50); progressCallback('Patching domain references...', 50);
} }
// Perform the domain replacement
console.log('Patching domain references...'); console.log('Patching domain references...');
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain); const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
console.log('Patching Discord URLs...'); if (count === 0) {
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData); console.log('No occurrences of hytale.com found - binary may already be modified or has different format');
return { success: true, patchCount: 0, warning: 'No domain occurrences found' };
if (count === 0 && discordCount === 0) {
console.log('No occurrences found - binary may already be modified or has different format');
return { success: true, patchCount: 0, warning: 'No occurrences found' };
} }
if (progressCallback) { if (progressCallback) {
progressCallback('Writing patched binary...', 80); progressCallback('Writing patched binary...', 80);
} }
// Write the patched binary
console.log('Writing patched binary...'); console.log('Writing patched binary...');
fs.writeFileSync(clientPath, finalData); fs.writeFileSync(clientPath, patchedData);
// Mark as patched
this.markAsPatched(clientPath); this.markAsPatched(clientPath);
if (progressCallback) { if (progressCallback) {
progressCallback('Patching complete', 100); progressCallback('Patching complete', 100);
} }
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`); console.log(`Successfully patched ${count} occurrences`);
console.log('=== Patching Complete ==='); console.log('=== Patching Complete ===');
return { success: true, patchCount: count + discordCount }; return { success: true, patchCount: count };
} }
/** /**
@@ -319,12 +320,14 @@ class ClientPatcher {
console.log(`Target: ${serverPath}`); console.log(`Target: ${serverPath}`);
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`); console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
// Check if file exists
if (!fs.existsSync(serverPath)) { if (!fs.existsSync(serverPath)) {
const error = `Server JAR not found: ${serverPath}`; const error = `Server JAR not found: ${serverPath}`;
console.error(error); console.error(error);
return { success: false, error }; return { success: false, error };
} }
// Check if already patched
if (this.isPatchedAlready(serverPath)) { if (this.isPatchedAlready(serverPath)) {
console.log(`Server already patched for ${newDomain}, skipping`); console.log(`Server already patched for ${newDomain}, skipping`);
if (progressCallback) { if (progressCallback) {
@@ -337,6 +340,7 @@ class ClientPatcher {
progressCallback('Preparing to patch server...', 10); progressCallback('Preparing to patch server...', 10);
} }
// Create backup
console.log('Creating backup...'); console.log('Creating backup...');
this.backupClient(serverPath); this.backupClient(serverPath);
@@ -344,6 +348,7 @@ class ClientPatcher {
progressCallback('Extracting server JAR...', 20); progressCallback('Extracting server JAR...', 20);
} }
// Open the JAR file as a ZIP
console.log('Opening server JAR...'); console.log('Opening server JAR...');
const zip = new AdmZip(serverPath); const zip = new AdmZip(serverPath);
const entries = zip.getEntries(); const entries = zip.getEntries();
@@ -353,17 +358,20 @@ class ClientPatcher {
progressCallback('Patching class files...', 40); progressCallback('Patching class files...', 40);
} }
// Patch each entry that might contain domain strings
let totalCount = 0; let totalCount = 0;
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN); const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
const newUtf8 = this.stringToUtf8(newDomain); const newUtf8 = this.stringToUtf8(newDomain);
for (const entry of entries) { for (const entry of entries) {
// Only patch class files and certain resource files
const name = entry.entryName; const name = entry.entryName;
if (name.endsWith('.class') || name.endsWith('.properties') || if (name.endsWith('.class') || name.endsWith('.properties') ||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) { name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
const data = entry.getData(); const data = entry.getData();
// Check if this entry contains the domain
if (data.includes(oldUtf8)) { if (data.includes(oldUtf8)) {
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, newDomain); const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, newDomain);
if (count > 0) { if (count > 0) {
@@ -384,9 +392,11 @@ class ClientPatcher {
progressCallback('Writing patched JAR...', 80); progressCallback('Writing patched JAR...', 80);
} }
// Write the patched JAR
console.log('Writing patched JAR...'); console.log('Writing patched JAR...');
zip.writeZip(serverPath); zip.writeZip(serverPath);
// Mark as patched
this.markAsPatched(serverPath); this.markAsPatched(serverPath);
if (progressCallback) { if (progressCallback) {
@@ -406,6 +416,7 @@ class ClientPatcher {
const candidates = []; const candidates = [];
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// macOS: Check both app bundle and direct binary
candidates.push(path.join(gameDir, 'Client', 'Hytale.app', 'Contents', 'MacOS', 'HytaleClient')); candidates.push(path.join(gameDir, 'Client', 'Hytale.app', 'Contents', 'MacOS', 'HytaleClient'));
candidates.push(path.join(gameDir, 'Client', 'HytaleClient')); candidates.push(path.join(gameDir, 'Client', 'HytaleClient'));
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
@@ -422,7 +433,9 @@ class ClientPatcher {
return null; return null;
} }
/**
* Find the server JAR path
*/
findServerPath(gameDir) { findServerPath(gameDir) {
const candidates = [ const candidates = [
path.join(gameDir, 'Server', 'HytaleServer.jar'), path.join(gameDir, 'Server', 'HytaleServer.jar'),
@@ -449,6 +462,7 @@ class ClientPatcher {
success: true success: true
}; };
// Patch client
const clientPath = this.findClientPath(gameDir); const clientPath = this.findClientPath(gameDir);
if (clientPath) { if (clientPath) {
if (progressCallback) { if (progressCallback) {
@@ -464,6 +478,7 @@ class ClientPatcher {
results.client = { success: false, error: 'Client binary not found' }; results.client = { success: false, error: 'Client binary not found' };
} }
// Patch server
const serverPath = this.findServerPath(gameDir); const serverPath = this.findServerPath(gameDir);
if (serverPath) { if (serverPath) {
if (progressCallback) { if (progressCallback) {
@@ -479,6 +494,7 @@ class ClientPatcher {
results.server = { success: false, error: 'Server JAR not found' }; results.server = { success: false, error: 'Server JAR not found' };
} }
// Calculate overall success
results.success = (results.client && results.client.success) || (results.server && results.server.success); results.success = (results.client && results.client.success) || (results.server && results.server.success);
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched); results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0); results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
@@ -491,4 +507,5 @@ class ClientPatcher {
} }
} }
// Export singleton instance
module.exports = new ClientPatcher(); module.exports = new ClientPatcher();

View File

@@ -2,151 +2,42 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const axios = require('axios'); const axios = require('axios');
async function downloadFile(url, dest, progressCallback, maxRetries = 3) { async function downloadFile(url, dest, progressCallback) {
let lastError = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
if (attempt > 0 && progressCallback) {
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null);
await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif
}
const response = await axios({ const response = await axios({
method: 'GET', method: 'GET',
url: url, url: url,
responseType: 'stream', responseType: 'stream',
timeout: 60000, // 60 secondes timeout
headers: { 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', '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': '*/*',
'Accept-Language': 'en-US,en;q=0.9', 'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://launcher.hytale.com/', 'Referer': 'https://launcher.hytale.com/'
'Connection': 'keep-alive' }
},
// Configuration Axios pour la robustesse réseau
validateStatus: function (status) {
return status >= 200 && status < 300;
},
// Retry configuration
maxRedirects: 5,
// Network resilience
family: 4 // Force IPv4
}); });
const totalSize = parseInt(response.headers['content-length'], 10); const totalSize = parseInt(response.headers['content-length'], 10);
let downloaded = 0; let downloaded = 0;
let lastProgressTime = Date.now();
const startTime = Date.now(); const startTime = Date.now();
// Nettoyer le fichier de destination s'il existe
if (fs.existsSync(dest)) {
fs.unlinkSync(dest);
}
const writer = fs.createWriteStream(dest); const writer = fs.createWriteStream(dest);
let downloadStalled = false;
let stalledTimeout = null;
response.data.on('data', (chunk) => { response.data.on('data', (chunk) => {
downloaded += chunk.length; downloaded += chunk.length;
const now = Date.now(); if (progressCallback && totalSize > 0) {
// Reset stalled timer on data received
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
// Set new stalled timer (30 seconds without data = stalled)
stalledTimeout = setTimeout(() => {
downloadStalled = true;
writer.destroy();
response.data.destroy();
}, 30000);
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100)); const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
const elapsed = (now - startTime) / 1000; const elapsed = (Date.now() - startTime) / 1000;
const speed = elapsed > 0 ? downloaded / elapsed : 0; const speed = elapsed > 0 ? downloaded / elapsed : 0;
progressCallback(null, percent, speed, downloaded, totalSize); progressCallback(null, percent, speed, downloaded, totalSize);
lastProgressTime = now;
} }
}); });
response.data.on('error', (error) => {
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
console.error(`Stream error on attempt ${attempt + 1}:`, error.code || error.message);
writer.destroy();
});
response.data.pipe(writer); response.data.pipe(writer);
await new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
writer.on('finish', () => { writer.on('finish', resolve);
if (stalledTimeout) { writer.on('error', reject);
clearTimeout(stalledTimeout); response.data.on('error', reject);
}
if (!downloadStalled) {
console.log(`Download completed successfully on attempt ${attempt + 1}`);
resolve();
} else {
reject(new Error('Download stalled'));
}
}); });
writer.on('error', (error) => {
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
reject(error);
});
response.data.on('error', (error) => {
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
reject(error);
});
});
// Si on arrive ici, le téléchargement a réussi
return;
} catch (error) {
lastError = error;
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
// Nettoyer le fichier partiel en cas d'erreur
if (fs.existsSync(dest)) {
try {
fs.unlinkSync(dest);
} catch (cleanupError) {
console.warn('Could not cleanup partial file:', cleanupError.message);
}
}
// Vérifier si c'est une erreur réseau que l'on peut retry
const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO'];
const isRetryable = retryableErrors.includes(error.code) ||
error.message.includes('timeout') ||
error.message.includes('stalled') ||
(error.response && error.response.status >= 500);
if (!isRetryable || attempt === maxRetries - 1) {
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
break;
}
console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`);
}
}
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
} }
function findHomePageUIPath(gameLatest) { function findHomePageUIPath(gameLatest) {

View File

@@ -1,5 +1,4 @@
const { execSync } = require('child_process'); const { execSync } = require('child_process');
const fs = require('fs');
function getOS() { function getOS() {
if (process.platform === 'win32') return 'windows'; if (process.platform === 'win32') return 'windows';
@@ -66,182 +65,9 @@ function setupWaylandEnvironment() {
return envVars; return envVars;
} }
function detectGpu() {
const platform = getOS();
try {
if (platform === 'linux') {
return detectGpuLinux();
} else if (platform === 'windows') {
return detectGpuWindows();
} else if (platform === 'darwin') {
return detectGpuMac();
} else {
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
}
} catch (error) {
console.warn('GPU detection failed, falling back to integrated:', error.message);
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
}
}
function detectGpuLinux() {
const output = execSync('lspci -nn | grep \'VGA\\|3D\'', { encoding: 'utf8' });
const lines = output.split('\n').filter(line => line.trim());
let integratedName = null;
let dedicatedName = null;
let hasNvidia = false;
let hasAmd = false;
for (const line of lines) {
if (line.includes('VGA') || line.includes('3D')) {
const match = line.match(/\[([^\]]+)\]/g);
let modelName = null;
if (match && match.length >= 2) {
modelName = match[1].slice(1, -1);
}
if (line.includes('10de:') || line.toLowerCase().includes('nvidia')) {
hasNvidia = true;
dedicatedName = "NVIDIA " + modelName || 'NVIDIA GPU';
console.log('Detected NVIDIA GPU:', dedicatedName);
} else if (line.includes('1002:') || line.toLowerCase().includes('amd') || line.toLowerCase().includes('radeon')) {
hasAmd = true;
dedicatedName = "AMD " + modelName || 'AMD GPU';
console.log('Detected AMD GPU:', dedicatedName);
} else if (line.includes('8086:') || line.toLowerCase().includes('intel')) {
integratedName = "Intel " + modelName || 'Intel GPU';
console.log('Detected Intel GPU:', integratedName);
}
}
}
if (hasNvidia) {
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Intel GPU', dedicatedName };
} else if (hasAmd) {
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Intel GPU', dedicatedName };
} else {
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Intel GPU', dedicatedName: null };
}
}
function detectGpuWindows() {
const output = execSync('wmic path win32_VideoController get name', { encoding: 'utf8' });
const lines = output.split('\n').map(line => line.trim()).filter(line => line && line !== 'Name');
let integratedName = null;
let dedicatedName = null;
let hasNvidia = false;
let hasAmd = false;
for (const line of lines) {
const lowerLine = line.toLowerCase();
if (lowerLine.includes('nvidia')) {
hasNvidia = true;
dedicatedName = line;
console.log('Detected NVIDIA GPU:', dedicatedName);
} else if (lowerLine.includes('amd') || lowerLine.includes('radeon')) {
hasAmd = true;
dedicatedName = line;
console.log('Detected AMD GPU:', dedicatedName);
} else if (lowerLine.includes('intel')) {
integratedName = line;
console.log('Detected Intel GPU:', integratedName);
}
}
if (hasNvidia) {
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Intel GPU', dedicatedName };
} else if (hasAmd) {
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Intel GPU', dedicatedName };
} else {
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Intel GPU', dedicatedName: null };
}
}
function detectGpuMac() {
const output = execSync('system_profiler SPDisplaysDataType', { encoding: 'utf8' });
const lines = output.split('\n');
let integratedName = null;
let dedicatedName = null;
let hasNvidia = false;
let hasAmd = false;
for (const line of lines) {
if (line.includes('Chipset Model:')) {
const gpuName = line.split('Chipset Model:')[1].trim();
const lowerGpu = gpuName.toLowerCase();
if (lowerGpu.includes('nvidia')) {
hasNvidia = true;
dedicatedName = gpuName;
console.log('Detected NVIDIA GPU:', dedicatedName);
} else if (lowerGpu.includes('amd') || lowerGpu.includes('radeon')) {
hasAmd = true;
dedicatedName = gpuName;
console.log('Detected AMD GPU:', dedicatedName);
} else if (lowerGpu.includes('intel') || lowerGpu.includes('iris') || lowerGpu.includes('uhd')) {
integratedName = gpuName;
console.log('Detected Intel GPU:', integratedName);
} else if (!dedicatedName && !integratedName) {
// Fallback for Apple Silicon or other
integratedName = gpuName;
}
}
}
if (hasNvidia) {
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Integrated GPU', dedicatedName };
} else if (hasAmd) {
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Integrated GPU', dedicatedName };
} else {
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Integrated GPU', dedicatedName: null };
}
}
function setupGpuEnvironment(gpuPreference) {
if (process.platform !== 'linux') {
return {};
}
let finalPreference = gpuPreference;
let detected = detectGpu();
if (gpuPreference === 'auto') {
finalPreference = detected.mode;
console.log(`Auto-detected GPU: ${detected.vendor} (${detected.mode})`);
}
console.log('Preferred GPU set to:', finalPreference);
const envVars = {};
if (finalPreference === 'dedicated') {
if (detected.vendor === 'nvidia') {
envVars.__NV_PRIME_RENDER_OFFLOAD = '1';
envVars.__GLX_VENDOR_LIBRARY_NAME = 'nvidia';
const nvidiaEglFile = '/usr/share/glvnd/egl_vendor.d/10_nvidia.json';
if (fs.existsSync(nvidiaEglFile)) {
envVars.__EGL_VENDOR_LIBRARY_FILENAMES = nvidiaEglFile;
} else {
console.warn('NVIDIA EGL vendor library file not found, not setting __EGL_VENDOR_LIBRARY_FILENAMES');
}
} else {
envVars.DRI_PRIME = '1';
}
console.log('GPU environment variables:', envVars);
} else {
console.log('Using integrated GPU, no environment variables set');
}
return envVars;
}
module.exports = { module.exports = {
getOS, getOS,
getArch, getArch,
isWaylandSession, isWaylandSession,
setupWaylandEnvironment, setupWaylandEnvironment
detectGpu,
setupGpuEnvironment
}; };

View File

@@ -1,3 +0,0 @@
provider: github
owner: amiayweb # Change to your own GitHub username
repo: Hytale-F2P

View File

@@ -1,284 +0,0 @@
# Auto-Updates System
This document explains how the automatic update system works in the Hytale F2P Launcher.
## Overview
The launcher uses [electron-updater](https://www.electron.build/auto-update) to automatically check for, download, and install updates. When a new version is available, users are notified and the update is downloaded in the background.
## How It Works
### 1. Update Checking
- **Automatic Check**: The app automatically checks for updates 3 seconds after startup
- **Manual Check**: Users can manually check for updates through the UI
- **Update Source**: Updates are fetched from GitHub Releases
### 2. Update Process
1. **Check for Updates**: The app queries GitHub Releases for a newer version
2. **Notify User**: If an update is available, the user is notified via the UI
3. **Download**: The update is automatically downloaded in the background
4. **Progress Tracking**: Download progress is shown to the user
5. **Install**: When the download completes, the user can choose to install immediately or wait until the app restarts
### 3. Installation
- Updates are installed when the app quits (if `autoInstallOnAppQuit` is enabled)
- Users can also manually trigger installation through the UI
- The app will restart automatically after installation
## Version Detection & Comparison
### Current Version Source
The app's current version is read from `package.json`:
```json
{
"version": "2.0.2b"
}
```
This version is embedded into the built application and is accessible via `app.getVersion()` in Electron. When the app is built, electron-builder also creates an internal `app-update.yml` file in the app's resources that contains this version information.
### How Version Detection Works
1. **Current Version**: The app knows its own version from `package.json`, which is:
- Read at build time
- Embedded in the application binary
- Stored in the app's metadata
2. **Fetching Latest Version**: When checking for updates, electron-updater:
- Queries the GitHub Releases API: `https://api.github.com/repos/amiayweb/Hytale-F2P/releases/latest`
- Or reads the update metadata file: `https://github.com/amiayweb/Hytale-F2P/releases/download/latest/latest.yml` (or `latest-mac.yml` for macOS)
- The metadata file contains:
```yaml
version: 2.0.3
releaseDate: '2024-01-15T10:30:00.000Z'
path: Hytale-F2P-Launcher-2.0.3-x64.exe
sha512: ...
```
3. **Version Comparison**: electron-updater uses semantic versioning comparison:
- Compares the **current version** (from `package.json`) with the **latest version** (from GitHub Releases)
- Uses semantic versioning rules: `major.minor.patch` (e.g., `2.0.2` vs `2.0.3`)
- An update is available if the remote version is **greater than** the current version
- Examples:
- Current: `2.0.2` → Remote: `2.0.3` ✅ Update available
- Current: `2.0.2` → Remote: `2.0.2` ❌ No update (same version)
- Current: `2.0.3` → Remote: `2.0.2` ❌ No update (current is newer)
- Current: `2.0.2b` → Remote: `2.0.3` ✅ Update available (prerelease tags are handled)
4. **Version Format Handling**:
- **Semantic versions** (e.g., `1.0.0`, `2.1.3`) are compared numerically
- **Prerelease versions** (e.g., `2.0.2b`, `2.0.2-beta`) are compared with special handling
- **Non-semantic versions** may cause issues - it's recommended to use semantic versioning
### Update Metadata Files
When you build and publish a release, electron-builder generates platform-specific metadata files:
**Windows/Linux** (`latest.yml`):
```yaml
version: 2.0.3
files:
- url: Hytale-F2P-Launcher-2.0.3-x64.exe
sha512: abc123...
size: 12345678
path: Hytale-F2P-Launcher-2.0.3-x64.exe
sha512: abc123...
releaseDate: '2024-01-15T10:30:00.000Z'
```
**macOS** (`latest-mac.yml`):
```yaml
version: 2.0.3
files:
- url: Hytale-F2P-Launcher-2.0.3-arm64-mac.zip
sha512: def456...
size: 23456789
path: Hytale-F2P-Launcher-2.0.3-arm64-mac.zip
sha512: def456...
releaseDate: '2024-01-15T10:30:00.000Z'
```
These files are:
- Automatically generated during build
- Uploaded to GitHub Releases
- Fetched by electron-updater to check for updates
- Used to determine if an update is available and what to download
### The Check Process in Detail
When `appUpdater.checkForUpdatesAndNotify()` is called:
1. **Read Current Version**: Gets version from `app.getVersion()` (which reads from `package.json`)
2. **Fetch Update Info**:
- Makes HTTP request to GitHub Releases API or reads `latest.yml`
- Gets the version number from the metadata
3. **Compare Versions**:
- Uses semantic versioning comparison (e.g., `semver.gt(remoteVersion, currentVersion)`)
- If remote > current: update available
- If remote <= current: no update
4. **Emit Events**:
- `update-available` if newer version found
- `update-not-available` if already up to date
5. **Download if Available**: If `autoDownload` is enabled, starts downloading automatically
### Example Flow
```
App Version: 2.0.2 (from package.json)
Check GitHub Releases API
Latest Release: 2.0.3
Compare: 2.0.3 > 2.0.2? YES
Emit: 'update-available' event
Download update automatically
Emit: 'update-downloaded' event
User can install on next restart
```
## Components
### AppUpdater Class (`backend/appUpdater.js`)
The main class that handles all update operations:
- **`checkForUpdatesAndNotify()`**: Checks for updates and shows a system notification if available
- **`checkForUpdates()`**: Manually checks for updates (returns a promise)
- **`quitAndInstall()`**: Quits the app and installs the downloaded update
### Events
The AppUpdater emits the following events that the UI can listen to:
- `update-checking`: Update check has started
- `update-available`: A new update is available
- `update-not-available`: App is up to date
- `update-download-progress`: Download progress updates
- `update-downloaded`: Update has finished downloading
- `update-error`: An error occurred during the update process
## Configuration
### Package.json
The publish configuration in `package.json` tells electron-builder where to publish updates:
```json
"publish": {
"provider": "github",
"owner": "amiayweb",
"repo": "Hytale-F2P"
}
```
This means updates will be fetched from GitHub Releases for the `amiayweb/Hytale-F2P` repository.
## Publishing Updates
### For Developers
1. **Update Version**: Bump the version in `package.json` (e.g., `2.0.2b` → `2.0.3`)
2. **Build the App**: Run the build command for your platform:
```bash
npm run build:win # Windows
npm run build:mac # macOS
npm run build:linux # Linux
```
3. **Publish to GitHub**: When building with electron-builder, it will:
- Generate update metadata files (`latest.yml`, `latest-mac.yml`, etc.)
- Upload the built files to GitHub Releases (if configured with `GH_TOKEN`)
- Make them available for auto-update
4. **Release on GitHub**: Create a GitHub Release with the new version tag
### Important Notes
- **macOS Code Signing**: macOS apps **must** be code-signed for auto-updates to work
- **Version Format**: Use semantic versioning (e.g., `1.0.0`, `2.0.1`) for best compatibility
- **Update Files**: electron-builder automatically generates the required metadata files (`latest.yml`, etc.)
## Testing Updates
### Development Mode
To test updates during development, create a `dev-app-update.yml` file in the project root:
```yaml
owner: amiayweb
repo: Hytale-F2P
provider: github
```
Then enable dev mode in the code:
```javascript
autoUpdater.forceDevUpdateConfig = true;
```
### Local Testing
For local testing, you can use a local server (like Minio) or a generic HTTP server to host update files.
## User Experience
### What Users See
1. **On Startup**: The app silently checks for updates in the background
2. **Update Available**: A notification appears if an update is found
3. **Downloading**: Progress bar shows download status
4. **Ready to Install**: User is notified when the update is ready
5. **Installation**: Update installs on app restart or when user clicks "Install Now"
### User Actions
- Users can manually check for updates through the settings/update menu
- Users can choose to install immediately or wait until next app launch
- Users can continue using the app while updates download in the background
## Troubleshooting
### Updates Not Working
1. **Check GitHub Releases**: Ensure releases are published on GitHub
2. **Check Version**: Make sure the version in `package.json` is higher than the current release
3. **Check Logs**: Check the app logs for update-related errors
4. **Code Signing (macOS)**: Verify the app is properly code-signed
### Common Issues
- **"Update not available"**: Version in `package.json` may not be higher than the current release
- **"Download failed"**: Network issues or GitHub API rate limits
- **"Installation failed"**: Permissions issue or app is running from an unsupported location
## Technical Details
### Supported Platforms
- **Windows**: NSIS installer (auto-update supported)
- **macOS**: DMG + ZIP (auto-update supported, requires code signing)
- **Linux**: AppImage, DEB, RPM, Pacman (auto-update supported)
### Update Files Generated
When building, electron-builder generates:
- `latest.yml` (Windows/Linux)
- `latest-mac.yml` (macOS)
- `latest-linux.yml` (Linux)
These files contain metadata about the latest release and are automatically uploaded to GitHub Releases.
## References
- [electron-updater Documentation](https://www.electron.build/auto-update)
- [electron-builder Auto Update Guide](https://www.electron.build/auto-update)

View File

@@ -1,78 +0,0 @@
# Clearing Electron-Updater Cache
To force electron-updater to re-download an update file, you need to clear the cached download.
## Quick Method (Terminal)
### macOS
```bash
# Remove the entire cache directory
rm -rf ~/Library/Caches/hytale-f2p-launcher
# Or just remove pending downloads
rm -rf ~/Library/Caches/hytale-f2p-launcher/pending
```
### Windows
```bash
# Remove the entire cache directory
rmdir /s "%LOCALAPPDATA%\hytale-f2p-launcher-updater"
# Or just remove pending downloads
rmdir /s "%LOCALAPPDATA%\hytale-f2p-launcher-updater\pending"
```
### Linux
```bash
# Remove the entire cache directory
rm -rf ~/.cache/hytale-f2p-launcher-updater
# Or just remove pending downloads
rm -rf ~/.cache/hytale-f2p-launcher-updater/pending
```
## Cache Locations
electron-updater stores downloaded updates in:
- **macOS**: `~/Library/Caches/hytale-f2p-launcher/`
- **Windows**: `%LOCALAPPDATA%\hytale-f2p-launcher-updater\`
- **Linux**: `~/.cache/hytale-f2p-launcher-updater/`
The cache typically contains:
- `pending/` - Downloaded update files waiting to be installed
- Metadata files about available updates
## After Clearing
After clearing the cache:
1. Restart the launcher
2. It will check for updates again
3. The update will be re-downloaded from scratch
## Programmatic Method
You can also clear the cache programmatically by adding this to your code:
```javascript
const { autoUpdater } = require('electron-updater');
const path = require('path');
const fs = require('fs');
const os = require('os');
function clearUpdateCache() {
const cacheDir = path.join(
os.homedir(),
process.platform === 'win32'
? 'AppData/Local/hytale-f2p-launcher-updater'
: process.platform === 'darwin'
? 'Library/Caches/hytale-f2p-launcher'
: '.cache/hytale-f2p-launcher-updater'
);
if (fs.existsSync(cacheDir)) {
fs.rmSync(cacheDir, { recursive: true, force: true });
console.log('Update cache cleared');
}
}
```

View File

@@ -1,196 +0,0 @@
# Testing Auto-Updates
This guide explains how to test the auto-update system during development.
## Quick Start
### Option 1: Test with GitHub Releases (Easiest)
1. **Set up dev-app-update.yml** (already done):
```yaml
provider: github
owner: amiayweb
repo: Hytale-F2P
```
2. **Lower your current version** in `package.json`:
- Change version to something lower than what's on GitHub (e.g., `2.0.1` if GitHub has `2.0.3`)
3. **Run the app in dev mode**:
```bash
npm run dev
# or
npm start
```
4. **The app will check for updates** 3 seconds after startup
- If a newer version exists on GitHub, it will detect it
- Check the console logs for update messages
### Option 2: Test with Local HTTP Server
For more control, you can set up a local server:
1. **Create a test update server**:
```bash
# Create a test directory
mkdir -p test-updates
cd test-updates
```
2. **Build a test version** with a higher version number:
```bash
# In package.json, set version to 2.0.4
npm run build
```
3. **Copy the generated files** to your test server:
- Copy `dist/latest.yml` (or `latest-mac.yml` for macOS)
- Copy the built installer/package
4. **Start a simple HTTP server**:
```bash
# Using Python
python3 -m http.server 8080
# Or using Node.js http-server
npx http-server -p 8080
```
5. **Update dev-app-update.yml** to point to local server:
```yaml
provider: generic
url: http://localhost:8080
```
6. **Run the app** and it will check your local server
## Testing Steps
### 1. Prepare Test Environment
**Current version**: `2.0.3` (in package.json)
**Test version**: `2.0.4` (on GitHub or local server)
### 2. Run the App
```bash
npm run dev
```
### 3. Watch for Update Events
The app will automatically check for updates 3 seconds after startup. Watch the console for:
```
Checking for updates...
Update available: 2.0.4
```
### 4. Check Console Logs
Look for these messages:
- `Checking for updates...` - Update check started
- `Update available: 2.0.4` - New version found
- `Download speed: ...` - Download progress
- `Update downloaded: 2.0.4` - Download complete
### 5. Test UI Integration
The app sends these events to the renderer:
- `update-checking`
- `update-available` (with version info)
- `update-download-progress` (with progress data)
- `update-downloaded` (ready to install)
You can listen to these in your frontend code to show update notifications.
## Manual Testing
### Trigger Manual Update Check
You can also trigger a manual check via IPC:
```javascript
// In renderer process
const result = await window.electronAPI.invoke('check-for-updates');
console.log(result);
```
### Install Update
After an update is downloaded:
```javascript
// In renderer process
await window.electronAPI.invoke('quit-and-install-update');
```
## Testing Scenarios
### Scenario 1: Update Available
1. Set `package.json` version to `2.0.1`
2. Ensure GitHub has version `2.0.3` or higher
3. Run app → Should detect update
### Scenario 2: Already Up to Date
1. Set `package.json` version to `2.0.3`
2. Ensure GitHub has version `2.0.3` or lower
3. Run app → Should show "no update available"
### Scenario 3: Prerelease Version
1. Set `package.json` version to `2.0.2b`
2. Ensure GitHub has version `2.0.3`
3. Run app → Should detect update (prerelease < release)
## Troubleshooting
### Update Not Detected
1. **Check dev-app-update.yml exists** in project root
2. **Verify dev mode is enabled** - Check console for "Dev update mode enabled"
3. **Check version numbers** - Remote version must be higher than current
4. **Check network** - App needs internet to reach GitHub/local server
5. **Check logs** - Look for error messages in console
### Common Errors
- **"Cannot find module 'electron-updater'"**: Run `npm install`
- **"Update check failed"**: Check network connection or GitHub API access
- **"No update available"**: Version comparison issue - check versions
### Debug Mode
Enable more verbose logging by checking the console output. The logger will show:
- Update check requests
- Version comparisons
- Download progress
- Any errors
## Testing with Real GitHub Releases
For the most realistic test:
1. **Create a test release on GitHub**:
- Build the app with version `2.0.4`
- Create a GitHub release with tag `v2.0.4`
- Upload the built files
2. **Lower your local version**:
- Set `package.json` to `2.0.3`
3. **Run the app**:
- It will check GitHub and find `2.0.4`
- Download and install the update
## Notes
- **Dev mode only works when app is NOT packaged** (`!app.isPackaged`)
- **Production builds** ignore `dev-app-update.yml` and use the built-in `app-update.yml`
- **macOS**: Code signing is required for updates to work in production
- **Windows**: NSIS installer is required for auto-updates
## Next Steps
Once testing is complete:
1. Remove or comment out `forceDevUpdateConfig` for production
2. Ensure proper code signing for macOS
3. Set up CI/CD to automatically publish releases

456
main.js
View File

@@ -1,46 +1,21 @@
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '.env') });
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); 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 UpdateManager = require('./backend/updateManager');
const logger = require('./backend/logger'); const logger = require('./backend/logger');
const profileManager = require('./backend/managers/profileManager');
logger.interceptConsole(); logger.interceptConsole();
// Single instance lock
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
console.log('Another instance is already running. Quitting...');
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
let mainWindow; let mainWindow;
let updateManager; let updateManager;
let discordRPC = null; let discordRPC = null;
// Discord Rich Presence setup // Discord Rich Presence setup
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; const DISCORD_CLIENT_ID = '1462244937868513373';
function initDiscordRPC() { function initDiscordRPC() {
try { try {
// Check if Discord RPC is enabled in settings
const rpcEnabled = loadDiscordRPC();
if (!rpcEnabled) {
console.log('Discord RPC disabled in settings');
return;
}
const { Client } = require('discord-rpc'); const { Client } = require('discord-rpc');
discordRPC = new Client({ transport: 'ipc' }); discordRPC = new Client({ transport: 'ipc' });
@@ -82,83 +57,25 @@ function setDiscordActivity() {
} }
} }
function toggleDiscordRPC(enabled) {
console.log('Toggling Discord RPC:', enabled);
if (enabled && !discordRPC) {
console.log('Initializing Discord RPC...');
initDiscordRPC();
} else if (!enabled && discordRPC) {
try {
console.log('Disconnecting Discord RPC...');
discordRPC.clearActivity();
discordRPC.destroy();
discordRPC = null;
console.log('Discord RPC disconnected successfully');
} catch (error) {
console.error('Error disconnecting Discord RPC:', error.message);
discordRPC = null;
}
}
}
function createSplashScreen() {
const splashWindow = new BrowserWindow({
width: 500,
height: 350,
frame: false,
transparent: true,
alwaysOnTop: true,
resizable: false,
skipTaskbar: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
splashWindow.loadFile('GUI/splash.html');
splashWindow.center();
// close splash after 2.5s , need to implement a files check or whatever. just mock for now
setTimeout(() => {
splashWindow.close();
createWindow();
}, 2500);
}
function createWindow() { function createWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1280, width: 1280,
height: 720, height: 720,
minWidth: 900,
minHeight: 600,
frame: false, frame: false,
resizable: true, resizable: false,
alwaysOnTop: false, alwaysOnTop: false,
backgroundColor: '#090909', backgroundColor: '#090909',
show: false,
webPreferences: { webPreferences: {
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
devTools: false, devTools: true,
webSecurity: true webSecurity: true
} }
}); });
mainWindow.loadFile('GUI/index.html'); mainWindow.loadFile('GUI/index.html');
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
// Cleanup Discord RPC when window is closed
mainWindow.on('closed', () => {
console.log('Main window closed, cleaning up Discord RPC...');
cleanupDiscordRPC();
});
// Initialize Discord Rich Presence // Initialize Discord Rich Presence
initDiscordRPC(); initDiscordRPC();
@@ -170,6 +87,7 @@ function createWindow() {
} }
}, 3000); }, 3000);
mainWindow.webContents.on('devtools-opened', () => { mainWindow.webContents.on('devtools-opened', () => {
mainWindow.webContents.closeDevTools(); mainWindow.webContents.closeDevTools();
}); });
@@ -190,20 +108,9 @@ function createWindow() {
if (input.key === 'F5') { if (input.key === 'F5') {
event.preventDefault(); event.preventDefault();
} }
// Close application shortcuts
const isMac = process.platform === 'darwin';
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
(!isMac && input.alt && input.key === 'F4');
if (quitShortcut) {
app.quit();
}
}); });
mainWindow.webContents.on('context-menu', (e) => { mainWindow.webContents.on('context-menu', (e) => {
e.preventDefault(); e.preventDefault();
}); });
@@ -212,34 +119,14 @@ function createWindow() {
} }
app.whenReady().then(async () => { app.whenReady().then(async () => {
const packageJson = require('./package.json');
console.log('=== HYTALE F2P LAUNCHER STARTED ==='); console.log('=== HYTALE F2P LAUNCHER STARTED ===');
console.log('Launcher version:', packageJson.version);
console.log('Platform:', process.platform); console.log('Platform:', process.platform);
console.log('Architecture:', process.arch); console.log('Architecture:', process.arch);
console.log('Electron version:', process.versions.electron); console.log('Electron version:', process.versions.electron);
console.log('Node.js version:', process.versions.node); console.log('Node.js version:', process.versions.node);
console.log('Log directory:', logger.getLogDirectory()); console.log('Log directory:', logger.getLogDirectory());
try { createWindow();
const { loadGpuPreference, detectGpu } = require('./backend/launcher');
const savedPreference = loadGpuPreference();
if (savedPreference === 'auto') {
global.detectedGpu = detectGpu(); // if 'auto' selected = preload GPU detection
console.log('GPU auto-detection completed on startup:', global.detectedGpu);
} else {
console.log('GPU preference is manual, skipping auto-detection');
}
} catch (error) {
console.warn('Failed to preload GPU detection:', error.message);
global.detectedGpu = { mode: 'integrated', vendor: 'intel' };
}
// Initialize Profile Manager (runs migration if needed)
profileManager.init();
createSplashScreen();
setTimeout(async () => { setTimeout(async () => {
let timeoutReached = false; let timeoutReached = false;
@@ -320,41 +207,24 @@ app.whenReady().then(async () => {
}, 3000); }, 3000);
}); });
function cleanupDiscordRPC() {
if (discordRPC) {
try {
console.log('Cleaning up Discord RPC...');
discordRPC.clearActivity();
setTimeout(() => {
try {
discordRPC.destroy();
} catch (error) {
console.log('Error during final Discord RPC cleanup:', error.message);
}
}, 100);
discordRPC = null;
} catch (error) {
console.log('Error cleaning up Discord RPC:', error.message);
discordRPC = null;
}
}
}
app.on('before-quit', () => {
console.log('=== LAUNCHER BEFORE QUIT ===');
cleanupDiscordRPC();
});
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ==='); console.log('=== LAUNCHER CLOSING ===');
cleanupDiscordRPC(); // Clean up Discord RPC connection
if (discordRPC) {
try {
discordRPC.destroy();
} catch (error) {
console.log('Error cleaning up Discord RPC:', error.message);
}
}
if (process.platform !== 'darwin') {
app.quit(); app.quit();
}
}); });
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) => {
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
try { try {
const progressCallback = (message, percent, speed, downloaded, total) => { const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
@@ -369,23 +239,7 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
} }
}; };
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
if (result.success && result.launched) {
const closeOnStart = loadCloseLauncherOnStart();
if (closeOnStart) {
console.log('Close Launcher on start enabled, quitting application...');
setTimeout(() => {
app.quit();
}, 1000);
}
}
return result;
} catch (error) {
console.error('Launch error:', error);
const errorMessage = error.message || error.toString();
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
setTimeout(() => { setTimeout(() => {
@@ -393,17 +247,16 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
}, 2000); }, 2000);
} }
return result;
} catch (error) {
console.error('Launch error:', error);
const errorMessage = error.message || error.toString();
return { success: false, error: errorMessage }; return { success: false, error: errorMessage };
} }
}); });
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => { ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => {
try { try {
// Signal installation start
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('installation-start');
}
const progressCallback = (message, percent, speed, downloaded, total) => { const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
const data = { const data = {
@@ -419,21 +272,16 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath)
const result = await installGame(playerName, progressCallback, javaPath, installPath); const result = await installGame(playerName, progressCallback, javaPath, installPath);
// Signal installation end
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('installation-end'); setTimeout(() => {
mainWindow.webContents.send('progress-complete');
}, 1000);
} }
return result; return result;
} catch (error) { } catch (error) {
console.error('Install error:', error); console.error('Install error:', error);
const errorMessage = error.message || error.toString(); const errorMessage = error.message || error.toString();
// Signal installation end on error too
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('installation-end');
}
return { success: false, error: errorMessage }; return { success: false, error: errorMessage };
} }
}); });
@@ -453,16 +301,6 @@ ipcMain.handle('save-chat-username', async (event, chatUsername) => {
ipcMain.handle('load-chat-username', async () => { ipcMain.handle('load-chat-username', async () => {
return loadChatUsername(); return loadChatUsername();
}); });
ipcMain.handle('save-chat-color', (event, color) => {
saveChatColor(color);
return { success: true };
});
ipcMain.handle('load-chat-color', () => {
return loadChatColor();
});
ipcMain.handle('save-java-path', (event, javaPath) => { ipcMain.handle('save-java-path', (event, javaPath) => {
saveJavaPath(javaPath); saveJavaPath(javaPath);
return { success: true }; return { success: true };
@@ -482,36 +320,7 @@ ipcMain.handle('load-install-path', () => {
return loadInstallPath(); return loadInstallPath();
}); });
ipcMain.handle('save-discord-rpc', (event, enabled) => {
saveDiscordRPC(enabled);
toggleDiscordRPC(enabled);
return { success: true };
});
ipcMain.handle('load-discord-rpc', () => {
return loadDiscordRPC();
});
ipcMain.handle('save-language', (event, language) => {
saveLanguage(language);
return { success: true };
});
ipcMain.handle('load-language', () => {
return loadLanguage();
});
ipcMain.handle('save-close-launcher', (event, enabled) => {
saveCloseLauncherOnStart(enabled);
return { success: true };
});
ipcMain.handle('load-close-launcher', () => {
return loadCloseLauncherOnStart();
});
ipcMain.handle('select-install-path', async () => { ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'], properties: ['openDirectory'],
title: 'Select Installation Folder' title: 'Select Installation Folder'
@@ -573,35 +382,13 @@ ipcMain.handle('is-game-installed', async () => {
ipcMain.handle('uninstall-game', async () => { ipcMain.handle('uninstall-game', async () => {
try { try {
await uninstallGame(); await uninstallGame();
return { success: true };
} catch (error) { } catch (error) {
console.error('Uninstall error:', error); console.error('Uninstall error:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
}); });
ipcMain.handle('repair-game', async () => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('progress-update', data);
}
};
const result = await repairGame(progressCallback);
return result;
} catch (error) {
console.error('Repair error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-hytale-news', async () => { ipcMain.handle('get-hytale-news', async () => {
try { try {
const news = await getHytaleNews(); const news = await getHytaleNews();
@@ -716,17 +503,13 @@ ipcMain.handle('load-settings', async () => {
} }
}); });
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher'); const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod } = require('./backend/launcher');
const os = require('os'); const os = require('os');
ipcMain.handle('get-local-app-data', async () => { ipcMain.handle('get-local-app-data', async () => {
return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
}); });
ipcMain.handle('get-env-var', async (event, key) => {
return process.env[key];
});
ipcMain.handle('get-user-id', async () => { ipcMain.handle('get-user-id', async () => {
try { try {
const { getOrCreatePlayerId } = require('./backend/launcher'); const { getOrCreatePlayerId } = require('./backend/launcher');
@@ -837,10 +620,11 @@ ipcMain.handle('open-download-page', async () => {
await shell.openExternal(updateManager.getDownloadUrl()); await shell.openExternal(updateManager.getDownloadUrl());
setTimeout(() => { setTimeout(() => {
app.quit(); if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
}, 1000); }, 1000);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error opening download page:', error); console.error('Error opening download page:', error);
@@ -852,132 +636,28 @@ ipcMain.handle('get-update-info', async () => {
return updateManager.getUpdateInfo(); return updateManager.getUpdateInfo();
}); });
ipcMain.handle('get-gpu-info', () => {
try {
return app.getGPUInfo('complete');
} catch (error) {
console.error('Error getting GPU info:', error);
return {};
}
});
ipcMain.handle('save-gpu-preference', (event, gpuPreference) => {
const { saveGpuPreference } = require('./backend/launcher');
saveGpuPreference(gpuPreference);
return { success: true };
});
ipcMain.handle('load-gpu-preference', () => {
const { loadGpuPreference } = require('./backend/launcher');
return loadGpuPreference();
});
ipcMain.handle('get-detected-gpu', () => {
if (global.detectedGpu) {
return global.detectedGpu;
}
const { detectGpu } = require('./backend/launcher');
global.detectedGpu = detectGpu();
return global.detectedGpu;
});
ipcMain.handle('window-close', () => { ipcMain.handle('window-close', () => {
app.quit(); if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
}); });
ipcMain.handle('window-minimize', () => { ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.minimize(); mainWindow.minimize();
} }
}); });
ipcMain.handle('window-maximize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
}
});
ipcMain.handle('get-version', () => {
const packageJson = require('./package.json');
return packageJson.version;
});
ipcMain.handle('get-log-directory', () => { ipcMain.handle('get-log-directory', () => {
return logger.getLogDirectory(); return logger.getLogDirectory();
}); });
ipcMain.handle('get-current-uuid', async () => {
try {
return getCurrentUuid();
} catch (error) {
console.error('Error getting current UUID:', error);
return null;
}
});
ipcMain.handle('get-all-uuid-mappings', async () => {
try {
const mappings = getAllUuidMappings();
return Object.entries(mappings).map(([username, uuid]) => ({
username,
uuid,
isCurrent: username === require('./backend/launcher').loadUsername()
}));
} catch (error) {
console.error('Error getting UUID mappings:', error);
return [];
}
});
ipcMain.handle('set-uuid-for-user', async (event, username, uuid) => {
try {
await setUuidForUser(username, uuid);
return { success: true };
} catch (error) {
console.error('Error setting UUID for user:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('generate-new-uuid', async () => {
try {
return generateNewUuid();
} catch (error) {
console.error('Error generating new UUID:', error);
return null;
}
});
ipcMain.handle('delete-uuid-for-user', async (event, username) => {
try {
const result = deleteUuidForUser(username);
return { success: result };
} catch (error) {
console.error('Error deleting UUID for user:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('reset-current-user-uuid', async () => {
try {
const newUuid = resetCurrentUserUuid();
return { success: true, uuid: newUuid };
} catch (error) {
console.error('Error resetting current user UUID:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => { ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
try { try {
const logDir = logger.getLogDirectory(); const logDir = logger.getLogDirectory();
if (!logDir) return null; if (!logDir) return null;
// Find the most recent log file
const files = fs.readdirSync(logDir) const files = fs.readdirSync(logDir)
.filter(file => file.startsWith('launcher-') && file.endsWith('.log')) .filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
.map(file => ({ .map(file => ({
@@ -993,73 +673,9 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
const content = fs.readFileSync(latestLogFile, 'utf8'); const content = fs.readFileSync(latestLogFile, 'utf8');
const lines = content.split('\n'); const lines = content.split('\n');
let result = lines.slice(-maxLines).join('\n'); return lines.slice(-maxLines).join('\n');
if (lines.length > maxLines) {
const truncatedMsg = `\n--- ⚠️ LOG TRUNCATED: Showing last ${maxLines} lines of ${lines.length}. Open Logs Folder for full history ---\n\n`;
return result + truncatedMsg;
}
return result;
} catch (error) { } catch (error) {
console.error('Error reading logs:', error); console.error('Error reading logs:', error);
return null; return null;
} }
}); });
ipcMain.handle('open-logs-folder', async () => {
try {
const logDir = logger.getLogDirectory();
if (logDir && fs.existsSync(logDir)) {
await shell.openPath(logDir);
return { success: true };
}
return { success: false, error: 'Logs directory not found' };
} catch (error) {
console.error('Error opening logs folder:', error);
return { success: false, error: error.message };
}
});
// Profile Management IPC
ipcMain.handle('profile-create', async (event, name) => {
try {
return profileManager.createProfile(name);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-list', async () => {
return profileManager.getProfiles();
});
ipcMain.handle('profile-get-active', async () => {
return profileManager.getActiveProfile();
});
ipcMain.handle('profile-activate', async (event, id) => {
try {
return await profileManager.activateProfile(id);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-delete', async (event, id) => {
try {
return profileManager.deleteProfile(id);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-update', async (event, id, updates) => {
try {
return profileManager.updateProfile(id, updates);
} catch (error) {
return { error: error.message };
}
});

169
package-lock.json generated
View File

@@ -1,19 +1,17 @@
{ {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.0.11", "version": "2.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.0.11", "version": "2.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"axios": "^1.6.0", "axios": "^1.6.0",
"discord-rpc": "^4.0.1", "discord-rpc": "^4.0.1",
"dotenv": "^17.2.3",
"electron-updater": "^6.7.3",
"tar": "^6.2.1", "tar": "^6.2.1",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
@@ -1020,19 +1018,6 @@
"electron-builder-squirrel-windows": "26.4.0" "electron-builder-squirrel-windows": "26.4.0"
} }
}, },
"node_modules/app-builder-lib/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/app-builder-lib/node_modules/fs-extra": { "node_modules/app-builder-lib/node_modules/fs-extra": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@@ -1088,6 +1073,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/assert-plus": { "node_modules/assert-plus": {
@@ -1297,6 +1283,7 @@
"version": "9.5.1", "version": "9.5.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1724,6 +1711,7 @@
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@@ -1974,9 +1962,10 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "17.2.3", "version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -2001,19 +1990,6 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dotenv-expand/node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2202,69 +2178,6 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/electron-updater": {
"version": "6.7.3",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz",
"integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==",
"license": "MIT",
"dependencies": {
"builder-util-runtime": "9.5.1",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.5.0",
"semver": "~7.7.3",
"tiny-typed-emitter": "^2.1.0"
}
},
"node_modules/electron-updater/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-updater/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-updater/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/electron-updater/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/electron-winstaller": { "node_modules/electron-winstaller": {
"version": "5.4.0", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
@@ -2846,6 +2759,7 @@
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/has-flag": { "node_modules/has-flag": {
@@ -3168,6 +3082,7 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -3235,6 +3150,7 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash": { "node_modules/lodash": {
@@ -3244,19 +3160,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
"license": "MIT"
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/log-symbols": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -3556,23 +3459,22 @@
} }
}, },
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "0.5.6", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": { "bin": {
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
} }
}, },
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/negotiator": { "node_modules/negotiator": {
@@ -4227,6 +4129,7 @@
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": ">=11.0.0" "node": ">=11.0.0"
@@ -4601,18 +4504,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tar/node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/temp": { "node_modules/temp": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
@@ -4677,6 +4568,20 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/temp/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/tiny-async-pool": { "node_modules/tiny-async-pool": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz",
@@ -4697,12 +4602,6 @@
"semver": "bin/semver" "semver": "bin/semver"
} }
}, },
"node_modules/tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.0.11", "version": "2.0.1",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P", "homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js", "main": "main.js",
@@ -24,17 +24,6 @@
"mod-manager", "mod-manager",
"chat" "chat"
], ],
"maintainers": [
{
"name": "Terromur",
"url": "https://github.com/Terromur"
},
{
"name": "Fari Gading",
"email": "fazrigading@gmail.com",
"url": "https://github.com/fazrigading"
}
],
"author": { "author": {
"name": "AMIAY", "name": "AMIAY",
"email": "support@amiay.dev" "email": "support@amiay.dev"
@@ -48,8 +37,6 @@
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"axios": "^1.6.0", "axios": "^1.6.0",
"discord-rpc": "^4.0.1", "discord-rpc": "^4.0.1",
"dotenv": "^17.2.3",
"electron-updater": "^6.7.3",
"tar": "^6.2.1", "tar": "^6.2.1",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
@@ -58,8 +45,7 @@
}, },
"build": { "build": {
"appId": "com.hytalef2p.launcher", "appId": "com.hytalef2p.launcher",
"productName": "Hytale F2P Launcher", "productName": "Hytale F2P",
"artifactName": "${name}_${version}_${arch}.${ext}",
"directories": { "directories": {
"output": "dist" "output": "dist"
}, },
@@ -68,16 +54,20 @@
"preload.js", "preload.js",
"backend/**/*", "backend/**/*",
"GUI/**/*", "GUI/**/*",
"package.json", "package.json"
".env"
], ],
"win": { "win": {
"target": [ "target": [
{ {
"target": "nsis", "target": "nsis",
"arch": [ "arch": [
"x64", "x64"
"arm64" ]
},
{
"target": "portable",
"arch": [
"x64"
] ]
} }
], ],
@@ -88,41 +78,27 @@
{ {
"target": "AppImage", "target": "AppImage",
"arch": [ "arch": [
"x64", "x64"
"arm64"
] ]
}, },
{ {
"target": "deb", "target": "deb",
"arch": [ "arch": [
"x64", "x64"
"arm64"
]
},
{
"target": "rpm",
"arch": [
"x64",
"arm64"
]
},
{
"target": "pacman",
"arch": [
"x64",
"arm64"
] ]
} }
], ],
"icon": "build/icon.png", "icon": "build/icon.png",
"category": "Game" "category": "Game",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support"
}, },
"mac": { "mac": {
"target": [ "target": [
{ {
"target": "dmg", "target": "dmg",
"arch": [ "arch": [
"universal" "x64",
"arm64"
] ]
}, },
{ {
@@ -140,11 +116,9 @@
"allowToChangeInstallationDirectory": true, "allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true, "createDesktopShortcut": true,
"createStartMenuShortcut": true "createStartMenuShortcut": true
},
"publish": {
"provider": "github",
"owner": "amiayweb",
"repo": "Hytale-F2P"
} }
} }
} }

View File

@@ -1,40 +1,28 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
launchGame: (playerName, javaPath, installPath, gpuPreference) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath, gpuPreference), launchGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath),
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath), installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
closeWindow: () => ipcRenderer.invoke('window-close'), closeWindow: () => ipcRenderer.invoke('window-close'),
minimizeWindow: () => ipcRenderer.invoke('window-minimize'), minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
getVersion: () => ipcRenderer.invoke('get-version'),
saveUsername: (username) => ipcRenderer.invoke('save-username', username), saveUsername: (username) => ipcRenderer.invoke('save-username', username),
loadUsername: () => ipcRenderer.invoke('load-username'), loadUsername: () => ipcRenderer.invoke('load-username'),
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername), saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
loadChatUsername: () => ipcRenderer.invoke('load-chat-username'), loadChatUsername: () => ipcRenderer.invoke('load-chat-username'),
saveChatColor: (chatColor) => ipcRenderer.invoke('save-chat-color', chatColor),
loadChatColor: () => ipcRenderer.invoke('load-chat-color'),
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath), saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
loadJavaPath: () => ipcRenderer.invoke('load-java-path'), loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath), saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
loadInstallPath: () => ipcRenderer.invoke('load-install-path'), loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
saveLanguage: (language) => ipcRenderer.invoke('save-language', language),
loadLanguage: () => ipcRenderer.invoke('load-language'),
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
selectInstallPath: () => ipcRenderer.invoke('select-install-path'), selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'), browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'), isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
uninstallGame: () => ipcRenderer.invoke('uninstall-game'), uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
repairGame: () => ipcRenderer.invoke('repair-game'),
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'), getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
openExternal: (url) => ipcRenderer.invoke('open-external', url), openExternal: (url) => ipcRenderer.invoke('open-external', url),
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url), openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
openGameLocation: () => ipcRenderer.invoke('open-game-location'), openGameLocation: () => ipcRenderer.invoke('open-game-location'),
saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings), saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
loadSettings: () => ipcRenderer.invoke('load-settings'), loadSettings: () => ipcRenderer.invoke('load-settings'),
getEnvVar: (key) => ipcRenderer.invoke('get-env-var', key),
getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'), getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'),
getModsPath: () => ipcRenderer.invoke('get-mods-path'), getModsPath: () => ipcRenderer.invoke('get-mods-path'),
loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath), loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath),
@@ -49,12 +37,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
onProgressComplete: (callback) => { onProgressComplete: (callback) => {
ipcRenderer.on('progress-complete', () => callback()); ipcRenderer.on('progress-complete', () => callback());
}, },
onInstallationStart: (callback) => {
ipcRenderer.on('installation-start', () => callback());
},
onInstallationEnd: (callback) => {
ipcRenderer.on('installation-end', () => callback());
},
getUserId: () => ipcRenderer.invoke('get-user-id'), getUserId: () => ipcRenderer.invoke('get-user-id'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'), checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
openDownloadPage: () => ipcRenderer.invoke('open-download-page'), openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
@@ -63,11 +45,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('show-update-popup', (event, data) => callback(data)); ipcRenderer.on('show-update-popup', (event, data) => callback(data));
}, },
getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'),
saveGpuPreference: (gpuPreference) => ipcRenderer.invoke('save-gpu-preference', gpuPreference),
loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'),
getDetectedGpu: () => ipcRenderer.invoke('get-detected-gpu'),
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame), acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'), markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
onFirstLaunchUpdate: (callback) => { onFirstLaunchUpdate: (callback) => {
@@ -84,24 +61,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
}, },
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'), getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'), getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines)
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
// UUID Management methods
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
getAllUuidMappings: () => ipcRenderer.invoke('get-all-uuid-mappings'),
setUuidForUser: (username, uuid) => ipcRenderer.invoke('set-uuid-for-user', username, uuid),
generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'),
deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username),
resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'),
// Profile API
profile: {
create: (name) => ipcRenderer.invoke('profile-create', name),
list: () => ipcRenderer.invoke('profile-list'),
getActive: () => ipcRenderer.invoke('profile-get-active'),
activate: (id) => ipcRenderer.invoke('profile-activate', id),
delete: (id) => ipcRenderer.invoke('profile-delete', id),
update: (id, updates) => ipcRenderer.invoke('profile-update', id, updates)
}
}); });