Compare commits

..

14 Commits

Author SHA1 Message Date
sanasol
0b1716c168 v2.4.3: Replace Discord links with Community Chat & Telegram
Discord server was DMCA'd. All Discord links replaced with:
- Community Chat (Stoat): chat.sanhost.net
- Telegram Channel: @hf2p_og
- Telegram Group: @sanhostnet

Launcher UI: added community links on main screen, renamed
Discord nav to Community Chat, updated popup modal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 01:05:49 +01:00
sanasol
9628363455 Update default wrapper config: remove -XX:+UseCompactObjectHeaders strip flag 2026-02-24 19:32:25 +01:00
amiay
4932a7a51c v2.4.2: Update Mods manager 2026-02-24 17:55:28 +01:00
amiay
5170f453ea Merge pull request 'added search bar in "my mods"' (#3) from amiay/hytale-f2p:develop into develop
Reviewed-on: https://git.sanhost.net/sanasol/hytale-f2p/pulls/3
2026-02-24 16:31:54 +00:00
amiay
db3b2fc966 added search bar in my mods
added search bar in my mods
2026-02-24 17:31:13 +01:00
amiay
2f5820e850 Merge pull request 'Mods update' (#2) from amiay/hytale-f2p:develop into develop
Reviewed-on: https://git.sanhost.net/sanasol/hytale-f2p/pulls/2
2026-02-24 16:22:55 +00:00
dsqd2505-netizen
4abb455e0f update "My mods" 2026-02-24 17:15:47 +01:00
dsqd2505-netizen
d5828463f9 Added mods version selector
Added mods version selector
2026-02-24 17:04:46 +01:00
sanasol
0d15659dc0 v2.4.1: Replace raw wrapper script editor with structured config UI
Replace the raw textarea script editor with a structured form for Java
wrapper configuration. Users now manage two lists (JVM flags to strip,
args to inject with server/always condition) instead of editing bash/batch
scripts directly. Scripts are generated at launch time from the structured
config. Includes collapsible script preview for power users.
2026-02-24 16:53:19 +01:00
sanasol
19c8991a44 v2.4.0: Add Send Logs, fix Intel Arc iGPU misdetection
- Add Send Logs button for one-click log submission to support
- Fix Intel Arc Graphics (Meteor Lake/Lunar Lake) on bus 00 being
  classified as discrete GPU instead of integrated
2026-02-24 13:57:10 +01:00
sanasol
320ca54758 Add Send Logs button for one-click log submission to support
Collects launcher logs, game client logs, and config snapshot into a ZIP
file and uploads to auth server. Shows submission ID for sharing with
support. Includes i18n for all 11 locales.
2026-02-24 13:50:23 +01:00
sanasol
e14d56ef48 v2.3.9: Fix stalled processes and AOT cache crash
- Auto-kill stalled HytaleClient/java processes before launch and repair
  (cross-platform: Windows taskkill+PowerShell, macOS/Linux pkill)
- Remove HytaleServer.aot before launch to prevent incompatible AOT cache
  causing fastutil ClassNotFoundException on singleplayer
- safeRemoveDirectory auto-kills processes on EPERM/EBUSY before retry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 12:52:07 +01:00
sanasol
a649bf1fcc Update README: replace outdated GitHub URL with Forgejo URL for start.sh script 2026-02-23 03:12:39 +01:00
sanasol
66faa1bb1e Update README files with free hosted server instructions
Added detailed steps to set up a free Hytale server via play.hosting in both README files. Updated outdated URLs for the F2P Launcher to the new repository location. Improved clarity in self-hosted setup sections.
2026-02-23 03:11:02 +01:00
35 changed files with 1737 additions and 78 deletions

View File

@@ -36,7 +36,8 @@ This Code of Conduct applies within all community spaces, and also applies when
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Discord Server, message Founders/Devs](https://discord.gg/Fhbb9Yk5WW). All complaints will be reviewed and investigated promptly and fairly.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Community Chat, message Founders/Devs](https://chat.sanhost.net/invite/Tfz4jCK4).
<!-- Discord: https://discord.gg/Fhbb9Yk5WW --> All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.

View File

@@ -22,7 +22,7 @@ body:
value: |
If you need help or support with using the launcher, please fill out this support request.
Provide as much detail as possible so we can assist you effectively.
**Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/Fhbb9Yk5WW)!
**Need a quick assistance?** Join our [Community Chat](https://chat.sanhost.net/invite/Tfz4jCK4) or [Telegram](https://t.me/hf2p_og)!
- type: textarea
id: question

View File

@@ -57,8 +57,8 @@
<span class="nav-tooltip">Logs</span>
</div>
<div class="nav-item" onclick="openDiscordExternal()">
<i class="fab fa-discord"></i>
<span class="nav-tooltip">Discord</span>
<i class="fas fa-comments"></i>
<span class="nav-tooltip">Community Chat</span>
</div>
</div>
@@ -199,6 +199,21 @@
<i class="fas fa-play"></i>
<span data-i18n="play.playButton">PLAY HYTALE</span>
</button>
<div style="display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 12px; font-size: 12px;">
<span style="color: #93a3b8;">Telegram:</span>
<a href="#" onclick="window.electronAPI?.openExternal('https://t.me/sanhostnet'); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
<i class="fas fa-users"></i> Group
</a>
<span style="color: #4b5563;">|</span>
<a href="#" onclick="window.electronAPI?.openExternal('https://t.me/hf2p_og'); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
<i class="fab fa-telegram"></i> Channel
</a>
<span style="color: #4b5563;">|</span>
<a href="#" onclick="openDiscordExternal(); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
<i class="fas fa-comments"></i> Community Chat
</a>
</div>
</div>
</div>
@@ -430,8 +445,70 @@
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-scroll"></i>
<span data-i18n="settings.wrapperConfig">Java Wrapper Configuration</span>
</h3>
<p class="settings-hint" style="margin-bottom: 12px;">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.wrapperConfigHint">Configure how the Java wrapper handles JVM flags and arguments at launch time.</span>
</p>
<!-- Strip Flags -->
<label class="settings-label" style="margin-bottom: 6px;">
<span data-i18n="settings.wrapperStripFlags">JVM Flags to Remove</span>
</label>
<div id="wrapperStripFlagsList" class="wrapper-items-list"></div>
<div style="display: flex; gap: 6px; margin-top: 6px;">
<input type="text" id="wrapperAddFlagInput" class="settings-input" style="flex:1;"
data-i18n-placeholder="settings.wrapperAddFlagPlaceholder" placeholder="e.g. -XX:+SomeFlag" spellcheck="false">
<button id="wrapperAddFlagBtn" class="settings-browse-btn">
<i class="fas fa-plus"></i>
<span data-i18n="settings.wrapperAdd">Add</span>
</button>
</div>
<!-- Inject Args -->
<label class="settings-label" style="margin-top: 16px; margin-bottom: 6px;">
<span data-i18n="settings.wrapperInjectArgs">Arguments to Inject</span>
</label>
<div id="wrapperInjectArgsList" class="wrapper-items-list"></div>
<div style="display: flex; gap: 6px; margin-top: 6px;">
<input type="text" id="wrapperAddArgInput" class="settings-input" style="flex:1;"
data-i18n-placeholder="settings.wrapperAddArgPlaceholder" placeholder="e.g. --some-flag" spellcheck="false">
<select id="wrapperAddArgCondition" class="wrapper-condition-select">
<option value="server" data-i18n="settings.wrapperConditionServer">Server Only</option>
<option value="always" data-i18n="settings.wrapperConditionAlways">Always</option>
</select>
<button id="wrapperAddArgBtn" class="settings-browse-btn">
<i class="fas fa-plus"></i>
<span data-i18n="settings.wrapperAdd">Add</span>
</button>
</div>
<!-- Restore Defaults -->
<div style="margin-top: 12px;">
<button id="wrapperRestoreDefaultsBtn" class="settings-browse-btn" style="background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.3);">
<i class="fas fa-undo"></i>
<span data-i18n="settings.wrapperRestoreDefaults">Restore Defaults</span>
</button>
</div>
<!-- Script Preview (collapsible) -->
<div style="margin-top: 12px;">
<button id="wrapperPreviewToggle" class="wrapper-preview-toggle">
<i class="fas fa-chevron-right" id="wrapperPreviewChevron"></i>
<span data-i18n="settings.wrapperAdvancedPreview">Advanced: Script Preview</span>
</button>
<div id="wrapperPreviewContainer" style="display: none; margin-top: 8px;">
<pre id="wrapperPreviewContent" class="wrapper-preview-content"></pre>
</div>
</div>
</div>
</div>
<div class="settings-column">
<div class="settings-section">
<h3 class="settings-section-title">
@@ -575,6 +652,9 @@
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
Folder</span>
</button>
<button class="logs-action-btn logs-send-btn" id="sendLogsBtn" onclick="sendLogs()">
<i class="fas fa-paper-plane"></i> <span data-i18n="settings.logsSend">Send Logs</span>
</button>
</div>
</div>
<div id="logsTerminal" class="logs-terminal">
@@ -598,7 +678,11 @@
<i class="fas fa-times"></i>
</button>
</div>
<div class="mods-modal-body">
<div class="mods-modal-body" style="padding-top: 0;">
<div class="mods-search-container" style="margin: 1.5rem; margin-bottom: 1rem;">
<i class="fas fa-search"></i>
<input type="text" id="myModsSearch" placeholder="Search installed mods..." class="mods-search" />
</div>
<div id="installedModsList" class="installed-mods-list">
</div>
</div>
@@ -768,14 +852,14 @@
<div class="modal-content discord-popup-modal">
<div class="modal-header">
<div class="discord-popup-header">
<i class="fab fa-discord"></i>
<h2 class="modal-title">Join Our Discord Community</h2>
<i class="fas fa-comments"></i>
<h2 class="modal-title">Join Our Community</h2>
</div>
</div>
<div class="modal-body">
<div class="discord-popup-body">
<p class="discord-popup-text">
Join our community of over <strong>5000 members</strong> and stay connected!
Join our community and stay connected!
</p>
<p class="discord-popup-text">
Get the latest news, updates, and announcements about the launcher.
@@ -783,11 +867,11 @@
<p class="discord-popup-text">
Find help, report bugs, share your feedback, and connect with other players.
</p>
<div class="discord-popup-actions">
<button class="discord-popup-btn primary" onclick="joinDiscord()">
<i class="fab fa-discord"></i>
Join Discord
<i class="fas fa-comments"></i>
Join Community Chat
</button>
<button class="discord-popup-btn secondary" onclick="closeDiscordPopup()">
Maybe Later
@@ -802,6 +886,25 @@
<script src="js/featured.js"></script>
<script type="module" src="js/settings.js"></script>
<script type="module" src="js/update.js"></script>
<!-- Version Selection Modal (Isolated Container) -->
<div id="versionSelectModal" class="modal-overlay" style="display: none; position: fixed; inset: 0; z-index: 9999; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(5px); align-items: center; justify-content: center;">
<div class="glass-panel" style="width: 100%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; border-radius: 12px; overflow: hidden; margin: 20px;">
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 1.5rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<h3 style="margin: 0; font-size: 1.25rem;">Select Version</h3>
<button id="closeVersionModal" class="modal-close" style="background: none; border: none; color: #a0a0a0; font-size: 1.25rem; cursor: pointer;"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto;">
<div id="versionList" class="version-list-container">
<div class="loading-versions" style="display: flex; flex-direction: column; align-items: center; gap: 1rem; color: #a0a0a0;">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<span>Loading versions...</span>
</div>
</div>
</div>
</div>
</div>
<!-- updater.js disabled - using update.js instead which has skip button and macOS handling -->
</body>

View File

@@ -66,6 +66,113 @@ async function openLogsFolder() {
await window.electronAPI.openLogsFolder();
}
async function sendLogs() {
const btn = document.getElementById('sendLogsBtn');
if (!btn || btn.disabled) return;
// Get i18n strings with fallbacks
const i18n = window.i18n || {};
const sendingText = (i18n.settings && i18n.settings.logsSending) || 'Sending...';
const sentText = (i18n.settings && i18n.settings.logsSent) || 'Sent!';
const failedText = (i18n.settings && i18n.settings.logsSendFailed) || 'Failed';
const sendText = (i18n.settings && i18n.settings.logsSend) || 'Send Logs';
const originalHTML = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${sendingText}`;
try {
const result = await window.electronAPI.sendLogs();
if (result.success) {
btn.innerHTML = `<i class="fas fa-check"></i> ${sentText}`;
showLogSubmissionResult(result.id);
} else {
btn.innerHTML = `<i class="fas fa-times"></i> ${failedText}`;
console.error('Send logs failed:', result.error);
// Show error notification if available
if (window.LauncherUI && window.LauncherUI.showNotification) {
window.LauncherUI.showNotification(result.error || 'Failed to send logs', 'error');
}
}
} catch (err) {
console.error('Send logs error:', err);
btn.innerHTML = `<i class="fas fa-times"></i> ${failedText}`;
}
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = originalHTML;
}, 3000);
}
function showLogSubmissionResult(id) {
// Remove existing popup if any
const existing = document.getElementById('logSubmissionPopup');
if (existing) existing.remove();
const i18n = window.i18n || {};
const idLabel = (i18n.settings && i18n.settings.logsSubmissionId) || 'Submission ID';
const copyText = (i18n.common && i18n.common.copy) || 'Copy';
const closeText = (i18n.common && i18n.common.close) || 'Close';
const shareText = (i18n.settings && i18n.settings.logsShareId) || 'Share this ID with support when reporting issues';
const popup = document.createElement('div');
popup.id = 'logSubmissionPopup';
popup.style.cssText = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(20, 20, 35, 0.98); border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 12px; padding: 24px 32px; z-index: 10000;
box-shadow: 0 20px 60px rgba(0,0,0,0.5); text-align: center;
min-width: 320px; backdrop-filter: blur(10px);
`;
popup.innerHTML = `
<div style="margin-bottom: 16px;">
<i class="fas fa-check-circle" style="font-size: 2em; color: #00d4ff;"></i>
</div>
<div style="color: #888; font-size: 0.85em; margin-bottom: 8px;">${idLabel}</div>
<div id="logSubId" style="font-family: monospace; font-size: 1.5em; color: #00d4ff; letter-spacing: 2px; margin-bottom: 12px; user-select: all;">${id}</div>
<div style="color: #666; font-size: 0.8em; margin-bottom: 20px;">${shareText}</div>
<div style="display: flex; gap: 10px; justify-content: center;">
<button onclick="copyLogSubmissionId('${id}')" style="
background: rgba(0,212,255,0.2); border: 1px solid rgba(0,212,255,0.3);
color: #00d4ff; padding: 8px 20px; border-radius: 6px; cursor: pointer;
font-size: 0.9em;
"><i class="fas fa-copy"></i> ${copyText}</button>
<button onclick="document.getElementById('logSubmissionPopup').remove()" style="
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
color: #ccc; padding: 8px 20px; border-radius: 6px; cursor: pointer;
font-size: 0.9em;
">${closeText}</button>
</div>
`;
document.body.appendChild(popup);
// Auto-close after 30s
setTimeout(() => {
if (document.getElementById('logSubmissionPopup')) {
popup.remove();
}
}, 30000);
}
async function copyLogSubmissionId(id) {
try {
await navigator.clipboard.writeText(id);
const btn = event.target.closest('button');
if (btn) {
const orig = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
setTimeout(() => { btn.innerHTML = orig; }, 1500);
}
} catch (err) {
console.error('Failed to copy submission ID:', err);
}
}
function openLogs() {
// Navigation is handled by sidebar logic, but we can trigger a refresh
window.LauncherUI.showPage('logs-page');
@@ -77,6 +184,8 @@ function openLogs() {
window.refreshLogs = refreshLogs;
window.copyLogs = copyLogs;
window.openLogsFolder = openLogsFolder;
window.sendLogs = sendLogs;
window.copyLogSubmissionId = copyLogSubmissionId;
window.openLogs = openLogs;
// Auto-load logs when the page becomes active

View File

@@ -49,6 +49,18 @@ function setupModsEventListeners() {
closeModalBtn.addEventListener('click', closeMyModsModal);
}
const myModsSearchInput = document.getElementById('myModsSearch');
if (myModsSearchInput) {
let myModsSearchTimeout;
myModsSearchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase().trim();
clearTimeout(myModsSearchTimeout);
myModsSearchTimeout = setTimeout(() => {
filterInstalledMods(query);
}, 300);
});
}
const modal = document.getElementById('myModsModal');
if (modal) {
modal.addEventListener('click', (e) => {
@@ -78,12 +90,30 @@ function setupModsEventListeners() {
}
});
}
const browseContainer = document.getElementById('browseModsList');
if (browseContainer) {
browseContainer.addEventListener('click', (e) => {
const installBtn = e.target.closest('[data-install-mod-id]');
if (installBtn) {
const modId = installBtn.getAttribute('data-install-mod-id');
const mod = browseMods.find(m => m.id == modId);
if (mod) {
openVersionSelectModal(mod);
}
}
});
}
}
function openMyModsModal() {
const modal = document.getElementById('myModsModal');
if (modal) {
modal.classList.add('active');
const searchInput = document.getElementById('myModsSearch');
if (searchInput) {
searchInput.value = '';
}
loadInstalledMods();
}
}
@@ -92,6 +122,10 @@ function closeMyModsModal() {
const modal = document.getElementById('myModsModal');
if (modal) {
modal.classList.remove('active');
const searchInput = document.getElementById('myModsSearch');
if (searchInput) {
searchInput.value = '';
}
}
}
@@ -113,19 +147,39 @@ async function loadInstalledMods() {
}
}
function filterInstalledMods(query) {
if (!query || query === '') {
displayInstalledMods(installedMods);
return;
}
const filtered = installedMods.filter(mod => {
const nameMatch = mod.name?.toLowerCase().includes(query);
const fileNameMatch = mod.fileName?.toLowerCase().includes(query);
const descriptionMatch = mod.description?.toLowerCase().includes(query);
const authorMatch = mod.author?.toLowerCase().includes(query);
return nameMatch || fileNameMatch || descriptionMatch || authorMatch;
});
displayInstalledMods(filtered);
}
function displayInstalledMods(mods) {
const modsContainer = document.getElementById('installedModsList');
if (!modsContainer) return;
if (mods.length === 0) {
const searchInput = document.getElementById('myModsSearch');
const isSearching = searchInput && searchInput.value.trim() !== '';
modsContainer.innerHTML = `
<div class=\"empty-installed-mods\">
<i class=\"fas fa-box-open\"></i>
<h4 data-i18n="mods.noModsInstalled">No Mods Installed</h4>
<p data-i18n="mods.noModsInstalledDesc">Add mods from CurseForge or import local files</p>
<i class=\"fas fa-${isSearching ? 'search' : 'box-open'}\"></i>
<h4 data-i18n="${isSearching ? 'mods.noModsFound' : 'mods.noModsInstalled'}">${isSearching ? 'No Mods Found' : 'No Mods Installed'}</h4>
<p data-i18n="${isSearching ? 'mods.noModsFoundDesc' : 'mods.noModsInstalledDesc'}">${isSearching ? 'Try a different search term' : 'Add mods from CurseForge or import local files'}</p>
</div>
`;
if (window.i18n) {
if (window.i18n && !isSearching) {
const container = modsContainer.querySelector('.empty-installed-mods');
container.querySelector('h4').textContent = window.i18n.t('mods.noModsInstalled');
container.querySelector('p').textContent = window.i18n.t('mods.noModsInstalledDesc');
@@ -165,7 +219,7 @@ function createInstalledModCard(mod) {
<div class="installed-mod-info">
<div class="installed-mod-header">
<h4 class="installed-mod-name">${mod.name}</h4>
<span class="installed-mod-version">v${mod.version}</span>
<span class="installed-mod-version">${mod.fileName || 'v' + mod.version}</span>
</div>
<p class="installed-mod-description">${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}</p>
</div>
@@ -295,13 +349,6 @@ function displayBrowseMods(mods) {
}
browseContainer.innerHTML = mods.map(mod => createBrowseModCard(mod)).join('');
mods.forEach(mod => {
const installBtn = document.getElementById(`install-${mod.id}`);
if (installBtn) {
installBtn.addEventListener('click', () => downloadAndInstallMod(mod));
}
});
}
function createBrowseModCard(mod) {
@@ -350,12 +397,12 @@ function createBrowseModCard(mod) {
${window.i18n ? window.i18n.t('mods.view') : 'VIEW'}
</button>
${!isInstalled ?
`<button id="install-${mod.id}" class="mod-btn-toggle bg-primary text-black hover:bg-primary/80">
<i class="fas fa-download"></i>
`<button data-install-mod-id=\"${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
<i class=\"fas fa-download\"></i>
${window.i18n ? window.i18n.t('mods.install') : 'INSTALL'}
</button>` :
`<button class="mod-btn-toggle bg-white/10 text-white" disabled>
<i class="fas fa-check"></i>
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
<i class=\"fas fa-check\"></i>
${window.i18n ? window.i18n.t('mods.installed') : 'INSTALLED'}
</button>`
}
@@ -364,6 +411,104 @@ function createBrowseModCard(mod) {
`;
}
let currentSelectedMod = null;
function openVersionSelectModal(mod) {
currentSelectedMod = mod;
const modal = document.getElementById('versionSelectModal');
const closeBtn = document.getElementById('closeVersionModal');
const versionList = document.getElementById('versionList');
if (modal) {
modal.style.display = 'flex';
modal.classList.add('active');
const closeHandler = () => {
modal.classList.remove('active');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
currentSelectedMod = null;
};
if (closeBtn) {
closeBtn.onclick = closeHandler;
}
modal.onclick = (e) => {
if (e.target === modal) closeHandler();
};
loadModVersions(mod.id, versionList);
}
}
async function loadModVersions(modId, container) {
container.innerHTML = `
<div class="loading-versions">
<i class="fas fa-spinner fa-spin fa-2x" style="margin-bottom: 10px; display: block;"></i>
<span>Loading versions...</span>
</div>
`;
try {
const versions = await window.electronAPI.getModFiles(modId);
if (!versions || versions.length === 0) {
container.innerHTML = `<div class="p-4 text-center text-gray-400" style="padding: 2rem;">No versions found for this mod.</div>`;
return;
}
// Sort versions by date desc (API returns desc but ensure)
versions.sort((a, b) => new Date(b.fileDate) - new Date(a.fileDate));
container.innerHTML = versions.map(file => `
<div class="version-item">
<div class="version-info">
<div class="version-name">${file.displayName}</div>
<div class="version-meta">
<span><i class="fas fa-calendar"></i> ${new Date(file.fileDate).toLocaleDateString()}</span>
<span><i class="fas fa-download"></i> ${formatNumber(file.downloadCount)}</span>
<span><i class="fas fa-file-archive"></i> ${(file.fileLength / 1024 / 1024).toFixed(2)} MB</span>
</div>
</div>
<div class="version-actions">
<button class="btn-install" data-file-id="${file.id}">
Install
</button>
</div>
</div>
`).join('');
// Add event listeners securely
container.querySelectorAll('.btn-install').forEach((btn, index) => {
const file = versions[index]; // Map index to file data
btn.onclick = () => installVersion(file);
});
} catch (error) {
console.error('Error loading versions:', error);
container.innerHTML = `<div class="p-4 text-center text-red-400" style="padding: 2rem;">Error loading versions.<br><small>${error.message}</small></div>`;
}
}
async function installVersion(file) {
if (!currentSelectedMod) return;
const modal = document.getElementById('versionSelectModal');
modal.style.display = 'none';
const modInfo = {
...currentSelectedMod,
fileId: file.id,
downloadUrl: file.downloadUrl,
fileName: file.fileName,
fileSize: file.fileLength
};
await downloadAndInstallMod(modInfo);
currentSelectedMod = null;
}
async function downloadAndInstallMod(modInfo) {
try {
const downloadMsg = window.i18n ? window.i18n.t('notifications.modsDownloading').replace('{name}', modInfo.name) : `Downloading ${modInfo.name}...`;
@@ -762,7 +907,10 @@ window.modsManager = {
closeMyModsModal,
viewModPage,
loadInstalledMods,
loadBrowseMods
loadBrowseMods,
openVersionSelectModal
};
document.addEventListener('DOMContentLoaded', initModsManager);
// Remove auto-init since we are now calling it from script.js explicitly
// which guarantees order and environment readiness
// document.addEventListener('DOMContentLoaded', initModsManager);

View File

@@ -2,7 +2,7 @@ import './ui.js';
import './install.js';
import './launcher.js';
import './news.js';
import './mods.js';
import { initModsManager } from './mods.js';
import './players.js';
import './settings.js';
import './logs.js';
@@ -15,6 +15,12 @@ let i18nInitialized = false;
if (document.readyState === 'complete' || document.readyState === 'interactive') {
updateLanguageSelector();
initModsManager();
} else {
document.addEventListener('DOMContentLoaded', () => {
updateLanguageSelector();
initModsManager();
});
}
})();
@@ -53,7 +59,8 @@ window.closeDiscordPopup = function() {
};
window.joinDiscord = async function() {
await window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
// await window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
await window.electronAPI?.openExternal('https://chat.sanhost.net/invite/Tfz4jCK4');
try {
await window.electronAPI?.saveConfig({ discordPopup: true });

View File

@@ -569,6 +569,7 @@ async function loadAllSettings() {
await loadLauncherHwAccel();
await loadGpuPreference();
await loadVersionBranch();
await loadWrapperConfigUI();
}
@@ -1254,3 +1255,235 @@ async function loadVersionBranch() {
return 'release';
}
}
// === Java Wrapper Configuration UI ===
let _wrapperConfig = null;
let _wrapperPreviewOpen = false;
async function loadWrapperConfigUI() {
try {
if (!window.electronAPI || !window.electronAPI.loadWrapperConfig) return;
_wrapperConfig = await window.electronAPI.loadWrapperConfig();
renderStripFlagsList();
renderInjectArgsList();
setupWrapperEventListeners();
} catch (error) {
console.error('Error loading wrapper config UI:', error);
}
}
function renderStripFlagsList() {
const container = document.getElementById('wrapperStripFlagsList');
if (!container || !_wrapperConfig) return;
if (_wrapperConfig.stripFlags.length === 0) {
container.innerHTML = '<div class="wrapper-items-empty">No flags configured</div>';
return;
}
container.innerHTML = '';
_wrapperConfig.stripFlags.forEach((flag, index) => {
const item = document.createElement('div');
item.className = 'wrapper-item';
item.innerHTML = `
<span class="wrapper-item-text">${escapeHtml(flag)}</span>
<button class="wrapper-item-delete" data-index="${index}" title="Remove">
<i class="fas fa-trash-alt"></i>
</button>
`;
item.querySelector('.wrapper-item-delete').addEventListener('click', () => removeStripFlag(index));
container.appendChild(item);
});
}
function renderInjectArgsList() {
const container = document.getElementById('wrapperInjectArgsList');
if (!container || !_wrapperConfig) return;
if (_wrapperConfig.injectArgs.length === 0) {
container.innerHTML = '<div class="wrapper-items-empty">No arguments configured</div>';
return;
}
container.innerHTML = '';
_wrapperConfig.injectArgs.forEach((entry, index) => {
const item = document.createElement('div');
item.className = 'wrapper-item';
const serverLabel = window.i18n ? window.i18n.t('settings.wrapperConditionServer') : 'Server Only';
const alwaysLabel = window.i18n ? window.i18n.t('settings.wrapperConditionAlways') : 'Always';
item.innerHTML = `
<span class="wrapper-item-text">${escapeHtml(entry.arg)}</span>
<div class="wrapper-item-condition">
<select data-index="${index}">
<option value="server"${entry.condition === 'server' ? ' selected' : ''}>${serverLabel}</option>
<option value="always"${entry.condition === 'always' ? ' selected' : ''}>${alwaysLabel}</option>
</select>
</div>
<button class="wrapper-item-delete" data-index="${index}" title="Remove">
<i class="fas fa-trash-alt"></i>
</button>
`;
item.querySelector('select').addEventListener('change', (e) => updateArgCondition(index, e.target.value));
item.querySelector('.wrapper-item-delete').addEventListener('click', () => removeInjectArg(index));
container.appendChild(item);
});
}
async function addStripFlag() {
const input = document.getElementById('wrapperAddFlagInput');
if (!input || !_wrapperConfig) return;
const flag = input.value.trim();
if (!flag) return;
if (_wrapperConfig.stripFlags.includes(flag)) {
const msg = window.i18n ? window.i18n.t('notifications.wrapperFlagExists') : 'This flag is already in the list';
showNotification(msg, 'error');
return;
}
_wrapperConfig.stripFlags.push(flag);
input.value = '';
renderStripFlagsList();
await saveWrapperConfigToBackend();
await updateWrapperPreview();
}
async function removeStripFlag(index) {
if (!_wrapperConfig) return;
_wrapperConfig.stripFlags.splice(index, 1);
renderStripFlagsList();
await saveWrapperConfigToBackend();
await updateWrapperPreview();
}
async function addInjectArg() {
const input = document.getElementById('wrapperAddArgInput');
const condSelect = document.getElementById('wrapperAddArgCondition');
if (!input || !condSelect || !_wrapperConfig) return;
const arg = input.value.trim();
if (!arg) return;
const exists = _wrapperConfig.injectArgs.some(e => e.arg === arg);
if (exists) {
const msg = window.i18n ? window.i18n.t('notifications.wrapperArgExists') : 'This argument is already in the list';
showNotification(msg, 'error');
return;
}
_wrapperConfig.injectArgs.push({ arg, condition: condSelect.value });
input.value = '';
renderInjectArgsList();
await saveWrapperConfigToBackend();
await updateWrapperPreview();
}
async function removeInjectArg(index) {
if (!_wrapperConfig) return;
_wrapperConfig.injectArgs.splice(index, 1);
renderInjectArgsList();
await saveWrapperConfigToBackend();
await updateWrapperPreview();
}
async function updateArgCondition(index, condition) {
if (!_wrapperConfig || !_wrapperConfig.injectArgs[index]) return;
_wrapperConfig.injectArgs[index].condition = condition;
await saveWrapperConfigToBackend();
await updateWrapperPreview();
}
async function saveWrapperConfigToBackend() {
try {
const result = await window.electronAPI.saveWrapperConfig(_wrapperConfig);
if (!result || !result.success) {
throw new Error(result?.error || 'Save failed');
}
} catch (error) {
console.error('Error saving wrapper config:', error);
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigSaveFailed') : 'Failed to save wrapper configuration';
showNotification(msg, 'error');
}
}
function setupWrapperEventListeners() {
const addFlagBtn = document.getElementById('wrapperAddFlagBtn');
const addFlagInput = document.getElementById('wrapperAddFlagInput');
const addArgBtn = document.getElementById('wrapperAddArgBtn');
const addArgInput = document.getElementById('wrapperAddArgInput');
const restoreBtn = document.getElementById('wrapperRestoreDefaultsBtn');
const previewToggle = document.getElementById('wrapperPreviewToggle');
if (addFlagBtn) addFlagBtn.addEventListener('click', addStripFlag);
if (addFlagInput) addFlagInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addStripFlag(); });
if (addArgBtn) addArgBtn.addEventListener('click', addInjectArg);
if (addArgInput) addArgInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addInjectArg(); });
if (restoreBtn) {
restoreBtn.addEventListener('click', () => {
const message = window.i18n ? window.i18n.t('confirm.resetWrapperMessage') : 'Are you sure you want to restore defaults? Your custom changes will be lost.';
const title = window.i18n ? window.i18n.t('confirm.resetWrapperTitle') : 'Restore Defaults';
showCustomConfirm(message, title, async () => {
try {
const result = await window.electronAPI.resetWrapperConfig();
if (result && result.success) {
_wrapperConfig = result.config;
renderStripFlagsList();
renderInjectArgsList();
await updateWrapperPreview();
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigReset') : 'Wrapper configuration restored to defaults';
showNotification(msg, 'success');
} else {
throw new Error(result?.error || 'Reset failed');
}
} catch (error) {
console.error('Error resetting wrapper config:', error);
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigResetFailed') : 'Failed to restore wrapper configuration';
showNotification(msg, 'error');
}
});
});
}
if (previewToggle) {
previewToggle.addEventListener('click', toggleWrapperPreview);
}
}
async function toggleWrapperPreview() {
const container = document.getElementById('wrapperPreviewContainer');
const chevron = document.getElementById('wrapperPreviewChevron');
if (!container) return;
_wrapperPreviewOpen = !_wrapperPreviewOpen;
if (_wrapperPreviewOpen) {
container.style.display = 'block';
if (chevron) chevron.classList.add('expanded');
await updateWrapperPreview();
} else {
container.style.display = 'none';
if (chevron) chevron.classList.remove('expanded');
}
}
async function updateWrapperPreview() {
if (!_wrapperPreviewOpen || !_wrapperConfig) return;
const previewEl = document.getElementById('wrapperPreviewContent');
if (!previewEl) return;
try {
const platform = await window.electronAPI.getCurrentPlatform();
const script = await window.electronAPI.previewWrapperScript(_wrapperConfig, platform);
previewEl.textContent = script;
} catch (error) {
previewEl.textContent = 'Error generating preview: ' + error.message;
}
}

View File

@@ -1103,7 +1103,8 @@ function getRetryContextMessage() {
}
window.openDiscordExternal = function() {
window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
// window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
window.electronAPI?.openExternal('https://chat.sanhost.net/invite/Tfz4jCK4');
};
window.toggleMaximize = toggleMaximize;

View File

@@ -127,6 +127,12 @@
"logsCopy": "نسخ",
"logsRefresh": "تحديث",
"logsFolder": "فتح المجلد",
"logsSend": "إرسال السجلات",
"logsSending": "جارٍ الإرسال...",
"logsSent": "تم الإرسال!",
"logsSendFailed": "فشل",
"logsSubmissionId": "معرف الإرسال",
"logsShareId": "شارك هذا المعرف مع الدعم عند الإبلاغ عن المشاكل",
"logsLoading": "جاري تحميل السجلات...",
"closeLauncher": "سلوك المشغل",
"closeOnStart": "إغلاق المشغل عند بدء اللعبة",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Kopieren",
"logsRefresh": "Aktualisieren",
"logsFolder": "Ordner öffnen",
"logsSend": "Logs senden",
"logsSending": "Senden...",
"logsSent": "Gesendet!",
"logsSendFailed": "Fehlgeschlagen",
"logsSubmissionId": "Einreichungs-ID",
"logsShareId": "Teilen Sie diese ID dem Support mit, wenn Sie Probleme melden",
"logsLoading": "Protokolle werden geladen...",
"closeLauncher": "Launcher-Verhalten",
"closeOnStart": "Launcher beim Spielstart schließen",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Copy",
"logsRefresh": "Refresh",
"logsFolder": "Open Folder",
"logsSend": "Send Logs",
"logsSending": "Sending...",
"logsSent": "Sent!",
"logsSendFailed": "Failed",
"logsSubmissionId": "Submission ID",
"logsShareId": "Share this ID with support when reporting issues",
"logsLoading": "Loading logs...",
"closeLauncher": "Launcher Behavior",
"closeOnStart": "Close Launcher on game start",
@@ -141,7 +147,18 @@
"branchSwitching": "Switching to {branch}...",
"branchSwitched": "Switched to {branch} successfully!",
"installRequired": "Installation Required",
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?"
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?",
"wrapperConfig": "Java Wrapper Configuration",
"wrapperConfigHint": "Configure how the Java wrapper handles JVM flags and arguments at launch time.",
"wrapperStripFlags": "JVM Flags to Remove",
"wrapperInjectArgs": "Arguments to Inject",
"wrapperAddFlagPlaceholder": "e.g. -XX:+SomeFlag",
"wrapperAddArgPlaceholder": "e.g. --some-flag",
"wrapperAdd": "Add",
"wrapperConditionServer": "Server Only",
"wrapperConditionAlways": "Always",
"wrapperRestoreDefaults": "Restore Defaults",
"wrapperAdvancedPreview": "Advanced: Script Preview"
},
"uuid": {
"modalTitle": "UUID Management",
@@ -215,7 +232,13 @@
"noUsername": "No username configured. Please save your username first.",
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
"switchUsernameFailed": "Failed to switch username",
"playerNameTooLong": "Player name must be 16 characters or less"
"playerNameTooLong": "Player name must be 16 characters or less",
"wrapperConfigSaved": "Wrapper configuration saved",
"wrapperConfigSaveFailed": "Failed to save wrapper configuration",
"wrapperConfigReset": "Wrapper configuration restored to defaults",
"wrapperConfigResetFailed": "Failed to restore wrapper configuration",
"wrapperFlagExists": "This flag is already in the list",
"wrapperArgExists": "This argument is already in the list"
},
"confirm": {
"defaultTitle": "Confirm action",
@@ -233,7 +256,9 @@
"uninstallGameButton": "Uninstall",
"switchUsernameTitle": "Switch Identity",
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
"switchUsernameButton": "Switch"
"switchUsernameButton": "Switch",
"resetWrapperTitle": "Restore Defaults",
"resetWrapperMessage": "Are you sure you want to restore the default wrapper configuration? Your custom changes will be lost."
},
"progress": {
"initializing": "Initializing...",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Copiar",
"logsRefresh": "Actualizar",
"logsFolder": "Abrir Carpeta",
"logsSend": "Enviar logs",
"logsSending": "Enviando...",
"logsSent": "Enviado!",
"logsSendFailed": "Fallido",
"logsSubmissionId": "ID de envío",
"logsShareId": "Comparte este ID con soporte al reportar problemas",
"logsLoading": "Cargando registros...",
"closeLauncher": "Comportamiento del Launcher",
"closeOnStart": "Cerrar Launcher al iniciar el juego",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Copier",
"logsRefresh": "Actualiser",
"logsFolder": "Ouvrir le Dossier",
"logsSend": "Envoyer les logs",
"logsSending": "Envoi...",
"logsSent": "Envoyé !",
"logsSendFailed": "Échoué",
"logsSubmissionId": "ID de soumission",
"logsShareId": "Partagez cet ID avec le support pour signaler des problèmes",
"logsLoading": "Chargement des journaux...",
"closeLauncher": "Comportement du Launcher",
"closeOnStart": "Fermer le Launcher au démarrage du jeu",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Salin",
"logsRefresh": "Segarkan",
"logsFolder": "Buka Folder",
"logsSend": "Kirim Log",
"logsSending": "Mengirim...",
"logsSent": "Terkirim!",
"logsSendFailed": "Gagal",
"logsSubmissionId": "ID Pengiriman",
"logsShareId": "Bagikan ID ini ke dukungan saat melaporkan masalah",
"logsLoading": "Memuat log...",
"closeLauncher": "Perilaku Launcher",
"closeOnStart": "Tutup launcher saat game dimulai",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Kopiuj",
"logsRefresh": "Odśwież",
"logsFolder": "Otwórz Folder",
"logsSend": "Wyślij logi",
"logsSending": "Wysyłanie...",
"logsSent": "Wysłano!",
"logsSendFailed": "Błąd",
"logsSubmissionId": "ID zgłoszenia",
"logsShareId": "Udostępnij ten ID wsparciu technicznemu przy zgłaszaniu problemów",
"logsLoading": "Ładowanie logów...",
"closeLauncher": "Zachowanie Launchera",
"closeOnStart": "Zamknij Launcher przy starcie gry",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Copiar",
"logsRefresh": "Atualizar",
"logsFolder": "Abrir Pasta",
"logsSend": "Enviar logs",
"logsSending": "Enviando...",
"logsSent": "Enviado!",
"logsSendFailed": "Falhou",
"logsSubmissionId": "ID de envio",
"logsShareId": "Compartilhe este ID com o suporte ao relatar problemas",
"logsLoading": "Carregando registros...",
"closeLauncher": "Comportamento do Lançador",
"closeOnStart": "Fechar Lançador ao iniciar o jogo",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Копировать",
"logsRefresh": "Обновить",
"logsFolder": "Открыть папку",
"logsSend": "Отправить логи",
"logsSending": "Отправка...",
"logsSent": "Отправлено!",
"logsSendFailed": "Ошибка",
"logsSubmissionId": "ID отправки",
"logsShareId": "Поделитесь этим ID с поддержкой при обращении",
"logsLoading": "Загрузка логов...",
"closeLauncher": "Поведение лаунчера",
"closeOnStart": "Закрыть лаунчер при старте игры",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Kopiera",
"logsRefresh": "Uppdatera",
"logsFolder": "Öppna mapp",
"logsSend": "Skicka loggar",
"logsSending": "Skickar...",
"logsSent": "Skickat!",
"logsSendFailed": "Misslyckades",
"logsSubmissionId": "Inlämnings-ID",
"logsShareId": "Dela detta ID med support vid felanmälan",
"logsLoading": "Laddar loggar...",
"closeLauncher": "Launcher-beteende",
"closeOnStart": "Stäng launcher vid spelstart",

View File

@@ -127,6 +127,12 @@
"logsCopy": "Kopyala",
"logsRefresh": "Yenile",
"logsFolder": "Klasörü Aç",
"logsSend": "Logları Gönder",
"logsSending": "Gönderiliyor...",
"logsSent": "Gönderildi!",
"logsSendFailed": "Başarısız",
"logsSubmissionId": "Gönderim ID",
"logsShareId": "Sorun bildirirken bu ID'yi destekle paylaşın",
"logsLoading": "Loglar yükleniyor...",
"closeLauncher": "Başlatıcı Davranışı",
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",

View File

@@ -873,6 +873,22 @@ body {
border-color: rgba(255, 255, 255, 0.2);
}
.logs-action-btn.logs-send-btn {
background: rgba(0, 212, 255, 0.15);
border-color: rgba(0, 212, 255, 0.3);
color: #00d4ff;
}
.logs-action-btn.logs-send-btn:hover {
background: rgba(0, 212, 255, 0.25);
border-color: rgba(0, 212, 255, 0.5);
}
.logs-action-btn.logs-send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.logs-terminal {
flex: 1;
background: #0d1117;
@@ -4813,6 +4829,140 @@ select.settings-input option {
transform: translateY(0);
}
.wrapper-items-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.wrapper-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
transition: all 0.3s ease;
}
.wrapper-item:hover {
border-color: rgba(147, 51, 234, 0.3);
background: rgba(147, 51, 234, 0.05);
}
.wrapper-item-text {
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.82rem;
color: #e5e7eb;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wrapper-item-condition select {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #e5e7eb;
font-size: 0.75rem;
padding: 4px 8px;
cursor: pointer;
margin: 0 8px;
}
.wrapper-item-condition select:focus {
outline: none;
border-color: rgba(147, 51, 234, 0.5);
}
.wrapper-item-delete {
padding: 4px 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.8rem;
display: flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 28px;
}
.wrapper-item-delete:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.4);
color: #ef4444;
}
.wrapper-items-empty {
font-style: italic;
text-align: center;
color: rgba(255, 255, 255, 0.4);
padding: 12px;
font-size: 0.82rem;
}
.wrapper-condition-select {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #e5e7eb;
font-size: 0.82rem;
padding: 6px 10px;
cursor: pointer;
}
.wrapper-condition-select:focus {
outline: none;
border-color: rgba(147, 51, 234, 0.5);
}
.wrapper-preview-toggle {
background: none;
border: none;
color: rgba(255, 255, 255, 0.6);
cursor: pointer;
font-size: 0.82rem;
padding: 4px 0;
display: flex;
align-items: center;
gap: 6px;
transition: color 0.2s;
}
.wrapper-preview-toggle:hover {
color: rgba(255, 255, 255, 0.9);
}
.wrapper-preview-toggle i {
transition: transform 0.2s;
font-size: 0.7rem;
}
.wrapper-preview-toggle i.expanded {
transform: rotate(90deg);
}
.wrapper-preview-content {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: rgba(255, 255, 255, 0.7);
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.72rem;
line-height: 1.5;
padding: 12px;
max-height: 300px;
overflow: auto;
white-space: pre;
tab-size: 4;
}
.settings-hint {
margin-top: 0.5rem;
font-size: 0.8rem;
@@ -6460,3 +6610,85 @@ input[type="text"].uuid-input,
opacity: 0.5;
cursor: not-allowed;
}
/* Version Selection Styles */
.version-list-container::-webkit-scrollbar {
width: 6px;
}
.version-list-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.version-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
margin-bottom: 0.5rem;
transition: all 0.2s ease;
}
.version-item:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(4px);
}
.version-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.version-name {
font-weight: 600;
color: #fff;
font-size: 0.95rem;
}
.version-meta {
font-size: 0.8rem;
color: #a0a0a0;
display: flex;
gap: 1rem;
align-items: center;
}
.version-meta span {
display: flex;
align-items: center;
gap: 0.35rem;
}
.version-meta i {
font-size: 0.8em;
opacity: 0.7;
}
.version-actions .btn-install {
padding: 0.5rem 1.25rem;
background: linear-gradient(135deg, #3b82f6, #2563eb);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
}
.version-actions .btn-install:hover {
transform: translateY(-1px);
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.3);
background: linear-gradient(135deg, #4f93f6, #3b82f6);
}
.loading-versions {
padding: 3rem;
text-align: center;
}

View File

@@ -18,7 +18,8 @@
### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️
#### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/Fhbb9Yk5WW) and head to `#-⚠️-community-help`** 🛑
#### 🛑 **Found a problem? [Join the Community Chat](https://chat.sanhost.net/invite/Tfz4jCK4) | [TG Channel](https://t.me/hf2p_og) | [TG Group](https://t.me/sanhostnet)** 🛑
<!-- #### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/Fhbb9Yk5WW) and head to `#-⚠️-community-help`** 🛑 -->
<p>
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
@@ -455,7 +456,8 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
<div align="center">
**Questions? Ads? Collaboration? Endorsement? Other business-related?**
Message the founders at https://discord.gg/Fhbb9Yk5WW
Message the founders at https://chat.sanhost.net/invite/Tfz4jCK4 | [TG Channel](https://t.me/hf2p_og) | [TG Group](https://t.me/sanhostnet)
<!-- Message the founders at https://discord.gg/Fhbb9Yk5WW -->
</div>

View File

@@ -2,7 +2,8 @@
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/Fhbb9Yk5WW**
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://chat.sanhost.net/invite/Tfz4jCK4**
<!-- ### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/Fhbb9Yk5WW** -->
**Table of Contents**

View File

@@ -1,6 +1,7 @@
# Hytale F2P Launcher - Troubleshooting Guide
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/Fhbb9Yk5WW).
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Community Chat](https://chat.sanhost.net/invite/Tfz4jCK4) | [TG Channel](https://t.me/hf2p_og) | [TG Group](https://t.me/sanhostnet).
<!-- Discord: https://discord.gg/Fhbb9Yk5WW -->
---
@@ -437,7 +438,8 @@ Game sessions have a 10-hour TTL. This is by design for security.
If your issue isn't resolved by this guide:
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
2. **Join Discord:** [discord.gg/Fhbb9Yk5WW](https://discord.gg/Fhbb9Yk5WW)
2. **Join Community:** [Chat](https://chat.sanhost.net/invite/Tfz4jCK4) | [Telegram](https://t.me/hf2p_og)
<!-- Discord: https://discord.gg/Fhbb9Yk5WW -->
3. **Open a new issue** with:
- Your operating system and version
- Launcher version

View File

@@ -858,6 +858,212 @@ function checkLaunchReady() {
};
}
// =============================================================================
// JAVA WRAPPER CONFIGURATION (Structured)
// =============================================================================
const DEFAULT_WRAPPER_CONFIG = {
stripFlags: [],
injectArgs: [
{ arg: '--disable-sentry', condition: 'server' }
]
};
function getDefaultWrapperConfig() {
return JSON.parse(JSON.stringify(DEFAULT_WRAPPER_CONFIG));
}
function loadWrapperConfig() {
const config = loadConfig();
if (config.javaWrapperConfig && typeof config.javaWrapperConfig === 'object') {
const wc = config.javaWrapperConfig;
if (Array.isArray(wc.stripFlags) && Array.isArray(wc.injectArgs)) {
const loaded = JSON.parse(JSON.stringify(wc));
// Normalize entries: ensure every injectArg has a valid condition
for (const entry of loaded.injectArgs) {
if (!['server', 'always'].includes(entry.condition)) {
entry.condition = 'always';
}
}
return loaded;
}
}
return getDefaultWrapperConfig();
}
function saveWrapperConfig(wrapperConfig) {
if (!wrapperConfig || typeof wrapperConfig !== 'object') {
throw new Error('Invalid wrapper config');
}
if (!Array.isArray(wrapperConfig.stripFlags) || !Array.isArray(wrapperConfig.injectArgs)) {
throw new Error('Invalid wrapper config structure');
}
// Validate injectArgs entries
for (const entry of wrapperConfig.injectArgs) {
if (!entry.arg || typeof entry.arg !== 'string') {
throw new Error('Each inject arg must have a string "arg" property');
}
if (!['server', 'always'].includes(entry.condition)) {
throw new Error('Inject arg condition must be "server" or "always"');
}
}
saveConfig({ javaWrapperConfig: wrapperConfig });
console.log('[Config] Wrapper config saved');
}
function resetWrapperConfig() {
const config = loadConfig();
delete config.javaWrapperConfig;
delete config.javaWrapperScripts; // Clean up legacy key if present
// Write the cleaned config using the same atomic pattern as saveConfig.
// We cannot use saveConfig() here because it merges (spread) which cannot remove keys.
const data = JSON.stringify(config, null, 2);
fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
if (fs.existsSync(CONFIG_FILE)) {
fs.copyFileSync(CONFIG_FILE, CONFIG_BACKUP);
}
fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
console.log('[Config] Wrapper config reset to default');
return getDefaultWrapperConfig();
}
/**
* Generate a platform-specific wrapper script from structured config
* @param {Object} config - { stripFlags: string[], injectArgs: {arg, condition}[] }
* @param {string} platform - 'darwin', 'win32', or 'linux'
* @param {string|null} javaBin - Path to real java binary (required for darwin/linux)
* @returns {string} Generated script content
*/
function generateWrapperScript(config, platform, javaBin) {
const { stripFlags, injectArgs } = config;
const alwaysArgs = injectArgs.filter(a => a.condition === 'always');
const serverArgs = injectArgs.filter(a => a.condition === 'server');
if (platform === 'win32') {
return _generateWindowsWrapper(stripFlags, alwaysArgs, serverArgs);
} else {
return _generateUnixWrapper(stripFlags, alwaysArgs, serverArgs, javaBin);
}
}
function _generateUnixWrapper(stripFlags, alwaysArgs, serverArgs, javaBin) {
const lines = [
'#!/bin/bash',
'# Java wrapper - generated by HytaleF2P launcher',
`REAL_JAVA="${javaBin || '${JAVA_BIN}'}"`,
'ARGS=("$@")',
''
];
// Strip flags
if (stripFlags.length > 0) {
lines.push('# Strip JVM flags');
lines.push('FILTERED_ARGS=()');
lines.push('for arg in "${ARGS[@]}"; do');
lines.push(' case "$arg" in');
for (const flag of stripFlags) {
lines.push(` "${flag}") echo "[Wrapper] Stripped: $arg" ;;`);
}
lines.push(' *) FILTERED_ARGS+=("$arg") ;;');
lines.push(' esac');
lines.push('done');
} else {
lines.push('FILTERED_ARGS=("${ARGS[@]}")');
}
lines.push('');
// Always-inject args
if (alwaysArgs.length > 0) {
lines.push('# Inject args (always)');
for (const a of alwaysArgs) {
lines.push(`FILTERED_ARGS+=("${a.arg}")`);
lines.push(`echo "[Wrapper] Injected ${a.arg}"`);
}
lines.push('');
}
// Server-conditional args (appended after HytaleServer.jar if present)
if (serverArgs.length > 0) {
lines.push('# Inject args (server only)');
lines.push('IS_SERVER=false');
lines.push('for arg in "${FILTERED_ARGS[@]}"; do');
lines.push(' if [[ "$arg" == *"HytaleServer.jar"* ]]; then');
lines.push(' IS_SERVER=true');
lines.push(' break');
lines.push(' fi');
lines.push('done');
lines.push('if [ "$IS_SERVER" = true ]; then');
for (const a of serverArgs) {
lines.push(` FILTERED_ARGS+=("${a.arg}")`);
lines.push(` echo "[Wrapper] Injected ${a.arg}"`);
}
lines.push('fi');
lines.push('');
}
lines.push('echo "[Wrapper] Executing: $REAL_JAVA ${FILTERED_ARGS[*]}"');
lines.push('exec "$REAL_JAVA" "${FILTERED_ARGS[@]}"');
lines.push('');
return lines.join('\n');
}
function _generateWindowsWrapper(stripFlags, alwaysArgs, serverArgs) {
const lines = [
'@echo off',
'setlocal EnableDelayedExpansion',
'',
'REM Java wrapper - generated by HytaleF2P launcher',
'set "REAL_JAVA=%~dp0java-original.exe"',
'set "ARGS=%*"',
''
];
// Strip flags using string replacement
if (stripFlags.length > 0) {
lines.push('REM Strip JVM flags');
for (const flag of stripFlags) {
lines.push(`set "ARGS=!ARGS:${flag}=!"`);
}
lines.push('');
}
// Always-inject args
const alwaysExtra = alwaysArgs.map(a => a.arg).join(' ');
// Server-conditional args
if (serverArgs.length > 0) {
const serverExtra = serverArgs.map(a => a.arg).join(' ');
lines.push('REM Check if running HytaleServer.jar and inject server args');
lines.push('echo !ARGS! | findstr /i "HytaleServer.jar" >nul 2>&1');
lines.push('if "!ERRORLEVEL!"=="0" (');
if (alwaysExtra) {
lines.push(` echo [Wrapper] Injected ${alwaysExtra} ${serverExtra}`);
lines.push(` "%REAL_JAVA%" !ARGS! ${alwaysExtra} ${serverExtra}`);
} else {
lines.push(` echo [Wrapper] Injected ${serverExtra}`);
lines.push(` "%REAL_JAVA%" !ARGS! ${serverExtra}`);
}
lines.push(') else (');
if (alwaysExtra) {
lines.push(` "%REAL_JAVA%" !ARGS! ${alwaysExtra}`);
} else {
lines.push(' "%REAL_JAVA%" !ARGS!');
}
lines.push(')');
} else if (alwaysExtra) {
lines.push(`"%REAL_JAVA%" !ARGS! ${alwaysExtra}`);
} else {
lines.push('"%REAL_JAVA%" !ARGS!');
}
lines.push('exit /b !ERRORLEVEL!');
lines.push('');
return lines.join('\r\n');
}
// =============================================================================
// EXPORTS
// =============================================================================
@@ -924,6 +1130,13 @@ module.exports = {
saveVersionBranch,
loadVersionBranch,
// Java Wrapper Config
getDefaultWrapperConfig,
loadWrapperConfig,
saveWrapperConfig,
resetWrapperConfig,
generateWrapperScript,
// Constants
CONFIG_FILE,
UUID_STORE_FILE

View File

@@ -45,7 +45,13 @@ const {
saveVersionClient,
loadVersionClient,
saveVersionBranch,
loadVersionBranch
loadVersionBranch,
// Java Wrapper Config
getDefaultWrapperConfig,
loadWrapperConfig,
saveWrapperConfig,
resetWrapperConfig,
generateWrapperScript
} = require('./core/config');
const { getResolvedAppDir, getModsPath } = require('./core/paths');
@@ -78,7 +84,8 @@ const {
loadInstalledMods,
downloadMod,
uninstallMod,
toggleMod
toggleMod,
getModFiles
} = require('./managers/modManager');
// Services
@@ -181,6 +188,7 @@ module.exports = {
downloadMod,
uninstallMod,
toggleMod,
getModFiles,
saveModsToConfig,
loadModsFromConfig,
@@ -197,6 +205,13 @@ module.exports = {
proposeGameUpdate,
handleFirstLaunchCheck,
// Java Wrapper Config functions
getDefaultWrapperConfig,
loadWrapperConfig,
saveWrapperConfig,
resetWrapperConfig,
generateWrapperScript,
// Path functions
getResolvedAppDir
};

View File

@@ -18,7 +18,9 @@ const {
saveVersionClient,
loadUsername,
hasUsername,
checkLaunchReady
checkLaunchReady,
loadWrapperConfig,
generateWrapperScript
} = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getLatestClientVersion } = require('../services/versionManager');
@@ -27,6 +29,7 @@ const { ensureGameInstalled } = require('./differentialUpdateManager');
const { syncModsForCurrentProfile } = require('./modManager');
const { getUserDataPath } = require('../utils/userDataMigration');
const { syncServerList } = require('../utils/serverListSync');
const { killGameProcesses } = require('./gameManager');
// Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null;
@@ -327,23 +330,13 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
console.log('Signed server binaries (after patching)');
}
// Create java wrapper (must be signed on macOS)
if (javaBin && fs.existsSync(javaBin)) {
const javaWrapperPath = path.join(path.dirname(javaBin), 'java-wrapper');
const wrapperScript = `#!/bin/bash
# Java wrapper for macOS - adds --disable-sentry to fix Sentry hang issue
REAL_JAVA="${javaBin}"
ARGS=("$@")
for i in "\${!ARGS[@]}"; do
if [[ "\${ARGS[$i]}" == *"HytaleServer.jar"* ]]; then
ARGS=("\${ARGS[@]:0:$((i+1))}" "--disable-sentry" "\${ARGS[@]:$((i+1))}")
break
fi
done
exec "$REAL_JAVA" "\${ARGS[@]}"
`;
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'darwin', javaBin);
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
await signPath(javaWrapperPath, false);
console.log('Created java wrapper with --disable-sentry fix');
console.log('Created java wrapper from config template');
javaBin = javaWrapperPath;
}
} catch (signError) {
@@ -352,6 +345,40 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}
}
// Windows: Create java wrapper to strip/inject JVM flags per wrapper config
if (process.platform === 'win32' && javaBin && fs.existsSync(javaBin)) {
try {
const javaDir = path.dirname(javaBin);
const javaOriginal = path.join(javaDir, 'java-original.exe');
const javaWrapperPath = path.join(javaDir, 'java-wrapper.bat');
if (!fs.existsSync(javaOriginal)) {
fs.copyFileSync(javaBin, javaOriginal);
console.log('Backed up java.exe as java-original.exe');
}
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'win32', null);
fs.writeFileSync(javaWrapperPath, wrapperScript);
console.log('Created Windows java wrapper from config template');
javaBin = javaWrapperPath;
} catch (wrapperError) {
console.log('Notice: Windows java wrapper creation failed:', wrapperError.message);
}
}
// Linux: Create java wrapper to strip/inject JVM flags per wrapper config
if (process.platform === 'linux' && javaBin && fs.existsSync(javaBin)) {
try {
const javaWrapperPath = path.join(path.dirname(javaBin), 'java-wrapper');
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'linux', javaBin);
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
console.log('Created Linux java wrapper from config template');
javaBin = javaWrapperPath;
} catch (wrapperError) {
console.log('Notice: Linux java wrapper creation failed:', wrapperError.message);
}
}
const args = [
'--app-dir', gameLatest,
'--java-exec', javaBin,
@@ -436,6 +463,22 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}
}
// Kill any stalled game processes from a previous launch to prevent file locks
// and "game already running" issues
await killGameProcesses();
// Remove AOT cache: generated by official Hytale JRE, incompatible with F2P JRE.
// Client adds -XX:AOTCache when this file exists, causing classloading failures.
const aotCache = path.join(gameLatest, 'Server', 'HytaleServer.aot');
if (fs.existsSync(aotCache)) {
try {
fs.unlinkSync(aotCache);
console.log('Removed incompatible AOT cache (HytaleServer.aot)');
} catch (aotErr) {
console.warn('Could not remove AOT cache:', aotErr.message);
}
}
// DualAuth Agent: Set JAVA_TOOL_OPTIONS so java picks up -javaagent: flag
// This enables runtime auth patching without modifying the server JAR
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');

