Compare commits

..

72 Commits
WINDOWS ... v2

Author SHA1 Message Date
AMIAY
c0dc65c59a Update dependencies and fix tar version to avoid workflow & electron-builder problem 2026-01-18 14:07:50 +01:00
AMIAY
b748e7316d Update package-lock.json 2026-01-18 13:59:41 +01:00
AMIAY
1c7f24c67c Update package-lock.json 2026-01-18 13:58:23 +01:00
AMIAY
b0c8c6affa Add npm install step to release workflow
Added npm install step before npm ci for all platforms.
2026-01-18 13:55:39 +01:00
AMIAY
472e55668a Update 'tar' dependency version
Updated 'tar' dependency version from 7.0.0 to 7.5.3.
2026-01-18 13:52:50 +01:00
AMIAY
6d09bba996 Merge pull request #34 from fazrigading/fix/tar-7.5.3
fix: update tar to 7.5.3 and add override for GHSA-8qq5-rm4j-mr97
2026-01-18 13:51:11 +01:00
AMIAY
6f3ae4aed7 Add README1.md to the .github directory 2026-01-18 13:45:48 +01:00
AMIAY
ee40bab9c3 Merge pull request #33 from sanasol/feature/build-releases
Github action to build binaries
2026-01-18 13:44:38 +01:00
AMIAY
84dc63b13e Change VPN software reference in server setup
Updated instructions to use Radmin VPN instead of Hamachi.
2026-01-18 13:44:00 +01:00
Fazri Gading
022a1bfde1 Update .gitignore 2026-01-18 20:36:03 +08:00
sanasol
2896ca862b Add README for GitHub Actions build and release workflow detailing triggers, platforms, and artifact generation 2026-01-18 13:27:36 +01:00
Fazri Gading
651cc16485 add override to ensure
nested dependencies in electron-builder are also patched against
arbitrary file overwrite exploits
2026-01-18 20:26:42 +08:00
Fazri Gading
5d986768d9 Delete package-lock.json 2026-01-18 20:18:09 +08:00
Fazri Gading
0f0e360cad Merge branch 'fix/tar-7.5.3' of https://github.com/fazrigading/Hytale-F2P into fix/tar-7.5.3 2026-01-18 20:16:10 +08:00
Fazri Gading
9c95bbb174 removed override for tar>=7.5.3 dep 2026-01-18 20:11:59 +08:00
Fazri Gading
23e32b3688 ignore package lock to avoid conflicts 2026-01-18 20:11:09 +08:00
Fazri Gading
f138ada0a6 Merge branch 'amiayweb:main' into fix/tar-7.5.3 2026-01-18 20:09:57 +08:00
sanasol
b5adf4aa6c Add GitHub Actions workflow for building and releasing artifacts across Linux, Windows, and macOS platforms 2026-01-18 13:08:51 +01:00
sanasol
6df5b056c5 Add GitHub Actions workflow for building and releasing artifacts across Linux, Windows, and macOS platforms 2026-01-18 12:43:19 +01:00
sanasol
fe45c5810c Add GitHub Actions workflow for building and releasing artifacts across Linux, Windows, and macOS platforms 2026-01-18 12:39:22 +01:00
sanasol
df46a74089 Add GitHub Actions workflow for building and releasing artifacts across Linux, Windows, and macOS platforms 2026-01-18 12:36:11 +01:00
AMIAY
e14c80f3dd Merge pull request #31 from Terromur/main
Add PKGBUILD
thanks for your contributions
2026-01-18 12:08:12 +01:00
Terromur
7f61d9d5b8 Update PKGBUILD 2026-01-18 16:07:00 +05:00
Fazri Gading
224f3f77fb chore: pin tar >=7.5.3 and add overrides to address vuln 2026-01-18 19:02:57 +08:00
Terromur
abcbd6823e Add PKGBUILD 2026-01-18 15:58:34 +05:00
AMIAY
2dcd0064e8 Merge pull request #29 from letha11/main
fix: typo css file link for the gui
2026-01-18 11:36:10 +01:00
AMIAY
ad8b6a7d3b Merge pull request #30 from Terromur/main
Fix build deb [Ubuntu]
2026-01-18 11:34:35 +01:00
Terromur
ebcfdc4e3b Fix build deb 2026-01-18 13:15:18 +05:00
letha11
a275d15d38 fix: typo css file link for the gui 2026-01-18 14:29:50 +07:00
AMIAY
778d48fb62 Refactor main.js for improved structure and updates 2026-01-18 04:21:47 +01:00
AMIAY
f251f77c07 Merge pull request #25 from Terromur/main
Removing unnecessary variables in launcher.js and deleting package-lock.json
2026-01-18 04:12:14 +01:00
Terromur
ec03f9e6bc Delete package-lock.json
Fixed problems with npm install
2026-01-18 08:01:59 +05:00
Terromur
517dedc5c7 Update launcher.js
Removing unnecessary variables
2026-01-18 07:59:24 +05:00
AMIAY
d6bfda964b Add Discord server badge to README 2026-01-18 03:33:12 +01:00
AMIAY
f06c97b70b Update instructions for using Hamachi
Added a note to check YouTube for Hamachi usage.
2026-01-18 02:32:34 +01:00
AMIAY
ed84f46386 Add contact section with Discord badge
Added contact section with Discord link to README.
2026-01-18 02:14:32 +01:00
AMIAY
07c78f2fb0 Update version badge from 1.0.2 to 2.0.0 2026-01-18 02:11:01 +01:00
AMIAY
b64f8fa0a3 Add screenshots section to README
Added a new section for screenshots in the README.
2026-01-18 02:03:35 +01:00
AMIAY
395b317e80 Delete index.html 2026-01-18 01:59:10 +01:00
AMIAY
68bd0a1902 Update README.md 2026-01-18 01:58:33 +01:00
AMIAY
45f8f17751 Update README with third-party software requirement
Added a note about needing third-party software for server setup.
2026-01-18 01:56:05 +01:00
AMIAY
1e93105cac Add files via upload 2026-01-18 01:52:38 +01:00
AMIAY
3ebe727f58 Add files via upload 2026-01-18 01:52:24 +01:00
AMIAY
01757fcdfa Add imports for multiple modules in script.js 2026-01-18 01:52:14 +01:00
AMIAY
1c9becc6b9 Add files via upload 2026-01-18 01:51:46 +01:00
AMIAY
a81bb943b6 Create style.css 2026-01-18 01:51:37 +01:00
AMIAY
b7e44ca418 Add files via upload 2026-01-18 01:50:47 +01:00
AMIAY
b6bb1c722a Add files via upload 2026-01-18 01:50:24 +01:00
AMIAY
09b2d05e31 Merge pull request #23 from colbster937/main
resize icon.png from 256x256 to 512x512
2026-01-17 22:17:10 +01:00
Colbster937
08e3fa4222 resize icon.png from 256x256 to 512x512 2026-01-17 15:06:10 -06:00
AMIAY
71f635c537 Merge pull request #22 from Citeli-py/main
Fix: Error on build for linux [Ubuntu 22] #20
2026-01-17 20:54:18 +01:00
Citeli-py
567a1fadde build: update packages 2026-01-17 16:32:15 -03:00
Citeli-py
10cdb14610 fix: email for .deb build 2026-01-17 16:32:05 -03:00
Citeli-py
049937718b add: icon in png for linux 2026-01-17 16:26:01 -03:00
AMIAY
528d10e902 Add contributor link for @sanasol 2026-01-17 16:16:42 +01:00
AMIAY
eb9aa5084f Merge pull request #11 from sanasol/hotfix/macos-code-sign-sentry-disable
Fix: macOS build crash on game launch and server not booting
2026-01-17 15:07:28 +01:00
AMIAY
569a06b13d Add disclaimer about project affiliation and usage
Added a disclaimer regarding the project's purpose and affiliation.
2026-01-17 15:06:38 +01:00
sanasol
335db04cee MacOs build crash on game launch and server not botting 2026-01-17 14:32:43 +01:00
AMIAY
f49c5c02dd Update README.md 2026-01-16 19:44:55 +01:00
AMIAY
0bf83df285 Refactor launcher.js for improved app directory management
Refactor launcher.js to improve directory handling and function parameters.
2026-01-16 16:19:46 +01:00
AMIAY
c597a08a38 Refactor index.html for improved launcher functionality
Updated HTML structure and JavaScript functionality for the Hytale Launcher, including enhancements to the installation process and UI elements.
2026-01-16 16:19:20 +01:00
AMIAY
7870b9e3d8 Update package-lock.json 2026-01-16 16:18:57 +01:00
AMIAY
3a7e2e762e Update package.json 2026-01-16 16:18:26 +01:00
AMIAY
2fae72e3f9 Add install path handling to preload.js 2026-01-16 16:18:16 +01:00
AMIAY
4ca8091bb9 Update main.js 2026-01-16 16:18:03 +01:00
AMIAY
df675f270a Merge pull request #5 from crimera/main
Only generate uuid for new username strings
2026-01-16 14:56:42 +01:00
crimera
112801f220 Only generate uuid for new username strings 2026-01-16 21:08:43 +08:00
AMIAY
d80b905879 Merge pull request #2 from chasem-dev/chasem.dev/fix-mac
Better support for MAC. Add Java path configuration and input handling.
2026-01-14 17:53:28 +01:00
chasem-dev
6873e2e4bf Add Java path configuration and input handling
- Introduced a new input field for Java path in the launcher UI.
- Updated the main process to handle saving and loading of the Java path.
- Enhanced game launch functionality to accept a Java path parameter.
- Added Java detection logic to find the Java executable on the system.
- Created a .gitignore file to exclude build artifacts and dependencies.
2026-01-14 10:11:01 -05:00
AMIAY
b90eeb344b Update README.md 2026-01-14 00:47:29 +01:00
AMIAY
00287302ff Add files via upload 2026-01-14 00:42:29 +01:00
AMIAY
692de4eb26 Add launcher.js for game management and launching 2026-01-14 00:42:12 +01:00
27 changed files with 10601 additions and 24 deletions

61
.github/README1.md vendored Normal file
View File

@@ -0,0 +1,61 @@
# GitHub Actions
## Build and Release Workflow
The `release.yml` workflow automatically builds the launcher for all platforms.
### Triggers
| Trigger | Builds | Creates Release |
|---------|--------|-----------------|
| Push to `main` | Yes | No |
| Push tag `v*` | Yes | Yes |
| Manual dispatch | Yes | No |
### Platforms
All builds run in parallel:
- **Linux** (ubuntu-latest): AppImage, deb
- **Windows** (windows-latest): NSIS installer, portable exe
- **macOS** (macos-latest): Universal DMG (Intel + Apple Silicon)
### Creating a Release
1. Update version in `package.json`
2. Commit and push to `main`
3. Create and push a version tag:
```bash
git tag v2.0.1
git push origin v2.0.1
```
The workflow will:
1. Build all platforms in parallel
2. Upload artifacts to GitHub Release
3. Generate release notes automatically
### Build Artifacts
After each build, artifacts are available in the Actions tab for 90 days:
- `linux-builds`: `.AppImage`, `.deb`
- `windows-builds`: `.exe`
- `macos-builds`: `.dmg`, `.zip`, `latest-mac.yml`
### Local Development
Build locally for your platform:
```bash
npm run build:linux
npm run build:win
npm run build:mac
```
Or build all platforms (requires appropriate OS):
```bash
npm run build:all
```

