Compare commits

..

14 Commits

Author SHA1 Message Date
AMIAY
f2a05d2079 Update release.yml 2026-01-20 09:49:11 +01:00
AMIAY
cece338609 Update release.yml 2026-01-20 09:48:15 +01:00
AMIAY
261582a882 Update release.yml 2026-01-20 09:47:34 +01:00
AMIAY
0f0f0fa308 Update release.yml 2026-01-20 09:46:33 +01:00
AMIAY
5e6a07f0a6 Allow manual trigger for release workflow 2026-01-20 09:42:16 +01:00
AMIAY
713377fdc6 Merge pull request #71 from Rahul-Sahani04/feature/profile-system
Profile System & Mod Loading Fixes
2026-01-20 09:33:20 +01:00
Rahul Sahani
e962a8880d Delete PR_DESCRIPTION.md 2026-01-20 11:57:23 +05:30
Rahul Sahani
c2d5536dd0 Improve comments in profileManager.js
Refactor comments for clarity and conciseness in profileManager.js.
2026-01-20 11:57:01 +05:30
Rahul Sahani
727be2ca5c Improve comments in saveModsToConfig function
Refactor comments in saveModsToConfig function for clarity.
2026-01-20 11:55:18 +05:30
Rahul-Sahani04
64892c81e9 Profile System & Mod Loading Fixes
Added a full profile system and fixed a few critical mod loading issues.

What changed

Profiles — Implemented proper profile management (create, switch, delete). Each profile now has its own isolated mod list.

Mod Isolation — Fixed ModManager so mods are strictly scoped to the active profile. Browsing and installing only affects the selected profile.

Critical Fix — Fixed a path bug where mods were being saved to ~/AppData/Local on macOS (Windows path) instead of ~/Library/Application Support. Mods now save to the correct location and load correctly in-game.

Stability — Added an auto-sync step before every launch to make sure the physical mods folder always matches the active profile.

UI — Added a profile selector dropdown and a profile management modal.
2026-01-20 11:52:36 +05:30
AMIAY
fffc730788 Merge pull request #68 from Terromur/main
Update PKGBUILD
2026-01-19 23:36:24 +01:00
Terromur
c475ec7879 Update Hytale-F2P.desktop 2026-01-20 03:34:51 +05:00
Terromur
7efa0d07b0 Update PKGBUILD 2026-01-20 03:34:38 +05:00
AMIAY
21f8527ed4 update 2.0.2 2026-01-19 23:15:29 +01:00
32 changed files with 4098 additions and 855 deletions

View File

@@ -17,7 +17,6 @@ jobs:
with: with:
node-version: '22' node-version: '22'
cache: 'npm' cache: 'npm'
- run: npm install
- run: npm ci - run: npm ci
- run: npx electron-builder --linux --publish never - run: npx electron-builder --linux --publish never
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@@ -35,7 +34,6 @@ jobs:
with: with:
node-version: '22' node-version: '22'
cache: 'npm' cache: 'npm'
- run: npm install
- run: npm ci - run: npm ci
- run: npx electron-builder --win --publish never - run: npx electron-builder --win --publish never
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@@ -52,7 +50,6 @@ jobs:
with: with:
node-version: '22' node-version: '22'
cache: 'npm' cache: 'npm'
- run: npm install
- run: npm ci - run: npm ci
- run: npx electron-builder --mac --publish never - run: npx electron-builder --mac --publish never
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
@@ -66,7 +63,7 @@ jobs:
release: release:
needs: [build-linux, build-windows, build-macos] needs: [build-linux, build-windows, build-macos]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
permissions: permissions:
contents: write contents: write
@@ -83,10 +80,14 @@ jobs:
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('manual-{0}', github.sha) }}
name: ${{ github.ref_type == 'tag' && github.ref_name || format('Manual build {0}', github.sha) }}
files: | files: |
artifacts/linux-builds/* artifacts/linux-builds/**/*
artifacts/windows-builds/* artifacts/windows-builds/**/*
artifacts/macos-builds/* artifacts/macos-builds/**/*
generate_release_notes: true generate_release_notes: true
draft: false draft: false
prerelease: false prerelease: false

1
.gitignore vendored
View File

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

View File

@@ -1,25 +1,32 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hytale F2P Launcher</title> <title>Hytale F2P Launcher</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"> <link
<link rel="stylesheet" href="style.css"> href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head> </head>
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1"> <body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
<div class="absolute inset-0 z-0"> <div class="absolute inset-0 z-0">
<img src="https://i.imgur.com/Visrk66.png" alt="Background" class="w-full h-full object-cover" /> <img src="https://i.imgur.com/Visrk66.png" alt="Background" class="w-full h-full object-cover" />
<div class="absolute inset-0 bg-black/60"></div> <div class="absolute inset-0 bg-black/60"></div>
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"%3E%3Cfilter id="noiseFilter"%3E%3CfeTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/%3E%3C/filter%3E%3Crect width="100%25" height="100%25" filter="url(%23noiseFilter)" opacity="0.1"/%3E%3C/svg%3E')] opacity-20"></div> <div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox=" 0 0 256 256"
xmlns="http://www.w3.org/2000/svg" %3E%3Cfilter id="noiseFilter" %3E%3CfeTurbulence type="fractalNoise"
baseFrequency="0.65" numOctaves="3" stitchTiles="stitch" /%3E%3C/filter%3E%3Crect width="100%25"
height="100%25" filter="url(%23noiseFilter)" opacity="0.1" /%3E%3C/svg%3E')] opacity-20"></div>
</div> </div>
<div class="flex w-full h-screen relative z-10"> <div class="flex w-full h-screen relative z-10">
<nav class="sidebar"> <nav class="sidebar">
<div class="sidebar-logo"> <div class="sidebar-logo">
<img src="./icon.png" alt="Hytale Logo" /> <img src="./icon.png" alt="Hytale Logo" />
</div> </div>
@@ -50,7 +57,7 @@
</div> </div>
</div> </div>
</nav> </nav>
<main class="main-content"> <main class="main-content">
@@ -61,6 +68,24 @@
<span id="onlineCount" class="counter-value">0</span> <span id="onlineCount" class="counter-value">0</span>
</div> </div>
<div class="profile-selector" id="profileSelector">
<button class="profile-btn" onclick="toggleProfileDropdown()">
<i class="fas fa-user-circle"></i>
<span id="currentProfileName">Default</span>
<i class="fas fa-chevron-down"></i>
</button>
<div class="profile-dropdown" id="profileDropdown">
<div class="profile-list" id="profileList">
<!-- Profiles populated by JS -->
</div>
<div class="profile-divider"></div>
<div class="profile-action" onclick="openProfileManager()">
<i class="fas fa-cog"></i>
<span>Manage Profiles</span>
</div>
</div>
</div>
<div class="window-controls"> <div class="window-controls">
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()"> <button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
<i class="fas fa-minus"></i> <i class="fas fa-minus"></i>
@@ -89,24 +114,26 @@
</h1> </h1>
<p class="install-subtitle">FREE TO PLAY LAUNCHER</p> <p class="install-subtitle">FREE TO PLAY LAUNCHER</p>
</div> </div>
<div class="install-form"> <div class="install-form">
<div class="form-group"> <div class="form-group">
<label class="form-label">Player Name</label> <label class="form-label">Player Name</label>
<input type="text" id="installPlayerName" placeholder="Enter your name" class="form-input" value="Player" /> <input type="text" id="installPlayerName" placeholder="Enter your name"
class="form-input" value="Player" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-group"> <label class="checkbox-group">
<input type="checkbox" id="installCustomCheck" class="custom-checkbox"> <input type="checkbox" id="installCustomCheck" class="custom-checkbox">
<span class="checkbox-label">Custom Installation</span> <span class="checkbox-label">Custom Installation</span>
</label> </label>
<div id="installCustomOptions" class="custom-options"> <div id="installCustomOptions" class="custom-options">
<div class="form-subgroup"> <div class="form-subgroup">
<label class="form-label">Installation Folder</label> <label class="form-label">Installation Folder</label>
<div class="input-with-button"> <div class="input-with-button">
<input type="text" id="installPath" placeholder="Default location" class="form-input" readonly /> <input type="text" id="installPath" placeholder="Default location"
class="form-input" readonly />
<button onclick="browseInstallPath()" class="browse-btn"> <button onclick="browseInstallPath()" class="browse-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
</button> </button>
@@ -114,7 +141,7 @@
</div> </div>
</div> </div>
</div> </div>
<button id="installBtn" class="install-button" onclick="installGame()"> <button id="installBtn" class="install-button" onclick="installGame()">
<i class="fas fa-download mr-2"></i> <i class="fas fa-download mr-2"></i>
<span id="installText">INSTALL HYTALE</span> <span id="installText">INSTALL HYTALE</span>
@@ -134,7 +161,7 @@
</h2> </h2>
<p class="play-subtitle">Launch Hytale and enter the adventure</p> <p class="play-subtitle">Launch Hytale and enter the adventure</p>
</div> </div>
<button id="homePlayBtn" class="home-play-button" onclick="launch()"> <button id="homePlayBtn" class="home-play-button" onclick="launch()">
<i class="fas fa-play"></i> <i class="fas fa-play"></i>
<span>PLAY HYTALE</span> <span>PLAY HYTALE</span>
@@ -205,26 +232,28 @@
<i class="fas fa-comments mr-2"></i> <i class="fas fa-comments mr-2"></i>
PLAYERS CHAT PLAYERS CHAT
</h2> </h2>
<div class="chat-online-badge"> <div class="chat-header-actions">
<i class="fas fa-circle"></i> <button id="chatColorBtn" class="chat-color-btn">
<span id="chatOnlineCount">0</span> online <i class="fas fa-palette"></i>
<span>Color</span>
</button>
<div class="chat-online-badge">
<i class="fas fa-circle"></i>
<span id="chatOnlineCount">0</span> online
</div>
</div> </div>
</div> </div>
<div class="chat-body"> <div class="chat-body">
<div id="chatMessages" class="chat-messages"> <div id="chatMessages" class="chat-messages">
</div> </div>
</div> </div>
<div class="chat-footer"> <div class="chat-footer">
<div class="chat-input-container"> <div class="chat-input-container">
<textarea <textarea id="chatInput" class="chat-input"
id="chatInput" placeholder="Type your message... (Links are automatically censored)" rows="1"
class="chat-input" maxlength="500"></textarea>
placeholder="Type your message... (Links are automatically censored)"
rows="1"
maxlength="500"
></textarea>
<button id="chatSendBtn" class="chat-send-btn"> <button id="chatSendBtn" class="chat-send-btn">
<i class="fas fa-paper-plane"></i> <i class="fas fa-paper-plane"></i>
</button> </button>
@@ -248,36 +277,32 @@
SETTINGS SETTINGS
</h2> </h2>
</div> </div>
<div class="settings-content"> <div class="settings-content">
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-coffee"></i> <i class="fas fa-coffee"></i>
Java Runtime Java Runtime
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
<label class="settings-checkbox"> <label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" /> <input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span> <span class="checkmark"></span>
<div class="checkbox-content"> <div class="checkbox-content">
<div class="checkbox-title">Use Custom Java Path</div> <div class="checkbox-title">Use Custom Java Path</div>
<div class="checkbox-description">Override the bundled Java runtime with your own installation</div> <div class="checkbox-description">Override the bundled Java runtime with
your own installation</div>
</div> </div>
</label> </label>
</div> </div>
<div id="customJavaOptions" class="custom-java-options" style="display: none;"> <div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label">Java Executable Path</label> <label class="settings-input-label">Java Executable Path</label>
<div class="settings-input-with-button"> <div class="settings-input-with-button">
<input <input type="text" id="customJavaPath" class="settings-input"
type="text" placeholder="Select Java path..." readonly />
id="customJavaPath"
class="settings-input"
placeholder="Select Java path..."
readonly
/>
<button id="browseJavaBtn" class="settings-browse-btn"> <button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
Browse Browse
@@ -291,32 +316,47 @@
</div> </div>
</div> </div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
Discord Integration
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable Discord Rich Presence</div>
<div class="checkbox-description">Show your launcher activity on Discord
</div>
</div>
</label>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title"> <h3 class="settings-section-title">
<i class="fas fa-gamepad"></i> <i class="fas fa-gamepad"></i>
Game Options Game Options
</h3> </h3>
<div class="settings-option"> <div class="settings-option">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label">Player Name</label> <label class="settings-input-label">Player Name</label>
<input <input type="text" id="settingsPlayerName" class="settings-input"
type="text" placeholder="Enter your player name" maxlength="16" />
id="settingsPlayerName"
class="settings-input"
placeholder="Enter your player name"
maxlength="16"
/>
<p class="settings-hint"> <p class="settings-hint">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
This name will be used in-game (1-16 characters) This name will be used in-game (1-16 characters)
</p> </p>
</div> </div>
</div> </div>
<div class="settings-option"> <div class="settings-option">
<div class="settings-button-group"> <div class="settings-button-group">
<button id="openGameLocationBtn" class="settings-action-btn" onclick="openGameLocation()"> <button id="openGameLocationBtn" class="settings-action-btn"
onclick="openGameLocation()">
<i class="fas fa-folder-open"></i> <i class="fas fa-folder-open"></i>
<div class="btn-content"> <div class="btn-content">
<div class="btn-title">Open Game Location</div> <div class="btn-title">Open Game Location</div>
@@ -326,6 +366,46 @@
</div> </div>
</div> </div>
</div> </div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-fingerprint"></i>
Player UUID Management
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label">Current UUID</label>
<div class="uuid-display-container">
<input type="text" id="currentUuid" class="settings-input uuid-input"
readonly placeholder="Loading UUID..." />
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
Your unique player identifier for this username
</p>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="manageUuidsBtn" class="settings-action-btn">
<i class="fas fa-list"></i>
<div class="btn-content">
<div class="btn-title">Manage All UUIDs</div>
<div class="btn-description">View and manage all player UUIDs</div>
</div>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -390,14 +470,8 @@
</p> </p>
<div class="chat-username-input-group"> <div class="chat-username-input-group">
<label for="chatUsernameInput" class="chat-username-label">Username</label> <label for="chatUsernameInput" class="chat-username-label">Username</label>
<input <input type="text" id="chatUsernameInput" class="chat-username-input"
type="text" placeholder="Enter your username..." maxlength="20" autocomplete="off" />
id="chatUsernameInput"
class="chat-username-input"
placeholder="Enter your username..."
maxlength="20"
autocomplete="off"
/>
<span class="chat-username-hint">3-20 characters, letters, numbers, - and _ only</span> <span class="chat-username-hint">3-20 characters, letters, numbers, - and _ only</span>
<span id="chatUsernameError" class="chat-username-error"></span> <span id="chatUsernameError" class="chat-username-error"></span>
</div> </div>
@@ -415,26 +489,128 @@
</div> </div>
</div> </div>
<!-- UUID Management Modal -->
<div id="uuidModal" class="uuid-modal" style="display: none;">
<div class="uuid-modal-content">
<div class="uuid-modal-header">
<h2 class="uuid-modal-title">
<i class="fas fa-fingerprint mr-2"></i>
UUID Management
</h2>
<button id="uuidModalClose" class="modal-close-btn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="uuid-modal-body">
<div class="uuid-current-section">
<h3 class="uuid-section-title">Current User UUID</h3>
<div class="uuid-current-display">
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="modalRegenerateUuidBtn" class="uuid-action-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="uuid-list-section">
<div class="uuid-list-header">
<h3 class="uuid-section-title">All Player UUIDs</h3>
<button id="generateNewUuidBtn" class="uuid-generate-btn">
<i class="fas fa-plus"></i>
Generate New UUID
</button>
</div>
<div id="uuidList" class="uuid-list">
<div class="uuid-loading">
<i class="fas fa-spinner fa-spin"></i>
Loading UUIDs...
</div>
</div>
</div>
<div class="uuid-custom-section">
<h3 class="uuid-section-title">Set Custom UUID</h3>
<div class="uuid-custom-form">
<input type="text" id="customUuidInput" class="uuid-input"
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
maxlength="36" />
<button id="setCustomUuidBtn" class="uuid-set-btn">
<i class="fas fa-check"></i>
Set UUID
</button>
</div>
<p class="uuid-custom-hint">
<i class="fas fa-exclamation-triangle"></i>
Warning: Setting a custom UUID will change your current player identity
</p>
</div>
</div>
</div>
</div>
<!-- Profile Manager Modal -->
<div id="profileManagerModal" class="profile-modal" style="display: none;">
<div class="profile-modal-content">
<div class="profile-modal-header">
<h2 class="profile-modal-title">
<i class="fas fa-users-cog mr-2"></i>
Manage Profiles
</h2>
<button class="modal-close-btn" onclick="closeProfileManager()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="profile-modal-body">
<div class="profile-manager-list" id="managerProfileList">
<!-- Populated by JS -->
</div>
<div class="profile-create-section">
<input type="text" id="newProfileName" placeholder="New Profile Name" class="profile-input"
maxlength="20">
<button class="profile-create-btn" onclick="createNewProfile()">
<i class="fas fa-plus"></i> Create Profile
</button>
</div>
</div>
</div>
</div>
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2"> <footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
<div class="flex items-center justify-center text-xs text-gray-400"> <div class="flex items-center justify-center text-xs text-gray-400">
<span>Made by <a href="https://github.com/amiayweb" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a></span> <span>Made by <a href="https://github.com/amiayweb" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a> & <a
href="https://github.com/Relyz1993" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@Relyz</a></span>
<span class="mx-2">|</span> <span class="mx-2">|</span>
<span>Contributors: <span>Contributors:
<a href="https://github.com/chasem-dev" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>, <a href="https://github.com/chasem-dev" target="_blank"
<a href="https://github.com/crimera" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>, class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>,
<a href="https://github.com/sanasol" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>, <a href="https://github.com/crimera" target="_blank"
<a href="https://github.com/Terromur" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>, class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>,
<a href="https://github.com/ericiskoolbeans" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a> <a href="https://github.com/sanasol" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>,
<a href="https://github.com/Terromur" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
<a href="https://github.com/ericiskoolbeans" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
<a href="https://github.com/fazrigading" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
</span> </span>
</div> </div>
</footer> </footer>
<script type="module" src="js/script.js"></script> <!-- Discord Notification --> <script type="module" src="js/script.js"></script> <!-- Discord Notification -->
<div id="discordNotification" class="discord-notification"> <div id="discordNotification" class="discord-notification">
<div class="notification-content"> <div class="notification-content">
<i class="fab fa-discord"></i> <i class="fab fa-discord"></i>
<span class="notification-text">Join our Discord community!</span> <span class="notification-text">Join our Discord community!</span>
<button class="notification-action" onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')"> <button class="notification-action"
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
Join Discord Join Discord
</button> </button>
</div> </div>
@@ -442,7 +618,51 @@
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<!-- Modal pour sélectionner la couleur du chat -->
<div id="chatColorModal" class="chat-color-modal" style="display: none;">
<div class="chat-color-modal-content">
<div class="chat-color-modal-header">
<h3 class="chat-color-modal-title">
<i class="fas fa-palette"></i>
Customize Username Color
</h3>
<button class="modal-close-btn" onclick="closeChatColorModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="chat-color-modal-body">
<div id="solidColorSection" class="color-section">
<h4>Choose a solid color:</h4>
<div class="predefined-colors">
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
<div class="color-option" data-color="#2ecc71" style="background: #2ecc71;"></div>
<div class="color-option" data-color="#f39c12" style="background: #f39c12;"></div>
<div class="color-option" data-color="#9b59b6" style="background: #9b59b6;"></div>
<div class="color-option" data-color="#1abc9c" style="background: #1abc9c;"></div>
<div class="color-option" data-color="#e91e63" style="background: #e91e63;"></div>
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
</div>
<div class="custom-color-input">
<label for="customColor">Custom color:</label>
<input type="color" id="customColor" value="#3498db">
</div>
</div>
<div class="color-preview">
<h4>Preview:</h4>
<div id="colorPreview" class="preview-username">YourUsername</div>
</div>
</div>
<div class="chat-color-modal-footer">
<button class="btn-secondary" onclick="closeChatColorModal()">Cancel</button>
<button class="btn-primary" onclick="applyChatColor()">Apply Color</button>
</div>
</div>
</div>
<script type="module" src="js/update.js"></script> <script type="module" src="js/update.js"></script>
</body> </body>
</html> </html>