View File

@@ -39,6 +39,41 @@ async function isGameRunning() {
}
}
// Force-kill stalled game processes to release file locks before repair/reinstall.
// Cross-platform: Windows (taskkill/PowerShell), macOS (pkill), Linux (pkill).
async function killGameProcesses() {
const killed = [];
async function tryKill(command, label) {
try {
await execAsync(command);
killed.push(label);
} catch (_) { /* process not found is expected */ }
}
if (process.platform === 'win32') {
// Kill client
await tryKill('taskkill /F /IM "HytaleClient.exe" /T', 'HytaleClient.exe');
// Kill java.exe instances running HytaleServer.jar via PowerShell
// (Get-CimInstance replaces deprecated wmic, works on Windows 10+)
await tryKill(
'powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name=\'java.exe\'\\" | Where-Object { $_.CommandLine -like \'*HytaleServer*\' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"',
'java.exe(HytaleServer)'
);
} else {
// macOS and Linux
await tryKill('pkill -9 -f HytaleClient', 'HytaleClient');
await tryKill('pkill -9 -f HytaleServer', 'HytaleServer');
}
if (killed.length > 0) {
console.log(`[GameManager] Force-killed stalled processes: ${killed.join(', ')}`);
// Wait for OS to release file handles
await new Promise(resolve => setTimeout(resolve, 2000));
}
return killed;
}
// Helper function to safely remove directory with retry logic
async function safeRemoveDirectory(dirPath, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -50,8 +85,13 @@ async function safeRemoveDirectory(dirPath, maxRetries = 3) {
return; // Success, exit the loop
} catch (error) {
console.warn(`Attempt ${attempt}/${maxRetries} failed to remove ${dirPath}: ${error.message}`);
if (attempt < maxRetries) {
// On EPERM/EBUSY, try killing stalled game processes that hold file locks
if (attempt === 1 && (error.code === 'EPERM' || error.code === 'EBUSY')) {
console.log('Permission error detected, killing stalled game processes...');
await killGameProcesses();
}
// Wait before retrying (exponential backoff)
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
console.log(`Waiting ${delay}ms before retry...`);
@@ -833,11 +873,14 @@ async function repairGame(progressCallback, branchOverride = null) {
progressCallback('Removing old game files...', 30, null, null, null);
}
// Check if game is running before attempting to delete files
// Kill stalled game processes before attempting to delete files
const gameRunning = await isGameRunning();
if (gameRunning) {
console.warn('[RepairGame] Game appears to be running. This may cause permission errors during repair.');
console.log('[RepairGame] Please close the game before repairing, or wait for the repair to complete.');
console.warn('[RepairGame] Game processes detected. Force-killing to release file locks...');
if (progressCallback) {
progressCallback('Stopping stalled game processes...', 20, null, null, null);
}
await killGameProcesses();
}
// Delete Game and Cache Directory with retry logic
@@ -964,5 +1007,6 @@ module.exports = {
installGame,
uninstallGame,
checkExistingGameInstallation,
repairGame
repairGame,
killGameProcesses
};

View File

@@ -285,6 +285,27 @@ async function toggleMod(modId, modsPath) {
}
}
async function getModFiles(modId) {
try {
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modId}/files`, {
headers: {
'x-api-key': API_KEY,
'Accept': 'application/json'
},
params: {
pageSize: 20,
sortOrder: 'desc'
}
});
return response.data.data;
} catch (error) {
console.error('Error fetching mod files:', error);
return [];
}
}
async function syncModsForCurrentProfile() {
try {
const activeProfile = profileManager.getActiveProfile();
@@ -455,5 +476,6 @@ module.exports = {
syncModsForCurrentProfile,
generateModId,
extractModName,
extractVersion
};
extractVersion,
getModFiles
};

View File

@@ -0,0 +1,238 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const zlib = require('zlib');
const logger = require('../logger');
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB per file
/**
* Get the HytaleSaves directory (game client logs)
*/
function getHytaleSavesDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleSaves');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleSaves');
} else {
return path.join(home, '.hytalesaves');
}
}
/**
* Read a log file, capping at MAX_FILE_SIZE (keeps tail/most recent lines)
*/
function readLogFile(filePath) {
try {
const stats = fs.statSync(filePath);
if (stats.size <= MAX_FILE_SIZE) {
return fs.readFileSync(filePath, 'utf8');
}
// Read only the last MAX_FILE_SIZE bytes
const fd = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(MAX_FILE_SIZE);
fs.readSync(fd, buffer, 0, MAX_FILE_SIZE, stats.size - MAX_FILE_SIZE);
fs.closeSync(fd);
const content = buffer.toString('utf8');
// Skip first partial line
const firstNewline = content.indexOf('\n');
const trimmed = firstNewline >= 0 ? content.substring(firstNewline + 1) : content;
return `[... truncated ${stats.size - MAX_FILE_SIZE} bytes ...]\n` + trimmed;
} catch (err) {
return `[Error reading file: ${err.message}]`;
}
}
/**
* Get files matching a date pattern from a directory
*/
function getFilesForDate(dir, dateStr, pattern) {
if (!fs.existsSync(dir)) return [];
try {
return fs.readdirSync(dir)
.filter(f => f.includes(dateStr) && (pattern ? pattern.test(f) : true))
.map(f => ({
name: f,
path: path.join(dir, f),
mtime: fs.statSync(path.join(dir, f)).mtime
}))
.sort((a, b) => b.mtime - a.mtime);
} catch (err) {
return [];
}
}
// ============================================================================
// ZIP BUILDER (pure Node.js, no dependencies)
// ============================================================================
/**
* CRC32 lookup table
*/
const crc32Table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
}
crc32Table[i] = c;
}
function crc32(buf) {
let crc = 0xFFFFFFFF;
for (let i = 0; i < buf.length; i++) {
crc = crc32Table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
}
return (crc ^ 0xFFFFFFFF) >>> 0;
}
/**
* Create a ZIP file from an array of {name, content} entries
* Uses DEFLATE compression via built-in zlib
*/
function createZipBuffer(files) {
const localHeaders = [];
const centralEntries = [];
let offset = 0;
for (const file of files) {
const nameBytes = Buffer.from(file.name, 'utf8');
const contentBytes = Buffer.from(file.content, 'utf8');
const compressed = zlib.deflateRawSync(contentBytes);
const crcVal = crc32(contentBytes);
// Local file header (30 bytes + filename)
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0); // signature
local.writeUInt16LE(20, 4); // version needed
local.writeUInt16LE(0, 6); // flags
local.writeUInt16LE(8, 8); // compression: DEFLATE
local.writeUInt16LE(0, 10); // mod time
local.writeUInt16LE(0, 12); // mod date
local.writeUInt32LE(crcVal, 14); // crc32
local.writeUInt32LE(compressed.length, 18); // compressed size
local.writeUInt32LE(contentBytes.length, 22); // uncompressed size
local.writeUInt16LE(nameBytes.length, 26); // filename length
local.writeUInt16LE(0, 28); // extra field length
localHeaders.push(local, nameBytes, compressed);
// Central directory entry (46 bytes + filename)
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0); // signature
central.writeUInt16LE(20, 4); // version made by
central.writeUInt16LE(20, 6); // version needed
central.writeUInt16LE(0, 8); // flags
central.writeUInt16LE(8, 10); // compression
central.writeUInt16LE(0, 12); // mod time
central.writeUInt16LE(0, 14); // mod date
central.writeUInt32LE(crcVal, 16); // crc32
central.writeUInt32LE(compressed.length, 20); // compressed size
central.writeUInt32LE(contentBytes.length, 24); // uncompressed size
central.writeUInt16LE(nameBytes.length, 28); // filename length
central.writeUInt16LE(0, 30); // extra field length
central.writeUInt16LE(0, 32); // comment length
central.writeUInt16LE(0, 34); // disk number
central.writeUInt16LE(0, 36); // internal attrs
central.writeUInt32LE(0, 38); // external attrs
central.writeUInt32LE(offset, 42); // local header offset
centralEntries.push(central, nameBytes);
offset += 30 + nameBytes.length + compressed.length;
}
const centralDirBuf = Buffer.concat(centralEntries);
// End of central directory (22 bytes)
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(0x06054b50, 0); // signature
eocd.writeUInt16LE(0, 4); // disk number
eocd.writeUInt16LE(0, 6); // cd disk number
eocd.writeUInt16LE(files.length, 8); // entries on disk
eocd.writeUInt16LE(files.length, 10); // total entries
eocd.writeUInt32LE(centralDirBuf.length, 12); // cd size
eocd.writeUInt32LE(offset, 16); // cd offset
eocd.writeUInt16LE(0, 20); // comment length
return Buffer.concat([...localHeaders, centralDirBuf, eocd]);
}
// ============================================================================
/**
* Collect all relevant logs for submission
* Returns { files: [{name, content}], meta: {username, platform, version} }
*/
function collectLogs() {
const files = [];
const today = new Date();
const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
// 1. Launcher logs
const launcherLogDir = logger.getLogDirectory();
if (launcherLogDir && fs.existsSync(launcherLogDir)) {
// Today's launcher logs
const todayLogs = getFilesForDate(launcherLogDir, todayStr, /^launcher-.*\.log$/);
for (const f of todayLogs) {
files.push({ name: `launcher/${f.name}`, content: readLogFile(f.path) });
}
// Most recent from yesterday (just one)
const yesterdayLogs = getFilesForDate(launcherLogDir, yesterdayStr, /^launcher-.*\.log$/);
if (yesterdayLogs.length > 0) {
files.push({ name: `launcher/${yesterdayLogs[0].name}`, content: readLogFile(yesterdayLogs[0].path) });
}
}
// 2. Game client logs
const savesDir = getHytaleSavesDir();
const clientLogDir = path.join(savesDir, 'Logs');
if (fs.existsSync(clientLogDir)) {
const clientLogs = getFilesForDate(clientLogDir, todayStr, /client\.log$/);
for (const f of clientLogs) {
files.push({ name: `client/${f.name}`, content: readLogFile(f.path) });
}
}
// 3. Config snapshot
const appDir = logger.getAppDir ? logger.getAppDir() : logger.getInstallPath();
const configPath = path.join(appDir, 'config.json');
if (fs.existsSync(configPath)) {
try {
const configContent = fs.readFileSync(configPath, 'utf8');
files.push({ name: 'config.json', content: configContent });
} catch (err) {
files.push({ name: 'config.json', content: `[Error reading config: ${err.message}]` });
}
}
// Build metadata
const { app } = require('electron');
let username = 'unknown';
try {
const configFile = path.join(appDir, 'config.json');
if (fs.existsSync(configFile)) {
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
username = cfg.username || cfg.playerName || 'unknown';
}
} catch (err) {}
return {
files,
meta: {
username,
platform: `${process.platform}-${process.arch}`,
version: app.getVersion()
}
};
}
module.exports = { collectLogs, createZipBuffer };

View File

@@ -164,8 +164,14 @@ function detectGpuLinux() {
const isAmd = lowerLine.includes('amd') || lowerLine.includes('radeon') || vendorId === '1002';
const isIntel = lowerLine.includes('intel') || vendorId === '8086';
// Intel Arc detection
const isIntelArc = isIntel && (lowerName.includes('arc') || lowerName.includes('a770') || lowerName.includes('a750') || lowerName.includes('a380'));
// Intel Arc discrete GPU detection
// Discrete Arc cards have specific model numbers: A310, A380, A580, A750, A770, B570, B580
// Integrated "Intel Arc Graphics" (Meteor Lake, Lunar Lake, Arrow Lake) have NO model suffix
// and sit on bus 00 (slot 00:02.0) — these are iGPUs, not dGPUs
const pciSlot = line.split(' ')[0] || '';
const pciBus = parseInt(pciSlot.split(':')[0], 16) || 0;
const hasArcModelNumber = isIntel && /\b[ab]\d{3}\b/i.test(lowerName);
const isIntelArc = isIntel && (hasArcModelNumber || (lowerName.includes('arc') && pciBus > 0));
let vendor = 'unknown';
if (isNvidia) vendor = 'nvidia';

135
main.js
View File

@@ -88,8 +88,9 @@ function setDiscordActivity() {
url: 'https://git.sanhost.net/sanasol/hytale-f2p/releases'
},
{
label: 'Discord',
url: 'https://discord.gg/Fhbb9Yk5WW'
label: 'Community',
// url: 'https://discord.gg/Fhbb9Yk5WW'
url: 'https://chat.sanhost.net/invite/Tfz4jCK4'
}
]
});
@@ -1068,7 +1069,7 @@ ipcMain.handle('load-settings', async () => {
}
});
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getModFiles, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
const os = require('os');
ipcMain.handle('get-local-app-data', async () => {
@@ -1118,6 +1119,15 @@ ipcMain.handle('download-mod', async (event, modInfo) => {
}
});
ipcMain.handle('get-mod-files', async (event, modId) => {
try {
return await getModFiles(modId);
} catch (error) {
console.error('Error getting mod files:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('uninstall-mod', async (event, modId, modsPath) => {
try {
return await uninstallMod(modId, modsPath);
@@ -1407,6 +1417,83 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
ipcMain.handle('send-logs', async () => {
try {
const https = require('https');
const http = require('http');
const { collectLogs, createZipBuffer } = require('./backend/utils/logCollector');
const { files, meta } = collectLogs();
if (files.length === 0) {
return { success: false, error: 'No log files found' };
}
// Create ZIP with individual log files
const zipBuffer = createZipBuffer(files);
// Get auth server URL from core config
const { getAuthServerUrl } = require('./backend/core/config');
const authUrl = getAuthServerUrl();
// Build file names list
const fileNames = files.map(f => f.name).join(',');
return await new Promise((resolve) => {
const url = new URL(authUrl + '/logs/submit');
const transport = url.protocol === 'https:' ? https : http;
const options = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/zip',
'Content-Length': zipBuffer.length,
'X-Log-Username': meta.username || 'unknown',
'X-Log-Platform': meta.platform || 'unknown',
'X-Log-Version': meta.version || 'unknown',
'X-Log-File-Count': String(files.length),
'X-Log-Files': fileNames
},
timeout: 30000
};
const req = transport.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try {
const data = JSON.parse(body);
if (res.statusCode === 200) {
resolve({ success: true, id: data.id, message: data.message });
} else {
resolve({ success: false, error: data.error || `Server error ${res.statusCode}` });
}
} catch (e) {
resolve({ success: false, error: `Invalid response: ${res.statusCode}` });
}
});
});
req.on('error', (err) => {
resolve({ success: false, error: err.message });
});
req.on('timeout', () => {
req.destroy();
resolve({ success: false, error: 'Request timed out' });
});
req.write(zipBuffer);
req.end();
});
} catch (error) {
console.error('Error sending logs:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('open-logs-folder', async () => {
try {
const logDir = logger.getLogDirectory();
@@ -1462,3 +1549,45 @@ ipcMain.handle('profile-update', async (event, id, updates) => {
}
});
// Java Wrapper Config IPC
ipcMain.handle('load-wrapper-config', () => {
const { loadWrapperConfig } = require('./backend/launcher');
return loadWrapperConfig();
});
ipcMain.handle('save-wrapper-config', (event, config) => {
try {
const { saveWrapperConfig } = require('./backend/launcher');
saveWrapperConfig(config);
return { success: true };
} catch (error) {
console.error('Error saving wrapper config:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('reset-wrapper-config', () => {
try {
const { resetWrapperConfig } = require('./backend/launcher');
const config = resetWrapperConfig();
return { success: true, config };
} catch (error) {
console.error('Error resetting wrapper config:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-default-wrapper-config', () => {
const { getDefaultWrapperConfig } = require('./backend/launcher');
return getDefaultWrapperConfig();
});
ipcMain.handle('preview-wrapper-script', (event, config, platform) => {
const { generateWrapperScript } = require('./backend/launcher');
return generateWrapperScript(config || require('./backend/launcher').loadWrapperConfig(), platform || process.platform, '/path/to/java');
});
ipcMain.handle('get-current-platform', () => {
return process.platform;
});

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "hytale-f2p-launcher",
"version": "2.2.0",
"version": "2.4.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hytale-f2p-launcher",
"version": "2.2.0",
"version": "2.4.2",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.10",

View File

@@ -1,6 +1,6 @@
{
"name": "hytale-f2p-launcher",
"version": "2.3.8",
"version": "2.4.3",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://git.sanhost.net/sanasol/hytale-f2p",
"main": "main.js",
@@ -26,7 +26,6 @@
"electron",
"auto-update",
"mod-manager"
],
"maintainers": [
{

View File

@@ -45,6 +45,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath),
downloadMod: (modInfo) => ipcRenderer.invoke('download-mod', modInfo),
uninstallMod: (modId, modsPath) => ipcRenderer.invoke('uninstall-mod', modId, modsPath),
getModFiles: (modId) => ipcRenderer.invoke('get-mod-files', modId),
toggleMod: (modId, modsPath) => ipcRenderer.invoke('toggle-mod', modId, modsPath),
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
@@ -94,6 +95,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
sendLogs: () => ipcRenderer.invoke('send-logs'),
// UUID Management methods
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
@@ -103,6 +105,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username),
resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'),
// Java Wrapper Config API
loadWrapperConfig: () => ipcRenderer.invoke('load-wrapper-config'),
saveWrapperConfig: (config) => ipcRenderer.invoke('save-wrapper-config', config),
resetWrapperConfig: () => ipcRenderer.invoke('reset-wrapper-config'),
getDefaultWrapperConfig: () => ipcRenderer.invoke('get-default-wrapper-config'),
previewWrapperScript: (config, platform) => ipcRenderer.invoke('preview-wrapper-script', config, platform),
getCurrentPlatform: () => ipcRenderer.invoke('get-current-platform'),
// Profile API
profile: {
create: (name) => ipcRenderer.invoke('profile-create', name),

View File

@@ -15,17 +15,32 @@ Host your own Hytale server. The scripts handle everything automatically.
## Quick Start
### Windows
### Free Hosted Server (no PC required)
Use [play.hosting](https://play.hosting) to get a free Hytale server with F2P support:
1. Register at [play.hosting](https://play.hosting)
2. Create a **Hytale** server
3. Start the server once and wait for it to fully load
4. Go to **Files** → open the `mods` folder
5. Click **New****File via URL**
6. Paste: `https://github.com/sanasol/hytale-auth-server/releases/latest/download/dualauth-agent.jar`
7. Click **Query** and download the file
8. Go to **Console** and **Restart** the server
Done — your F2P server is ready to join.
### Windows (self-hosted)
1. Download `start.bat` to an empty folder
2. Double-click `start.bat`
3. Done — server starts on port **5520**
### Linux / macOS
### Linux / macOS (self-hosted)
```bash
mkdir hytale-server && cd hytale-server
curl -O https://raw.githubusercontent.com/amiayweb/Hytale-F2P/develop/server/start.sh
curl -O https://git.sanhost.net/sanasol/hytale-f2p/raw/branch/develop/server/start.sh
chmod +x start.sh
./start.sh
```
@@ -113,6 +128,7 @@ your-folder/
| Out of memory | Set more RAM: `JVM_XMX=4G ./start.sh` |
| Friends can't connect | Forward port 5520 (TCP+UDP) on your router, or use [playit.gg](https://playit.gg) if you can't port forward |
## Discord
## Community
Need help? Join the community: https://discord.gg/Fhbb9Yk5WW
Need help? Join the community: https://chat.sanhost.net/invite/Tfz4jCK4 | TG Channel: https://t.me/hf2p_og | TG Group: https://t.me/sanhostnet
<!-- Discord: https://discord.gg/Fhbb9Yk5WW -->