92
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
name: Build and Release
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npx electron-builder --linux --publish never
- uses: actions/upload-artifact@v4
with:
name: linux-builds
path: |
dist/*.AppImage
dist/*.deb
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npx electron-builder --win --publish never
- uses: actions/upload-artifact@v4
with:
name: windows-builds
path: |
dist/*.exe
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm install
- run: npm ci
- run: npx electron-builder --mac --publish never
- uses: actions/upload-artifact@v4
with:
name: macos-builds
path: |
dist/*.dmg
dist/*.zip
dist/latest-mac.yml
release:
needs: [build-linux, build-windows, build-macos]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Display structure of downloaded files
run: ls -R artifacts
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/linux-builds/*
artifacts/windows-builds/*
artifacts/macos-builds/*
generate_release_notes: true
draft: false
prerelease: false

3
.gitignore vendored Normal file
View File

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

BIN
GUI/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

423
GUI/index.html Normal file
View File

@@ -0,0 +1,423 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hytale F2P Launcher</title>
<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 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>
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
<div class="absolute inset-0 z-0">
<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-[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 class="flex w-full h-screen relative z-10">
<nav class="sidebar">
<div class="sidebar-logo">
<img src="./icon.png" alt="Hytale Logo" />
</div>
<div class="sidebar-nav">
<div class="nav-item active" data-page="play">
<i class="fas fa-play"></i>
<span class="nav-tooltip">Play</span>
</div>
<div class="nav-item" data-page="mods">
<i class="fas fa-box"></i>
<span class="nav-tooltip">Mods</span>
</div>
<div class="nav-item" data-page="news">
<i class="fas fa-newspaper"></i>
<span class="nav-tooltip">News</span>
</div>
<div class="nav-item" data-page="chat">
<i class="fas fa-comments"></i>
<span class="nav-tooltip">Players Chat</span>
</div>
<div class="nav-item" data-page="settings">
<i class="fas fa-cog"></i>
<span class="nav-tooltip">Settings</span>
</div>
<div class="nav-item" data-page="skins">
<i class="fas fa-user"></i>
<span class="nav-tooltip">Skins</span>
</div>
</div>
</nav>
<main class="main-content">
<header class="header">
<div id="playersOnlineCounter" class="players-counter">
<i class="fas fa-users"></i>
<span class="counter-label">Players:</span>
<span id="onlineCount" class="counter-value">0</span>
</div>
<div class="window-controls">
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
<i class="fas fa-minus"></i>
</button>
<button class="control-btn close" onclick="window.electronAPI?.closeWindow()">
<i class="fas fa-times"></i>
</button>
</div>
</header>
<div class="game-title-section">
<h1 class="game-title">
HY<span class="title-accent">TALE</span>
</h1>
<div class="game-tags">
<span class="tag">FREE TO PLAY</span>
</div>
</div>
<div class="content-pages">
<div id="install-page" class="page install-page">
<div class="install-content">
<div class="install-header">
<h1 class="install-title">
HYTA<span class="title-accent">LE</span>
</h1>
<p class="install-subtitle">FREE TO PLAY LAUNCHER</p>
</div>
<div class="install-form">
<div class="form-group">
<label class="form-label">Player Name</label>
<input type="text" id="installPlayerName" placeholder="Enter your name" class="form-input" value="Player" />
</div>
<div class="form-group">
<label class="checkbox-group">
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
<span class="checkbox-label">Custom Installation</span>
</label>
<div id="installCustomOptions" class="custom-options">
<div class="form-subgroup">
<label class="form-label">Installation Folder</label>
<div class="input-with-button">
<input type="text" id="installPath" placeholder="Default location" class="form-input" readonly />
<button onclick="browseInstallPath()" class="browse-btn">
<i class="fas fa-folder-open"></i>
</button>
</div>
</div>
</div>
</div>
<button id="installBtn" class="install-button" onclick="installGame()">
<i class="fas fa-download mr-2"></i>
<span id="installText">INSTALL HYTALE</span>
</button>
</div>
</div>
</div>
<div id="launcher-container" class="launcher-container" style="display: none;">
<div id="play-page" class="page active">
<div class="play-section">
<div class="play-content">
<div class="play-header">
<h2 class="play-title">
<i class="fas fa-play-circle mr-2"></i>
READY TO PLAY
</h2>
<p class="play-subtitle">Launch Hytale and enter the adventure</p>
</div>
<button id="homePlayBtn" class="home-play-button" onclick="launch()">
<i class="fas fa-play"></i>
<span>PLAY HYTALE</span>
</button>
</div>
</div>
<div class="news-section">
<div class="news-header">
<h2 class="news-title">
<i class="fas fa-star mr-2"></i>
LATEST NEWS
</h2>
<button class="view-all-btn" onclick="navigateToPage('news')">
VIEW ALL <i class="fas fa-arrow-right ml-1"></i>
</button>
</div>
<div id="newsGrid" class="news-grid-horizontal"></div>
</div>
</div>
<div id="mods-page" class="page">
<div class="mods-header">
<div class="mods-search-container">
<i class="fas fa-search"></i>
<input type="text" id="modsSearch" placeholder="Search mods..." class="mods-search" />
</div>
<div class="mods-actions">
<button id="myModsBtn" class="mods-btn-primary">
<i class="fas fa-box"></i>
MY MODS
</button>
</div>
</div>
<div id="browseModsList" class="mods-browse-container">
</div>
<div class="mods-pagination">
<button id="prevPage" class="pagination-btn">
<i class="fas fa-chevron-left"></i>
PREVIOUS
</button>
<span class="pagination-info">
Page <span id="currentPage">1</span> of <span id="totalPages">1</span>
</span>
<button id="nextPage" class="pagination-btn">
NEXT
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<div id="news-page" class="page">
<div class="news-header">
<h2 class="news-title">
<i class="fas fa-newspaper mr-2"></i>
ALL NEWS
</h2>
</div>
<div id="allNewsGrid" class="news-grid-full"></div>
</div>
<div id="chat-page" class="page">
<div class="chat-container">
<div class="chat-header">
<h2 class="chat-title">
<i class="fas fa-comments mr-2"></i>
PLAYERS CHAT
</h2>
<div class="chat-online-badge">
<i class="fas fa-circle"></i>
<span id="chatOnlineCount">0</span> online
</div>
</div>
<div class="chat-body">
<div id="chatMessages" class="chat-messages">
</div>
</div>
<div class="chat-footer">
<div class="chat-input-container">
<textarea
id="chatInput"
class="chat-input"
placeholder="Type your message... (Links are automatically censored)"
rows="1"
maxlength="500"
></textarea>
<button id="chatSendBtn" class="chat-send-btn">
<i class="fas fa-paper-plane"></i>
</button>
</div>
<div class="chat-footer-info">
<span class="chat-char-counter" id="chatCharCounter">0/500</span>
<span class="chat-warning-text">
<i class="fas fa-shield-alt"></i>
Secure chat - Links are censored
</span>
</div>
</div>
</div>
</div>
<div id="settings-page" class="page">
<div class="settings-container">
<div class="settings-header">
<h2 class="settings-title">
<i class="fas fa-cog mr-2"></i>
SETTINGS
</h2>
</div>
<div class="settings-content">
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
Java Runtime
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Use Custom Java Path</div>
<div class="checkbox-description">Override the bundled Java runtime with your own installation</div>
</div>
</label>
</div>
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group">
<label class="settings-input-label">Java Executable Path</label>
<div class="settings-input-with-button">
<input
type="text"
id="customJavaPath"
class="settings-input"
placeholder="Select Java path..."
readonly
/>
<button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i>
Browse
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
Select the Java installation folder (supports Windows, Mac, Linux)
</p>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-gamepad"></i>
Game Options
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label">Player Name</label>
<input
type="text"
id="settingsPlayerName"
class="settings-input"
placeholder="Enter your player name"
maxlength="16"
/>
<p class="settings-hint">
<i class="fas fa-user"></i>
This name will be used in-game (1-16 characters)
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="skins-page" class="page">
<div class="placeholder-content">
<i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
<h2>Skins</h2>
<p>Skin customization coming soon...</p>
</div>
</div>
</div>
</div>
</main>
</div>
<div id="myModsModal" class="mods-modal">
<div class="mods-modal-content">
<div class="mods-modal-header">
<h2 class="mods-modal-title">
<i class="fas fa-box mr-2"></i>
MY MODS
</h2>
<button id="closeMyModsModal" class="mods-modal-close">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mods-modal-body">
<div id="installedModsList" class="installed-mods-list">
</div>
</div>
</div>
</div>
<div id="progressOverlay" class="progress-overlay" style="display: none;">
<div class="progress-content">
<div class="progress-info">
<span id="progressText">Initializing...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar-container">
<div id="progressBarFill" class="progress-bar-fill"></div>
</div>
<div class="progress-details">
<span id="progressSpeed"></span>
<span id="progressSize"></span>
</div>
</div>
</div>
<div id="chatUsernameModal" class="chat-username-modal" style="display: none;">
<div class="chat-username-modal-content">
<div class="chat-username-modal-header">
<h2 class="chat-username-modal-title">
<i class="fas fa-comments mr-2"></i>
Join Chat
</h2>
</div>
<div class="chat-username-modal-body">
<p class="chat-username-modal-description">
Choose a username to join the Players Chat
</p>
<div class="chat-username-input-group">
<label for="chatUsernameInput" class="chat-username-label">Username</label>
<input
type="text"
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 id="chatUsernameError" class="chat-username-error"></span>
</div>
</div>
<div class="chat-username-modal-footer">
<button id="chatUsernameCancel" class="chat-username-btn-cancel">
<i class="fas fa-times"></i>
Cancel
</button>
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
<i class="fas fa-check"></i>
Join Chat
</button>
</div>
</div>
</div>
<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">
<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 class="mx-2">|</span>
<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/crimera" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</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>
</span>
</div>
</footer>
<script type="module" src="js/script.js"></script>
<script type="module" src="js/update.js"></script>
</body>
</html>

358
GUI/js/chat.js Normal file
View File

@@ -0,0 +1,358 @@
let socket = null;
let isAuthenticated = false;
let messageQueue = [];
let chatUsername = '';
const SOCKET_URL = 'http://3.10.208.30:3001';
const MAX_MESSAGE_LENGTH = 500;
export async function initChat() {
if (window.electronAPI?.loadChatUsername) {
chatUsername = await window.electronAPI.loadChatUsername();
}
if (!chatUsername || chatUsername.trim() === '') {
showUsernameModal();
return;
}
setupChatUI();
await connectToChat();
}
function showUsernameModal() {
const modal = document.getElementById('chatUsernameModal');
if (modal) {
modal.style.display = 'flex';
const input = document.getElementById('chatUsernameInput');
if (input) {
setTimeout(() => input.focus(), 100);
}
}
}
function hideUsernameModal() {
const modal = document.getElementById('chatUsernameModal');
if (modal) {
modal.style.display = 'none';
}
}
async function submitChatUsername() {
const input = document.getElementById('chatUsernameInput');
const errorMsg = document.getElementById('chatUsernameError');
if (!input) return;
const username = input.value.trim();
if (username.length === 0) {
if (errorMsg) errorMsg.textContent = 'Username cannot be empty';
return;
}
if (username.length < 3) {
if (errorMsg) errorMsg.textContent = 'Username must be at least 3 characters';
return;
}
if (username.length > 20) {
if (errorMsg) errorMsg.textContent = 'Username must be 20 characters or less';
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
if (errorMsg) errorMsg.textContent = 'Username can only contain letters, numbers, - and _';
return;
}
chatUsername = username;
if (window.electronAPI?.saveChatUsername) {
await window.electronAPI.saveChatUsername(username);
}
hideUsernameModal();
setupChatUI();
await connectToChat();
}
function setupChatUI() {
const sendBtn = document.getElementById('chatSendBtn');
const chatInput = document.getElementById('chatInput');
const chatMessages = document.getElementById('chatMessages');
if (!sendBtn || !chatInput || !chatMessages) {
console.warn('Chat UI elements not found');
return;
}
sendBtn.addEventListener('click', () => {
sendMessage();
});
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
chatInput.addEventListener('input', () => {
if (chatInput.value.length > MAX_MESSAGE_LENGTH) {
chatInput.value = chatInput.value.substring(0, MAX_MESSAGE_LENGTH);
}
updateCharCounter();
});
updateCharCounter();
}
async function connectToChat() {
try {
if (!window.io) {
await loadSocketIO();
}
const userId = await window.electronAPI?.getUserId();
if (!userId) {
console.error('User ID not available');
addSystemMessage('Error: Could not connect to chat');
return;
}
if (!chatUsername || chatUsername.trim() === '') {
console.error('Chat username not set');
addSystemMessage('Error: Username not set');
return;
}
socket = io(SOCKET_URL, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
socket.on('connect', () => {
console.log('Connected to chat server');
socket.emit('authenticate', { username: chatUsername, userId });
});
socket.on('authenticated', (data) => {
isAuthenticated = true;
addSystemMessage(`Connected as ${data.username}`);
while (messageQueue.length > 0) {
const msg = messageQueue.shift();
socket.emit('send_message', { message: msg });
}
});
socket.on('message', (data) => {
if (data.type === 'system') {
addSystemMessage(data.message);
} else if (data.type === 'user') {
addUserMessage(data.username, data.message, data.timestamp);
}
});
socket.on('users_update', (data) => {
updateOnlineCount(data.count);
});
socket.on('error', (data) => {
addSystemMessage(`Error: ${data.message}`, 'error');
});
socket.on('clear_chat', (data) => {
clearAllMessages();
addSystemMessage(data.message || 'Chat cleared by server', 'warning');
});
socket.on('disconnect', () => {
isAuthenticated = false;
console.log('Disconnected from chat server');
addSystemMessage('Disconnected from chat', 'error');
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
addSystemMessage('Connection error. Retrying...', 'error');
});
} catch (error) {
console.error('Error connecting to chat:', error);
addSystemMessage('Failed to connect to chat server', 'error');
}
}
function loadSocketIO() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.socket.io/4.6.1/socket.io.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function sendMessage() {
const chatInput = document.getElementById('chatInput');
const message = chatInput.value.trim();
if (!message || message.length === 0) {
return;
}
if (message.length > MAX_MESSAGE_LENGTH) {
addSystemMessage(`Message too long (max ${MAX_MESSAGE_LENGTH} characters)`, 'error');
return;
}
if (!socket || !isAuthenticated) {
messageQueue.push(message);
addSystemMessage('Connecting... Your message will be sent soon.', 'warning');
chatInput.value = '';
updateCharCounter();
return;
}
socket.emit('send_message', { message });
chatInput.value = '';
updateCharCounter();
}
function addUserMessage(username, message, timestamp) {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
const messageDiv = document.createElement('div');
messageDiv.className = 'chat-message user-message';
const time = new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
messageDiv.innerHTML = `
<div class="message-header">
<span class="message-username">${escapeHtml(username)}</span>
<span class="message-time">${time}</span>
</div>
<div class="message-content">${message}</div>
`;
chatMessages.appendChild(messageDiv);
scrollToBottom();
}
function addSystemMessage(message, type = 'info') {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message system-message system-${type}`;
messageDiv.innerHTML = `
<div class="message-content">
<i class="fas fa-info-circle"></i> ${escapeHtml(message)}
</div>
`;
chatMessages.appendChild(messageDiv);
scrollToBottom();
}
function updateOnlineCount(count) {
const onlineCountElement = document.getElementById('chatOnlineCount');
if (onlineCountElement) {
onlineCountElement.textContent = count;
}
}
function updateCharCounter() {
const chatInput = document.getElementById('chatInput');
const charCounter = document.getElementById('chatCharCounter');
if (chatInput && charCounter) {
const length = chatInput.value.length;
charCounter.textContent = `${length}/${MAX_MESSAGE_LENGTH}`;
if (length > MAX_MESSAGE_LENGTH * 0.9) {
charCounter.classList.add('warning');
} else {
charCounter.classList.remove('warning');
}
}
}
function scrollToBottom() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
function clearAllMessages() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.innerHTML = '';
console.log('Chat cleared');
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.addEventListener('beforeunload', () => {
if (socket && socket.connected) {
socket.disconnect();
}
});
document.addEventListener('DOMContentLoaded', () => {
const usernameSubmitBtn = document.getElementById('chatUsernameSubmit');
const usernameCancelBtn = document.getElementById('chatUsernameCancel');
const usernameInput = document.getElementById('chatUsernameInput');
if (usernameSubmitBtn) {
usernameSubmitBtn.addEventListener('click', submitChatUsername);
}
if (usernameCancelBtn) {
usernameCancelBtn.addEventListener('click', () => {
hideUsernameModal();
const playNavItem = document.querySelector('[data-page="play"]');
if (playNavItem) playNavItem.click();
});
}
if (usernameInput) {
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
submitChatUsername();
}
});
}
const chatNavItem = document.querySelector('[data-page="chat"]');
if (chatNavItem) {
chatNavItem.addEventListener('click', () => {
if (!socket) {
initChat();
}
});
}
});
window.ChatAPI = {
send: sendMessage,
disconnect: () => socket?.disconnect()
};

194
GUI/js/install.js Normal file
View File

@@ -0,0 +1,194 @@
let isDownloading = false;
let installPage;
let installBtn;
let installText;
let installPlayerName;
let installCustomCheck;
let installCustomOptions;
let installPathInput;
export function setupInstallation() {
installPage = document.getElementById('install-page');
installBtn = document.getElementById('installBtn');
installText = document.getElementById('installText');
installPlayerName = document.getElementById('installPlayerName');
installCustomCheck = document.getElementById('installCustomCheck');
installCustomOptions = document.getElementById('installCustomOptions');
installPathInput = document.getElementById('installPath');
if (installCustomCheck && installCustomOptions) {
installCustomCheck.addEventListener('change', (e) => {
if (e.target.checked) {
installCustomOptions.classList.add('show');
} else {
installCustomOptions.classList.remove('show');
}
});
}
if (installPlayerName) {
installPlayerName.addEventListener('change', savePlayerName);
}
}
export async function installGame() {
if (isDownloading || (installBtn && installBtn.disabled)) return;
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
const installPath = installPathInput ? installPathInput.value.trim() : '';
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
if (installBtn) {
installBtn.disabled = true;
installText.textContent = 'INSTALLING...';
}
try {
if (window.electronAPI && window.electronAPI.installGame) {
const result = await window.electronAPI.installGame(playerName, '', installPath);
if (result.success) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
}, 2000);
}
} else {
throw new Error(result.error || 'Installation failed');
}
} else {
simulateInstallation(playerName);
}
} catch (error) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Installation failed: ${error.message}` });
setTimeout(() => {
window.LauncherUI.hideProgress();
resetInstallButton();
}, 3000);
}
}
}
function simulateInstallation(playerName) {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 3;
if (progress > 100) progress = 100;
if (window.LauncherUI) {
window.LauncherUI.updateProgress({
percent: progress,
message: progress < 100 ? 'Installing game files...' : 'Installation complete!',
speed: 1024 * 1024 * (5 + Math.random() * 10),
downloaded: progress * 1024 * 1024 * 20,
total: 1024 * 1024 * 2000
});
}
if (progress >= 100) {
clearInterval(interval);
setTimeout(() => {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
resetInstallButton();
}, 2000);
}
}, 1000);
}
}, 200);
}
function resetInstallButton() {
isDownloading = false;
if (installBtn) {
installBtn.disabled = false;
installText.textContent = 'INSTALL HYTALE';
}
}
export async function browseInstallPath() {
try {
if (window.electronAPI && window.electronAPI.selectInstallPath) {
const result = await window.electronAPI.selectInstallPath();
if (result && installPathInput) {
installPathInput.value = result;
}
}
} catch (error) {
console.error('Error browsing install path:', error);
}
}
async function savePlayerName() {
try {
if (window.electronAPI && window.electronAPI.saveSettings) {
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
await window.electronAPI.saveSettings({ playerName });
}
} catch (error) {
console.error('Error saving player name:', error);
}
}
export async function checkGameStatusAndShowInterface() {
try {
if (window.electronAPI && window.electronAPI.isGameInstalled) {
const installed = await window.electronAPI.isGameInstalled();
if (window.LauncherUI) {
window.LauncherUI.showLauncherOrInstall(installed);
}
if (installed) {
await loadPlayerSettings();
}
} else {
if (window.LauncherUI) {
window.LauncherUI.showLauncherOrInstall(false);
}
}
} catch (error) {
console.error('Error checking game status:', error);
if (window.LauncherUI) {
window.LauncherUI.showLauncherOrInstall(false);
}
}
}
async function loadPlayerSettings() {
try {
if (window.electronAPI && window.electronAPI.loadSettings) {
const settings = await window.electronAPI.loadSettings();
if (settings) {
const playerNameInput = document.getElementById('playerName');
const javaPathInput = document.getElementById('javaPath');
if (settings.playerName && playerNameInput) {
playerNameInput.value = settings.playerName;
}
if (settings.javaPath && javaPathInput) {
javaPathInput.value = settings.javaPath;
}
}
}
} catch (error) {
console.error('Error loading settings:', error);
}
}
window.installGame = installGame;
window.browseInstallPath = browseInstallPath;
document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});

235
GUI/js/launcher.js Normal file
View File

@@ -0,0 +1,235 @@
let isDownloading = false;
let playBtn;
let playText;
let homePlayBtn;
let uninstallBtn;
let playerNameInput;
let javaPathInput;
export function setupLauncher() {
playBtn = document.getElementById('playBtn');
playText = document.getElementById('playText');
homePlayBtn = document.getElementById('homePlayBtn');
uninstallBtn = document.getElementById('uninstallBtn');
playerNameInput = document.getElementById('playerName');
javaPathInput = document.getElementById('javaPath');
if (playerNameInput) {
playerNameInput.addEventListener('change', savePlayerName);
}
if (javaPathInput) {
javaPathInput.addEventListener('change', saveJavaPath);
}
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => {
if (window.LauncherUI) {
window.LauncherUI.showProgress();
window.LauncherUI.updateProgress(data);
}
});
}
}
export async function launch() {
if (isDownloading || (playBtn && playBtn.disabled)) return;
let playerName = 'Player';
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
playerName = window.SettingsAPI.getCurrentPlayerName();
} else if (playerNameInput && playerNameInput.value.trim()) {
playerName = playerNameInput.value.trim();
}
let javaPath = '';
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
javaPath = window.SettingsAPI.getCurrentJavaPath();
}
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
if (playBtn) {
playBtn.disabled = true;
playText.textContent = 'LAUNCHING...';
}
try {
if (window.electronAPI && window.electronAPI.launchGame) {
const result = await window.electronAPI.launchGame(playerName, javaPath, '');
if (result.success) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game started successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
if (window.electronAPI.minimizeWindow) {
window.electronAPI.minimizeWindow();
}
}, 2000);
}
} else {
throw new Error(result.error || 'Launch failed');
}
} else {
setTimeout(() => {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game started successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
resetPlayButton();
}, 2000);
}
}, 2000);
}
} catch (error) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Failed: ${error.message}` });
setTimeout(() => {
window.LauncherUI.hideProgress();
resetPlayButton();
}, 3000);
}
}
}
export async function uninstallGame() {
if (!confirm('Are you sure you want to uninstall Hytale? All game files will be deleted.')) {
return;
}
if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' });
if (uninstallBtn) uninstallBtn.disabled = true;
try {
if (window.electronAPI && window.electronAPI.uninstallGame) {
const result = await window.electronAPI.uninstallGame();
if (result.success) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false);
}, 2000);
}
} else {
throw new Error(result.error || 'Uninstall failed');
}
} else {
setTimeout(() => {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false);
}, 2000);
}
}, 2000);
}
} catch (error) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Uninstall failed: ${error.message}` });
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
}
} finally {
if (uninstallBtn) uninstallBtn.disabled = false;
}
}
function resetPlayButton() {
isDownloading = false;
if (playBtn) {
playBtn.disabled = false;
playText.textContent = 'PLAY';
}
}
async function savePlayerName() {
try {
if (window.electronAPI && window.electronAPI.saveSettings) {
const playerName = (playerNameInput ? playerNameInput.value.trim() : '') || 'Player';
await window.electronAPI.saveSettings({ playerName });
}
} catch (error) {
console.error('Error saving player name:', error);
}
}
async function saveJavaPath() {
try {
if (window.electronAPI && window.electronAPI.saveSettings) {
const javaPath = (javaPathInput ? javaPathInput.value.trim() : '') || '';
await window.electronAPI.saveSettings({ javaPath });
}
} catch (error) {
console.error('Error saving Java path:', error);
}
}
function toggleCustomJava() {
if (!customJavaOptions) return;
if (customJavaCheck && customJavaCheck.checked) {
customJavaOptions.style.display = 'block';
} else {
customJavaOptions.style.display = 'none';
if (customJavaPath) customJavaPath.value = '';
saveCustomJavaPath('');
}
}
async function browseJavaPath() {
try {
if (window.electronAPI && window.electronAPI.browseJavaPath) {
const result = await window.electronAPI.browseJavaPath();
if (result && result.filePaths && result.filePaths.length > 0) {
const selectedPath = result.filePaths[0];
if (customJavaPath) {
customJavaPath.value = selectedPath;
}
await saveCustomJavaPath(selectedPath);
}
}
} catch (error) {
console.error('Error browsing Java path:', error);
}
}
async function saveCustomJavaPath(path) {
try {
if (window.electronAPI && window.electronAPI.saveJavaPath) {
await window.electronAPI.saveJavaPath(path);
}
} catch (error) {
console.error('Error saving custom Java path:', error);
}
}
async function loadCustomJavaPath() {
try {
if (window.electronAPI && window.electronAPI.loadJavaPath) {
const savedPath = await window.electronAPI.loadJavaPath();
if (savedPath && savedPath.trim()) {
if (customJavaPath) {
customJavaPath.value = savedPath;
}
if (customJavaCheck) {
customJavaCheck.checked = true;
}
if (customJavaOptions) {
customJavaOptions.style.display = 'block';
}
}
}
} catch (error) {
console.error('Error loading custom Java path:', error);
}
}
window.launch = launch;
window.uninstallGame = uninstallGame;
document.addEventListener('DOMContentLoaded', setupLauncher);

724
GUI/js/mods.js Normal file
View File

@@ -0,0 +1,724 @@
const API_KEY = '$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32';
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
const HYTALE_GAME_ID = 70216;
let installedMods = [];
let browseMods = [];
let searchQuery = '';
let modsPage = 0;
let modsPageSize = 20;
let modsTotalPages = 1;
export async function initModsManager() {
setupModsEventListeners();
await loadInstalledMods();
await loadBrowseMods();
}
function setupModsEventListeners() {
const searchInput = document.getElementById('modsSearch');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value.toLowerCase().trim();
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
modsPage = 0;
loadBrowseMods();
}, 500);
});
}
const myModsBtn = document.getElementById('myModsBtn');
if (myModsBtn) {
myModsBtn.addEventListener('click', openMyModsModal);
}
const closeModalBtn = document.getElementById('closeMyModsModal');
if (closeModalBtn) {
closeModalBtn.addEventListener('click', closeMyModsModal);
}
const modal = document.getElementById('myModsModal');
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeMyModsModal();
}
});
}
const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage');
if (prevPageBtn) {
prevPageBtn.addEventListener('click', () => {
if (modsPage > 0) {
modsPage--;
loadBrowseMods();
}
});
}
if (nextPageBtn) {
nextPageBtn.addEventListener('click', () => {
if (modsPage < modsTotalPages - 1) {
modsPage++;
loadBrowseMods();
}
});
}
}
function openMyModsModal() {
const modal = document.getElementById('myModsModal');
if (modal) {
modal.classList.add('active');
loadInstalledMods();
}
}
function closeMyModsModal() {
const modal = document.getElementById('myModsModal');
if (modal) {
modal.classList.remove('active');
}
}
async function loadInstalledMods() {
try {
const modsPath = await window.electronAPI?.getModsPath();
if (!modsPath) {
showInstalledModsError('Could not get mods directory');
return;
}
const mods = await window.electronAPI?.loadInstalledMods(modsPath);
installedMods = mods || [];
displayInstalledMods(installedMods);
} catch (error) {
console.error('Error loading installed mods:', error);
showInstalledModsError('Failed to load installed mods');
}
}
function displayInstalledMods(mods) {
const modsContainer = document.getElementById('installedModsList');
if (!modsContainer) return;
if (mods.length === 0) {
modsContainer.innerHTML = `
<div class=\"empty-installed-mods\">
<i class=\"fas fa-box-open\"></i>
<h4>No Mods Installed</h4>
<p>Add mods from CurseForge or import local files</p>
</div>
`;
return;
}
modsContainer.innerHTML = mods.map(mod => createInstalledModCard(mod)).join('');
mods.forEach(mod => {
const toggleBtn = document.getElementById(`toggle-installed-${mod.id}`);
const deleteBtn = document.getElementById(`delete-installed-${mod.id}`);
if (toggleBtn) {
toggleBtn.addEventListener('click', () => toggleMod(mod.id));
}
if (deleteBtn) {
deleteBtn.addEventListener('click', () => deleteMod(mod.id));
}
});
}
function createInstalledModCard(mod) {
const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500';
const statusText = mod.enabled ? 'ACTIVE' : 'DISABLED';
const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable';
const toggleBtnText = mod.enabled ? 'DISABLE' : 'ENABLE';
const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play';
return `
<div class="installed-mod-card" data-mod-id="${mod.id}">
<div class="installed-mod-icon">
<i class="fas fa-cube"></i>
</div>
<div class="installed-mod-info">
<div class="installed-mod-header">
<h4 class="installed-mod-name">${mod.name}</h4>
<span class="installed-mod-version">v${mod.version}</span>
</div>
<p class="installed-mod-description">${mod.description || 'No description available'}</p>
</div>
<div class="installed-mod-actions">
<div class="installed-mod-status ${statusClass}">
<i class="fas fa-circle"></i>
${statusText}
</div>
<div class="installed-mod-buttons">
<button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="Delete mod">
<i class="fas fa-trash"></i>
</button>
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
<i class="fas ${toggleIcon}"></i>
${toggleBtnText}
</button>
</div>
</div>
</div>
`;
}
async function loadBrowseMods() {
const browseContainer = document.getElementById('browseModsList');
if (!browseContainer) return;
browseContainer.innerHTML = '<div class=\"loading-mods\"><div class=\"loading-spinner\"></div><span>Loading mods from CurseForge...</span></div>';
try {
if (!API_KEY || API_KEY.length < 10) {
browseContainer.innerHTML = `
<div class=\"empty-browse-mods\">
<i class=\"fas fa-key\"></i>
<h4>API Key Required</h4>
<p>CurseForge API key is needed to browse mods</p>
</div>
`;
return;
}
const offset = modsPage * modsPageSize;
let url = `${CURSEFORGE_API}/mods/search?gameId=${HYTALE_GAME_ID}&pageSize=${modsPageSize}&sortOrder=desc&sortField=6&index=${offset}`;
if (searchQuery && searchQuery.length > 0) {
url += `&searchFilter=${encodeURIComponent(searchQuery)}`;
}
console.log('Fetching mods from page', modsPage + 1, 'offset:', offset, 'search:', searchQuery || 'none', 'URL:', url);
const response = await fetch(url, {
headers: {
'x-api-key': API_KEY,
'Accept': 'application/json'
}
});
console.log('Response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('API Error Response:', errorText);
throw new Error(`CurseForge API error: ${response.status} - ${errorText}`);
}
const data = await response.json();
console.log('API Response data:', data);
console.log('Total mods found:', data.data?.length || 0);
browseMods = (data.data || []).map(mod => ({
id: mod.id.toString(),
name: mod.name,
slug: mod.slug,
summary: mod.summary || 'No description available',
downloadCount: mod.downloadCount || 0,
author: mod.authors?.[0]?.name || 'Unknown',
version: mod.latestFiles?.[0]?.displayName || 'Unknown',
thumbnailUrl: mod.logo?.thumbnailUrl || null,
websiteUrl: mod.links?.websiteUrl || null,
modId: mod.id,
fileId: mod.latestFiles?.[0]?.id,
fileName: mod.latestFiles?.[0]?.fileName,
downloadUrl: mod.latestFiles?.[0]?.downloadUrl
}));
console.log('Processed mods:', browseMods.length);
modsTotalPages = Math.ceil((data.pagination?.totalCount || 1) / modsPageSize);
displayBrowseMods(browseMods);
updatePagination();
} catch (error) {
console.error('Error loading browse mods:', error);
browseContainer.innerHTML = `
<div class=\"empty-browse-mods error\">
<i class=\"fas fa-exclamation-triangle\"></i>
<h4>API Error</h4>
<p>Failed to load mods from CurseForge</p>
<small>${error.message}</small>
</div>
`;
}
}
function displayBrowseMods(mods) {
const browseContainer = document.getElementById('browseModsList');
if (!browseContainer) return;
if (mods.length === 0) {
browseContainer.innerHTML = `
<div class=\"empty-browse-mods\">
<i class=\"fas fa-search\"></i>
<h4>No Mods Found</h4>
<p>Try adjusting your search</p>
</div>
`;
return;
}
browseContainer.innerHTML = mods.map(mod => createBrowseModCard(mod)).join('');
mods.forEach(mod => {
const installBtn = document.getElementById(`install-${mod.id}`);
if (installBtn) {
installBtn.addEventListener('click', () => downloadAndInstallMod(mod));
}
});
}
function createBrowseModCard(mod) {
const isInstalled = installedMods.some(installed =>
installed.name.toLowerCase().includes(mod.name.toLowerCase()) ||
installed.curseForgeId == mod.id
);
return `
<div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\">
<div class=\"mod-image\">
${mod.thumbnailUrl ?
`<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>`
}
</div>
<div class=\"mod-info\">
<div class=\"mod-header\">
<h3 class=\"mod-name\">${mod.name}</h3>
<span class=\"mod-version\">${mod.version}</span>
</div>
<p class=\"mod-description\">${mod.summary}</p>
<div class=\"mod-meta\">
<span class=\"mod-meta-item\">
<i class=\"fas fa-user\"></i>
${mod.author}
</span>
<span class=\"mod-meta-item\">
<i class=\"fas fa-download\"></i>
${formatNumber(mod.downloadCount)}
</span>
</div>
</div>
<div class=\"mod-actions\">
<button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\">
<i class=\"fas fa-external-link-alt\"></i>
VIEW
</button>
${!isInstalled ?
`<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
<i class=\"fas fa-download\"></i>
INSTALL
</button>` :
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
<i class=\"fas fa-check\"></i>
INSTALLED
</button>`
}
</div>
</div>
`;
}
async function downloadAndInstallMod(modInfo) {
try {
window.LauncherUI?.showProgress(`Downloading ${modInfo.name}...`);
const result = await window.electronAPI?.downloadMod(modInfo);
if (result?.success) {
const newMod = {
id: result.modInfo.id,
name: modInfo.name,
version: modInfo.version,
description: modInfo.summary,
author: modInfo.author,
enabled: true,
fileName: result.fileName,
fileSize: result.modInfo.fileSize,
dateInstalled: new Date().toISOString(),
curseForgeId: modInfo.modId,
curseForgeFileId: modInfo.fileId
};
installedMods.push(newMod);
await loadInstalledMods();
await loadBrowseMods();
window.LauncherUI?.hideProgress();
showNotification(`${modInfo.name} installed successfully! 🎉`, 'success');
} else {
throw new Error(result?.error || 'Failed to download mod');
}
} catch (error) {
console.error('Error downloading mod:', error);
window.LauncherUI?.hideProgress();
showNotification('Failed to download mod: ' + error.message, 'error');
}
}
async function toggleMod(modId) {
try {
window.LauncherUI?.showProgress('Toggling mod...');
const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.toggleMod(modId, modsPath);
if (result?.success) {
await loadInstalledMods();
window.LauncherUI?.hideProgress();
} else {
throw new Error(result?.error || 'Failed to toggle mod');
}
} catch (error) {
console.error('Error toggling mod:', error);
window.LauncherUI?.hideProgress();
showNotification('Failed to toggle mod: ' + error.message, 'error');
}
}
async function deleteMod(modId) {
const mod = installedMods.find(m => m.id === modId);
if (!mod) return;
showConfirmModal(
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`,
async () => {
try {
window.LauncherUI?.showProgress('Deleting mod...');
const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.uninstallMod(modId, modsPath);
if (result?.success) {
await loadInstalledMods();
await loadBrowseMods();
window.LauncherUI?.hideProgress();
showNotification(`"${mod.name}" deleted successfully`, 'success');
} else {
throw new Error(result?.error || 'Failed to delete mod');
}
} catch (error) {
console.error('Error deleting mod:', error);
window.LauncherUI?.hideProgress();
showNotification('Failed to delete mod: ' + error.message, 'error');
}
}
);
}
function formatNumber(num) {
if (!num) return '0';
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
function showNotification(message, type = 'info', duration = 4000) {
const existing = document.querySelector(`.mod-notification.${type}`);
if (existing) {
existing.remove();
}
const notification = document.createElement('div');
notification.className = `mod-notification ${type}`;
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
info: 'fa-info-circle',
warning: 'fa-exclamation-triangle'
};
const colors = {
success: '#10b981',
error: '#ef4444',
info: '#3b82f6',
warning: '#f59e0b'
};
notification.innerHTML = `
<div class="notification-content">
<i class="fas ${icons[type]}"></i>
<span>${message}</span>
</div>
<button class="notification-close" onclick="this.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${colors[type]};
color: white;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10000;
min-width: 300px;
max-width: 400px;
transform: translateX(100%);
transition: transform 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: 500;
`;
const contentStyle = `
display: flex;
align-items: center;
gap: 10px;
flex: 1;
`;
const closeStyle = `
background: none;
border: none;
color: white;
cursor: pointer;
padding: 4px;
border-radius: 4px;
opacity: 0.8;
transition: opacity 0.2s;
margin-left: 10px;
`;
notification.querySelector('.notification-content').style.cssText = contentStyle;
notification.querySelector('.notification-close').style.cssText = closeStyle;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 10);
// Auto remove
setTimeout(() => {
if (notification.parentElement) {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
notification.remove();
}, 300);
}
}, duration);
}
// Custom confirmation modal
function showConfirmModal(message, onConfirm, onCancel = null) {
const existingModal = document.querySelector('.mod-confirm-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.className = 'mod-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 = 'mod-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;">Confirm Deletion</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="mod-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;
">Cancel</button>
<button class="mod-confirm-delete" style="
background: #ef4444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">Delete</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('.mod-confirm-cancel');
const deleteBtn = dialog.querySelector('.mod-confirm-delete');
const closeModal = () => {
modal.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
modal.remove();
}, 300);
};
cancelBtn.onclick = () => {
closeModal();
if (onCancel) onCancel();
};
deleteBtn.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);
}
function updatePagination() {
const currentPageEl = document.getElementById('currentPage');
const totalPagesEl = document.getElementById('totalPages');
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
if (currentPageEl) currentPageEl.textContent = modsPage + 1;
if (totalPagesEl) totalPagesEl.textContent = modsTotalPages;
if (prevBtn) {
prevBtn.disabled = modsPage === 0;
prevBtn.style.opacity = modsPage === 0 ? '0.5' : '1';
prevBtn.style.cursor = modsPage === 0 ? 'not-allowed' : 'pointer';
}
if (nextBtn) {
nextBtn.disabled = modsPage >= modsTotalPages - 1;
nextBtn.style.opacity = modsPage >= modsTotalPages - 1 ? '0.5' : '1';
nextBtn.style.cursor = modsPage >= modsTotalPages - 1 ? 'not-allowed' : 'pointer';
}
}
function showInstalledModsError(message) {
const modsContainer = document.getElementById('installedModsList');
if (!modsContainer) return;
modsContainer.innerHTML = `
<div class=\"empty-installed-mods error\">
<i class=\"fas fa-exclamation-triangle\"></i>
<h4>Error</h4>
<p>${message}</p>
</div>
`;
}
function viewModPage(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 })));
const mod = browseMods.find(m => m.id.toString() === modId.toString());
if (mod) {
console.log('Found mod:', mod.name);
let modUrl;
if (mod.websiteUrl && mod.websiteUrl.includes('curseforge.com')) {
modUrl = mod.websiteUrl;
} else if (mod.slug) {
modUrl = `https://www.curseforge.com/hytale/mods/${mod.slug}`;
} else {
const nameSlug = mod.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
modUrl = `https://www.curseforge.com/hytale/mods/${nameSlug}`;
}
console.log('Opening URL:', modUrl);
if (window.electronAPI && window.electronAPI.openExternalLink) {
window.electronAPI.openExternalLink(modUrl);
} else {
if (window.electronAPI && window.electronAPI.shell) {
window.electronAPI.shell.openExternal(modUrl);
} else {
window.open(modUrl, '_blank');
}
}
} else {
console.error('Mod not found with ID:', modId);
showNotification('Mod information not found', 'error');
}
}
window.modsManager = {
toggleMod,
deleteMod,
openMyModsModal,
closeMyModsModal,
viewModPage
};
document.addEventListener('DOMContentLoaded', initModsManager);

124
GUI/js/news.js Normal file
View File

@@ -0,0 +1,124 @@
let newsData = [];
export async function loadNews() {
try {
if (window.electronAPI && window.electronAPI.getHytaleNews) {
try {
const realNews = await window.electronAPI.getHytaleNews();
if (realNews && realNews.length > 0) {
newsData = realNews.slice(0, 10).map((article, index) => ({
id: index + 1,
title: article.title,
summary: article.description,
type: "NEWS",
image: article.imageUrl || '',
date: formatDate(article.date),
url: article.destUrl
}));
displayHomeNews(newsData.slice(0, 5));
displayFullNews(newsData);
} else {
showErrorNews();
}
} catch (error) {
console.log('Failed to load news:', error.message);
showErrorNews();
}
} else {
showErrorNews();
}
} catch (error) {
console.error('Error loading news:', error);
showErrorNews();
}
}
function displayHomeNews(news) {
const newsGrid = document.getElementById('newsGrid');
if (!newsGrid) return;
newsGrid.innerHTML = news.map(article => `
<div class="news-item news-card" onclick="openNewsDetails(${article.id})">
<div class="news-image" style="background-image: url('${article.image}');"></div>
<div class="news-overlay">
<span class="news-type">${article.type}</span>
<span class="news-date">${article.date}</span>
</div>
<div class="news-content">
<h3 class="news-title">${article.title}</h3>
<p class="news-summary">${article.summary}</p>
</div>
</div>
`).join('');
}
function displayFullNews(news) {
const allNewsGrid = document.getElementById('allNewsGrid');
if (!allNewsGrid) return;
allNewsGrid.innerHTML = news.map(article => `
<div class="news-item news-card" onclick="openNewsDetails(${article.id})">
<div class="news-image" style="background-image: url('${article.image}');"></div>
<div class="news-overlay">
<span class="news-type">${article.type}</span>
<span class="news-date">${article.date}</span>
</div>
<div class="news-content">
<h3 class="news-title">${article.title}</h3>
<p class="news-summary">${article.summary}</p>
</div>
</div>
`).join('');
}
function showErrorNews() {
const newsGrid = document.getElementById('newsGrid');
if (newsGrid) {
newsGrid.innerHTML = `
<div class="loading-news">
<i class="fas fa-exclamation-triangle text-4xl mb-4 text-yellow-500"></i>
<span>Unable to load news</span>
</div>
`;
}
}
function openNewsDetails(newsId) {
const article = newsData.find(item => item.id === newsId);
if (article && article.url) {
openNewsArticle(article.url);
} else {
console.log('Opening news article:', article);
}
}
function openNewsArticle(url) {
if (url && url !== '#' && window.electronAPI && window.electronAPI.openExternal) {
window.electronAPI.openExternal(url);
}
}
function formatDate(dateString) {
if (!dateString) return 'RECENTLY';
const date = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now - date);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 1) return '1 DAY AGO';
if (diffDays < 7) return `${diffDays} DAYS AGO`;
if (diffDays < 30) return `${Math.ceil(diffDays / 7)} WEEKS AGO`;
return date.toLocaleDateString();
}
window.openNewsDetails = openNewsDetails;
window.navigateToPage = (page) => {
if (window.LauncherUI) {
window.LauncherUI.showPage(`${page}-page`);
window.LauncherUI.setActiveNav(page);
}
};
document.addEventListener('DOMContentLoaded', loadNews);

154
GUI/js/players.js Normal file
View File

@@ -0,0 +1,154 @@
const API_URL = 'http://3.10.208.30/api';
let updateInterval = null;
let currentUserId = null;
export async function initPlayersCounter() {
setupPlayersCounter();
if (window.electronAPI && window.electronAPI.getUserId) {
currentUserId = await window.electronAPI.getUserId();
} else {
console.error('Electron API not available');
return;
}
let username = 'Player';
if (window.electronAPI.loadUsername) {
const savedUsername = await window.electronAPI.loadUsername();
if (savedUsername) username = savedUsername;
}
await registerPlayer(username, currentUserId);
await fetchPlayerStats();
startAutoUpdate();
}
function setupPlayersCounter() {
const counterElement = document.getElementById('playersOnlineCounter');
if (!counterElement) {
console.warn('Players counter element not found');
}
}
async function fetchPlayerStats() {
try {
const response = await fetch(`${API_URL}/players/stats`);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
updateCounterDisplay(data);
} catch (error) {
console.error('Error fetching player stats:', error);
updateCounterDisplay({ online: 0, peak: 0 });
}
}
function updateCounterDisplay(stats) {
const counterElement = document.getElementById('playersOnlineCounter');
const onlineCount = document.getElementById('onlineCount');
if (onlineCount) {
onlineCount.textContent = stats.online || 0;
}
if (counterElement) {
counterElement.classList.add('updated');
setTimeout(() => {
counterElement.classList.remove('updated');
}, 300);
}
}
async function registerPlayer(username, userId) {
try {
const response = await fetch(`${API_URL}/players/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, userId })
});
if (!response.ok) {
throw new Error(`Failed to register player: ${response.status}`);
}
const data = await response.json();
currentUserId = userId;
console.log('Player registered:', data);
await fetchPlayerStats();
return data;
} catch (error) {
console.error('Error registering player:', error);
return null;
}
}
async function unregisterPlayer(userId) {
try {
const response = await fetch(`${API_URL}/players/unregister`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId })
});
if (!response.ok) {
throw new Error(`Failed to unregister player: ${response.status}`);
}
const data = await response.json();
currentUserId = null;
console.log('Player unregistered:', data);
await fetchPlayerStats();
return data;
} catch (error) {
console.error('Error unregistering player:', error);
return null;
}
}
function startAutoUpdate() {
updateInterval = setInterval(async () => {
await fetchPlayerStats();
if (currentUserId) {
const username = window.LauncherState?.username || 'Player';
await registerPlayer(username, currentUserId);
}
}, 3000);
}
function stopAutoUpdate() {
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = null;
}
}
window.addEventListener('beforeunload', () => {
if (currentUserId) {
const data = JSON.stringify({ userId: currentUserId });
navigator.sendBeacon(`${API_URL}/players/unregister`, data);
}
stopAutoUpdate();
});
window.PlayersAPI = {
register: registerPlayer,
unregister: unregisterPlayer,
fetchStats: fetchPlayerStats
};
document.addEventListener('DOMContentLoaded', initPlayersCounter);

9
GUI/js/script.js Normal file
View File

@@ -0,0 +1,9 @@
import './ui.js';
import './install.js';
import './launcher.js';
import './news.js';
import './mods.js';
import './players.js';
import './chat.js';
import './settings.js';

143
GUI/js/settings.js Normal file
View File

@@ -0,0 +1,143 @@
let customJavaCheck;
let customJavaOptions;
let customJavaPath;
let browseJavaBtn;
let settingsPlayerName;
export function initSettings() {
setupSettingsElements();
loadAllSettings();
}
function setupSettingsElements() {
customJavaCheck = document.getElementById('customJavaCheck');
customJavaOptions = document.getElementById('customJavaOptions');
customJavaPath = document.getElementById('customJavaPath');
browseJavaBtn = document.getElementById('browseJavaBtn');
settingsPlayerName = document.getElementById('settingsPlayerName');
if (customJavaCheck) {
customJavaCheck.addEventListener('change', toggleCustomJava);
}
if (browseJavaBtn) {
browseJavaBtn.addEventListener('click', browseJavaPath);
}
if (settingsPlayerName) {
settingsPlayerName.addEventListener('change', savePlayerName);
}
}
function toggleCustomJava() {
if (!customJavaOptions) return;
if (customJavaCheck && customJavaCheck.checked) {
customJavaOptions.style.display = 'block';
} else {
customJavaOptions.style.display = 'none';
if (customJavaPath) customJavaPath.value = '';
saveCustomJavaPath('');
}
}
async function browseJavaPath() {
try {
if (window.electronAPI && window.electronAPI.browseJavaPath) {
const result = await window.electronAPI.browseJavaPath();
if (result && result.filePaths && result.filePaths.length > 0) {
const selectedPath = result.filePaths[0];
if (customJavaPath) {
customJavaPath.value = selectedPath;
}
await saveCustomJavaPath(selectedPath);
}
}
} catch (error) {
console.error('Error browsing Java path:', error);
}
}
async function saveCustomJavaPath(path) {
try {
if (window.electronAPI && window.electronAPI.saveJavaPath) {
await window.electronAPI.saveJavaPath(path);
}
} catch (error) {
console.error('Error saving custom Java path:', error);
}
}
async function loadCustomJavaPath() {
try {
if (window.electronAPI && window.electronAPI.loadJavaPath) {
const savedPath = await window.electronAPI.loadJavaPath();
if (savedPath && savedPath.trim()) {
if (customJavaPath) {
customJavaPath.value = savedPath;
}
if (customJavaCheck) {
customJavaCheck.checked = true;
}
if (customJavaOptions) {
customJavaOptions.style.display = 'block';
}
}
}
} catch (error) {
console.error('Error loading custom Java path:', error);
}
}
async function savePlayerName() {
try {
if (window.electronAPI && window.electronAPI.saveUsername && settingsPlayerName) {
const playerName = settingsPlayerName.value.trim() || 'Player';
await window.electronAPI.saveUsername(playerName);
}
} catch (error) {
console.error('Error saving player name:', error);
}
}
async function loadPlayerName() {
try {
if (window.electronAPI && window.electronAPI.loadUsername && settingsPlayerName) {
const savedName = await window.electronAPI.loadUsername();
if (savedName) {
settingsPlayerName.value = savedName;
}
}
} catch (error) {
console.error('Error loading player name:', error);
}
}
async function loadAllSettings() {
await loadCustomJavaPath();
await loadPlayerName();
}
export function getCurrentJavaPath() {
if (customJavaCheck && customJavaCheck.checked && customJavaPath) {
return customJavaPath.value.trim();
}
return '';
}
export function getCurrentPlayerName() {
if (settingsPlayerName && settingsPlayerName.value.trim()) {
return settingsPlayerName.value.trim();
}
return 'Player';
}
document.addEventListener('DOMContentLoaded', initSettings);
window.SettingsAPI = {
getCurrentJavaPath,
getCurrentPlayerName
};

469
GUI/js/ui.js Normal file
View File

@@ -0,0 +1,469 @@
let progressOverlay;
let progressBar;
let progressBarFill;
let progressText;
let progressPercent;
let progressSpeed;
let progressSize;
function showPage(pageId) {
const pages = document.querySelectorAll('.page');
pages.forEach(page => {
if (page.id === pageId) {
page.classList.add('active');
page.style.display = '';
} else {
page.classList.remove('active');
page.style.display = 'none';
}
});
}
function setActiveNav(page) {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
if (item.getAttribute('data-page') === page) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
function handleNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
const page = item.getAttribute('data-page');
showPage(`${page}-page`);
setActiveNav(page);
});
});
}
function setupWindowControls() {
const minimizeBtn = document.querySelector('.window-controls .minimize');
const closeBtn = document.querySelector('.window-controls .close');
const windowControls = document.querySelector('.window-controls');
const header = document.querySelector('.header');
if (windowControls) {
windowControls.style.pointerEvents = 'auto';
windowControls.style.zIndex = '10000';
}
if (header) {
header.style.webkitAppRegion = 'drag';
if (windowControls) {
windowControls.style.webkitAppRegion = 'no-drag';
}
}
if (window.electronAPI) {
if (minimizeBtn) {
minimizeBtn.onclick = (e) => {
e.stopPropagation();
window.electronAPI.minimizeWindow();
};
}
if (closeBtn) {
closeBtn.onclick = (e) => {
e.stopPropagation();
window.electronAPI.closeWindow();
};
}
}
}
function showLauncherOrInstall(isInstalled) {
const launcher = document.getElementById('launcher-container');
const install = document.getElementById('install-page');
const sidebar = document.querySelector('.sidebar');
const gameTitle = document.querySelector('.game-title-section');
if (isInstalled) {
if (launcher) launcher.style.display = '';
if (install) install.style.display = 'none';
if (sidebar) sidebar.style.pointerEvents = 'auto';
if (gameTitle) gameTitle.style.display = '';
showPage('play-page');
setActiveNav('play');
} else {
if (launcher) launcher.style.display = 'none';
if (install) {
install.style.display = '';
install.classList.add('active');
}
if (sidebar) sidebar.style.pointerEvents = 'none';
if (gameTitle) gameTitle.style.display = 'none';
const pages = document.querySelectorAll('#launcher-container .page');
pages.forEach(page => page.classList.remove('active'));
}
}
function setupSidebarLogo() {
const logo = document.querySelector('.sidebar-logo img');
if (logo) {
logo.addEventListener('click', () => {
showPage('play-page');
setActiveNav('play');
});
}
}
function showProgress() {
if (progressOverlay) {
progressOverlay.style.display = 'block';
setTimeout(() => {
progressOverlay.style.opacity = '1';
progressOverlay.style.transform = 'translateY(0)';
}, 10);
}
}
function hideProgress() {
if (progressOverlay) {
progressOverlay.style.opacity = '0';
progressOverlay.style.transform = 'translateY(20px)';
setTimeout(() => {
progressOverlay.style.display = 'none';
}, 300);
}
}
function updateProgress(data) {
if (data.message && progressText) {
progressText.textContent = data.message;
}
if (data.percent !== null && data.percent !== undefined) {
const percent = Math.min(100, Math.max(0, Math.round(data.percent)));
if (progressPercent) progressPercent.textContent = `${percent}%`;
if (progressBarFill) progressBarFill.style.width = `${percent}%`;
if (progressBar) progressBar.style.width = `${percent}%`;
}
if (data.speed && data.downloaded && data.total) {
const speedMB = (data.speed / 1024 / 1024).toFixed(2);
const downloadedMB = (data.downloaded / 1024 / 1024).toFixed(2);
const totalMB = (data.total / 1024 / 1024).toFixed(2);
if (progressSpeed) progressSpeed.textContent = `${speedMB} MB/s`;
if (progressSize) progressSize.textContent = `${downloadedMB} / ${totalMB} MB`;
}
}
function setupAnimations() {
document.body.style.opacity = '0';
document.body.style.transform = 'translateY(20px)';
setTimeout(() => {
document.body.style.transition = 'all 0.6s ease';
document.body.style.opacity = '1';
document.body.style.transform = 'translateY(0)';
}, 100);
const style = document.createElement('style');
style.textContent = `
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
document.head.appendChild(style);
}
function setupFirstLaunchHandlers() {
console.log('Setting up first launch handlers...');
window.electronAPI.onFirstLaunchUpdate((data) => {
console.log('Received first launch update event:', data);
showFirstLaunchUpdateDialog(data);
});
window.electronAPI.onFirstLaunchWelcome(() => {
});
window.electronAPI.onFirstLaunchProgress((data) => {
showProgress();
updateProgress(data);
});
window.electronAPI.onLockPlayButton((locked) => {
lockPlayButton(locked);
});
}
function showFirstLaunchUpdateDialog(data) {
console.log('Creating first launch modal...');
const existingModal = document.querySelector('.first-launch-modal-overlay');
if (existingModal) {
existingModal.remove();
}
const modalOverlay = document.createElement('div');
modalOverlay.className = 'first-launch-modal-overlay';
modalOverlay.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
background: rgba(0, 0, 0, 0.95) !important;
backdrop-filter: blur(10px) !important;
z-index: 999999 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
pointer-events: all !important;
`;
const modalDialog = document.createElement('div');
modalDialog.className = 'first-launch-modal-dialog';
modalDialog.style.cssText = `
background: #1a1a1a !important;
border-radius: 12px !important;
padding: 0 !important;
width: 500px !important;
max-width: 90vw !important;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8) !important;
border: 1px solid rgba(147, 51, 234, 0.5) !important;
overflow: hidden !important;
animation: modalSlideIn 0.3s ease-out !important;
`;
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);">
<h2 style="margin: 0; color: #fff; font-size: 1.5rem; font-weight: 600; text-align: center;">
🔄 Game Update Required
</h2>
</div>
<div style="padding: 30px; color: #e5e7eb; line-height: 1.6;">
<div style="text-align: center; margin-bottom: 25px;">
<p style="font-size: 1.1rem; margin-bottom: 15px;">
An existing Hytale installation has been detected and must be updated to the latest version.
</p>
<p style="color: #10b981; font-weight: 500; margin-bottom: 20px;">
✅ Your game saves and settings will be preserved
</p>
</div>
<div style="background: rgba(59, 130, 246, 0.1); padding: 20px; border-radius: 8px; border-left: 4px solid #3b82f6; margin: 20px 0;">
<p style="margin: 8px 0; font-family: 'Courier New', monospace; font-size: 0.9em;">
<strong>📁 Location:</strong> ${data.existingGame.installPath}
</p>
<p style="margin: 8px 0; font-family: 'Courier New', monospace; font-size: 0.9em;">
<strong>💾 UserData:</strong> ${data.existingGame.hasUserData ? '✅ Found (will be preserved)' : '❌ Not found'}
</p>
</div>
<div style="background: rgba(234, 179, 8, 0.1); padding: 15px; border-radius: 8px; border-left: 4px solid #eab308; margin: 20px 0;">
<p style="margin: 0; color: #fbbf24; font-weight: 500; font-size: 0.95em;">
⚠️ This update is mandatory and cannot be skipped
</p>
</div>
</div>
<div style="padding: 25px; border-top: 1px solid rgba(255,255,255,0.1); text-align: center;">
<button id="updateGameBtn" style="
background: linear-gradient(135deg, #9333ea, #3b82f6) !important;
color: white !important;
border: none !important;
padding: 15px 30px !important;
border-radius: 8px !important;
font-size: 1rem !important;
font-weight: 600 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
min-width: 200px !important;
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
🚀 Update Game Now
</button>
</div>
`;
modalOverlay.appendChild(modalDialog);
modalOverlay.onclick = (e) => {
if (e.target === modalOverlay) {
e.preventDefault();
e.stopPropagation();
return false;
}
};
document.addEventListener('keydown', function preventEscape(e) {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
return false;
}
});
document.body.appendChild(modalOverlay);
const updateBtn = document.getElementById('updateGameBtn');
updateBtn.onclick = () => {
acceptFirstLaunchUpdate();
};
window.firstLaunchExistingGame = data.existingGame;
console.log('First launch modal created and displayed');
}
function lockPlayButton(locked) {
const playButton = document.getElementById('homePlayBtn');
if (!playButton) {
console.warn('Play button not found');
return;
}
if (locked) {
playButton.style.opacity = '0.5';
playButton.style.pointerEvents = 'none';
playButton.style.cursor = 'not-allowed';
playButton.setAttribute('data-locked', 'true');
const spanElement = playButton.querySelector('span');
if (spanElement) {
if (!playButton.getAttribute('data-original-text')) {
playButton.setAttribute('data-original-text', spanElement.textContent);
}
spanElement.textContent = 'CHECKING...';
}
console.log('Play button locked');
} else {
playButton.style.opacity = '';
playButton.style.pointerEvents = '';
playButton.style.cursor = '';
playButton.removeAttribute('data-locked');
const spanElement = playButton.querySelector('span');
const originalText = playButton.getAttribute('data-original-text');
if (spanElement && originalText) {
spanElement.textContent = originalText;
playButton.removeAttribute('data-original-text');
}
console.log('Play button unlocked');
}
}
async function acceptFirstLaunchUpdate() {
const existingGame = window.firstLaunchExistingGame;
if (!existingGame) {
showNotification('Error: Game data not found', 'error');
return;
}
const modal = document.querySelector('.first-launch-modal-overlay');
if (modal) {
modal.style.pointerEvents = 'none';
const btn = document.getElementById('updateGameBtn');
if (btn) {
btn.style.opacity = '0.5';
btn.style.cursor = 'not-allowed';
btn.textContent = '🔄 Updating...';
}
}
try {
showProgress();
updateProgress({ message: 'Starting mandatory game update...', percent: 0 });
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
if (modal) {
modal.remove();
}
lockPlayButton(false);
if (result.success) {
hideProgress();
showNotification('Game updated successfully! 🎉', 'success');
} else {
hideProgress();
showNotification(`Update failed: ${result.error}`, 'error');
}
} catch (error) {
if (modal) {
modal.remove();
}
lockPlayButton(false);
hideProgress();
showNotification(`Update error: ${error.message}`, 'error');
}
}
function dismissFirstLaunchDialog() {
const modal = document.querySelector('.first-launch-modal-overlay');
if (modal) {
modal.remove();
}
lockPlayButton(false);
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 100);
setTimeout(() => {
notification.remove();
}, 5000);
}
function setupUI() {
progressOverlay = document.getElementById('progressOverlay');
progressBar = document.getElementById('progressBar');
progressBarFill = document.getElementById('progressBarFill');
progressText = document.getElementById('progressText');
progressPercent = document.getElementById('progressPercent');
progressSpeed = document.getElementById('progressSpeed');
progressSize = document.getElementById('progressSize');
lockPlayButton(true);
handleNavigation();
setupWindowControls();
setupSidebarLogo();
setupAnimations();
setupFirstLaunchHandlers();
document.body.focus();
}
window.LauncherUI = {
showPage,
setActiveNav,
showLauncherOrInstall,
showProgress,
hideProgress,
updateProgress
};
document.addEventListener('DOMContentLoaded', setupUI);

162
GUI/js/update.js Normal file
View File

@@ -0,0 +1,162 @@
class ClientUpdateManager {
constructor() {
this.updatePopupVisible = false;
this.init();
}
init() {
window.electronAPI.onUpdatePopup((updateInfo) => {
this.showUpdatePopup(updateInfo);
});
this.checkForUpdatesOnDemand();
}
showUpdatePopup(updateInfo) {
if (this.updatePopupVisible) return;
this.updatePopupVisible = true;
const popupHTML = `
<div id="update-popup-overlay">
<div class="update-popup-container update-popup-pulse">
<div class="update-popup-header">
<div class="update-popup-icon">
<i class="fas fa-download"></i>
</div>
<h2 class="update-popup-title">
NEW UPDATE AVAILABLE
</h2>
</div>
<div class="update-popup-versions">
<div class="version-row">
<span class="version-label">Current Version:</span>
<span class="version-current">${updateInfo.currentVersion}</span>
</div>
<div class="version-row">
<span class="version-label">New Version:</span>
<span class="version-new">${updateInfo.newVersion}</span>
</div>
</div>
<div class="update-popup-message">
A new version of Hytale F2P Launcher is available.<br>
Please download the latest version to continue using the launcher.
</div>
<button id="update-download-btn" class="update-download-btn">
<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>
Download Update
</button>
<div class="update-popup-footer">
This popup cannot be closed until you update the launcher
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', popupHTML);
this.blockInterface();
const downloadBtn = document.getElementById('update-download-btn');
if (downloadBtn) {
downloadBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
downloadBtn.disabled = true;
downloadBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Opening GitHub...';
try {
await window.electronAPI.openDownloadPage();
console.log('✅ Download page opened, launcher will close...');
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Launcher closing...';
} catch (error) {
console.error('❌ Error opening download page:', error);
downloadBtn.disabled = false;
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update';
}
});
}
const overlay = document.getElementById('update-popup-overlay');
if (overlay) {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
}
console.log('🔔 Update popup displayed with new style');
}
blockInterface() {
const mainContent = document.querySelector('.flex.w-full.h-screen');
if (mainContent) {
mainContent.classList.add('interface-blocked');
}
document.body.classList.add('no-select');
document.addEventListener('keydown', this.blockKeyEvents.bind(this), true);
document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true);
console.log('🚫 Interface blocked for update');
}
blockKeyEvents(event) {
if (event.target.closest('#update-popup-overlay')) {
if ((event.key === 'Enter' || event.key === ' ') &&
event.target.id === 'update-download-btn') {
return;
}
if (event.key !== 'Tab') {
event.preventDefault();
event.stopPropagation();
}
return;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
blockContextMenu(event) {
if (!event.target.closest('#update-popup-overlay')) {
event.preventDefault();
event.stopPropagation();
return false;
}
}
async checkForUpdatesOnDemand() {
try {
const updateInfo = await window.electronAPI.checkForUpdates();
if (updateInfo.updateAvailable) {
this.showUpdatePopup(updateInfo);
}
return updateInfo;
} catch (error) {
console.error('Error checking for updates:', error);
return { updateAvailable: false, error: error.message };
}
}
}
document.addEventListener('DOMContentLoaded', () => {
window.updateManager = new ClientUpdateManager();
});
window.ClientUpdateManager = ClientUpdateManager;

4189
GUI/style.css Normal file

File diff suppressed because it is too large Load Diff

9
Hytale-F2P.desktop Normal file
View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=Hytale-F2P
Comment=A modern, cross-platform launcher for Hytale with automatic updates and multi-client support
Exec=/opt/Hytale-F2P/hytale-f2p-launcher
Categories=Game;
Icon=Hytale-F2P
Terminal=false
StartupNotify=true

31
PKGBUILD Normal file
View File

@@ -0,0 +1,31 @@
# Maintainer: Terromur <terromuroz@proton.me>
pkgname=Hytale-F2P-git
_pkgname=Hytale-F2P
pkgver=2.0.0.r47.gebcfdc4
pkgrel=1
pkgdesc="HyLauncher - unofficial Hytale Launcher for free to play gamers"
arch=('x86_64')
url="https://github.com/amiayweb/Hytale-F2P"
license=('custom')
makedepends=('npm')
source=("git+$url.git" "Hytale-F2P.desktop")
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
pkgver() {
cd "$_pkgname"
printf "2.0.0.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$_pkgname"
npm install
npm run build:linux
}
package() {
mkdir -p "$pkgdir/opt/$_pkgname"
cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname"
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
install -Dm644 "$_pkgname/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png"
}

210
README.md
View File

@@ -1,41 +1,203 @@
# Hytale F2P Launcher
# 🎮 Hytale F2P Launcher | Multiplayer Support
A simple offline launcher for Hytale that allows you to play the game for free.
<div align="center">
## Features
![Version](https://img.shields.io/badge/Version-2.0.0-green?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)
[![Join Discord](https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white)](https://discord.gg/MHkEjepMQ7)
- Offline gameplay support
- Automatic game file download and installation
- Java runtime management
- Clean and modern interface
**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (windows users & non-premium only)**
## Installation
[![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)
### Windows
**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.** 🛑
1. Download the latest release from the [Releases](https://github.com/amiayweb/Hytale-F2P/releases) page
2. Run `Hytale F2P Launcher Setup.exe` to install
3. Launch the application from your desktop or start menu
</div>
### Linux
---
## 📸 Screenshots
<div align="center">
![Hytale F2P Launcher](https://i.imgur.com/9iDuzST.png)
![Hytale F2P Mods](https://i.imgur.com/NaareIS.png)
![Hytale F2P News](https://i.imgur.com/n1nEqRS.png)
![Hytale F2P Chat](https://i.imgur.com/Y4hL3sx.png)
</div>
---
## ✨ Features
🎯 **Core Features**
- 🔄 **Automatic Updates** - Smart version checking and seamless game updates
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
- 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS
-**Java Management** - Automatic Java runtime detection and installation
- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows)
🛡️ **Advanced Features**
- 📁 **Custom Installation** - Choose your own installation directory
- 🔍 **Smart Detection** - Automatic game and dependency detection
- 🗂️ **Mod Support** - Built-in mod management system
- 💬 **Player Chat** - Integrated chat system for community interaction
- 📰 **News Feed** - Stay updated with the latest Hytale news
- 🎨 **Modern UI** - Clean, responsive interface with dark theme
---
## 🚀 Quick Start
### 📥 Installation
#### Windows
1. Download the latest `Hytale-F2P.exe` from [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases)
2. Run the installer
3. Launch from desktop or start menu
#### Linux
See [BUILD.md](BUILD.md) for detailed build instructions.
### macOS
#### macOS
See [BUILD.md](BUILD.md) for detailed build instructions.
## Usage
#### 🖥️ How to create server (Windows Only)?
1. Download the server files directly from: `http://3.10.208.30:3002/server`
2. Replace the existing files in your `HytaleF2P` installation folder
3. Run the server launcher (.bat) to start hosting your own Hytale server
4. You will need a third party software like Radmin VPN (check on youtube how to use Radmin VPN).
1. Enter your player name
2. Click "PLAY"
3. The launcher will automatically download and install required files
4. The game will launch automatically
### 🎮 Usage
## Building from Source
1. **Enter your player name**
2. **Click "PLAY"**
3. **Automatic setup** - The launcher handles everything automatically
4. **Game launches** - Enjoy playing Hytale!
See [BUILD.md](BUILD.md) for detailed build instructions.
---
## 🛠️ Building from Source
See [BUILD.md](BUILD.md) for comprehensive build instructions.
---
## 📋 Changelog
### 🆕 v2.0.0 *(Latest)*
-**Automatic Game Update System** - Smart version checking and seamless updates
-**Partial Automatic Launcher Update System** - This will inform you when I release a new update.
- 🛡️ **UserData Preservation** - Intelligent backup/restore of game saves during updates
- 🐧 **Enhanced Linux Support** - Full Wayland and X11 compatibility
- 🔄 **Multiplayer Auto-Install** - Automatic multiplayer client setup on updates (Windows)
- 📡 **API Integration** - Real-time version checking and client management
- 🎨 **UI Improvements** - Added contributor credits footer
- 🔄 **Complete Launcher Overhaul** - Total redesign of the launcher architecture and interface
- 🗂️ **Integrated Mod Manager** - Built-in mod installation, management
- 💬 **Community Chat System** - Real-time chat for launcher users to connect and communicate
### 🔧 v1.0.1
- 📁 **Custom Installation** - Choose installation directory with file browser
- 🏠 **Always on Top** - Launcher stays visible during installation
- 🧠 **Smart Detection** - Automatic game detection and UI adaptation
- 🗑️ **Uninstall Feature** - Easy game removal with one click
- 🔄 **Dynamic UI** - "INSTALL" vs "PLAY" button based on game state
- 🛠️ **Path Management** - Proper custom directory handling
- 💫 **UI Polish** - Improved layout and overflow prevention
### 🎉 v1.0.0 *(Initial Release)*
- 🎮 **Offline Gameplay** - Play Hytale without internet connection
-**Auto Installation** - One-click game setup
-**Java Management** - Automatic Java runtime handling
- 🎨 **Modern Interface** - Clean, intuitive design
- 🌟 **First Release** - Core launcher functionality
---
## 👥 Contributors
<div align="center">
**Made with ❤️ by the community**
[![Contributors](https://contrib.rocks/image?repo=amiayweb/Hytale-F2P)](https://github.com/amiayweb/Hytale-F2P/graphs/contributors)
</div>
### 🏆 Project Creator
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
### 🌟 Contributors
- [**@chasem-dev**](https://github.com/chasem-dev) - *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*
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
---
## 📊 GitHub Stats
<div align="center">
![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub issues](https://img.shields.io/github/issues/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub downloads](https://img.shields.io/github/downloads/amiayweb/Hytale-F2P/total?style=for-the-badge&logo=github)
</div>
## 📞 Support
<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)
[![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>
---
## ⚖️ Legal Disclaimer
<div align="center">
⚠️ **Important Notice** ⚠️
</div>
This launcher is created for **educational purposes only**.
🏛️ **Not Official** - This is an independent fan project **not affiliated with, endorsed by, or associated with** Hypixel Studios or Hytale.
🛡️ **No Warranty** - This software is provided **"as is"** without any warranty of any kind.
📝 **Responsibility** - The authors take no responsibility for how this software is used.
🛑 **Takedown Policy** - If Hypixel Studios or Hytale requests removal, this project will be taken down immediately.
❤️ **Support Official** - Please support the official game by purchasing it when available.
---
## 📬 Contact
[![Discord](https://img.shields.io/badge/Discord-amiay3-5865F2?logo=discord&logoColor=white)](https://discord.com/users/1433515183606599873)
<div align="center">
**⭐ Star this project if you found it helpful! ⭐**
*Made with ❤️ by [@amiayweb](https://github.com/amiayweb) and the amazing community*
[![Star History Chart](https://api.star-history.com/svg?repos=amiayweb/Hytale-F2P&type=date&legend=top-left)](https://www.star-history.com/#amiayweb/Hytale-F2P&type=date&legend=top-left)
</div>
## Disclaimer
This launcher is for educational purposes only.

2216
backend/launcher.js Normal file

File diff suppressed because it is too large Load Diff

73
backend/updateManager.js Normal file
View File

@@ -0,0 +1,73 @@
const axios = require('axios');
const UPDATE_CHECK_URL = 'http://3.10.208.30:3002/api/version_launcher';
const CURRENT_VERSION = '2.0.0';
const GITHUB_DOWNLOAD_URL = 'https://github.com/amiayweb/Hytale-F2P/';
class UpdateManager {
constructor() {
this.updateAvailable = false;
this.remoteVersion = null;
}
async checkForUpdates() {
try {
console.log('Checking for updates...');
console.log(`Local version: ${CURRENT_VERSION}`);
const response = await axios.get(UPDATE_CHECK_URL, {
timeout: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.launcher_version) {
this.remoteVersion = response.data.launcher_version;
console.log(`Remote version: ${this.remoteVersion}`);
if (this.remoteVersion !== CURRENT_VERSION) {
this.updateAvailable = true;
console.log('Update available!');
return {
updateAvailable: true,
currentVersion: CURRENT_VERSION,
newVersion: this.remoteVersion,
downloadUrl: GITHUB_DOWNLOAD_URL
};
} else {
console.log('Launcher is up to date');
return {
updateAvailable: false,
currentVersion: CURRENT_VERSION,
newVersion: this.remoteVersion
};
}
} else {
throw new Error('Invalid API response');
}
} catch (error) {
console.error('Error checking for updates:', error.message);
return {
updateAvailable: false,
error: error.message,
currentVersion: CURRENT_VERSION
};
}
}
getDownloadUrl() {
return GITHUB_DOWNLOAD_URL;
}
getUpdateInfo() {
return {
updateAvailable: this.updateAvailable,
currentVersion: CURRENT_VERSION,
remoteVersion: this.remoteVersion,
downloadUrl: this.getDownloadUrl()
};
}
}
module.exports = UpdateManager;

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

566
main.js Normal file
View File

@@ -0,0 +1,566 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path');
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 UpdateManager = require('./backend/updateManager');
let mainWindow;
let updateManager;
let discordRPC = null;
// Discord Rich Presence setup
const DISCORD_CLIENT_ID = '1462244937868513373';
function initDiscordRPC() {
try {
const { Client } = require('discord-rpc');
discordRPC = new Client({ transport: 'ipc' });
discordRPC.on('ready', () => {
console.log('Discord RPC connected');
setDiscordActivity();
});
discordRPC.on('disconnected', () => {
console.log('Discord RPC disconnected');
});
discordRPC.login({ clientId: DISCORD_CLIENT_ID }).catch(err => {
console.log('Failed to connect to Discord:', err.message);
});
} catch (error) {
console.log('Discord RPC module not available:', error.message);
}
}
function setDiscordActivity() {
if (!discordRPC) return;
try {
discordRPC.setActivity({
details: 'Using HytaleF2P',
startTimestamp: Date.now(),
largeImageKey: 'hytale_logo',
largeImageText: 'Hytale F2P Launcher',
buttons: [
{
label: 'GitHub',
url: 'https://github.com/amiayweb/Hytale-F2P'
}
]
});
} catch (error) {
console.error('Failed to set Discord activity:', error.message);
}
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 720,
frame: false,
resizable: false,
alwaysOnTop: false,
backgroundColor: '#090909',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
devTools: false,
webSecurity: true
}
});
mainWindow.loadFile('GUI/index.html');
// Initialize Discord Rich Presence
initDiscordRPC();
updateManager = new UpdateManager();
setTimeout(async () => {
const updateInfo = await updateManager.checkForUpdates();
if (updateInfo.updateAvailable) {
mainWindow.webContents.send('show-update-popup', updateInfo);
}
}, 3000);
mainWindow.webContents.on('devtools-opened', () => {
mainWindow.webContents.closeDevTools();
});
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
event.preventDefault();
}
if (input.control && input.shift && input.key.toLowerCase() === 'j') {
event.preventDefault();
}
if (input.control && input.shift && input.key.toLowerCase() === 'c') {
event.preventDefault();
}
if (input.key === 'F12') {
event.preventDefault();
}
if (input.key === 'F5') {
event.preventDefault();
}
});
mainWindow.webContents.on('context-menu', (e) => {
e.preventDefault();
});
mainWindow.webContents.setIgnoreMenuShortcuts(true);
}
app.whenReady().then(async () => {
createWindow();
setTimeout(async () => {
try {
console.log('Starting first launch check...');
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('lock-play-button', true);
}
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total });
}
};
const firstLaunchResult = await handleFirstLaunchCheck(progressCallback);
console.log('First launch check result:', firstLaunchResult);
if (mainWindow && !mainWindow.isDestroyed()) {
if (firstLaunchResult.needsUpdate && firstLaunchResult.existingGame) {
console.log('Sending show-first-launch-update event...');
setTimeout(() => {
mainWindow.webContents.send('show-first-launch-update', {
existingGame: firstLaunchResult.existingGame,
isFirstLaunch: firstLaunchResult.isFirstLaunch
});
}, 1000);
} else if (firstLaunchResult.isFirstLaunch && !firstLaunchResult.existingGame) {
console.log('Sending show-first-launch-welcome event...');
setTimeout(() => {
mainWindow.webContents.send('show-first-launch-welcome');
}, 1000);
} else {
mainWindow.webContents.send('lock-play-button', false);
}
}
} catch (error) {
console.error('Error during first launch check:', error);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('lock-play-button', false);
}
}
}, 3000);
});
app.on('window-all-closed', () => {
// Clean up Discord RPC connection
if (discordRPC) {
try {
discordRPC.destroy();
} catch (error) {
console.log('Error cleaning up Discord RPC:', error.message);
}
}
if (process.platform !== 'darwin') {
app.quit();
}
});
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('progress-update', data);
}
};
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
return result;
} catch (error) {
console.error('Launch error:', error);
const errorMessage = error.message || error.toString();
return { success: false, error: errorMessage };
}
});
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('progress-update', data);
}
};
const result = await installGame(playerName, progressCallback, javaPath, installPath);
return result;
} catch (error) {
console.error('Install error:', error);
const errorMessage = error.message || error.toString();
return { success: false, error: errorMessage };
}
});
ipcMain.handle('save-username', (event, username) => {
saveUsername(username);
return { success: true };
});
ipcMain.handle('load-username', () => {
return loadUsername();
});
ipcMain.handle('save-chat-username', async (event, chatUsername) => {
saveChatUsername(chatUsername);
});
ipcMain.handle('load-chat-username', async () => {
return loadChatUsername();
});
ipcMain.handle('save-java-path', (event, javaPath) => {
saveJavaPath(javaPath);
return { success: true };
});
ipcMain.handle('load-java-path', () => {
return loadJavaPath();
});
ipcMain.handle('save-install-path', (event, installPath) => {
saveInstallPath(installPath);
return { success: true };
});
ipcMain.handle('load-install-path', () => {
return loadInstallPath();
});
ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'],
title: 'Select Installation Folder'
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('accept-first-launch-update', async (event, existingGame) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('first-launch-progress', data);
}
};
const result = await proposeGameUpdate(existingGame, progressCallback);
return result;
} catch (error) {
console.error('First launch update error:', error);
const errorMessage = error.message || error.toString();
return { success: false, error: errorMessage };
}
});
ipcMain.handle('mark-as-launched', async () => {
try {
markAsLaunched();
return { success: true };
} catch (error) {
console.error('Mark as launched error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('is-game-installed', () => {
return isGameInstalled();
});
ipcMain.handle('uninstall-game', async () => {
try {
await uninstallGame();
return { success: true };
} catch (error) {
console.error('Uninstall error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-hytale-news', async () => {
try {
const news = await getHytaleNews();
return news;
} catch (error) {
console.error('News fetch error:', error);
return [];
}
});
ipcMain.handle('open-external', async (event, url) => {
try {
await shell.openExternal(url);
return { success: true };
} catch (error) {
console.error('Failed to open external URL:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('browse-java-path', async () => {
const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
let dialogOptions;
if (isWindows) {
dialogOptions = {
properties: ['openFile'],
title: 'Select Java Executable',
filters: [
{ name: 'Java Executable', extensions: ['exe'] },
{ name: 'All Files', extensions: ['*'] }
]
};
} else if (isMac) {
dialogOptions = {
properties: ['openFile'],
title: 'Select Java Executable',
message: 'Select java executable (usually in /Library/Java/JavaVirtualMachines/*/Contents/Home/bin/java)',
filters: [
{ name: 'All Files', extensions: ['*'] }
]
};
} else {
dialogOptions = {
properties: ['openFile'],
title: 'Select Java Executable',
message: 'Select java executable (usually /usr/bin/java or similar)',
filters: [
{ name: 'All Files', extensions: ['*'] }
]
};
}
const result = await dialog.showOpenDialog(mainWindow, dialogOptions);
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('save-settings', async (event, settings) => {
try {
if (settings.playerName) saveUsername(settings.playerName);
if (settings.javaPath !== undefined) saveJavaPath(settings.javaPath);
if (settings.installPath !== undefined) saveInstallPath(settings.installPath);
return { success: true };
} catch (error) {
console.error('Save settings error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('load-settings', async () => {
try {
return {
playerName: loadUsername() || 'Player',
javaPath: loadJavaPath() || '',
installPath: loadInstallPath() || '',
customInstall: false
};
} catch (error) {
console.error('Load settings error:', error);
return {
playerName: 'Player',
javaPath: '',
installPath: '',
customInstall: false
};
}
});
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod } = require('./backend/launcher');
const os = require('os');
ipcMain.handle('get-local-app-data', async () => {
return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
});
ipcMain.handle('get-user-id', async () => {
try {
const { getOrCreatePlayerId } = require('./backend/launcher');
return await getOrCreatePlayerId();
} catch (error) {
console.error('Error getting user ID:', error);
return null;
}
});
ipcMain.handle('load-installed-mods', async (event, modsPath) => {
try {
return await loadInstalledMods(modsPath);
} catch (error) {
console.error('Error loading installed mods:', error);
return [];
}
});
ipcMain.handle('openExternalLink', async (event, url) => {
try {
console.log('Opening external URL:', url);
await shell.openExternal(url);
return { success: true };
} catch (error) {
console.error('Error opening external link:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('download-mod', async (event, modInfo) => {
try {
return await downloadMod(modInfo);
} catch (error) {
console.error('Error downloading mod:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('uninstall-mod', async (event, modId, modsPath) => {
try {
return await uninstallMod(modId, modsPath);
} catch (error) {
console.error('Error uninstalling mod:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('toggle-mod', async (event, modId, modsPath) => {
try {
return await toggleMod(modId, modsPath);
} catch (error) {
console.error('Error toggling mod:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-mods-path', async () => {
try {
return await getModsPath();
} catch (error) {
console.error('Error getting mods path:', error);
return null;
}
});
ipcMain.handle('select-mod-files', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile', 'multiSelections'],
title: 'Select Mod Files',
filters: [
{ name: 'Mod Files', extensions: ['jar', 'zip'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths;
}
return null;
});
ipcMain.handle('copy-mod-file', async (event, sourcePath, modsPath) => {
try {
const fileName = path.basename(sourcePath);
const destPath = path.join(modsPath, fileName);
fs.copyFileSync(sourcePath, destPath);
return { success: true, fileName };
} catch (error) {
console.error('Error copying mod file:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('check-for-updates', async () => {
try {
return await updateManager.checkForUpdates();
} catch (error) {
console.error('Error checking for updates:', error);
return { updateAvailable: false, error: error.message };
}
});
ipcMain.handle('open-download-page', async () => {
try {
await shell.openExternal(updateManager.getDownloadUrl());
setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
}, 1000);
return { success: true };
} catch (error) {
console.error('Error opening download page:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-update-info', async () => {
return updateManager.getUpdateInfo();
});
ipcMain.handle('window-close', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
});
ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.minimize();
}
});

1
package-lock.json generated Normal file
View File

@@ -0,0 +1 @@

121
package.json Normal file
View File

@@ -0,0 +1,121 @@
{
"name": "hytale-f2p-launcher",
"version": "2.0.0",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --dev",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux",
"build:mac": "electron-builder --mac",
"build:all": "electron-builder --win --linux --mac"
},
"keywords": [
"hytale",
"launcher",
"game",
"client",
"cross-platform",
"electron",
"auto-update",
"mod-manager",
"chat"
],
"author": {
"name": "AMIAY",
"email": "support@amiay.dev"
},
"license": "MIT",
"devDependencies": {
"electron": "^40.0.0",
"electron-builder": "^26.4.0"
},
"dependencies": {
"adm-zip": "^0.5.10",
"axios": "^1.6.0",
"discord-rpc": "^4.0.1",
"tar": "6.2.1",
"uuid": "^9.0.1"
},
"overrides": {
"tar": "$tar"
},
"build": {
"appId": "com.hytalef2p.launcher",
"productName": "Hytale F2P",
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"backend/**/*",
"GUI/**/*",
"package.json"
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
},
{
"target": "portable",
"arch": [
"x64"
]
}
],
"icon": "icon.ico"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64"
]
}
],
"icon": "build/icon.png",
"category": "Game",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support"
},
"mac": {
"target": [
{
"target": "dmg",
"arch": [
"universal"
]
},
{
"target": "zip",
"arch": [
"universal"
]
}
],
"icon": "build/icon.icns",
"category": "public.app-category.games"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

58
preload.js Normal file
View File

@@ -0,0 +1,58 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
launchGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath),
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
closeWindow: () => ipcRenderer.invoke('window-close'),
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
loadUsername: () => ipcRenderer.invoke('load-username'),
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
loadChatUsername: () => ipcRenderer.invoke('load-chat-username'),
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
openExternal: (url) => ipcRenderer.invoke('open-external', url),
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
loadSettings: () => ipcRenderer.invoke('load-settings'),
getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'),
getModsPath: () => ipcRenderer.invoke('get-mods-path'),
loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath),
downloadMod: (modInfo) => ipcRenderer.invoke('download-mod', modInfo),
uninstallMod: (modId, modsPath) => ipcRenderer.invoke('uninstall-mod', modId, modsPath),
toggleMod: (modId, modsPath) => ipcRenderer.invoke('toggle-mod', modId, modsPath),
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
onProgressUpdate: (callback) => {
ipcRenderer.on('progress-update', (event, data) => callback(data));
},
getUserId: () => ipcRenderer.invoke('get-user-id'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
onUpdatePopup: (callback) => {
ipcRenderer.on('show-update-popup', (event, data) => callback(data));
},
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
onFirstLaunchUpdate: (callback) => {
ipcRenderer.on('show-first-launch-update', (event, data) => callback(data));
},
onFirstLaunchWelcome: (callback) => {
ipcRenderer.on('show-first-launch-welcome', () => callback());
},
onFirstLaunchProgress: (callback) => {
ipcRenderer.on('first-launch-progress', (event, data) => callback(data));
},
onLockPlayButton: (callback) => {
ipcRenderer.on('lock-play-button', (event, locked) => callback(locked));
}
});