mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 11:41:49 -03:00
Compare commits
84 Commits
fix/steamd
...
fix/uuid-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14dcf3dac3 | ||
|
|
34a3e40bd2 | ||
|
|
d15f2e2ceb | ||
|
|
74f99d0aaf | ||
|
|
7b2acd49b6 | ||
|
|
7b9951e72d | ||
|
|
e81a0167c1 | ||
|
|
50c04b64df | ||
|
|
28e5fa35e1 | ||
|
|
52e7eafe0b | ||
|
|
3de5c2eaa3 | ||
|
|
9c9b71bd4c | ||
|
|
c4bb15ce91 | ||
|
|
5147e1856f | ||
|
|
8719cd3138 | ||
|
|
611d436085 | ||
|
|
d5cc0868e9 | ||
|
|
a21e7e4910 | ||
|
|
14a63febc1 | ||
|
|
2cdef44fec | ||
|
|
f8cf41972d | ||
|
|
ea0f87c46a | ||
|
|
a5b3fe02c8 | ||
|
|
0bb82a0b3d | ||
|
|
eccdcf223e | ||
|
|
a09b082152 | ||
|
|
f1d01ac78c | ||
|
|
bfe0156606 | ||
|
|
78e97bdbb7 | ||
|
|
769bc2054c | ||
|
|
5337441d97 | ||
|
|
12453d2dda | ||
|
|
803df90fb6 | ||
|
|
6c31c39abd | ||
|
|
b5ab8b78e8 | ||
|
|
343f7b8016 | ||
|
|
fa568fcce7 | ||
|
|
a6ecd2c167 | ||
|
|
3e1c4aef73 | ||
|
|
1c14c3f603 | ||
|
|
30a4327655 | ||
|
|
33a0e219fc | ||
|
|
fbdd9ee0cf | ||
|
|
22ea2f56d3 | ||
|
|
5039bcdadf | ||
|
|
4db8016a28 | ||
|
|
e0fd7e6900 | ||
|
|
93a2a98028 | ||
|
|
4775e9adbd | ||
|
|
90db069e4c | ||
|
|
baa585d6b3 | ||
|
|
a5b930a9f0 | ||
|
|
b708f4a7d7 | ||
|
|
28a4f65f21 | ||
|
|
966de83ead | ||
|
|
bc7f46cf45 | ||
|
|
534b3f1f34 | ||
|
|
a07f0f1de1 | ||
|
|
bf29112848 | ||
|
|
0e4e332dab | ||
|
|
779f6820cb | ||
|
|
4fc4d77415 | ||
|
|
de193e991f | ||
|
|
d69695e499 | ||
|
|
4fff87f221 | ||
|
|
4cd76bb96d | ||
|
|
721d287036 | ||
|
|
e491bf1a84 | ||
|
|
89f981b586 | ||
|
|
9cf504bbcc | ||
|
|
e7110936d8 | ||
|
|
79456e43a6 | ||
|
|
dd2dbc6f08 | ||
|
|
c4acb32fcd | ||
|
|
fbcbafb9b5 | ||
|
|
86ed33358c | ||
|
|
9ec97f9d33 | ||
|
|
ee18455b4b | ||
|
|
a5c931b26d | ||
|
|
661a0c9eed | ||
|
|
6bd63f5b60 | ||
|
|
da186333cb | ||
|
|
663ac5f834 | ||
|
|
ae375f9b6e |
22
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
22
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -41,17 +41,17 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: screenshots
|
id: proof
|
||||||
attributes:
|
attributes:
|
||||||
label: Screenshots
|
label: Screenshots/Recordings
|
||||||
description: If applicable, add screenshots to help explain your problem.
|
description: If applicable, add Screenshots/Recordings to help explain your problem.
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version of the launcher are you running?
|
description: What version of the launcher are you running?
|
||||||
placeholder: "e.g. \"v2.0.11 stable/pre-release\""
|
placeholder: "e.g. \"v2.2.0 stable\""
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Hardware Specification
|
label: Hardware Specification
|
||||||
description: Tell us your CPU, iGPU, dGPU, VRAM, and RAM information.
|
description: Tell us your CPU, iGPU, dGPU, VRAM, and RAM information.
|
||||||
placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 | VRAM: 24 GB | RAM: 32 GB"
|
placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 24 GB VRAM | RAM: 32 GB"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -70,13 +70,11 @@ body:
|
|||||||
label: Operating System
|
label: Operating System
|
||||||
description: What operating system are you using?
|
description: What operating system are you using?
|
||||||
options:
|
options:
|
||||||
- Windows 10
|
- Windows 11/10
|
||||||
- Windows 11
|
- macOS (Apple Silicon, M1/M2/M3)
|
||||||
- macOS (Apple Silicon)
|
- Linux Ubuntu/Debian-based (Linux Mint, Pop!_OS, etc.)
|
||||||
- macOS (Intel)
|
- Linux Fedora/RHEL-based (Fedora, CentOS, etc.)
|
||||||
- Linux Ubuntu/Debian-based
|
- Linux Arch-based (Steamdeck, CachyOS, etc.)
|
||||||
- Linux Fedora/RHEL-based
|
|
||||||
- Linux Arch-based
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
47
.github/ISSUE_TEMPLATE/support_request.yml
vendored
47
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -1,8 +1,22 @@
|
|||||||
name: Support Request
|
name: Support Request
|
||||||
description: Request help or support
|
description: Request help or support
|
||||||
title: "[SUPPORT] "
|
title: "[SUPPORT] <ADD YOUR TITLE HERE>"
|
||||||
labels: ["support"]
|
labels: ["support"]
|
||||||
body:
|
body:
|
||||||
|
- type: dropdown
|
||||||
|
id: acknowledge
|
||||||
|
attributes:
|
||||||
|
label: Checklist
|
||||||
|
options:
|
||||||
|
- label: I have read the README.md before asking Support Request.
|
||||||
|
required: true
|
||||||
|
- label: I have read the TROUBLESHOOTING.md before asking Support Request.
|
||||||
|
required: true
|
||||||
|
- label: I have added title before submitting this Support Request.
|
||||||
|
required: true
|
||||||
|
- label: I acknowledge that my Support Request will not be responded as quick as in Discord Open-A-Ticket, I prefer this way.
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
@@ -24,10 +38,16 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Context
|
label: Context
|
||||||
description: Provide any relevant context or background information.
|
description: Provide any relevant context or background information.
|
||||||
placeholder: "I've tried..., but got..."
|
placeholder: "I've tried these steps, but got..."
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proof
|
||||||
|
attributes:
|
||||||
|
label: Screenshots/Recordings
|
||||||
|
description: If applicable, add Screenshots/Recordings to help explain your problem.
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: hardwarespec
|
id: hardwarespec
|
||||||
attributes:
|
attributes:
|
||||||
@@ -37,12 +57,17 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: dropdown
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version are you using?
|
description: What launcher version are you using?
|
||||||
placeholder: "e.g. v2.0.11 stable/pre-release"
|
options:
|
||||||
|
- v2.2.0
|
||||||
|
- v2.1.1
|
||||||
|
- v2.1.0
|
||||||
|
- v2.0.11
|
||||||
|
- v2.0.2
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -52,13 +77,11 @@ body:
|
|||||||
label: Platform
|
label: Platform
|
||||||
description: What platform are you using?
|
description: What platform are you using?
|
||||||
options:
|
options:
|
||||||
- Windows 10
|
- Windows 11/10
|
||||||
- Windows 11
|
- macOS (Apple Silicon, M1/M2/M3)
|
||||||
- macOS (Apple Silicon)
|
- Linux Ubuntu/Debian-based (Linux Mint, Pop!_OS, etc.)
|
||||||
- macOS (Intel)
|
- Linux Fedora/RHEL-based (Fedora, CentOS, etc.)
|
||||||
- Linux Ubuntu/Debian-based
|
- Linux Arch-based (Steamdeck, CachyOS, etc.)
|
||||||
- Linux Fedora/RHEL-based
|
|
||||||
- Linux Arch-based
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -2,8 +2,6 @@ name: Build and Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -66,7 +64,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Linux Packages
|
- name: Build Linux Packages
|
||||||
run: |
|
run: |
|
||||||
npx electron-builder --linux AppImage deb rpm --x64 --arm64 --publish never
|
npx electron-builder --linux AppImage deb rpm --publish never
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-builds
|
name: linux-builds
|
||||||
@@ -114,14 +112,19 @@ jobs:
|
|||||||
makepkg -s --noconfirm
|
makepkg -s --noconfirm
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
- name: Fix permissions for upload
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
sudo chown -R $(id -u):$(id -g) .
|
||||||
|
|
||||||
- name: Upload Arch Package
|
- name: Upload Arch Package
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: arch-package
|
name: arch-package
|
||||||
path: |
|
path: |
|
||||||
*.pkg.tar.zst
|
*.pkg.tar.zst
|
||||||
*.src.tar.zst
|
|
||||||
.SRCINFO
|
.SRCINFO
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [build-windows, build-macos, build-linux, build-arch]
|
needs: [build-windows, build-macos, build-linux, build-arch]
|
||||||
@@ -135,7 +138,6 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# FIX: './package.json' Module Not Found in `Get version` step
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -155,10 +157,6 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
# If it's a tag, use the tag.
|
|
||||||
# tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
|
||||||
# If it's the 'release' branch, use 'v2.0.2-beta.r42'
|
|
||||||
# name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}-beta.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
|
||||||
files: |
|
files: |
|
||||||
artifacts/arch-package/*.pkg.tar.zst
|
artifacts/arch-package/*.pkg.tar.zst
|
||||||
artifacts/arch-package/*.src.tar.zst
|
artifacts/arch-package/*.src.tar.zst
|
||||||
|
|||||||
340
GUI/index.html
340
GUI/index.html
@@ -35,6 +35,10 @@
|
|||||||
<i class="fas fa-play"></i>
|
<i class="fas fa-play"></i>
|
||||||
<span class="nav-tooltip" data-i18n="nav.play">Play</span>
|
<span class="nav-tooltip" data-i18n="nav.play">Play</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item" data-page="featured">
|
||||||
|
<i class="fas fa-server"></i>
|
||||||
|
<span class="nav-tooltip">Featured Servers</span>
|
||||||
|
</div>
|
||||||
<div class="nav-item" data-page="mods">
|
<div class="nav-item" data-page="mods">
|
||||||
<i class="fas fa-box"></i>
|
<i class="fas fa-box"></i>
|
||||||
<span class="nav-tooltip" data-i18n="nav.mods">Mods</span>
|
<span class="nav-tooltip" data-i18n="nav.mods">Mods</span>
|
||||||
@@ -43,10 +47,6 @@
|
|||||||
<i class="fas fa-newspaper"></i>
|
<i class="fas fa-newspaper"></i>
|
||||||
<span class="nav-tooltip" data-i18n="nav.news">News</span>
|
<span class="nav-tooltip" data-i18n="nav.news">News</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" data-page="chat">
|
|
||||||
<i class="fas fa-comments"></i>
|
|
||||||
<span class="nav-tooltip" data-i18n="nav.chat">Players Chat</span>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item" data-page="settings">
|
<div class="nav-item" data-page="settings">
|
||||||
<i class="fas fa-cog"></i>
|
<i class="fas fa-cog"></i>
|
||||||
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
|
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
|
||||||
@@ -55,6 +55,10 @@
|
|||||||
<i class="fas fa-terminal"></i>
|
<i class="fas fa-terminal"></i>
|
||||||
<span class="nav-tooltip">Logs</span>
|
<span class="nav-tooltip">Logs</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item" onclick="openDiscordExternal()">
|
||||||
|
<i class="fab fa-discord"></i>
|
||||||
|
<span class="nav-tooltip">Discord</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +116,7 @@
|
|||||||
<h1 class="install-title">
|
<h1 class="install-title">
|
||||||
HY<span class="title-accent">TALE</span>
|
HY<span class="title-accent">TALE</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="install-subtitle" data-i18n="install.title">FREE TO PLAY LAUNCHER</p>
|
<p class="install-subtitle" data-i18n="install.title">UNOFFICIAL HYTALE LAUNCHER</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="install-form">
|
<div class="install-form">
|
||||||
@@ -120,7 +124,7 @@
|
|||||||
<label class="form-label" data-i18n="install.playerName">Player Name</label>
|
<label class="form-label" data-i18n="install.playerName">Player Name</label>
|
||||||
<input type="text" id="installPlayerName"
|
<input type="text" id="installPlayerName"
|
||||||
data-i18n-placeholder="install.playerNamePlaceholder" class="form-input"
|
data-i18n-placeholder="install.playerNamePlaceholder" class="form-input"
|
||||||
value="Player" />
|
value="Player" maxlength="16" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -212,6 +216,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="featured-page" class="page">
|
||||||
|
<div class="featured-layout">
|
||||||
|
<div class="featured-left">
|
||||||
|
<div class="featured-header">
|
||||||
|
<h2 class="featured-title">
|
||||||
|
<i class="fas fa-star mr-2"></i>
|
||||||
|
<span>FEATURED SERVERS</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div id="featuredServersList" class="featured-list">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="featured-right">
|
||||||
|
<div class="featured-header">
|
||||||
|
<h2 class="featured-title">
|
||||||
|
<i class="fas fa-server mr-2"></i>
|
||||||
|
<span>HF2P SERVERS</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div id="myServersList" class="featured-list">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="mods-page" class="page">
|
<div id="mods-page" class="page">
|
||||||
<div class="mods-header">
|
<div class="mods-header">
|
||||||
<div class="mods-search-container">
|
<div class="mods-search-container">
|
||||||
@@ -256,50 +292,6 @@
|
|||||||
<div id="allNewsGrid" class="news-grid-full"></div>
|
<div id="allNewsGrid" class="news-grid-full"></div>
|
||||||
</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>
|
|
||||||
<span data-i18n="chat.title">PLAYERS CHAT</span>
|
|
||||||
</h2>
|
|
||||||
<div class="chat-header-actions">
|
|
||||||
<button id="chatColorBtn" class="chat-color-btn">
|
|
||||||
<i class="fas fa-palette"></i>
|
|
||||||
<span data-i18n="chat.pickColor">Color</span>
|
|
||||||
</button>
|
|
||||||
<div class="chat-online-badge">
|
|
||||||
<i class="fas fa-circle"></i>
|
|
||||||
<span id="chatOnlineCount">0</span> <span data-i18n="chat.online">online</span>
|
|
||||||
</div>
|
|
||||||
</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"
|
|
||||||
data-i18n-placeholder="chat.inputPlaceholder" 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>
|
|
||||||
<span data-i18n="chat.secureChat">Secure chat - Links are censored</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="settings-page" class="page">
|
<div id="settings-page" class="page">
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<div class="settings-header">
|
<div class="settings-header">
|
||||||
@@ -310,6 +302,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
|
<div class="settings-column">
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-gamepad"></i>
|
<i class="fas fa-gamepad"></i>
|
||||||
@@ -406,14 +399,55 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="settings-hint">
|
<p class="settings-hint">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux:
|
<span data-i18n="settings.gpuHint">Laptop-only feature; set to Integrated if on PC</span>
|
||||||
affects DRI_PRIME)</span>
|
|
||||||
</p>
|
</p>
|
||||||
<div id="gpu-detection-info" class="gpu-detection-info"></div>
|
<div id="gpu-detection-info" class="gpu-detection-info"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3 class="settings-section-title">
|
||||||
|
<i class="fas fa-coffee"></i>
|
||||||
|
<span data-i18n="settings.java">Java Runtime</span>
|
||||||
|
</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" data-i18n="settings.useCustomJava">Use
|
||||||
|
Custom Java Path</div>
|
||||||
|
<div class="checkbox-description" data-i18n="settings.javaDescription">
|
||||||
|
Override the bundled Java runtime with
|
||||||
|
your own installation</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
|
||||||
|
<div class="settings-input-group">
|
||||||
|
<label class="settings-input-label" data-i18n="settings.javaPath">Java
|
||||||
|
Executable Path</label>
|
||||||
|
<div class="settings-input-with-button">
|
||||||
|
<input type="text" id="customJavaPath" class="settings-input"
|
||||||
|
data-i18n-placeholder="settings.javaPathPlaceholder" readonly />
|
||||||
|
<button id="browseJavaBtn" class="settings-browse-btn">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
<span data-i18n="settings.javaBrowse">Browse</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="settings-hint">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span data-i18n="settings.javaHint">Select the Java installation folder
|
||||||
|
(supports Windows, Mac, Linux)</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-column">
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-fingerprint"></i>
|
<i class="fas fa-fingerprint"></i>
|
||||||
@@ -458,28 +492,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
|
||||||
<h3 class="settings-section-title">
|
|
||||||
<i class="fab fa-discord"></i>
|
|
||||||
<span data-i18n="settings.discord">Discord Integration</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<label class="settings-checkbox">
|
|
||||||
<input type="checkbox" id="discordRPCCheck" checked />
|
|
||||||
<span class="checkmark"></span>
|
|
||||||
<div class="checkbox-content">
|
|
||||||
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable
|
|
||||||
Discord Rich Presence</div>
|
|
||||||
<div class="checkbox-description" data-i18n="settings.discordDescription">
|
|
||||||
Show your launcher activity
|
|
||||||
on Discord
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-window-close"></i>
|
<i class="fas fa-window-close"></i>
|
||||||
@@ -515,48 +527,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="settings-section">
|
|
||||||
<h3 class="settings-section-title">
|
|
||||||
<i class="fas fa-coffee"></i>
|
|
||||||
<span data-i18n="settings.java">Java Runtime</span>
|
|
||||||
</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" data-i18n="settings.useCustomJava">Use
|
|
||||||
Custom Java Path</div>
|
|
||||||
<div class="checkbox-description" data-i18n="settings.javaDescription">
|
|
||||||
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" data-i18n="settings.javaPath">Java
|
|
||||||
Executable Path</label>
|
|
||||||
<div class="settings-input-with-button">
|
|
||||||
<input type="text" id="customJavaPath" class="settings-input"
|
|
||||||
data-i18n-placeholder="settings.javaPathPlaceholder" readonly />
|
|
||||||
<button id="browseJavaBtn" class="settings-browse-btn">
|
|
||||||
<i class="fas fa-folder-open"></i>
|
|
||||||
<span data-i18n="settings.javaBrowse">Browse</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="settings-hint">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
<span data-i18n="settings.javaHint">Select the Java installation folder
|
|
||||||
(supports Windows, Mac, Linux)</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-language"></i>
|
<i class="fas fa-language"></i>
|
||||||
@@ -573,6 +543,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3 class="settings-section-title">
|
||||||
|
<i class="fab fa-discord"></i>
|
||||||
|
<span data-i18n="settings.discord">Discord Integration</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<label class="settings-checkbox">
|
||||||
|
<input type="checkbox" id="discordRPCCheck" checked />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<div class="checkbox-content">
|
||||||
|
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable
|
||||||
|
Discord Rich Presence</div>
|
||||||
|
<div class="checkbox-description" data-i18n="settings.discordDescription">
|
||||||
|
Show your launcher activity
|
||||||
|
on Discord
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -659,41 +653,6 @@
|
|||||||
</div>
|
</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>
|
|
||||||
<span data-i18n="chat.joinChat">Join Chat</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div class="chat-username-modal-body">
|
|
||||||
<p class="chat-username-modal-description" data-i18n="chat.chooseUsername">
|
|
||||||
Choose a username to join the Players Chat
|
|
||||||
</p>
|
|
||||||
<div class="chat-username-input-group">
|
|
||||||
<label for="chatUsernameInput" class="chat-username-label"
|
|
||||||
data-i18n="chat.username">Username</label>
|
|
||||||
<input type="text" id="chatUsernameInput" class="chat-username-input"
|
|
||||||
data-i18n-placeholder="chat.usernamePlaceholder" maxlength="20" autocomplete="off" />
|
|
||||||
<span class="chat-username-hint" data-i18n="chat.usernameHint">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>
|
|
||||||
<span data-i18n="common.cancel">Cancel</span>
|
|
||||||
</button>
|
|
||||||
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
|
|
||||||
<i class="fas fa-check"></i>
|
|
||||||
<span data-i18n="chat.joinButton">Join Chat</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- UUID Management Modal -->
|
<!-- UUID Management Modal -->
|
||||||
<div id="uuidModal" class="uuid-modal" style="display: none;">
|
<div id="uuidModal" class="uuid-modal" style="display: none;">
|
||||||
<div class="uuid-modal-content">
|
<div class="uuid-modal-content">
|
||||||
@@ -787,7 +746,6 @@
|
|||||||
|
|
||||||
<div class="version-display-bottom">
|
<div class="version-display-bottom">
|
||||||
<i class="fas fa-code-branch"></i>
|
<i class="fas fa-code-branch"></i>
|
||||||
<span id="launcherVersion">Loading...</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
|
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
|
||||||
@@ -818,71 +776,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="js/script.js"></script> <!-- Discord Notification -->
|
<script type="module" src="js/script.js"></script>
|
||||||
<div id="discordNotification" class="discord-notification">
|
|
||||||
<div class="notification-content">
|
<div id="discordPopupModal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-content discord-popup-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="discord-popup-header">
|
||||||
<i class="fab fa-discord"></i>
|
<i class="fab fa-discord"></i>
|
||||||
<span class="notification-text" data-i18n="discord.notificationText">Join our Discord community!</span>
|
<h2 class="modal-title">Join Our Discord Community</h2>
|
||||||
<button class="notification-action"
|
|
||||||
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
|
|
||||||
<span data-i18n="discord.joinButton">Join Discord</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="notification-close" onclick="closeDiscordNotification()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="discord-popup-body">
|
||||||
|
<p class="discord-popup-text">
|
||||||
|
Join our community of over <strong>5000 members</strong> and stay connected!
|
||||||
|
</p>
|
||||||
|
<p class="discord-popup-text">
|
||||||
|
Get the latest news, updates, and announcements about the launcher.
|
||||||
|
</p>
|
||||||
|
<p class="discord-popup-text">
|
||||||
|
Find help, report bugs, share your feedback, and connect with other players.
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Modal pour sélectionner la couleur du chat -->
|
<div class="discord-popup-actions">
|
||||||
<div id="chatColorModal" class="chat-color-modal" style="display: none;">
|
<button class="discord-popup-btn primary" onclick="joinDiscord()">
|
||||||
<div class="chat-color-modal-content">
|
<i class="fab fa-discord"></i>
|
||||||
<div class="chat-color-modal-header">
|
Join Discord
|
||||||
<h3 class="chat-color-modal-title">
|
</button>
|
||||||
<i class="fas fa-palette"></i>
|
<button class="discord-popup-btn secondary" onclick="closeDiscordPopup()">
|
||||||
<span data-i18n="chat.colorModal.title">Customize Username Color</span>
|
Maybe Later
|
||||||
</h3>
|
|
||||||
<button class="modal-close-btn" onclick="closeChatColorModal()">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-color-modal-body">
|
|
||||||
<div id="solidColorSection" class="color-section">
|
|
||||||
<h4 data-i18n="chat.colorModal.chooseSolid">Choose a solid color:</h4>
|
|
||||||
<div class="predefined-colors">
|
|
||||||
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
|
|
||||||
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
|
|
||||||
<div class="color-option" data-color="#2ecc71" style="background: #2ecc71;"></div>
|
|
||||||
<div class="color-option" data-color="#f39c12" style="background: #f39c12;"></div>
|
|
||||||
<div class="color-option" data-color="#9b59b6" style="background: #9b59b6;"></div>
|
|
||||||
<div class="color-option" data-color="#1abc9c" style="background: #1abc9c;"></div>
|
|
||||||
<div class="color-option" data-color="#e91e63" style="background: #e91e63;"></div>
|
|
||||||
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-color-input">
|
|
||||||
<label for="customColor" data-i18n="chat.colorModal.customColor">Custom color:</label>
|
|
||||||
<input type="color" id="customColor" value="#3498db">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="color-preview">
|
|
||||||
<h4 data-i18n="chat.colorModal.preview">Preview:</h4>
|
|
||||||
<div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">
|
|
||||||
YourUsername</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chat-color-modal-footer">
|
|
||||||
<button class="btn-secondary" onclick="closeChatColorModal()"><span
|
|
||||||
data-i18n="common.cancel">Cancel</span></button>
|
|
||||||
<button class="btn-primary" onclick="applyChatColor()"><span data-i18n="chat.colorModal.apply">Apply
|
|
||||||
Color</span></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
|
<script src="js/featured.js"></script>
|
||||||
<script type="module" src="js/settings.js"></script>
|
<script type="module" src="js/settings.js"></script>
|
||||||
<script type="module" src="js/update.js"></script>
|
<script type="module" src="js/update.js"></script>
|
||||||
<script src="js/updater.js"></script>
|
<!-- updater.js disabled - using update.js instead which has skip button and macOS handling -->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
500
GUI/js/chat.js
500
GUI/js/chat.js
@@ -1,500 +0,0 @@
|
|||||||
|
|
||||||
let socket = null;
|
|
||||||
let isAuthenticated = false;
|
|
||||||
let messageQueue = [];
|
|
||||||
let chatUsername = '';
|
|
||||||
let userColor = '#3498db';
|
|
||||||
let userBadge = null;
|
|
||||||
const SOCKET_URL = 'https://chat.hytalef2p.com';
|
|
||||||
const MAX_MESSAGE_LENGTH = 500;
|
|
||||||
|
|
||||||
async function getOrCreatePlayerId() {
|
|
||||||
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initChat() {
|
|
||||||
if (window.electronAPI?.loadChatUsername) {
|
|
||||||
chatUsername = await window.electronAPI.loadChatUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.electronAPI?.loadChatColor) {
|
|
||||||
const savedColor = await window.electronAPI.loadChatColor();
|
|
||||||
if (savedColor) {
|
|
||||||
userColor = savedColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chatUsername || chatUsername.trim() === '') {
|
|
||||||
showUsernameModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupChatUI();
|
|
||||||
setupColorSelector();
|
|
||||||
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', async () => {
|
|
||||||
console.log('Connected to chat server');
|
|
||||||
|
|
||||||
const uuid = await window.electronAPI?.getCurrentUuid();
|
|
||||||
|
|
||||||
socket.emit('authenticate', {
|
|
||||||
username: chatUsername,
|
|
||||||
userId,
|
|
||||||
uuid: uuid,
|
|
||||||
userColor: userColor
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('authenticated', (data) => {
|
|
||||||
isAuthenticated = true;
|
|
||||||
userBadge = data.badge;
|
|
||||||
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, data.userColor, data.badge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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, userColor = '#3498db', badge = null) {
|
|
||||||
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'
|
|
||||||
});
|
|
||||||
|
|
||||||
let badgeHTML = '';
|
|
||||||
if (badge) {
|
|
||||||
let badgeStyle = '';
|
|
||||||
if (badge.style === 'rainbow') {
|
|
||||||
badgeStyle = `background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #ffeaa7, #fab1a0, #fd79a8); background-size: 400% 400%; animation: rainbow 3s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`;
|
|
||||||
} else if (badge.style === 'gradient') {
|
|
||||||
if (badge.badge === 'CONTRIBUTOR') {
|
|
||||||
badgeStyle = `background: linear-gradient(45deg, #22c55e, #16a34a); background-size: 200% 200%; animation: contributorGlow 2s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`;
|
|
||||||
} else {
|
|
||||||
badgeStyle = `color: ${badge.color}; font-weight: bold; display: inline;`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
badgeHTML = `<span class="user-badge" style="${badgeStyle}">[${badge.badge}]</span> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageDiv.innerHTML = `
|
|
||||||
<div class="message-header">
|
|
||||||
<span class="message-user-info">${badgeHTML}<span class="message-username" style="font-weight: bold;" data-username-color="${userColor}">${escapeHtml(username)}</span></span>
|
|
||||||
<span class="message-time">${time}</span>
|
|
||||||
</div>
|
|
||||||
<div class="message-content">${message}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const usernameElement = messageDiv.querySelector('.message-username');
|
|
||||||
if (usernameElement) {
|
|
||||||
applyUserColorStyle(usernameElement, userColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupColorSelector() {
|
|
||||||
const colorBtn = document.getElementById('chatColorBtn');
|
|
||||||
if (colorBtn) {
|
|
||||||
colorBtn.addEventListener('click', showChatColorModal);
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorOptions = document.querySelectorAll('.color-option');
|
|
||||||
colorOptions.forEach(option => {
|
|
||||||
option.addEventListener('click', () => {
|
|
||||||
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
|
|
||||||
option.classList.add('selected');
|
|
||||||
updateColorPreview();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const customColor = document.getElementById('customColor');
|
|
||||||
if (customColor) {
|
|
||||||
customColor.addEventListener('input', () => {
|
|
||||||
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
|
|
||||||
updateColorPreview();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showChatColorModal() {
|
|
||||||
const modal = document.getElementById('chatColorModal');
|
|
||||||
if (modal) {
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
updateColorPreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.closeChatColorModal = function() {
|
|
||||||
const modal = document.getElementById('chatColorModal');
|
|
||||||
if (modal) {
|
|
||||||
modal.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateColorPreview() {
|
|
||||||
const preview = document.getElementById('colorPreview');
|
|
||||||
if (!preview) return;
|
|
||||||
|
|
||||||
const selectedOption = document.querySelector('.color-option.selected');
|
|
||||||
let color = '#3498db';
|
|
||||||
|
|
||||||
if (selectedOption) {
|
|
||||||
color = selectedOption.dataset.color;
|
|
||||||
} else {
|
|
||||||
const customColor = document.getElementById('customColor');
|
|
||||||
if (customColor) color = customColor.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
preview.style.color = color;
|
|
||||||
preview.style.background = 'transparent';
|
|
||||||
preview.style.webkitBackgroundClip = 'initial';
|
|
||||||
preview.style.webkitTextFillColor = 'initial';
|
|
||||||
}
|
|
||||||
|
|
||||||
window.applyChatColor = async function() {
|
|
||||||
let newColor;
|
|
||||||
|
|
||||||
const selectedOption = document.querySelector('.color-option.selected');
|
|
||||||
if (selectedOption) {
|
|
||||||
newColor = selectedOption.dataset.color;
|
|
||||||
} else {
|
|
||||||
const customColor = document.getElementById('customColor');
|
|
||||||
newColor = customColor ? customColor.value : '#3498db';
|
|
||||||
}
|
|
||||||
|
|
||||||
userColor = newColor;
|
|
||||||
|
|
||||||
if (window.electronAPI?.saveChatColor) {
|
|
||||||
await window.electronAPI.saveChatColor(newColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socket && isAuthenticated) {
|
|
||||||
const uuid = await window.electronAPI?.getCurrentUuid();
|
|
||||||
socket.emit('authenticate', {
|
|
||||||
username: chatUsername,
|
|
||||||
userId: await getOrCreatePlayerId(),
|
|
||||||
uuid: uuid,
|
|
||||||
userColor: userColor
|
|
||||||
});
|
|
||||||
|
|
||||||
addSystemMessage('Username color updated successfully', 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
closeChatColorModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyUserColorStyle(element, color) {
|
|
||||||
element.style.color = color;
|
|
||||||
element.style.background = 'transparent';
|
|
||||||
element.style.webkitBackgroundClip = 'initial';
|
|
||||||
element.style.webkitTextFillColor = 'initial';
|
|
||||||
}
|
|
||||||
|
|
||||||
window.ChatAPI = {
|
|
||||||
send: sendMessage,
|
|
||||||
disconnect: () => socket?.disconnect()
|
|
||||||
};
|
|
||||||
176
GUI/js/featured.js
Normal file
176
GUI/js/featured.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
// Featured Servers Management
|
||||||
|
const FEATURED_SERVERS_API = 'https://assets.authbp.xyz/featured.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely escape HTML while preserving UTF-8 characters
|
||||||
|
*/
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and display featured servers
|
||||||
|
*/
|
||||||
|
async function loadFeaturedServers() {
|
||||||
|
const featuredContainer = document.getElementById('featuredServersList');
|
||||||
|
const myServersContainer = document.getElementById('myServersList');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[FeaturedServers] Fetching from', FEATURED_SERVERS_API);
|
||||||
|
|
||||||
|
// Fetch featured servers from API (no cache)
|
||||||
|
const response = await fetch(FEATURED_SERVERS_API, {
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Accept-Charset': 'utf-8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
const featuredServers = data.featuredServers || [];
|
||||||
|
|
||||||
|
console.log('[FeaturedServers] Loaded', featuredServers.length, 'featured servers');
|
||||||
|
|
||||||
|
// Render featured servers
|
||||||
|
if (featuredServers.length === 0) {
|
||||||
|
featuredContainer.innerHTML = `
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-info-circle fa-2x"></i>
|
||||||
|
<p>No featured servers</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
const featuredHTML = featuredServers.map((server, index) => {
|
||||||
|
console.log(`[FeaturedServers] Building featured card ${index + 1}:`, server.Name);
|
||||||
|
|
||||||
|
const escapedName = escapeHtml(server.Name || 'Unknown Server');
|
||||||
|
const escapedAddress = escapeHtml(server.Address || '');
|
||||||
|
const bannerUrl = server.img_Banner || 'https://via.placeholder.com/400x240/1e293b/ffffff?text=Server+Banner';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="featured-server-card">
|
||||||
|
<img
|
||||||
|
src="${bannerUrl}"
|
||||||
|
alt="${escapedName}"
|
||||||
|
class="featured-server-banner"
|
||||||
|
onerror="this.src='https://via.placeholder.com/400x240/1e293b/ffffff?text=Server'"
|
||||||
|
/>
|
||||||
|
<div class="featured-server-content">
|
||||||
|
<h3 class="featured-server-name">${escapedName}</h3>
|
||||||
|
<div class="featured-server-address">
|
||||||
|
<span class="server-address-text">${escapedAddress}</span>
|
||||||
|
<button class="copy-address-btn" onclick="copyServerAddress('${escapedAddress}', this)">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
<span>Copy</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
featuredContainer.innerHTML = featuredHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show "Coming Soon" for my servers
|
||||||
|
myServersContainer.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #94a3b8; font-size: 1.2rem;">
|
||||||
|
<p>Coming Soon</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[FeaturedServers] Error loading servers:', error);
|
||||||
|
featuredContainer.innerHTML = `
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<i class="fas fa-exclamation-triangle fa-2x" style="color: #ef4444;"></i>
|
||||||
|
<p>Failed to load servers</p>
|
||||||
|
<p style="font-size: 0.9rem; color: #64748b;">${error.message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
myServersContainer.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #94a3b8; font-size: 1.2rem;">
|
||||||
|
<p>Coming Soon</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy server address to clipboard
|
||||||
|
*/
|
||||||
|
async function copyServerAddress(address, button) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(address);
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
const originalHTML = button.innerHTML;
|
||||||
|
button.classList.add('copied');
|
||||||
|
button.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.classList.remove('copied');
|
||||||
|
button.innerHTML = originalHTML;
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
console.log('[FeaturedServers] Copied address:', address);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[FeaturedServers] Failed to copy address:', error);
|
||||||
|
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = address;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-999999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
const originalHTML = button.innerHTML;
|
||||||
|
button.classList.add('copied');
|
||||||
|
button.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.classList.remove('copied');
|
||||||
|
button.innerHTML = originalHTML;
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[FeaturedServers] Fallback copy also failed:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load featured servers when the featured page becomes visible
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||||
|
const featuredPage = document.getElementById('featured-page');
|
||||||
|
if (featuredPage && featuredPage.classList.contains('active')) {
|
||||||
|
loadFeaturedServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const featuredPage = document.getElementById('featured-page');
|
||||||
|
if (featuredPage) {
|
||||||
|
observer.observe(featuredPage, { attributes: true });
|
||||||
|
|
||||||
|
// Load immediately if already visible
|
||||||
|
if (featuredPage.classList.contains('active')) {
|
||||||
|
loadFeaturedServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -4,13 +4,15 @@ const i18n = (() => {
|
|||||||
let translations = {};
|
let translations = {};
|
||||||
const availableLanguages = [
|
const availableLanguages = [
|
||||||
{ code: 'en', name: 'English' },
|
{ code: 'en', name: 'English' },
|
||||||
{ code: 'fr', name: 'Français' },
|
{ code: 'de-DE', name: 'German (Germany)' },
|
||||||
{ code: 'de', name: 'Deutsch' },
|
{ code: 'es-ES', name: 'Spanish (Spain)' },
|
||||||
{ code: 'sv', name: 'Svenska' },
|
{ code: 'fr-FR', name: 'French (France)' },
|
||||||
{ code: 'es-ES', name: 'Español (España)' },
|
{ code: 'pl-PL', name: 'Polish (Poland)' },
|
||||||
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
|
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
|
||||||
|
{ code: 'ru-RU', name: 'Russian (Russia)' },
|
||||||
|
{ code: 'sv-SE', name: 'Swedish (Sweden)' },
|
||||||
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
|
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
|
||||||
{ code: 'pl-PL', name: 'Polish (Poland)' }
|
{ code: 'id-ID', name: 'Indonesian (Indonesia)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Load single language file
|
// Load single language file
|
||||||
|
|||||||
@@ -45,9 +45,17 @@ export function setupInstallation() {
|
|||||||
export async function installGame() {
|
export async function installGame() {
|
||||||
if (isDownloading || (installBtn && installBtn.disabled)) return;
|
if (isDownloading || (installBtn && installBtn.disabled)) return;
|
||||||
|
|
||||||
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
|
let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
|
||||||
const installPath = installPathInput ? installPathInput.value.trim() : '';
|
const installPath = installPathInput ? installPathInput.value.trim() : '';
|
||||||
|
|
||||||
|
// Limit player name to 16 characters
|
||||||
|
if (playerName.length > 16) {
|
||||||
|
playerName = playerName.substring(0, 16);
|
||||||
|
if (installPlayerName) {
|
||||||
|
installPlayerName.value = playerName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked');
|
const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked');
|
||||||
const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release';
|
const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release';
|
||||||
|
|
||||||
@@ -72,8 +80,11 @@ export async function installGame() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
|
// Sync player name to both launcher and settings inputs
|
||||||
const playerNameInput = document.getElementById('playerName');
|
const playerNameInput = document.getElementById('playerName');
|
||||||
if (playerNameInput) playerNameInput.value = playerName;
|
if (playerNameInput) playerNameInput.value = playerName;
|
||||||
|
const settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
|
if (settingsPlayerName) settingsPlayerName.value = playerName;
|
||||||
resetInstallButton();
|
resetInstallButton();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@@ -125,8 +136,11 @@ function simulateInstallation(playerName) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
|
// Sync player name to both launcher and settings inputs
|
||||||
const playerNameInput = document.getElementById('playerName');
|
const playerNameInput = document.getElementById('playerName');
|
||||||
if (playerNameInput) playerNameInput.value = playerName;
|
if (playerNameInput) playerNameInput.value = playerName;
|
||||||
|
const settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
|
if (settingsPlayerName) settingsPlayerName.value = playerName;
|
||||||
resetInstallButton();
|
resetInstallButton();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@@ -188,7 +202,16 @@ export async function browseInstallPath() {
|
|||||||
async function savePlayerName() {
|
async function savePlayerName() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.saveSettings) {
|
if (window.electronAPI && window.electronAPI.saveSettings) {
|
||||||
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
|
let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
|
||||||
|
|
||||||
|
// Limit player name to 16 characters
|
||||||
|
if (playerName.length > 16) {
|
||||||
|
playerName = playerName.substring(0, 16);
|
||||||
|
if (installPlayerName) {
|
||||||
|
installPlayerName.value = playerName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await window.electronAPI.saveSettings({ playerName });
|
await window.electronAPI.saveSettings({ playerName });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -246,9 +269,3 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
setupInstallation();
|
setupInstallation();
|
||||||
await checkGameStatusAndShowInterface();
|
await checkGameStatusAndShowInterface();
|
||||||
});
|
});
|
||||||
window.browseInstallPath = browseInstallPath;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
setupInstallation();
|
|
||||||
await checkGameStatusAndShowInterface();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -194,16 +194,67 @@ window.switchProfile = async (id) => {
|
|||||||
export async function launch() {
|
export async function launch() {
|
||||||
if (isDownloading || (playBtn && playBtn.disabled)) return;
|
if (isDownloading || (playBtn && playBtn.disabled)) return;
|
||||||
|
|
||||||
let playerName = 'Player';
|
// ==========================================================================
|
||||||
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
|
// STEP 1: Check launch readiness from backend (single source of truth)
|
||||||
playerName = window.SettingsAPI.getCurrentPlayerName();
|
// ==========================================================================
|
||||||
} else if (playerNameInput && playerNameInput.value.trim()) {
|
let launchState = null;
|
||||||
playerName = playerNameInput.value.trim();
|
let playerName = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.checkLaunchReady) {
|
||||||
|
launchState = await window.electronAPI.checkLaunchReady();
|
||||||
|
playerName = launchState?.username;
|
||||||
|
} else if (window.electronAPI && window.electronAPI.loadUsername) {
|
||||||
|
// Fallback to loadUsername if checkLaunchReady not available
|
||||||
|
playerName = await window.electronAPI.loadUsername();
|
||||||
|
launchState = { ready: !!playerName, hasUsername: !!playerName, username: playerName, issues: [] };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Launcher] Error checking launch readiness:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate launch readiness
|
||||||
|
if (!launchState?.ready || !playerName) {
|
||||||
|
const issues = launchState?.issues || ['No username configured'];
|
||||||
|
const errorMsg = window.i18n
|
||||||
|
? window.i18n.t('errors.noUsername')
|
||||||
|
: 'Please set your username in Settings before playing.';
|
||||||
|
|
||||||
|
console.error('[Launcher] Launch blocked:', issues.join(', '));
|
||||||
|
|
||||||
|
// Show error to user
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||||
|
window.LauncherUI.showError(errorMsg);
|
||||||
|
} else {
|
||||||
|
alert(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to settings if possible
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showPage) {
|
||||||
|
window.LauncherUI.showPage('settings-page');
|
||||||
|
window.LauncherUI.setActiveNav('settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if using default 'Player' name (shouldn't happen with new logic, but keep as safety)
|
||||||
|
if (playerName === 'Player') {
|
||||||
|
console.warn('[Launcher] Warning: Using default username "Player"');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Launcher] Launching game for: "${playerName}"`);
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 2: Load other settings from backend
|
||||||
|
// ==========================================================================
|
||||||
let javaPath = '';
|
let javaPath = '';
|
||||||
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
|
try {
|
||||||
javaPath = window.SettingsAPI.getCurrentJavaPath();
|
if (window.electronAPI && window.electronAPI.loadJavaPath) {
|
||||||
|
javaPath = await window.electronAPI.loadJavaPath() || '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Launcher] Error loading Java path:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
let gpuPreference = 'auto';
|
let gpuPreference = 'auto';
|
||||||
@@ -212,9 +263,12 @@ export async function launch() {
|
|||||||
gpuPreference = await window.electronAPI.loadGpuPreference();
|
gpuPreference = await window.electronAPI.loadGpuPreference();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading GPU preference:', error);
|
console.error('[Launcher] Error loading GPU preference:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 3: Start launch process
|
||||||
|
// ==========================================================================
|
||||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
if (playBtn) {
|
if (playBtn) {
|
||||||
@@ -227,6 +281,7 @@ export async function launch() {
|
|||||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.launchGame) {
|
if (window.electronAPI && window.electronAPI.launchGame) {
|
||||||
|
// Pass playerName from config - backend will validate again
|
||||||
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
||||||
|
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
@@ -243,7 +298,35 @@ export async function launch() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Launch failed:', result.error);
|
console.error('[Launcher] Launch failed:', result.error);
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (result.needsUsername) {
|
||||||
|
const errorMsg = window.i18n
|
||||||
|
? window.i18n.t('errors.noUsername')
|
||||||
|
: 'Please set your username in Settings before playing.';
|
||||||
|
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||||
|
window.LauncherUI.showError(errorMsg);
|
||||||
|
} else {
|
||||||
|
alert(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to settings
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showPage) {
|
||||||
|
window.LauncherUI.showPage('settings-page');
|
||||||
|
window.LauncherUI.setActiveNav('settings');
|
||||||
|
}
|
||||||
|
} else if (result.error) {
|
||||||
|
// Show generic error
|
||||||
|
const errorMsg = window.i18n
|
||||||
|
? window.i18n.t('errors.launchFailed').replace('{error}', result.error)
|
||||||
|
: `Launch failed: ${result.error}`;
|
||||||
|
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||||
|
window.LauncherUI.showError(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
@@ -260,7 +343,13 @@ export async function launch() {
|
|||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
}
|
}
|
||||||
resetPlayButton();
|
resetPlayButton();
|
||||||
console.error('Launch error:', error);
|
console.error('[Launcher] Launch error:', error);
|
||||||
|
|
||||||
|
// Show error to user
|
||||||
|
const errorMsg = error.message || 'Unknown launch error';
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||||
|
window.LauncherUI.showError(errorMsg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,66 @@ import './launcher.js';
|
|||||||
import './news.js';
|
import './news.js';
|
||||||
import './mods.js';
|
import './mods.js';
|
||||||
import './players.js';
|
import './players.js';
|
||||||
import './chat.js';
|
|
||||||
import './settings.js';
|
import './settings.js';
|
||||||
import './logs.js';
|
import './logs.js';
|
||||||
|
|
||||||
// Initialize i18n immediately (before DOMContentLoaded)
|
|
||||||
let i18nInitialized = false;
|
let i18nInitialized = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
const savedLang = await window.electronAPI?.loadLanguage();
|
const savedLang = await window.electronAPI?.loadLanguage();
|
||||||
await i18n.init(savedLang);
|
await i18n.init(savedLang);
|
||||||
i18nInitialized = true;
|
i18nInitialized = true;
|
||||||
|
|
||||||
// Update language selector if DOM is already loaded
|
|
||||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
updateLanguageSelector();
|
updateLanguageSelector();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
async function checkDiscordPopup() {
|
||||||
|
try {
|
||||||
|
const config = await window.electronAPI?.loadConfig();
|
||||||
|
if (!config || config.discordPopup === undefined || config.discordPopup === false) {
|
||||||
|
const modal = document.getElementById('discordPopupModal');
|
||||||
|
if (modal) {
|
||||||
|
const buttons = modal.querySelectorAll('.discord-popup-btn');
|
||||||
|
buttons.forEach(btn => btn.disabled = true);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
modal.classList.add('active');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
buttons.forEach(btn => btn.disabled = false);
|
||||||
|
}, 2000);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check Discord popup:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.closeDiscordPopup = function() {
|
||||||
|
const modal = document.getElementById('discordPopupModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('active');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.joinDiscord = async function() {
|
||||||
|
await window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electronAPI?.saveConfig({ discordPopup: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save Discord popup state:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDiscordPopup();
|
||||||
|
};
|
||||||
|
|
||||||
function updateLanguageSelector() {
|
function updateLanguageSelector() {
|
||||||
const langSelect = document.getElementById('languageSelect');
|
const langSelect = document.getElementById('languageSelect');
|
||||||
if (langSelect) {
|
if (langSelect) {
|
||||||
@@ -51,32 +94,9 @@ function updateLanguageSelector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Populate language selector (wait for i18n if needed)
|
|
||||||
if (i18nInitialized) {
|
if (i18nInitialized) {
|
||||||
updateLanguageSelector();
|
updateLanguageSelector();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discord notification
|
checkDiscordPopup();
|
||||||
const notification = document.getElementById('discordNotification');
|
|
||||||
if (notification) {
|
|
||||||
const dismissed = localStorage.getItem('discordNotificationDismissed');
|
|
||||||
if (!dismissed) {
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.style.display = 'flex';
|
|
||||||
}, 3000);
|
|
||||||
} else {
|
|
||||||
notification.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.closeDiscordNotification = function() {
|
|
||||||
const notification = document.getElementById('discordNotification');
|
|
||||||
if (notification) {
|
|
||||||
notification.classList.add('hidden');
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.style.display = 'none';
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
localStorage.setItem('discordNotificationDismissed', 'true');
|
|
||||||
};
|
|
||||||
@@ -439,10 +439,34 @@ async function savePlayerName() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.electronAPI.saveUsername(playerName);
|
if (playerName.length > 16) {
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.playerNameTooLong') : 'Player name must be 16 characters or less';
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
settingsPlayerName.value = playerName.substring(0, 16);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await window.electronAPI.saveUsername(playerName);
|
||||||
|
|
||||||
|
// Check if save was successful
|
||||||
|
if (result && result.success === false) {
|
||||||
|
console.error('[Settings] Failed to save username:', result.error);
|
||||||
|
const errorMsg = window.i18n
|
||||||
|
? window.i18n.t('notifications.playerNameSaveFailed')
|
||||||
|
: `Failed to save player name: ${result.error || 'Unknown error'}`;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
|
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
|
||||||
showNotification(successMsg, 'success');
|
showNotification(successMsg, 'success');
|
||||||
|
|
||||||
|
// Refresh UUID display since it may have changed for the new username
|
||||||
|
await loadCurrentUuid();
|
||||||
|
|
||||||
|
// Also refresh the UUID list to update which entry is marked as current
|
||||||
|
await loadAllUuids();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving player name:', error);
|
console.error('Error saving player name:', error);
|
||||||
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
|
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
|
||||||
@@ -566,11 +590,26 @@ export function getCurrentJavaPath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current player name from UI input
|
||||||
|
* Returns null if no name is set (caller must handle this)
|
||||||
|
* NOTE: launcher.js now loads username directly from backend config
|
||||||
|
* This function is used for display purposes only
|
||||||
|
*/
|
||||||
export function getCurrentPlayerName() {
|
export function getCurrentPlayerName() {
|
||||||
if (settingsPlayerName && settingsPlayerName.value.trim()) {
|
if (settingsPlayerName && settingsPlayerName.value.trim()) {
|
||||||
return settingsPlayerName.value.trim();
|
return settingsPlayerName.value.trim();
|
||||||
}
|
}
|
||||||
return 'Player';
|
// Return null instead of 'Player' - caller must handle missing username
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current player name with fallback for display purposes only
|
||||||
|
* DO NOT use this for launching game - use backend loadUsername() instead
|
||||||
|
*/
|
||||||
|
export function getCurrentPlayerNameForDisplay() {
|
||||||
|
return getCurrentPlayerName() || 'Player';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.openGameLocation = openGameLocation;
|
window.openGameLocation = openGameLocation;
|
||||||
@@ -580,6 +619,7 @@ document.addEventListener('DOMContentLoaded', initSettings);
|
|||||||
window.SettingsAPI = {
|
window.SettingsAPI = {
|
||||||
getCurrentJavaPath,
|
getCurrentJavaPath,
|
||||||
getCurrentPlayerName,
|
getCurrentPlayerName,
|
||||||
|
getCurrentPlayerNameForDisplay,
|
||||||
reloadBranch: loadVersionBranch
|
reloadBranch: loadVersionBranch
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -722,6 +762,9 @@ async function loadAllUuids() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="uuid-item-actions">
|
<div class="uuid-item-actions">
|
||||||
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
|
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
|
||||||
|
${!mapping.isCurrent ? `<button class="uuid-item-btn switch" onclick="switchToUsername('${escapeHtml(mapping.username)}')" title="Switch to this identity">
|
||||||
|
<i class="fas fa-user-check"></i>
|
||||||
|
</button>` : ''}
|
||||||
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
|
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -806,7 +849,17 @@ async function setCustomUuid() {
|
|||||||
async function performSetCustomUuid(uuid) {
|
async function performSetCustomUuid(uuid) {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
||||||
const username = getCurrentPlayerName();
|
// IMPORTANT: Use saved username from config, not unsaved DOM input
|
||||||
|
// This prevents setting UUID for wrong user if username field was edited but not saved
|
||||||
|
let username = null;
|
||||||
|
if (window.electronAPI.loadUsername) {
|
||||||
|
username = await window.electronAPI.loadUsername();
|
||||||
|
}
|
||||||
|
if (!username) {
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.noUsername') : 'No username configured. Please save your username first.';
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -843,6 +896,73 @@ window.copyUuid = async function (uuid) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different username/UUID identity
|
||||||
|
* This changes the active username to use that username's UUID
|
||||||
|
*/
|
||||||
|
window.switchToUsername = async function (username) {
|
||||||
|
try {
|
||||||
|
const message = window.i18n
|
||||||
|
? window.i18n.t('confirm.switchUsernameMessage').replace('{username}', username)
|
||||||
|
: `Switch to username "${username}"? This will change your active player identity.`;
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.switchUsernameTitle') : 'Switch Identity';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.switchUsernameButton') : 'Switch';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
|
showCustomConfirm(
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
async () => {
|
||||||
|
await performSwitchToUsername(username);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
confirmBtn,
|
||||||
|
cancelBtn
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in switchToUsername:', error);
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.switchUsernameFailed') : 'Failed to switch username';
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function performSwitchToUsername(username) {
|
||||||
|
try {
|
||||||
|
if (!window.electronAPI || !window.electronAPI.saveUsername) {
|
||||||
|
throw new Error('API not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await window.electronAPI.saveUsername(username);
|
||||||
|
|
||||||
|
if (result && result.success === false) {
|
||||||
|
throw new Error(result.error || 'Failed to save username');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the username input field
|
||||||
|
if (settingsPlayerName) {
|
||||||
|
settingsPlayerName.value = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the current UUID display
|
||||||
|
await loadCurrentUuid();
|
||||||
|
|
||||||
|
// Refresh the UUID list to show new "Current" badge
|
||||||
|
await loadAllUuids();
|
||||||
|
|
||||||
|
const msg = window.i18n
|
||||||
|
? window.i18n.t('notifications.switchUsernameSuccess').replace('{username}', username)
|
||||||
|
: `Switched to "${username}" successfully!`;
|
||||||
|
showNotification(msg, 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error switching username:', error);
|
||||||
|
const msg = window.i18n
|
||||||
|
? window.i18n.t('notifications.switchUsernameFailed')
|
||||||
|
: `Failed to switch username: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.deleteUuid = async function (username) {
|
window.deleteUuid = async function (username) {
|
||||||
try {
|
try {
|
||||||
const message = window.i18n ? window.i18n.t('confirm.deleteUuidMessage').replace('{username}', username) : `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`;
|
const message = window.i18n ? window.i18n.t('confirm.deleteUuidMessage').replace('{username}', username) : `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`;
|
||||||
|
|||||||
13
GUI/js/ui.js
13
GUI/js/ui.js
@@ -63,8 +63,10 @@ function handleNavigation() {
|
|||||||
navItems.forEach(item => {
|
navItems.forEach(item => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
const page = item.getAttribute('data-page');
|
const page = item.getAttribute('data-page');
|
||||||
|
if (page) {
|
||||||
showPage(`${page}-page`);
|
showPage(`${page}-page`);
|
||||||
setActiveNav(page);
|
setActiveNav(page);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -843,7 +845,7 @@ function getErrorMessage(technicalMessage, errorType) {
|
|||||||
case 'stall':
|
case 'stall':
|
||||||
return 'Download stalled due to slow connection. Please retry.';
|
return 'Download stalled due to slow connection. Please retry.';
|
||||||
case 'file':
|
case 'file':
|
||||||
return 'Unable to save file. Check disk space and permissions. Please retry.';
|
return 'Unable to save file. Check permissions. Please retry.';
|
||||||
case 'permission':
|
case 'permission':
|
||||||
return 'Permission denied. Check if launcher has write access. Please retry.';
|
return 'Permission denied. Check if launcher has write access. Please retry.';
|
||||||
case 'server':
|
case 'server':
|
||||||
@@ -972,7 +974,7 @@ function setupRetryButton() {
|
|||||||
if (!currentDownloadState.retryData || currentDownloadState.errorType === 'jre') {
|
if (!currentDownloadState.retryData || currentDownloadState.errorType === 'jre') {
|
||||||
currentDownloadState.retryData = {
|
currentDownloadState.retryData = {
|
||||||
branch: 'release',
|
branch: 'release',
|
||||||
fileName: '4.pwr'
|
fileName: '7.pwr'
|
||||||
};
|
};
|
||||||
console.log('[UI] Created default PWR retry data:', currentDownloadState.retryData);
|
console.log('[UI] Created default PWR retry data:', currentDownloadState.retryData);
|
||||||
}
|
}
|
||||||
@@ -1040,7 +1042,7 @@ function setupRetryButton() {
|
|||||||
} else {
|
} else {
|
||||||
currentDownloadState.retryData = {
|
currentDownloadState.retryData = {
|
||||||
branch: 'release',
|
branch: 'release',
|
||||||
fileName: '4.pwr'
|
fileName: '7.pwr'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
console.log('[UI] Created default retry data:', currentDownloadState.retryData);
|
console.log('[UI] Created default retry data:', currentDownloadState.retryData);
|
||||||
@@ -1100,7 +1102,10 @@ function getRetryContextMessage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make toggleMaximize globally available
|
window.openDiscordExternal = function() {
|
||||||
|
window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
|
||||||
|
};
|
||||||
|
|
||||||
window.toggleMaximize = toggleMaximize;
|
window.toggleMaximize = toggleMaximize;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', setupUI);
|
document.addEventListener('DOMContentLoaded', setupUI);
|
||||||
|
|||||||
198
GUI/js/update.js
198
GUI/js/update.js
@@ -6,12 +6,12 @@ class ClientUpdateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
window.electronAPI.onUpdatePopup((updateInfo) => {
|
console.log('🔧 ClientUpdateManager initializing...');
|
||||||
this.showUpdatePopup(updateInfo);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for electron-updater events
|
// Listen for electron-updater events from main.js
|
||||||
|
// This is the primary update trigger - main.js checks for updates on startup
|
||||||
window.electronAPI.onUpdateAvailable((updateInfo) => {
|
window.electronAPI.onUpdateAvailable((updateInfo) => {
|
||||||
|
console.log('📥 update-available event received:', updateInfo);
|
||||||
this.showUpdatePopup(updateInfo);
|
this.showUpdatePopup(updateInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,18 +20,30 @@ class ClientUpdateManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.onUpdateDownloaded((updateInfo) => {
|
window.electronAPI.onUpdateDownloaded((updateInfo) => {
|
||||||
|
console.log('📦 update-downloaded event received:', updateInfo);
|
||||||
this.showUpdateDownloaded(updateInfo);
|
this.showUpdateDownloaded(updateInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.electronAPI.onUpdateError((errorInfo) => {
|
window.electronAPI.onUpdateError((errorInfo) => {
|
||||||
|
console.log('❌ update-error event received:', errorInfo);
|
||||||
this.handleUpdateError(errorInfo);
|
this.handleUpdateError(errorInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.checkForUpdatesOnDemand();
|
console.log('✅ ClientUpdateManager initialized');
|
||||||
|
|
||||||
|
// Note: Don't call checkForUpdatesOnDemand() here - main.js already checks
|
||||||
|
// for updates after 3 seconds and sends 'update-available' event.
|
||||||
|
// Calling it here would cause duplicate popups.
|
||||||
}
|
}
|
||||||
|
|
||||||
showUpdatePopup(updateInfo) {
|
showUpdatePopup(updateInfo) {
|
||||||
if (this.updatePopupVisible) return;
|
console.log('🔔 showUpdatePopup called, updatePopupVisible:', this.updatePopupVisible);
|
||||||
|
|
||||||
|
// Check if popup already exists in DOM (extra safety)
|
||||||
|
if (this.updatePopupVisible || document.getElementById('update-popup-overlay')) {
|
||||||
|
console.log('⚠️ Update popup already visible, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.updatePopupVisible = true;
|
this.updatePopupVisible = true;
|
||||||
|
|
||||||
@@ -92,7 +104,10 @@ class ClientUpdateManager {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="update-popup-footer">
|
<div class="update-popup-footer">
|
||||||
This popup cannot be closed until you update the launcher
|
<span id="update-footer-text">Downloading update...</span>
|
||||||
|
<button id="update-skip-btn" class="update-skip-btn" style="display: none; margin-top: 0.5rem; background: transparent; border: 1px solid rgba(255,255,255,0.2); color: #9ca3af; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.75rem;">
|
||||||
|
Skip for now (not recommended)
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,10 +134,37 @@ class ClientUpdateManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.quitAndInstallUpdate();
|
await window.electronAPI.quitAndInstallUpdate();
|
||||||
|
|
||||||
|
// If we're still here after 5 seconds, the install probably failed
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('⚠️ Install may have failed - showing skip option');
|
||||||
|
installBtn.disabled = false;
|
||||||
|
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Again';
|
||||||
|
|
||||||
|
// Show skip button
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Install not working? Skip for now:';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error installing update:', error);
|
console.error('❌ Error installing update:', error);
|
||||||
installBtn.disabled = false;
|
installBtn.disabled = false;
|
||||||
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
|
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
|
||||||
|
|
||||||
|
// Show skip button on error
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Install failed. Skip for now:';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -138,9 +180,14 @@ class ClientUpdateManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.openDownloadPage();
|
await window.electronAPI.openDownloadPage();
|
||||||
console.log('✅ Download page opened, launcher will close...');
|
console.log('✅ Download page opened');
|
||||||
|
|
||||||
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Launcher closing...';
|
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Opened in browser';
|
||||||
|
|
||||||
|
// Close the popup after opening download page
|
||||||
|
setTimeout(() => {
|
||||||
|
this.closeUpdatePopup();
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error opening download page:', error);
|
console.error('❌ Error opening download page:', error);
|
||||||
@@ -161,9 +208,39 @@ class ClientUpdateManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show skip button after 30 seconds as fallback (in case update is stuck)
|
||||||
|
setTimeout(() => {
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Update taking too long?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.closeUpdatePopup();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🔔 Update popup displayed with new style');
|
console.log('🔔 Update popup displayed with new style');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeUpdatePopup() {
|
||||||
|
const overlay = document.getElementById('update-popup-overlay');
|
||||||
|
if (overlay) {
|
||||||
|
overlay.remove();
|
||||||
|
}
|
||||||
|
this.updatePopupVisible = false;
|
||||||
|
this.unblockInterface();
|
||||||
|
}
|
||||||
|
|
||||||
updateDownloadProgress(progress) {
|
updateDownloadProgress(progress) {
|
||||||
const progressBar = document.getElementById('update-progress-bar');
|
const progressBar = document.getElementById('update-progress-bar');
|
||||||
const progressPercent = document.getElementById('update-progress-percent');
|
const progressPercent = document.getElementById('update-progress-percent');
|
||||||
@@ -197,25 +274,86 @@ class ClientUpdateManager {
|
|||||||
const statusText = document.getElementById('update-status-text');
|
const statusText = document.getElementById('update-status-text');
|
||||||
const progressContainer = document.getElementById('update-progress-container');
|
const progressContainer = document.getElementById('update-progress-container');
|
||||||
const buttonsContainer = document.getElementById('update-buttons-container');
|
const buttonsContainer = document.getElementById('update-buttons-container');
|
||||||
|
const installBtn = document.getElementById('update-install-btn');
|
||||||
|
const downloadBtn = document.getElementById('update-download-btn');
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
const popupContainer = document.querySelector('.update-popup-container');
|
||||||
|
|
||||||
if (statusText) {
|
// Remove breathing/pulse animation when download is complete
|
||||||
statusText.textContent = 'Update downloaded! Ready to install.';
|
if (popupContainer) {
|
||||||
|
popupContainer.classList.remove('update-popup-pulse');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressContainer) {
|
if (progressContainer) {
|
||||||
progressContainer.style.display = 'none';
|
progressContainer.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use platform info from main process if available, fallback to browser detection
|
||||||
|
const autoInstallSupported = updateInfo.autoInstallSupported !== undefined
|
||||||
|
? updateInfo.autoInstallSupported
|
||||||
|
: navigator.platform.toUpperCase().indexOf('MAC') < 0;
|
||||||
|
|
||||||
|
if (!autoInstallSupported) {
|
||||||
|
// macOS: Show manual download as primary since auto-update doesn't work
|
||||||
|
if (statusText) {
|
||||||
|
statusText.textContent = 'Update downloaded but auto-install may not work on macOS.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installBtn) {
|
||||||
|
// Still show install button but as secondary option
|
||||||
|
installBtn.classList.add('update-download-btn-secondary');
|
||||||
|
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Install & Restart';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadBtn) {
|
||||||
|
// Make manual download primary
|
||||||
|
downloadBtn.classList.remove('update-download-btn-secondary');
|
||||||
|
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Manually (Recommended)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Auto-install often fails on macOS:';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Windows/Linux: Auto-install should work
|
||||||
|
if (statusText) {
|
||||||
|
statusText.textContent = 'Update downloaded! Ready to install.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Click to install the update:';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (buttonsContainer) {
|
if (buttonsContainer) {
|
||||||
buttonsContainer.style.display = 'block';
|
buttonsContainer.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Update downloaded, ready to install');
|
// Always show skip button in downloaded state
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
console.log('✅ Skip button made visible');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Skip button not found in DOM!');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Update downloaded, ready to install. autoInstallSupported:', autoInstallSupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateError(errorInfo) {
|
handleUpdateError(errorInfo) {
|
||||||
console.error('Update error:', errorInfo);
|
console.error('Update error:', errorInfo);
|
||||||
|
|
||||||
|
// Show skip button immediately on any error
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Update failed. You can skip for now.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If manual download is required, update the UI (this will handle status text)
|
// If manual download is required, update the UI (this will handle status text)
|
||||||
if (errorInfo.requiresManualDownload) {
|
if (errorInfo.requiresManualDownload) {
|
||||||
this.showManualDownloadRequired(errorInfo);
|
this.showManualDownloadRequired(errorInfo);
|
||||||
@@ -289,6 +427,16 @@ class ClientUpdateManager {
|
|||||||
buttonsContainer.style.display = 'block';
|
buttonsContainer.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show skip button for manual download errors
|
||||||
|
const skipBtn = document.getElementById('update-skip-btn');
|
||||||
|
const footerText = document.getElementById('update-footer-text');
|
||||||
|
if (skipBtn) {
|
||||||
|
skipBtn.style.display = 'inline-block';
|
||||||
|
if (footerText) {
|
||||||
|
footerText.textContent = 'Or continue without updating:';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('⚠️ Manual download required due to update error');
|
console.log('⚠️ Manual download required due to update error');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,13 +448,35 @@ class ClientUpdateManager {
|
|||||||
|
|
||||||
document.body.classList.add('no-select');
|
document.body.classList.add('no-select');
|
||||||
|
|
||||||
document.addEventListener('keydown', this.blockKeyEvents.bind(this), true);
|
// Store bound functions so we can remove them later
|
||||||
|
this._boundBlockKeyEvents = this.blockKeyEvents.bind(this);
|
||||||
|
this._boundBlockContextMenu = this.blockContextMenu.bind(this);
|
||||||
|
|
||||||
document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true);
|
document.addEventListener('keydown', this._boundBlockKeyEvents, true);
|
||||||
|
document.addEventListener('contextmenu', this._boundBlockContextMenu, true);
|
||||||
|
|
||||||
console.log('🚫 Interface blocked for update');
|
console.log('🚫 Interface blocked for update');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unblockInterface() {
|
||||||
|
const mainContent = document.querySelector('.flex.w-full.h-screen');
|
||||||
|
if (mainContent) {
|
||||||
|
mainContent.classList.remove('interface-blocked');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.classList.remove('no-select');
|
||||||
|
|
||||||
|
// Remove event listeners
|
||||||
|
if (this._boundBlockKeyEvents) {
|
||||||
|
document.removeEventListener('keydown', this._boundBlockKeyEvents, true);
|
||||||
|
}
|
||||||
|
if (this._boundBlockContextMenu) {
|
||||||
|
document.removeEventListener('contextmenu', this._boundBlockContextMenu, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Interface unblocked');
|
||||||
|
}
|
||||||
|
|
||||||
blockKeyEvents(event) {
|
blockKeyEvents(event) {
|
||||||
if (event.target.closest('#update-popup-overlay')) {
|
if (event.target.closest('#update-popup-overlay')) {
|
||||||
if ((event.key === 'Enter' || event.key === ' ') &&
|
if ((event.key === 'Enter' || event.key === ' ') &&
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
"repairGame": "Spiel reparieren",
|
"repairGame": "Spiel reparieren",
|
||||||
"reinstallGame": "Spieldateien neu installieren (behält Daten)",
|
"reinstallGame": "Spieldateien neu installieren (behält Daten)",
|
||||||
"gpuPreference": "GPU-Präferenz",
|
"gpuPreference": "GPU-Präferenz",
|
||||||
"gpuHint": "Wähle deine bevorzugte GPU (Linux: betrifft DRI_PRIME)",
|
"gpuHint": "Funktion nur für Laptops; auf „Integriert“ stellen, wenn auf einem PC.",
|
||||||
"gpuAuto": "Auto",
|
"gpuAuto": "Auto",
|
||||||
"gpuIntegrated": "Integriert",
|
"gpuIntegrated": "Integriert",
|
||||||
"gpuDedicated": "Dediziert",
|
"gpuDedicated": "Dediziert",
|
||||||
@@ -212,39 +212,10 @@
|
|||||||
"modsModNotFound": "Mod-Informationen nicht gefunden",
|
"modsModNotFound": "Mod-Informationen nicht gefunden",
|
||||||
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
|
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
|
||||||
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden",
|
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden",
|
||||||
"javaPathCopied": "Java-Pfad in die Zwischenablage kopiert!",
|
"noUsername": "Kein Benutzername konfiguriert. Bitte speichere zuerst deinen Benutzernamen.",
|
||||||
"javaPathCopyFailed": "Java-Pfad konnte nicht kopiert werden",
|
"switchUsernameSuccess": "Erfolgreich zu \"{username}\" gewechselt!",
|
||||||
"javaPathSaved": "Java-Pfad erfolgreich gespeichert!",
|
"switchUsernameFailed": "Benutzername konnte nicht gewechselt werden",
|
||||||
"javaPathSaveFailed": "Java-Pfad konnte nicht gespeichert werden",
|
"playerNameTooLong": "Spielername darf maximal 16 Zeichen haben"
|
||||||
"javaPathInvalid": "Ungültiger Java-Pfad",
|
|
||||||
"javaPathReset": "Java-Pfad auf Standardwerte zurückgesetzt",
|
|
||||||
"gameLocationError": "Spielordner konnte nicht geöffnet werden",
|
|
||||||
"launcherRestartRequired": "Launcher-Neustart erforderlich, um Änderungen anzuwenden",
|
|
||||||
"gameRepairConfirm": "Möchtest du das Spiel wirklich reparieren? Dies wird alle Spieldateien neu installieren.",
|
|
||||||
"gameRepairInProgress": "Spiel wird repariert...",
|
|
||||||
"gameRepairSuccess": "Spiel erfolgreich repariert!",
|
|
||||||
"gameRepairFailed": "Spielreparatur fehlgeschlagen: {error}",
|
|
||||||
"invalidUsername": "Ungültiger Benutzername",
|
|
||||||
"usernameInUse": "Benutzername bereits vergeben",
|
|
||||||
"chatJoinSuccess": "Du bist dem Chat beigetreten!",
|
|
||||||
"chatJoinFailed": "Chat-Beitritt fehlgeschlagen",
|
|
||||||
"messageTooLong": "Nachricht zu lang",
|
|
||||||
"messageSent": "Nachricht gesendet",
|
|
||||||
"messageSendFailed": "Nachricht konnte nicht gesendet werden",
|
|
||||||
"colorUpdated": "Farbe aktualisiert!",
|
|
||||||
"colorUpdateFailed": "Farbe konnte nicht aktualisiert werden",
|
|
||||||
"profileCreated": "Profil erfolgreich erstellt!",
|
|
||||||
"profileCreateFailed": "Profil konnte nicht erstellt werden",
|
|
||||||
"profileDeleted": "Profil gelöscht",
|
|
||||||
"profileDeleteFailed": "Profil konnte nicht gelöscht werden",
|
|
||||||
"profileSwitched": "Profil gewechselt zu: {name}",
|
|
||||||
"profileSwitchFailed": "Profilwechsel fehlgeschlagen",
|
|
||||||
"invalidProfileName": "Ungültiger Profilname",
|
|
||||||
"profileNameExists": "Ein Profil mit diesem Namen existiert bereits",
|
|
||||||
"noInternet": "Keine Internetverbindung",
|
|
||||||
"checkInternetConnection": "Überprüfe deine Internetverbindung",
|
|
||||||
"serverError": "Serverfehler. Bitte versuche es später erneut.",
|
|
||||||
"unknownError": "Ein unbekannter Fehler ist aufgetreten"
|
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Aktion bestätigen",
|
"defaultTitle": "Aktion bestätigen",
|
||||||
@@ -259,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Löschen",
|
"deleteUuidButton": "Löschen",
|
||||||
"uninstallGameTitle": "Spiel deinstallieren",
|
"uninstallGameTitle": "Spiel deinstallieren",
|
||||||
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
|
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
|
||||||
"uninstallGameButton": "Deinstallieren"
|
"uninstallGameButton": "Deinstallieren",
|
||||||
|
"switchUsernameTitle": "Identität wechseln",
|
||||||
|
"switchUsernameMessage": "Zu Benutzername \"{username}\" wechseln? Dies ändert deine aktuelle Spieleridentität.",
|
||||||
|
"switchUsernameButton": "Wechseln"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Initialisiere...",
|
"initializing": "Initialisiere...",
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"repairGame": "Repair Game",
|
"repairGame": "Repair Game",
|
||||||
"reinstallGame": "Reinstall game files (preserves data)",
|
"reinstallGame": "Reinstall game files (preserves data)",
|
||||||
"gpuPreference": "GPU Preference",
|
"gpuPreference": "GPU Preference",
|
||||||
"gpuHint": "Select your preferred GPU (Linux: affects DRI_PRIME)",
|
"gpuHint": "Laptop-only feature; set to Integrated if on PC",
|
||||||
"gpuAuto": "Auto",
|
"gpuAuto": "Auto",
|
||||||
"gpuIntegrated": "Integrated",
|
"gpuIntegrated": "Integrated",
|
||||||
"gpuDedicated": "Dedicated",
|
"gpuDedicated": "Dedicated",
|
||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Failed to delete mod: {error}",
|
"modsDeleteFailed": "Failed to delete mod: {error}",
|
||||||
"modsModNotFound": "Mod information not found",
|
"modsModNotFound": "Mod information not found",
|
||||||
"hwAccelSaved": "Hardware acceleration setting saved",
|
"hwAccelSaved": "Hardware acceleration setting saved",
|
||||||
"hwAccelSaveFailed": "Failed to save hardware acceleration setting"
|
"hwAccelSaveFailed": "Failed to save hardware acceleration setting",
|
||||||
|
"noUsername": "No username configured. Please save your username first.",
|
||||||
|
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
|
||||||
|
"switchUsernameFailed": "Failed to switch username",
|
||||||
|
"playerNameTooLong": "Player name must be 16 characters or less"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Confirm action",
|
"defaultTitle": "Confirm action",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Delete",
|
"deleteUuidButton": "Delete",
|
||||||
"uninstallGameTitle": "Uninstall game",
|
"uninstallGameTitle": "Uninstall game",
|
||||||
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
|
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
|
||||||
"uninstallGameButton": "Uninstall"
|
"uninstallGameButton": "Uninstall",
|
||||||
|
"switchUsernameTitle": "Switch Identity",
|
||||||
|
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
|
||||||
|
"switchUsernameButton": "Switch"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Initializing...",
|
"initializing": "Initializing...",
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
"repairGame": "Reparar juego",
|
"repairGame": "Reparar juego",
|
||||||
"reinstallGame": "Reinstalar archivos del juego (conserva los datos)",
|
"reinstallGame": "Reinstalar archivos del juego (conserva los datos)",
|
||||||
"gpuPreference": "Preferencia de GPU",
|
"gpuPreference": "Preferencia de GPU",
|
||||||
"gpuHint": "Selecciona tu GPU preferida (Linux: afecta DRI_PRIME)",
|
"gpuHint": "Función exclusiva para computadora portátil; configúrela como Integrada si está en una PC",
|
||||||
"gpuAuto": "Automático",
|
"gpuAuto": "Automático",
|
||||||
"gpuIntegrated": "Integrada",
|
"gpuIntegrated": "Integrada",
|
||||||
"gpuDedicated": "Dedicada",
|
"gpuDedicated": "Dedicada",
|
||||||
@@ -131,6 +131,8 @@
|
|||||||
"closeLauncher": "Comportamiento del Launcher",
|
"closeLauncher": "Comportamiento del Launcher",
|
||||||
"closeOnStart": "Cerrar Launcher al iniciar el juego",
|
"closeOnStart": "Cerrar Launcher al iniciar el juego",
|
||||||
"closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado",
|
"closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado",
|
||||||
|
"hwAccel": "Aceleración por Hardware",
|
||||||
|
"hwAccelDescription": "Habilitar aceleración por hardware para el launcher",
|
||||||
"gameBranch": "Rama del Juego",
|
"gameBranch": "Rama del Juego",
|
||||||
"branchRelease": "Lanzamiento",
|
"branchRelease": "Lanzamiento",
|
||||||
"branchPreRelease": "Pre-Lanzamiento",
|
"branchPreRelease": "Pre-Lanzamiento",
|
||||||
@@ -207,7 +209,13 @@
|
|||||||
"modsDownloadFailed": "Error al descargar mod: {error}",
|
"modsDownloadFailed": "Error al descargar mod: {error}",
|
||||||
"modsToggleFailed": "Error al alternar mod: {error}",
|
"modsToggleFailed": "Error al alternar mod: {error}",
|
||||||
"modsDeleteFailed": "Error al eliminar mod: {error}",
|
"modsDeleteFailed": "Error al eliminar mod: {error}",
|
||||||
"modsModNotFound": "Información del mod no encontrada"
|
"modsModNotFound": "Información del mod no encontrada",
|
||||||
|
"hwAccelSaved": "Configuración de aceleración por hardware guardada",
|
||||||
|
"hwAccelSaveFailed": "Error al guardar la configuración de aceleración por hardware",
|
||||||
|
"noUsername": "No hay nombre de usuario configurado. Por favor, guarda tu nombre de usuario primero.",
|
||||||
|
"switchUsernameSuccess": "¡Cambiado a \"{username}\" con éxito!",
|
||||||
|
"switchUsernameFailed": "Error al cambiar nombre de usuario",
|
||||||
|
"playerNameTooLong": "El nombre del jugador debe tener 16 caracteres o menos"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Confirmar acción",
|
"defaultTitle": "Confirmar acción",
|
||||||
@@ -222,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Eliminar",
|
"deleteUuidButton": "Eliminar",
|
||||||
"uninstallGameTitle": "Desinstalar juego",
|
"uninstallGameTitle": "Desinstalar juego",
|
||||||
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
|
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
|
||||||
"uninstallGameButton": "Desinstalar"
|
"uninstallGameButton": "Desinstalar",
|
||||||
|
"switchUsernameTitle": "Cambiar identidad",
|
||||||
|
"switchUsernameMessage": "¿Cambiar al nombre de usuario \"{username}\"? Esto cambiará tu identidad de jugador actual.",
|
||||||
|
"switchUsernameButton": "Cambiar"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Inicializando...",
|
"initializing": "Inicializando...",
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
"repairGame": "Réparer le Jeu",
|
"repairGame": "Réparer le Jeu",
|
||||||
"reinstallGame": "Réinstaller les fichiers du jeu (préserve les données)",
|
"reinstallGame": "Réinstaller les fichiers du jeu (préserve les données)",
|
||||||
"gpuPreference": "Préférence GPU",
|
"gpuPreference": "Préférence GPU",
|
||||||
"gpuHint": "Sélectionnez votre GPU préféré (Linux: affecte DRI_PRIME)",
|
"gpuHint": "Fonctionnalité exclusive aux ordinateurs portables; à définir sur Intégré sur PC",
|
||||||
"gpuAuto": "Auto",
|
"gpuAuto": "Auto",
|
||||||
"gpuIntegrated": "Intégré",
|
"gpuIntegrated": "Intégré",
|
||||||
"gpuDedicated": "Dédié",
|
"gpuDedicated": "Dédié",
|
||||||
@@ -198,38 +198,60 @@
|
|||||||
"uuidInvalidFormat": "Format UUID invalide",
|
"uuidInvalidFormat": "Format UUID invalide",
|
||||||
"uuidSetFailed": "Échec de la définition de l'UUID personnalisé",
|
"uuidSetFailed": "Échec de la définition de l'UUID personnalisé",
|
||||||
"uuidSetSuccess": "UUID personnalisé défini avec succès!",
|
"uuidSetSuccess": "UUID personnalisé défini avec succès!",
|
||||||
"javaPathCopied": "Chemin Java copié dans le presse-papiers!",
|
"uuidDeleteFailed": "Échec de la suppression de l'UUID",
|
||||||
"javaPathCopyFailed": "Échec de la copie du chemin Java",
|
"uuidDeleteSuccess": "UUID supprimé avec succès!",
|
||||||
"javaPathSaved": "Chemin Java sauvegardé avec succès!",
|
"modsDownloading": "Téléchargement de {name}...",
|
||||||
"javaPathSaveFailed": "Échec de la sauvegarde du chemin Java",
|
"modsTogglingMod": "Basculement du mod...",
|
||||||
"javaPathInvalid": "Chemin Java invalide",
|
"modsDeletingMod": "Suppression du mod...",
|
||||||
"javaPathReset": "Chemin Java réinitialisé aux valeurs par défaut",
|
"modsLoadingMods": "Chargement des mods depuis CurseForge...",
|
||||||
"gameLocationError": "Impossible d'ouvrir l'emplacement du jeu",
|
"modsInstalledSuccess": "{name} installé avec succès! 🎉",
|
||||||
"launcherRestartRequired": "Redémarrage du launcher requis pour appliquer les modifications",
|
"modsDeletedSuccess": "{name} supprimé avec succès",
|
||||||
"gameRepairConfirm": "Êtes-vous sûr de vouloir réparer le jeu? Cela réinstallera tous les fichiers du jeu.",
|
"modsDownloadFailed": "Échec du téléchargement du mod: {error}",
|
||||||
"gameRepairInProgress": "Réparation du jeu en cours...",
|
"modsToggleFailed": "Échec du basculement du mod: {error}",
|
||||||
"gameRepairSuccess": "Jeu réparé avec succès!",
|
"modsDeleteFailed": "Échec de la suppression du mod: {error}",
|
||||||
"gameRepairFailed": "Échec de la réparation du jeu: {error}",
|
"modsModNotFound": "Informations du mod introuvables",
|
||||||
"invalidUsername": "Nom d'utilisateur invalide",
|
"hwAccelSaved": "Paramètre d'accélération matérielle sauvegardé",
|
||||||
"usernameInUse": "Nom d'utilisateur déjà utilisé",
|
"hwAccelSaveFailed": "Échec de la sauvegarde du paramètre d'accélération matérielle",
|
||||||
"chatJoinSuccess": "Vous avez rejoint le chat!",
|
"noUsername": "Aucun nom d'utilisateur configuré. Veuillez d'abord enregistrer votre nom d'utilisateur.",
|
||||||
"chatJoinFailed": "Échec de la connexion au chat",
|
"switchUsernameSuccess": "Basculé vers \"{username}\" avec succès!",
|
||||||
"messageTooLong": "Message trop long",
|
"switchUsernameFailed": "Échec du changement de nom d'utilisateur",
|
||||||
"messageSent": "Message envoyé",
|
"playerNameTooLong": "Le nom du joueur doit comporter 16 caractères ou moins"
|
||||||
"messageSendFailed": "Échec de l'envoi du message",
|
},
|
||||||
"colorUpdated": "Couleur mise à jour!",
|
"confirm": {
|
||||||
"colorUpdateFailed": "Échec de la mise à jour de la couleur",
|
"defaultTitle": "Confirmer l'action",
|
||||||
"profileCreated": "Profil créé avec succès!",
|
"regenerateUuidTitle": "Générer un nouvel UUID",
|
||||||
"profileCreateFailed": "Échec de la création du profil",
|
"regenerateUuidMessage": "Êtes-vous sûr de vouloir générer un nouvel UUID? Cela changera votre identité de joueur.",
|
||||||
"profileDeleted": "Profil supprimé",
|
"regenerateUuidButton": "Générer",
|
||||||
"profileDeleteFailed": "Échec de la suppression du profil",
|
"setCustomUuidTitle": "Définir UUID personnalisé",
|
||||||
"profileSwitched": "Profil changé vers: {name}",
|
"setCustomUuidMessage": "Êtes-vous sûr de vouloir définir cet UUID personnalisé? Cela changera votre identité de joueur.",
|
||||||
"profileSwitchFailed": "Échec du changement de profil",
|
"setCustomUuidButton": "Définir UUID",
|
||||||
"invalidProfileName": "Nom de profil invalide",
|
"deleteUuidTitle": "Supprimer UUID",
|
||||||
"profileNameExists": "Un profil avec ce nom existe déjà",
|
"deleteUuidMessage": "Êtes-vous sûr de vouloir supprimer l'UUID de \"{username}\"? Cette action est irréversible.",
|
||||||
"noInternet": "Pas de connexion Internet",
|
"deleteUuidButton": "Supprimer",
|
||||||
"checkInternetConnection": "Vérifiez votre connexion Internet",
|
"uninstallGameTitle": "Désinstaller le jeu",
|
||||||
"serverError": "Erreur serveur. Veuillez réessayer plus tard.",
|
"uninstallGameMessage": "Êtes-vous sûr de vouloir désinstaller Hytale? Tous les fichiers du jeu seront supprimés.",
|
||||||
"unknownError": "Une erreur inconnue s'est produite"
|
"uninstallGameButton": "Désinstaller",
|
||||||
|
"switchUsernameTitle": "Changer d'identité",
|
||||||
|
"switchUsernameMessage": "Basculer vers le nom d'utilisateur \"{username}\"? Cela changera votre identité de joueur actuelle.",
|
||||||
|
"switchUsernameButton": "Changer"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Initialisation...",
|
||||||
|
"downloading": "Téléchargement...",
|
||||||
|
"installing": "Installation...",
|
||||||
|
"extracting": "Extraction...",
|
||||||
|
"verifying": "Vérification...",
|
||||||
|
"switchingProfile": "Changement de profil...",
|
||||||
|
"profileSwitched": "Profil changé!",
|
||||||
|
"startingGame": "Démarrage du jeu...",
|
||||||
|
"launching": "LANCEMENT...",
|
||||||
|
"uninstallingGame": "Désinstallation du jeu...",
|
||||||
|
"gameUninstalled": "Jeu désinstallé avec succès!",
|
||||||
|
"uninstallFailed": "Échec de la désinstallation: {error}",
|
||||||
|
"startingUpdate": "Démarrage de la mise à jour obligatoire du jeu...",
|
||||||
|
"installationComplete": "Installation terminée avec succès!",
|
||||||
|
"installationFailed": "Échec de l'installation: {error}",
|
||||||
|
"installingGameFiles": "Installation des fichiers du jeu...",
|
||||||
|
"installComplete": "Installation terminée!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
257
GUI/locales/id-ID.json
Normal file
257
GUI/locales/id-ID.json
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Main",
|
||||||
|
"mods": "Mod",
|
||||||
|
"news": "Berita",
|
||||||
|
"chat": "Obrolan Pemain",
|
||||||
|
"settings": "Pengaturan"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Pemain:",
|
||||||
|
"manageProfiles": "Kelola Profil",
|
||||||
|
"defaultProfile": "Default"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "LAUNCHER GRATIS UNTUK DIMAINKAN",
|
||||||
|
"playerName": "Nama Pemain",
|
||||||
|
"playerNamePlaceholder": "Masukkan namamu",
|
||||||
|
"gameBranch": "Versi Game",
|
||||||
|
"releaseVersion": "Rilis (Stabil)",
|
||||||
|
"preReleaseVersion": "Pra-Rilis (Eksperimental)",
|
||||||
|
"customInstallation": "Instalasi Kustom",
|
||||||
|
"installationFolder": "Folder Instalasi",
|
||||||
|
"pathPlaceholder": "Lokasi default",
|
||||||
|
"browse": "Telusuri",
|
||||||
|
"installButton": "INSTAL HYTALE",
|
||||||
|
"installing": "MENGINSTAL..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "SIAP BERMAIN",
|
||||||
|
"subtitle": "Luncurkan Hytale dan mulai petualanganmu",
|
||||||
|
"playButton": "MAIN HYTALE",
|
||||||
|
"latestNews": "BERITA TERBARU",
|
||||||
|
"viewAll": "LIHAT SEMUA",
|
||||||
|
"checking": "MEMERIKSA...",
|
||||||
|
"play": "MAIN"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Cari mod...",
|
||||||
|
"myMods": "MOD SAYA",
|
||||||
|
"previous": "SEBELUMNYA",
|
||||||
|
"next": "BERIKUTNYA",
|
||||||
|
"page": "Halaman",
|
||||||
|
"of": "dari",
|
||||||
|
"modalTitle": "MOD SAYA",
|
||||||
|
"noModsFound": "Mod Tidak Ditemukan",
|
||||||
|
"noModsFoundDesc": "Coba sesuaikan pencarianmu",
|
||||||
|
"noModsInstalled": "Tidak ada Mod Terinstal",
|
||||||
|
"noModsInstalledDesc": "Tambahkan mod dari CurseForge atau impor file lokal",
|
||||||
|
"view": "LIHAT",
|
||||||
|
"install": "INSTAL",
|
||||||
|
"installed": "TERINSTAL",
|
||||||
|
"enable": "AKTIFKAN",
|
||||||
|
"disable": "NONAKTIFKAN",
|
||||||
|
"active": "AKTIF",
|
||||||
|
"disabled": "NONAKTIF",
|
||||||
|
"delete": "Hapus mod",
|
||||||
|
"noDescription": "Tidak ada deskripsi tersedia",
|
||||||
|
"confirmDelete": "Apakah kamu yakin ingin menghapus \"{name}\"?",
|
||||||
|
"confirmDeleteDesc": "Tindakan ini tidak dapat dibatalkan.",
|
||||||
|
"confirmDeletion": "Konfirmasi Penghapusan",
|
||||||
|
"apiKeyRequired": "Kunci API Diperlukan",
|
||||||
|
"apiKeyRequiredDesc": "Kunci API CurseForge diperlukan untuk menelusuri mod"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "SEMUA BERITA",
|
||||||
|
"readMore": "Baca Selengkapnya"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "OBROLAN PEMAIN",
|
||||||
|
"pickColor": "Warna",
|
||||||
|
"inputPlaceholder": "Ketik pesanmu...",
|
||||||
|
"send": "Kirim",
|
||||||
|
"online": "aktif",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Obrolan aman - Tautan disensor",
|
||||||
|
"joinChat": "Gabung Obrolan",
|
||||||
|
"chooseUsername": "Pilih nama pengguna untuk bergabung ke Obrolan Pemain",
|
||||||
|
"username": "Nama Pengguna",
|
||||||
|
"usernamePlaceholder": "Masukkan nama penggunamu...",
|
||||||
|
"usernameHint": "3-20 karakter, huruf, angka, - dan _ saja",
|
||||||
|
"joinButton": "Gabung Obrolan",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Kustomisasi Warna Nama Pengguna",
|
||||||
|
"chooseSolid": "Pilih warna solid:",
|
||||||
|
"customColor": "Warna kustom:",
|
||||||
|
"preview": "Pratinjau:",
|
||||||
|
"previewUsername": "Nama Pengguna",
|
||||||
|
"apply": "Terapkan Warna"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "PENGATURAN",
|
||||||
|
"java": "Runtime Java",
|
||||||
|
"useCustomJava": "Gunakan lokasi Java Kustom",
|
||||||
|
"javaDescription": "Ganti runtime Java bawaan dengan instalasi milikmu",
|
||||||
|
"javaPath": "Lokasi Eksekutabel Java",
|
||||||
|
"javaPathPlaceholder": "Pilih lokasi Java...",
|
||||||
|
"javaBrowse": "Telusuri",
|
||||||
|
"javaHint": "Pilih folder instalasi Java (mendukung Windows, Mac, Linux)",
|
||||||
|
"discord": "Integrasi Discord",
|
||||||
|
"enableRPC": "Aktifkan Discord Rich Presence",
|
||||||
|
"discordDescription": "Tampilkan aktivitas launchermu di Discord",
|
||||||
|
"game": "Opsi Game",
|
||||||
|
"playerName": "Nama Pemain",
|
||||||
|
"playerNamePlaceholder": "Masukkan nama pemainmu",
|
||||||
|
"playerNameHint": "Nama ini akan digunakan di dalam game (1-16 karakter)",
|
||||||
|
"openGameLocation": "Buka Lokasi Game",
|
||||||
|
"openGameLocationDesc": "Buka folder instalasi game",
|
||||||
|
"account": "Manajemen UUID Pemain",
|
||||||
|
"currentUUID": "UUID Saat Ini",
|
||||||
|
"uuidPlaceholder": "Memuat UUID...",
|
||||||
|
"copyUUID": "Salin UUID",
|
||||||
|
"regenerateUUID": "Regenerasi UUID",
|
||||||
|
"uuidHint": "Pengidentifikasi pemain unikmu untuk nama pengguna ini",
|
||||||
|
"manageUUIDs": "Kelola Semua UUID",
|
||||||
|
"manageUUIDsDesc": "Lihat dan kelola semua UUID pemain",
|
||||||
|
"language": "Bahasa",
|
||||||
|
"selectLanguage": "Pilih Bahasa",
|
||||||
|
"repairGame": "Perbaiki Game",
|
||||||
|
"reinstallGame": "Instal ulang file game (tetap menyimpan data)",
|
||||||
|
"gpuPreference": "Preferensi GPU",
|
||||||
|
"gpuHint": "Fitur khusus laptop; setel ke Terintegrasi jika di PC",
|
||||||
|
"gpuAuto": "Otomatis",
|
||||||
|
"gpuIntegrated": "Terintegrasi",
|
||||||
|
"gpuDedicated": "Terdedikasi",
|
||||||
|
"logs": "LOG SISTEM",
|
||||||
|
"logsCopy": "Salin",
|
||||||
|
"logsRefresh": "Segarkan",
|
||||||
|
"logsFolder": "Buka Folder",
|
||||||
|
"logsLoading": "Memuat log...",
|
||||||
|
"closeLauncher": "Perilaku Launcher",
|
||||||
|
"closeOnStart": "Tutup launcher saat game dimulai",
|
||||||
|
"closeOnStartDescription": "Tutup launcher secara otomatis setelah Hytale diluncurkan",
|
||||||
|
"hwAccel": "Akselerasi Perangkat Keras",
|
||||||
|
"hwAccelDescription": "Aktifkan akselerasi perangkat keras untuk launcher`",
|
||||||
|
"gameBranch": "Cabang Game",
|
||||||
|
"branchRelease": "Rilis",
|
||||||
|
"branchPreRelease": "Pra-Rilis",
|
||||||
|
"branchHint": "Beralih antara rilis stabil dan versi pra-rilis eksperimental",
|
||||||
|
"branchWarning": "Mengubah cabang akan mengunduh dan menginstal versi game yang berbeda",
|
||||||
|
"branchSwitching": "Beralih ke {branch}...",
|
||||||
|
"branchSwitched": "Berhasil beralih ke {branch}!",
|
||||||
|
"installRequired": "Instalasi Diperlukan",
|
||||||
|
"branchInstallConfirm": "Game akan diinstal untuk cabang {branch}. Lanjutkan?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "Manajemen UUID",
|
||||||
|
"currentUserUUID": "UUID Pengguna Saat Ini",
|
||||||
|
"allPlayerUUIDs": "Semua UUID Pemain",
|
||||||
|
"generateNew": "Hasilkan UUID Baru",
|
||||||
|
"loadingUUIDs": "Memuat UUID...",
|
||||||
|
"setCustomUUID": "Setel UUID Kustom",
|
||||||
|
"customPlaceholder": "Masukkan UUID kustom (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Setel UUID",
|
||||||
|
"warning": "Peringatan: Menyetel UUID secara kustom akan mengubah identitas pemainmu saat ini",
|
||||||
|
"copyTooltip": "Salin UUID",
|
||||||
|
"regenerateTooltip": "Hasilkan UUID Baru"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Kelola Profil",
|
||||||
|
"newProfilePlaceholder": "Nama Profil Baru",
|
||||||
|
"createProfile": "Buat Profil"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Gabung komunitas Discord kami!",
|
||||||
|
"joinButton": "Gabung Discord"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Konfirmasi",
|
||||||
|
"cancel": "Batal",
|
||||||
|
"save": "Simpan",
|
||||||
|
"close": "Tutup",
|
||||||
|
"delete": "Hapus",
|
||||||
|
"edit": "Edit",
|
||||||
|
"loading": "Memuat...",
|
||||||
|
"apply": "Terapkan",
|
||||||
|
"install": "Instal"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Kesalahan: Data game tidak ditemukan",
|
||||||
|
"gameUpdatedSuccess": "Game berhasil diperbarui! 🎉",
|
||||||
|
"updateFailed": "Pembaruan gagal: {error}",
|
||||||
|
"updateError": "Kesalahan pembaruan: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence diaktifkan",
|
||||||
|
"discordDisabled": "Discord Rich Presence dinonaktifkan",
|
||||||
|
"discordSaveFailed": "Gagal menyimpan pengaturan Discord",
|
||||||
|
"playerNameRequired": "Silakan masukkan nama pemain yang valid",
|
||||||
|
"playerNameSaved": "Nama pemain berhasil disimpan",
|
||||||
|
"playerNameSaveFailed": "Gagal menyimpan nama pemain",
|
||||||
|
"uuidCopied": "UUID disalin ke papan klip!",
|
||||||
|
"uuidCopyFailed": "Gagal menyalin UUID",
|
||||||
|
"uuidRegenNotAvailable": "Regenerasi UUID tidak tersedia",
|
||||||
|
"uuidRegenFailed": "Gagal meregenerasi UUID",
|
||||||
|
"uuidGenerated": "UUID baru berhasil dihasilkan!",
|
||||||
|
"uuidGeneratedShort": "UUID baru dihasilkan!",
|
||||||
|
"uuidGenerateFailed": "Gagal menghasilkan UUID baru",
|
||||||
|
"uuidRequired": "Silakan masukkan UUID",
|
||||||
|
"uuidInvalidFormat": "Format UUID tidak valid",
|
||||||
|
"uuidSetFailed": "Gagal menyetel UUID kustom",
|
||||||
|
"uuidSetSuccess": "UUID kustom berhasil disetel!",
|
||||||
|
"uuidDeleteFailed": "Gagal menghapus UUID",
|
||||||
|
"uuidDeleteSuccess": "UUID berhasil dihapus!",
|
||||||
|
"modsDownloading": "Mengunduh {name}...",
|
||||||
|
"modsTogglingMod": "Beralih mod...",
|
||||||
|
"modsDeletingMod": "Menghapus mod...",
|
||||||
|
"modsLoadingMods": "Memuat mod dari CurseForge...",
|
||||||
|
"modsInstalledSuccess": "{name} berhasil diinstal! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} berhasil dihapus",
|
||||||
|
"modsDownloadFailed": "Gagal mengunduh mod: {error}",
|
||||||
|
"modsToggleFailed": "Gagal beralih mod: {error}",
|
||||||
|
"modsDeleteFailed": "Gagal menghapus mod: {error}",
|
||||||
|
"modsModNotFound": "Informasi mod tidak ditemukan",
|
||||||
|
"hwAccelSaved": "Pengaturan akselerasi perangkat keras disimpan",
|
||||||
|
"hwAccelSaveFailed": "Gagal menyimpan pengaturan akselerasi perangkat keras",
|
||||||
|
"noUsername": "Nama pengguna belum dikonfigurasi. Silakan simpan nama pengguna terlebih dahulu.",
|
||||||
|
"switchUsernameSuccess": "Berhasil beralih ke \"{username}\"!",
|
||||||
|
"switchUsernameFailed": "Gagal beralih nama pengguna",
|
||||||
|
"playerNameTooLong": "Nama pemain harus 16 karakter atau kurang"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Konfirmasi tindakan",
|
||||||
|
"regenerateUuidTitle": "Hasilkan UUID baru",
|
||||||
|
"regenerateUuidMessage": "Apakah kamu yakin ingin menghasilkan UUID baru? Ini akan mengubah identitas pemainmu.",
|
||||||
|
"regenerateUuidButton": "Hasilkan",
|
||||||
|
"setCustomUuidTitle": "Setel UUID kustom",
|
||||||
|
"setCustomUuidMessage": "Apakah kamu yakin ingin menyetel UUID kustom ini? Ini akan mengubah identitas pemainmu.",
|
||||||
|
"setCustomUuidButton": "Setel UUID",
|
||||||
|
"deleteUuidTitle": "Hapus UUID",
|
||||||
|
"deleteUuidMessage": "Apakah kamu yakin ingin menghapus UUID untuk \"{username}\"? Tindakan ini tidak dapat dibatalkan.",
|
||||||
|
"deleteUuidButton": "Hapus",
|
||||||
|
"uninstallGameTitle": "Hapus instalasi game",
|
||||||
|
"uninstallGameMessage": "Apakah kamu yakin ingin menghapus instalasi Hytale? Semua file game akan dihapus.",
|
||||||
|
"uninstallGameButton": "Hapus Instalasi",
|
||||||
|
"switchUsernameTitle": "Ganti Identitas",
|
||||||
|
"switchUsernameMessage": "Beralih ke nama pengguna \"{username}\"? Ini akan mengubah identitas pemain saat ini.",
|
||||||
|
"switchUsernameButton": "Ganti"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Menginisialisasi...",
|
||||||
|
"downloading": "Mengunduh...",
|
||||||
|
"installing": "Menginstal...",
|
||||||
|
"extracting": "Mengekstrak...",
|
||||||
|
"verifying": "Memverifikasi...",
|
||||||
|
"switchingProfile": "Beralih profil...",
|
||||||
|
"profileSwitched": "Profil dialihkan!",
|
||||||
|
"startingGame": "Memulai game...",
|
||||||
|
"launching": "MELUNCURKAN...",
|
||||||
|
"uninstallingGame": "Menghapus instalasi game...",
|
||||||
|
"gameUninstalled": "Instalasi game berhasil dihapus!",
|
||||||
|
"uninstallFailed": "Penghapusan instalasi gagal: {error}",
|
||||||
|
"startingUpdate": "Memulai pembaruan game wajib...",
|
||||||
|
"installationComplete": "Instalasi berhasil diselesaikan!",
|
||||||
|
"installationFailed": "Instalasi gagal: {error}",
|
||||||
|
"installingGameFiles": "Menginstal file game...",
|
||||||
|
"installComplete": "Instalasi selesai!"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,19 +4,20 @@
|
|||||||
"mods": "Mody",
|
"mods": "Mody",
|
||||||
"news": "Wiadomości",
|
"news": "Wiadomości",
|
||||||
"chat": "Chat z graczami",
|
"chat": "Chat z graczami",
|
||||||
"settings": "Ustawienia",
|
"settings": "Ustawienia"
|
||||||
"skins": "Skiny"
|
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"playersLabel": "Graczy:",
|
"playersLabel": "Graczy:",
|
||||||
"manageProfiles": "Zarządzaj Profilami",
|
"manageProfiles": "Zarządzaj Profilami",
|
||||||
"defaultProfile": "Domyślny",
|
"defaultProfile": "Domyślny"
|
||||||
"f2p": "FREE TO PLAY"
|
|
||||||
},
|
},
|
||||||
"install": {
|
"install": {
|
||||||
"title": "FREE TO PLAY LAUNCHER",
|
"title": "DARMOWY LAUNCHER",
|
||||||
"playerName": "Nazwa Gracza",
|
"playerName": "Nazwa Gracza",
|
||||||
"playerNamePlaceholder": "Wprowadź Nazwę",
|
"playerNamePlaceholder": "Wprowadź Nazwę",
|
||||||
|
"gameBranch": "Wersja Gry",
|
||||||
|
"releaseVersion": "Wydanie (Stabilna)",
|
||||||
|
"preReleaseVersion": "Przed-Wydaniem (Eksperymentalna)",
|
||||||
"customInstallation": "Dostosuj Instalacje",
|
"customInstallation": "Dostosuj Instalacje",
|
||||||
"installationFolder": "Folder docelowy",
|
"installationFolder": "Folder docelowy",
|
||||||
"pathPlaceholder": "Domyślna lokalizacja",
|
"pathPlaceholder": "Domyślna lokalizacja",
|
||||||
@@ -56,7 +57,9 @@
|
|||||||
"noDescription": "Brak opisu",
|
"noDescription": "Brak opisu",
|
||||||
"confirmDelete": "Czy na pewno chcesz usunąć \"{name}\"?",
|
"confirmDelete": "Czy na pewno chcesz usunąć \"{name}\"?",
|
||||||
"confirmDeleteDesc": "Tej czynności nie można cofnąć.",
|
"confirmDeleteDesc": "Tej czynności nie można cofnąć.",
|
||||||
"confirmDeletion": "Potwierdź"
|
"confirmDeletion": "Potwierdź",
|
||||||
|
"apiKeyRequired": "Wymagany Klucz API",
|
||||||
|
"apiKeyRequiredDesc": "Klucz API CurseForge jest potrzebny do przeglądania modów"
|
||||||
},
|
},
|
||||||
"news": {
|
"news": {
|
||||||
"title": "WSZYSTKIE WIADOMOŚCI",
|
"title": "WSZYSTKIE WIADOMOŚCI",
|
||||||
@@ -116,15 +119,29 @@
|
|||||||
"repairGame": "Napraw Grę",
|
"repairGame": "Napraw Grę",
|
||||||
"reinstallGame": "Zainstaluj ponownie pliki gry (zachowuje dane)",
|
"reinstallGame": "Zainstaluj ponownie pliki gry (zachowuje dane)",
|
||||||
"gpuPreference": "Preferencje GPU",
|
"gpuPreference": "Preferencje GPU",
|
||||||
"gpuHint": "Wybierz preferowany procesor graficzny (Linux: wpływa na DRI_PRIME)",
|
"gpuHint": "Funkcja dostępna tylko na laptopie; ustaw na Zintegrowaną, jeśli na komputerze PC",
|
||||||
"gpuAuto": "Auto",
|
"gpuAuto": "Auto",
|
||||||
"gpuIntegrated": "Zintegrowana",
|
"gpuIntegrated": "Zintegrowana",
|
||||||
"gpuDedicated": "Dedykowana",
|
"gpuDedicated": "Dedykowana",
|
||||||
"logs": "SYSTEM LOGS",
|
"logs": "DZIENNIKI SYSTEMOWE",
|
||||||
"logsCopy": "Kopiuj",
|
"logsCopy": "Kopiuj",
|
||||||
"logsRefresh": "Odśwież",
|
"logsRefresh": "Odśwież",
|
||||||
"logsFolder": "Otwórz Folder",
|
"logsFolder": "Otwórz Folder",
|
||||||
"logsLoading": "Ładowanie logów..."
|
"logsLoading": "Ładowanie logów...",
|
||||||
|
"closeLauncher": "Zachowanie Launchera",
|
||||||
|
"closeOnStart": "Zamknij Launcher przy starcie gry",
|
||||||
|
"closeOnStartDescription": "Automatycznie zamknij launcher po uruchomieniu Hytale",
|
||||||
|
"hwAccel": "Przyspieszenie Sprzętowe",
|
||||||
|
"hwAccelDescription": "Włącz przyspieszenie sprzętowe dla launchera",
|
||||||
|
"gameBranch": "Gałąź Gry",
|
||||||
|
"branchRelease": "Wydanie",
|
||||||
|
"branchPreRelease": "Przed-Wydaniem",
|
||||||
|
"branchHint": "Przełączaj między stabilnym wydaniem a eksperymentalną wersją przed-wydaniem",
|
||||||
|
"branchWarning": "Zmiana gałęzi spowoduje pobranie i instalację innej wersji gry",
|
||||||
|
"branchSwitching": "Przełączanie na {branch}...",
|
||||||
|
"branchSwitched": "Pomyślnie przełączono na {branch}!",
|
||||||
|
"installRequired": "Wymagana Instalacja",
|
||||||
|
"branchInstallConfirm": "Gra zostanie zainstalowana dla gałęzi {branch}. Kontynuować?"
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"modalTitle": "Zarządzanie UUID",
|
"modalTitle": "Zarządzanie UUID",
|
||||||
@@ -148,10 +165,6 @@
|
|||||||
"notificationText": "Dołącz do naszej społeczności Discord!",
|
"notificationText": "Dołącz do naszej społeczności Discord!",
|
||||||
"joinButton": "Dołącz Discord"
|
"joinButton": "Dołącz Discord"
|
||||||
},
|
},
|
||||||
"skins": {
|
|
||||||
"title": "Skiny",
|
|
||||||
"comingSoon": "Personalizacja skórek już wkrótce..."
|
|
||||||
},
|
|
||||||
"common": {
|
"common": {
|
||||||
"confirm": "Potwierdź",
|
"confirm": "Potwierdź",
|
||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
@@ -160,7 +173,8 @@
|
|||||||
"delete": "Usuń",
|
"delete": "Usuń",
|
||||||
"edit": "Edytuj",
|
"edit": "Edytuj",
|
||||||
"loading": "Ładowanie...",
|
"loading": "Ładowanie...",
|
||||||
"apply": "Zastosuj"
|
"apply": "Zastosuj",
|
||||||
|
"install": "Zainstaluj"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"gameDataNotFound": "Błąd: Nie znaleziono danych gry",
|
"gameDataNotFound": "Błąd: Nie znaleziono danych gry",
|
||||||
@@ -195,7 +209,13 @@
|
|||||||
"modsDownloadFailed": "Nie udało się pobrać moda: {error}",
|
"modsDownloadFailed": "Nie udało się pobrać moda: {error}",
|
||||||
"modsToggleFailed": "Nie udało się przełączyć moda: {error}",
|
"modsToggleFailed": "Nie udało się przełączyć moda: {error}",
|
||||||
"modsDeleteFailed": "Nie udało się usunąć moda: {error}",
|
"modsDeleteFailed": "Nie udało się usunąć moda: {error}",
|
||||||
"modsModNotFound": "Nie znaleziono informacji o modzie"
|
"modsModNotFound": "Nie znaleziono informacji o modzie",
|
||||||
|
"hwAccelSaved": "Zapisano ustawienie przyspieszenia sprzętowego",
|
||||||
|
"hwAccelSaveFailed": "Nie udało się zapisać ustawienia przyspieszenia sprzętowego",
|
||||||
|
"noUsername": "Nie skonfigurowano nazwy użytkownika. Najpierw zapisz swoją nazwę użytkownika.",
|
||||||
|
"switchUsernameSuccess": "Pomyślnie przełączono na \"{username}\"!",
|
||||||
|
"switchUsernameFailed": "Nie udało się przełączyć nazwy użytkownika",
|
||||||
|
"playerNameTooLong": "Nazwa gracza musi mieć 16 znaków lub mniej"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Potwierdź działanie",
|
"defaultTitle": "Potwierdź działanie",
|
||||||
@@ -210,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Usuń",
|
"deleteUuidButton": "Usuń",
|
||||||
"uninstallGameTitle": "Odinstaluj grę",
|
"uninstallGameTitle": "Odinstaluj grę",
|
||||||
"uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.",
|
"uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.",
|
||||||
"uninstallGameButton": "Odinstaluj"
|
"uninstallGameButton": "Odinstaluj",
|
||||||
|
"switchUsernameTitle": "Zmień tożsamość",
|
||||||
|
"switchUsernameMessage": "Przełączyć na nazwę użytkownika \"{username}\"? To zmieni Twoją aktualną tożsamość gracza.",
|
||||||
|
"switchUsernameButton": "Przełącz"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Inicjalizacja...",
|
"initializing": "Inicjalizacja...",
|
||||||
|
|||||||
@@ -14,9 +14,11 @@
|
|||||||
"install": {
|
"install": {
|
||||||
"title": "LANÇADOR JOGO GRATUITO",
|
"title": "LANÇADOR JOGO GRATUITO",
|
||||||
"playerName": "Nome do Jogador",
|
"playerName": "Nome do Jogador",
|
||||||
"playerNamePlaceholder": "Digite seu nome", "gameBranch": "Versão do Jogo",
|
"playerNamePlaceholder": "Digite seu nome",
|
||||||
|
"gameBranch": "Versão do Jogo",
|
||||||
"releaseVersion": "Lançamento (Estável)",
|
"releaseVersion": "Lançamento (Estável)",
|
||||||
"preReleaseVersion": "Pré-Lançamento (Experimental)", "customInstallation": "Instalação Personalizada",
|
"preReleaseVersion": "Pré-Lançamento (Experimental)",
|
||||||
|
"customInstallation": "Instalação Personalizada",
|
||||||
"installationFolder": "Pasta de Instalação",
|
"installationFolder": "Pasta de Instalação",
|
||||||
"pathPlaceholder": "Local padrão",
|
"pathPlaceholder": "Local padrão",
|
||||||
"browse": "Procurar",
|
"browse": "Procurar",
|
||||||
@@ -117,7 +119,7 @@
|
|||||||
"repairGame": "Reparar jogo",
|
"repairGame": "Reparar jogo",
|
||||||
"reinstallGame": "Reinstalar arquivos do jogo (mantém os dados)",
|
"reinstallGame": "Reinstalar arquivos do jogo (mantém os dados)",
|
||||||
"gpuPreference": "Preferência de GPU",
|
"gpuPreference": "Preferência de GPU",
|
||||||
"gpuHint": "Selecione sua GPU preferida (Linux: afeta o DRI_PRIME)",
|
"gpuHint": "Recurso exclusivo para laptops; defina como Integrado se estiver em um PC.",
|
||||||
"gpuAuto": "Automático",
|
"gpuAuto": "Automático",
|
||||||
"gpuIntegrated": "Integrada",
|
"gpuIntegrated": "Integrada",
|
||||||
"gpuDedicated": "Dedicada",
|
"gpuDedicated": "Dedicada",
|
||||||
@@ -129,6 +131,8 @@
|
|||||||
"closeLauncher": "Comportamento do Lançador",
|
"closeLauncher": "Comportamento do Lançador",
|
||||||
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
|
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
|
||||||
"closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado",
|
"closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado",
|
||||||
|
"hwAccel": "Aceleração de Hardware",
|
||||||
|
"hwAccelDescription": "Ativar aceleração de hardware para o lançador",
|
||||||
"gameBranch": "Versão do Jogo",
|
"gameBranch": "Versão do Jogo",
|
||||||
"branchRelease": "Lançamento",
|
"branchRelease": "Lançamento",
|
||||||
"branchPreRelease": "Pré-Lançamento",
|
"branchPreRelease": "Pré-Lançamento",
|
||||||
@@ -161,7 +165,6 @@
|
|||||||
"notificationText": "Junte-se à nossa comunidade do Discord!",
|
"notificationText": "Junte-se à nossa comunidade do Discord!",
|
||||||
"joinButton": "Entrar no Discord"
|
"joinButton": "Entrar no Discord"
|
||||||
},
|
},
|
||||||
|
|
||||||
"common": {
|
"common": {
|
||||||
"confirm": "Confirmar",
|
"confirm": "Confirmar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
@@ -206,7 +209,13 @@
|
|||||||
"modsDownloadFailed": "Falha ao baixar mod: {error}",
|
"modsDownloadFailed": "Falha ao baixar mod: {error}",
|
||||||
"modsToggleFailed": "Falha ao alternar mod: {error}",
|
"modsToggleFailed": "Falha ao alternar mod: {error}",
|
||||||
"modsDeleteFailed": "Falha ao excluir mod: {error}",
|
"modsDeleteFailed": "Falha ao excluir mod: {error}",
|
||||||
"modsModNotFound": "Informações do mod não encontradas"
|
"modsModNotFound": "Informações do mod não encontradas",
|
||||||
|
"hwAccelSaved": "Configuração de aceleração de hardware salva",
|
||||||
|
"hwAccelSaveFailed": "Falha ao salvar configuração de aceleração de hardware",
|
||||||
|
"noUsername": "Nenhum nome de usuário configurado. Por favor, salve seu nome de usuário primeiro.",
|
||||||
|
"switchUsernameSuccess": "Alterado para \"{username}\" com sucesso!",
|
||||||
|
"switchUsernameFailed": "Falha ao trocar nome de usuário",
|
||||||
|
"playerNameTooLong": "O nome do jogador deve ter 16 caracteres ou menos"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Confirmar ação",
|
"defaultTitle": "Confirmar ação",
|
||||||
@@ -221,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Excluir",
|
"deleteUuidButton": "Excluir",
|
||||||
"uninstallGameTitle": "Desinstalar jogo",
|
"uninstallGameTitle": "Desinstalar jogo",
|
||||||
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
|
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
|
||||||
"uninstallGameButton": "Desinstalar"
|
"uninstallGameButton": "Desinstalar",
|
||||||
|
"switchUsernameTitle": "Trocar Identidade",
|
||||||
|
"switchUsernameMessage": "Trocar para o nome de usuário \"{username}\"? Isso mudará sua identidade de jogador atual.",
|
||||||
|
"switchUsernameButton": "Trocar"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Inicializando...",
|
"initializing": "Inicializando...",
|
||||||
|
|||||||
257
GUI/locales/ru-RU.json
Normal file
257
GUI/locales/ru-RU.json
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Играть",
|
||||||
|
"mods": "Моды",
|
||||||
|
"news": "Новости",
|
||||||
|
"chat": "Чат игроков",
|
||||||
|
"settings": "Настройки"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Игроки:",
|
||||||
|
"manageProfiles": "Управлять профилями:",
|
||||||
|
"defaultProfile": "По умолчанию"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "FREE TO PLAY LAUNCHER",
|
||||||
|
"playerName": "Ник игрока",
|
||||||
|
"playerNamePlaceholder": "Введите ваш ник",
|
||||||
|
"gameBranch": "Версия игры",
|
||||||
|
"releaseVersion": "Релиз (Стабильная)",
|
||||||
|
"preReleaseVersion": "Пре-Релиз (Экспериментально)",
|
||||||
|
"customInstallation": "Модифицированная установка",
|
||||||
|
"installationFolder": "Папка установки",
|
||||||
|
"pathPlaceholder": "Путь по умолчанию",
|
||||||
|
"browse": "Обзор",
|
||||||
|
"installButton": "УСТАНОВИТЬ HYTALE",
|
||||||
|
"installing": "УСТАНОВКА..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "ГОТОВ К ИГРЕ",
|
||||||
|
"subtitle": "Запусти Hytale и приготовься к приключению!",
|
||||||
|
"playButton": "ЗАПУСТИТЬ HYTALE",
|
||||||
|
"latestNews": "ПОСЛЕДНИЕ НОВОСТИ",
|
||||||
|
"viewAll": "ПОСМОТРЕТЬ ВСЁ",
|
||||||
|
"checking": "ПРОВЕРКА...",
|
||||||
|
"play": "ЗАПУСТИТЬ"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Искать моды...",
|
||||||
|
"myMods": "Мои моды",
|
||||||
|
"previous": "Предыдущая",
|
||||||
|
"next": "Вперёд",
|
||||||
|
"page": "Страница",
|
||||||
|
"of": "",
|
||||||
|
"modalTitle": "МОИ МОДЫ",
|
||||||
|
"noModsFound": "Моды не найдены",
|
||||||
|
"noModsFoundDesc": "Попробуйте изменить свой запрос",
|
||||||
|
"noModsInstalled": "Нет установленных модов",
|
||||||
|
"noModsInstalledDesc": "Добавьте моды с CurseForge или импортируйте свои!",
|
||||||
|
"view": "Посмотреть",
|
||||||
|
"install": "Установить",
|
||||||
|
"installed": "УСТАНОВЛЕННЫЕ",
|
||||||
|
"enable": "ВКЛЮЧИТЬ",
|
||||||
|
"disable": "ВЫКЛЮЧИТЬ",
|
||||||
|
"active": "ВКЛЮЧЁН",
|
||||||
|
"disabled": "ВЫКЛЮЧЕН",
|
||||||
|
"delete": "Удалить мод",
|
||||||
|
"noDescription": "Нет доступного описания",
|
||||||
|
"confirmDelete": "Вы точно уверены, что хотите удалить \"{name}\"?",
|
||||||
|
"confirmDeleteDesc": "Это действие не отменить.",
|
||||||
|
"confirmDeletion": "Подтвердите удаление",
|
||||||
|
"apiKeyRequired": "Требуется ключ API",
|
||||||
|
"apiKeyRequiredDesc": "Ключ CurseForge API требуется для просмотра модов"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "ВСЕ НОВОСТИ",
|
||||||
|
"readMore": "Читать дальше"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "ЧАТ ИГРОКОВ",
|
||||||
|
"pickColor": "Цвет",
|
||||||
|
"inputPlaceholder": "Введите своё сообщение...",
|
||||||
|
"send": "Отправить",
|
||||||
|
"online": "онлайн",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Безопасный чат - все ссылки зацензурены",
|
||||||
|
"joinChat": "Присоединиться к чату",
|
||||||
|
"chooseUsername": "Выберите имя пользователя для входа в чат игроков",
|
||||||
|
"username": "Ник",
|
||||||
|
"usernamePlaceholder": "Введите ваш ник...",
|
||||||
|
"usernameHint": "3-20 символов, букв, цифр, только - и _",
|
||||||
|
"joinButton": "Присоединиться к чату",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Выберите цвет ника",
|
||||||
|
"chooseSolid": "Выберите цвет:",
|
||||||
|
"customColor": "Модифицированный цвет:",
|
||||||
|
"preview": "Предварительный просмотр:",
|
||||||
|
"previewUsername": "Ник",
|
||||||
|
"apply": "Применить цвет"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "НАСТРОЙКИ",
|
||||||
|
"java": "Java Runtime",
|
||||||
|
"useCustomJava": "Укажите свой путь Java",
|
||||||
|
"javaDescription": "Переопределить встроенный Java Runtime с вашей установкой",
|
||||||
|
"javaPath": "Путь исполняемого файла Java",
|
||||||
|
"javaPathPlaceholder": "Выберите путь Java...",
|
||||||
|
"javaBrowse": "Обзор",
|
||||||
|
"javaHint": "Выберите папку установки Java (поддерживается Windows, Mac, Linux)",
|
||||||
|
"discord": "Интеграция Discord",
|
||||||
|
"enableRPC": "Включить Discord Rich Presence",
|
||||||
|
"discordDescription": "Показывать вашу активность лаунчера в Discord",
|
||||||
|
"game": "Настройки игры",
|
||||||
|
"playerName": "Ник игрока",
|
||||||
|
"playerNamePlaceholder": "Введите ваш ник",
|
||||||
|
"playerNameHint": "Этот ник будет использован в игре (1-16 символов)",
|
||||||
|
"openGameLocation": "Открыть местоположение игры",
|
||||||
|
"openGameLocationDesc": "Открыть папку установки игры",
|
||||||
|
"account": "Управление UUID игрока",
|
||||||
|
"currentUUID": "Текущий UUID",
|
||||||
|
"uuidPlaceholder": "Загрузка UUID...",
|
||||||
|
"copyUUID": "Копировать UUID",
|
||||||
|
"regenerateUUID": "Перегенерировать UUID",
|
||||||
|
"uuidHint": "Уникальный идентификатор игрока для этого ника",
|
||||||
|
"manageUUIDs": "Управление всеми UUID",
|
||||||
|
"manageUUIDsDesc": "Смотреть и управлять всеми UUID игрока",
|
||||||
|
"language": "Язык",
|
||||||
|
"selectLanguage": "Выберите язык",
|
||||||
|
"repairGame": "Починить игру",
|
||||||
|
"reinstallGame": "Переустановить файлы игры (сохраняет данные)",
|
||||||
|
"gpuPreference": "Предпочтение GPU",
|
||||||
|
"gpuHint": "Функция доступна только на ноутбуках; при использовании на ПК выберите встроенную видеокарту.",
|
||||||
|
"gpuAuto": "Автоматический выбор",
|
||||||
|
"gpuIntegrated": "Интегрированная видеокарта",
|
||||||
|
"gpuDedicated": "Дискретная видеокарта",
|
||||||
|
"logs": "ЛОГИ",
|
||||||
|
"logsCopy": "Копировать",
|
||||||
|
"logsRefresh": "Обновить",
|
||||||
|
"logsFolder": "Открыть папку",
|
||||||
|
"logsLoading": "Загрузка логов...",
|
||||||
|
"closeLauncher": "Поведение лаунчера",
|
||||||
|
"closeOnStart": "Закрыть лаунчер при старте игры",
|
||||||
|
"closeOnStartDescription": "Автоматически закрыть лаунчер после запуска Hytale",
|
||||||
|
"hwAccel": "Аппаратное ускорение",
|
||||||
|
"hwAccelDescription": "Включить аппаратное ускорение для лаунчера",
|
||||||
|
"gameBranch": "Ветка игры",
|
||||||
|
"branchRelease": "Релиз",
|
||||||
|
"branchPreRelease": "Пре-Релиз",
|
||||||
|
"branchHint": "Переключает между релизом и пре-релизом игры",
|
||||||
|
"branchWarning": "Изменение ветки скачает и установит другую версию игры",
|
||||||
|
"branchSwitching": "Переключение на {branch}...",
|
||||||
|
"branchSwitched": "Переключение на {branch} выполнено успешно!",
|
||||||
|
"installRequired": "Необходима установка",
|
||||||
|
"branchInstallConfirm": "Игра будет установлена для ветки {branch}. Продолжить?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "Управление UUID",
|
||||||
|
"currentUserUUID": "UUID текущего пользователя",
|
||||||
|
"allPlayerUUIDs": "UUID всех игроков",
|
||||||
|
"generateNew": "Сгенерировать новый UUID",
|
||||||
|
"loadingUUIDs": "Загрузка UUID...",
|
||||||
|
"setCustomUUID": "Установить кастомный UUID",
|
||||||
|
"customPlaceholder": "Ввести кастомный UUID (форматы: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Установить UUID",
|
||||||
|
"warning": "Внимание! Установка кастомного UUID изменит вашу текущую личность игрока!",
|
||||||
|
"copyTooltip": "Скопировать UUID",
|
||||||
|
"regenerateTooltip": "Сгенерировать новый UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Управление профилями",
|
||||||
|
"newProfilePlaceholder": "Новое имя профиля",
|
||||||
|
"createProfile": "Создать профиль"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Присоединитесь к нашему сообществу в Discord!",
|
||||||
|
"joinButton": "Присоединиться к Discord"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Подтвердить",
|
||||||
|
"cancel": "Отменить",
|
||||||
|
"save": "Сохранить",
|
||||||
|
"close": "Закрыть",
|
||||||
|
"delete": "Удалить",
|
||||||
|
"edit": "Редактировать",
|
||||||
|
"loading": "Загружается...",
|
||||||
|
"apply": "Применить",
|
||||||
|
"install": "Установить"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Ошибка: данные игры не найдены",
|
||||||
|
"gameUpdatedSuccess": "Игра успешно обновлена! Ура! 🎉",
|
||||||
|
"updateFailed": "Обновление прервалось с ошибкой: {error}",
|
||||||
|
"updateError": "Ошибка обновления: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence включен",
|
||||||
|
"discordDisabled": "Discord Rich Presence выключен",
|
||||||
|
"discordSaveFailed": "Не удалось сохранить настройку Discord",
|
||||||
|
"playerNameRequired": "Пожалуйста, введите действительное имя игрока",
|
||||||
|
"playerNameSaved": "Имя игрока успешно сохранено!",
|
||||||
|
"playerNameSaveFailed": "Не удалось сохранить имя игрока",
|
||||||
|
"uuidCopied": "UUID скопирован в буфер обмена!",
|
||||||
|
"uuidCopyFailed": "Не удалось скопировать UUID",
|
||||||
|
"uuidRegenNotAvailable": "UUID перегенерация к сожалению не доступна",
|
||||||
|
"uuidRegenFailed": "Не удалось перегенерировать UUID",
|
||||||
|
"uuidGenerated": "Новый UUID сгенерирован успешно!",
|
||||||
|
"uuidGeneratedShort": "Новый UUID сгенерирован!",
|
||||||
|
"uuidGenerateFailed": "Не получилось сгенерировать новый UUID",
|
||||||
|
"uuidRequired": "Пожалуйста введите UUID",
|
||||||
|
"uuidInvalidFormat": "Неправильный формат UUID",
|
||||||
|
"uuidSetFailed": "Не удалось поставить кастомный UUID",
|
||||||
|
"uuidSetSuccess": "Кастомный UUID успешно установлен!",
|
||||||
|
"uuidDeleteFailed": "Не удалось удалить UUID",
|
||||||
|
"uuidDeleteSuccess": "Удаление UUID успешно завершено!",
|
||||||
|
"modsDownloading": "Скачивание {name}...",
|
||||||
|
"modsTogglingMod": "Включение мода...",
|
||||||
|
"modsDeletingMod": "Удаление мода...",
|
||||||
|
"modsLoadingMods": "Загрузка модов с CurseForge...",
|
||||||
|
"modsInstalledSuccess": "{name} успешно установлен! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} удалён успешно!",
|
||||||
|
"modsDownloadFailed": "Не получилось скачать мод: {error}",
|
||||||
|
"modsToggleFailed": "Не получилось включить мод: {error}",
|
||||||
|
"modsDeleteFailed": "Не получилось удалить мод: {error}",
|
||||||
|
"modsModNotFound": "Информация по моду не найдена",
|
||||||
|
"hwAccelSaved": "Настройка аппаратного ускорения сохранена!",
|
||||||
|
"hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения",
|
||||||
|
"noUsername": "Имя пользователя не настроено. Пожалуйста, сначала сохраните имя пользователя.",
|
||||||
|
"switchUsernameSuccess": "Успешно переключено на \"{username}\"!",
|
||||||
|
"switchUsernameFailed": "Не удалось переключить имя пользователя",
|
||||||
|
"playerNameTooLong": "Имя игрока должно быть не более 16 символов"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Подтвердить действие",
|
||||||
|
"regenerateUuidTitle": "Сгенерировать новый UUID",
|
||||||
|
"regenerateUuidMessage": "Вы уверены, что хотите сгенерировать новый UUID? Генерация нового UUID изменит вашу текущую личность игрока!",
|
||||||
|
"regenerateUuidButton": "Сгенерировать",
|
||||||
|
"setCustomUuidTitle": "Установить кастомный UUID",
|
||||||
|
"setCustomUuidMessage": "Вы уверены, что хотите установить кастомный UUID? Установка кастомного UUID изменит вашу текущую личность игрока!",
|
||||||
|
"setCustomUuidButton": "Установить UUID",
|
||||||
|
"deleteUuidTitle": "Удалить UUID",
|
||||||
|
"deleteUuidMessage": "Вы уверены, что хотите удалить UUID для \"{username}\"? Это действие необратимо!",
|
||||||
|
"deleteUuidButton": "Удалить",
|
||||||
|
"uninstallGameTitle": "Удалить игру",
|
||||||
|
"uninstallGameMessage": "Вы уверены, что хотите удалить Hytale? Все данные игры будут безвозвратно удалены!",
|
||||||
|
"uninstallGameButton": "Удалить",
|
||||||
|
"switchUsernameTitle": "Сменить личность",
|
||||||
|
"switchUsernameMessage": "Переключиться на имя пользователя \"{username}\"? Это изменит вашу текущую личность игрока.",
|
||||||
|
"switchUsernameButton": "Переключить"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Инициализация...",
|
||||||
|
"downloading": "Скачивание...",
|
||||||
|
"installing": "Установка...",
|
||||||
|
"extracting": "Извлечение...",
|
||||||
|
"verifying": "Проверка...",
|
||||||
|
"switchingProfile": "Смена профиля...",
|
||||||
|
"profileSwitched": "Профиль сменён!",
|
||||||
|
"startingGame": "Запуск игры...",
|
||||||
|
"launching": "ЗАПУСК...",
|
||||||
|
"uninstallingGame": "Удаление игры...",
|
||||||
|
"gameUninstalled": "Игра успешно удалена!",
|
||||||
|
"uninstallFailed": "Удаление игры прервано с ошибкой: {error}",
|
||||||
|
"startingUpdate": "Начало обязательного обновления игры...",
|
||||||
|
"installationComplete": "Установка успешно завершена!",
|
||||||
|
"installationFailed": "Установка прервана с ошибкой: {error}",
|
||||||
|
"installingGameFiles": "Установка файлов игры...",
|
||||||
|
"installComplete": "Установка завершена!"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"repairGame": "Reparera spel",
|
"repairGame": "Reparera spel",
|
||||||
"reinstallGame": "Ominstallera spelfiler (bevarar data)",
|
"reinstallGame": "Ominstallera spelfiler (bevarar data)",
|
||||||
"gpuPreference": "GPU-preferens",
|
"gpuPreference": "GPU-preferens",
|
||||||
"gpuHint": "Välj din föredragna GPU (Linux: påverkar DRI_PRIME)",
|
"gpuHint": "Endast för bärbar dator; inställd på Integrerad om den är på datorn",
|
||||||
"gpuAuto": "Auto",
|
"gpuAuto": "Auto",
|
||||||
"gpuIntegrated": "Integrerad",
|
"gpuIntegrated": "Integrerad",
|
||||||
"gpuDedicated": "Dedikerad",
|
"gpuDedicated": "Dedikerad",
|
||||||
@@ -212,39 +212,10 @@
|
|||||||
"modsModNotFound": "Moddinformation hittades inte",
|
"modsModNotFound": "Moddinformation hittades inte",
|
||||||
"hwAccelSaved": "Hårdvaruaccelerationsinställning sparad",
|
"hwAccelSaved": "Hårdvaruaccelerationsinställning sparad",
|
||||||
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning",
|
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning",
|
||||||
"javaPathCopied": "Java-sökväg kopierad till urklipp!",
|
"noUsername": "Inget användarnamn konfigurerat. Vänligen spara ditt användarnamn först.",
|
||||||
"javaPathCopyFailed": "Misslyckades med att kopiera Java-sökväg",
|
"switchUsernameSuccess": "Bytte till \"{username}\" framgångsrikt!",
|
||||||
"javaPathSaved": "Java-sökväg sparad framgångsrikt!",
|
"switchUsernameFailed": "Misslyckades med att byta användarnamn",
|
||||||
"javaPathSaveFailed": "Misslyckades med att spara Java-sökväg",
|
"playerNameTooLong": "Spelarnamnet måste vara 16 tecken eller mindre"
|
||||||
"javaPathInvalid": "Ogiltig Java-sökväg",
|
|
||||||
"javaPathReset": "Java-sökväg återställd till standardvärden",
|
|
||||||
"gameLocationError": "Kunde inte öppna spelplats",
|
|
||||||
"launcherRestartRequired": "Launcher-omstart krävs för att tillämpa ändringar",
|
|
||||||
"gameRepairConfirm": "Är du säker på att du vill reparera spelet? Detta kommer att ominstallera alla spelfiler.",
|
|
||||||
"gameRepairInProgress": "Reparerar spel...",
|
|
||||||
"gameRepairSuccess": "Spel reparerat framgångsrikt!",
|
|
||||||
"gameRepairFailed": "Spelreparation misslyckades: {error}",
|
|
||||||
"invalidUsername": "Ogiltigt användarnamn",
|
|
||||||
"usernameInUse": "Användarnamn upptaget",
|
|
||||||
"chatJoinSuccess": "Du har gått med i chatten!",
|
|
||||||
"chatJoinFailed": "Misslyckades med att gå med i chatten",
|
|
||||||
"messageTooLong": "Meddelande för långt",
|
|
||||||
"messageSent": "Meddelande skickat",
|
|
||||||
"messageSendFailed": "Misslyckades med att skicka meddelande",
|
|
||||||
"colorUpdated": "Färg uppdaterad!",
|
|
||||||
"colorUpdateFailed": "Misslyckades med att uppdatera färg",
|
|
||||||
"profileCreated": "Profil skapad framgångsrikt!",
|
|
||||||
"profileCreateFailed": "Misslyckades med att skapa profil",
|
|
||||||
"profileDeleted": "Profil borttagen",
|
|
||||||
"profileDeleteFailed": "Misslyckades med att ta bort profil",
|
|
||||||
"profileSwitched": "Bytte profil till: {name}",
|
|
||||||
"profileSwitchFailed": "Profilbyte misslyckades",
|
|
||||||
"invalidProfileName": "Ogiltigt profilnamn",
|
|
||||||
"profileNameExists": "En profil med detta namn finns redan",
|
|
||||||
"noInternet": "Ingen internetanslutning",
|
|
||||||
"checkInternetConnection": "Kontrollera din internetanslutning",
|
|
||||||
"serverError": "Serverfel. Försök igen senare.",
|
|
||||||
"unknownError": "Ett okänt fel inträffade"
|
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Bekräfta åtgärd",
|
"defaultTitle": "Bekräfta åtgärd",
|
||||||
@@ -259,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Ta bort",
|
"deleteUuidButton": "Ta bort",
|
||||||
"uninstallGameTitle": "Avinstallera spel",
|
"uninstallGameTitle": "Avinstallera spel",
|
||||||
"uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.",
|
"uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.",
|
||||||
"uninstallGameButton": "Avinstallera"
|
"uninstallGameButton": "Avinstallera",
|
||||||
|
"switchUsernameTitle": "Byt identitet",
|
||||||
|
"switchUsernameMessage": "Byta till användarnamn \"{username}\"? Detta kommer att ändra din nuvarande spelaridentitet.",
|
||||||
|
"switchUsernameButton": "Byt"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Initierar...",
|
"initializing": "Initierar...",
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"repairGame": "Oyunu Onarı",
|
"repairGame": "Oyunu Onarı",
|
||||||
"reinstallGame": "Oyun dosyalarını yeniden kur (veri korur)",
|
"reinstallGame": "Oyun dosyalarını yeniden kur (veri korur)",
|
||||||
"gpuPreference": "GPU Tercihi",
|
"gpuPreference": "GPU Tercihi",
|
||||||
"gpuHint": "Tercih ettiğiniz GPU'yu seçin (Linux: DRI_PRIME'ı etkiler)",
|
"gpuHint": "Sadece dizüstü bilgisayarlarda bulunan bir özellik; PC'de kullanılıyorsa Entegre olarak ayarlayın.",
|
||||||
"gpuAuto": "Otomatik",
|
"gpuAuto": "Otomatik",
|
||||||
"gpuIntegrated": "Entegre",
|
"gpuIntegrated": "Entegre",
|
||||||
"gpuDedicated": "Ayrılmış",
|
"gpuDedicated": "Ayrılmış",
|
||||||
@@ -131,6 +131,8 @@
|
|||||||
"closeLauncher": "Başlatıcı Davranışı",
|
"closeLauncher": "Başlatıcı Davranışı",
|
||||||
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
|
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
|
||||||
"closeOnStartDescription": "Hytale başlatıldıktan sonra başlatıcıyı otomatik olarak kapatın",
|
"closeOnStartDescription": "Hytale başlatıldıktan sonra başlatıcıyı otomatik olarak kapatın",
|
||||||
|
"hwAccel": "Donanım Hızlandırma",
|
||||||
|
"hwAccelDescription": "Başlatıcı için donanım hızlandırmasını etkinleştir",
|
||||||
"gameBranch": "Oyun Dalı",
|
"gameBranch": "Oyun Dalı",
|
||||||
"branchRelease": "Yayın",
|
"branchRelease": "Yayın",
|
||||||
"branchPreRelease": "Ön-Yayın",
|
"branchPreRelease": "Ön-Yayın",
|
||||||
@@ -207,7 +209,13 @@
|
|||||||
"modsDownloadFailed": "Mod indirilemedi: {error}",
|
"modsDownloadFailed": "Mod indirilemedi: {error}",
|
||||||
"modsToggleFailed": "Mod değiştirilemedi: {error}",
|
"modsToggleFailed": "Mod değiştirilemedi: {error}",
|
||||||
"modsDeleteFailed": "Mod silinemedi: {error}",
|
"modsDeleteFailed": "Mod silinemedi: {error}",
|
||||||
"modsModNotFound": "Mod bilgileri bulunamadı"
|
"modsModNotFound": "Mod bilgileri bulunamadı",
|
||||||
|
"hwAccelSaved": "Donanım hızlandırma ayarı kaydedildi",
|
||||||
|
"hwAccelSaveFailed": "Donanım hızlandırma ayarı kaydedilemedi",
|
||||||
|
"noUsername": "Kullanıcı adı yapılandırılmadı. Lütfen önce kullanıcı adınızı kaydedin.",
|
||||||
|
"switchUsernameSuccess": "\"{username}\" adına başarıyla geçildi!",
|
||||||
|
"switchUsernameFailed": "Kullanıcı adı değiştirilemedi",
|
||||||
|
"playerNameTooLong": "Oyuncu adı 16 karakter veya daha az olmalıdır"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Eylemi onayla",
|
"defaultTitle": "Eylemi onayla",
|
||||||
@@ -222,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Sil",
|
"deleteUuidButton": "Sil",
|
||||||
"uninstallGameTitle": "Oyunu kaldır",
|
"uninstallGameTitle": "Oyunu kaldır",
|
||||||
"uninstallGameMessage": "Hytale'yi kaldırmak istediğinizden emin misiniz? Tüm oyun dosyaları silinecektir.",
|
"uninstallGameMessage": "Hytale'yi kaldırmak istediğinizden emin misiniz? Tüm oyun dosyaları silinecektir.",
|
||||||
"uninstallGameButton": "Kaldır"
|
"uninstallGameButton": "Kaldır",
|
||||||
|
"switchUsernameTitle": "Kimlik Değiştir",
|
||||||
|
"switchUsernameMessage": "\"{username}\" kullanıcı adına geçilsin mi? Bu mevcut oyuncu kimliğinizi değiştirecektir.",
|
||||||
|
"switchUsernameButton": "Değiştir"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Başlatılıyor...",
|
"initializing": "Başlatılıyor...",
|
||||||
|
|||||||
494
GUI/style.css
494
GUI/style.css
@@ -333,109 +333,6 @@ body {
|
|||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.discord-notification {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: rgba(0, 0, 0, 0.9);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border: 1px solid rgba(88, 101, 242, 0.3);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1rem;
|
|
||||||
max-width: 300px;
|
|
||||||
z-index: 10000;
|
|
||||||
pointer-events: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
||||||
animation: slideIn 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-notification .fab.fa-discord {
|
|
||||||
color: #5865f2;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-text {
|
|
||||||
color: white;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-action {
|
|
||||||
background: #5865f2;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
color: white;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
font-family: 'Space Grotesk', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-action:hover {
|
|
||||||
background: #4752c4;
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #9ca3af;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-close:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-close i {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discord-notification.hidden {
|
|
||||||
animation: slideOut 0.3s ease-in forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideOut {
|
|
||||||
to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-btn {
|
.control-btn {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@@ -1107,6 +1004,216 @@ body {
|
|||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Featured Servers Styles */
|
||||||
|
.featured-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
height: calc(100vh - 180px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-left,
|
||||||
|
.featured-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(147, 51, 234, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(147, 51, 234, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-server-card {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
min-height: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-server-card:hover {
|
||||||
|
transform: translateX(4px);
|
||||||
|
border-color: rgba(147, 51, 234, 0.5);
|
||||||
|
box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-server-banner {
|
||||||
|
width: 200px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: linear-gradient(135deg, #1e293b, #334155);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-server-content {
|
||||||
|
padding: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-server-name {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-server-address {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-address-text {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-address-btn {
|
||||||
|
background: linear-gradient(135deg, #9333ea, #7c3aed);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-address-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-address-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-address-btn.copied {
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner i {
|
||||||
|
color: #9333ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* My server card - without banner */
|
||||||
|
.my-server-card {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 1.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-server-card:hover {
|
||||||
|
transform: translateX(4px);
|
||||||
|
border-color: rgba(147, 51, 234, 0.5);
|
||||||
|
box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-server-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-server-name {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-server-address {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 0.625rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.news-view-all:hover {
|
.news-view-all:hover {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
@@ -1291,6 +1398,26 @@ body {
|
|||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.settings-content {
|
||||||
|
max-width: 1000px;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-column {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-group {
|
.setting-group {
|
||||||
@@ -2811,6 +2938,126 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.discord-popup-modal {
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-header i {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #5865f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-body {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #d1d5db;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-text strong {
|
||||||
|
color: #5865f2;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
color: #fbbf24;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-warning i {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.875rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn.primary {
|
||||||
|
background: #5865f2;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 0 0 #4752c4, 0 8px 20px rgba(88, 101, 242, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn.primary:hover {
|
||||||
|
background: #4752c4;
|
||||||
|
box-shadow: 0 2px 0 0 #4752c4, 0 12px 30px rgba(88, 101, 242, 0.4);
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn.primary:active {
|
||||||
|
transform: translateY(4px);
|
||||||
|
box-shadow: 0 0px 0 0 #4752c4, 0 4px 15px rgba(88, 101, 242, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn.secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #d1d5db;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn.secondary:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn.primary:disabled:hover {
|
||||||
|
background: #5865f2;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-popup-btn.secondary:disabled:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
.mods-grid::-webkit-scrollbar-thumb:hover,
|
.mods-grid::-webkit-scrollbar-thumb:hover,
|
||||||
.modal-body::-webkit-scrollbar-thumb:hover {
|
.modal-body::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(147, 51, 234, 0.8);
|
background: rgba(147, 51, 234, 0.8);
|
||||||
@@ -3795,10 +4042,7 @@ body {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
|
||||||
|
|
||||||
.chat-container {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100vh - 180px);
|
height: calc(100vh - 180px);
|
||||||
@@ -4242,6 +4486,12 @@ body {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.settings-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.settings-header {
|
.settings-header {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -4271,6 +4521,11 @@ body {
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-content .settings-section,
|
||||||
|
.settings-column .settings-section {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-section:hover {
|
.settings-section:hover {
|
||||||
border-color: rgba(147, 51, 234, 0.3);
|
border-color: rgba(147, 51, 234, 0.3);
|
||||||
box-shadow: 0 8px 32px rgba(147, 51, 234, 0.1);
|
box-shadow: 0 8px 32px rgba(147, 51, 234, 0.1);
|
||||||
@@ -4981,6 +5236,21 @@ select.settings-input option {
|
|||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
-webkit-user-select: text !important;
|
||||||
|
-moz-user-select: text !important;
|
||||||
|
-ms-user-select: text !important;
|
||||||
|
user-select: text !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure all input fields allow text selection and paste */
|
||||||
|
input[type="text"].uuid-input,
|
||||||
|
#customUuidInput {
|
||||||
|
-webkit-user-select: text !important;
|
||||||
|
-moz-user-select: text !important;
|
||||||
|
-ms-user-select: text !important;
|
||||||
|
user-select: text !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uuid-btn {
|
.uuid-btn {
|
||||||
@@ -5368,6 +5638,12 @@ select.settings-input option {
|
|||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uuid-item-btn.switch:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
border-color: rgba(59, 130, 246, 0.4);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.uuid-modal-content {
|
.uuid-modal-content {
|
||||||
width: 95vw;
|
width: 95vw;
|
||||||
@@ -5441,8 +5717,8 @@ select.settings-input option {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles pour le sélecteur de couleur dans le chat */
|
.settings-container {
|
||||||
.chat-header-actions {
|
padding: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
4
PKGBUILD
4
PKGBUILD
@@ -2,7 +2,7 @@
|
|||||||
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
||||||
# This PKGBUILD is for Github Releases
|
# This PKGBUILD is for Github Releases
|
||||||
pkgname=Hytale-F2P
|
pkgname=Hytale-F2P
|
||||||
pkgver=2.1.1
|
pkgver=2.2.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support"
|
pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
@@ -10,6 +10,8 @@ url="https://github.com/amiayweb/Hytale-F2P"
|
|||||||
license=('custom')
|
license=('custom')
|
||||||
depends=('gtk3' 'nss' 'libxcrypt-compat')
|
depends=('gtk3' 'nss' 'libxcrypt-compat')
|
||||||
makedepends=('npm')
|
makedepends=('npm')
|
||||||
|
provides=('Hytale-F2P-git' 'hytale-f2p-git')
|
||||||
|
conflicts=('Hytale-F2P-git' 'hytale-f2p-git')
|
||||||
source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
|
source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
|
||||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||||
|
|
||||||
|
|||||||
14
PKGBUILD-git
14
PKGBUILD-git
@@ -10,25 +10,25 @@ url="https://github.com/amiayweb/Hytale-F2P"
|
|||||||
license=('custom')
|
license=('custom')
|
||||||
depends=('gtk3' 'nss' 'libxcrypt-compat')
|
depends=('gtk3' 'nss' 'libxcrypt-compat')
|
||||||
makedepends=('git' 'npm')
|
makedepends=('git' 'npm')
|
||||||
|
provides=('Hytale-F2P' 'hytale-f2p-git')
|
||||||
|
conflicts=('Hytale-F2P' 'hytale-f2p-git')
|
||||||
source=("git+$url.git" "$_pkgname.desktop")
|
source=("git+$url.git" "$_pkgname.desktop")
|
||||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||||
|
|
||||||
pkgver() {
|
pkgver() {
|
||||||
cd "$srcdir/$_pkgname"
|
cd "$_pkgname"
|
||||||
git describe --tags --long | sed 's/^v//;s/-/.r/;s/-/./'
|
git describe --tags --long | sed 's/^v//;s/-/.r/;s/-/./'
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$srcdir/$_pkgname"
|
cd "$_pkgname"
|
||||||
npm ci
|
npm ci
|
||||||
npm run build:arch
|
npm run build:arch
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "$srcdir/$_pkgname"
|
mkdir -p "$pkgdir/opt/$_pkgname"
|
||||||
install -d "$pkgdir/opt/$_pkgname"
|
|
||||||
cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname"
|
cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname"
|
||||||
|
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
||||||
install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
install -Dm644 "$_pkgname/GUI/icon.png" "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png"
|
||||||
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png"
|
|
||||||
}
|
}
|
||||||
|
|||||||
149
README.md
149
README.md
@@ -4,19 +4,20 @@
|
|||||||
<h1>🎮 Hytale F2P Launcher 🚀</h1>
|
<h1>🎮 Hytale F2P Launcher 🚀</h1>
|
||||||
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
|
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
|
||||||
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
|
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
|
||||||
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)</small></p>
|
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support!</small></p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
[](https://github.com/amiayweb/Hytale-F2P/stargazers)
|
[](https://github.com/amiayweb/Hytale-F2P/stargazers)
|
||||||
[](https://github.com/amiayweb/Hytale-F2P/network/members)
|
[](https://github.com/amiayweb/Hytale-F2P/network/members)
|
||||||
|
|
||||||
⭐ **If you find this project useful, please give it a STAR!** ⭐
|
⭐ **If you find this project useful, please give it a STAR!** ⭐
|
||||||
|
|
||||||
### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
### ⚠️ **READ [QUICK START](README.md#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
||||||
|
|
||||||
#### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
|
#### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
|
||||||
|
|
||||||
@@ -35,37 +36,37 @@
|
|||||||
## 📸 Screenshots
|
## 📸 Screenshots
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="https://i.imgur.com/xW9do3d.png" alt="Hytale F2P Launcher" width="1000">
|
<img src="https://i.imgur.com/wwuuMUf.png" alt="Hytale F2P Launcher" width="1000">
|
||||||
<details>
|
<details>
|
||||||
<summary><b>View Gallery</b></summary>
|
<summary><b>View Gallery</b></summary>
|
||||||
<table style="width: 100%; border-spacing: 15px; border-collapse: separate;">
|
<table style="width: 100%; border-spacing: 15px; border-collapse: separate;">
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="vertical-align: top; width: 50%;">
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
<b>Mods Preview</b><br>
|
<b>Featured Servers 🆕</b><br>
|
||||||
<img src="https://i.imgur.com/f8qyIJy.png" alt="Hytale F2P Mods" width="100%">
|
<img src="https://i.imgur.com/fEu9y3Z.png" alt="Hytale F2P Featured Servers" width="100%">
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="vertical-align: top; width: 50%;">
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
<b>Latest News</b><br>
|
<b>Settings Page ⚙️</b><br>
|
||||||
<img src="https://i.imgur.com/qu0HltD.png" alt="Hytale F2P News" width="100%">
|
<img src="https://i.imgur.com/l5iBzxc.png" alt="Hytale F2P Settings Page" width="100%">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="vertical-align: top; width: 50%;">
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
<b>Social & Chat</b><br>
|
<b>Downloadable Mods from CurseForge 🛠️</b><br>
|
||||||
<img src="https://i.imgur.com/t3GmbfF.png" alt="Hytale F2P Chat" width="100%">
|
<img src="https://i.imgur.com/QIDbqYn.png" alt="Hytale F2P Mods Download" width="100%">
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="vertical-align: top; width: 50%;">
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
<b>Settings</b><br>
|
<b>My Mods Menu 🔧</b><br>
|
||||||
<img src="https://i.imgur.com/uUD7lDB.png" alt="Hytale F2P Settings" width="100%">
|
<img src="https://i.imgur.com/rjvwUfq.png" alt="Hytale F2P My Mods Menu" width="100%">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" style="vertical-align: top; width: 50%;">
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
<b>In-Game Screenshot - Spawn Point</b><br>
|
<b>In-Game Screenshot - Spawn Point 🎮</b><br>
|
||||||
<img src="https://i.imgur.com/X8lNFQ7.png" alt="Hytale F2P In-Game Screenshot-1" width="100%">
|
<img src="https://i.imgur.com/X8lNFQ7.png" alt="Hytale F2P In-Game Screenshot-1" width="100%">
|
||||||
</td>
|
</td>
|
||||||
<td align="center" style="vertical-align: top; width: 50%;">
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
<b>In-Game Screenshot - Gameplay Terrain</b><br>
|
<b>In-Game Screenshot - Gameplay Terrain 🌳</b><br>
|
||||||
<img src="https://i.imgur.com/3iRScPa.png" alt="Hytale F2P In-Game Screenshot-2" width="100%">
|
<img src="https://i.imgur.com/3iRScPa.png" alt="Hytale F2P In-Game Screenshot-2" width="100%">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
🎯 **Core Features**
|
🎯 **Core Features**
|
||||||
- 🔄 **Automatic Updates** - Smart version checking and seamless game updates
|
- 🔄 **Automatic Updates** - Smart version checking and seamless game updates
|
||||||
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
|
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
|
||||||
- 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS
|
- 🌐 **Cross-Platform** - Full support for Windows x64, Linux x64 (X11/Wayland, SteamDeck), and macOS Silicon
|
||||||
- ☕ **Java Management** - Automatic Java runtime detection and installation
|
- ☕ **Java Management** - Automatic Java runtime detection and installation
|
||||||
- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows, macOS & Linux !)
|
- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows, macOS & Linux !)
|
||||||
|
|
||||||
@@ -87,7 +88,6 @@
|
|||||||
- 📁 **Custom Installation** - Choose your own installation directory
|
- 📁 **Custom Installation** - Choose your own installation directory
|
||||||
- 🔍 **Smart Detection** - Automatic game and dependency detection
|
- 🔍 **Smart Detection** - Automatic game and dependency detection
|
||||||
- 🗂️ **Mod Support** - Built-in mod management system
|
- 🗂️ **Mod Support** - Built-in mod management system
|
||||||
- 💬 **Player Chat** - Integrated chat system for community interaction
|
|
||||||
- 📰 **News Feed** - Stay updated with the latest Hytale news
|
- 📰 **News Feed** - Stay updated with the latest Hytale news
|
||||||
- 🎨 **Modern UI** - Clean, responsive interface with dark theme
|
- 🎨 **Modern UI** - Clean, responsive interface with dark theme
|
||||||
|
|
||||||
@@ -118,9 +118,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><b>🖥️ OS</b></td>
|
<td><b>🖥️ OS</b></td>
|
||||||
<td colspan="3" align="center">
|
<td colspan="3" align="center">
|
||||||
Windows 10/11 (64-bit; X64/ARM64) | Linux (x64/ARM64) | macOS (Apple Silicon only)
|
Windows 10/11 (64-bit X64) | Linux (x64) | macOS (ARM64/Apple Silicon)
|
||||||
<br />
|
<br />
|
||||||
<small><i>⚠️ Note: macOS Intel (x86) is not yet supported <sup><a href="#fn1" id="ref1">1</a></sup></i></small>
|
<small><i>⚠️ Note: ARM64 (Windows & Linux), macOS (x86/Intel) <b>are not supported!</b> ⚠️</i></small>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>🧠 RAM</b></td>
|
<td><b>🧠 RAM</b></td>
|
||||||
<td>8GB (dGPU)<sup><a href="#fn1" id="ref2">2</a></sup> /<br>12GB (iGPU)<sup><a href="#fn1" id="ref3">3</a></sup></td>
|
<td>8GB (dGPU) / 12GB (iGPU)<sup><a href="#fn1" id="ref1">1</a></sup></td>
|
||||||
<td>16 GB</td>
|
<td>16 GB</td>
|
||||||
<td>32 GB</td>
|
<td>32 GB</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -156,31 +156,28 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p id="fn1"><sup>Note 1</sup> Hytale did not provide game files for macOS Intel, yet.</p>
|
<p id="fn1"><sup>Note 1</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum, while using Integrated GPU (iGPU) must have 12 GB RAM.</p>
|
||||||
<p id="fn2"><sup>Note 2</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.</p>
|
|
||||||
<p id="fn3"><sup>Note 3</sup> Using Integrated GPU (dGPU) must have 12 GB RAM minimum.</p>
|
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Our launcher has **not yet** supported Offline Mode (playing Hytale without internet).
|
||||||
|
> We will surely add the feature as soon as possible. Kindly wait for the update.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 🪟 Windows Prequisites
|
### 🪟 Windows Prequisites
|
||||||
* **
|
|
||||||
* **Java JDK 25:**
|
* **Java JDK 25:**
|
||||||
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21.
|
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows)
|
||||||
* [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only.
|
* [Adoptium](https://adoptium.net/temurin/releases/?version=25)
|
||||||
* [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25.
|
* [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25.
|
||||||
* Download from any vendor if your OS is not Windows with ARM64 architecture.
|
|
||||||
* **Latest Visual Studio Redist:**
|
* **Latest Visual Studio Redist:**
|
||||||
* Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe)
|
* Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe)
|
||||||
* Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
|
* Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
|
||||||
|
|
||||||
### 🐧 Linux Prequisites
|
### 🐧 Linux Prequisites
|
||||||
|
|
||||||
> [!WARNING]
|
* Make sure you have already installed newest **GPU driver** especially proprietary NVIDIA, consult your distro docs or wiki.
|
||||||
> Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973).
|
* Also make sure that your GPU can be connected to EGL, try checking it first (again, consult your distro docs or wiki) before installing Hytale game via our launcher.
|
||||||
|
* Install `libpng` package to avoid `SDL3_Image` error:
|
||||||
* Make sure you have already installed newest **GPU driver**, consult your distro docs or wiki.
|
|
||||||
|
|
||||||
* Install `libpng` package to avoid SDL3_Image error:
|
|
||||||
* `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro
|
* `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro
|
||||||
* `libpng libpng-devel` for Fedora/RHEL-based Distro
|
* `libpng libpng-devel` for Fedora/RHEL-based Distro
|
||||||
* `libpng` for Arch-based Distro
|
* `libpng` for Arch-based Distro
|
||||||
@@ -194,11 +191,12 @@
|
|||||||
1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above.
|
1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above.
|
||||||
2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
|
2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
|
||||||
3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
|
3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
|
||||||
* Click **More info**.
|
* Click **More info**, then click **Run anyway**.
|
||||||
* Click **Run anyway**.
|
|
||||||
4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu.
|
4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu.
|
||||||
|
5. **Whitelist in Windows Firewall** [#192](https://github.com/amiayweb/Hytale-F2P/issues/192#issuecomment-3803042908)
|
||||||
---
|
* Open the Windows Start Menu and search for `Allow an app through Windows Firewall`
|
||||||
|
* Click "Change settings" (you may need Admin privileges) and Locate `HytaleClient.exe` in the list.
|
||||||
|
* Ensure both the Private and Public checkboxes are checked. Click OK to save.
|
||||||
|
|
||||||
### 🐧 Linux Installation
|
### 🐧 Linux Installation
|
||||||
|
|
||||||
@@ -219,7 +217,7 @@
|
|||||||
# Fedora/RHEL-based
|
# Fedora/RHEL-based
|
||||||
sudo dnf install hytale-f2p-launcher.rpm
|
sudo dnf install hytale-f2p-launcher.rpm
|
||||||
# Debian/Ubuntu
|
# Debian/Ubuntu
|
||||||
sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76
|
sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76 # Not needed in v2.2.0+
|
||||||
sudo dpkg -i hytale-f2p-launcher.deb
|
sudo dpkg -i hytale-f2p-launcher.deb
|
||||||
```
|
```
|
||||||
* **Arch Linux (pacman):** Install the package using:
|
* **Arch Linux (pacman):** Install the package using:
|
||||||
@@ -238,13 +236,6 @@
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Make sure to adjust the filename correctly with the version and the architecture type. TIP: Use `cd` command to the package location.
|
> Make sure to adjust the filename correctly with the version and the architecture type. TIP: Use `cd` command to the package location.
|
||||||
|
|
||||||
4. **Troubleshooting:**
|
|
||||||
* **FUSE:** If the AppImage fails to launch on newer distributions, ensure `libfuse2` (or `fuse2` on Arch/Fedora) is installed.
|
|
||||||
* **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid.
|
|
||||||
* Missing libxcrypt.so.1: Install `libxcrypt-compat` using your package manager
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🍎 macOS Installation
|
### 🍎 macOS Installation
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
@@ -259,7 +250,7 @@
|
|||||||
* Look for the message regarding "Hytale F2P Launcher" and click **Open Anyway**.
|
* Look for the message regarding "Hytale F2P Launcher" and click **Open Anyway**.
|
||||||
* Authenticate with your password and click **Open**.
|
* Authenticate with your password and click **Open**.
|
||||||
|
|
||||||
#### **Advanced: Manual Installation (.zip)**
|
#### **Advanced macOS: Manual Installation (.zip)**
|
||||||
The `.zip` version is useful for users who prefer a portable installation or need to bypass specific permission issues.
|
The `.zip` version is useful for users who prefer a portable installation or need to bypass specific permission issues.
|
||||||
|
|
||||||
1. **Extract:** Download and unzip the file to your desired location (e.g., `~/Applications`).
|
1. **Extract:** Download and unzip the file to your desired location (e.g., `~/Applications`).
|
||||||
@@ -272,34 +263,41 @@ The `.zip` version is useful for users who prefer a portable installation or nee
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# How to Host a Server
|
# 📢 How to Host a Server
|
||||||
|
|
||||||
## Host your Singleplayer Server (Online-Play Feature)
|
## 🌐 Host your Singleplayer Server (Online-Play Feature)
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> You have to play the game to host the server. See Dedicated Server section below if you want to host it without you playing as the host.
|
> You have to play the game to host the server. See Dedicated Server section below if you want to host it without you playing as the host.
|
||||||
|
|
||||||
1. Open your Singleplayer World
|
1. Open your Singleplayer World
|
||||||
2. Pause the game (Esc) > select Online Play > Turn on `Allow Other Players to Join` > Set password if needed > Press `Save`.
|
2. Pause the game (Esc) > select Online Play > Turn on `Allow Other Players to Join` > Set password if needed > Press `Save`.
|
||||||
3. Check the status `Connected via STUN` or `Connected via UPnP`.
|
3. Check the status `Connected via UPnP`.
|
||||||
|
4. If your friends can't connect to your hosted Online-Play feature, please follow **Local Dedicated Server** tutorial.
|
||||||
|
|
||||||
## Dedicated Server
|
## 🖧 Host a Dedicated Server
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If you have already `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
|
> If you already have the patched `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server. Put the `.bat`/`.sh` script from our Discord server inside your `.../latest/Server` folder.
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
|
> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting).
|
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). Additional: **Linux ARM64** is supported for server only, not client.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> See detailed information of setting up a server here: [SERVER.md](SERVER.md)
|
> See detailed information of setting up a server here: [SERVER.md](SERVER.md). Download the latest patched JAR, the patched RAR, or the SH/BAT scripts from channel `#open-public-server` in our Discord Server.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Building from Source
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed Troubleshooting guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔨 Building from Source
|
||||||
|
|
||||||
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
||||||
|
|
||||||
@@ -307,15 +305,28 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
|||||||
|
|
||||||
## 📋 Changelog
|
## 📋 Changelog
|
||||||
|
|
||||||
### 🆕 v2.1.1
|
### 🆕 v2.2.0
|
||||||
|
- 🔃 **Game Patches Auto-Update Improvement:** No need to install 1.5GB for every updates! Game updates now reduced to almost **~90%** (Hytale Game Update 3 to 4 only take ~150MB).
|
||||||
|
- 🩹 **Improved Patch System Pre-Release JAR:** In previous version, only Release JAR could be patched. Now it also can be used for Pre-Release JAR!
|
||||||
|
- 🔗 **Fix Mods Manager Issue:** Mods now can be downloaded seamlessly from the launcher, use Profiles to install your preferred mod. It will also automatically copy from selected `Profile/<profilename>` to the `Mods` folder.
|
||||||
|
- 💾 **New User Data Location:** UserData Migration to Centralized Location. User data now preserves in `HytaleSaves` located beside `HytaleF2P` folder.
|
||||||
|
- 🎮 **SteamDeck and Ubuntu/Debian-based Library Fix:** Replace bundled `libzstd.so` with system version to fix `glibc 2.41+` crash.
|
||||||
|
- 🍎 **Launcher auto-update Improvement for macOS:** Fix auto-install fails on unsigned app. Added option to download the new launcher version on Github website.
|
||||||
|
- 🌎 **New Translations**: Added France 🇲🇫, German 🇩🇪, Indonesian 🇮🇩, Russia 🇷🇺, and Swedish 🇸🇪 translations to the launcher.
|
||||||
|
- 🔐 **Fixes Tar Vulnerability:** Updates `tar` from version `6.2.1` to `7.5.7` for vulnerability issue.
|
||||||
|
- ⚙️ **Improved Settings Pane UI:** Settings are now shown in two columns instead of one. No more doom scrolling just to change your language.
|
||||||
|
- ⭐ **Added Features Servers:** Don't know which one to play? Join our Featured Servers!
|
||||||
|
- 💬 **Removed Chat Pane and Add Discord Feature:** Useless chat feature, we got Discord. Join it, NOW. Also added Discord RPC features to Github and our Discord Server. SHOW OFF TO YOUR FRIENDS.
|
||||||
|
- 🔍 **Investigation on Avatar Not Saving Bug:** We are currently investigating this issue.
|
||||||
|
|
||||||
|
<details><summary>Click here to see older Changelogs</summary>
|
||||||
|
|
||||||
|
### 🔄 v2.1.1
|
||||||
- 🛠️ **Fix Bug EPERM**: EPERM or Error Permission in creating/removing process in reinstalling is now fixed.
|
- 🛠️ **Fix Bug EPERM**: EPERM or Error Permission in creating/removing process in reinstalling is now fixed.
|
||||||
- 🅰️ **Adds .pkg.tar.zst Build for Arch Users**: This Arch-package has been needed since the first release.
|
- 🅰️ **Adds .pkg.tar.zst Build for Arch Users**: This Arch-package has been needed since the first release.
|
||||||
- ❎ **Removes .pacman Build for Arch**: Based on the established conventions within the Arch Linux community, the file extension .pacman should not be used for package files.
|
- ❎ **Removes .pacman Build for Arch**: Based on the established conventions within the Arch Linux community, the file extension .pacman should not be used for package files.
|
||||||
- 🌎 **New Translation**: New Polish 🇵🇱 Translation added to the Launcher.
|
- 🌎 **New Translation**: New Polish 🇵🇱 Translation added to the Launcher.
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Click here to see older Changelogs</summary>
|
|
||||||
|
|
||||||
### 🔄 v2.1.0
|
### 🔄 v2.1.0
|
||||||
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
|
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
|
||||||
- ⚡ **Hardware Acceleration** —
|
- ⚡ **Hardware Acceleration** —
|
||||||
@@ -383,6 +394,7 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
|||||||
- 🎨 **Modern Interface** - Clean, intuitive design
|
- 🎨 **Modern Interface** - Clean, intuitive design
|
||||||
- 🌟 **First Release** - Core launcher functionality
|
- 🌟 **First Release** - Core launcher functionality
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 👥 Contributors
|
## 👥 Contributors
|
||||||
@@ -396,19 +408,30 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
### 🏆 Project Creator
|
### 🏆 Project Creator
|
||||||
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator | Windows*
|
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
|
||||||
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
|
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
|
||||||
|
|
||||||
### 🌟 Contributors
|
### 🌟 Main Contributors
|
||||||
- [**@sanasol**](https://github.com/sanasol) - *Main Issues fixer | Multiplayer Patcher*
|
- [**@sanasol**](https://github.com/sanasol) - *Main Issues fixer | Multiplayer Patcher*
|
||||||
- [**@Terromur**](https://github.com/Terromur) - *Main Issues fixer | Beta tester*
|
- [**@Terromur**](https://github.com/Terromur) - *Main Issues fixer | Beta tester*
|
||||||
- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester*
|
- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester | Github Release Maintainer*
|
||||||
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
|
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
|
||||||
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
|
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
|
||||||
- [**@crimera**](https://github.com/crimera) - *Issues fixer*
|
|
||||||
- [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer*
|
|
||||||
- [**@Rahul-Sahani04**](https://github.com/Rahul-Sahani04) - *Issues fixer*
|
- [**@Rahul-Sahani04**](https://github.com/Rahul-Sahani04) - *Issues fixer*
|
||||||
- [**@xSamiVS**](https://github.com/xSamiVS) - *Language Translator*
|
- [**@xSamiVS**](https://github.com/xSamiVS) - *Issues fixer | Language Translator*
|
||||||
|
|
||||||
|
#### 🎟️ Fresh Contributors
|
||||||
|
- [**@GreenKod**](https://github.com/GreenKod) - *Code refractor*
|
||||||
|
- [**@Citeli-py**](https://github.com/Citeli-py) - *Linux fix & packages version in early release*
|
||||||
|
- [**@crimera**](https://github.com/crimera) - *Generate new UUID for new username string feature*
|
||||||
|
- [**@letha11**](https://github.com/letha11) - *CSS filename typo fix*
|
||||||
|
- [**@colbster937**](https://github.com/colbster937) - *Icon upscaler*
|
||||||
|
- [**@ArnavSingh77**](https://github.com/ArnavSingh77) - *Close game launcher on start feature, improve app termination behavior*
|
||||||
|
- [**@TalesAmaral**](https://github.com/TalesAmaral) - *BUILD.md link fix in README.md*
|
||||||
|
|
||||||
|
#### 🌐 Language Translators
|
||||||
|
- [**@BlackSystemCoder**](https://github.com/BlackSystemCoder) - *Russian Language Translator*
|
||||||
|
- [**@walti0**](https://github.com/walti0) - *Polish Language Translator*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
351
SERVER.md
351
SERVER.md
@@ -1,34 +1,99 @@
|
|||||||
# Hytale F2P Server Guide
|
# 🎮 Hytale F2P Server Guide
|
||||||
|
|
||||||
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
|
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
|
||||||
|
|
||||||
DOWNLOAD SERVER FILES HERE: https://discord.gg/MEyWUxt77m
|
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/MEyWUxt77m**
|
||||||
|
|
||||||
|
**Table of Contents**
|
||||||
|
|
||||||
|
* ["Server" Term and Definition](#server-term-and-definiton)
|
||||||
|
* [Server Directory Location](#server-directory-location)
|
||||||
|
* [A. Online Play Feature](#a-online-play-feature)
|
||||||
|
* [1. Host Your Singleplayer World using In-Game Invite Code](#1-host-your-singleplayer-world-using-in-game-invite-code)
|
||||||
|
* [Common Issues (UPnP/NAT/STUN) on Online Play](#common-issues-upnpnatstun-on-online-play)
|
||||||
|
* [2. Host Your Singleplayer World using Tailscale](#2-host-your-singleplayer-world-using-tailscale)
|
||||||
|
* [B. Local Dedicated Server](#b-local-dedicated-server)
|
||||||
|
* [1. Using Playit.gg (Recommended) ✅](#1-using-playitgg-recommended-)
|
||||||
|
* [2. Using Radmin VPN](#2-using-radmin-vpn)
|
||||||
|
* [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced)
|
||||||
|
* [Step 1: Get the Files Ready](#step-1-get-the-files-ready)
|
||||||
|
* [Step 2: Place HytaleServer.jar in the Server directory](#step-2-place-hytaleserverjar-in-the-server-directory)
|
||||||
|
* [Step 3: Run the Server](#step-3-run-the-server)
|
||||||
|
* [D. Tinkering Guides](#d-tinkering-guides)
|
||||||
|
* [1. Network Setup](#1-network-setup)
|
||||||
|
* [2. Configuration](#2-configuration)
|
||||||
|
* [3. RAM Allocation Guide](#3-ram-allocation-guide)
|
||||||
|
* [4. Server Commands](#4-server-commands)
|
||||||
|
* [5. Command Line Options](#5-command-line-options)
|
||||||
|
* [6. File Structure](#6-file-structure)
|
||||||
|
* [7. Backups](#7-backups)
|
||||||
|
* [8. Troubleshooting](#8-troubleshooting)
|
||||||
|
* [9. Docker Deployment (Advanced)](#9-docker-deployment-advanced)
|
||||||
|
* [10. Getting Help](#10-getting-help)
|
||||||
|
---
|
||||||
|
|
||||||
|
### "Server" Term and Definiton
|
||||||
|
|
||||||
|
"HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (Thanks to Sanasol), handling approximately thousands of players worldwide to play this game for free.
|
||||||
|
|
||||||
|
Kindly support us via [our Buy Me a Coffee link](https://buymeacoffee.com/hf2p) if you think our launcher took a big part of developing this Hytale community for the love of the game itself.
|
||||||
|
**We will always advertise, always pushing, and always asking, to every users of this launcher to purchase the original game to help the official development of Hytale**.
|
||||||
|
|
||||||
|
### Server Directory Location
|
||||||
|
|
||||||
|
Here are the directory locations of Server folder if you have installed
|
||||||
|
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
|
||||||
|
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
|
||||||
|
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This location only exists if the user installed the game using our launcher. The `Server` folder needed to auth the HytaleClient to play Hytale online
|
||||||
|
> (for now; we planned to add offline mode in later version of our launcher).
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Hosting a dedicated Hytale server will not need the exact similar tree. You can put it anywhere, as long as the directory has `Assets.zip` which
|
||||||
|
> can be acquired from our launcher via our `HytaleServer.rar` server file (which contains patched `HytaleServer.jar`, `Assets.zip`, and `run_server` scripts in `.sh & .bat`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Part 1: Playing with Friends (Online Play)
|
# A. Host Your Singleplayer World
|
||||||
|
|
||||||
|
This feature is perfect for 1-5 players that want to just play instantly with friends.
|
||||||
|
Terms and conditions applies.
|
||||||
|
|
||||||
|
## 1. Using Online-Play Feature / In-Game Invite Code
|
||||||
|
|
||||||
The easiest way to play with friends - no manual server setup required!
|
The easiest way to play with friends - no manual server setup required!
|
||||||
|
*The game automatically handles networking using UPnP/STUN/NAT traversal.*
|
||||||
|
|
||||||
### How It Works
|
**For Online Play to work, you need:**
|
||||||
|
|
||||||
1. **Start the game** via F2P Launcher
|
|
||||||
2. **Click "Online Play"** in the main menu
|
|
||||||
3. **Share the invite code** with your friends
|
|
||||||
4. Friends enter your invite code to join
|
|
||||||
|
|
||||||
The game automatically handles networking using UPnP/STUN/NAT traversal.
|
|
||||||
|
|
||||||
### Network Requirements
|
|
||||||
|
|
||||||
For Online Play to work, you need:
|
|
||||||
|
|
||||||
- **UPnP enabled** on your router (most routers have this on by default)
|
- **UPnP enabled** on your router (most routers have this on by default)
|
||||||
- **Public IP address** from your ISP (not behind CGNAT)
|
- **Public IP address** from your ISP (not behind CGNAT)
|
||||||
|
|
||||||
### Common Issues
|
> [!TIP]
|
||||||
|
> Hoster need to make sure that the router can use UPnP: read router docs, wiki, or watch Youtube tutorials.
|
||||||
|
>
|
||||||
|
> If you encounter any problem, check Common Issues section below!
|
||||||
|
|
||||||
#### "NAT Type: Carrier-Grade NAT (CGNAT)" Warning
|
1. Press **Worlds** on the Main Menu.
|
||||||
|
2. Select which world you want to play with your friend.
|
||||||
|
3. Once you get in the world, press **ESC**/Pause the game.
|
||||||
|
4. Press **Online Play** in the Pause Menu.
|
||||||
|
5. Set option "Allow Other Players to Join" from OFF to **ON**. You can set Password if you want.
|
||||||
|
6. Press **Save**, the Invite Code will appear.
|
||||||
|
7. Press **Copy to Clipboard** and **Share the Invite Code** to your friends!
|
||||||
|
8. Friends: Press **Servers** in the Main Menu > Press **Join via Code** > Paste the Code > Join.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> If other players can't join the Hoster with error: `Failed to connect to any available address. The host may be offline or behind a strict firewall.`
|
||||||
|
>
|
||||||
|
> **AND ALSO** the Hoster "Online Play" menu shows `Connected to STUN - NAT Type: Restricted (No UPnP)`,
|
||||||
|
>
|
||||||
|
> this means the Online Play is **unavailable** on the Hoster machine, and it is neccessary to use services to host your world. **We recommend Playit.gg!**
|
||||||
|
|
||||||
|
|
||||||
|
### Common Issues (UPnP/NAT/STUN) on Online Play
|
||||||
|
<details><summary><b>a. "NAT Type: Carrier-Grade NAT (CGNAT)" Warning</b></summary>
|
||||||
|
|
||||||
If you see this message:
|
If you see this message:
|
||||||
```
|
```
|
||||||
@@ -40,14 +105,13 @@ Warning: Your network configuration may prevent other players from connecting.
|
|||||||
**What this means:** Your ISP doesn't give you a public IP address. Multiple customers share one public IP, which blocks incoming connections.
|
**What this means:** Your ISP doesn't give you a public IP address. Multiple customers share one public IP, which blocks incoming connections.
|
||||||
|
|
||||||
**Solutions:**
|
**Solutions:**
|
||||||
|
|
||||||
1. **Contact your ISP** - Request a public/static IP address (may cost extra)
|
1. **Contact your ISP** - Request a public/static IP address (may cost extra)
|
||||||
2. **Use a VPN with port forwarding** - Services like Mullvad, PIA, or AirVPN offer this
|
2. **Use a VPN with port forwarding** - Services like Mullvad, PIA, or AirVPN offer this
|
||||||
3. **Use Radmin VPN or Playit.gg** - Create a virtual LAN with friends (see below)
|
3. **Use Playit.gg / Tailscale / Radmin VPN** - Create a virtual LAN with friends (see below)
|
||||||
4. **Have a friend with public IP host instead**
|
4. **Have a friend with public IP host instead**
|
||||||
|
</details>
|
||||||
|
|
||||||
#### "UPnP Failed" or "Port Mapping Failed"
|
<details><summary><b>b. "UPnP Failed" or "Port Mapping Failed" Warning</b></summary>
|
||||||
|
|
||||||
**Check your router:**
|
**Check your router:**
|
||||||
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`)
|
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`)
|
||||||
2. Find UPnP settings (often under "Advanced" or "NAT")
|
2. Find UPnP settings (often under "Advanced" or "NAT")
|
||||||
@@ -56,117 +120,138 @@ Warning: Your network configuration may prevent other players from connecting.
|
|||||||
|
|
||||||
**If UPnP isn't available:**
|
**If UPnP isn't available:**
|
||||||
- Manually forward **port 5520 UDP** to your computer's local IP
|
- Manually forward **port 5520 UDP** to your computer's local IP
|
||||||
- See "Port Forwarding" section below
|
- See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below
|
||||||
|
</details>
|
||||||
#### "Strict NAT" or "Symmetric NAT"
|
|
||||||
|
|
||||||
|
<details><summary><b>c. "Strict NAT" or "Symmetric NAT" Warning</b></summary>
|
||||||
Some routers have restrictive NAT that blocks peer connections.
|
Some routers have restrictive NAT that blocks peer connections.
|
||||||
|
|
||||||
**Try:**
|
**Try:**
|
||||||
1. Enable "NAT Passthrough" or "NAT Filtering: Open" in router settings
|
1. Enable "NAT Passthrough" or "NAT Filtering: Open" in router settings
|
||||||
2. Put your device in router's DMZ (temporary test only)
|
2. Put your device in router's DMZ (temporary test only)
|
||||||
3. Use Radmin VPN as workaround
|
3. Use Playit.gg / Tailscale / Radmin VPN as workaround
|
||||||
|
</details>
|
||||||
|
|
||||||
### Workarounds for NAT/CGNAT Issues
|
## 2. Using Tailscale
|
||||||
|
Tailscale creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
|
||||||
|
|
||||||
#### Option 1: playit.gg (Recommended)
|
1. All members are required to download [Tailscale](https://tailscale.com/download) on your device.
|
||||||
|
[Once installed, Tailscale starts and live inside your hidden icon section in Windows, Mac and Linux]
|
||||||
|
2. Create a **common Tailscale** account which will shared among your friends to log in.
|
||||||
|
3. Ask your **host to login in to thier Tailscale client first**, then the other members.
|
||||||
|
* Host
|
||||||
|
* Open your singleplayer world
|
||||||
|
* Go to Online Play settings
|
||||||
|
* Re-save your settings to generate a new share code
|
||||||
|
* Friends
|
||||||
|
* Ensure Tailscale is connected
|
||||||
|
* Use the new share code to connect
|
||||||
|
* To test your connection, ping the host's ipv4 mentioned in Tailscale
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# B. Local Dedicated Server
|
||||||
|
|
||||||
|
This option is perfect for any players size. From small to high.
|
||||||
|
|
||||||
|
## 1. Using Playit.gg (Recommended) ✅
|
||||||
|
|
||||||
Free tunneling service - only the host needs to install it:
|
Free tunneling service - only the host needs to install it:
|
||||||
|
|
||||||
1. **Download [playit.gg](https://playit.gg/)** and run it - Connect your account from the terminal (do not close it when playing on the server)
|
1. Go to https://playit.gg/login and **Log In** with your existing account, **Create Account** if you don't have one
|
||||||
2. **Add a tunnel** - Select "UDP", tunnel description of "Hytale Server", port count `1`, and local port `5520`
|
2. Press "Add a tunnel" > Select `UDP` > Tunnel description of `Hytale Server` > Port count `1` > and Local Port `5520`
|
||||||
3. **Start the tunnel** - You'll get a public address like `xx-xx.gl.at.ply.gg:5520`
|
3. Press **Start the tunnel** (or you can just run the Playit.gg.EXE if you already installed it on your machine) - You'll get a public address like `xx-xx.gl.at.ply.gg:5520`
|
||||||
4. **Share the address** - Friends connect directly using this address
|
4. Go to https://playit.gg/download : `Installer` (Windows) or `x86-64` (Linux) or follow `Debian Install Script` (Debian-based only)
|
||||||
|
* Windows: Install the `playit-windows.msi`
|
||||||
|
* Linux:
|
||||||
|
* Right-click file > Properties > Turn on 'Executable as a Program' | or `chmod +x playit-linux-amd64` on terminal
|
||||||
|
* Run by double-clicking the file or `./playit-linux-amd64` via terminal
|
||||||
|
5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account.
|
||||||
|
6. **WARNING: Do not close the terminal if you are still playing or hosting the server**
|
||||||
|
7. Once it done, download the `run_server_with_tokens` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server`
|
||||||
|
8. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`)
|
||||||
|
9. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder. (TIP: You can use Symlink of that file to reduce disk usage!)
|
||||||
|
10. Double-click the .BAT file to host your server, wait until it shows:
|
||||||
|
```
|
||||||
|
===================================================
|
||||||
|
Hytale Server Booted! [Multiplayer, Fresh Universe]
|
||||||
|
===================================================
|
||||||
|
```
|
||||||
|
11. Connect to the server by go to `Servers` in your game client, press `Add Server`, type `localhost` in the address box, use any name for your server.
|
||||||
|
12. Send the public address in Step 3 to your friends.
|
||||||
|
|
||||||
Works with both Online Play and dedicated servers. No software needed for players joining.
|
## 2. Using Radmin VPN
|
||||||
|
|
||||||
#### Option 2: Radmin VPN
|
|
||||||
|
|
||||||
Creates a virtual LAN - all players need to install it:
|
Creates a virtual LAN - all players need to install it:
|
||||||
|
|
||||||
1. **Download [Radmin VPN](https://www.radmin-vpn.com/)** - All players install it
|
1. Download [Radmin VPN](https://www.radmin-vpn.com/) - All players install it
|
||||||
2. **Create a network** - One person creates, others join with network name/password
|
2. One person create a room/network, others join with network name/password
|
||||||
3. **Host via Online Play** - Use your Radmin VPN IP instead
|
3. Host joined the world, others will connect to it.
|
||||||
4. **Friends connect** - They'll see you on the virtual LAN
|
4. Open Hytale Game > Servers > Add Servers > Direct Connect > Type IP Address of the Host from Radmin.
|
||||||
|
|
||||||
Both options bypass all NAT/CGNAT issues. But for **Windows machines only!**
|
These options bypass all NAT/CGNAT issues. But for **Windows machines only!**
|
||||||
|
|
||||||
#### Option 3: Tailscale
|
|
||||||
It creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
|
|
||||||
|
|
||||||
1. All member's are required to download [Tailscale](https://tailscale.com/download) on your device.
|
|
||||||
[Once installed, Tailwind starts and live inside your hidden icon section in Windows, Mac and Linux]
|
|
||||||
2. Create a **common tailscale** account which will shared among your friends to log in.
|
|
||||||
3. Ask your **host to login in to thier tailscale client first**, then the other members.
|
|
||||||
##### Host
|
|
||||||
1. Open your singleplayer world
|
|
||||||
2. Go to Online Play settings
|
|
||||||
3. Re-save your settings to generate a new share code
|
|
||||||
##### Friends
|
|
||||||
1. Ensure Tailscale is connected
|
|
||||||
2. Use the new share code to connect
|
|
||||||
[To test your connection, ping the host's ipv4 mentioned in tailwind]
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Part 2: Dedicated Server (Advanced)
|
# C. 24/7 Dedicated Server (Advanced)
|
||||||
|
|
||||||
For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
|
For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
|
||||||
|
|
||||||
### Quick Start
|
## Step 1: Get the Files Ready
|
||||||
|
|
||||||
#### Step 1: Get the Server JAR
|
### Prequisites
|
||||||
|
|
||||||
The server scripts will automatically download the pre-patched server JAR if it's not present.
|
1. `HytaleServer.jar` (pre-patched for F2P players; dual-auth soon for Official + F2P play)
|
||||||
|
2. `Assets.zip`
|
||||||
|
3. `run_scripts_with_token.bat` for Windows or `run_scripts_with_token.sh` for macOS/Linux
|
||||||
|
|
||||||
**Option A:** Let the scripts download automatically (requires `HYTALE_SERVER_URL` to be configured)
|
> [!NOTE]
|
||||||
|
> The `HytaleServer.rar` available on our Discord Server (`#open-public-server` channel; typo on the Discord, not `zip`) includes all of the prequisites.
|
||||||
|
> Unfortunately, the JAR inside the RAR isn't updated so you'll need to download the JAR from the link on Discord.
|
||||||
|
|
||||||
**Option B:** Manually place `HytaleServer.jar` (pre-patched for F2P) in the Server directory:
|
> [!TIP]
|
||||||
|
> You can copy `Assets.zip` generated from the launcher to be used for the dedicated server. It's located in `HytaleF2P/release/package/game/latest`.
|
||||||
|
|
||||||
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
|
|
||||||
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
|
|
||||||
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
|
|
||||||
|
|
||||||
If you have a custom install path, the Server folder is inside your custom location under `HytaleF2P/release/package/game/latest/Server`.
|
## Step 2: Place `HytaleServer.jar` in the `Server` directory
|
||||||
|
|
||||||
#### Step 2: Run the Server
|
* Windows
|
||||||
|
* `%localappdata%\HytaleF2P\release\package\game\latest\Server`
|
||||||
|
* macOS
|
||||||
|
* `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
|
||||||
|
* Linux
|
||||||
|
* `~/.hytalef2p/release/package/game/latest/Server`
|
||||||
|
* If you have a custom install path, the Server folder is inside your custom location under
|
||||||
|
* `HytaleF2P/release/package/game/latest/Server`.
|
||||||
|
|
||||||
|
## Step 3: Run the Server
|
||||||
|
|
||||||
**Windows:**
|
**Windows:**
|
||||||
```batch
|
```batch
|
||||||
cd scripts
|
|
||||||
run_server.bat
|
run_server.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
**macOS / Linux:**
|
**macOS / Linux:**
|
||||||
```bash
|
```bash
|
||||||
cd scripts
|
|
||||||
./run_server.sh
|
./run_server.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The scripts will:
|
|
||||||
1. Find your game installation automatically
|
|
||||||
2. Download the pre-patched server JAR if needed
|
|
||||||
3. Fetch session tokens from the auth server
|
|
||||||
4. Start the server
|
|
||||||
|
|
||||||
#### Step 3: Connect Players
|
|
||||||
|
|
||||||
Share your server IP address with players. They connect via the F2P Launcher's server browser or direct connect.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Network Setup (Dedicated Server)
|
# D. Tinkering Guides
|
||||||
|
|
||||||
### Local Network (LAN)
|
## 1. Network Setup
|
||||||
|
|
||||||
|
### a. Local Network (LAN)
|
||||||
|
|
||||||
If all players are on the same network:
|
If all players are on the same network:
|
||||||
1. Find your local IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux)
|
1. Find your local IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux)
|
||||||
2. Share this IP with players on your network
|
2. Share this IP with players on your network
|
||||||
3. Default port is `5520`
|
3. Default port is `5520`
|
||||||
|
|
||||||
### Port Forwarding (Internet Play)
|
### b. Port Forwarding (Internet Play)
|
||||||
|
|
||||||
To allow direct internet connections:
|
To allow direct internet connections:
|
||||||
|
|
||||||
1. Forward **port 5520 (UDP)** in your router
|
1. Forward **port 5520 (UDP)** in your router
|
||||||
2. Find your public IP at [whatismyip.com](https://whatismyip.com)
|
2. Find your public IP at [whatismyip.com](https://whatismyip.com)
|
||||||
3. Share your public IP with players
|
3. Share your public IP with players
|
||||||
@@ -179,36 +264,35 @@ netsh advfirewall firewall add rule name="Hytale Server" dir=in action=allow pro
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Configuration
|
## 2. Configuration
|
||||||
|
|
||||||
### Environment Variables
|
### a. Environment Variables
|
||||||
|
|
||||||
Set these before running to customize your server:
|
Write this in the script file `.BAT`/`.SH` or set these manually in command before running to customize your server:
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
|
|
||||||
| `HYTALE_AUTH_DOMAIN` | `auth.sanasol.ws` | Auth server domain (4-16 chars) |
|
| `HYTALE_AUTH_DOMAIN` | `auth.sanasol.ws` | Auth server domain (4-16 chars) |
|
||||||
| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port |
|
| `BIND_ADRESS` | `0.0.0.0:5520` | Server IP and port |
|
||||||
| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) |
|
| `AUTH_MODE` | `authenticated` | Auth mode (see below) |
|
||||||
| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name |
|
| `SERVER_NAME` | `My Hytale Server` | Server display name |
|
||||||
| `HYTALE_GAME_PATH` | (auto-detected) | Override game location |
|
| `ASSETS_PATH` | `./Assets.zip` | Assets file location |
|
||||||
| `JVM_XMS` | `2G` | Minimum Java memory |
|
| `JVM_XMS` | `2G` | Minimum Java memory |
|
||||||
| `JVM_XMX` | `4G` | Maximum Java memory |
|
| `JVM_XMX` | `4G` | Maximum Java memory |
|
||||||
|
|
||||||
**Example (Windows):**
|
**Example (Windows):**
|
||||||
```batch
|
```batch
|
||||||
set HYTALE_SERVER_NAME=My Awesome Server
|
set SERVER_NAME=My Awesome Server
|
||||||
set JVM_XMX=8G
|
set JVM_XMX=8G
|
||||||
run_server.bat
|
run_server.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example (Linux/macOS):**
|
**Example (Linux/macOS):**
|
||||||
```bash
|
```bash
|
||||||
HYTALE_SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
|
SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Authentication Modes
|
### b. Authentication Modes
|
||||||
|
|
||||||
| Mode | Description | Use Case |
|
| Mode | Description | Use Case |
|
||||||
|------|-------------|----------|
|
|------|-------------|----------|
|
||||||
@@ -218,7 +302,7 @@ HYTALE_SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## RAM Allocation Guide
|
## 3. RAM Allocation Guide
|
||||||
|
|
||||||
Adjust memory based on your system:
|
Adjust memory based on your system:
|
||||||
|
|
||||||
@@ -242,7 +326,7 @@ JVM_XMS=4G JVM_XMX=12G ./run_server.sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Server Commands
|
## 4. Server Commands
|
||||||
|
|
||||||
Once running, use these commands in the console:
|
Once running, use these commands in the console:
|
||||||
|
|
||||||
@@ -259,9 +343,12 @@ Once running, use these commands in the console:
|
|||||||
| `unban <player>` | Unban a player |
|
| `unban <player>` | Unban a player |
|
||||||
| `tp <player> <x> <y> <z>` | Teleport player |
|
| `tp <player> <x> <y> <z>` | Teleport player |
|
||||||
|
|
||||||
|
|
||||||
|
Use `/` slash for these commands.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Command Line Options
|
## 5. Command Line Options
|
||||||
|
|
||||||
Pass these when starting the server:
|
Pass these when starting the server:
|
||||||
|
|
||||||
@@ -290,7 +377,7 @@ Pass these when starting the server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## File Structure
|
## 6. File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
<game_path>/
|
<game_path>/
|
||||||
@@ -308,21 +395,21 @@ Pass these when starting the server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Backups
|
## 7. Backups
|
||||||
|
|
||||||
### Automatic Backups
|
### a. Automatic Backups
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./run_server.sh --backup --backup-dir ./backups --backup-frequency 30
|
./run_server.sh --backup --backup-dir ./backups --backup-frequency 30
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Backup
|
### b. Manual Backup
|
||||||
|
|
||||||
1. Use `save` command or stop the server
|
1. Use `save` command or stop the server
|
||||||
2. Copy the `universe/` folder
|
2. Copy the `universe/` folder
|
||||||
3. Store in a safe location
|
3. Store in a safe location
|
||||||
|
|
||||||
### Restore
|
### c. Restore
|
||||||
|
|
||||||
1. Stop the server
|
1. Stop the server
|
||||||
2. Delete/rename current `universe/`
|
2. Delete/rename current `universe/`
|
||||||
@@ -331,9 +418,9 @@ Pass these when starting the server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## 8. Troubleshooting
|
||||||
|
|
||||||
### "Java not found" or "Java version too old"
|
### a. "Java not found" or "Java version too old"
|
||||||
|
|
||||||
**Java 21 is REQUIRED** (the server uses class file version 65.0).
|
**Java 21 is REQUIRED** (the server uses class file version 65.0).
|
||||||
|
|
||||||
@@ -352,30 +439,20 @@ export PATH="$JAVA_HOME/bin:$PATH"
|
|||||||
```
|
```
|
||||||
Add these lines to `~/.zshrc` or `~/.bash_profile` to make permanent.
|
Add these lines to `~/.zshrc` or `~/.bash_profile` to make permanent.
|
||||||
|
|
||||||
### "Game directory not found"
|
### b. "Port already in use"
|
||||||
|
|
||||||
- Download game via F2P Launcher first
|
|
||||||
- Or set `HYTALE_GAME_PATH` environment variable
|
|
||||||
- Check custom install path in launcher settings
|
|
||||||
|
|
||||||
### "Assets.zip not found"
|
|
||||||
|
|
||||||
Game files incomplete. Re-download via the launcher.
|
|
||||||
|
|
||||||
### "Port already in use"
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./run_server.sh --bind 0.0.0.0:5521
|
./run_server.sh --bind 0.0.0.0:5521
|
||||||
```
|
```
|
||||||
|
|
||||||
### "Out of memory"
|
### c. "Out of memory"
|
||||||
|
|
||||||
Increase JVM_XMX:
|
Increase JVM_XMX:
|
||||||
```bash
|
```bash
|
||||||
JVM_XMX=6G ./run_server.sh
|
JVM_XMX=6G ./run_server.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Players can't connect
|
### d. Players can't connect
|
||||||
|
|
||||||
1. Server shows "Server Ready"?
|
1. Server shows "Server Ready"?
|
||||||
2. Using F2P Launcher (not official)?
|
2. Using F2P Launcher (not official)?
|
||||||
@@ -383,7 +460,7 @@ JVM_XMX=6G ./run_server.sh
|
|||||||
4. Port forwarding configured (for internet)?
|
4. Port forwarding configured (for internet)?
|
||||||
5. Try `--auth-mode unauthenticated` for testing
|
5. Try `--auth-mode unauthenticated` for testing
|
||||||
|
|
||||||
### "Authentication failed"
|
### e. "Authentication failed"
|
||||||
|
|
||||||
- Ensure players use F2P Launcher
|
- Ensure players use F2P Launcher
|
||||||
- Auth server may be temporarily down
|
- Auth server may be temporarily down
|
||||||
@@ -391,7 +468,7 @@ JVM_XMX=6G ./run_server.sh
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Docker Deployment (Advanced)
|
## 9. Docker Deployment (Advanced)
|
||||||
|
|
||||||
For production servers, use Docker:
|
For production servers, use Docker:
|
||||||
|
|
||||||
@@ -410,40 +487,7 @@ See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Server Settings Summary
|
## 10. Getting Help
|
||||||
|
|
||||||
### Minimal Setup
|
|
||||||
```bash
|
|
||||||
./run_server.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Memory
|
|
||||||
```bash
|
|
||||||
JVM_XMS=2G JVM_XMX=8G ./run_server.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Port
|
|
||||||
```bash
|
|
||||||
HYTALE_BIND=0.0.0.0:25565 ./run_server.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### LAN Party (No Auth)
|
|
||||||
```bash
|
|
||||||
./run_server.sh --auth-mode unauthenticated
|
|
||||||
```
|
|
||||||
|
|
||||||
### Full Custom Setup
|
|
||||||
```bash
|
|
||||||
HYTALE_SERVER_NAME="Epic Server" \
|
|
||||||
HYTALE_BIND=0.0.0.0:5520 \
|
|
||||||
JVM_XMS=2G \
|
|
||||||
JVM_XMX=8G \
|
|
||||||
./run_server.sh --backup --backup-frequency 15 --allow-op
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting Help
|
|
||||||
|
|
||||||
- Check server console logs for errors
|
- Check server console logs for errors
|
||||||
- Test with `--auth-mode unauthenticated` first
|
- Test with `--auth-mode unauthenticated` first
|
||||||
@@ -452,8 +496,9 @@ JVM_XMX=8G \
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Credits
|
# Credits
|
||||||
|
|
||||||
- Hytale F2P Project
|
- Hytale F2P Project
|
||||||
- [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker)
|
- [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker)
|
||||||
- Auth Server: sanasol.ws
|
- Auth Server: sanasol.ws
|
||||||
|
|
||||||
|
|||||||
460
TROUBLESHOOTING.md
Normal file
460
TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
# Hytale F2P Launcher - Troubleshooting Guide
|
||||||
|
|
||||||
|
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/gME8rUy3MB).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Windows Issues](#windows-issues)
|
||||||
|
- [Linux Issues](#linux-issues)
|
||||||
|
- [macOS Issues](#macos-issues)
|
||||||
|
- [Connection & Server Issues](#connection--server-issues)
|
||||||
|
- [Authentication & Token Issues](#authentication--token-issues)
|
||||||
|
- [Avatar & Cosmetics Issues](#avatar--cosmetics-issues)
|
||||||
|
- [General Issues](#general-issues)
|
||||||
|
- [Known Limitations](#known-limitations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Windows Issues
|
||||||
|
|
||||||
|
### "Failed to connect to server" / Server won't boot
|
||||||
|
|
||||||
|
**Symptoms:** Singleplayer world fails to load, "Server failed to boot" error.
|
||||||
|
|
||||||
|
**Solution - Whitelist in Windows Firewall:**
|
||||||
|
1. Open **Windows Settings** > **Privacy & Security** > **Windows Security**
|
||||||
|
2. Click **Firewall & network protection** > **Allow an app through firewall**
|
||||||
|
3. Click **Change settings**
|
||||||
|
4. Find `HytaleClient.exe` and check both **Private** and **Public**
|
||||||
|
5. If not listed, click **Allow another app** and browse to:
|
||||||
|
```
|
||||||
|
%localappdata%\HytaleF2P\release\package\game\latest\Client\HytaleClient.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Duplicate mod error
|
||||||
|
|
||||||
|
**Symptoms:** `java.lang.IllegalArgumentException: Tried to load duplicate plugin`
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Navigate to your mods folder:
|
||||||
|
```
|
||||||
|
%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\Mods
|
||||||
|
```
|
||||||
|
2. Remove any duplicate `.jar` files
|
||||||
|
3. Restart the launcher
|
||||||
|
|
||||||
|
### SmartScreen blocks the launcher
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Click **More info**
|
||||||
|
2. Click **Run anyway**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Linux Issues
|
||||||
|
|
||||||
|
### GPU not detected / Using software rendering (llvmpipe)
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- Game uses integrated GPU instead of dedicated GPU
|
||||||
|
- Very low FPS / unplayable performance
|
||||||
|
- Play button not clickable
|
||||||
|
- Log shows `llvmpipe` instead of your GPU
|
||||||
|
|
||||||
|
**Solution for NVIDIA:**
|
||||||
|
```bash
|
||||||
|
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json ./HytaleF2P.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution for AMD (hybrid GPU systems):**
|
||||||
|
```bash
|
||||||
|
DRI_PRIME=1 ./HytaleF2P.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permanent fix - Create a launcher script:**
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json
|
||||||
|
export DRI_PRIME=1
|
||||||
|
./HytaleF2P.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** On some desktop systems with AMD iGPU + dGPU, the GPU selector may be inverted (selecting iGPU actually uses dGPU). Use whichever option works.
|
||||||
|
|
||||||
|
### SDL3_image / libpng errors
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- `DllNotFoundException: Unable to load shared library 'SDL3_image'`
|
||||||
|
- `libpng` related errors
|
||||||
|
- Game crashes on startup
|
||||||
|
|
||||||
|
**Solution - Install dependencies:**
|
||||||
|
|
||||||
|
**Fedora / RHEL:**
|
||||||
|
```bash
|
||||||
|
sudo dnf install libpng libpng-devel
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debian / Ubuntu:**
|
||||||
|
```bash
|
||||||
|
sudo apt install libpng16-16 libpng-dev libgdiplus libc6-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arch Linux:**
|
||||||
|
```bash
|
||||||
|
sudo pacman -S libpng
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative - Replace corrupted library:**
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client/
|
||||||
|
mv libSDL3_image.so libSDL3_image.so.bak
|
||||||
|
wget https://github.com/user-attachments/files/24710966/libSDL3_image.zip
|
||||||
|
unzip libSDL3_image.zip
|
||||||
|
chmod 644 libSDL3_image.so
|
||||||
|
rm libSDL3_image.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
### AppImage won't launch / FUSE error
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu
|
||||||
|
sudo apt install libfuse2
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install fuse
|
||||||
|
|
||||||
|
# Arch
|
||||||
|
sudo pacman -S fuse2
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing libxcrypt.so.1
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Fedora/RHEL
|
||||||
|
sudo dnf install libxcrypt-compat
|
||||||
|
|
||||||
|
# Arch
|
||||||
|
sudo pacman -S libxcrypt-compat
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wayland display issues
|
||||||
|
|
||||||
|
**Symptoms:** Game doesn't launch, stuck at loading, or display glitches on Wayland.
|
||||||
|
|
||||||
|
**Solution - Force X11:**
|
||||||
|
```bash
|
||||||
|
GDK_BACKEND=x11 ./HytaleF2P.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
**Alternative - Electron Wayland hint:**
|
||||||
|
```bash
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT=auto ./HytaleF2P.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steam Deck / Gamescope issues
|
||||||
|
|
||||||
|
**Solution 1 - Add custom launch options in Steam:**
|
||||||
|
```
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT=x11 %command%
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 2 - Launch from Desktop Mode** instead of Game Mode.
|
||||||
|
|
||||||
|
**Solution 3 - Force X11:**
|
||||||
|
```bash
|
||||||
|
GDK_BACKEND=x11 ./HytaleF2P.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ubuntu LTS-based distros (Linux Mint, Zorin OS, Pop!_OS)
|
||||||
|
|
||||||
|
These distributions may have compatibility issues due to older system packages. This is a limitation of the Hytale game client, not the launcher.
|
||||||
|
|
||||||
|
**Workarounds:**
|
||||||
|
1. Install all dependencies listed above
|
||||||
|
2. Try the SDL3_image replacement
|
||||||
|
3. Consider using a more recent distribution or Flatpak/AppImage with bundled dependencies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## macOS Issues
|
||||||
|
|
||||||
|
### "Butler system error -86" (Apple Silicon)
|
||||||
|
|
||||||
|
**Symptoms:** `Butler execution failed: spawn Unknown system error -86` (EXC_BAD_CPU_TYPE)
|
||||||
|
|
||||||
|
**Cause:** Butler (the update tool) is x86_64 only.
|
||||||
|
|
||||||
|
**Solution - Install Rosetta 2:**
|
||||||
|
```bash
|
||||||
|
softwareupdate --install-rosetta
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart the launcher.
|
||||||
|
|
||||||
|
### Auto-update fails with code signature error
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
```
|
||||||
|
Code signature at URL did not pass validation
|
||||||
|
domain: 'SQRLCodeSignatureErrorDomain'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution - Manual update:**
|
||||||
|
1. Download the latest version manually from [Releases](https://github.com/amiayweb/Hytale-F2P/releases/latest)
|
||||||
|
2. Backup your data first (see [Backup Locations](#backup-locations))
|
||||||
|
3. Install the fresh download
|
||||||
|
|
||||||
|
### "Unidentified developer" warning
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Open **System Settings** > **Privacy & Security**
|
||||||
|
2. Scroll to **Security** section
|
||||||
|
3. Find the message about "Hytale F2P Launcher"
|
||||||
|
4. Click **Open Anyway**
|
||||||
|
5. Authenticate and click **Open**
|
||||||
|
|
||||||
|
### App won't open (quarantine)
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
xattr -rd com.apple.quarantine /Applications/Hytale-F2P-Launcher.app
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connection & Server Issues
|
||||||
|
|
||||||
|
### "Failed to connect to server" in Singleplayer
|
||||||
|
|
||||||
|
**Possible causes:**
|
||||||
|
1. Windows Firewall blocking (see [Windows section](#failed-to-connect-to-server--server-wont-boot))
|
||||||
|
2. Patched server JAR download failed
|
||||||
|
3. Regional network restrictions
|
||||||
|
|
||||||
|
**Solution - Check patched JAR:**
|
||||||
|
1. Look for `HytaleServer.jar` in:
|
||||||
|
- Windows: `%localappdata%\HytaleF2P\release\package\game\latest\Server\`
|
||||||
|
- Linux: `~/.hytalef2p/release/package/game/latest/Server/`
|
||||||
|
- macOS: `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server/`
|
||||||
|
2. If missing or very small, the download may have failed
|
||||||
|
|
||||||
|
**Solution - Regional restrictions:**
|
||||||
|
|
||||||
|
Some countries (Russia, Turkey, Indonesia, etc.) may have issues accessing download servers.
|
||||||
|
- Try using a VPN for the initial download
|
||||||
|
- Once downloaded, the patched JAR is cached locally
|
||||||
|
|
||||||
|
### "Infinite Booting Server" / Server stuck loading
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check if the patched JAR downloaded successfully (see above)
|
||||||
|
2. Ensure your GPU meets minimum requirements
|
||||||
|
3. Check launcher logs for specific errors
|
||||||
|
4. Try with a VPN if in a restricted region
|
||||||
|
|
||||||
|
### "Connection timed out from inactivity"
|
||||||
|
|
||||||
|
**This is expected behavior.** Sessions have a 10-hour TTL and will timeout after extended inactivity. Simply reconnect to continue playing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication & Token Issues
|
||||||
|
|
||||||
|
### "Invalid identity token" / "Failed to start Hytale"
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. **Restart the launcher** - This fetches fresh tokens
|
||||||
|
2. **Check system time** - JWT validation requires accurate system time
|
||||||
|
3. **Clear cached tokens:**
|
||||||
|
- Delete `config.json` from your HytaleF2P folder
|
||||||
|
- Restart the launcher
|
||||||
|
- Re-enter your username
|
||||||
|
|
||||||
|
**Locations:**
|
||||||
|
- Windows: `%localappdata%\HytaleF2P\config.json`
|
||||||
|
- Linux: `~/.hytalef2p/config.json`
|
||||||
|
- macOS: `~/Library/Application Support/HytaleF2P/config.json`
|
||||||
|
|
||||||
|
### Token refresh errors
|
||||||
|
|
||||||
|
If you see issuer mismatch errors in logs:
|
||||||
|
1. Delete `config.json` and `player_id.json`
|
||||||
|
2. Restart the launcher
|
||||||
|
3. This forces a fresh authentication
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Avatar & Cosmetics Issues
|
||||||
|
|
||||||
|
### Avatar/skin changes not saving
|
||||||
|
|
||||||
|
**This is a known F2P limitation:**
|
||||||
|
- F2P mode has no password protection for usernames
|
||||||
|
- Anyone can use any username
|
||||||
|
- Cosmetics are stored server-side by username
|
||||||
|
- If someone else uses your username, they can change your cosmetics
|
||||||
|
|
||||||
|
**Workaround:** Use a unique username that others are unlikely to choose.
|
||||||
|
|
||||||
|
### Character invisible / Customization crashes
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Use **Repair Game** in launcher Settings
|
||||||
|
2. Verify `Assets.zip` exists in your game folder
|
||||||
|
3. Clear cached assets:
|
||||||
|
- Windows: Delete `%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\CachedAssets\`
|
||||||
|
4. Restart the launcher
|
||||||
|
|
||||||
|
### Avatar creator shows "Offline Mode"
|
||||||
|
|
||||||
|
**Cause:** Cannot connect to auth server.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check your internet connection
|
||||||
|
2. Test connectivity: Open `https://auth.sanasol.ws/health` in browser (should show "OK")
|
||||||
|
3. Check if firewall is blocking the connection
|
||||||
|
4. Try disabling VPN (or enabling one if in restricted region)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## General Issues
|
||||||
|
|
||||||
|
### Mods not showing up
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Ensure mods are placed in the correct folder:
|
||||||
|
- Windows: `%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\Mods\`
|
||||||
|
- Linux: `~/.hytalef2p/release/package/game/latest/Client/UserData/Mods/`
|
||||||
|
- macOS: `~/Library/Application Support/HytaleF2P/release/package/game/latest/Client/UserData/Mods/`
|
||||||
|
2. Verify mod files are `.jar` format
|
||||||
|
3. Check launcher logs for mod loading errors
|
||||||
|
|
||||||
|
### Game updates delete configurations/mods
|
||||||
|
|
||||||
|
**This is a known issue being worked on.**
|
||||||
|
|
||||||
|
**Prevention - Always backup before updating:**
|
||||||
|
- Server configs and worlds
|
||||||
|
- Mods folder
|
||||||
|
- `config.json` and `player_id.json`
|
||||||
|
|
||||||
|
See [Backup Locations](#backup-locations) below.
|
||||||
|
|
||||||
|
### Play button not clickable
|
||||||
|
|
||||||
|
Usually caused by GPU detection failure. See [GPU not detected](#gpu-not-detected--using-software-rendering-llvmpipe).
|
||||||
|
|
||||||
|
**Alternative:**
|
||||||
|
1. Go to **Settings** > **Graphics**
|
||||||
|
2. Manually select your GPU
|
||||||
|
3. Restart the launcher
|
||||||
|
|
||||||
|
### Read timeout errors
|
||||||
|
|
||||||
|
**Cause:** Network connectivity issues.
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Check your internet connection stability
|
||||||
|
2. Try using a VPN
|
||||||
|
3. Check firewall settings
|
||||||
|
4. Try at a different time (server load varies)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
### Linux ARM64 not supported
|
||||||
|
|
||||||
|
Hytale does not provide ARM64 game client builds. The launcher downloads from official Hytale servers which only provide:
|
||||||
|
- Windows x64
|
||||||
|
- macOS (Universal/Intel)
|
||||||
|
- Linux x64
|
||||||
|
|
||||||
|
This is outside our control.
|
||||||
|
|
||||||
|
### F2P Username System
|
||||||
|
|
||||||
|
- No password protection for usernames
|
||||||
|
- Anyone can claim any username
|
||||||
|
- Cosmetics shared by username
|
||||||
|
- UUIDs generated based on username
|
||||||
|
|
||||||
|
A per-player password system is planned for future versions.
|
||||||
|
|
||||||
|
### Session Timeout
|
||||||
|
|
||||||
|
Game sessions have a 10-hour TTL. This is by design for security.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup Locations
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
```
|
||||||
|
%localappdata%\HytaleF2P\
|
||||||
|
├── config.json # Launcher settings
|
||||||
|
├── player_id.json # Player identity
|
||||||
|
└── release\package\game\latest\
|
||||||
|
├── Client\UserData\ # Saves, settings, mods
|
||||||
|
└── Server\
|
||||||
|
├── universe\ # World data
|
||||||
|
└── config.json # Server config
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
```
|
||||||
|
~/.hytalef2p/
|
||||||
|
├── config.json
|
||||||
|
├── player_id.json
|
||||||
|
└── release/package/game/latest/
|
||||||
|
├── Client/UserData/
|
||||||
|
└── Server/
|
||||||
|
├── universe/
|
||||||
|
└── config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
```
|
||||||
|
~/Library/Application Support/HytaleF2P/
|
||||||
|
├── config.json
|
||||||
|
├── player_id.json
|
||||||
|
└── release/package/game/latest/
|
||||||
|
├── Client/UserData/
|
||||||
|
└── Server/
|
||||||
|
├── universe/
|
||||||
|
└── config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If your issue isn't resolved by this guide:
|
||||||
|
|
||||||
|
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
|
||||||
|
2. **Join Discord:** [discord.gg/gME8rUy3MB](https://discord.gg/gME8rUy3MB)
|
||||||
|
3. **Open a new issue** with:
|
||||||
|
- Your operating system and version
|
||||||
|
- Launcher version
|
||||||
|
- Full launcher logs from:
|
||||||
|
- Windows: `%localappdata%\HytaleF2P\logs\`
|
||||||
|
- Linux: `~/.hytalef2p/logs/`
|
||||||
|
- macOS: `~/Library/Application Support/HytaleF2P/logs/`
|
||||||
|
- Steps to reproduce the issue
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs Location
|
||||||
|
|
||||||
|
For bug reports, please include logs from:
|
||||||
|
|
||||||
|
| OS | Path |
|
||||||
|
|----|------|
|
||||||
|
| Windows | `%localappdata%\HytaleF2P\logs\` |
|
||||||
|
| Linux | `~/.hytalef2p/logs/` |
|
||||||
|
| macOS | `~/Library/Application Support/HytaleF2P/logs/` |
|
||||||
@@ -2,6 +2,9 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// UUID PERSISTENCE FIX - Atomic writes, backups, validation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
// Default auth domain - can be overridden by env var or config
|
// Default auth domain - can be overridden by env var or config
|
||||||
const DEFAULT_AUTH_DOMAIN = 'auth.sanasol.ws';
|
const DEFAULT_AUTH_DOMAIN = 'auth.sanasol.ws';
|
||||||
@@ -49,66 +52,443 @@ function getAppDir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_FILE = path.join(getAppDir(), 'config.json');
|
const CONFIG_FILE = path.join(getAppDir(), 'config.json');
|
||||||
|
const CONFIG_BACKUP = path.join(getAppDir(), 'config.json.bak');
|
||||||
|
const CONFIG_TEMP = path.join(getAppDir(), 'config.json.tmp');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIG VALIDATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate config structure - ensures critical data is intact
|
||||||
|
*/
|
||||||
|
function validateConfig(config) {
|
||||||
|
if (!config || typeof config !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If userUuids exists, it must be an object
|
||||||
|
if (config.userUuids !== undefined && typeof config.userUuids !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If username exists, it must be a non-empty string
|
||||||
|
if (config.username !== undefined && (typeof config.username !== 'string')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIG LOADING - With backup recovery
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load config with automatic backup recovery
|
||||||
|
* Never returns empty object silently if data existed before
|
||||||
|
*/
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
|
// Try primary config first
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
const data = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||||
|
if (data.trim()) {
|
||||||
|
const config = JSON.parse(data);
|
||||||
|
if (validateConfig(config)) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
console.warn('[Config] Primary config invalid structure, trying backup...');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Notice: could not load config:', err.message);
|
console.error('[Config] Failed to load primary config:', err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try backup config
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_BACKUP)) {
|
||||||
|
const data = fs.readFileSync(CONFIG_BACKUP, 'utf8');
|
||||||
|
if (data.trim()) {
|
||||||
|
const config = JSON.parse(data);
|
||||||
|
if (validateConfig(config)) {
|
||||||
|
console.log('[Config] Recovered from backup successfully');
|
||||||
|
// Restore primary from backup
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(CONFIG_FILE, data, 'utf8');
|
||||||
|
console.log('[Config] Primary config restored from backup');
|
||||||
|
} catch (restoreErr) {
|
||||||
|
console.error('[Config] Failed to restore primary from backup:', restoreErr.message);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Config] Failed to load backup config:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No valid config - return empty (fresh install)
|
||||||
|
console.log('[Config] No valid config found - fresh install');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIG SAVING - Atomic writes with backup
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save config atomically with backup
|
||||||
|
* Uses temp file + rename pattern to prevent corruption
|
||||||
|
* Creates backup before overwriting
|
||||||
|
*/
|
||||||
function saveConfig(update) {
|
function saveConfig(update) {
|
||||||
|
const maxRetries = 3;
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
const configDir = path.dirname(CONFIG_FILE);
|
const configDir = path.dirname(CONFIG_FILE);
|
||||||
if (!fs.existsSync(configDir)) {
|
if (!fs.existsSync(configDir)) {
|
||||||
fs.mkdirSync(configDir, { recursive: true });
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
}
|
}
|
||||||
const config = loadConfig();
|
|
||||||
const next = { ...config, ...update };
|
// Load current config
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
const currentConfig = loadConfig();
|
||||||
|
const newConfig = { ...currentConfig, ...update };
|
||||||
|
const data = JSON.stringify(newConfig, null, 2);
|
||||||
|
|
||||||
|
// 1. Write to temp file first
|
||||||
|
fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
|
||||||
|
|
||||||
|
// 2. Verify temp file is valid JSON
|
||||||
|
const verification = JSON.parse(fs.readFileSync(CONFIG_TEMP, 'utf8'));
|
||||||
|
if (!validateConfig(verification)) {
|
||||||
|
throw new Error('Config validation failed after write');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Backup current config (if exists and valid)
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
try {
|
||||||
|
const currentData = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||||
|
if (currentData.trim()) {
|
||||||
|
fs.writeFileSync(CONFIG_BACKUP, currentData, 'utf8');
|
||||||
|
}
|
||||||
|
} catch (backupErr) {
|
||||||
|
console.warn('[Config] Could not create backup:', backupErr.message);
|
||||||
|
// Continue anyway - saving new config is more important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Atomic rename (this is the critical operation)
|
||||||
|
fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
|
||||||
|
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Notice: could not save config:', err.message);
|
lastError = err;
|
||||||
|
console.error(`[Config] Save attempt ${attempt}/${maxRetries} failed:`, err.message);
|
||||||
|
|
||||||
|
// Clean up temp file on failure
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_TEMP)) {
|
||||||
|
fs.unlinkSync(CONFIG_TEMP);
|
||||||
|
}
|
||||||
|
} catch (cleanupErr) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
// Small delay before retry
|
||||||
|
const delay = attempt * 100;
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < delay) {
|
||||||
|
// Busy wait (sync delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All retries failed - this is critical
|
||||||
|
console.error('[Config] CRITICAL: Failed to save config after all retries:', lastError.message);
|
||||||
|
throw new Error(`Failed to save config: ${lastError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// USERNAME MANAGEMENT - No silent fallbacks
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save username to config
|
||||||
|
* When changing username, the UUID is preserved (rename, not new identity)
|
||||||
|
* Validates username before saving
|
||||||
|
*/
|
||||||
function saveUsername(username) {
|
function saveUsername(username) {
|
||||||
saveConfig({ username: username || 'Player' });
|
if (!username || typeof username !== 'string') {
|
||||||
|
throw new Error('Invalid username: must be a non-empty string');
|
||||||
|
}
|
||||||
|
const newName = username.trim();
|
||||||
|
if (!newName) {
|
||||||
|
throw new Error('Invalid username: cannot be empty or whitespace');
|
||||||
|
}
|
||||||
|
if (newName.length > 16) {
|
||||||
|
throw new Error('Invalid username: must be 16 characters or less');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const currentName = config.username ? config.username.trim() : null;
|
||||||
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
|
// Check if we're actually changing the username (case-insensitive comparison)
|
||||||
|
const isRename = currentName && currentName.toLowerCase() !== newName.toLowerCase();
|
||||||
|
|
||||||
|
if (isRename) {
|
||||||
|
// Find the UUID for the current username
|
||||||
|
const currentKey = Object.keys(userUuids).find(
|
||||||
|
k => k.toLowerCase() === currentName.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentKey && userUuids[currentKey]) {
|
||||||
|
// Check if target username already exists (would be a different identity)
|
||||||
|
const targetKey = Object.keys(userUuids).find(
|
||||||
|
k => k.toLowerCase() === newName.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetKey) {
|
||||||
|
// Target username already exists - this is switching identity, not renaming
|
||||||
|
console.log(`[Config] Switching to existing identity: "${newName}" (UUID already exists)`);
|
||||||
|
} else {
|
||||||
|
// Rename: move UUID from old name to new name
|
||||||
|
const uuid = userUuids[currentKey];
|
||||||
|
delete userUuids[currentKey];
|
||||||
|
userUuids[newName] = uuid;
|
||||||
|
console.log(`[Config] Renamed identity: "${currentKey}" → "${newName}" (UUID preserved: ${uuid})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (currentName && currentName !== newName) {
|
||||||
|
// Case change only - update the key to preserve the new casing
|
||||||
|
const currentKey = Object.keys(userUuids).find(
|
||||||
|
k => k.toLowerCase() === currentName.toLowerCase()
|
||||||
|
);
|
||||||
|
if (currentKey && currentKey !== newName) {
|
||||||
|
const uuid = userUuids[currentKey];
|
||||||
|
delete userUuids[currentKey];
|
||||||
|
userUuids[newName] = uuid;
|
||||||
|
console.log(`[Config] Updated username case: "${currentKey}" → "${newName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save both username and updated userUuids
|
||||||
|
saveConfig({ username: newName, userUuids });
|
||||||
|
console.log(`[Config] Username saved: "${newName}"`);
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load username from config
|
||||||
|
* Returns null if no username set (caller must handle)
|
||||||
|
*/
|
||||||
function loadUsername() {
|
function loadUsername() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
return config.username || 'Player';
|
const username = config.username;
|
||||||
|
if (username && typeof username === 'string' && username.trim()) {
|
||||||
|
return username.trim();
|
||||||
|
}
|
||||||
|
return null; // No username set - caller must handle this
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveChatUsername(chatUsername) {
|
/**
|
||||||
saveConfig({ chatUsername: chatUsername || '' });
|
* Load username with fallback to 'Player'
|
||||||
|
* Use this only for display purposes, NOT for UUID lookup
|
||||||
|
*/
|
||||||
|
function loadUsernameWithDefault() {
|
||||||
|
return loadUsername() || 'Player';
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadChatUsername() {
|
/**
|
||||||
const config = loadConfig();
|
* Check if username is configured
|
||||||
return config.chatUsername || '';
|
*/
|
||||||
|
function hasUsername() {
|
||||||
|
return loadUsername() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// UUID MANAGEMENT - Persistent and safe
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize username for UUID lookup (case-insensitive, trimmed)
|
||||||
|
*/
|
||||||
|
function normalizeUsername(username) {
|
||||||
|
if (!username || typeof username !== 'string') return null;
|
||||||
|
return username.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get UUID for a username
|
||||||
|
* Creates new UUID only if user explicitly doesn't exist
|
||||||
|
* Uses case-insensitive lookup to prevent duplicates, but preserves original case for display
|
||||||
|
*/
|
||||||
function getUuidForUser(username) {
|
function getUuidForUser(username) {
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
if (!username || typeof username !== 'string' || !username.trim()) {
|
||||||
|
throw new Error('Cannot get UUID: username is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = username.trim();
|
||||||
|
const normalizedLookup = displayName.toLowerCase();
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const userUuids = config.userUuids || {};
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
if (userUuids[username]) {
|
// Case-insensitive lookup - find existing key regardless of case
|
||||||
return userUuids[username];
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||||
|
|
||||||
|
if (existingKey) {
|
||||||
|
// Found existing - return UUID, update display name if case changed
|
||||||
|
const existingUuid = userUuids[existingKey];
|
||||||
|
|
||||||
|
// If user typed different case, update the key to new case (preserving UUID)
|
||||||
|
if (existingKey !== displayName) {
|
||||||
|
console.log(`[Config] Updating username case: "${existingKey}" → "${displayName}"`);
|
||||||
|
delete userUuids[existingKey];
|
||||||
|
userUuids[displayName] = existingUuid;
|
||||||
|
saveConfig({ userUuids });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return existingUuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new UUID for new user - store with original case
|
||||||
const newUuid = uuidv4();
|
const newUuid = uuidv4();
|
||||||
userUuids[username] = newUuid;
|
userUuids[displayName] = newUuid;
|
||||||
saveConfig({ userUuids });
|
saveConfig({ userUuids });
|
||||||
|
console.log(`[Config] Created new UUID for "${displayName}": ${newUuid}`);
|
||||||
|
|
||||||
return newUuid;
|
return newUuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user's UUID (based on saved username)
|
||||||
|
*/
|
||||||
|
function getCurrentUuid() {
|
||||||
|
const username = loadUsername();
|
||||||
|
if (!username) {
|
||||||
|
throw new Error('Cannot get current UUID: no username configured');
|
||||||
|
}
|
||||||
|
return getUuidForUser(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all UUID mappings (raw object)
|
||||||
|
*/
|
||||||
|
function getAllUuidMappings() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.userUuids || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all UUID mappings as array with current user flag
|
||||||
|
*/
|
||||||
|
function getAllUuidMappingsArray() {
|
||||||
|
const config = loadConfig();
|
||||||
|
const userUuids = config.userUuids || {};
|
||||||
|
const currentUsername = loadUsername();
|
||||||
|
// Case-insensitive comparison for isCurrent
|
||||||
|
const normalizedCurrent = currentUsername ? currentUsername.toLowerCase() : null;
|
||||||
|
|
||||||
|
return Object.entries(userUuids).map(([username, uuid]) => ({
|
||||||
|
username, // Original case preserved
|
||||||
|
uuid,
|
||||||
|
isCurrent: username.toLowerCase() === normalizedCurrent
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set UUID for a specific user
|
||||||
|
* Validates UUID format before saving
|
||||||
|
* Preserves original case of username
|
||||||
|
*/
|
||||||
|
function setUuidForUser(username, uuid) {
|
||||||
|
const { validate: validateUuid } = require('uuid');
|
||||||
|
|
||||||
|
if (!username || typeof username !== 'string' || !username.trim()) {
|
||||||
|
throw new Error('Invalid username');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateUuid(uuid)) {
|
||||||
|
throw new Error('Invalid UUID format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = username.trim();
|
||||||
|
const normalizedLookup = displayName.toLowerCase();
|
||||||
|
const config = loadConfig();
|
||||||
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
|
// Remove any existing entry with same name (case-insensitive)
|
||||||
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||||
|
if (existingKey) {
|
||||||
|
delete userUuids[existingKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store with original case
|
||||||
|
userUuids[displayName] = uuid;
|
||||||
|
saveConfig({ userUuids });
|
||||||
|
|
||||||
|
console.log(`[Config] UUID set for "${displayName}": ${uuid}`);
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new UUID (without saving)
|
||||||
|
*/
|
||||||
|
function generateNewUuid() {
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
return uuidv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete UUID for a specific user
|
||||||
|
* Uses case-insensitive lookup
|
||||||
|
*/
|
||||||
|
function deleteUuidForUser(username) {
|
||||||
|
if (!username || typeof username !== 'string') {
|
||||||
|
throw new Error('Invalid username');
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedLookup = username.trim().toLowerCase();
|
||||||
|
const config = loadConfig();
|
||||||
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
|
// Case-insensitive lookup
|
||||||
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||||
|
|
||||||
|
if (existingKey) {
|
||||||
|
delete userUuids[existingKey];
|
||||||
|
saveConfig({ userUuids });
|
||||||
|
console.log(`[Config] UUID deleted for "${username}"`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset current user's UUID (generates new one)
|
||||||
|
*/
|
||||||
|
function resetCurrentUserUuid() {
|
||||||
|
const username = loadUsername();
|
||||||
|
if (!username) {
|
||||||
|
throw new Error('Cannot reset UUID: no username configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const newUuid = uuidv4();
|
||||||
|
|
||||||
|
return setUuidForUser(username, newUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// JAVA PATH MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveJavaPath(javaPath) {
|
function saveJavaPath(javaPath) {
|
||||||
const trimmed = (javaPath || '').trim();
|
const trimmed = (javaPath || '').trim();
|
||||||
saveConfig({ javaPath: trimmed });
|
saveConfig({ javaPath: trimmed });
|
||||||
@@ -129,6 +509,10 @@ function loadJavaPath() {
|
|||||||
return config.javaPath || '';
|
return config.javaPath || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// INSTALL PATH MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveInstallPath(installPath) {
|
function saveInstallPath(installPath) {
|
||||||
const trimmed = (installPath || '').trim();
|
const trimmed = (installPath || '').trim();
|
||||||
saveConfig({ installPath: trimmed });
|
saveConfig({ installPath: trimmed });
|
||||||
@@ -139,6 +523,10 @@ function loadInstallPath() {
|
|||||||
return config.installPath || '';
|
return config.installPath || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DISCORD RPC SETTINGS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveDiscordRPC(enabled) {
|
function saveDiscordRPC(enabled) {
|
||||||
saveConfig({ discordRPC: !!enabled });
|
saveConfig({ discordRPC: !!enabled });
|
||||||
}
|
}
|
||||||
@@ -148,6 +536,10 @@ function loadDiscordRPC() {
|
|||||||
return config.discordRPC !== undefined ? config.discordRPC : true;
|
return config.discordRPC !== undefined ? config.discordRPC : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LANGUAGE SETTINGS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveLanguage(language) {
|
function saveLanguage(language) {
|
||||||
saveConfig({ language: language || 'en' });
|
saveConfig({ language: language || 'en' });
|
||||||
}
|
}
|
||||||
@@ -157,6 +549,10 @@ function loadLanguage() {
|
|||||||
return config.language || 'en';
|
return config.language || 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LAUNCHER SETTINGS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveCloseLauncherOnStart(enabled) {
|
function saveCloseLauncherOnStart(enabled) {
|
||||||
saveConfig({ closeLauncherOnStart: !!enabled });
|
saveConfig({ closeLauncherOnStart: !!enabled });
|
||||||
}
|
}
|
||||||
@@ -175,31 +571,38 @@ function loadLauncherHardwareAcceleration() {
|
|||||||
return config.launcherHardwareAcceleration !== undefined ? config.launcherHardwareAcceleration : true;
|
return config.launcherHardwareAcceleration !== undefined ? config.launcherHardwareAcceleration : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MODS MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveModsToConfig(mods) {
|
function saveModsToConfig(mods) {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
// Config migration handles structure, but mod saves must go to the ACTIVE profile.
|
|
||||||
// Global installedMods is kept mainly for reference/migration.
|
|
||||||
// The profile is the source of truth for enabled mods.
|
|
||||||
|
|
||||||
|
|
||||||
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
||||||
config.profiles[config.activeProfileId].mods = mods;
|
config.profiles[config.activeProfileId].mods = mods;
|
||||||
} else {
|
} else {
|
||||||
// Fallback for legacy or no-profile state
|
|
||||||
config.installedMods = mods;
|
config.installedMods = mods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use atomic save
|
||||||
const configDir = path.dirname(CONFIG_FILE);
|
const configDir = path.dirname(CONFIG_FILE);
|
||||||
if (!fs.existsSync(configDir)) {
|
if (!fs.existsSync(configDir)) {
|
||||||
fs.mkdirSync(configDir, { recursive: true });
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
// Write atomically
|
||||||
console.log('Mods saved to config.json');
|
const data = JSON.stringify(config, null, 2);
|
||||||
|
fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
fs.copyFileSync(CONFIG_FILE, CONFIG_BACKUP);
|
||||||
|
}
|
||||||
|
fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
|
||||||
|
|
||||||
|
console.log('[Config] Mods saved successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving mods to config:', error);
|
console.error('[Config] Error saving mods:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,25 +610,34 @@ function loadModsFromConfig() {
|
|||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
// Prefer Active Profile
|
|
||||||
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
||||||
return config.profiles[config.activeProfileId].mods || [];
|
return config.profiles[config.activeProfileId].mods || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.installedMods || [];
|
return config.installedMods || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading mods from config:', error);
|
console.error('[Config] Error loading mods:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// FIRST LAUNCH DETECTION - FIXED
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is the first launch
|
||||||
|
* FIXED: Was always returning true due to bug
|
||||||
|
*/
|
||||||
function isFirstLaunch() {
|
function isFirstLaunch() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
|
// If explicitly marked, use that
|
||||||
if ('hasLaunchedBefore' in config) {
|
if ('hasLaunchedBefore' in config) {
|
||||||
return !config.hasLaunchedBefore;
|
return !config.hasLaunchedBefore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for any existing user data
|
||||||
const hasUserData = config.installPath || config.username || config.javaPath ||
|
const hasUserData = config.installPath || config.username || config.javaPath ||
|
||||||
config.chatUsername || config.userUuids ||
|
config.chatUsername || config.userUuids ||
|
||||||
Object.keys(config).length > 0;
|
Object.keys(config).length > 0;
|
||||||
@@ -234,76 +646,17 @@ function isFirstLaunch() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// FIXED: Was returning true here, should be false
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markAsLaunched() {
|
function markAsLaunched() {
|
||||||
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
|
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// UUID Management Functions
|
// =============================================================================
|
||||||
function getCurrentUuid() {
|
// GPU PREFERENCE
|
||||||
const username = loadUsername();
|
// =============================================================================
|
||||||
return getUuidForUser(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllUuidMappings() {
|
|
||||||
const config = loadConfig();
|
|
||||||
return config.userUuids || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUuidForUser(username, uuid) {
|
|
||||||
const { v4: uuidv4, validate: validateUuid } = require('uuid');
|
|
||||||
|
|
||||||
// Validate UUID format
|
|
||||||
if (!validateUuid(uuid)) {
|
|
||||||
throw new Error('Invalid UUID format');
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
const userUuids = config.userUuids || {};
|
|
||||||
userUuids[username] = uuid;
|
|
||||||
saveConfig({ userUuids });
|
|
||||||
|
|
||||||
return uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateNewUuid() {
|
|
||||||
const { v4: uuidv4 } = require('uuid');
|
|
||||||
return uuidv4();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteUuidForUser(username) {
|
|
||||||
const config = loadConfig();
|
|
||||||
const userUuids = config.userUuids || {};
|
|
||||||
|
|
||||||
if (userUuids[username]) {
|
|
||||||
delete userUuids[username];
|
|
||||||
saveConfig({ userUuids });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetCurrentUserUuid() {
|
|
||||||
const username = loadUsername();
|
|
||||||
const { v4: uuidv4 } = require('uuid');
|
|
||||||
const newUuid = uuidv4();
|
|
||||||
|
|
||||||
return setUuidForUser(username, newUuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveChatColor(color) {
|
|
||||||
const config = loadConfig();
|
|
||||||
config.chatColor = color;
|
|
||||||
saveConfig(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadChatColor() {
|
|
||||||
const config = loadConfig();
|
|
||||||
return config.chatColor || '#3498db';
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveGpuPreference(gpuPreference) {
|
function saveGpuPreference(gpuPreference) {
|
||||||
saveConfig({ gpuPreference: gpuPreference || 'auto' });
|
saveConfig({ gpuPreference: gpuPreference || 'auto' });
|
||||||
@@ -314,6 +667,10 @@ function loadGpuPreference() {
|
|||||||
return config.gpuPreference || 'auto';
|
return config.gpuPreference || 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// VERSION MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveVersionClient(versionClient) {
|
function saveVersionClient(versionClient) {
|
||||||
saveConfig({ version_client: versionClient });
|
saveConfig({ version_client: versionClient });
|
||||||
}
|
}
|
||||||
@@ -326,7 +683,7 @@ function loadVersionClient() {
|
|||||||
function saveVersionBranch(versionBranch) {
|
function saveVersionBranch(versionBranch) {
|
||||||
const branch = versionBranch || 'release';
|
const branch = versionBranch || 'release';
|
||||||
if (branch !== 'release' && branch !== 'pre-release') {
|
if (branch !== 'release' && branch !== 'pre-release') {
|
||||||
console.warn(`Invalid branch "${branch}", defaulting to "release"`);
|
console.warn(`[Config] Invalid branch "${branch}", defaulting to "release"`);
|
||||||
saveConfig({ version_branch: 'release' });
|
saveConfig({ version_branch: 'release' });
|
||||||
} else {
|
} else {
|
||||||
saveConfig({ version_branch: branch });
|
saveConfig({ version_branch: branch });
|
||||||
@@ -338,54 +695,98 @@ function loadVersionBranch() {
|
|||||||
return config.version_branch || 'release';
|
return config.version_branch || 'release';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// READY STATE - For UI to check before allowing launch
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if launcher is ready to launch game
|
||||||
|
* Returns object with ready state and any issues
|
||||||
|
*/
|
||||||
|
function checkLaunchReady() {
|
||||||
|
const issues = [];
|
||||||
|
|
||||||
|
const username = loadUsername();
|
||||||
|
if (!username) {
|
||||||
|
issues.push('No username configured');
|
||||||
|
} else if (username === 'Player') {
|
||||||
|
issues.push('Using default username "Player"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: issues.length === 0,
|
||||||
|
hasUsername: !!username,
|
||||||
|
username: username,
|
||||||
|
issues: issues
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// EXPORTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
// Core config
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
|
validateConfig,
|
||||||
|
|
||||||
|
// Username (no silent fallbacks)
|
||||||
saveUsername,
|
saveUsername,
|
||||||
loadUsername,
|
loadUsername,
|
||||||
saveChatUsername,
|
loadUsernameWithDefault,
|
||||||
loadChatUsername,
|
hasUsername,
|
||||||
saveChatColor,
|
|
||||||
loadChatColor,
|
// UUID management
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
saveJavaPath,
|
|
||||||
loadJavaPath,
|
|
||||||
saveInstallPath,
|
|
||||||
loadInstallPath,
|
|
||||||
saveDiscordRPC,
|
|
||||||
loadDiscordRPC,
|
|
||||||
saveLanguage,
|
|
||||||
loadLanguage,
|
|
||||||
saveModsToConfig,
|
|
||||||
loadModsFromConfig,
|
|
||||||
isFirstLaunch,
|
|
||||||
markAsLaunched,
|
|
||||||
CONFIG_FILE,
|
|
||||||
// Auth server exports
|
|
||||||
getAuthServerUrl,
|
|
||||||
getAuthDomain,
|
|
||||||
saveAuthDomain,
|
|
||||||
// UUID Management exports
|
|
||||||
getCurrentUuid,
|
getCurrentUuid,
|
||||||
getAllUuidMappings,
|
getAllUuidMappings,
|
||||||
|
getAllUuidMappingsArray,
|
||||||
setUuidForUser,
|
setUuidForUser,
|
||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
resetCurrentUserUuid,
|
resetCurrentUserUuid,
|
||||||
// GPU Preference exports
|
|
||||||
saveGpuPreference,
|
// Java/Install paths
|
||||||
loadGpuPreference,
|
saveJavaPath,
|
||||||
// Close Launcher export
|
loadJavaPath,
|
||||||
|
saveInstallPath,
|
||||||
|
loadInstallPath,
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
saveDiscordRPC,
|
||||||
|
loadDiscordRPC,
|
||||||
|
saveLanguage,
|
||||||
|
loadLanguage,
|
||||||
saveCloseLauncherOnStart,
|
saveCloseLauncherOnStart,
|
||||||
loadCloseLauncherOnStart,
|
loadCloseLauncherOnStart,
|
||||||
|
|
||||||
// Hardware Acceleration functions
|
|
||||||
saveLauncherHardwareAcceleration,
|
saveLauncherHardwareAcceleration,
|
||||||
loadLauncherHardwareAcceleration,
|
loadLauncherHardwareAcceleration,
|
||||||
|
|
||||||
// Version Management exports
|
// Mods
|
||||||
|
saveModsToConfig,
|
||||||
|
loadModsFromConfig,
|
||||||
|
|
||||||
|
// Launch state
|
||||||
|
isFirstLaunch,
|
||||||
|
markAsLaunched,
|
||||||
|
checkLaunchReady,
|
||||||
|
|
||||||
|
// Auth server
|
||||||
|
getAuthServerUrl,
|
||||||
|
getAuthDomain,
|
||||||
|
saveAuthDomain,
|
||||||
|
|
||||||
|
// GPU
|
||||||
|
saveGpuPreference,
|
||||||
|
loadGpuPreference,
|
||||||
|
|
||||||
|
// Version
|
||||||
saveVersionClient,
|
saveVersionClient,
|
||||||
loadVersionClient,
|
loadVersionClient,
|
||||||
saveVersionBranch,
|
saveVersionBranch,
|
||||||
loadVersionBranch
|
loadVersionBranch,
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
CONFIG_FILE
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function getAppDir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get centralized UserData saves directory (NEW in 2.1.2)
|
* Get centralized UserData saves directory (NEW in 2.2.0)
|
||||||
* UserData is now stored separately from game installation
|
* UserData is now stored separately from game installation
|
||||||
*/
|
*/
|
||||||
function getHytaleSavesDir() {
|
function getHytaleSavesDir() {
|
||||||
@@ -233,7 +233,7 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
|
|
||||||
function getProfilesDir(customInstallPath = null) {
|
function getProfilesDir(customInstallPath = null) {
|
||||||
try {
|
try {
|
||||||
// NEW 2.1.2: Use centralized UserData location
|
// NEW 2.2.0: Use centralized UserData location
|
||||||
const userDataPath = getHytaleSavesDir();
|
const userDataPath = getHytaleSavesDir();
|
||||||
const profilesDir = path.join(userDataPath, 'Profiles');
|
const profilesDir = path.join(userDataPath, 'Profiles');
|
||||||
|
|
||||||
|
|||||||
7
backend/core/testConfig.js
Normal file
7
backend/core/testConfig.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const FORCE_CLEAN_INSTALL_VERSION = false;
|
||||||
|
const CLEAN_INSTALL_TEST_VERSION = '4.pwr';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
FORCE_CLEAN_INSTALL_VERSION,
|
||||||
|
CLEAN_INSTALL_TEST_VERSION
|
||||||
|
};
|
||||||
@@ -5,10 +5,8 @@
|
|||||||
const {
|
const {
|
||||||
saveUsername,
|
saveUsername,
|
||||||
loadUsername,
|
loadUsername,
|
||||||
saveChatUsername,
|
loadUsernameWithDefault,
|
||||||
loadChatUsername,
|
hasUsername,
|
||||||
saveChatColor,
|
|
||||||
loadChatColor,
|
|
||||||
saveJavaPath,
|
saveJavaPath,
|
||||||
loadJavaPath,
|
loadJavaPath,
|
||||||
saveInstallPath,
|
saveInstallPath,
|
||||||
@@ -20,19 +18,22 @@ const {
|
|||||||
saveCloseLauncherOnStart,
|
saveCloseLauncherOnStart,
|
||||||
loadCloseLauncherOnStart,
|
loadCloseLauncherOnStart,
|
||||||
|
|
||||||
// Hardware Acceleration
|
|
||||||
saveLauncherHardwareAcceleration,
|
saveLauncherHardwareAcceleration,
|
||||||
loadLauncherHardwareAcceleration,
|
loadLauncherHardwareAcceleration,
|
||||||
|
|
||||||
|
loadConfig,
|
||||||
|
saveConfig,
|
||||||
|
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
isFirstLaunch,
|
isFirstLaunch,
|
||||||
markAsLaunched,
|
markAsLaunched,
|
||||||
|
checkLaunchReady,
|
||||||
// UUID Management
|
// UUID Management
|
||||||
getCurrentUuid,
|
getCurrentUuid,
|
||||||
getAllUuidMappings,
|
getAllUuidMappings,
|
||||||
|
getAllUuidMappingsArray,
|
||||||
setUuidForUser,
|
setUuidForUser,
|
||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
@@ -113,11 +114,10 @@ module.exports = {
|
|||||||
// User configuration functions
|
// User configuration functions
|
||||||
saveUsername,
|
saveUsername,
|
||||||
loadUsername,
|
loadUsername,
|
||||||
saveChatUsername,
|
loadUsernameWithDefault,
|
||||||
loadChatUsername,
|
hasUsername,
|
||||||
saveChatColor,
|
|
||||||
loadChatColor,
|
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
|
checkLaunchReady,
|
||||||
|
|
||||||
// Java configuration functions
|
// Java configuration functions
|
||||||
saveJavaPath,
|
saveJavaPath,
|
||||||
@@ -144,6 +144,10 @@ module.exports = {
|
|||||||
saveLauncherHardwareAcceleration,
|
saveLauncherHardwareAcceleration,
|
||||||
loadLauncherHardwareAcceleration,
|
loadLauncherHardwareAcceleration,
|
||||||
|
|
||||||
|
// Config functions
|
||||||
|
loadConfig,
|
||||||
|
saveConfig,
|
||||||
|
|
||||||
// GPU Preference functions
|
// GPU Preference functions
|
||||||
saveGpuPreference,
|
saveGpuPreference,
|
||||||
loadGpuPreference,
|
loadGpuPreference,
|
||||||
@@ -165,6 +169,7 @@ module.exports = {
|
|||||||
// UUID Management functions
|
// UUID Management functions
|
||||||
getCurrentUuid,
|
getCurrentUuid,
|
||||||
getAllUuidMappings,
|
getAllUuidMappings,
|
||||||
|
getAllUuidMappingsArray,
|
||||||
setUuidForUser,
|
setUuidForUser,
|
||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
|
|||||||
272
backend/managers/differentialUpdateManager.js
Normal file
272
backend/managers/differentialUpdateManager.js
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execFile } = require('child_process');
|
||||||
|
const { downloadFile, retryDownload } = require('../utils/fileManager');
|
||||||
|
const { getOS, getArch } = require('../utils/platformUtils');
|
||||||
|
const { validateChecksum, extractVersionDetails, canUseDifferentialUpdate, needsIntermediatePatches, getInstalledClientVersion } = require('../services/versionManager');
|
||||||
|
const { installButler } = require('./butlerManager');
|
||||||
|
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||||
|
const { saveVersionClient } = require('../core/config');
|
||||||
|
|
||||||
|
async function acquireGameArchive(downloadUrl, targetPath, checksum, progressCallback, allowRetry = true) {
|
||||||
|
const osName = getOS();
|
||||||
|
const arch = getArch();
|
||||||
|
|
||||||
|
if (osName === 'darwin' && arch === 'amd64') {
|
||||||
|
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(targetPath)) {
|
||||||
|
const stats = fs.statSync(targetPath);
|
||||||
|
if (stats.size > 1024 * 1024) {
|
||||||
|
const isValid = await validateChecksum(targetPath, checksum);
|
||||||
|
if (isValid) {
|
||||||
|
console.log(`Valid archive found in cache: ${targetPath}`);
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
console.log('Cached archive checksum mismatch, re-downloading');
|
||||||
|
fs.unlinkSync(targetPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Downloading game archive from: ${downloadUrl}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (allowRetry) {
|
||||||
|
await retryDownload(downloadUrl, targetPath, progressCallback);
|
||||||
|
} else {
|
||||||
|
await downloadFile(downloadUrl, targetPath, progressCallback);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const enhancedError = new Error(`Archive download failed: ${error.message}`);
|
||||||
|
enhancedError.originalError = error;
|
||||||
|
enhancedError.downloadUrl = downloadUrl;
|
||||||
|
enhancedError.targetPath = targetPath;
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(targetPath);
|
||||||
|
console.log(`Archive downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
|
const isValid = await validateChecksum(targetPath, checksum);
|
||||||
|
if (!isValid) {
|
||||||
|
console.log('Downloaded archive checksum validation failed, removing corrupted file');
|
||||||
|
fs.unlinkSync(targetPath);
|
||||||
|
throw new Error('Downloaded archive is corrupted or invalid. Please retry');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Archive validation passed: ${targetPath}`);
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deployGameArchive(archivePath, destinationDir, toolsDir, progressCallback, isDifferential = false) {
|
||||||
|
if (!archivePath || !fs.existsSync(archivePath)) {
|
||||||
|
throw new Error(`Archive not found: ${archivePath || 'undefined'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(archivePath);
|
||||||
|
console.log(`Deploying archive: ${archivePath}`);
|
||||||
|
console.log(`Archive size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(`Deployment mode: ${isDifferential ? 'differential' : 'full'}`);
|
||||||
|
|
||||||
|
const butlerPath = await installButler(toolsDir);
|
||||||
|
const stagingDir = path.join(destinationDir, 'staging-temp');
|
||||||
|
|
||||||
|
if (!fs.existsSync(destinationDir)) {
|
||||||
|
fs.mkdirSync(destinationDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(stagingDir)) {
|
||||||
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(stagingDir, { recursive: true });
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(isDifferential ? 'Applying differential update...' : 'Installing game files...', null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
'apply',
|
||||||
|
'--staging-dir',
|
||||||
|
stagingDir,
|
||||||
|
archivePath,
|
||||||
|
destinationDir
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(`Executing deployment: ${butlerPath} ${args.join(' ')}`);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = execFile(butlerPath, args, {
|
||||||
|
maxBuffer: 1024 * 1024 * 10,
|
||||||
|
timeout: 600000
|
||||||
|
}, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
const cleanStderr = stderr.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
|
||||||
|
const cleanStdout = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
|
||||||
|
|
||||||
|
if (cleanStderr) console.error('Deployment stderr:', cleanStderr);
|
||||||
|
if (cleanStdout) console.error('Deployment stdout:', cleanStdout);
|
||||||
|
|
||||||
|
const errorText = (stderr + ' ' + error.message).toLowerCase();
|
||||||
|
let message = 'Game deployment failed';
|
||||||
|
|
||||||
|
if (errorText.includes('unexpected eof')) {
|
||||||
|
message = 'Corrupted archive detected. Please retry download.';
|
||||||
|
if (fs.existsSync(archivePath)) {
|
||||||
|
fs.unlinkSync(archivePath);
|
||||||
|
}
|
||||||
|
} else if (errorText.includes('permission denied')) {
|
||||||
|
message = 'Permission denied. Check file permissions and try again.';
|
||||||
|
} else if (errorText.includes('no space left') || errorText.includes('device full')) {
|
||||||
|
message = 'Insufficient disk space. Free up space and try again.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const deployError = new Error(message);
|
||||||
|
deployError.originalError = error;
|
||||||
|
deployError.stderr = cleanStderr;
|
||||||
|
deployError.stdout = cleanStdout;
|
||||||
|
return reject(deployError);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Game deployment completed successfully');
|
||||||
|
const cleanOutput = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
|
||||||
|
if (cleanOutput) {
|
||||||
|
console.log(cleanOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(stagingDir)) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
||||||
|
} catch (cleanupErr) {
|
||||||
|
console.warn('Failed to cleanup staging directory:', cleanupErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
console.error('Deployment process error:', err);
|
||||||
|
reject(new Error(`Failed to execute deployment tool: ${err.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performIntelligentUpdate(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
|
||||||
|
console.log(`Initiating intelligent update to version ${targetVersion}`);
|
||||||
|
|
||||||
|
const currentVersion = getInstalledClientVersion();
|
||||||
|
console.log(`Current version: ${currentVersion || 'none (clean install)'}`);
|
||||||
|
console.log(`Target version: ${targetVersion}`);
|
||||||
|
console.log(`Branch: ${branch}`);
|
||||||
|
|
||||||
|
if (branch !== 'release') {
|
||||||
|
console.log(`Pre-release branch detected - forcing full archive download`);
|
||||||
|
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||||
|
const archiveName = path.basename(versionDetails.fullUrl);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||||
|
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||||
|
saveVersionClient(targetVersion);
|
||||||
|
console.log(`Pre-release installation completed. Version ${targetVersion} is now installed.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentVersion) {
|
||||||
|
console.log('No existing installation detected - downloading full archive');
|
||||||
|
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||||
|
const archiveName = path.basename(versionDetails.fullUrl);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(`Downloading full game archive (first install - v${targetVersion})...`, 0, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||||
|
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||||
|
saveVersionClient(targetVersion);
|
||||||
|
console.log(`Initial installation completed. Version ${targetVersion} is now installed.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchesToApply = needsIntermediatePatches(currentVersion, targetVersion);
|
||||||
|
|
||||||
|
if (patchesToApply.length === 0) {
|
||||||
|
console.log('Already at target version or invalid version sequence');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Applying ${patchesToApply.length} differential patch(es): ${patchesToApply.join(' -> ')}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < patchesToApply.length; i++) {
|
||||||
|
const patchVersion = patchesToApply[i];
|
||||||
|
const versionDetails = await extractVersionDetails(patchVersion, branch);
|
||||||
|
|
||||||
|
const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails);
|
||||||
|
|
||||||
|
if (!canDifferential || !versionDetails.differentialUrl) {
|
||||||
|
console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`);
|
||||||
|
const archiveName = path.basename(versionDetails.fullUrl);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(`Downloading full archive for ${patchVersion} (${i + 1}/${patchesToApply.length})...`, 0, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||||
|
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||||
|
} else {
|
||||||
|
console.log(`Applying differential patch: ${versionDetails.sourceVersion} -> ${patchVersion}`);
|
||||||
|
const archiveName = path.basename(versionDetails.differentialUrl);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_patch_${archiveName}`);
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(`Applying patch ${i + 1}/${patchesToApply.length}: ${patchVersion}...`, 0, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await acquireGameArchive(versionDetails.differentialUrl, archivePath, versionDetails.checksum, progressCallback);
|
||||||
|
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, true);
|
||||||
|
|
||||||
|
if (fs.existsSync(archivePath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(archivePath);
|
||||||
|
console.log(`Cleaned up patch file: ${archiveName}`);
|
||||||
|
} catch (cleanupErr) {
|
||||||
|
console.warn(`Failed to cleanup patch file: ${cleanupErr.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveVersionClient(patchVersion);
|
||||||
|
console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Update completed successfully. Version ${targetVersion} is now installed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
|
||||||
|
const { findClientPath } = require('../core/paths');
|
||||||
|
const clientPath = findClientPath(gameDir);
|
||||||
|
|
||||||
|
if (clientPath) {
|
||||||
|
const currentVersion = getInstalledClientVersion();
|
||||||
|
if (currentVersion === targetVersion) {
|
||||||
|
console.log(`Game already installed at correct version: ${targetVersion}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await performIntelligentUpdate(targetVersion, branch, progressCallback, gameDir, cacheDir, toolsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
acquireGameArchive,
|
||||||
|
deployGameArchive,
|
||||||
|
performIntelligentUpdate,
|
||||||
|
ensureGameInstalled
|
||||||
|
};
|
||||||
@@ -7,12 +7,26 @@ const { spawn } = require('child_process');
|
|||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { getResolvedAppDir, findClientPath } = require('../core/paths');
|
const { getResolvedAppDir, findClientPath } = require('../core/paths');
|
||||||
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils');
|
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils');
|
||||||
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain, loadVersionBranch, loadVersionClient, saveVersionClient } = require('../core/config');
|
const {
|
||||||
|
saveInstallPath,
|
||||||
|
loadJavaPath,
|
||||||
|
getUuidForUser,
|
||||||
|
getAuthServerUrl,
|
||||||
|
getAuthDomain,
|
||||||
|
loadVersionBranch,
|
||||||
|
loadVersionClient,
|
||||||
|
saveVersionClient,
|
||||||
|
loadUsername,
|
||||||
|
hasUsername,
|
||||||
|
checkLaunchReady
|
||||||
|
} = require('../core/config');
|
||||||
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
||||||
const { getLatestClientVersion } = require('../services/versionManager');
|
const { getLatestClientVersion } = require('../services/versionManager');
|
||||||
const { updateGameFiles } = require('./gameManager');
|
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
||||||
|
const { ensureGameInstalled } = require('./differentialUpdateManager');
|
||||||
const { syncModsForCurrentProfile } = require('./modManager');
|
const { syncModsForCurrentProfile } = require('./modManager');
|
||||||
const { getUserDataPath } = require('../utils/userDataMigration');
|
const { getUserDataPath } = require('../utils/userDataMigration');
|
||||||
|
const { syncServerList } = require('../utils/serverListSync');
|
||||||
|
|
||||||
// Client patcher for custom auth server (sanasol.ws)
|
// Client patcher for custom auth server (sanasol.ws)
|
||||||
let clientPatcher = null;
|
let clientPatcher = null;
|
||||||
@@ -102,13 +116,58 @@ function generateLocalTokens(uuid, name) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
async function launchGame(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 1: Validate player identity FIRST (before any other operations)
|
||||||
|
// ==========================================================================
|
||||||
|
const launchState = checkLaunchReady();
|
||||||
|
|
||||||
|
// Load username from config - single source of truth
|
||||||
|
let playerName = loadUsername();
|
||||||
|
|
||||||
|
if (!playerName) {
|
||||||
|
// No username configured - this is a critical error
|
||||||
|
const error = new Error('No username configured. Please set your username in Settings before playing.');
|
||||||
|
console.error('[Launcher] Launch blocked:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow override only if explicitly provided (for testing/migration)
|
||||||
|
if (playerNameOverride && typeof playerNameOverride === 'string' && playerNameOverride.trim()) {
|
||||||
|
const overrideName = playerNameOverride.trim();
|
||||||
|
if (overrideName !== playerName && overrideName !== 'Player') {
|
||||||
|
console.warn(`[Launcher] Username override requested: "${overrideName}" (saved: "${playerName}")`);
|
||||||
|
// Use override for this session but DON'T save it - config is source of truth
|
||||||
|
playerName = overrideName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if using default 'Player' name (likely misconfiguration)
|
||||||
|
if (playerName === 'Player') {
|
||||||
|
console.warn('[Launcher] Warning: Using default username "Player". This may cause cosmetic issues.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Launcher] Launching game for player: "${playerName}"`);
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 2: Synchronize server list
|
||||||
|
// ==========================================================================
|
||||||
|
try {
|
||||||
|
console.log('[Launcher] Synchronizing server list...');
|
||||||
|
await syncServerList();
|
||||||
|
} catch (syncError) {
|
||||||
|
console.warn('[Launcher] Server list sync failed, continuing launch:', syncError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 3: Setup paths and directories
|
||||||
|
// ==========================================================================
|
||||||
const branch = branchOverride || loadVersionBranch();
|
const branch = branchOverride || loadVersionBranch();
|
||||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||||
|
|
||||||
// NEW 2.1.2: Use centralized UserData location
|
// NEW 2.2.0: Use centralized UserData location
|
||||||
const userDataDir = getUserDataPath();
|
const userDataDir = getUserDataPath();
|
||||||
|
|
||||||
const gameLatest = customGameDir;
|
const gameLatest = customGameDir;
|
||||||
@@ -118,7 +177,10 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
throw new Error('Game is not installed. Please install the game first.');
|
throw new Error('Game is not installed. Please install the game first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
saveUsername(playerName);
|
// NOTE: We do NOT save username here anymore - username is only saved
|
||||||
|
// when user explicitly changes it in Settings. This prevents accidental
|
||||||
|
// overwrites from race conditions or default values.
|
||||||
|
|
||||||
if (installPathOverride) {
|
if (installPathOverride) {
|
||||||
saveInstallPath(installPathOverride);
|
saveInstallPath(installPathOverride);
|
||||||
}
|
}
|
||||||
@@ -169,7 +231,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
if (progressCallback && msg) {
|
if (progressCallback && msg) {
|
||||||
progressCallback(msg, percent, null, null, null);
|
progressCallback(msg, percent, null, null, null);
|
||||||
}
|
}
|
||||||
});
|
}, null, branch);
|
||||||
|
|
||||||
if (patchResult.success) {
|
if (patchResult.success) {
|
||||||
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
|
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
|
||||||
@@ -285,6 +347,55 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||||
Object.assign(env, gpuEnv);
|
Object.assign(env, gpuEnv);
|
||||||
|
|
||||||
|
// Linux: Replace bundled libzstd.so with system version to fix glibc 2.41+ crash
|
||||||
|
// The bundled libzstd causes "free(): invalid pointer" on Steam Deck / Ubuntu LTS
|
||||||
|
if (process.platform === 'linux' && process.env.HYTALE_NO_LIBZSTD_FIX !== '1') {
|
||||||
|
const clientDir = path.dirname(clientPath);
|
||||||
|
const bundledLibzstd = path.join(clientDir, 'libzstd.so');
|
||||||
|
const backupLibzstd = path.join(clientDir, 'libzstd.so.bundled');
|
||||||
|
|
||||||
|
// Common system libzstd paths
|
||||||
|
const systemLibzstdPaths = [
|
||||||
|
'/usr/lib/libzstd.so.1', // Arch Linux, Steam Deck
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libzstd.so.1', // Debian/Ubuntu
|
||||||
|
'/usr/lib64/libzstd.so.1' // Fedora/RHEL
|
||||||
|
];
|
||||||
|
|
||||||
|
let systemLibzstd = null;
|
||||||
|
for (const p of systemLibzstdPaths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
systemLibzstd = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemLibzstd && fs.existsSync(bundledLibzstd)) {
|
||||||
|
try {
|
||||||
|
const stats = fs.lstatSync(bundledLibzstd);
|
||||||
|
|
||||||
|
// Only replace if it's not already a symlink to system version
|
||||||
|
if (!stats.isSymbolicLink()) {
|
||||||
|
// Backup bundled version
|
||||||
|
if (!fs.existsSync(backupLibzstd)) {
|
||||||
|
fs.renameSync(bundledLibzstd, backupLibzstd);
|
||||||
|
console.log(`Linux: Backed up bundled libzstd.so`);
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(bundledLibzstd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create symlink to system version
|
||||||
|
fs.symlinkSync(systemLibzstd, bundledLibzstd);
|
||||||
|
console.log(`Linux: Linked libzstd.so to system version (${systemLibzstd}) for glibc 2.41+ compatibility`);
|
||||||
|
} else {
|
||||||
|
const linkTarget = fs.readlinkSync(bundledLibzstd);
|
||||||
|
console.log(`Linux: libzstd.so already linked to ${linkTarget}`);
|
||||||
|
}
|
||||||
|
} catch (libzstdError) {
|
||||||
|
console.warn(`Linux: Could not replace libzstd.so: ${libzstdError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let spawnOptions = {
|
let spawnOptions = {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
@@ -358,8 +469,24 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
async function launchGameWithVersionCheck(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||||
try {
|
try {
|
||||||
|
// ==========================================================================
|
||||||
|
// PRE-LAUNCH VALIDATION: Check username is configured
|
||||||
|
// ==========================================================================
|
||||||
|
const launchState = checkLaunchReady();
|
||||||
|
|
||||||
|
if (!launchState.hasUsername) {
|
||||||
|
const error = 'No username configured. Please set your username in Settings before playing.';
|
||||||
|
console.error('[Launcher] Launch blocked:', error);
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(error, -1, null, null, null);
|
||||||
|
}
|
||||||
|
return { success: false, error: error, needsUsername: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Launcher] Pre-launch check passed. Username: "${launchState.username}"`);
|
||||||
|
|
||||||
const branch = branchOverride || loadVersionBranch();
|
const branch = branchOverride || loadVersionBranch();
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -388,7 +515,13 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
|||||||
const customCacheDir = path.join(customAppDir, 'cache');
|
const customCacheDir = path.join(customAppDir, 'cache');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir, branch);
|
let versionToInstall = latestVersion;
|
||||||
|
if (FORCE_CLEAN_INSTALL_VERSION && !installedVersion) {
|
||||||
|
versionToInstall = CLEAN_INSTALL_TEST_VERSION;
|
||||||
|
console.log(`TESTING MODE: Clean install detected, forcing version ${versionToInstall} instead of ${latestVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureGameInstalled(versionToInstall, branch, progressCallback, customGameDir, customCacheDir, customToolsDir);
|
||||||
console.log('Game updated successfully, patching will be forced on launch...');
|
console.log('Game updated successfully, patching will be forced on launch...');
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -409,7 +542,7 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
|||||||
progressCallback('Launching game...', 80, null, null, null);
|
progressCallback('Launching game...', 80, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchResult = await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
|
const launchResult = await launchGame(playerNameOverride, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
|
||||||
|
|
||||||
// Ensure we always return a result
|
// Ensure we always return a result
|
||||||
if (!launchResult) {
|
if (!launchResult) {
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursi
|
|||||||
const { getOS, getArch } = require('../utils/platformUtils');
|
const { getOS, getArch } = require('../utils/platformUtils');
|
||||||
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
|
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
|
||||||
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
|
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
|
||||||
|
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
||||||
const { installButler } = require('./butlerManager');
|
const { installButler } = require('./butlerManager');
|
||||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||||
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
||||||
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
||||||
const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration');
|
const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration');
|
||||||
|
|
||||||
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||||
const osName = getOS();
|
const osName = getOS();
|
||||||
const arch = getArch();
|
const arch = getArch();
|
||||||
|
|
||||||
@@ -300,6 +301,16 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
fs.rmSync(stagingDir, { recursive: true, force: true });
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete PWR file from cache after successful installation
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(pwrFile)) {
|
||||||
|
fs.unlinkSync(pwrFile);
|
||||||
|
console.log('[Butler] PWR file deleted from cache after successful installation:', pwrFile);
|
||||||
|
}
|
||||||
|
} catch (delErr) {
|
||||||
|
console.warn('[Butler] Failed to delete PWR file from cache:', delErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Installation complete', null, null, null, null);
|
progressCallback('Installation complete', null, null, null, null);
|
||||||
}
|
}
|
||||||
@@ -316,7 +327,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// NEW 2.1.2: Ensure UserData migration to centralized location
|
// NEW 2.2.0: Ensure UserData migration to centralized location
|
||||||
try {
|
try {
|
||||||
console.log('[UpdateGameFiles] Ensuring UserData migration...');
|
console.log('[UpdateGameFiles] Ensuring UserData migration...');
|
||||||
const migrationResult = await migrateUserDataToCentralized();
|
const migrationResult = await migrateUserDataToCentralized();
|
||||||
@@ -352,7 +363,15 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
}
|
}
|
||||||
|
|
||||||
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
|
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
|
||||||
|
// Delete PWR file from cache after successful update
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(pwrFile)) {
|
||||||
|
fs.unlinkSync(pwrFile);
|
||||||
|
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
|
||||||
|
}
|
||||||
|
} catch (delErr) {
|
||||||
|
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
|
||||||
|
}
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Replacing game files...', 80, null, null, null);
|
progressCallback('Replacing game files...', 80, null, null, null);
|
||||||
}
|
}
|
||||||
@@ -384,7 +403,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
||||||
console.log('Logo@2x.png update result after update:', logoResult);
|
console.log('Logo@2x.png update result after update:', logoResult);
|
||||||
|
|
||||||
// NEW 2.1.2: No longer create UserData in game installation
|
// NEW 2.2.0: No longer create UserData in game installation
|
||||||
// UserData is now in centralized location (getUserDataPath())
|
// UserData is now in centralized location (getUserDataPath())
|
||||||
console.log('[UpdateGameFiles] UserData is managed in centralized location');
|
console.log('[UpdateGameFiles] UserData is managed in centralized location');
|
||||||
|
|
||||||
@@ -434,7 +453,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||||
|
|
||||||
// NEW 2.1.2: Ensure UserData migration to centralized location
|
// NEW 2.2.0: Ensure UserData migration to centralized location
|
||||||
try {
|
try {
|
||||||
console.log('[InstallGame] Ensuring UserData migration...');
|
console.log('[InstallGame] Ensuring UserData migration...');
|
||||||
const migrationResult = await migrateUserDataToCentralized();
|
const migrationResult = await migrateUserDataToCentralized();
|
||||||
@@ -453,7 +472,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
saveUsername(playerName);
|
// NOTE: Do NOT save username here - username should only be saved when user explicitly
|
||||||
|
// changes it in Settings. Saving here could overwrite a good username with 'Player' default.
|
||||||
|
// The username is only needed for launching, not for installing.
|
||||||
if (installPathOverride) {
|
if (installPathOverride) {
|
||||||
saveInstallPath(installPathOverride);
|
saveInstallPath(installPathOverride);
|
||||||
}
|
}
|
||||||
@@ -510,31 +531,33 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
console.log(`Installing game files for branch: ${branch}...`);
|
console.log(`Installing game files for branch: ${branch}...`);
|
||||||
|
|
||||||
const latestVersion = await getLatestClientVersion(branch);
|
const latestVersion = await getLatestClientVersion(branch);
|
||||||
let pwrFile;
|
const targetVersion = FORCE_CLEAN_INSTALL_VERSION ? CLEAN_INSTALL_TEST_VERSION : latestVersion;
|
||||||
try {
|
|
||||||
pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir);
|
|
||||||
|
|
||||||
// If downloadPWR returns false, it means the file doesn't exist or is invalid
|
if (FORCE_CLEAN_INSTALL_VERSION) {
|
||||||
// We should retry the download with a manual retry flag
|
console.log(`TESTING MODE: Forcing installation of ${targetVersion} instead of ${latestVersion}`);
|
||||||
if (!pwrFile) {
|
}
|
||||||
console.log('[Install] PWR file not found or invalid, attempting retry...');
|
|
||||||
pwrFile = await retryPWRDownload(branch, latestVersion, progressCallback, customCacheDir);
|
let pwrFile;
|
||||||
|
try {
|
||||||
|
pwrFile = await downloadPWR(branch, targetVersion, progressCallback, customCacheDir);
|
||||||
|
|
||||||
|
if (!pwrFile) {
|
||||||
|
console.log('[Install] PWR file not found or invalid, attempting retry...');
|
||||||
|
pwrFile = await retryPWRDownload(branch, targetVersion, progressCallback, customCacheDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check we have a valid file path
|
|
||||||
if (!pwrFile || typeof pwrFile !== 'string') {
|
if (!pwrFile || typeof pwrFile !== 'string') {
|
||||||
throw new Error(`PWR file download failed: received invalid path ${pwrFile}. Please retry download.`);
|
throw new Error(`PWR file download failed: received invalid path ${pwrFile}. Please retry download.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (downloadError) {
|
} catch (downloadError) {
|
||||||
console.error('[Install] PWR download failed:', downloadError.message);
|
console.error('[Install] PWR download failed:', downloadError.message);
|
||||||
throw downloadError; // Re-throw to be handled by the main installGame error handler
|
throw downloadError;
|
||||||
}
|
}
|
||||||
|
|
||||||
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir, branch, customCacheDir);
|
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir, branch, customCacheDir);
|
||||||
|
|
||||||
// Save the installed version and branch to config
|
saveVersionClient(targetVersion);
|
||||||
saveVersionClient(latestVersion);
|
|
||||||
const { saveVersionBranch } = require('../core/config');
|
const { saveVersionBranch } = require('../core/config');
|
||||||
saveVersionBranch(branch);
|
saveVersionBranch(branch);
|
||||||
|
|
||||||
@@ -544,7 +567,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
||||||
console.log('Logo@2x.png update result after installation:', logoResult);
|
console.log('Logo@2x.png update result after installation:', logoResult);
|
||||||
|
|
||||||
// NEW 2.1.2: No longer create UserData in game installation
|
// NEW 2.2.0: No longer create UserData in game installation
|
||||||
// UserData is managed in centralized location (getUserDataPath())
|
// UserData is managed in centralized location (getUserDataPath())
|
||||||
console.log('[InstallGame] UserData is managed in centralized location');
|
console.log('[InstallGame] UserData is managed in centralized location');
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const path = require('path');
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { getOS } = require('../utils/platformUtils');
|
const { getOS } = require('../utils/platformUtils');
|
||||||
const { getModsPath, getProfilesDir } = require('../core/paths');
|
const { getModsPath, getProfilesDir, getHytaleSavesDir } = require('../core/paths');
|
||||||
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
||||||
const profileManager = require('./profileManager');
|
const profileManager = require('./profileManager');
|
||||||
|
|
||||||
@@ -296,8 +296,9 @@ async function syncModsForCurrentProfile() {
|
|||||||
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`);
|
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`);
|
||||||
|
|
||||||
// 1. Resolve Paths
|
// 1. Resolve Paths
|
||||||
// globalModsPath is the one the game uses (symlink target)
|
// centralModsPath is HytaleSaves\Mods (centralized location for active mods)
|
||||||
const globalModsPath = await getModsPath();
|
const hytaleSavesDir = getHytaleSavesDir();
|
||||||
|
const centralModsPath = path.join(hytaleSavesDir, 'Mods');
|
||||||
// profileModsPath is the real storage for this profile
|
// profileModsPath is the real storage for this profile
|
||||||
const profileModsPath = getProfileModsPath(activeProfile.id);
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
||||||
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
||||||
@@ -306,94 +307,49 @@ async function syncModsForCurrentProfile() {
|
|||||||
fs.mkdirSync(profileDisabledModsPath, { recursive: true });
|
fs.mkdirSync(profileDisabledModsPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Symlink / Migration Logic
|
// 2. Copy-based Mod Sync (No symlinks - avoids permission issues)
|
||||||
let needsLink = false;
|
// Ensure HytaleSaves\Mods directory exists
|
||||||
let globalStats = null;
|
if (!fs.existsSync(centralModsPath)) {
|
||||||
|
fs.mkdirSync(centralModsPath, { recursive: true });
|
||||||
|
console.log(`[ModManager] Created centralized mods directory: ${centralModsPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for old symlink and convert to real directory if needed (one-time migration)
|
||||||
try {
|
try {
|
||||||
globalStats = fs.lstatSync(globalModsPath);
|
const centralStats = fs.lstatSync(centralModsPath);
|
||||||
|
if (centralStats.isSymbolicLink()) {
|
||||||
|
console.log('[ModManager] Removing old symlink, converting to copy-based system...');
|
||||||
|
fs.unlinkSync(centralModsPath);
|
||||||
|
fs.mkdirSync(centralModsPath, { recursive: true });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Path doesn't exist
|
// Path doesn't exist, will be created above
|
||||||
}
|
}
|
||||||
|
|
||||||
if (globalStats) {
|
// Copy enabled mods from profile to HytaleSaves\Mods (for game to use)
|
||||||
if (globalStats.isSymbolicLink()) {
|
console.log(`[ModManager] Copying enabled mods from ${profileModsPath} to ${centralModsPath}`);
|
||||||
const linkTarget = fs.readlinkSync(globalModsPath);
|
|
||||||
// Normalize paths for comparison
|
|
||||||
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
|
|
||||||
console.log(`[ModManager] Updating symlink from ${linkTarget} to ${profileModsPath}`);
|
|
||||||
fs.unlinkSync(globalModsPath);
|
|
||||||
needsLink = true;
|
|
||||||
}
|
|
||||||
} else if (globalStats.isDirectory()) {
|
|
||||||
// MIGRATION: It's a real directory. Move contents to profile.
|
|
||||||
console.log('[ModManager] Migrating global mods folder to profile folder...');
|
|
||||||
const files = fs.readdirSync(globalModsPath);
|
|
||||||
for (const file of files) {
|
|
||||||
const src = path.join(globalModsPath, file);
|
|
||||||
const dest = path.join(profileModsPath, file);
|
|
||||||
// Only move if dest doesn't exist to avoid overwriting
|
|
||||||
if (!fs.existsSync(dest)) {
|
|
||||||
fs.renameSync(src, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also migrate DisabledMods if it exists globally
|
// First, clear central mods folder
|
||||||
const globalDisabledPath = path.join(path.dirname(globalModsPath), 'DisabledMods');
|
const existingCentralMods = fs.existsSync(centralModsPath) ? fs.readdirSync(centralModsPath) : [];
|
||||||
if (fs.existsSync(globalDisabledPath) && fs.lstatSync(globalDisabledPath).isDirectory()) {
|
for (const file of existingCentralMods) {
|
||||||
const dFiles = fs.readdirSync(globalDisabledPath);
|
const filePath = path.join(centralModsPath, file);
|
||||||
for (const file of dFiles) {
|
|
||||||
const src = path.join(globalDisabledPath, file);
|
|
||||||
const dest = path.join(profileDisabledModsPath, file);
|
|
||||||
if (!fs.existsSync(dest)) {
|
|
||||||
fs.renameSync(src, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We can remove global DisabledMods now, as it's not used by game
|
|
||||||
try { fs.rmSync(globalDisabledPath, { recursive: true, force: true }); } catch(e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the directory so we can link it
|
|
||||||
try {
|
try {
|
||||||
let retries = 3;
|
fs.unlinkSync(filePath);
|
||||||
while (retries > 0) {
|
|
||||||
try {
|
|
||||||
fs.rmSync(globalModsPath, { recursive: true, force: true });
|
|
||||||
break;
|
|
||||||
} catch (err) {
|
|
||||||
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
|
||||||
retries--;
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 500));
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
needsLink = true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to remove global mods dir:', e);
|
console.warn(`Failed to remove ${file} from central mods:`, e.message);
|
||||||
// Throw error to stop.
|
|
||||||
throw new Error('Failed to migrate mods directory. Please clear ' + globalModsPath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
needsLink = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsLink) {
|
// Copy enabled mods to HytaleSaves\Mods
|
||||||
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
|
const enabledModFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
||||||
|
for (const file of enabledModFiles) {
|
||||||
|
const src = path.join(profileModsPath, file);
|
||||||
|
const dest = path.join(centralModsPath, file);
|
||||||
try {
|
try {
|
||||||
const symlinkType = getOS() === 'windows' ? 'junction' : 'dir';
|
fs.copyFileSync(src, dest);
|
||||||
fs.symlinkSync(profileModsPath, globalModsPath, symlinkType);
|
console.log(`[ModManager] Copied ${file} to HytaleSaves\\Mods`);
|
||||||
} catch (err) {
|
} catch (e) {
|
||||||
// If we can't create the symlink, try creating the directory first
|
console.error(`Failed to copy ${file}:`, e.message);
|
||||||
console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.');
|
|
||||||
console.error(err.message);
|
|
||||||
|
|
||||||
// Fallback: create a real directory so the game still works
|
|
||||||
if (!fs.existsSync(globalModsPath)) {
|
|
||||||
fs.mkdirSync(globalModsPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,7 +416,7 @@ async function syncModsForCurrentProfile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods)
|
// 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods)
|
||||||
// Note: Since Global/Mods IS Profile/Mods (via symlink), moving out of Profile/Mods disables it for the game.
|
// Note: Enabled mods are copied to HytaleSaves\Mods, disabled mods stay in Profile/DisabledMods
|
||||||
|
|
||||||
const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
||||||
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
|
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
|
||||||
|
|||||||
@@ -3,32 +3,117 @@ const path = require('path');
|
|||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
|
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DEPRECATED: This file is kept for backward compatibility.
|
||||||
|
*
|
||||||
|
* The primary UUID system is now in config.js using userUuids.
|
||||||
|
* This player_id.json system was a separate UUID storage that could
|
||||||
|
* cause desync issues.
|
||||||
|
*
|
||||||
|
* New code should use config.js functions:
|
||||||
|
* - getUuidForUser(username) - Get/create UUID for a username
|
||||||
|
* - getCurrentUuid() - Get current user's UUID
|
||||||
|
* - setUuidForUser(username, uuid) - Set UUID for a user
|
||||||
|
*
|
||||||
|
* This function is kept for migration purposes only.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create a legacy player ID
|
||||||
|
* NOTE: This is DEPRECATED - use config.js getUuidForUser() instead
|
||||||
|
*
|
||||||
|
* FIXED: No longer returns random UUID on error - throws instead
|
||||||
|
*/
|
||||||
function getOrCreatePlayerId() {
|
function getOrCreatePlayerId() {
|
||||||
|
const maxRetries = 3;
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(APP_DIR)) {
|
if (!fs.existsSync(APP_DIR)) {
|
||||||
fs.mkdirSync(APP_DIR, { recursive: true });
|
fs.mkdirSync(APP_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(PLAYER_ID_FILE)) {
|
if (fs.existsSync(PLAYER_ID_FILE)) {
|
||||||
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
|
const data = fs.readFileSync(PLAYER_ID_FILE, 'utf8');
|
||||||
if (data.playerId) {
|
if (data.trim()) {
|
||||||
return data.playerId;
|
const parsed = JSON.parse(data);
|
||||||
|
if (parsed.playerId) {
|
||||||
|
return parsed.playerId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No existing ID - create new one atomically
|
||||||
const newPlayerId = uuidv4();
|
const newPlayerId = uuidv4();
|
||||||
fs.writeFileSync(PLAYER_ID_FILE, JSON.stringify({
|
const tempFile = PLAYER_ID_FILE + '.tmp';
|
||||||
|
const playerData = {
|
||||||
playerId: newPlayerId,
|
playerId: newPlayerId,
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString(),
|
||||||
}, null, 2));
|
note: 'DEPRECATED: This file is for legacy compatibility. UUID is now stored in config.json userUuids.'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write to temp file first
|
||||||
|
fs.writeFileSync(tempFile, JSON.stringify(playerData, null, 2));
|
||||||
|
|
||||||
|
// Atomic rename
|
||||||
|
fs.renameSync(tempFile, PLAYER_ID_FILE);
|
||||||
|
|
||||||
|
console.log(`[PlayerManager] Created new legacy player ID: ${newPlayerId}`);
|
||||||
return newPlayerId;
|
return newPlayerId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error managing player ID:', error);
|
lastError = error;
|
||||||
return uuidv4();
|
console.error(`[PlayerManager] Attempt ${attempt}/${maxRetries} failed:`, error.message);
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
// Small delay before retry
|
||||||
|
const delay = attempt * 100;
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < delay) {
|
||||||
|
// Busy wait
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXED: Do NOT return random UUID - throw error instead
|
||||||
|
// Returning random UUID was causing silent identity loss
|
||||||
|
console.error('[PlayerManager] CRITICAL: Failed to get/create player ID after all retries');
|
||||||
|
throw new Error(`Failed to manage player ID: ${lastError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate legacy player_id.json to config.json userUuids
|
||||||
|
* Call this during app startup
|
||||||
|
*/
|
||||||
|
function migrateLegacyPlayerId() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(PLAYER_ID_FILE)) {
|
||||||
|
return null; // No legacy file to migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
|
||||||
|
if (!data.playerId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[PlayerManager] Found legacy player_id.json with ID: ${data.playerId}`);
|
||||||
|
|
||||||
|
// Mark file as migrated by renaming
|
||||||
|
const migratedFile = PLAYER_ID_FILE + '.migrated';
|
||||||
|
if (!fs.existsSync(migratedFile)) {
|
||||||
|
fs.renameSync(PLAYER_ID_FILE, migratedFile);
|
||||||
|
console.log('[PlayerManager] Legacy player_id.json marked as migrated');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.playerId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[PlayerManager] Error during legacy migration:', error.message);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getOrCreatePlayerId
|
getOrCreatePlayerId,
|
||||||
|
migrateLegacyPlayerId
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { getOS, getArch } = require('../utils/platformUtils');
|
||||||
|
|
||||||
|
const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches';
|
||||||
|
const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest';
|
||||||
|
|
||||||
async function getLatestClientVersion(branch = 'release') {
|
async function getLatestClientVersion(branch = 'release') {
|
||||||
try {
|
try {
|
||||||
console.log(`Fetching latest client version from API (branch: ${branch})...`);
|
console.log(`Fetching latest client version from API (branch: ${branch})...`);
|
||||||
const response = await axios.get('https://files.hytalef2p.com/api/version_client', {
|
const response = await axios.get('https://files.hytalef2p.com/api/version_client', {
|
||||||
params: { branch },
|
params: { branch },
|
||||||
timeout: 5000,
|
timeout: 40000,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Hytale-F2P-Launcher'
|
'User-Agent': 'Hytale-F2P-Launcher'
|
||||||
}
|
}
|
||||||
@@ -16,16 +22,144 @@ async function getLatestClientVersion(branch = 'release') {
|
|||||||
console.log(`Latest client version for ${branch}: ${version}`);
|
console.log(`Latest client version for ${branch}: ${version}`);
|
||||||
return version;
|
return version;
|
||||||
} else {
|
} else {
|
||||||
console.log('Warning: Invalid API response, falling back to default version');
|
console.log('Warning: Invalid API response, falling back to latest known version (7.pwr)');
|
||||||
return '4.pwr';
|
return '7.pwr';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching client version:', error.message);
|
console.error('Error fetching client version:', error.message);
|
||||||
console.log('Warning: API unavailable, falling back to default version');
|
console.log('Warning: API unavailable, falling back to latest known version (7.pwr)');
|
||||||
return '4.pwr';
|
return '7.pwr';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildArchiveUrl(buildNumber, branch = 'release') {
|
||||||
|
const os = getOS();
|
||||||
|
const arch = getArch();
|
||||||
|
return `${BASE_PATCH_URL}/${os}/${arch}/${branch}/0/${buildNumber}.pwr`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkArchiveExists(buildNumber, branch = 'release') {
|
||||||
|
const url = buildArchiveUrl(buildNumber, branch);
|
||||||
|
try {
|
||||||
|
const response = await axios.head(url, { timeout: 10000 });
|
||||||
|
return response.status === 200;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function discoverAvailableVersions(latestKnown, branch = 'release', maxProbe = 50) {
|
||||||
|
const available = [];
|
||||||
|
const latest = parseInt(latestKnown.replace('.pwr', ''));
|
||||||
|
|
||||||
|
for (let i = latest; i >= Math.max(1, latest - maxProbe); i--) {
|
||||||
|
const exists = await checkArchiveExists(i, branch);
|
||||||
|
if (exists) {
|
||||||
|
available.push(`${i}.pwr`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPatchManifest(branch = 'release') {
|
||||||
|
try {
|
||||||
|
const os = getOS();
|
||||||
|
const arch = getArch();
|
||||||
|
const response = await axios.get(MANIFEST_API, {
|
||||||
|
params: { branch, os, arch },
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
return response.data.patches || {};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch patch manifest:', error.message);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractVersionDetails(targetVersion, branch = 'release') {
|
||||||
|
const buildNumber = parseInt(targetVersion.replace('.pwr', ''));
|
||||||
|
const previousBuild = buildNumber - 1;
|
||||||
|
|
||||||
|
const manifest = await fetchPatchManifest(branch);
|
||||||
|
const patchInfo = manifest[buildNumber];
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: targetVersion,
|
||||||
|
buildNumber: buildNumber,
|
||||||
|
buildName: `HYTALE-Build-${buildNumber}`,
|
||||||
|
fullUrl: patchInfo?.original_url || buildArchiveUrl(buildNumber, branch),
|
||||||
|
differentialUrl: patchInfo?.patch_url || null,
|
||||||
|
checksum: patchInfo?.patch_hash || null,
|
||||||
|
sourceVersion: patchInfo?.from ? `${patchInfo.from}.pwr` : (previousBuild > 0 ? `${previousBuild}.pwr` : null),
|
||||||
|
isDifferential: !!patchInfo?.proper_patch,
|
||||||
|
releaseNotes: patchInfo?.patch_note || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function canUseDifferentialUpdate(currentVersion, targetDetails) {
|
||||||
|
if (!targetDetails) return false;
|
||||||
|
if (!targetDetails.differentialUrl) return false;
|
||||||
|
if (!targetDetails.isDifferential) return false;
|
||||||
|
|
||||||
|
if (!currentVersion) return false;
|
||||||
|
|
||||||
|
const currentBuild = parseInt(currentVersion.replace('.pwr', ''));
|
||||||
|
const expectedSource = parseInt(targetDetails.sourceVersion?.replace('.pwr', '') || '0');
|
||||||
|
|
||||||
|
return currentBuild === expectedSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function needsIntermediatePatches(currentVersion, targetVersion) {
|
||||||
|
if (!currentVersion) return [];
|
||||||
|
|
||||||
|
const current = parseInt(currentVersion.replace('.pwr', ''));
|
||||||
|
const target = parseInt(targetVersion.replace('.pwr', ''));
|
||||||
|
|
||||||
|
const intermediates = [];
|
||||||
|
for (let i = current + 1; i <= target; i++) {
|
||||||
|
intermediates.push(`${i}.pwr`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return intermediates;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeFileChecksum(filePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = crypto.createHash('sha256');
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
stream.on('data', data => hash.update(data));
|
||||||
|
stream.on('end', () => resolve(hash.digest('hex')));
|
||||||
|
stream.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateChecksum(filePath, expectedChecksum) {
|
||||||
|
if (!expectedChecksum) return true;
|
||||||
|
|
||||||
|
const actualChecksum = await computeFileChecksum(filePath);
|
||||||
|
return actualChecksum === expectedChecksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstalledClientVersion() {
|
||||||
|
try {
|
||||||
|
const { loadVersionClient } = require('../core/config');
|
||||||
|
return loadVersionClient();
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getLatestClientVersion
|
getLatestClientVersion,
|
||||||
|
buildArchiveUrl,
|
||||||
|
checkArchiveExists,
|
||||||
|
discoverAvailableVersions,
|
||||||
|
extractVersionDetails,
|
||||||
|
canUseDifferentialUpdate,
|
||||||
|
needsIntermediatePatches,
|
||||||
|
computeFileChecksum,
|
||||||
|
validateChecksum,
|
||||||
|
getInstalledClientVersion
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
|
||||||
const AdmZip = require('adm-zip');
|
|
||||||
const { execSync, spawn } = require('child_process');
|
|
||||||
const { getJavaExec, getBundledJavaPath } = require('../managers/javaManager');
|
|
||||||
const { JRE_DIR } = require('../core/paths');
|
|
||||||
|
|
||||||
// Domain configuration
|
// Domain configuration
|
||||||
const ORIGINAL_DOMAIN = 'hytale.com';
|
const ORIGINAL_DOMAIN = 'hytale.com';
|
||||||
@@ -26,15 +21,13 @@ function getTargetDomain() {
|
|||||||
const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
|
const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patches HytaleClient and HytaleServer binaries to replace hytale.com with custom domain
|
* Patches HytaleClient binary to replace hytale.com with custom domain
|
||||||
* This allows the game to connect to a custom authentication server
|
* Server patching is done via pre-patched JAR download from CDN
|
||||||
*
|
*
|
||||||
* Supports domains from 4 to 16 characters:
|
* Supports domains from 4 to 16 characters:
|
||||||
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
|
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
|
||||||
* - Domains <= 10 chars: Direct replacement, subdomains stripped
|
* - Domains <= 10 chars: Direct replacement, subdomains stripped
|
||||||
* - Domains 11-16 chars: Split mode - first 6 chars replace subdomain prefix, rest replaces domain
|
* - Domains 11-16 chars: Split mode - first 6 chars replace subdomain prefix, rest replaces domain
|
||||||
*
|
|
||||||
* Official hytale.com keeps original subdomain behavior (sessions., account-data., etc.)
|
|
||||||
*/
|
*/
|
||||||
class ClientPatcher {
|
class ClientPatcher {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -61,19 +54,16 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the domain patching strategy based on length
|
* Calculate the domain patching strategy based on length
|
||||||
* @returns {object} Strategy with mainDomain and subdomainPrefix
|
|
||||||
*/
|
*/
|
||||||
getDomainStrategy(domain) {
|
getDomainStrategy(domain) {
|
||||||
if (domain.length <= 10) {
|
if (domain.length <= 10) {
|
||||||
// Direct replacement - subdomains will be stripped
|
|
||||||
return {
|
return {
|
||||||
mode: 'direct',
|
mode: 'direct',
|
||||||
mainDomain: domain,
|
mainDomain: domain,
|
||||||
subdomainPrefix: '', // Empty = subdomains stripped
|
subdomainPrefix: '',
|
||||||
description: `Direct replacement: hytale.com -> ${domain}`
|
description: `Direct replacement: hytale.com -> ${domain}`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Split mode: first 6 chars become subdomain prefix, rest replaces hytale.com
|
|
||||||
const prefix = domain.slice(0, 6);
|
const prefix = domain.slice(0, 6);
|
||||||
const suffix = domain.slice(6);
|
const suffix = domain.slice(6);
|
||||||
return {
|
return {
|
||||||
@@ -87,21 +77,15 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a string to the length-prefixed byte format used by the client
|
* Convert a string to the length-prefixed byte format used by the client
|
||||||
* Format: [length byte] [00 00 00 padding] [char1] [00] [char2] [00] ... [lastChar]
|
|
||||||
* Note: No null byte after the last character
|
|
||||||
*/
|
*/
|
||||||
stringToLengthPrefixed(str) {
|
stringToLengthPrefixed(str) {
|
||||||
const length = str.length;
|
const length = str.length;
|
||||||
const result = Buffer.alloc(4 + length + (length - 1)); // length byte + padding + chars + separators
|
const result = Buffer.alloc(4 + length + (length - 1));
|
||||||
|
|
||||||
// Length byte
|
|
||||||
result[0] = length;
|
result[0] = length;
|
||||||
// Padding: 00 00 00
|
|
||||||
result[1] = 0x00;
|
result[1] = 0x00;
|
||||||
result[2] = 0x00;
|
result[2] = 0x00;
|
||||||
result[3] = 0x00;
|
result[3] = 0x00;
|
||||||
|
|
||||||
// Characters with null separators (no separator after last char)
|
|
||||||
let pos = 4;
|
let pos = 4;
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result[pos++] = str.charCodeAt(i);
|
result[pos++] = str.charCodeAt(i);
|
||||||
@@ -109,7 +93,6 @@ class ClientPatcher {
|
|||||||
result[pos++] = 0x00;
|
result[pos++] = 0x00;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,13 +107,6 @@ class ClientPatcher {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a string to UTF-8 bytes (how Java stores strings)
|
|
||||||
*/
|
|
||||||
stringToUtf8(str) {
|
|
||||||
return Buffer.from(str, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all occurrences of a pattern in a buffer
|
* Find all occurrences of a pattern in a buffer
|
||||||
*/
|
*/
|
||||||
@@ -148,7 +124,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace bytes in buffer - only overwrites the length of new bytes
|
* Replace bytes in buffer - only overwrites the length of new bytes
|
||||||
* Prevents offset corruption by not expanding the replacement
|
|
||||||
*/
|
*/
|
||||||
replaceBytes(buffer, oldBytes, newBytes) {
|
replaceBytes(buffer, oldBytes, newBytes) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -160,9 +135,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const positions = this.findAllOccurrences(result, oldBytes);
|
const positions = this.findAllOccurrences(result, oldBytes);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
// Only overwrite the length of the new bytes
|
|
||||||
newBytes.copy(result, pos);
|
newBytes.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
@@ -171,32 +144,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UTF-8 domain replacement for Java JAR files.
|
* Smart domain replacement that handles both null-terminated and non-null-terminated strings
|
||||||
* Java stores strings in UTF-8 format in the constant pool.
|
|
||||||
*/
|
|
||||||
findAndReplaceDomainUtf8(data, oldDomain, newDomain) {
|
|
||||||
let count = 0;
|
|
||||||
const result = Buffer.from(data);
|
|
||||||
|
|
||||||
const oldUtf8 = this.stringToUtf8(oldDomain);
|
|
||||||
const newUtf8 = this.stringToUtf8(newDomain);
|
|
||||||
|
|
||||||
const positions = this.findAllOccurrences(result, oldUtf8);
|
|
||||||
|
|
||||||
for (const pos of positions) {
|
|
||||||
newUtf8.copy(result, pos);
|
|
||||||
count++;
|
|
||||||
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { buffer: result, count };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Smart domain replacement that handles both null-terminated and non-null-terminated strings.
|
|
||||||
* .NET AOT stores some strings in various formats:
|
|
||||||
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
|
|
||||||
* - Length-prefixed where last char may have metadata byte instead of \x00
|
|
||||||
*/
|
*/
|
||||||
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -218,7 +166,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
if (lastCharFirstByte === oldLastCharByte) {
|
if (lastCharFirstByte === oldLastCharByte) {
|
||||||
newUtf16NoLast.copy(result, pos);
|
newUtf16NoLast.copy(result, pos);
|
||||||
|
|
||||||
result[lastCharPos] = newLastCharByte;
|
result[lastCharPos] = newLastCharByte;
|
||||||
|
|
||||||
if (lastCharPos + 1 < result.length) {
|
if (lastCharPos + 1 < result.length) {
|
||||||
@@ -238,7 +185,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply all domain patches using length-prefixed format
|
* Apply all domain patches using length-prefixed format
|
||||||
* This is the main patching method for variable-length domains
|
|
||||||
*/
|
*/
|
||||||
applyDomainPatches(data, domain, protocol = 'https://') {
|
applyDomainPatches(data, domain, protocol = 'https://') {
|
||||||
let result = Buffer.from(data);
|
let result = Buffer.from(data);
|
||||||
@@ -263,7 +209,7 @@ class ClientPatcher {
|
|||||||
totalCount += sentryResult.count;
|
totalCount += sentryResult.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Patch main domain (hytale.com -> mainDomain)
|
// 2. Patch main domain
|
||||||
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
||||||
const domainResult = this.replaceBytes(
|
const domainResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
@@ -298,16 +244,15 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
* Patch Discord invite URLs
|
||||||
*/
|
*/
|
||||||
patchDiscordUrl(data) {
|
patchDiscordUrl(data) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const result = Buffer.from(data);
|
const result = Buffer.from(data);
|
||||||
|
|
||||||
const oldUrl = '.gg/hytale';
|
const oldUrl = '.gg/hytale';
|
||||||
const newUrl = '.gg/MHkEjepMQ7';
|
const newUrl = '.gg/hf2pdc';
|
||||||
|
|
||||||
// Try length-prefixed format first
|
|
||||||
const lpResult = this.replaceBytes(
|
const lpResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(oldUrl),
|
this.stringToLengthPrefixed(oldUrl),
|
||||||
@@ -323,7 +268,6 @@ class ClientPatcher {
|
|||||||
const newUtf16 = this.stringToUtf16LE(newUrl);
|
const newUtf16 = this.stringToUtf16LE(newUrl);
|
||||||
|
|
||||||
const positions = this.findAllOccurrences(result, oldUtf16);
|
const positions = this.findAllOccurrences(result, oldUtf16);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
newUtf16.copy(result, pos);
|
newUtf16.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
@@ -333,39 +277,66 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the client binary has already been patched
|
* Check patch status of client binary
|
||||||
* Also verifies the binary actually contains the patched domain
|
|
||||||
*/
|
*/
|
||||||
isPatchedAlready(clientPath) {
|
getPatchStatus(clientPath) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
const patchFlagFile = clientPath + this.patchedFlag;
|
const patchFlagFile = clientPath + this.patchedFlag;
|
||||||
|
|
||||||
// First check flag file
|
|
||||||
if (fs.existsSync(patchFlagFile)) {
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
try {
|
try {
|
||||||
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
||||||
if (flagData.targetDomain === newDomain) {
|
const currentDomain = flagData.targetDomain;
|
||||||
// Verify the binary actually contains the patched domain
|
|
||||||
|
if (currentDomain === newDomain) {
|
||||||
const data = fs.readFileSync(clientPath);
|
const data = fs.readFileSync(clientPath);
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
const domainPattern = this.stringToLengthPrefixed(strategy.mainDomain);
|
const domainPattern = this.stringToLengthPrefixed(strategy.mainDomain);
|
||||||
|
|
||||||
if (data.includes(domainPattern)) {
|
if (data.includes(domainPattern)) {
|
||||||
return true;
|
return { patched: true, currentDomain, needsRestore: false };
|
||||||
} else {
|
} else {
|
||||||
console.log(' Flag exists but binary not patched (was updated?), re-patching...');
|
console.log(' Flag exists but binary not patched (was updated?), needs re-patching...');
|
||||||
return false;
|
return { patched: false, currentDomain: null, needsRestore: false };
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` Currently patched for "${currentDomain}", need to change to "${newDomain}"`);
|
||||||
|
return { patched: false, currentDomain, needsRestore: true };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Flag file corrupt or unreadable
|
// Flag file corrupt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { patched: false, currentDomain: null, needsRestore: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if client is already patched (backward compat)
|
||||||
|
*/
|
||||||
|
isPatchedAlready(clientPath) {
|
||||||
|
return this.getPatchStatus(clientPath).patched;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore client from backup
|
||||||
|
*/
|
||||||
|
restoreFromBackup(clientPath) {
|
||||||
|
const backupPath = clientPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
console.log(' Restoring original binary from backup for re-patching...');
|
||||||
|
fs.copyFileSync(backupPath, clientPath);
|
||||||
|
const patchFlagFile = clientPath + this.patchedFlag;
|
||||||
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
|
fs.unlinkSync(patchFlagFile);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
console.warn(' No backup found to restore - will try patching anyway');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the client as patched
|
* Mark client as patched
|
||||||
*/
|
*/
|
||||||
markAsPatched(clientPath) {
|
markAsPatched(clientPath) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
@@ -378,29 +349,28 @@ class ClientPatcher {
|
|||||||
patchMode: strategy.mode,
|
patchMode: strategy.mode,
|
||||||
mainDomain: strategy.mainDomain,
|
mainDomain: strategy.mainDomain,
|
||||||
subdomainPrefix: strategy.subdomainPrefix,
|
subdomainPrefix: strategy.subdomainPrefix,
|
||||||
patcherVersion: '2.0.0',
|
patcherVersion: '2.1.0',
|
||||||
verified: 'binary_contents'
|
verified: 'binary_contents'
|
||||||
};
|
};
|
||||||
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
|
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a backup of the original client binary
|
* Create backup of original client binary
|
||||||
*/
|
*/
|
||||||
backupClient(clientPath) {
|
backupClient(clientPath) {
|
||||||
const backupPath = clientPath + '.original';
|
const backupPath = clientPath + '.original';
|
||||||
|
try {
|
||||||
if (!fs.existsSync(backupPath)) {
|
if (!fs.existsSync(backupPath)) {
|
||||||
console.log(` Creating backup at ${path.basename(backupPath)}`);
|
console.log(` Creating backup at ${path.basename(backupPath)}`);
|
||||||
fs.copyFileSync(clientPath, backupPath);
|
fs.copyFileSync(clientPath, backupPath);
|
||||||
return backupPath;
|
return backupPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if current file differs from backup (might have been updated)
|
|
||||||
const currentSize = fs.statSync(clientPath).size;
|
const currentSize = fs.statSync(clientPath).size;
|
||||||
const backupSize = fs.statSync(backupPath).size;
|
const backupSize = fs.statSync(backupPath).size;
|
||||||
|
|
||||||
if (currentSize !== backupSize) {
|
if (currentSize !== backupSize) {
|
||||||
// File was updated, create timestamped backup of old backup
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
const oldBackupPath = `${clientPath}.original.${timestamp}`;
|
const oldBackupPath = `${clientPath}.original.${timestamp}`;
|
||||||
console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`);
|
console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`);
|
||||||
@@ -411,10 +381,14 @@ class ClientPatcher {
|
|||||||
|
|
||||||
console.log(' Backup already exists');
|
console.log(' Backup already exists');
|
||||||
return backupPath;
|
return backupPath;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` Failed to create backup: ${e.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the original client binary from backup
|
* Restore original client binary
|
||||||
*/
|
*/
|
||||||
restoreClient(clientPath) {
|
restoreClient(clientPath) {
|
||||||
const backupPath = clientPath + '.original';
|
const backupPath = clientPath + '.original';
|
||||||
@@ -433,15 +407,12 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the client binary to use the custom domain
|
* Patch the client binary to use the custom domain
|
||||||
* @param {string} clientPath - Path to the HytaleClient binary
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @returns {object} Result object with success status and details
|
|
||||||
*/
|
*/
|
||||||
async patchClient(clientPath, progressCallback) {
|
async patchClient(clientPath, progressCallback) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
|
|
||||||
console.log('=== Client Patcher v2.0 ===');
|
console.log('=== Client Patcher v2.1 ===');
|
||||||
console.log(`Target: ${clientPath}`);
|
console.log(`Target: ${clientPath}`);
|
||||||
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
||||||
console.log(`Mode: ${strategy.mode}`);
|
console.log(`Mode: ${strategy.mode}`);
|
||||||
@@ -456,32 +427,34 @@ class ClientPatcher {
|
|||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isPatchedAlready(clientPath)) {
|
const patchStatus = this.getPatchStatus(clientPath);
|
||||||
|
|
||||||
|
if (patchStatus.patched) {
|
||||||
console.log(`Client already patched for ${newDomain}, skipping`);
|
console.log(`Client already patched for ${newDomain}, skipping`);
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Client already patched', 100);
|
||||||
progressCallback('Client already patched', 100);
|
|
||||||
}
|
|
||||||
return { success: true, alreadyPatched: true, patchCount: 0 };
|
return { success: true, alreadyPatched: true, patchCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (patchStatus.needsRestore) {
|
||||||
progressCallback('Preparing to patch client...', 10);
|
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
|
||||||
|
this.restoreFromBackup(clientPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback) progressCallback('Preparing to patch client...', 10);
|
||||||
|
|
||||||
console.log('Creating backup...');
|
console.log('Creating backup...');
|
||||||
this.backupClient(clientPath);
|
const backupResult = this.backupClient(clientPath);
|
||||||
|
if (!backupResult) {
|
||||||
if (progressCallback) {
|
console.warn(' Could not create backup - proceeding without backup');
|
||||||
progressCallback('Reading client binary...', 20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback) progressCallback('Reading client binary...', 20);
|
||||||
|
|
||||||
console.log('Reading client binary...');
|
console.log('Reading client binary...');
|
||||||
const data = fs.readFileSync(clientPath);
|
const data = fs.readFileSync(clientPath);
|
||||||
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching domain references...', 50);
|
||||||
progressCallback('Patching domain references...', 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Applying domain patches (length-prefixed format)...');
|
console.log('Applying domain patches (length-prefixed format)...');
|
||||||
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
||||||
@@ -492,7 +465,6 @@ class ClientPatcher {
|
|||||||
if (count === 0 && discordCount === 0) {
|
if (count === 0 && discordCount === 0) {
|
||||||
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
||||||
|
|
||||||
// Fallback to legacy patching for older binary formats
|
|
||||||
const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
||||||
if (legacyResult.count > 0) {
|
if (legacyResult.count > 0) {
|
||||||
console.log(`Found ${legacyResult.count} occurrences with legacy format`);
|
console.log(`Found ${legacyResult.count} occurrences with legacy format`);
|
||||||
@@ -505,18 +477,14 @@ class ClientPatcher {
|
|||||||
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Writing patched binary...', 80);
|
||||||
progressCallback('Writing patched binary...', 80);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Writing patched binary...');
|
console.log('Writing patched binary...');
|
||||||
fs.writeFileSync(clientPath, finalData);
|
fs.writeFileSync(clientPath, finalData);
|
||||||
|
|
||||||
this.markAsPatched(clientPath);
|
this.markAsPatched(clientPath);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching complete', 100);
|
||||||
progressCallback('Patching complete', 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
|
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
|
||||||
console.log('=== Patching Complete ===');
|
console.log('=== Patching Complete ===');
|
||||||
@@ -525,17 +493,47 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the server JAR by downloading pre-patched version
|
* Check if server JAR contains DualAuth classes (was patched)
|
||||||
* @param {string} serverPath - Path to the HytaleServer.jar
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @param {string} javaPath - Path to Java executable (unused, kept for compatibility)
|
|
||||||
* @returns {object} Result object with success status and details
|
|
||||||
*/
|
*/
|
||||||
async patchServer(serverPath, progressCallback, javaPath = null) {
|
serverJarContainsDualAuth(serverPath) {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(serverPath);
|
||||||
|
// Check for DualAuthContext class signature in JAR
|
||||||
|
const signature = Buffer.from('DualAuthContext', 'utf8');
|
||||||
|
return data.includes(signature);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate downloaded file is not corrupt/partial
|
||||||
|
* Server JAR should be at least 50MB
|
||||||
|
*/
|
||||||
|
validateServerJarSize(serverPath) {
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(serverPath);
|
||||||
|
const minSize = 50 * 1024 * 1024; // 50MB minimum
|
||||||
|
if (stats.size < minSize) {
|
||||||
|
console.error(` Downloaded JAR too small: ${(stats.size / 1024 / 1024).toFixed(2)} MB (expected >50MB)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(` Downloaded size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch server JAR by downloading pre-patched version from CDN
|
||||||
|
*/
|
||||||
|
async patchServer(serverPath, progressCallback, branch = 'release') {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
|
|
||||||
console.log('=== Server Patcher TEMP SYSTEM NEED TO BE FIXED ===');
|
console.log('=== Server Patcher (Pre-patched Download) ===');
|
||||||
console.log(`Target: ${serverPath}`);
|
console.log(`Target: ${serverPath}`);
|
||||||
|
console.log(`Branch: ${branch}`);
|
||||||
console.log(`Domain: ${newDomain}`);
|
console.log(`Domain: ${newDomain}`);
|
||||||
|
|
||||||
if (!fs.existsSync(serverPath)) {
|
if (!fs.existsSync(serverPath)) {
|
||||||
@@ -546,61 +544,91 @@ class ClientPatcher {
|
|||||||
|
|
||||||
// Check if already patched
|
// Check if already patched
|
||||||
const patchFlagFile = serverPath + '.dualauth_patched';
|
const patchFlagFile = serverPath + '.dualauth_patched';
|
||||||
|
let needsRestore = false;
|
||||||
|
|
||||||
if (fs.existsSync(patchFlagFile)) {
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
try {
|
try {
|
||||||
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
||||||
if (flagData.domain === newDomain) {
|
if (flagData.domain === newDomain && flagData.branch === branch) {
|
||||||
console.log(`Server already patched for ${newDomain}, skipping`);
|
// Verify JAR actually contains DualAuth classes (game may have auto-updated)
|
||||||
|
if (this.serverJarContainsDualAuth(serverPath)) {
|
||||||
|
console.log(`Server already patched for ${newDomain} (${branch}), skipping`);
|
||||||
if (progressCallback) progressCallback('Server already patched', 100);
|
if (progressCallback) progressCallback('Server already patched', 100);
|
||||||
return { success: true, alreadyPatched: true };
|
return { success: true, alreadyPatched: true };
|
||||||
|
} else {
|
||||||
|
console.log(' Flag exists but JAR not patched (was auto-updated?), will re-download...');
|
||||||
|
// Delete stale flag file
|
||||||
|
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Server patched for "${flagData.domain}" (${flagData.branch}), need to change to "${newDomain}" (${branch})`);
|
||||||
|
needsRestore = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Flag file corrupt, re-patch
|
// Flag file corrupt, re-patch
|
||||||
|
console.log(' Flag file corrupt, will re-download');
|
||||||
|
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore backup if patched for different domain
|
||||||
|
if (needsRestore) {
|
||||||
|
const backupPath = serverPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
|
||||||
|
console.log('Restoring original JAR from backup for re-patching...');
|
||||||
|
fs.copyFileSync(backupPath, serverPath);
|
||||||
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
|
fs.unlinkSync(patchFlagFile);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(' No backup found to restore - will download fresh patched JAR');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create backup
|
// Create backup
|
||||||
if (progressCallback) progressCallback('Creating backup...', 10);
|
if (progressCallback) progressCallback('Creating backup...', 10);
|
||||||
console.log('Creating backup...');
|
console.log('Creating backup...');
|
||||||
this.backupClient(serverPath);
|
const backupResult = this.backupClient(serverPath);
|
||||||
|
if (!backupResult) {
|
||||||
|
console.warn(' Could not create backup - proceeding without backup');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only support standard domain (auth.sanasol.ws) via pre-patched download
|
||||||
|
if (newDomain !== 'auth.sanasol.ws' && newDomain !== 'sanasol.ws') {
|
||||||
|
console.error(`Domain "${newDomain}" requires DualAuthPatcher - only auth.sanasol.ws is supported via pre-patched download`);
|
||||||
|
return { success: false, error: `Unsupported domain: ${newDomain}. Only auth.sanasol.ws is supported.` };
|
||||||
|
}
|
||||||
|
|
||||||
// Download pre-patched JAR
|
// Download pre-patched JAR
|
||||||
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
|
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
|
||||||
console.log('Downloading pre-patched HytaleServer.jar');
|
console.log('Downloading pre-patched HytaleServer.jar...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
|
|
||||||
|
// Use different URL for pre-release vs release
|
||||||
|
let url;
|
||||||
|
if (branch === 'pre-release') {
|
||||||
|
url = 'https://patcher.authbp.xyz/download/patched_prerelease';
|
||||||
|
console.log(' Using pre-release patched server from:', url);
|
||||||
|
} else {
|
||||||
|
url = 'https://patcher.authbp.xyz/download/patched_release';
|
||||||
|
console.log(' Using release patched server from:', url);
|
||||||
|
}
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
https.get(url, (response) => {
|
const handleResponse = (response) => {
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||||
// Follow redirect
|
https.get(response.headers.location, handleResponse).on('error', reject);
|
||||||
https.get(response.headers.location, (redirectResponse) => {
|
|
||||||
if (redirectResponse.statusCode !== 200) {
|
|
||||||
reject(new Error(`Failed to download: HTTP ${redirectResponse.statusCode}`));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = fs.createWriteStream(serverPath);
|
if (response.statusCode !== 200) {
|
||||||
const totalSize = parseInt(redirectResponse.headers['content-length'], 10);
|
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
||||||
let downloaded = 0;
|
return;
|
||||||
|
|
||||||
redirectResponse.on('data', (chunk) => {
|
|
||||||
downloaded += chunk.length;
|
|
||||||
if (progressCallback && totalSize) {
|
|
||||||
const percent = 30 + Math.floor((downloaded / totalSize) * 60);
|
|
||||||
progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
redirectResponse.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}).on('error', reject);
|
|
||||||
} else if (response.statusCode === 200) {
|
|
||||||
const file = fs.createWriteStream(serverPath);
|
const file = fs.createWriteStream(serverPath);
|
||||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
@@ -618,10 +646,9 @@ class ClientPatcher {
|
|||||||
file.close();
|
file.close();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
} else {
|
};
|
||||||
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
|
||||||
}
|
https.get(url, handleResponse).on('error', (err) => {
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(serverPath, () => {});
|
fs.unlink(serverPath, () => {});
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
@@ -629,12 +656,47 @@ class ClientPatcher {
|
|||||||
|
|
||||||
console.log(' Download successful');
|
console.log(' Download successful');
|
||||||
|
|
||||||
|
// Verify downloaded JAR size and contents
|
||||||
|
if (progressCallback) progressCallback('Verifying downloaded JAR...', 95);
|
||||||
|
|
||||||
|
if (!this.validateServerJarSize(serverPath)) {
|
||||||
|
console.error('Downloaded JAR appears corrupt or incomplete');
|
||||||
|
|
||||||
|
// Restore backup on verification failure
|
||||||
|
const backupPath = serverPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
fs.copyFileSync(backupPath, serverPath);
|
||||||
|
console.log('Restored backup after verification failure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: 'Downloaded JAR verification failed - file too small (corrupt/partial download)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.serverJarContainsDualAuth(serverPath)) {
|
||||||
|
console.error('Downloaded JAR does not contain DualAuth classes - invalid or corrupt download');
|
||||||
|
|
||||||
|
// Restore backup on verification failure
|
||||||
|
const backupPath = serverPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
fs.copyFileSync(backupPath, serverPath);
|
||||||
|
console.log('Restored backup after verification failure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: 'Downloaded JAR verification failed - missing DualAuth classes' };
|
||||||
|
}
|
||||||
|
console.log(' Verification successful - DualAuth classes present');
|
||||||
|
|
||||||
// Mark as patched
|
// Mark as patched
|
||||||
|
const sourceUrl = branch === 'pre-release'
|
||||||
|
? 'https://patcher.authbp.xyz/download/patched_prerelease'
|
||||||
|
: 'https://patcher.authbp.xyz/download/patched_release';
|
||||||
|
|
||||||
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
||||||
domain: newDomain,
|
domain: newDomain,
|
||||||
|
branch: branch,
|
||||||
patchedAt: new Date().toISOString(),
|
patchedAt: new Date().toISOString(),
|
||||||
patcher: 'PrePatchedDownload',
|
patcher: 'PrePatchedDownload',
|
||||||
source: 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar'
|
source: sourceUrl
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Server patching complete', 100);
|
if (progressCallback) progressCallback('Server patching complete', 100);
|
||||||
@@ -656,290 +718,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find Java executable - uses bundled JRE first (same as game uses)
|
* Find client binary path based on platform
|
||||||
* Falls back to system Java if bundled not available
|
|
||||||
*/
|
|
||||||
findJava() {
|
|
||||||
// 1. Try bundled JRE first (comes with the game)
|
|
||||||
try {
|
|
||||||
const bundled = getBundledJavaPath(JRE_DIR);
|
|
||||||
if (bundled && fs.existsSync(bundled)) {
|
|
||||||
console.log(`Using bundled Java: ${bundled}`);
|
|
||||||
return bundled;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Bundled not available
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Try javaManager's getJavaExec (handles all fallbacks)
|
|
||||||
try {
|
|
||||||
const javaExec = getJavaExec(JRE_DIR);
|
|
||||||
if (javaExec && fs.existsSync(javaExec)) {
|
|
||||||
console.log(`Using Java from javaManager: ${javaExec}`);
|
|
||||||
return javaExec;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Not available
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check JAVA_HOME
|
|
||||||
if (process.env.JAVA_HOME) {
|
|
||||||
const javaHome = process.env.JAVA_HOME;
|
|
||||||
const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
|
|
||||||
if (fs.existsSync(javaBin)) {
|
|
||||||
console.log(`Using Java from JAVA_HOME: ${javaBin}`);
|
|
||||||
return javaBin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Try 'java' from PATH
|
|
||||||
try {
|
|
||||||
execSync('java -version 2>&1', { encoding: 'utf8' });
|
|
||||||
console.log('Using Java from PATH');
|
|
||||||
return 'java';
|
|
||||||
} catch (e) {
|
|
||||||
// Not in PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download DualAuthPatcher from hytale-auth-server if not present
|
|
||||||
*/
|
|
||||||
async ensurePatcherDownloaded(patcherDir) {
|
|
||||||
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
|
||||||
const patcherUrl = 'https://raw.githubusercontent.com/sanasol/hytale-auth-server/master/patcher/DualAuthPatcher.java';
|
|
||||||
|
|
||||||
if (!fs.existsSync(patcherDir)) {
|
|
||||||
fs.mkdirSync(patcherDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(patcherJava)) {
|
|
||||||
console.log('Downloading DualAuthPatcher from hytale-auth-server...');
|
|
||||||
try {
|
|
||||||
const https = require('https');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const file = fs.createWriteStream(patcherJava);
|
|
||||||
https.get(patcherUrl, (response) => {
|
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
||||||
// Follow redirect
|
|
||||||
https.get(response.headers.location, (redirectResponse) => {
|
|
||||||
redirectResponse.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}).on('error', reject);
|
|
||||||
} else {
|
|
||||||
response.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(patcherJava, () => {});
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log(' Downloaded DualAuthPatcher.java');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(` Failed to download DualAuthPatcher: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download ASM libraries if not present
|
|
||||||
*/
|
|
||||||
async ensureAsmLibraries(libDir) {
|
|
||||||
if (!fs.existsSync(libDir)) {
|
|
||||||
fs.mkdirSync(libDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const libs = [
|
|
||||||
{ name: 'asm-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar' },
|
|
||||||
{ name: 'asm-tree-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar' },
|
|
||||||
{ name: 'asm-util-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-util/9.6/asm-util-9.6.jar' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const lib of libs) {
|
|
||||||
const libPath = path.join(libDir, lib.name);
|
|
||||||
if (!fs.existsSync(libPath)) {
|
|
||||||
console.log(`Downloading ${lib.name}...`);
|
|
||||||
try {
|
|
||||||
const https = require('https');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const file = fs.createWriteStream(libPath);
|
|
||||||
https.get(lib.url, (response) => {
|
|
||||||
response.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(libPath, () => {});
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log(` Downloaded ${lib.name}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(` Failed to download ${lib.name}: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile DualAuthPatcher if needed
|
|
||||||
*/
|
|
||||||
async compileDualAuthPatcher(java, patcherDir, libDir) {
|
|
||||||
const patcherClass = path.join(patcherDir, 'DualAuthPatcher.class');
|
|
||||||
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
|
||||||
|
|
||||||
// Check if already compiled and up to date
|
|
||||||
if (fs.existsSync(patcherClass)) {
|
|
||||||
const classTime = fs.statSync(patcherClass).mtime;
|
|
||||||
const javaTime = fs.statSync(patcherJava).mtime;
|
|
||||||
if (classTime > javaTime) {
|
|
||||||
console.log('DualAuthPatcher already compiled');
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Compiling DualAuthPatcher...');
|
|
||||||
|
|
||||||
const javac = java.replace(/java(\.exe)?$/, 'javac$1');
|
|
||||||
const classpath = [
|
|
||||||
path.join(libDir, 'asm-9.6.jar'),
|
|
||||||
path.join(libDir, 'asm-tree-9.6.jar'),
|
|
||||||
path.join(libDir, 'asm-util-9.6.jar')
|
|
||||||
].join(process.platform === 'win32' ? ';' : ':');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fix PATH for packaged Electron apps on Windows
|
|
||||||
const execOptions = {
|
|
||||||
stdio: 'pipe',
|
|
||||||
cwd: patcherDir,
|
|
||||||
env: { ...process.env }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add system32 to PATH for Windows to find cmd.exe
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
const systemRoot = process.env.SystemRoot || 'C:\\WINDOWS';
|
|
||||||
const systemPath = `${systemRoot}\\system32;${systemRoot};${systemRoot}\\System32\\Wbem`;
|
|
||||||
execOptions.env.PATH = execOptions.env.PATH
|
|
||||||
? `${systemPath};${execOptions.env.PATH}`
|
|
||||||
: systemPath;
|
|
||||||
execOptions.shell = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, execOptions);
|
|
||||||
console.log(' Compilation successful');
|
|
||||||
return { success: true };
|
|
||||||
} catch (e) {
|
|
||||||
const error = `Failed to compile DualAuthPatcher: ${e.message}`;
|
|
||||||
console.error(error);
|
|
||||||
if (e.stderr) console.error(e.stderr.toString());
|
|
||||||
return { success: false, error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run DualAuthPatcher on the server JAR
|
|
||||||
*/
|
|
||||||
async runDualAuthPatcher(java, classpath, serverPath, domain) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const args = ['-cp', classpath, 'DualAuthPatcher', serverPath];
|
|
||||||
const env = { ...process.env, HYTALE_AUTH_DOMAIN: domain };
|
|
||||||
|
|
||||||
console.log(`Running: java ${args.join(' ')}`);
|
|
||||||
console.log(` HYTALE_AUTH_DOMAIN=${domain}`);
|
|
||||||
|
|
||||||
const proc = spawn(java, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
||||||
|
|
||||||
let stdout = '';
|
|
||||||
let stderr = '';
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data) => {
|
|
||||||
const str = data.toString();
|
|
||||||
stdout += str;
|
|
||||||
console.log(str.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr.on('data', (data) => {
|
|
||||||
const str = data.toString();
|
|
||||||
stderr += str;
|
|
||||||
console.error(str.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve({ success: true, stdout });
|
|
||||||
} else {
|
|
||||||
resolve({ success: false, error: `Patcher exited with code ${code}: ${stderr}` });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('error', (err) => {
|
|
||||||
resolve({ success: false, error: `Failed to run patcher: ${err.message}` });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy server patcher (simple domain replacement, no dual auth)
|
|
||||||
* Use patchServer() for full dual auth support
|
|
||||||
*/
|
|
||||||
async patchServerLegacy(serverPath, progressCallback) {
|
|
||||||
const newDomain = this.getNewDomain();
|
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
|
||||||
|
|
||||||
console.log('=== Legacy Server Patcher ===');
|
|
||||||
console.log(`Target: ${serverPath}`);
|
|
||||||
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
|
||||||
|
|
||||||
if (!fs.existsSync(serverPath)) {
|
|
||||||
return { success: false, error: `Server JAR not found: ${serverPath}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Patching server...', 20);
|
|
||||||
|
|
||||||
console.log('Opening server JAR...');
|
|
||||||
const zip = new AdmZip(serverPath);
|
|
||||||
const entries = zip.getEntries();
|
|
||||||
|
|
||||||
let totalCount = 0;
|
|
||||||
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const name = entry.entryName;
|
|
||||||
if (name.endsWith('.class') || name.endsWith('.properties') ||
|
|
||||||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
|
|
||||||
const data = entry.getData();
|
|
||||||
if (data.includes(oldUtf8)) {
|
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
|
||||||
if (count > 0) {
|
|
||||||
zip.updateFile(entry.entryName, patchedData);
|
|
||||||
totalCount += count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalCount > 0) {
|
|
||||||
zip.writeZip(serverPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Complete', 100);
|
|
||||||
return { success: true, patchCount: totalCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the client binary path based on platform
|
|
||||||
*/
|
*/
|
||||||
findClientPath(gameDir) {
|
findClientPath(gameDir) {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
@@ -961,7 +740,9 @@ class ClientPatcher {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find server JAR path
|
||||||
|
*/
|
||||||
findServerPath(gameDir) {
|
findServerPath(gameDir) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
||||||
@@ -978,11 +759,8 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure both client and server are patched before launching
|
* Ensure both client and server are patched before launching
|
||||||
* @param {string} gameDir - Path to the game directory
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @param {string} javaPath - Optional path to Java executable for server patching
|
|
||||||
*/
|
*/
|
||||||
async ensureClientPatched(gameDir, progressCallback, javaPath = null) {
|
async ensureClientPatched(gameDir, progressCallback, javaPath = null, branch = 'release') {
|
||||||
const results = {
|
const results = {
|
||||||
client: null,
|
client: null,
|
||||||
server: null,
|
server: null,
|
||||||
@@ -991,9 +769,7 @@ class ClientPatcher {
|
|||||||
|
|
||||||
const clientPath = this.findClientPath(gameDir);
|
const clientPath = this.findClientPath(gameDir);
|
||||||
if (clientPath) {
|
if (clientPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching client binary...', 10);
|
||||||
progressCallback('Patching client binary...', 10);
|
|
||||||
}
|
|
||||||
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
||||||
@@ -1006,14 +782,12 @@ class ClientPatcher {
|
|||||||
|
|
||||||
const serverPath = this.findServerPath(gameDir);
|
const serverPath = this.findServerPath(gameDir);
|
||||||
if (serverPath) {
|
if (serverPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching server JAR...', 50);
|
||||||
progressCallback('Patching server JAR...', 50);
|
|
||||||
}
|
|
||||||
results.server = await this.patchServer(serverPath, (msg, pct) => {
|
results.server = await this.patchServer(serverPath, (msg, pct) => {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
||||||
}
|
}
|
||||||
}, javaPath);
|
}, branch);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Could not find HytaleServer.jar');
|
console.warn('Could not find HytaleServer.jar');
|
||||||
results.server = { success: false, error: 'Server JAR not found' };
|
results.server = { success: false, error: 'Server JAR not found' };
|
||||||
@@ -1023,9 +797,7 @@ class ClientPatcher {
|
|||||||
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
|
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
|
||||||
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
|
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching complete', 100);
|
||||||
progressCallback('Patching complete', 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ function isWaylandSession() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sessionType = process.env.XDG_SESSION_TYPE;
|
const sessionType = process.env.XDG_SESSION_TYPE;
|
||||||
|
const waylandDisplay = process.env.WAYLAND_DISPLAY;
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log(`[PlatformUtils] Checking Wayland: XDG_SESSION_TYPE=${sessionType}, WAYLAND_DISPLAY=${waylandDisplay}`);
|
||||||
|
|
||||||
if (sessionType && sessionType.toLowerCase() === 'wayland') {
|
if (sessionType && sessionType.toLowerCase() === 'wayland') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.WAYLAND_DISPLAY) {
|
if (waylandDisplay) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,18 +50,47 @@ function setupWaylandEnvironment() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user has manually set SDL_VIDEODRIVER (e.g. to 'x11'), strictly respect it.
|
||||||
|
if (process.env.SDL_VIDEODRIVER) {
|
||||||
|
console.log(`User manually set SDL_VIDEODRIVER=${process.env.SDL_VIDEODRIVER}, ignoring internal Wayland configuration.`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (!isWaylandSession()) {
|
if (!isWaylandSession()) {
|
||||||
console.log('Detected X11 session, using default environment');
|
console.log('Detected X11 session, using default environment');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Detected Wayland session, configuring environment...');
|
console.log('Detected Wayland session, checking for Gamescope/Steam Deck...');
|
||||||
|
|
||||||
const envVars = {
|
const envVars = {};
|
||||||
SDL_VIDEODRIVER: 'wayland'
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Only set Ozone hint if not already set by user
|
||||||
|
if (!process.env.ELECTRON_OZONE_PLATFORM_HINT) {
|
||||||
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
|
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. DETECT GAMESCOPE / STEAM DECK
|
||||||
|
// Native Wayland often fails for SDL games in Gaming Mode (gamescope), so we force X11 (XWayland).
|
||||||
|
// Checks:
|
||||||
|
// - XDG_CURRENT_DESKTOP == 'gamescope'
|
||||||
|
// - SteamDeck=1 (often set in SteamOS)
|
||||||
|
const currentDesktop = process.env.XDG_CURRENT_DESKTOP || '';
|
||||||
|
const isGamescope = currentDesktop.toLowerCase() === 'gamescope' || process.env.SteamDeck === '1';
|
||||||
|
|
||||||
|
if (isGamescope) {
|
||||||
|
console.log('Gamescope / Steam Deck detected, forcing SDL_VIDEODRIVER=x11 for compatibility');
|
||||||
|
envVars.SDL_VIDEODRIVER = 'x11';
|
||||||
|
} else {
|
||||||
|
// For standard desktop Wayland (GNOME, KDE), we leave SDL_VIDEODRIVER unset.
|
||||||
|
// This allows SDL3/SDL2 to use its internal preference (Wayland > X11).
|
||||||
|
// EXCEPT if it was somehow force-set to 'wayland' by the parent process (rare but possible),
|
||||||
|
// we strictly want to allow fallback, so we might unset it if it was 'wayland'.
|
||||||
|
// But since we checked process.env.SDL_VIDEODRIVER at the start, we know it's NOT set manually.
|
||||||
|
|
||||||
|
// So we effectively do nothing for standard Wayland, letting SDL decide.
|
||||||
|
console.log('Standard Wayland session detected, letting SDL decide backend (auto-fallback enabled).');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Wayland environment variables:', envVars);
|
console.log('Wayland environment variables:', envVars);
|
||||||
return envVars;
|
return envVars;
|
||||||
|
|||||||
120
backend/utils/serverListSync.js
Normal file
120
backend/utils/serverListSync.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const axios = require('axios');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const { getHytaleSavesDir } = require('../core/paths');
|
||||||
|
|
||||||
|
const SERVER_LIST_URL = 'https://assets.authbp.xyz/server.json';
|
||||||
|
|
||||||
|
|
||||||
|
function getLocalDateTime() {
|
||||||
|
return formatLocalDateTime(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLocalDateTime(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
|
||||||
|
const offsetMinutes = -date.getTimezoneOffset();
|
||||||
|
const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
|
||||||
|
const offsetMins = Math.abs(offsetMinutes) % 60;
|
||||||
|
const offsetSign = offsetMinutes >= 0 ? '+' : '-';
|
||||||
|
const offset = `${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(offsetMins).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}0000${offset}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncServerList() {
|
||||||
|
try {
|
||||||
|
const hytaleSavesDir = getHytaleSavesDir();
|
||||||
|
const serverListPath = path.join(hytaleSavesDir, 'ServerList.json');
|
||||||
|
console.log('[ServerListSync] Fetching server list from', SERVER_LIST_URL);
|
||||||
|
let remoteData;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(SERVER_LIST_URL, {
|
||||||
|
timeout: 40000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Hytale-F2P-Launcher'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
remoteData = response.data;
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.warn('[ServerListSync] Failed to fetch remote server list:', fetchError.message);
|
||||||
|
remoteData = { SavedServers: [] };
|
||||||
|
}
|
||||||
|
let localData = { SavedServers: [] };
|
||||||
|
if (fs.existsSync(serverListPath)) {
|
||||||
|
try {
|
||||||
|
const localContent = fs.readFileSync(serverListPath, 'utf-8');
|
||||||
|
localData = JSON.parse(localContent);
|
||||||
|
console.log('[ServerListSync] Loaded existing local server list with', localData.SavedServers?.length || 0, 'servers');
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('[ServerListSync] Failed to parse local server list, creating new one:', parseError.message);
|
||||||
|
localData = { SavedServers: [] };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[ServerListSync] Local server list does not exist, creating new one');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localData.SavedServers) {
|
||||||
|
localData.SavedServers = [];
|
||||||
|
}
|
||||||
|
if (!remoteData.SavedServers) {
|
||||||
|
remoteData.SavedServers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingServersByAddress = new Map();
|
||||||
|
const userServers = [];
|
||||||
|
|
||||||
|
for (const server of localData.SavedServers) {
|
||||||
|
existingServersByAddress.set(server.Address.toLowerCase(), server);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteAddresses = new Set(remoteData.SavedServers.map(s => s.Address.toLowerCase()));
|
||||||
|
for (const server of localData.SavedServers) {
|
||||||
|
if (!remoteAddresses.has(server.Address.toLowerCase())) {
|
||||||
|
userServers.push(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = getLocalDateTime();
|
||||||
|
|
||||||
|
|
||||||
|
const apiServers = [];
|
||||||
|
for (const remoteServer of remoteData.SavedServers) {
|
||||||
|
const serverToAdd = {
|
||||||
|
Id: uuidv4(),
|
||||||
|
Name: "@ " + remoteServer.Name,
|
||||||
|
Address: remoteServer.Address,
|
||||||
|
DateSaved: currentDate,
|
||||||
|
img_Banner: remoteServer.img_Banner || null // Copy banner if exists
|
||||||
|
};
|
||||||
|
apiServers.push(serverToAdd);
|
||||||
|
console.log('[ServerListSync] Added/Updated server with new ID:', remoteServer.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
localData.SavedServers = [...apiServers, ...userServers];
|
||||||
|
|
||||||
|
const addedCount = apiServers.length;
|
||||||
|
|
||||||
|
if (!fs.existsSync(hytaleSavesDir)) {
|
||||||
|
fs.mkdirSync(hytaleSavesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(serverListPath, JSON.stringify(localData, null, 2), 'utf-8');
|
||||||
|
console.log('[ServerListSync] Server list synchronized:', addedCount, 'API servers added, total:', localData.SavedServers.length);
|
||||||
|
|
||||||
|
return { success: true, added: addedCount, total: localData.SavedServers.length };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ServerListSync] Failed to synchronize server list:', error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
syncServerList
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths');
|
|||||||
const { loadConfig, saveConfig } = require('../core/config');
|
const { loadConfig, saveConfig } = require('../core/config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location
|
* NEW SYSTEM (2.2.0+): UserData Migration to Centralized Location
|
||||||
*
|
*
|
||||||
* UserData is now stored in a centralized location instead of inside game installation:
|
* UserData is now stored in a centralized location instead of inside game installation:
|
||||||
* - Windows: %LOCALAPPDATA%\HytaleSaves\
|
* - Windows: %LOCALAPPDATA%\HytaleSaves\
|
||||||
@@ -31,7 +31,7 @@ function markMigrationCompleted() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find old UserData location (pre-2.1.2)
|
* Find old UserData location (pre-2.2.0)
|
||||||
* Searches in: installPath/branch/package/game/latest/Client/UserData
|
* Searches in: installPath/branch/package/game/latest/Client/UserData
|
||||||
*/
|
*/
|
||||||
function findOldUserDataPath() {
|
function findOldUserDataPath() {
|
||||||
@@ -77,7 +77,7 @@ function findOldUserDataPath() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate UserData from old location to new centralized location
|
* Migrate UserData from old location to new centralized location
|
||||||
* One-time operation when upgrading to 2.1.2
|
* One-time operation when upgrading to 2.2.0
|
||||||
*/
|
*/
|
||||||
async function migrateUserDataToCentralized() {
|
async function migrateUserDataToCentralized() {
|
||||||
// Check if already migrated
|
// Check if already migrated
|
||||||
@@ -149,7 +149,7 @@ async function migrateUserDataToCentralized() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the centralized UserData path (always use this in 2.1.2+)
|
* Get the centralized UserData path (always use this in 2.2.0+)
|
||||||
* Ensures directory exists
|
* Ensures directory exists
|
||||||
*/
|
*/
|
||||||
function getUserDataPath() {
|
function getUserDataPath() {
|
||||||
|
|||||||
123
docs/STEAMDECK_CRASH_INVESTIGATION.md
Normal file
123
docs/STEAMDECK_CRASH_INVESTIGATION.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Steam Deck / Ubuntu LTS Crash Investigation
|
||||||
|
|
||||||
|
## Status: SOLVED
|
||||||
|
|
||||||
|
**Last updated:** 2026-01-27
|
||||||
|
|
||||||
|
**Solution:** Replace bundled `libzstd.so` with system version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Summary
|
||||||
|
|
||||||
|
The Hytale F2P launcher's client patcher causes crashes on Steam Deck and Ubuntu LTS with the error:
|
||||||
|
```
|
||||||
|
free(): invalid pointer
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```
|
||||||
|
SIGSEGV (Segmentation fault)
|
||||||
|
```
|
||||||
|
|
||||||
|
The crash occurs after successful authentication, specifically right after "Finished handling RequiredAssets".
|
||||||
|
|
||||||
|
**Affected Systems:**
|
||||||
|
- Steam Deck (glibc 2.41)
|
||||||
|
- Ubuntu LTS
|
||||||
|
|
||||||
|
**Working Systems:**
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Older Arch Linux (glibc < 2.41)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
The **bundled `libzstd.so`** in the game client is incompatible with glibc 2.41's stricter heap validation. When the game decompresses assets using this library, it triggers heap corruption detected by glibc 2.41.
|
||||||
|
|
||||||
|
The crash occurs in `libzstd.so` during `free()` after "Finished handling RequiredAssets" (asset decompression).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Replace the bundled `libzstd.so` with the system's `libzstd.so.1`.
|
||||||
|
|
||||||
|
### Automatic (Launcher)
|
||||||
|
|
||||||
|
The launcher automatically detects and replaces `libzstd.so` on Linux systems. No manual action needed.
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
|
||||||
|
# Backup bundled version
|
||||||
|
mv libzstd.so libzstd.so.bundled
|
||||||
|
|
||||||
|
# Link to system version
|
||||||
|
# Steam Deck / Arch Linux:
|
||||||
|
ln -s /usr/lib/libzstd.so.1 libzstd.so
|
||||||
|
|
||||||
|
# Debian / Ubuntu:
|
||||||
|
ln -s /usr/lib/x86_64-linux-gnu/libzstd.so.1 libzstd.so
|
||||||
|
|
||||||
|
# Fedora / RHEL:
|
||||||
|
ln -s /usr/lib64/libzstd.so.1 libzstd.so
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Original
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
rm libzstd.so
|
||||||
|
mv libzstd.so.bundled libzstd.so
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why This Works
|
||||||
|
|
||||||
|
1. The bundled `libzstd.so` was likely compiled with different allocator settings or an older toolchain
|
||||||
|
2. glibc 2.41 has stricter heap validation that catches invalid memory operations
|
||||||
|
3. The system `libzstd.so.1` is compiled with the system's glibc and uses compatible memory allocation patterns
|
||||||
|
4. By using the system library, we avoid the incompatibility entirely
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previous Investigation (for reference)
|
||||||
|
|
||||||
|
### What Was Tried Before Finding Solution
|
||||||
|
|
||||||
|
| Approach | Result |
|
||||||
|
|----------|--------|
|
||||||
|
| jemalloc allocator | Worked ~30% of time, not stable |
|
||||||
|
| GLIBC_TUNABLES | No effect |
|
||||||
|
| taskset (CPU pinning) | Single core too slow |
|
||||||
|
| nice/chrt (scheduling) | No effect |
|
||||||
|
| Various patching approaches | All crashed |
|
||||||
|
|
||||||
|
### Key Insight
|
||||||
|
|
||||||
|
The crash was in `libzstd.so`, not in our patched code. The patching just changed timing enough to expose the libzstd incompatibility more frequently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GDB Stack Trace (Historical)
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 0x00007ffff7d3f5a4 in ?? () from /usr/lib/libc.so.6
|
||||||
|
#1 raise () from /usr/lib/libc.so.6
|
||||||
|
#2 abort () from /usr/lib/libc.so.6
|
||||||
|
#3-#4 ?? () from /usr/lib/libc.so.6
|
||||||
|
#5 free () from /usr/lib/libc.so.6
|
||||||
|
#6 ?? () from libzstd.so <-- CRASH POINT (bundled library)
|
||||||
|
#7-#24 HytaleClient code (asset decompression)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branch
|
||||||
|
|
||||||
|
`fix/steamdeck-libzstd`
|
||||||
65
docs/STEAMDECK_DEBUG_COMMANDS.md
Normal file
65
docs/STEAMDECK_DEBUG_COMMANDS.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Steam Deck / Linux Crash Fix
|
||||||
|
|
||||||
|
## SOLUTION: Use system libzstd
|
||||||
|
|
||||||
|
The crash is caused by the bundled `libzstd.so` being incompatible with glibc 2.41's stricter heap validation.
|
||||||
|
|
||||||
|
### Automatic Fix
|
||||||
|
|
||||||
|
The launcher automatically replaces `libzstd.so` with the system version. No manual action needed.
|
||||||
|
|
||||||
|
### Manual Fix
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
|
||||||
|
# Backup and replace
|
||||||
|
mv libzstd.so libzstd.so.bundled
|
||||||
|
ln -s /usr/lib/libzstd.so.1 libzstd.so
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Original
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
rm libzstd.so
|
||||||
|
mv libzstd.so.bundled libzstd.so
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Commands (for troubleshooting)
|
||||||
|
|
||||||
|
### Check libzstd Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if symlinked
|
||||||
|
ls -la ~/.hytalef2p/release/package/game/latest/Client/libzstd.so
|
||||||
|
|
||||||
|
# Find system libzstd
|
||||||
|
find /usr/lib -name "libzstd.so*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
file ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
|
||||||
|
ldd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Client Binary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest/Client
|
||||||
|
cp HytaleClient.original HytaleClient
|
||||||
|
rm -f HytaleClient.patched_custom
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `HYTALE_AUTH_DOMAIN` | Custom auth domain | `auth.sanasol.ws` |
|
||||||
|
| `HYTALE_NO_LIBZSTD_FIX` | Disable libzstd replacement | `1` |
|
||||||
482
docs/UUID_BUGS_FIX_PLAN.md
Normal file
482
docs/UUID_BUGS_FIX_PLAN.md
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
# UUID/Skin Reset Bug Fix Plan
|
||||||
|
|
||||||
|
## Problem Summary
|
||||||
|
|
||||||
|
Players experience random skin/cosmetic resets without intentionally changing anything. The root cause is that the UUID system has multiple failure points that can silently generate new UUIDs or use the wrong UUID during gameplay.
|
||||||
|
|
||||||
|
**Impact**: Players lose their customized cosmetics/skins randomly, causing frustration and confusion.
|
||||||
|
|
||||||
|
**Status**: ✅ **FIXED** - All critical and high priority bugs have been addressed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
|
||||||
|
### What Was Fixed
|
||||||
|
|
||||||
|
| Bug | Severity | Status | Description |
|
||||||
|
|-----|----------|--------|-------------|
|
||||||
|
| BUG-001 | Critical | ✅ Fixed | Username not loaded before play click |
|
||||||
|
| BUG-002 | High | ✅ Fixed | isFirstLaunch() always returns true |
|
||||||
|
| BUG-003 | Critical | ✅ Fixed | Silent config corruption returns empty object |
|
||||||
|
| BUG-004 | Critical | ✅ Fixed | Non-atomic config writes |
|
||||||
|
| BUG-005 | High | ✅ Fixed | Username fallback to 'Player' |
|
||||||
|
| BUG-006 | Medium | ✅ Fixed | Launch overwrites username every time |
|
||||||
|
| BUG-007 | Medium | ✅ Fixed | Dual UUID systems (playerManager vs config) |
|
||||||
|
| BUG-008 | High | ✅ Fixed | Error returns random UUID |
|
||||||
|
| BUG-009 | Medium | ✅ Fixed | Username case sensitivity |
|
||||||
|
| BUG-010 | Medium | ⏳ Pending | Migration marks complete on partial failure |
|
||||||
|
| BUG-011 | Medium | ⏳ Pending | Race condition on concurrent config access |
|
||||||
|
| BUG-012 | High | ✅ Fixed | UUID modal isCurrent flag broken |
|
||||||
|
| BUG-013 | High | ✅ Fixed | UUID setting uses unsaved DOM username |
|
||||||
|
| BUG-014 | Medium | ✅ Fixed | No way to switch between saved identities |
|
||||||
|
| BUG-015 | High | ✅ Fixed | installGame saves username (overwrites good value) |
|
||||||
|
| BUG-016 | High | ✅ Fixed | Username rename creates new UUID instead of preserving |
|
||||||
|
| BUG-017 | Medium | ✅ Fixed | UUID list not refreshing when player name changes |
|
||||||
|
| BUG-018 | Low | ✅ Fixed | Custom UUID input doesn't allow copy/paste |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Scenario Analysis
|
||||||
|
|
||||||
|
All user scenarios have been analyzed for UUID/username persistence:
|
||||||
|
|
||||||
|
| Scenario | Risk | Status | Details |
|
||||||
|
|----------|------|--------|---------|
|
||||||
|
| **Fresh Install** | Low | ✅ Safe | firstLaunch.js reads but doesn't modify username/UUID |
|
||||||
|
| **Username Change** | Low | ✅ Safe | Rename preserves UUID, user-initiated saves work correctly |
|
||||||
|
| **Auto-Update** | Low | ✅ Safe | Config is on disk before update, backup recovery available |
|
||||||
|
| **Manual Update** | Low | ✅ Safe | Config file persists across manual updates |
|
||||||
|
| **Different Install Location** | Low | ✅ Safe | Config uses central app directory, not install-relative |
|
||||||
|
| **Repair Game** | Low | ✅ Safe | repairGame() doesn't touch config |
|
||||||
|
| **UUID Modal** | Low | ✅ Fixed | Fixed isCurrent badge, unsaved username bug, added switch button |
|
||||||
|
| **Profile Switch** | Low | ✅ Safe | Profiles only control mods/java, not username/UUID |
|
||||||
|
| **Branch Change** | Low | ✅ Safe | Only changes game version, not identity |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `backend/core/config.js` | Atomic writes, backup/recovery, validation, case-insensitive UUID lookup, checkLaunchReady(), username rename preserves UUID |
|
||||||
|
| `backend/managers/gameLauncher.js` | Pre-launch validation, removed saveUsername call |
|
||||||
|
| `backend/managers/gameManager.js` | Removed saveUsername call from installGame |
|
||||||
|
| `backend/services/playerManager.js` | Marked DEPRECATED, throws on error, retry logic |
|
||||||
|
| `backend/launcher.js` | Export new functions (checkLaunchReady, hasUsername, etc.) |
|
||||||
|
| `GUI/js/launcher.js` | Uses checkLaunchReady API, blocks launch if no username |
|
||||||
|
| `GUI/js/settings.js` | UUID modal fixes, switchToUsername function, proper error handling, refreshes UUID list on name change |
|
||||||
|
| `GUI/style.css` | Switch button styling, user-select: text for UUID input |
|
||||||
|
| `GUI/locales/*.json` | Added translation keys for switch username functionality (all 10 locales) |
|
||||||
|
| `main.js` | Fixed UUID IPC handlers, added checkLaunchReady handler, enabled Ctrl+V/C/X/A shortcuts |
|
||||||
|
| `preload.js` | Exposed checkLaunchReady to renderer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Categories
|
||||||
|
|
||||||
|
### Category A: Race Conditions & Initialization
|
||||||
|
### Category B: Silent Failures & Fallbacks
|
||||||
|
### Category C: Data Integrity & Persistence
|
||||||
|
### Category D: Design Issues
|
||||||
|
### Category E: UI/UX Issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Bug List & Fixes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-001: Username Not Loaded Before Play Click (CRITICAL) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: A - Race Condition
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `GUI/js/launcher.js`
|
||||||
|
- `GUI/js/settings.js`
|
||||||
|
|
||||||
|
**Problem**: If user clicks Play before settings DOM initializes, returns 'Player' silently.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- launcher.js now uses `checkLaunchReady()` API to validate before launch
|
||||||
|
- Loads username from backend config (single source of truth)
|
||||||
|
- Blocks launch and shows error if no username configured
|
||||||
|
- Navigates user to settings page to set username
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-002: `isFirstLaunch()` Always Returns True (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: Function always returns `true` even when user has data (typo: `return true` instead of `return false`).
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Fixed return statement: `return true` → `return false`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-003: Silent Config Corruption Returns Empty Object (CRITICAL) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: Corrupted config silently returns `{}`, causing UUID regeneration.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Added config validation after load
|
||||||
|
- Implemented backup config system (config.json.bak)
|
||||||
|
- Tries loading backup if primary fails
|
||||||
|
- Logs detailed errors for debugging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-004: Non-Atomic Config Writes (CRITICAL) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: C - Data Integrity
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: Direct write can corrupt file if interrupted. Silent error logging.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Atomic write: write to temp file → verify JSON → backup current → rename
|
||||||
|
- Throws error on save failure (no silent continuation)
|
||||||
|
- Cleans up temp file on failure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-005: Username Fallback to 'Player' (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: Missing username silently falls back to 'Player', causing wrong UUID.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- `loadUsername()` returns `null` instead of 'Player'
|
||||||
|
- Added `loadUsernameWithDefault()` for display purposes
|
||||||
|
- Added `hasUsername()` helper function
|
||||||
|
- All callers updated to handle null case explicitly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-006: Launch Overwrites Username Every Time (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `backend/managers/gameLauncher.js`
|
||||||
|
|
||||||
|
**Problem**: If playerName parameter is wrong, it overwrites the saved username.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Removed `saveUsername()` call from launch process
|
||||||
|
- Username only saved when user explicitly changes it in Settings
|
||||||
|
- Launch loads username from config (single source of truth)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-007: Dual UUID Systems (playerManager vs config) (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `backend/services/playerManager.js` → `player_id.json`
|
||||||
|
- `backend/core/config.js` → `config.json` → `userUuids`
|
||||||
|
|
||||||
|
**Problem**: Two independent UUID systems can desync.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- `playerManager.js` marked as DEPRECATED
|
||||||
|
- All code uses `config.js` `getUuidForUser()`
|
||||||
|
- Migration function added for legacy `player_id.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-008: Error Returns Random UUID (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `backend/services/playerManager.js`
|
||||||
|
|
||||||
|
**Problem**: Any error generates random UUID, losing player identity.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Now throws error instead of returning random UUID
|
||||||
|
- Retry logic added (3 attempts before failure)
|
||||||
|
- Caller must handle the error appropriately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-009: Username Case Sensitivity (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: "PlayerOne" and "playerone" are different UUIDs.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- `getUuidForUser()` uses case-insensitive lookup
|
||||||
|
- Username stored with ORIGINAL case (preserves "Sanasol", "SaAnAsOl", etc.)
|
||||||
|
- Lookup normalized to lowercase for matching
|
||||||
|
- Case changes update the stored key while preserving UUID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-010: Migration Marks Complete Even on Partial Failure (MEDIUM) ⏳ PENDING
|
||||||
|
|
||||||
|
**Category**: C - Data Integrity
|
||||||
|
|
||||||
|
**Location**: `backend/utils/userDataMigration.js`
|
||||||
|
|
||||||
|
**Problem**: Partial copy is marked as complete, preventing retry.
|
||||||
|
|
||||||
|
**Status**: Not yet implemented - low priority since migration runs once.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-011: Race Condition on Concurrent Config Access (MEDIUM) ⏳ PENDING
|
||||||
|
|
||||||
|
**Category**: A - Race Condition
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: No file locking - concurrent processes can overwrite each other.
|
||||||
|
|
||||||
|
**Status**: Not yet implemented - would require `proper-lockfile` package. Low risk since launcher is single-instance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-012: UUID Modal isCurrent Flag Broken (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `main.js` - `get-all-uuid-mappings` IPC handler
|
||||||
|
|
||||||
|
**Problem**: Case-sensitive comparison between normalized key (lowercase) and current username.
|
||||||
|
```javascript
|
||||||
|
// BROKEN:
|
||||||
|
isCurrent: username === loadUsername() // "player1" === "Player1" → FALSE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- IPC handler now uses `getAllUuidMappingsArray()` from config.js
|
||||||
|
- This function correctly compares against normalized username
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-013: UUID Setting Uses Unsaved DOM Username (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `GUI/js/settings.js` - `performSetCustomUuid()`
|
||||||
|
|
||||||
|
**Problem**: Gets username from DOM input field instead of saved config.
|
||||||
|
```javascript
|
||||||
|
// BROKEN:
|
||||||
|
const username = getCurrentPlayerName(); // From UI input, not saved!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risk Scenario**: User types new name but doesn't save → opens UUID modal → sets custom UUID → UUID gets set for unsaved name while config has old name.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Now loads username from backend config via `window.electronAPI.loadUsername()`
|
||||||
|
- Shows error if no username is saved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-014: No Way to Switch Between Saved Identities (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `GUI/js/settings.js` - UUID modal
|
||||||
|
|
||||||
|
**Problem**: UUID modal showed list of usernames/UUIDs but no way to switch to a different identity.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Added `switchToUsername()` function
|
||||||
|
- New switch button (user-check icon) on non-current entries
|
||||||
|
- Confirmation dialog before switching
|
||||||
|
- Updates username input and refreshes UUID display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-015: installGame Saves Username (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `backend/managers/gameManager.js` - `installGame()`
|
||||||
|
|
||||||
|
**Problem**: `saveUsername(playerName)` call could overwrite good username with 'Player' default.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Removed `saveUsername()` call from `installGame()`
|
||||||
|
- Username only saved when user explicitly changes it in Settings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-016: Username Rename Creates New UUID (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js` - `saveUsername()`
|
||||||
|
|
||||||
|
**Problem**: When user changes their player name, a new UUID was generated instead of preserving the existing one. User's identity (cosmetics/skins) was lost on every name change.
|
||||||
|
|
||||||
|
**Symptom**: Change "Player1" to "NewPlayer" → gets completely new UUID → loses all cosmetics.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- `saveUsername()` now handles UUID mapping renames atomically
|
||||||
|
- When renaming: old username's UUID is moved to new username
|
||||||
|
- When switching to existing identity: uses that identity's existing UUID
|
||||||
|
- Case changes only: updates key casing, preserves UUID
|
||||||
|
- Both username and userUuids saved in single atomic operation
|
||||||
|
|
||||||
|
**Behavior After Fix**:
|
||||||
|
```javascript
|
||||||
|
// Rename: "Player1" → "NewPlayer"
|
||||||
|
// Before: Player1=uuid-123, NewPlayer=uuid-NEW (wrong!)
|
||||||
|
// After: NewPlayer=uuid-123 (same UUID, just renamed)
|
||||||
|
|
||||||
|
// Switch to existing: "Player1" → "ExistingPlayer"
|
||||||
|
// Uses ExistingPlayer's existing UUID (switching identity)
|
||||||
|
|
||||||
|
// Case change: "Player1" → "PLAYER1"
|
||||||
|
// UUID preserved, key updated to new case
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-017: UUID List Not Refreshing on Name Change (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: E - UI/UX Issue
|
||||||
|
|
||||||
|
**Location**: `GUI/js/settings.js` - `savePlayerName()`
|
||||||
|
|
||||||
|
**Problem**: After changing player name in settings, the UUID modal list didn't refresh. The "Current" badge showed on the old username instead of the new one.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Added `await loadAllUuids()` call after `loadCurrentUuid()` in `savePlayerName()`
|
||||||
|
- UUID modal now shows correct "Current" badge after name changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-018: Custom UUID Input Doesn't Allow Copy/Paste (LOW) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: E - UI/UX Issue
|
||||||
|
|
||||||
|
**Location**: `GUI/style.css`, `main.js`
|
||||||
|
|
||||||
|
**Problem**: Two issues prevented copy/paste:
|
||||||
|
1. The body element has `select-none` class (Tailwind) which applies `user-select: none` globally
|
||||||
|
2. Electron's `setIgnoreMenuShortcuts(true)` was blocking Ctrl+V/C/X/A shortcuts
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Added `user-select: text` with all vendor prefixes to `.uuid-input` class
|
||||||
|
- Removed `setIgnoreMenuShortcuts(true)` from main.js
|
||||||
|
- Added early return in `before-input-event` handler to allow Ctrl/Cmd + V/C/X/A shortcuts
|
||||||
|
- DevTools shortcuts (Ctrl+Shift+I/J/C, F12) remain blocked
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Translation Keys Added
|
||||||
|
|
||||||
|
The following translation keys were added to `GUI/locales/en.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notifications": {
|
||||||
|
"noUsername": "No username configured. Please save your username first.",
|
||||||
|
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
|
||||||
|
"switchUsernameFailed": "Failed to switch username",
|
||||||
|
"playerNameTooLong": "Player name must be 16 characters or less"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"switchUsernameTitle": "Switch Identity",
|
||||||
|
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
|
||||||
|
"switchUsernameButton": "Switch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
After implementing fixes, verify:
|
||||||
|
|
||||||
|
- [x] Launch with freshly installed launcher - UUID persists
|
||||||
|
- [x] Change username in settings - UUID preserved (renamed, not new)
|
||||||
|
- [x] Config corruption - recovers from backup
|
||||||
|
- [x] Click Play immediately after opening - correct UUID used
|
||||||
|
- [x] Manual update from GitHub - UUID persists
|
||||||
|
- [x] Username with different casing - same UUID used, case preserved
|
||||||
|
- [x] UUID modal shows correct "Current" badge
|
||||||
|
- [x] UUID modal refreshes after username change
|
||||||
|
- [x] Switch identity from UUID modal works
|
||||||
|
- [x] Profile switching doesn't affect username/UUID
|
||||||
|
- [x] Custom UUID input allows copy/paste
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture: How UUID/Username Persistence Works
|
||||||
|
|
||||||
|
**Config Structure** (`config.json`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "CurrentPlayer",
|
||||||
|
"userUuids": {
|
||||||
|
"Sanasol": "uuid-123-abc",
|
||||||
|
"SaAnAsOl": "uuid-456-def",
|
||||||
|
"Player1": "uuid-789-ghi"
|
||||||
|
},
|
||||||
|
"hasLaunchedBefore": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Design Decisions**:
|
||||||
|
- Username stored with ORIGINAL case (e.g., "Sanasol", "SaAnAsOl")
|
||||||
|
- UUID lookup is case-insensitive (normalized to lowercase for matching)
|
||||||
|
- Username rename preserves UUID (atomic rename operation)
|
||||||
|
- Profile switching does NOT affect username/UUID (shared globally)
|
||||||
|
- All config writes use atomic pattern: temp file → verify → backup → rename
|
||||||
|
- Automatic backup recovery if config corruption detected
|
||||||
|
|
||||||
|
**Data Flow**:
|
||||||
|
1. User sets username in Settings → `saveUsername()` handles rename logic → saves to config.json
|
||||||
|
2. If renaming: UUID moved from old name to new name (same UUID preserved)
|
||||||
|
3. Launch game → `checkLaunchReady()` validates username exists
|
||||||
|
4. Launch game → `getUuidForUser(username)` gets UUID (case-insensitive lookup)
|
||||||
|
5. UUID modal → shows all username→UUID mappings from config
|
||||||
|
6. Switch identity → saves new username → gets that username's UUID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- ✅ Zero silent UUID regeneration
|
||||||
|
- ✅ Config corruption recovery working
|
||||||
|
- ✅ No UUID change without explicit user action
|
||||||
|
- ✅ Username rename preserves UUID
|
||||||
|
- ✅ Username case is preserved in display
|
||||||
|
- ✅ UUID modal correctly identifies current user
|
||||||
|
- ✅ UUID modal refreshes on changes
|
||||||
|
- ✅ Users can switch between saved identities
|
||||||
|
- ✅ Copy/paste works in UUID input
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining Work
|
||||||
|
|
||||||
|
1. **BUG-010**: Verify migration completeness before marking done (low priority)
|
||||||
|
2. **BUG-011**: Add file locking with `proper-lockfile` (low priority - single instance)
|
||||||
|
3. Add telemetry for config load failures and UUID regeneration events
|
||||||
|
|
||||||
|
## Completed Additional Tasks
|
||||||
|
|
||||||
|
- ✅ Added translation keys to all 10 locale files (de-DE, es-ES, fr-FR, id-ID, pl-PL, pt-BR, ru-RU, sv-SE, tr-TR, en)
|
||||||
135
main.js
135
main.js
@@ -3,7 +3,7 @@ require('dotenv').config({ path: path.join(__dirname, '.env') });
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||||
const { autoUpdater } = require('electron-updater');
|
const { autoUpdater } = require('electron-updater');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
|
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched, loadConfig, saveConfig, checkLaunchReady } = require('./backend/launcher');
|
||||||
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||||
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
||||||
|
|
||||||
@@ -86,6 +86,10 @@ function setDiscordActivity() {
|
|||||||
{
|
{
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
url: 'https://github.com/amiayweb/Hytale-F2P'
|
url: 'https://github.com/amiayweb/Hytale-F2P'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Discord',
|
||||||
|
url: 'https://discord.gg/hf2pdc'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -176,7 +180,8 @@ function createWindow() {
|
|||||||
initDiscordRPC();
|
initDiscordRPC();
|
||||||
|
|
||||||
// Configure and initialize electron-updater
|
// Configure and initialize electron-updater
|
||||||
autoUpdater.autoDownload = false;
|
// Enable auto-download so updates start immediately when available
|
||||||
|
autoUpdater.autoDownload = true;
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
autoUpdater.on('checking-for-update', () => {
|
autoUpdater.on('checking-for-update', () => {
|
||||||
@@ -201,6 +206,20 @@ function createWindow() {
|
|||||||
|
|
||||||
autoUpdater.on('error', (err) => {
|
autoUpdater.on('error', (err) => {
|
||||||
console.error('Error in auto-updater:', err);
|
console.error('Error in auto-updater:', err);
|
||||||
|
|
||||||
|
// Handle macOS code signing errors - requires manual download
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
const isMacSigningError = process.platform === 'darwin' &&
|
||||||
|
(err.code === 'ERR_UPDATER_INVALID_SIGNATURE' ||
|
||||||
|
err.message.includes('signature') ||
|
||||||
|
err.message.includes('code sign'));
|
||||||
|
|
||||||
|
mainWindow.webContents.send('update-error', {
|
||||||
|
message: err.message,
|
||||||
|
isMacSigningError: isMacSigningError,
|
||||||
|
requiresManualDownload: isMacSigningError || process.platform === 'darwin'
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('download-progress', (progressObj) => {
|
autoUpdater.on('download-progress', (progressObj) => {
|
||||||
@@ -218,7 +237,10 @@ function createWindow() {
|
|||||||
console.log('Update downloaded:', info.version);
|
console.log('Update downloaded:', info.version);
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('update-downloaded', {
|
mainWindow.webContents.send('update-downloaded', {
|
||||||
version: info.version
|
version: info.version,
|
||||||
|
platform: process.platform,
|
||||||
|
// macOS auto-install often fails on unsigned apps
|
||||||
|
autoInstallSupported: process.platform !== 'darwin'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -235,6 +257,17 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
|
// Allow standard copy/paste/cut/select-all shortcuts
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
const modKey = isMac ? input.meta : input.control;
|
||||||
|
const key = input.key.toLowerCase();
|
||||||
|
|
||||||
|
// Allow Ctrl/Cmd + V (paste), C (copy), X (cut), A (select all)
|
||||||
|
if (modKey && !input.shift && ['v', 'c', 'x', 'a'].includes(key)) {
|
||||||
|
return; // Don't block these
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block devtools shortcuts
|
||||||
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
|
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -252,7 +285,6 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close application shortcuts
|
// Close application shortcuts
|
||||||
const isMac = process.platform === 'darwin';
|
|
||||||
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
|
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
|
||||||
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
|
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
|
||||||
(!isMac && input.alt && input.key === 'F4');
|
(!isMac && input.alt && input.key === 'F4');
|
||||||
@@ -268,7 +300,7 @@ function createWindow() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.setIgnoreMenuShortcuts(true);
|
// Note: Not using setIgnoreMenuShortcuts to allow copy/paste to work
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
@@ -529,7 +561,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
|||||||
console.log('[Main] Processing Butler error with retry context');
|
console.log('[Main] Processing Butler error with retry context');
|
||||||
errorData.retryData = {
|
errorData.retryData = {
|
||||||
branch: error.branch || 'release',
|
branch: error.branch || 'release',
|
||||||
fileName: error.fileName || '4.pwr',
|
fileName: error.fileName || '7.pwr',
|
||||||
cacheDir: error.cacheDir
|
cacheDir: error.cacheDir
|
||||||
};
|
};
|
||||||
errorData.canRetry = error.canRetry !== false;
|
errorData.canRetry = error.canRetry !== false;
|
||||||
@@ -549,7 +581,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
|||||||
console.log('[Main] Processing generic error, creating default retry data');
|
console.log('[Main] Processing generic error, creating default retry data');
|
||||||
errorData.retryData = {
|
errorData.retryData = {
|
||||||
branch: 'release',
|
branch: 'release',
|
||||||
fileName: '4.pwr'
|
fileName: '7.pwr'
|
||||||
};
|
};
|
||||||
// For generic errors, assume it's retryable unless specified
|
// For generic errors, assume it's retryable unless specified
|
||||||
errorData.canRetry = error.canRetry !== false;
|
errorData.canRetry = error.canRetry !== false;
|
||||||
@@ -574,28 +606,24 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-username', (event, username) => {
|
ipcMain.handle('save-username', (event, username) => {
|
||||||
|
try {
|
||||||
saveUsername(username);
|
saveUsername(username);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Main] Failed to save username:', error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('load-username', () => {
|
ipcMain.handle('load-username', () => {
|
||||||
|
// Returns null if no username configured (no silent 'Player' fallback)
|
||||||
return loadUsername();
|
return loadUsername();
|
||||||
});
|
});
|
||||||
ipcMain.handle('save-chat-username', async (event, chatUsername) => {
|
|
||||||
saveChatUsername(chatUsername);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('load-chat-username', async () => {
|
ipcMain.handle('check-launch-ready', () => {
|
||||||
return loadChatUsername();
|
// Returns launch readiness state with detailed info
|
||||||
});
|
// { ready: boolean, hasUsername: boolean, username: string|null, issues: string[] }
|
||||||
|
return checkLaunchReady();
|
||||||
ipcMain.handle('save-chat-color', (event, color) => {
|
|
||||||
saveChatColor(color);
|
|
||||||
return { success: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('load-chat-color', () => {
|
|
||||||
return loadChatColor();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-java-path', (event, javaPath) => {
|
ipcMain.handle('save-java-path', (event, javaPath) => {
|
||||||
@@ -654,6 +682,15 @@ ipcMain.handle('load-launcher-hw-accel', () => {
|
|||||||
return loadLauncherHardwareAcceleration();
|
return loadLauncherHardwareAcceleration();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('load-config', () => {
|
||||||
|
return loadConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('save-config', (event, configUpdate) => {
|
||||||
|
saveConfig(configUpdate);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('select-install-path', async () => {
|
ipcMain.handle('select-install-path', async () => {
|
||||||
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
@@ -784,7 +821,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
|
|||||||
console.log('[IPC] Invalid retry data, using PWR defaults');
|
console.log('[IPC] Invalid retry data, using PWR defaults');
|
||||||
retryData = {
|
retryData = {
|
||||||
branch: 'release',
|
branch: 'release',
|
||||||
fileName: '4.pwr'
|
fileName: '7.pwr'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,7 +855,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
|
|||||||
} :
|
} :
|
||||||
{
|
{
|
||||||
branch: retryData?.branch || 'release',
|
branch: retryData?.branch || 'release',
|
||||||
fileName: retryData?.fileName || '4.pwr',
|
fileName: retryData?.fileName || '7.pwr',
|
||||||
cacheDir: retryData?.cacheDir
|
cacheDir: retryData?.cacheDir
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -859,6 +896,17 @@ ipcMain.handle('open-external', async (event, url) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('open-download-page', async () => {
|
||||||
|
try {
|
||||||
|
// Open GitHub releases page for manual download
|
||||||
|
await shell.openExternal('https://github.com/amiayweb/Hytale-F2P/releases/latest');
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open download page:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-game-location', async () => {
|
ipcMain.handle('open-game-location', async () => {
|
||||||
try {
|
try {
|
||||||
const { getResolvedAppDir, loadVersionBranch } = require('./backend/launcher');
|
const { getResolvedAppDir, loadVersionBranch } = require('./backend/launcher');
|
||||||
@@ -1086,8 +1134,37 @@ ipcMain.handle('download-update', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('install-update', () => {
|
ipcMain.handle('install-update', async () => {
|
||||||
|
console.log('[AutoUpdater] Installing update...');
|
||||||
|
|
||||||
|
// On macOS, quitAndInstall often fails silently
|
||||||
|
// Use a more aggressive approach
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
console.log('[AutoUpdater] macOS detected, using force quit approach');
|
||||||
|
// Give user feedback that something is happening
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('update-installing');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay to show the "Installing..." state
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
try {
|
||||||
autoUpdater.quitAndInstall(false, true);
|
autoUpdater.quitAndInstall(false, true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[AutoUpdater] quitAndInstall failed:', err);
|
||||||
|
// Force quit the app - the update should install on next launch
|
||||||
|
app.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If quitAndInstall didn't work, force exit after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('[AutoUpdater] Force exiting app...');
|
||||||
|
app.exit(0);
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
autoUpdater.quitAndInstall(false, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-launcher-version', () => {
|
ipcMain.handle('get-launcher-version', () => {
|
||||||
@@ -1180,12 +1257,9 @@ ipcMain.handle('get-current-uuid', async () => {
|
|||||||
|
|
||||||
ipcMain.handle('get-all-uuid-mappings', async () => {
|
ipcMain.handle('get-all-uuid-mappings', async () => {
|
||||||
try {
|
try {
|
||||||
const mappings = getAllUuidMappings();
|
// Use getAllUuidMappingsArray which correctly normalizes username for comparison
|
||||||
return Object.entries(mappings).map(([username, uuid]) => ({
|
const { getAllUuidMappingsArray } = require('./backend/launcher');
|
||||||
username,
|
return getAllUuidMappingsArray();
|
||||||
uuid,
|
|
||||||
isCurrent: username === require('./backend/launcher').loadUsername()
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting UUID mappings:', error);
|
console.error('Error getting UUID mappings:', error);
|
||||||
return [];
|
return [];
|
||||||
@@ -1321,3 +1395,4 @@ ipcMain.handle('profile-update', async (event, id, updates) => {
|
|||||||
return { error: error.message };
|
return { error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
680
package-lock.json
generated
680
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.1.2",
|
"version": "2.2.0",
|
||||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||||
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -25,8 +25,7 @@
|
|||||||
"cross-platform",
|
"cross-platform",
|
||||||
"electron",
|
"electron",
|
||||||
"auto-update",
|
"auto-update",
|
||||||
"mod-manager",
|
"mod-manager"
|
||||||
"chat"
|
|
||||||
],
|
],
|
||||||
"maintainers": [
|
"maintainers": [
|
||||||
{
|
{
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
"url": "https://github.com/Terromur"
|
"url": "https://github.com/Terromur"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Fari Gading",
|
"name": "Fazri Gading",
|
||||||
"email": "fazrigading@gmail.com",
|
"email": "fazrigading@gmail.com",
|
||||||
"url": "https://github.com/fazrigading"
|
"url": "https://github.com/fazrigading"
|
||||||
}
|
}
|
||||||
@@ -55,16 +54,13 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"electron-updater": "^6.7.3",
|
"electron-updater": "^6.7.3",
|
||||||
"fs-extra": "^11.3.3",
|
"fs-extra": "^11.3.3",
|
||||||
"tar": "^6.2.1",
|
"tar": "^7.5.7",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
|
||||||
"tar": "$tar"
|
|
||||||
},
|
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.hytalef2p.launcher",
|
"appId": "com.hytalef2p.launcher",
|
||||||
"productName": "Hytale F2P Launcher",
|
"productName": "Hytale F2P Launcher",
|
||||||
"artifactName": "${name}_${version}_${arch}.${ext}",
|
"artifactName": "${name}_${version}.${ext}",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist"
|
"output": "dist"
|
||||||
},
|
},
|
||||||
@@ -77,40 +73,14 @@
|
|||||||
".env"
|
".env"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": [
|
|
||||||
{
|
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
"arch": [
|
|
||||||
"x64",
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"icon": "build/icon.ico"
|
"icon": "build/icon.ico"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
{
|
"AppImage",
|
||||||
"target": "AppImage",
|
"deb",
|
||||||
"arch": [
|
"rpm"
|
||||||
"x64",
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "deb",
|
|
||||||
"arch": [
|
|
||||||
"x64",
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "rpm",
|
|
||||||
"arch": [
|
|
||||||
"x64",
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"icon": "build/icon.png",
|
"icon": "build/icon.png",
|
||||||
"category": "Game"
|
"category": "Game"
|
||||||
@@ -131,6 +101,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "build/icon.icns",
|
"icon": "build/icon.icns",
|
||||||
|
"artifactName": "${name}_${version}_${arch}.${ext}",
|
||||||
"category": "public.app-category.games"
|
"category": "public.app-category.games"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
|
|||||||
20
preload.js
20
preload.js
@@ -9,10 +9,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getVersion: () => ipcRenderer.invoke('get-version'),
|
getVersion: () => ipcRenderer.invoke('get-version'),
|
||||||
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
|
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
|
||||||
loadUsername: () => ipcRenderer.invoke('load-username'),
|
loadUsername: () => ipcRenderer.invoke('load-username'),
|
||||||
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
|
checkLaunchReady: () => ipcRenderer.invoke('check-launch-ready'),
|
||||||
loadChatUsername: () => ipcRenderer.invoke('load-chat-username'),
|
|
||||||
saveChatColor: (chatColor) => ipcRenderer.invoke('save-chat-color', chatColor),
|
|
||||||
loadChatColor: () => ipcRenderer.invoke('load-chat-color'),
|
|
||||||
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
|
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
|
||||||
loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
|
loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
|
||||||
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
|
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
|
||||||
@@ -23,8 +20,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
loadLanguage: () => ipcRenderer.invoke('load-language'),
|
loadLanguage: () => ipcRenderer.invoke('load-language'),
|
||||||
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
|
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
|
||||||
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
|
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
|
||||||
|
loadConfig: () => ipcRenderer.invoke('load-config'),
|
||||||
|
saveConfig: (configUpdate) => ipcRenderer.invoke('save-config', configUpdate),
|
||||||
|
|
||||||
// Harwadre Acceleration
|
// Hardware Acceleration
|
||||||
saveLauncherHardwareAcceleration: (enabled) => ipcRenderer.invoke('save-launcher-hw-accel', enabled),
|
saveLauncherHardwareAcceleration: (enabled) => ipcRenderer.invoke('save-launcher-hw-accel', enabled),
|
||||||
loadLauncherHardwareAcceleration: () => ipcRenderer.invoke('load-launcher-hw-accel'),
|
loadLauncherHardwareAcceleration: () => ipcRenderer.invoke('load-launcher-hw-accel'),
|
||||||
|
|
||||||
@@ -50,14 +49,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
|
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
|
||||||
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
|
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
|
||||||
onProgressUpdate: (callback) => {
|
onProgressUpdate: (callback) => {
|
||||||
ipcRenderer.on('progress-update', (event, data) => {
|
ipcRenderer.on('progress-update', (event, data) => callback(data));
|
||||||
// Ensure data includes retry state if available
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
callback(data);
|
|
||||||
} else {
|
|
||||||
callback(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onProgressComplete: (callback) => {
|
onProgressComplete: (callback) => {
|
||||||
ipcRenderer.on('progress-complete', () => callback());
|
ipcRenderer.on('progress-complete', () => callback());
|
||||||
@@ -69,7 +61,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.on('installation-end', () => callback());
|
ipcRenderer.on('installation-end', () => callback());
|
||||||
},
|
},
|
||||||
getUserId: () => ipcRenderer.invoke('get-user-id'),
|
getUserId: () => ipcRenderer.invoke('get-user-id'),
|
||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
|
||||||
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
|
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
|
||||||
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
|
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
|
||||||
onUpdatePopup: (callback) => {
|
onUpdatePopup: (callback) => {
|
||||||
@@ -126,6 +117,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||||
|
quitAndInstallUpdate: () => ipcRenderer.invoke('install-update'), // Alias for update.js compatibility
|
||||||
getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'),
|
getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'),
|
||||||
onUpdateAvailable: (callback) => {
|
onUpdateAvailable: (callback) => {
|
||||||
ipcRenderer.on('update-available', (event, data) => callback(data));
|
ipcRenderer.on('update-available', (event, data) => callback(data));
|
||||||
|
|||||||
Reference in New Issue
Block a user