View File

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

View File

@@ -33,21 +33,12 @@ export function setupInstallation() {
if (window.electronAPI && window.electronAPI.onProgressUpdate) { if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => { window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.showProgress();
window.LauncherUI.updateProgress(data); window.LauncherUI.updateProgress(data);
} }
}); });
} }
if (window.electronAPI && window.electronAPI.onProgressComplete) {
window.electronAPI.onProgressComplete(() => {
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetInstallButton();
});
}
} }
export async function installGame() { export async function installGame() {
@@ -75,6 +66,7 @@ export async function installGame() {
window.LauncherUI.showLauncherOrInstall(true); window.LauncherUI.showLauncherOrInstall(true);
const playerNameInput = document.getElementById('playerName'); const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName; if (playerNameInput) playerNameInput.value = playerName;
resetInstallButton();
}, 2000); }, 2000);
} }
} else { } else {
@@ -210,3 +202,9 @@ document.addEventListener('DOMContentLoaded', async () => {
setupInstallation(); setupInstallation();
await checkGameStatusAndShowInterface(); await checkGameStatusAndShowInterface();
}); });
window.browseInstallPath = browseInstallPath;
document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});

View File

@@ -14,108 +14,396 @@ export function setupLauncher() {
uninstallBtn = document.getElementById('uninstallBtn'); uninstallBtn = document.getElementById('uninstallBtn');
playerNameInput = document.getElementById('playerName'); playerNameInput = document.getElementById('playerName');
javaPathInput = document.getElementById('javaPath'); javaPathInput = document.getElementById('javaPath');
if (playerNameInput) { if (playerNameInput) {
playerNameInput.addEventListener('change', savePlayerName); playerNameInput.addEventListener('change', savePlayerName);
} }
if (javaPathInput) { if (javaPathInput) {
javaPathInput.addEventListener('change', saveJavaPath); javaPathInput.addEventListener('change', saveJavaPath);
} }
if (window.electronAPI && window.electronAPI.onProgressUpdate) { if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => { window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) {
window.LauncherUI.updateProgress(data);
}
});
}
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.showProgress();
window.LauncherUI.updateProgress(data); window.LauncherUI.updateProgress(data);
} }
}); });
} }
if (window.electronAPI && window.electronAPI.onProgressComplete) { // Initial Profile Load
window.electronAPI.onProgressComplete(() => { loadProfiles();
if (window.LauncherUI) {
window.LauncherUI.hideProgress(); // Close dropdown on outside click
} document.addEventListener('click', (e) => {
resetPlayButton(); const selector = document.getElementById('profileSelector');
}); if (selector && !selector.contains(e.target)) {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
}
});
}
// ==========================================
// PROFILE MANAGEMENT
// ==========================================
async function loadProfiles() {
try {
if (!window.electronAPI || !window.electronAPI.profile) return;
const profiles = await window.electronAPI.profile.list();
const activeProfile = await window.electronAPI.profile.getActive();
renderProfileList(profiles, activeProfile);
updateCurrentProfileUI(activeProfile);
} catch (error) {
console.error('Failed to load profiles:', error);
} }
} }
function renderProfileList(profiles, activeProfile) {
const list = document.getElementById('profileList');
const managerList = document.getElementById('managerProfileList');
if (!list) return;
// Dropdown List
list.innerHTML = profiles.map(p => `
<div class="profile-item ${p.id === activeProfile.id ? 'active' : ''}"
onclick="switchProfile('${p.id}')">
<span>${p.name}</span>
${p.id === activeProfile.id ? '<i class="fas fa-check ml-auto"></i>' : ''}
</div>
`).join('');
// Manager Modal List
if (managerList) {
managerList.innerHTML = profiles.map(p => `
<div class="profile-manager-item ${p.id === activeProfile.id ? 'active' : ''}">
<div class="flex items-center gap-3">
<i class="fas fa-user-circle text-xl text-gray-400"></i>
<div>
<div class="font-bold">${p.name}</div>
<div class="text-xs text-gray-500">ID: ${p.id.substring(0, 8)}...</div>
</div>
</div>
${p.id !== activeProfile.id ? `
<button class="profile-delete-btn" onclick="deleteProfile('${p.id}')" title="Delete Profile">
<i class="fas fa-trash"></i>
</button>
` : '<span class="text-xs text-green-500 font-bold px-2">ACTIVE</span>'}
</div>
`).join('');
}
}
function updateCurrentProfileUI(profile) {
const nameEl = document.getElementById('currentProfileName');
if (nameEl && profile) {
nameEl.textContent = profile.name;
}
}
window.toggleProfileDropdown = () => {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) {
dropdown.classList.toggle('show');
}
};
window.openProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) {
modal.style.display = 'flex';
// Refresh list
loadProfiles();
}
// Close dropdown
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
};
window.closeProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) modal.style.display = 'none';
};
window.createNewProfile = async () => {
const input = document.getElementById('newProfileName');
if (!input || !input.value.trim()) return;
try {
const name = input.value.trim();
await window.electronAPI.profile.create(name);
input.value = '';
await loadProfiles();
} catch (error) {
console.error('Failed to create profile:', error);
alert('Failed to create profile: ' + error.message);
}
};
window.deleteProfile = async (id) => {
if (!confirm('Are you sure you want to delete this profile? parameters and mods configuration will be lost.')) return;
try {
await window.electronAPI.profile.delete(id);
await loadProfiles();
} catch (error) {
console.error('Failed to delete profile:', error);
alert('Failed to delete profile: ' + error.message);
}
};
window.switchProfile = async (id) => {
try {
if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Switching Profile...' });
await window.electronAPI.profile.activate(id);
// Refresh UI
await loadProfiles();
// Refresh Mods
if (window.modsManager) {
if (window.modsManager.loadInstalledMods) await window.modsManager.loadInstalledMods();
if (window.modsManager.loadBrowseMods) await window.modsManager.loadBrowseMods();
}
// Close dropdown
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Profile Switched!' });
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
}
} catch (error) {
console.error('Failed to switch profile:', error);
alert('Failed to switch profile: ' + error.message);
if (window.LauncherUI) window.LauncherUI.hideProgress();
}
};
export async function launch() { export async function launch() {
if (isDownloading || (playBtn && playBtn.disabled)) return; if (isDownloading || (playBtn && playBtn.disabled)) return;
let playerName = 'Player'; let playerName = 'Player';
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) { if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
playerName = window.SettingsAPI.getCurrentPlayerName(); playerName = window.SettingsAPI.getCurrentPlayerName();
} else if (playerNameInput && playerNameInput.value.trim()) { } else if (playerNameInput && playerNameInput.value.trim()) {
playerName = playerNameInput.value.trim(); playerName = playerNameInput.value.trim();
} }
let javaPath = ''; let javaPath = '';
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) { if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
javaPath = window.SettingsAPI.getCurrentJavaPath(); javaPath = window.SettingsAPI.getCurrentJavaPath();
} }
if (window.LauncherUI) window.LauncherUI.showProgress(); if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true; isDownloading = true;
if (playBtn) { if (playBtn) {
playBtn.disabled = true; playBtn.disabled = true;
playText.textContent = 'LAUNCHING...'; playText.textContent = 'LAUNCHING...';
} }
try { try {
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Starting game...' });
if (window.electronAPI && window.electronAPI.launchGame) { if (window.electronAPI && window.electronAPI.launchGame) {
const result = await window.electronAPI.launchGame(playerName, javaPath, ''); const result = await window.electronAPI.launchGame(playerName, javaPath, '');
isDownloading = false;
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetPlayButton();
if (result.success) { if (result.success) {
if (window.LauncherUI) { if (window.electronAPI.minimizeWindow) {
window.LauncherUI.updateProgress({ message: 'Game started successfully!' });
setTimeout(() => { setTimeout(() => {
window.LauncherUI.hideProgress(); window.electronAPI.minimizeWindow();
if (window.electronAPI.minimizeWindow) { }, 500);
window.electronAPI.minimizeWindow();
}
}, 2000);
} }
} else { } else {
throw new Error(result.error || 'Launch failed'); console.error('Launch failed:', result.error);
} }
} else { } else {
setTimeout(() => { isDownloading = false;
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game started successfully!' }); if (window.LauncherUI) {
setTimeout(() => { window.LauncherUI.hideProgress();
window.LauncherUI.hideProgress(); }
resetPlayButton(); resetPlayButton();
}, 2000);
}
}, 2000);
} }
} catch (error) { } catch (error) {
isDownloading = false;
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Failed: ${error.message}` }); window.LauncherUI.hideProgress();
setTimeout(() => {
window.LauncherUI.hideProgress();
resetPlayButton();
}, 3000);
} }
resetPlayButton();
console.error('Launch error:', error);
} }
} }
export async function uninstallGame() { function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') {
if (!confirm('Are you sure you want to uninstall Hytale? All game files will be deleted.')) { const existingModal = document.querySelector('.custom-confirm-modal');
return; if (existingModal) {
existingModal.remove();
} }
const modal = document.createElement('div');
modal.className = 'custom-confirm-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.className = 'custom-confirm-dialog';
dialog.style.cssText = `
background: #1f2937;
border-radius: 12px;
padding: 0;
min-width: 400px;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(239, 68, 68, 0.3);
transform: scale(0.9);
transition: transform 0.3s ease;
`;
dialog.innerHTML = `
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</h3>
</div>
</div>
<div style="padding: 24px; color: #e5e7eb;">
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
</div>
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button class="custom-confirm-cancel" style="
background: transparent;
color: #9ca3af;
border: 1px solid rgba(156, 163, 175, 0.3);
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${cancelText}</button>
<button class="custom-confirm-action" style="
background: #ef4444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${confirmText}</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// Animate in
setTimeout(() => {
modal.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// Event handlers
const cancelBtn = dialog.querySelector('.custom-confirm-cancel');
const actionBtn = dialog.querySelector('.custom-confirm-action');
const closeModal = () => {
modal.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
modal.remove();
}, 300);
};
cancelBtn.onclick = () => {
closeModal();
if (onCancel) onCancel();
};
actionBtn.onclick = () => {
closeModal();
onConfirm();
};
modal.onclick = (e) => {
if (e.target === modal) {
closeModal();
if (onCancel) onCancel();
}
};
// Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeModal();
if (onCancel) onCancel();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
export async function uninstallGame() {
showCustomConfirm(
'Are you sure you want to uninstall Hytale? All game files will be deleted.',
'Uninstall Game',
async () => {
await performUninstall();
},
null,
'Uninstall',
'Cancel'
);
}
async function performUninstall() {
if (window.LauncherUI) window.LauncherUI.showProgress(); if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' }); if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' });
if (uninstallBtn) uninstallBtn.disabled = true; if (uninstallBtn) uninstallBtn.disabled = true;
try { try {
if (window.electronAPI && window.electronAPI.uninstallGame) { if (window.electronAPI && window.electronAPI.uninstallGame) {
const result = await window.electronAPI.uninstallGame(); const result = await window.electronAPI.uninstallGame();
if (result.success) { if (result.success) {
if (window.LauncherUI) { if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' }); window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
@@ -180,7 +468,7 @@ async function saveJavaPath() {
function toggleCustomJava() { function toggleCustomJava() {
if (!customJavaOptions) return; if (!customJavaOptions) return;
if (customJavaCheck && customJavaCheck.checked) { if (customJavaCheck && customJavaCheck.checked) {
customJavaOptions.style.display = 'block'; customJavaOptions.style.display = 'block';
} else { } else {

View File

@@ -22,10 +22,10 @@ function setupModsEventListeners() {
let searchTimeout; let searchTimeout;
searchInput.addEventListener('input', (e) => { searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value.toLowerCase().trim(); searchQuery = e.target.value.toLowerCase().trim();
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
modsPage = 0; modsPage = 0;
loadBrowseMods(); loadBrowseMods();
}, 500); }, 500);
}); });
@@ -52,7 +52,7 @@ function setupModsEventListeners() {
const prevPageBtn = document.getElementById('prevPage'); const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage'); const nextPageBtn = document.getElementById('nextPage');
if (prevPageBtn) { if (prevPageBtn) {
prevPageBtn.addEventListener('click', () => { prevPageBtn.addEventListener('click', () => {
if (modsPage > 0) { if (modsPage > 0) {
@@ -61,7 +61,7 @@ function setupModsEventListeners() {
} }
}); });
} }
if (nextPageBtn) { if (nextPageBtn) {
nextPageBtn.addEventListener('click', () => { nextPageBtn.addEventListener('click', () => {
if (modsPage < modsTotalPages - 1) { if (modsPage < modsTotalPages - 1) {
@@ -97,7 +97,7 @@ async function loadInstalledMods() {
const mods = await window.electronAPI?.loadInstalledMods(modsPath); const mods = await window.electronAPI?.loadInstalledMods(modsPath);
installedMods = mods || []; installedMods = mods || [];
displayInstalledMods(installedMods); displayInstalledMods(installedMods);
} catch (error) { } catch (error) {
console.error('Error loading installed mods:', error); console.error('Error loading installed mods:', error);
@@ -196,13 +196,13 @@ async function loadBrowseMods() {
const offset = modsPage * modsPageSize; const offset = modsPage * modsPageSize;
let url = `${CURSEFORGE_API}/mods/search?gameId=${HYTALE_GAME_ID}&pageSize=${modsPageSize}&sortOrder=desc&sortField=6&index=${offset}`; let url = `${CURSEFORGE_API}/mods/search?gameId=${HYTALE_GAME_ID}&pageSize=${modsPageSize}&sortOrder=desc&sortField=6&index=${offset}`;
if (searchQuery && searchQuery.length > 0) { if (searchQuery && searchQuery.length > 0) {
url += `&searchFilter=${encodeURIComponent(searchQuery)}`; url += `&searchFilter=${encodeURIComponent(searchQuery)}`;
} }
console.log('Fetching mods from page', modsPage + 1, 'offset:', offset, 'search:', searchQuery || 'none', 'URL:', url); console.log('Fetching mods from page', modsPage + 1, 'offset:', offset, 'search:', searchQuery || 'none', 'URL:', url);
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
'x-api-key': API_KEY, 'x-api-key': API_KEY,
@@ -221,7 +221,7 @@ async function loadBrowseMods() {
const data = await response.json(); const data = await response.json();
console.log('API Response data:', data); console.log('API Response data:', data);
console.log('Total mods found:', data.data?.length || 0); console.log('Total mods found:', data.data?.length || 0);
browseMods = (data.data || []).map(mod => ({ browseMods = (data.data || []).map(mod => ({
id: mod.id.toString(), id: mod.id.toString(),
name: mod.name, name: mod.name,
@@ -282,18 +282,25 @@ function displayBrowseMods(mods) {
} }
function createBrowseModCard(mod) { function createBrowseModCard(mod) {
const isInstalled = installedMods.some(installed => const isInstalled = installedMods.some(installed => {
installed.name.toLowerCase().includes(mod.name.toLowerCase()) || // Check by CurseForge ID (most reliable)
installed.curseForgeId == mod.id if (installed.curseForgeId && installed.curseForgeId.toString() === mod.id.toString()) {
); return true;
}
// Check by exact name match for manually installed mods
if (installed.name.toLowerCase() === mod.name.toLowerCase()) {
return true;
}
return false;
});
return ` return `
<div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\"> <div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\">
<div class=\"mod-image\"> <div class=\"mod-image\">
${mod.thumbnailUrl ? ${mod.thumbnailUrl ?
`<img src=\"${mod.thumbnailUrl}\" alt=\"${mod.name}\" onerror=\"this.parentElement.innerHTML='<i class=\\\"fas fa-puzzle-piece\\\"></i>'\">` : `<img src=\"${mod.thumbnailUrl}\" alt=\"${mod.name}\" onerror=\"this.parentElement.innerHTML='<i class=\\\"fas fa-puzzle-piece\\\"></i>'\">` :
`<i class=\"fas fa-puzzle-piece\"></i>` `<i class=\"fas fa-puzzle-piece\"></i>`
} }
</div> </div>
<div class=\"mod-info\"> <div class=\"mod-info\">
@@ -319,16 +326,16 @@ function createBrowseModCard(mod) {
<i class=\"fas fa-external-link-alt\"></i> <i class=\"fas fa-external-link-alt\"></i>
VIEW VIEW
</button> </button>
${!isInstalled ? ${!isInstalled ?
`<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\"> `<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
<i class=\"fas fa-download\"></i> <i class=\"fas fa-download\"></i>
INSTALL INSTALL
</button>` : </button>` :
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled> `<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
<i class=\"fas fa-check\"></i> <i class=\"fas fa-check\"></i>
INSTALLED INSTALLED
</button>` </button>`
} }
</div> </div>
</div> </div>
`; `;
@@ -337,9 +344,9 @@ function createBrowseModCard(mod) {
async function downloadAndInstallMod(modInfo) { async function downloadAndInstallMod(modInfo) {
try { try {
window.LauncherUI?.showProgress(`Downloading ${modInfo.name}...`); window.LauncherUI?.showProgress(`Downloading ${modInfo.name}...`);
const result = await window.electronAPI?.downloadMod(modInfo); const result = await window.electronAPI?.downloadMod(modInfo);
if (result?.success) { if (result?.success) {
const newMod = { const newMod = {
id: result.modInfo.id, id: result.modInfo.id,
@@ -354,11 +361,11 @@ async function downloadAndInstallMod(modInfo) {
curseForgeId: modInfo.modId, curseForgeId: modInfo.modId,
curseForgeFileId: modInfo.fileId curseForgeFileId: modInfo.fileId
}; };
installedMods.push(newMod); installedMods.push(newMod);
await loadInstalledMods(); await loadInstalledMods();
await loadBrowseMods(); await loadBrowseMods();
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
showNotification(`${modInfo.name} installed successfully! 🎉`, 'success'); showNotification(`${modInfo.name} installed successfully! 🎉`, 'success');
} else { } else {
@@ -374,10 +381,10 @@ async function downloadAndInstallMod(modInfo) {
async function toggleMod(modId) { async function toggleMod(modId) {
try { try {
window.LauncherUI?.showProgress('Toggling mod...'); window.LauncherUI?.showProgress('Toggling mod...');
const modsPath = await window.electronAPI?.getModsPath(); const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.toggleMod(modId, modsPath); const result = await window.electronAPI?.toggleMod(modId, modsPath);
if (result?.success) { if (result?.success) {
await loadInstalledMods(); await loadInstalledMods();
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
@@ -400,13 +407,13 @@ async function deleteMod(modId) {
async () => { async () => {
try { try {
window.LauncherUI?.showProgress('Deleting mod...'); window.LauncherUI?.showProgress('Deleting mod...');
const modsPath = await window.electronAPI?.getModsPath(); const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.uninstallMod(modId, modsPath); const result = await window.electronAPI?.uninstallMod(modId, modsPath);
if (result?.success) { if (result?.success) {
await loadInstalledMods(); await loadInstalledMods();
await loadBrowseMods(); await loadBrowseMods();
window.LauncherUI?.hideProgress(); window.LauncherUI?.hideProgress();
showNotification(`"${mod.name}" deleted successfully`, 'success'); showNotification(`"${mod.name}" deleted successfully`, 'success');
} else { } else {
@@ -436,21 +443,21 @@ function showNotification(message, type = 'info', duration = 4000) {
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.className = `mod-notification ${type}`; notification.className = `mod-notification ${type}`;
const icons = { const icons = {
success: 'fa-check-circle', success: 'fa-check-circle',
error: 'fa-exclamation-circle', error: 'fa-exclamation-circle',
info: 'fa-info-circle', info: 'fa-info-circle',
warning: 'fa-exclamation-triangle' warning: 'fa-exclamation-triangle'
}; };
const colors = { const colors = {
success: '#10b981', success: '#10b981',
error: '#ef4444', error: '#ef4444',
info: '#3b82f6', info: '#3b82f6',
warning: '#f59e0b' warning: '#f59e0b'
}; };
notification.innerHTML = ` notification.innerHTML = `
<div class="notification-content"> <div class="notification-content">
<i class="fas ${icons[type]}"></i> <i class="fas ${icons[type]}"></i>
@@ -460,7 +467,7 @@ function showNotification(message, type = 'info', duration = 4000) {
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
`; `;
notification.style.cssText = ` notification.style.cssText = `
position: fixed; position: fixed;
top: 20px; top: 20px;
@@ -481,14 +488,14 @@ function showNotification(message, type = 'info', duration = 4000) {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
`; `;
const contentStyle = ` const contentStyle = `
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
flex: 1; flex: 1;
`; `;
const closeStyle = ` const closeStyle = `
background: none; background: none;
border: none; border: none;
@@ -500,17 +507,17 @@ function showNotification(message, type = 'info', duration = 4000) {
transition: opacity 0.2s; transition: opacity 0.2s;
margin-left: 10px; margin-left: 10px;
`; `;
notification.querySelector('.notification-content').style.cssText = contentStyle; notification.querySelector('.notification-content').style.cssText = contentStyle;
notification.querySelector('.notification-close').style.cssText = closeStyle; notification.querySelector('.notification-close').style.cssText = closeStyle;
document.body.appendChild(notification); document.body.appendChild(notification);
// Animate in // Animate in
setTimeout(() => { setTimeout(() => {
notification.style.transform = 'translateX(0)'; notification.style.transform = 'translateX(0)';
}, 10); }, 10);
// Auto remove // Auto remove
setTimeout(() => { setTimeout(() => {
if (notification.parentElement) { if (notification.parentElement) {
@@ -522,7 +529,6 @@ function showNotification(message, type = 'info', duration = 4000) {
}, duration); }, duration);
} }
// Custom confirmation modal
function showConfirmModal(message, onConfirm, onCancel = null) { function showConfirmModal(message, onConfirm, onCancel = null) {
const existingModal = document.querySelector('.mod-confirm-modal'); const existingModal = document.querySelector('.mod-confirm-modal');
if (existingModal) { if (existingModal) {
@@ -652,13 +658,13 @@ function updatePagination() {
if (currentPageEl) currentPageEl.textContent = modsPage + 1; if (currentPageEl) currentPageEl.textContent = modsPage + 1;
if (totalPagesEl) totalPagesEl.textContent = modsTotalPages; if (totalPagesEl) totalPagesEl.textContent = modsTotalPages;
if (prevBtn) { if (prevBtn) {
prevBtn.disabled = modsPage === 0; prevBtn.disabled = modsPage === 0;
prevBtn.style.opacity = modsPage === 0 ? '0.5' : '1'; prevBtn.style.opacity = modsPage === 0 ? '0.5' : '1';
prevBtn.style.cursor = modsPage === 0 ? 'not-allowed' : 'pointer'; prevBtn.style.cursor = modsPage === 0 ? 'not-allowed' : 'pointer';
} }
if (nextBtn) { if (nextBtn) {
nextBtn.disabled = modsPage >= modsTotalPages - 1; nextBtn.disabled = modsPage >= modsTotalPages - 1;
nextBtn.style.opacity = modsPage >= modsTotalPages - 1 ? '0.5' : '1'; nextBtn.style.opacity = modsPage >= modsTotalPages - 1 ? '0.5' : '1';
@@ -682,7 +688,7 @@ function showInstalledModsError(message) {
function viewModPage(modId) { function viewModPage(modId) {
console.log('Looking for mod with ID:', modId, 'Type:', typeof modId); console.log('Looking for mod with ID:', modId, 'Type:', typeof modId);
console.log('Available mods:', browseMods.map(m => ({ id: m.id, name: m.name, type: typeof m.id }))); console.log('Available mods:', browseMods.map(m => ({ id: m.id, name: m.name, type: typeof m.id })));
const mod = browseMods.find(m => m.id.toString() === modId.toString()); const mod = browseMods.find(m => m.id.toString() === modId.toString());
if (mod) { if (mod) {
console.log('Found mod:', mod.name); console.log('Found mod:', mod.name);
@@ -695,9 +701,9 @@ function viewModPage(modId) {
const nameSlug = mod.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); const nameSlug = mod.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
modUrl = `https://www.curseforge.com/hytale/mods/${nameSlug}`; modUrl = `https://www.curseforge.com/hytale/mods/${nameSlug}`;
} }
console.log('Opening URL:', modUrl); console.log('Opening URL:', modUrl);
if (window.electronAPI && window.electronAPI.openExternalLink) { if (window.electronAPI && window.electronAPI.openExternalLink) {
window.electronAPI.openExternalLink(modUrl); window.electronAPI.openExternalLink(modUrl);
} else { } else {
@@ -718,7 +724,9 @@ window.modsManager = {
deleteMod, deleteMod,
openMyModsModal, openMyModsModal,
closeMyModsModal, closeMyModsModal,
viewModPage viewModPage,
loadInstalledMods,
loadBrowseMods
}; };
document.addEventListener('DOMContentLoaded', initModsManager); document.addEventListener('DOMContentLoaded', initModsManager);

View File

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

View File

@@ -7,7 +7,6 @@ import './players.js';
import './chat.js'; import './chat.js';
import './settings.js'; import './settings.js';
// Discord notification functions
window.closeDiscordNotification = function() { window.closeDiscordNotification = function() {
const notification = document.getElementById('discordNotification'); const notification = document.getElementById('discordNotification');
if (notification) { if (notification) {
@@ -18,23 +17,20 @@ window.closeDiscordNotification = function() {
} }
}; };
// Show notification after a delay
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const notification = document.getElementById('discordNotification'); const notification = document.getElementById('discordNotification');
if (notification) { if (notification) {
// Check if user has previously dismissed the notification
const dismissed = localStorage.getItem('discordNotificationDismissed'); const dismissed = localStorage.getItem('discordNotificationDismissed');
if (!dismissed) { if (!dismissed) {
setTimeout(() => { setTimeout(() => {
notification.style.display = 'flex'; notification.style.display = 'flex';
}, 3000); // Show after 3 seconds }, 3000);
} else { } else {
notification.style.display = 'none'; notification.style.display = 'none';
} }
} }
}); });
// Remember when user closes notification
const originalClose = window.closeDiscordNotification; const originalClose = window.closeDiscordNotification;
window.closeDiscordNotification = function() { window.closeDiscordNotification = function() {
localStorage.setItem('discordNotificationDismissed', 'true'); localStorage.setItem('discordNotificationDismissed', 'true');

View File

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

View File

@@ -45,22 +45,32 @@ function handleNavigation() {
function setupWindowControls() { function setupWindowControls() {
const minimizeBtn = document.querySelector('.window-controls .minimize'); const minimizeBtn = document.querySelector('.window-controls .minimize');
const closeBtn = document.querySelector('.window-controls .close'); const closeBtn = document.querySelector('.window-controls .close');
const windowControls = document.querySelector('.window-controls'); const windowControls = document.querySelector('.window-controls');
const header = document.querySelector('.header'); const header = document.querySelector('.header');
const profileSelector = document.querySelector('.profile-selector');
if (profileSelector) {
profileSelector.style.pointerEvents = 'auto';
profileSelector.style.zIndex = '10000';
}
if (windowControls) { if (windowControls) {
windowControls.style.pointerEvents = 'auto'; windowControls.style.pointerEvents = 'auto';
windowControls.style.zIndex = '10000'; windowControls.style.zIndex = '10000';
} }
if (header) { if (header) {
header.style.webkitAppRegion = 'drag'; header.style.webkitAppRegion = 'drag';
if (windowControls) { if (windowControls) {
windowControls.style.webkitAppRegion = 'no-drag'; windowControls.style.webkitAppRegion = 'no-drag';
} }
if (profileSelector) {
profileSelector.style.webkitAppRegion = 'no-drag';
}
} }
if (window.electronAPI) { if (window.electronAPI) {
if (minimizeBtn) { if (minimizeBtn) {
minimizeBtn.onclick = (e) => { minimizeBtn.onclick = (e) => {
@@ -82,7 +92,7 @@ function showLauncherOrInstall(isInstalled) {
const install = document.getElementById('install-page'); const install = document.getElementById('install-page');
const sidebar = document.querySelector('.sidebar'); const sidebar = document.querySelector('.sidebar');
const gameTitle = document.querySelector('.game-title-section'); const gameTitle = document.querySelector('.game-title-section');
if (isInstalled) { if (isInstalled) {
if (launcher) launcher.style.display = ''; if (launcher) launcher.style.display = '';
if (install) install.style.display = 'none'; if (install) install.style.display = 'none';
@@ -137,14 +147,14 @@ function updateProgress(data) {
if (data.message && progressText) { if (data.message && progressText) {
progressText.textContent = data.message; progressText.textContent = data.message;
} }
if (data.percent !== null && data.percent !== undefined) { if (data.percent !== null && data.percent !== undefined) {
const percent = Math.min(100, Math.max(0, Math.round(data.percent))); const percent = Math.min(100, Math.max(0, Math.round(data.percent)));
if (progressPercent) progressPercent.textContent = `${percent}%`; if (progressPercent) progressPercent.textContent = `${percent}%`;
if (progressBarFill) progressBarFill.style.width = `${percent}%`; if (progressBarFill) progressBarFill.style.width = `${percent}%`;
if (progressBar) progressBar.style.width = `${percent}%`; if (progressBar) progressBar.style.width = `${percent}%`;
} }
if (data.speed && data.downloaded && data.total) { if (data.speed && data.downloaded && data.total) {
const speedMB = (data.speed / 1024 / 1024).toFixed(2); const speedMB = (data.speed / 1024 / 1024).toFixed(2);
const downloadedMB = (data.downloaded / 1024 / 1024).toFixed(2); const downloadedMB = (data.downloaded / 1024 / 1024).toFixed(2);
@@ -157,13 +167,13 @@ function updateProgress(data) {
function setupAnimations() { function setupAnimations() {
document.body.style.opacity = '0'; document.body.style.opacity = '0';
document.body.style.transform = 'translateY(20px)'; document.body.style.transform = 'translateY(20px)';
setTimeout(() => { setTimeout(() => {
document.body.style.transition = 'all 0.6s ease'; document.body.style.transition = 'all 0.6s ease';
document.body.style.opacity = '1'; document.body.style.opacity = '1';
document.body.style.transform = 'translateY(0)'; document.body.style.transform = 'translateY(0)';
}, 100); }, 100);
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `
@keyframes fadeInUp { @keyframes fadeInUp {
@@ -182,7 +192,7 @@ function setupAnimations() {
function setupFirstLaunchHandlers() { function setupFirstLaunchHandlers() {
console.log('Setting up first launch handlers...'); console.log('Setting up first launch handlers...');
window.electronAPI.onFirstLaunchUpdate((data) => { window.electronAPI.onFirstLaunchUpdate((data) => {
console.log('Received first launch update event:', data); console.log('Received first launch update event:', data);
showFirstLaunchUpdateDialog(data); showFirstLaunchUpdateDialog(data);
@@ -195,12 +205,12 @@ function setupFirstLaunchHandlers() {
showProgress(); showProgress();
updateProgress(data); updateProgress(data);
}); });
let lockButtonTimeout = null; let lockButtonTimeout = null;
window.electronAPI.onLockPlayButton((locked) => { window.electronAPI.onLockPlayButton((locked) => {
lockPlayButton(locked); lockPlayButton(locked);
if (locked) { if (locked) {
if (lockButtonTimeout) { if (lockButtonTimeout) {
clearTimeout(lockButtonTimeout); clearTimeout(lockButtonTimeout);
@@ -221,12 +231,12 @@ function setupFirstLaunchHandlers() {
function showFirstLaunchUpdateDialog(data) { function showFirstLaunchUpdateDialog(data) {
console.log('Creating first launch modal...'); console.log('Creating first launch modal...');
const existingModal = document.querySelector('.first-launch-modal-overlay'); const existingModal = document.querySelector('.first-launch-modal-overlay');
if (existingModal) { if (existingModal) {
existingModal.remove(); existingModal.remove();
} }
const modalOverlay = document.createElement('div'); const modalOverlay = document.createElement('div');
modalOverlay.className = 'first-launch-modal-overlay'; modalOverlay.className = 'first-launch-modal-overlay';
modalOverlay.style.cssText = ` modalOverlay.style.cssText = `
@@ -243,7 +253,7 @@ function showFirstLaunchUpdateDialog(data) {
justify-content: center !important; justify-content: center !important;
pointer-events: all !important; pointer-events: all !important;
`; `;
const modalDialog = document.createElement('div'); const modalDialog = document.createElement('div');
modalDialog.className = 'first-launch-modal-dialog'; modalDialog.className = 'first-launch-modal-dialog';
modalDialog.style.cssText = ` modalDialog.style.cssText = `
@@ -257,7 +267,7 @@ function showFirstLaunchUpdateDialog(data) {
overflow: hidden !important; overflow: hidden !important;
animation: modalSlideIn 0.3s ease-out !important; animation: modalSlideIn 0.3s ease-out !important;
`; `;
modalDialog.innerHTML = ` modalDialog.innerHTML = `
<div style="background: linear-gradient(135deg, rgba(147, 51, 234, 0.2), rgba(59, 130, 246, 0.2)); padding: 25px; border-bottom: 1px solid rgba(255,255,255,0.1);"> <div style="background: linear-gradient(135deg, rgba(147, 51, 234, 0.2), rgba(59, 130, 246, 0.2)); padding: 25px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<h2 style="margin: 0; color: #fff; font-size: 1.5rem; font-weight: 600; text-align: center;"> <h2 style="margin: 0; color: #fff; font-size: 1.5rem; font-weight: 600; text-align: center;">
@@ -306,9 +316,9 @@ function showFirstLaunchUpdateDialog(data) {
</button> </button>
</div> </div>
`; `;
modalOverlay.appendChild(modalDialog); modalOverlay.appendChild(modalDialog);
modalOverlay.onclick = (e) => { modalOverlay.onclick = (e) => {
if (e.target === modalOverlay) { if (e.target === modalOverlay) {
e.preventDefault(); e.preventDefault();
@@ -316,7 +326,7 @@ function showFirstLaunchUpdateDialog(data) {
return false; return false;
} }
}; };
document.addEventListener('keydown', function preventEscape(e) { document.addEventListener('keydown', function preventEscape(e) {
if (e.key === 'Escape') { if (e.key === 'Escape') {
e.preventDefault(); e.preventDefault();
@@ -324,33 +334,33 @@ function showFirstLaunchUpdateDialog(data) {
return false; return false;
} }
}); });
document.body.appendChild(modalOverlay); document.body.appendChild(modalOverlay);
const updateBtn = document.getElementById('updateGameBtn'); const updateBtn = document.getElementById('updateGameBtn');
updateBtn.onclick = () => { updateBtn.onclick = () => {
acceptFirstLaunchUpdate(); acceptFirstLaunchUpdate();
}; };
window.firstLaunchExistingGame = data.existingGame; window.firstLaunchExistingGame = data.existingGame;
console.log('First launch modal created and displayed'); console.log('First launch modal created and displayed');
} }
function lockPlayButton(locked) { function lockPlayButton(locked) {
const playButton = document.getElementById('homePlayBtn'); const playButton = document.getElementById('homePlayBtn');
if (!playButton) { if (!playButton) {
console.warn('Play button not found'); console.warn('Play button not found');
return; return;
} }
if (locked) { if (locked) {
playButton.style.opacity = '0.5'; playButton.style.opacity = '0.5';
playButton.style.pointerEvents = 'none'; playButton.style.pointerEvents = 'none';
playButton.style.cursor = 'not-allowed'; playButton.style.cursor = 'not-allowed';
playButton.setAttribute('data-locked', 'true'); playButton.setAttribute('data-locked', 'true');
const spanElement = playButton.querySelector('span'); const spanElement = playButton.querySelector('span');
if (spanElement) { if (spanElement) {
if (!playButton.getAttribute('data-original-text')) { if (!playButton.getAttribute('data-original-text')) {
@@ -358,21 +368,21 @@ function lockPlayButton(locked) {
} }
spanElement.textContent = 'CHECKING...'; spanElement.textContent = 'CHECKING...';
} }
console.log('Play button locked'); console.log('Play button locked');
} else { } else {
playButton.style.opacity = ''; playButton.style.opacity = '';
playButton.style.pointerEvents = ''; playButton.style.pointerEvents = '';
playButton.style.cursor = ''; playButton.style.cursor = '';
playButton.removeAttribute('data-locked'); playButton.removeAttribute('data-locked');
const spanElement = playButton.querySelector('span'); const spanElement = playButton.querySelector('span');
const originalText = playButton.getAttribute('data-original-text'); const originalText = playButton.getAttribute('data-original-text');
if (spanElement && originalText) { if (spanElement && originalText) {
spanElement.textContent = originalText; spanElement.textContent = originalText;
playButton.removeAttribute('data-original-text'); playButton.removeAttribute('data-original-text');
} }
console.log('Play button unlocked'); console.log('Play button unlocked');
} }
} }
@@ -381,12 +391,12 @@ function lockPlayButton(locked) {
async function acceptFirstLaunchUpdate() { async function acceptFirstLaunchUpdate() {
const existingGame = window.firstLaunchExistingGame; const existingGame = window.firstLaunchExistingGame;
if (!existingGame) { if (!existingGame) {
showNotification('Error: Game data not found', 'error'); showNotification('Error: Game data not found', 'error');
return; return;
} }
const modal = document.querySelector('.first-launch-modal-overlay'); const modal = document.querySelector('.first-launch-modal-overlay');
if (modal) { if (modal) {
modal.style.pointerEvents = 'none'; modal.style.pointerEvents = 'none';
@@ -397,21 +407,21 @@ async function acceptFirstLaunchUpdate() {
btn.textContent = '🔄 Updating...'; btn.textContent = '🔄 Updating...';
} }
} }
try { try {
showProgress(); showProgress();
updateProgress({ message: 'Starting mandatory game update...', percent: 0 }); updateProgress({ message: 'Starting mandatory game update...', percent: 0 });
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame); const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched(); window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
if (modal) { if (modal) {
modal.remove(); modal.remove();
} }
lockPlayButton(false); lockPlayButton(false);
if (result.success) { if (result.success) {
hideProgress(); hideProgress();
showNotification('Game updated successfully! 🎉', 'success'); showNotification('Game updated successfully! 🎉', 'success');
@@ -434,7 +444,7 @@ function dismissFirstLaunchDialog() {
if (modal) { if (modal) {
modal.remove(); modal.remove();
} }
lockPlayButton(false); lockPlayButton(false);
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched(); window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
} }
@@ -443,13 +453,13 @@ function showNotification(message, type = 'info') {
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.className = `notification notification-${type}`; notification.className = `notification notification-${type}`;
notification.textContent = message; notification.textContent = message;
document.body.appendChild(notification); document.body.appendChild(notification);
setTimeout(() => { setTimeout(() => {
notification.classList.add('show'); notification.classList.add('show');
}, 100); }, 100);
setTimeout(() => { setTimeout(() => {
notification.remove(); notification.remove();
}, 5000); }, 5000);
@@ -463,9 +473,9 @@ function setupUI() {
progressPercent = document.getElementById('progressPercent'); progressPercent = document.getElementById('progressPercent');
progressSpeed = document.getElementById('progressSpeed'); progressSpeed = document.getElementById('progressSpeed');
progressSize = document.getElementById('progressSize'); progressSize = document.getElementById('progressSize');
lockPlayButton(true); lockPlayButton(true);
setTimeout(() => { setTimeout(() => {
const playButton = document.getElementById('homePlayBtn'); const playButton = document.getElementById('homePlayBtn');
if (playButton && playButton.getAttribute('data-locked') === 'true') { if (playButton && playButton.getAttribute('data-locked') === 'true') {
@@ -476,13 +486,13 @@ function setupUI() {
} }
} }
}, 25000); }, 25000);
handleNavigation(); handleNavigation();
setupWindowControls(); setupWindowControls();
setupSidebarLogo(); setupSidebarLogo();
setupAnimations(); setupAnimations();
setupFirstLaunchHandlers(); setupFirstLaunchHandlers();
document.body.focus(); document.body.focus();
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,18 +1,19 @@
# 🎮 Hytale F2P Launcher | Multiplayer Support # 🎮 Hytale F2P Launcher | Multiplayer Support [Windows, MacOS, Linux]
<div align="center"> <div align="center">
![Version](https://img.shields.io/badge/Version-2.0.1-green?style=for-the-badge) ![Version](https://img.shields.io/badge/Version-2.0.2-green?style=for-the-badge)
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=for-the-badge) ![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge) ![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (windows users & non-premium only)** **A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (all OS supported)**
[![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers) [![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members) [![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members)
**If you find this project useful, please give it a star!** **If you find this project useful, please give it a star!**
🛑 **Found a problem? Open an issue! Im on Windows, so I cant test on macOS or Linux.** 🛑
🛑 **Found a problem? Join the Discord: https://discord.gg/gME8rUy3MB** 🛑
</div> </div>
@@ -36,7 +37,7 @@
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates - 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
- 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS - 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS
-**Java Management** - Automatic Java runtime detection and installation -**Java Management** - Automatic Java runtime detection and installation
- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows) - 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows, macOS & Linux !)
🛡️ **Advanced Features** 🛡️ **Advanced Features**
- 📁 **Custom Installation** - Choose your own installation directory - 📁 **Custom Installation** - Choose your own installation directory
@@ -63,7 +64,7 @@ See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https
#### macOS #### macOS
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section. See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
#### 🖥️ How to create server (Windows Only)? #### 🖥️ How to play online on F2P?
See [SERVER.md](SERVER.md) See [SERVER.md](SERVER.md)
@@ -77,7 +78,15 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
## 📋 Changelog ## 📋 Changelog
### 🆕 v2.0.1 *(Latest)* ### 🆕 v2.0.2 *(Latest)*
- 🎮 **Discord RPC Integration** - Added Discord Rich Presence with toggle in settings (enabled by default)
- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS
- 🎨 **Chat Improvements** - Simplified chat color system
- 🏆 **Badge System Expansion** - Added new FOUNDER UUID to the badge system
- 🔧 **Progress Bar Fix** - Resolved issue where download progress bar stayed active after game launch
- 🐛 **Bug Fixes**: General fixes
### 🔄 v2.0.1
- 📊 **Advanced Logging System** - Complete logging with timestamps, file rotation, and session tracking - 📊 **Advanced Logging System** - Complete logging with timestamps, file rotation, and session tracking
- 🔧 **Play Button Fix** - Resolved issue where play button could get stuck in "CHECKING..." state - 🔧 **Play Button Fix** - Resolved issue where play button could get stuck in "CHECKING..." state
- 💬 **Discord Integration** - Added closable Discord notification for community engagement - 💬 **Discord Integration** - Added closable Discord notification for community engagement
@@ -129,14 +138,16 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
### 🏆 Project Creator ### 🏆 Project Creator
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator* - [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
### 🌟 Contributors ### 🌟 Contributors
- [**@sanasol**](https://github.com/sanasol) - *Main Issues fixer | Multiplayer Patcher*
- [**@Terromur**](https://github.com/Terromur) - *Main Issues fixer | Beta tester*
- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester*
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer* - [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
- [**@crimera**](https://github.com/crimera) - *Issues fixer* - [**@crimera**](https://github.com/crimera) - *Issues fixer*
- [**@sanasol**](https://github.com/sanasol) - *Issues fixer*
- [**@Terromur**](https://github.com/Terromur) - *Issues fixer*
- [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer* - [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer*
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
--- ---
@@ -156,10 +167,7 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
<div align="center"> <div align="center">
[![GitHub Issues](https://img.shields.io/badge/GitHub-Issues-red?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/issues) **Need help?** Join us: https://discord.gg/gME8rUy3MB
[![Discussions](https://img.shields.io/badge/GitHub-Discussions-blue?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/discussions)
**Need help?** Open an [issue](https://github.com/amiayweb/Hytale-F2P/issues) or start a [discussion](https://github.com/amiayweb/Hytale-F2P/discussions)!
</div> </div>

473
SERVER.md
View File

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

View File

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

View File

@@ -77,32 +77,32 @@ function findClientPath(gameLatest) {
function findUserDataPath(gameLatest) { function findUserDataPath(gameLatest) {
const candidates = []; const candidates = [];
candidates.push(path.join(gameLatest, 'Client', 'UserData')); candidates.push(path.join(gameLatest, 'Client', 'UserData'));
candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'UserData')); candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'UserData'));
candidates.push(path.join(gameLatest, 'Hytale.app', 'Contents', 'UserData')); candidates.push(path.join(gameLatest, 'Hytale.app', 'Contents', 'UserData'));
candidates.push(path.join(gameLatest, 'UserData')); candidates.push(path.join(gameLatest, 'UserData'));
candidates.push(path.join(gameLatest, 'Client', 'UserData')); candidates.push(path.join(gameLatest, 'Client', 'UserData'));
for (const candidate of candidates) { for (const candidate of candidates) {
if (fs.existsSync(candidate)) { if (fs.existsSync(candidate)) {
return candidate; return candidate;
} }
} }
let defaultPath; let defaultPath;
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
defaultPath = path.join(gameLatest, 'Client', 'UserData'); defaultPath = path.join(gameLatest, 'Client', 'UserData');
} else { } else {
defaultPath = path.join(gameLatest, 'Client', 'UserData'); defaultPath = path.join(gameLatest, 'Client', 'UserData');
} }
if (!fs.existsSync(defaultPath)) { if (!fs.existsSync(defaultPath)) {
fs.mkdirSync(defaultPath, { recursive: true }); fs.mkdirSync(defaultPath, { recursive: true });
} }
return defaultPath; return defaultPath;
} }
@@ -110,15 +110,15 @@ function findUserDataRecursive(gameLatest) {
function searchDirectory(dir) { function searchDirectory(dir) {
try { try {
const items = fs.readdirSync(dir, { withFileTypes: true }); const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) { for (const item of items) {
if (item.isDirectory()) { if (item.isDirectory()) {
const fullPath = path.join(dir, item.name); const fullPath = path.join(dir, item.name);
if (item.name === 'UserData') { if (item.name === 'UserData') {
return fullPath; return fullPath;
} }
const found = searchDirectory(fullPath); const found = searchDirectory(fullPath);
if (found) { if (found) {
return found; return found;
@@ -127,14 +127,14 @@ function findUserDataRecursive(gameLatest) {
} }
} catch (error) { } catch (error) {
} }
return null; return null;
} }
if (!fs.existsSync(gameLatest)) { if (!fs.existsSync(gameLatest)) {
return null; return null;
} }
const found = searchDirectory(gameLatest); const found = searchDirectory(gameLatest);
return found; return found;
} }
@@ -152,16 +152,14 @@ async function getModsPath(customInstallPath = null) {
} }
if (!installPath) { if (!installPath) {
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); // Use the standard app directory logic which handles platforms correctly
installPath = path.join(localAppData, 'HytaleF2P'); installPath = getAppDir();
} else {
installPath = path.join(installPath, 'HytaleF2P');
} }
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest'); const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
const userDataPath = findUserDataPath(gameLatest); const userDataPath = findUserDataPath(gameLatest);
const modsPath = path.join(userDataPath, 'Mods'); const modsPath = path.join(userDataPath, 'Mods');
const disabledModsPath = path.join(userDataPath, 'DisabledMods'); const disabledModsPath = path.join(userDataPath, 'DisabledMods');

View File

@@ -7,16 +7,27 @@ const {
loadUsername, loadUsername,
saveChatUsername, saveChatUsername,
loadChatUsername, loadChatUsername,
saveChatColor,
loadChatColor,
saveJavaPath, saveJavaPath,
loadJavaPath, loadJavaPath,
saveInstallPath, saveInstallPath,
loadInstallPath, loadInstallPath,
saveDiscordRPC,
loadDiscordRPC,
saveModsToConfig, saveModsToConfig,
loadModsFromConfig, loadModsFromConfig,
getUuidForUser, getUuidForUser,
isFirstLaunch, isFirstLaunch,
markAsLaunched, markAsLaunched,
CONFIG_FILE CONFIG_FILE,
// UUID Management
getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid
} = require('./core/config'); } = require('./core/config');
const { getResolvedAppDir, getModsPath } = require('./core/paths'); const { getResolvedAppDir, getModsPath } = require('./core/paths');
@@ -37,8 +48,6 @@ const {
const { getJavaDetection } = require('./managers/javaManager'); const { getJavaDetection } = require('./managers/javaManager');
const { checkAndInstallMultiClient } = require('./managers/multiClientManager');
const { const {
downloadAndReplaceHomePageUI, downloadAndReplaceHomePageUI,
findHomePageUIPath, findHomePageUIPath,
@@ -85,6 +94,8 @@ module.exports = {
loadUsername, loadUsername,
saveChatUsername, saveChatUsername,
loadChatUsername, loadChatUsername,
saveChatColor,
loadChatColor,
getUuidForUser, getUuidForUser,
// Java configuration functions // Java configuration functions
@@ -96,6 +107,10 @@ module.exports = {
saveInstallPath, saveInstallPath,
loadInstallPath, loadInstallPath,
// Discord RPC functions
saveDiscordRPC,
loadDiscordRPC,
// Version functions // Version functions
getInstalledClientVersion, getInstalledClientVersion,
getLatestClientVersion, getLatestClientVersion,
@@ -106,8 +121,13 @@ module.exports = {
// Player ID functions // Player ID functions
getOrCreatePlayerId, getOrCreatePlayerId,
// Multi-client functions // UUID Management functions
checkAndInstallMultiClient, getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid,
// Mod management functions // Mod management functions
getModsPath, getModsPath,

View File

@@ -11,6 +11,7 @@ const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServ
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager'); const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager'); const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager'); const { updateGameFiles } = require('./gameManager');
const { syncModsForCurrentProfile } = require('./modManager');
// Client patcher for custom auth server (sanasol.ws) // Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null; let clientPatcher = null;
@@ -130,7 +131,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
} }
} else { } else {
javaBin = getJavaExec(customJreDir); javaBin = getJavaExec(customJreDir);
if (!getBundledJavaPath(customJreDir)) { if (!getBundledJavaPath(customJreDir)) {
const fallback = await detectSystemJava(); const fallback = await detectSystemJava();
if (fallback) { if (fallback) {
@@ -192,9 +193,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
const serverDir = path.join(gameLatest, 'Server'); const serverDir = path.join(gameLatest, 'Server');
const signPath = async (targetPath, deep = false) => { const signPath = async (targetPath, deep = false) => {
await execAsync(`xattr -cr "${targetPath}"`).catch(() => {}); await execAsync(`xattr -cr "${targetPath}"`).catch(() => { });
const deepFlag = deep ? '--deep ' : ''; const deepFlag = deep ? '--deep ' : '';
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => {}); await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => { });
}; };
if (fs.existsSync(appBundle)) { if (fs.existsSync(appBundle)) {
@@ -216,8 +217,8 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
} }
if (fs.existsSync(serverDir)) { if (fs.existsSync(serverDir)) {
await execAsync(`xattr -cr "${serverDir}"`).catch(() => {}); await execAsync(`xattr -cr "${serverDir}"`).catch(() => { });
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => {}); await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => { });
console.log('Signed server binaries (after patching)'); console.log('Signed server binaries (after patching)');
} }
@@ -260,11 +261,23 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
if (progressCallback) { if (progressCallback) {
progressCallback('Starting game...', null, null, null, null); progressCallback('Starting game...', null, null, null, null);
} }
// Ensure mods are synced for the active profile before launching
try {
console.log('Syncing mods for active profile before launch...');
if (progressCallback) progressCallback('Syncing mods...', null, null, null, null);
await syncModsForCurrentProfile();
} catch (syncError) {
console.error('Failed to sync mods before launch:', syncError);
// Continue anyway? Or fail?
// Warn user but continue might be safer to avoid blocking play if sync is just glitchy
}
console.log('Starting game...'); console.log('Starting game...');
console.log(`Command: "${clientPath}" ${args.join(' ')}`); console.log(`Command: "${clientPath}" ${args.join(' ')}`);
const env = { ...process.env }; const env = { ...process.env };
const waylandEnv = setupWaylandEnvironment(); const waylandEnv = setupWaylandEnvironment();
Object.assign(env, waylandEnv); Object.assign(env, waylandEnv);
@@ -276,8 +289,8 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}; };
if (process.platform === 'win32') { if (process.platform === 'win32') {
spawnOptions.shell = false; spawnOptions.shell = false;
spawnOptions.windowsHide = true; spawnOptions.windowsHide = true;
} }
const child = spawn(clientPath, args, spawnOptions); const child = spawn(clientPath, args, spawnOptions);
@@ -371,12 +384,12 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
try { try {
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir); await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir);
console.log('Game updated successfully, waiting before launch...'); console.log('Game updated successfully, waiting before launch...');
if (progressCallback) { if (progressCallback) {
progressCallback('Preparing game launch...', 90, null, null, null); progressCallback('Preparing game launch...', 90, null, null, null);
} }
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise(resolve => setTimeout(resolve, 3000));
} catch (updateError) { } catch (updateError) {
console.error('Update failed:', updateError); console.error('Update failed:', updateError);
if (progressCallback) { if (progressCallback) {
@@ -403,4 +416,4 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
module.exports = { module.exports = {
launchGame, launchGame,
launchGameWithVersionCheck launchGameWithVersionCheck
}; };

View File

@@ -6,7 +6,6 @@ const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile } = require('../utils/fileManager'); const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager'); const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
const { installButler } = require('./butlerManager'); const { installButler } = require('./butlerManager');
const { checkAndInstallMultiClient } = require('./multiClientManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config'); const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config');
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager'); const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
@@ -165,9 +164,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
fs.renameSync(tempUpdateDir, gameDir); fs.renameSync(tempUpdateDir, gameDir);
const multiResult = await checkAndInstallMultiClient(gameDir, progressCallback);
console.log('Multiplayer-client check result after update:', multiResult);
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback); const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
console.log('HomePage.ui update result after update:', homeUIResult); console.log('HomePage.ui update result after update:', homeUIResult);
@@ -318,9 +314,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir); const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir); await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
const multiResult = await checkAndInstallMultiClient(customGameDir, progressCallback);
console.log('Multiplayer check result:', multiResult);
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback); const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
console.log('HomePage.ui update result after installation:', homeUIResult); console.log('HomePage.ui update result after installation:', homeUIResult);
@@ -334,8 +327,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
return { return {
success: true, success: true,
installed: true, installed: true
multiClient: multiResult
}; };
} }

View File

@@ -4,6 +4,7 @@ const crypto = require('crypto');
const axios = require('axios'); const axios = require('axios');
const { getModsPath } = require('../core/paths'); const { getModsPath } = require('../core/paths');
const { saveModsToConfig, loadModsFromConfig } = require('../core/config'); const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
const profileManager = require('./profileManager');
function generateModId(filename) { function generateModId(filename) {
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8); return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
@@ -11,13 +12,13 @@ function generateModId(filename) {
function extractModName(filename) { function extractModName(filename) {
let name = path.parse(filename).name; let name = path.parse(filename).name;
name = name.replace(/-v?\d+\.[\d\.]+.*$/i, ''); name = name.replace(/-v?\d+\.[\d\.]+.*$/i, '');
name = name.replace(/-\d+\.[\d\.]+.*$/i, ''); name = name.replace(/-\d+\.[\d\.]+.*$/i, '');
name = name.replace(/[-_]/g, ' '); name = name.replace(/[-_]/g, ' ');
name = name.replace(/\b\w/g, l => l.toUpperCase()); name = name.replace(/\b\w/g, l => l.toUpperCase());
return name || 'Unknown Mod'; return name || 'Unknown Mod';
} }
@@ -26,77 +27,53 @@ function extractVersion(filename) {
return versionMatch ? versionMatch[1] : null; return versionMatch ? versionMatch[1] : null;
} }
// Helper to get mods from active profile
function getProfileMods() {
const profile = profileManager.getActiveProfile();
return profile ? (profile.mods || []) : [];
}
async function loadInstalledMods(modsPath) { async function loadInstalledMods(modsPath) {
try { try {
const configMods = loadModsFromConfig(); const activeProfile = profileManager.getActiveProfile();
const modsMap = new Map(); if (!activeProfile) return [];
configMods.forEach(mod => { const profileMods = activeProfile.mods || [];
modsMap.set(mod.fileName, mod); const profileModFiles = new Set(profileMods.map(m => m.fileName));
});
// We only return mods that are explicitly in the profile
if (fs.existsSync(modsPath)) { // Check which ones are physically present (either in mods/ or DisabledMods/)
const files = fs.readdirSync(modsPath);
const physicalModsPath = modsPath; // .../mods
for (const file of files) {
const filePath = path.join(modsPath, file);
const stats = fs.statSync(filePath);
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
const configMod = modsMap.get(file);
const modInfo = {
id: configMod?.id || generateModId(file),
name: configMod?.name || extractModName(file),
version: configMod?.version || extractVersion(file) || '1.0.0',
description: configMod?.description || 'Installed mod',
author: configMod?.author || 'Unknown',
enabled: true,
filePath: filePath,
fileName: file,
fileSize: configMod?.fileSize || stats.size,
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
curseForgeId: configMod?.curseForgeId,
curseForgeFileId: configMod?.curseForgeFileId
};
modsMap.set(file, modInfo);
}
}
}
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (fs.existsSync(disabledModsPath)) {
const files = fs.readdirSync(disabledModsPath); const validMods = [];
for (const file of files) { for (const modConfig of profileMods) {
const filePath = path.join(disabledModsPath, file); // Check if file exists in either location
const stats = fs.statSync(filePath); const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName));
const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName));
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
const configMod = modsMap.get(file); if (inEnabled || inDisabled) {
validMods.push({
const modInfo = { ...modConfig,
id: configMod?.id || generateModId(file), // Set filePath based on physical location
name: configMod?.name || extractModName(file), filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName),
version: configMod?.version || extractVersion(file) || '1.0.0', enabled: modConfig.enabled !== false // Default true
description: configMod?.description || 'Disabled mod', });
author: configMod?.author || 'Unknown', } else {
enabled: false, console.warn(`[ModManager] Mod ${modConfig.fileName} listed in profile but not found on disk.`);
filePath: filePath, // Include it so user can see it's missing or remove it
fileName: file, validMods.push({
fileSize: configMod?.fileSize || stats.size, ...modConfig,
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime, filePath: null,
curseForgeId: configMod?.curseForgeId, missing: true,
curseForgeFileId: configMod?.curseForgeFileId enabled: modConfig.enabled !== false
}; });
modsMap.set(file, modInfo);
}
} }
} }
return Array.from(modsMap.values()); return validMods;
} catch (error) { } catch (error) {
console.error('Error loading installed mods:', error); console.error('Error loading installed mods:', error);
return []; return [];
@@ -106,13 +83,13 @@ async function loadInstalledMods(modsPath) {
async function downloadMod(modInfo) { async function downloadMod(modInfo) {
try { try {
const modsPath = await getModsPath(); const modsPath = await getModsPath();
if (!modInfo.downloadUrl && !modInfo.fileId) { if (!modInfo.downloadUrl && !modInfo.fileId) {
throw new Error('No download URL or file ID provided'); throw new Error('No download URL or file ID provided');
} }
let downloadUrl = modInfo.downloadUrl; let downloadUrl = modInfo.downloadUrl;
if (!downloadUrl && modInfo.fileId && modInfo.modId) { if (!downloadUrl && modInfo.fileId && modInfo.modId) {
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, { const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
headers: { headers: {
@@ -120,56 +97,61 @@ async function downloadMod(modInfo) {
'Accept': 'application/json' 'Accept': 'application/json'
} }
}); });
downloadUrl = response.data.data.downloadUrl; downloadUrl = response.data.data.downloadUrl;
} }
if (!downloadUrl) { if (!downloadUrl) {
throw new Error('Could not determine download URL'); throw new Error('Could not determine download URL');
} }
const fileName = modInfo.fileName || `mod-${modInfo.modId}.jar`; const fileName = modInfo.fileName || `mod-${modInfo.modId}.jar`;
const filePath = path.join(modsPath, fileName); const filePath = path.join(modsPath, fileName);
const response = await axios({ const response = await axios({
method: 'get', method: 'get',
url: downloadUrl, url: downloadUrl,
responseType: 'stream' responseType: 'stream'
}); });
const writer = fs.createWriteStream(filePath); const writer = fs.createWriteStream(filePath);
response.data.pipe(writer); response.data.pipe(writer);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
writer.on('finish', () => { writer.on('finish', () => {
const configMods = loadModsFromConfig(); // NEW: Update Active Profile instead of global config
const newMod = { const activeProfile = profileManager.getActiveProfile();
id: modInfo.id || generateModId(fileName), if (activeProfile) {
name: modInfo.name || extractModName(fileName), const newMod = {
version: modInfo.version || '1.0.0', id: modInfo.id || generateModId(fileName),
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge', name: modInfo.name || extractModName(fileName),
author: modInfo.author || 'Unknown', version: modInfo.version || '1.0.0',
enabled: true, description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
fileName: fileName, author: modInfo.author || 'Unknown',
fileSize: fs.statSync(filePath).size, enabled: true,
dateInstalled: new Date().toISOString(), fileName: fileName,
curseForgeId: modInfo.modId, fileSize: fs.statSync(filePath).size,
curseForgeFileId: modInfo.fileId dateInstalled: new Date().toISOString(),
}; curseForgeId: modInfo.modId,
curseForgeFileId: modInfo.fileId
configMods.push(newMod); };
saveModsToConfig(configMods);
const updatedMods = [...(activeProfile.mods || []), newMod];
resolve({ profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
success: true,
filePath: filePath, resolve({
fileName: fileName, success: true,
modInfo: newMod filePath: filePath,
}); fileName: fileName,
modInfo: newMod
});
} else {
reject(new Error('No active profile to save mod to'));
}
}); });
writer.on('error', reject); writer.on('error', reject);
}); });
} catch (error) { } catch (error) {
console.error('Error downloading mod:', error); console.error('Error downloading mod:', error);
return { return {
@@ -181,36 +163,39 @@ async function downloadMod(modInfo) {
async function uninstallMod(modId, modsPath) { async function uninstallMod(modId, modsPath) {
try { try {
const configMods = loadModsFromConfig(); const activeProfile = profileManager.getActiveProfile();
const mod = configMods.find(m => m.id === modId); if (!activeProfile) throw new Error('No active profile');
const profileMods = activeProfile.mods || [];
const mod = profileMods.find(m => m.id === modId);
if (!mod) { if (!mod) {
throw new Error('Mod not found in config'); throw new Error('Mod not found in profile');
} }
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
const enabledPath = path.join(modsPath, mod.fileName); const enabledPath = path.join(modsPath, mod.fileName);
const disabledPath = path.join(disabledModsPath, mod.fileName); const disabledPath = path.join(disabledModsPath, mod.fileName);
let fileRemoved = false; let fileRemoved = false;
// Try to remove file from both locations to be safe
if (fs.existsSync(enabledPath)) { if (fs.existsSync(enabledPath)) {
fs.unlinkSync(enabledPath); fs.unlinkSync(enabledPath);
fileRemoved = true; fileRemoved = true;
console.log('Removed mod from Mods folder:', enabledPath);
} else if (fs.existsSync(disabledPath)) {
fs.unlinkSync(disabledPath);
fileRemoved = true;
console.log('Removed mod from DisabledMods folder:', disabledPath);
} }
if (fs.existsSync(disabledPath)) {
try { fs.unlinkSync(disabledPath); fileRemoved = true; } catch (e) { }
}
if (!fileRemoved) { if (!fileRemoved) {
console.warn('Mod file not found on filesystem, removing from config anyway'); console.warn('Mod file not found on filesystem, removing from profile anyway');
} }
const updatedMods = configMods.filter(m => m.id !== modId); const updatedMods = profileMods.filter(m => m.id !== modId);
saveModsToConfig(updatedMods); profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
console.log('Mod removed from config.json');
console.log('Mod removed from profile');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error uninstalling mod:', error); console.error('Error uninstalling mod:', error);
@@ -223,38 +208,53 @@ async function uninstallMod(modId, modsPath) {
async function toggleMod(modId, modsPath) { async function toggleMod(modId, modsPath) {
try { try {
const mods = await loadInstalledMods(modsPath); const activeProfile = profileManager.getActiveProfile();
const mod = mods.find(m => m.id === modId); if (!activeProfile) throw new Error('No active profile');
if (!mod) { const profileMods = activeProfile.mods || [];
throw new Error('Mod not found'); const modIndex = profileMods.findIndex(m => m.id === modId);
if (modIndex === -1) {
throw new Error('Mod not found in profile');
} }
const mod = profileMods[modIndex];
const newEnabled = !mod.enabled; // Toggle
// Update Profile First
const updatedMods = [...profileMods];
updatedMods[modIndex] = { ...mod, enabled: newEnabled };
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
// Manually move the file to reflect the new state
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) { if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
fs.mkdirSync(disabledModsPath, { recursive: true });
}
const currentPath = mod.filePath; const currentPath = mod.enabled ? path.join(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
let newPath, newEnabled;
if (mod.enabled) { // Determine target paths
newPath = path.join(disabledModsPath, path.basename(currentPath));
newEnabled = false; const targetDir = newEnabled ? modsPath : disabledModsPath;
const targetPath = path.join(targetDir, mod.fileName);
if (fs.existsSync(currentPath)) {
fs.renameSync(currentPath, targetPath);
} else { } else {
newPath = path.join(modsPath, path.basename(currentPath)); // Fallback: check if it's already in target?
newEnabled = true;
if (fs.existsSync(targetPath)) {
// It's already there, maybe just state was wrong.
console.log(`[ModManager] Mod ${mod.fileName} is already in the correct state`);
} else {
// Try finding it
const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(modsPath, mod.fileName);
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
}
} }
fs.renameSync(currentPath, newPath);
const configMods = loadModsFromConfig();
const configModIndex = configMods.findIndex(m => m.id === modId);
if (configModIndex !== -1) {
configMods[configModIndex].enabled = newEnabled;
saveModsToConfig(configMods);
}
return { success: true, enabled: newEnabled }; return { success: true, enabled: newEnabled };
} catch (error) { } catch (error) {
console.error('Error toggling mod:', error); console.error('Error toggling mod:', error);
@@ -265,11 +265,69 @@ async function toggleMod(modId, modsPath) {
} }
} }
async function syncModsForCurrentProfile() {
try {
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) {
console.warn('No active profile found during mod sync');
return;
}
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`);
const modsPath = await getModsPath();
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
}
// Get all physical files from both folders
const enabledFiles = fs.existsSync(modsPath) ? fs.readdirSync(modsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
const disabledFiles = fs.existsSync(disabledModsPath) ? fs.readdirSync(disabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
// Profile.mods contains the list of ALL mods for that profile, with their enabled state.
const profileMods = activeProfile.mods || [];
for (const fileName of allFiles) {
const modConfig = profileMods.find(m => m.fileName === fileName);
const shouldBeEnabled = modConfig && modConfig.enabled !== false; // Default to true if in list, unless explicitly false
// Logic:
// If it should be enabled -> Move to mods/
// If it should be disabled -> Move to DisabledMods/
const currentPath = enabledFiles.includes(fileName) ? path.join(modsPath, fileName) : path.join(disabledModsPath, fileName);
const targetDir = shouldBeEnabled ? modsPath : disabledModsPath;
const targetPath = path.join(targetDir, fileName);
if (path.dirname(currentPath) !== targetDir) {
console.log(`[Mod Sync] Moving ${fileName} to ${shouldBeEnabled ? 'Enabled' : 'Disabled'}`);
try {
fs.renameSync(currentPath, targetPath);
} catch (err) {
console.error(`Failed to move ${fileName}: ${err.message}`);
}
}
}
return { success: true };
} catch (error) {
console.error('[ModManager] Error syncing mods:', error);
return { success: false, error: error.message };
}
}
module.exports = { module.exports = {
loadInstalledMods, loadInstalledMods,
downloadMod, downloadMod,
uninstallMod, uninstallMod,
toggleMod, toggleMod,
syncModsForCurrentProfile,
generateModId, generateModId,
extractModName, extractModName,
extractVersion extractVersion

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ const axios = require('axios');
async function getLatestClientVersion() { async function getLatestClientVersion() {
try { try {
console.log('Fetching latest client version from API...'); console.log('Fetching latest client version from API...');
const response = await axios.get('http://3.10.208.30:3002/api/version_client', { const response = await axios.get('https://files.hytalef2p.com/api/version_client', {
timeout: 5000, timeout: 5000,
headers: { headers: {
'User-Agent': 'Hytale-F2P-Launcher' 'User-Agent': 'Hytale-F2P-Launcher'
@@ -28,7 +28,7 @@ async function getLatestClientVersion() {
async function getInstalledClientVersion() { async function getInstalledClientVersion() {
try { try {
console.log('Fetching installed client version from API...'); console.log('Fetching installed client version from API...');
const response = await axios.get('http://3.10.208.30:3002/api/clientCheck', { const response = await axios.get('https://files.hytalef2p.com/api/clientCheck', {
timeout: 5000, timeout: 5000,
headers: { headers: {
'User-Agent': 'Hytale-F2P-Launcher' 'User-Agent': 'Hytale-F2P-Launcher'
@@ -50,33 +50,7 @@ async function getInstalledClientVersion() {
} }
} }
async function getMultiClientVersion() {
try {
console.log('Fetching Multiplayer version from API...');
const response = await axios.get('http://3.10.208.30:3002/api/multi', {
timeout: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.multi_version) {
const version = response.data.multi_version;
console.log(`Multiplayer version: ${version}`);
return version;
} else {
console.log('Warning: Invalid multi API response');
return null;
}
} catch (error) {
console.error('Error fetching Multiplayer version:', error.message);
console.log('Multiplayer not available');
return null;
}
}
module.exports = { module.exports = {
getLatestClientVersion, getLatestClientVersion,
getInstalledClientVersion, getInstalledClientVersion
getMultiClientVersion
}; };

View File

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

View File

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

View File

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

295
main.js
View File

@@ -1,9 +1,10 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, isGameInstalled, uninstallGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
const UpdateManager = require('./backend/updateManager'); const UpdateManager = require('./backend/updateManager');
const logger = require('./backend/logger'); const logger = require('./backend/logger');
const profileManager = require('./backend/managers/profileManager');
logger.interceptConsole(); logger.interceptConsole();
@@ -16,18 +17,25 @@ const DISCORD_CLIENT_ID = '1462244937868513373';
function initDiscordRPC() { function initDiscordRPC() {
try { try {
// Check if Discord RPC is enabled in settings
const rpcEnabled = loadDiscordRPC();
if (!rpcEnabled) {
console.log('Discord RPC disabled in settings');
return;
}
const { Client } = require('discord-rpc'); const { Client } = require('discord-rpc');
discordRPC = new Client({ transport: 'ipc' }); discordRPC = new Client({ transport: 'ipc' });
discordRPC.on('ready', () => { discordRPC.on('ready', () => {
console.log('Discord RPC connected'); console.log('Discord RPC connected');
setDiscordActivity(); setDiscordActivity();
}); });
discordRPC.on('disconnected', () => { discordRPC.on('disconnected', () => {
console.log('Discord RPC disconnected'); console.log('Discord RPC disconnected');
}); });
discordRPC.login({ clientId: DISCORD_CLIENT_ID }).catch(err => { discordRPC.login({ clientId: DISCORD_CLIENT_ID }).catch(err => {
console.log('Failed to connect to Discord:', err.message); console.log('Failed to connect to Discord:', err.message);
}); });
@@ -38,7 +46,7 @@ function initDiscordRPC() {
function setDiscordActivity() { function setDiscordActivity() {
if (!discordRPC) return; if (!discordRPC) return;
try { try {
discordRPC.setActivity({ discordRPC.setActivity({
details: 'Using HytaleF2P', details: 'Using HytaleF2P',
@@ -57,6 +65,26 @@ function setDiscordActivity() {
} }
} }
function toggleDiscordRPC(enabled) {
console.log('Toggling Discord RPC:', enabled);
if (enabled && !discordRPC) {
console.log('Initializing Discord RPC...');
initDiscordRPC();
} else if (!enabled && discordRPC) {
try {
console.log('Disconnecting Discord RPC...');
discordRPC.clearActivity();
discordRPC.destroy();
discordRPC = null;
console.log('Discord RPC disconnected successfully');
} catch (error) {
console.error('Error disconnecting Discord RPC:', error.message);
discordRPC = null; // Force null même en cas d'erreur
}
}
}
function createWindow() { function createWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1280, width: 1280,
@@ -69,13 +97,19 @@ function createWindow() {
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
devTools: true, devTools: false,
webSecurity: true webSecurity: true
} }
}); });
mainWindow.loadFile('GUI/index.html'); mainWindow.loadFile('GUI/index.html');
// Cleanup Discord RPC when window is closed
mainWindow.on('closed', () => {
console.log('Main window closed, cleaning up Discord RPC...');
cleanupDiscordRPC();
});
// Initialize Discord Rich Presence // Initialize Discord Rich Presence
initDiscordRPC(); initDiscordRPC();
@@ -86,10 +120,11 @@ function createWindow() {
mainWindow.webContents.send('show-update-popup', updateInfo); mainWindow.webContents.send('show-update-popup', updateInfo);
} }
}, 3000); }, 3000);
//mainWindow.webContents.openDevTools();
mainWindow.webContents.on('devtools-opened', () => { mainWindow.webContents.on('devtools-opened', () => {
mainWindow.webContents.closeDevTools(); mainWindow.webContents.closeDevTools();
}); });
mainWindow.webContents.on('before-input-event', (event, input) => { mainWindow.webContents.on('before-input-event', (event, input) => {
@@ -125,31 +160,36 @@ app.whenReady().then(async () => {
console.log('Electron version:', process.versions.electron); console.log('Electron version:', process.versions.electron);
console.log('Node.js version:', process.versions.node); console.log('Node.js version:', process.versions.node);
console.log('Log directory:', logger.getLogDirectory()); console.log('Log directory:', logger.getLogDirectory());
// Initialize Profile Manager (runs migration if needed)
profileManager.init();
createWindow(); createWindow();
setTimeout(async () => { setTimeout(async () => {
let timeoutReached = false; let timeoutReached = false;
const unlockPlayButton = () => { const unlockPlayButton = () => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('lock-play-button', false); mainWindow.webContents.send('lock-play-button', false);
} }
}; };
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
timeoutReached = true; timeoutReached = true;
console.warn('First launch check timeout reached, unlocking play button'); console.warn('First launch check timeout reached, unlocking play button');
unlockPlayButton(); unlockPlayButton();
}, 15000); }, 15000);
try { try {
console.log('Starting first launch check...'); console.log('Starting first launch check...');
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('lock-play-button', true); mainWindow.webContents.send('lock-play-button', true);
} }
const progressCallback = (message, percent, speed, downloaded, total) => { const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total }); mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total });
@@ -162,20 +202,20 @@ app.whenReady().then(async () => {
setTimeout(() => reject(new Error('First launch check timeout')), 12000); setTimeout(() => reject(new Error('First launch check timeout')), 12000);
}) })
]); ]);
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (timeoutReached) { if (timeoutReached) {
console.log('Timeout already reached, skipping result processing'); console.log('Timeout already reached, skipping result processing');
return; return;
} }
console.log('First launch check result:', firstLaunchResult); console.log('First launch check result:', firstLaunchResult);
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
if (firstLaunchResult.needsUpdate && firstLaunchResult.existingGame) { if (firstLaunchResult.needsUpdate && firstLaunchResult.existingGame) {
console.log('Sending show-first-launch-update event...'); console.log('Sending show-first-launch-update event...');
setTimeout(() => { setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('show-first-launch-update', { mainWindow.webContents.send('show-first-launch-update', {
@@ -184,10 +224,10 @@ app.whenReady().then(async () => {
}); });
} }
}, 1000); }, 1000);
} else if (firstLaunchResult.isFirstLaunch && !firstLaunchResult.existingGame) { } else if (firstLaunchResult.isFirstLaunch && !firstLaunchResult.existingGame) {
console.log('Sending show-first-launch-welcome event...'); console.log('Sending show-first-launch-welcome event...');
setTimeout(() => { setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('show-first-launch-welcome'); mainWindow.webContents.send('show-first-launch-welcome');
@@ -207,18 +247,36 @@ app.whenReady().then(async () => {
}, 3000); }, 3000);
}); });
app.on('window-all-closed', () => { function cleanupDiscordRPC() {
console.log('=== LAUNCHER CLOSING ===');
// Clean up Discord RPC connection
if (discordRPC) { if (discordRPC) {
try { try {
discordRPC.destroy(); console.log('Cleaning up Discord RPC...');
discordRPC.clearActivity();
setTimeout(() => {
try {
discordRPC.destroy();
} catch (error) {
console.log('Error during final Discord RPC cleanup:', error.message);
}
}, 100);
discordRPC = null;
} catch (error) { } catch (error) {
console.log('Error cleaning up Discord RPC:', error.message); console.log('Error cleaning up Discord RPC:', error.message);
discordRPC = null;
} }
} }
}
app.on('before-quit', () => {
console.log('=== LAUNCHER BEFORE QUIT ===');
cleanupDiscordRPC();
});
app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ===');
cleanupDiscordRPC();
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
@@ -240,17 +298,18 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) =
}; };
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath); const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
return result;
} catch (error) {
console.error('Launch error:', error);
const errorMessage = error.message || error.toString();
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
setTimeout(() => { setTimeout(() => {
mainWindow.webContents.send('progress-complete'); mainWindow.webContents.send('progress-complete');
}, 2000); }, 2000);
} }
return result;
} catch (error) {
console.error('Launch error:', error);
const errorMessage = error.message || error.toString();
return { success: false, error: errorMessage }; return { success: false, error: errorMessage };
} }
}); });
@@ -271,17 +330,12 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath)
}; };
const result = await installGame(playerName, progressCallback, javaPath, installPath); const result = await installGame(playerName, progressCallback, javaPath, installPath);
if (mainWindow && !mainWindow.isDestroyed()) {
setTimeout(() => {
mainWindow.webContents.send('progress-complete');
}, 1000);
}
return result; return result;
} catch (error) { } catch (error) {
console.error('Install error:', error); console.error('Install error:', error);
const errorMessage = error.message || error.toString(); const errorMessage = error.message || error.toString();
return { success: false, error: errorMessage }; return { success: false, error: errorMessage };
} }
}); });
@@ -301,6 +355,16 @@ ipcMain.handle('save-chat-username', async (event, chatUsername) => {
ipcMain.handle('load-chat-username', async () => { ipcMain.handle('load-chat-username', async () => {
return loadChatUsername(); return loadChatUsername();
}); });
ipcMain.handle('save-chat-color', (event, color) => {
saveChatColor(color);
return { success: true };
});
ipcMain.handle('load-chat-color', () => {
return loadChatColor();
});
ipcMain.handle('save-java-path', (event, javaPath) => { ipcMain.handle('save-java-path', (event, javaPath) => {
saveJavaPath(javaPath); saveJavaPath(javaPath);
return { success: true }; return { success: true };
@@ -320,12 +384,22 @@ ipcMain.handle('load-install-path', () => {
return loadInstallPath(); return loadInstallPath();
}); });
ipcMain.handle('save-discord-rpc', (event, enabled) => {
saveDiscordRPC(enabled);
toggleDiscordRPC(enabled);
return { success: true };
});
ipcMain.handle('load-discord-rpc', () => {
return loadDiscordRPC();
});
ipcMain.handle('select-install-path', async () => { ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'], properties: ['openDirectory'],
title: 'Select Installation Folder' title: 'Select Installation Folder'
}); });
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0]; return result.filePaths[0];
} }
@@ -348,7 +422,7 @@ ipcMain.handle('accept-first-launch-update', async (event, existingGame) => {
}; };
const result = await proposeGameUpdate(existingGame, progressCallback); const result = await proposeGameUpdate(existingGame, progressCallback);
return result; return result;
} catch (error) { } catch (error) {
console.error('First launch update error:', error); console.error('First launch update error:', error);
@@ -413,7 +487,7 @@ ipcMain.handle('open-game-location', async () => {
try { try {
const { getResolvedAppDir } = require('./backend/launcher'); const { getResolvedAppDir } = require('./backend/launcher');
const gameDir = path.join(getResolvedAppDir(), 'release', 'package', 'game'); const gameDir = path.join(getResolvedAppDir(), 'release', 'package', 'game');
if (fs.existsSync(gameDir)) { if (fs.existsSync(gameDir)) {
await shell.openPath(gameDir); await shell.openPath(gameDir);
return { success: true }; return { success: true };
@@ -429,9 +503,9 @@ ipcMain.handle('open-game-location', async () => {
ipcMain.handle('browse-java-path', async () => { ipcMain.handle('browse-java-path', async () => {
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin'; const isMac = process.platform === 'darwin';
let dialogOptions; let dialogOptions;
if (isWindows) { if (isWindows) {
dialogOptions = { dialogOptions = {
properties: ['openFile'], properties: ['openFile'],
@@ -460,9 +534,9 @@ ipcMain.handle('browse-java-path', async () => {
] ]
}; };
} }
const result = await dialog.showOpenDialog(mainWindow, dialogOptions); const result = await dialog.showOpenDialog(mainWindow, dialogOptions);
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0]; return result.filePaths[0];
} }
@@ -503,7 +577,7 @@ ipcMain.handle('load-settings', async () => {
} }
}); });
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod } = require('./backend/launcher'); const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
const os = require('os'); const os = require('os');
ipcMain.handle('get-local-app-data', async () => { ipcMain.handle('get-local-app-data', async () => {
@@ -585,7 +659,7 @@ ipcMain.handle('select-mod-files', async () => {
{ name: 'All Files', extensions: ['*'] } { name: 'All Files', extensions: ['*'] }
] ]
}); });
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths; return result.filePaths;
} }
@@ -596,9 +670,9 @@ ipcMain.handle('copy-mod-file', async (event, sourcePath, modsPath) => {
try { try {
const fileName = path.basename(sourcePath); const fileName = path.basename(sourcePath);
const destPath = path.join(modsPath, fileName); const destPath = path.join(modsPath, fileName);
fs.copyFileSync(sourcePath, destPath); fs.copyFileSync(sourcePath, destPath);
return { success: true, fileName }; return { success: true, fileName };
} catch (error) { } catch (error) {
console.error('Error copying mod file:', error); console.error('Error copying mod file:', error);
@@ -618,13 +692,13 @@ ipcMain.handle('check-for-updates', async () => {
ipcMain.handle('open-download-page', async () => { ipcMain.handle('open-download-page', async () => {
try { try {
await shell.openExternal(updateManager.getDownloadUrl()); await shell.openExternal(updateManager.getDownloadUrl());
setTimeout(() => { setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close(); mainWindow.close();
} }
}, 1000); }, 1000);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error opening download page:', error); console.error('Error opening download page:', error);
@@ -652,12 +726,73 @@ ipcMain.handle('get-log-directory', () => {
return logger.getLogDirectory(); return logger.getLogDirectory();
}); });
ipcMain.handle('get-current-uuid', async () => {
try {
return getCurrentUuid();
} catch (error) {
console.error('Error getting current UUID:', error);
return null;
}
});
ipcMain.handle('get-all-uuid-mappings', async () => {
try {
const mappings = getAllUuidMappings();
return Object.entries(mappings).map(([username, uuid]) => ({
username,
uuid,
isCurrent: username === require('./backend/launcher').loadUsername()
}));
} catch (error) {
console.error('Error getting UUID mappings:', error);
return [];
}
});
ipcMain.handle('set-uuid-for-user', async (event, username, uuid) => {
try {
await setUuidForUser(username, uuid);
return { success: true };
} catch (error) {
console.error('Error setting UUID for user:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('generate-new-uuid', async () => {
try {
return generateNewUuid();
} catch (error) {
console.error('Error generating new UUID:', error);
return null;
}
});
ipcMain.handle('delete-uuid-for-user', async (event, username) => {
try {
const result = deleteUuidForUser(username);
return { success: result };
} catch (error) {
console.error('Error deleting UUID for user:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('reset-current-user-uuid', async () => {
try {
const newUuid = resetCurrentUserUuid();
return { success: true, uuid: newUuid };
} catch (error) {
console.error('Error resetting current user UUID:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => { ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
try { try {
const logDir = logger.getLogDirectory(); const logDir = logger.getLogDirectory();
if (!logDir) return null; if (!logDir) return null;
// Find the most recent log file
const files = fs.readdirSync(logDir) const files = fs.readdirSync(logDir)
.filter(file => file.startsWith('launcher-') && file.endsWith('.log')) .filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
.map(file => ({ .map(file => ({
@@ -666,16 +801,58 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
mtime: fs.statSync(path.join(logDir, file)).mtime mtime: fs.statSync(path.join(logDir, file)).mtime
})) }))
.sort((a, b) => b.mtime - a.mtime); .sort((a, b) => b.mtime - a.mtime);
if (files.length === 0) return null; if (files.length === 0) return null;
const latestLogFile = files[0].path; const latestLogFile = files[0].path;
const content = fs.readFileSync(latestLogFile, 'utf8'); const content = fs.readFileSync(latestLogFile, 'utf8');
const lines = content.split('\n'); const lines = content.split('\n');
return lines.slice(-maxLines).join('\n'); return lines.slice(-maxLines).join('\n');
} catch (error) { } catch (error) {
console.error('Error reading logs:', error); console.error('Error reading logs:', error);
return null; return null;
} }
}); });
// Profile Management IPC
ipcMain.handle('profile-create', async (event, name) => {
try {
return profileManager.createProfile(name);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-list', async () => {
return profileManager.getProfiles();
});
ipcMain.handle('profile-get-active', async () => {
return profileManager.getActiveProfile();
});
ipcMain.handle('profile-activate', async (event, id) => {
try {
return await profileManager.activateProfile(id);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-delete', async (event, id) => {
try {
return profileManager.deleteProfile(id);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-update', async (event, id, updates) => {
try {
return profileManager.updateProfile(id, updates);
} catch (error) {
return { error: error.message };
}
});

View File

@@ -1,5 +1,5 @@
{ {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcherv2",
"version": "2.0.1", "version": "2.0.1",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P", "homepage": "https://github.com/amiayweb/Hytale-F2P",
@@ -97,8 +97,7 @@
{ {
"target": "dmg", "target": "dmg",
"arch": [ "arch": [
"x64", "universal"
"arm64"
] ]
}, },
{ {

View File

@@ -9,10 +9,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
loadUsername: () => ipcRenderer.invoke('load-username'), loadUsername: () => ipcRenderer.invoke('load-username'),
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername), saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
loadChatUsername: () => ipcRenderer.invoke('load-chat-username'), loadChatUsername: () => ipcRenderer.invoke('load-chat-username'),
saveChatColor: (chatColor) => ipcRenderer.invoke('save-chat-color', chatColor),
loadChatColor: () => ipcRenderer.invoke('load-chat-color'),
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath), saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
loadJavaPath: () => ipcRenderer.invoke('load-java-path'), loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath), saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
loadInstallPath: () => ipcRenderer.invoke('load-install-path'), loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
selectInstallPath: () => ipcRenderer.invoke('select-install-path'), selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'), browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'), isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
@@ -44,7 +48,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
onUpdatePopup: (callback) => { onUpdatePopup: (callback) => {
ipcRenderer.on('show-update-popup', (event, data) => callback(data)); ipcRenderer.on('show-update-popup', (event, data) => callback(data));
}, },
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame), acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'), markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
onFirstLaunchUpdate: (callback) => { onFirstLaunchUpdate: (callback) => {
@@ -61,5 +65,23 @@ contextBridge.exposeInMainWorld('electronAPI', {
}, },
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'), getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines) getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
// UUID Management methods
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
getAllUuidMappings: () => ipcRenderer.invoke('get-all-uuid-mappings'),
setUuidForUser: (username, uuid) => ipcRenderer.invoke('set-uuid-for-user', username, uuid),
generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'),
deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username),
resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'),
// Profile API
profile: {
create: (name) => ipcRenderer.invoke('profile-create', name),
list: () => ipcRenderer.invoke('profile-list'),
getActive: () => ipcRenderer.invoke('profile-get-active'),
activate: (id) => ipcRenderer.invoke('profile-activate', id),
delete: (id) => ipcRenderer.invoke('profile-delete', id),
update: (id, updates) => ipcRenderer.invoke('profile-update', id, updates)
}
}); });