mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 11:41:49 -03:00
Compare commits
174 Commits
v2.0.12
...
v2.1.3-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0aaf74a3db | ||
|
|
be78f67439 | ||
|
|
d0b9ae1da8 | ||
|
|
e8105cb30e | ||
|
|
79456e43a6 | ||
|
|
dd2dbc6f08 | ||
|
|
c4acb32fcd | ||
|
|
fbcbafb9b5 | ||
|
|
86ed33358c | ||
|
|
9ec97f9d33 | ||
|
|
ee18455b4b | ||
|
|
a5c931b26d | ||
|
|
661a0c9eed | ||
|
|
9025800820 | ||
|
|
34ee099ae2 | ||
|
|
e56b12cd72 | ||
|
|
3edee4b4eb | ||
|
|
e5fec7c326 | ||
|
|
7d2672b684 | ||
|
|
01823729ec | ||
|
|
639a2ab1b5 | ||
|
|
6b76eb365e | ||
|
|
6fa933fece | ||
|
|
e7023dcf95 | ||
|
|
faf21b830b | ||
|
|
f4d966ee65 | ||
|
|
ca835a868b | ||
|
|
3a1b6039d0 | ||
|
|
7828454631 | ||
|
|
cc1c6c334c | ||
|
|
081ac926e3 | ||
|
|
75a450c9ec | ||
|
|
e426690632 | ||
|
|
78f76afe0a | ||
|
|
131de1dcd7 | ||
|
|
b39877f561 | ||
|
|
6f10b1390d | ||
|
|
0b1b448cce | ||
|
|
aed00cd067 | ||
|
|
c4a32ce1e0 | ||
|
|
eff6fcd520 | ||
|
|
94d4586b97 | ||
|
|
20faf36b37 | ||
|
|
375b422c73 | ||
|
|
b668bdb45a | ||
|
|
653d4429ed | ||
|
|
17e15c17f0 | ||
|
|
b99b22e8bf | ||
|
|
9303c17e57 | ||
|
|
615ee5cadc | ||
|
|
7a9a67d8e8 | ||
|
|
4c854953fe | ||
|
|
4cd0539ce3 | ||
|
|
fa2d451f90 | ||
|
|
a4faa7138c | ||
|
|
d285dc7517 | ||
|
|
ceadd69eea | ||
|
|
6f0dd27c1d | ||
|
|
ba95187ee6 | ||
|
|
9e54e07b22 | ||
|
|
a8e7e57c86 | ||
|
|
d1ab58d51b | ||
|
|
8781025df9 | ||
|
|
81c52e9507 | ||
|
|
45314620e4 | ||
|
|
43d5d20351 | ||
|
|
72b4e0cba8 | ||
|
|
25d5131a7b | ||
|
|
ad3c73563d | ||
|
|
f0f19f690f | ||
|
|
b27860a655 | ||
|
|
9788d0e496 | ||
|
|
2a5780c2d4 | ||
|
|
8263b3f99b | ||
|
|
db56ef1624 | ||
|
|
35f900d6ab | ||
|
|
e1d1383ab7 | ||
|
|
8326deddb1 | ||
|
|
b11b78f7dc | ||
|
|
62a2d76e4a | ||
|
|
0ca8b4e02f | ||
|
|
c6a9d0ae07 | ||
|
|
f438d6c8e0 | ||
|
|
f07e4a2004 | ||
|
|
131580d3ba | ||
|
|
084347db03 | ||
|
|
589c5b457f | ||
|
|
790d4d3f29 | ||
|
|
52313910dc | ||
|
|
a3f4d8e9d8 | ||
|
|
86d617a4d3 | ||
|
|
0a97ac95fc | ||
|
|
b94b45681b | ||
|
|
4086612e9d | ||
|
|
e7fca5a4c7 | ||
|
|
e7bd20a1ec | ||
|
|
151b017653 | ||
|
|
da3e14c434 | ||
|
|
6302734eeb | ||
|
|
07191860be | ||
|
|
2f767f191e | ||
|
|
de9c7d81f5 | ||
|
|
4c3277392e | ||
|
|
f287cb55b9 | ||
|
|
d87db04653 | ||
|
|
67aa41aefe | ||
|
|
bd1dd146a9 | ||
|
|
c8d7707b70 | ||
|
|
127c38f98b | ||
|
|
f974d9c767 | ||
|
|
7e4a45e466 | ||
|
|
ea21fb15d6 | ||
|
|
3d54cea9e7 | ||
|
|
9f43a32779 | ||
|
|
9c8a12f25c | ||
|
|
a7d0523186 | ||
|
|
a6f716c61b | ||
|
|
ca8ed171d1 | ||
|
|
679799c074 | ||
|
|
87b168dd4c | ||
|
|
679f065e24 | ||
|
|
ecae7d2ee5 | ||
|
|
fa50fec34d | ||
|
|
c900129c1f | ||
|
|
6b75858515 | ||
|
|
61bcdf9413 | ||
|
|
411d7d8aaf | ||
|
|
8a87c7c4d9 | ||
|
|
34f93e962b | ||
|
|
d8393543df | ||
|
|
3579d82776 | ||
|
|
e005b4293b | ||
|
|
e43897f816 | ||
|
|
3983fdb1bc | ||
|
|
2a87acfe46 | ||
|
|
a2e2d5e5fd | ||
|
|
34143d9872 | ||
|
|
08c2218cf8 | ||
|
|
032418b7f7 | ||
|
|
fc05725a43 | ||
|
|
203a56879f | ||
|
|
7a0065ea2b | ||
|
|
ac08eb50ff | ||
|
|
70fe4203ef | ||
|
|
f433120084 | ||
|
|
f4099acbed | ||
|
|
da843257c1 | ||
|
|
e4576042be | ||
|
|
1ba6b22b74 | ||
|
|
a1bc88b754 | ||
|
|
24c2371b50 | ||
|
|
4c6e1a616e | ||
|
|
b54eb4e834 | ||
|
|
a1c74e4175 | ||
|
|
260e6c1126 | ||
|
|
6eb628559b | ||
|
|
052b5dc7dc | ||
|
|
7e9b5046df | ||
|
|
204d6b21f6 | ||
|
|
740d516cfe | ||
|
|
ce052add0d | ||
|
|
d7a904c641 | ||
|
|
d5d2f60c97 | ||
|
|
61433bfeea | ||
|
|
9eb5d1759c | ||
|
|
68d697576a | ||
|
|
a8da559e93 | ||
|
|
75f9403888 | ||
|
|
b61c94d348 | ||
|
|
c0109575d6 | ||
|
|
2a024b61dd | ||
|
|
1c39e8e4c6 | ||
|
|
753bd4fd61 | ||
|
|
cefb4c5575 |
@@ -1,2 +0,0 @@
|
|||||||
CURSEFORGE_API_KEY=$1234asdxXXXXXXkQCXXXXXXXXXXASDb32
|
|
||||||
DISCORD_CLIENT_ID=561263XXXXXX
|
|
||||||
54
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
54
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -3,6 +3,13 @@ description: Create a report to help us improve
|
|||||||
title: "[BUG] "
|
title: "[BUG] "
|
||||||
labels: ["bug"]
|
labels: ["bug"]
|
||||||
body:
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Bug is a problem which impairs or prevents the functions of the launcher from working as intended.
|
||||||
|
Thanks for taking the time to fill out a bug report!
|
||||||
|
Please provide as much information as you can to help us understand and reproduce the issue.
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
@@ -43,8 +50,17 @@ body:
|
|||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version of the project are you running?
|
description: What version of the launcher are you running?
|
||||||
placeholder: "e.g. v1.2.3"
|
placeholder: "e.g. \"v2.0.11 stable/pre-release\""
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: hardwarespec
|
||||||
|
attributes:
|
||||||
|
label: Hardware Specification
|
||||||
|
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"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -54,31 +70,27 @@ body:
|
|||||||
label: Operating System
|
label: Operating System
|
||||||
description: What operating system are you using?
|
description: What operating system are you using?
|
||||||
options:
|
options:
|
||||||
- Windows
|
- Windows 10
|
||||||
- macOS
|
- Windows 11
|
||||||
- Linux
|
- macOS (Apple Silicon)
|
||||||
- iOS
|
- macOS (Intel)
|
||||||
- Android
|
- Linux Ubuntu/Debian-based
|
||||||
- Other
|
- Linux Fedora/RHEL-based
|
||||||
|
- Linux Arch-based
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: textarea
|
||||||
id: browser
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Browser (if applicable)
|
label: Logs or Error Messages
|
||||||
description: What browser are you using?
|
description: If applicable, paste any error messages or logs here.
|
||||||
options:
|
render: shell
|
||||||
- Chrome
|
validations:
|
||||||
- Firefox
|
required: true
|
||||||
- Safari
|
|
||||||
- Edge
|
|
||||||
- Opera
|
|
||||||
- Other
|
|
||||||
- N/A
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add any other context about the problem here.
|
description: Add any other context about the problem here.
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
14
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -16,8 +16,10 @@ body:
|
|||||||
id: problem
|
id: problem
|
||||||
attributes:
|
attributes:
|
||||||
label: Is your feature request related to a problem? Please describe.
|
label: Is your feature request related to a problem? Please describe.
|
||||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
description: A clear and concise description of what the problem is.
|
||||||
placeholder: "Ex. I'm always frustrated when [...]"
|
placeholder: "Ex. I'm always frustrated when [...]"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: solution
|
id: solution
|
||||||
@@ -34,9 +36,17 @@ body:
|
|||||||
label: Describe alternatives you've considered
|
label: Describe alternatives you've considered
|
||||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
placeholder: "Describe any alternative solutions or features you've considered."
|
placeholder: "Describe any alternative solutions or features you've considered."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots (Optional)
|
||||||
|
description: If applicable, add screenshots to help explain your request.
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional context
|
label: Additional context
|
||||||
description: Add any other context or screenshots about the feature request here.
|
description: Add any other context or screenshots about the feature request here.
|
||||||
|
|||||||
24
.github/ISSUE_TEMPLATE/new_translation_request.yml
vendored
Normal file
24
.github/ISSUE_TEMPLATE/new_translation_request.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: New Translation Request
|
||||||
|
description: Request new language translation for text or content on the launcher
|
||||||
|
title: "[TRANSLATION REQUEST] "
|
||||||
|
labels: ["translation request"]
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: language
|
||||||
|
attributes:
|
||||||
|
label: Request New Language
|
||||||
|
description: What language do you want our launcher to support?
|
||||||
|
placeholder: "e.g. German (de-DE), Russian (ru-RU), etc."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: contriution_willingness
|
||||||
|
attributes:
|
||||||
|
label: Willingness to Contribute
|
||||||
|
description: Are you willing to help with the translation effort?
|
||||||
|
options:
|
||||||
|
- Yes, I can help translate from English to the requested language!
|
||||||
|
- No, I just want to request the language.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
66
.github/ISSUE_TEMPLATE/support_request.yml
vendored
66
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -1,8 +1,29 @@
|
|||||||
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
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
If you need help or support with using the launcher, please fill out this support request.
|
||||||
|
Provide as much detail as possible so we can assist you effectively.
|
||||||
|
**Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/gME8rUy3MB)!
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
attributes:
|
attributes:
|
||||||
@@ -17,14 +38,32 @@ 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..., I expected..., but got..."
|
placeholder: "I've tried these steps, but got..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: input
|
- type: textarea
|
||||||
|
id: hardwarespec
|
||||||
|
attributes:
|
||||||
|
label: Hardware Specification
|
||||||
|
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"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Version
|
label: Version
|
||||||
description: What version are you using?
|
description: What version are you using?
|
||||||
placeholder: "e.g. v1.2.3"
|
options:
|
||||||
|
- v2.1.2
|
||||||
|
- v2.1.1
|
||||||
|
- v2.1.0
|
||||||
|
- v2.0.11
|
||||||
|
- v2.0.2
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: platform
|
id: platform
|
||||||
@@ -32,13 +71,14 @@ body:
|
|||||||
label: Platform
|
label: Platform
|
||||||
description: What platform are you using?
|
description: What platform are you using?
|
||||||
options:
|
options:
|
||||||
- Windows
|
- Windows 11 x64
|
||||||
- macOS
|
- Windows 10 x64
|
||||||
- Linux
|
- macOS ARM64 (Apple Silicon)
|
||||||
- iOS
|
- Linux x64 Ubuntu/Debian-based
|
||||||
- Android
|
- Linux x64 Fedora/RHEL-based
|
||||||
- Web Browser
|
- Linux x64 Arch-based
|
||||||
- Other
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
@@ -46,9 +86,11 @@ body:
|
|||||||
label: Logs or Error Messages
|
label: Logs or Error Messages
|
||||||
description: If applicable, paste any error messages or logs here.
|
description: If applicable, paste any error messages or logs here.
|
||||||
render: shell
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional Information
|
label: Additional Information
|
||||||
description: Any other information that might help us assist you.
|
description: Any other information that might help us assist you.
|
||||||
|
|||||||
41
.github/ISSUE_TEMPLATE/translation_fix_request.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/translation_fix_request.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Translation Fix Request
|
||||||
|
description: Request a fix of translation for text or content in the launcher
|
||||||
|
title: "[TRANSLATION FIX] "
|
||||||
|
labels: ["translation fix"]
|
||||||
|
body:
|
||||||
|
- type: input
|
||||||
|
id: language
|
||||||
|
attributes:
|
||||||
|
label: Target Language
|
||||||
|
description: What language do you want to translate to?
|
||||||
|
placeholder: "e.g. Spanish (es-ES), Portuguese (pt-BR), etc."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: source_text
|
||||||
|
attributes:
|
||||||
|
label: Source Text
|
||||||
|
description: The original text that needs to be translated.
|
||||||
|
placeholder: "Paste the text here..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Context
|
||||||
|
description: Provide context about where this text appears or how it's used.
|
||||||
|
placeholder: "This text appears in..., It's used for..."
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: notes
|
||||||
|
attributes:
|
||||||
|
label: Additional Notes
|
||||||
|
description: Any specific instructions or notes for the translator.
|
||||||
210
.github/workflows/release.yml
vendored
210
.github/workflows/release.yml
vendored
@@ -3,50 +3,12 @@ name: Build and Release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- release
|
- main
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-linux:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
# FIX Install bsdtar for Pacman builds
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libarchive-tools
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
cache: 'npm'
|
|
||||||
- run: npm ci
|
|
||||||
|
|
||||||
- name: Create .env file
|
|
||||||
env:
|
|
||||||
CF_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
|
||||||
DISCORD_ID: ${{ secrets.DISCORD_CLIENT_ID }}
|
|
||||||
run: |
|
|
||||||
echo "CURSEFORGE_API_KEY=$CF_KEY" > .env
|
|
||||||
echo "DISCORD_CLIENT_ID=$DISCORD_ID" >> .env
|
|
||||||
|
|
||||||
- name: Build Linux Packages
|
|
||||||
run: |
|
|
||||||
npx electron-builder --linux --x64 --arm64 --publish never
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: linux-builds
|
|
||||||
path: |
|
|
||||||
dist/*.AppImage
|
|
||||||
dist/*.AppImage.blockmap
|
|
||||||
dist/*.deb
|
|
||||||
dist/*.rpm
|
|
||||||
dist/*.pacman
|
|
||||||
dist/latest-linux.yml
|
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -56,14 +18,6 @@ jobs:
|
|||||||
node-version: '22'
|
node-version: '22'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Create .env file
|
|
||||||
env:
|
|
||||||
CF_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
|
||||||
DISCORD_ID: ${{ secrets.DISCORD_CLIENT_ID }}
|
|
||||||
run: |
|
|
||||||
echo "CURSEFORGE_API_KEY=$CF_KEY" > .env
|
|
||||||
echo "DISCORD_CLIENT_ID=$DISCORD_ID" >> .env
|
|
||||||
|
|
||||||
- name: Build Windows Packages
|
- name: Build Windows Packages
|
||||||
run: npx electron-builder --win --publish never
|
run: npx electron-builder --win --publish never
|
||||||
@@ -85,15 +39,15 @@ jobs:
|
|||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Create .env file
|
- name: Build macOS Packages
|
||||||
env:
|
env:
|
||||||
CF_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
|
# Code signing
|
||||||
DISCORD_ID: ${{ secrets.DISCORD_CLIENT_ID }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
run: |
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
echo "CURSEFORGE_API_KEY=$CF_KEY" > .env
|
# Notarization
|
||||||
echo "DISCORD_CLIENT_ID=$DISCORD_ID" >> .env
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
- name: Build Windows Packages
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
run: npx electron-builder --mac --publish never
|
run: npx electron-builder --mac --publish never
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -103,26 +57,113 @@ jobs:
|
|||||||
dist/*.zip
|
dist/*.zip
|
||||||
dist/latest-mac.yml
|
dist/latest-mac.yml
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libarchive-tools
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm ci
|
||||||
|
|
||||||
|
- name: Build Linux Packages
|
||||||
|
run: |
|
||||||
|
npx electron-builder --linux AppImage deb rpm --x64 --arm64 --publish never
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-builds
|
||||||
|
path: |
|
||||||
|
dist/*.AppImage
|
||||||
|
dist/*.AppImage.blockmap
|
||||||
|
dist/*.deb
|
||||||
|
dist/*.rpm
|
||||||
|
dist/latest-linux.yml
|
||||||
|
|
||||||
|
build-arch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: archlinux:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install base packages
|
||||||
|
run: |
|
||||||
|
pacman -Syu --noconfirm
|
||||||
|
pacman -S --noconfirm \
|
||||||
|
base-devel \
|
||||||
|
git \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
rpm-tools \
|
||||||
|
libxcrypt-compat
|
||||||
|
|
||||||
|
- name: Create build user
|
||||||
|
run: |
|
||||||
|
useradd -m builder
|
||||||
|
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||||
|
|
||||||
|
- name: Fix Permissions
|
||||||
|
run: chown -R builder:builder .
|
||||||
|
|
||||||
|
- name: Build Arch Package
|
||||||
|
run: |
|
||||||
|
sudo -u builder bash << 'EOF'
|
||||||
|
set -e
|
||||||
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
|
makepkg -s --noconfirm
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload Arch Package
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: arch-package
|
||||||
|
path: |
|
||||||
|
*.pkg.tar.zst
|
||||||
|
*.src.tar.zst
|
||||||
|
.SRCINFO
|
||||||
|
|
||||||
|
# Create release with Windows, Linux, Arch (fast builds)
|
||||||
release:
|
release:
|
||||||
needs: [build-linux, build-windows, build-macos]
|
needs: [build-windows, build-linux, build-arch]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: |
|
if: |
|
||||||
startsWith(github.ref, 'refs/tags/v') ||
|
startsWith(github.ref, 'refs/tags/v') ||
|
||||||
github.ref == 'refs/heads/release' ||
|
github.ref == 'refs/heads/main' ||
|
||||||
github.event_name == 'workflow_dispatch'
|
github.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# FIX: './package.json' Module Not Found in `Get version` step
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download all artifacts
|
- name: Download Windows artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
name: windows-builds
|
||||||
|
path: artifacts/windows-builds
|
||||||
|
|
||||||
|
- name: Download Linux artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-builds
|
||||||
|
path: artifacts/linux-builds
|
||||||
|
|
||||||
|
- name: Download Arch artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: arch-package
|
||||||
|
path: artifacts/arch-package
|
||||||
|
|
||||||
- name: Display structure of downloaded files
|
- name: Display structure of downloaded files
|
||||||
run: ls -R artifacts
|
run: ls -R artifacts
|
||||||
@@ -134,15 +175,44 @@ jobs:
|
|||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
# If it's a tag, use the tag.
|
tag_name: ${{ github.ref_name }}
|
||||||
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/linux-builds/**/*
|
artifacts/arch-package/*.pkg.tar.zst
|
||||||
artifacts/windows-builds/**/*
|
artifacts/arch-package/*.src.tar.zst
|
||||||
artifacts/macos-builds/**/*
|
artifacts/arch-package/.SRCINFO
|
||||||
|
artifacts/linux-builds/*
|
||||||
|
artifacts/windows-builds/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
draft: true
|
draft: true
|
||||||
# DYNAMIC FLAGS: Mark as pre-release ONLY IF it's NOT a tag (meaning it's a branch push)
|
prerelease: false
|
||||||
prerelease: ${{ github.ref_type != 'tag' }}
|
|
||||||
|
# Upload macOS builds separately (slow due to notarization)
|
||||||
|
release-macos:
|
||||||
|
needs: [build-macos, release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
startsWith(github.ref, 'refs/tags/v') ||
|
||||||
|
github.ref == 'refs/heads/main' ||
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download macOS artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-builds
|
||||||
|
path: artifacts/macos-builds
|
||||||
|
|
||||||
|
- name: Display macOS files
|
||||||
|
run: ls -R artifacts
|
||||||
|
|
||||||
|
- name: Upload macOS to Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
files: |
|
||||||
|
artifacts/macos-builds/*
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
35
.gitignore
vendored
35
.gitignore
vendored
@@ -1,14 +1,23 @@
|
|||||||
dist/*
|
# General / Node
|
||||||
node_modules/*
|
node_modules/
|
||||||
bun.lock
|
dist/
|
||||||
|
|
||||||
# Build artifacts
|
|
||||||
src/
|
|
||||||
pkg/
|
|
||||||
|
|
||||||
# Package files
|
|
||||||
*.tar.zst
|
|
||||||
*.zst.DS_Store
|
|
||||||
*.zst
|
|
||||||
bun.lockb
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Arch Linux / makepkg: Ignore folders created when running 'makepkg' locally
|
||||||
|
/src/
|
||||||
|
/pkg/
|
||||||
|
|
||||||
|
# Built packages: {revents committing large binaries
|
||||||
|
*.pkg.tar.zst
|
||||||
|
*.pkg.tar.xz
|
||||||
|
|
||||||
|
# Source downloads used by PKGBUILD
|
||||||
|
*.src.tar.gz
|
||||||
|
|
||||||
|
# Project Specific: Downloaded patcher (from hytale-auth-server)
|
||||||
|
backend/patcher/
|
||||||
|
|
||||||
|
# macOS Specific
|
||||||
|
.DS_Store
|
||||||
|
*.zst.DS_Store
|
||||||
|
bun.lock
|
||||||
|
|||||||
455
GUI/index.html
455
GUI/index.html
@@ -123,6 +123,29 @@
|
|||||||
value="Player" />
|
value="Player" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" data-i18n="install.gameBranch">Game Version</label>
|
||||||
|
<div class="radio-group">
|
||||||
|
<label class="radio-label">
|
||||||
|
<input type="radio" name="installBranch" value="release" class="custom-radio"
|
||||||
|
checked>
|
||||||
|
<span class="radio-text">
|
||||||
|
<i class="fas fa-check-circle mr-2"></i>
|
||||||
|
<span data-i18n="install.releaseVersion">Release (Stable)</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-label">
|
||||||
|
<input type="radio" name="installBranch" value="pre-release"
|
||||||
|
class="custom-radio">
|
||||||
|
<span class="radio-text">
|
||||||
|
<i class="fas fa-flask mr-2"></i>
|
||||||
|
<span data-i18n="install.preReleaseVersion">Pre-Release
|
||||||
|
(Experimental)</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="checkbox-group">
|
<label class="checkbox-group">
|
||||||
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
|
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
|
||||||
@@ -337,210 +360,251 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU
|
<label class="settings-input-label" data-i18n="settings.gameBranch">Game
|
||||||
Preference</label>
|
Branch</label>
|
||||||
<div class="segmented-control">
|
<div class="segmented-control">
|
||||||
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto"
|
<input type="radio" id="branch-release" name="gameBranch"
|
||||||
checked>
|
value="release" checked>
|
||||||
<label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
|
<label for="branch-release"
|
||||||
<input type="radio" id="gpu-integrated" name="gpuPreference"
|
data-i18n="settings.branchRelease">Release</label>
|
||||||
value="integrated">
|
<input type="radio" id="branch-pre-release" name="gameBranch"
|
||||||
<label for="gpu-integrated"
|
value="pre-release">
|
||||||
data-i18n="settings.gpuIntegrated">Integrated</label>
|
<label for="branch-pre-release"
|
||||||
<input type="radio" id="gpu-dedicated" name="gpuPreference"
|
data-i18n="settings.branchPreRelease">Pre-Release</label>
|
||||||
value="dedicated">
|
|
||||||
<label for="gpu-dedicated"
|
|
||||||
data-i18n="settings.gpuDedicated">Dedicated</label>
|
|
||||||
</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.branchHint">Switch between stable release and
|
||||||
affects DRI_PRIME)</span>
|
experimental pre-release versions</span>
|
||||||
|
</p>
|
||||||
|
<p class="settings-hint" style="color: #f39c12;">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<span data-i18n="settings.branchWarning">Changing branch will download
|
||||||
|
and install a different game version</span>
|
||||||
</p>
|
</p>
|
||||||
<div id="gpu-detection-info" class="gpu-detection-info"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU
|
||||||
|
Preference</label>
|
||||||
|
<div class="segmented-control">
|
||||||
|
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto" checked>
|
||||||
|
<label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
|
||||||
|
<input type="radio" id="gpu-integrated" name="gpuPreference"
|
||||||
|
value="integrated">
|
||||||
|
<label for="gpu-integrated"
|
||||||
|
data-i18n="settings.gpuIntegrated">Integrated</label>
|
||||||
|
<input type="radio" id="gpu-dedicated" name="gpuPreference"
|
||||||
|
value="dedicated">
|
||||||
|
<label for="gpu-dedicated"
|
||||||
|
data-i18n="settings.gpuDedicated">Dedicated</label>
|
||||||
|
</div>
|
||||||
|
<p class="settings-hint">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux:
|
||||||
|
affects DRI_PRIME)</span>
|
||||||
|
</p>
|
||||||
|
<div id="gpu-detection-info" class="gpu-detection-info"></div>
|
||||||
|
</div>
|
||||||
</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-fingerprint"></i>
|
<i class="fas fa-fingerprint"></i>
|
||||||
<span data-i18n="settings.account">Player UUID Management</span>
|
<span data-i18n="settings.account">Player UUID Management</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
<div class="settings-input-group">
|
<div class="settings-input-group">
|
||||||
<label class="settings-input-label" data-i18n="settings.currentUUID">Current
|
<label class="settings-input-label" data-i18n="settings.currentUUID">Current
|
||||||
UUID</label>
|
UUID</label>
|
||||||
<div class="uuid-display-container">
|
<div class="uuid-display-container">
|
||||||
<input type="text" id="currentUuid" class="settings-input uuid-input"
|
<input type="text" id="currentUuid" class="settings-input uuid-input"
|
||||||
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
|
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
|
||||||
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
|
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
|
||||||
title="Generate New UUID">
|
title="Generate New UUID">
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="settings-hint">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
<span data-i18n="settings.uuidHint">Your unique player identifier for
|
|
||||||
this username</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<div class="settings-button-group">
|
|
||||||
<button id="manageUuidsBtn" class="settings-action-btn">
|
|
||||||
<i class="fas fa-list"></i>
|
|
||||||
<div class="btn-content">
|
|
||||||
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All
|
|
||||||
UUIDs</div>
|
|
||||||
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">
|
|
||||||
View and manage all player UUIDs</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="settings-hint">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
<span data-i18n="settings.uuidHint">Your unique player identifier for
|
||||||
|
this username</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-option">
|
||||||
<h3 class="settings-section-title">
|
<div class="settings-button-group">
|
||||||
<i class="fab fa-discord"></i>
|
<button id="manageUuidsBtn" class="settings-action-btn">
|
||||||
<span data-i18n="settings.discord">Discord Integration</span>
|
<i class="fas fa-list"></i>
|
||||||
</h3>
|
<div class="btn-content">
|
||||||
|
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All
|
||||||
<div class="settings-option">
|
UUIDs</div>
|
||||||
<label class="settings-checkbox">
|
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">
|
||||||
<input type="checkbox" id="discordRPCCheck" checked />
|
View and manage all player UUIDs</div>
|
||||||
<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>
|
</div>
|
||||||
</label>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-section">
|
|
||||||
<h3 class="settings-section-title">
|
|
||||||
<i class="fas fa-window-close"></i>
|
|
||||||
<span data-i18n="settings.closeLauncher">Launcher Behavior</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<label class="settings-checkbox">
|
|
||||||
<input type="checkbox" id="closeLauncherCheck" />
|
|
||||||
<span class="checkmark"></span>
|
|
||||||
<div class="checkbox-content">
|
|
||||||
<div class="checkbox-title" data-i18n="settings.closeOnStart">Close Launcher on game start</div>
|
|
||||||
<div class="checkbox-description" data-i18n="settings.closeOnStartDescription">
|
|
||||||
Automatically close the launcher after Hytale has launched
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</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">
|
|
||||||
<h3 class="settings-section-title">
|
|
||||||
<i class="fas fa-language"></i>
|
|
||||||
<span data-i18n="settings.language">Language</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="settings-option">
|
|
||||||
<div class="settings-input-group">
|
|
||||||
<label class="settings-input-label"
|
|
||||||
data-i18n="settings.selectLanguage">Select Language</label>
|
|
||||||
<select id="languageSelect" class="settings-input">
|
|
||||||
<!-- Options populated by i18n.js -->
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="logs-page" class="page">
|
<div class="settings-section">
|
||||||
<div class="logs-container">
|
<h3 class="settings-section-title">
|
||||||
<div class="logs-header">
|
<i class="fab fa-discord"></i>
|
||||||
<h2 class="logs-title">
|
<span data-i18n="settings.discord">Discord Integration</span>
|
||||||
<i class="fas fa-terminal"></i>
|
</h3>
|
||||||
<span data-i18n="settings.logs">SYSTEM LOGS</span>
|
|
||||||
</h2>
|
<div class="settings-option">
|
||||||
<div class="logs-actions">
|
<label class="settings-checkbox">
|
||||||
<button class="logs-action-btn" onclick="copyLogs()">
|
<input type="checkbox" id="discordRPCCheck" checked />
|
||||||
<i class="fas fa-copy"></i> <span data-i18n="settings.logsCopy">Copy</span>
|
<span class="checkmark"></span>
|
||||||
</button>
|
<div class="checkbox-content">
|
||||||
<button class="logs-action-btn" onclick="refreshLogs()">
|
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable
|
||||||
<i class="fas fa-sync-alt"></i> <span
|
Discord Rich Presence</div>
|
||||||
data-i18n="settings.logsRefresh">Refresh</span>
|
<div class="checkbox-description" data-i18n="settings.discordDescription">
|
||||||
</button>
|
Show your launcher activity
|
||||||
<button class="logs-action-btn" onclick="openLogsFolder()">
|
on Discord
|
||||||
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
|
</div>
|
||||||
Folder</span>
|
</div>
|
||||||
</button>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="logsTerminal" class="logs-terminal">
|
|
||||||
<div class="text-gray-500 text-center mt-10" data-i18n="settings.logsLoading">Loading
|
<div class="settings-section">
|
||||||
logs...</div>
|
<h3 class="settings-section-title">
|
||||||
|
<i class="fas fa-window-close"></i>
|
||||||
|
<span data-i18n="settings.closeLauncher">Launcher Behavior</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<label class="settings-checkbox">
|
||||||
|
<input type="checkbox" id="closeLauncherCheck" />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<div class="checkbox-content">
|
||||||
|
<div class="checkbox-title" data-i18n="settings.closeOnStart">Close Launcher
|
||||||
|
on game start</div>
|
||||||
|
<div class="checkbox-description"
|
||||||
|
data-i18n="settings.closeOnStartDescription">
|
||||||
|
Automatically close the launcher after Hytale has launched
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="settings-option">
|
||||||
|
<label class="settings-checkbox">
|
||||||
|
<input type="checkbox" id="launcherHwAccelCheck" />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<div class="checkbox-content">
|
||||||
|
<div class="checkbox-title" data-i18n="settings.hwAccel">Launcher Hardware
|
||||||
|
Acceleration</div>
|
||||||
|
<div class="checkbox-description" data-i18n="settings.hwAccelDescription">
|
||||||
|
Enable hardware acceleration for the launcher UI (Requires restart)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</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">
|
||||||
|
<h3 class="settings-section-title">
|
||||||
|
<i class="fas fa-language"></i>
|
||||||
|
<span data-i18n="settings.language">Language</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<div class="settings-input-group">
|
||||||
|
<label class="settings-input-label" data-i18n="settings.selectLanguage">Select
|
||||||
|
Language</label>
|
||||||
|
<select id="languageSelect" class="settings-input">
|
||||||
|
<!-- Options populated by i18n.js -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="logs-page" class="page">
|
||||||
|
<div class="logs-container">
|
||||||
|
<div class="logs-header">
|
||||||
|
<h2 class="logs-title">
|
||||||
|
<i class="fas fa-terminal"></i>
|
||||||
|
<span data-i18n="settings.logs">SYSTEM LOGS</span>
|
||||||
|
</h2>
|
||||||
|
<div class="logs-actions">
|
||||||
|
<button class="logs-action-btn" onclick="copyLogs()">
|
||||||
|
<i class="fas fa-copy"></i> <span data-i18n="settings.logsCopy">Copy</span>
|
||||||
|
</button>
|
||||||
|
<button class="logs-action-btn" onclick="refreshLogs()">
|
||||||
|
<i class="fas fa-sync-alt"></i> <span
|
||||||
|
data-i18n="settings.logsRefresh">Refresh</span>
|
||||||
|
</button>
|
||||||
|
<button class="logs-action-btn" onclick="openLogsFolder()">
|
||||||
|
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
|
||||||
|
Folder</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="logsTerminal" class="logs-terminal">
|
||||||
|
<div class="text-gray-500 text-center mt-10" data-i18n="settings.logsLoading">Loading
|
||||||
|
logs...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -575,20 +639,23 @@
|
|||||||
<span id="progressSpeed"></span>
|
<span id="progressSpeed"></span>
|
||||||
<span id="progressSize"></span>
|
<span id="progressSize"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div id="progressErrorContainer" class="progress-error-container" style="display: none;">
|
||||||
</div>
|
<div id="progressErrorMessage" class="progress-error-message"></div>
|
||||||
|
<div class="progress-retry-section">
|
||||||
<!-- Installation effects overlay -->
|
<span id="progressRetryInfo" class="progress-retry-info"></span>
|
||||||
<div id="installationEffects" class="installation-effects" style="display: none;">
|
<div class="progress-retry-buttons">
|
||||||
<div class="space-effects">
|
<button id="progressJRRetryBtn" class="progress-retry-btn" style="display: none;">
|
||||||
<div class="warp-line"></div>
|
Retry Java Download
|
||||||
<div class="warp-line"></div>
|
</button>
|
||||||
<div class="warp-line"></div>
|
<button id="progressPWRRetryBtn" class="progress-retry-btn" style="display: none;">
|
||||||
<div class="warp-line"></div>
|
Retry Game Download
|
||||||
<div class="warp-line"></div>
|
</button>
|
||||||
<div class="warp-line"></div>
|
<button id="progressRetryBtn" class="progress-retry-btn" style="display: none;">
|
||||||
<div class="warp-line"></div>
|
Retry Download
|
||||||
<div class="warp-line"></div>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -742,7 +809,11 @@
|
|||||||
<a href="https://github.com/ericiskoolbeans" target="_blank"
|
<a href="https://github.com/ericiskoolbeans" target="_blank"
|
||||||
class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
|
class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
|
||||||
<a href="https://github.com/fazrigading" target="_blank"
|
<a href="https://github.com/fazrigading" target="_blank"
|
||||||
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
|
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>,
|
||||||
|
<a href="https://github.com/Rahul-Sahani04" target="_blank"
|
||||||
|
class="text-blue-400 hover:text-blue-300 transition-colors">@Rahul-Sahani04</a>,
|
||||||
|
<a href="https://github.com/xSamiVS" target="_blank"
|
||||||
|
class="text-blue-400 hover:text-blue-300 transition-colors">@xSamiVS</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -809,7 +880,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/i18n.js"></script>
|
<script src="js/i18n.js"></script>
|
||||||
|
<script type="module" src="js/settings.js"></script>
|
||||||
<script type="module" src="js/update.js"></script>
|
<script type="module" src="js/update.js"></script>
|
||||||
|
<!-- updater.js disabled - using update.js instead which has skip button and macOS handling -->
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,13 @@ const i18n = (() => {
|
|||||||
let translations = {};
|
let translations = {};
|
||||||
const availableLanguages = [
|
const availableLanguages = [
|
||||||
{ code: 'en', name: 'English' },
|
{ code: 'en', name: 'English' },
|
||||||
{ code: 'es', name: 'Español' },
|
{ code: 'fr', name: 'Français' },
|
||||||
{ code: 'pt-BR', name: 'Português (Brasil)' }
|
{ code: 'de', name: 'Deutsch' },
|
||||||
|
{ code: 'sv', name: 'Svenska' },
|
||||||
|
{ code: 'es-ES', name: 'Español (España)' },
|
||||||
|
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
|
||||||
|
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
|
||||||
|
{ code: 'pl-PL', name: 'Polish (Poland)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Load single language file
|
// Load single language file
|
||||||
|
|||||||
@@ -40,18 +40,6 @@ export function setupInstallation() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup installation effects listeners
|
|
||||||
if (window.electronAPI && window.electronAPI.onInstallationStart) {
|
|
||||||
window.electronAPI.onInstallationStart(() => {
|
|
||||||
showInstallationEffects();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.onInstallationEnd) {
|
|
||||||
window.electronAPI.onInstallationEnd(() => {
|
|
||||||
hideInstallationEffects();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installGame() {
|
export async function installGame() {
|
||||||
@@ -60,8 +48,14 @@ export async function installGame() {
|
|||||||
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
|
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
|
||||||
const installPath = installPathInput ? installPathInput.value.trim() : '';
|
const installPath = installPathInput ? installPathInput.value.trim() : '';
|
||||||
|
|
||||||
|
const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked');
|
||||||
|
const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release';
|
||||||
|
|
||||||
|
console.log(`[Install] Installing game with branch: ${selectedBranch}`);
|
||||||
|
|
||||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
|
lockInstallForm();
|
||||||
if (installBtn) {
|
if (installBtn) {
|
||||||
installBtn.disabled = true;
|
installBtn.disabled = true;
|
||||||
installText.textContent = window.i18n ? window.i18n.t('install.installing') : 'INSTALLING...';
|
installText.textContent = window.i18n ? window.i18n.t('install.installing') : 'INSTALLING...';
|
||||||
@@ -69,7 +63,7 @@ export async function installGame() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.installGame) {
|
if (window.electronAPI && window.electronAPI.installGame) {
|
||||||
const result = await window.electronAPI.installGame(playerName, '', installPath);
|
const result = await window.electronAPI.installGame(playerName, '', installPath, selectedBranch);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
|
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';
|
||||||
@@ -78,8 +72,11 @@ export async function installGame() {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
|
// Sync player name to both launcher and settings inputs
|
||||||
const playerNameInput = document.getElementById('playerName');
|
const playerNameInput = document.getElementById('playerName');
|
||||||
if (playerNameInput) playerNameInput.value = playerName;
|
if (playerNameInput) playerNameInput.value = playerName;
|
||||||
|
const settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
|
if (settingsPlayerName) settingsPlayerName.value = playerName;
|
||||||
resetInstallButton();
|
resetInstallButton();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@@ -92,12 +89,7 @@ export async function installGame() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = window.i18n ? window.i18n.t('progress.installationFailed').replace('{error}', error.message) : `Installation failed: ${error.message}`;
|
const errorMsg = window.i18n ? window.i18n.t('progress.installationFailed').replace('{error}', error.message) : `Installation failed: ${error.message}`;
|
||||||
|
|
||||||
// Hide installation effects on error
|
// Reset button state and unlock form on error
|
||||||
if (window.hideInstallationEffects) {
|
|
||||||
window.hideInstallationEffects();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset button state on error
|
|
||||||
resetInstallButton();
|
resetInstallButton();
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
@@ -136,8 +128,11 @@ function simulateInstallation(playerName) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
|
// Sync player name to both launcher and settings inputs
|
||||||
const playerNameInput = document.getElementById('playerName');
|
const playerNameInput = document.getElementById('playerName');
|
||||||
if (playerNameInput) playerNameInput.value = playerName;
|
if (playerNameInput) playerNameInput.value = playerName;
|
||||||
|
const settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
|
if (settingsPlayerName) settingsPlayerName.value = playerName;
|
||||||
resetInstallButton();
|
resetInstallButton();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@@ -152,6 +147,35 @@ function resetInstallButton() {
|
|||||||
installBtn.disabled = false;
|
installBtn.disabled = false;
|
||||||
installText.textContent = 'INSTALL HYTALE';
|
installText.textContent = 'INSTALL HYTALE';
|
||||||
}
|
}
|
||||||
|
unlockInstallForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function lockInstallForm() {
|
||||||
|
const playerNameInput = document.getElementById('installPlayerName');
|
||||||
|
const installPathInput = document.getElementById('installPath');
|
||||||
|
const customCheckbox = document.getElementById('installCustomCheck');
|
||||||
|
const branchRadios = document.querySelectorAll('input[name="installBranch"]');
|
||||||
|
const browseBtn = document.querySelector('.browse-btn');
|
||||||
|
|
||||||
|
if (playerNameInput) playerNameInput.disabled = true;
|
||||||
|
if (installPathInput) installPathInput.disabled = true;
|
||||||
|
if (customCheckbox) customCheckbox.disabled = true;
|
||||||
|
if (browseBtn) browseBtn.disabled = true;
|
||||||
|
branchRadios.forEach(radio => radio.disabled = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlockInstallForm() {
|
||||||
|
const playerNameInput = document.getElementById('installPlayerName');
|
||||||
|
const installPathInput = document.getElementById('installPath');
|
||||||
|
const customCheckbox = document.getElementById('installCustomCheck');
|
||||||
|
const branchRadios = document.querySelectorAll('input[name="installBranch"]');
|
||||||
|
const browseBtn = document.querySelector('.browse-btn');
|
||||||
|
|
||||||
|
if (playerNameInput) playerNameInput.disabled = false;
|
||||||
|
if (installPathInput) installPathInput.disabled = false;
|
||||||
|
if (customCheckbox) customCheckbox.disabled = false;
|
||||||
|
if (browseBtn) browseBtn.disabled = false;
|
||||||
|
branchRadios.forEach(radio => radio.disabled = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function browseInstallPath() {
|
export async function browseInstallPath() {
|
||||||
@@ -228,9 +252,3 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
setupInstallation();
|
setupInstallation();
|
||||||
await checkGameStatusAndShowInterface();
|
await checkGameStatusAndShowInterface();
|
||||||
});
|
});
|
||||||
window.browseInstallPath = browseInstallPath;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
setupInstallation();
|
|
||||||
await checkGameStatusAndShowInterface();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
let API_KEY = null;
|
let API_KEY = "$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32";
|
||||||
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
|
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
|
||||||
const HYTALE_GAME_ID = 70216;
|
const HYTALE_GAME_ID = 70216;
|
||||||
|
|
||||||
@@ -13,7 +13,6 @@ let modsTotalPages = 1;
|
|||||||
export async function initModsManager() {
|
export async function initModsManager() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.getEnvVar) {
|
if (window.electronAPI && window.electronAPI.getEnvVar) {
|
||||||
API_KEY = await window.electronAPI.getEnvVar('CURSEFORGE_API_KEY');
|
|
||||||
console.log('Loaded API Key:', API_KEY ? 'Yes' : 'No');
|
console.log('Loaded API Key:', API_KEY ? 'Yes' : 'No');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -201,10 +200,15 @@ async function loadBrowseMods() {
|
|||||||
browseContainer.innerHTML = `
|
browseContainer.innerHTML = `
|
||||||
<div class=\"empty-browse-mods\">
|
<div class=\"empty-browse-mods\">
|
||||||
<i class=\"fas fa-key\"></i>
|
<i class=\"fas fa-key\"></i>
|
||||||
<h4>API Key Required</h4>
|
<h4 data-i18n="mods.apiKeyRequired">API Key Required</h4>
|
||||||
<p>CurseForge API key is needed to browse mods</p>
|
<p data-i18n="mods.apiKeyRequiredDesc">CurseForge API key is needed to browse mods</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
if (window.i18n) {
|
||||||
|
const container = modsContainer.querySelector('.empty-browse-mods');
|
||||||
|
container.querySelector('h4').textContent = window.i18n.t('mods.apiKeyRequired');
|
||||||
|
container.querySelector('p').textContent = window.i18n.t('mods.apiKeyRequiredDesc');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ let customJavaCheck;
|
|||||||
let customJavaOptions;
|
let customJavaOptions;
|
||||||
let customJavaPath;
|
let customJavaPath;
|
||||||
let browseJavaBtn;
|
let browseJavaBtn;
|
||||||
let settingsPlayerName;
|
let settingsPlayerName;
|
||||||
let discordRPCCheck;
|
let discordRPCCheck;
|
||||||
let closeLauncherCheck;
|
let closeLauncherCheck;
|
||||||
let gpuPreferenceRadios;
|
let launcherHwAccelCheck;
|
||||||
|
let gpuPreferenceRadios;
|
||||||
|
let gameBranchRadios;
|
||||||
|
|
||||||
|
|
||||||
// UUID Management elements
|
// UUID Management elements
|
||||||
let currentUuidDisplay;
|
let currentUuidDisplay;
|
||||||
@@ -29,7 +31,7 @@ function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmTe
|
|||||||
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
|
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
|
||||||
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
|
confirmText = confirmText || (window.i18n ? window.i18n.t('common.confirm') : 'Confirm');
|
||||||
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
|
cancelText = cancelText || (window.i18n ? window.i18n.t('common.cancel') : 'Cancel');
|
||||||
|
|
||||||
const existingModal = document.querySelector('.custom-confirm-modal');
|
const existingModal = document.querySelector('.custom-confirm-modal');
|
||||||
if (existingModal) {
|
if (existingModal) {
|
||||||
existingModal.remove();
|
existingModal.remove();
|
||||||
@@ -151,9 +153,9 @@ function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmTe
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function initSettings() {
|
export async function initSettings() {
|
||||||
setupSettingsElements();
|
setupSettingsElements();
|
||||||
loadAllSettings();
|
await loadAllSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSettingsElements() {
|
function setupSettingsElements() {
|
||||||
@@ -161,11 +163,15 @@ function setupSettingsElements() {
|
|||||||
customJavaOptions = document.getElementById('customJavaOptions');
|
customJavaOptions = document.getElementById('customJavaOptions');
|
||||||
customJavaPath = document.getElementById('customJavaPath');
|
customJavaPath = document.getElementById('customJavaPath');
|
||||||
browseJavaBtn = document.getElementById('browseJavaBtn');
|
browseJavaBtn = document.getElementById('browseJavaBtn');
|
||||||
settingsPlayerName = document.getElementById('settingsPlayerName');
|
settingsPlayerName = document.getElementById('settingsPlayerName');
|
||||||
discordRPCCheck = document.getElementById('discordRPCCheck');
|
discordRPCCheck = document.getElementById('discordRPCCheck');
|
||||||
closeLauncherCheck = document.getElementById('closeLauncherCheck');
|
closeLauncherCheck = document.getElementById('closeLauncherCheck');
|
||||||
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
|
launcherHwAccelCheck = document.getElementById('launcherHwAccelCheck');
|
||||||
|
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
|
||||||
|
gameBranchRadios = document.querySelectorAll('input[name="gameBranch"]');
|
||||||
|
|
||||||
|
console.log('[Settings] gameBranchRadios found:', gameBranchRadios.length);
|
||||||
|
|
||||||
|
|
||||||
// UUID Management elements
|
// UUID Management elements
|
||||||
currentUuidDisplay = document.getElementById('currentUuid');
|
currentUuidDisplay = document.getElementById('currentUuid');
|
||||||
@@ -194,14 +200,18 @@ function setupSettingsElements() {
|
|||||||
settingsPlayerName.addEventListener('change', savePlayerName);
|
settingsPlayerName.addEventListener('change', savePlayerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (discordRPCCheck) {
|
if (discordRPCCheck) {
|
||||||
discordRPCCheck.addEventListener('change', saveDiscordRPC);
|
discordRPCCheck.addEventListener('change', saveDiscordRPC);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closeLauncherCheck) {
|
if (closeLauncherCheck) {
|
||||||
closeLauncherCheck.addEventListener('change', saveCloseLauncher);
|
closeLauncherCheck.addEventListener('change', saveCloseLauncher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (launcherHwAccelCheck) {
|
||||||
|
launcherHwAccelCheck.addEventListener('change', saveLauncherHwAccel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// UUID event listeners
|
// UUID event listeners
|
||||||
if (copyUuidBtn) {
|
if (copyUuidBtn) {
|
||||||
@@ -252,11 +262,17 @@ function setupSettingsElements() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gameBranchRadios) {
|
||||||
|
gameBranchRadios.forEach(radio => {
|
||||||
|
radio.addEventListener('change', handleBranchChange);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleCustomJava() {
|
function toggleCustomJava() {
|
||||||
if (!customJavaOptions) return;
|
if (!customJavaOptions) return;
|
||||||
|
|
||||||
if (customJavaCheck && customJavaCheck.checked) {
|
if (customJavaCheck && customJavaCheck.checked) {
|
||||||
customJavaOptions.style.display = 'block';
|
customJavaOptions.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
@@ -319,12 +335,12 @@ async function saveDiscordRPC() {
|
|||||||
if (window.electronAPI && window.electronAPI.saveDiscordRPC && discordRPCCheck) {
|
if (window.electronAPI && window.electronAPI.saveDiscordRPC && discordRPCCheck) {
|
||||||
const enabled = discordRPCCheck.checked;
|
const enabled = discordRPCCheck.checked;
|
||||||
console.log('Saving Discord RPC setting:', enabled);
|
console.log('Saving Discord RPC setting:', enabled);
|
||||||
|
|
||||||
const result = await window.electronAPI.saveDiscordRPC(enabled);
|
const result = await window.electronAPI.saveDiscordRPC(enabled);
|
||||||
|
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
console.log('Discord RPC setting saved successfully:', enabled);
|
console.log('Discord RPC setting saved successfully:', enabled);
|
||||||
|
|
||||||
// Feedback visuel pour l'utilisateur
|
// Feedback visuel pour l'utilisateur
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
const msg = window.i18n ? window.i18n.t('notifications.discordEnabled') : 'Discord Rich Presence enabled';
|
const msg = window.i18n ? window.i18n.t('notifications.discordEnabled') : 'Discord Rich Presence enabled';
|
||||||
@@ -344,50 +360,79 @@ async function saveDiscordRPC() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDiscordRPC() {
|
async function loadDiscordRPC() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
|
if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
|
||||||
const enabled = await window.electronAPI.loadDiscordRPC();
|
const enabled = await window.electronAPI.loadDiscordRPC();
|
||||||
if (discordRPCCheck) {
|
if (discordRPCCheck) {
|
||||||
discordRPCCheck.checked = enabled;
|
discordRPCCheck.checked = enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading Discord RPC setting:', error);
|
console.error('Error loading Discord RPC setting:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCloseLauncher() {
|
async function saveCloseLauncher() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) {
|
if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) {
|
||||||
const enabled = closeLauncherCheck.checked;
|
const enabled = closeLauncherCheck.checked;
|
||||||
await window.electronAPI.saveCloseLauncher(enabled);
|
await window.electronAPI.saveCloseLauncher(enabled);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving close launcher setting:', error);
|
console.error('Error saving close launcher setting:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCloseLauncher() {
|
async function loadCloseLauncher() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.loadCloseLauncher) {
|
if (window.electronAPI && window.electronAPI.loadCloseLauncher) {
|
||||||
const enabled = await window.electronAPI.loadCloseLauncher();
|
const enabled = await window.electronAPI.loadCloseLauncher();
|
||||||
if (closeLauncherCheck) {
|
if (closeLauncherCheck) {
|
||||||
closeLauncherCheck.checked = enabled;
|
closeLauncherCheck.checked = enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading close launcher setting:', error);
|
console.error('Error loading close launcher setting:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveLauncherHwAccel() {
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.saveLauncherHardwareAcceleration && launcherHwAccelCheck) {
|
||||||
|
const enabled = launcherHwAccelCheck.checked;
|
||||||
|
await window.electronAPI.saveLauncherHardwareAcceleration(enabled);
|
||||||
|
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.hwAccelSaved') : 'Setting saved. Please restart the launcher to apply changes.';
|
||||||
|
showNotification(msg, 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving hardware acceleration setting:', error);
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.hwAccelSaveFailed') : 'Failed to save setting';
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLauncherHwAccel() {
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.loadLauncherHardwareAcceleration) {
|
||||||
|
const enabled = await window.electronAPI.loadLauncherHardwareAcceleration();
|
||||||
|
if (launcherHwAccelCheck) {
|
||||||
|
launcherHwAccelCheck.checked = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading hardware acceleration setting:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function savePlayerName() {
|
async function savePlayerName() {
|
||||||
try {
|
try {
|
||||||
if (!window.electronAPI || !settingsPlayerName) return;
|
if (!window.electronAPI || !settingsPlayerName) return;
|
||||||
|
|
||||||
const playerName = settingsPlayerName.value.trim();
|
const playerName = settingsPlayerName.value.trim();
|
||||||
|
|
||||||
if (!playerName) {
|
if (!playerName) {
|
||||||
const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a valid player name';
|
const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a valid player name';
|
||||||
showNotification(msg, 'error');
|
showNotification(msg, 'error');
|
||||||
@@ -397,7 +442,7 @@ async function savePlayerName() {
|
|||||||
await window.electronAPI.saveUsername(playerName);
|
await window.electronAPI.saveUsername(playerName);
|
||||||
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');
|
||||||
|
|
||||||
} 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';
|
||||||
@@ -408,7 +453,7 @@ async function savePlayerName() {
|
|||||||
async function loadPlayerName() {
|
async function loadPlayerName() {
|
||||||
try {
|
try {
|
||||||
if (!window.electronAPI || !settingsPlayerName) return;
|
if (!window.electronAPI || !settingsPlayerName) return;
|
||||||
|
|
||||||
const savedName = await window.electronAPI.loadUsername();
|
const savedName = await window.electronAPI.loadUsername();
|
||||||
if (savedName) {
|
if (savedName) {
|
||||||
settingsPlayerName.value = savedName;
|
settingsPlayerName.value = savedName;
|
||||||
@@ -491,15 +536,17 @@ async function loadGpuPreference() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAllSettings() {
|
async function loadAllSettings() {
|
||||||
await loadCustomJavaPath();
|
await loadCustomJavaPath();
|
||||||
await loadPlayerName();
|
await loadPlayerName();
|
||||||
await loadCurrentUuid();
|
await loadCurrentUuid();
|
||||||
await loadDiscordRPC();
|
await loadDiscordRPC();
|
||||||
await loadCloseLauncher();
|
await loadCloseLauncher();
|
||||||
await loadGpuPreference();
|
await loadLauncherHwAccel();
|
||||||
}
|
await loadGpuPreference();
|
||||||
|
await loadVersionBranch();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function openGameLocation() {
|
async function openGameLocation() {
|
||||||
try {
|
try {
|
||||||
@@ -532,7 +579,8 @@ document.addEventListener('DOMContentLoaded', initSettings);
|
|||||||
|
|
||||||
window.SettingsAPI = {
|
window.SettingsAPI = {
|
||||||
getCurrentJavaPath,
|
getCurrentJavaPath,
|
||||||
getCurrentPlayerName
|
getCurrentPlayerName,
|
||||||
|
reloadBranch: loadVersionBranch
|
||||||
};
|
};
|
||||||
|
|
||||||
async function loadCurrentUuid() {
|
async function loadCurrentUuid() {
|
||||||
@@ -571,7 +619,7 @@ async function regenerateCurrentUuid() {
|
|||||||
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
|
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
|
||||||
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
|
||||||
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
message,
|
message,
|
||||||
title,
|
title,
|
||||||
@@ -602,7 +650,7 @@ async function performRegenerateUuid() {
|
|||||||
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
|
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
|
||||||
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerated') : 'New UUID generated successfully!';
|
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerated') : 'New UUID generated successfully!';
|
||||||
showNotification(msg, 'success');
|
showNotification(msg, 'success');
|
||||||
|
|
||||||
if (uuidModal && uuidModal.style.display !== 'none') {
|
if (uuidModal && uuidModal.style.display !== 'none') {
|
||||||
await loadAllUuids();
|
await loadAllUuids();
|
||||||
}
|
}
|
||||||
@@ -640,7 +688,7 @@ function closeUuidModal() {
|
|||||||
async function loadAllUuids() {
|
async function loadAllUuids() {
|
||||||
try {
|
try {
|
||||||
if (!uuidList) return;
|
if (!uuidList) return;
|
||||||
|
|
||||||
uuidList.innerHTML = `
|
uuidList.innerHTML = `
|
||||||
<div class="uuid-loading">
|
<div class="uuid-loading">
|
||||||
<i class="fas fa-spinner fa-spin"></i>
|
<i class="fas fa-spinner fa-spin"></i>
|
||||||
@@ -650,7 +698,7 @@ async function loadAllUuids() {
|
|||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
|
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
|
||||||
const mappings = await window.electronAPI.getAllUuidMappings();
|
const mappings = await window.electronAPI.getAllUuidMappings();
|
||||||
|
|
||||||
if (mappings.length === 0) {
|
if (mappings.length === 0) {
|
||||||
uuidList.innerHTML = `
|
uuidList.innerHTML = `
|
||||||
<div class="uuid-loading">
|
<div class="uuid-loading">
|
||||||
@@ -662,11 +710,11 @@ async function loadAllUuids() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uuidList.innerHTML = '';
|
uuidList.innerHTML = '';
|
||||||
|
|
||||||
for (const mapping of mappings) {
|
for (const mapping of mappings) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = `uuid-list-item${mapping.isCurrent ? ' current' : ''}`;
|
item.className = `uuid-list-item${mapping.isCurrent ? ' current' : ''}`;
|
||||||
|
|
||||||
item.innerHTML = `
|
item.innerHTML = `
|
||||||
<div class="uuid-item-info">
|
<div class="uuid-item-info">
|
||||||
<div class="uuid-item-username">${escapeHtml(mapping.username)}</div>
|
<div class="uuid-item-username">${escapeHtml(mapping.username)}</div>
|
||||||
@@ -682,7 +730,7 @@ async function loadAllUuids() {
|
|||||||
</button>` : ''}
|
</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
uuidList.appendChild(item);
|
uuidList.appendChild(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -725,7 +773,7 @@ async function setCustomUuid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uuid = customUuidInput.value.trim();
|
const uuid = customUuidInput.value.trim();
|
||||||
|
|
||||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
if (!uuidRegex.test(uuid)) {
|
if (!uuidRegex.test(uuid)) {
|
||||||
const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
|
const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
|
||||||
@@ -755,33 +803,33 @@ 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();
|
const username = getCurrentPlayerName();
|
||||||
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
|
|
||||||
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
|
|
||||||
if (customUuidInput) customUuidInput.value = '';
|
|
||||||
|
|
||||||
const msg = window.i18n ? window.i18n.t('notifications.uuidSetSuccess') : 'Custom UUID set successfully!';
|
|
||||||
showNotification(msg, 'success');
|
|
||||||
|
|
||||||
await loadAllUuids();
|
|
||||||
} else {
|
|
||||||
throw new Error(result.error || 'Failed to set custom UUID');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error setting custom UUID:', error);
|
|
||||||
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed').replace('{error}', error.message) : `Failed to set custom UUID: ${error.message}`;
|
|
||||||
showNotification(msg, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.copyUuid = async function(uuid) {
|
if (result.success) {
|
||||||
|
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
|
||||||
|
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
|
||||||
|
if (customUuidInput) customUuidInput.value = '';
|
||||||
|
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetSuccess') : 'Custom UUID set successfully!';
|
||||||
|
showNotification(msg, 'success');
|
||||||
|
|
||||||
|
await loadAllUuids();
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Failed to set custom UUID');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting custom UUID:', error);
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.uuidSetFailed').replace('{error}', error.message) : `Failed to set custom UUID: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.copyUuid = async function (uuid) {
|
||||||
try {
|
try {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
await navigator.clipboard.writeText(uuid);
|
await navigator.clipboard.writeText(uuid);
|
||||||
@@ -795,13 +843,13 @@ window.copyUuid = async function(uuid) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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.`;
|
||||||
const title = window.i18n ? window.i18n.t('confirm.deleteUuidTitle') : 'Delete UUID';
|
const title = window.i18n ? window.i18n.t('confirm.deleteUuidTitle') : 'Delete UUID';
|
||||||
const confirmBtn = window.i18n ? window.i18n.t('confirm.deleteUuidButton') : 'Delete';
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.deleteUuidButton') : 'Delete';
|
||||||
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
showCustomConfirm(
|
showCustomConfirm(
|
||||||
message,
|
message,
|
||||||
title,
|
title,
|
||||||
@@ -822,21 +870,21 @@ window.deleteUuid = async function(username) {
|
|||||||
async function performDeleteUuid(username) {
|
async function performDeleteUuid(username) {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.deleteUuidForUser) {
|
if (window.electronAPI && window.electronAPI.deleteUuidForUser) {
|
||||||
const result = await window.electronAPI.deleteUuidForUser(username);
|
const result = await window.electronAPI.deleteUuidForUser(username);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
|
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
|
||||||
showNotification(msg, 'success');
|
showNotification(msg, 'success');
|
||||||
await loadAllUuids();
|
await loadAllUuids();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(result.error || 'Failed to delete UUID');
|
throw new Error(result.error || 'Failed to delete UUID');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting UUID:', error);
|
|
||||||
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed').replace('{error}', error.message) : `Failed to delete UUID: ${error.message}`;
|
|
||||||
showNotification(msg, 'error');
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting UUID:', error);
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed').replace('{error}', error.message) : `Failed to delete UUID: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
@@ -891,4 +939,198 @@ function showNotification(message, type = 'info') {
|
|||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}// Append this to settings.js for branch management
|
||||||
|
|
||||||
|
// === Game Branch Management ===
|
||||||
|
async function handleBranchChange(event) {
|
||||||
|
const newBranch = event.target.value;
|
||||||
|
const currentBranch = await loadVersionBranch();
|
||||||
|
|
||||||
|
if (newBranch === currentBranch) {
|
||||||
|
return; // No change
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm branch change
|
||||||
|
const branchName = window.i18n ?
|
||||||
|
window.i18n.t(`settings.branch${newBranch === 'pre-release' ? 'PreRelease' : 'Release'}`) :
|
||||||
|
newBranch;
|
||||||
|
|
||||||
|
const message = window.i18n ?
|
||||||
|
window.i18n.t('settings.branchWarning') :
|
||||||
|
'Changing branch will download and install a different game version';
|
||||||
|
|
||||||
|
showCustomConfirm(
|
||||||
|
message,
|
||||||
|
window.i18n ? window.i18n.t('settings.gameBranch') : 'Game Branch',
|
||||||
|
async () => {
|
||||||
|
await switchBranch(newBranch);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Cancel: revert radio selection
|
||||||
|
loadVersionBranch().then(branch => {
|
||||||
|
const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${branch}"]`);
|
||||||
|
if (radioToCheck) {
|
||||||
|
radioToCheck.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchBranch(newBranch) {
|
||||||
|
try {
|
||||||
|
const switchingMsg = window.i18n ?
|
||||||
|
window.i18n.t('settings.branchSwitching').replace('{branch}', newBranch) :
|
||||||
|
`Switching to ${newBranch}...`;
|
||||||
|
|
||||||
|
showNotification(switchingMsg, 'info');
|
||||||
|
|
||||||
|
// Lock play button
|
||||||
|
const playButton = document.getElementById('playButton');
|
||||||
|
if (playButton) {
|
||||||
|
playButton.disabled = true;
|
||||||
|
playButton.classList.add('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// DON'T save branch yet - wait for installation confirmation
|
||||||
|
|
||||||
|
// Suggest reinstalling
|
||||||
|
setTimeout(() => {
|
||||||
|
const branchLabel = newBranch === 'release' ?
|
||||||
|
(window.i18n ? window.i18n.t('install.releaseVersion') : 'Release') :
|
||||||
|
(window.i18n ? window.i18n.t('install.preReleaseVersion') : 'Pre-Release');
|
||||||
|
|
||||||
|
const confirmMsg = window.i18n ?
|
||||||
|
window.i18n.t('settings.branchInstallConfirm').replace('{branch}', branchLabel) :
|
||||||
|
`The game will be installed for the ${branchLabel} branch. Continue?`;
|
||||||
|
|
||||||
|
showCustomConfirm(
|
||||||
|
confirmMsg,
|
||||||
|
window.i18n ? window.i18n.t('settings.installRequired') : 'Installation Required',
|
||||||
|
async () => {
|
||||||
|
// Show progress and trigger game installation
|
||||||
|
if (window.LauncherUI) {
|
||||||
|
window.LauncherUI.showProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const playerName = await window.electronAPI.loadUsername();
|
||||||
|
const result = await window.electronAPI.installGame(playerName || 'Player', '', '', newBranch);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Save branch ONLY after successful installation
|
||||||
|
await window.electronAPI.saveVersionBranch(newBranch);
|
||||||
|
|
||||||
|
const switchedMsg = window.i18n ?
|
||||||
|
window.i18n.t('settings.branchSwitched').replace('{branch}', newBranch) :
|
||||||
|
`Switched to ${newBranch} successfully!`;
|
||||||
|
|
||||||
|
const successMsg = window.i18n ?
|
||||||
|
window.i18n.t('progress.installationComplete') :
|
||||||
|
'Installation completed successfully!';
|
||||||
|
|
||||||
|
showNotification(switchedMsg, 'success');
|
||||||
|
showNotification(successMsg, 'success');
|
||||||
|
|
||||||
|
// Refresh radio buttons to reflect the new branch
|
||||||
|
await loadVersionBranch();
|
||||||
|
console.log('[Settings] Radio buttons updated after branch switch');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (window.LauncherUI) {
|
||||||
|
window.LauncherUI.hideProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock play button
|
||||||
|
const playButton = document.getElementById('playButton');
|
||||||
|
if (playButton) {
|
||||||
|
playButton.disabled = false;
|
||||||
|
playButton.classList.remove('disabled');
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Installation failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Installation error:', error);
|
||||||
|
const errorMsg = window.i18n ?
|
||||||
|
window.i18n.t('progress.installationFailed').replace('{error}', error.message) :
|
||||||
|
`Installation failed: ${error.message}`;
|
||||||
|
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
|
|
||||||
|
if (window.LauncherUI) {
|
||||||
|
window.LauncherUI.hideProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert radio selection to old branch
|
||||||
|
loadVersionBranch().then(oldBranch => {
|
||||||
|
const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${oldBranch}"]`);
|
||||||
|
if (radioToCheck) {
|
||||||
|
radioToCheck.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unlock play button
|
||||||
|
const playButton = document.getElementById('playButton');
|
||||||
|
if (playButton) {
|
||||||
|
playButton.disabled = false;
|
||||||
|
playButton.classList.remove('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Cancel - unlock play button
|
||||||
|
const playButton = document.getElementById('playButton');
|
||||||
|
if (playButton) {
|
||||||
|
playButton.disabled = false;
|
||||||
|
playButton.classList.remove('disabled');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
window.i18n ? window.i18n.t('common.install') : 'Install',
|
||||||
|
window.i18n ? window.i18n.t('common.cancel') : 'Cancel'
|
||||||
|
);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error switching branch:', error);
|
||||||
|
showNotification(`Failed to switch branch: ${error.message}`, 'error');
|
||||||
|
|
||||||
|
// Revert radio selection
|
||||||
|
loadVersionBranch().then(branch => {
|
||||||
|
const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${branch}"]`);
|
||||||
|
if (radioToCheck) {
|
||||||
|
radioToCheck.checked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadVersionBranch() {
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.loadVersionBranch) {
|
||||||
|
const branch = await window.electronAPI.loadVersionBranch();
|
||||||
|
console.log('[Settings] Loaded version_branch from config:', branch);
|
||||||
|
|
||||||
|
// Use default if branch is null/undefined
|
||||||
|
const selectedBranch = branch || 'release';
|
||||||
|
console.log('[Settings] Selected branch:', selectedBranch);
|
||||||
|
|
||||||
|
// Update radio buttons
|
||||||
|
if (gameBranchRadios && gameBranchRadios.length > 0) {
|
||||||
|
gameBranchRadios.forEach(radio => {
|
||||||
|
radio.checked = radio.value === selectedBranch;
|
||||||
|
console.log(`[Settings] Radio ${radio.value}: ${radio.checked ? 'checked' : 'unchecked'}`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('[Settings] gameBranchRadios not found or empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedBranch;
|
||||||
|
}
|
||||||
|
return 'release'; // Default
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading version branch:', error);
|
||||||
|
return 'release';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
521
GUI/js/ui.js
521
GUI/js/ui.js
@@ -6,6 +6,24 @@ let progressText;
|
|||||||
let progressPercent;
|
let progressPercent;
|
||||||
let progressSpeed;
|
let progressSpeed;
|
||||||
let progressSize;
|
let progressSize;
|
||||||
|
let progressErrorContainer;
|
||||||
|
let progressErrorMessage;
|
||||||
|
let progressRetryInfo;
|
||||||
|
let progressRetryBtn;
|
||||||
|
let progressJRRetryBtn;
|
||||||
|
let progressPWRRetryBtn;
|
||||||
|
|
||||||
|
// Download retry state
|
||||||
|
let currentDownloadState = {
|
||||||
|
isDownloading: false,
|
||||||
|
canRetry: false,
|
||||||
|
retryData: null,
|
||||||
|
lastError: null,
|
||||||
|
errorType: null,
|
||||||
|
branch: null,
|
||||||
|
fileName: null,
|
||||||
|
cacheDir: null
|
||||||
|
};
|
||||||
|
|
||||||
function showPage(pageId) {
|
function showPage(pageId) {
|
||||||
const pages = document.querySelectorAll('.page');
|
const pages = document.querySelectorAll('.page');
|
||||||
@@ -13,6 +31,15 @@ function showPage(pageId) {
|
|||||||
if (page.id === pageId) {
|
if (page.id === pageId) {
|
||||||
page.classList.add('active');
|
page.classList.add('active');
|
||||||
page.style.display = '';
|
page.style.display = '';
|
||||||
|
|
||||||
|
// Reload settings when settings page becomes visible
|
||||||
|
if (pageId === 'settings-page') {
|
||||||
|
console.log('[UI] Settings page activated, reloading branch...');
|
||||||
|
// Dynamically import and call loadVersionBranch from settings
|
||||||
|
if (window.SettingsAPI && window.SettingsAPI.reloadBranch) {
|
||||||
|
window.SettingsAPI.reloadBranch();
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
page.classList.remove('active');
|
page.classList.remove('active');
|
||||||
page.style.display = 'none';
|
page.style.display = 'none';
|
||||||
@@ -144,6 +171,12 @@ function hideProgress() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProgress(data) {
|
function updateProgress(data) {
|
||||||
|
// Handle retry state
|
||||||
|
if (data.retryState) {
|
||||||
|
currentDownloadState.retryData = data.retryState;
|
||||||
|
updateRetryState(data.retryState);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.message && progressText) {
|
if (data.message && progressText) {
|
||||||
progressText.textContent = data.message;
|
progressText.textContent = data.message;
|
||||||
}
|
}
|
||||||
@@ -162,6 +195,120 @@ function updateProgress(data) {
|
|||||||
if (progressSpeed) progressSpeed.textContent = `${speedMB} MB/s`;
|
if (progressSpeed) progressSpeed.textContent = `${speedMB} MB/s`;
|
||||||
if (progressSize) progressSize.textContent = `${downloadedMB} / ${totalMB} MB`;
|
if (progressSize) progressSize.textContent = `${downloadedMB} / ${totalMB} MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle error states with enhanced categorization
|
||||||
|
// Don't show error during automatic retries - let the retry message display instead
|
||||||
|
if ((data.error || (data.message && data.message.includes('failed'))) &&
|
||||||
|
!(data.retryState && data.retryState.isAutomaticRetry)) {
|
||||||
|
const errorType = categorizeError(data.message);
|
||||||
|
console.log('[UI] Showing download error:', { message: data.message, canRetry: data.canRetry, errorType });
|
||||||
|
showDownloadError(data.message, data.canRetry, errorType, data);
|
||||||
|
} else if (data.percent === 100) {
|
||||||
|
hideDownloadError();
|
||||||
|
} else if (data.retryState && data.retryState.isAutomaticRetry) {
|
||||||
|
// Hide any existing error during automatic retries
|
||||||
|
hideDownloadError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRetryState(retryState) {
|
||||||
|
if (!progressRetryInfo) return;
|
||||||
|
|
||||||
|
if (retryState.isAutomaticRetry && retryState.automaticStallRetries > 0) {
|
||||||
|
// Show automatic stall retry count
|
||||||
|
progressRetryInfo.textContent = `Auto-retry ${retryState.automaticStallRetries}/3`;
|
||||||
|
progressRetryInfo.style.display = 'block';
|
||||||
|
progressRetryInfo.style.background = 'rgba(255, 193, 7, 0.2)'; // Light orange background for auto-retries
|
||||||
|
progressRetryInfo.style.color = '#ff9800'; // Orange text for auto-retries
|
||||||
|
} else if (retryState.attempts > 1) {
|
||||||
|
// Show manual retry count
|
||||||
|
progressRetryInfo.textContent = `Attempt ${retryState.attempts}/${retryState.maxRetries}`;
|
||||||
|
progressRetryInfo.style.display = 'block';
|
||||||
|
progressRetryInfo.style.background = ''; // Reset background
|
||||||
|
progressRetryInfo.style.color = ''; // Reset color
|
||||||
|
} else {
|
||||||
|
progressRetryInfo.style.display = 'none';
|
||||||
|
progressRetryInfo.style.background = ''; // Reset background
|
||||||
|
progressRetryInfo.style.color = ''; // Reset color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDownloadError(errorMessage, canRetry = true, errorType = 'general', data = null) {
|
||||||
|
if (!progressErrorContainer || !progressErrorMessage) return;
|
||||||
|
|
||||||
|
console.log('[UI] showDownloadError called with:', { errorMessage, canRetry, errorType, data });
|
||||||
|
console.log('[UI] Data properties:', {
|
||||||
|
hasData: !!data,
|
||||||
|
hasRetryData: !!(data && data.retryData),
|
||||||
|
dataErrorType: data && data.errorType,
|
||||||
|
dataIsJREError: data && data.retryData && data.retryData.isJREError
|
||||||
|
});
|
||||||
|
|
||||||
|
currentDownloadState.lastError = errorMessage;
|
||||||
|
currentDownloadState.canRetry = canRetry;
|
||||||
|
currentDownloadState.errorType = errorType;
|
||||||
|
|
||||||
|
// Update retry context if available
|
||||||
|
if (data && data.retryData) {
|
||||||
|
currentDownloadState.branch = data.retryData.branch;
|
||||||
|
currentDownloadState.fileName = data.retryData.fileName;
|
||||||
|
currentDownloadState.cacheDir = data.retryData.cacheDir;
|
||||||
|
// Override errorType if specified in data
|
||||||
|
if (data.errorType) {
|
||||||
|
currentDownloadState.errorType = data.errorType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide all retry buttons first
|
||||||
|
if (progressRetryBtn) progressRetryBtn.style.display = 'none';
|
||||||
|
if (progressJRRetryBtn) progressJRRetryBtn.style.display = 'none';
|
||||||
|
if (progressPWRRetryBtn) progressPWRRetryBtn.style.display = 'none';
|
||||||
|
|
||||||
|
// User-friendly error messages
|
||||||
|
const userMessage = getErrorMessage(errorMessage, errorType);
|
||||||
|
progressErrorMessage.textContent = userMessage;
|
||||||
|
progressErrorContainer.style.display = 'block';
|
||||||
|
|
||||||
|
// Show appropriate retry button based on error type
|
||||||
|
if (canRetry) {
|
||||||
|
if (errorType === 'jre') {
|
||||||
|
if (progressJRRetryBtn) {
|
||||||
|
console.log('[UI] Showing JRE retry button');
|
||||||
|
progressJRRetryBtn.style.display = 'block';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// All other errors use PWR retry button (game download, butler, etc.)
|
||||||
|
if (progressPWRRetryBtn) {
|
||||||
|
console.log('[UI] Showing PWR retry button');
|
||||||
|
progressPWRRetryBtn.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add visual indicators based on error type
|
||||||
|
progressErrorContainer.className = `progress-error-container error-${errorType}`;
|
||||||
|
|
||||||
|
if (progressOverlay) {
|
||||||
|
progressOverlay.classList.add('error-state');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDownloadError() {
|
||||||
|
if (!progressErrorContainer) return;
|
||||||
|
|
||||||
|
// Hide all retry buttons
|
||||||
|
if (progressRetryBtn) progressRetryBtn.style.display = 'none';
|
||||||
|
if (progressJRRetryBtn) progressJRRetryBtn.style.display = 'none';
|
||||||
|
if (progressPWRRetryBtn) progressPWRRetryBtn.style.display = 'none';
|
||||||
|
|
||||||
|
progressErrorContainer.style.display = 'none';
|
||||||
|
currentDownloadState.canRetry = false;
|
||||||
|
currentDownloadState.lastError = null;
|
||||||
|
currentDownloadState.errorType = null;
|
||||||
|
|
||||||
|
if (progressOverlay) {
|
||||||
|
progressOverlay.classList.remove('error-state');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupAnimations() {
|
function setupAnimations() {
|
||||||
@@ -478,10 +625,19 @@ function setupUI() {
|
|||||||
progressPercent = document.getElementById('progressPercent');
|
progressPercent = document.getElementById('progressPercent');
|
||||||
progressSpeed = document.getElementById('progressSpeed');
|
progressSpeed = document.getElementById('progressSpeed');
|
||||||
progressSize = document.getElementById('progressSize');
|
progressSize = document.getElementById('progressSize');
|
||||||
|
progressErrorContainer = document.getElementById('progressErrorContainer');
|
||||||
|
progressErrorMessage = document.getElementById('progressErrorMessage');
|
||||||
|
progressRetryInfo = document.getElementById('progressRetryInfo');
|
||||||
|
progressRetryBtn = document.getElementById('progressRetryBtn');
|
||||||
|
progressJRRetryBtn = document.getElementById('progressJRRetryBtn');
|
||||||
|
progressPWRRetryBtn = document.getElementById('progressPWRRetryBtn');
|
||||||
|
|
||||||
// Setup draggable progress bar
|
// Setup draggable progress bar
|
||||||
setupProgressDrag();
|
setupProgressDrag();
|
||||||
|
|
||||||
|
// Setup retry button
|
||||||
|
setupRetryButton();
|
||||||
|
|
||||||
lockPlayButton(true);
|
lockPlayButton(true);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -501,6 +657,10 @@ function setupUI() {
|
|||||||
setupAnimations();
|
setupAnimations();
|
||||||
setupFirstLaunchHandlers();
|
setupFirstLaunchHandlers();
|
||||||
loadLauncherVersion();
|
loadLauncherVersion();
|
||||||
|
checkGameInstallation().catch(err => {
|
||||||
|
console.error('Critical error in checkGameInstallation:', err);
|
||||||
|
lockPlayButton(false);
|
||||||
|
});
|
||||||
|
|
||||||
document.body.focus();
|
document.body.focus();
|
||||||
}
|
}
|
||||||
@@ -520,6 +680,53 @@ async function loadLauncherVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check game installation status on startup
|
||||||
|
async function checkGameInstallation() {
|
||||||
|
try {
|
||||||
|
console.log('Checking game installation status...');
|
||||||
|
|
||||||
|
// Verify electronAPI is available
|
||||||
|
if (!window.electronAPI || !window.electronAPI.isGameInstalled) {
|
||||||
|
console.error('electronAPI not available, unlocking play button as fallback');
|
||||||
|
lockPlayButton(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if game is installed
|
||||||
|
const isInstalled = await window.electronAPI.isGameInstalled();
|
||||||
|
|
||||||
|
// Load version_client from config
|
||||||
|
let versionClient = null;
|
||||||
|
if (window.electronAPI.loadVersionClient) {
|
||||||
|
versionClient = await window.electronAPI.loadVersionClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Game installed: ${isInstalled}, version_client: ${versionClient}`);
|
||||||
|
|
||||||
|
lockPlayButton(false);
|
||||||
|
|
||||||
|
// If version_client is null and game is not installed, show install page
|
||||||
|
if (versionClient === null && !isInstalled) {
|
||||||
|
console.log('Game not installed and version_client is null, showing install page...');
|
||||||
|
|
||||||
|
// Show installation page
|
||||||
|
const installPage = document.getElementById('install-page');
|
||||||
|
const launcher = document.getElementById('launcher-container');
|
||||||
|
const sidebar = document.querySelector('.sidebar');
|
||||||
|
|
||||||
|
if (installPage) {
|
||||||
|
installPage.style.display = 'block';
|
||||||
|
if (launcher) launcher.style.display = 'none';
|
||||||
|
if (sidebar) sidebar.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking game installation:', error);
|
||||||
|
// Unlock on error to prevent permanent lock
|
||||||
|
lockPlayButton(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.LauncherUI = {
|
window.LauncherUI = {
|
||||||
showPage,
|
showPage,
|
||||||
setActiveNav,
|
setActiveNav,
|
||||||
@@ -530,8 +737,7 @@ window.LauncherUI = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make installation effects globally available
|
// Make installation effects globally available
|
||||||
window.showInstallationEffects = showInstallationEffects;
|
|
||||||
window.hideInstallationEffects = hideInstallationEffects;
|
|
||||||
|
|
||||||
// Draggable progress bar functionality
|
// Draggable progress bar functionality
|
||||||
function setupProgressDrag() {
|
function setupProgressDrag() {
|
||||||
@@ -591,21 +797,6 @@ function setupProgressDrag() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide installation effects
|
|
||||||
function showInstallationEffects() {
|
|
||||||
const installationEffects = document.getElementById('installationEffects');
|
|
||||||
if (installationEffects) {
|
|
||||||
installationEffects.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideInstallationEffects() {
|
|
||||||
const installationEffects = document.getElementById('installationEffects');
|
|
||||||
if (installationEffects) {
|
|
||||||
installationEffects.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle maximize/restore window function
|
// Toggle maximize/restore window function
|
||||||
function toggleMaximize() {
|
function toggleMaximize() {
|
||||||
if (window.electronAPI && window.electronAPI.maximizeWindow) {
|
if (window.electronAPI && window.electronAPI.maximizeWindow) {
|
||||||
@@ -613,6 +804,302 @@ function toggleMaximize() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error categorization and user-friendly messages
|
||||||
|
function categorizeError(message) {
|
||||||
|
const msg = message.toLowerCase();
|
||||||
|
|
||||||
|
if (msg.includes('network') || msg.includes('connection') || msg.includes('offline')) {
|
||||||
|
return 'network';
|
||||||
|
} else if (msg.includes('stalled') || msg.includes('timeout')) {
|
||||||
|
return 'stall';
|
||||||
|
} else if (msg.includes('file') || msg.includes('disk')) {
|
||||||
|
return 'file';
|
||||||
|
} else if (msg.includes('permission') || msg.includes('access')) {
|
||||||
|
return 'permission';
|
||||||
|
} else if (msg.includes('server') || msg.includes('5')) {
|
||||||
|
return 'server';
|
||||||
|
} else if (msg.includes('corrupted') || msg.includes('pwr file') || msg.includes('unexpected eof')) {
|
||||||
|
return 'corruption';
|
||||||
|
} else if (msg.includes('butler') || msg.includes('patch installation')) {
|
||||||
|
return 'butler';
|
||||||
|
} else if (msg.includes('space') || msg.includes('full') || msg.includes('device full')) {
|
||||||
|
return 'space';
|
||||||
|
} else if (msg.includes('conflict') || msg.includes('already exists')) {
|
||||||
|
return 'conflict';
|
||||||
|
} else if (msg.includes('jre') || msg.includes('java runtime')) {
|
||||||
|
return 'jre';
|
||||||
|
} else {
|
||||||
|
return 'general';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(technicalMessage, errorType) {
|
||||||
|
// Technical errors go to console, user gets friendly messages
|
||||||
|
console.error(`Download error [${errorType}]:`, technicalMessage);
|
||||||
|
|
||||||
|
switch (errorType) {
|
||||||
|
case 'network':
|
||||||
|
return 'Network connection lost. Please check your internet connection and retry.';
|
||||||
|
case 'stall':
|
||||||
|
return 'Download stalled due to slow connection. Please retry.';
|
||||||
|
case 'file':
|
||||||
|
return 'Unable to save file. Check disk space and permissions. Please retry.';
|
||||||
|
case 'permission':
|
||||||
|
return 'Permission denied. Check if launcher has write access. Please retry.';
|
||||||
|
case 'server':
|
||||||
|
return 'Server error. Please wait a moment and retry.';
|
||||||
|
case 'corruption':
|
||||||
|
return 'Corrupted PWR file detected. File deleted and will retry.';
|
||||||
|
case 'butler':
|
||||||
|
return 'Patch installation failed. Please retry.';
|
||||||
|
case 'space':
|
||||||
|
return 'Insufficient disk space. Free up space and retry.';
|
||||||
|
case 'conflict':
|
||||||
|
return 'Installation directory conflict. Please retry.';
|
||||||
|
case 'jre':
|
||||||
|
return 'Java runtime download failed. Please retry.';
|
||||||
|
default:
|
||||||
|
return 'Download failed. Please retry.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection quality indicator (simplified)
|
||||||
|
function updateConnectionQuality(quality) {
|
||||||
|
if (!progressSize) return;
|
||||||
|
|
||||||
|
const qualityColors = {
|
||||||
|
'Good': '#10b981',
|
||||||
|
'Fair': '#fbbf24',
|
||||||
|
'Poor': '#f87171'
|
||||||
|
};
|
||||||
|
|
||||||
|
const color = qualityColors[quality] || '#6b7280';
|
||||||
|
progressSize.style.color = color;
|
||||||
|
|
||||||
|
// Add subtle quality indicator
|
||||||
|
if (progressSize.dataset.quality !== quality) {
|
||||||
|
progressSize.dataset.quality = quality;
|
||||||
|
progressSize.style.transition = 'color 0.5s ease';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced retry button setup
|
||||||
|
function setupRetryButton() {
|
||||||
|
// Setup JRE retry button
|
||||||
|
if (progressJRRetryBtn) {
|
||||||
|
progressJRRetryBtn.addEventListener('click', async () => {
|
||||||
|
if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
progressJRRetryBtn.disabled = true;
|
||||||
|
progressJRRetryBtn.textContent = 'Retrying...';
|
||||||
|
progressJRRetryBtn.classList.add('retrying');
|
||||||
|
currentDownloadState.isDownloading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
hideDownloadError();
|
||||||
|
|
||||||
|
if (progressRetryInfo) {
|
||||||
|
progressRetryInfo.style.background = '';
|
||||||
|
progressRetryInfo.style.color = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressText) {
|
||||||
|
progressText.textContent = 'Re-downloading Java runtime...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentDownloadState.retryData || currentDownloadState.errorType !== 'jre') {
|
||||||
|
currentDownloadState.retryData = {
|
||||||
|
isJREError: true,
|
||||||
|
jreUrl: '',
|
||||||
|
fileName: 'jre.tar.gz',
|
||||||
|
cacheDir: '',
|
||||||
|
osName: 'linux',
|
||||||
|
arch: 'amd64'
|
||||||
|
};
|
||||||
|
console.log('[UI] Created default JRE retry data:', currentDownloadState.retryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.electronAPI && window.electronAPI.retryDownload) {
|
||||||
|
const result = await window.electronAPI.retryDownload(currentDownloadState.retryData);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'JRE retry failed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('electronAPI.retryDownload not available, simulating JRE retry...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
throw new Error('JRE retry API not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JRE retry failed:', error);
|
||||||
|
showDownloadError(`JRE retry failed: ${error.message}`, true, 'jre');
|
||||||
|
} finally {
|
||||||
|
if (progressJRRetryBtn) {
|
||||||
|
progressJRRetryBtn.disabled = false;
|
||||||
|
progressJRRetryBtn.textContent = 'Retry Java Download';
|
||||||
|
progressJRRetryBtn.classList.remove('retrying');
|
||||||
|
}
|
||||||
|
currentDownloadState.isDownloading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup PWR retry button
|
||||||
|
if (progressPWRRetryBtn) {
|
||||||
|
progressPWRRetryBtn.addEventListener('click', async () => {
|
||||||
|
if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
progressPWRRetryBtn.disabled = true;
|
||||||
|
progressPWRRetryBtn.textContent = 'Retrying...';
|
||||||
|
progressPWRRetryBtn.classList.add('retrying');
|
||||||
|
currentDownloadState.isDownloading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
hideDownloadError();
|
||||||
|
|
||||||
|
if (progressRetryInfo) {
|
||||||
|
progressRetryInfo.style.background = '';
|
||||||
|
progressRetryInfo.style.color = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressText) {
|
||||||
|
const contextMessage = getRetryContextMessage();
|
||||||
|
progressText.textContent = contextMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentDownloadState.retryData || currentDownloadState.errorType === 'jre') {
|
||||||
|
currentDownloadState.retryData = {
|
||||||
|
branch: 'release',
|
||||||
|
fileName: '4.pwr'
|
||||||
|
};
|
||||||
|
console.log('[UI] Created default PWR retry data:', currentDownloadState.retryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.electronAPI && window.electronAPI.retryDownload) {
|
||||||
|
const result = await window.electronAPI.retryDownload(currentDownloadState.retryData);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Game retry failed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('electronAPI.retryDownload not available, simulating PWR retry...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
throw new Error('Game retry API not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PWR retry failed:', error);
|
||||||
|
const errorType = categorizeError(error.message);
|
||||||
|
showDownloadError(`Game retry failed: ${error.message}`, true, errorType, error);
|
||||||
|
} finally {
|
||||||
|
if (progressPWRRetryBtn) {
|
||||||
|
progressPWRRetryBtn.disabled = false;
|
||||||
|
progressPWRRetryBtn.textContent = error && error.isJREError ? 'Retry Java Download' : 'Retry Game Download';
|
||||||
|
progressPWRRetryBtn.classList.remove('retrying');
|
||||||
|
}
|
||||||
|
currentDownloadState.isDownloading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup generic retry button (fallback)
|
||||||
|
if (progressRetryBtn) {
|
||||||
|
progressRetryBtn.addEventListener('click', async () => {
|
||||||
|
if (!currentDownloadState.canRetry || currentDownloadState.isDownloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
progressRetryBtn.disabled = true;
|
||||||
|
progressRetryBtn.textContent = 'Retrying...';
|
||||||
|
progressRetryBtn.classList.add('retrying');
|
||||||
|
currentDownloadState.isDownloading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
hideDownloadError();
|
||||||
|
|
||||||
|
if (progressRetryInfo) {
|
||||||
|
progressRetryInfo.style.background = '';
|
||||||
|
progressRetryInfo.style.color = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressText) {
|
||||||
|
const contextMessage = getRetryContextMessage();
|
||||||
|
progressText.textContent = contextMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentDownloadState.retryData) {
|
||||||
|
if (currentDownloadState.errorType === 'jre') {
|
||||||
|
currentDownloadState.retryData = {
|
||||||
|
isJREError: true,
|
||||||
|
jreUrl: '',
|
||||||
|
fileName: 'jre.tar.gz',
|
||||||
|
cacheDir: '',
|
||||||
|
osName: 'linux',
|
||||||
|
arch: 'amd64'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
currentDownloadState.retryData = {
|
||||||
|
branch: 'release',
|
||||||
|
fileName: '4.pwr'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
console.log('[UI] Created default retry data:', currentDownloadState.retryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.electronAPI && window.electronAPI.retryDownload) {
|
||||||
|
const result = await window.electronAPI.retryDownload(currentDownloadState.retryData);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Retry failed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('electronAPI.retryDownload not available, simulating retry...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
throw new Error('Retry API not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Retry failed:', error);
|
||||||
|
const errorType = categorizeError(error.message);
|
||||||
|
showDownloadError(`Retry failed: ${error.message}`, true, errorType);
|
||||||
|
} finally {
|
||||||
|
if (progressRetryBtn) {
|
||||||
|
progressRetryBtn.disabled = false;
|
||||||
|
progressRetryBtn.textContent = 'Retry Download';
|
||||||
|
progressRetryBtn.classList.remove('retrying');
|
||||||
|
}
|
||||||
|
currentDownloadState.isDownloading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRetryContextMessage() {
|
||||||
|
const errorType = currentDownloadState.errorType;
|
||||||
|
|
||||||
|
switch (errorType) {
|
||||||
|
case 'network':
|
||||||
|
return 'Reconnecting and retrying download...';
|
||||||
|
case 'stall':
|
||||||
|
return 'Resuming stalled download...';
|
||||||
|
case 'server':
|
||||||
|
return 'Waiting for server and retrying...';
|
||||||
|
case 'corruption':
|
||||||
|
return 'Re-downloading corrupted PWR file...';
|
||||||
|
case 'butler':
|
||||||
|
return 'Re-attempting patch installation...';
|
||||||
|
case 'space':
|
||||||
|
return 'Retrying after clearing disk space...';
|
||||||
|
case 'permission':
|
||||||
|
return 'Retrying with corrected permissions...';
|
||||||
|
case 'conflict':
|
||||||
|
return 'Retrying after resolving conflicts...';
|
||||||
|
case 'jre':
|
||||||
|
return 'Re-downloading Java runtime...';
|
||||||
|
default:
|
||||||
|
return 'Initiating retry download...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make toggleMaximize globally available
|
// Make toggleMaximize globally available
|
||||||
window.toggleMaximize = toggleMaximize;
|
window.toggleMaximize = toggleMaximize;
|
||||||
|
|
||||||
|
|||||||
216
GUI/js/update.js
216
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>
|
||||||
@@ -113,16 +128,43 @@ class ClientUpdateManager {
|
|||||||
installBtn.addEventListener('click', async (e) => {
|
installBtn.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
installBtn.disabled = true;
|
installBtn.disabled = true;
|
||||||
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Installing...';
|
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Installing...';
|
||||||
|
|
||||||
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,10 +180,15 @@ 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);
|
||||||
downloadBtn.disabled = false;
|
downloadBtn.disabled = false;
|
||||||
@@ -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,35 +274,96 @@ 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);
|
||||||
return; // Don't do anything else, showManualDownloadRequired handles everything
|
return; // Don't do anything else, showManualDownloadRequired handles everything
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-critical errors, just show error message without changing status
|
// For non-critical errors, just show error message without changing status
|
||||||
const errorMessage = document.getElementById('update-error-message');
|
const errorMessage = document.getElementById('update-error-message');
|
||||||
const errorText = document.getElementById('update-error-text');
|
const errorText = document.getElementById('update-error-text');
|
||||||
|
|
||||||
if (errorMessage && errorText) {
|
if (errorMessage && errorText) {
|
||||||
let message = errorInfo.message || 'An error occurred during the update process.';
|
let message = errorInfo.message || 'An error occurred during the update process.';
|
||||||
if (errorInfo.isMacSigningError) {
|
if (errorInfo.isMacSigningError) {
|
||||||
@@ -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);
|
||||||
document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true);
|
this._boundBlockContextMenu = this.blockContextMenu.bind(this);
|
||||||
|
|
||||||
|
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 === ' ') &&
|
||||||
|
|||||||
149
GUI/js/updater.js
Normal file
149
GUI/js/updater.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// Launcher Update Manager UI
|
||||||
|
|
||||||
|
let updateModal = null;
|
||||||
|
let downloadProgressBar = null;
|
||||||
|
|
||||||
|
function initUpdater() {
|
||||||
|
// Listen for update events from main process
|
||||||
|
if (window.electronAPI && window.electronAPI.onUpdateAvailable) {
|
||||||
|
window.electronAPI.onUpdateAvailable((updateInfo) => {
|
||||||
|
showUpdateModal(updateInfo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.electronAPI && window.electronAPI.onUpdateDownloadProgress) {
|
||||||
|
window.electronAPI.onUpdateDownloadProgress((progress) => {
|
||||||
|
updateDownloadProgress(progress);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.electronAPI && window.electronAPI.onUpdateDownloaded) {
|
||||||
|
window.electronAPI.onUpdateDownloaded((info) => {
|
||||||
|
showInstallUpdatePrompt(info);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUpdateModal(updateInfo) {
|
||||||
|
if (updateModal) {
|
||||||
|
updateModal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateModal = document.createElement('div');
|
||||||
|
updateModal.className = 'update-modal-overlay';
|
||||||
|
updateModal.innerHTML = `
|
||||||
|
<div class="update-modal">
|
||||||
|
<div class="update-header">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
<h2>Launcher Update Available</h2>
|
||||||
|
</div>
|
||||||
|
<div class="update-content">
|
||||||
|
<p class="update-version">Version ${updateInfo.newVersion} is available!</p>
|
||||||
|
<p class="current-version">Current version: ${updateInfo.currentVersion}</p>
|
||||||
|
${updateInfo.releaseNotes ? `<div class="release-notes">${updateInfo.releaseNotes}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="update-progress" style="display: none;">
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" id="updateProgressBar"></div>
|
||||||
|
</div>
|
||||||
|
<p class="progress-text" id="updateProgressText">Downloading...</p>
|
||||||
|
</div>
|
||||||
|
<div class="update-actions">
|
||||||
|
<button class="btn-primary" onclick="downloadUpdate()">
|
||||||
|
<i class="fas fa-download"></i> Download Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(updateModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadUpdate() {
|
||||||
|
const downloadBtn = updateModal.querySelector('.btn-primary');
|
||||||
|
const progressDiv = updateModal.querySelector('.update-progress');
|
||||||
|
|
||||||
|
// Disable button and show progress
|
||||||
|
downloadBtn.disabled = true;
|
||||||
|
progressDiv.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.electronAPI.downloadUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download update:', error);
|
||||||
|
alert('Failed to download update. Please try again later.');
|
||||||
|
dismissUpdateModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDownloadProgress(progress) {
|
||||||
|
if (!updateModal) return;
|
||||||
|
|
||||||
|
const progressBar = document.getElementById('updateProgressBar');
|
||||||
|
const progressText = document.getElementById('updateProgressText');
|
||||||
|
|
||||||
|
if (progressBar) {
|
||||||
|
progressBar.style.width = `${progress.percent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressText) {
|
||||||
|
const mbTransferred = (progress.transferred / 1024 / 1024).toFixed(2);
|
||||||
|
const mbTotal = (progress.total / 1024 / 1024).toFixed(2);
|
||||||
|
const speed = (progress.bytesPerSecond / 1024 / 1024).toFixed(2);
|
||||||
|
progressText.textContent = `Downloading... ${mbTransferred}MB / ${mbTotal}MB (${speed} MB/s)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showInstallUpdatePrompt(info) {
|
||||||
|
if (updateModal) {
|
||||||
|
updateModal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateModal = document.createElement('div');
|
||||||
|
updateModal.className = 'update-modal-overlay';
|
||||||
|
updateModal.innerHTML = `
|
||||||
|
<div class="update-modal">
|
||||||
|
<div class="update-header">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
<h2>Update Downloaded</h2>
|
||||||
|
</div>
|
||||||
|
<div class="update-content">
|
||||||
|
<p>Version ${info.version} has been downloaded and is ready to install.</p>
|
||||||
|
<p class="update-note">The launcher will restart to complete the installation.</p>
|
||||||
|
</div>
|
||||||
|
<div class="update-actions">
|
||||||
|
<button class="btn-primary" onclick="installUpdate()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Restart & Install
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(updateModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installUpdate() {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.installUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to install update:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissUpdateModal() {
|
||||||
|
if (updateModal) {
|
||||||
|
updateModal.remove();
|
||||||
|
updateModal = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', initUpdater);
|
||||||
|
|
||||||
|
// Export functions
|
||||||
|
window.UpdaterUI = {
|
||||||
|
showUpdateModal,
|
||||||
|
dismissUpdateModal,
|
||||||
|
downloadUpdate,
|
||||||
|
installUpdate
|
||||||
|
};
|
||||||
283
GUI/locales/de.json
Normal file
283
GUI/locales/de.json
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Spielen",
|
||||||
|
"mods": "Mods",
|
||||||
|
"news": "Neuigkeiten",
|
||||||
|
"chat": "Spieler-Chat",
|
||||||
|
"settings": "Einstellungen"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Spieler:",
|
||||||
|
"manageProfiles": "Profile verwalten",
|
||||||
|
"defaultProfile": "Standard"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "KOSTENLOSER LAUNCHER",
|
||||||
|
"playerName": "Spielername",
|
||||||
|
"playerNamePlaceholder": "Namen eingeben",
|
||||||
|
"gameBranch": "Spielversion",
|
||||||
|
"releaseVersion": "Release (Stabil)",
|
||||||
|
"preReleaseVersion": "Pre-Release (Experimentell)",
|
||||||
|
"customInstallation": "Benutzerdefinierte Installation",
|
||||||
|
"installationFolder": "Installationsordner",
|
||||||
|
"pathPlaceholder": "Standardspeicherort",
|
||||||
|
"browse": "Durchsuchen",
|
||||||
|
"installButton": "HYTALE INSTALLIEREN",
|
||||||
|
"installing": "INSTALLIERE..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "BEREIT ZUM SPIELEN",
|
||||||
|
"subtitle": "Starte Hytale und beginne das Abenteuer",
|
||||||
|
"playButton": "HYTALE SPIELEN",
|
||||||
|
"latestNews": "NEUESTE NACHRICHTEN",
|
||||||
|
"viewAll": "ALLE ANZEIGEN",
|
||||||
|
"checking": "ÜBERPRÜFE...",
|
||||||
|
"play": "SPIELEN"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Mods suchen...",
|
||||||
|
"myMods": "MEINE MODS",
|
||||||
|
"previous": "ZURÜCK",
|
||||||
|
"next": "WEITER",
|
||||||
|
"page": "Seite",
|
||||||
|
"of": "von",
|
||||||
|
"modalTitle": "MEINE MODS",
|
||||||
|
"noModsFound": "Keine Mods gefunden",
|
||||||
|
"noModsFoundDesc": "Versuche deine Suche anzupassen",
|
||||||
|
"noModsInstalled": "Keine Mods installiert",
|
||||||
|
"noModsInstalledDesc": "Füge Mods von CurseForge hinzu oder importiere lokale Dateien",
|
||||||
|
"view": "ANZEIGEN",
|
||||||
|
"install": "INSTALLIEREN",
|
||||||
|
"installed": "INSTALLIERT",
|
||||||
|
"enable": "AKTIVIEREN",
|
||||||
|
"disable": "DEAKTIVIEREN",
|
||||||
|
"active": "AKTIV",
|
||||||
|
"disabled": "DEAKTIVIERT",
|
||||||
|
"delete": "Mod löschen",
|
||||||
|
"noDescription": "Keine Beschreibung verfügbar",
|
||||||
|
"confirmDelete": "Möchtest du \"{name}\" wirklich löschen?",
|
||||||
|
"confirmDeleteDesc": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"confirmDeletion": "Löschung bestätigen",
|
||||||
|
"apiKeyRequired": "API-Schlüssel erforderlich",
|
||||||
|
"apiKeyRequiredDesc": "CurseForge API-Schlüssel wird benötigt, um Mods zu durchsuchen"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "ALLE NACHRICHTEN",
|
||||||
|
"readMore": "Mehr lesen"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "SPIELER-CHAT",
|
||||||
|
"pickColor": "Farbe",
|
||||||
|
"inputPlaceholder": "Nachricht eingeben...",
|
||||||
|
"send": "Senden",
|
||||||
|
"online": "online",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Sicherer Chat - Links werden zensiert",
|
||||||
|
"joinChat": "Chat beitreten",
|
||||||
|
"chooseUsername": "Wähle einen Benutzernamen, um dem Spieler-Chat beizutreten",
|
||||||
|
"username": "Benutzername",
|
||||||
|
"usernamePlaceholder": "Benutzernamen eingeben...",
|
||||||
|
"usernameHint": "3-20 Zeichen, nur Buchstaben, Zahlen, - und _",
|
||||||
|
"joinButton": "Chat beitreten",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Benutzernamenfarbe anpassen",
|
||||||
|
"chooseSolid": "Wähle eine einfarbige Farbe:",
|
||||||
|
"customColor": "Benutzerdefinierte Farbe:",
|
||||||
|
"preview": "Vorschau:",
|
||||||
|
"previewUsername": "Benutzername",
|
||||||
|
"apply": "Farbe anwenden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "EINSTELLUNGEN",
|
||||||
|
"java": "Java Runtime",
|
||||||
|
"useCustomJava": "Benutzerdefinierten Java-Pfad verwenden",
|
||||||
|
"javaDescription": "Ersetze die mitgelieferte Java-Installation durch deine eigene",
|
||||||
|
"javaPath": "Java-Ausführungsdatei-Pfad",
|
||||||
|
"javaPathPlaceholder": "Java-Pfad auswählen...",
|
||||||
|
"javaBrowse": "Durchsuchen",
|
||||||
|
"javaHint": "Wähle den Java-Installationsordner (unterstützt Windows, Mac, Linux)",
|
||||||
|
"discord": "Discord-Integration",
|
||||||
|
"enableRPC": "Discord Rich Presence aktivieren",
|
||||||
|
"discordDescription": "Zeige deine Launcher-Aktivität auf Discord",
|
||||||
|
"game": "Spieloptionen",
|
||||||
|
"playerName": "Spielername",
|
||||||
|
"playerNamePlaceholder": "Spielernamen eingeben",
|
||||||
|
"playerNameHint": "Dieser Name wird im Spiel verwendet (1-16 Zeichen)",
|
||||||
|
"openGameLocation": "Spielordner öffnen",
|
||||||
|
"openGameLocationDesc": "Öffne den Spielinstallationsordner",
|
||||||
|
"account": "Spieler-UUID-Verwaltung",
|
||||||
|
"currentUUID": "Aktuelle UUID",
|
||||||
|
"uuidPlaceholder": "UUID wird geladen...",
|
||||||
|
"copyUUID": "UUID kopieren",
|
||||||
|
"regenerateUUID": "UUID neu generieren",
|
||||||
|
"uuidHint": "Deine eindeutige Spielerkennung für diesen Benutzernamen",
|
||||||
|
"manageUUIDs": "Alle UUIDs verwalten",
|
||||||
|
"manageUUIDsDesc": "Alle Spieler-UUIDs anzeigen und verwalten",
|
||||||
|
"language": "Sprache",
|
||||||
|
"selectLanguage": "Sprache auswählen",
|
||||||
|
"repairGame": "Spiel reparieren",
|
||||||
|
"reinstallGame": "Spieldateien neu installieren (behält Daten)",
|
||||||
|
"gpuPreference": "GPU-Präferenz",
|
||||||
|
"gpuHint": "Wähle deine bevorzugte GPU (Linux: betrifft DRI_PRIME)",
|
||||||
|
"gpuAuto": "Auto",
|
||||||
|
"gpuIntegrated": "Integriert",
|
||||||
|
"gpuDedicated": "Dediziert",
|
||||||
|
"logs": "SYSTEMPROTOKOLLE",
|
||||||
|
"logsCopy": "Kopieren",
|
||||||
|
"logsRefresh": "Aktualisieren",
|
||||||
|
"logsFolder": "Ordner öffnen",
|
||||||
|
"logsLoading": "Protokolle werden geladen...",
|
||||||
|
"closeLauncher": "Launcher-Verhalten",
|
||||||
|
"closeOnStart": "Launcher beim Spielstart schließen",
|
||||||
|
"closeOnStartDescription": "Schließe den Launcher automatisch, nachdem Hytale gestartet wurde",
|
||||||
|
"hwAccel": "Hardware-Beschleunigung",
|
||||||
|
"hwAccelDescription": "Hardware-Beschleunigung für den Launcher aktivieren",
|
||||||
|
"gameBranch": "Spiel-Branch",
|
||||||
|
"branchRelease": "Release",
|
||||||
|
"branchPreRelease": "Pre-Release",
|
||||||
|
"branchHint": "Wechsel zwischen stabiler Release- und experimenteller Pre-Release-Version",
|
||||||
|
"branchWarning": "Das Ändern des Branches lädt eine andere Spielversion herunter und installiert sie",
|
||||||
|
"branchSwitching": "Wechsle zu {branch}...",
|
||||||
|
"branchSwitched": "Erfolgreich zu {branch} gewechselt!",
|
||||||
|
"installRequired": "Installation erforderlich",
|
||||||
|
"branchInstallConfirm": "Das Spiel wird für den {branch}-Branch installiert. Fortfahren?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "UUID-Verwaltung",
|
||||||
|
"currentUserUUID": "Aktuelle Benutzer-UUID",
|
||||||
|
"allPlayerUUIDs": "Alle Spieler-UUIDs",
|
||||||
|
"generateNew": "Neue UUID generieren",
|
||||||
|
"loadingUUIDs": "UUIDs werden geladen...",
|
||||||
|
"setCustomUUID": "Benutzerdefinierte UUID festlegen",
|
||||||
|
"customPlaceholder": "Benutzerdefinierte UUID eingeben (Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "UUID festlegen",
|
||||||
|
"warning": "Warnung: Das Festlegen einer benutzerdefinierten UUID ändert deine aktuelle Spieleridentität",
|
||||||
|
"copyTooltip": "UUID kopieren",
|
||||||
|
"regenerateTooltip": "Neue UUID generieren"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Profile verwalten",
|
||||||
|
"newProfilePlaceholder": "Neuer Profilname",
|
||||||
|
"createProfile": "Profil erstellen"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Tritt unserer Discord-Community bei!",
|
||||||
|
"joinButton": "Discord beitreten"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"save": "Speichern",
|
||||||
|
"close": "Schließen",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"loading": "Lädt...",
|
||||||
|
"apply": "Anwenden",
|
||||||
|
"install": "Installieren"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Fehler: Spieldaten nicht gefunden",
|
||||||
|
"gameUpdatedSuccess": "Spiel erfolgreich aktualisiert! 🎉",
|
||||||
|
"updateFailed": "Update fehlgeschlagen: {error}",
|
||||||
|
"updateError": "Update-Fehler: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence aktiviert",
|
||||||
|
"discordDisabled": "Discord Rich Presence deaktiviert",
|
||||||
|
"discordSaveFailed": "Discord-Einstellung konnte nicht gespeichert werden",
|
||||||
|
"playerNameRequired": "Bitte gib einen gültigen Spielernamen ein",
|
||||||
|
"playerNameSaved": "Spielername erfolgreich gespeichert",
|
||||||
|
"playerNameSaveFailed": "Spielername konnte nicht gespeichert werden",
|
||||||
|
"uuidCopied": "UUID in die Zwischenablage kopiert!",
|
||||||
|
"uuidCopyFailed": "UUID konnte nicht kopiert werden",
|
||||||
|
"uuidRegenNotAvailable": "UUID-Neugenerierung nicht verfügbar",
|
||||||
|
"uuidRegenFailed": "UUID konnte nicht neu generiert werden",
|
||||||
|
"uuidGenerated": "Neue UUID erfolgreich generiert!",
|
||||||
|
"uuidGeneratedShort": "Neue UUID generiert!",
|
||||||
|
"uuidGenerateFailed": "Neue UUID konnte nicht generiert werden",
|
||||||
|
"uuidRequired": "Bitte gib eine UUID ein",
|
||||||
|
"uuidInvalidFormat": "Ungültiges UUID-Format",
|
||||||
|
"uuidSetFailed": "Benutzerdefinierte UUID konnte nicht festgelegt werden",
|
||||||
|
"uuidSetSuccess": "Benutzerdefinierte UUID erfolgreich festgelegt!",
|
||||||
|
"uuidDeleteFailed": "UUID konnte nicht gelöscht werden",
|
||||||
|
"uuidDeleteSuccess": "UUID erfolgreich gelöscht!",
|
||||||
|
"modsDownloading": "{name} wird heruntergeladen...",
|
||||||
|
"modsTogglingMod": "Mod wird umgeschaltet...",
|
||||||
|
"modsDeletingMod": "Mod wird gelöscht...",
|
||||||
|
"modsLoadingMods": "Mods von CurseForge werden geladen...",
|
||||||
|
"modsInstalledSuccess": "{name} erfolgreich installiert! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} erfolgreich gelöscht",
|
||||||
|
"modsDownloadFailed": "Mod konnte nicht heruntergeladen werden: {error}",
|
||||||
|
"modsToggleFailed": "Mod konnte nicht umgeschaltet werden: {error}",
|
||||||
|
"modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}",
|
||||||
|
"modsModNotFound": "Mod-Informationen nicht gefunden",
|
||||||
|
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
|
||||||
|
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden",
|
||||||
|
"javaPathCopied": "Java-Pfad in die Zwischenablage kopiert!",
|
||||||
|
"javaPathCopyFailed": "Java-Pfad konnte nicht kopiert werden",
|
||||||
|
"javaPathSaved": "Java-Pfad erfolgreich gespeichert!",
|
||||||
|
"javaPathSaveFailed": "Java-Pfad konnte nicht gespeichert werden",
|
||||||
|
"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": {
|
||||||
|
"defaultTitle": "Aktion bestätigen",
|
||||||
|
"regenerateUuidTitle": "Neue UUID generieren",
|
||||||
|
"regenerateUuidMessage": "Möchtest du wirklich eine neue UUID generieren? Dies ändert deine Spieleridentität.",
|
||||||
|
"regenerateUuidButton": "Generieren",
|
||||||
|
"setCustomUuidTitle": "Benutzerdefinierte UUID festlegen",
|
||||||
|
"setCustomUuidMessage": "Möchtest du wirklich diese benutzerdefinierte UUID festlegen? Dies ändert deine Spieleridentität.",
|
||||||
|
"setCustomUuidButton": "UUID festlegen",
|
||||||
|
"deleteUuidTitle": "UUID löschen",
|
||||||
|
"deleteUuidMessage": "Möchtest du wirklich die UUID für \"{username}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"deleteUuidButton": "Löschen",
|
||||||
|
"uninstallGameTitle": "Spiel deinstallieren",
|
||||||
|
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
|
||||||
|
"uninstallGameButton": "Deinstallieren"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Initialisiere...",
|
||||||
|
"downloading": "Lädt herunter...",
|
||||||
|
"installing": "Installiere...",
|
||||||
|
"extracting": "Entpacke...",
|
||||||
|
"verifying": "Überprüfe...",
|
||||||
|
"switchingProfile": "Profil wird gewechselt...",
|
||||||
|
"profileSwitched": "Profil gewechselt!",
|
||||||
|
"startingGame": "Spiel wird gestartet...",
|
||||||
|
"launching": "STARTET...",
|
||||||
|
"uninstallingGame": "Spiel wird deinstalliert...",
|
||||||
|
"gameUninstalled": "Spiel erfolgreich deinstalliert!",
|
||||||
|
"uninstallFailed": "Deinstallation fehlgeschlagen: {error}",
|
||||||
|
"startingUpdate": "Obligatorisches Spiel-Update wird gestartet...",
|
||||||
|
"installationComplete": "Installation erfolgreich abgeschlossen!",
|
||||||
|
"installationFailed": "Installation fehlgeschlagen: {error}",
|
||||||
|
"installingGameFiles": "Spieldateien werden installiert...",
|
||||||
|
"installComplete": "Installation abgeschlossen!"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
"title": "FREE TO PLAY LAUNCHER",
|
"title": "FREE TO PLAY LAUNCHER",
|
||||||
"playerName": "Player Name",
|
"playerName": "Player Name",
|
||||||
"playerNamePlaceholder": "Enter your name",
|
"playerNamePlaceholder": "Enter your name",
|
||||||
|
"gameBranch": "Game Version",
|
||||||
|
"releaseVersion": "Release (Stable)",
|
||||||
|
"preReleaseVersion": "Pre-Release (Experimental)",
|
||||||
"customInstallation": "Custom Installation",
|
"customInstallation": "Custom Installation",
|
||||||
"installationFolder": "Installation Folder",
|
"installationFolder": "Installation Folder",
|
||||||
"pathPlaceholder": "Default location",
|
"pathPlaceholder": "Default location",
|
||||||
@@ -54,7 +57,9 @@
|
|||||||
"noDescription": "No description available",
|
"noDescription": "No description available",
|
||||||
"confirmDelete": "Are you sure you want to delete \"{name}\"?",
|
"confirmDelete": "Are you sure you want to delete \"{name}\"?",
|
||||||
"confirmDeleteDesc": "This action cannot be undone.",
|
"confirmDeleteDesc": "This action cannot be undone.",
|
||||||
"confirmDeletion": "Confirm Deletion"
|
"confirmDeletion": "Confirm Deletion",
|
||||||
|
"apiKeyRequired": "API Key Required",
|
||||||
|
"apiKeyRequiredDesc": "CurseForge API key is needed to browse mods"
|
||||||
},
|
},
|
||||||
"news": {
|
"news": {
|
||||||
"title": "ALL NEWS",
|
"title": "ALL NEWS",
|
||||||
@@ -125,7 +130,18 @@
|
|||||||
"logsLoading": "Loading logs...",
|
"logsLoading": "Loading logs...",
|
||||||
"closeLauncher": "Launcher Behavior",
|
"closeLauncher": "Launcher Behavior",
|
||||||
"closeOnStart": "Close Launcher on game start",
|
"closeOnStart": "Close Launcher on game start",
|
||||||
"closeOnStartDescription": "Automatically close the launcher after Hytale has launched"
|
"closeOnStartDescription": "Automatically close the launcher after Hytale has launched",
|
||||||
|
"hwAccel": "Hardware Acceleration",
|
||||||
|
"hwAccelDescription": "Enable hardware acceleration for the launcher",
|
||||||
|
"gameBranch": "Game Branch",
|
||||||
|
"branchRelease": "Release",
|
||||||
|
"branchPreRelease": "Pre-Release",
|
||||||
|
"branchHint": "Switch between stable release and experimental pre-release versions",
|
||||||
|
"branchWarning": "Changing branch will download and install a different game version",
|
||||||
|
"branchSwitching": "Switching to {branch}...",
|
||||||
|
"branchSwitched": "Switched to {branch} successfully!",
|
||||||
|
"installRequired": "Installation Required",
|
||||||
|
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?"
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"modalTitle": "UUID Management",
|
"modalTitle": "UUID Management",
|
||||||
@@ -157,7 +173,8 @@
|
|||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"apply": "Apply"
|
"apply": "Apply",
|
||||||
|
"install": "Install"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"gameDataNotFound": "Error: Game data not found",
|
"gameDataNotFound": "Error: Game data not found",
|
||||||
@@ -192,7 +209,9 @@
|
|||||||
"modsDownloadFailed": "Failed to download mod: {error}",
|
"modsDownloadFailed": "Failed to download mod: {error}",
|
||||||
"modsToggleFailed": "Failed to toggle mod: {error}",
|
"modsToggleFailed": "Failed to toggle mod: {error}",
|
||||||
"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",
|
||||||
|
"hwAccelSaveFailed": "Failed to save hardware acceleration setting"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Confirm action",
|
"defaultTitle": "Confirm action",
|
||||||
@@ -228,4 +247,4 @@
|
|||||||
"installingGameFiles": "Installing game files...",
|
"installingGameFiles": "Installing game files...",
|
||||||
"installComplete": "Installation complete!"
|
"installComplete": "Installation complete!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
"title": "LAUNCHER GRATUITO",
|
"title": "LAUNCHER GRATUITO",
|
||||||
"playerName": "Nombre del Jugador",
|
"playerName": "Nombre del Jugador",
|
||||||
"playerNamePlaceholder": "Ingresa tu nombre",
|
"playerNamePlaceholder": "Ingresa tu nombre",
|
||||||
|
"gameBranch": "Versión del Juego",
|
||||||
|
"releaseVersion": "Lanzamiento (Estable)",
|
||||||
|
"preReleaseVersion": "Pre-Lanzamiento (Experimental)",
|
||||||
"customInstallation": "Instalación Personalizada",
|
"customInstallation": "Instalación Personalizada",
|
||||||
"installationFolder": "Carpeta de Instalación",
|
"installationFolder": "Carpeta de Instalación",
|
||||||
"pathPlaceholder": "Ubicación predeterminada",
|
"pathPlaceholder": "Ubicación predeterminada",
|
||||||
@@ -54,7 +57,9 @@
|
|||||||
"noDescription": "Sin descripción disponible",
|
"noDescription": "Sin descripción disponible",
|
||||||
"confirmDelete": "¿Estás seguro de que quieres eliminar \"{name}\"?",
|
"confirmDelete": "¿Estás seguro de que quieres eliminar \"{name}\"?",
|
||||||
"confirmDeleteDesc": "Esta acción no se puede deshacer.",
|
"confirmDeleteDesc": "Esta acción no se puede deshacer.",
|
||||||
"confirmDeletion": "Confirmar eliminación"
|
"confirmDeletion": "Confirmar eliminación",
|
||||||
|
"apiKeyRequired": "Clave API Requerida",
|
||||||
|
"apiKeyRequiredDesc": "Se necesita una clave API de CurseForge para explorar mods"
|
||||||
},
|
},
|
||||||
"news": {
|
"news": {
|
||||||
"title": "TODAS LAS NOTICIAS",
|
"title": "TODAS LAS NOTICIAS",
|
||||||
@@ -125,7 +130,16 @@
|
|||||||
"logsLoading": "Cargando registros...",
|
"logsLoading": "Cargando registros...",
|
||||||
"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",
|
||||||
|
"gameBranch": "Rama del Juego",
|
||||||
|
"branchRelease": "Lanzamiento",
|
||||||
|
"branchPreRelease": "Pre-Lanzamiento",
|
||||||
|
"branchHint": "Cambia entre la versión estable y la versión experimental de pre-lanzamiento",
|
||||||
|
"branchWarning": "Cambiar de rama descargará e instalará una versión diferente del juego",
|
||||||
|
"branchSwitching": "Cambiando a {branch}...",
|
||||||
|
"branchSwitched": "¡Cambiado a {branch} con éxito!",
|
||||||
|
"installRequired": "Instalación Requerida",
|
||||||
|
"branchInstallConfirm": "El juego se instalará para la rama {branch}. ¿Continuar?"
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"modalTitle": "Gestión de UUID",
|
"modalTitle": "Gestión de UUID",
|
||||||
@@ -157,7 +171,8 @@
|
|||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
"apply": "Aplicar"
|
"apply": "Aplicar",
|
||||||
|
"install": "Instalar"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"gameDataNotFound": "Error: No se encontraron datos del juego",
|
"gameDataNotFound": "Error: No se encontraron datos del juego",
|
||||||
235
GUI/locales/fr.json
Normal file
235
GUI/locales/fr.json
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Jouer",
|
||||||
|
"mods": "Mods",
|
||||||
|
"news": "Actualités",
|
||||||
|
"chat": "Chat Joueurs",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Joueurs:",
|
||||||
|
"manageProfiles": "Gérer les Profils",
|
||||||
|
"defaultProfile": "Par défaut"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "LAUNCHER GRATUIT",
|
||||||
|
"playerName": "Nom du Joueur",
|
||||||
|
"playerNamePlaceholder": "Entrez votre nom",
|
||||||
|
"gameBranch": "Version du Jeu",
|
||||||
|
"releaseVersion": "Release (Stable)",
|
||||||
|
"preReleaseVersion": "Pré-Release (Expérimental)",
|
||||||
|
"customInstallation": "Installation Personnalisée",
|
||||||
|
"installationFolder": "Dossier d'Installation",
|
||||||
|
"pathPlaceholder": "Emplacement par défaut",
|
||||||
|
"browse": "Parcourir",
|
||||||
|
"installButton": "INSTALLER HYTALE",
|
||||||
|
"installing": "INSTALLATION..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "PRÊT À JOUER",
|
||||||
|
"subtitle": "Lancez Hytale et entrez dans l'aventure",
|
||||||
|
"playButton": "JOUER À HYTALE",
|
||||||
|
"latestNews": "DERNIÈRES ACTUALITÉS",
|
||||||
|
"viewAll": "VOIR TOUT",
|
||||||
|
"checking": "VÉRIFICATION...",
|
||||||
|
"play": "JOUER"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Rechercher des mods...",
|
||||||
|
"myMods": "MES MODS",
|
||||||
|
"previous": "PRÉCÉDENT",
|
||||||
|
"next": "SUIVANT",
|
||||||
|
"page": "Page",
|
||||||
|
"of": "sur",
|
||||||
|
"modalTitle": "MES MODS",
|
||||||
|
"noModsFound": "Aucun Mod Trouvé",
|
||||||
|
"noModsFoundDesc": "Essayez d'ajuster votre recherche",
|
||||||
|
"noModsInstalled": "Aucun Mod Installé",
|
||||||
|
"noModsInstalledDesc": "Ajoutez des mods depuis CurseForge ou importez des fichiers locaux",
|
||||||
|
"view": "VOIR",
|
||||||
|
"install": "INSTALLER",
|
||||||
|
"installed": "INSTALLÉ",
|
||||||
|
"enable": "ACTIVER",
|
||||||
|
"disable": "DÉSACTIVER",
|
||||||
|
"active": "ACTIF",
|
||||||
|
"disabled": "DÉSACTIVÉ",
|
||||||
|
"delete": "Supprimer le mod",
|
||||||
|
"noDescription": "Aucune description disponible",
|
||||||
|
"confirmDelete": "Êtes-vous sûr de vouloir supprimer \"{name}\" ?",
|
||||||
|
"confirmDeleteDesc": "Cette action est irréversible.",
|
||||||
|
"confirmDeletion": "Confirmer la Suppression",
|
||||||
|
"apiKeyRequired": "Clé API Requise",
|
||||||
|
"apiKeyRequiredDesc": "Une clé API CurseForge est nécessaire pour parcourir les mods"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "TOUTES LES ACTUALITÉS",
|
||||||
|
"readMore": "Lire Plus"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "CHAT JOUEURS",
|
||||||
|
"pickColor": "Couleur",
|
||||||
|
"inputPlaceholder": "Tapez votre message...",
|
||||||
|
"send": "Envoyer",
|
||||||
|
"online": "en ligne",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Chat sécurisé - Les liens sont censurés",
|
||||||
|
"joinChat": "Rejoindre le Chat",
|
||||||
|
"chooseUsername": "Choisissez un nom d'utilisateur pour rejoindre le Chat Joueurs",
|
||||||
|
"username": "Nom d'utilisateur",
|
||||||
|
"usernamePlaceholder": "Entrez votre nom d'utilisateur...",
|
||||||
|
"usernameHint": "3-20 caractères, lettres, chiffres, - et _ uniquement",
|
||||||
|
"joinButton": "Rejoindre le Chat",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Personnaliser la Couleur du Nom",
|
||||||
|
"chooseSolid": "Choisissez une couleur unie:",
|
||||||
|
"customColor": "Couleur personnalisée:",
|
||||||
|
"preview": "Aperçu:",
|
||||||
|
"previewUsername": "Nom d'utilisateur",
|
||||||
|
"apply": "Appliquer la Couleur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "PARAMÈTRES",
|
||||||
|
"java": "Java Runtime",
|
||||||
|
"useCustomJava": "Utiliser un Chemin Java Personnalisé",
|
||||||
|
"javaDescription": "Remplacer le Java intégré par votre propre installation",
|
||||||
|
"javaPath": "Chemin de l'Exécutable Java",
|
||||||
|
"javaPathPlaceholder": "Sélectionnez le chemin Java...",
|
||||||
|
"javaBrowse": "Parcourir",
|
||||||
|
"javaHint": "Sélectionnez le dossier d'installation de Java (compatible Windows, Mac, Linux)",
|
||||||
|
"discord": "Intégration Discord",
|
||||||
|
"enableRPC": "Activer Discord Rich Presence",
|
||||||
|
"discordDescription": "Afficher votre activité du launcher sur Discord",
|
||||||
|
"game": "Options de Jeu",
|
||||||
|
"playerName": "Nom du Joueur",
|
||||||
|
"playerNamePlaceholder": "Entrez le nom du joueur",
|
||||||
|
"playerNameHint": "Ce nom sera utilisé en jeu (1-16 caractères)",
|
||||||
|
"openGameLocation": "Ouvrir l'Emplacement du Jeu",
|
||||||
|
"openGameLocationDesc": "Ouvrir le dossier d'installation du jeu",
|
||||||
|
"account": "Gestion UUID Joueur",
|
||||||
|
"currentUUID": "UUID Actuel",
|
||||||
|
"uuidPlaceholder": "Chargement UUID...",
|
||||||
|
"copyUUID": "Copier UUID",
|
||||||
|
"regenerateUUID": "Régénérer UUID",
|
||||||
|
"uuidHint": "Votre identifiant unique de joueur pour ce nom d'utilisateur",
|
||||||
|
"manageUUIDs": "Gérer Tous les UUIDs",
|
||||||
|
"manageUUIDsDesc": "Voir et gérer tous les UUIDs de joueurs",
|
||||||
|
"language": "Langue",
|
||||||
|
"selectLanguage": "Sélectionner la Langue",
|
||||||
|
"repairGame": "Réparer le Jeu",
|
||||||
|
"reinstallGame": "Réinstaller les fichiers du jeu (préserve les données)",
|
||||||
|
"gpuPreference": "Préférence GPU",
|
||||||
|
"gpuHint": "Sélectionnez votre GPU préféré (Linux: affecte DRI_PRIME)",
|
||||||
|
"gpuAuto": "Auto",
|
||||||
|
"gpuIntegrated": "Intégré",
|
||||||
|
"gpuDedicated": "Dédié",
|
||||||
|
"logs": "JOURNAUX SYSTÈME",
|
||||||
|
"logsCopy": "Copier",
|
||||||
|
"logsRefresh": "Actualiser",
|
||||||
|
"logsFolder": "Ouvrir le Dossier",
|
||||||
|
"logsLoading": "Chargement des journaux...",
|
||||||
|
"closeLauncher": "Comportement du Launcher",
|
||||||
|
"closeOnStart": "Fermer le Launcher au démarrage du jeu",
|
||||||
|
"closeOnStartDescription": "Fermer automatiquement le launcher après le lancement d'Hytale",
|
||||||
|
"hwAccel": "Accélération Matérielle",
|
||||||
|
"hwAccelDescription": "Activer l'accélération matérielle pour le launcher",
|
||||||
|
"gameBranch": "Branche du Jeu",
|
||||||
|
"branchRelease": "Release",
|
||||||
|
"branchPreRelease": "Pré-Release",
|
||||||
|
"branchHint": "Basculer entre la version stable release et la pré-release expérimentale",
|
||||||
|
"branchWarning": "Changer de branche téléchargera et installera une version différente du jeu",
|
||||||
|
"branchSwitching": "Passage à {branch}...",
|
||||||
|
"branchSwitched": "Passage à {branch} réussi!",
|
||||||
|
"installRequired": "Installation Requise",
|
||||||
|
"branchInstallConfirm": "Le jeu sera installé pour la branche {branch}. Continuer?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "Gestion UUID",
|
||||||
|
"currentUserUUID": "UUID Utilisateur Actuel",
|
||||||
|
"allPlayerUUIDs": "Tous les UUIDs Joueurs",
|
||||||
|
"generateNew": "Générer Nouvel UUID",
|
||||||
|
"loadingUUIDs": "Chargement des UUIDs...",
|
||||||
|
"setCustomUUID": "Définir UUID Personnalisé",
|
||||||
|
"customPlaceholder": "Entrez UUID personnalisé (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Définir UUID",
|
||||||
|
"warning": "Attention: Définir un UUID personnalisé changera votre identité de joueur actuelle",
|
||||||
|
"copyTooltip": "Copier UUID",
|
||||||
|
"regenerateTooltip": "Générer Nouvel UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Gérer les Profils",
|
||||||
|
"newProfilePlaceholder": "Nom du Nouveau Profil",
|
||||||
|
"createProfile": "Créer un Profil"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Rejoignez notre communauté Discord!",
|
||||||
|
"joinButton": "Rejoindre Discord"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"save": "Sauvegarder",
|
||||||
|
"close": "Fermer",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"apply": "Appliquer",
|
||||||
|
"install": "Installer"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Erreur: Données du jeu introuvables",
|
||||||
|
"gameUpdatedSuccess": "Jeu mis à jour avec succès! 🎉",
|
||||||
|
"updateFailed": "Mise à jour échouée: {error}",
|
||||||
|
"updateError": "Erreur de mise à jour: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence activé",
|
||||||
|
"discordDisabled": "Discord Rich Presence désactivé",
|
||||||
|
"discordSaveFailed": "Échec de la sauvegarde des paramètres Discord",
|
||||||
|
"playerNameRequired": "Veuillez entrer un nom de joueur valide",
|
||||||
|
"playerNameSaved": "Nom du joueur sauvegardé avec succès",
|
||||||
|
"playerNameSaveFailed": "Échec de la sauvegarde du nom du joueur",
|
||||||
|
"uuidCopied": "UUID copié dans le presse-papiers!",
|
||||||
|
"uuidCopyFailed": "Échec de la copie de l'UUID",
|
||||||
|
"uuidRegenNotAvailable": "Régénération UUID non disponible",
|
||||||
|
"uuidRegenFailed": "Échec de la régénération de l'UUID",
|
||||||
|
"uuidGenerated": "Nouvel UUID généré avec succès!",
|
||||||
|
"uuidGeneratedShort": "Nouvel UUID généré!",
|
||||||
|
"uuidGenerateFailed": "Échec de la génération du nouvel UUID",
|
||||||
|
"uuidRequired": "Veuillez entrer un UUID",
|
||||||
|
"uuidInvalidFormat": "Format UUID invalide",
|
||||||
|
"uuidSetFailed": "Échec de la définition de l'UUID personnalisé",
|
||||||
|
"uuidSetSuccess": "UUID personnalisé défini avec succès!",
|
||||||
|
"javaPathCopied": "Chemin Java copié dans le presse-papiers!",
|
||||||
|
"javaPathCopyFailed": "Échec de la copie du chemin Java",
|
||||||
|
"javaPathSaved": "Chemin Java sauvegardé avec succès!",
|
||||||
|
"javaPathSaveFailed": "Échec de la sauvegarde du chemin Java",
|
||||||
|
"javaPathInvalid": "Chemin Java invalide",
|
||||||
|
"javaPathReset": "Chemin Java réinitialisé aux valeurs par défaut",
|
||||||
|
"gameLocationError": "Impossible d'ouvrir l'emplacement du jeu",
|
||||||
|
"launcherRestartRequired": "Redémarrage du launcher requis pour appliquer les modifications",
|
||||||
|
"gameRepairConfirm": "Êtes-vous sûr de vouloir réparer le jeu? Cela réinstallera tous les fichiers du jeu.",
|
||||||
|
"gameRepairInProgress": "Réparation du jeu en cours...",
|
||||||
|
"gameRepairSuccess": "Jeu réparé avec succès!",
|
||||||
|
"gameRepairFailed": "Échec de la réparation du jeu: {error}",
|
||||||
|
"invalidUsername": "Nom d'utilisateur invalide",
|
||||||
|
"usernameInUse": "Nom d'utilisateur déjà utilisé",
|
||||||
|
"chatJoinSuccess": "Vous avez rejoint le chat!",
|
||||||
|
"chatJoinFailed": "Échec de la connexion au chat",
|
||||||
|
"messageTooLong": "Message trop long",
|
||||||
|
"messageSent": "Message envoyé",
|
||||||
|
"messageSendFailed": "Échec de l'envoi du message",
|
||||||
|
"colorUpdated": "Couleur mise à jour!",
|
||||||
|
"colorUpdateFailed": "Échec de la mise à jour de la couleur",
|
||||||
|
"profileCreated": "Profil créé avec succès!",
|
||||||
|
"profileCreateFailed": "Échec de la création du profil",
|
||||||
|
"profileDeleted": "Profil supprimé",
|
||||||
|
"profileDeleteFailed": "Échec de la suppression du profil",
|
||||||
|
"profileSwitched": "Profil changé vers: {name}",
|
||||||
|
"profileSwitchFailed": "Échec du changement de profil",
|
||||||
|
"invalidProfileName": "Nom de profil invalide",
|
||||||
|
"profileNameExists": "Un profil avec ce nom existe déjà",
|
||||||
|
"noInternet": "Pas de connexion Internet",
|
||||||
|
"checkInternetConnection": "Vérifiez votre connexion Internet",
|
||||||
|
"serverError": "Erreur serveur. Veuillez réessayer plus tard.",
|
||||||
|
"unknownError": "Une erreur inconnue s'est produite"
|
||||||
|
}
|
||||||
|
}
|
||||||
234
GUI/locales/pl-PL.json
Normal file
234
GUI/locales/pl-PL.json
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Graj",
|
||||||
|
"mods": "Mody",
|
||||||
|
"news": "Wiadomości",
|
||||||
|
"chat": "Chat z graczami",
|
||||||
|
"settings": "Ustawienia",
|
||||||
|
"skins": "Skiny"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Graczy:",
|
||||||
|
"manageProfiles": "Zarządzaj Profilami",
|
||||||
|
"defaultProfile": "Domyślny",
|
||||||
|
"f2p": "FREE TO PLAY"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "FREE TO PLAY LAUNCHER",
|
||||||
|
"playerName": "Nazwa Gracza",
|
||||||
|
"playerNamePlaceholder": "Wprowadź Nazwę",
|
||||||
|
"customInstallation": "Dostosuj Instalacje",
|
||||||
|
"installationFolder": "Folder docelowy",
|
||||||
|
"pathPlaceholder": "Domyślna lokalizacja",
|
||||||
|
"browse": "Przeglądaj",
|
||||||
|
"installButton": "ZAINSTALUJ HYTALE",
|
||||||
|
"installing": "INSTALOWANIE..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "GOTOWE",
|
||||||
|
"subtitle": "Uruchom Hytale i rozpocznij przygodę",
|
||||||
|
"playButton": "GRAJ W HYTALE",
|
||||||
|
"latestNews": "NAJNOWSZE WIADOMOŚCI",
|
||||||
|
"viewAll": "ZOBACZ CAŁOŚĆ",
|
||||||
|
"checking": "SPRAWDZANIE...",
|
||||||
|
"play": "GRAJ"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Wyszukaj mody...",
|
||||||
|
"myMods": "MOJE MODY",
|
||||||
|
"previous": "POPRZEDNIA",
|
||||||
|
"next": "NASTĘPNA",
|
||||||
|
"page": "Strona",
|
||||||
|
"of": "z",
|
||||||
|
"modalTitle": "MOJE MODY",
|
||||||
|
"noModsFound": "Nie Znaleziono Modów",
|
||||||
|
"noModsFoundDesc": "Spróbuj dostosować wyszukiwanie",
|
||||||
|
"noModsInstalled": "Brak Zainstalowanych Modów",
|
||||||
|
"noModsInstalledDesc": "Dodaj mody z CurseForge lub zaimportuj lokalne pliki",
|
||||||
|
"view": "WIDOK",
|
||||||
|
"install": "ZAINSTALUJ",
|
||||||
|
"installed": "ZAINSTALOWANE",
|
||||||
|
"enable": "WŁĄCZ",
|
||||||
|
"disable": "WYŁĄCZ",
|
||||||
|
"active": "AKTYWNE",
|
||||||
|
"disabled": "WYŁĄCZONE",
|
||||||
|
"delete": "Usuń mod",
|
||||||
|
"noDescription": "Brak opisu",
|
||||||
|
"confirmDelete": "Czy na pewno chcesz usunąć \"{name}\"?",
|
||||||
|
"confirmDeleteDesc": "Tej czynności nie można cofnąć.",
|
||||||
|
"confirmDeletion": "Potwierdź"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "WSZYSTKIE WIADOMOŚCI",
|
||||||
|
"readMore": "Zobacz Więcej"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "Chat z graczami",
|
||||||
|
"pickColor": "Kolor",
|
||||||
|
"inputPlaceholder": "Wprowadź swoją wiadomość...",
|
||||||
|
"send": "Wyślij",
|
||||||
|
"online": "online",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Bezpieczny czat – Linki są ocenzurowane",
|
||||||
|
"joinChat": "Dołącz do Czatu",
|
||||||
|
"chooseUsername": "Wybierz nazwę użytkownika, aby dołączyć do Czatu z graczami",
|
||||||
|
"username": "Nazwa Gracza",
|
||||||
|
"usernamePlaceholder": "Wprowadź swoją nazwę...",
|
||||||
|
"usernameHint": "Między 3-20 znaków, tylko litery, cyfry i znaki - i _",
|
||||||
|
"joinButton": "Dołącz do Czatu",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Dostosuj Kolor Użytkownika",
|
||||||
|
"chooseSolid": "Wybierz jednolity kolor:",
|
||||||
|
"customColor": "Kolor niestandardowy:",
|
||||||
|
"preview": "Podgląd:",
|
||||||
|
"previewUsername": "Nazwa",
|
||||||
|
"apply": "Zastosuj Kolor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "USTAWIENIA",
|
||||||
|
"java": "Środowisko Java",
|
||||||
|
"useCustomJava": "Użyj niestandardowej ścieżki Java",
|
||||||
|
"javaDescription": "Zastąp dołączone środowisko wykonawcze Java własnym",
|
||||||
|
"javaPath": "Ścieżka Wykonywalna Java",
|
||||||
|
"javaPathPlaceholder": "Wybierz ścieżkę Java...",
|
||||||
|
"javaBrowse": "Przeglądaj",
|
||||||
|
"javaHint": "Wybierz folder instalacyjny Java (obsługiwane Windows, Mac, Linux)",
|
||||||
|
"discord": "Integracja z Discordem",
|
||||||
|
"enableRPC": "Włącz Discord Rich Presence",
|
||||||
|
"discordDescription": "Pokaż swoją aktywność na Discordzie",
|
||||||
|
"game": "Opcje gry",
|
||||||
|
"playerName": "Nazwa Gracza",
|
||||||
|
"playerNamePlaceholder": "Wprowadź swoją nazwę",
|
||||||
|
"playerNameHint": "Ta nazwa będzie używana w grze (1-16 znaków)",
|
||||||
|
"openGameLocation": "Otwórz Lokalizację Gry",
|
||||||
|
"openGameLocationDesc": "Otwórz folder instalacyjny gry",
|
||||||
|
"account": "Zarządzanie identyfikatorami UUID gracza",
|
||||||
|
"currentUUID": "Obecny UUID",
|
||||||
|
"uuidPlaceholder": "Ładowanie UUID...",
|
||||||
|
"copyUUID": "Skopiuj UUID",
|
||||||
|
"regenerateUUID": "Generuj UUID",
|
||||||
|
"uuidHint": "Twój unikalny identyfikator gracza dla tej nazwy użytkownika",
|
||||||
|
"manageUUIDs": "Zarządzaj wszystkimi UUID",
|
||||||
|
"manageUUIDsDesc": "Wyświetl i zarządzaj wszystkimi identyfikatorami UUID graczy",
|
||||||
|
"language": "Język",
|
||||||
|
"selectLanguage": "Wybierz Język",
|
||||||
|
"repairGame": "Napraw Grę",
|
||||||
|
"reinstallGame": "Zainstaluj ponownie pliki gry (zachowuje dane)",
|
||||||
|
"gpuPreference": "Preferencje GPU",
|
||||||
|
"gpuHint": "Wybierz preferowany procesor graficzny (Linux: wpływa na DRI_PRIME)",
|
||||||
|
"gpuAuto": "Auto",
|
||||||
|
"gpuIntegrated": "Zintegrowana",
|
||||||
|
"gpuDedicated": "Dedykowana",
|
||||||
|
"logs": "SYSTEM LOGS",
|
||||||
|
"logsCopy": "Kopiuj",
|
||||||
|
"logsRefresh": "Odśwież",
|
||||||
|
"logsFolder": "Otwórz Folder",
|
||||||
|
"logsLoading": "Ładowanie logów..."
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "Zarządzanie UUID",
|
||||||
|
"currentUserUUID": "Aktualny UUID użytkownika",
|
||||||
|
"allPlayerUUIDs": "Wszystkie identyfikatory UUID graczy",
|
||||||
|
"generateNew": "Wygeneruj nowy UUID",
|
||||||
|
"loadingUUIDs": "Ładowanie UUID...",
|
||||||
|
"setCustomUUID": "Ustaw niestandardowy UUID",
|
||||||
|
"customPlaceholder": "Wprowadź niestandardowy UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Ustaw UUID",
|
||||||
|
"warning": "Ostrzeżenie: Ustawienie niestandardowego identyfikatora UUID spowoduje zmianę Twojego obecnego identyfikatora gracza",
|
||||||
|
"copyTooltip": "Kopiuj UUID",
|
||||||
|
"regenerateTooltip": "Wygeneruj nowy UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Zarządzaj Profilami",
|
||||||
|
"newProfilePlaceholder": "Nowa Nazwa Profilu",
|
||||||
|
"createProfile": "Utwórz Profil"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Dołącz do naszej społeczności Discord!",
|
||||||
|
"joinButton": "Dołącz Discord"
|
||||||
|
},
|
||||||
|
"skins": {
|
||||||
|
"title": "Skiny",
|
||||||
|
"comingSoon": "Personalizacja skórek już wkrótce..."
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Potwierdź",
|
||||||
|
"cancel": "Anuluj",
|
||||||
|
"save": "Zapisz",
|
||||||
|
"close": "Zamknij",
|
||||||
|
"delete": "Usuń",
|
||||||
|
"edit": "Edytuj",
|
||||||
|
"loading": "Ładowanie...",
|
||||||
|
"apply": "Zastosuj"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Błąd: Nie znaleziono danych gry",
|
||||||
|
"gameUpdatedSuccess": "Gra została zaktualizowana pomyślnie! 🎉",
|
||||||
|
"updateFailed": "Aktualizacja nie powiodła się: {error}",
|
||||||
|
"updateError": "Błąd aktualizacji: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence włączony",
|
||||||
|
"discordDisabled": "Discord Rich Presence wyłączony",
|
||||||
|
"discordSaveFailed": "Nie udało się zapisać ustawień Discorda",
|
||||||
|
"playerNameRequired": "Proszę podać prawidłową nazwę gracza",
|
||||||
|
"playerNameSaved": "Nazwa gracza została zapisana pomyślnie",
|
||||||
|
"playerNameSaveFailed": "Nie udało się zapisać nazwy gracza",
|
||||||
|
"uuidCopied": "Identyfikator UUID skopiowany do schowka!",
|
||||||
|
"uuidCopyFailed": "Nie udało się skopiować UUID",
|
||||||
|
"uuidRegenNotAvailable": "Ponowna gerowanie UUID niedostępne",
|
||||||
|
"uuidRegenFailed": "Nie udało się ponownie wygenerować UUID",
|
||||||
|
"uuidGenerated": "Nowy UUID został pomyślnie wygenerowany!",
|
||||||
|
"uuidGeneratedShort": "Wygenerowano nowy UUID!",
|
||||||
|
"uuidGenerateFailed": "Nie udało się wygenerować nowego UUID",
|
||||||
|
"uuidRequired": "Wprowadzić UUID",
|
||||||
|
"uuidInvalidFormat": "Nieprawidłowy format UUID",
|
||||||
|
"uuidSetFailed": "Nie udało się ustawić niestandardowego UUID",
|
||||||
|
"uuidSetSuccess": "Niestandardowy UUID został ustawiony pomyślnie!",
|
||||||
|
"uuidDeleteFailed": "Nie udało się usunąć UUID",
|
||||||
|
"uuidDeleteSuccess": "UUID został pomyślnie usunięty!",
|
||||||
|
"modsDownloading": "Pobieranie {name}...",
|
||||||
|
"modsTogglingMod": "Przełączanie moda...",
|
||||||
|
"modsDeletingMod": "Usuwanie moda...",
|
||||||
|
"modsLoadingMods": "Ładowanie modów z CurseForge...",
|
||||||
|
"modsInstalledSuccess": "{name} zainstalowany pomyślnie! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} usunięto pomyślnie",
|
||||||
|
"modsDownloadFailed": "Nie udało się pobrać moda: {error}",
|
||||||
|
"modsToggleFailed": "Nie udało się przełączyć moda: {error}",
|
||||||
|
"modsDeleteFailed": "Nie udało się usunąć moda: {error}",
|
||||||
|
"modsModNotFound": "Nie znaleziono informacji o modzie"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Potwierdź działanie",
|
||||||
|
"regenerateUuidTitle": "Wygeneruj nowy UUID",
|
||||||
|
"regenerateUuidMessage": "Czy na pewno chcesz wygenerować nowy UUID? To spowoduje zmianę Twojego identyfikatora gracza.",
|
||||||
|
"regenerateUuidButton": "Generuj",
|
||||||
|
"setCustomUuidTitle": "Ustaw niestandardowy UUID",
|
||||||
|
"setCustomUuidMessage": "Czy na pewno chcesz ustawić ten UUID? To spowoduje zmianę Twojego identyfikatora gracza.",
|
||||||
|
"setCustomUuidButton": "Ustaw UUID",
|
||||||
|
"deleteUuidTitle": "Usuń UUID",
|
||||||
|
"deleteUuidMessage": "Czy na pewno chcesz usunąć UUID dla \"{username}\"? Tej czynności nie można cofnąć.",
|
||||||
|
"deleteUuidButton": "Usuń",
|
||||||
|
"uninstallGameTitle": "Odinstaluj grę",
|
||||||
|
"uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.",
|
||||||
|
"uninstallGameButton": "Odinstaluj"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Inicjalizacja...",
|
||||||
|
"downloading": "Pobieranie...",
|
||||||
|
"installing": "Instalowanie...",
|
||||||
|
"extracting": "Ekstraktowanie...",
|
||||||
|
"verifying": "Weryfikowanie...",
|
||||||
|
"switchingProfile": "Przełączanie profilu...",
|
||||||
|
"profileSwitched": "Profil zmieniony!",
|
||||||
|
"startingGame": "Uruchamianie gry...",
|
||||||
|
"launching": "URUCHAMIANIE...",
|
||||||
|
"uninstallingGame": "Odinstalowywanie gry...",
|
||||||
|
"gameUninstalled": "Gra została pomyślnie odinstalowana!",
|
||||||
|
"uninstallFailed": "Odinstalowanie nie powiodło się: {error}",
|
||||||
|
"startingUpdate": "Rozpoczynanie obowiązkowej aktualizacji gry...",
|
||||||
|
"installationComplete": "Instalacja zakończona pomyślnie!",
|
||||||
|
"installationFailed": "Instalacja nie powiodła się: {error}",
|
||||||
|
"installingGameFiles": "Instalowanie plików gry...",
|
||||||
|
"installComplete": "Instalacja zakończona!"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,9 @@
|
|||||||
"install": {
|
"install": {
|
||||||
"title": "LANÇADOR JOGO GRATUITO",
|
"title": "LANÇADOR JOGO GRATUITO",
|
||||||
"playerName": "Nome do Jogador",
|
"playerName": "Nome do Jogador",
|
||||||
"playerNamePlaceholder": "Digite seu nome",
|
"playerNamePlaceholder": "Digite seu nome", "gameBranch": "Versão do Jogo",
|
||||||
"customInstallation": "Instalação Personalizada",
|
"releaseVersion": "Lançamento (Estável)",
|
||||||
|
"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",
|
||||||
@@ -54,7 +55,9 @@
|
|||||||
"noDescription": "Nenhuma descrição disponível",
|
"noDescription": "Nenhuma descrição disponível",
|
||||||
"confirmDelete": "Tem certeza de que deseja excluir \"{name}\"?",
|
"confirmDelete": "Tem certeza de que deseja excluir \"{name}\"?",
|
||||||
"confirmDeleteDesc": "Esta ação não pode ser desfeita.",
|
"confirmDeleteDesc": "Esta ação não pode ser desfeita.",
|
||||||
"confirmDeletion": "Confirmar exclusão"
|
"confirmDeletion": "Confirmar exclusão",
|
||||||
|
"apiKeyRequired": "Chave de API Necessária",
|
||||||
|
"apiKeyRequiredDesc": "Chave de API do CurseForge é necessária para procurar mods"
|
||||||
},
|
},
|
||||||
"news": {
|
"news": {
|
||||||
"title": "TODAS AS NOTÍCIAS",
|
"title": "TODAS AS NOTÍCIAS",
|
||||||
@@ -125,7 +128,16 @@
|
|||||||
"logsLoading": "Carregando registros...",
|
"logsLoading": "Carregando registros...",
|
||||||
"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",
|
||||||
|
"gameBranch": "Versão do Jogo",
|
||||||
|
"branchRelease": "Lançamento",
|
||||||
|
"branchPreRelease": "Pré-Lançamento",
|
||||||
|
"branchHint": "Alterne entre a versão estável e a versão experimental de pré-lançamento",
|
||||||
|
"branchWarning": "Mudar de versão irá baixar e instalar uma versão diferente do jogo",
|
||||||
|
"branchSwitching": "Mudando para {branch}...",
|
||||||
|
"branchSwitched": "Mudado para {branch} com sucesso!",
|
||||||
|
"installRequired": "Instalação Necessária",
|
||||||
|
"branchInstallConfirm": "O jogo será instalado para o ramo {branch}. Continuar?"
|
||||||
},
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"modalTitle": "Gerenciamento de UUID",
|
"modalTitle": "Gerenciamento de UUID",
|
||||||
@@ -158,7 +170,8 @@
|
|||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"loading": "Carregando...",
|
"loading": "Carregando...",
|
||||||
"apply": "Aplicar"
|
"apply": "Aplicar",
|
||||||
|
"install": "Instalar"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"gameDataNotFound": "Erro: Dados do jogo não encontrados",
|
"gameDataNotFound": "Erro: Dados do jogo não encontrados",
|
||||||
|
|||||||
283
GUI/locales/sv.json
Normal file
283
GUI/locales/sv.json
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Spela",
|
||||||
|
"mods": "Moddar",
|
||||||
|
"news": "Nyheter",
|
||||||
|
"chat": "Spelarchatt",
|
||||||
|
"settings": "Inställningar"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Spelare:",
|
||||||
|
"manageProfiles": "Hantera profiler",
|
||||||
|
"defaultProfile": "Standard"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "GRATIS LAUNCHER",
|
||||||
|
"playerName": "Spelarnamn",
|
||||||
|
"playerNamePlaceholder": "Ange ditt namn",
|
||||||
|
"gameBranch": "Spelversion",
|
||||||
|
"releaseVersion": "Release (Stabil)",
|
||||||
|
"preReleaseVersion": "Pre-Release (Experimentell)",
|
||||||
|
"customInstallation": "Anpassad installation",
|
||||||
|
"installationFolder": "Installationsmapp",
|
||||||
|
"pathPlaceholder": "Standardplats",
|
||||||
|
"browse": "Bläddra",
|
||||||
|
"installButton": "INSTALLERA HYTALE",
|
||||||
|
"installing": "INSTALLERAR..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "REDO ATT SPELA",
|
||||||
|
"subtitle": "Starta Hytale och börja äventyret",
|
||||||
|
"playButton": "SPELA HYTALE",
|
||||||
|
"latestNews": "SENASTE NYHETERNA",
|
||||||
|
"viewAll": "VISA ALLA",
|
||||||
|
"checking": "KONTROLLERAR...",
|
||||||
|
"play": "SPELA"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Sök moddar...",
|
||||||
|
"myMods": "MINA MODDAR",
|
||||||
|
"previous": "FÖREGÅENDE",
|
||||||
|
"next": "NÄSTA",
|
||||||
|
"page": "Sida",
|
||||||
|
"of": "av",
|
||||||
|
"modalTitle": "MINA MODDAR",
|
||||||
|
"noModsFound": "Inga moddar hittades",
|
||||||
|
"noModsFoundDesc": "Försök justera din sökning",
|
||||||
|
"noModsInstalled": "Inga moddar installerade",
|
||||||
|
"noModsInstalledDesc": "Lägg till moddar från CurseForge eller importera lokala filer",
|
||||||
|
"view": "VISA",
|
||||||
|
"install": "INSTALLERA",
|
||||||
|
"installed": "INSTALLERAD",
|
||||||
|
"enable": "AKTIVERA",
|
||||||
|
"disable": "INAKTIVERA",
|
||||||
|
"active": "AKTIV",
|
||||||
|
"disabled": "INAKTIVERAD",
|
||||||
|
"delete": "Ta bort modd",
|
||||||
|
"noDescription": "Ingen beskrivning tillgänglig",
|
||||||
|
"confirmDelete": "Är du säker på att du vill ta bort \"{name}\"?",
|
||||||
|
"confirmDeleteDesc": "Denna åtgärd kan inte ångras.",
|
||||||
|
"confirmDeletion": "Bekräfta borttagning",
|
||||||
|
"apiKeyRequired": "API-nyckel krävs",
|
||||||
|
"apiKeyRequiredDesc": "CurseForge API-nyckel behövs för att bläddra bland moddar"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "ALLA NYHETER",
|
||||||
|
"readMore": "Läs mer"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "SPELARCHATT",
|
||||||
|
"pickColor": "Färg",
|
||||||
|
"inputPlaceholder": "Skriv ditt meddelande...",
|
||||||
|
"send": "Skicka",
|
||||||
|
"online": "online",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Säker chatt - Länkar är censurerade",
|
||||||
|
"joinChat": "Gå med i chatten",
|
||||||
|
"chooseUsername": "Välj ett användarnamn för att gå med i spelarchartten",
|
||||||
|
"username": "Användarnamn",
|
||||||
|
"usernamePlaceholder": "Ange ditt användarnamn...",
|
||||||
|
"usernameHint": "3-20 tecken, endast bokstäver, siffror, - och _",
|
||||||
|
"joinButton": "Gå med i chatten",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Anpassa användarnamnsfargen",
|
||||||
|
"chooseSolid": "Välj en enfärgad färg:",
|
||||||
|
"customColor": "Anpassad färg:",
|
||||||
|
"preview": "Förhandsvisning:",
|
||||||
|
"previewUsername": "Användarnamn",
|
||||||
|
"apply": "Använd färg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "INSTÄLLNINGAR",
|
||||||
|
"java": "Java Runtime",
|
||||||
|
"useCustomJava": "Använd anpassad Java-sökväg",
|
||||||
|
"javaDescription": "Ersätt den medföljande Java-installationen med din egen",
|
||||||
|
"javaPath": "Java-körbar fil-sökväg",
|
||||||
|
"javaPathPlaceholder": "Välj Java-sökväg...",
|
||||||
|
"javaBrowse": "Bläddra",
|
||||||
|
"javaHint": "Välj Java-installationsmappen (stöder Windows, Mac, Linux)",
|
||||||
|
"discord": "Discord-integration",
|
||||||
|
"enableRPC": "Aktivera Discord Rich Presence",
|
||||||
|
"discordDescription": "Visa din launcher-aktivitet på Discord",
|
||||||
|
"game": "Spelalternativ",
|
||||||
|
"playerName": "Spelarnamn",
|
||||||
|
"playerNamePlaceholder": "Ange spelarnamn",
|
||||||
|
"playerNameHint": "Detta namn kommer att användas i spelet (1-16 tecken)",
|
||||||
|
"openGameLocation": "Öppna spelplats",
|
||||||
|
"openGameLocationDesc": "Öppna spelinstallationsmappen",
|
||||||
|
"account": "Spelare UUID-hantering",
|
||||||
|
"currentUUID": "Nuvarande UUID",
|
||||||
|
"uuidPlaceholder": "Laddar UUID...",
|
||||||
|
"copyUUID": "Kopiera UUID",
|
||||||
|
"regenerateUUID": "Återskapa UUID",
|
||||||
|
"uuidHint": "Din unika spelaridentifierare för detta användarnamn",
|
||||||
|
"manageUUIDs": "Hantera alla UUID:er",
|
||||||
|
"manageUUIDsDesc": "Visa och hantera alla spelare-UUID:er",
|
||||||
|
"language": "Språk",
|
||||||
|
"selectLanguage": "Välj språk",
|
||||||
|
"repairGame": "Reparera spel",
|
||||||
|
"reinstallGame": "Ominstallera spelfiler (bevarar data)",
|
||||||
|
"gpuPreference": "GPU-preferens",
|
||||||
|
"gpuHint": "Välj din föredragna GPU (Linux: påverkar DRI_PRIME)",
|
||||||
|
"gpuAuto": "Auto",
|
||||||
|
"gpuIntegrated": "Integrerad",
|
||||||
|
"gpuDedicated": "Dedikerad",
|
||||||
|
"logs": "SYSTEMLOGGAR",
|
||||||
|
"logsCopy": "Kopiera",
|
||||||
|
"logsRefresh": "Uppdatera",
|
||||||
|
"logsFolder": "Öppna mapp",
|
||||||
|
"logsLoading": "Laddar loggar...",
|
||||||
|
"closeLauncher": "Launcher-beteende",
|
||||||
|
"closeOnStart": "Stäng launcher vid spelstart",
|
||||||
|
"closeOnStartDescription": "Stäng automatiskt launcher efter att Hytale har startats",
|
||||||
|
"hwAccel": "Hårdvaruacceleration",
|
||||||
|
"hwAccelDescription": "Aktivera hårdvaruacceleration för launchern",
|
||||||
|
"gameBranch": "Spelgren",
|
||||||
|
"branchRelease": "Release",
|
||||||
|
"branchPreRelease": "Pre-Release",
|
||||||
|
"branchHint": "Växla mellan stabil release- och experimentell pre-release-version",
|
||||||
|
"branchWarning": "Att byta gren kommer att ladda ner och installera en annan spelversion",
|
||||||
|
"branchSwitching": "Byter till {branch}...",
|
||||||
|
"branchSwitched": "Bytte framgångsrikt till {branch}!",
|
||||||
|
"installRequired": "Installation krävs",
|
||||||
|
"branchInstallConfirm": "Spelet kommer att installeras för {branch}-grenen. Fortsätt?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "UUID-hantering",
|
||||||
|
"currentUserUUID": "Nuvarande användar-UUID",
|
||||||
|
"allPlayerUUIDs": "Alla spelare-UUID:er",
|
||||||
|
"generateNew": "Generera ny UUID",
|
||||||
|
"loadingUUIDs": "Laddar UUID:er...",
|
||||||
|
"setCustomUUID": "Ange anpassad UUID",
|
||||||
|
"customPlaceholder": "Ange anpassad UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "Ange UUID",
|
||||||
|
"warning": "Varning: Att ange en anpassad UUID kommer att ändra din nuvarande spelaridentitet",
|
||||||
|
"copyTooltip": "Kopiera UUID",
|
||||||
|
"regenerateTooltip": "Generera ny UUID"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Hantera profiler",
|
||||||
|
"newProfilePlaceholder": "Nytt profilnamn",
|
||||||
|
"createProfile": "Skapa profil"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Gå med i vår Discord-gemenskap!",
|
||||||
|
"joinButton": "Gå med i Discord"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Bekräfta",
|
||||||
|
"cancel": "Avbryt",
|
||||||
|
"save": "Spara",
|
||||||
|
"close": "Stäng",
|
||||||
|
"delete": "Ta bort",
|
||||||
|
"edit": "Redigera",
|
||||||
|
"loading": "Laddar...",
|
||||||
|
"apply": "Verkställ",
|
||||||
|
"install": "Installera"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Fel: Speldata hittades inte",
|
||||||
|
"gameUpdatedSuccess": "Spelet uppdaterades framgångsrikt! 🎉",
|
||||||
|
"updateFailed": "Uppdatering misslyckades: {error}",
|
||||||
|
"updateError": "Uppdateringsfel: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence aktiverad",
|
||||||
|
"discordDisabled": "Discord Rich Presence inaktiverad",
|
||||||
|
"discordSaveFailed": "Misslyckades med att spara Discord-inställning",
|
||||||
|
"playerNameRequired": "Ange ett giltigt spelarnamn",
|
||||||
|
"playerNameSaved": "Spelarnamn sparat framgångsrikt",
|
||||||
|
"playerNameSaveFailed": "Misslyckades med att spara spelarnamn",
|
||||||
|
"uuidCopied": "UUID kopierad till urklipp!",
|
||||||
|
"uuidCopyFailed": "Misslyckades med att kopiera UUID",
|
||||||
|
"uuidRegenNotAvailable": "UUID-återgenerering ej tillgänglig",
|
||||||
|
"uuidRegenFailed": "Misslyckades med att återgenerera UUID",
|
||||||
|
"uuidGenerated": "Ny UUID genererad framgångsrikt!",
|
||||||
|
"uuidGeneratedShort": "Ny UUID genererad!",
|
||||||
|
"uuidGenerateFailed": "Misslyckades med att generera ny UUID",
|
||||||
|
"uuidRequired": "Ange en UUID",
|
||||||
|
"uuidInvalidFormat": "Ogiltigt UUID-format",
|
||||||
|
"uuidSetFailed": "Misslyckades med att ange anpassad UUID",
|
||||||
|
"uuidSetSuccess": "Anpassad UUID angiven framgångsrikt!",
|
||||||
|
"uuidDeleteFailed": "Misslyckades med att ta bort UUID",
|
||||||
|
"uuidDeleteSuccess": "UUID borttagen framgångsrikt!",
|
||||||
|
"modsDownloading": "Laddar ner {name}...",
|
||||||
|
"modsTogglingMod": "Växlar modd...",
|
||||||
|
"modsDeletingMod": "Tar bort modd...",
|
||||||
|
"modsLoadingMods": "Laddar moddar från CurseForge...",
|
||||||
|
"modsInstalledSuccess": "{name} installerad framgångsrikt! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} borttagen framgångsrikt",
|
||||||
|
"modsDownloadFailed": "Misslyckades med att ladda ner modd: {error}",
|
||||||
|
"modsToggleFailed": "Misslyckades med att växla modd: {error}",
|
||||||
|
"modsDeleteFailed": "Misslyckades med att ta bort modd: {error}",
|
||||||
|
"modsModNotFound": "Moddinformation hittades inte",
|
||||||
|
"hwAccelSaved": "Hårdvaruaccelerationsinställning sparad",
|
||||||
|
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning",
|
||||||
|
"javaPathCopied": "Java-sökväg kopierad till urklipp!",
|
||||||
|
"javaPathCopyFailed": "Misslyckades med att kopiera Java-sökväg",
|
||||||
|
"javaPathSaved": "Java-sökväg sparad framgångsrikt!",
|
||||||
|
"javaPathSaveFailed": "Misslyckades med att spara Java-sökväg",
|
||||||
|
"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": {
|
||||||
|
"defaultTitle": "Bekräfta åtgärd",
|
||||||
|
"regenerateUuidTitle": "Generera ny UUID",
|
||||||
|
"regenerateUuidMessage": "Är du säker på att du vill generera en ny UUID? Detta kommer att ändra din spelaridentitet.",
|
||||||
|
"regenerateUuidButton": "Generera",
|
||||||
|
"setCustomUuidTitle": "Ange anpassad UUID",
|
||||||
|
"setCustomUuidMessage": "Är du säker på att du vill ange denna anpassade UUID? Detta kommer att ändra din spelaridentitet.",
|
||||||
|
"setCustomUuidButton": "Ange UUID",
|
||||||
|
"deleteUuidTitle": "Ta bort UUID",
|
||||||
|
"deleteUuidMessage": "Är du säker på att du vill ta bort UUID:n för \"{username}\"? Denna åtgärd kan inte ångras.",
|
||||||
|
"deleteUuidButton": "Ta bort",
|
||||||
|
"uninstallGameTitle": "Avinstallera spel",
|
||||||
|
"uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.",
|
||||||
|
"uninstallGameButton": "Avinstallera"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Initierar...",
|
||||||
|
"downloading": "Laddar ner...",
|
||||||
|
"installing": "Installerar...",
|
||||||
|
"extracting": "Extraherar...",
|
||||||
|
"verifying": "Verifierar...",
|
||||||
|
"switchingProfile": "Byter profil...",
|
||||||
|
"profileSwitched": "Profil bytt!",
|
||||||
|
"startingGame": "Startar spel...",
|
||||||
|
"launching": "STARTAR...",
|
||||||
|
"uninstallingGame": "Avinstallerar spel...",
|
||||||
|
"gameUninstalled": "Spel avinstallerat framgångsrikt!",
|
||||||
|
"uninstallFailed": "Avinstallation misslyckades: {error}",
|
||||||
|
"startingUpdate": "Startar obligatorisk speluppdatering...",
|
||||||
|
"installationComplete": "Installation slutförd framgångsrikt!",
|
||||||
|
"installationFailed": "Installation misslyckades: {error}",
|
||||||
|
"installingGameFiles": "Installerar spelfiler...",
|
||||||
|
"installComplete": "Installation slutförd!"
|
||||||
|
}
|
||||||
|
}
|
||||||
246
GUI/locales/tr-TR.json
Normal file
246
GUI/locales/tr-TR.json
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
{
|
||||||
|
"nav": {
|
||||||
|
"play": "Oyna",
|
||||||
|
"mods": "Modlar",
|
||||||
|
"news": "Haberler",
|
||||||
|
"chat": "Oyuncu Sohbeti",
|
||||||
|
"settings": "Ayarlar"
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"playersLabel": "Oyuncular:",
|
||||||
|
"manageProfiles": "Profilleri Yönet",
|
||||||
|
"defaultProfile": "Varsayılan"
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "ÜCRETSİZ OYNA BAŞLATICI",
|
||||||
|
"playerName": "Oyuncu Adı",
|
||||||
|
"playerNamePlaceholder": "Adınızı girin",
|
||||||
|
"gameBranch": "Oyun Sürümü",
|
||||||
|
"releaseVersion": "Yayın (Stabil)",
|
||||||
|
"preReleaseVersion": "Ön-Yayın (Deneysel)",
|
||||||
|
"customInstallation": "Özel Kurulum",
|
||||||
|
"installationFolder": "Kurulum Klasörü",
|
||||||
|
"pathPlaceholder": "Varsayılan konum",
|
||||||
|
"browse": "Gözat",
|
||||||
|
"installButton": "HYTALE KURU",
|
||||||
|
"installing": "KURULUYOR..."
|
||||||
|
},
|
||||||
|
"play": {
|
||||||
|
"ready": "OYNAMAYA HAZIR",
|
||||||
|
"subtitle": "Hytale'i başlat ve maceraya başla",
|
||||||
|
"playButton": "HYTALE'YI OYNA",
|
||||||
|
"latestNews": "SON HABERLER",
|
||||||
|
"viewAll": "HEPSINI GÖR",
|
||||||
|
"checking": "KONTROL EDİLİYOR...",
|
||||||
|
"play": "OYNA"
|
||||||
|
},
|
||||||
|
"mods": {
|
||||||
|
"searchPlaceholder": "Modları ara...",
|
||||||
|
"myMods": "BENİM MODLARIM",
|
||||||
|
"previous": "ÖNCEKİ",
|
||||||
|
"next": "SONRAKİ",
|
||||||
|
"page": "Sayfa",
|
||||||
|
"of": "nın",
|
||||||
|
"modalTitle": "BENİM MODLARIM",
|
||||||
|
"noModsFound": "Mod Bulunamadı",
|
||||||
|
"noModsFoundDesc": "Aramanızı ayarlamayı deneyin",
|
||||||
|
"noModsInstalled": "Hiçbir Mod Kurulu Değil",
|
||||||
|
"noModsInstalledDesc": "CurseForge'dan modlar ekleyin veya yerel dosyalar içe aktarın",
|
||||||
|
"view": "GÖR",
|
||||||
|
"install": "KURU",
|
||||||
|
"installed": "KURULU",
|
||||||
|
"enable": "ETKİNLEŞTİR",
|
||||||
|
"disable": "DEĞİ",
|
||||||
|
"active": "AKTİF",
|
||||||
|
"disabled": "DEĞİ",
|
||||||
|
"delete": "Modı sil",
|
||||||
|
"noDescription": "Açıklama yok",
|
||||||
|
"confirmDelete": "\"{name}\" öğesini silmek istediğinizden emin misiniz?",
|
||||||
|
"confirmDeleteDesc": "Bu işlem geri alınamaz.",
|
||||||
|
"confirmDeletion": "Silmeyi Onayla",
|
||||||
|
"apiKeyRequired": "API Anahtarı Gerekli",
|
||||||
|
"apiKeyRequiredDesc": "Modlara göz atmak için CurseForge API anahtarı gereklidir"
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"title": "TÜM HABERLER",
|
||||||
|
"readMore": "Daha Fazla Oku"
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"title": "OYUNCU SOHBETI",
|
||||||
|
"pickColor": "Renk",
|
||||||
|
"inputPlaceholder": "Mesajınızı yazın...",
|
||||||
|
"send": "Gönder",
|
||||||
|
"online": "çevrimiçi",
|
||||||
|
"charCounter": "{current}/{max}",
|
||||||
|
"secureChat": "Güvenli sohbet - Bağlantılar sansürlenir",
|
||||||
|
"joinChat": "Sohbete Katıl",
|
||||||
|
"chooseUsername": "Oyuncu Sohbetine katılmak için bir kullanıcı adı seçin",
|
||||||
|
"username": "Kullanıcı Adı",
|
||||||
|
"usernamePlaceholder": "Kullanıcı adınızı girin...",
|
||||||
|
"usernameHint": "3-20 karakter, yalnızca harfler, sayılar, - ve _",
|
||||||
|
"joinButton": "Sohbete Katıl",
|
||||||
|
"colorModal": {
|
||||||
|
"title": "Kullanıcı Adı Rengini Özelleştir",
|
||||||
|
"chooseSolid": "Düz bir renk seçin:",
|
||||||
|
"customColor": "Özel renk:",
|
||||||
|
"preview": "Ön izleme:",
|
||||||
|
"previewUsername": "Kullanıcı Adı",
|
||||||
|
"apply": "Rengi Uygula"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "AYARLAR",
|
||||||
|
"java": "Java Çalışma Zamanı",
|
||||||
|
"useCustomJava": "Özel Java Yolunu Kullan",
|
||||||
|
"javaDescription": "Yüklü Java çalışma zamanını kendi kurulumunuzla geçersiz kılın",
|
||||||
|
"javaPath": "Java Çalıştırılabilir Yolu",
|
||||||
|
"javaPathPlaceholder": "Java yolunu seçin...",
|
||||||
|
"javaBrowse": "Gözat",
|
||||||
|
"javaHint": "Java kurulum klasörünü seçin (Windows, Mac, Linux destekler)",
|
||||||
|
"discord": "Discord Entegrasyonu",
|
||||||
|
"enableRPC": "Discord Rich Presence'ı Etkinleştir",
|
||||||
|
"discordDescription": "Başlatıcı etkinliğinizi Discord'da gösterin",
|
||||||
|
"game": "Oyun Seçenekleri",
|
||||||
|
"playerName": "Oyuncu Adı",
|
||||||
|
"playerNamePlaceholder": "Oyuncu adınızı girin",
|
||||||
|
"playerNameHint": "Bu ad oyun içinde kullanılacak (1-16 karakter)",
|
||||||
|
"openGameLocation": "Oyun Konumunu Aç",
|
||||||
|
"openGameLocationDesc": "Oyun kurulum klasörünü açın",
|
||||||
|
"account": "Oyuncu UUID Yönetimi",
|
||||||
|
"currentUUID": "Geçerli UUID",
|
||||||
|
"uuidPlaceholder": "UUID yükleniyor...",
|
||||||
|
"copyUUID": "UUID'yi Kopyala",
|
||||||
|
"regenerateUUID": "UUID'yi Yeniden Oluştur",
|
||||||
|
"uuidHint": "Bu kullanıcı adı için benzersiz oyuncu tanımlayıcınız",
|
||||||
|
"manageUUIDs": "Tüm UUID'leri Yönet",
|
||||||
|
"manageUUIDsDesc": "Tüm oyuncu UUID'lerini görüntüleyin ve yönetin",
|
||||||
|
"language": "Dil",
|
||||||
|
"selectLanguage": "Dil Seçin",
|
||||||
|
"repairGame": "Oyunu Onarı",
|
||||||
|
"reinstallGame": "Oyun dosyalarını yeniden kur (veri korur)",
|
||||||
|
"gpuPreference": "GPU Tercihi",
|
||||||
|
"gpuHint": "Tercih ettiğiniz GPU'yu seçin (Linux: DRI_PRIME'ı etkiler)",
|
||||||
|
"gpuAuto": "Otomatik",
|
||||||
|
"gpuIntegrated": "Entegre",
|
||||||
|
"gpuDedicated": "Ayrılmış",
|
||||||
|
"logs": "SİSTEM KAYITLARI",
|
||||||
|
"logsCopy": "Kopyala",
|
||||||
|
"logsRefresh": "Yenile",
|
||||||
|
"logsFolder": "Klasörü Aç",
|
||||||
|
"logsLoading": "Loglar yükleniyor...",
|
||||||
|
"closeLauncher": "Başlatıcı Davranışı",
|
||||||
|
"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",
|
||||||
|
"gameBranch": "Oyun Dalı",
|
||||||
|
"branchRelease": "Yayın",
|
||||||
|
"branchPreRelease": "Ön-Yayın",
|
||||||
|
"branchHint": "Stabil yayın ve deneysel ön-yayın sürümleri arasında geçiş yapın",
|
||||||
|
"branchWarning": "Dalı değiştirmek farklı bir oyun sürümünü indirecek ve kuracaktır",
|
||||||
|
"branchSwitching": "{branch} sürümüne geçiliyor...",
|
||||||
|
"branchSwitched": "{branch} sürümüne başarıyla geçildi!",
|
||||||
|
"installRequired": "Kurulum Gerekli",
|
||||||
|
"branchInstallConfirm": "Oyun {branch} dalı için kurulacak. Devam et?"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"modalTitle": "UUID Yönetimi",
|
||||||
|
"currentUserUUID": "Geçerli Kullanıcı UUID",
|
||||||
|
"allPlayerUUIDs": "Tüm Oyuncu UUID'leri",
|
||||||
|
"generateNew": "Yeni UUID Oluştur",
|
||||||
|
"loadingUUIDs": "UUID'ler yükleniyor...",
|
||||||
|
"setCustomUUID": "Özel UUID Ayarla",
|
||||||
|
"customPlaceholder": "Özel UUID girin (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||||
|
"setUUID": "UUID Ayarla",
|
||||||
|
"warning": "Uyarı: Özel bir UUID ayarlamak geçerli oyuncu kimliğinizi değiştirecektir",
|
||||||
|
"copyTooltip": "UUID'yi Kopyala",
|
||||||
|
"regenerateTooltip": "Yeni UUID Oluştur"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"modalTitle": "Profilleri Yönet",
|
||||||
|
"newProfilePlaceholder": "Yeni Profil Adı",
|
||||||
|
"createProfile": "Profil Oluştur"
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"notificationText": "Discord topluluğumuza katılın!",
|
||||||
|
"joinButton": "Discord'a Katıl"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"confirm": "Onayla",
|
||||||
|
"cancel": "İptal",
|
||||||
|
"save": "Kaydet",
|
||||||
|
"close": "Kapat",
|
||||||
|
"delete": "Sil",
|
||||||
|
"edit": "Düzenle",
|
||||||
|
"loading": "Yükleniyor...",
|
||||||
|
"apply": "Uygula",
|
||||||
|
"install": "Kur"
|
||||||
|
},
|
||||||
|
"notifications": {
|
||||||
|
"gameDataNotFound": "Hata: Oyun verileri bulunamadı",
|
||||||
|
"gameUpdatedSuccess": "Oyun başarıyla güncellendi! 🎉",
|
||||||
|
"updateFailed": "Güncelleme başarısız: {error}",
|
||||||
|
"updateError": "Güncelleme hatası: {error}",
|
||||||
|
"discordEnabled": "Discord Rich Presence etkinleştirildi",
|
||||||
|
"discordDisabled": "Discord Rich Presence devre dışı bırakıldı",
|
||||||
|
"discordSaveFailed": "Discord ayarı kaydedilemedi",
|
||||||
|
"playerNameRequired": "Lütfen geçerli bir oyuncu adı girin",
|
||||||
|
"playerNameSaved": "Oyuncu adı başarıyla kaydedildi",
|
||||||
|
"playerNameSaveFailed": "Oyuncu adı kaydedilemedi",
|
||||||
|
"uuidCopied": "UUID panoya kopyalandı!",
|
||||||
|
"uuidCopyFailed": "UUID kopyalanamadı",
|
||||||
|
"uuidRegenNotAvailable": "UUID yeniden oluşturma kullanılamıyor",
|
||||||
|
"uuidRegenFailed": "UUID yeniden oluşturulamadı",
|
||||||
|
"uuidGenerated": "Yeni UUID başarıyla oluşturuldu!",
|
||||||
|
"uuidGeneratedShort": "Yeni UUID oluşturuldu!",
|
||||||
|
"uuidGenerateFailed": "Yeni UUID oluşturulamadı",
|
||||||
|
"uuidRequired": "Lütfen bir UUID girin",
|
||||||
|
"uuidInvalidFormat": "Geçersiz UUID formatı",
|
||||||
|
"uuidSetFailed": "Özel UUID ayarlanamadı",
|
||||||
|
"uuidSetSuccess": "Özel UUID başarıyla ayarlandı!",
|
||||||
|
"uuidDeleteFailed": "UUID silinemedi",
|
||||||
|
"uuidDeleteSuccess": "UUID başarıyla silindi!",
|
||||||
|
"modsDownloading": "{name} indiriliyor...",
|
||||||
|
"modsTogglingMod": "Mod değiştiriliyor...",
|
||||||
|
"modsDeletingMod": "Mod siliniyor...",
|
||||||
|
"modsLoadingMods": "CurseForge'dan modlar yükleniyor...",
|
||||||
|
"modsInstalledSuccess": "{name} başarıyla kuruldu! 🎉",
|
||||||
|
"modsDeletedSuccess": "{name} başarıyla silindi",
|
||||||
|
"modsDownloadFailed": "Mod indirilemedi: {error}",
|
||||||
|
"modsToggleFailed": "Mod değiştirilemedi: {error}",
|
||||||
|
"modsDeleteFailed": "Mod silinemedi: {error}",
|
||||||
|
"modsModNotFound": "Mod bilgileri bulunamadı"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"defaultTitle": "Eylemi onayla",
|
||||||
|
"regenerateUuidTitle": "Yeni UUID oluştur",
|
||||||
|
"regenerateUuidMessage": "Yeni bir UUID oluşturmak istediğinizden emin misiniz? Bu oyuncu kimliğinizi değiştirecektir.",
|
||||||
|
"regenerateUuidButton": "Oluştur",
|
||||||
|
"setCustomUuidTitle": "Özel UUID ayarla",
|
||||||
|
"setCustomUuidMessage": "Bu özel UUID'yi ayarlamak istediğinizden emin misiniz? Bu oyuncu kimliğinizi değiştirecektir.",
|
||||||
|
"setCustomUuidButton": "UUID Ayarla",
|
||||||
|
"deleteUuidTitle": "UUID'yi sil",
|
||||||
|
"deleteUuidMessage": "\"{username}\" için UUID'yi silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||||
|
"deleteUuidButton": "Sil",
|
||||||
|
"uninstallGameTitle": "Oyunu kaldır",
|
||||||
|
"uninstallGameMessage": "Hytale'yi kaldırmak istediğinizden emin misiniz? Tüm oyun dosyaları silinecektir.",
|
||||||
|
"uninstallGameButton": "Kaldır"
|
||||||
|
},
|
||||||
|
"progress": {
|
||||||
|
"initializing": "Başlatılıyor...",
|
||||||
|
"downloading": "İndiriliyor...",
|
||||||
|
"installing": "Kuruluyur...",
|
||||||
|
"extracting": "Ayıklanıyor...",
|
||||||
|
"verifying": "Doğrulanıyor...",
|
||||||
|
"switchingProfile": "Profil değiştiriliyor...",
|
||||||
|
"profileSwitched": "Profil değiştirildi!",
|
||||||
|
"startingGame": "Oyun başlatılıyor...",
|
||||||
|
"launching": "BAŞLATILIYOR...",
|
||||||
|
"uninstallingGame": "Oyun kaldırılıyor...",
|
||||||
|
"gameUninstalled": "Oyun başarıyla kaldırıldı!",
|
||||||
|
"uninstallFailed": "Kaldırma başarısız: {error}",
|
||||||
|
"startingUpdate": "Zorunlu oyun güncellemesi başlatılıyor...",
|
||||||
|
"installationComplete": "Kurulum başarıyla tamamlandı!",
|
||||||
|
"installationFailed": "Kurulum başarısız: {error}",
|
||||||
|
"installingGameFiles": "Oyun dosyaları kuruluyor...",
|
||||||
|
"installComplete": "Kurulum tamamlandı!"
|
||||||
|
}
|
||||||
|
}
|
||||||
535
GUI/style.css
535
GUI/style.css
@@ -662,6 +662,57 @@ body {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Radio buttons for install page */
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(147, 51, 234, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label .custom-radio {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label .custom-radio:checked ~ .radio-text {
|
||||||
|
color: #9333ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-label:has(.custom-radio:checked) {
|
||||||
|
background: rgba(147, 51, 234, 0.15);
|
||||||
|
border-color: #9333ea;
|
||||||
|
box-shadow: 0 0 20px rgba(147, 51, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #d1d5db;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-text i {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.launcher-container {
|
.launcher-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1719,15 +1770,252 @@ body {
|
|||||||
animation: shimmer 2s infinite;
|
animation: shimmer 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% {
|
0% {
|
||||||
left: -100%;
|
left: -100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
left: 100%;
|
left: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Progress Error and Retry Styles */
|
||||||
|
.progress-error-container {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
border-top: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
animation: errorSlideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes errorSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-message {
|
||||||
|
color: #f87171;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-shadow: 0 0 8px rgba(248, 113, 113, 0.4);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-retry-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-retry-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-retry-info {
|
||||||
|
color: #fbbf24;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-retry-btn {
|
||||||
|
background: linear-gradient(135deg, #dc2626, #ef4444);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3);
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-retry-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #b91c1c, #dc2626);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-retry-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 6px rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-retry-btn:disabled {
|
||||||
|
background: linear-gradient(135deg, #4b5563, #6b7280);
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress overlay error state */
|
||||||
|
.progress-overlay.error-state {
|
||||||
|
border-color: rgba(239, 68, 68, 0.5);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 16px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 30px rgba(239, 68, 68, 0.2),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-overlay.error-state #progressBarFill {
|
||||||
|
background: linear-gradient(90deg, #dc2626, #ef4444);
|
||||||
|
animation: errorPulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes errorPulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error type specific styling */
|
||||||
|
.progress-error-container.error-network {
|
||||||
|
border-top-color: rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-network .progress-error-message {
|
||||||
|
color: #60a5fa;
|
||||||
|
text-shadow: 0 0 8px rgba(96, 165, 250, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-stall {
|
||||||
|
border-top-color: rgba(245, 158, 11, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-stall .progress-error-message {
|
||||||
|
color: #fbbf24;
|
||||||
|
text-shadow: 0 0 8px rgba(251, 191, 36, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-file {
|
||||||
|
border-top-color: rgba(239, 68, 68, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-file .progress-error-message {
|
||||||
|
color: #f87171;
|
||||||
|
text-shadow: 0 0 8px rgba(248, 113, 113, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-permission {
|
||||||
|
border-top-color: rgba(168, 85, 247, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-permission .progress-error-message {
|
||||||
|
color: #a855f7;
|
||||||
|
text-shadow: 0 0 8px rgba(168, 85, 247, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-server {
|
||||||
|
border-top-color: rgba(236, 72, 153, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-server .progress-error-message {
|
||||||
|
color: #ec4899;
|
||||||
|
text-shadow: 0 0 8px rgba(236, 72, 153, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-corruption {
|
||||||
|
border-top-color: rgba(220, 38, 38, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-corruption .progress-error-message {
|
||||||
|
color: #dc2626;
|
||||||
|
text-shadow: 0 0 8px rgba(220, 38, 38, 0.6);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-butler {
|
||||||
|
border-top-color: rgba(245, 158, 11, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-butler .progress-error-message {
|
||||||
|
color: #f59e0b;
|
||||||
|
text-shadow: 0 0 8px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-space {
|
||||||
|
border-top-color: rgba(168, 85, 247, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-space .progress-error-message {
|
||||||
|
color: #a855f7;
|
||||||
|
text-shadow: 0 0 8px rgba(168, 85, 247, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-conflict {
|
||||||
|
border-top-color: rgba(6, 182, 212, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-error-container.error-conflict .progress-error-message {
|
||||||
|
color: #06b6d4;
|
||||||
|
text-shadow: 0 0 8px rgba(6, 182, 212, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connection quality indicators */
|
||||||
|
.progress-details {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-details #progressSize {
|
||||||
|
transition: color 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced retry button states */
|
||||||
|
.progress-retry-btn.retrying {
|
||||||
|
background: linear-gradient(135deg, #059669, #10b981);
|
||||||
|
animation: retryingPulse 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes retryingPulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 2px 8px rgba(5, 150, 105, 0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Network status indicator (optional future enhancement) */
|
||||||
|
.network-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #10b981;
|
||||||
|
box-shadow: 0 0 6px rgba(16, 185, 129, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-status.poor {
|
||||||
|
background: #f87171;
|
||||||
|
box-shadow: 0 0 6px rgba(248, 113, 113, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-status.fair {
|
||||||
|
background: #fbbf24;
|
||||||
|
box-shadow: 0 0 6px rgba(251, 191, 36, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
.progress-bar-fill {
|
.progress-bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -1795,70 +2083,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Installation effects */
|
/* Installation effects */
|
||||||
.installation-effects {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 80px;
|
|
||||||
width: calc(100% - 80px);
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
z-index: 40;
|
|
||||||
pointer-events: auto;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-effects {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
perspective: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warp-line {
|
|
||||||
position: absolute;
|
|
||||||
width: 2px;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(180deg,
|
|
||||||
transparent 0%,
|
|
||||||
rgba(147, 51, 234, 0.8) 50%,
|
|
||||||
transparent 100%);
|
|
||||||
box-shadow: 0 0 10px rgba(147, 51, 234, 0.8),
|
|
||||||
0 0 20px rgba(147, 51, 234, 0.4);
|
|
||||||
animation: warpSpeed 1.5s linear infinite;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warp-line:nth-child(1) { left: 10%; animation-delay: 0s; }
|
|
||||||
.warp-line:nth-child(2) { left: 25%; animation-delay: 0.2s; }
|
|
||||||
.warp-line:nth-child(3) { left: 40%; animation-delay: 0.4s; }
|
|
||||||
.warp-line:nth-child(4) { left: 55%; animation-delay: 0.6s; }
|
|
||||||
.warp-line:nth-child(5) { left: 70%; animation-delay: 0.8s; }
|
|
||||||
.warp-line:nth-child(6) { left: 85%; animation-delay: 1s; }
|
|
||||||
.warp-line:nth-child(7) { left: 15%; animation-delay: 0.3s; }
|
|
||||||
.warp-line:nth-child(8) { left: 60%; animation-delay: 0.7s; }
|
|
||||||
|
|
||||||
@keyframes warpSpeed {
|
|
||||||
0% {
|
|
||||||
transform: translateY(-100%) scaleY(0);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0%) scaleY(1);
|
|
||||||
}
|
|
||||||
90% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(100%) scaleY(2);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.mods-manager {
|
.mods-manager {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -5766,4 +5990,167 @@ select.settings-input option {
|
|||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Launcher Update Modal Styles */
|
||||||
|
.update-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100000;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-modal {
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||||
|
border: 2px solid rgba(147, 51, 234, 0.3);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2rem;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 20px 60px rgba(147, 51, 234, 0.3);
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(30px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #9333ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-header i {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-content {
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-version {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #9333ea;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-version {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-left: 3px solid #9333ea;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-progress {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-container {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #9333ea 0%, #7c3aed 100%);
|
||||||
|
width: 0%;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-note {
|
||||||
|
background: rgba(147, 51, 234, 0.1);
|
||||||
|
border: 1px solid rgba(147, 51, 234, 0.3);
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions .btn-primary {
|
||||||
|
background: linear-gradient(135deg, #9333ea 0%, #7c3aed 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions .btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(147, 51, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions .btn-secondary {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions .btn-secondary:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-actions button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|||||||
31
PKGBUILD
31
PKGBUILD
@@ -1,31 +1,28 @@
|
|||||||
# Maintainer: Terromur <terromuroz@proton.me>
|
# Maintainer: Terromur <terromuroz@proton.me>
|
||||||
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
||||||
pkgname=Hytale-F2P-git
|
# This PKGBUILD is for Github Releases
|
||||||
_pkgname=Hytale-F2P
|
pkgname=Hytale-F2P
|
||||||
pkgver=2.0.11.r120.gb05aeef
|
pkgver=2.1.1
|
||||||
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')
|
||||||
url="https://github.com/amiayweb/Hytale-F2P"
|
url="https://github.com/amiayweb/Hytale-F2P"
|
||||||
license=('custom')
|
license=('custom')
|
||||||
makedepends=('npm' 'git' 'rpm-tools' 'libxcrypt-compat')
|
depends=('gtk3' 'nss' 'libxcrypt-compat')
|
||||||
source=("git+$url.git" "Hytale-F2P.desktop")
|
makedepends=('npm')
|
||||||
|
source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
|
||||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||||
|
|
||||||
pkgver() {
|
|
||||||
cd "$_pkgname"
|
|
||||||
printf "2.0.11.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
|
||||||
}
|
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$_pkgname"
|
cd "$pkgname-$pkgver"
|
||||||
npm install
|
npm ci
|
||||||
npm run build:linux
|
npm run build:arch
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
mkdir -p "$pkgdir/opt/$_pkgname"
|
cd "$pkgname-$pkgver"
|
||||||
cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname"
|
install -d "$pkgdir/opt/$pkgname"
|
||||||
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
cp -r dist/linux-unpacked/* "$pkgdir/opt/$pkgname"
|
||||||
install -Dm644 "$_pkgname/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png"
|
install -Dm644 "$srcdir/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
|
||||||
|
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$pkgname.png"
|
||||||
}
|
}
|
||||||
|
|||||||
34
PKGBUILD-git
Normal file
34
PKGBUILD-git
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Maintainer: Terromur <terromuroz@proton.me>
|
||||||
|
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
||||||
|
pkgname=Hytale-F2P-git
|
||||||
|
_pkgname=Hytale-F2P
|
||||||
|
pkgver=0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Hytale-F2P - Unofficial Hytale Launcher for free to play with multiplayer support (rolling git build)"
|
||||||
|
arch=('x86_64')
|
||||||
|
url="https://github.com/amiayweb/Hytale-F2P"
|
||||||
|
license=('custom')
|
||||||
|
depends=('gtk3' 'nss' 'libxcrypt-compat')
|
||||||
|
makedepends=('git' 'npm')
|
||||||
|
source=("git+$url.git" "$_pkgname.desktop")
|
||||||
|
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||||
|
|
||||||
|
pkgver() {
|
||||||
|
cd "$srcdir/$_pkgname"
|
||||||
|
git describe --tags --long | sed 's/^v//;s/-/.r/;s/-/./'
|
||||||
|
}
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$srcdir/$_pkgname"
|
||||||
|
npm ci
|
||||||
|
npm run build:arch
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$srcdir/$_pkgname"
|
||||||
|
install -d "$pkgdir/opt/$_pkgname"
|
||||||
|
cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname"
|
||||||
|
|
||||||
|
install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
||||||
|
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png"
|
||||||
|
}
|
||||||
329
README.md
329
README.md
@@ -1,19 +1,34 @@
|
|||||||
# 🎮 Hytale F2P Launcher | Multiplayer Support [Windows, MacOS, Linux]
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|
<header>
|
||||||

|
<h1>🎮 Hytale F2P Launcher 🚀</h1>
|
||||||

|
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
|
||||||
|
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
|
||||||
|
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support!</small></p>
|
||||||
|
</header>
|
||||||
|
|
||||||
**A modern, cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)**
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
[](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!** ⭐
|
||||||
|
|
||||||
🛑 **Found a problem? Join the Discord: https://discord.gg/gME8rUy3MB** 🛑
|
### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
||||||
|
|
||||||
|
#### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
|
||||||
|
|
||||||
|
<p>
|
||||||
|
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
|
||||||
|
Any support is appreciated and helps keep the project going.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="https://buymeacoffee.com/hf2p">
|
||||||
|
<img src="https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExem14OW1tanN3eHlyYmR4NW1sYmJkOTZmbmJxejdjZXB6MXY5cW12MSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9cw/TDQOtnWgsBx99cNoyH/giphy.gif" width="120">
|
||||||
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -21,12 +36,42 @@
|
|||||||
## 📸 Screenshots
|
## 📸 Screenshots
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
<img src="https://i.imgur.com/xW9do3d.png" alt="Hytale F2P Launcher" width="1000">
|
||||||

|
<details>
|
||||||

|
<summary><b>View Gallery</b></summary>
|
||||||

|
<table style="width: 100%; border-spacing: 15px; border-collapse: separate;">
|
||||||

|
<tr>
|
||||||
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
|
<b>Mods Preview</b><br>
|
||||||
|
<img src="https://i.imgur.com/f8qyIJy.png" alt="Hytale F2P Mods" width="100%">
|
||||||
|
</td>
|
||||||
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
|
<b>Latest News</b><br>
|
||||||
|
<img src="https://i.imgur.com/qu0HltD.png" alt="Hytale F2P News" width="100%">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
|
<b>Social & Chat</b><br>
|
||||||
|
<img src="https://i.imgur.com/t3GmbfF.png" alt="Hytale F2P Chat" width="100%">
|
||||||
|
</td>
|
||||||
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
|
<b>Settings</b><br>
|
||||||
|
<img src="https://i.imgur.com/uUD7lDB.png" alt="Hytale F2P Settings" width="100%">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
|
<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%">
|
||||||
|
</td>
|
||||||
|
<td align="center" style="vertical-align: top; width: 50%;">
|
||||||
|
<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%">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -49,69 +94,256 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Quick Start
|
# 🚀 Quick Start
|
||||||
|
|
||||||
### 📥 Installation
|
## 🖥️ System Requirements
|
||||||
|
|
||||||
#### Windows
|
### 🎮 Hytale Hardware Requirements
|
||||||
1. Download the latest `Hytale-F2P.exe` from [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases)
|
|
||||||
2. Run the installer
|
|
||||||
3. Launch from desktop or start menu
|
|
||||||
|
|
||||||
#### Linux
|
> [!IMPORTANT]
|
||||||
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
|
> Hytale is designed to be accessible while scaling for high-end performance.
|
||||||
|
> Below are the [official system requirements for the Early Access](https://hytale.com/news/2025/12/hytale-hardware-requirements) release.
|
||||||
|
|
||||||
#### macOS
|
<div align="center">
|
||||||
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
|
|
||||||
|
|
||||||
#### 🖥️ How to play online on F2P?
|
<table>
|
||||||
See [SERVER.md](SERVER.md)
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>🥉 Minimum (1080p @ 30 FPS)</th>
|
||||||
|
<th>🥈 Recommended (1080p @ 60 FPS)</th>
|
||||||
|
<th>🥇 Best (1440p @ 60 FPS)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><b>🖥️ OS</b></td>
|
||||||
|
<td colspan="3" align="center">
|
||||||
|
Windows 10/11 (64-bit X64) | Linux (x64) | macOS (ARM64/Apple Silicon)
|
||||||
|
<br />
|
||||||
|
<small><i>⚠️ Note: ARM64 (Windows & Linux), macOS (x86/Intel) <b>are not supported!</b> ⚠️</i></small>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>⚙️ CPU</b></td>
|
||||||
|
<td>Intel i5-7500 / Ryzen 3 1200 / Apple M1</td>
|
||||||
|
<td>Intel i5-10400 / Ryzen 5 3600 / Apple M2</td>
|
||||||
|
<td>Intel i7-10700K / Ryzen 9 3800X / Apple M3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>🧠 RAM</b></td>
|
||||||
|
<td>8GB (dGPU) / 12GB (iGPU)<sup><a href="#fn1" id="ref1">1</a></sup></td>
|
||||||
|
<td>16 GB</td>
|
||||||
|
<td>32 GB</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>🎮 GPU</b></td>
|
||||||
|
<td>GTX 900 / RX 400 / UHD 620</td>
|
||||||
|
<td>GTX 1060 / RX 580 / Iris Xe</td>
|
||||||
|
<td>RTX 30 Series / RX 7000 Series</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>💾 Storage</b></td>
|
||||||
|
<td>20 GB (SATA SSD)</td>
|
||||||
|
<td>20 GB (NVMe SSD)</td>
|
||||||
|
<td>50 GB+ (NVMe SSD)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>🌐 Network</b></td>
|
||||||
|
<td>2 Mbit/s</td>
|
||||||
|
<td>8 Mbit/s</td>
|
||||||
|
<td>10+ Mbit/s</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
> [!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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Building from Source
|
### 🪟 Windows Prequisites
|
||||||
|
* **Java JDK 25:**
|
||||||
|
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows)
|
||||||
|
* [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.
|
||||||
|
* **Latest Visual Studio Redist:**
|
||||||
|
* 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/)
|
||||||
|
|
||||||
See [BUILD.md](BUILD.md) for comprehensive build instructions.
|
### 🐧 Linux Prequisites
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973).
|
||||||
|
|
||||||
|
* Make sure you have already installed newest **GPU driver** especially proprietary NVIDIA, consult your distro docs or wiki.
|
||||||
|
* Also make sure that your GPU can be connected to EGL, try checking it first (again, consult your distro docs or wiki) before installing Hytale game via our launcher.
|
||||||
|
* Install `libpng` package to avoid `SDL3_Image` error:
|
||||||
|
* `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro
|
||||||
|
* `libpng libpng-devel` for Fedora/RHEL-based Distro
|
||||||
|
* `libpng` for Arch-based Distro
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📌 Versioning Policy
|
## 📥 Installation
|
||||||
|
|
||||||
**⚠️ Important: Semantic Versioning Required**
|
### 🪟 Windows Installation
|
||||||
|
|
||||||
This project follows **strict semantic versioning** with **numerical versions only**:
|
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.
|
||||||
|
3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
|
||||||
|
* Click **More info**, then click **Run anyway**.
|
||||||
|
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.
|
||||||
|
|
||||||
- ✅ **Valid**: `2.0.1`, `2.0.11`, `2.1.0`, `3.0.0`
|
### 🐧 Linux Installation
|
||||||
- ❌ **Invalid**: `2.0.2b`, `2.0.2a`, `2.0.1-beta`, `v2.0.2b`
|
|
||||||
|
|
||||||
**Format**: `MAJOR.MINOR.PATCH` (e.g., `2.0.11`)
|
1. **Prerequisites:** Ensure you have installed all [**Linux Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-linux-prequisites) above.
|
||||||
|
2. **Download:** Choose the package that fits your distribution from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page:
|
||||||
|
* **Universal:** `.AppImage`
|
||||||
|
* **Arch Linux:** `.pkg.tar.zst`
|
||||||
|
* **Fedora/RHEL/openSUSE:** `.rpm`
|
||||||
|
* **Debian/Ubuntu:** `.deb`
|
||||||
|
3. **Permissions & Execution:**
|
||||||
|
* **AppImage:** Make the file executable and run it:
|
||||||
|
```bash
|
||||||
|
chmod +x hytale-f2p-launcher.AppImage
|
||||||
|
./hytale-f2p-launcher.AppImage
|
||||||
|
```
|
||||||
|
* **Ubuntu/Debian-based or Fedora/RHEL-based:** Install the DEB/RPM:
|
||||||
|
```bash
|
||||||
|
# Fedora/RHEL-based
|
||||||
|
sudo dnf install hytale-f2p-launcher.rpm
|
||||||
|
# Debian/Ubuntu
|
||||||
|
sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76
|
||||||
|
sudo dpkg -i hytale-f2p-launcher.deb
|
||||||
|
```
|
||||||
|
* **Arch Linux (pacman):** Install the package using:
|
||||||
|
```bash
|
||||||
|
# Stable Build
|
||||||
|
sudo pacman -U hytale-f2p-launcher.pkg.tar.zst
|
||||||
|
# Development Build
|
||||||
|
yay -S hytale-f2p-git # or
|
||||||
|
paru -S hytale-f2p-git
|
||||||
|
# Manual Build
|
||||||
|
git clone https://aur.archlinux.org/hytale-f2p-git.git
|
||||||
|
cd hytale-f2p-git
|
||||||
|
makepkg -si
|
||||||
|
```
|
||||||
|
|
||||||
- **MAJOR**: Breaking changes
|
> [!NOTE]
|
||||||
- **MINOR**: New features (backward compatible)
|
> Make sure to adjust the filename correctly with the version and the architecture type. TIP: Use `cd` command to the package location.
|
||||||
- **PATCH**: Bug fixes (backward compatible)
|
|
||||||
|
|
||||||
**Why?** The auto-update system requires semantic versioning for proper version comparison. Letter suffixes (like `2.0.2b`) are not supported and will cause update detection issues.
|
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
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Apple Silicon Users: If you are on an M1, M2, or M3 Mac, you may be prompted to install Rosetta 2 the first time you run the launcher. This is normal and required for compatibility.
|
||||||
|
|
||||||
|
1. **Download:** Get the latest `.dmg` file from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
|
||||||
|
2. **Mount:** Double-click the `.dmg` file to open it.
|
||||||
|
3. **Install:** Drag the **Hytale F2P Launcher** icon into your **Applications** folder.
|
||||||
|
4. **First Run:** If macOS prevents the app from opening because it is from an "unidentified developer":
|
||||||
|
* Open **System Settings** > **Privacy & Security**.
|
||||||
|
* Scroll down to the **Security** section.
|
||||||
|
* Look for the message regarding "Hytale F2P Launcher" and click **Open Anyway**.
|
||||||
|
* Authenticate with your password and click **Open**.
|
||||||
|
|
||||||
|
#### **Advanced: Manual Installation (.zip)**
|
||||||
|
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`).
|
||||||
|
2. **Remove Quarantine:** macOS often "quarantines" apps downloaded via browser. If the app won't open, open **Terminal** and run:
|
||||||
|
```bash
|
||||||
|
xattr -rd com.apple.quarantine /path/to/Hytale-F2P-Launcher.app
|
||||||
|
```
|
||||||
|
> [!TIP]
|
||||||
|
> Type the first part of the command, then drag the app icon into the Terminal window to auto-fill the path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📢 How to Host a Server
|
||||||
|
|
||||||
|
## 🌐 Host your Singleplayer Server (Online-Play Feature)
|
||||||
|
|
||||||
|
> [!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.
|
||||||
|
|
||||||
|
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`.
|
||||||
|
3. Check the status `Connected via STUN` or `Connected via UPnP`.
|
||||||
|
|
||||||
|
## 🖧 Host a Dedicated Server
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> If you already have the patched `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). Linux ARM64 is supported for server only.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed Troubleshooting guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔨 Building from Source
|
||||||
|
|
||||||
|
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 Changelog
|
## 📋 Changelog
|
||||||
|
|
||||||
### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)*
|
### 🆕 v2.1.1
|
||||||
|
- 🛠️ **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.
|
||||||
|
- ❎ **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.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click here to see older Changelogs</summary>
|
||||||
|
|
||||||
|
### 🔄 v2.1.0
|
||||||
|
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
|
||||||
|
- ⚡ **Hardware Acceleration** —
|
||||||
|
- 🔎 **Browse CurseForge Mods** — Browsing mods now easier with our dedicated CurseForge API Key.
|
||||||
|
- 🌎 **Fixes and Release New Translation** — Fixed 🇪🇸 🇧🇷 and added more translation for current build. Turkish 🇹🇷 language now added.
|
||||||
|
|
||||||
|
### 🔄 v2.0.2b *(Minor Update: Performance & Utilities)*
|
||||||
- 🌎 **Language Translation** — A big welcome for Spanish 🇪🇸 and Portuguese (Brazil) 🇧🇷 players! **Language setting can be found in the bottom part of Settings pane.**
|
- 🌎 **Language Translation** — A big welcome for Spanish 🇪🇸 and Portuguese (Brazil) 🇧🇷 players! **Language setting can be found in the bottom part of Settings pane.**
|
||||||
- 💻 **Laptop/Hybrid GPU Performance Issue Fix** — Added automatic GPU detection system and options to choose which GPU will be used for the game, *specifically for Linux users*.
|
- 💻 **Laptop/Hybrid GPU Performance Issue Fix** — Added automatic GPU detection system and options to choose which GPU will be used for the game, *specifically for Linux users*.
|
||||||
- 👨💻 **In-App Logging** — Reporting bugs and issues to `Github Issues` tab or `Open A Ticket` channel in our Discord Server has been made easier for players, no more finding logs file manually.
|
- 👨💻 **In-App Logging** — Reporting bugs and issues to `Github Issues` tab or `Open A Ticket` channel in our Discord Server has been made easier for players, no more finding logs file manually.
|
||||||
- 🛠️ **Repair Button** — Your game's broken? One button will fix them, go to Settings pane to Repair your game in one-click, **without losing any data**. If doing so did not fix your issue, please report it to us immediately!
|
- 🛠️ **Repair Button** — Your game's broken? One button will fix them, go to Settings pane to Repair your game in one-click, **without losing any data**. If doing so did not fix your issue, please report it to us immediately!
|
||||||
- 🐛 **Fixed Bugs** — Fixed issue [#84](https://github.com/amiayweb/Hytale-F2P/issues/84) where mods disappearing when game starts in previous launcher (v2.0.2a).
|
- 🐛 **Fixed Bugs** — Fixed issue [#84](https://github.com/amiayweb/Hytale-F2P/issues/84) where mods disappearing when game starts in previous launcher (v2.0.2a).
|
||||||
|
|
||||||
### 🆕 v2.0.2a *(Minor Update)*
|
|
||||||
|
### 🔄 v2.0.2a *(Minor Update)*
|
||||||
- 🧑🚀 **Profiles System** — Added proper profile management: create, switch, and delete profiles. Each profile now has its own **isolated mod list**.
|
- 🧑🚀 **Profiles System** — Added proper profile management: create, switch, and delete profiles. Each profile now has its own **isolated mod list**.
|
||||||
- 🔒 **Mod Isolation** — Fixed ModManager so mods are **strictly scoped to the active profile**. Browsing and installing now only affects the selected profile.
|
- 🔒 **Mod Isolation** — Fixed ModManager so mods are **strictly scoped to the active profile**. Browsing and installing now only affects the selected profile.
|
||||||
- 🚨 **Critical Path Fix** — Resolved a macOS bug where mods were being saved to a Windows path (`~/AppData/Local`) instead of `~/Library/Application Support`. Mods now save to the **correct location** and load properly in-game.
|
- 🚨 **Critical Path Fix** — Resolved a macOS bug where mods were being saved to a Windows path (`~/AppData/Local`) instead of `~/Library/Application Support`.
|
||||||
- 🛡️ **Stability Improvements** — Added an **auto-sync step before every launch** to ensure the physical mods folder always matches the active profile.
|
- 🛡️ **Stability Improvements** — Added an **auto-sync step before every launch** to ensure the physical mods folder always matches the active profile.
|
||||||
- 🎨 **UI Enhancements** — Added a **profile selector dropdown** and a **profile management modal**.
|
- 🎨 **UI Enhancements** — Added a **profile selector dropdown** and a **profile management modal**.
|
||||||
|
|
||||||
### 🆕 v2.0.2
|
### 🔄 v2.0.2
|
||||||
- 🎮 **Discord RPC Integration** - Added Discord Rich Presence with toggle in settings (enabled by default)
|
- 🎮 **Discord RPC Integration** - Added Discord Rich Presence with toggle in settings (enabled by default)
|
||||||
- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS
|
- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS
|
||||||
- 🎨 **Chat Improvements** - Simplified chat color system
|
- 🎨 **Chat Improvements** - Simplified chat color system
|
||||||
@@ -156,7 +388,7 @@ This project follows **strict semantic versioning** with **numerical versions on
|
|||||||
- ☕ **Java Management** - Automatic Java runtime handling
|
- ☕ **Java Management** - Automatic Java runtime handling
|
||||||
- 🎨 **Modern Interface** - Clean, intuitive design
|
- 🎨 **Modern Interface** - Clean, intuitive design
|
||||||
- 🌟 **First Release** - Core launcher functionality
|
- 🌟 **First Release** - Core launcher functionality
|
||||||
|
</details>
|
||||||
---
|
---
|
||||||
|
|
||||||
## 👥 Contributors
|
## 👥 Contributors
|
||||||
@@ -170,7 +402,7 @@ This project follows **strict semantic versioning** with **numerical versions on
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
### 🏆 Project Creator
|
### 🏆 Project Creator
|
||||||
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
|
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator | Windows*
|
||||||
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
|
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
|
||||||
|
|
||||||
### 🌟 Contributors
|
### 🌟 Contributors
|
||||||
@@ -181,6 +413,8 @@ This project follows **strict semantic versioning** with **numerical versions on
|
|||||||
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
|
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
|
||||||
- [**@crimera**](https://github.com/crimera) - *Issues fixer*
|
- [**@crimera**](https://github.com/crimera) - *Issues fixer*
|
||||||
- [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer*
|
- [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer*
|
||||||
|
- [**@Rahul-Sahani04**](https://github.com/Rahul-Sahani04) - *Issues fixer*
|
||||||
|
- [**@xSamiVS**](https://github.com/xSamiVS) - *Language Translator*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -224,7 +458,7 @@ This launcher is created for **educational purposes only**.
|
|||||||
|
|
||||||
🛑 **Takedown Policy** - If Hypixel Studios or Hytale requests removal, this project will be taken down immediately.
|
🛑 **Takedown Policy** - If Hypixel Studios or Hytale requests removal, this project will be taken down immediately.
|
||||||
|
|
||||||
❤️ **Support Official** - Please support the official game by purchasing it when available.
|
❤️ **Support Official** - Please support the official game by **purchasing** it legally when available.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -232,7 +466,8 @@ This launcher is created for **educational purposes only**.
|
|||||||
|
|
||||||
**⭐ Star this project if you found it helpful! ⭐**
|
**⭐ Star this project if you found it helpful! ⭐**
|
||||||
|
|
||||||
*Made with ❤️ by [@amiayweb](https://github.com/amiayweb) and the amazing community*
|
*Made with ❤️ by [@amiayweb](https://github.com/amiayweb) and the legendary contributors with amazing community*
|
||||||
|
|
||||||
[](https://www.star-history.com/#amiayweb/Hytale-F2P&type=date&legend=top-left)
|
[](https://www.star-history.com/#amiayweb/Hytale-F2P&type=date&legend=top-left)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ Set these before running to customize your server:
|
|||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
|
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
|
||||||
| `HYTALE_AUTH_DOMAIN` | `sanasol.ws` | Auth server domain |
|
| `HYTALE_AUTH_DOMAIN` | `auth.sanasol.ws` | Auth server domain (4-16 chars) |
|
||||||
| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port |
|
| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port |
|
||||||
| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) |
|
| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) |
|
||||||
| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name |
|
| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name |
|
||||||
@@ -400,7 +400,7 @@ docker run -d \
|
|||||||
--name hytale-server \
|
--name hytale-server \
|
||||||
-p 5520:5520/udp \
|
-p 5520:5520/udp \
|
||||||
-v ./data:/data \
|
-v ./data:/data \
|
||||||
-e HYTALE_AUTH_DOMAIN=sanasol.ws \
|
-e HYTALE_AUTH_DOMAIN=auth.sanasol.ws \
|
||||||
-e HYTALE_SERVER_NAME="My Server" \
|
-e HYTALE_SERVER_NAME="My Server" \
|
||||||
-e JVM_XMX=8G \
|
-e JVM_XMX=8G \
|
||||||
ghcr.io/hybrowse/hytale-server-docker:latest
|
ghcr.io/hybrowse/hytale-server-docker:latest
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ class AppUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupAutoUpdater() {
|
setupAutoUpdater() {
|
||||||
// Enable dev mode for testing (reads dev-app-update.yml)
|
|
||||||
// Only enable in development, not in production builds
|
|
||||||
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
|
|
||||||
autoUpdater.forceDevUpdateConfig = true;
|
|
||||||
console.log('Dev update mode enabled - using dev-app-update.yml');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure logger for electron-updater
|
// Configure logger for electron-updater
|
||||||
// Create a compatible logger interface
|
// Create a compatible logger interface
|
||||||
@@ -176,7 +170,7 @@ class AppUpdater {
|
|||||||
console.warn('macOS update error: App may not be code-signed. Auto-update requires code signing.');
|
console.warn('macOS update error: App may not be code-signed. Auto-update requires code signing.');
|
||||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||||
this.mainWindow.webContents.send('update-error', {
|
this.mainWindow.webContents.send('update-error', {
|
||||||
message: 'Auto-update requires code signing. Please download manually from GitHub.',
|
message: 'Please download manually from GitHub.',
|
||||||
code: err.code,
|
code: err.code,
|
||||||
isMacSigningError: true,
|
isMacSigningError: true,
|
||||||
requiresManualDownload: true,
|
requiresManualDownload: true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const os = require('os');
|
|||||||
|
|
||||||
|
|
||||||
// Default auth domain - can be overridden by env var or config
|
// Default auth domain - can be overridden by env var or config
|
||||||
const DEFAULT_AUTH_DOMAIN = 'sanasol.ws';
|
const DEFAULT_AUTH_DOMAIN = 'auth.sanasol.ws';
|
||||||
|
|
||||||
// Get auth domain from env, config, or default
|
// Get auth domain from env, config, or default
|
||||||
function getAuthDomain() {
|
function getAuthDomain() {
|
||||||
@@ -26,9 +26,10 @@ function getAuthDomain() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get full auth server URL
|
// Get full auth server URL
|
||||||
|
// Domain already includes subdomain (auth.sanasol.ws), so use directly
|
||||||
function getAuthServerUrl() {
|
function getAuthServerUrl() {
|
||||||
const domain = getAuthDomain();
|
const domain = getAuthDomain();
|
||||||
return `https://sessions.${domain}`;
|
return `https://${domain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save auth domain to config
|
// Save auth domain to config
|
||||||
@@ -165,13 +166,22 @@ function loadCloseLauncherOnStart() {
|
|||||||
return config.closeLauncherOnStart !== undefined ? config.closeLauncherOnStart : false;
|
return config.closeLauncherOnStart !== undefined ? config.closeLauncherOnStart : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveLauncherHardwareAcceleration(enabled) {
|
||||||
|
saveConfig({ launcherHardwareAcceleration: !!enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLauncherHardwareAcceleration() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.launcherHardwareAcceleration !== undefined ? config.launcherHardwareAcceleration : true;
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
// Config migration handles structure, but mod saves must go to the ACTIVE profile.
|
||||||
// Global installedMods is kept mainly for reference/migration.
|
// Global installedMods is kept mainly for reference/migration.
|
||||||
// The profile is the source of truth for enabled mods.
|
// 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]) {
|
||||||
@@ -304,6 +314,30 @@ function loadGpuPreference() {
|
|||||||
return config.gpuPreference || 'auto';
|
return config.gpuPreference || 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveVersionClient(versionClient) {
|
||||||
|
saveConfig({ version_client: versionClient });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVersionClient() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.version_client !== undefined ? config.version_client : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveVersionBranch(versionBranch) {
|
||||||
|
const branch = versionBranch || 'release';
|
||||||
|
if (branch !== 'release' && branch !== 'pre-release') {
|
||||||
|
console.warn(`Invalid branch "${branch}", defaulting to "release"`);
|
||||||
|
saveConfig({ version_branch: 'release' });
|
||||||
|
} else {
|
||||||
|
saveConfig({ version_branch: branch });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVersionBranch() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.version_branch || 'release';
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
@@ -343,5 +377,15 @@ module.exports = {
|
|||||||
loadGpuPreference,
|
loadGpuPreference,
|
||||||
// Close Launcher export
|
// Close Launcher export
|
||||||
saveCloseLauncherOnStart,
|
saveCloseLauncherOnStart,
|
||||||
loadCloseLauncherOnStart
|
loadCloseLauncherOnStart,
|
||||||
|
|
||||||
|
// Hardware Acceleration functions
|
||||||
|
saveLauncherHardwareAcceleration,
|
||||||
|
loadLauncherHardwareAcceleration,
|
||||||
|
|
||||||
|
// Version Management exports
|
||||||
|
saveVersionClient,
|
||||||
|
loadVersionClient,
|
||||||
|
saveVersionBranch,
|
||||||
|
loadVersionBranch
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const { loadVersionBranch } = require('./config');
|
||||||
|
|
||||||
function getAppDir() {
|
function getAppDir() {
|
||||||
const home = os.homedir();
|
const home = os.homedir();
|
||||||
@@ -13,6 +14,21 @@ function getAppDir() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get centralized UserData saves directory (NEW in 2.1.2)
|
||||||
|
* UserData is now stored separately from game installation
|
||||||
|
*/
|
||||||
|
function getHytaleSavesDir() {
|
||||||
|
const home = os.homedir();
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return path.join(home, 'AppData', 'Local', 'HytaleSaves');
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
return path.join(home, 'Library', 'Application Support', 'HytaleSaves');
|
||||||
|
} else {
|
||||||
|
return path.join(home, '.hytalesaves');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_APP_DIR = getAppDir();
|
const DEFAULT_APP_DIR = getAppDir();
|
||||||
|
|
||||||
function getResolvedAppDir(customPath) {
|
function getResolvedAppDir(customPath) {
|
||||||
@@ -48,8 +64,20 @@ function expandHome(inputPath) {
|
|||||||
const APP_DIR = DEFAULT_APP_DIR;
|
const APP_DIR = DEFAULT_APP_DIR;
|
||||||
const CACHE_DIR = path.join(APP_DIR, 'cache');
|
const CACHE_DIR = path.join(APP_DIR, 'cache');
|
||||||
const TOOLS_DIR = path.join(APP_DIR, 'butler');
|
const TOOLS_DIR = path.join(APP_DIR, 'butler');
|
||||||
const GAME_DIR = path.join(APP_DIR, 'release', 'package', 'game', 'latest');
|
|
||||||
const JRE_DIR = path.join(APP_DIR, 'release', 'package', 'jre', 'latest');
|
// Dynamic GAME_DIR and JRE_DIR based on version_branch from config
|
||||||
|
function getGameDir() {
|
||||||
|
const branch = loadVersionBranch();
|
||||||
|
return path.join(APP_DIR, branch, 'package', 'game', 'latest');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJreDir() {
|
||||||
|
const branch = loadVersionBranch();
|
||||||
|
return path.join(APP_DIR, branch, 'package', 'jre', 'latest');
|
||||||
|
}
|
||||||
|
|
||||||
|
const GAME_DIR = getGameDir();
|
||||||
|
const JRE_DIR = getJreDir();
|
||||||
const PLAYER_ID_FILE = path.join(APP_DIR, 'player_id.json');
|
const PLAYER_ID_FILE = path.join(APP_DIR, 'player_id.json');
|
||||||
|
|
||||||
function getClientCandidates(gameLatest) {
|
function getClientCandidates(gameLatest) {
|
||||||
@@ -156,7 +184,8 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
installPath = getAppDir();
|
installPath = getAppDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
|
const branch = loadVersionBranch();
|
||||||
|
const gameLatest = path.join(installPath, branch, 'package', 'game', 'latest');
|
||||||
|
|
||||||
const userDataPath = findUserDataPath(gameLatest);
|
const userDataPath = findUserDataPath(gameLatest);
|
||||||
|
|
||||||
@@ -165,8 +194,28 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
const profilesPath = path.join(userDataPath, 'Profiles');
|
const profilesPath = path.join(userDataPath, 'Profiles');
|
||||||
|
|
||||||
if (!fs.existsSync(modsPath)) {
|
if (!fs.existsSync(modsPath)) {
|
||||||
// Ensure the Mods directory exists
|
// Check for broken symlink to avoid EEXIST/EPERM on mkdir
|
||||||
fs.mkdirSync(modsPath, { recursive: true });
|
let isBrokenLink = false;
|
||||||
|
let pathExists = false;
|
||||||
|
try {
|
||||||
|
const stats = fs.lstatSync(modsPath);
|
||||||
|
pathExists = true;
|
||||||
|
if (stats.isSymbolicLink()) {
|
||||||
|
// Check if target exists
|
||||||
|
try {
|
||||||
|
fs.statSync(modsPath);
|
||||||
|
} catch {
|
||||||
|
isBrokenLink = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* path doesn't exist at all */ }
|
||||||
|
|
||||||
|
if (isBrokenLink) {
|
||||||
|
fs.unlinkSync(modsPath); // Remove broken symlink
|
||||||
|
}
|
||||||
|
if (!pathExists || isBrokenLink) {
|
||||||
|
fs.mkdirSync(modsPath, { recursive: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(disabledModsPath)) {
|
if (!fs.existsSync(disabledModsPath)) {
|
||||||
fs.mkdirSync(disabledModsPath, { recursive: true });
|
fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||||
@@ -184,19 +233,8 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
|
|
||||||
function getProfilesDir(customInstallPath = null) {
|
function getProfilesDir(customInstallPath = null) {
|
||||||
try {
|
try {
|
||||||
// get UserData path
|
// NEW 2.1.2: Use centralized UserData location
|
||||||
let installPath = customInstallPath;
|
const userDataPath = getHytaleSavesDir();
|
||||||
if (!installPath) {
|
|
||||||
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
|
|
||||||
if (fs.existsSync(configFile)) {
|
|
||||||
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
||||||
installPath = config.installPath || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!installPath) installPath = getAppDir();
|
|
||||||
|
|
||||||
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
|
|
||||||
const userDataPath = findUserDataPath(gameLatest);
|
|
||||||
const profilesDir = path.join(userDataPath, 'Profiles');
|
const profilesDir = path.join(userDataPath, 'Profiles');
|
||||||
|
|
||||||
if (!fs.existsSync(profilesDir)) {
|
if (!fs.existsSync(profilesDir)) {
|
||||||
@@ -212,6 +250,7 @@ function getProfilesDir(customInstallPath = null) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAppDir,
|
getAppDir,
|
||||||
|
getHytaleSavesDir,
|
||||||
getResolvedAppDir,
|
getResolvedAppDir,
|
||||||
expandHome,
|
expandHome,
|
||||||
APP_DIR,
|
APP_DIR,
|
||||||
@@ -219,6 +258,8 @@ module.exports = {
|
|||||||
TOOLS_DIR,
|
TOOLS_DIR,
|
||||||
GAME_DIR,
|
GAME_DIR,
|
||||||
JRE_DIR,
|
JRE_DIR,
|
||||||
|
getGameDir,
|
||||||
|
getJreDir,
|
||||||
PLAYER_ID_FILE,
|
PLAYER_ID_FILE,
|
||||||
getClientCandidates,
|
getClientCandidates,
|
||||||
findClientPath,
|
findClientPath,
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ const {
|
|||||||
loadLanguage,
|
loadLanguage,
|
||||||
saveCloseLauncherOnStart,
|
saveCloseLauncherOnStart,
|
||||||
loadCloseLauncherOnStart,
|
loadCloseLauncherOnStart,
|
||||||
|
|
||||||
|
// Hardware Acceleration
|
||||||
|
saveLauncherHardwareAcceleration,
|
||||||
|
loadLauncherHardwareAcceleration,
|
||||||
|
|
||||||
|
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
@@ -33,7 +39,12 @@ const {
|
|||||||
resetCurrentUserUuid,
|
resetCurrentUserUuid,
|
||||||
// GPU Preference
|
// GPU Preference
|
||||||
saveGpuPreference,
|
saveGpuPreference,
|
||||||
loadGpuPreference
|
loadGpuPreference,
|
||||||
|
// Version Management
|
||||||
|
saveVersionClient,
|
||||||
|
loadVersionClient,
|
||||||
|
saveVersionBranch,
|
||||||
|
loadVersionBranch
|
||||||
} = require('./core/config');
|
} = require('./core/config');
|
||||||
|
|
||||||
const { getResolvedAppDir, getModsPath } = require('./core/paths');
|
const { getResolvedAppDir, getModsPath } = require('./core/paths');
|
||||||
@@ -71,7 +82,6 @@ const {
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
const {
|
const {
|
||||||
getInstalledClientVersion,
|
|
||||||
getLatestClientVersion
|
getLatestClientVersion
|
||||||
} = require('./services/versionManager');
|
} = require('./services/versionManager');
|
||||||
|
|
||||||
@@ -121,23 +131,30 @@ module.exports = {
|
|||||||
// Discord RPC functions
|
// Discord RPC functions
|
||||||
saveDiscordRPC,
|
saveDiscordRPC,
|
||||||
loadDiscordRPC,
|
loadDiscordRPC,
|
||||||
|
|
||||||
// Language functions
|
// Language functions
|
||||||
saveLanguage,
|
saveLanguage,
|
||||||
loadLanguage,
|
loadLanguage,
|
||||||
|
|
||||||
// Close Launcher functions
|
// Close Launcher functions
|
||||||
saveCloseLauncherOnStart,
|
saveCloseLauncherOnStart,
|
||||||
loadCloseLauncherOnStart,
|
loadCloseLauncherOnStart,
|
||||||
|
|
||||||
|
// Hardware Acceleration functions
|
||||||
|
saveLauncherHardwareAcceleration,
|
||||||
|
loadLauncherHardwareAcceleration,
|
||||||
|
|
||||||
// GPU Preference functions
|
// GPU Preference functions
|
||||||
saveGpuPreference,
|
saveGpuPreference,
|
||||||
loadGpuPreference,
|
loadGpuPreference,
|
||||||
detectGpu,
|
detectGpu,
|
||||||
|
|
||||||
// Version functions
|
// Version functions
|
||||||
getInstalledClientVersion,
|
|
||||||
getLatestClientVersion,
|
getLatestClientVersion,
|
||||||
|
saveVersionClient,
|
||||||
|
loadVersionClient,
|
||||||
|
saveVersionBranch,
|
||||||
|
loadVersionBranch,
|
||||||
|
|
||||||
// News functions
|
// News functions
|
||||||
getHytaleNews,
|
getHytaleNews,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class Logger {
|
|||||||
|
|
||||||
fs.appendFileSync(this.logFile, message, 'utf8');
|
fs.appendFileSync(this.logFile, message, 'utf8');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.originalConsole.error('Impossible d\'écrire dans le fichier de log:', error.message);
|
this.originalConsole.error('Unable to write to log file:', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ 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 } = require('../core/config');
|
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain, loadVersionBranch, loadVersionClient, saveVersionClient } = require('../core/config');
|
||||||
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
||||||
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
|
const { getLatestClientVersion } = require('../services/versionManager');
|
||||||
const { updateGameFiles } = require('./gameManager');
|
const { updateGameFiles } = require('./gameManager');
|
||||||
const { syncModsForCurrentProfile } = require('./modManager');
|
const { syncModsForCurrentProfile } = require('./modManager');
|
||||||
|
const { getUserDataPath } = require('../utils/userDataMigration');
|
||||||
|
|
||||||
// Client patcher for custom auth server (sanasol.ws)
|
// Client patcher for custom auth server (sanasol.ws)
|
||||||
let clientPatcher = null;
|
let clientPatcher = null;
|
||||||
@@ -101,11 +102,14 @@ function generateLocalTokens(uuid, name) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') {
|
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||||
|
const branch = branchOverride || loadVersionBranch();
|
||||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||||
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
|
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||||
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
|
|
||||||
|
// NEW 2.1.2: Use centralized UserData location
|
||||||
|
const userDataDir = getUserDataPath();
|
||||||
|
|
||||||
const gameLatest = customGameDir;
|
const gameLatest = customGameDir;
|
||||||
let clientPath = findClientPath(gameLatest);
|
let clientPath = findClientPath(gameLatest);
|
||||||
@@ -151,32 +155,29 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName);
|
const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName);
|
||||||
|
|
||||||
// Patch client and server binaries to use custom auth server (BEFORE signing on macOS)
|
// Patch client and server binaries to use custom auth server (BEFORE signing on macOS)
|
||||||
|
// FORCE patch on every launch to ensure consistency
|
||||||
const authDomain = getAuthDomain();
|
const authDomain = getAuthDomain();
|
||||||
if (clientPatcher) {
|
if (clientPatcher) {
|
||||||
try {
|
try {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Patching game for custom server...', null, null, null, null);
|
progressCallback('Patching game for custom server...', null, null, null, null);
|
||||||
}
|
}
|
||||||
console.log(`Patching game binaries for ${authDomain}...`);
|
console.log(`Force patching game binaries for ${authDomain}...`);
|
||||||
|
|
||||||
const patchResult = await clientPatcher.ensureClientPatched(gameLatest, (msg, percent) => {
|
const patchResult = await clientPatcher.ensureClientPatched(gameLatest, (msg, percent) => {
|
||||||
console.log(`[Patcher] ${msg}`);
|
// console.log(`[Patcher] ${msg}`);
|
||||||
if (progressCallback && msg) {
|
if (progressCallback && msg) {
|
||||||
progressCallback(msg, percent, null, null, null);
|
progressCallback(msg, percent, null, null, null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (patchResult.success) {
|
if (patchResult.success) {
|
||||||
if (patchResult.alreadyPatched) {
|
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
|
||||||
console.log(`Game already patched for ${authDomain}`);
|
if (patchResult.client) {
|
||||||
} else {
|
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
|
||||||
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
|
}
|
||||||
if (patchResult.client) {
|
if (patchResult.server) {
|
||||||
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
|
console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
|
||||||
}
|
|
||||||
if (patchResult.server) {
|
|
||||||
console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Game patching failed:', patchResult.error);
|
console.warn('Game patching failed:', patchResult.error);
|
||||||
@@ -284,6 +285,55 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||||
Object.assign(env, gpuEnv);
|
Object.assign(env, gpuEnv);
|
||||||
|
|
||||||
|
// Linux: Replace bundled libzstd.so with system version to fix glibc 2.41+ crash
|
||||||
|
// The bundled libzstd causes "free(): invalid pointer" on Steam Deck / Ubuntu LTS
|
||||||
|
if (process.platform === 'linux' && process.env.HYTALE_NO_LIBZSTD_FIX !== '1') {
|
||||||
|
const clientDir = path.dirname(clientPath);
|
||||||
|
const bundledLibzstd = path.join(clientDir, 'libzstd.so');
|
||||||
|
const backupLibzstd = path.join(clientDir, 'libzstd.so.bundled');
|
||||||
|
|
||||||
|
// Common system libzstd paths
|
||||||
|
const systemLibzstdPaths = [
|
||||||
|
'/usr/lib/libzstd.so.1', // Arch Linux, Steam Deck
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libzstd.so.1', // Debian/Ubuntu
|
||||||
|
'/usr/lib64/libzstd.so.1' // Fedora/RHEL
|
||||||
|
];
|
||||||
|
|
||||||
|
let systemLibzstd = null;
|
||||||
|
for (const p of systemLibzstdPaths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
systemLibzstd = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemLibzstd && fs.existsSync(bundledLibzstd)) {
|
||||||
|
try {
|
||||||
|
const stats = fs.lstatSync(bundledLibzstd);
|
||||||
|
|
||||||
|
// Only replace if it's not already a symlink to system version
|
||||||
|
if (!stats.isSymbolicLink()) {
|
||||||
|
// Backup bundled version
|
||||||
|
if (!fs.existsSync(backupLibzstd)) {
|
||||||
|
fs.renameSync(bundledLibzstd, backupLibzstd);
|
||||||
|
console.log(`Linux: Backed up bundled libzstd.so`);
|
||||||
|
} else {
|
||||||
|
fs.unlinkSync(bundledLibzstd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create symlink to system version
|
||||||
|
fs.symlinkSync(systemLibzstd, bundledLibzstd);
|
||||||
|
console.log(`Linux: Linked libzstd.so to system version (${systemLibzstd}) for glibc 2.41+ compatibility`);
|
||||||
|
} else {
|
||||||
|
const linkTarget = fs.readlinkSync(bundledLibzstd);
|
||||||
|
console.log(`Linux: libzstd.so already linked to ${linkTarget}`);
|
||||||
|
}
|
||||||
|
} catch (libzstdError) {
|
||||||
|
console.warn(`Linux: Could not replace libzstd.so: ${libzstdError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let spawnOptions = {
|
let spawnOptions = {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
@@ -333,6 +383,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Monitor game process status in background
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!hasExited) {
|
if (!hasExited) {
|
||||||
console.log('Game appears to be running successfully');
|
console.log('Game appears to be running successfully');
|
||||||
@@ -345,6 +396,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
|
// Return immediately, don't wait for setTimeout
|
||||||
return { success: true, installed: true, launched: true, pid: child.pid };
|
return { success: true, installed: true, launched: true, pid: child.pid };
|
||||||
} catch (spawnError) {
|
} catch (spawnError) {
|
||||||
console.error(`Error spawning game process: ${spawnError.message}`);
|
console.error(`Error spawning game process: ${spawnError.message}`);
|
||||||
@@ -355,23 +407,23 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') {
|
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||||
try {
|
try {
|
||||||
|
const branch = branchOverride || loadVersionBranch();
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Checking for updates...', 0, null, null, null);
|
progressCallback('Checking for updates...', 0, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [installedVersion, latestVersion] = await Promise.all([
|
const installedVersion = loadVersionClient();
|
||||||
getInstalledClientVersion(),
|
const latestVersion = await getLatestClientVersion(branch);
|
||||||
getLatestClientVersion()
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(`Installed version: ${installedVersion}, Latest version: ${latestVersion}`);
|
console.log(`Installed version: ${installedVersion}, Latest version: ${latestVersion} (branch: ${branch})`);
|
||||||
|
|
||||||
let needsUpdate = false;
|
let needsUpdate = false;
|
||||||
if (installedVersion && latestVersion && installedVersion !== latestVersion) {
|
if (!installedVersion || installedVersion !== latestVersion) {
|
||||||
needsUpdate = true;
|
needsUpdate = true;
|
||||||
console.log('Version mismatch detected, update required');
|
console.log('Version mismatch or not installed, update required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsUpdate) {
|
if (needsUpdate) {
|
||||||
@@ -380,13 +432,13 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||||
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customToolsDir = path.join(customAppDir, 'butler');
|
const customToolsDir = path.join(customAppDir, 'butler');
|
||||||
const customCacheDir = path.join(customAppDir, 'cache');
|
const customCacheDir = path.join(customAppDir, 'cache');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir);
|
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir, branch);
|
||||||
console.log('Game updated successfully, waiting before launch...');
|
console.log('Game updated successfully, patching will be forced on launch...');
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Preparing game launch...', 90, null, null, null);
|
progressCallback('Preparing game launch...', 90, null, null, null);
|
||||||
@@ -406,13 +458,22 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
|||||||
progressCallback('Launching game...', 80, null, null, null);
|
progressCallback('Launching game...', 80, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference);
|
const launchResult = await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
|
||||||
|
|
||||||
|
// Ensure we always return a result
|
||||||
|
if (!launchResult) {
|
||||||
|
console.error('launchGame returned null/undefined, creating fallback response');
|
||||||
|
return { success: false, error: 'Game launch failed - no response from launcher' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return launchResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in version check and launch:', error);
|
console.error('Error in version check and launch:', error);
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Error: ${error.message}`, -1, null, null, null);
|
progressCallback(`Error: ${error.message}`, -1, null, null, null);
|
||||||
}
|
}
|
||||||
throw error;
|
// Always return an error response instead of throwing
|
||||||
|
return { success: false, error: error.message || 'Unknown launch error' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ const path = require('path');
|
|||||||
const { execFile } = require('child_process');
|
const { execFile } = require('child_process');
|
||||||
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||||
const { getOS, getArch } = require('../utils/platformUtils');
|
const { getOS, getArch } = require('../utils/platformUtils');
|
||||||
const { downloadFile } = 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 { installButler } = require('./butlerManager');
|
const { installButler } = require('./butlerManager');
|
||||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||||
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config');
|
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, 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');
|
||||||
|
|
||||||
async function downloadPWR(version = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR) {
|
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||||
const osName = getOS();
|
const osName = getOS();
|
||||||
const arch = getArch();
|
const arch = getArch();
|
||||||
|
|
||||||
@@ -18,24 +19,143 @@ async function downloadPWR(version = 'release', fileName = '4.pwr', progressCall
|
|||||||
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
|
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`;
|
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}`;
|
||||||
|
const dest = path.join(cacheDir, `${branch}_${fileName}`);
|
||||||
|
|
||||||
const dest = path.join(cacheDir, fileName);
|
// Check if file exists and validate it
|
||||||
|
if (fs.existsSync(dest) && !manualRetry) {
|
||||||
if (fs.existsSync(dest)) {
|
|
||||||
console.log('PWR file found in cache:', dest);
|
console.log('PWR file found in cache:', dest);
|
||||||
return dest;
|
|
||||||
|
// Validate file size (PWR files should be > 1MB and >= 1.5GB for complete downloads)
|
||||||
|
const stats = fs.statSync(dest);
|
||||||
|
if (stats.size < 1024 * 1024) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file is under 1.5 GB (incomplete download)
|
||||||
|
const sizeInMB = stats.size / 1024 / 1024;
|
||||||
|
if (sizeInMB < 1500) {
|
||||||
|
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Fetching PWR patch file:', url);
|
console.log('Fetching PWR patch file:', url);
|
||||||
await downloadFile(url, dest, progressCallback);
|
|
||||||
|
try {
|
||||||
|
if (manualRetry) {
|
||||||
|
await retryDownload(url, dest, progressCallback);
|
||||||
|
} else {
|
||||||
|
await downloadFile(url, dest, progressCallback);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Check for automatic stall retry conditions (only for stall errors, not manual retries)
|
||||||
|
if (!manualRetry &&
|
||||||
|
error.message &&
|
||||||
|
error.message.includes('stalled') &&
|
||||||
|
error.canRetry !== false && // Explicitly check it's not false
|
||||||
|
(!error.retryState || error.retryState.automaticStallRetries < MAX_AUTOMATIC_STALL_RETRIES)) {
|
||||||
|
|
||||||
|
console.log(`[PWR] Automatic stall retry triggered (${(error.retryState && error.retryState.automaticStallRetries || 0) + 1}/${MAX_AUTOMATIC_STALL_RETRIES})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await retryStalledDownload(url, dest, progressCallback, error);
|
||||||
|
console.log('[PWR] Automatic stall retry successful');
|
||||||
|
|
||||||
|
// After successful automatic retry, continue with normal flow - the file should be valid now
|
||||||
|
const retryStats = fs.statSync(dest);
|
||||||
|
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
|
if (!validatePWRFile(dest)) {
|
||||||
|
console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`);
|
||||||
|
fs.unlinkSync(dest);
|
||||||
|
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} catch (retryError) {
|
||||||
|
console.error('[PWR] Automatic stall retry failed:', retryError.message);
|
||||||
|
|
||||||
|
// Create enhanced error with updated retry state
|
||||||
|
const enhancedError = new Error(`PWR download failed after automatic retries: ${retryError.message}`);
|
||||||
|
enhancedError.originalError = retryError;
|
||||||
|
enhancedError.retryState = retryError.retryState || error.retryState || null;
|
||||||
|
enhancedError.canRetry = true; // Still allow manual retry
|
||||||
|
enhancedError.pwrUrl = url;
|
||||||
|
enhancedError.pwrDest = dest;
|
||||||
|
enhancedError.branch = branch;
|
||||||
|
enhancedError.fileName = fileName;
|
||||||
|
enhancedError.cacheDir = cacheDir;
|
||||||
|
enhancedError.automaticRetriesExhausted = true;
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced error handling for retry UI (non-stall errors or exhausted automatic retries)
|
||||||
|
const enhancedError = new Error(`PWR download failed: ${error.message}`);
|
||||||
|
enhancedError.originalError = error;
|
||||||
|
enhancedError.retryState = error.retryState || null;
|
||||||
|
enhancedError.canRetry = error.isConnectionLost ? false : (error.canRetry !== false); // Don't allow retry for connection lost
|
||||||
|
enhancedError.pwrUrl = url;
|
||||||
|
enhancedError.pwrDest = dest;
|
||||||
|
enhancedError.branch = branch;
|
||||||
|
enhancedError.fileName = fileName;
|
||||||
|
enhancedError.cacheDir = cacheDir;
|
||||||
|
enhancedError.isConnectionLost = error.isConnectionLost || false;
|
||||||
|
|
||||||
|
console.log(`[PWR] Error handling:`, {
|
||||||
|
message: enhancedError.message,
|
||||||
|
isConnectionLost: enhancedError.isConnectionLost,
|
||||||
|
canRetry: enhancedError.canRetry,
|
||||||
|
retryState: enhancedError.retryState
|
||||||
|
});
|
||||||
|
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced PWR file validation
|
||||||
|
const stats = fs.statSync(dest);
|
||||||
|
console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
|
if (!validatePWRFile(dest)) {
|
||||||
|
console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`);
|
||||||
|
fs.unlinkSync(dest);
|
||||||
|
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('PWR saved to:', dest);
|
console.log('PWR saved to:', dest);
|
||||||
|
console.log(`[PWR Validation] PWR file validation passed: ${dest}`);
|
||||||
|
|
||||||
return dest;
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR) {
|
// Manual retry function for PWR downloads
|
||||||
|
async function retryPWRDownload(branch, fileName, progressCallback, cacheDir = CACHE_DIR) {
|
||||||
|
console.log('Initiating manual PWR retry...');
|
||||||
|
return await downloadPWR(branch, fileName, progressCallback, cacheDir, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR) {
|
||||||
|
console.log(`[Butler] Starting PWR application with:`);
|
||||||
|
console.log(`[Butler] - PWR file: ${pwrFile}`);
|
||||||
|
console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`);
|
||||||
|
console.log(`[Butler] - Game dir: ${gameDir}`);
|
||||||
|
console.log(`[Butler] - Branch: ${branch}`);
|
||||||
|
console.log(`[Butler] - Cache dir: ${cacheDir}`);
|
||||||
|
|
||||||
|
// Validate PWR file exists and get diagnostic info
|
||||||
|
if (!pwrFile || typeof pwrFile !== 'string' || !fs.existsSync(pwrFile)) {
|
||||||
|
throw new Error(`PWR file not found: ${pwrFile || 'undefined'}. Please retry download.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwrStats = fs.statSync(pwrFile);
|
||||||
|
console.log(`[Butler] PWR file size: ${(pwrStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(`[Butler] PWR file exists: ${fs.existsSync(pwrFile)}`);
|
||||||
|
|
||||||
const butlerPath = await installButler(toolsDir);
|
const butlerPath = await installButler(toolsDir);
|
||||||
|
console.log(`[Butler] Butler path: ${butlerPath}`);
|
||||||
|
console.log(`[Butler] Butler executable: ${fs.existsSync(butlerPath)}`);
|
||||||
|
|
||||||
const gameLatest = gameDir;
|
const gameLatest = gameDir;
|
||||||
const stagingDir = path.join(gameLatest, 'staging-temp');
|
const stagingDir = path.join(gameLatest, 'staging-temp');
|
||||||
|
|
||||||
@@ -46,12 +166,11 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(gameLatest)) {
|
// Validate and prepare directories
|
||||||
fs.mkdirSync(gameLatest, { recursive: true });
|
validateGameDirectory(gameLatest, stagingDir);
|
||||||
}
|
|
||||||
if (!fs.existsSync(stagingDir)) {
|
console.log(`[Butler] Game directory validated: ${gameLatest}`);
|
||||||
fs.mkdirSync(stagingDir, { recursive: true });
|
console.log(`[Butler] Staging directory validated: ${stagingDir}`);
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Installing game patch...', null, null, null, null);
|
progressCallback('Installing game patch...', null, null, null, null);
|
||||||
@@ -75,6 +194,8 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
gameLatest
|
gameLatest
|
||||||
];
|
];
|
||||||
|
|
||||||
|
console.log(`[Butler] Executing command: ${butlerPath} ${args.join(' ')}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
const child = execFile(butlerPath, args, {
|
const child = execFile(butlerPath, args, {
|
||||||
@@ -82,16 +203,97 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
timeout: 600000
|
timeout: 600000
|
||||||
}, (error, stdout, stderr) => {
|
}, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Butler stderr:', stderr);
|
console.error('[Butler] stderr:', stderr);
|
||||||
console.error('Butler stdout:', stdout);
|
console.error('[Butler] stdout:', stdout);
|
||||||
reject(new Error(`Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`));
|
console.error('[Butler] error code:', error.code);
|
||||||
|
console.error('[Butler] error signal:', error.signal);
|
||||||
|
|
||||||
|
// Enhanced error pattern detection
|
||||||
|
const errorPatterns = {
|
||||||
|
'unexpected EOF': {
|
||||||
|
message: 'Corrupted PWR file detected and deleted. Please try launching the game again.',
|
||||||
|
shouldDeletePWR: true
|
||||||
|
},
|
||||||
|
'permission denied': {
|
||||||
|
message: 'Permission denied. Check file permissions and try again.',
|
||||||
|
shouldDeletePWR: false
|
||||||
|
},
|
||||||
|
'no space left': {
|
||||||
|
message: 'Insufficient disk space. Free up space and try again.',
|
||||||
|
shouldDeletePWR: false
|
||||||
|
},
|
||||||
|
'device full': {
|
||||||
|
message: 'Insufficient disk space. Free up space and try again.',
|
||||||
|
shouldDeletePWR: false
|
||||||
|
},
|
||||||
|
'already exists': {
|
||||||
|
message: 'Installation directory conflict. Clean directories and retry.',
|
||||||
|
shouldDeletePWR: false
|
||||||
|
},
|
||||||
|
'network error': {
|
||||||
|
message: 'Network error during patch installation. Please retry.',
|
||||||
|
shouldDeletePWR: false
|
||||||
|
},
|
||||||
|
'connection refused': {
|
||||||
|
message: 'Connection refused. Check network and retry.',
|
||||||
|
shouldDeletePWR: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let enhancedMessage = `Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`;
|
||||||
|
let shouldDeletePWR = false;
|
||||||
|
|
||||||
|
// Check error patterns
|
||||||
|
const errorText = (stderr + ' ' + error.message).toLowerCase();
|
||||||
|
for (const [pattern, config] of Object.entries(errorPatterns)) {
|
||||||
|
if (errorText.includes(pattern)) {
|
||||||
|
enhancedMessage = config.message;
|
||||||
|
shouldDeletePWR = config.shouldDeletePWR;
|
||||||
|
console.log(`[Butler] Pattern matched: ${pattern}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete corrupted PWR file if needed
|
||||||
|
if (shouldDeletePWR) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(pwrFile)) {
|
||||||
|
fs.unlinkSync(pwrFile);
|
||||||
|
console.log('[Butler] Corrupted PWR file deleted:', pwrFile);
|
||||||
|
}
|
||||||
|
} catch (delErr) {
|
||||||
|
console.error('[Butler] Failed to delete corrupted PWR file:', delErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced error with retry context
|
||||||
|
const enhancedError = new Error(enhancedMessage);
|
||||||
|
enhancedError.canRetry = true;
|
||||||
|
enhancedError.branch = branch;
|
||||||
|
enhancedError.fileName = path.basename(pwrFile);
|
||||||
|
enhancedError.cacheDir = cacheDir;
|
||||||
|
enhancedError.butlerError = true;
|
||||||
|
enhancedError.errorCode = error.code;
|
||||||
|
enhancedError.stderr = stderr;
|
||||||
|
enhancedError.stdout = stdout;
|
||||||
|
|
||||||
|
console.log('[Butler] Enhanced error created with retry context');
|
||||||
|
reject(enhancedError);
|
||||||
} else {
|
} else {
|
||||||
|
console.log('[Butler] Patch installation completed successfully');
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
console.error('[Butler] Exception during Butler execution:', error);
|
||||||
|
const enhancedError = new Error(`Butler execution failed: ${error.message}`);
|
||||||
|
enhancedError.canRetry = true;
|
||||||
|
enhancedError.branch = branch;
|
||||||
|
enhancedError.fileName = path.basename(pwrFile);
|
||||||
|
enhancedError.cacheDir = cacheDir;
|
||||||
|
enhancedError.butlerError = true;
|
||||||
|
throw enhancedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(stagingDir)) {
|
if (fs.existsSync(stagingDir)) {
|
||||||
@@ -104,13 +306,33 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
console.log('Installation complete');
|
console.log('Installation complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR) {
|
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR, branchOverride = null) {
|
||||||
let tempUpdateDir;
|
let tempUpdateDir;
|
||||||
|
const branch = branchOverride || loadVersionBranch();
|
||||||
|
const installPath = path.dirname(path.dirname(path.dirname(path.dirname(gameDir))));
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const oldBranch = config.version_branch || 'release';
|
||||||
|
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (progressCallback) {
|
// NEW 2.1.2: Ensure UserData migration to centralized location
|
||||||
progressCallback('Updating game files...', 0, null, null, null);
|
try {
|
||||||
|
console.log('[UpdateGameFiles] Ensuring UserData migration...');
|
||||||
|
const migrationResult = await migrateUserDataToCentralized();
|
||||||
|
if (migrationResult.migrated) {
|
||||||
|
console.log('[UpdateGameFiles] ✓ UserData migrated to centralized location');
|
||||||
|
} else if (migrationResult.alreadyMigrated) {
|
||||||
|
console.log('[UpdateGameFiles] ✓ UserData already in centralized location');
|
||||||
|
}
|
||||||
|
} catch (migrationError) {
|
||||||
|
console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message);
|
||||||
}
|
}
|
||||||
console.log(`Updating game files to version: ${newVersion}`);
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback('Updating game files...', 10, null, null, null);
|
||||||
|
}
|
||||||
|
console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`);
|
||||||
|
|
||||||
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
||||||
|
|
||||||
@@ -120,51 +342,38 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
fs.mkdirSync(tempUpdateDir, { recursive: true });
|
fs.mkdirSync(tempUpdateDir, { recursive: true });
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Downloading new game version...', 10, null, null, null);
|
progressCallback('Downloading new game version...', 20, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir);
|
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Extracting new files...', 50, null, null, null);
|
progressCallback('Extracting new files...', 60, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
|
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Replacing game files...', 80, null, null, null);
|
progressCallback('Replacing game files...', 80, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let userDataBackup = null;
|
|
||||||
const userDataPath = findUserDataRecursive(gameDir);
|
|
||||||
|
|
||||||
if (userDataPath && fs.existsSync(userDataPath)) {
|
|
||||||
userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now());
|
|
||||||
console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`);
|
|
||||||
|
|
||||||
function copyRecursive(src, dest) {
|
|
||||||
const stat = fs.statSync(src);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
if (!fs.existsSync(dest)) {
|
|
||||||
fs.mkdirSync(dest, { recursive: true });
|
|
||||||
}
|
|
||||||
const files = fs.readdirSync(src);
|
|
||||||
for (const file of files) {
|
|
||||||
copyRecursive(path.join(src, file), path.join(dest, file));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copyRecursive(userDataPath, userDataBackup);
|
|
||||||
} else {
|
|
||||||
console.log('No UserData folder found in game directory');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fs.existsSync(gameDir)) {
|
if (fs.existsSync(gameDir)) {
|
||||||
console.log('Removing old game files...');
|
console.log('Removing old game files...');
|
||||||
fs.rmSync(gameDir, { recursive: true, force: true });
|
let retries = 3;
|
||||||
|
while (retries > 0) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
||||||
|
retries--;
|
||||||
|
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.renameSync(tempUpdateDir, gameDir);
|
fs.renameSync(tempUpdateDir, gameDir);
|
||||||
@@ -175,44 +384,16 @@ 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);
|
||||||
|
|
||||||
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
// NEW 2.1.2: No longer create UserData in game installation
|
||||||
const newUserDataPath = findUserDataPath(gameDir);
|
// UserData is now in centralized location (getUserDataPath())
|
||||||
const userDataParent = path.dirname(newUserDataPath);
|
console.log('[UpdateGameFiles] UserData is managed in centralized location');
|
||||||
|
|
||||||
if (!fs.existsSync(userDataParent)) {
|
|
||||||
fs.mkdirSync(userDataParent, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Restoring UserData to: ${newUserDataPath}`);
|
|
||||||
|
|
||||||
function copyRecursive(src, dest) {
|
|
||||||
const stat = fs.statSync(src);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
if (!fs.existsSync(dest)) {
|
|
||||||
fs.mkdirSync(dest, { recursive: true });
|
|
||||||
}
|
|
||||||
const files = fs.readdirSync(src);
|
|
||||||
for (const file of files) {
|
|
||||||
copyRecursive(path.join(src, file), path.join(dest, file));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copyRecursive(userDataBackup, newUserDataPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Game files updated successfully to version: ${newVersion}`);
|
console.log(`Game files updated successfully to version: ${newVersion}`);
|
||||||
|
|
||||||
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
// Save the updated version and branch to config
|
||||||
try {
|
saveVersionClient(newVersion);
|
||||||
fs.rmSync(userDataBackup, { recursive: true, force: true });
|
const { saveVersionBranch } = require('../core/config');
|
||||||
console.log('UserData backup cleaned up');
|
saveVersionBranch(branch);
|
||||||
} catch (cleanupError) {
|
|
||||||
console.warn('Could not clean up UserData backup:', cleanupError.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Waiting for file system sync...');
|
console.log('Waiting for file system sync...');
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
@@ -225,15 +406,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating game files:', error);
|
console.error('Error updating game files:', error);
|
||||||
|
|
||||||
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
|
||||||
try {
|
|
||||||
fs.rmSync(userDataBackup, { recursive: true, force: true });
|
|
||||||
console.log('UserData backup cleaned up after error');
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.warn('Could not clean up UserData backup:', cleanupError.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
|
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
|
||||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -242,20 +414,38 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGameInstalled() {
|
function isGameInstalled(branchOverride = null) {
|
||||||
|
const branch = branchOverride || loadVersionBranch();
|
||||||
const appDir = getResolvedAppDir();
|
const appDir = getResolvedAppDir();
|
||||||
const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
|
const gameDir = path.join(appDir, branch, 'package', 'game', 'latest');
|
||||||
const clientPath = findClientPath(gameDir);
|
const clientPath = findClientPath(gameDir);
|
||||||
return clientPath !== null;
|
return clientPath !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
|
async function installGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, branchOverride = null) {
|
||||||
|
console.log(`[InstallGame] branchOverride parameter received: ${branchOverride}`);
|
||||||
|
const loadedBranch = loadVersionBranch();
|
||||||
|
console.log(`[InstallGame] loadVersionBranch() returned: ${loadedBranch}`);
|
||||||
|
const branch = branchOverride || loadedBranch;
|
||||||
|
console.log(`[InstallGame] Final branch selected: ${branch}`);
|
||||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||||
const customCacheDir = path.join(customAppDir, 'cache');
|
const customCacheDir = path.join(customAppDir, 'cache');
|
||||||
const customToolsDir = path.join(customAppDir, 'butler');
|
const customToolsDir = path.join(customAppDir, 'butler');
|
||||||
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
|
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||||
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
|
|
||||||
|
// NEW 2.1.2: Ensure UserData migration to centralized location
|
||||||
|
try {
|
||||||
|
console.log('[InstallGame] Ensuring UserData migration...');
|
||||||
|
const migrationResult = await migrateUserDataToCentralized();
|
||||||
|
if (migrationResult.migrated) {
|
||||||
|
console.log('[InstallGame] ✓ UserData migrated to centralized location');
|
||||||
|
} else if (migrationResult.alreadyMigrated) {
|
||||||
|
console.log('[InstallGame] ✓ UserData already in centralized location');
|
||||||
|
}
|
||||||
|
} catch (migrationError) {
|
||||||
|
console.warn('[InstallGame] UserData migration warning:', migrationError.message);
|
||||||
|
}
|
||||||
|
|
||||||
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
|
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
@@ -263,10 +453,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!fs.existsSync(userDataDir)) {
|
|
||||||
fs.mkdirSync(userDataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
saveUsername(playerName);
|
saveUsername(playerName);
|
||||||
if (installPathOverride) {
|
if (installPathOverride) {
|
||||||
saveInstallPath(installPathOverride);
|
saveInstallPath(installPathOverride);
|
||||||
@@ -297,9 +483,17 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
try {
|
try {
|
||||||
await downloadJRE(progressCallback, customCacheDir, customJreDir);
|
await downloadJRE(progressCallback, customCacheDir, customJreDir);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Don't immediately fall back to system Java for JRE download errors - let user retry
|
||||||
|
if (error.isJREError) {
|
||||||
|
console.error('[Install] JRE download failed, allowing user retry:', error.message);
|
||||||
|
throw error; // Re-throw JRE errors to trigger retry UI
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-download JRE errors, fall back to system Java
|
||||||
const fallback = await detectSystemJava();
|
const fallback = await detectSystemJava();
|
||||||
if (fallback) {
|
if (fallback) {
|
||||||
javaBin = fallback;
|
javaBin = fallback;
|
||||||
|
console.log('[Install] Using system Java as fallback');
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -313,11 +507,36 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Fetching game files...', null, null, null, null);
|
progressCallback('Fetching game files...', null, null, null, null);
|
||||||
}
|
}
|
||||||
console.log('Installing game files...');
|
console.log(`Installing game files for branch: ${branch}...`);
|
||||||
|
|
||||||
const latestVersion = await getLatestClientVersion();
|
const latestVersion = await getLatestClientVersion(branch);
|
||||||
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
|
let pwrFile;
|
||||||
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
|
try {
|
||||||
|
pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir);
|
||||||
|
|
||||||
|
// If downloadPWR returns false, it means the file doesn't exist or is invalid
|
||||||
|
// We should retry the download with a manual retry flag
|
||||||
|
if (!pwrFile) {
|
||||||
|
console.log('[Install] PWR file not found or invalid, attempting retry...');
|
||||||
|
pwrFile = await retryPWRDownload(branch, latestVersion, progressCallback, customCacheDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-check we have a valid file path
|
||||||
|
if (!pwrFile || typeof pwrFile !== 'string') {
|
||||||
|
throw new Error(`PWR file download failed: received invalid path ${pwrFile}. Please retry download.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (downloadError) {
|
||||||
|
console.error('[Install] PWR download failed:', downloadError.message);
|
||||||
|
throw downloadError; // Re-throw to be handled by the main installGame error handler
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir, branch, customCacheDir);
|
||||||
|
|
||||||
|
// Save the installed version and branch to config
|
||||||
|
saveVersionClient(latestVersion);
|
||||||
|
const { saveVersionBranch } = require('../core/config');
|
||||||
|
saveVersionBranch(branch);
|
||||||
|
|
||||||
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
|
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
|
||||||
console.log('HomePage.ui update result after installation:', homeUIResult);
|
console.log('HomePage.ui update result after installation:', homeUIResult);
|
||||||
@@ -325,6 +544,10 @@ 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
|
||||||
|
// UserData is managed in centralized location (getUserDataPath())
|
||||||
|
console.log('[InstallGame] UserData is managed in centralized location');
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Installation complete', 100, null, null, null);
|
progressCallback('Installation complete', 100, null, null, null);
|
||||||
}
|
}
|
||||||
@@ -357,8 +580,9 @@ async function uninstallGame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkExistingGameInstallation() {
|
function checkExistingGameInstallation(branchOverride = null) {
|
||||||
try {
|
try {
|
||||||
|
const branch = branchOverride || loadVersionBranch();
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
if (!config.installPath || !config.installPath.trim()) {
|
if (!config.installPath || !config.installPath.trim()) {
|
||||||
@@ -366,7 +590,7 @@ function checkExistingGameInstallation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const installPath = config.installPath.trim();
|
const installPath = config.installPath.trim();
|
||||||
const gameDir = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest');
|
const gameDir = path.join(installPath, 'HytaleF2P', branch, 'package', 'game', 'latest');
|
||||||
|
|
||||||
if (!fs.existsSync(gameDir)) {
|
if (!fs.existsSync(gameDir)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -384,7 +608,8 @@ function checkExistingGameInstallation() {
|
|||||||
clientPath: clientPath,
|
clientPath: clientPath,
|
||||||
userDataPath: userDataPath,
|
userDataPath: userDataPath,
|
||||||
installPath: installPath,
|
installPath: installPath,
|
||||||
hasUserData: userDataPath && fs.existsSync(userDataPath)
|
hasUserData: userDataPath && fs.existsSync(userDataPath),
|
||||||
|
branch: branch
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking existing game installation:', error);
|
console.error('Error checking existing game installation:', error);
|
||||||
@@ -392,40 +617,32 @@ function checkExistingGameInstallation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function repairGame(progressCallback) {
|
async function repairGame(progressCallback, branchOverride = null) {
|
||||||
|
const branch = branchOverride || loadVersionBranch();
|
||||||
const appDir = getResolvedAppDir();
|
const appDir = getResolvedAppDir();
|
||||||
const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
|
const gameDir = path.join(appDir, branch, 'package', 'game', 'latest');
|
||||||
|
const installPath = appDir;
|
||||||
|
let backupPath = null;
|
||||||
|
|
||||||
|
// Vérifier si on a version_client et version_branch dans config.json
|
||||||
|
const config = loadConfig();
|
||||||
|
const hasVersionConfig = !!(config.version_client && config.version_branch);
|
||||||
|
console.log(`[RepairGame] hasVersionConfig: ${hasVersionConfig}`);
|
||||||
|
|
||||||
// Check if game exists
|
// Check if game exists
|
||||||
if (!fs.existsSync(gameDir)) {
|
if (!fs.existsSync(gameDir)) {
|
||||||
throw new Error('Game directory not found. Cannot repair.');
|
throw new Error('Game directory not found. Cannot repair.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Locate UserData
|
|
||||||
const userDataPath = findUserDataRecursive(gameDir);
|
|
||||||
let userDataBackup = null;
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Backing up user data...', 10, null, null, null);
|
progressCallback('Backing up user data...', 10, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup UserData
|
// Backup UserData using new system
|
||||||
if (userDataPath && fs.existsSync(userDataPath)) {
|
try {
|
||||||
userDataBackup = path.join(appDir, 'UserData_backup_repair_' + Date.now());
|
backupPath = await userDataBackup.backupUserData(installPath, branch, hasVersionConfig);
|
||||||
console.log(`Backing up UserData during repair from ${userDataPath} to ${userDataBackup}`);
|
} catch (backupError) {
|
||||||
|
console.warn('UserData backup failed during repair:', backupError.message);
|
||||||
// Copy function
|
|
||||||
function copyRecursive(src, dest) {
|
|
||||||
const stat = fs.statSync(src);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
||||||
fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child)));
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copyRecursive(userDataPath, userDataBackup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -446,39 +663,21 @@ async function repairGame(progressCallback) {
|
|||||||
|
|
||||||
// Passing null/undefined for overrides to use defaults/saved configs
|
// Passing null/undefined for overrides to use defaults/saved configs
|
||||||
// installGame calls progressCallback internally
|
// installGame calls progressCallback internally
|
||||||
await installGame('Player', progressCallback);
|
await installGame('Player', progressCallback, null, null, branch);
|
||||||
|
|
||||||
// Restore UserData
|
// Restore UserData using new system
|
||||||
if (userDataBackup && fs.existsSync(userDataBackup)) {
|
if (backupPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Restoring user data...', 90, null, null, null);
|
progressCallback('Restoring user data...', 90, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// installGame creates: path.join(customGameDir, 'Client', 'UserData')
|
try {
|
||||||
const newGameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
|
await userDataBackup.restoreUserData(backupPath, installPath, branch);
|
||||||
const newUserDataPath = path.join(newGameDir, 'Client', 'UserData');
|
await userDataBackup.cleanupBackup(backupPath);
|
||||||
|
console.log('UserData restored successfully after repair');
|
||||||
if (!fs.existsSync(newUserDataPath)) {
|
} catch (restoreError) {
|
||||||
fs.mkdirSync(newUserDataPath, { recursive: true });
|
console.warn('UserData restore failed after repair:', restoreError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Restoring UserData to ${newUserDataPath}`);
|
|
||||||
|
|
||||||
function copyRecursive(src, dest) {
|
|
||||||
const stat = fs.statSync(src);
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
||||||
fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child)));
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(src, dest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
copyRecursive(userDataBackup, newUserDataPath);
|
|
||||||
|
|
||||||
// Cleanup Backup
|
|
||||||
console.log('Cleaning up repair backup...');
|
|
||||||
fs.rmSync(userDataBackup, { recursive: true, force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -488,8 +687,79 @@ async function repairGame(progressCallback) {
|
|||||||
return { success: true, repaired: true };
|
return { success: true, repaired: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Directory validation and cleanup function
|
||||||
|
function validateGameDirectory(gameDir, stagingDir) {
|
||||||
|
try {
|
||||||
|
// Ensure game directory exists and is writable
|
||||||
|
if (!fs.existsSync(gameDir)) {
|
||||||
|
fs.mkdirSync(gameDir, { recursive: true });
|
||||||
|
console.log(`[Butler] Created game directory: ${gameDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test write permissions
|
||||||
|
const testFile = path.join(gameDir, '.permission_test');
|
||||||
|
fs.writeFileSync(testFile, 'test');
|
||||||
|
fs.unlinkSync(testFile);
|
||||||
|
console.log(`[Butler] Game directory is writable: ${gameDir}`);
|
||||||
|
|
||||||
|
// Clean and ensure staging directory
|
||||||
|
if (fs.existsSync(stagingDir)) {
|
||||||
|
console.log(`[Butler] Cleaning existing staging directory: ${stagingDir}`);
|
||||||
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(stagingDir, { recursive: true });
|
||||||
|
console.log(`[Butler] Created clean staging directory: ${stagingDir}`);
|
||||||
|
|
||||||
|
// Check disk space (basic check)
|
||||||
|
const freeSpace = fs.statSync(gameDir);
|
||||||
|
console.log(`[Butler] Directory validation completed successfully`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Directory validation failed: ${error.message}. Please check permissions and disk space.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced PWR file validation
|
||||||
|
function validatePWRFile(filePath) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
const sizeInMB = stats.size / 1024 / 1024;
|
||||||
|
|
||||||
|
if (stats.size < 1024 * 1024) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file is under 1.5 GB (incomplete download)
|
||||||
|
if (sizeInMB < 1500) {
|
||||||
|
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic file header validation (PWR files should have specific headers)
|
||||||
|
const buffer = fs.readFileSync(filePath, { start: 0, end: 20 });
|
||||||
|
if (buffer.length < 10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common PWR magic bytes or patterns
|
||||||
|
// This is a basic check - could be enhanced with actual PWR format specification
|
||||||
|
const header = buffer.toString('hex', 0, 10);
|
||||||
|
console.log(`[PWR Validation] File header: ${header}`);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[PWR Validation] Error:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
downloadPWR,
|
downloadPWR,
|
||||||
|
retryPWRDownload,
|
||||||
applyPWR,
|
applyPWR,
|
||||||
updateGameFiles,
|
updateGameFiles,
|
||||||
isGameInstalled,
|
isGameInstalled,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const tar = require('tar');
|
|||||||
const { expandHome, JRE_DIR } = require('../core/paths');
|
const { expandHome, JRE_DIR } = require('../core/paths');
|
||||||
const { getOS, getArch } = require('../utils/platformUtils');
|
const { getOS, getArch } = require('../utils/platformUtils');
|
||||||
const { loadConfig } = require('../core/config');
|
const { loadConfig } = require('../core/config');
|
||||||
const { downloadFile } = require('../utils/fileManager');
|
const { downloadFile, retryDownload } = require('../utils/fileManager');
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
const JAVA_EXECUTABLE = 'java' + (process.platform === 'win32' ? '.exe' : '');
|
const JAVA_EXECUTABLE = 'java' + (process.platform === 'win32' ? '.exe' : '');
|
||||||
@@ -188,6 +188,20 @@ async function getJavaDetection() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manual retry function for JRE downloads
|
||||||
|
async function retryJREDownload(url, cacheFile, progressCallback) {
|
||||||
|
console.log('Initiating manual JRE retry...');
|
||||||
|
|
||||||
|
// Ensure cache directory exists before retrying
|
||||||
|
const cacheDir = path.dirname(cacheFile);
|
||||||
|
if (!fs.existsSync(cacheDir)) {
|
||||||
|
console.log('Creating JRE cache directory:', cacheDir);
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return await retryDownload(url, cacheFile, progressCallback);
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) {
|
async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) {
|
||||||
if (!fs.existsSync(cacheDir)) {
|
if (!fs.existsSync(cacheDir)) {
|
||||||
fs.mkdirSync(cacheDir, { recursive: true });
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
@@ -230,7 +244,40 @@ async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) {
|
|||||||
progressCallback('Fetching Java runtime...', null, null, null, null);
|
progressCallback('Fetching Java runtime...', null, null, null, null);
|
||||||
}
|
}
|
||||||
console.log('Fetching Java runtime...');
|
console.log('Fetching Java runtime...');
|
||||||
await downloadFile(platform.url, cacheFile, progressCallback);
|
let jreFile;
|
||||||
|
try {
|
||||||
|
jreFile = await downloadFile(platform.url, cacheFile, progressCallback);
|
||||||
|
|
||||||
|
// If downloadFile returns false or undefined, it means the download failed
|
||||||
|
// We should retry the download with a manual retry
|
||||||
|
if (!jreFile || typeof jreFile !== 'string') {
|
||||||
|
console.log('[JRE Download] JRE file download failed or incomplete, attempting retry...');
|
||||||
|
jreFile = await retryJREDownload(platform.url, cacheFile, progressCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-check we have a valid file
|
||||||
|
if (!jreFile || typeof jreFile !== 'string') {
|
||||||
|
throw new Error(`JRE download failed: received invalid path ${jreFile}. Please retry download.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (downloadError) {
|
||||||
|
console.error('[JRE Download] JRE download failed:', downloadError.message);
|
||||||
|
|
||||||
|
// Enhance error with retry information for the UI
|
||||||
|
const enhancedError = new Error(`JRE download failed: ${downloadError.message}`);
|
||||||
|
enhancedError.originalError = downloadError;
|
||||||
|
enhancedError.canRetry = downloadError.isConnectionLost ? false : (downloadError.canRetry !== false);
|
||||||
|
enhancedError.jreUrl = platform.url;
|
||||||
|
enhancedError.jreDest = cacheFile;
|
||||||
|
enhancedError.osName = osName;
|
||||||
|
enhancedError.arch = arch;
|
||||||
|
enhancedError.fileName = fileName;
|
||||||
|
enhancedError.cacheDir = cacheDir;
|
||||||
|
enhancedError.isJREError = true; // Flag to identify JRE errors
|
||||||
|
enhancedError.isConnectionLost = downloadError.isConnectionLost || false;
|
||||||
|
|
||||||
|
throw enhancedError;
|
||||||
|
}
|
||||||
console.log('Download finished');
|
console.log('Download finished');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,5 +406,6 @@ module.exports = {
|
|||||||
getJavaDetection,
|
getJavaDetection,
|
||||||
downloadJRE,
|
downloadJRE,
|
||||||
extractJRE,
|
extractJRE,
|
||||||
|
retryJREDownload,
|
||||||
JAVA_EXECUTABLE
|
JAVA_EXECUTABLE
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
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 { getModsPath, getProfilesDir } = require('../core/paths');
|
const { getModsPath, getProfilesDir } = require('../core/paths');
|
||||||
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
||||||
const profileManager = require('./profileManager');
|
const profileManager = require('./profileManager');
|
||||||
|
|
||||||
const API_KEY = process.env.CURSEFORGE_API_KEY;
|
const API_KEY = "$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the physical mods path for a specific profile.
|
* Get the physical mods path for a specific profile.
|
||||||
@@ -307,11 +308,16 @@ async function syncModsForCurrentProfile() {
|
|||||||
|
|
||||||
// 2. Symlink / Migration Logic
|
// 2. Symlink / Migration Logic
|
||||||
let needsLink = false;
|
let needsLink = false;
|
||||||
|
let globalStats = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
globalStats = fs.lstatSync(globalModsPath);
|
||||||
|
} catch (e) {
|
||||||
|
// Path doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
if (fs.existsSync(globalModsPath)) {
|
if (globalStats) {
|
||||||
const stats = fs.lstatSync(globalModsPath);
|
if (globalStats.isSymbolicLink()) {
|
||||||
|
|
||||||
if (stats.isSymbolicLink()) {
|
|
||||||
const linkTarget = fs.readlinkSync(globalModsPath);
|
const linkTarget = fs.readlinkSync(globalModsPath);
|
||||||
// Normalize paths for comparison
|
// Normalize paths for comparison
|
||||||
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
|
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
|
||||||
@@ -319,7 +325,7 @@ async function syncModsForCurrentProfile() {
|
|||||||
fs.unlinkSync(globalModsPath);
|
fs.unlinkSync(globalModsPath);
|
||||||
needsLink = true;
|
needsLink = true;
|
||||||
}
|
}
|
||||||
} else if (stats.isDirectory()) {
|
} else if (globalStats.isDirectory()) {
|
||||||
// MIGRATION: It's a real directory. Move contents to profile.
|
// MIGRATION: It's a real directory. Move contents to profile.
|
||||||
console.log('[ModManager] Migrating global mods folder to profile folder...');
|
console.log('[ModManager] Migrating global mods folder to profile folder...');
|
||||||
const files = fs.readdirSync(globalModsPath);
|
const files = fs.readdirSync(globalModsPath);
|
||||||
@@ -349,7 +355,20 @@ async function syncModsForCurrentProfile() {
|
|||||||
|
|
||||||
// Remove the directory so we can link it
|
// Remove the directory so we can link it
|
||||||
try {
|
try {
|
||||||
fs.rmSync(globalModsPath, { recursive: true, force: true });
|
let retries = 3;
|
||||||
|
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;
|
needsLink = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to remove global mods dir:', e);
|
console.error('Failed to remove global mods dir:', e);
|
||||||
@@ -364,8 +383,8 @@ async function syncModsForCurrentProfile() {
|
|||||||
if (needsLink) {
|
if (needsLink) {
|
||||||
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
|
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
|
||||||
try {
|
try {
|
||||||
// 'junction' is key for Windows without admin
|
const symlinkType = getOS() === 'windows' ? 'junction' : 'dir';
|
||||||
fs.symlinkSync(profileModsPath, globalModsPath, 'junction');
|
fs.symlinkSync(profileModsPath, globalModsPath, symlinkType);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If we can't create the symlink, try creating the directory first
|
// If we can't create the symlink, try creating the directory first
|
||||||
console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.');
|
console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { markAsLaunched, loadConfig } = require('../core/config');
|
const { markAsLaunched, loadConfig, saveVersionBranch, saveVersionClient, loadVersionBranch, loadVersionClient } = require('../core/config');
|
||||||
const { checkExistingGameInstallation, updateGameFiles } = require('../managers/gameManager');
|
const { checkExistingGameInstallation, updateGameFiles } = require('../managers/gameManager');
|
||||||
const { getInstalledClientVersion, getLatestClientVersion } = require('./versionManager');
|
const { getInstalledClientVersion, getLatestClientVersion } = require('./versionManager');
|
||||||
|
|
||||||
@@ -56,6 +56,14 @@ async function handleFirstLaunchCheck(progressCallback) {
|
|||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
|
// Initialize version_client if not set (but don't force version_branch)
|
||||||
|
const currentVersion = loadVersionClient();
|
||||||
|
|
||||||
|
if (currentVersion === undefined || currentVersion === null) {
|
||||||
|
console.log('Initializing version_client to null (will trigger installation)');
|
||||||
|
saveVersionClient(null);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.hasLaunchedBefore === true) {
|
if (config.hasLaunchedBefore === true) {
|
||||||
return { isFirstLaunch: false, needsUpdate: false };
|
return { isFirstLaunch: false, needsUpdate: false };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
async function getLatestClientVersion() {
|
async function getLatestClientVersion(branch = 'release') {
|
||||||
try {
|
try {
|
||||||
console.log('Fetching latest client version from API...');
|
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 },
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Hytale-F2P-Launcher'
|
'User-Agent': 'Hytale-F2P-Launcher'
|
||||||
@@ -12,7 +13,7 @@ async function getLatestClientVersion() {
|
|||||||
|
|
||||||
if (response.data && response.data.client_version) {
|
if (response.data && response.data.client_version) {
|
||||||
const version = response.data.client_version;
|
const version = response.data.client_version;
|
||||||
console.log(`Latest client version: ${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 default version');
|
||||||
@@ -25,32 +26,6 @@ async function getLatestClientVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getInstalledClientVersion() {
|
|
||||||
try {
|
|
||||||
console.log('Fetching installed client version from API...');
|
|
||||||
const response = await axios.get('https://files.hytalef2p.com/api/clientCheck', {
|
|
||||||
timeout: 5000,
|
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Hytale-F2P-Launcher'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data && response.data.client_version) {
|
|
||||||
const version = response.data.client_version;
|
|
||||||
console.log(`Installed client version: ${version}`);
|
|
||||||
return version;
|
|
||||||
} else {
|
|
||||||
console.log('Warning: Invalid clientCheck API response');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching installed client version:', error.message);
|
|
||||||
console.log('Warning: clientCheck API unavailable');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getLatestClientVersion,
|
getLatestClientVersion
|
||||||
getInstalledClientVersion
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
const axios = require('axios');
|
|
||||||
|
|
||||||
const UPDATE_CHECK_URL = 'https://files.hytalef2p.com/api/version_launcher';
|
|
||||||
const CURRENT_VERSION = '2.0.2';
|
|
||||||
const GITHUB_DOWNLOAD_URL = 'https://github.com/amiayweb/Hytale-F2P/';
|
|
||||||
|
|
||||||
class UpdateManager {
|
|
||||||
constructor() {
|
|
||||||
this.updateAvailable = false;
|
|
||||||
this.remoteVersion = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkForUpdates() {
|
|
||||||
try {
|
|
||||||
console.log('Checking for updates...');
|
|
||||||
console.log(`Local version: ${CURRENT_VERSION}`);
|
|
||||||
|
|
||||||
const response = await axios.get(UPDATE_CHECK_URL, {
|
|
||||||
timeout: 5000,
|
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Hytale-F2P-Launcher'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data && response.data.launcher_version) {
|
|
||||||
this.remoteVersion = response.data.launcher_version;
|
|
||||||
console.log(`Remote version: ${this.remoteVersion}`);
|
|
||||||
|
|
||||||
if (this.remoteVersion !== CURRENT_VERSION) {
|
|
||||||
this.updateAvailable = true;
|
|
||||||
console.log('Update available!');
|
|
||||||
return {
|
|
||||||
updateAvailable: true,
|
|
||||||
currentVersion: CURRENT_VERSION,
|
|
||||||
newVersion: this.remoteVersion,
|
|
||||||
downloadUrl: GITHUB_DOWNLOAD_URL
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log('Launcher is up to date');
|
|
||||||
return {
|
|
||||||
updateAvailable: false,
|
|
||||||
currentVersion: CURRENT_VERSION,
|
|
||||||
newVersion: this.remoteVersion
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Invalid API response');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking for updates:', error.message);
|
|
||||||
return {
|
|
||||||
updateAvailable: false,
|
|
||||||
error: error.message,
|
|
||||||
currentVersion: CURRENT_VERSION
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDownloadUrl() {
|
|
||||||
return GITHUB_DOWNLOAD_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUpdateInfo() {
|
|
||||||
return {
|
|
||||||
updateAvailable: this.updateAvailable,
|
|
||||||
currentVersion: CURRENT_VERSION,
|
|
||||||
remoteVersion: this.remoteVersion,
|
|
||||||
downloadUrl: this.getDownloadUrl()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = UpdateManager;
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
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');
|
|
||||||
|
|
||||||
// Domain configuration
|
// Domain configuration
|
||||||
const ORIGINAL_DOMAIN = 'hytale.com';
|
const ORIGINAL_DOMAIN = 'hytale.com';
|
||||||
|
const MIN_DOMAIN_LENGTH = 4;
|
||||||
|
const MAX_DOMAIN_LENGTH = 16;
|
||||||
|
|
||||||
function getTargetDomain() {
|
function getTargetDomain() {
|
||||||
if (process.env.HYTALE_AUTH_DOMAIN) {
|
if (process.env.HYTALE_AUTH_DOMAIN) {
|
||||||
@@ -14,15 +14,20 @@ function getTargetDomain() {
|
|||||||
const { getAuthDomain } = require('../core/config');
|
const { getAuthDomain } = require('../core/config');
|
||||||
return getAuthDomain();
|
return getAuthDomain();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'sanasol.ws';
|
return 'auth.sanasol.ws';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_NEW_DOMAIN = '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:
|
||||||
|
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
|
||||||
|
* - Domains <= 10 chars: Direct replacement, subdomains stripped
|
||||||
|
* - Domains 11-16 chars: Split mode - first 6 chars replace subdomain prefix, rest replaces domain
|
||||||
*/
|
*/
|
||||||
class ClientPatcher {
|
class ClientPatcher {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -34,14 +39,63 @@ class ClientPatcher {
|
|||||||
*/
|
*/
|
||||||
getNewDomain() {
|
getNewDomain() {
|
||||||
const domain = getTargetDomain();
|
const domain = getTargetDomain();
|
||||||
if (domain.length !== ORIGINAL_DOMAIN.length) {
|
if (domain.length < MIN_DOMAIN_LENGTH) {
|
||||||
console.warn(`Warning: Domain "${domain}" length (${domain.length}) doesn't match original "${ORIGINAL_DOMAIN}" (${ORIGINAL_DOMAIN.length})`);
|
console.warn(`Warning: Domain "${domain}" is too short (min ${MIN_DOMAIN_LENGTH} chars)`);
|
||||||
|
console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`);
|
||||||
|
return DEFAULT_NEW_DOMAIN;
|
||||||
|
}
|
||||||
|
if (domain.length > MAX_DOMAIN_LENGTH) {
|
||||||
|
console.warn(`Warning: Domain "${domain}" is too long (max ${MAX_DOMAIN_LENGTH} chars)`);
|
||||||
console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`);
|
console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`);
|
||||||
return DEFAULT_NEW_DOMAIN;
|
return DEFAULT_NEW_DOMAIN;
|
||||||
}
|
}
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the domain patching strategy based on length
|
||||||
|
*/
|
||||||
|
getDomainStrategy(domain) {
|
||||||
|
if (domain.length <= 10) {
|
||||||
|
return {
|
||||||
|
mode: 'direct',
|
||||||
|
mainDomain: domain,
|
||||||
|
subdomainPrefix: '',
|
||||||
|
description: `Direct replacement: hytale.com -> ${domain}`
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const prefix = domain.slice(0, 6);
|
||||||
|
const suffix = domain.slice(6);
|
||||||
|
return {
|
||||||
|
mode: 'split',
|
||||||
|
mainDomain: suffix,
|
||||||
|
subdomainPrefix: prefix,
|
||||||
|
description: `Split mode: subdomain prefix="${prefix}", main domain="${suffix}"`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a string to the length-prefixed byte format used by the client
|
||||||
|
*/
|
||||||
|
stringToLengthPrefixed(str) {
|
||||||
|
const length = str.length;
|
||||||
|
const result = Buffer.alloc(4 + length + (length - 1));
|
||||||
|
result[0] = length;
|
||||||
|
result[1] = 0x00;
|
||||||
|
result[2] = 0x00;
|
||||||
|
result[3] = 0x00;
|
||||||
|
|
||||||
|
let pos = 4;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result[pos++] = str.charCodeAt(i);
|
||||||
|
if (i < length - 1) {
|
||||||
|
result[pos++] = 0x00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a string to UTF-16LE bytes (how .NET stores strings)
|
* Convert a string to UTF-16LE bytes (how .NET stores strings)
|
||||||
*/
|
*/
|
||||||
@@ -53,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
|
||||||
*/
|
*/
|
||||||
@@ -76,32 +123,28 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UTF-8 domain replacement for Java JAR files.
|
* Replace bytes in buffer - only overwrites the length of new bytes
|
||||||
* Java stores strings in UTF-8 format in the constant pool.
|
|
||||||
*/
|
*/
|
||||||
findAndReplaceDomainUtf8(data, oldDomain, newDomain) {
|
replaceBytes(buffer, oldBytes, newBytes) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const result = Buffer.from(data);
|
const result = Buffer.from(buffer);
|
||||||
|
|
||||||
const oldUtf8 = this.stringToUtf8(oldDomain);
|
if (newBytes.length > oldBytes.length) {
|
||||||
const newUtf8 = this.stringToUtf8(newDomain);
|
console.warn(` Warning: New pattern (${newBytes.length}) longer than old (${oldBytes.length}), skipping`);
|
||||||
|
return { buffer: result, count: 0 };
|
||||||
const positions = this.findAllOccurrences(result, oldUtf8);
|
}
|
||||||
|
|
||||||
|
const positions = this.findAllOccurrences(result, oldBytes);
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
newUtf8.copy(result, pos);
|
newBytes.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { buffer: result, count };
|
return { buffer: result, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smart domain replacement that handles both null-terminated and non-null-terminated strings.
|
* 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;
|
||||||
@@ -109,8 +152,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
|
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
|
||||||
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
||||||
const oldLastChar = this.stringToUtf16LE(oldDomain.slice(-1));
|
|
||||||
const newLastChar = this.stringToUtf16LE(newDomain.slice(-1));
|
|
||||||
|
|
||||||
const oldLastCharByte = oldDomain.charCodeAt(oldDomain.length - 1);
|
const oldLastCharByte = oldDomain.charCodeAt(oldDomain.length - 1);
|
||||||
const newLastCharByte = newDomain.charCodeAt(newDomain.length - 1);
|
const newLastCharByte = newDomain.charCodeAt(newDomain.length - 1);
|
||||||
@@ -125,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) {
|
||||||
@@ -144,7 +184,67 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
* Apply all domain patches using length-prefixed format
|
||||||
|
*/
|
||||||
|
applyDomainPatches(data, domain, protocol = 'https://') {
|
||||||
|
let result = Buffer.from(data);
|
||||||
|
let totalCount = 0;
|
||||||
|
const strategy = this.getDomainStrategy(domain);
|
||||||
|
|
||||||
|
console.log(` Patching strategy: ${strategy.description}`);
|
||||||
|
|
||||||
|
// 1. Patch telemetry/sentry URL
|
||||||
|
const oldSentry = 'https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/2';
|
||||||
|
const newSentry = `${protocol}t@${domain}/2`;
|
||||||
|
|
||||||
|
console.log(` Patching sentry: ${oldSentry.slice(0, 30)}... -> ${newSentry}`);
|
||||||
|
const sentryResult = this.replaceBytes(
|
||||||
|
result,
|
||||||
|
this.stringToLengthPrefixed(oldSentry),
|
||||||
|
this.stringToLengthPrefixed(newSentry)
|
||||||
|
);
|
||||||
|
result = sentryResult.buffer;
|
||||||
|
if (sentryResult.count > 0) {
|
||||||
|
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
|
||||||
|
totalCount += sentryResult.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Patch main domain
|
||||||
|
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
||||||
|
const domainResult = this.replaceBytes(
|
||||||
|
result,
|
||||||
|
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
||||||
|
this.stringToLengthPrefixed(strategy.mainDomain)
|
||||||
|
);
|
||||||
|
result = domainResult.buffer;
|
||||||
|
if (domainResult.count > 0) {
|
||||||
|
console.log(` Replaced ${domainResult.count} domain occurrence(s)`);
|
||||||
|
totalCount += domainResult.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Patch subdomain prefixes
|
||||||
|
const subdomains = ['https://tools.', 'https://sessions.', 'https://account-data.', 'https://telemetry.'];
|
||||||
|
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
||||||
|
|
||||||
|
for (const sub of subdomains) {
|
||||||
|
console.log(` Patching subdomain: ${sub} -> ${newSubdomainPrefix}`);
|
||||||
|
const subResult = this.replaceBytes(
|
||||||
|
result,
|
||||||
|
this.stringToLengthPrefixed(sub),
|
||||||
|
this.stringToLengthPrefixed(newSubdomainPrefix)
|
||||||
|
);
|
||||||
|
result = subResult.buffer;
|
||||||
|
if (subResult.count > 0) {
|
||||||
|
console.log(` Replaced ${subResult.count} occurrence(s)`);
|
||||||
|
totalCount += subResult.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { buffer: result, count: totalCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch Discord invite URLs
|
||||||
*/
|
*/
|
||||||
patchDiscordUrl(data) {
|
patchDiscordUrl(data) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -153,11 +253,21 @@ class ClientPatcher {
|
|||||||
const oldUrl = '.gg/hytale';
|
const oldUrl = '.gg/hytale';
|
||||||
const newUrl = '.gg/MHkEjepMQ7';
|
const newUrl = '.gg/MHkEjepMQ7';
|
||||||
|
|
||||||
|
const lpResult = this.replaceBytes(
|
||||||
|
result,
|
||||||
|
this.stringToLengthPrefixed(oldUrl),
|
||||||
|
this.stringToLengthPrefixed(newUrl)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lpResult.count > 0) {
|
||||||
|
return { buffer: lpResult.buffer, count: lpResult.count };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to UTF-16LE
|
||||||
const oldUtf16 = this.stringToUtf16LE(oldUrl);
|
const oldUtf16 = this.stringToUtf16LE(oldUrl);
|
||||||
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++;
|
||||||
@@ -167,54 +277,118 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the client binary has already been patched
|
* Check patch status of client binary
|
||||||
*/
|
*/
|
||||||
isPatchedAlready(clientPath) {
|
getPatchStatus(clientPath) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
const patchFlagFile = clientPath + this.patchedFlag;
|
const patchFlagFile = clientPath + this.patchedFlag;
|
||||||
|
|
||||||
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;
|
||||||
return true;
|
|
||||||
|
if (currentDomain === newDomain) {
|
||||||
|
const data = fs.readFileSync(clientPath);
|
||||||
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
|
const domainPattern = this.stringToLengthPrefixed(strategy.mainDomain);
|
||||||
|
|
||||||
|
if (data.includes(domainPattern)) {
|
||||||
|
return { patched: true, currentDomain, needsRestore: false };
|
||||||
|
} else {
|
||||||
|
console.log(' Flag exists but binary not patched (was updated?), needs re-patching...');
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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();
|
||||||
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
const patchFlagFile = clientPath + this.patchedFlag;
|
const patchFlagFile = clientPath + this.patchedFlag;
|
||||||
const flagData = {
|
const flagData = {
|
||||||
patchedAt: new Date().toISOString(),
|
patchedAt: new Date().toISOString(),
|
||||||
originalDomain: ORIGINAL_DOMAIN,
|
originalDomain: ORIGINAL_DOMAIN,
|
||||||
targetDomain: newDomain,
|
targetDomain: newDomain,
|
||||||
patcherVersion: '1.0.0'
|
patchMode: strategy.mode,
|
||||||
|
mainDomain: strategy.mainDomain,
|
||||||
|
subdomainPrefix: strategy.subdomainPrefix,
|
||||||
|
patcherVersion: '2.1.0',
|
||||||
|
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';
|
||||||
if (!fs.existsSync(backupPath)) {
|
try {
|
||||||
console.log(` Creating backup at ${path.basename(backupPath)}`);
|
if (!fs.existsSync(backupPath)) {
|
||||||
fs.copyFileSync(clientPath, backupPath);
|
console.log(` Creating backup at ${path.basename(backupPath)}`);
|
||||||
|
fs.copyFileSync(clientPath, backupPath);
|
||||||
|
return backupPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSize = fs.statSync(clientPath).size;
|
||||||
|
const backupSize = fs.statSync(backupPath).size;
|
||||||
|
|
||||||
|
if (currentSize !== backupSize) {
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
|
const oldBackupPath = `${clientPath}.original.${timestamp}`;
|
||||||
|
console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`);
|
||||||
|
fs.renameSync(backupPath, oldBackupPath);
|
||||||
|
fs.copyFileSync(clientPath, backupPath);
|
||||||
|
return backupPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(' Backup already exists');
|
||||||
return backupPath;
|
return backupPath;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` Failed to create backup: ${e.message}`);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
console.log(' Backup already exists');
|
|
||||||
return backupPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the original client binary from backup
|
* Restore original client binary
|
||||||
*/
|
*/
|
||||||
restoreClient(clientPath) {
|
restoreClient(clientPath) {
|
||||||
const backupPath = clientPath + '.original';
|
const backupPath = clientPath + '.original';
|
||||||
@@ -233,15 +407,19 @@ 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();
|
||||||
console.log('=== Client Patcher ===');
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
|
|
||||||
|
console.log('=== Client Patcher v2.1 ===');
|
||||||
console.log(`Target: ${clientPath}`);
|
console.log(`Target: ${clientPath}`);
|
||||||
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
|
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
||||||
|
console.log(`Mode: ${strategy.mode}`);
|
||||||
|
if (strategy.mode === 'split') {
|
||||||
|
console.log(` Subdomain prefix: ${strategy.subdomainPrefix}`);
|
||||||
|
console.log(` Main domain: ${strategy.mainDomain}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(clientPath)) {
|
if (!fs.existsSync(clientPath)) {
|
||||||
const error = `Client binary not found: ${clientPath}`;
|
const error = `Client binary not found: ${clientPath}`;
|
||||||
@@ -249,56 +427,64 @@ 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('Patching domain references...');
|
console.log('Applying domain patches (length-prefixed format)...');
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
|
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
||||||
|
|
||||||
console.log('Patching Discord URLs...');
|
console.log('Patching Discord URLs...');
|
||||||
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
||||||
|
|
||||||
if (count === 0 && discordCount === 0) {
|
if (count === 0 && discordCount === 0) {
|
||||||
|
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
||||||
|
|
||||||
|
const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
||||||
|
if (legacyResult.count > 0) {
|
||||||
|
console.log(`Found ${legacyResult.count} occurrences with legacy format`);
|
||||||
|
fs.writeFileSync(clientPath, legacyResult.buffer);
|
||||||
|
this.markAsPatched(clientPath);
|
||||||
|
return { success: true, patchCount: legacyResult.count, format: 'legacy' };
|
||||||
|
}
|
||||||
|
|
||||||
console.log('No occurrences found - binary may already be modified or has different format');
|
console.log('No occurrences found - binary may already be modified or has different format');
|
||||||
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 ===');
|
||||||
@@ -307,17 +493,47 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the server JAR to use the custom domain
|
* Check if server JAR contains DualAuth classes (was patched)
|
||||||
* JAR files are ZIP archives, so we need to extract, patch class files, and repackage
|
*/
|
||||||
* @param {string} serverPath - Path to the HytaleServer.jar
|
serverJarContainsDualAuth(serverPath) {
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
try {
|
||||||
* @returns {object} Result object with success status and details
|
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) {
|
async patchServer(serverPath, progressCallback) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
console.log('=== Server Patcher ===');
|
|
||||||
|
console.log('=== Server Patcher (Pre-patched Download) ===');
|
||||||
console.log(`Target: ${serverPath}`);
|
console.log(`Target: ${serverPath}`);
|
||||||
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
|
console.log(`Domain: ${newDomain}`);
|
||||||
|
|
||||||
if (!fs.existsSync(serverPath)) {
|
if (!fs.existsSync(serverPath)) {
|
||||||
const error = `Server JAR not found: ${serverPath}`;
|
const error = `Server JAR not found: ${serverPath}`;
|
||||||
@@ -325,82 +541,169 @@ class ClientPatcher {
|
|||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isPatchedAlready(serverPath)) {
|
// Check if already patched
|
||||||
console.log(`Server already patched for ${newDomain}, skipping`);
|
const patchFlagFile = serverPath + '.dualauth_patched';
|
||||||
if (progressCallback) {
|
let needsRestore = false;
|
||||||
progressCallback('Server already patched', 100);
|
|
||||||
}
|
|
||||||
return { success: true, alreadyPatched: true, patchCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
progressCallback('Preparing to patch server...', 10);
|
try {
|
||||||
}
|
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
||||||
|
if (flagData.domain === newDomain) {
|
||||||
console.log('Creating backup...');
|
// Verify JAR actually contains DualAuth classes (game may have auto-updated)
|
||||||
this.backupClient(serverPath);
|
if (this.serverJarContainsDualAuth(serverPath)) {
|
||||||
|
console.log(`Server already patched for ${newDomain}, skipping`);
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Server already patched', 100);
|
||||||
progressCallback('Extracting server JAR...', 20);
|
return { success: true, alreadyPatched: true };
|
||||||
}
|
} else {
|
||||||
|
console.log(' Flag exists but JAR not patched (was auto-updated?), will re-download...');
|
||||||
console.log('Opening server JAR...');
|
// Delete stale flag file
|
||||||
const zip = new AdmZip(serverPath);
|
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
|
||||||
const entries = zip.getEntries();
|
|
||||||
console.log(`JAR contains ${entries.length} entries`);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Patching class files...', 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalCount = 0;
|
|
||||||
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
|
||||||
const newUtf8 = this.stringToUtf8(newDomain);
|
|
||||||
|
|
||||||
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, newDomain);
|
|
||||||
if (count > 0) {
|
|
||||||
zip.updateFile(entry.entryName, patchedData);
|
|
||||||
console.log(` Patched ${count} occurrences in ${name}`);
|
|
||||||
totalCount += count;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Server patched for "${flagData.domain}", need to change to "${newDomain}"`);
|
||||||
|
needsRestore = true;
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Flag file corrupt, re-patch
|
||||||
|
console.log(' Flag file corrupt, will re-download');
|
||||||
|
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalCount === 0) {
|
// Restore backup if patched for different domain
|
||||||
console.log('No occurrences of hytale.com found in server JAR entries');
|
if (needsRestore) {
|
||||||
return { success: true, patchCount: 0, warning: 'No domain occurrences found in JAR' };
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
// Create backup
|
||||||
progressCallback('Writing patched JAR...', 80);
|
if (progressCallback) progressCallback('Creating backup...', 10);
|
||||||
|
console.log('Creating backup...');
|
||||||
|
const backupResult = this.backupClient(serverPath);
|
||||||
|
if (!backupResult) {
|
||||||
|
console.warn(' Could not create backup - proceeding without backup');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Writing patched JAR...');
|
// Only support standard domain (auth.sanasol.ws) via pre-patched download
|
||||||
zip.writeZip(serverPath);
|
if (newDomain !== 'auth.sanasol.ws' && newDomain !== 'sanasol.ws') {
|
||||||
|
console.error(`Domain "${newDomain}" requires DualAuthPatcher - only auth.sanasol.ws is supported via pre-patched download`);
|
||||||
this.markAsPatched(serverPath);
|
return { success: false, error: `Unsupported domain: ${newDomain}. Only auth.sanasol.ws is supported.` };
|
||||||
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Server patching complete', 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Successfully patched ${totalCount} occurrences in server`);
|
// Download pre-patched JAR
|
||||||
console.log('=== Server Patching Complete ===');
|
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
|
||||||
|
console.log('Downloading pre-patched HytaleServer.jar...');
|
||||||
|
|
||||||
return { success: true, patchCount: totalCount };
|
try {
|
||||||
|
const https = require('https');
|
||||||
|
const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const handleResponse = (response) => {
|
||||||
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||||
|
https.get(response.headers.location, handleResponse).on('error', reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fs.createWriteStream(serverPath);
|
||||||
|
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||||
|
let downloaded = 0;
|
||||||
|
|
||||||
|
response.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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
response.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
https.get(url, handleResponse).on('error', (err) => {
|
||||||
|
fs.unlink(serverPath, () => {});
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
||||||
|
domain: newDomain,
|
||||||
|
patchedAt: new Date().toISOString(),
|
||||||
|
patcher: 'PrePatchedDownload',
|
||||||
|
source: 'https://download.sanasol.ws/download/HytaleServer.jar'
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (progressCallback) progressCallback('Server patching complete', 100);
|
||||||
|
console.log('=== Server Patching Complete ===');
|
||||||
|
return { success: true, patchCount: 1 };
|
||||||
|
|
||||||
|
} catch (downloadError) {
|
||||||
|
console.error(`Failed to download patched JAR: ${downloadError.message}`);
|
||||||
|
|
||||||
|
// Restore backup on failure
|
||||||
|
const backupPath = serverPath + '.original';
|
||||||
|
if (fs.existsSync(backupPath)) {
|
||||||
|
fs.copyFileSync(backupPath, serverPath);
|
||||||
|
console.log('Restored backup after download failure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: `Failed to download patched server: ${downloadError.message}` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the client binary path based on platform
|
* Find client binary path based on platform
|
||||||
*/
|
*/
|
||||||
findClientPath(gameDir) {
|
findClientPath(gameDir) {
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
@@ -422,7 +725,9 @@ class ClientPatcher {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find server JAR path
|
||||||
|
*/
|
||||||
findServerPath(gameDir) {
|
findServerPath(gameDir) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
||||||
@@ -439,10 +744,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
|
|
||||||
*/
|
*/
|
||||||
async ensureClientPatched(gameDir, progressCallback) {
|
async ensureClientPatched(gameDir, progressCallback, javaPath = null) {
|
||||||
const results = {
|
const results = {
|
||||||
client: null,
|
client: null,
|
||||||
server: null,
|
server: null,
|
||||||
@@ -451,9 +754,7 @@ class ClientPatcher {
|
|||||||
|
|
||||||
const clientPath = this.findClientPath(gameDir);
|
const clientPath = this.findClientPath(gameDir);
|
||||||
if (clientPath) {
|
if (clientPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching client binary...', 10);
|
||||||
progressCallback('Patching client binary...', 10);
|
|
||||||
}
|
|
||||||
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
||||||
@@ -466,9 +767,7 @@ 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);
|
||||||
@@ -483,12 +782,10 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new ClientPatcher();
|
module.exports = new ClientPatcher();
|
||||||
|
|||||||
@@ -2,151 +2,454 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
// Automatic stall retry constants
|
||||||
|
const MAX_AUTOMATIC_STALL_RETRIES = 3;
|
||||||
|
const AUTOMATIC_STALL_RETRY_DELAY = 3000; // 3 seconds in milliseconds
|
||||||
|
|
||||||
|
// Network monitoring utilities using Node.js built-in methods
|
||||||
|
function checkNetworkConnection() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { lookup } = require('dns');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
// Try DNS lookup first (faster) - using callback version
|
||||||
|
lookup('8.8.8.8', (err) => {
|
||||||
|
if (err) {
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try HTTP request to confirm internet connectivity
|
||||||
|
const req = http.get('http://www.google.com', { timeout: 3000 }, (res) => {
|
||||||
|
resolve(true);
|
||||||
|
res.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', () => {
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
|
let retryState = {
|
||||||
|
attempts: 0,
|
||||||
|
maxRetries: maxRetries,
|
||||||
|
canRetry: true,
|
||||||
|
lastError: null,
|
||||||
|
automaticStallRetries: 0,
|
||||||
|
isAutomaticRetry: false
|
||||||
|
};
|
||||||
|
let downloadStalled = false;
|
||||||
|
let streamCompleted = false;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
|
retryState.attempts = attempt + 1;
|
||||||
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
|
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
|
||||||
|
|
||||||
if (attempt > 0 && progressCallback) {
|
if (attempt > 0 && progressCallback) {
|
||||||
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null);
|
// Exponential backoff with jitter - longer delays for unstable connections
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif
|
const baseDelay = 3000;
|
||||||
|
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
|
||||||
|
const jitter = Math.random() * 2000;
|
||||||
|
const delay = Math.min(exponentialDelay + jitter, 60000);
|
||||||
|
|
||||||
|
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null, retryState);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create AbortController for proper stream control
|
||||||
|
const controller = new AbortController();
|
||||||
|
let hasReceivedData = false;
|
||||||
|
let lastProgressTime = Date.now(); // Initialize before timeout
|
||||||
|
|
||||||
|
// Smart overall timeout - only trigger if no progress for extended period
|
||||||
|
const overallTimeout = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastProgress = now - lastProgressTime;
|
||||||
|
|
||||||
|
// Only timeout if no data received for 15 minutes (900 seconds) - for very slow connections
|
||||||
|
if (timeSinceLastProgress > 900000 && hasReceivedData) {
|
||||||
|
console.log('Download stalled for 15 minutes, aborting...');
|
||||||
|
console.log(`Download had progress before stall: ${(downloaded / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
controller.abort();
|
||||||
|
}
|
||||||
|
}, 60000); // Check every minute
|
||||||
|
|
||||||
|
// Check if we can resume existing download
|
||||||
|
let startByte = 0;
|
||||||
|
if (fs.existsSync(dest)) {
|
||||||
|
const existingStats = fs.statSync(dest);
|
||||||
|
|
||||||
|
// If file size matches remote size, skip download
|
||||||
|
if (existingStats.size == fs.statSync(dest).size) {
|
||||||
|
console.log('File already exists and is complete. Skipping download.');
|
||||||
|
return { success: true, downloaded: existingStats.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only resume if file exists and is substantial (> 1MB)
|
||||||
|
if (existingStats.size > 1024 * 1024) {
|
||||||
|
startByte = existingStats.size;
|
||||||
|
console.log(`Resuming download from byte ${startByte} (${(existingStats.size / 1024 / 1024).toFixed(2)} MB already downloaded)`);
|
||||||
|
} else {
|
||||||
|
// File too small, start fresh
|
||||||
|
fs.unlinkSync(dest);
|
||||||
|
console.log('Existing file too small, starting fresh download');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
'Accept': '*/*'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add Range header ONLY if resuming (startByte > 0)
|
||||||
|
if (startByte > 0) {
|
||||||
|
headers['Range'] = `bytes=${startByte}-`;
|
||||||
|
console.log(`Adding Range header: bytes=${startByte}-`);
|
||||||
|
} else {
|
||||||
|
console.log('Fresh download, no Range header');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: url,
|
url: url,
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
timeout: 60000, // 60 secondes timeout
|
timeout: 120000, // 120 seconds for slow connections
|
||||||
headers: {
|
signal: controller.signal,
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
headers: headers,
|
||||||
'Accept': '*/*',
|
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
|
||||||
'Referer': 'https://launcher.hytale.com/',
|
|
||||||
'Connection': 'keep-alive'
|
|
||||||
},
|
|
||||||
// Configuration Axios pour la robustesse réseau
|
|
||||||
validateStatus: function (status) {
|
validateStatus: function (status) {
|
||||||
return status >= 200 && status < 300;
|
return (status >= 200 && status < 300) || status === 206;
|
||||||
},
|
},
|
||||||
// Retry configuration
|
|
||||||
maxRedirects: 5,
|
maxRedirects: 5,
|
||||||
// Network resilience
|
family: 4
|
||||||
family: 4 // Force IPv4
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
const contentLength = response.headers['content-length'];
|
||||||
let downloaded = 0;
|
const totalSize = contentLength ? parseInt(contentLength, 10) + startByte : 0;
|
||||||
let lastProgressTime = Date.now();
|
let downloaded = startByte;
|
||||||
|
lastProgressTime = Date.now();
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
// Nettoyer le fichier de destination s'il existe
|
// Check network status before attempting download, in case of known offline state
|
||||||
if (fs.existsSync(dest)) {
|
try {
|
||||||
fs.unlinkSync(dest);
|
const isNetworkOnline = await checkNetworkConnection();
|
||||||
|
if (!isNetworkOnline) {
|
||||||
|
throw new Error('Network connection unavailable. Please check your connection and retry.');
|
||||||
|
}
|
||||||
|
} catch (networkError) {
|
||||||
|
console.error('[Network] Network check failed, proceeding anyway:', networkError.message);
|
||||||
|
// Continue with download attempt - network check failure shouldn't block
|
||||||
}
|
}
|
||||||
|
|
||||||
const writer = fs.createWriteStream(dest);
|
const writer = fs.createWriteStream(dest, {
|
||||||
let downloadStalled = false;
|
flags: startByte > 0 ? 'a' : 'w', // 'a' for append (resume), 'w' for write (fresh)
|
||||||
|
start: startByte > 0 ? startByte : 0
|
||||||
|
});
|
||||||
|
let streamError = null;
|
||||||
let stalledTimeout = null;
|
let stalledTimeout = null;
|
||||||
|
|
||||||
|
// Reset state for this attempt
|
||||||
|
downloadStalled = false;
|
||||||
|
streamCompleted = false;
|
||||||
|
|
||||||
|
// Enhanced stream event handling
|
||||||
response.data.on('data', (chunk) => {
|
response.data.on('data', (chunk) => {
|
||||||
downloaded += chunk.length;
|
downloaded += chunk.length;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
hasReceivedData = true; // Mark that we've received data
|
||||||
|
|
||||||
// Reset stalled timer on data received
|
// Reset simple stall timer on data received
|
||||||
if (stalledTimeout) {
|
if (stalledTimeout) {
|
||||||
clearTimeout(stalledTimeout);
|
clearTimeout(stalledTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set new stalled timer (30 seconds without data = stalled)
|
// Set new stall timer (30 seconds without data = stalled)
|
||||||
stalledTimeout = setTimeout(() => {
|
stalledTimeout = setTimeout(async () => {
|
||||||
|
console.log('Download stalled - checking network connectivity...');
|
||||||
|
|
||||||
|
// Check if network is actually available before retrying
|
||||||
|
try {
|
||||||
|
const isNetworkOnline = await checkNetworkConnection();
|
||||||
|
if (!isNetworkOnline) {
|
||||||
|
console.log('Network connection lost - stopping download and showing error');
|
||||||
|
downloadStalled = true;
|
||||||
|
streamError = new Error('Network connection lost. Please check your internet connection and retry.');
|
||||||
|
streamError.isConnectionLost = true;
|
||||||
|
streamError.canRetry = false;
|
||||||
|
controller.abort();
|
||||||
|
writer.destroy();
|
||||||
|
response.data.destroy();
|
||||||
|
// Immediately reject the promise to prevent hanging
|
||||||
|
setTimeout(() => promiseReject(streamError), 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (networkError) {
|
||||||
|
console.error('Network check failed during stall detection:', networkError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Network available - download stalled due to slow connection, aborting for retry...');
|
||||||
downloadStalled = true;
|
downloadStalled = true;
|
||||||
|
streamError = new Error('Download stalled due to slow network connection. Please retry.');
|
||||||
|
controller.abort();
|
||||||
writer.destroy();
|
writer.destroy();
|
||||||
response.data.destroy();
|
response.data.destroy();
|
||||||
|
// Immediately reject the promise to prevent hanging
|
||||||
|
setTimeout(() => promiseReject(streamError), 100);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
|
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
|
||||||
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
|
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
|
||||||
const elapsed = (now - startTime) / 1000;
|
const elapsed = (now - startTime) / 1000;
|
||||||
const speed = elapsed > 0 ? downloaded / elapsed : 0;
|
const speed = elapsed > 0 ? downloaded / elapsed : 0;
|
||||||
progressCallback(null, percent, speed, downloaded, totalSize);
|
|
||||||
|
progressCallback(null, percent, speed, downloaded, totalSize, retryState);
|
||||||
lastProgressTime = now;
|
lastProgressTime = now;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enhanced stream error handling
|
||||||
response.data.on('error', (error) => {
|
response.data.on('error', (error) => {
|
||||||
|
// Ignore errors if it was intentionally cancelled or already handled
|
||||||
|
if (downloadStalled || streamCompleted || controller.signal.aborted) {
|
||||||
|
console.log(`Ignoring stream error after cancellation: ${error.code || error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!streamError) {
|
||||||
|
streamError = new Error(`Stream error: ${error.code || error.message}. Please retry.`);
|
||||||
|
// Check for connection lost indicators
|
||||||
|
if (error.code === 'ERR_NETWORK_CHANGED' ||
|
||||||
|
error.code === 'ERR_INTERNET_DISCONNECTED' ||
|
||||||
|
error.code === 'ERR_CONNECTION_LOST') {
|
||||||
|
streamError.isConnectionLost = true;
|
||||||
|
streamError.canRetry = false;
|
||||||
|
}
|
||||||
|
console.error(`Stream error on attempt ${attempt + 1}:`, error.code || error.message);
|
||||||
|
}
|
||||||
if (stalledTimeout) {
|
if (stalledTimeout) {
|
||||||
clearTimeout(stalledTimeout);
|
clearTimeout(stalledTimeout);
|
||||||
}
|
}
|
||||||
console.error(`Stream error on attempt ${attempt + 1}:`, error.code || error.message);
|
if (overallTimeout) {
|
||||||
|
clearInterval(overallTimeout);
|
||||||
|
}
|
||||||
writer.destroy();
|
writer.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
response.data.on('close', () => {
|
||||||
|
// Only treat as error if not already handled by cancellation and writer didn't complete
|
||||||
|
if (!streamError && !streamCompleted && !downloadStalled && !controller.signal.aborted) {
|
||||||
|
// Check if writer actually completed but stream close came first
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!streamCompleted) {
|
||||||
|
streamError = new Error('Stream closed unexpectedly. Please retry.');
|
||||||
|
console.log('Stream closed unexpectedly on attempt', attempt + 1);
|
||||||
|
}
|
||||||
|
}, 500); // Small delay to check if writer completes
|
||||||
|
}
|
||||||
|
if (stalledTimeout) {
|
||||||
|
clearTimeout(stalledTimeout);
|
||||||
|
}
|
||||||
|
if (overallTimeout) {
|
||||||
|
clearInterval(overallTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
response.data.on('abort', () => {
|
||||||
|
// Only treat as error if not already handled by stall detection
|
||||||
|
if (!streamError && !streamCompleted && !downloadStalled) {
|
||||||
|
streamError = new Error('Download aborted due to network issue. Please retry.');
|
||||||
|
console.log('Stream aborted on attempt', attempt + 1);
|
||||||
|
}
|
||||||
|
if (stalledTimeout) {
|
||||||
|
clearTimeout(stalledTimeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
response.data.pipe(writer);
|
response.data.pipe(writer);
|
||||||
|
|
||||||
|
let promiseReject = null;
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
|
// Store promise reject function for immediate use by stall timeout
|
||||||
|
promiseReject = reject;
|
||||||
writer.on('finish', () => {
|
writer.on('finish', () => {
|
||||||
|
streamCompleted = true;
|
||||||
|
console.log(`Writer finished on attempt ${attempt + 1}, downloaded: ${(downloaded / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
|
// Clear ALL timeouts to prevent them from firing after completion
|
||||||
if (stalledTimeout) {
|
if (stalledTimeout) {
|
||||||
clearTimeout(stalledTimeout);
|
clearTimeout(stalledTimeout);
|
||||||
|
console.log('Cleared stall timeout after writer finished');
|
||||||
}
|
}
|
||||||
|
if (overallTimeout) {
|
||||||
|
clearInterval(overallTimeout);
|
||||||
|
console.log('Cleared overall timeout after writer finished');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download is successful if writer finished - regardless of stream state
|
||||||
if (!downloadStalled) {
|
if (!downloadStalled) {
|
||||||
console.log(`Download completed successfully on attempt ${attempt + 1}`);
|
console.log(`Download completed successfully on attempt ${attempt + 1}`);
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Download stalled'));
|
// Don't reject here if we already rejected due to network loss - prevents duplicate rejection
|
||||||
|
console.log('Writer finished after stall detection, ignoring...');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
writer.on('error', (error) => {
|
writer.on('error', (error) => {
|
||||||
|
// Ignore write errors if stream was intentionally cancelled
|
||||||
|
if (downloadStalled || controller.signal.aborted) {
|
||||||
|
console.log(`Ignoring writer error after cancellation: ${error.code || error.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!streamError) {
|
||||||
|
streamError = new Error(`File write error: ${error.code || error.message}. Please retry.`);
|
||||||
|
console.error(`Writer error on attempt ${attempt + 1}:`, error.code || error.message);
|
||||||
|
}
|
||||||
if (stalledTimeout) {
|
if (stalledTimeout) {
|
||||||
clearTimeout(stalledTimeout);
|
clearTimeout(stalledTimeout);
|
||||||
}
|
}
|
||||||
reject(error);
|
if (overallTimeout) {
|
||||||
|
clearInterval(overallTimeout);
|
||||||
|
}
|
||||||
|
reject(streamError);
|
||||||
});
|
});
|
||||||
|
|
||||||
response.data.on('error', (error) => {
|
// Handle case where stream ends without finishing writer
|
||||||
if (stalledTimeout) {
|
response.data.on('end', () => {
|
||||||
clearTimeout(stalledTimeout);
|
if (!streamCompleted && !downloadStalled && !streamError) {
|
||||||
|
// Give a small delay for writer to finish - this is normal behavior
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!streamCompleted) {
|
||||||
|
console.log('Stream ended but writer not finished - waiting longer...');
|
||||||
|
// Give more time for writer to finish - this might be slow disk I/O
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!streamCompleted) {
|
||||||
|
streamError = new Error('Download incomplete. Please retry.');
|
||||||
|
reject(streamError);
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
reject(error);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Si on arrive ici, le téléchargement a réussi
|
return dest;
|
||||||
return;
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error;
|
||||||
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
|
retryState.lastError = error;
|
||||||
|
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
|
||||||
|
console.error(`Error details:`, {
|
||||||
|
isConnectionLost: error.isConnectionLost,
|
||||||
|
canRetry: error.canRetry,
|
||||||
|
message: error.message,
|
||||||
|
downloadStalled: downloadStalled,
|
||||||
|
streamCompleted: streamCompleted
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if download actually completed successfully despite the error
|
||||||
|
if (fs.existsSync(dest)) {
|
||||||
|
const stats = fs.statSync(dest);
|
||||||
|
const sizeInMB = stats.size / 1024 / 1024;
|
||||||
|
console.log(`File size after error: ${sizeInMB.toFixed(2)} MB`);
|
||||||
|
|
||||||
|
// If file is substantial size (> 1.5GB), treat as success and break
|
||||||
|
if (sizeInMB >= 1500) {
|
||||||
|
console.log('File appears to be complete despite error, treating as success');
|
||||||
|
return dest; // Exit the retry loop successfully
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Nettoyer le fichier partiel en cas d'erreur
|
// Enhanced file cleanup with validation
|
||||||
if (fs.existsSync(dest)) {
|
if (fs.existsSync(dest)) {
|
||||||
try {
|
try {
|
||||||
|
// HTTP 416 = Range Not Satisfiable, delete corrupted partial file
|
||||||
|
const isRangeError = error.message && error.message.includes('416');
|
||||||
|
|
||||||
|
// Check if file is corrupted (small or invalid) or if error is non-resumable
|
||||||
|
const partialStats = fs.statSync(dest);
|
||||||
|
const isResumableError = error.message && (
|
||||||
|
error.message.includes('stalled') ||
|
||||||
|
error.message.includes('timeout') ||
|
||||||
|
error.message.includes('network') ||
|
||||||
|
error.message.includes('aborted')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if download appears to be complete (close to expected PWR size)
|
||||||
|
const isPossiblyComplete = partialStats.size >= 1500 * 1024 * 1024; // >= 1.5GB
|
||||||
|
|
||||||
|
if (isRangeError || partialStats.size < 1024 * 1024 || (!isResumableError && !isPossiblyComplete)) {
|
||||||
|
// Delete if HTTP 416 OR file is too small OR error is non-resumable AND not possibly complete
|
||||||
|
const reason = isRangeError ? 'HTTP 416 range error' : (!isResumableError && !isPossiblyComplete ? 'non-resumable error' : 'too small');
|
||||||
|
console.log(`[Cleanup] Removing file (${reason}): ${(partialStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
fs.unlinkSync(dest);
|
fs.unlinkSync(dest);
|
||||||
} catch (cleanupError) {
|
} else {
|
||||||
console.warn('Could not cleanup partial file:', cleanupError.message);
|
// Keep the file for resume on resumable errors or if possibly complete
|
||||||
|
console.log(`[Resume] Keeping file (${isPossiblyComplete ? 'possibly complete' : 'for resume'}): ${(partialStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
}
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Could not handle partial file:', cleanupError.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vérifier si c'est une erreur réseau que l'on peut retry
|
// Expanded retryable error codes for better network detection
|
||||||
const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO'];
|
const retryableErrors = [
|
||||||
const isRetryable = retryableErrors.includes(error.code) ||
|
'ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT',
|
||||||
error.message.includes('timeout') ||
|
'ESOCKETTIMEDOUT', 'EPROTO', 'ENETDOWN', 'EHOSTUNREACH',
|
||||||
error.message.includes('stalled') ||
|
'ECONNABORTED', 'EPIPE', 'ENETRESET', 'EADDRNOTAVAIL',
|
||||||
(error.response && error.response.status >= 500);
|
'ERR_NETWORK', 'ERR_INTERNET_DISCONNECTED', 'ERR_CONNECTION_RESET',
|
||||||
|
'ERR_CONNECTION_TIMED_OUT', 'ERR_NAME_NOT_RESOLVED', 'ERR_CONNECTION_CLOSED'
|
||||||
|
];
|
||||||
|
|
||||||
|
const isRetryable = retryableErrors.includes(error.code) ||
|
||||||
|
error.message.includes('timeout') ||
|
||||||
|
error.message.includes('stalled') ||
|
||||||
|
error.message.includes('aborted') ||
|
||||||
|
error.message.includes('network') ||
|
||||||
|
error.message.includes('connection') ||
|
||||||
|
error.message.includes('Please retry') ||
|
||||||
|
error.message.includes('corrupted') ||
|
||||||
|
error.message.includes('invalid') ||
|
||||||
|
(error.response && error.response.status >= 500);
|
||||||
|
|
||||||
if (!isRetryable || attempt === maxRetries - 1) {
|
// Respect error's canRetry property if set
|
||||||
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
|
const canRetry = (error.canRetry === false) ? false : isRetryable;
|
||||||
|
|
||||||
break;
|
if (!canRetry || attempt === maxRetries - 1) {
|
||||||
}
|
// Don't set retryState.canRetry to false for max retries - user should still be able to retry manually
|
||||||
|
retryState.canRetry = error.canRetry === false ? false : true;
|
||||||
|
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`);
|
console.log(`Retryable error detected, will retry...`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
|
// Enhanced error with retry state and user-friendly message
|
||||||
|
const detailedError = lastError?.code || lastError?.message || 'Unknown error';
|
||||||
|
const errorMessage = `Download failed after ${maxRetries} attempts. Last error: ${detailedError}. Please retry`;
|
||||||
|
const enhancedError = new Error(errorMessage);
|
||||||
|
enhancedError.retryState = retryState;
|
||||||
|
enhancedError.lastError = lastError;
|
||||||
|
enhancedError.detailedError = detailedError;
|
||||||
|
|
||||||
|
// Allow manual retry unless it's a connection lost error
|
||||||
|
enhancedError.canRetry = !lastError?.isConnectionLost && lastError?.canRetry !== false;
|
||||||
|
throw enhancedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findHomePageUIPath(gameLatest) {
|
function findHomePageUIPath(gameLatest) {
|
||||||
@@ -205,8 +508,82 @@ function findLogoPath(gameLatest) {
|
|||||||
return searchDirectory(gameLatest);
|
return searchDirectory(gameLatest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatic stall retry function for network stalls
|
||||||
|
async function retryStalledDownload(url, dest, progressCallback, previousError = null) {
|
||||||
|
console.log('Automatic stall retry initiated for:', url);
|
||||||
|
|
||||||
|
// Wait before retry to allow network recovery
|
||||||
|
console.log(`Waiting ${AUTOMATIC_STALL_RETRY_DELAY/1000} seconds before automatic retry...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, AUTOMATIC_STALL_RETRY_DELAY));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create new retryState for automatic retry
|
||||||
|
const automaticRetryState = {
|
||||||
|
attempts: 1,
|
||||||
|
maxRetries: 1,
|
||||||
|
canRetry: true,
|
||||||
|
lastError: null,
|
||||||
|
automaticStallRetries: (previousError && previousError.retryState) ? previousError.retryState.automaticStallRetries + 1 : 1,
|
||||||
|
isAutomaticRetry: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update progress callback with automatic retry info
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(
|
||||||
|
`Automatic stall retry ${automaticRetryState.automaticStallRetries}/${MAX_AUTOMATIC_STALL_RETRIES}...`,
|
||||||
|
null, null, null, null, automaticRetryState
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await downloadFile(url, dest, progressCallback, 1);
|
||||||
|
console.log('Automatic stall retry successful');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Automatic stall retry failed:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual retry function for user-initiated retries
|
||||||
|
async function retryDownload(url, dest, progressCallback, previousError = null) {
|
||||||
|
console.log('Manual retry initiated for:', url);
|
||||||
|
|
||||||
|
// If we have a previous error with retry state, continue from there
|
||||||
|
let additionalRetries = 3; // Allow 3 additional manual retries
|
||||||
|
if (previousError && previousError.retryState) {
|
||||||
|
additionalRetries = Math.max(2, 5 - previousError.retryState.attempts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure cache directory exists before retrying
|
||||||
|
const destDir = path.dirname(dest);
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
console.log('Creating cache directory:', destDir);
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL: Delete partial file before manual retry to avoid HTTP 416
|
||||||
|
if (fs.existsSync(dest)) {
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(dest);
|
||||||
|
console.log(`[Retry] Deleting partial file before retry: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
fs.unlinkSync(dest);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Could not delete partial file:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(url, dest, progressCallback, additionalRetries);
|
||||||
|
console.log('Manual retry successful');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Manual retry failed:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
downloadFile,
|
downloadFile,
|
||||||
|
retryDownload,
|
||||||
|
retryStalledDownload,
|
||||||
findHomePageUIPath,
|
findHomePageUIPath,
|
||||||
findLogoPath
|
findLogoPath
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,11 +53,7 @@ function setupWaylandEnvironment() {
|
|||||||
console.log('Detected Wayland session, configuring environment...');
|
console.log('Detected Wayland session, configuring environment...');
|
||||||
|
|
||||||
const envVars = {
|
const envVars = {
|
||||||
SDL_VIDEODRIVER: 'wayland',
|
SDL_VIDEODRIVER: 'wayland'
|
||||||
GDK_BACKEND: 'wayland',
|
|
||||||
QT_QPA_PLATFORM: 'wayland',
|
|
||||||
MOZ_ENABLE_WAYLAND: '1',
|
|
||||||
_JAVA_AWT_WM_NONREPARENTING: '1'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
|
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
|
||||||
|
|||||||
131
backend/utils/userDataBackup.js
Normal file
131
backend/utils/userDataBackup.js
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup and restore UserData folder during game updates
|
||||||
|
*/
|
||||||
|
class UserDataBackup {
|
||||||
|
/**
|
||||||
|
* Backup UserData folder to a temporary location
|
||||||
|
* @param {string} installPath - Base installation path (e.g., C:\Users\...\HytaleF2P)
|
||||||
|
* @param {string} branch - Branch name (release or pre-release)
|
||||||
|
* @param {boolean} hasVersionConfig - True if config.json has version_client and version_branch
|
||||||
|
* @returns {Promise<string|null>} - Path to backup or null if no UserData found
|
||||||
|
*/
|
||||||
|
async backupUserData(installPath, branch, hasVersionConfig = true) {
|
||||||
|
let userDataPath;
|
||||||
|
|
||||||
|
// Si on n'a pas de version_client/version_branch dans config.json,
|
||||||
|
// c'est une ancienne installation, on cherche dans installPath/HytaleF2P/release
|
||||||
|
if (!hasVersionConfig) {
|
||||||
|
const oldPath = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
console.log(`[UserDataBackup] No version_client/version_branch detected, searching old installation in: ${oldPath}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(oldPath)) {
|
||||||
|
userDataPath = oldPath;
|
||||||
|
console.log(`[UserDataBackup] ✓ Old installation found! UserData exists in old location`);
|
||||||
|
} else {
|
||||||
|
console.log(`[UserDataBackup] ✗ No old installation found in ${oldPath}`);
|
||||||
|
userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si on a version_client/version_branch, on cherche dans installPath/HytaleF2P/<branch>
|
||||||
|
userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
console.log(`[UserDataBackup] Version configured, searching in: ${userDataPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(userDataPath)) {
|
||||||
|
console.log(`[UserDataBackup] ✗ No UserData found at ${userDataPath}, backup skipped`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[UserDataBackup] ✓ UserData found at ${userDataPath}`);
|
||||||
|
const backupPath = path.join(installPath, `UserData_backup_${branch}_${Date.now()}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`);
|
||||||
|
await fs.copy(userDataPath, backupPath, {
|
||||||
|
overwrite: true,
|
||||||
|
errorOnExist: false,
|
||||||
|
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||||
|
});
|
||||||
|
console.log('[UserDataBackup] ✓ Backup completed successfully');
|
||||||
|
return backupPath;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserDataBackup] ✗ Erreur lors du backup:', error);
|
||||||
|
throw new Error(`Failed to backup UserData: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore UserData folder from backup
|
||||||
|
* @param {string} backupPath - Path to the backup folder
|
||||||
|
* @param {string} installPath - Base installation path
|
||||||
|
* @param {string} branch - Branch name (release or pre-release)
|
||||||
|
* @returns {Promise<boolean>} - True if restored, false otherwise
|
||||||
|
*/
|
||||||
|
async restoreUserData(backupPath, installPath, branch) {
|
||||||
|
if (!backupPath || !fs.existsSync(backupPath)) {
|
||||||
|
console.log('No backup to restore or backup path does not exist');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Restoring UserData from ${backupPath} to ${userDataPath}`);
|
||||||
|
|
||||||
|
// Ensure parent directory exists
|
||||||
|
const parentDir = path.dirname(userDataPath);
|
||||||
|
if (!fs.existsSync(parentDir)) {
|
||||||
|
await fs.ensureDir(parentDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.copy(backupPath, userDataPath, {
|
||||||
|
overwrite: true,
|
||||||
|
errorOnExist: false,
|
||||||
|
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('UserData restore completed successfully');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restoring UserData:', error);
|
||||||
|
throw new Error(`Failed to restore UserData: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up backup folder
|
||||||
|
* @param {string} backupPath - Path to the backup folder to delete
|
||||||
|
* @returns {Promise<boolean>} - True if deleted, false otherwise
|
||||||
|
*/
|
||||||
|
async cleanupBackup(backupPath) {
|
||||||
|
if (!backupPath || !fs.existsSync(backupPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Cleaning up backup at ${backupPath}`);
|
||||||
|
await fs.remove(backupPath);
|
||||||
|
console.log('Backup cleanup completed');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cleaning up backup:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if UserData exists for a specific branch
|
||||||
|
* @param {string} installPath - Base installation path
|
||||||
|
* @param {string} branch - Branch name (release or pre-release)
|
||||||
|
* @returns {boolean} - True if UserData exists
|
||||||
|
*/
|
||||||
|
hasUserData(installPath, branch) {
|
||||||
|
const userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
return fs.existsSync(userDataPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new UserDataBackup();
|
||||||
172
backend/utils/userDataMigration.js
Normal file
172
backend/utils/userDataMigration.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths');
|
||||||
|
const { loadConfig, saveConfig } = require('../core/config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location
|
||||||
|
*
|
||||||
|
* UserData is now stored in a centralized location instead of inside game installation:
|
||||||
|
* - Windows: %LOCALAPPDATA%\HytaleSaves\
|
||||||
|
* - macOS: ~/Library/Application Support/HytaleSaves/
|
||||||
|
* - Linux: ~/.hytalesaves/
|
||||||
|
*
|
||||||
|
* This eliminates the need for backup/restore during updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if migration to centralized UserData has been completed
|
||||||
|
*/
|
||||||
|
function isMigrationCompleted() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.userDataMigrated === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark migration as completed
|
||||||
|
*/
|
||||||
|
function markMigrationCompleted() {
|
||||||
|
saveConfig({ userDataMigrated: true });
|
||||||
|
console.log('[UserDataMigration] Migration marked as completed in config');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find old UserData location (pre-2.1.2)
|
||||||
|
* Searches in: installPath/branch/package/game/latest/Client/UserData
|
||||||
|
*/
|
||||||
|
function findOldUserDataPath() {
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
const installPath = getResolvedAppDir();
|
||||||
|
const branch = config.version_branch || 'release';
|
||||||
|
|
||||||
|
console.log(`[UserDataMigration] Looking for old UserData...`);
|
||||||
|
console.log(`[UserDataMigration] Install path: ${installPath}`);
|
||||||
|
console.log(`[UserDataMigration] Branch: ${branch}`);
|
||||||
|
|
||||||
|
// Old location
|
||||||
|
const oldPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
console.log(`[UserDataMigration] Checking: ${oldPath}`);
|
||||||
|
console.log(`[UserDataMigration] Checking: ${oldPath}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(oldPath)) {
|
||||||
|
console.log(`[UserDataMigration] ✓ Found old UserData at: ${oldPath}`);
|
||||||
|
return oldPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[UserDataMigration] ✗ Not found at current branch location`);
|
||||||
|
|
||||||
|
// Try other branch if current doesn't exist
|
||||||
|
const otherBranch = branch === 'release' ? 'pre-release' : 'release';
|
||||||
|
const otherPath = path.join(installPath, otherBranch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||||
|
console.log(`[UserDataMigration] Checking other branch: ${otherPath}`);
|
||||||
|
console.log(`[UserDataMigration] Checking other branch: ${otherPath}`);
|
||||||
|
|
||||||
|
if (fs.existsSync(otherPath)) {
|
||||||
|
console.log(`[UserDataMigration] ✓ Found old UserData in other branch at: ${otherPath}`);
|
||||||
|
return otherPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[UserDataMigration] ✗ No old UserData found in any branch');
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserDataMigration] Error finding old UserData:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate UserData from old location to new centralized location
|
||||||
|
* One-time operation when upgrading to 2.1.2
|
||||||
|
*/
|
||||||
|
async function migrateUserDataToCentralized() {
|
||||||
|
// Check if already migrated
|
||||||
|
if (isMigrationCompleted()) {
|
||||||
|
console.log('[UserDataMigration] Migration already completed, skipping');
|
||||||
|
return { success: true, alreadyMigrated: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[UserDataMigration] === Starting UserData Migration to Centralized Location ===');
|
||||||
|
|
||||||
|
const newUserDataPath = getHytaleSavesDir();
|
||||||
|
console.log(`[UserDataMigration] Target location: ${newUserDataPath}`);
|
||||||
|
|
||||||
|
// Ensure new directory exists
|
||||||
|
if (!fs.existsSync(newUserDataPath)) {
|
||||||
|
fs.mkdirSync(newUserDataPath, { recursive: true });
|
||||||
|
console.log('[UserDataMigration] Created new HytaleSaves directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find old UserData
|
||||||
|
const oldUserDataPath = findOldUserDataPath();
|
||||||
|
|
||||||
|
if (!oldUserDataPath) {
|
||||||
|
console.log('[UserDataMigration] No old UserData found - fresh install or already migrated');
|
||||||
|
// Don't mark as migrated - let it check again next time in case game gets installed later
|
||||||
|
return { success: true, freshInstall: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if new location already has data (shouldn't happen, but safety check)
|
||||||
|
const existingFiles = fs.readdirSync(newUserDataPath);
|
||||||
|
if (existingFiles.length > 0) {
|
||||||
|
console.warn('[UserDataMigration] New location already contains files, marking as migrated to avoid re-attempts');
|
||||||
|
markMigrationCompleted();
|
||||||
|
return { success: true, skipped: true, reason: 'target_not_empty' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[UserDataMigration] Copying from ${oldUserDataPath} to ${newUserDataPath}...`);
|
||||||
|
|
||||||
|
// Copy all UserData to new location
|
||||||
|
await fs.copy(oldUserDataPath, newUserDataPath, {
|
||||||
|
overwrite: false,
|
||||||
|
errorOnExist: false,
|
||||||
|
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[UserDataMigration] ✓ UserData copied successfully');
|
||||||
|
|
||||||
|
// Mark migration as completed
|
||||||
|
markMigrationCompleted();
|
||||||
|
|
||||||
|
console.log('[UserDataMigration] === Migration Completed Successfully ===');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
migrated: true,
|
||||||
|
from: oldUserDataPath,
|
||||||
|
to: newUserDataPath
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[UserDataMigration] ✗ Migration failed:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
from: oldUserDataPath,
|
||||||
|
to: newUserDataPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the centralized UserData path (always use this in 2.1.2+)
|
||||||
|
* Ensures directory exists
|
||||||
|
*/
|
||||||
|
function getUserDataPath() {
|
||||||
|
const userDataPath = getHytaleSavesDir();
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!fs.existsSync(userDataPath)) {
|
||||||
|
fs.mkdirSync(userDataPath, { recursive: true });
|
||||||
|
console.log(`[UserDataMigration] Created UserData directory: ${userDataPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userDataPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
migrateUserDataToCentralized,
|
||||||
|
getUserDataPath,
|
||||||
|
isMigrationCompleted,
|
||||||
|
findOldUserDataPath
|
||||||
|
};
|
||||||
18
build/entitlements.mac.plist
Normal file
18
build/entitlements.mac.plist
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
build/icon.icns
Normal file
BIN
build/icon.icns
Normal file
Binary file not shown.
BIN
build/icon.ico
Normal file
BIN
build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
build/icon.png
Normal file
BIN
build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
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` |
|
||||||
550
main.js
550
main.js
@@ -1,10 +1,23 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
require('dotenv').config({ path: path.join(__dirname, '.env') });
|
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 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, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
|
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 { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||||
const UpdateManager = require('./backend/updateManager');
|
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
||||||
|
|
||||||
|
// Handle Hardware Acceleration
|
||||||
|
try {
|
||||||
|
const hwEnabled = loadLauncherHardwareAcceleration();
|
||||||
|
if (!hwEnabled) {
|
||||||
|
console.log('Hardware acceleration disabled by user setting');
|
||||||
|
app.disableHardwareAcceleration();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load hardware acceleration setting:', error);
|
||||||
|
}
|
||||||
|
|
||||||
const logger = require('./backend/logger');
|
const logger = require('./backend/logger');
|
||||||
const profileManager = require('./backend/managers/profileManager');
|
const profileManager = require('./backend/managers/profileManager');
|
||||||
|
|
||||||
@@ -26,11 +39,10 @@ if (!gotTheLock) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let updateManager;
|
|
||||||
let discordRPC = null;
|
let discordRPC = null;
|
||||||
|
|
||||||
// Discord Rich Presence setup
|
// Discord Rich Presence setup
|
||||||
const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID;
|
const DISCORD_CLIENT_ID = "1462244937868513373";
|
||||||
|
|
||||||
function initDiscordRPC() {
|
function initDiscordRPC() {
|
||||||
try {
|
try {
|
||||||
@@ -82,7 +94,7 @@ function setDiscordActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleDiscordRPC(enabled) {
|
async function toggleDiscordRPC(enabled) {
|
||||||
console.log('Toggling Discord RPC:', enabled);
|
console.log('Toggling Discord RPC:', enabled);
|
||||||
|
|
||||||
if (enabled && !discordRPC) {
|
if (enabled && !discordRPC) {
|
||||||
@@ -92,12 +104,13 @@ function toggleDiscordRPC(enabled) {
|
|||||||
try {
|
try {
|
||||||
console.log('Disconnecting Discord RPC...');
|
console.log('Disconnecting Discord RPC...');
|
||||||
discordRPC.clearActivity();
|
discordRPC.clearActivity();
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
discordRPC.destroy();
|
discordRPC.destroy();
|
||||||
discordRPC = null;
|
|
||||||
console.log('Discord RPC disconnected successfully');
|
console.log('Discord RPC disconnected successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error disconnecting Discord RPC:', error.message);
|
console.error('Error disconnecting Discord RPC:', error.message);
|
||||||
discordRPC = null;
|
} finally {
|
||||||
|
discordRPC = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,12 +175,77 @@ function createWindow() {
|
|||||||
// Initialize Discord Rich Presence
|
// Initialize Discord Rich Presence
|
||||||
initDiscordRPC();
|
initDiscordRPC();
|
||||||
|
|
||||||
updateManager = new UpdateManager();
|
// Configure and initialize electron-updater
|
||||||
setTimeout(async () => {
|
// Enable auto-download so updates start immediately when available
|
||||||
const updateInfo = await updateManager.checkForUpdates();
|
autoUpdater.autoDownload = true;
|
||||||
if (updateInfo.updateAvailable) {
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
mainWindow.webContents.send('show-update-popup', updateInfo);
|
|
||||||
|
autoUpdater.on('checking-for-update', () => {
|
||||||
|
console.log('Checking for launcher updates...');
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', (info) => {
|
||||||
|
console.log('Update available:', info.version);
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('update-available', {
|
||||||
|
currentVersion: app.getVersion(),
|
||||||
|
newVersion: info.version,
|
||||||
|
releaseNotes: info.releaseNotes,
|
||||||
|
releaseDate: info.releaseDate
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-not-available', (info) => {
|
||||||
|
console.log('Launcher is up to date:', info.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('error', (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) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('update-download-progress', {
|
||||||
|
percent: progressObj.percent,
|
||||||
|
transferred: progressObj.transferred,
|
||||||
|
total: progressObj.total,
|
||||||
|
bytesPerSecond: progressObj.bytesPerSecond
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-downloaded', (info) => {
|
||||||
|
console.log('Update downloaded:', info.version);
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('update-downloaded', {
|
||||||
|
version: info.version,
|
||||||
|
platform: process.platform,
|
||||||
|
// macOS auto-install often fails on unsigned apps
|
||||||
|
autoInstallSupported: process.platform !== 'darwin'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for updates after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
autoUpdater.checkForUpdates().catch(err => {
|
||||||
|
console.log('Failed to check for updates:', err.message);
|
||||||
|
});
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
mainWindow.webContents.on('devtools-opened', () => {
|
mainWindow.webContents.on('devtools-opened', () => {
|
||||||
@@ -187,21 +265,21 @@ function createWindow() {
|
|||||||
if (input.key === 'F12') {
|
if (input.key === 'F12') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
if (input.key === 'F5') {
|
if (input.key === 'F5') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close application shortcuts
|
// Close application shortcuts
|
||||||
const isMac = process.platform === 'darwin';
|
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');
|
||||||
|
|
||||||
if (quitShortcut) {
|
if (quitShortcut) {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
mainWindow.webContents.on('context-menu', (e) => {
|
mainWindow.webContents.on('context-menu', (e) => {
|
||||||
@@ -239,6 +317,14 @@ app.whenReady().then(async () => {
|
|||||||
// Initialize Profile Manager (runs migration if needed)
|
// Initialize Profile Manager (runs migration if needed)
|
||||||
profileManager.init();
|
profileManager.init();
|
||||||
|
|
||||||
|
// Migrate UserData to centralized location (v2.1.2+)
|
||||||
|
console.log('[Startup] Checking UserData migration...');
|
||||||
|
try {
|
||||||
|
await migrateUserDataToCentralized();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Startup] UserData migration failed:', error);
|
||||||
|
}
|
||||||
|
|
||||||
createSplashScreen();
|
createSplashScreen();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -263,9 +349,9 @@ app.whenReady().then(async () => {
|
|||||||
mainWindow.webContents.send('lock-play-button', true);
|
mainWindow.webContents.send('lock-play-button', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total });
|
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total, retryState });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -320,23 +406,18 @@ app.whenReady().then(async () => {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
function cleanupDiscordRPC() {
|
async function cleanupDiscordRPC() {
|
||||||
if (discordRPC) {
|
if (!discordRPC) return;
|
||||||
try {
|
try {
|
||||||
console.log('Cleaning up Discord RPC...');
|
console.log('Cleaning up Discord RPC...');
|
||||||
discordRPC.clearActivity();
|
discordRPC.clearActivity();
|
||||||
setTimeout(() => {
|
await new Promise(r => setTimeout(r, 100));
|
||||||
try {
|
discordRPC.destroy();
|
||||||
discordRPC.destroy();
|
console.log('Discord RPC cleaned up successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error during final Discord RPC cleanup:', error.message);
|
console.log('Error cleaning up Discord RPC:', error.message);
|
||||||
}
|
} finally {
|
||||||
}, 100);
|
discordRPC = null;
|
||||||
discordRPC = null;
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error cleaning up Discord RPC:', error.message);
|
|
||||||
discordRPC = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,44 +426,42 @@ app.on('before-quit', () => {
|
|||||||
cleanupDiscordRPC();
|
cleanupDiscordRPC();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
console.log('=== LAUNCHER CLOSING ===');
|
console.log('=== LAUNCHER CLOSING ===');
|
||||||
|
app.quit();
|
||||||
cleanupDiscordRPC();
|
});
|
||||||
|
|
||||||
app.quit();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
|
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
|
||||||
try {
|
try {
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const data = {
|
const data = {
|
||||||
message: message || null,
|
message: message || null,
|
||||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||||
speed: speed !== null && speed !== undefined ? speed : null,
|
speed: speed !== null && speed !== undefined ? speed : null,
|
||||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||||
total: total !== null && total !== undefined ? total : null
|
total: total !== null && total !== undefined ? total : null,
|
||||||
|
retryState: retryState || null
|
||||||
};
|
};
|
||||||
mainWindow.webContents.send('progress-update', data);
|
mainWindow.webContents.send('progress-update', data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
|
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
|
||||||
|
|
||||||
if (result.success && result.launched) {
|
if (result.success && result.launched) {
|
||||||
const closeOnStart = loadCloseLauncherOnStart();
|
const closeOnStart = loadCloseLauncherOnStart();
|
||||||
if (closeOnStart) {
|
if (closeOnStart) {
|
||||||
console.log('Close Launcher on start enabled, quitting application...');
|
console.log('Close Launcher on start enabled, quitting application...');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
app.quit();
|
app.quit();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Launch error:', error);
|
console.error('Launch error:', error);
|
||||||
const errorMessage = error.message || error.toString();
|
const errorMessage = error.message || error.toString();
|
||||||
@@ -397,44 +476,118 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => {
|
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, branch) => {
|
||||||
try {
|
try {
|
||||||
|
console.log(`[IPC] install-game called with parameters:`);
|
||||||
|
console.log(` - playerName: ${playerName}`);
|
||||||
|
console.log(` - javaPath: ${javaPath}`);
|
||||||
|
console.log(` - installPath: ${installPath}`);
|
||||||
|
console.log(` - branch: ${branch}`);
|
||||||
|
console.log(`[IPC] branch type: ${typeof branch}, value: ${JSON.stringify(branch)}`);
|
||||||
|
|
||||||
// Signal installation start
|
// Signal installation start
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('installation-start');
|
mainWindow.webContents.send('installation-start');
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const data = {
|
const data = {
|
||||||
message: message || null,
|
message: message || null,
|
||||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||||
speed: speed !== null && speed !== undefined ? speed : null,
|
speed: speed !== null && speed !== undefined ? speed : null,
|
||||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||||
total: total !== null && total !== undefined ? total : null
|
total: total !== null && total !== undefined ? total : null,
|
||||||
|
retryState: retryState || null
|
||||||
};
|
};
|
||||||
mainWindow.webContents.send('progress-update', data);
|
mainWindow.webContents.send('progress-update', data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await installGame(playerName, progressCallback, javaPath, installPath);
|
const result = await installGame(playerName, progressCallback, javaPath, installPath, branch);
|
||||||
|
|
||||||
// Signal installation end
|
// Signal installation end
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('installation-end');
|
mainWindow.webContents.send('installation-end');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// Ensure we always return a result for the IPC handler
|
||||||
|
const successResponse = result || { success: true };
|
||||||
|
console.log('[Main] Returning success response for install-game:', successResponse);
|
||||||
|
return successResponse;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Install error:', error);
|
// console.error('Install error:', error);
|
||||||
const errorMessage = error.message || error.toString();
|
const errorMessage = error.message || error.toString();
|
||||||
|
|
||||||
|
// Enhanced error data extraction for both download and Butler errors
|
||||||
|
let errorData = {
|
||||||
|
message: errorMessage,
|
||||||
|
error: true,
|
||||||
|
canRetry: true, // Default to true, will be overridden by specific error props
|
||||||
|
retryData: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prioritize JRE errors first
|
||||||
|
if (error.isJREError) {
|
||||||
|
console.log('[Main] Processing JRE download error with retry context');
|
||||||
|
errorData.retryData = {
|
||||||
|
isJREError: true,
|
||||||
|
jreUrl: error.jreUrl,
|
||||||
|
fileName: error.fileName,
|
||||||
|
cacheDir: error.cacheDir,
|
||||||
|
osName: error.osName,
|
||||||
|
arch: error.arch
|
||||||
|
};
|
||||||
|
// For JRE errors, allow manual retry unless explicitly disabled
|
||||||
|
errorData.canRetry = error.canRetry !== false;
|
||||||
|
errorData.errorType = 'jre';
|
||||||
|
}
|
||||||
|
// Handle Butler-specific errors
|
||||||
|
else if (error.butlerError) {
|
||||||
|
console.log('[Main] Processing Butler error with retry context');
|
||||||
|
errorData.retryData = {
|
||||||
|
branch: error.branch || 'release',
|
||||||
|
fileName: error.fileName || '4.pwr',
|
||||||
|
cacheDir: error.cacheDir
|
||||||
|
};
|
||||||
|
errorData.canRetry = error.canRetry !== false;
|
||||||
|
}
|
||||||
|
// Handle PWR download errors
|
||||||
|
else if (error.branch && error.fileName) {
|
||||||
|
console.log('[Main] Processing PWR download error with retry context');
|
||||||
|
errorData.retryData = {
|
||||||
|
branch: error.branch,
|
||||||
|
fileName: error.fileName,
|
||||||
|
cacheDir: error.cacheDir
|
||||||
|
};
|
||||||
|
errorData.canRetry = error.canRetry !== false;
|
||||||
|
}
|
||||||
|
// Default fallback for other errors
|
||||||
|
else {
|
||||||
|
console.log('[Main] Processing generic error, creating default retry data');
|
||||||
|
errorData.retryData = {
|
||||||
|
branch: 'release',
|
||||||
|
fileName: '4.pwr'
|
||||||
|
};
|
||||||
|
// For generic errors, assume it's retryable unless specified
|
||||||
|
errorData.canRetry = error.canRetry !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send enhanced error info for retry UI
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
console.log('[Main] Sending error data to renderer:', errorData);
|
||||||
|
mainWindow.webContents.send('progress-update', errorData);
|
||||||
|
}
|
||||||
|
|
||||||
// Signal installation end on error too
|
// Signal installation end on error too
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send('installation-end');
|
mainWindow.webContents.send('installation-end');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: errorMessage };
|
// Always return a proper response to prevent timeout
|
||||||
|
const errorResponse = { success: false, error: errorMessage };
|
||||||
|
console.log('[Main] Returning error response for install-game:', errorResponse);
|
||||||
|
return errorResponse;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -497,21 +650,30 @@ ipcMain.handle('save-language', (event, language) => {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('load-language', () => {
|
ipcMain.handle('load-language', () => {
|
||||||
return loadLanguage();
|
return loadLanguage();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-close-launcher', (event, enabled) => {
|
ipcMain.handle('save-close-launcher', (event, enabled) => {
|
||||||
saveCloseLauncherOnStart(enabled);
|
saveCloseLauncherOnStart(enabled);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('load-close-launcher', () => {
|
ipcMain.handle('load-close-launcher', () => {
|
||||||
return loadCloseLauncherOnStart();
|
return loadCloseLauncherOnStart();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('select-install-path', async () => {
|
ipcMain.handle('save-launcher-hw-accel', (event, enabled) => {
|
||||||
|
saveLauncherHardwareAcceleration(enabled);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('load-launcher-hw-accel', () => {
|
||||||
|
return loadLauncherHardwareAcceleration();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('select-install-path', async () => {
|
||||||
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
properties: ['openDirectory'],
|
properties: ['openDirectory'],
|
||||||
title: 'Select Installation Folder'
|
title: 'Select Installation Folder'
|
||||||
@@ -525,14 +687,15 @@ ipcMain.handle('select-install-path', async () => {
|
|||||||
|
|
||||||
ipcMain.handle('accept-first-launch-update', async (event, existingGame) => {
|
ipcMain.handle('accept-first-launch-update', async (event, existingGame) => {
|
||||||
try {
|
try {
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const data = {
|
const data = {
|
||||||
message: message || null,
|
message: message || null,
|
||||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||||
speed: speed !== null && speed !== undefined ? speed : null,
|
speed: speed !== null && speed !== undefined ? speed : null,
|
||||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||||
total: total !== null && total !== undefined ? total : null
|
total: total !== null && total !== undefined ? total : null,
|
||||||
|
retryState: retryState || null
|
||||||
};
|
};
|
||||||
mainWindow.webContents.send('first-launch-progress', data);
|
mainWindow.webContents.send('first-launch-progress', data);
|
||||||
}
|
}
|
||||||
@@ -574,21 +737,22 @@ ipcMain.handle('uninstall-game', async () => {
|
|||||||
try {
|
try {
|
||||||
await uninstallGame();
|
await uninstallGame();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Uninstall error:', error);
|
// console.error('Uninstall error:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('repair-game', async () => {
|
ipcMain.handle('repair-game', async () => {
|
||||||
try {
|
try {
|
||||||
const progressCallback = (message, percent, speed, downloaded, total) => {
|
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const data = {
|
const data = {
|
||||||
message: message || null,
|
message: message || null,
|
||||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||||
speed: speed !== null && speed !== undefined ? speed : null,
|
speed: speed !== null && speed !== undefined ? speed : null,
|
||||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||||
total: total !== null && total !== undefined ? total : null
|
total: total !== null && total !== undefined ? total : null,
|
||||||
|
retryState: retryState || null
|
||||||
};
|
};
|
||||||
mainWindow.webContents.send('progress-update', data);
|
mainWindow.webContents.send('progress-update', data);
|
||||||
}
|
}
|
||||||
@@ -598,7 +762,98 @@ ipcMain.handle('repair-game', async () => {
|
|||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Repair error:', error);
|
console.error('Repair error:', error);
|
||||||
return { success: false, error: error.message };
|
const errorMessage = error.message || error.toString();
|
||||||
|
return { success: false, error: errorMessage };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('retry-download', async (event, retryData) => {
|
||||||
|
try {
|
||||||
|
console.log('[IPC] retry-download called with data:', retryData);
|
||||||
|
|
||||||
|
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
const data = {
|
||||||
|
message: message || null,
|
||||||
|
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||||
|
speed: speed !== null && speed !== undefined ? speed : null,
|
||||||
|
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||||
|
total: total !== null && total !== undefined ? total : null,
|
||||||
|
retryState: retryState || null
|
||||||
|
};
|
||||||
|
mainWindow.webContents.send('progress-update', data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle JRE download retries
|
||||||
|
if (retryData && retryData.isJREError) {
|
||||||
|
console.log(`[IPC] Retrying JRE download: jreUrl=${retryData.jreUrl}, fileName=${retryData.fileName}`);
|
||||||
|
console.log('[IPC] Full JRE retry data:', JSON.stringify(retryData, null, 2));
|
||||||
|
|
||||||
|
const { retryJREDownload } = require('./backend/managers/javaManager');
|
||||||
|
const jreCacheFile = path.join(retryData.cacheDir, retryData.fileName);
|
||||||
|
await retryJREDownload(retryData.jreUrl, jreCacheFile, progressCallback);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle PWR download retries (default)
|
||||||
|
if (!retryData || !retryData.branch || !retryData.fileName) {
|
||||||
|
console.log('[IPC] Invalid retry data, using PWR defaults');
|
||||||
|
retryData = {
|
||||||
|
branch: 'release',
|
||||||
|
fileName: '4.pwr'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract PWR download info from retryData
|
||||||
|
const branch = retryData.branch;
|
||||||
|
const fileName = retryData.fileName;
|
||||||
|
const cacheDir = retryData.cacheDir;
|
||||||
|
|
||||||
|
console.log(`[IPC] Retrying PWR download: branch=${branch}, fileName=${fileName}`);
|
||||||
|
console.log('[IPC] Full PWR retry data:', JSON.stringify(retryData, null, 2));
|
||||||
|
|
||||||
|
// Perform retry with enhanced context
|
||||||
|
await retryPWRDownload(branch, fileName, progressCallback, cacheDir);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Retry download error:', error);
|
||||||
|
const errorMessage = error.message || error.toString();
|
||||||
|
|
||||||
|
// Send error update to frontend with context
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
const isJreError = retryData?.isJREError;
|
||||||
|
const errorRetryData = isJreError ?
|
||||||
|
{
|
||||||
|
isJREError: true,
|
||||||
|
jreUrl: retryData?.jreUrl,
|
||||||
|
fileName: retryData?.fileName,
|
||||||
|
cacheDir: retryData?.cacheDir,
|
||||||
|
osName: retryData?.osName,
|
||||||
|
arch: retryData?.arch
|
||||||
|
} :
|
||||||
|
{
|
||||||
|
branch: retryData?.branch || 'release',
|
||||||
|
fileName: retryData?.fileName || '4.pwr',
|
||||||
|
cacheDir: retryData?.cacheDir
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
message: errorMessage,
|
||||||
|
error: true,
|
||||||
|
canRetry: error.canRetry !== false, // Respect canRetry from the thrown error
|
||||||
|
retryData: errorRetryData,
|
||||||
|
errorType: isJreError ? 'jre' : 'general' // Add errorType for the UI
|
||||||
|
};
|
||||||
|
mainWindow.webContents.send('progress-update', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always return a proper response to prevent timeout
|
||||||
|
const errorResponse = { success: false, error: errorMessage };
|
||||||
|
console.log('[Main] Returning error response for retry-download:', errorResponse);
|
||||||
|
return errorResponse;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -622,10 +877,22 @@ 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 } = require('./backend/launcher');
|
const { getResolvedAppDir, loadVersionBranch } = require('./backend/launcher');
|
||||||
const gameDir = path.join(getResolvedAppDir(), 'release', 'package', 'game');
|
const branch = loadVersionBranch();
|
||||||
|
const gameDir = path.join(getResolvedAppDir(), branch, 'package', 'game');
|
||||||
|
|
||||||
if (fs.existsSync(gameDir)) {
|
if (fs.existsSync(gameDir)) {
|
||||||
await shell.openPath(gameDir);
|
await shell.openPath(gameDir);
|
||||||
@@ -823,33 +1090,66 @@ ipcMain.handle('copy-mod-file', async (event, sourcePath, modsPath) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Electron-updater IPC handlers
|
||||||
ipcMain.handle('check-for-updates', async () => {
|
ipcMain.handle('check-for-updates', async () => {
|
||||||
try {
|
try {
|
||||||
return await updateManager.checkForUpdates();
|
const result = await autoUpdater.checkForUpdates();
|
||||||
|
return {
|
||||||
|
updateAvailable: result && result.updateInfo,
|
||||||
|
currentVersion: app.getVersion(),
|
||||||
|
updateInfo: result ? result.updateInfo : null
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking for updates:', error);
|
console.error('Error checking for updates:', error);
|
||||||
return { updateAvailable: false, error: error.message };
|
return { updateAvailable: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-download-page', async () => {
|
ipcMain.handle('download-update', async () => {
|
||||||
try {
|
try {
|
||||||
await shell.openExternal(updateManager.getDownloadUrl());
|
await autoUpdater.downloadUpdate();
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
app.quit();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening download page:', error);
|
console.error('Error downloading update:', error);
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-update-info', async () => {
|
ipcMain.handle('install-update', async () => {
|
||||||
return updateManager.getUpdateInfo();
|
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);
|
||||||
|
} 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', () => {
|
||||||
|
return app.getVersion();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-gpu-info', () => {
|
ipcMain.handle('get-gpu-info', () => {
|
||||||
@@ -881,10 +1181,26 @@ ipcMain.handle('get-detected-gpu', () => {
|
|||||||
return global.detectedGpu;
|
return global.detectedGpu;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('window-close', () => {
|
ipcMain.handle('save-version-branch', (event, branch) => {
|
||||||
app.quit();
|
const { saveVersionBranch } = require('./backend/launcher');
|
||||||
});
|
saveVersionBranch(branch);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('load-version-branch', () => {
|
||||||
|
const { loadVersionBranch } = require('./backend/launcher');
|
||||||
|
return loadVersionBranch();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('load-version-client', () => {
|
||||||
|
const { loadVersionClient } = require('./backend/launcher');
|
||||||
|
return loadVersionClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('window-close', () => {
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('window-minimize', () => {
|
ipcMain.handle('window-minimize', () => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
|||||||
144
package-lock.json
generated
144
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.0.11",
|
"version": "2.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.0.11",
|
"version": "2.1.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.10",
|
"adm-zip": "^0.5.10",
|
||||||
@@ -14,10 +14,12 @@
|
|||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"electron-updater": "^6.7.3",
|
"electron-updater": "^6.7.3",
|
||||||
|
"fs-extra": "^11.3.3",
|
||||||
"tar": "^6.2.1",
|
"tar": "^6.2.1",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/notarize": "^2.5.0",
|
||||||
"electron": "^40.0.0",
|
"electron": "^40.0.0",
|
||||||
"electron-builder": "^26.4.0"
|
"electron-builder": "^26.4.0"
|
||||||
}
|
}
|
||||||
@@ -147,6 +149,21 @@
|
|||||||
"global-agent": "^3.0.0"
|
"global-agent": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/get/node_modules/fs-extra": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6 <7 || >=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/notarize": {
|
"node_modules/@electron/notarize": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
|
||||||
@@ -345,34 +362,6 @@
|
|||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/universal/node_modules/fs-extra": {
|
|
||||||
"version": "11.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
|
||||||
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"graceful-fs": "^4.2.0",
|
|
||||||
"jsonfile": "^6.0.1",
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/universal/node_modules/jsonfile": {
|
|
||||||
"version": "6.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
|
||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"graceful-fs": "^4.1.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/universal/node_modules/minimatch": {
|
"node_modules/@electron/universal/node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
@@ -389,16 +378,6 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/universal/node_modules/universalify": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/windows-sign": {
|
"node_modules/@electron/windows-sign": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz",
|
||||||
@@ -421,50 +400,6 @@
|
|||||||
"node": ">=14.14"
|
"node": ">=14.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
|
|
||||||
"version": "11.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
|
||||||
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"graceful-fs": "^4.2.0",
|
|
||||||
"jsonfile": "^6.0.1",
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/windows-sign/node_modules/jsonfile": {
|
|
||||||
"version": "6.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
|
||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"universalify": "^2.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"graceful-fs": "^4.1.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/windows-sign/node_modules/universalify": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/balanced-match": {
|
"node_modules/@isaacs/balanced-match": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||||
@@ -2612,18 +2547,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "11.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
||||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^4.0.0",
|
"jsonfile": "^6.0.1",
|
||||||
"universalify": "^0.1.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6 <7 || >=8"
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra/node_modules/jsonfile": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra/node_modules/universalify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fs-minipass": {
|
"node_modules/fs-minipass": {
|
||||||
@@ -3238,9 +3193,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.23",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -4530,6 +4485,7 @@
|
|||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||||
|
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chownr": "^2.0.0",
|
"chownr": "^2.0.0",
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.0.11",
|
"version": "2.1.2",
|
||||||
"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",
|
||||||
@@ -11,7 +11,11 @@
|
|||||||
"build:win": "electron-builder --win",
|
"build:win": "electron-builder --win",
|
||||||
"build:linux": "electron-builder --linux",
|
"build:linux": "electron-builder --linux",
|
||||||
"build:mac": "electron-builder --mac",
|
"build:mac": "electron-builder --mac",
|
||||||
"build:all": "electron-builder --win --linux --mac"
|
"build:all": "electron-builder --win --linux --mac",
|
||||||
|
"build:arch": "electron-builder --linux dir",
|
||||||
|
"build:appimage": "electron-builder --linux AppImage --publish never",
|
||||||
|
"build:deb": "electron-builder --linux deb --publish never",
|
||||||
|
"build:rpm": "electron-builder --linux rpm --publish never"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"hytale",
|
"hytale",
|
||||||
@@ -41,6 +45,7 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/notarize": "^2.5.0",
|
||||||
"electron": "^40.0.0",
|
"electron": "^40.0.0",
|
||||||
"electron-builder": "^26.4.0"
|
"electron-builder": "^26.4.0"
|
||||||
},
|
},
|
||||||
@@ -50,6 +55,7 @@
|
|||||||
"discord-rpc": "^4.0.1",
|
"discord-rpc": "^4.0.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"electron-updater": "^6.7.3",
|
"electron-updater": "^6.7.3",
|
||||||
|
"fs-extra": "^11.3.3",
|
||||||
"tar": "^6.2.1",
|
"tar": "^6.2.1",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
@@ -81,7 +87,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon.ico"
|
"icon": "build/icon.ico"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
@@ -105,13 +111,6 @@
|
|||||||
"x64",
|
"x64",
|
||||||
"arm64"
|
"arm64"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "pacman",
|
|
||||||
"arch": [
|
|
||||||
"x64",
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "build/icon.png",
|
"icon": "build/icon.png",
|
||||||
@@ -133,8 +132,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "build/icon.icns",
|
"icon": "build/icon.icns",
|
||||||
"category": "public.app-category.games"
|
"category": "public.app-category.games",
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"gatekeeperAssess": false,
|
||||||
|
"entitlements": "build/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "build/entitlements.mac.plist"
|
||||||
},
|
},
|
||||||
|
"afterSign": "scripts/notarize.js",
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
|||||||
32
preload.js
32
preload.js
@@ -2,7 +2,7 @@ const { contextBridge, ipcRenderer } = require('electron');
|
|||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
launchGame: (playerName, javaPath, installPath, gpuPreference) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath, gpuPreference),
|
launchGame: (playerName, javaPath, installPath, gpuPreference) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath, gpuPreference),
|
||||||
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
|
installGame: (playerName, javaPath, installPath, branch) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath, branch),
|
||||||
closeWindow: () => ipcRenderer.invoke('window-close'),
|
closeWindow: () => ipcRenderer.invoke('window-close'),
|
||||||
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
||||||
maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
|
maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
|
||||||
@@ -23,11 +23,17 @@ 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'),
|
||||||
|
|
||||||
|
// Hardware Acceleration
|
||||||
|
saveLauncherHardwareAcceleration: (enabled) => ipcRenderer.invoke('save-launcher-hw-accel', enabled),
|
||||||
|
loadLauncherHardwareAcceleration: () => ipcRenderer.invoke('load-launcher-hw-accel'),
|
||||||
|
|
||||||
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
|
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
|
||||||
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
|
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
|
||||||
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
||||||
uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
|
uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
|
||||||
repairGame: () => ipcRenderer.invoke('repair-game'),
|
repairGame: () => ipcRenderer.invoke('repair-game'),
|
||||||
|
retryDownload: (retryData) => ipcRenderer.invoke('retry-download', retryData),
|
||||||
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
|
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
|
||||||
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
||||||
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
|
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
|
||||||
@@ -56,7 +62,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
ipcRenderer.on('installation-end', () => callback());
|
ipcRenderer.on('installation-end', () => callback());
|
||||||
},
|
},
|
||||||
getUserId: () => ipcRenderer.invoke('get-user-id'),
|
getUserId: () => ipcRenderer.invoke('get-user-id'),
|
||||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
|
||||||
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
|
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
|
||||||
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
|
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
|
||||||
onUpdatePopup: (callback) => {
|
onUpdatePopup: (callback) => {
|
||||||
@@ -68,6 +73,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'),
|
loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'),
|
||||||
getDetectedGpu: () => ipcRenderer.invoke('get-detected-gpu'),
|
getDetectedGpu: () => ipcRenderer.invoke('get-detected-gpu'),
|
||||||
|
|
||||||
|
saveVersionBranch: (branch) => ipcRenderer.invoke('save-version-branch', branch),
|
||||||
|
loadVersionBranch: () => ipcRenderer.invoke('load-version-branch'),
|
||||||
|
loadVersionClient: () => ipcRenderer.invoke('load-version-client'),
|
||||||
|
|
||||||
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
|
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
|
||||||
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
|
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
|
||||||
onFirstLaunchUpdate: (callback) => {
|
onFirstLaunchUpdate: (callback) => {
|
||||||
@@ -103,5 +112,24 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
activate: (id) => ipcRenderer.invoke('profile-activate', id),
|
activate: (id) => ipcRenderer.invoke('profile-activate', id),
|
||||||
delete: (id) => ipcRenderer.invoke('profile-delete', id),
|
delete: (id) => ipcRenderer.invoke('profile-delete', id),
|
||||||
update: (id, updates) => ipcRenderer.invoke('profile-update', id, updates)
|
update: (id, updates) => ipcRenderer.invoke('profile-update', id, updates)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Launcher Update API
|
||||||
|
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||||
|
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||||
|
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||||
|
quitAndInstallUpdate: () => ipcRenderer.invoke('install-update'), // Alias for update.js compatibility
|
||||||
|
getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'),
|
||||||
|
onUpdateAvailable: (callback) => {
|
||||||
|
ipcRenderer.on('update-available', (event, data) => callback(data));
|
||||||
|
},
|
||||||
|
onUpdateDownloadProgress: (callback) => {
|
||||||
|
ipcRenderer.on('update-download-progress', (event, data) => callback(data));
|
||||||
|
},
|
||||||
|
onUpdateDownloaded: (callback) => {
|
||||||
|
ipcRenderer.on('update-downloaded', (event, data) => callback(data));
|
||||||
|
},
|
||||||
|
onUpdateError: (callback) => {
|
||||||
|
ipcRenderer.on('update-error', (event, data) => callback(data));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
62
scripts/notarize.js
Normal file
62
scripts/notarize.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
console.log('[Notarize] Script loaded');
|
||||||
|
|
||||||
|
let notarize;
|
||||||
|
try {
|
||||||
|
notarize = require('@electron/notarize').notarize;
|
||||||
|
console.log('[Notarize] @electron/notarize loaded successfully');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Notarize] Failed to load @electron/notarize:', err.message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
exports.default = async function notarizing(context) {
|
||||||
|
console.log('[Notarize] afterSign hook called');
|
||||||
|
console.log('[Notarize] Context:', JSON.stringify({
|
||||||
|
platform: context.electronPlatformName,
|
||||||
|
appOutDir: context.appOutDir,
|
||||||
|
outDir: context.outDir
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
const { electronPlatformName, appOutDir } = context;
|
||||||
|
|
||||||
|
// Only notarize macOS builds
|
||||||
|
if (electronPlatformName !== 'darwin') {
|
||||||
|
console.log('[Notarize] Skipping: not macOS');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check credentials
|
||||||
|
const hasAppleId = !!process.env.APPLE_ID;
|
||||||
|
const hasPassword = !!process.env.APPLE_APP_SPECIFIC_PASSWORD;
|
||||||
|
const hasTeamId = !!process.env.APPLE_TEAM_ID;
|
||||||
|
|
||||||
|
console.log('[Notarize] Credentials check:', { hasAppleId, hasPassword, hasTeamId });
|
||||||
|
|
||||||
|
if (!hasAppleId || !hasPassword || !hasTeamId) {
|
||||||
|
console.log('[Notarize] Skipping: missing credentials');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appName = context.packager.appInfo.productFilename;
|
||||||
|
const appPath = path.join(appOutDir, `${appName}.app`);
|
||||||
|
|
||||||
|
console.log('[Notarize] Starting notarization...');
|
||||||
|
console.log('[Notarize] App path:', appPath);
|
||||||
|
console.log('[Notarize] Team ID:', process.env.APPLE_TEAM_ID);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await notarize({
|
||||||
|
appPath,
|
||||||
|
appleId: process.env.APPLE_ID,
|
||||||
|
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
|
||||||
|
teamId: process.env.APPLE_TEAM_ID,
|
||||||
|
});
|
||||||
|
console.log('[Notarize] Notarization complete!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Notarize] Notarization failed:', error.message);
|
||||||
|
console.error('[Notarize] Full error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user