mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 11:41:49 -03:00
Compare commits
13 Commits
fix/steamd
...
v2.1.3-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aaf74a3db | ||
|
|
be78f67439 | ||
|
|
d0b9ae1da8 | ||
|
|
e8105cb30e | ||
|
|
79456e43a6 | ||
|
|
dd2dbc6f08 | ||
|
|
c4acb32fcd | ||
|
|
fbcbafb9b5 | ||
|
|
86ed33358c | ||
|
|
9ec97f9d33 | ||
|
|
ee18455b4b | ||
|
|
a5c931b26d | ||
|
|
661a0c9eed |
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
|
||||||
|
|
||||||
|
|||||||
68
.github/workflows/release.yml
vendored
68
.github/workflows/release.yml
vendored
@@ -40,6 +40,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:
|
||||||
@@ -123,8 +131,9 @@ 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') ||
|
||||||
@@ -135,14 +144,26 @@ jobs:
|
|||||||
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 +176,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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
|
|||||||
198
GUI/js/update.js
198
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>
|
||||||
@@ -119,10 +134,37 @@ class ClientUpdateManager {
|
|||||||
|
|
||||||
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,9 +180,14 @@ 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);
|
||||||
@@ -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,25 +274,86 @@ 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);
|
||||||
@@ -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);
|
||||||
|
this._boundBlockContextMenu = this.blockContextMenu.bind(this);
|
||||||
|
|
||||||
document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true);
|
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 === ' ') &&
|
||||||
|
|||||||
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.
|
||||||
|
|
||||||
|
|||||||
@@ -285,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'],
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
|
||||||
const AdmZip = require('adm-zip');
|
|
||||||
const { execSync, spawn } = require('child_process');
|
|
||||||
const { getJavaExec, getBundledJavaPath } = require('../managers/javaManager');
|
|
||||||
const { JRE_DIR } = require('../core/paths');
|
|
||||||
|
|
||||||
// Domain configuration
|
// Domain configuration
|
||||||
const ORIGINAL_DOMAIN = 'hytale.com';
|
const ORIGINAL_DOMAIN = 'hytale.com';
|
||||||
@@ -26,15 +21,13 @@ function getTargetDomain() {
|
|||||||
const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
|
const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patches HytaleClient and HytaleServer binaries to replace hytale.com with custom domain
|
* Patches HytaleClient binary to replace hytale.com with custom domain
|
||||||
* This allows the game to connect to a custom authentication server
|
* Server patching is done via pre-patched JAR download from CDN
|
||||||
*
|
*
|
||||||
* Supports domains from 4 to 16 characters:
|
* Supports domains from 4 to 16 characters:
|
||||||
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
|
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
|
||||||
* - Domains <= 10 chars: Direct replacement, subdomains stripped
|
* - Domains <= 10 chars: Direct replacement, subdomains stripped
|
||||||
* - Domains 11-16 chars: Split mode - first 6 chars replace subdomain prefix, rest replaces domain
|
* - Domains 11-16 chars: Split mode - first 6 chars replace subdomain prefix, rest replaces domain
|
||||||
*
|
|
||||||
* Official hytale.com keeps original subdomain behavior (sessions., account-data., etc.)
|
|
||||||
*/
|
*/
|
||||||
class ClientPatcher {
|
class ClientPatcher {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -61,19 +54,16 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the domain patching strategy based on length
|
* Calculate the domain patching strategy based on length
|
||||||
* @returns {object} Strategy with mainDomain and subdomainPrefix
|
|
||||||
*/
|
*/
|
||||||
getDomainStrategy(domain) {
|
getDomainStrategy(domain) {
|
||||||
if (domain.length <= 10) {
|
if (domain.length <= 10) {
|
||||||
// Direct replacement - subdomains will be stripped
|
|
||||||
return {
|
return {
|
||||||
mode: 'direct',
|
mode: 'direct',
|
||||||
mainDomain: domain,
|
mainDomain: domain,
|
||||||
subdomainPrefix: '', // Empty = subdomains stripped
|
subdomainPrefix: '',
|
||||||
description: `Direct replacement: hytale.com -> ${domain}`
|
description: `Direct replacement: hytale.com -> ${domain}`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Split mode: first 6 chars become subdomain prefix, rest replaces hytale.com
|
|
||||||
const prefix = domain.slice(0, 6);
|
const prefix = domain.slice(0, 6);
|
||||||
const suffix = domain.slice(6);
|
const suffix = domain.slice(6);
|
||||||
return {
|
return {
|
||||||
@@ -87,21 +77,15 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a string to the length-prefixed byte format used by the client
|
* Convert a string to the length-prefixed byte format used by the client
|
||||||
* Format: [length byte] [00 00 00 padding] [char1] [00] [char2] [00] ... [lastChar]
|
|
||||||
* Note: No null byte after the last character
|
|
||||||
*/
|
*/
|
||||||
stringToLengthPrefixed(str) {
|
stringToLengthPrefixed(str) {
|
||||||
const length = str.length;
|
const length = str.length;
|
||||||
const result = Buffer.alloc(4 + length + (length - 1)); // length byte + padding + chars + separators
|
const result = Buffer.alloc(4 + length + (length - 1));
|
||||||
|
|
||||||
// Length byte
|
|
||||||
result[0] = length;
|
result[0] = length;
|
||||||
// Padding: 00 00 00
|
|
||||||
result[1] = 0x00;
|
result[1] = 0x00;
|
||||||
result[2] = 0x00;
|
result[2] = 0x00;
|
||||||
result[3] = 0x00;
|
result[3] = 0x00;
|
||||||
|
|
||||||
// Characters with null separators (no separator after last char)
|
|
||||||
let pos = 4;
|
let pos = 4;
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result[pos++] = str.charCodeAt(i);
|
result[pos++] = str.charCodeAt(i);
|
||||||
@@ -109,7 +93,6 @@ class ClientPatcher {
|
|||||||
result[pos++] = 0x00;
|
result[pos++] = 0x00;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,13 +107,6 @@ class ClientPatcher {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a string to UTF-8 bytes (how Java stores strings)
|
|
||||||
*/
|
|
||||||
stringToUtf8(str) {
|
|
||||||
return Buffer.from(str, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all occurrences of a pattern in a buffer
|
* Find all occurrences of a pattern in a buffer
|
||||||
*/
|
*/
|
||||||
@@ -148,7 +124,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace bytes in buffer - only overwrites the length of new bytes
|
* Replace bytes in buffer - only overwrites the length of new bytes
|
||||||
* Prevents offset corruption by not expanding the replacement
|
|
||||||
*/
|
*/
|
||||||
replaceBytes(buffer, oldBytes, newBytes) {
|
replaceBytes(buffer, oldBytes, newBytes) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -160,9 +135,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const positions = this.findAllOccurrences(result, oldBytes);
|
const positions = this.findAllOccurrences(result, oldBytes);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
// Only overwrite the length of the new bytes
|
|
||||||
newBytes.copy(result, pos);
|
newBytes.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
@@ -171,32 +144,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UTF-8 domain replacement for Java JAR files.
|
* Smart domain replacement that handles both null-terminated and non-null-terminated strings
|
||||||
* Java stores strings in UTF-8 format in the constant pool.
|
|
||||||
*/
|
|
||||||
findAndReplaceDomainUtf8(data, oldDomain, newDomain) {
|
|
||||||
let count = 0;
|
|
||||||
const result = Buffer.from(data);
|
|
||||||
|
|
||||||
const oldUtf8 = this.stringToUtf8(oldDomain);
|
|
||||||
const newUtf8 = this.stringToUtf8(newDomain);
|
|
||||||
|
|
||||||
const positions = this.findAllOccurrences(result, oldUtf8);
|
|
||||||
|
|
||||||
for (const pos of positions) {
|
|
||||||
newUtf8.copy(result, pos);
|
|
||||||
count++;
|
|
||||||
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { buffer: result, count };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Smart domain replacement that handles both null-terminated and non-null-terminated strings.
|
|
||||||
* .NET AOT stores some strings in various formats:
|
|
||||||
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
|
|
||||||
* - Length-prefixed where last char may have metadata byte instead of \x00
|
|
||||||
*/
|
*/
|
||||||
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -218,7 +166,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
if (lastCharFirstByte === oldLastCharByte) {
|
if (lastCharFirstByte === oldLastCharByte) {
|
||||||
newUtf16NoLast.copy(result, pos);
|
newUtf16NoLast.copy(result, pos);
|
||||||
|
|
||||||
result[lastCharPos] = newLastCharByte;
|
result[lastCharPos] = newLastCharByte;
|
||||||
|
|
||||||
if (lastCharPos + 1 < result.length) {
|
if (lastCharPos + 1 < result.length) {
|
||||||
@@ -238,7 +185,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply all domain patches using length-prefixed format
|
* Apply all domain patches using length-prefixed format
|
||||||
* This is the main patching method for variable-length domains
|
|
||||||
*/
|
*/
|
||||||
applyDomainPatches(data, domain, protocol = 'https://') {
|
applyDomainPatches(data, domain, protocol = 'https://') {
|
||||||
let result = Buffer.from(data);
|
let result = Buffer.from(data);
|
||||||
@@ -263,7 +209,7 @@ class ClientPatcher {
|
|||||||
totalCount += sentryResult.count;
|
totalCount += sentryResult.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Patch main domain (hytale.com -> mainDomain)
|
// 2. Patch main domain
|
||||||
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
||||||
const domainResult = this.replaceBytes(
|
const domainResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
@@ -298,7 +244,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
* Patch Discord invite URLs
|
||||||
*/
|
*/
|
||||||
patchDiscordUrl(data) {
|
patchDiscordUrl(data) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -307,7 +253,6 @@ class ClientPatcher {
|
|||||||
const oldUrl = '.gg/hytale';
|
const oldUrl = '.gg/hytale';
|
||||||
const newUrl = '.gg/MHkEjepMQ7';
|
const newUrl = '.gg/MHkEjepMQ7';
|
||||||
|
|
||||||
// Try length-prefixed format first
|
|
||||||
const lpResult = this.replaceBytes(
|
const lpResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(oldUrl),
|
this.stringToLengthPrefixed(oldUrl),
|
||||||
@@ -323,7 +268,6 @@ class ClientPatcher {
|
|||||||
const newUtf16 = this.stringToUtf16LE(newUrl);
|
const newUtf16 = this.stringToUtf16LE(newUrl);
|
||||||
|
|
||||||
const positions = this.findAllOccurrences(result, oldUtf16);
|
const positions = this.findAllOccurrences(result, oldUtf16);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
newUtf16.copy(result, pos);
|
newUtf16.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
@@ -333,39 +277,66 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the client binary has already been patched
|
* Check patch status of client binary
|
||||||
* Also verifies the binary actually contains the patched domain
|
|
||||||
*/
|
*/
|
||||||
isPatchedAlready(clientPath) {
|
getPatchStatus(clientPath) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
const patchFlagFile = clientPath + this.patchedFlag;
|
const patchFlagFile = clientPath + this.patchedFlag;
|
||||||
|
|
||||||
// First check flag file
|
|
||||||
if (fs.existsSync(patchFlagFile)) {
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
try {
|
try {
|
||||||
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
||||||
if (flagData.targetDomain === newDomain) {
|
const currentDomain = flagData.targetDomain;
|
||||||
// Verify the binary actually contains the patched domain
|
|
||||||
|
if (currentDomain === newDomain) {
|
||||||
const data = fs.readFileSync(clientPath);
|
const data = fs.readFileSync(clientPath);
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
const domainPattern = this.stringToLengthPrefixed(strategy.mainDomain);
|
const domainPattern = this.stringToLengthPrefixed(strategy.mainDomain);
|
||||||
|
|
||||||
if (data.includes(domainPattern)) {
|
if (data.includes(domainPattern)) {
|
||||||
return true;
|
return { patched: true, currentDomain, needsRestore: false };
|
||||||
} else {
|
} else {
|
||||||
console.log(' Flag exists but binary not patched (was updated?), re-patching...');
|
console.log(' Flag exists but binary not patched (was updated?), needs re-patching...');
|
||||||
return false;
|
return { patched: false, currentDomain: null, needsRestore: false };
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` Currently patched for "${currentDomain}", need to change to "${newDomain}"`);
|
||||||
|
return { patched: false, currentDomain, needsRestore: true };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Flag file corrupt or unreadable
|
// Flag file corrupt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { patched: false, currentDomain: null, needsRestore: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if client is already patched (backward compat)
|
||||||
|
*/
|
||||||
|
isPatchedAlready(clientPath) {
|
||||||
|
return this.getPatchStatus(clientPath).patched;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore client from backup
|
||||||
|
*/
|
||||||
|
restoreFromBackup(clientPath) {
|
||||||
|
const backupPath = clientPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
console.log(' Restoring original binary from backup for re-patching...');
|
||||||
|
fs.copyFileSync(backupPath, clientPath);
|
||||||
|
const patchFlagFile = clientPath + this.patchedFlag;
|
||||||
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
|
fs.unlinkSync(patchFlagFile);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.warn(' No backup found to restore - will try patching anyway');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the client as patched
|
* Mark client as patched
|
||||||
*/
|
*/
|
||||||
markAsPatched(clientPath) {
|
markAsPatched(clientPath) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
@@ -378,29 +349,28 @@ class ClientPatcher {
|
|||||||
patchMode: strategy.mode,
|
patchMode: strategy.mode,
|
||||||
mainDomain: strategy.mainDomain,
|
mainDomain: strategy.mainDomain,
|
||||||
subdomainPrefix: strategy.subdomainPrefix,
|
subdomainPrefix: strategy.subdomainPrefix,
|
||||||
patcherVersion: '2.0.0',
|
patcherVersion: '2.1.0',
|
||||||
verified: 'binary_contents'
|
verified: 'binary_contents'
|
||||||
};
|
};
|
||||||
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
|
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a backup of the original client binary
|
* Create backup of original client binary
|
||||||
*/
|
*/
|
||||||
backupClient(clientPath) {
|
backupClient(clientPath) {
|
||||||
const backupPath = clientPath + '.original';
|
const backupPath = clientPath + '.original';
|
||||||
|
try {
|
||||||
if (!fs.existsSync(backupPath)) {
|
if (!fs.existsSync(backupPath)) {
|
||||||
console.log(` Creating backup at ${path.basename(backupPath)}`);
|
console.log(` Creating backup at ${path.basename(backupPath)}`);
|
||||||
fs.copyFileSync(clientPath, backupPath);
|
fs.copyFileSync(clientPath, backupPath);
|
||||||
return backupPath;
|
return backupPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if current file differs from backup (might have been updated)
|
|
||||||
const currentSize = fs.statSync(clientPath).size;
|
const currentSize = fs.statSync(clientPath).size;
|
||||||
const backupSize = fs.statSync(backupPath).size;
|
const backupSize = fs.statSync(backupPath).size;
|
||||||
|
|
||||||
if (currentSize !== backupSize) {
|
if (currentSize !== backupSize) {
|
||||||
// File was updated, create timestamped backup of old backup
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
const oldBackupPath = `${clientPath}.original.${timestamp}`;
|
const oldBackupPath = `${clientPath}.original.${timestamp}`;
|
||||||
console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`);
|
console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`);
|
||||||
@@ -411,10 +381,14 @@ class ClientPatcher {
|
|||||||
|
|
||||||
console.log(' Backup already exists');
|
console.log(' Backup already exists');
|
||||||
return backupPath;
|
return backupPath;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` Failed to create backup: ${e.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the original client binary from backup
|
* Restore original client binary
|
||||||
*/
|
*/
|
||||||
restoreClient(clientPath) {
|
restoreClient(clientPath) {
|
||||||
const backupPath = clientPath + '.original';
|
const backupPath = clientPath + '.original';
|
||||||
@@ -433,15 +407,12 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the client binary to use the custom domain
|
* Patch the client binary to use the custom domain
|
||||||
* @param {string} clientPath - Path to the HytaleClient binary
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @returns {object} Result object with success status and details
|
|
||||||
*/
|
*/
|
||||||
async patchClient(clientPath, progressCallback) {
|
async patchClient(clientPath, progressCallback) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
|
|
||||||
console.log('=== Client Patcher v2.0 ===');
|
console.log('=== Client Patcher v2.1 ===');
|
||||||
console.log(`Target: ${clientPath}`);
|
console.log(`Target: ${clientPath}`);
|
||||||
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
||||||
console.log(`Mode: ${strategy.mode}`);
|
console.log(`Mode: ${strategy.mode}`);
|
||||||
@@ -456,32 +427,34 @@ class ClientPatcher {
|
|||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isPatchedAlready(clientPath)) {
|
const patchStatus = this.getPatchStatus(clientPath);
|
||||||
|
|
||||||
|
if (patchStatus.patched) {
|
||||||
console.log(`Client already patched for ${newDomain}, skipping`);
|
console.log(`Client already patched for ${newDomain}, skipping`);
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Client already patched', 100);
|
||||||
progressCallback('Client already patched', 100);
|
|
||||||
}
|
|
||||||
return { success: true, alreadyPatched: true, patchCount: 0 };
|
return { success: true, alreadyPatched: true, patchCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (patchStatus.needsRestore) {
|
||||||
progressCallback('Preparing to patch client...', 10);
|
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
|
||||||
|
this.restoreFromBackup(clientPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback) progressCallback('Preparing to patch client...', 10);
|
||||||
|
|
||||||
console.log('Creating backup...');
|
console.log('Creating backup...');
|
||||||
this.backupClient(clientPath);
|
const backupResult = this.backupClient(clientPath);
|
||||||
|
if (!backupResult) {
|
||||||
if (progressCallback) {
|
console.warn(' Could not create backup - proceeding without backup');
|
||||||
progressCallback('Reading client binary...', 20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback) progressCallback('Reading client binary...', 20);
|
||||||
|
|
||||||
console.log('Reading client binary...');
|
console.log('Reading client binary...');
|
||||||
const data = fs.readFileSync(clientPath);
|
const data = fs.readFileSync(clientPath);
|
||||||
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching domain references...', 50);
|
||||||
progressCallback('Patching domain references...', 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Applying domain patches (length-prefixed format)...');
|
console.log('Applying domain patches (length-prefixed format)...');
|
||||||
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
||||||
@@ -492,7 +465,6 @@ class ClientPatcher {
|
|||||||
if (count === 0 && discordCount === 0) {
|
if (count === 0 && discordCount === 0) {
|
||||||
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
||||||
|
|
||||||
// Fallback to legacy patching for older binary formats
|
|
||||||
const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
||||||
if (legacyResult.count > 0) {
|
if (legacyResult.count > 0) {
|
||||||
console.log(`Found ${legacyResult.count} occurrences with legacy format`);
|
console.log(`Found ${legacyResult.count} occurrences with legacy format`);
|
||||||
@@ -505,18 +477,14 @@ class ClientPatcher {
|
|||||||
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Writing patched binary...', 80);
|
||||||
progressCallback('Writing patched binary...', 80);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Writing patched binary...');
|
console.log('Writing patched binary...');
|
||||||
fs.writeFileSync(clientPath, finalData);
|
fs.writeFileSync(clientPath, finalData);
|
||||||
|
|
||||||
this.markAsPatched(clientPath);
|
this.markAsPatched(clientPath);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching complete', 100);
|
||||||
progressCallback('Patching complete', 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
|
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
|
||||||
console.log('=== Patching Complete ===');
|
console.log('=== Patching Complete ===');
|
||||||
@@ -525,16 +493,45 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the server JAR by downloading pre-patched version
|
* Check if server JAR contains DualAuth classes (was patched)
|
||||||
* @param {string} serverPath - Path to the HytaleServer.jar
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @param {string} javaPath - Path to Java executable (unused, kept for compatibility)
|
|
||||||
* @returns {object} Result object with success status and details
|
|
||||||
*/
|
*/
|
||||||
async patchServer(serverPath, progressCallback, javaPath = null) {
|
serverJarContainsDualAuth(serverPath) {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(serverPath);
|
||||||
|
// Check for DualAuthContext class signature in JAR
|
||||||
|
const signature = Buffer.from('DualAuthContext', 'utf8');
|
||||||
|
return data.includes(signature);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate downloaded file is not corrupt/partial
|
||||||
|
* Server JAR should be at least 50MB
|
||||||
|
*/
|
||||||
|
validateServerJarSize(serverPath) {
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(serverPath);
|
||||||
|
const minSize = 50 * 1024 * 1024; // 50MB minimum
|
||||||
|
if (stats.size < minSize) {
|
||||||
|
console.error(` Downloaded JAR too small: ${(stats.size / 1024 / 1024).toFixed(2)} MB (expected >50MB)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(` Downloaded size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch server JAR by downloading pre-patched version from CDN
|
||||||
|
*/
|
||||||
|
async patchServer(serverPath, progressCallback) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
|
|
||||||
console.log('=== Server Patcher TEMP SYSTEM NEED TO BE FIXED ===');
|
console.log('=== Server Patcher (Pre-patched Download) ===');
|
||||||
console.log(`Target: ${serverPath}`);
|
console.log(`Target: ${serverPath}`);
|
||||||
console.log(`Domain: ${newDomain}`);
|
console.log(`Domain: ${newDomain}`);
|
||||||
|
|
||||||
@@ -546,61 +543,82 @@ class ClientPatcher {
|
|||||||
|
|
||||||
// Check if already patched
|
// Check if already patched
|
||||||
const patchFlagFile = serverPath + '.dualauth_patched';
|
const patchFlagFile = serverPath + '.dualauth_patched';
|
||||||
|
let needsRestore = false;
|
||||||
|
|
||||||
if (fs.existsSync(patchFlagFile)) {
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
try {
|
try {
|
||||||
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
||||||
if (flagData.domain === newDomain) {
|
if (flagData.domain === newDomain) {
|
||||||
|
// Verify JAR actually contains DualAuth classes (game may have auto-updated)
|
||||||
|
if (this.serverJarContainsDualAuth(serverPath)) {
|
||||||
console.log(`Server already patched for ${newDomain}, skipping`);
|
console.log(`Server already patched for ${newDomain}, skipping`);
|
||||||
if (progressCallback) progressCallback('Server already patched', 100);
|
if (progressCallback) progressCallback('Server already patched', 100);
|
||||||
return { success: true, alreadyPatched: true };
|
return { success: true, alreadyPatched: true };
|
||||||
|
} else {
|
||||||
|
console.log(' Flag exists but JAR not patched (was auto-updated?), will re-download...');
|
||||||
|
// Delete stale flag file
|
||||||
|
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Server patched for "${flagData.domain}", need to change to "${newDomain}"`);
|
||||||
|
needsRestore = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Flag file corrupt, re-patch
|
// Flag file corrupt, re-patch
|
||||||
|
console.log(' Flag file corrupt, will re-download');
|
||||||
|
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore backup if patched for different domain
|
||||||
|
if (needsRestore) {
|
||||||
|
const backupPath = serverPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
|
||||||
|
console.log('Restoring original JAR from backup for re-patching...');
|
||||||
|
fs.copyFileSync(backupPath, serverPath);
|
||||||
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
|
fs.unlinkSync(patchFlagFile);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(' No backup found to restore - will download fresh patched JAR');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create backup
|
// Create backup
|
||||||
if (progressCallback) progressCallback('Creating backup...', 10);
|
if (progressCallback) progressCallback('Creating backup...', 10);
|
||||||
console.log('Creating backup...');
|
console.log('Creating backup...');
|
||||||
this.backupClient(serverPath);
|
const backupResult = this.backupClient(serverPath);
|
||||||
|
if (!backupResult) {
|
||||||
|
console.warn(' Could not create backup - proceeding without backup');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only support standard domain (auth.sanasol.ws) via pre-patched download
|
||||||
|
if (newDomain !== 'auth.sanasol.ws' && newDomain !== 'sanasol.ws') {
|
||||||
|
console.error(`Domain "${newDomain}" requires DualAuthPatcher - only auth.sanasol.ws is supported via pre-patched download`);
|
||||||
|
return { success: false, error: `Unsupported domain: ${newDomain}. Only auth.sanasol.ws is supported.` };
|
||||||
|
}
|
||||||
|
|
||||||
// Download pre-patched JAR
|
// Download pre-patched JAR
|
||||||
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
|
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
|
||||||
console.log('Downloading pre-patched HytaleServer.jar');
|
console.log('Downloading pre-patched HytaleServer.jar...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
|
const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
https.get(url, (response) => {
|
const handleResponse = (response) => {
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||||
// Follow redirect
|
https.get(response.headers.location, handleResponse).on('error', reject);
|
||||||
https.get(response.headers.location, (redirectResponse) => {
|
|
||||||
if (redirectResponse.statusCode !== 200) {
|
|
||||||
reject(new Error(`Failed to download: HTTP ${redirectResponse.statusCode}`));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = fs.createWriteStream(serverPath);
|
if (response.statusCode !== 200) {
|
||||||
const totalSize = parseInt(redirectResponse.headers['content-length'], 10);
|
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
||||||
let downloaded = 0;
|
return;
|
||||||
|
|
||||||
redirectResponse.on('data', (chunk) => {
|
|
||||||
downloaded += chunk.length;
|
|
||||||
if (progressCallback && totalSize) {
|
|
||||||
const percent = 30 + Math.floor((downloaded / totalSize) * 60);
|
|
||||||
progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
redirectResponse.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}).on('error', reject);
|
|
||||||
} else if (response.statusCode === 200) {
|
|
||||||
const file = fs.createWriteStream(serverPath);
|
const file = fs.createWriteStream(serverPath);
|
||||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
@@ -618,10 +636,9 @@ class ClientPatcher {
|
|||||||
file.close();
|
file.close();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
} else {
|
};
|
||||||
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
|
||||||
}
|
https.get(url, handleResponse).on('error', (err) => {
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(serverPath, () => {});
|
fs.unlink(serverPath, () => {});
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
@@ -629,12 +646,42 @@ class ClientPatcher {
|
|||||||
|
|
||||||
console.log(' Download successful');
|
console.log(' Download successful');
|
||||||
|
|
||||||
|
// Verify downloaded JAR size and contents
|
||||||
|
if (progressCallback) progressCallback('Verifying downloaded JAR...', 95);
|
||||||
|
|
||||||
|
if (!this.validateServerJarSize(serverPath)) {
|
||||||
|
console.error('Downloaded JAR appears corrupt or incomplete');
|
||||||
|
|
||||||
|
// Restore backup on verification failure
|
||||||
|
const backupPath = serverPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
fs.copyFileSync(backupPath, serverPath);
|
||||||
|
console.log('Restored backup after verification failure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: 'Downloaded JAR verification failed - file too small (corrupt/partial download)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.serverJarContainsDualAuth(serverPath)) {
|
||||||
|
console.error('Downloaded JAR does not contain DualAuth classes - invalid or corrupt download');
|
||||||
|
|
||||||
|
// Restore backup on verification failure
|
||||||
|
const backupPath = serverPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
fs.copyFileSync(backupPath, serverPath);
|
||||||
|
console.log('Restored backup after verification failure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: 'Downloaded JAR verification failed - missing DualAuth classes' };
|
||||||
|
}
|
||||||
|
console.log(' Verification successful - DualAuth classes present');
|
||||||
|
|
||||||
// Mark as patched
|
// Mark as patched
|
||||||
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
||||||
domain: newDomain,
|
domain: newDomain,
|
||||||
patchedAt: new Date().toISOString(),
|
patchedAt: new Date().toISOString(),
|
||||||
patcher: 'PrePatchedDownload',
|
patcher: 'PrePatchedDownload',
|
||||||
source: 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar'
|
source: 'https://download.sanasol.ws/download/HytaleServer.jar'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Server patching complete', 100);
|
if (progressCallback) progressCallback('Server patching complete', 100);
|
||||||
@@ -656,290 +703,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find Java executable - uses bundled JRE first (same as game uses)
|
* Find client binary path based on platform
|
||||||
* Falls back to system Java if bundled not available
|
|
||||||
*/
|
|
||||||
findJava() {
|
|
||||||
// 1. Try bundled JRE first (comes with the game)
|
|
||||||
try {
|
|
||||||
const bundled = getBundledJavaPath(JRE_DIR);
|
|
||||||
if (bundled && fs.existsSync(bundled)) {
|
|
||||||
console.log(`Using bundled Java: ${bundled}`);
|
|
||||||
return bundled;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Bundled not available
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Try javaManager's getJavaExec (handles all fallbacks)
|
|
||||||
try {
|
|
||||||
const javaExec = getJavaExec(JRE_DIR);
|
|
||||||
if (javaExec && fs.existsSync(javaExec)) {
|
|
||||||
console.log(`Using Java from javaManager: ${javaExec}`);
|
|
||||||
return javaExec;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Not available
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check JAVA_HOME
|
|
||||||
if (process.env.JAVA_HOME) {
|
|
||||||
const javaHome = process.env.JAVA_HOME;
|
|
||||||
const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
|
|
||||||
if (fs.existsSync(javaBin)) {
|
|
||||||
console.log(`Using Java from JAVA_HOME: ${javaBin}`);
|
|
||||||
return javaBin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Try 'java' from PATH
|
|
||||||
try {
|
|
||||||
execSync('java -version 2>&1', { encoding: 'utf8' });
|
|
||||||
console.log('Using Java from PATH');
|
|
||||||
return 'java';
|
|
||||||
} catch (e) {
|
|
||||||
// Not in PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download DualAuthPatcher from hytale-auth-server if not present
|
|
||||||
*/
|
|
||||||
async ensurePatcherDownloaded(patcherDir) {
|
|
||||||
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
|
||||||
const patcherUrl = 'https://raw.githubusercontent.com/sanasol/hytale-auth-server/master/patcher/DualAuthPatcher.java';
|
|
||||||
|
|
||||||
if (!fs.existsSync(patcherDir)) {
|
|
||||||
fs.mkdirSync(patcherDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(patcherJava)) {
|
|
||||||
console.log('Downloading DualAuthPatcher from hytale-auth-server...');
|
|
||||||
try {
|
|
||||||
const https = require('https');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const file = fs.createWriteStream(patcherJava);
|
|
||||||
https.get(patcherUrl, (response) => {
|
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
||||||
// Follow redirect
|
|
||||||
https.get(response.headers.location, (redirectResponse) => {
|
|
||||||
redirectResponse.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}).on('error', reject);
|
|
||||||
} else {
|
|
||||||
response.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(patcherJava, () => {});
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log(' Downloaded DualAuthPatcher.java');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(` Failed to download DualAuthPatcher: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download ASM libraries if not present
|
|
||||||
*/
|
|
||||||
async ensureAsmLibraries(libDir) {
|
|
||||||
if (!fs.existsSync(libDir)) {
|
|
||||||
fs.mkdirSync(libDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const libs = [
|
|
||||||
{ name: 'asm-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar' },
|
|
||||||
{ name: 'asm-tree-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar' },
|
|
||||||
{ name: 'asm-util-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-util/9.6/asm-util-9.6.jar' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const lib of libs) {
|
|
||||||
const libPath = path.join(libDir, lib.name);
|
|
||||||
if (!fs.existsSync(libPath)) {
|
|
||||||
console.log(`Downloading ${lib.name}...`);
|
|
||||||
try {
|
|
||||||
const https = require('https');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const file = fs.createWriteStream(libPath);
|
|
||||||
https.get(lib.url, (response) => {
|
|
||||||
response.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(libPath, () => {});
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log(` Downloaded ${lib.name}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(` Failed to download ${lib.name}: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile DualAuthPatcher if needed
|
|
||||||
*/
|
|
||||||
async compileDualAuthPatcher(java, patcherDir, libDir) {
|
|
||||||
const patcherClass = path.join(patcherDir, 'DualAuthPatcher.class');
|
|
||||||
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
|
||||||
|
|
||||||
// Check if already compiled and up to date
|
|
||||||
if (fs.existsSync(patcherClass)) {
|
|
||||||
const classTime = fs.statSync(patcherClass).mtime;
|
|
||||||
const javaTime = fs.statSync(patcherJava).mtime;
|
|
||||||
if (classTime > javaTime) {
|
|
||||||
console.log('DualAuthPatcher already compiled');
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Compiling DualAuthPatcher...');
|
|
||||||
|
|
||||||
const javac = java.replace(/java(\.exe)?$/, 'javac$1');
|
|
||||||
const classpath = [
|
|
||||||
path.join(libDir, 'asm-9.6.jar'),
|
|
||||||
path.join(libDir, 'asm-tree-9.6.jar'),
|
|
||||||
path.join(libDir, 'asm-util-9.6.jar')
|
|
||||||
].join(process.platform === 'win32' ? ';' : ':');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fix PATH for packaged Electron apps on Windows
|
|
||||||
const execOptions = {
|
|
||||||
stdio: 'pipe',
|
|
||||||
cwd: patcherDir,
|
|
||||||
env: { ...process.env }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add system32 to PATH for Windows to find cmd.exe
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
const systemRoot = process.env.SystemRoot || 'C:\\WINDOWS';
|
|
||||||
const systemPath = `${systemRoot}\\system32;${systemRoot};${systemRoot}\\System32\\Wbem`;
|
|
||||||
execOptions.env.PATH = execOptions.env.PATH
|
|
||||||
? `${systemPath};${execOptions.env.PATH}`
|
|
||||||
: systemPath;
|
|
||||||
execOptions.shell = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, execOptions);
|
|
||||||
console.log(' Compilation successful');
|
|
||||||
return { success: true };
|
|
||||||
} catch (e) {
|
|
||||||
const error = `Failed to compile DualAuthPatcher: ${e.message}`;
|
|
||||||
console.error(error);
|
|
||||||
if (e.stderr) console.error(e.stderr.toString());
|
|
||||||
return { success: false, error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run DualAuthPatcher on the server JAR
|
|
||||||
*/
|
|
||||||
async runDualAuthPatcher(java, classpath, serverPath, domain) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const args = ['-cp', classpath, 'DualAuthPatcher', serverPath];
|
|
||||||
const env = { ...process.env, HYTALE_AUTH_DOMAIN: domain };
|
|
||||||
|
|
||||||
console.log(`Running: java ${args.join(' ')}`);
|
|
||||||
console.log(` HYTALE_AUTH_DOMAIN=${domain}`);
|
|
||||||
|
|
||||||
const proc = spawn(java, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
||||||
|
|
||||||
let stdout = '';
|
|
||||||
let stderr = '';
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data) => {
|
|
||||||
const str = data.toString();
|
|
||||||
stdout += str;
|
|
||||||
console.log(str.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr.on('data', (data) => {
|
|
||||||
const str = data.toString();
|
|
||||||
stderr += str;
|
|
||||||
console.error(str.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve({ success: true, stdout });
|
|
||||||
} else {
|
|
||||||
resolve({ success: false, error: `Patcher exited with code ${code}: ${stderr}` });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('error', (err) => {
|
|
||||||
resolve({ success: false, error: `Failed to run patcher: ${err.message}` });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy server patcher (simple domain replacement, no dual auth)
|
|
||||||
* Use patchServer() for full dual auth support
|
|
||||||
*/
|
|
||||||
async patchServerLegacy(serverPath, progressCallback) {
|
|
||||||
const newDomain = this.getNewDomain();
|
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
|
||||||
|
|
||||||
console.log('=== Legacy Server Patcher ===');
|
|
||||||
console.log(`Target: ${serverPath}`);
|
|
||||||
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
|
||||||
|
|
||||||
if (!fs.existsSync(serverPath)) {
|
|
||||||
return { success: false, error: `Server JAR not found: ${serverPath}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Patching server...', 20);
|
|
||||||
|
|
||||||
console.log('Opening server JAR...');
|
|
||||||
const zip = new AdmZip(serverPath);
|
|
||||||
const entries = zip.getEntries();
|
|
||||||
|
|
||||||
let totalCount = 0;
|
|
||||||
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const name = entry.entryName;
|
|
||||||
if (name.endsWith('.class') || name.endsWith('.properties') ||
|
|
||||||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
|
|
||||||
const data = entry.getData();
|
|
||||||
if (data.includes(oldUtf8)) {
|
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
|
||||||
if (count > 0) {
|
|
||||||
zip.updateFile(entry.entryName, patchedData);
|
|
||||||
totalCount += count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalCount > 0) {
|
|
||||||
zip.writeZip(serverPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Complete', 100);
|
|
||||||
return { success: true, patchCount: totalCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the client binary path based on platform
|
|
||||||
*/
|
*/
|
||||||
findClientPath(gameDir) {
|
findClientPath(gameDir) {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
@@ -961,7 +725,9 @@ class ClientPatcher {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find server JAR path
|
||||||
|
*/
|
||||||
findServerPath(gameDir) {
|
findServerPath(gameDir) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
||||||
@@ -978,9 +744,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure both client and server are patched before launching
|
* Ensure both client and server are patched before launching
|
||||||
* @param {string} gameDir - Path to the game directory
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @param {string} javaPath - Optional path to Java executable for server patching
|
|
||||||
*/
|
*/
|
||||||
async ensureClientPatched(gameDir, progressCallback, javaPath = null) {
|
async ensureClientPatched(gameDir, progressCallback, javaPath = null) {
|
||||||
const results = {
|
const results = {
|
||||||
@@ -991,9 +754,7 @@ class ClientPatcher {
|
|||||||
|
|
||||||
const clientPath = this.findClientPath(gameDir);
|
const clientPath = this.findClientPath(gameDir);
|
||||||
if (clientPath) {
|
if (clientPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching client binary...', 10);
|
||||||
progressCallback('Patching client binary...', 10);
|
|
||||||
}
|
|
||||||
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
||||||
@@ -1006,14 +767,12 @@ class ClientPatcher {
|
|||||||
|
|
||||||
const serverPath = this.findServerPath(gameDir);
|
const serverPath = this.findServerPath(gameDir);
|
||||||
if (serverPath) {
|
if (serverPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching server JAR...', 50);
|
||||||
progressCallback('Patching server JAR...', 50);
|
|
||||||
}
|
|
||||||
results.server = await this.patchServer(serverPath, (msg, pct) => {
|
results.server = await this.patchServer(serverPath, (msg, pct) => {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
||||||
}
|
}
|
||||||
}, javaPath);
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('Could not find HytaleServer.jar');
|
console.warn('Could not find HytaleServer.jar');
|
||||||
results.server = { success: false, error: 'Server JAR not found' };
|
results.server = { success: false, error: 'Server JAR not found' };
|
||||||
@@ -1023,9 +782,7 @@ class ClientPatcher {
|
|||||||
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
|
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
|
||||||
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
|
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching complete', 100);
|
||||||
progressCallback('Patching complete', 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
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` |
|
||||||
64
main.js
64
main.js
@@ -176,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', () => {
|
||||||
@@ -201,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) => {
|
||||||
@@ -218,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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -859,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');
|
||||||
@@ -1086,8 +1115,37 @@ ipcMain.handle('download-update', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('install-update', () => {
|
ipcMain.handle('install-update', async () => {
|
||||||
|
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);
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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