mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 18:41:48 -03:00
Compare commits
48 Commits
v2.1.1
...
v2.1.4-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe9ed2a89 | ||
|
|
0aaf74a3db | ||
|
|
be78f67439 | ||
|
|
d0b9ae1da8 | ||
|
|
e8105cb30e | ||
|
|
79456e43a6 | ||
|
|
dd2dbc6f08 | ||
|
|
c4acb32fcd | ||
|
|
fbcbafb9b5 | ||
|
|
86ed33358c | ||
|
|
9ec97f9d33 | ||
|
|
ee18455b4b | ||
|
|
a5c931b26d | ||
|
|
661a0c9eed | ||
|
|
9025800820 | ||
|
|
34ee099ae2 | ||
|
|
e56b12cd72 | ||
|
|
3edee4b4eb | ||
|
|
e5fec7c326 | ||
|
|
7d2672b684 | ||
|
|
01823729ec | ||
|
|
639a2ab1b5 | ||
|
|
6b76eb365e | ||
|
|
6fa933fece | ||
|
|
e7023dcf95 | ||
|
|
f4d966ee65 | ||
|
|
ca835a868b | ||
|
|
3a1b6039d0 | ||
|
|
7828454631 | ||
|
|
cc1c6c334c | ||
|
|
081ac926e3 | ||
|
|
75a450c9ec | ||
|
|
e426690632 | ||
|
|
78f76afe0a | ||
|
|
131de1dcd7 | ||
|
|
b39877f561 | ||
|
|
0b1b448cce | ||
|
|
aed00cd067 | ||
|
|
eff6fcd520 | ||
|
|
94d4586b97 | ||
|
|
20faf36b37 | ||
|
|
375b422c73 | ||
|
|
b668bdb45a | ||
|
|
653d4429ed | ||
|
|
17e15c17f0 | ||
|
|
b99b22e8bf | ||
|
|
9303c17e57 | ||
|
|
615ee5cadc |
40
.github/ISSUE_TEMPLATE/support_request.yml
vendored
40
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -1,8 +1,22 @@
|
|||||||
name: Support Request
|
name: Support Request
|
||||||
description: Request help or support
|
description: Request help or support
|
||||||
title: "[SUPPORT] "
|
title: "[SUPPORT] <ADD YOUR TITLE HERE>"
|
||||||
labels: ["support"]
|
labels: ["support"]
|
||||||
body:
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
id: acknowledge
|
||||||
|
attributes:
|
||||||
|
label: Checklist
|
||||||
|
options:
|
||||||
|
- label: I have read the README.md before asking Support Request.
|
||||||
|
required: true
|
||||||
|
- label: I have read the TROUBLESHOOTING.md before asking Support Request.
|
||||||
|
required: true
|
||||||
|
- label: I have added title before submitting this Support Request.
|
||||||
|
required: true
|
||||||
|
- label: I acknowledge that my Support Request will not be responded as quick as in Discord Open-A-Ticket, I prefer this way.
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
@@ -24,7 +38,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Context
|
label: Context
|
||||||
description: Provide any relevant context or background information.
|
description: Provide any relevant context or background information.
|
||||||
placeholder: "I've tried..., but got..."
|
placeholder: "I've tried these steps, but got..."
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -37,12 +51,17 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: dropdown
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version are you using?
|
description: What version are you using?
|
||||||
placeholder: "e.g. v2.0.11 stable/pre-release"
|
options:
|
||||||
|
- v2.1.2
|
||||||
|
- v2.1.1
|
||||||
|
- v2.1.0
|
||||||
|
- v2.0.11
|
||||||
|
- v2.0.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -52,13 +71,12 @@ body:
|
|||||||
label: Platform
|
label: Platform
|
||||||
description: What platform are you using?
|
description: What platform are you using?
|
||||||
options:
|
options:
|
||||||
- Windows 10
|
- Windows 11 x64
|
||||||
- Windows 11
|
- Windows 10 x64
|
||||||
- macOS (Apple Silicon)
|
- macOS ARM64 (Apple Silicon)
|
||||||
- macOS (Intel)
|
- Linux x64 Ubuntu/Debian-based
|
||||||
- Linux Ubuntu/Debian-based
|
- Linux x64 Fedora/RHEL-based
|
||||||
- Linux Fedora/RHEL-based
|
- Linux x64 Arch-based
|
||||||
- Linux Arch-based
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
81
.github/workflows/release.yml
vendored
81
.github/workflows/release.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
node-version: '22'
|
node-version: '22'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Build Windows Packages
|
- name: Build Windows Packages
|
||||||
run: npx electron-builder --win --publish never
|
run: npx electron-builder --win --publish never
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@@ -31,6 +31,7 @@ jobs:
|
|||||||
|
|
||||||
build-macos:
|
build-macos:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
timeout-minutes: 360 # Max allowed (6 hours) for notarization
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@@ -40,6 +41,14 @@ jobs:
|
|||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Build macOS Packages
|
- name: Build macOS Packages
|
||||||
|
env:
|
||||||
|
# Code signing
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
# Notarization
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
run: npx electron-builder --mac --publish never
|
run: npx electron-builder --mac --publish never
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -57,7 +66,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libarchive-tools
|
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'
|
||||||
@@ -86,7 +95,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install base packages
|
- name: Install base packages
|
||||||
run: |
|
run: |
|
||||||
pacman -Syu --noconfirm
|
pacman -Syu --noconfirm
|
||||||
@@ -123,26 +132,39 @@ jobs:
|
|||||||
*.src.tar.zst
|
*.src.tar.zst
|
||||||
.SRCINFO
|
.SRCINFO
|
||||||
|
|
||||||
|
# Create release with Windows, Linux, Arch (fast builds)
|
||||||
release:
|
release:
|
||||||
needs: [build-windows, build-macos, build-linux, build-arch]
|
needs: [build-windows, build-linux, build-arch]
|
||||||
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/main' ||
|
github.ref == 'refs/heads/main' ||
|
||||||
github.event_name == 'workflow_dispatch'
|
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
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download Windows artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
name: windows-builds
|
||||||
|
path: artifacts/windows-builds
|
||||||
|
|
||||||
|
- name: Download Linux artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-builds
|
||||||
|
path: artifacts/linux-builds
|
||||||
|
|
||||||
|
- name: Download Arch artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: arch-package
|
||||||
|
path: artifacts/arch-package
|
||||||
|
|
||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
run: ls -R artifacts
|
run: ls -R artifacts
|
||||||
@@ -155,18 +177,43 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
# 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/arch-package/*.pkg.tar.zst
|
artifacts/arch-package/*.pkg.tar.zst
|
||||||
artifacts/arch-package/*.src.tar.zst
|
artifacts/arch-package/*.src.tar.zst
|
||||||
artifacts/arch-package/.SRCINFO
|
artifacts/arch-package/.SRCINFO
|
||||||
artifacts/linux-builds/**/*
|
artifacts/linux-builds/*
|
||||||
artifacts/windows-builds/**/*
|
artifacts/windows-builds/*
|
||||||
artifacts/macos-builds/**/*
|
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
|
# Upload macOS builds separately (slow due to notarization)
|
||||||
|
release-macos:
|
||||||
|
needs: [build-macos, release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
startsWith(github.ref, 'refs/tags/v') ||
|
||||||
|
github.ref == 'refs/heads/main' ||
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download macOS artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-builds
|
||||||
|
path: artifacts/macos-builds
|
||||||
|
|
||||||
|
- name: Display macOS files
|
||||||
|
run: ls -R artifacts
|
||||||
|
|
||||||
|
- name: Upload macOS to Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
files: |
|
||||||
|
artifacts/macos-builds/*
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
@@ -882,7 +882,7 @@
|
|||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
<script type="module" src="js/settings.js"></script>
|
<script type="module" src="js/settings.js"></script>
|
||||||
<script type="module" src="js/update.js"></script>
|
<script type="module" src="js/update.js"></script>
|
||||||
<script src="js/updater.js"></script>
|
<!-- updater.js disabled - using update.js instead which has skip button and macOS handling -->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const i18n = (() => {
|
|||||||
let translations = {};
|
let translations = {};
|
||||||
const availableLanguages = [
|
const availableLanguages = [
|
||||||
{ code: 'en', name: 'English' },
|
{ code: 'en', name: 'English' },
|
||||||
|
{ code: 'fr', name: 'Français' },
|
||||||
|
{ code: 'de', name: 'Deutsch' },
|
||||||
|
{ code: 'sv', name: 'Svenska' },
|
||||||
{ code: 'es-ES', name: 'Español (España)' },
|
{ code: 'es-ES', name: 'Español (España)' },
|
||||||
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
|
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
|
||||||
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
|
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
|
||||||
|
|||||||
@@ -72,8 +72,11 @@ export async function installGame() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
|
// Sync player name to both launcher and settings inputs
|
||||||
const playerNameInput = document.getElementById('playerName');
|
const playerNameInput = document.getElementById('playerName');
|
||||||
if (playerNameInput) playerNameInput.value = playerName;
|
if (playerNameInput) playerNameInput.value = playerName;
|
||||||
|
const settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
|
if (settingsPlayerName) settingsPlayerName.value = playerName;
|
||||||
resetInstallButton();
|
resetInstallButton();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@@ -125,8 +128,11 @@ function simulateInstallation(playerName) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
|
// Sync player name to both launcher and settings inputs
|
||||||
const playerNameInput = document.getElementById('playerName');
|
const playerNameInput = document.getElementById('playerName');
|
||||||
if (playerNameInput) playerNameInput.value = playerName;
|
if (playerNameInput) playerNameInput.value = playerName;
|
||||||
|
const settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
|
if (settingsPlayerName) settingsPlayerName.value = playerName;
|
||||||
resetInstallButton();
|
resetInstallButton();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@@ -246,9 +252,3 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
setupInstallation();
|
setupInstallation();
|
||||||
await checkGameStatusAndShowInterface();
|
await checkGameStatusAndShowInterface();
|
||||||
});
|
});
|
||||||
window.browseInstallPath = browseInstallPath;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
setupInstallation();
|
|
||||||
await checkGameStatusAndShowInterface();
|
|
||||||
});
|
|
||||||
|
|||||||
216
GUI/js/update.js
216
GUI/js/update.js
@@ -6,12 +6,12 @@ class ClientUpdateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
window.electronAPI.onUpdatePopup((updateInfo) => {
|
console.log('🔧 ClientUpdateManager initializing...');
|
||||||
this.showUpdatePopup(updateInfo);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for electron-updater events
|
// Listen for electron-updater events from main.js
|
||||||
|
// This is the primary update trigger - main.js checks for updates on startup
|
||||||
window.electronAPI.onUpdateAvailable((updateInfo) => {
|
window.electronAPI.onUpdateAvailable((updateInfo) => {
|
||||||
|
console.log('📥 update-available event received:', updateInfo);
|
||||||
this.showUpdatePopup(updateInfo);
|
this.showUpdatePopup(updateInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,18 +20,30 @@ class ClientUpdateManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.onUpdateDownloaded((updateInfo) => {
|
window.electronAPI.onUpdateDownloaded((updateInfo) => {
|
||||||
|
console.log('📦 update-downloaded event received:', updateInfo);
|
||||||
this.showUpdateDownloaded(updateInfo);
|
this.showUpdateDownloaded(updateInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.onUpdateError((errorInfo) => {
|
window.electronAPI.onUpdateError((errorInfo) => {
|
||||||
|
console.log('❌ update-error event received:', errorInfo);
|
||||||
this.handleUpdateError(errorInfo);
|
this.handleUpdateError(errorInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.checkForUpdatesOnDemand();
|
console.log('✅ ClientUpdateManager initialized');
|
||||||
|
|
||||||
|
// Note: Don't call checkForUpdatesOnDemand() here - main.js already checks
|
||||||
|
// for updates after 3 seconds and sends 'update-available' event.
|
||||||
|
// Calling it here would cause duplicate popups.
|
||||||
}
|
}
|
||||||
|
|
||||||
showUpdatePopup(updateInfo) {
|
showUpdatePopup(updateInfo) {
|
||||||
if (this.updatePopupVisible) return;
|
console.log('🔔 showUpdatePopup called, updatePopupVisible:', this.updatePopupVisible);
|
||||||
|
|
||||||
|
// Check if popup already exists in DOM (extra safety)
|
||||||
|
if (this.updatePopupVisible || document.getElementById('update-popup-overlay')) {
|
||||||
|
console.log('⚠️ Update popup already visible, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.updatePopupVisible = true;
|
this.updatePopupVisible = true;
|
||||||
|
|
||||||
@@ -92,7 +104,10 @@ class ClientUpdateManager {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="update-popup-footer">
|
<div class="update-popup-footer">
|
||||||
This popup cannot be closed until you update the launcher
|
<span id="update-footer-text">Downloading update...</span>
|
||||||
|
<button id="update-skip-btn" class="update-skip-btn" style="display: none; margin-top: 0.5rem; background: transparent; border: 1px solid rgba(255,255,255,0.2); color: #9ca3af; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.75rem;">
|
||||||
|
Skip for now (not recommended)
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,16 +128,43 @@ class ClientUpdateManager {
|
|||||||
installBtn.addEventListener('click', async (e) => {
|
installBtn.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
installBtn.disabled = true;
|
installBtn.disabled = true;
|
||||||
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Installing...';
|
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Installing...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.quitAndInstallUpdate();
|
await window.electronAPI.quitAndInstallUpdate();
|
||||||
|
|
||||||
|
// If we're still here after 5 seconds, the install probably failed
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('⚠️ Install may have failed - showing skip option');
|
||||||
|
installBtn.disabled = false;
|
||||||
|
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Again';
|
||||||
|
|
||||||
|
// Show skip button
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Install not working? Skip for now:';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error installing update:', error);
|
console.error('❌ Error installing update:', error);
|
||||||
installBtn.disabled = false;
|
installBtn.disabled = false;
|
||||||
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
|
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
|
||||||
|
|
||||||
|
// Show skip button on error
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Install failed. Skip for now:';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -138,10 +180,15 @@ class ClientUpdateManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.openDownloadPage();
|
await window.electronAPI.openDownloadPage();
|
||||||
console.log('✅ Download page opened, launcher will close...');
|
console.log('✅ Download page opened');
|
||||||
|
|
||||||
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Launcher closing...';
|
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Opened in browser';
|
||||||
|
|
||||||
|
// Close the popup after opening download page
|
||||||
|
setTimeout(() => {
|
||||||
|
this.closeUpdatePopup();
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error opening download page:', error);
|
console.error('❌ Error opening download page:', error);
|
||||||
downloadBtn.disabled = false;
|
downloadBtn.disabled = false;
|
||||||
@@ -161,9 +208,39 @@ class ClientUpdateManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show skip button after 30 seconds as fallback (in case update is stuck)
|
||||||
|
setTimeout(() => {
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Update taking too long?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.closeUpdatePopup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🔔 Update popup displayed with new style');
|
console.log('🔔 Update popup displayed with new style');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeUpdatePopup() {
|
||||||
|
const overlay = document.getElementById('update-popup-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.remove();
|
||||||
|
}
|
||||||
|
this.updatePopupVisible = false;
|
||||||
|
this.unblockInterface();
|
||||||
|
}
|
||||||
|
|
||||||
updateDownloadProgress(progress) {
|
updateDownloadProgress(progress) {
|
||||||
const progressBar = document.getElementById('update-progress-bar');
|
const progressBar = document.getElementById('update-progress-bar');
|
||||||
const progressPercent = document.getElementById('update-progress-percent');
|
const progressPercent = document.getElementById('update-progress-percent');
|
||||||
@@ -197,35 +274,96 @@ class ClientUpdateManager {
|
|||||||
const statusText = document.getElementById('update-status-text');
|
const statusText = document.getElementById('update-status-text');
|
||||||
const progressContainer = document.getElementById('update-progress-container');
|
const progressContainer = document.getElementById('update-progress-container');
|
||||||
const buttonsContainer = document.getElementById('update-buttons-container');
|
const buttonsContainer = document.getElementById('update-buttons-container');
|
||||||
|
const installBtn = document.getElementById('update-install-btn');
|
||||||
|
const downloadBtn = document.getElementById('update-download-btn');
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
const popupContainer = document.querySelector('.update-popup-container');
|
||||||
|
|
||||||
if (statusText) {
|
// Remove breathing/pulse animation when download is complete
|
||||||
statusText.textContent = 'Update downloaded! Ready to install.';
|
if (popupContainer) {
|
||||||
|
popupContainer.classList.remove('update-popup-pulse');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressContainer) {
|
if (progressContainer) {
|
||||||
progressContainer.style.display = 'none';
|
progressContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use platform info from main process if available, fallback to browser detection
|
||||||
|
const autoInstallSupported = updateInfo.autoInstallSupported !== undefined
|
||||||
|
? updateInfo.autoInstallSupported
|
||||||
|
: navigator.platform.toUpperCase().indexOf('MAC') < 0;
|
||||||
|
|
||||||
|
if (!autoInstallSupported) {
|
||||||
|
// macOS: Show manual download as primary since auto-update doesn't work
|
||||||
|
if (statusText) {
|
||||||
|
statusText.textContent = 'Update downloaded but auto-install may not work on macOS.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installBtn) {
|
||||||
|
// Still show install button but as secondary option
|
||||||
|
installBtn.classList.add('update-download-btn-secondary');
|
||||||
|
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Install & Restart';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadBtn) {
|
||||||
|
// Make manual download primary
|
||||||
|
downloadBtn.classList.remove('update-download-btn-secondary');
|
||||||
|
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Manually (Recommended)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Auto-install often fails on macOS:';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Windows/Linux: Auto-install should work
|
||||||
|
if (statusText) {
|
||||||
|
statusText.textContent = 'Update downloaded! Ready to install.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Click to install the update:';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (buttonsContainer) {
|
if (buttonsContainer) {
|
||||||
buttonsContainer.style.display = 'block';
|
buttonsContainer.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Update downloaded, ready to install');
|
// Always show skip button in downloaded state
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
console.log('✅ Skip button made visible');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Skip button not found in DOM!');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Update downloaded, ready to install. autoInstallSupported:', autoInstallSupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateError(errorInfo) {
|
handleUpdateError(errorInfo) {
|
||||||
console.error('Update error:', errorInfo);
|
console.error('Update error:', errorInfo);
|
||||||
|
|
||||||
|
// Show skip button immediately on any error
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Update failed. You can skip for now.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If manual download is required, update the UI (this will handle status text)
|
// If manual download is required, update the UI (this will handle status text)
|
||||||
if (errorInfo.requiresManualDownload) {
|
if (errorInfo.requiresManualDownload) {
|
||||||
this.showManualDownloadRequired(errorInfo);
|
this.showManualDownloadRequired(errorInfo);
|
||||||
return; // Don't do anything else, showManualDownloadRequired handles everything
|
return; // Don't do anything else, showManualDownloadRequired handles everything
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-critical errors, just show error message without changing status
|
// For non-critical errors, just show error message without changing status
|
||||||
const errorMessage = document.getElementById('update-error-message');
|
const errorMessage = document.getElementById('update-error-message');
|
||||||
const errorText = document.getElementById('update-error-text');
|
const errorText = document.getElementById('update-error-text');
|
||||||
|
|
||||||
if (errorMessage && errorText) {
|
if (errorMessage && errorText) {
|
||||||
let message = errorInfo.message || 'An error occurred during the update process.';
|
let message = errorInfo.message || 'An error occurred during the update process.';
|
||||||
if (errorInfo.isMacSigningError) {
|
if (errorInfo.isMacSigningError) {
|
||||||
@@ -289,6 +427,16 @@ class ClientUpdateManager {
|
|||||||
buttonsContainer.style.display = 'block';
|
buttonsContainer.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show skip button for manual download errors
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Or continue without updating:';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('⚠️ Manual download required due to update error');
|
console.log('⚠️ Manual download required due to update error');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,13 +448,35 @@ class ClientUpdateManager {
|
|||||||
|
|
||||||
document.body.classList.add('no-select');
|
document.body.classList.add('no-select');
|
||||||
|
|
||||||
document.addEventListener('keydown', this.blockKeyEvents.bind(this), true);
|
// Store bound functions so we can remove them later
|
||||||
|
this._boundBlockKeyEvents = this.blockKeyEvents.bind(this);
|
||||||
document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true);
|
this._boundBlockContextMenu = this.blockContextMenu.bind(this);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', this._boundBlockKeyEvents, true);
|
||||||
|
document.addEventListener('contextmenu', this._boundBlockContextMenu, true);
|
||||||
|
|
||||||
console.log('🚫 Interface blocked for update');
|
console.log('🚫 Interface blocked for update');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unblockInterface() {
|
||||||
|
const mainContent = document.querySelector('.flex.w-full.h-screen');
|
||||||
|
if (mainContent) {
|
||||||
|
mainContent.classList.remove('interface-blocked');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.classList.remove('no-select');
|
||||||
|
|
||||||
|
// Remove event listeners
|
||||||
|
if (this._boundBlockKeyEvents) {
|
||||||
|
document.removeEventListener('keydown', this._boundBlockKeyEvents, true);
|
||||||
|
}
|
||||||
|
if (this._boundBlockContextMenu) {
|
||||||
|
document.removeEventListener('contextmenu', this._boundBlockContextMenu, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Interface unblocked');
|
||||||
|
}
|
||||||
|
|
||||||
blockKeyEvents(event) {
|
blockKeyEvents(event) {
|
||||||
if (event.target.closest('#update-popup-overlay')) {
|
if (event.target.closest('#update-popup-overlay')) {
|
||||||
if ((event.key === 'Enter' || event.key === ' ') &&
|
if ((event.key === 'Enter' || event.key === ' ') &&
|
||||||
|
|||||||
283
GUI/locales/de.json
Normal file
283
GUI/locales/de.json
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Spielen",
|
||||||
|
"mods": "Mods",
|
||||||
|
"news": "Neuigkeiten",
|
||||||
|
"chat": "Spieler-Chat",
|
||||||
|
"settings": "Einstellungen"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Spieler:",
|
||||||
|
"manageProfiles": "Profile verwalten",
|
||||||
|
"defaultProfile": "Standard"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "KOSTENLOSER LAUNCHER",
|
||||||
|
"playerName": "Spielername",
|
||||||
|
"playerNamePlaceholder": "Namen eingeben",
|
||||||
|
"gameBranch": "Spielversion",
|
||||||
|
"releaseVersion": "Release (Stabil)",
|
||||||
|
"preReleaseVersion": "Pre-Release (Experimentell)",
|
||||||
|
"customInstallation": "Benutzerdefinierte Installation",
|
||||||
|
"installationFolder": "Installationsordner",
|
||||||
|
"pathPlaceholder": "Standardspeicherort",
|
||||||
|
"browse": "Durchsuchen",
|
||||||
|
"installButton": "HYTALE INSTALLIEREN",
|
||||||
|
"installing": "INSTALLIERE..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "BEREIT ZUM SPIELEN",
|
||||||
|
"subtitle": "Starte Hytale und beginne das Abenteuer",
|
||||||
|
"playButton": "HYTALE SPIELEN",
|
||||||
|
"latestNews": "NEUESTE NACHRICHTEN",
|
||||||
|
"viewAll": "ALLE ANZEIGEN",
|
||||||
|
"checking": "ÜBERPRÜFE...",
|
||||||
|
"play": "SPIELEN"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Mods suchen...",
|
||||||
|
"myMods": "MEINE MODS",
|
||||||
|
"previous": "ZURÜCK",
|
||||||
|
"next": "WEITER",
|
||||||
|
"page": "Seite",
|
||||||
|
"of": "von",
|
||||||
|
"modalTitle": "MEINE MODS",
|
||||||
|
"noModsFound": "Keine Mods gefunden",
|
||||||
|
"noModsFoundDesc": "Versuche deine Suche anzupassen",
|
||||||
|
"noModsInstalled": "Keine Mods installiert",
|
||||||
|
"noModsInstalledDesc": "Füge Mods von CurseForge hinzu oder importiere lokale Dateien",
|
||||||
|
"view": "ANZEIGEN",
|
||||||
|
"install": "INSTALLIEREN",
|
||||||
|
"installed": "INSTALLIERT",
|
||||||
|
"enable": "AKTIVIEREN",
|
||||||
|
"disable": "DEAKTIVIEREN",
|
||||||
|
"active": "AKTIV",
|
||||||
|
"disabled": "DEAKTIVIERT",
|
||||||
|
"delete": "Mod löschen",
|
||||||
|
"noDescription": "Keine Beschreibung verfügbar",
|
||||||
|
"confirmDelete": "Möchtest du \"{name}\" wirklich löschen?",
|
||||||
|
"confirmDeleteDesc": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"confirmDeletion": "Löschung bestätigen",
|
||||||
|
"apiKeyRequired": "API-Schlüssel erforderlich",
|
||||||
|
"apiKeyRequiredDesc": "CurseForge API-Schlüssel wird benötigt, um Mods zu durchsuchen"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "ALLE NACHRICHTEN",
|
||||||
|
"readMore": "Mehr lesen"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "SPIELER-CHAT",
|
||||||
|
"pickColor": "Farbe",
|
||||||
|
"inputPlaceholder": "Nachricht eingeben...",
|
||||||
|
"send": "Senden",
|
||||||
|
"online": "online",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Sicherer Chat - Links werden zensiert",
|
||||||
|
"joinChat": "Chat beitreten",
|
||||||
|
"chooseUsername": "Wähle einen Benutzernamen, um dem Spieler-Chat beizutreten",
|
||||||
|
"username": "Benutzername",
|
||||||
|
"usernamePlaceholder": "Benutzernamen eingeben...",
|
||||||
|
"usernameHint": "3-20 Zeichen, nur Buchstaben, Zahlen, - und _",
|
||||||
|
"joinButton": "Chat beitreten",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Benutzernamenfarbe anpassen",
|
||||||
|
"chooseSolid": "Wähle eine einfarbige Farbe:",
|
||||||
|
"customColor": "Benutzerdefinierte Farbe:",
|
||||||
|
"preview": "Vorschau:",
|
||||||
|
"previewUsername": "Benutzername",
|
||||||
|
"apply": "Farbe anwenden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "EINSTELLUNGEN",
|
||||||
|
"java": "Java Runtime",
|
||||||
|
"useCustomJava": "Benutzerdefinierten Java-Pfad verwenden",
|
||||||
|
"javaDescription": "Ersetze die mitgelieferte Java-Installation durch deine eigene",
|
||||||
|
"javaPath": "Java-Ausführungsdatei-Pfad",
|
||||||
|
"javaPathPlaceholder": "Java-Pfad auswählen...",
|
||||||
|
"javaBrowse": "Durchsuchen",
|
||||||
|
"javaHint": "Wähle den Java-Installationsordner (unterstützt Windows, Mac, Linux)",
|
||||||
|
"discord": "Discord-Integration",
|
||||||
|
"enableRPC": "Discord Rich Presence aktivieren",
|
||||||
|
"discordDescription": "Zeige deine Launcher-Aktivität auf Discord",
|
||||||
|
"game": "Spieloptionen",
|
||||||
|
"playerName": "Spielername",
|
||||||
|
"playerNamePlaceholder": "Spielernamen eingeben",
|
||||||
|
"playerNameHint": "Dieser Name wird im Spiel verwendet (1-16 Zeichen)",
|
||||||
|
"openGameLocation": "Spielordner öffnen",
|
||||||
|
"openGameLocationDesc": "Öffne den Spielinstallationsordner",
|
||||||
|
"account": "Spieler-UUID-Verwaltung",
|
||||||
|
"currentUUID": "Aktuelle UUID",
|
||||||
|
"uuidPlaceholder": "UUID wird geladen...",
|
||||||
|
"copyUUID": "UUID kopieren",
|
||||||
|
"regenerateUUID": "UUID neu generieren",
|
||||||
|
"uuidHint": "Deine eindeutige Spielerkennung für diesen Benutzernamen",
|
||||||
|
"manageUUIDs": "Alle UUIDs verwalten",
|
||||||
|
"manageUUIDsDesc": "Alle Spieler-UUIDs anzeigen und verwalten",
|
||||||
|
"language": "Sprache",
|
||||||
|
"selectLanguage": "Sprache auswählen",
|
||||||
|
"repairGame": "Spiel reparieren",
|
||||||
|
"reinstallGame": "Spieldateien neu installieren (behält Daten)",
|
||||||
|
"gpuPreference": "GPU-Präferenz",
|
||||||
|
"gpuHint": "Wähle deine bevorzugte GPU (Linux: betrifft DRI_PRIME)",
|
||||||
|
"gpuAuto": "Auto",
|
||||||
|
"gpuIntegrated": "Integriert",
|
||||||
|
"gpuDedicated": "Dediziert",
|
||||||
|
"logs": "SYSTEMPROTOKOLLE",
|
||||||
|
"logsCopy": "Kopieren",
|
||||||
|
"logsRefresh": "Aktualisieren",
|
||||||
|
"logsFolder": "Ordner öffnen",
|
||||||
|
"logsLoading": "Protokolle werden geladen...",
|
||||||
|
"closeLauncher": "Launcher-Verhalten",
|
||||||
|
"closeOnStart": "Launcher beim Spielstart schließen",
|
||||||
|
"closeOnStartDescription": "Schließe den Launcher automatisch, nachdem Hytale gestartet wurde",
|
||||||
|
"hwAccel": "Hardware-Beschleunigung",
|
||||||
|
"hwAccelDescription": "Hardware-Beschleunigung für den Launcher aktivieren",
|
||||||
|
"gameBranch": "Spiel-Branch",
|
||||||
|
"branchRelease": "Release",
|
||||||
|
"branchPreRelease": "Pre-Release",
|
||||||
|
"branchHint": "Wechsel zwischen stabiler Release- und experimenteller Pre-Release-Version",
|
||||||
|
"branchWarning": "Das Ändern des Branches lädt eine andere Spielversion herunter und installiert sie",
|
||||||
|
"branchSwitching": "Wechsle zu {branch}...",
|
||||||
|
"branchSwitched": "Erfolgreich zu {branch} gewechselt!",
|
||||||
|
"installRequired": "Installation erforderlich",
|
||||||
|
"branchInstallConfirm": "Das Spiel wird für den {branch}-Branch installiert. Fortfahren?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "UUID-Verwaltung",
|
||||||
|
"currentUserUUID": "Aktuelle Benutzer-UUID",
|
||||||
|
"allPlayerUUIDs": "Alle Spieler-UUIDs",
|
||||||
|
"generateNew": "Neue UUID generieren",
|
||||||
|
"loadingUUIDs": "UUIDs werden geladen...",
|
||||||
|
"setCustomUUID": "Benutzerdefinierte UUID festlegen",
|
||||||
|
"customPlaceholder": "Benutzerdefinierte UUID eingeben (Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "UUID festlegen",
|
||||||
|
"warning": "Warnung: Das Festlegen einer benutzerdefinierten UUID ändert deine aktuelle Spieleridentität",
|
||||||
|
"copyTooltip": "UUID kopieren",
|
||||||
|
"regenerateTooltip": "Neue UUID generieren"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Profile verwalten",
|
||||||
|
"newProfilePlaceholder": "Neuer Profilname",
|
||||||
|
"createProfile": "Profil erstellen"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Tritt unserer Discord-Community bei!",
|
||||||
|
"joinButton": "Discord beitreten"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"save": "Speichern",
|
||||||
|
"close": "Schließen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"loading": "Lädt...",
|
||||||
|
"apply": "Anwenden",
|
||||||
|
"install": "Installieren"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Fehler: Spieldaten nicht gefunden",
|
||||||
|
"gameUpdatedSuccess": "Spiel erfolgreich aktualisiert! 🎉",
|
||||||
|
"updateFailed": "Update fehlgeschlagen: {error}",
|
||||||
|
"updateError": "Update-Fehler: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence aktiviert",
|
||||||
|
"discordDisabled": "Discord Rich Presence deaktiviert",
|
||||||
|
"discordSaveFailed": "Discord-Einstellung konnte nicht gespeichert werden",
|
||||||
|
"playerNameRequired": "Bitte gib einen gültigen Spielernamen ein",
|
||||||
|
"playerNameSaved": "Spielername erfolgreich gespeichert",
|
||||||
|
"playerNameSaveFailed": "Spielername konnte nicht gespeichert werden",
|
||||||
|
"uuidCopied": "UUID in die Zwischenablage kopiert!",
|
||||||
|
"uuidCopyFailed": "UUID konnte nicht kopiert werden",
|
||||||
|
"uuidRegenNotAvailable": "UUID-Neugenerierung nicht verfügbar",
|
||||||
|
"uuidRegenFailed": "UUID konnte nicht neu generiert werden",
|
||||||
|
"uuidGenerated": "Neue UUID erfolgreich generiert!",
|
||||||
|
"uuidGeneratedShort": "Neue UUID generiert!",
|
||||||
|
"uuidGenerateFailed": "Neue UUID konnte nicht generiert werden",
|
||||||
|
"uuidRequired": "Bitte gib eine UUID ein",
|
||||||
|
"uuidInvalidFormat": "Ungültiges UUID-Format",
|
||||||
|
"uuidSetFailed": "Benutzerdefinierte UUID konnte nicht festgelegt werden",
|
||||||
|
"uuidSetSuccess": "Benutzerdefinierte UUID erfolgreich festgelegt!",
|
||||||
|
"uuidDeleteFailed": "UUID konnte nicht gelöscht werden",
|
||||||
|
"uuidDeleteSuccess": "UUID erfolgreich gelöscht!",
|
||||||
|
"modsDownloading": "{name} wird heruntergeladen...",
|
||||||
|
"modsTogglingMod": "Mod wird umgeschaltet...",
|
||||||
|
"modsDeletingMod": "Mod wird gelöscht...",
|
||||||
|
"modsLoadingMods": "Mods von CurseForge werden geladen...",
|
||||||
|
"modsInstalledSuccess": "{name} erfolgreich installiert! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} erfolgreich gelöscht",
|
||||||
|
"modsDownloadFailed": "Mod konnte nicht heruntergeladen werden: {error}",
|
||||||
|
"modsToggleFailed": "Mod konnte nicht umgeschaltet werden: {error}",
|
||||||
|
"modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}",
|
||||||
|
"modsModNotFound": "Mod-Informationen nicht gefunden",
|
||||||
|
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
|
||||||
|
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden",
|
||||||
|
"javaPathCopied": "Java-Pfad in die Zwischenablage kopiert!",
|
||||||
|
"javaPathCopyFailed": "Java-Pfad konnte nicht kopiert werden",
|
||||||
|
"javaPathSaved": "Java-Pfad erfolgreich gespeichert!",
|
||||||
|
"javaPathSaveFailed": "Java-Pfad konnte nicht gespeichert werden",
|
||||||
|
"javaPathInvalid": "Ungültiger Java-Pfad",
|
||||||
|
"javaPathReset": "Java-Pfad auf Standardwerte zurückgesetzt",
|
||||||
|
"gameLocationError": "Spielordner konnte nicht geöffnet werden",
|
||||||
|
"launcherRestartRequired": "Launcher-Neustart erforderlich, um Änderungen anzuwenden",
|
||||||
|
"gameRepairConfirm": "Möchtest du das Spiel wirklich reparieren? Dies wird alle Spieldateien neu installieren.",
|
||||||
|
"gameRepairInProgress": "Spiel wird repariert...",
|
||||||
|
"gameRepairSuccess": "Spiel erfolgreich repariert!",
|
||||||
|
"gameRepairFailed": "Spielreparatur fehlgeschlagen: {error}",
|
||||||
|
"invalidUsername": "Ungültiger Benutzername",
|
||||||
|
"usernameInUse": "Benutzername bereits vergeben",
|
||||||
|
"chatJoinSuccess": "Du bist dem Chat beigetreten!",
|
||||||
|
"chatJoinFailed": "Chat-Beitritt fehlgeschlagen",
|
||||||
|
"messageTooLong": "Nachricht zu lang",
|
||||||
|
"messageSent": "Nachricht gesendet",
|
||||||
|
"messageSendFailed": "Nachricht konnte nicht gesendet werden",
|
||||||
|
"colorUpdated": "Farbe aktualisiert!",
|
||||||
|
"colorUpdateFailed": "Farbe konnte nicht aktualisiert werden",
|
||||||
|
"profileCreated": "Profil erfolgreich erstellt!",
|
||||||
|
"profileCreateFailed": "Profil konnte nicht erstellt werden",
|
||||||
|
"profileDeleted": "Profil gelöscht",
|
||||||
|
"profileDeleteFailed": "Profil konnte nicht gelöscht werden",
|
||||||
|
"profileSwitched": "Profil gewechselt zu: {name}",
|
||||||
|
"profileSwitchFailed": "Profilwechsel fehlgeschlagen",
|
||||||
|
"invalidProfileName": "Ungültiger Profilname",
|
||||||
|
"profileNameExists": "Ein Profil mit diesem Namen existiert bereits",
|
||||||
|
"noInternet": "Keine Internetverbindung",
|
||||||
|
"checkInternetConnection": "Überprüfe deine Internetverbindung",
|
||||||
|
"serverError": "Serverfehler. Bitte versuche es später erneut.",
|
||||||
|
"unknownError": "Ein unbekannter Fehler ist aufgetreten"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Aktion bestätigen",
|
||||||
|
"regenerateUuidTitle": "Neue UUID generieren",
|
||||||
|
"regenerateUuidMessage": "Möchtest du wirklich eine neue UUID generieren? Dies ändert deine Spieleridentität.",
|
||||||
|
"regenerateUuidButton": "Generieren",
|
||||||
|
"setCustomUuidTitle": "Benutzerdefinierte UUID festlegen",
|
||||||
|
"setCustomUuidMessage": "Möchtest du wirklich diese benutzerdefinierte UUID festlegen? Dies ändert deine Spieleridentität.",
|
||||||
|
"setCustomUuidButton": "UUID festlegen",
|
||||||
|
"deleteUuidTitle": "UUID löschen",
|
||||||
|
"deleteUuidMessage": "Möchtest du wirklich die UUID für \"{username}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"deleteUuidButton": "Löschen",
|
||||||
|
"uninstallGameTitle": "Spiel deinstallieren",
|
||||||
|
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
|
||||||
|
"uninstallGameButton": "Deinstallieren"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Initialisiere...",
|
||||||
|
"downloading": "Lädt herunter...",
|
||||||
|
"installing": "Installiere...",
|
||||||
|
"extracting": "Entpacke...",
|
||||||
|
"verifying": "Überprüfe...",
|
||||||
|
"switchingProfile": "Profil wird gewechselt...",
|
||||||
|
"profileSwitched": "Profil gewechselt!",
|
||||||
|
"startingGame": "Spiel wird gestartet...",
|
||||||
|
"launching": "STARTET...",
|
||||||
|
"uninstallingGame": "Spiel wird deinstalliert...",
|
||||||
|
"gameUninstalled": "Spiel erfolgreich deinstalliert!",
|
||||||
|
"uninstallFailed": "Deinstallation fehlgeschlagen: {error}",
|
||||||
|
"startingUpdate": "Obligatorisches Spiel-Update wird gestartet...",
|
||||||
|
"installationComplete": "Installation erfolgreich abgeschlossen!",
|
||||||
|
"installationFailed": "Installation fehlgeschlagen: {error}",
|
||||||
|
"installingGameFiles": "Spieldateien werden installiert...",
|
||||||
|
"installComplete": "Installation abgeschlossen!"
|
||||||
|
}
|
||||||
|
}
|
||||||
235
GUI/locales/fr.json
Normal file
235
GUI/locales/fr.json
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Jouer",
|
||||||
|
"mods": "Mods",
|
||||||
|
"news": "Actualités",
|
||||||
|
"chat": "Chat Joueurs",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Joueurs:",
|
||||||
|
"manageProfiles": "Gérer les Profils",
|
||||||
|
"defaultProfile": "Par défaut"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "LAUNCHER GRATUIT",
|
||||||
|
"playerName": "Nom du Joueur",
|
||||||
|
"playerNamePlaceholder": "Entrez votre nom",
|
||||||
|
"gameBranch": "Version du Jeu",
|
||||||
|
"releaseVersion": "Release (Stable)",
|
||||||
|
"preReleaseVersion": "Pré-Release (Expérimental)",
|
||||||
|
"customInstallation": "Installation Personnalisée",
|
||||||
|
"installationFolder": "Dossier d'Installation",
|
||||||
|
"pathPlaceholder": "Emplacement par défaut",
|
||||||
|
"browse": "Parcourir",
|
||||||
|
"installButton": "INSTALLER HYTALE",
|
||||||
|
"installing": "INSTALLATION..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "PRÊT À JOUER",
|
||||||
|
"subtitle": "Lancez Hytale et entrez dans l'aventure",
|
||||||
|
"playButton": "JOUER À HYTALE",
|
||||||
|
"latestNews": "DERNIÈRES ACTUALITÉS",
|
||||||
|
"viewAll": "VOIR TOUT",
|
||||||
|
"checking": "VÉRIFICATION...",
|
||||||
|
"play": "JOUER"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Rechercher des mods...",
|
||||||
|
"myMods": "MES MODS",
|
||||||
|
"previous": "PRÉCÉDENT",
|
||||||
|
"next": "SUIVANT",
|
||||||
|
"page": "Page",
|
||||||
|
"of": "sur",
|
||||||
|
"modalTitle": "MES MODS",
|
||||||
|
"noModsFound": "Aucun Mod Trouvé",
|
||||||
|
"noModsFoundDesc": "Essayez d'ajuster votre recherche",
|
||||||
|
"noModsInstalled": "Aucun Mod Installé",
|
||||||
|
"noModsInstalledDesc": "Ajoutez des mods depuis CurseForge ou importez des fichiers locaux",
|
||||||
|
"view": "VOIR",
|
||||||
|
"install": "INSTALLER",
|
||||||
|
"installed": "INSTALLÉ",
|
||||||
|
"enable": "ACTIVER",
|
||||||
|
"disable": "DÉSACTIVER",
|
||||||
|
"active": "ACTIF",
|
||||||
|
"disabled": "DÉSACTIVÉ",
|
||||||
|
"delete": "Supprimer le mod",
|
||||||
|
"noDescription": "Aucune description disponible",
|
||||||
|
"confirmDelete": "Êtes-vous sûr de vouloir supprimer \"{name}\" ?",
|
||||||
|
"confirmDeleteDesc": "Cette action est irréversible.",
|
||||||
|
"confirmDeletion": "Confirmer la Suppression",
|
||||||
|
"apiKeyRequired": "Clé API Requise",
|
||||||
|
"apiKeyRequiredDesc": "Une clé API CurseForge est nécessaire pour parcourir les mods"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "TOUTES LES ACTUALITÉS",
|
||||||
|
"readMore": "Lire Plus"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "CHAT JOUEURS",
|
||||||
|
"pickColor": "Couleur",
|
||||||
|
"inputPlaceholder": "Tapez votre message...",
|
||||||
|
"send": "Envoyer",
|
||||||
|
"online": "en ligne",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Chat sécurisé - Les liens sont censurés",
|
||||||
|
"joinChat": "Rejoindre le Chat",
|
||||||
|
"chooseUsername": "Choisissez un nom d'utilisateur pour rejoindre le Chat Joueurs",
|
||||||
|
"username": "Nom d'utilisateur",
|
||||||
|
"usernamePlaceholder": "Entrez votre nom d'utilisateur...",
|
||||||
|
"usernameHint": "3-20 caractères, lettres, chiffres, - et _ uniquement",
|
||||||
|
"joinButton": "Rejoindre le Chat",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Personnaliser la Couleur du Nom",
|
||||||
|
"chooseSolid": "Choisissez une couleur unie:",
|
||||||
|
"customColor": "Couleur personnalisée:",
|
||||||
|
"preview": "Aperçu:",
|
||||||
|
"previewUsername": "Nom d'utilisateur",
|
||||||
|
"apply": "Appliquer la Couleur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "PARAMÈTRES",
|
||||||
|
"java": "Java Runtime",
|
||||||
|
"useCustomJava": "Utiliser un Chemin Java Personnalisé",
|
||||||
|
"javaDescription": "Remplacer le Java intégré par votre propre installation",
|
||||||
|
"javaPath": "Chemin de l'Exécutable Java",
|
||||||
|
"javaPathPlaceholder": "Sélectionnez le chemin Java...",
|
||||||
|
"javaBrowse": "Parcourir",
|
||||||
|
"javaHint": "Sélectionnez le dossier d'installation de Java (compatible Windows, Mac, Linux)",
|
||||||
|
"discord": "Intégration Discord",
|
||||||
|
"enableRPC": "Activer Discord Rich Presence",
|
||||||
|
"discordDescription": "Afficher votre activité du launcher sur Discord",
|
||||||
|
"game": "Options de Jeu",
|
||||||
|
"playerName": "Nom du Joueur",
|
||||||
|
"playerNamePlaceholder": "Entrez le nom du joueur",
|
||||||
|
"playerNameHint": "Ce nom sera utilisé en jeu (1-16 caractères)",
|
||||||
|
"openGameLocation": "Ouvrir l'Emplacement du Jeu",
|
||||||
|
"openGameLocationDesc": "Ouvrir le dossier d'installation du jeu",
|
||||||
|
"account": "Gestion UUID Joueur",
|
||||||
|
"currentUUID": "UUID Actuel",
|
||||||
|
"uuidPlaceholder": "Chargement UUID...",
|
||||||
|
"copyUUID": "Copier UUID",
|
||||||
|
"regenerateUUID": "Régénérer UUID",
|
||||||
|
"uuidHint": "Votre identifiant unique de joueur pour ce nom d'utilisateur",
|
||||||
|
"manageUUIDs": "Gérer Tous les UUIDs",
|
||||||
|
"manageUUIDsDesc": "Voir et gérer tous les UUIDs de joueurs",
|
||||||
|
"language": "Langue",
|
||||||
|
"selectLanguage": "Sélectionner la Langue",
|
||||||
|
"repairGame": "Réparer le Jeu",
|
||||||
|
"reinstallGame": "Réinstaller les fichiers du jeu (préserve les données)",
|
||||||
|
"gpuPreference": "Préférence GPU",
|
||||||
|
"gpuHint": "Sélectionnez votre GPU préféré (Linux: affecte DRI_PRIME)",
|
||||||
|
"gpuAuto": "Auto",
|
||||||
|
"gpuIntegrated": "Intégré",
|
||||||
|
"gpuDedicated": "Dédié",
|
||||||
|
"logs": "JOURNAUX SYSTÈME",
|
||||||
|
"logsCopy": "Copier",
|
||||||
|
"logsRefresh": "Actualiser",
|
||||||
|
"logsFolder": "Ouvrir le Dossier",
|
||||||
|
"logsLoading": "Chargement des journaux...",
|
||||||
|
"closeLauncher": "Comportement du Launcher",
|
||||||
|
"closeOnStart": "Fermer le Launcher au démarrage du jeu",
|
||||||
|
"closeOnStartDescription": "Fermer automatiquement le launcher après le lancement d'Hytale",
|
||||||
|
"hwAccel": "Accélération Matérielle",
|
||||||
|
"hwAccelDescription": "Activer l'accélération matérielle pour le launcher",
|
||||||
|
"gameBranch": "Branche du Jeu",
|
||||||
|
"branchRelease": "Release",
|
||||||
|
"branchPreRelease": "Pré-Release",
|
||||||
|
"branchHint": "Basculer entre la version stable release et la pré-release expérimentale",
|
||||||
|
"branchWarning": "Changer de branche téléchargera et installera une version différente du jeu",
|
||||||
|
"branchSwitching": "Passage à {branch}...",
|
||||||
|
"branchSwitched": "Passage à {branch} réussi!",
|
||||||
|
"installRequired": "Installation Requise",
|
||||||
|
"branchInstallConfirm": "Le jeu sera installé pour la branche {branch}. Continuer?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "Gestion UUID",
|
||||||
|
"currentUserUUID": "UUID Utilisateur Actuel",
|
||||||
|
"allPlayerUUIDs": "Tous les UUIDs Joueurs",
|
||||||
|
"generateNew": "Générer Nouvel UUID",
|
||||||
|
"loadingUUIDs": "Chargement des UUIDs...",
|
||||||
|
"setCustomUUID": "Définir UUID Personnalisé",
|
||||||
|
"customPlaceholder": "Entrez UUID personnalisé (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Définir UUID",
|
||||||
|
"warning": "Attention: Définir un UUID personnalisé changera votre identité de joueur actuelle",
|
||||||
|
"copyTooltip": "Copier UUID",
|
||||||
|
"regenerateTooltip": "Générer Nouvel UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Gérer les Profils",
|
||||||
|
"newProfilePlaceholder": "Nom du Nouveau Profil",
|
||||||
|
"createProfile": "Créer un Profil"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Rejoignez notre communauté Discord!",
|
||||||
|
"joinButton": "Rejoindre Discord"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"save": "Sauvegarder",
|
||||||
|
"close": "Fermer",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"apply": "Appliquer",
|
||||||
|
"install": "Installer"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Erreur: Données du jeu introuvables",
|
||||||
|
"gameUpdatedSuccess": "Jeu mis à jour avec succès! 🎉",
|
||||||
|
"updateFailed": "Mise à jour échouée: {error}",
|
||||||
|
"updateError": "Erreur de mise à jour: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence activé",
|
||||||
|
"discordDisabled": "Discord Rich Presence désactivé",
|
||||||
|
"discordSaveFailed": "Échec de la sauvegarde des paramètres Discord",
|
||||||
|
"playerNameRequired": "Veuillez entrer un nom de joueur valide",
|
||||||
|
"playerNameSaved": "Nom du joueur sauvegardé avec succès",
|
||||||
|
"playerNameSaveFailed": "Échec de la sauvegarde du nom du joueur",
|
||||||
|
"uuidCopied": "UUID copié dans le presse-papiers!",
|
||||||
|
"uuidCopyFailed": "Échec de la copie de l'UUID",
|
||||||
|
"uuidRegenNotAvailable": "Régénération UUID non disponible",
|
||||||
|
"uuidRegenFailed": "Échec de la régénération de l'UUID",
|
||||||
|
"uuidGenerated": "Nouvel UUID généré avec succès!",
|
||||||
|
"uuidGeneratedShort": "Nouvel UUID généré!",
|
||||||
|
"uuidGenerateFailed": "Échec de la génération du nouvel UUID",
|
||||||
|
"uuidRequired": "Veuillez entrer un UUID",
|
||||||
|
"uuidInvalidFormat": "Format UUID invalide",
|
||||||
|
"uuidSetFailed": "Échec de la définition de l'UUID personnalisé",
|
||||||
|
"uuidSetSuccess": "UUID personnalisé défini avec succès!",
|
||||||
|
"javaPathCopied": "Chemin Java copié dans le presse-papiers!",
|
||||||
|
"javaPathCopyFailed": "Échec de la copie du chemin Java",
|
||||||
|
"javaPathSaved": "Chemin Java sauvegardé avec succès!",
|
||||||
|
"javaPathSaveFailed": "Échec de la sauvegarde du chemin Java",
|
||||||
|
"javaPathInvalid": "Chemin Java invalide",
|
||||||
|
"javaPathReset": "Chemin Java réinitialisé aux valeurs par défaut",
|
||||||
|
"gameLocationError": "Impossible d'ouvrir l'emplacement du jeu",
|
||||||
|
"launcherRestartRequired": "Redémarrage du launcher requis pour appliquer les modifications",
|
||||||
|
"gameRepairConfirm": "Êtes-vous sûr de vouloir réparer le jeu? Cela réinstallera tous les fichiers du jeu.",
|
||||||
|
"gameRepairInProgress": "Réparation du jeu en cours...",
|
||||||
|
"gameRepairSuccess": "Jeu réparé avec succès!",
|
||||||
|
"gameRepairFailed": "Échec de la réparation du jeu: {error}",
|
||||||
|
"invalidUsername": "Nom d'utilisateur invalide",
|
||||||
|
"usernameInUse": "Nom d'utilisateur déjà utilisé",
|
||||||
|
"chatJoinSuccess": "Vous avez rejoint le chat!",
|
||||||
|
"chatJoinFailed": "Échec de la connexion au chat",
|
||||||
|
"messageTooLong": "Message trop long",
|
||||||
|
"messageSent": "Message envoyé",
|
||||||
|
"messageSendFailed": "Échec de l'envoi du message",
|
||||||
|
"colorUpdated": "Couleur mise à jour!",
|
||||||
|
"colorUpdateFailed": "Échec de la mise à jour de la couleur",
|
||||||
|
"profileCreated": "Profil créé avec succès!",
|
||||||
|
"profileCreateFailed": "Échec de la création du profil",
|
||||||
|
"profileDeleted": "Profil supprimé",
|
||||||
|
"profileDeleteFailed": "Échec de la suppression du profil",
|
||||||
|
"profileSwitched": "Profil changé vers: {name}",
|
||||||
|
"profileSwitchFailed": "Échec du changement de profil",
|
||||||
|
"invalidProfileName": "Nom de profil invalide",
|
||||||
|
"profileNameExists": "Un profil avec ce nom existe déjà",
|
||||||
|
"noInternet": "Pas de connexion Internet",
|
||||||
|
"checkInternetConnection": "Vérifiez votre connexion Internet",
|
||||||
|
"serverError": "Erreur serveur. Veuillez réessayer plus tard.",
|
||||||
|
"unknownError": "Une erreur inconnue s'est produite"
|
||||||
|
}
|
||||||
|
}
|
||||||
283
GUI/locales/sv.json
Normal file
283
GUI/locales/sv.json
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Spela",
|
||||||
|
"mods": "Moddar",
|
||||||
|
"news": "Nyheter",
|
||||||
|
"chat": "Spelarchatt",
|
||||||
|
"settings": "Inställningar"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Spelare:",
|
||||||
|
"manageProfiles": "Hantera profiler",
|
||||||
|
"defaultProfile": "Standard"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "GRATIS LAUNCHER",
|
||||||
|
"playerName": "Spelarnamn",
|
||||||
|
"playerNamePlaceholder": "Ange ditt namn",
|
||||||
|
"gameBranch": "Spelversion",
|
||||||
|
"releaseVersion": "Release (Stabil)",
|
||||||
|
"preReleaseVersion": "Pre-Release (Experimentell)",
|
||||||
|
"customInstallation": "Anpassad installation",
|
||||||
|
"installationFolder": "Installationsmapp",
|
||||||
|
"pathPlaceholder": "Standardplats",
|
||||||
|
"browse": "Bläddra",
|
||||||
|
"installButton": "INSTALLERA HYTALE",
|
||||||
|
"installing": "INSTALLERAR..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "REDO ATT SPELA",
|
||||||
|
"subtitle": "Starta Hytale och börja äventyret",
|
||||||
|
"playButton": "SPELA HYTALE",
|
||||||
|
"latestNews": "SENASTE NYHETERNA",
|
||||||
|
"viewAll": "VISA ALLA",
|
||||||
|
"checking": "KONTROLLERAR...",
|
||||||
|
"play": "SPELA"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Sök moddar...",
|
||||||
|
"myMods": "MINA MODDAR",
|
||||||
|
"previous": "FÖREGÅENDE",
|
||||||
|
"next": "NÄSTA",
|
||||||
|
"page": "Sida",
|
||||||
|
"of": "av",
|
||||||
|
"modalTitle": "MINA MODDAR",
|
||||||
|
"noModsFound": "Inga moddar hittades",
|
||||||
|
"noModsFoundDesc": "Försök justera din sökning",
|
||||||
|
"noModsInstalled": "Inga moddar installerade",
|
||||||
|
"noModsInstalledDesc": "Lägg till moddar från CurseForge eller importera lokala filer",
|
||||||
|
"view": "VISA",
|
||||||
|
"install": "INSTALLERA",
|
||||||
|
"installed": "INSTALLERAD",
|
||||||
|
"enable": "AKTIVERA",
|
||||||
|
"disable": "INAKTIVERA",
|
||||||
|
"active": "AKTIV",
|
||||||
|
"disabled": "INAKTIVERAD",
|
||||||
|
"delete": "Ta bort modd",
|
||||||
|
"noDescription": "Ingen beskrivning tillgänglig",
|
||||||
|
"confirmDelete": "Är du säker på att du vill ta bort \"{name}\"?",
|
||||||
|
"confirmDeleteDesc": "Denna åtgärd kan inte ångras.",
|
||||||
|
"confirmDeletion": "Bekräfta borttagning",
|
||||||
|
"apiKeyRequired": "API-nyckel krävs",
|
||||||
|
"apiKeyRequiredDesc": "CurseForge API-nyckel behövs för att bläddra bland moddar"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "ALLA NYHETER",
|
||||||
|
"readMore": "Läs mer"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "SPELARCHATT",
|
||||||
|
"pickColor": "Färg",
|
||||||
|
"inputPlaceholder": "Skriv ditt meddelande...",
|
||||||
|
"send": "Skicka",
|
||||||
|
"online": "online",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Säker chatt - Länkar är censurerade",
|
||||||
|
"joinChat": "Gå med i chatten",
|
||||||
|
"chooseUsername": "Välj ett användarnamn för att gå med i spelarchartten",
|
||||||
|
"username": "Användarnamn",
|
||||||
|
"usernamePlaceholder": "Ange ditt användarnamn...",
|
||||||
|
"usernameHint": "3-20 tecken, endast bokstäver, siffror, - och _",
|
||||||
|
"joinButton": "Gå med i chatten",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Anpassa användarnamnsfargen",
|
||||||
|
"chooseSolid": "Välj en enfärgad färg:",
|
||||||
|
"customColor": "Anpassad färg:",
|
||||||
|
"preview": "Förhandsvisning:",
|
||||||
|
"previewUsername": "Användarnamn",
|
||||||
|
"apply": "Använd färg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "INSTÄLLNINGAR",
|
||||||
|
"java": "Java Runtime",
|
||||||
|
"useCustomJava": "Använd anpassad Java-sökväg",
|
||||||
|
"javaDescription": "Ersätt den medföljande Java-installationen med din egen",
|
||||||
|
"javaPath": "Java-körbar fil-sökväg",
|
||||||
|
"javaPathPlaceholder": "Välj Java-sökväg...",
|
||||||
|
"javaBrowse": "Bläddra",
|
||||||
|
"javaHint": "Välj Java-installationsmappen (stöder Windows, Mac, Linux)",
|
||||||
|
"discord": "Discord-integration",
|
||||||
|
"enableRPC": "Aktivera Discord Rich Presence",
|
||||||
|
"discordDescription": "Visa din launcher-aktivitet på Discord",
|
||||||
|
"game": "Spelalternativ",
|
||||||
|
"playerName": "Spelarnamn",
|
||||||
|
"playerNamePlaceholder": "Ange spelarnamn",
|
||||||
|
"playerNameHint": "Detta namn kommer att användas i spelet (1-16 tecken)",
|
||||||
|
"openGameLocation": "Öppna spelplats",
|
||||||
|
"openGameLocationDesc": "Öppna spelinstallationsmappen",
|
||||||
|
"account": "Spelare UUID-hantering",
|
||||||
|
"currentUUID": "Nuvarande UUID",
|
||||||
|
"uuidPlaceholder": "Laddar UUID...",
|
||||||
|
"copyUUID": "Kopiera UUID",
|
||||||
|
"regenerateUUID": "Återskapa UUID",
|
||||||
|
"uuidHint": "Din unika spelaridentifierare för detta användarnamn",
|
||||||
|
"manageUUIDs": "Hantera alla UUID:er",
|
||||||
|
"manageUUIDsDesc": "Visa och hantera alla spelare-UUID:er",
|
||||||
|
"language": "Språk",
|
||||||
|
"selectLanguage": "Välj språk",
|
||||||
|
"repairGame": "Reparera spel",
|
||||||
|
"reinstallGame": "Ominstallera spelfiler (bevarar data)",
|
||||||
|
"gpuPreference": "GPU-preferens",
|
||||||
|
"gpuHint": "Välj din föredragna GPU (Linux: påverkar DRI_PRIME)",
|
||||||
|
"gpuAuto": "Auto",
|
||||||
|
"gpuIntegrated": "Integrerad",
|
||||||
|
"gpuDedicated": "Dedikerad",
|
||||||
|
"logs": "SYSTEMLOGGAR",
|
||||||
|
"logsCopy": "Kopiera",
|
||||||
|
"logsRefresh": "Uppdatera",
|
||||||
|
"logsFolder": "Öppna mapp",
|
||||||
|
"logsLoading": "Laddar loggar...",
|
||||||
|
"closeLauncher": "Launcher-beteende",
|
||||||
|
"closeOnStart": "Stäng launcher vid spelstart",
|
||||||
|
"closeOnStartDescription": "Stäng automatiskt launcher efter att Hytale har startats",
|
||||||
|
"hwAccel": "Hårdvaruacceleration",
|
||||||
|
"hwAccelDescription": "Aktivera hårdvaruacceleration för launchern",
|
||||||
|
"gameBranch": "Spelgren",
|
||||||
|
"branchRelease": "Release",
|
||||||
|
"branchPreRelease": "Pre-Release",
|
||||||
|
"branchHint": "Växla mellan stabil release- och experimentell pre-release-version",
|
||||||
|
"branchWarning": "Att byta gren kommer att ladda ner och installera en annan spelversion",
|
||||||
|
"branchSwitching": "Byter till {branch}...",
|
||||||
|
"branchSwitched": "Bytte framgångsrikt till {branch}!",
|
||||||
|
"installRequired": "Installation krävs",
|
||||||
|
"branchInstallConfirm": "Spelet kommer att installeras för {branch}-grenen. Fortsätt?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "UUID-hantering",
|
||||||
|
"currentUserUUID": "Nuvarande användar-UUID",
|
||||||
|
"allPlayerUUIDs": "Alla spelare-UUID:er",
|
||||||
|
"generateNew": "Generera ny UUID",
|
||||||
|
"loadingUUIDs": "Laddar UUID:er...",
|
||||||
|
"setCustomUUID": "Ange anpassad UUID",
|
||||||
|
"customPlaceholder": "Ange anpassad UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Ange UUID",
|
||||||
|
"warning": "Varning: Att ange en anpassad UUID kommer att ändra din nuvarande spelaridentitet",
|
||||||
|
"copyTooltip": "Kopiera UUID",
|
||||||
|
"regenerateTooltip": "Generera ny UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Hantera profiler",
|
||||||
|
"newProfilePlaceholder": "Nytt profilnamn",
|
||||||
|
"createProfile": "Skapa profil"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Gå med i vår Discord-gemenskap!",
|
||||||
|
"joinButton": "Gå med i Discord"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Bekräfta",
|
||||||
|
"cancel": "Avbryt",
|
||||||
|
"save": "Spara",
|
||||||
|
"close": "Stäng",
|
||||||
|
"delete": "Ta bort",
|
||||||
|
"edit": "Redigera",
|
||||||
|
"loading": "Laddar...",
|
||||||
|
"apply": "Verkställ",
|
||||||
|
"install": "Installera"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Fel: Speldata hittades inte",
|
||||||
|
"gameUpdatedSuccess": "Spelet uppdaterades framgångsrikt! 🎉",
|
||||||
|
"updateFailed": "Uppdatering misslyckades: {error}",
|
||||||
|
"updateError": "Uppdateringsfel: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence aktiverad",
|
||||||
|
"discordDisabled": "Discord Rich Presence inaktiverad",
|
||||||
|
"discordSaveFailed": "Misslyckades med att spara Discord-inställning",
|
||||||
|
"playerNameRequired": "Ange ett giltigt spelarnamn",
|
||||||
|
"playerNameSaved": "Spelarnamn sparat framgångsrikt",
|
||||||
|
"playerNameSaveFailed": "Misslyckades med att spara spelarnamn",
|
||||||
|
"uuidCopied": "UUID kopierad till urklipp!",
|
||||||
|
"uuidCopyFailed": "Misslyckades med att kopiera UUID",
|
||||||
|
"uuidRegenNotAvailable": "UUID-återgenerering ej tillgänglig",
|
||||||
|
"uuidRegenFailed": "Misslyckades med att återgenerera UUID",
|
||||||
|
"uuidGenerated": "Ny UUID genererad framgångsrikt!",
|
||||||
|
"uuidGeneratedShort": "Ny UUID genererad!",
|
||||||
|
"uuidGenerateFailed": "Misslyckades med att generera ny UUID",
|
||||||
|
"uuidRequired": "Ange en UUID",
|
||||||
|
"uuidInvalidFormat": "Ogiltigt UUID-format",
|
||||||
|
"uuidSetFailed": "Misslyckades med att ange anpassad UUID",
|
||||||
|
"uuidSetSuccess": "Anpassad UUID angiven framgångsrikt!",
|
||||||
|
"uuidDeleteFailed": "Misslyckades med att ta bort UUID",
|
||||||
|
"uuidDeleteSuccess": "UUID borttagen framgångsrikt!",
|
||||||
|
"modsDownloading": "Laddar ner {name}...",
|
||||||
|
"modsTogglingMod": "Växlar modd...",
|
||||||
|
"modsDeletingMod": "Tar bort modd...",
|
||||||
|
"modsLoadingMods": "Laddar moddar från CurseForge...",
|
||||||
|
"modsInstalledSuccess": "{name} installerad framgångsrikt! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} borttagen framgångsrikt",
|
||||||
|
"modsDownloadFailed": "Misslyckades med att ladda ner modd: {error}",
|
||||||
|
"modsToggleFailed": "Misslyckades med att växla modd: {error}",
|
||||||
|
"modsDeleteFailed": "Misslyckades med att ta bort modd: {error}",
|
||||||
|
"modsModNotFound": "Moddinformation hittades inte",
|
||||||
|
"hwAccelSaved": "Hårdvaruaccelerationsinställning sparad",
|
||||||
|
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning",
|
||||||
|
"javaPathCopied": "Java-sökväg kopierad till urklipp!",
|
||||||
|
"javaPathCopyFailed": "Misslyckades med att kopiera Java-sökväg",
|
||||||
|
"javaPathSaved": "Java-sökväg sparad framgångsrikt!",
|
||||||
|
"javaPathSaveFailed": "Misslyckades med att spara Java-sökväg",
|
||||||
|
"javaPathInvalid": "Ogiltig Java-sökväg",
|
||||||
|
"javaPathReset": "Java-sökväg återställd till standardvärden",
|
||||||
|
"gameLocationError": "Kunde inte öppna spelplats",
|
||||||
|
"launcherRestartRequired": "Launcher-omstart krävs för att tillämpa ändringar",
|
||||||
|
"gameRepairConfirm": "Är du säker på att du vill reparera spelet? Detta kommer att ominstallera alla spelfiler.",
|
||||||
|
"gameRepairInProgress": "Reparerar spel...",
|
||||||
|
"gameRepairSuccess": "Spel reparerat framgångsrikt!",
|
||||||
|
"gameRepairFailed": "Spelreparation misslyckades: {error}",
|
||||||
|
"invalidUsername": "Ogiltigt användarnamn",
|
||||||
|
"usernameInUse": "Användarnamn upptaget",
|
||||||
|
"chatJoinSuccess": "Du har gått med i chatten!",
|
||||||
|
"chatJoinFailed": "Misslyckades med att gå med i chatten",
|
||||||
|
"messageTooLong": "Meddelande för långt",
|
||||||
|
"messageSent": "Meddelande skickat",
|
||||||
|
"messageSendFailed": "Misslyckades med att skicka meddelande",
|
||||||
|
"colorUpdated": "Färg uppdaterad!",
|
||||||
|
"colorUpdateFailed": "Misslyckades med att uppdatera färg",
|
||||||
|
"profileCreated": "Profil skapad framgångsrikt!",
|
||||||
|
"profileCreateFailed": "Misslyckades med att skapa profil",
|
||||||
|
"profileDeleted": "Profil borttagen",
|
||||||
|
"profileDeleteFailed": "Misslyckades med att ta bort profil",
|
||||||
|
"profileSwitched": "Bytte profil till: {name}",
|
||||||
|
"profileSwitchFailed": "Profilbyte misslyckades",
|
||||||
|
"invalidProfileName": "Ogiltigt profilnamn",
|
||||||
|
"profileNameExists": "En profil med detta namn finns redan",
|
||||||
|
"noInternet": "Ingen internetanslutning",
|
||||||
|
"checkInternetConnection": "Kontrollera din internetanslutning",
|
||||||
|
"serverError": "Serverfel. Försök igen senare.",
|
||||||
|
"unknownError": "Ett okänt fel inträffade"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Bekräfta åtgärd",
|
||||||
|
"regenerateUuidTitle": "Generera ny UUID",
|
||||||
|
"regenerateUuidMessage": "Är du säker på att du vill generera en ny UUID? Detta kommer att ändra din spelaridentitet.",
|
||||||
|
"regenerateUuidButton": "Generera",
|
||||||
|
"setCustomUuidTitle": "Ange anpassad UUID",
|
||||||
|
"setCustomUuidMessage": "Är du säker på att du vill ange denna anpassade UUID? Detta kommer att ändra din spelaridentitet.",
|
||||||
|
"setCustomUuidButton": "Ange UUID",
|
||||||
|
"deleteUuidTitle": "Ta bort UUID",
|
||||||
|
"deleteUuidMessage": "Är du säker på att du vill ta bort UUID:n för \"{username}\"? Denna åtgärd kan inte ångras.",
|
||||||
|
"deleteUuidButton": "Ta bort",
|
||||||
|
"uninstallGameTitle": "Avinstallera spel",
|
||||||
|
"uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.",
|
||||||
|
"uninstallGameButton": "Avinstallera"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Initierar...",
|
||||||
|
"downloading": "Laddar ner...",
|
||||||
|
"installing": "Installerar...",
|
||||||
|
"extracting": "Extraherar...",
|
||||||
|
"verifying": "Verifierar...",
|
||||||
|
"switchingProfile": "Byter profil...",
|
||||||
|
"profileSwitched": "Profil bytt!",
|
||||||
|
"startingGame": "Startar spel...",
|
||||||
|
"launching": "STARTAR...",
|
||||||
|
"uninstallingGame": "Avinstallerar spel...",
|
||||||
|
"gameUninstalled": "Spel avinstallerat framgångsrikt!",
|
||||||
|
"uninstallFailed": "Avinstallation misslyckades: {error}",
|
||||||
|
"startingUpdate": "Startar obligatorisk speluppdatering...",
|
||||||
|
"installationComplete": "Installation slutförd framgångsrikt!",
|
||||||
|
"installationFailed": "Installation misslyckades: {error}",
|
||||||
|
"installingGameFiles": "Installerar spelfiler...",
|
||||||
|
"installComplete": "Installation slutförd!"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
PKGBUILD
12
PKGBUILD
@@ -14,15 +14,15 @@ source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
|
|||||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$_pkgname-$pkgver"
|
cd "$pkgname-$pkgver"
|
||||||
npm ci
|
npm ci
|
||||||
npm run build:arch
|
npm run build:arch
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "$_pkgname-$pkgver"
|
cd "$pkgname-$pkgver"
|
||||||
install -d "$pkgdir/opt/$_pkgname"
|
install -d "$pkgdir/opt/$pkgname"
|
||||||
cp -r dist/linux-unpacked/* "$pkgdir/opt/$_pkgname"
|
cp -r dist/linux-unpacked/* "$pkgdir/opt/$pkgname"
|
||||||
install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
install -Dm644 "$srcdir/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
|
||||||
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png"
|
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$pkgname.png"
|
||||||
}
|
}
|
||||||
|
|||||||
60
README.md
60
README.md
@@ -4,9 +4,10 @@
|
|||||||
<h1>🎮 Hytale F2P Launcher 🚀</h1>
|
<h1>🎮 Hytale F2P Launcher 🚀</h1>
|
||||||
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
|
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
|
||||||
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
|
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
|
||||||
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)</small></p>
|
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support!</small></p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -118,9 +119,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><b>🖥️ OS</b></td>
|
<td><b>🖥️ OS</b></td>
|
||||||
<td colspan="3" align="center">
|
<td colspan="3" align="center">
|
||||||
Windows 10/11 (64-bit; X64/ARM64) | Linux (x64/ARM64) | macOS (Apple Silicon only)
|
Windows 10/11 (64-bit X64) | Linux (x64) | macOS (ARM64/Apple Silicon)
|
||||||
<br />
|
<br />
|
||||||
<small><i>⚠️ Note: macOS Intel (x86) is not yet supported <sup><a href="#fn1" id="ref1">1</a></sup></i></small>
|
<small><i>⚠️ Note: ARM64 (Windows & Linux), macOS (x86/Intel) <b>are not supported!</b> ⚠️</i></small>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -131,7 +132,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>🧠 RAM</b></td>
|
<td><b>🧠 RAM</b></td>
|
||||||
<td>8GB (dGPU)<sup><a href="#fn1" id="ref2">2</a></sup> /<br>12GB (iGPU)<sup><a href="#fn1" id="ref3">3</a></sup></td>
|
<td>8GB (dGPU) / 12GB (iGPU)<sup><a href="#fn1" id="ref1">1</a></sup></td>
|
||||||
<td>16 GB</td>
|
<td>16 GB</td>
|
||||||
<td>32 GB</td>
|
<td>32 GB</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -156,19 +157,19 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p id="fn1"><sup>Note 1</sup> Hytale did not provide game files for macOS Intel, yet.</p>
|
<p id="fn1"><sup>Note 1</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum, while using Integrated GPU (iGPU) must have 12 GB RAM.</p>
|
||||||
<p id="fn2"><sup>Note 2</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.</p>
|
|
||||||
<p id="fn3"><sup>Note 3</sup> Using Integrated GPU (dGPU) must have 12 GB RAM minimum.</p>
|
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Our launcher has **not yet** supported Offline Mode (playing Hytale without internet).
|
||||||
|
> We will surely add the feature as soon as possible. Kindly wait for the update.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 🪟 Windows Prequisites
|
### 🪟 Windows Prequisites
|
||||||
* **
|
|
||||||
* **Java JDK 25:**
|
* **Java JDK 25:**
|
||||||
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21.
|
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows)
|
||||||
* [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only.
|
* [Adoptium](https://adoptium.net/temurin/releases/?version=25)
|
||||||
* [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25.
|
* [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25.
|
||||||
* Download from any vendor if your OS is not Windows with ARM64 architecture.
|
|
||||||
* **Latest Visual Studio Redist:**
|
* **Latest Visual Studio Redist:**
|
||||||
* Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe)
|
* Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe)
|
||||||
* Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
|
* Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
|
||||||
@@ -178,9 +179,9 @@
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973).
|
> Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973).
|
||||||
|
|
||||||
* Make sure you have already installed newest **GPU driver**, consult your distro docs or wiki.
|
* Make sure you have already installed newest **GPU driver** especially proprietary NVIDIA, consult your distro docs or wiki.
|
||||||
|
* Also make sure that your GPU can be connected to EGL, try checking it first (again, consult your distro docs or wiki) before installing Hytale game via our launcher.
|
||||||
* Install `libpng` package to avoid SDL3_Image error:
|
* Install `libpng` package to avoid `SDL3_Image` error:
|
||||||
* `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro
|
* `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro
|
||||||
* `libpng libpng-devel` for Fedora/RHEL-based Distro
|
* `libpng libpng-devel` for Fedora/RHEL-based Distro
|
||||||
* `libpng` for Arch-based Distro
|
* `libpng` for Arch-based Distro
|
||||||
@@ -194,11 +195,12 @@
|
|||||||
1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above.
|
1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above.
|
||||||
2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
|
2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
|
||||||
3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
|
3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
|
||||||
* Click **More info**.
|
* Click **More info**, then click **Run anyway**.
|
||||||
* Click **Run anyway**.
|
|
||||||
4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu.
|
4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu.
|
||||||
|
5. **Whitelist in Windows Firewall** [#192](https://github.com/amiayweb/Hytale-F2P/issues/192#issuecomment-3803042908)
|
||||||
---
|
* Open the Windows Start Menu and search for `Allow an app through Windows Firewall`
|
||||||
|
* Click "Change settings" (you may need Admin privileges) and Locate `HytaleClient.exe` in the list.
|
||||||
|
* Ensure both the Private and Public checkboxes are checked. Click OK to save.
|
||||||
|
|
||||||
### 🐧 Linux Installation
|
### 🐧 Linux Installation
|
||||||
|
|
||||||
@@ -243,8 +245,6 @@
|
|||||||
* **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid.
|
* **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid.
|
||||||
* Missing libxcrypt.so.1: Install `libxcrypt-compat` using your package manager
|
* Missing libxcrypt.so.1: Install `libxcrypt-compat` using your package manager
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🍎 macOS Installation
|
### 🍎 macOS Installation
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
@@ -272,9 +272,9 @@ The `.zip` version is useful for users who prefer a portable installation or nee
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# How to Host a Server
|
# 📢 How to Host a Server
|
||||||
|
|
||||||
## Host your Singleplayer Server (Online-Play Feature)
|
## 🌐 Host your Singleplayer Server (Online-Play Feature)
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> You have to play the game to host the server. See Dedicated Server section below if you want to host it without you playing as the host.
|
> You have to play the game to host the server. See Dedicated Server section below if you want to host it without you playing as the host.
|
||||||
@@ -283,23 +283,29 @@ The `.zip` version is useful for users who prefer a portable installation or nee
|
|||||||
2. Pause the game (Esc) > select Online Play > Turn on `Allow Other Players to Join` > Set password if needed > Press `Save`.
|
2. Pause the game (Esc) > select Online Play > Turn on `Allow Other Players to Join` > Set password if needed > Press `Save`.
|
||||||
3. Check the status `Connected via STUN` or `Connected via UPnP`.
|
3. Check the status `Connected via STUN` or `Connected via UPnP`.
|
||||||
|
|
||||||
## Dedicated Server
|
## 🖧 Host a Dedicated Server
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you have already `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
|
> If you already have the patched `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
|
> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting).
|
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). Linux ARM64 is supported for server only.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> See detailed information of setting up a server here: [SERVER.md](SERVER.md)
|
> See detailed information of setting up a server here: [SERVER.md](SERVER.md). Download the latest patched JAR, the patched RAR, or the SH/BAT scripts from channel `#open-public-server` in our Discord Server.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Building from Source
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed Troubleshooting guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔨 Building from Source
|
||||||
|
|
||||||
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ function getAppDir() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get centralized UserData saves directory (NEW in 2.1.2)
|
||||||
|
* UserData is now stored separately from game installation
|
||||||
|
*/
|
||||||
|
function getHytaleSavesDir() {
|
||||||
|
const home = os.homedir();
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return path.join(home, 'AppData', 'Local', 'HytaleSaves');
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
return path.join(home, 'Library', 'Application Support', 'HytaleSaves');
|
||||||
|
} else {
|
||||||
|
return path.join(home, '.hytalesaves');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_APP_DIR = getAppDir();
|
const DEFAULT_APP_DIR = getAppDir();
|
||||||
|
|
||||||
function getResolvedAppDir(customPath) {
|
function getResolvedAppDir(customPath) {
|
||||||
@@ -218,20 +233,8 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
|
|
||||||
function getProfilesDir(customInstallPath = null) {
|
function getProfilesDir(customInstallPath = null) {
|
||||||
try {
|
try {
|
||||||
// get UserData path
|
// NEW 2.1.2: Use centralized UserData location
|
||||||
let installPath = customInstallPath;
|
const userDataPath = getHytaleSavesDir();
|
||||||
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 branch = loadVersionBranch();
|
|
||||||
const gameLatest = path.join(installPath, branch, 'package', 'game', 'latest');
|
|
||||||
const userDataPath = findUserDataPath(gameLatest);
|
|
||||||
const profilesDir = path.join(userDataPath, 'Profiles');
|
const profilesDir = path.join(userDataPath, 'Profiles');
|
||||||
|
|
||||||
if (!fs.existsSync(profilesDir)) {
|
if (!fs.existsSync(profilesDir)) {
|
||||||
@@ -247,6 +250,7 @@ function getProfilesDir(customInstallPath = null) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAppDir,
|
getAppDir,
|
||||||
|
getHytaleSavesDir,
|
||||||
getResolvedAppDir,
|
getResolvedAppDir,
|
||||||
expandHome,
|
expandHome,
|
||||||
APP_DIR,
|
APP_DIR,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA
|
|||||||
const { getLatestClientVersion } = require('../services/versionManager');
|
const { getLatestClientVersion } = require('../services/versionManager');
|
||||||
const { updateGameFiles } = require('./gameManager');
|
const { updateGameFiles } = require('./gameManager');
|
||||||
const { syncModsForCurrentProfile } = require('./modManager');
|
const { syncModsForCurrentProfile } = require('./modManager');
|
||||||
|
const { getUserDataPath } = require('../utils/userDataMigration');
|
||||||
|
|
||||||
// Client patcher for custom auth server (sanasol.ws)
|
// Client patcher for custom auth server (sanasol.ws)
|
||||||
let clientPatcher = null;
|
let clientPatcher = null;
|
||||||
@@ -106,7 +107,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||||
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
|
|
||||||
|
// NEW 2.1.2: Use centralized UserData location
|
||||||
|
const userDataDir = getUserDataPath();
|
||||||
|
|
||||||
const gameLatest = customGameDir;
|
const gameLatest = customGameDir;
|
||||||
let clientPath = findClientPath(gameLatest);
|
let clientPath = findClientPath(gameLatest);
|
||||||
@@ -282,6 +285,55 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||||
Object.assign(env, gpuEnv);
|
Object.assign(env, gpuEnv);
|
||||||
|
|
||||||
|
// Linux: Replace bundled libzstd.so with system version to fix glibc 2.41+ crash
|
||||||
|
// The bundled libzstd causes "free(): invalid pointer" on Steam Deck / Ubuntu LTS
|
||||||
|
if (process.platform === 'linux' && process.env.HYTALE_NO_LIBZSTD_FIX !== '1') {
|
||||||
|
const clientDir = path.dirname(clientPath);
|
||||||
|
const bundledLibzstd = path.join(clientDir, 'libzstd.so');
|
||||||
|
const backupLibzstd = path.join(clientDir, 'libzstd.so.bundled');
|
||||||
|
|
||||||
|
// Common system libzstd paths
|
||||||
|
const systemLibzstdPaths = [
|
||||||
|
'/usr/lib/libzstd.so.1', // Arch Linux, Steam Deck
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libzstd.so.1', // Debian/Ubuntu
|
||||||
|
'/usr/lib64/libzstd.so.1' // Fedora/RHEL
|
||||||
|
];
|
||||||
|
|
||||||
|
let systemLibzstd = null;
|
||||||
|
for (const p of systemLibzstdPaths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
systemLibzstd = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemLibzstd && fs.existsSync(bundledLibzstd)) {
|
||||||
|
try {
|
||||||
|
const stats = fs.lstatSync(bundledLibzstd);
|
||||||
|
|
||||||
|
// Only replace if it's not already a symlink to system version
|
||||||
|
if (!stats.isSymbolicLink()) {
|
||||||
|
// Backup bundled version
|
||||||
|
if (!fs.existsSync(backupLibzstd)) {
|
||||||
|
fs.renameSync(bundledLibzstd, backupLibzstd);
|
||||||
|
console.log(`Linux: Backed up bundled libzstd.so`);
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(bundledLibzstd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create symlink to system version
|
||||||
|
fs.symlinkSync(systemLibzstd, bundledLibzstd);
|
||||||
|
console.log(`Linux: Linked libzstd.so to system version (${systemLibzstd}) for glibc 2.41+ compatibility`);
|
||||||
|
} else {
|
||||||
|
const linkTarget = fs.readlinkSync(bundledLibzstd);
|
||||||
|
console.log(`Linux: libzstd.so already linked to ${linkTarget}`);
|
||||||
|
}
|
||||||
|
} catch (libzstdError) {
|
||||||
|
console.warn(`Linux: Could not replace libzstd.so: ${libzstdError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let spawnOptions = {
|
let spawnOptions = {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { installButler } = require('./butlerManager');
|
|||||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||||
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
||||||
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
||||||
const userDataBackup = require('../utils/userDataBackup');
|
const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration');
|
||||||
|
|
||||||
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||||
const osName = getOS();
|
const osName = getOS();
|
||||||
@@ -308,31 +308,25 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
|
|
||||||
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR, branchOverride = null) {
|
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR, branchOverride = null) {
|
||||||
let tempUpdateDir;
|
let tempUpdateDir;
|
||||||
let backupPath = null;
|
|
||||||
const branch = branchOverride || loadVersionBranch();
|
const branch = branchOverride || loadVersionBranch();
|
||||||
const installPath = path.dirname(path.dirname(path.dirname(path.dirname(gameDir))));
|
const installPath = path.dirname(path.dirname(path.dirname(path.dirname(gameDir))));
|
||||||
|
|
||||||
// Vérifier si on a version_client et version_branch dans config.json
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const hasVersionConfig = !!(config.version_client && config.version_branch);
|
const oldBranch = config.version_branch || 'release';
|
||||||
const oldBranch = config.version_branch || 'release'; // L'ancienne branche pour le backup
|
|
||||||
console.log(`[UpdateGameFiles] hasVersionConfig: ${hasVersionConfig}`);
|
|
||||||
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (progressCallback) {
|
// NEW 2.1.2: Ensure UserData migration to centralized location
|
||||||
progressCallback('Backing up user data...', 5, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup UserData AVANT de télécharger/installer (critical for same-branch updates)
|
|
||||||
try {
|
try {
|
||||||
console.log(`[UpdateGameFiles] Attempting to backup UserData from old branch: ${oldBranch}`);
|
console.log('[UpdateGameFiles] Ensuring UserData migration...');
|
||||||
backupPath = await userDataBackup.backupUserData(installPath, oldBranch, hasVersionConfig);
|
const migrationResult = await migrateUserDataToCentralized();
|
||||||
if (backupPath) {
|
if (migrationResult.migrated) {
|
||||||
console.log(`[UpdateGameFiles] ✓ UserData backed up from ${oldBranch}: ${backupPath}`);
|
console.log('[UpdateGameFiles] ✓ UserData migrated to centralized location');
|
||||||
|
} else if (migrationResult.alreadyMigrated) {
|
||||||
|
console.log('[UpdateGameFiles] ✓ UserData already in centralized location');
|
||||||
}
|
}
|
||||||
} catch (backupError) {
|
} catch (migrationError) {
|
||||||
console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message);
|
console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -390,31 +384,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
||||||
console.log('Logo@2x.png update result after update:', logoResult);
|
console.log('Logo@2x.png update result after update:', logoResult);
|
||||||
|
|
||||||
// Ensure UserData directory exists
|
// NEW 2.1.2: No longer create UserData in game installation
|
||||||
const userDataDir = path.join(gameDir, 'Client', 'UserData');
|
// UserData is now in centralized location (getUserDataPath())
|
||||||
if (!fs.existsSync(userDataDir)) {
|
console.log('[UpdateGameFiles] UserData is managed in centralized location');
|
||||||
console.log(`[UpdateGameFiles] Creating UserData directory at: ${userDataDir}`);
|
|
||||||
fs.mkdirSync(userDataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Restoring user data...', 90, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore UserData using new system
|
|
||||||
if (backupPath) {
|
|
||||||
try {
|
|
||||||
console.log(`[UpdateGameFiles] Restoring UserData from ${oldBranch} to ${branch}`);
|
|
||||||
console.log(`[UpdateGameFiles] Source backup: ${backupPath}`);
|
|
||||||
await userDataBackup.restoreUserData(backupPath, installPath, branch);
|
|
||||||
await userDataBackup.cleanupBackup(backupPath);
|
|
||||||
console.log(`[UpdateGameFiles] ✓ UserData migrated successfully from ${oldBranch} to ${branch}`);
|
|
||||||
} catch (restoreError) {
|
|
||||||
console.warn('[UpdateGameFiles] ✗ UserData restore failed:', restoreError.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[UpdateGameFiles] No backup to restore, empty UserData folder created');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Game files updated successfully to version: ${newVersion}`);
|
console.log(`Game files updated successfully to version: ${newVersion}`);
|
||||||
|
|
||||||
@@ -434,15 +406,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating game files:', error);
|
console.error('Error updating game files:', error);
|
||||||
|
|
||||||
if (backupPath) {
|
|
||||||
try {
|
|
||||||
await userDataBackup.cleanupBackup(backupPath);
|
|
||||||
console.log('UserData backup cleaned up after error');
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.warn('Could not clean up UserData backup:', cleanupError.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
|
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
|
||||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -470,28 +433,18 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
const customToolsDir = path.join(customAppDir, 'butler');
|
const customToolsDir = path.join(customAppDir, 'butler');
|
||||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||||
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
|
|
||||||
|
|
||||||
// Vérifier si on a version_client et version_branch dans config.json
|
|
||||||
const config = loadConfig();
|
|
||||||
const hasVersionConfig = !!(config.version_client && config.version_branch);
|
|
||||||
console.log(`[InstallGame] Configuration detected - version_client: ${config.version_client}, version_branch: ${config.version_branch}`);
|
|
||||||
console.log(`[InstallGame] hasVersionConfig: ${hasVersionConfig}`);
|
|
||||||
|
|
||||||
// Backup UserData AVANT l'installation si nécessaire
|
|
||||||
let backupPath = null;
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Checking for existing UserData...', 5, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// NEW 2.1.2: Ensure UserData migration to centralized location
|
||||||
try {
|
try {
|
||||||
console.log(`[InstallGame] Attempting UserData backup (hasVersionConfig: ${hasVersionConfig})...`);
|
console.log('[InstallGame] Ensuring UserData migration...');
|
||||||
backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig);
|
const migrationResult = await migrateUserDataToCentralized();
|
||||||
if (backupPath) {
|
if (migrationResult.migrated) {
|
||||||
console.log(`[InstallGame] ✓ UserData backed up to: ${backupPath}`);
|
console.log('[InstallGame] ✓ UserData migrated to centralized location');
|
||||||
|
} else if (migrationResult.alreadyMigrated) {
|
||||||
|
console.log('[InstallGame] ✓ UserData already in centralized location');
|
||||||
}
|
}
|
||||||
} catch (backupError) {
|
} catch (migrationError) {
|
||||||
console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message);
|
console.warn('[InstallGame] UserData migration warning:', migrationError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
|
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
|
||||||
@@ -500,10 +453,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!fs.existsSync(userDataDir)) {
|
|
||||||
fs.mkdirSync(userDataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
saveUsername(playerName);
|
saveUsername(playerName);
|
||||||
if (installPathOverride) {
|
if (installPathOverride) {
|
||||||
saveInstallPath(installPathOverride);
|
saveInstallPath(installPathOverride);
|
||||||
@@ -595,29 +544,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
||||||
console.log('Logo@2x.png update result after installation:', logoResult);
|
console.log('Logo@2x.png update result after installation:', logoResult);
|
||||||
|
|
||||||
// Ensure UserData directory exists
|
// NEW 2.1.2: No longer create UserData in game installation
|
||||||
if (!fs.existsSync(userDataDir)) {
|
// UserData is managed in centralized location (getUserDataPath())
|
||||||
console.log(`[InstallGame] Creating UserData directory at: ${userDataDir}`);
|
console.log('[InstallGame] UserData is managed in centralized location');
|
||||||
fs.mkdirSync(userDataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore UserData from backup if exists
|
|
||||||
if (backupPath) {
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Restoring UserData...', 95, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[InstallGame] Restoring UserData from: ${backupPath}`);
|
|
||||||
await userDataBackup.restoreUserData(backupPath, customAppDir, branch);
|
|
||||||
await userDataBackup.cleanupBackup(backupPath);
|
|
||||||
console.log('[InstallGame] ✓ UserData restored successfully');
|
|
||||||
} catch (restoreError) {
|
|
||||||
console.warn('[InstallGame] ✗ UserData restore failed:', restoreError.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[InstallGame] No backup to restore, empty UserData folder created');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Installation complete', 100, null, null, null);
|
progressCallback('Installation complete', 100, null, null, null);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,8 @@ class UserDataBackup {
|
|||||||
console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`);
|
console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`);
|
||||||
await fs.copy(userDataPath, backupPath, {
|
await fs.copy(userDataPath, backupPath, {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
errorOnExist: false
|
errorOnExist: false,
|
||||||
|
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||||
});
|
});
|
||||||
console.log('[UserDataBackup] ✓ Backup completed successfully');
|
console.log('[UserDataBackup] ✓ Backup completed successfully');
|
||||||
return backupPath;
|
return backupPath;
|
||||||
@@ -82,7 +83,8 @@ class UserDataBackup {
|
|||||||
|
|
||||||
await fs.copy(backupPath, userDataPath, {
|
await fs.copy(backupPath, userDataPath, {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
errorOnExist: false
|
errorOnExist: false,
|
||||||
|
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('UserData restore completed successfully');
|
console.log('UserData restore completed successfully');
|
||||||
|
|||||||
172
backend/utils/userDataMigration.js
Normal file
172
backend/utils/userDataMigration.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths');
|
||||||
|
const { loadConfig, saveConfig } = require('../core/config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location
|
||||||
|
*
|
||||||
|
* UserData is now stored in a centralized location instead of inside game installation:
|
||||||
|
* - Windows: %LOCALAPPDATA%\HytaleSaves\
|
||||||
|
* - macOS: ~/Library/Application Support/HytaleSaves/
|
||||||
|
* - Linux: ~/.hytalesaves/
|
||||||
|
*
|
||||||
|
* This eliminates the need for backup/restore during updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if migration to centralized UserData has been completed
|
||||||
|
*/
|
||||||
|
function isMigrationCompleted() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.userDataMigrated === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark migration as completed
|
||||||
|
*/
|
||||||
|
function markMigrationCompleted() {
|
||||||
|
saveConfig({ userDataMigrated: true });
|
||||||
|
console.log('[UserDataMigration] Migration marked as completed in config');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find old UserData location (pre-2.1.2)
|
||||||
|
* Searches in: installPath/branch/package/game/latest/Client/UserData
|
||||||
|
*/
|
||||||
|
function findOldUserDataPath() {
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
const installPath = getResolvedAppDir();
|
||||||
|
const branch = config.version_branch || 'release';
|
||||||
|
|
||||||
|
console.log(`[UserDataMigration] Looking for old UserData...`);
|
||||||
|
console.log(`[UserDataMigration] Install path: ${installPath}`);
|
||||||
|
console.log(`[UserDataMigration] Branch: ${branch}`);
|
||||||
|
|
||||||
|
// Old location
|
||||||
|
const oldPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
console.log(`[UserDataMigration] Checking: ${oldPath}`);
|
||||||
|
console.log(`[UserDataMigration] Checking: ${oldPath}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(oldPath)) {
|
||||||
|
console.log(`[UserDataMigration] ✓ Found old UserData at: ${oldPath}`);
|
||||||
|
return oldPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[UserDataMigration] ✗ Not found at current branch location`);
|
||||||
|
|
||||||
|
// Try other branch if current doesn't exist
|
||||||
|
const otherBranch = branch === 'release' ? 'pre-release' : 'release';
|
||||||
|
const otherPath = path.join(installPath, otherBranch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
console.log(`[UserDataMigration] Checking other branch: ${otherPath}`);
|
||||||
|
console.log(`[UserDataMigration] Checking other branch: ${otherPath}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(otherPath)) {
|
||||||
|
console.log(`[UserDataMigration] ✓ Found old UserData in other branch at: ${otherPath}`);
|
||||||
|
return otherPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[UserDataMigration] ✗ No old UserData found in any branch');
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserDataMigration] Error finding old UserData:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate UserData from old location to new centralized location
|
||||||
|
* One-time operation when upgrading to 2.1.2
|
||||||
|
*/
|
||||||
|
async function migrateUserDataToCentralized() {
|
||||||
|
// Check if already migrated
|
||||||
|
if (isMigrationCompleted()) {
|
||||||
|
console.log('[UserDataMigration] Migration already completed, skipping');
|
||||||
|
return { success: true, alreadyMigrated: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[UserDataMigration] === Starting UserData Migration to Centralized Location ===');
|
||||||
|
|
||||||
|
const newUserDataPath = getHytaleSavesDir();
|
||||||
|
console.log(`[UserDataMigration] Target location: ${newUserDataPath}`);
|
||||||
|
|
||||||
|
// Ensure new directory exists
|
||||||
|
if (!fs.existsSync(newUserDataPath)) {
|
||||||
|
fs.mkdirSync(newUserDataPath, { recursive: true });
|
||||||
|
console.log('[UserDataMigration] Created new HytaleSaves directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find old UserData
|
||||||
|
const oldUserDataPath = findOldUserDataPath();
|
||||||
|
|
||||||
|
if (!oldUserDataPath) {
|
||||||
|
console.log('[UserDataMigration] No old UserData found - fresh install or already migrated');
|
||||||
|
// Don't mark as migrated - let it check again next time in case game gets installed later
|
||||||
|
return { success: true, freshInstall: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if new location already has data (shouldn't happen, but safety check)
|
||||||
|
const existingFiles = fs.readdirSync(newUserDataPath);
|
||||||
|
if (existingFiles.length > 0) {
|
||||||
|
console.warn('[UserDataMigration] New location already contains files, marking as migrated to avoid re-attempts');
|
||||||
|
markMigrationCompleted();
|
||||||
|
return { success: true, skipped: true, reason: 'target_not_empty' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[UserDataMigration] Copying from ${oldUserDataPath} to ${newUserDataPath}...`);
|
||||||
|
|
||||||
|
// Copy all UserData to new location
|
||||||
|
await fs.copy(oldUserDataPath, newUserDataPath, {
|
||||||
|
overwrite: false,
|
||||||
|
errorOnExist: false,
|
||||||
|
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[UserDataMigration] ✓ UserData copied successfully');
|
||||||
|
|
||||||
|
// Mark migration as completed
|
||||||
|
markMigrationCompleted();
|
||||||
|
|
||||||
|
console.log('[UserDataMigration] === Migration Completed Successfully ===');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
migrated: true,
|
||||||
|
from: oldUserDataPath,
|
||||||
|
to: newUserDataPath
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserDataMigration] ✗ Migration failed:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
from: oldUserDataPath,
|
||||||
|
to: newUserDataPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the centralized UserData path (always use this in 2.1.2+)
|
||||||
|
* Ensures directory exists
|
||||||
|
*/
|
||||||
|
function getUserDataPath() {
|
||||||
|
const userDataPath = getHytaleSavesDir();
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!fs.existsSync(userDataPath)) {
|
||||||
|
fs.mkdirSync(userDataPath, { recursive: true });
|
||||||
|
console.log(`[UserDataMigration] Created UserData directory: ${userDataPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userDataPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
migrateUserDataToCentralized,
|
||||||
|
getUserDataPath,
|
||||||
|
isMigrationCompleted,
|
||||||
|
findOldUserDataPath
|
||||||
|
};
|
||||||
18
build/entitlements.mac.plist
Normal file
18
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
123
docs/STEAMDECK_CRASH_INVESTIGATION.md
Normal file
123
docs/STEAMDECK_CRASH_INVESTIGATION.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Steam Deck / Ubuntu LTS Crash Investigation
|
||||||
|
|
||||||
|
## Status: SOLVED
|
||||||
|
|
||||||
|
**Last updated:** 2026-01-27
|
||||||
|
|
||||||
|
**Solution:** Replace bundled `libzstd.so` with system version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Summary
|
||||||
|
|
||||||
|
The Hytale F2P launcher's client patcher causes crashes on Steam Deck and Ubuntu LTS with the error:
|
||||||
|
```
|
||||||
|
free(): invalid pointer
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```
|
||||||
|
SIGSEGV (Segmentation fault)
|
||||||
|
```
|
||||||
|
|
||||||
|
The crash occurs after successful authentication, specifically right after "Finished handling RequiredAssets".
|
||||||
|
|
||||||
|
**Affected Systems:**
|
||||||
|
- Steam Deck (glibc 2.41)
|
||||||
|
- Ubuntu LTS
|
||||||
|
|
||||||
|
**Working Systems:**
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Older Arch Linux (glibc < 2.41)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
The **bundled `libzstd.so`** in the game client is incompatible with glibc 2.41's stricter heap validation. When the game decompresses assets using this library, it triggers heap corruption detected by glibc 2.41.
|
||||||
|
|
||||||
|
The crash occurs in `libzstd.so` during `free()` after "Finished handling RequiredAssets" (asset decompression).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Replace the bundled `libzstd.so` with the system's `libzstd.so.1`.
|
||||||
|
|
||||||
|
### Automatic (Launcher)
|
||||||
|
|
||||||
|
The launcher automatically detects and replaces `libzstd.so` on Linux systems. No manual action needed.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
|
||||||
|
# Backup bundled version
|
||||||
|
mv libzstd.so libzstd.so.bundled
|
||||||
|
|
||||||
|
# Link to system version
|
||||||
|
# Steam Deck / Arch Linux:
|
||||||
|
ln -s /usr/lib/libzstd.so.1 libzstd.so
|
||||||
|
|
||||||
|
# Debian / Ubuntu:
|
||||||
|
ln -s /usr/lib/x86_64-linux-gnu/libzstd.so.1 libzstd.so
|
||||||
|
|
||||||
|
# Fedora / RHEL:
|
||||||
|
ln -s /usr/lib64/libzstd.so.1 libzstd.so
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Original
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
rm libzstd.so
|
||||||
|
mv libzstd.so.bundled libzstd.so
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why This Works
|
||||||
|
|
||||||
|
1. The bundled `libzstd.so` was likely compiled with different allocator settings or an older toolchain
|
||||||
|
2. glibc 2.41 has stricter heap validation that catches invalid memory operations
|
||||||
|
3. The system `libzstd.so.1` is compiled with the system's glibc and uses compatible memory allocation patterns
|
||||||
|
4. By using the system library, we avoid the incompatibility entirely
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previous Investigation (for reference)
|
||||||
|
|
||||||
|
### What Was Tried Before Finding Solution
|
||||||
|
|
||||||
|
| Approach | Result |
|
||||||
|
|----------|--------|
|
||||||
|
| jemalloc allocator | Worked ~30% of time, not stable |
|
||||||
|
| GLIBC_TUNABLES | No effect |
|
||||||
|
| taskset (CPU pinning) | Single core too slow |
|
||||||
|
| nice/chrt (scheduling) | No effect |
|
||||||
|
| Various patching approaches | All crashed |
|
||||||
|
|
||||||
|
### Key Insight
|
||||||
|
|
||||||
|
The crash was in `libzstd.so`, not in our patched code. The patching just changed timing enough to expose the libzstd incompatibility more frequently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GDB Stack Trace (Historical)
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 0x00007ffff7d3f5a4 in ?? () from /usr/lib/libc.so.6
|
||||||
|
#1 raise () from /usr/lib/libc.so.6
|
||||||
|
#2 abort () from /usr/lib/libc.so.6
|
||||||
|
#3-#4 ?? () from /usr/lib/libc.so.6
|
||||||
|
#5 free () from /usr/lib/libc.so.6
|
||||||
|
#6 ?? () from libzstd.so <-- CRASH POINT (bundled library)
|
||||||
|
#7-#24 HytaleClient code (asset decompression)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branch
|
||||||
|
|
||||||
|
`fix/steamdeck-libzstd`
|
||||||
65
docs/STEAMDECK_DEBUG_COMMANDS.md
Normal file
65
docs/STEAMDECK_DEBUG_COMMANDS.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Steam Deck / Linux Crash Fix
|
||||||
|
|
||||||
|
## SOLUTION: Use system libzstd
|
||||||
|
|
||||||
|
The crash is caused by the bundled `libzstd.so` being incompatible with glibc 2.41's stricter heap validation.
|
||||||
|
|
||||||
|
### Automatic Fix
|
||||||
|
|
||||||
|
The launcher automatically replaces `libzstd.so` with the system version. No manual action needed.
|
||||||
|
|
||||||
|
### Manual Fix
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
|
||||||
|
# Backup and replace
|
||||||
|
mv libzstd.so libzstd.so.bundled
|
||||||
|
ln -s /usr/lib/libzstd.so.1 libzstd.so
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Original
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
rm libzstd.so
|
||||||
|
mv libzstd.so.bundled libzstd.so
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Commands (for troubleshooting)
|
||||||
|
|
||||||
|
### Check libzstd Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if symlinked
|
||||||
|
ls -la ~/.hytalef2p/release/package/game/latest/Client/libzstd.so
|
||||||
|
|
||||||
|
# Find system libzstd
|
||||||
|
find /usr/lib -name "libzstd.so*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
file ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
|
||||||
|
ldd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Client Binary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
cp HytaleClient.original HytaleClient
|
||||||
|
rm -f HytaleClient.patched_custom
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `HYTALE_AUTH_DOMAIN` | Custom auth domain | `auth.sanasol.ws` |
|
||||||
|
| `HYTALE_NO_LIBZSTD_FIX` | Disable libzstd replacement | `1` |
|
||||||
75
main.js
75
main.js
@@ -5,6 +5,7 @@ const { autoUpdater } = require('electron-updater');
|
|||||||
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, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
|
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
|
||||||
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||||
|
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
||||||
|
|
||||||
// Handle Hardware Acceleration
|
// Handle Hardware Acceleration
|
||||||
try {
|
try {
|
||||||
@@ -175,7 +176,8 @@ function createWindow() {
|
|||||||
initDiscordRPC();
|
initDiscordRPC();
|
||||||
|
|
||||||
// Configure and initialize electron-updater
|
// Configure and initialize electron-updater
|
||||||
autoUpdater.autoDownload = false;
|
// Enable auto-download so updates start immediately when available
|
||||||
|
autoUpdater.autoDownload = true;
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
autoUpdater.on('checking-for-update', () => {
|
autoUpdater.on('checking-for-update', () => {
|
||||||
@@ -200,6 +202,20 @@ function createWindow() {
|
|||||||
|
|
||||||
autoUpdater.on('error', (err) => {
|
autoUpdater.on('error', (err) => {
|
||||||
console.error('Error in auto-updater:', err);
|
console.error('Error in auto-updater:', err);
|
||||||
|
|
||||||
|
// Handle macOS code signing errors - requires manual download
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
const isMacSigningError = process.platform === 'darwin' &&
|
||||||
|
(err.code === 'ERR_UPDATER_INVALID_SIGNATURE' ||
|
||||||
|
err.message.includes('signature') ||
|
||||||
|
err.message.includes('code sign'));
|
||||||
|
|
||||||
|
mainWindow.webContents.send('update-error', {
|
||||||
|
message: err.message,
|
||||||
|
isMacSigningError: isMacSigningError,
|
||||||
|
requiresManualDownload: isMacSigningError || process.platform === 'darwin'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('download-progress', (progressObj) => {
|
autoUpdater.on('download-progress', (progressObj) => {
|
||||||
@@ -217,7 +233,10 @@ function createWindow() {
|
|||||||
console.log('Update downloaded:', info.version);
|
console.log('Update downloaded:', info.version);
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('update-downloaded', {
|
mainWindow.webContents.send('update-downloaded', {
|
||||||
version: info.version
|
version: info.version,
|
||||||
|
platform: process.platform,
|
||||||
|
// macOS auto-install often fails on unsigned apps
|
||||||
|
autoInstallSupported: process.platform !== 'darwin'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -298,6 +317,14 @@ app.whenReady().then(async () => {
|
|||||||
// Initialize Profile Manager (runs migration if needed)
|
// Initialize Profile Manager (runs migration if needed)
|
||||||
profileManager.init();
|
profileManager.init();
|
||||||
|
|
||||||
|
// Migrate UserData to centralized location (v2.1.2+)
|
||||||
|
console.log('[Startup] Checking UserData migration...');
|
||||||
|
try {
|
||||||
|
await migrateUserDataToCentralized();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Startup] UserData migration failed:', error);
|
||||||
|
}
|
||||||
|
|
||||||
createSplashScreen();
|
createSplashScreen();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -850,6 +877,17 @@ ipcMain.handle('open-external', async (event, url) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('open-download-page', async () => {
|
||||||
|
try {
|
||||||
|
// Open GitHub releases page for manual download
|
||||||
|
await shell.openExternal('https://github.com/amiayweb/Hytale-F2P/releases/latest');
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open download page:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-game-location', async () => {
|
ipcMain.handle('open-game-location', async () => {
|
||||||
try {
|
try {
|
||||||
const { getResolvedAppDir, loadVersionBranch } = require('./backend/launcher');
|
const { getResolvedAppDir, loadVersionBranch } = require('./backend/launcher');
|
||||||
@@ -1077,8 +1115,37 @@ ipcMain.handle('download-update', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('install-update', () => {
|
ipcMain.handle('install-update', async () => {
|
||||||
autoUpdater.quitAndInstall(false, true);
|
console.log('[AutoUpdater] Installing update...');
|
||||||
|
|
||||||
|
// On macOS, quitAndInstall often fails silently
|
||||||
|
// Use a more aggressive approach
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
console.log('[AutoUpdater] macOS detected, using force quit approach');
|
||||||
|
// Give user feedback that something is happening
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('update-installing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to show the "Installing..." state
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
try {
|
||||||
|
autoUpdater.quitAndInstall(false, true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[AutoUpdater] quitAndInstall failed:', err);
|
||||||
|
// Force quit the app - the update should install on next launch
|
||||||
|
app.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If quitAndInstall didn't work, force exit after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[AutoUpdater] Force exiting app...');
|
||||||
|
app.exit(0);
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
autoUpdater.quitAndInstall(false, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-launcher-version', () => {
|
ipcMain.handle('get-launcher-version', () => {
|
||||||
|
|||||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/notarize": "^2.5.0",
|
||||||
"electron": "^40.0.0",
|
"electron": "^40.0.0",
|
||||||
"electron-builder": "^26.4.0"
|
"electron-builder": "^26.4.0"
|
||||||
}
|
}
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"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",
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/notarize": "^2.5.0",
|
||||||
"electron": "^40.0.0",
|
"electron": "^40.0.0",
|
||||||
"electron-builder": "^26.4.0"
|
"electron-builder": "^26.4.0"
|
||||||
},
|
},
|
||||||
@@ -131,8 +132,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "build/icon.icns",
|
"icon": "build/icon.icns",
|
||||||
"category": "public.app-category.games"
|
"category": "public.app-category.games",
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"gatekeeperAssess": false,
|
||||||
|
"entitlements": "build/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "build/entitlements.mac.plist"
|
||||||
},
|
},
|
||||||
|
"afterSign": "scripts/notarize.js",
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
|||||||
13
preload.js
13
preload.js
@@ -24,7 +24,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
|
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
|
||||||
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
|
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
|
||||||
|
|
||||||
// Harwadre Acceleration
|
// Hardware Acceleration
|
||||||
saveLauncherHardwareAcceleration: (enabled) => ipcRenderer.invoke('save-launcher-hw-accel', enabled),
|
saveLauncherHardwareAcceleration: (enabled) => ipcRenderer.invoke('save-launcher-hw-accel', enabled),
|
||||||
loadLauncherHardwareAcceleration: () => ipcRenderer.invoke('load-launcher-hw-accel'),
|
loadLauncherHardwareAcceleration: () => ipcRenderer.invoke('load-launcher-hw-accel'),
|
||||||
|
|
||||||
@@ -50,14 +50,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
|
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
|
||||||
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
|
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
|
||||||
onProgressUpdate: (callback) => {
|
onProgressUpdate: (callback) => {
|
||||||
ipcRenderer.on('progress-update', (event, data) => {
|
ipcRenderer.on('progress-update', (event, data) => callback(data));
|
||||||
// Ensure data includes retry state if available
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
callback(data);
|
|
||||||
} else {
|
|
||||||
callback(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onProgressComplete: (callback) => {
|
onProgressComplete: (callback) => {
|
||||||
ipcRenderer.on('progress-complete', () => callback());
|
ipcRenderer.on('progress-complete', () => callback());
|
||||||
@@ -69,7 +62,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.on('installation-end', () => callback());
|
ipcRenderer.on('installation-end', () => callback());
|
||||||
},
|
},
|
||||||
getUserId: () => ipcRenderer.invoke('get-user-id'),
|
getUserId: () => ipcRenderer.invoke('get-user-id'),
|
||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
|
||||||
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
|
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
|
||||||
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
|
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
|
||||||
onUpdatePopup: (callback) => {
|
onUpdatePopup: (callback) => {
|
||||||
@@ -126,6 +118,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||||
|
quitAndInstallUpdate: () => ipcRenderer.invoke('install-update'), // Alias for update.js compatibility
|
||||||
getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'),
|
getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'),
|
||||||
onUpdateAvailable: (callback) => {
|
onUpdateAvailable: (callback) => {
|
||||||
ipcRenderer.on('update-available', (event, data) => callback(data));
|
ipcRenderer.on('update-available', (event, data) => callback(data));
|
||||||
|
|||||||
62
scripts/notarize.js
Normal file
62
scripts/notarize.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
console.log('[Notarize] Script loaded');
|
||||||
|
|
||||||
|
let notarize;
|
||||||
|
try {
|
||||||
|
notarize = require('@electron/notarize').notarize;
|
||||||
|
console.log('[Notarize] @electron/notarize loaded successfully');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Notarize] Failed to load @electron/notarize:', err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
exports.default = async function notarizing(context) {
|
||||||
|
console.log('[Notarize] afterSign hook called');
|
||||||
|
console.log('[Notarize] Context:', JSON.stringify({
|
||||||
|
platform: context.electronPlatformName,
|
||||||
|
appOutDir: context.appOutDir,
|
||||||
|
outDir: context.outDir
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
const { electronPlatformName, appOutDir } = context;
|
||||||
|
|
||||||
|
// Only notarize macOS builds
|
||||||
|
if (electronPlatformName !== 'darwin') {
|
||||||
|
console.log('[Notarize] Skipping: not macOS');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check credentials
|
||||||
|
const hasAppleId = !!process.env.APPLE_ID;
|
||||||
|
const hasPassword = !!process.env.APPLE_APP_SPECIFIC_PASSWORD;
|
||||||
|
const hasTeamId = !!process.env.APPLE_TEAM_ID;
|
||||||
|
|
||||||
|
console.log('[Notarize] Credentials check:', { hasAppleId, hasPassword, hasTeamId });
|
||||||
|
|
||||||
|
if (!hasAppleId || !hasPassword || !hasTeamId) {
|
||||||
|
console.log('[Notarize] Skipping: missing credentials');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
|
const appPath = path.join(appOutDir, `${appName}.app`);
|
||||||
|
|
||||||
|
console.log('[Notarize] Starting notarization...');
|
||||||
|
console.log('[Notarize] App path:', appPath);
|
||||||
|
console.log('[Notarize] Team ID:', process.env.APPLE_TEAM_ID);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await notarize({
|
||||||
|
appPath,
|
||||||
|
appleId: process.env.APPLE_ID,
|
||||||
|
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
|
||||||
|
teamId: process.env.APPLE_TEAM_ID,
|
||||||
|
});
|
||||||
|
console.log('[Notarize] Notarization complete!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Notarize] Notarization failed:', error.message);
|
||||||
|
console.error('[Notarize] Full error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user