mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 17:31:48 -03:00
Compare commits
109 Commits
sanasol-fi
...
v2.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7042188696 | ||
|
|
c067efbcea | ||
|
|
b83b728d21 | ||
|
|
66493d35ca | ||
|
|
89b9135523 | ||
|
|
0b1716c168 | ||
|
|
9628363455 | ||
|
|
4932a7a51c | ||
|
|
5170f453ea | ||
|
|
db3b2fc966 | ||
|
|
2f5820e850 | ||
|
|
4abb455e0f | ||
|
|
d5828463f9 | ||
|
|
0d15659dc0 | ||
|
|
19c8991a44 | ||
|
|
320ca54758 | ||
|
|
e14d56ef48 | ||
|
|
a649bf1fcc | ||
|
|
66faa1bb1e | ||
|
|
a63e026700 | ||
|
|
552ec42d6c | ||
|
|
fb90277be9 | ||
|
|
27c220a757 | ||
|
|
30929ee0da | ||
|
|
44834e7d12 | ||
|
|
cb7f7e51bf | ||
|
|
9b20c454d3 | ||
|
|
4e04d657b7 | ||
|
|
6d811fd7e0 | ||
|
|
8435fc698c | ||
|
|
6c369edb0f | ||
|
|
fdd8e59ec4 | ||
|
|
e7a033932f | ||
|
|
11c6d40dfe | ||
|
|
0dafb17c7b | ||
|
|
66112f15b2 | ||
|
|
0a71fdac8c | ||
|
|
4b9eae215b | ||
|
|
1510eceb0f | ||
|
|
c4b5368538 | ||
|
|
e0ebf137fc | ||
|
|
5241a502e5 | ||
|
|
3e7c7ccff3 | ||
|
|
89d09f032f | ||
|
|
8bdb78d1e2 | ||
|
|
e9e66dbca7 | ||
|
|
92a0a26251 | ||
|
|
3abe885ab4 | ||
|
|
82f1dd2739 | ||
|
|
4502c11bd0 | ||
|
|
bcc7476322 | ||
|
|
ae6a7db80a | ||
|
|
48395fbff3 | ||
|
|
aae90a72e8 | ||
|
|
b93dc027e1 | ||
|
|
fdbca6b9da | ||
|
|
b2f65bd524 | ||
|
|
bd6b05d1e4 | ||
|
|
454ca7f075 | ||
|
|
e7324eb176 | ||
|
|
98123d7338 | ||
|
|
a6c61aef68 | ||
|
|
31653a37a7 | ||
|
|
1cb08f029a | ||
|
|
6761f6b3e0 | ||
|
|
b1aeb9fe4a | ||
|
|
c27e1f4cd4 | ||
|
|
f0939a60c9 | ||
|
|
23fad047c0 | ||
|
|
5f3c9e0411 | ||
|
|
2e0bdeee5a | ||
|
|
d8d7702d9d | ||
|
|
3ac2f25955 | ||
|
|
7c8a106f06 | ||
|
|
0015ecbe80 | ||
|
|
b5cd9ca791 | ||
|
|
d5da9ecb6d | ||
|
|
b1d01a2f34 | ||
|
|
cd25f124bd | ||
|
|
fc91560acb | ||
|
|
3370628b6e | ||
|
|
3fee5b0f72 | ||
|
|
38d436ceb7 | ||
|
|
6ee23e1944 | ||
|
|
5de155f190 | ||
|
|
b84457d88d | ||
|
|
1ef96561bf | ||
|
|
d91ba72969 | ||
|
|
ecaaa28866 | ||
|
|
ad34741627 | ||
|
|
a346e9d9e3 | ||
|
|
1f4e91c975 | ||
|
|
a1a45a2d31 | ||
|
|
2ed402b14b | ||
|
|
4ce6fbee0a | ||
|
|
3dfaa1c778 | ||
|
|
6a4da66a1e | ||
|
|
53939fc0ae | ||
|
|
fb135d3486 | ||
|
|
62430fe8f0 | ||
|
|
6847a54c0f | ||
|
|
95d47f0e60 | ||
|
|
6fbf37422f | ||
|
|
6a66ed831c | ||
|
|
78bb10588d | ||
|
|
d27663a1ce | ||
|
|
2db7d606bd | ||
|
|
39c12c0591 | ||
|
|
094bb938fc |
@@ -0,0 +1,2 @@
|
||||
HF2P_SECRET_KEY=YOUR_KEY_HERE
|
||||
HF2P_PROXY_URL=YOUR_PROXY
|
||||
5
.github/CODE_OF_CONDUCT.md
vendored
5
.github/CODE_OF_CONDUCT.md
vendored
@@ -36,7 +36,8 @@ This Code of Conduct applies within all community spaces, and also applies when
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Community Chat, message Founders/Devs](https://chat.sanhost.net/invite/Tfz4jCK4).
|
||||
<!-- Discord: https://discord.gg/Fhbb9Yk5WW --> All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
@@ -80,4 +81,4 @@ For answers to common questions about this code of conduct, see the FAQ at [http
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: "[BUG] "
|
||||
title: "[BUG] <Insert Bug Title Here>"
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -51,7 +51,7 @@ body:
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of the launcher are you running?
|
||||
placeholder: "e.g. \"v2.2.0 stable\""
|
||||
placeholder: "e.g. \"v2.2.1\""
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -59,7 +59,9 @@ body:
|
||||
id: hardwarespec
|
||||
attributes:
|
||||
label: Hardware Specification
|
||||
description: Tell us your CPU, iGPU, dGPU, VRAM, and RAM information.
|
||||
description: |
|
||||
Tell us your CPU, iGPU, dGPU, VRAM, and RAM information.
|
||||
(Use N/A if you think this is not correlated with the bug)
|
||||
placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 24 GB VRAM | RAM: 32 GB"
|
||||
validations:
|
||||
required: true
|
||||
@@ -72,9 +74,9 @@ body:
|
||||
options:
|
||||
- Windows 11/10
|
||||
- macOS (Apple Silicon, M1/M2/M3)
|
||||
- Linux Ubuntu/Debian-based (Linux Mint, Pop!_OS, etc.)
|
||||
- Linux Fedora/RHEL-based (Fedora, CentOS, etc.)
|
||||
- Linux Arch-based (Steamdeck, CachyOS, etc.)
|
||||
- Linux Ubuntu/Debian-based (Linux Mint, Pop!_OS, Zorin OS, etc.)
|
||||
- Linux Fedora/RHEL-based (Fedora, Bazzite, CentOS, etc.)
|
||||
- Linux Arch-based (Steamdeck, CachyOS, ArchLinux, etc.)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/support_request.yml
vendored
3
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -22,7 +22,7 @@ body:
|
||||
value: |
|
||||
If you need help or support with using the launcher, please fill out this support request.
|
||||
Provide as much detail as possible so we can assist you effectively.
|
||||
**Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/gME8rUy3MB)!
|
||||
**Need a quick assistance?** Join our [TG Group](https://t.me/sanhostnet) | [TG Channel](https://t.me/hf2p_og) | [Community Chat](https://chat.sanhost.net/invite/Tfz4jCK4)!
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
@@ -63,6 +63,7 @@ body:
|
||||
label: Version
|
||||
description: What launcher version are you using?
|
||||
options:
|
||||
- v2.2.1
|
||||
- v2.2.0
|
||||
- v2.1.1
|
||||
- v2.1.0
|
||||
|
||||
248
.github/workflows/release.yml
vendored
248
.github/workflows/release.yml
vendored
@@ -6,199 +6,139 @@ on:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
# Domain for small API calls (goes through Cloudflare - fine for <100MB)
|
||||
FORGEJO_API: https://git.sanhost.net/api/v1
|
||||
# Direct upload URL (bypasses Cloudflare for large files) - set in repo secrets
|
||||
FORGEJO_UPLOAD: ${{ secrets.FORGEJO_UPLOAD_URL }}
|
||||
|
||||
jobs:
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create Draft Release
|
||||
run: |
|
||||
curl -s -X POST "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases" \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${{ github.ref_name }}\",\"name\":\"${{ github.ref_name }}\",\"body\":\"Release ${{ github.ref_name }}\",\"draft\":true,\"prerelease\":false}" \
|
||||
-o release.json
|
||||
cat release.json
|
||||
echo "RELEASE_ID=$(cat release.json | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')" >> $GITHUB_ENV
|
||||
|
||||
build-windows:
|
||||
needs: [create-release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Wine for cross-compilation
|
||||
run: |
|
||||
sudo dpkg --add-architecture i386
|
||||
sudo mkdir -pm755 /etc/apt/keyrings
|
||||
sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
|
||||
sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -cs)/winehq-$(lsb_release -cs).sources
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --install-recommends winehq-stable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
|
||||
- name: Build Windows Packages
|
||||
run: npx electron-builder --win --publish never
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-builds
|
||||
path: |
|
||||
dist/*.exe
|
||||
dist/*.exe.blockmap
|
||||
dist/latest.yml
|
||||
run: npx electron-builder --win --publish never --config.npmRebuild=false
|
||||
|
||||
- name: Upload to Release
|
||||
run: |
|
||||
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
||||
echo "Release ID: ${RELEASE_ID}"
|
||||
echo "Upload URL: ${FORGEJO_UPLOAD}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets"
|
||||
for file in dist/*.exe dist/*.exe.blockmap dist/latest.yml; do
|
||||
[ -f "$file" ] || continue
|
||||
echo "Uploading $file ($(stat -c%s "$file" 2>/dev/null || stat -f%z "$file") bytes)..."
|
||||
HTTP_CODE=$(curl -w '%{http_code}' --max-time 600 -X POST "${FORGEJO_UPLOAD}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $file)" \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-F "attachment=@${file}" -o /tmp/upload_response.txt 2>/tmp/upload_err.txt)
|
||||
if [ "$HTTP_CODE" = "201" ]; then
|
||||
echo "OK — uploaded $(basename $file)"
|
||||
else
|
||||
echo "FAILED (HTTP $HTTP_CODE): $(cat /tmp/upload_response.txt)"
|
||||
echo "Curl stderr: $(cat /tmp/upload_err.txt)"
|
||||
fi
|
||||
done
|
||||
|
||||
# macOS Universal build (ARM64 + x64 in single binary)
|
||||
build-macos:
|
||||
needs: [create-release]
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
|
||||
- name: Build macOS Universal Package
|
||||
- name: Build macOS Packages
|
||||
env:
|
||||
# Code signing
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
# Notarization (built-in electron-builder)
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: npx electron-builder --mac --universal --publish never
|
||||
run: npx electron-builder --mac --publish never
|
||||
|
||||
- name: List built artifacts
|
||||
run: ls -la dist/
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: macos-builds
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
dist/*.blockmap
|
||||
dist/latest-mac.yml
|
||||
- name: Upload to Release
|
||||
run: |
|
||||
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
||||
echo "Release ID: ${RELEASE_ID}"
|
||||
for file in dist/*.dmg dist/*.zip dist/*.blockmap dist/latest-mac.yml; do
|
||||
[ -f "$file" ] || continue
|
||||
echo "Uploading $file..."
|
||||
HTTP_CODE=$(curl -w '%{http_code}' --max-time 600 -X POST "${FORGEJO_UPLOAD}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $file)" \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-F "attachment=@${file}" -o /tmp/upload_response.txt 2>/tmp/upload_err.txt)
|
||||
if [ "$HTTP_CODE" = "201" ]; then
|
||||
echo "OK — uploaded $(basename $file)"
|
||||
else
|
||||
echo "FAILED (HTTP $HTTP_CODE): $(cat /tmp/upload_response.txt)"
|
||||
echo "Curl stderr: $(cat /tmp/upload_err.txt)"
|
||||
fi
|
||||
done
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
sudo apt-get install -y libarchive-tools rpm
|
||||
|
||||
- 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 pacman --publish never
|
||||
|
||||
- name: Upload to Release
|
||||
run: |
|
||||
npx electron-builder --linux AppImage deb rpm --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 << 'EOFBUILD'
|
||||
set -e
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
makepkg -s --noconfirm
|
||||
EOFBUILD
|
||||
|
||||
- name: Fix permissions for upload
|
||||
if: always()
|
||||
run: |
|
||||
sudo chown -R $(id -u):$(id -g) .
|
||||
|
||||
- name: Upload Arch Package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: arch-package
|
||||
path: |
|
||||
*.pkg.tar.zst
|
||||
.SRCINFO
|
||||
include-hidden-files: true
|
||||
|
||||
# Create release with all builds
|
||||
release:
|
||||
needs: [build-windows, build-linux, build-arch, build-macos]
|
||||
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: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download Windows artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
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: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: macos-builds
|
||||
path: artifacts/macos-builds
|
||||
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R artifacts
|
||||
|
||||
- name: Get version from package.json
|
||||
id: pkg_version
|
||||
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
files: |
|
||||
artifacts/arch-package/*.pkg.tar.zst
|
||||
artifacts/arch-package/*.src.tar.zst
|
||||
artifacts/arch-package/.SRCINFO
|
||||
artifacts/linux-builds/*
|
||||
artifacts/windows-builds/*
|
||||
artifacts/macos-builds/*
|
||||
generate_release_notes: true
|
||||
draft: true
|
||||
prerelease: false
|
||||
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
||||
echo "Release ID: ${RELEASE_ID}"
|
||||
for file in dist/*.AppImage dist/*.AppImage.blockmap dist/*.deb dist/*.rpm dist/*.pacman dist/latest-linux.yml; do
|
||||
[ -f "$file" ] || continue
|
||||
echo "Uploading $file ($(stat -c%s "$file" 2>/dev/null || stat -f%z "$file") bytes)..."
|
||||
HTTP_CODE=$(curl -w '%{http_code}' --max-time 600 -X POST "${FORGEJO_UPLOAD}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $file)" \
|
||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||
-F "attachment=@${file}" -o /tmp/upload_response.txt 2>/tmp/upload_err.txt)
|
||||
if [ "$HTTP_CODE" = "201" ]; then
|
||||
echo "OK — uploaded $(basename $file)"
|
||||
else
|
||||
echo "FAILED (HTTP $HTTP_CODE): $(cat /tmp/upload_response.txt)"
|
||||
echo "Curl stderr: $(cat /tmp/upload_err.txt)"
|
||||
fi
|
||||
done
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,9 @@ dist/
|
||||
# Project Specific: Downloaded patcher (from hytale-auth-server)
|
||||
backend/patcher/
|
||||
|
||||
# Private docs (local only)
|
||||
docs/PATCH_CDN_INFRASTRUCTURE.md
|
||||
|
||||
# macOS Specific
|
||||
.DS_Store
|
||||
*.zst.DS_Store
|
||||
|
||||
271
GUI/index.html
271
GUI/index.html
@@ -8,9 +8,10 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Noto+Sans+Arabic:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="style-RTL.css">
|
||||
</head>
|
||||
|
||||
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
|
||||
@@ -56,8 +57,8 @@
|
||||
<span class="nav-tooltip">Logs</span>
|
||||
</div>
|
||||
<div class="nav-item" onclick="openDiscordExternal()">
|
||||
<i class="fab fa-discord"></i>
|
||||
<span class="nav-tooltip">Discord</span>
|
||||
<i class="fas fa-comments"></i>
|
||||
<span class="nav-tooltip">Community Chat</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,22 +73,40 @@
|
||||
<span id="onlineCount" class="counter-value">0</span>
|
||||
</div>
|
||||
|
||||
<div class="identity-selector" id="identitySelector">
|
||||
<button class="identity-btn" onclick="toggleIdentityDropdown()">
|
||||
<i class="fas fa-id-badge"></i>
|
||||
<span id="currentIdentityName">Player</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="identity-dropdown" id="identityDropdown">
|
||||
<div class="identity-list" id="identityList"></div>
|
||||
<div class="identity-divider"></div>
|
||||
<div class="identity-action" onclick="openIdentityManager()">
|
||||
<i class="fas fa-fingerprint"></i>
|
||||
<span data-i18n="header.manageIdentities">Manage</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="header-tooltip" data-i18n="header.identityTooltip">Your player name & UUID used in-game</span>
|
||||
</div>
|
||||
|
||||
<div class="profile-selector" id="profileSelector">
|
||||
<button class="profile-btn" onclick="toggleProfileDropdown()">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
<span id="currentProfileName">Default</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="profile-dropdown" id="profileDropdown">
|
||||
<div class="profile-list" id="profileList">
|
||||
<!-- Profiles populated by JS -->
|
||||
<!-- Configurations populated by JS -->
|
||||
</div>
|
||||
<div class="profile-divider"></div>
|
||||
<div class="profile-action" onclick="openProfileManager()">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span data-i18n="header.manageProfiles">Manage Profiles</span>
|
||||
<span data-i18n="header.manageProfiles">Manage</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="header-tooltip" data-i18n="header.configTooltip">Game config: mods, Java & memory settings</span>
|
||||
</div>
|
||||
|
||||
<div class="window-controls">
|
||||
@@ -198,6 +217,21 @@
|
||||
<i class="fas fa-play"></i>
|
||||
<span data-i18n="play.playButton">PLAY HYTALE</span>
|
||||
</button>
|
||||
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 12px; font-size: 12px;">
|
||||
<span style="color: #93a3b8;">Telegram:</span>
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://t.me/sanhostnet'); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
|
||||
<i class="fas fa-users"></i> Group
|
||||
</a>
|
||||
<span style="color: #4b5563;">|</span>
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://t.me/hf2p_og'); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
|
||||
<i class="fab fa-telegram"></i> Channel
|
||||
</a>
|
||||
<span style="color: #4b5563;">|</span>
|
||||
<a href="#" onclick="openDiscordExternal(); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
|
||||
<i class="fas fa-comments"></i> Community Chat
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -217,32 +251,16 @@
|
||||
</div>
|
||||
|
||||
<div id="featured-page" class="page">
|
||||
<div class="featured-layout">
|
||||
<div class="featured-left">
|
||||
<div class="featured-header">
|
||||
<h2 class="featured-title">
|
||||
<i class="fas fa-star mr-2"></i>
|
||||
<span>FEATURED SERVERS</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="featuredServersList" class="featured-list">
|
||||
<div class="loading-spinner">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="featured-container">
|
||||
<div class="featured-header">
|
||||
<h2 class="featured-title">
|
||||
<i class="fas fa-star mr-2"></i>
|
||||
<span>FEATURED SERVERS</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="featured-right">
|
||||
<div class="featured-header">
|
||||
<h2 class="featured-title">
|
||||
<i class="fas fa-server mr-2"></i>
|
||||
<span>HF2P SERVERS</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="myServersList" class="featured-list">
|
||||
<div class="loading-spinner">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
</div>
|
||||
<div id="featuredServersList" class="featured-list">
|
||||
<div class="loading-spinner">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,8 +463,70 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
<i class="fas fa-scroll"></i>
|
||||
<span data-i18n="settings.wrapperConfig">Java Wrapper Configuration</span>
|
||||
</h3>
|
||||
<p class="settings-hint" style="margin-bottom: 12px;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span data-i18n="settings.wrapperConfigHint">Configure how the Java wrapper handles JVM flags and arguments at launch time.</span>
|
||||
</p>
|
||||
|
||||
<!-- Strip Flags -->
|
||||
<label class="settings-label" style="margin-bottom: 6px;">
|
||||
<span data-i18n="settings.wrapperStripFlags">JVM Flags to Remove</span>
|
||||
</label>
|
||||
<div id="wrapperStripFlagsList" class="wrapper-items-list"></div>
|
||||
<div style="display: flex; gap: 6px; margin-top: 6px;">
|
||||
<input type="text" id="wrapperAddFlagInput" class="settings-input" style="flex:1;"
|
||||
data-i18n-placeholder="settings.wrapperAddFlagPlaceholder" placeholder="e.g. -XX:+SomeFlag" spellcheck="false">
|
||||
<button id="wrapperAddFlagBtn" class="settings-browse-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span data-i18n="settings.wrapperAdd">Add</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Inject Args -->
|
||||
<label class="settings-label" style="margin-top: 16px; margin-bottom: 6px;">
|
||||
<span data-i18n="settings.wrapperInjectArgs">Arguments to Inject</span>
|
||||
</label>
|
||||
<div id="wrapperInjectArgsList" class="wrapper-items-list"></div>
|
||||
<div style="display: flex; gap: 6px; margin-top: 6px;">
|
||||
<input type="text" id="wrapperAddArgInput" class="settings-input" style="flex:1;"
|
||||
data-i18n-placeholder="settings.wrapperAddArgPlaceholder" placeholder="e.g. --some-flag" spellcheck="false">
|
||||
<select id="wrapperAddArgCondition" class="wrapper-condition-select">
|
||||
<option value="server" data-i18n="settings.wrapperConditionServer">Server Only</option>
|
||||
<option value="always" data-i18n="settings.wrapperConditionAlways">Always</option>
|
||||
</select>
|
||||
<button id="wrapperAddArgBtn" class="settings-browse-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span data-i18n="settings.wrapperAdd">Add</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Restore Defaults -->
|
||||
<div style="margin-top: 12px;">
|
||||
<button id="wrapperRestoreDefaultsBtn" class="settings-browse-btn" style="background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.3);">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span data-i18n="settings.wrapperRestoreDefaults">Restore Defaults</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Script Preview (collapsible) -->
|
||||
<div style="margin-top: 12px;">
|
||||
<button id="wrapperPreviewToggle" class="wrapper-preview-toggle">
|
||||
<i class="fas fa-chevron-right" id="wrapperPreviewChevron"></i>
|
||||
<span data-i18n="settings.wrapperAdvancedPreview">Advanced: Script Preview</span>
|
||||
</button>
|
||||
<div id="wrapperPreviewContainer" style="display: none; margin-top: 8px;">
|
||||
<pre id="wrapperPreviewContent" class="wrapper-preview-content"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-column">
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
@@ -590,6 +670,9 @@
|
||||
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
|
||||
Folder</span>
|
||||
</button>
|
||||
<button class="logs-action-btn logs-send-btn" id="sendLogsBtn" onclick="sendLogs()">
|
||||
<i class="fas fa-paper-plane"></i> <span data-i18n="settings.logsSend">Send Logs</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logsTerminal" class="logs-terminal">
|
||||
@@ -613,7 +696,11 @@
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mods-modal-body">
|
||||
<div class="mods-modal-body" style="padding-top: 0;">
|
||||
<div class="mods-search-container" style="margin: 1.5rem; margin-bottom: 1rem;">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="myModsSearch" placeholder="Search installed mods..." class="mods-search" />
|
||||
</div>
|
||||
<div id="installedModsList" class="installed-mods-list">
|
||||
</div>
|
||||
</div>
|
||||
@@ -667,28 +754,40 @@
|
||||
</div>
|
||||
|
||||
<div class="uuid-modal-body">
|
||||
<div class="uuid-current-section">
|
||||
<h3 class="uuid-section-title" data-i18n="uuid.currentUserUUID">Current User UUID</h3>
|
||||
<div class="uuid-current-display">
|
||||
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
|
||||
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button id="modalRegenerateUuidBtn" class="uuid-action-btn regenerate-btn"
|
||||
title="Generate New UUID">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uuid-list-section">
|
||||
<div class="uuid-list-header">
|
||||
<h3 class="uuid-section-title" data-i18n="uuid.allPlayerUUIDs">All Player UUIDs</h3>
|
||||
<button id="generateNewUuidBtn" class="uuid-generate-btn">
|
||||
<button id="addIdentityBtn" class="uuid-generate-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span data-i18n="uuid.generateNew">Generate New UUID</span>
|
||||
<span data-i18n="uuid.addIdentity">Add Identity</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="uuidAddForm" class="uuid-add-form" style="display: none;">
|
||||
<div class="uuid-add-form-row">
|
||||
<input type="text" id="addIdentityUsername" class="uuid-input"
|
||||
data-i18n-placeholder="uuid.usernamePlaceholder"
|
||||
placeholder="Username" maxlength="16" />
|
||||
</div>
|
||||
<div class="uuid-add-form-row">
|
||||
<input type="text" id="addIdentityUuid" class="uuid-input"
|
||||
data-i18n-placeholder="uuid.customPlaceholder"
|
||||
placeholder="UUID (auto-generated)" maxlength="36" />
|
||||
<button id="addIdentityRegenerateBtn" class="uuid-action-btn regenerate-btn"
|
||||
title="Generate new UUID">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="uuid-add-form-actions">
|
||||
<button id="addIdentityConfirmBtn" class="uuid-set-btn">
|
||||
<i class="fas fa-check"></i>
|
||||
<span data-i18n="uuid.add">Add</span>
|
||||
</button>
|
||||
<button id="addIdentityCancelBtn" class="uuid-cancel-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
<span data-i18n="uuid.cancel">Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="uuidList" class="uuid-list">
|
||||
<div class="uuid-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
@@ -697,21 +796,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uuid-custom-section">
|
||||
<h3 class="uuid-section-title" data-i18n="uuid.setCustomUUID">Set Custom UUID</h3>
|
||||
<div class="uuid-custom-form">
|
||||
<input type="text" id="customUuidInput" class="uuid-input"
|
||||
data-i18n-placeholder="uuid.customPlaceholder" maxlength="36" />
|
||||
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
||||
<i class="fas fa-check"></i>
|
||||
<span data-i18n="uuid.setUUID">Set UUID</span>
|
||||
</button>
|
||||
<div class="uuid-advanced-section">
|
||||
<button id="uuidAdvancedToggle" class="uuid-advanced-toggle">
|
||||
<i class="fas fa-chevron-right uuid-advanced-chevron"></i>
|
||||
<span data-i18n="uuid.advanced">Advanced</span>
|
||||
</button>
|
||||
<div id="uuidAdvancedContent" class="uuid-advanced-content" style="display: none;">
|
||||
<h3 class="uuid-section-title" data-i18n="uuid.setCustomUUID">Set Custom UUID</h3>
|
||||
<div class="uuid-custom-form">
|
||||
<input type="text" id="customUuidInput" class="uuid-input"
|
||||
data-i18n-placeholder="uuid.customPlaceholder" maxlength="36" />
|
||||
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
||||
<i class="fas fa-check"></i>
|
||||
<span data-i18n="uuid.setUUID">Set UUID</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="uuid-custom-hint">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span data-i18n="uuid.warning">Warning: Setting a custom UUID will change your current player
|
||||
identity</span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="uuid-custom-hint">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span data-i18n="uuid.warning">Warning: Setting a custom UUID will change your current player
|
||||
identity</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -722,8 +827,8 @@
|
||||
<div class="profile-modal-content">
|
||||
<div class="profile-modal-header">
|
||||
<h2 class="profile-modal-title">
|
||||
<i class="fas fa-users-cog mr-2"></i>
|
||||
<span data-i18n="profiles.modalTitle">Manage Profiles</span>
|
||||
<i class="fas fa-sliders-h mr-2"></i>
|
||||
<span data-i18n="configurations.modalTitle">Manage Configurations</span>
|
||||
</h2>
|
||||
<button class="modal-close-btn" onclick="closeProfileManager()">
|
||||
<i class="fas fa-times"></i>
|
||||
@@ -734,10 +839,10 @@
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
<div class="profile-create-section">
|
||||
<input type="text" id="newProfileName" data-i18n-placeholder="profiles.newProfilePlaceholder"
|
||||
<input type="text" id="newProfileName" data-i18n-placeholder="configurations.newProfilePlaceholder"
|
||||
class="profile-input" maxlength="20">
|
||||
<button class="profile-create-btn" onclick="createNewProfile()">
|
||||
<i class="fas fa-plus"></i> <span data-i18n="profiles.createProfile">Create Profile</span>
|
||||
<i class="fas fa-plus"></i> <span data-i18n="configurations.createProfile">Create Configuration</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -746,6 +851,7 @@
|
||||
|
||||
<div class="version-display-bottom">
|
||||
<i class="fas fa-code-branch"></i>
|
||||
<span id="launcherVersion"></span>
|
||||
</div>
|
||||
|
||||
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
|
||||
@@ -782,14 +888,14 @@
|
||||
<div class="modal-content discord-popup-modal">
|
||||
<div class="modal-header">
|
||||
<div class="discord-popup-header">
|
||||
<i class="fab fa-discord"></i>
|
||||
<h2 class="modal-title">Join Our Discord Community</h2>
|
||||
<i class="fas fa-comments"></i>
|
||||
<h2 class="modal-title">Join Our Community</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="discord-popup-body">
|
||||
<p class="discord-popup-text">
|
||||
Join our community of over <strong>5000 members</strong> and stay connected!
|
||||
Join our community and stay connected!
|
||||
</p>
|
||||
<p class="discord-popup-text">
|
||||
Get the latest news, updates, and announcements about the launcher.
|
||||
@@ -797,11 +903,11 @@
|
||||
<p class="discord-popup-text">
|
||||
Find help, report bugs, share your feedback, and connect with other players.
|
||||
</p>
|
||||
|
||||
|
||||
<div class="discord-popup-actions">
|
||||
<button class="discord-popup-btn primary" onclick="joinDiscord()">
|
||||
<i class="fab fa-discord"></i>
|
||||
Join Discord
|
||||
<i class="fas fa-comments"></i>
|
||||
Join Community Chat
|
||||
</button>
|
||||
<button class="discord-popup-btn secondary" onclick="closeDiscordPopup()">
|
||||
Maybe Later
|
||||
@@ -816,6 +922,25 @@
|
||||
<script src="js/featured.js"></script>
|
||||
<script type="module" src="js/settings.js"></script>
|
||||
<script type="module" src="js/update.js"></script>
|
||||
|
||||
<!-- Version Selection Modal (Isolated Container) -->
|
||||
<div id="versionSelectModal" class="modal-overlay" style="display: none; position: fixed; inset: 0; z-index: 9999; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(5px); align-items: center; justify-content: center;">
|
||||
<div class="glass-panel" style="width: 100%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; border-radius: 12px; overflow: hidden; margin: 20px;">
|
||||
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 1.5rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
|
||||
<h3 style="margin: 0; font-size: 1.25rem;">Select Version</h3>
|
||||
<button id="closeVersionModal" class="modal-close" style="background: none; border: none; color: #a0a0a0; font-size: 1.25rem; cursor: pointer;"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto;">
|
||||
<div id="versionList" class="version-list-container">
|
||||
<div class="loading-versions" style="display: flex; flex-direction: column; align-items: center; gap: 1rem; color: #a0a0a0;">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<span>Loading versions...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- updater.js disabled - using update.js instead which has skip button and macOS handling -->
|
||||
</body>
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ function escapeHtml(text) {
|
||||
*/
|
||||
async function loadFeaturedServers() {
|
||||
const featuredContainer = document.getElementById('featuredServersList');
|
||||
const myServersContainer = document.getElementById('myServersList');
|
||||
|
||||
try {
|
||||
console.log('[FeaturedServers] Fetching from', FEATURED_SERVERS_API);
|
||||
@@ -54,6 +53,15 @@ async function loadFeaturedServers() {
|
||||
const escapedName = escapeHtml(server.Name || 'Unknown Server');
|
||||
const escapedAddress = escapeHtml(server.Address || '');
|
||||
const bannerUrl = server.img_Banner || 'https://via.placeholder.com/400x240/1e293b/ffffff?text=Server+Banner';
|
||||
const discordUrl = server.discord || '';
|
||||
|
||||
// Build Discord button HTML if discord link exists
|
||||
const discordButton = discordUrl ? `
|
||||
<button class="server-discord-btn" onclick="openServerDiscord('${discordUrl}')">
|
||||
<i class="fab fa-discord"></i>
|
||||
<span>Discord</span>
|
||||
</button>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="featured-server-card">
|
||||
@@ -67,10 +75,13 @@ async function loadFeaturedServers() {
|
||||
<h3 class="featured-server-name">${escapedName}</h3>
|
||||
<div class="featured-server-address">
|
||||
<span class="server-address-text">${escapedAddress}</span>
|
||||
<button class="copy-address-btn" onclick="copyServerAddress('${escapedAddress}', this)">
|
||||
<i class="fas fa-copy"></i>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
<div class="server-action-buttons">
|
||||
<button class="copy-address-btn" onclick="copyServerAddress('${escapedAddress}', this)">
|
||||
<i class="fas fa-copy"></i>
|
||||
<span>Copy</span>
|
||||
</button>
|
||||
${discordButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,13 +91,6 @@ async function loadFeaturedServers() {
|
||||
featuredContainer.innerHTML = featuredHTML;
|
||||
}
|
||||
|
||||
// Show "Coming Soon" for my servers
|
||||
myServersContainer.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #94a3b8; font-size: 1.2rem;">
|
||||
<p>Coming Soon</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[FeaturedServers] Error loading servers:', error);
|
||||
featuredContainer.innerHTML = `
|
||||
@@ -96,11 +100,6 @@ async function loadFeaturedServers() {
|
||||
<p style="font-size: 0.9rem; color: #64748b;">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
myServersContainer.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #94a3b8; font-size: 1.2rem;">
|
||||
<p>Coming Soon</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +150,22 @@ async function copyServerAddress(address, button) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open server Discord in external browser
|
||||
*/
|
||||
function openServerDiscord(discordUrl) {
|
||||
try {
|
||||
console.log('[FeaturedServers] Opening Discord:', discordUrl);
|
||||
if (window.electronAPI && window.electronAPI.openExternal) {
|
||||
window.electronAPI.openExternal(discordUrl);
|
||||
} else {
|
||||
window.open(discordUrl, '_blank');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[FeaturedServers] Failed to open Discord link:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load featured servers when the featured page becomes visible
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
|
||||
@@ -12,9 +12,18 @@ const i18n = (() => {
|
||||
{ code: 'ru-RU', name: 'Russian (Russia)' },
|
||||
{ code: 'sv-SE', name: 'Swedish (Sweden)' },
|
||||
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
|
||||
{ code: 'id-ID', name: 'Indonesian (Indonesia)' }
|
||||
{ code: 'id-ID', name: 'Indonesian (Indonesia)' },
|
||||
{ code: 'ar-SA', name: 'Arabic (Saudi Arabia)' }
|
||||
];
|
||||
|
||||
// RTL languages
|
||||
const rtlLanguages = ['ar-SA'];
|
||||
|
||||
// Check if current language is RTL
|
||||
function isRTL() {
|
||||
return rtlLanguages.includes(currentLang);
|
||||
}
|
||||
|
||||
// Load single language file
|
||||
async function loadLanguage(lang) {
|
||||
if (translations[lang]) return true;
|
||||
@@ -73,6 +82,24 @@ const i18n = (() => {
|
||||
const key = el.getAttribute('data-i18n-title');
|
||||
el.title = t(key);
|
||||
});
|
||||
// Update RTL layout
|
||||
updateRTL();
|
||||
}
|
||||
|
||||
// Update RTL layout
|
||||
function updateRTL() {
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
|
||||
if (isRTL()) {
|
||||
html.setAttribute('dir', 'rtl');
|
||||
html.setAttribute('lang', currentLang);
|
||||
body.classList.add('rtl');
|
||||
} else {
|
||||
html.removeAttribute('dir');
|
||||
html.setAttribute('lang', currentLang);
|
||||
body.classList.remove('rtl');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize - load saved language only
|
||||
@@ -88,7 +115,8 @@ const i18n = (() => {
|
||||
t,
|
||||
setLanguage,
|
||||
getAvailableLanguages: () => availableLanguages,
|
||||
getCurrentLanguage: () => currentLang
|
||||
getCurrentLanguage: () => currentLang,
|
||||
isRTL
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -35,13 +35,21 @@ export function setupLauncher() {
|
||||
// Initial Profile Load
|
||||
loadProfiles();
|
||||
|
||||
// Close dropdown on outside click
|
||||
// Initial Identity Load
|
||||
loadIdentities();
|
||||
|
||||
// Close dropdowns on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
const selector = document.getElementById('profileSelector');
|
||||
if (selector && !selector.contains(e.target)) {
|
||||
const profileSelector = document.getElementById('profileSelector');
|
||||
if (profileSelector && !profileSelector.contains(e.target)) {
|
||||
const dropdown = document.getElementById('profileDropdown');
|
||||
if (dropdown) dropdown.classList.remove('show');
|
||||
}
|
||||
const identitySelector = document.getElementById('identitySelector');
|
||||
if (identitySelector && !identitySelector.contains(e.target)) {
|
||||
const dropdown = document.getElementById('identityDropdown');
|
||||
if (dropdown) dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,7 +91,7 @@ function renderProfileList(profiles, activeProfile) {
|
||||
managerList.innerHTML = profiles.map(p => `
|
||||
<div class="profile-manager-item ${p.id === activeProfile.id ? 'active' : ''}">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="fas fa-user-circle text-xl text-gray-400"></i>
|
||||
<i class="fas fa-sliders-h text-xl text-gray-400"></i>
|
||||
<div>
|
||||
<div class="font-bold">${p.name}</div>
|
||||
<div class="text-xs text-gray-500">ID: ${p.id.substring(0, 8)}...</div>
|
||||
@@ -106,13 +114,6 @@ function updateCurrentProfileUI(profile) {
|
||||
}
|
||||
}
|
||||
|
||||
window.toggleProfileDropdown = () => {
|
||||
const dropdown = document.getElementById('profileDropdown');
|
||||
if (dropdown) {
|
||||
dropdown.classList.toggle('show');
|
||||
}
|
||||
};
|
||||
|
||||
window.openProfileManager = () => {
|
||||
const modal = document.getElementById('profileManagerModal');
|
||||
if (modal) {
|
||||
@@ -146,7 +147,7 @@ window.createNewProfile = async () => {
|
||||
};
|
||||
|
||||
window.deleteProfile = async (id) => {
|
||||
if (!confirm('Are you sure you want to delete this profile? parameters and mods configuration will be lost.')) return;
|
||||
if (!confirm('Are you sure you want to delete this configuration? Mod settings will be lost.')) return;
|
||||
|
||||
try {
|
||||
await window.electronAPI.profile.delete(id);
|
||||
@@ -160,7 +161,7 @@ window.deleteProfile = async (id) => {
|
||||
window.switchProfile = async (id) => {
|
||||
try {
|
||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||
const switchingMsg = window.i18n ? window.i18n.t('progress.switchingProfile') : 'Switching Profile...';
|
||||
const switchingMsg = window.i18n ? window.i18n.t('progress.switchingProfile') : 'Switching configuration...';
|
||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: switchingMsg });
|
||||
|
||||
await window.electronAPI.profile.activate(id);
|
||||
@@ -179,7 +180,7 @@ window.switchProfile = async (id) => {
|
||||
if (dropdown) dropdown.classList.remove('show');
|
||||
|
||||
if (window.LauncherUI) {
|
||||
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Profile Switched!';
|
||||
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Configuration switched!';
|
||||
window.LauncherUI.updateProgress({ message: switchedMsg });
|
||||
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
|
||||
}
|
||||
@@ -194,27 +195,81 @@ window.switchProfile = async (id) => {
|
||||
export async function launch() {
|
||||
if (isDownloading || (playBtn && playBtn.disabled)) return;
|
||||
|
||||
let playerName = 'Player';
|
||||
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
|
||||
playerName = window.SettingsAPI.getCurrentPlayerName();
|
||||
} else if (playerNameInput && playerNameInput.value.trim()) {
|
||||
playerName = playerNameInput.value.trim();
|
||||
// ==========================================================================
|
||||
// STEP 1: Check launch readiness from backend (single source of truth)
|
||||
// ==========================================================================
|
||||
let launchState = null;
|
||||
let playerName = null;
|
||||
|
||||
try {
|
||||
if (window.electronAPI && window.electronAPI.checkLaunchReady) {
|
||||
launchState = await window.electronAPI.checkLaunchReady();
|
||||
playerName = launchState?.username;
|
||||
} else if (window.electronAPI && window.electronAPI.loadUsername) {
|
||||
// Fallback to loadUsername if checkLaunchReady not available
|
||||
playerName = await window.electronAPI.loadUsername();
|
||||
launchState = { ready: !!playerName, hasUsername: !!playerName, username: playerName, issues: [] };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Launcher] Error checking launch readiness:', error);
|
||||
}
|
||||
|
||||
let javaPath = '';
|
||||
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
|
||||
javaPath = window.SettingsAPI.getCurrentJavaPath();
|
||||
// Validate launch readiness
|
||||
if (!launchState?.ready || !playerName) {
|
||||
const issues = launchState?.issues || ['No username configured'];
|
||||
const errorMsg = window.i18n
|
||||
? window.i18n.t('errors.noUsername')
|
||||
: 'Please set your username in Settings before playing.';
|
||||
|
||||
console.error('[Launcher] Launch blocked:', issues.join(', '));
|
||||
|
||||
// Show error to user
|
||||
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||
window.LauncherUI.showError(errorMsg);
|
||||
} else {
|
||||
alert(errorMsg);
|
||||
}
|
||||
|
||||
// Navigate to settings if possible
|
||||
if (window.LauncherUI && window.LauncherUI.showPage) {
|
||||
window.LauncherUI.showPage('settings-page');
|
||||
window.LauncherUI.setActiveNav('settings');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Warn if using default 'Player' name (shouldn't happen with new logic, but keep as safety)
|
||||
if (playerName === 'Player') {
|
||||
console.warn('[Launcher] Warning: Using default username "Player"');
|
||||
}
|
||||
|
||||
console.log(`[Launcher] Launching game for: "${playerName}"`);
|
||||
|
||||
// ==========================================================================
|
||||
// STEP 2: Load other settings from backend
|
||||
// ==========================================================================
|
||||
let javaPath = '';
|
||||
try {
|
||||
if (window.electronAPI && window.electronAPI.loadJavaPath) {
|
||||
javaPath = await window.electronAPI.loadJavaPath() || '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Launcher] Error loading Java path:', error);
|
||||
}
|
||||
|
||||
let gpuPreference = 'auto';
|
||||
try {
|
||||
if (window.electronAPI && window.electronAPI.loadGpuPreference) {
|
||||
gpuPreference = await window.electronAPI.loadGpuPreference();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading GPU preference:', error);
|
||||
console.error('[Launcher] Error loading GPU preference:', error);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// STEP 3: Start launch process
|
||||
// ==========================================================================
|
||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||
isDownloading = true;
|
||||
if (playBtn) {
|
||||
@@ -227,8 +282,9 @@ export async function launch() {
|
||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
|
||||
|
||||
if (window.electronAPI && window.electronAPI.launchGame) {
|
||||
// Pass playerName from config - backend will validate again
|
||||
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
||||
|
||||
|
||||
isDownloading = false;
|
||||
|
||||
if (window.LauncherUI) {
|
||||
@@ -243,7 +299,35 @@ export async function launch() {
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
console.error('Launch failed:', result.error);
|
||||
console.error('[Launcher] Launch failed:', result.error);
|
||||
|
||||
// Handle specific error cases
|
||||
if (result.needsUsername) {
|
||||
const errorMsg = window.i18n
|
||||
? window.i18n.t('errors.noUsername')
|
||||
: 'Please set your username in Settings before playing.';
|
||||
|
||||
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||
window.LauncherUI.showError(errorMsg);
|
||||
} else {
|
||||
alert(errorMsg);
|
||||
}
|
||||
|
||||
// Navigate to settings
|
||||
if (window.LauncherUI && window.LauncherUI.showPage) {
|
||||
window.LauncherUI.showPage('settings-page');
|
||||
window.LauncherUI.setActiveNav('settings');
|
||||
}
|
||||
} else if (result.error) {
|
||||
// Show generic error
|
||||
const errorMsg = window.i18n
|
||||
? window.i18n.t('errors.launchFailed').replace('{error}', result.error)
|
||||
: `Launch failed: ${result.error}`;
|
||||
|
||||
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||
window.LauncherUI.showError(errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isDownloading = false;
|
||||
@@ -260,7 +344,13 @@ export async function launch() {
|
||||
window.LauncherUI.hideProgress();
|
||||
}
|
||||
resetPlayButton();
|
||||
console.error('Launch error:', error);
|
||||
console.error('[Launcher] Launch error:', error);
|
||||
|
||||
// Show error to user
|
||||
const errorMsg = error.message || 'Unknown launch error';
|
||||
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||
window.LauncherUI.showError(errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,6 +677,121 @@ async function loadCustomJavaPath() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// IDENTITY SWITCHER
|
||||
// ==========================================
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function loadIdentities() {
|
||||
try {
|
||||
if (!window.electronAPI) return;
|
||||
|
||||
const nameEl = document.getElementById('currentIdentityName');
|
||||
|
||||
// Load current username
|
||||
let currentUsername = 'Player';
|
||||
if (window.electronAPI.loadUsername) {
|
||||
const name = await window.electronAPI.loadUsername();
|
||||
if (name) currentUsername = name;
|
||||
}
|
||||
if (nameEl) nameEl.textContent = currentUsername;
|
||||
|
||||
// Load all identities for dropdown
|
||||
const list = document.getElementById('identityList');
|
||||
if (!list || !window.electronAPI.getAllUuidMappings) return;
|
||||
|
||||
const mappings = await window.electronAPI.getAllUuidMappings();
|
||||
renderIdentityList(mappings, currentUsername);
|
||||
} catch (error) {
|
||||
console.error('Failed to load identities:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderIdentityList(mappings, currentUsername) {
|
||||
const list = document.getElementById('identityList');
|
||||
if (!list) return;
|
||||
|
||||
if (!mappings || mappings.length === 0) {
|
||||
list.innerHTML = '<div class="identity-empty">No identities</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = mappings.map(m => {
|
||||
const safe = escapeHtml(m.username);
|
||||
return `
|
||||
<div class="identity-item ${m.username === currentUsername ? 'active' : ''}"
|
||||
onclick="switchIdentity('${safe.replace(/'/g, "'")}')">
|
||||
<span>${safe}</span>
|
||||
${m.username === currentUsername ? '<i class="fas fa-check ml-auto"></i>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.toggleIdentityDropdown = () => {
|
||||
const dropdown = document.getElementById('identityDropdown');
|
||||
if (dropdown) {
|
||||
dropdown.classList.toggle('show');
|
||||
// Close profile dropdown
|
||||
const profileDropdown = document.getElementById('profileDropdown');
|
||||
if (profileDropdown) profileDropdown.classList.remove('show');
|
||||
}
|
||||
};
|
||||
|
||||
window.openIdentityManager = () => {
|
||||
// Close dropdown
|
||||
const dropdown = document.getElementById('identityDropdown');
|
||||
if (dropdown) dropdown.classList.remove('show');
|
||||
// Open UUID modal from settings
|
||||
if (window.openUuidModal) {
|
||||
window.openUuidModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.switchIdentity = async (username) => {
|
||||
try {
|
||||
if (!window.electronAPI || !window.electronAPI.saveUsername) return;
|
||||
|
||||
const result = await window.electronAPI.saveUsername(username);
|
||||
if (result && result.success === false) {
|
||||
throw new Error(result.error || 'Failed to switch identity');
|
||||
}
|
||||
|
||||
// Refresh identity dropdown
|
||||
await loadIdentities();
|
||||
|
||||
// Close dropdown
|
||||
const dropdown = document.getElementById('identityDropdown');
|
||||
if (dropdown) dropdown.classList.remove('show');
|
||||
|
||||
// Update settings page username field and UUID display
|
||||
const settingsInput = document.getElementById('settingsPlayerName');
|
||||
if (settingsInput) settingsInput.value = username;
|
||||
if (window.loadCurrentUuid) window.loadCurrentUuid();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to switch identity:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Make loadIdentities available globally for settings.js to call
|
||||
window.loadIdentities = loadIdentities;
|
||||
|
||||
window.toggleProfileDropdown = () => {
|
||||
const dropdown = document.getElementById('profileDropdown');
|
||||
if (dropdown) {
|
||||
dropdown.classList.toggle('show');
|
||||
// Close identity dropdown
|
||||
const identityDropdown = document.getElementById('identityDropdown');
|
||||
if (identityDropdown) identityDropdown.classList.remove('show');
|
||||
}
|
||||
};
|
||||
|
||||
window.launch = launch;
|
||||
window.uninstallGame = uninstallGame;
|
||||
window.repairGame = repairGame;
|
||||
|
||||
109
GUI/js/logs.js
109
GUI/js/logs.js
@@ -66,6 +66,113 @@ async function openLogsFolder() {
|
||||
await window.electronAPI.openLogsFolder();
|
||||
}
|
||||
|
||||
async function sendLogs() {
|
||||
const btn = document.getElementById('sendLogsBtn');
|
||||
if (!btn || btn.disabled) return;
|
||||
|
||||
// Get i18n strings with fallbacks
|
||||
const i18n = window.i18n || {};
|
||||
const sendingText = (i18n.settings && i18n.settings.logsSending) || 'Sending...';
|
||||
const sentText = (i18n.settings && i18n.settings.logsSent) || 'Sent!';
|
||||
const failedText = (i18n.settings && i18n.settings.logsSendFailed) || 'Failed';
|
||||
const sendText = (i18n.settings && i18n.settings.logsSend) || 'Send Logs';
|
||||
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${sendingText}`;
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.sendLogs();
|
||||
|
||||
if (result.success) {
|
||||
btn.innerHTML = `<i class="fas fa-check"></i> ${sentText}`;
|
||||
showLogSubmissionResult(result.id);
|
||||
} else {
|
||||
btn.innerHTML = `<i class="fas fa-times"></i> ${failedText}`;
|
||||
console.error('Send logs failed:', result.error);
|
||||
|
||||
// Show error notification if available
|
||||
if (window.LauncherUI && window.LauncherUI.showNotification) {
|
||||
window.LauncherUI.showNotification(result.error || 'Failed to send logs', 'error');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Send logs error:', err);
|
||||
btn.innerHTML = `<i class="fas fa-times"></i> ${failedText}`;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalHTML;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function showLogSubmissionResult(id) {
|
||||
// Remove existing popup if any
|
||||
const existing = document.getElementById('logSubmissionPopup');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const i18n = window.i18n || {};
|
||||
const idLabel = (i18n.settings && i18n.settings.logsSubmissionId) || 'Submission ID';
|
||||
const copyText = (i18n.common && i18n.common.copy) || 'Copy';
|
||||
const closeText = (i18n.common && i18n.common.close) || 'Close';
|
||||
const shareText = (i18n.settings && i18n.settings.logsShareId) || 'Share this ID with support when reporting issues';
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.id = 'logSubmissionPopup';
|
||||
popup.style.cssText = `
|
||||
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
background: rgba(20, 20, 35, 0.98); border: 1px solid rgba(0, 212, 255, 0.3);
|
||||
border-radius: 12px; padding: 24px 32px; z-index: 10000;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.5); text-align: center;
|
||||
min-width: 320px; backdrop-filter: blur(10px);
|
||||
`;
|
||||
|
||||
popup.innerHTML = `
|
||||
<div style="margin-bottom: 16px;">
|
||||
<i class="fas fa-check-circle" style="font-size: 2em; color: #00d4ff;"></i>
|
||||
</div>
|
||||
<div style="color: #888; font-size: 0.85em; margin-bottom: 8px;">${idLabel}</div>
|
||||
<div id="logSubId" style="font-family: monospace; font-size: 1.5em; color: #00d4ff; letter-spacing: 2px; margin-bottom: 12px; user-select: all;">${id}</div>
|
||||
<div style="color: #666; font-size: 0.8em; margin-bottom: 20px;">${shareText}</div>
|
||||
<div style="display: flex; gap: 10px; justify-content: center;">
|
||||
<button onclick="copyLogSubmissionId('${id}')" style="
|
||||
background: rgba(0,212,255,0.2); border: 1px solid rgba(0,212,255,0.3);
|
||||
color: #00d4ff; padding: 8px 20px; border-radius: 6px; cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
"><i class="fas fa-copy"></i> ${copyText}</button>
|
||||
<button onclick="document.getElementById('logSubmissionPopup').remove()" style="
|
||||
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
|
||||
color: #ccc; padding: 8px 20px; border-radius: 6px; cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
">${closeText}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(popup);
|
||||
|
||||
// Auto-close after 30s
|
||||
setTimeout(() => {
|
||||
if (document.getElementById('logSubmissionPopup')) {
|
||||
popup.remove();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
async function copyLogSubmissionId(id) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(id);
|
||||
const btn = event.target.closest('button');
|
||||
if (btn) {
|
||||
const orig = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||
setTimeout(() => { btn.innerHTML = orig; }, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to copy submission ID:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function openLogs() {
|
||||
// Navigation is handled by sidebar logic, but we can trigger a refresh
|
||||
window.LauncherUI.showPage('logs-page');
|
||||
@@ -77,6 +184,8 @@ function openLogs() {
|
||||
window.refreshLogs = refreshLogs;
|
||||
window.copyLogs = copyLogs;
|
||||
window.openLogsFolder = openLogsFolder;
|
||||
window.sendLogs = sendLogs;
|
||||
window.copyLogSubmissionId = copyLogSubmissionId;
|
||||
window.openLogs = openLogs;
|
||||
|
||||
// Auto-load logs when the page becomes active
|
||||
|
||||
184
GUI/js/mods.js
184
GUI/js/mods.js
@@ -49,6 +49,18 @@ function setupModsEventListeners() {
|
||||
closeModalBtn.addEventListener('click', closeMyModsModal);
|
||||
}
|
||||
|
||||
const myModsSearchInput = document.getElementById('myModsSearch');
|
||||
if (myModsSearchInput) {
|
||||
let myModsSearchTimeout;
|
||||
myModsSearchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
clearTimeout(myModsSearchTimeout);
|
||||
myModsSearchTimeout = setTimeout(() => {
|
||||
filterInstalledMods(query);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
const modal = document.getElementById('myModsModal');
|
||||
if (modal) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
@@ -78,12 +90,30 @@ function setupModsEventListeners() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const browseContainer = document.getElementById('browseModsList');
|
||||
if (browseContainer) {
|
||||
browseContainer.addEventListener('click', (e) => {
|
||||
const installBtn = e.target.closest('[data-install-mod-id]');
|
||||
if (installBtn) {
|
||||
const modId = installBtn.getAttribute('data-install-mod-id');
|
||||
const mod = browseMods.find(m => m.id == modId);
|
||||
if (mod) {
|
||||
openVersionSelectModal(mod);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openMyModsModal() {
|
||||
const modal = document.getElementById('myModsModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
const searchInput = document.getElementById('myModsSearch');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
loadInstalledMods();
|
||||
}
|
||||
}
|
||||
@@ -92,6 +122,10 @@ function closeMyModsModal() {
|
||||
const modal = document.getElementById('myModsModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
const searchInput = document.getElementById('myModsSearch');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,19 +147,39 @@ async function loadInstalledMods() {
|
||||
}
|
||||
}
|
||||
|
||||
function filterInstalledMods(query) {
|
||||
if (!query || query === '') {
|
||||
displayInstalledMods(installedMods);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = installedMods.filter(mod => {
|
||||
const nameMatch = mod.name?.toLowerCase().includes(query);
|
||||
const fileNameMatch = mod.fileName?.toLowerCase().includes(query);
|
||||
const descriptionMatch = mod.description?.toLowerCase().includes(query);
|
||||
const authorMatch = mod.author?.toLowerCase().includes(query);
|
||||
return nameMatch || fileNameMatch || descriptionMatch || authorMatch;
|
||||
});
|
||||
|
||||
displayInstalledMods(filtered);
|
||||
}
|
||||
|
||||
function displayInstalledMods(mods) {
|
||||
const modsContainer = document.getElementById('installedModsList');
|
||||
if (!modsContainer) return;
|
||||
|
||||
if (mods.length === 0) {
|
||||
const searchInput = document.getElementById('myModsSearch');
|
||||
const isSearching = searchInput && searchInput.value.trim() !== '';
|
||||
|
||||
modsContainer.innerHTML = `
|
||||
<div class=\"empty-installed-mods\">
|
||||
<i class=\"fas fa-box-open\"></i>
|
||||
<h4 data-i18n="mods.noModsInstalled">No Mods Installed</h4>
|
||||
<p data-i18n="mods.noModsInstalledDesc">Add mods from CurseForge or import local files</p>
|
||||
<i class=\"fas fa-${isSearching ? 'search' : 'box-open'}\"></i>
|
||||
<h4 data-i18n="${isSearching ? 'mods.noModsFound' : 'mods.noModsInstalled'}">${isSearching ? 'No Mods Found' : 'No Mods Installed'}</h4>
|
||||
<p data-i18n="${isSearching ? 'mods.noModsFoundDesc' : 'mods.noModsInstalledDesc'}">${isSearching ? 'Try a different search term' : 'Add mods from CurseForge or import local files'}</p>
|
||||
</div>
|
||||
`;
|
||||
if (window.i18n) {
|
||||
if (window.i18n && !isSearching) {
|
||||
const container = modsContainer.querySelector('.empty-installed-mods');
|
||||
container.querySelector('h4').textContent = window.i18n.t('mods.noModsInstalled');
|
||||
container.querySelector('p').textContent = window.i18n.t('mods.noModsInstalledDesc');
|
||||
@@ -165,7 +219,7 @@ function createInstalledModCard(mod) {
|
||||
<div class="installed-mod-info">
|
||||
<div class="installed-mod-header">
|
||||
<h4 class="installed-mod-name">${mod.name}</h4>
|
||||
<span class="installed-mod-version">v${mod.version}</span>
|
||||
<span class="installed-mod-version">${mod.fileName || 'v' + mod.version}</span>
|
||||
</div>
|
||||
<p class="installed-mod-description">${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}</p>
|
||||
</div>
|
||||
@@ -295,13 +349,6 @@ function displayBrowseMods(mods) {
|
||||
}
|
||||
|
||||
browseContainer.innerHTML = mods.map(mod => createBrowseModCard(mod)).join('');
|
||||
|
||||
mods.forEach(mod => {
|
||||
const installBtn = document.getElementById(`install-${mod.id}`);
|
||||
if (installBtn) {
|
||||
installBtn.addEventListener('click', () => downloadAndInstallMod(mod));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createBrowseModCard(mod) {
|
||||
@@ -350,12 +397,12 @@ function createBrowseModCard(mod) {
|
||||
${window.i18n ? window.i18n.t('mods.view') : 'VIEW'}
|
||||
</button>
|
||||
${!isInstalled ?
|
||||
`<button id="install-${mod.id}" class="mod-btn-toggle bg-primary text-black hover:bg-primary/80">
|
||||
<i class="fas fa-download"></i>
|
||||
`<button data-install-mod-id=\"${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
|
||||
<i class=\"fas fa-download\"></i>
|
||||
${window.i18n ? window.i18n.t('mods.install') : 'INSTALL'}
|
||||
</button>` :
|
||||
`<button class="mod-btn-toggle bg-white/10 text-white" disabled>
|
||||
<i class="fas fa-check"></i>
|
||||
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
|
||||
<i class=\"fas fa-check\"></i>
|
||||
${window.i18n ? window.i18n.t('mods.installed') : 'INSTALLED'}
|
||||
</button>`
|
||||
}
|
||||
@@ -364,6 +411,104 @@ function createBrowseModCard(mod) {
|
||||
`;
|
||||
}
|
||||
|
||||
let currentSelectedMod = null;
|
||||
|
||||
function openVersionSelectModal(mod) {
|
||||
currentSelectedMod = mod;
|
||||
const modal = document.getElementById('versionSelectModal');
|
||||
const closeBtn = document.getElementById('closeVersionModal');
|
||||
const versionList = document.getElementById('versionList');
|
||||
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
modal.classList.add('active');
|
||||
|
||||
const closeHandler = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
}, 300);
|
||||
currentSelectedMod = null;
|
||||
};
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.onclick = closeHandler;
|
||||
}
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) closeHandler();
|
||||
};
|
||||
|
||||
loadModVersions(mod.id, versionList);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModVersions(modId, container) {
|
||||
container.innerHTML = `
|
||||
<div class="loading-versions">
|
||||
<i class="fas fa-spinner fa-spin fa-2x" style="margin-bottom: 10px; display: block;"></i>
|
||||
<span>Loading versions...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const versions = await window.electronAPI.getModFiles(modId);
|
||||
|
||||
if (!versions || versions.length === 0) {
|
||||
container.innerHTML = `<div class="p-4 text-center text-gray-400" style="padding: 2rem;">No versions found for this mod.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort versions by date desc (API returns desc but ensure)
|
||||
versions.sort((a, b) => new Date(b.fileDate) - new Date(a.fileDate));
|
||||
|
||||
container.innerHTML = versions.map(file => `
|
||||
<div class="version-item">
|
||||
<div class="version-info">
|
||||
<div class="version-name">${file.displayName}</div>
|
||||
<div class="version-meta">
|
||||
<span><i class="fas fa-calendar"></i> ${new Date(file.fileDate).toLocaleDateString()}</span>
|
||||
<span><i class="fas fa-download"></i> ${formatNumber(file.downloadCount)}</span>
|
||||
<span><i class="fas fa-file-archive"></i> ${(file.fileLength / 1024 / 1024).toFixed(2)} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-actions">
|
||||
<button class="btn-install" data-file-id="${file.id}">
|
||||
Install
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add event listeners securely
|
||||
container.querySelectorAll('.btn-install').forEach((btn, index) => {
|
||||
const file = versions[index]; // Map index to file data
|
||||
btn.onclick = () => installVersion(file);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading versions:', error);
|
||||
container.innerHTML = `<div class="p-4 text-center text-red-400" style="padding: 2rem;">Error loading versions.<br><small>${error.message}</small></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function installVersion(file) {
|
||||
if (!currentSelectedMod) return;
|
||||
|
||||
const modal = document.getElementById('versionSelectModal');
|
||||
modal.style.display = 'none';
|
||||
|
||||
const modInfo = {
|
||||
...currentSelectedMod,
|
||||
fileId: file.id,
|
||||
downloadUrl: file.downloadUrl,
|
||||
fileName: file.fileName,
|
||||
fileSize: file.fileLength
|
||||
};
|
||||
|
||||
await downloadAndInstallMod(modInfo);
|
||||
currentSelectedMod = null;
|
||||
}
|
||||
|
||||
async function downloadAndInstallMod(modInfo) {
|
||||
try {
|
||||
const downloadMsg = window.i18n ? window.i18n.t('notifications.modsDownloading').replace('{name}', modInfo.name) : `Downloading ${modInfo.name}...`;
|
||||
@@ -762,7 +907,10 @@ window.modsManager = {
|
||||
closeMyModsModal,
|
||||
viewModPage,
|
||||
loadInstalledMods,
|
||||
loadBrowseMods
|
||||
loadBrowseMods,
|
||||
openVersionSelectModal
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initModsManager);
|
||||
// Remove auto-init since we are now calling it from script.js explicitly
|
||||
// which guarantees order and environment readiness
|
||||
// document.addEventListener('DOMContentLoaded', initModsManager);
|
||||
|
||||
@@ -2,7 +2,7 @@ import './ui.js';
|
||||
import './install.js';
|
||||
import './launcher.js';
|
||||
import './news.js';
|
||||
import './mods.js';
|
||||
import { initModsManager } from './mods.js';
|
||||
import './players.js';
|
||||
import './settings.js';
|
||||
import './logs.js';
|
||||
@@ -15,6 +15,12 @@ let i18nInitialized = false;
|
||||
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
updateLanguageSelector();
|
||||
initModsManager();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateLanguageSelector();
|
||||
initModsManager();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -53,7 +59,8 @@ window.closeDiscordPopup = function() {
|
||||
};
|
||||
|
||||
window.joinDiscord = async function() {
|
||||
await window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
|
||||
// await window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
|
||||
await window.electronAPI?.openExternal('https://chat.sanhost.net/invite/Tfz4jCK4');
|
||||
|
||||
try {
|
||||
await window.electronAPI?.saveConfig({ discordPopup: true });
|
||||
|
||||
@@ -18,10 +18,15 @@ let regenerateUuidBtn;
|
||||
let manageUuidsBtn;
|
||||
let uuidModal;
|
||||
let uuidModalClose;
|
||||
let modalCurrentUuid;
|
||||
let modalCopyUuidBtn;
|
||||
let modalRegenerateUuidBtn;
|
||||
let generateNewUuidBtn;
|
||||
let addIdentityBtn;
|
||||
let uuidAddForm;
|
||||
let addIdentityUsername;
|
||||
let addIdentityUuid;
|
||||
let addIdentityRegenerateBtn;
|
||||
let addIdentityConfirmBtn;
|
||||
let addIdentityCancelBtn;
|
||||
let uuidAdvancedToggle;
|
||||
let uuidAdvancedContent;
|
||||
let uuidList;
|
||||
let customUuidInput;
|
||||
let setCustomUuidBtn;
|
||||
@@ -180,10 +185,15 @@ function setupSettingsElements() {
|
||||
manageUuidsBtn = document.getElementById('manageUuidsBtn');
|
||||
uuidModal = document.getElementById('uuidModal');
|
||||
uuidModalClose = document.getElementById('uuidModalClose');
|
||||
modalCurrentUuid = document.getElementById('modalCurrentUuid');
|
||||
modalCopyUuidBtn = document.getElementById('modalCopyUuidBtn');
|
||||
modalRegenerateUuidBtn = document.getElementById('modalRegenerateUuidBtn');
|
||||
generateNewUuidBtn = document.getElementById('generateNewUuidBtn');
|
||||
addIdentityBtn = document.getElementById('addIdentityBtn');
|
||||
uuidAddForm = document.getElementById('uuidAddForm');
|
||||
addIdentityUsername = document.getElementById('addIdentityUsername');
|
||||
addIdentityUuid = document.getElementById('addIdentityUuid');
|
||||
addIdentityRegenerateBtn = document.getElementById('addIdentityRegenerateBtn');
|
||||
addIdentityConfirmBtn = document.getElementById('addIdentityConfirmBtn');
|
||||
addIdentityCancelBtn = document.getElementById('addIdentityCancelBtn');
|
||||
uuidAdvancedToggle = document.getElementById('uuidAdvancedToggle');
|
||||
uuidAdvancedContent = document.getElementById('uuidAdvancedContent');
|
||||
uuidList = document.getElementById('uuidList');
|
||||
customUuidInput = document.getElementById('customUuidInput');
|
||||
setCustomUuidBtn = document.getElementById('setCustomUuidBtn');
|
||||
@@ -230,16 +240,24 @@ function setupSettingsElements() {
|
||||
uuidModalClose.addEventListener('click', closeUuidModal);
|
||||
}
|
||||
|
||||
if (modalCopyUuidBtn) {
|
||||
modalCopyUuidBtn.addEventListener('click', copyCurrentUuid);
|
||||
if (addIdentityBtn) {
|
||||
addIdentityBtn.addEventListener('click', showAddIdentityForm);
|
||||
}
|
||||
|
||||
if (modalRegenerateUuidBtn) {
|
||||
modalRegenerateUuidBtn.addEventListener('click', regenerateCurrentUuid);
|
||||
if (addIdentityRegenerateBtn) {
|
||||
addIdentityRegenerateBtn.addEventListener('click', regenerateAddIdentityUuid);
|
||||
}
|
||||
|
||||
if (generateNewUuidBtn) {
|
||||
generateNewUuidBtn.addEventListener('click', generateNewUuid);
|
||||
if (addIdentityConfirmBtn) {
|
||||
addIdentityConfirmBtn.addEventListener('click', confirmAddIdentity);
|
||||
}
|
||||
|
||||
if (addIdentityCancelBtn) {
|
||||
addIdentityCancelBtn.addEventListener('click', hideAddIdentityForm);
|
||||
}
|
||||
|
||||
if (uuidAdvancedToggle) {
|
||||
uuidAdvancedToggle.addEventListener('click', toggleAdvancedSection);
|
||||
}
|
||||
|
||||
if (setCustomUuidBtn) {
|
||||
@@ -446,10 +464,30 @@ async function savePlayerName() {
|
||||
return;
|
||||
}
|
||||
|
||||
await window.electronAPI.saveUsername(playerName);
|
||||
const result = await window.electronAPI.saveUsername(playerName);
|
||||
|
||||
// Check if save was successful
|
||||
if (result && result.success === false) {
|
||||
console.error('[Settings] Failed to save username:', result.error);
|
||||
const errorMsg = window.i18n
|
||||
? window.i18n.t('notifications.playerNameSaveFailed')
|
||||
: `Failed to save player name: ${result.error || 'Unknown error'}`;
|
||||
showNotification(errorMsg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
|
||||
showNotification(successMsg, 'success');
|
||||
|
||||
// Refresh UUID display since it may have changed for the new username
|
||||
await loadCurrentUuid();
|
||||
|
||||
// Also refresh the UUID list to update which entry is marked as current
|
||||
await loadAllUuids();
|
||||
|
||||
// Refresh header identity dropdown
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving player name:', error);
|
||||
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
|
||||
@@ -552,6 +590,7 @@ async function loadAllSettings() {
|
||||
await loadLauncherHwAccel();
|
||||
await loadGpuPreference();
|
||||
await loadVersionBranch();
|
||||
await loadWrapperConfigUI();
|
||||
}
|
||||
|
||||
|
||||
@@ -573,11 +612,26 @@ export function getCurrentJavaPath() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get current player name from UI input
|
||||
* Returns null if no name is set (caller must handle this)
|
||||
* NOTE: launcher.js now loads username directly from backend config
|
||||
* This function is used for display purposes only
|
||||
*/
|
||||
export function getCurrentPlayerName() {
|
||||
if (settingsPlayerName && settingsPlayerName.value.trim()) {
|
||||
return settingsPlayerName.value.trim();
|
||||
}
|
||||
return 'Player';
|
||||
// Return null instead of 'Player' - caller must handle missing username
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current player name with fallback for display purposes only
|
||||
* DO NOT use this for launching game - use backend loadUsername() instead
|
||||
*/
|
||||
export function getCurrentPlayerNameForDisplay() {
|
||||
return getCurrentPlayerName() || 'Player';
|
||||
}
|
||||
|
||||
window.openGameLocation = openGameLocation;
|
||||
@@ -587,6 +641,7 @@ document.addEventListener('DOMContentLoaded', initSettings);
|
||||
window.SettingsAPI = {
|
||||
getCurrentJavaPath,
|
||||
getCurrentPlayerName,
|
||||
getCurrentPlayerNameForDisplay,
|
||||
reloadBranch: loadVersionBranch
|
||||
};
|
||||
|
||||
@@ -596,7 +651,6 @@ async function loadCurrentUuid() {
|
||||
const uuid = await window.electronAPI.getCurrentUuid();
|
||||
if (uuid) {
|
||||
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
|
||||
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -606,7 +660,7 @@ async function loadCurrentUuid() {
|
||||
|
||||
async function copyCurrentUuid() {
|
||||
try {
|
||||
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
|
||||
const uuid = currentUuidDisplay ? currentUuidDisplay.value : null;
|
||||
if (uuid && navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(uuid);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
|
||||
@@ -654,13 +708,13 @@ async function performRegenerateUuid() {
|
||||
const result = await window.electronAPI.resetCurrentUserUuid();
|
||||
if (result.success && result.uuid) {
|
||||
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
|
||||
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerated') : 'New UUID generated successfully!';
|
||||
showNotification(msg, 'success');
|
||||
|
||||
if (uuidModal && uuidModal.style.display !== 'none') {
|
||||
await loadAllUuids();
|
||||
}
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to generate new UUID');
|
||||
}
|
||||
@@ -683,6 +737,10 @@ async function openUuidModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// Expose globally so identity dropdown and Escape handler can use them
|
||||
window.openUuidModal = openUuidModal;
|
||||
window.loadCurrentUuid = loadCurrentUuid;
|
||||
|
||||
function closeUuidModal() {
|
||||
if (uuidModal) {
|
||||
uuidModal.classList.remove('active');
|
||||
@@ -691,6 +749,7 @@ function closeUuidModal() {
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
window.closeUuidModal = closeUuidModal;
|
||||
|
||||
async function loadAllUuids() {
|
||||
try {
|
||||
@@ -729,9 +788,15 @@ async function loadAllUuids() {
|
||||
</div>
|
||||
<div class="uuid-item-actions">
|
||||
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
|
||||
${!mapping.isCurrent ? `<button class="uuid-item-btn switch" onclick="switchToUsername('${escapeHtml(mapping.username)}')" title="Switch to this identity">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</button>` : ''}
|
||||
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
${mapping.isCurrent ? `<button class="uuid-item-btn regenerate" onclick="regenerateUuidForUser('${escapeHtml(mapping.username)}')" title="Regenerate UUID">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>` : ''}
|
||||
${!mapping.isCurrent ? `<button class="uuid-item-btn delete" onclick="deleteUuid('${escapeHtml(mapping.username)}')" title="Delete UUID">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>` : ''}
|
||||
@@ -754,23 +819,119 @@ async function loadAllUuids() {
|
||||
}
|
||||
}
|
||||
|
||||
async function generateNewUuid() {
|
||||
async function showAddIdentityForm() {
|
||||
if (!uuidAddForm) return;
|
||||
uuidAddForm.style.display = 'block';
|
||||
if (addIdentityUsername) {
|
||||
addIdentityUsername.value = '';
|
||||
addIdentityUsername.focus();
|
||||
}
|
||||
if (addIdentityUuid) {
|
||||
try {
|
||||
if (window.electronAPI && window.electronAPI.generateNewUuid) {
|
||||
const newUuid = await window.electronAPI.generateNewUuid();
|
||||
if (newUuid) addIdentityUuid.value = newUuid;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error pre-generating UUID:', e);
|
||||
}
|
||||
}
|
||||
if (addIdentityBtn) addIdentityBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
function hideAddIdentityForm() {
|
||||
if (uuidAddForm) uuidAddForm.style.display = 'none';
|
||||
if (addIdentityBtn) addIdentityBtn.style.display = '';
|
||||
}
|
||||
|
||||
async function regenerateAddIdentityUuid() {
|
||||
try {
|
||||
if (window.electronAPI && window.electronAPI.generateNewUuid) {
|
||||
const newUuid = await window.electronAPI.generateNewUuid();
|
||||
if (newUuid) {
|
||||
if (customUuidInput) customUuidInput.value = newUuid;
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidGeneratedShort') : 'New UUID generated!';
|
||||
showNotification(msg, 'success');
|
||||
if (newUuid && addIdentityUuid) {
|
||||
addIdentityUuid.value = newUuid;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating new UUID:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerateFailed') : 'Failed to generate new UUID';
|
||||
console.error('Error generating UUID:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmAddIdentity() {
|
||||
try {
|
||||
const username = addIdentityUsername ? addIdentityUsername.value.trim() : '';
|
||||
const uuid = addIdentityUuid ? addIdentityUuid.value.trim() : '';
|
||||
|
||||
if (!username) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.playerNameRequired') : 'Please enter a username';
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (username.length > 16) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.playerNameTooLong') : 'Username must be 16 characters or less';
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
if (!uuid || !uuidRegex.test(uuid)) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidInvalidFormat') : 'Invalid UUID format';
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
||||
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
||||
if (result.success) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.identityAdded') : 'Identity added successfully!';
|
||||
showNotification(msg, 'success');
|
||||
hideAddIdentityForm();
|
||||
await loadAllUuids();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to add identity');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding identity:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.identityAddFailed') : 'Failed to add identity';
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAdvancedSection() {
|
||||
if (!uuidAdvancedContent || !uuidAdvancedToggle) return;
|
||||
const isOpen = uuidAdvancedContent.style.display !== 'none';
|
||||
uuidAdvancedContent.style.display = isOpen ? 'none' : 'block';
|
||||
const chevron = uuidAdvancedToggle.querySelector('.uuid-advanced-chevron');
|
||||
if (chevron) {
|
||||
chevron.classList.toggle('open', !isOpen);
|
||||
}
|
||||
}
|
||||
|
||||
window.regenerateUuidForUser = async function (username) {
|
||||
try {
|
||||
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';
|
||||
const title = window.i18n ? window.i18n.t('confirm.regenerateUuidTitle') : 'Generate New UUID';
|
||||
const confirmBtn = window.i18n ? window.i18n.t('confirm.regenerateUuidButton') : 'Generate';
|
||||
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||
|
||||
showCustomConfirm(
|
||||
message,
|
||||
title,
|
||||
async () => {
|
||||
await performRegenerateUuid();
|
||||
},
|
||||
null,
|
||||
confirmBtn,
|
||||
cancelBtn
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error regenerating UUID:', error);
|
||||
}
|
||||
};
|
||||
|
||||
async function setCustomUuid() {
|
||||
try {
|
||||
if (!customUuidInput || !customUuidInput.value.trim()) {
|
||||
@@ -813,18 +974,28 @@ async function setCustomUuid() {
|
||||
async function performSetCustomUuid(uuid) {
|
||||
try {
|
||||
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
||||
const username = getCurrentPlayerName();
|
||||
// IMPORTANT: Use saved username from config, not unsaved DOM input
|
||||
// This prevents setting UUID for wrong user if username field was edited but not saved
|
||||
let username = null;
|
||||
if (window.electronAPI.loadUsername) {
|
||||
username = await window.electronAPI.loadUsername();
|
||||
}
|
||||
if (!username) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.noUsername') : 'No username configured. Please save your username first.';
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
||||
|
||||
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();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to set custom UUID');
|
||||
}
|
||||
@@ -850,6 +1021,76 @@ window.copyUuid = async function (uuid) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch to a different username/UUID identity
|
||||
* This changes the active username to use that username's UUID
|
||||
*/
|
||||
window.switchToUsername = async function (username) {
|
||||
try {
|
||||
const message = window.i18n
|
||||
? window.i18n.t('confirm.switchUsernameMessage').replace('{username}', username)
|
||||
: `Switch to username "${username}"? This will change your active player identity.`;
|
||||
const title = window.i18n ? window.i18n.t('confirm.switchUsernameTitle') : 'Switch Identity';
|
||||
const confirmBtn = window.i18n ? window.i18n.t('confirm.switchUsernameButton') : 'Switch';
|
||||
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||
|
||||
showCustomConfirm(
|
||||
message,
|
||||
title,
|
||||
async () => {
|
||||
await performSwitchToUsername(username);
|
||||
},
|
||||
null,
|
||||
confirmBtn,
|
||||
cancelBtn
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error in switchToUsername:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.switchUsernameFailed') : 'Failed to switch username';
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
async function performSwitchToUsername(username) {
|
||||
try {
|
||||
if (!window.electronAPI || !window.electronAPI.saveUsername) {
|
||||
throw new Error('API not available');
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.saveUsername(username);
|
||||
|
||||
if (result && result.success === false) {
|
||||
throw new Error(result.error || 'Failed to save username');
|
||||
}
|
||||
|
||||
// Update the username input field
|
||||
if (settingsPlayerName) {
|
||||
settingsPlayerName.value = username;
|
||||
}
|
||||
|
||||
// Refresh the current UUID display
|
||||
await loadCurrentUuid();
|
||||
|
||||
// Refresh the UUID list to show new "Current" badge
|
||||
await loadAllUuids();
|
||||
|
||||
// Refresh header identity dropdown
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
|
||||
const msg = window.i18n
|
||||
? window.i18n.t('notifications.switchUsernameSuccess').replace('{username}', username)
|
||||
: `Switched to "${username}" successfully!`;
|
||||
showNotification(msg, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error switching username:', error);
|
||||
const msg = window.i18n
|
||||
? window.i18n.t('notifications.switchUsernameFailed')
|
||||
: `Failed to switch username: ${error.message}`;
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
window.deleteUuid = async function (username) {
|
||||
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.`;
|
||||
@@ -883,6 +1124,7 @@ async function performDeleteUuid(username) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
|
||||
showNotification(msg, 'success');
|
||||
await loadAllUuids();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to delete UUID');
|
||||
}
|
||||
@@ -1141,3 +1383,235 @@ async function loadVersionBranch() {
|
||||
return 'release';
|
||||
}
|
||||
}
|
||||
|
||||
// === Java Wrapper Configuration UI ===
|
||||
|
||||
let _wrapperConfig = null;
|
||||
let _wrapperPreviewOpen = false;
|
||||
|
||||
async function loadWrapperConfigUI() {
|
||||
try {
|
||||
if (!window.electronAPI || !window.electronAPI.loadWrapperConfig) return;
|
||||
|
||||
_wrapperConfig = await window.electronAPI.loadWrapperConfig();
|
||||
renderStripFlagsList();
|
||||
renderInjectArgsList();
|
||||
setupWrapperEventListeners();
|
||||
} catch (error) {
|
||||
console.error('Error loading wrapper config UI:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStripFlagsList() {
|
||||
const container = document.getElementById('wrapperStripFlagsList');
|
||||
if (!container || !_wrapperConfig) return;
|
||||
|
||||
if (_wrapperConfig.stripFlags.length === 0) {
|
||||
container.innerHTML = '<div class="wrapper-items-empty">No flags configured</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
_wrapperConfig.stripFlags.forEach((flag, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'wrapper-item';
|
||||
item.innerHTML = `
|
||||
<span class="wrapper-item-text">${escapeHtml(flag)}</span>
|
||||
<button class="wrapper-item-delete" data-index="${index}" title="Remove">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
`;
|
||||
item.querySelector('.wrapper-item-delete').addEventListener('click', () => removeStripFlag(index));
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function renderInjectArgsList() {
|
||||
const container = document.getElementById('wrapperInjectArgsList');
|
||||
if (!container || !_wrapperConfig) return;
|
||||
|
||||
if (_wrapperConfig.injectArgs.length === 0) {
|
||||
container.innerHTML = '<div class="wrapper-items-empty">No arguments configured</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
_wrapperConfig.injectArgs.forEach((entry, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'wrapper-item';
|
||||
|
||||
const serverLabel = window.i18n ? window.i18n.t('settings.wrapperConditionServer') : 'Server Only';
|
||||
const alwaysLabel = window.i18n ? window.i18n.t('settings.wrapperConditionAlways') : 'Always';
|
||||
|
||||
item.innerHTML = `
|
||||
<span class="wrapper-item-text">${escapeHtml(entry.arg)}</span>
|
||||
<div class="wrapper-item-condition">
|
||||
<select data-index="${index}">
|
||||
<option value="server"${entry.condition === 'server' ? ' selected' : ''}>${serverLabel}</option>
|
||||
<option value="always"${entry.condition === 'always' ? ' selected' : ''}>${alwaysLabel}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="wrapper-item-delete" data-index="${index}" title="Remove">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
`;
|
||||
item.querySelector('select').addEventListener('change', (e) => updateArgCondition(index, e.target.value));
|
||||
item.querySelector('.wrapper-item-delete').addEventListener('click', () => removeInjectArg(index));
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async function addStripFlag() {
|
||||
const input = document.getElementById('wrapperAddFlagInput');
|
||||
if (!input || !_wrapperConfig) return;
|
||||
|
||||
const flag = input.value.trim();
|
||||
if (!flag) return;
|
||||
|
||||
if (_wrapperConfig.stripFlags.includes(flag)) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperFlagExists') : 'This flag is already in the list';
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
_wrapperConfig.stripFlags.push(flag);
|
||||
input.value = '';
|
||||
renderStripFlagsList();
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function removeStripFlag(index) {
|
||||
if (!_wrapperConfig) return;
|
||||
_wrapperConfig.stripFlags.splice(index, 1);
|
||||
renderStripFlagsList();
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function addInjectArg() {
|
||||
const input = document.getElementById('wrapperAddArgInput');
|
||||
const condSelect = document.getElementById('wrapperAddArgCondition');
|
||||
if (!input || !condSelect || !_wrapperConfig) return;
|
||||
|
||||
const arg = input.value.trim();
|
||||
if (!arg) return;
|
||||
|
||||
const exists = _wrapperConfig.injectArgs.some(e => e.arg === arg);
|
||||
if (exists) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperArgExists') : 'This argument is already in the list';
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
_wrapperConfig.injectArgs.push({ arg, condition: condSelect.value });
|
||||
input.value = '';
|
||||
renderInjectArgsList();
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function removeInjectArg(index) {
|
||||
if (!_wrapperConfig) return;
|
||||
_wrapperConfig.injectArgs.splice(index, 1);
|
||||
renderInjectArgsList();
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function updateArgCondition(index, condition) {
|
||||
if (!_wrapperConfig || !_wrapperConfig.injectArgs[index]) return;
|
||||
_wrapperConfig.injectArgs[index].condition = condition;
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function saveWrapperConfigToBackend() {
|
||||
try {
|
||||
const result = await window.electronAPI.saveWrapperConfig(_wrapperConfig);
|
||||
if (!result || !result.success) {
|
||||
throw new Error(result?.error || 'Save failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving wrapper config:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigSaveFailed') : 'Failed to save wrapper configuration';
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function setupWrapperEventListeners() {
|
||||
const addFlagBtn = document.getElementById('wrapperAddFlagBtn');
|
||||
const addFlagInput = document.getElementById('wrapperAddFlagInput');
|
||||
const addArgBtn = document.getElementById('wrapperAddArgBtn');
|
||||
const addArgInput = document.getElementById('wrapperAddArgInput');
|
||||
const restoreBtn = document.getElementById('wrapperRestoreDefaultsBtn');
|
||||
const previewToggle = document.getElementById('wrapperPreviewToggle');
|
||||
|
||||
if (addFlagBtn) addFlagBtn.addEventListener('click', addStripFlag);
|
||||
if (addFlagInput) addFlagInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addStripFlag(); });
|
||||
if (addArgBtn) addArgBtn.addEventListener('click', addInjectArg);
|
||||
if (addArgInput) addArgInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addInjectArg(); });
|
||||
|
||||
if (restoreBtn) {
|
||||
restoreBtn.addEventListener('click', () => {
|
||||
const message = window.i18n ? window.i18n.t('confirm.resetWrapperMessage') : 'Are you sure you want to restore defaults? Your custom changes will be lost.';
|
||||
const title = window.i18n ? window.i18n.t('confirm.resetWrapperTitle') : 'Restore Defaults';
|
||||
|
||||
showCustomConfirm(message, title, async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.resetWrapperConfig();
|
||||
if (result && result.success) {
|
||||
_wrapperConfig = result.config;
|
||||
renderStripFlagsList();
|
||||
renderInjectArgsList();
|
||||
await updateWrapperPreview();
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigReset') : 'Wrapper configuration restored to defaults';
|
||||
showNotification(msg, 'success');
|
||||
} else {
|
||||
throw new Error(result?.error || 'Reset failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resetting wrapper config:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigResetFailed') : 'Failed to restore wrapper configuration';
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (previewToggle) {
|
||||
previewToggle.addEventListener('click', toggleWrapperPreview);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleWrapperPreview() {
|
||||
const container = document.getElementById('wrapperPreviewContainer');
|
||||
const chevron = document.getElementById('wrapperPreviewChevron');
|
||||
if (!container) return;
|
||||
|
||||
_wrapperPreviewOpen = !_wrapperPreviewOpen;
|
||||
|
||||
if (_wrapperPreviewOpen) {
|
||||
container.style.display = 'block';
|
||||
if (chevron) chevron.classList.add('expanded');
|
||||
await updateWrapperPreview();
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
if (chevron) chevron.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateWrapperPreview() {
|
||||
if (!_wrapperPreviewOpen || !_wrapperConfig) return;
|
||||
|
||||
const previewEl = document.getElementById('wrapperPreviewContent');
|
||||
if (!previewEl) return;
|
||||
|
||||
try {
|
||||
const platform = await window.electronAPI.getCurrentPlatform();
|
||||
const script = await window.electronAPI.previewWrapperScript(_wrapperConfig, platform);
|
||||
previewEl.textContent = script;
|
||||
} catch (error) {
|
||||
previewEl.textContent = 'Error generating preview: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
58
GUI/js/ui.js
58
GUI/js/ui.js
@@ -79,12 +79,18 @@ function setupWindowControls() {
|
||||
const header = document.querySelector('.header');
|
||||
|
||||
const profileSelector = document.querySelector('.profile-selector');
|
||||
const identitySelector = document.querySelector('.identity-selector');
|
||||
|
||||
if (profileSelector) {
|
||||
profileSelector.style.pointerEvents = 'auto';
|
||||
profileSelector.style.zIndex = '10000';
|
||||
}
|
||||
|
||||
if (identitySelector) {
|
||||
identitySelector.style.pointerEvents = 'auto';
|
||||
identitySelector.style.zIndex = '10000';
|
||||
}
|
||||
|
||||
if (windowControls) {
|
||||
windowControls.style.pointerEvents = 'auto';
|
||||
windowControls.style.zIndex = '10000';
|
||||
@@ -98,6 +104,9 @@ function setupWindowControls() {
|
||||
if (profileSelector) {
|
||||
profileSelector.style.webkitAppRegion = 'no-drag';
|
||||
}
|
||||
if (identitySelector) {
|
||||
identitySelector.style.webkitAppRegion = 'no-drag';
|
||||
}
|
||||
}
|
||||
|
||||
if (window.electronAPI) {
|
||||
@@ -1103,9 +1112,56 @@ function getRetryContextMessage() {
|
||||
}
|
||||
|
||||
window.openDiscordExternal = function() {
|
||||
window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
|
||||
// window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
|
||||
window.electronAPI?.openExternal('https://chat.sanhost.net/invite/Tfz4jCK4');
|
||||
};
|
||||
|
||||
window.toggleMaximize = toggleMaximize;
|
||||
|
||||
// Global Escape key handler for closing popups/modals/dropdowns
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
|
||||
// Custom confirm dialogs handle their own Escape — skip if one is open
|
||||
if (document.querySelector('.custom-confirm-modal')) return;
|
||||
|
||||
// Close modals (highest priority)
|
||||
const profileModal = document.getElementById('profileManagerModal');
|
||||
if (profileModal && profileModal.style.display !== 'none') {
|
||||
if (window.closeProfileManager) window.closeProfileManager();
|
||||
return;
|
||||
}
|
||||
|
||||
const uuidModal = document.getElementById('uuidModal');
|
||||
if (uuidModal && uuidModal.style.display !== 'none') {
|
||||
if (window.closeUuidModal) window.closeUuidModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const discordModal = document.getElementById('discordPopupModal');
|
||||
if (discordModal && discordModal.style.display !== 'none') {
|
||||
discordModal.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const versionModal = document.getElementById('versionSelectModal');
|
||||
if (versionModal && versionModal.style.display !== 'none') {
|
||||
versionModal.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Close dropdowns (lower priority)
|
||||
const identityDropdown = document.getElementById('identityDropdown');
|
||||
if (identityDropdown && identityDropdown.classList.contains('show')) {
|
||||
identityDropdown.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
|
||||
const profileDropdown = document.getElementById('profileDropdown');
|
||||
if (profileDropdown && profileDropdown.classList.contains('show')) {
|
||||
profileDropdown.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', setupUI);
|
||||
|
||||
271
GUI/locales/ar-SA.json
Normal file
271
GUI/locales/ar-SA.json
Normal file
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"nav": {
|
||||
"play": "لعب",
|
||||
"mods": "المودات",
|
||||
"news": "الأخبار",
|
||||
"chat": "دردشة اللاعبين",
|
||||
"settings": "الإعدادات"
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "اللاعبون:",
|
||||
"manageProfiles": "إدارة",
|
||||
"manageIdentities": "إدارة",
|
||||
"identityTooltip": "اسم اللاعب ومعرّف UUID المستخدمان في اللعبة",
|
||||
"configTooltip": "إعدادات اللعبة: المودات، Java والذاكرة",
|
||||
"defaultProfile": "الافتراضي"
|
||||
},
|
||||
"install": {
|
||||
"title": "مشغل اللعب المجاني",
|
||||
"playerName": "اسم اللاعب",
|
||||
"playerNamePlaceholder": "أدخل اسمك",
|
||||
"gameBranch": "إصدار اللعبة",
|
||||
"releaseVersion": "إصدار نهائي (مستقر)",
|
||||
"preReleaseVersion": "إصدار تجريبي (تجريبي)",
|
||||
"customInstallation": "تثبيت مخصص",
|
||||
"installationFolder": "مجلد التثبيت",
|
||||
"pathPlaceholder": "الموقع الافتراضي",
|
||||
"browse": "تصفح",
|
||||
"installButton": "تثبيت HYTALE",
|
||||
"installing": "جاري التثبيت..."
|
||||
},
|
||||
"play": {
|
||||
"ready": "جاهز للعب",
|
||||
"subtitle": "شغل Hytale وابدأ المغامرة",
|
||||
"playButton": "لعب HYTALE",
|
||||
"latestNews": "آخر الأخبار",
|
||||
"viewAll": "عرض الكل",
|
||||
"checking": "جاري التحقق...",
|
||||
"play": "بدء"
|
||||
},
|
||||
"mods": {
|
||||
"searchPlaceholder": "البحث عن مودات...",
|
||||
"myMods": "موداتي",
|
||||
"previous": "السابق",
|
||||
"next": "التالي",
|
||||
"page": "صفحة",
|
||||
"of": "من",
|
||||
"modalTitle": "موداتي",
|
||||
"noModsFound": "لم يتم العثور على مودات",
|
||||
"noModsFoundDesc": "حاول تعديل معايير البحث",
|
||||
"noModsInstalled": "لا توجد مودات مثبتة",
|
||||
"noModsInstalledDesc": "أضف مودات من CurseForge أو استورد ملفات محلية",
|
||||
"view": "عرض",
|
||||
"install": "تثبيت",
|
||||
"installed": "مثبت",
|
||||
"enable": "تفعيل",
|
||||
"disable": "تعطيل",
|
||||
"active": "نشط",
|
||||
"disabled": "معطل",
|
||||
"delete": "حذف المود",
|
||||
"noDescription": "لا يوجد وصف متاح",
|
||||
"confirmDelete": "هل أنت متأكد أنك تريد حذف \"{name}\"؟",
|
||||
"confirmDeleteDesc": "لا يمكن التراجع عن هذا الإجراء.",
|
||||
"confirmDeletion": "تأكيد الحذف",
|
||||
"apiKeyRequired": "مطلوب مفتاح API",
|
||||
"apiKeyRequiredDesc": "مطلوب مفتاح CurseForge API لتصفح المودات"
|
||||
},
|
||||
"news": {
|
||||
"title": "كل الأخبار",
|
||||
"readMore": "اقرأ المزيد"
|
||||
},
|
||||
"chat": {
|
||||
"title": "دردشة اللاعبين",
|
||||
"pickColor": "اللون",
|
||||
"inputPlaceholder": "اكتب رسالتك...",
|
||||
"send": "إرسال",
|
||||
"online": "متصل",
|
||||
"charCounter": "{current}/{max}",
|
||||
"secureChat": "دردشة آمنة - الروابط محجوبة",
|
||||
"joinChat": "انضمام للدردشة",
|
||||
"chooseUsername": "اختر اسم مستخدم للانضمام إلى دردشة اللاعبين",
|
||||
"username": "اسم المستخدم",
|
||||
"usernamePlaceholder": "أدخل اسم المستخدم...",
|
||||
"usernameHint": "3-20 حرفاً، حروف، أرقام، و - و _ فقط",
|
||||
"joinButton": "انضمام",
|
||||
"colorModal": {
|
||||
"title": "تخصيص لون اسم المستخدم",
|
||||
"chooseSolid": "اختر لوناً ثابتاً:",
|
||||
"customColor": "لون مخصص:",
|
||||
"preview": "معاينة:",
|
||||
"previewUsername": "اسم المستخدم",
|
||||
"apply": "تطبيق اللون"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "الإعدادات",
|
||||
"java": "بيئة تشغيل جافا",
|
||||
"useCustomJava": "استخدام مسار جافا مخصص",
|
||||
"javaDescription": "تجاوز بيئة جافا المرفقة واستخدام تثبيت خاص بك",
|
||||
"javaPath": "مسار ملف جافا التنفيذي",
|
||||
"javaPathPlaceholder": "اختر مسار جافا...",
|
||||
"javaBrowse": "تصفح",
|
||||
"javaHint": "اختر مجلد تثبيت جافا (يدعم ويندوز، ماك، ولينكس)",
|
||||
"discord": "تكامل ديسكورد",
|
||||
"enableRPC": "تفعيل نشاط ديسكورد (Rich Presence)",
|
||||
"discordDescription": "إظهار نشاط المشغل الخاص بك على ديسكورد",
|
||||
"game": "خيارات اللعبة",
|
||||
"playerName": "اسم اللاعب",
|
||||
"playerNamePlaceholder": "أدخل اسم اللاعب",
|
||||
"playerNameHint": "سيتم استخدام هذا الاسم داخل اللعبة (1-16 حرفاً)",
|
||||
"openGameLocation": "فتح موقع اللعبة",
|
||||
"openGameLocationDesc": "فتح مجلد تثبيت اللعبة",
|
||||
"account": "إدارة UUID اللاعب",
|
||||
"currentUUID": "الـ UUID الحالي",
|
||||
"uuidPlaceholder": "جاري تحميل UUID...",
|
||||
"copyUUID": "نسخ UUID",
|
||||
"regenerateUUID": "إعادة إنشاء UUID",
|
||||
"uuidHint": "معرف اللاعب الفريد الخاص بك لهذا الاسم",
|
||||
"manageUUIDs": "إدارة جميع الـ UUIDs",
|
||||
"manageUUIDsDesc": "عرض وإدارة جميع معرفات اللاعبين",
|
||||
"language": "اللغة",
|
||||
"selectLanguage": "اختر اللغة",
|
||||
"repairGame": "إصلاح اللعبة",
|
||||
"reinstallGame": "إعادة تثبيت ملفات اللعبة (يحفظ البيانات)",
|
||||
"gpuPreference": "تفضيل معالج الرسوميات (GPU)",
|
||||
"gpuHint": "ميزة للمحمول فقط؛ اضبطها على Integrated إذا كنت تستخدم كمبيوتر مكتبي",
|
||||
"gpuAuto": "تلقائي",
|
||||
"gpuIntegrated": "مدمج",
|
||||
"gpuDedicated": "منفصل",
|
||||
"logs": "سجلات النظام",
|
||||
"logsCopy": "نسخ",
|
||||
"logsRefresh": "تحديث",
|
||||
"logsFolder": "فتح المجلد",
|
||||
"logsSend": "إرسال السجلات",
|
||||
"logsSending": "جارٍ الإرسال...",
|
||||
"logsSent": "تم الإرسال!",
|
||||
"logsSendFailed": "فشل",
|
||||
"logsSubmissionId": "معرف الإرسال",
|
||||
"logsShareId": "شارك هذا المعرف مع الدعم عند الإبلاغ عن المشاكل",
|
||||
"logsLoading": "جاري تحميل السجلات...",
|
||||
"closeLauncher": "سلوك المشغل",
|
||||
"closeOnStart": "إغلاق المشغل عند بدء اللعبة",
|
||||
"closeOnStartDescription": "إغلاق المشغل تلقائياً بعد تشغيل Hytale",
|
||||
"hwAccel": "تسريع الأجهزة (Hardware Acceleration)",
|
||||
"hwAccelDescription": "تفعيل تسريع الأجهزة للمشغل",
|
||||
"gameBranch": "فرع اللعبة",
|
||||
"branchRelease": "إصدار نهائي",
|
||||
"branchPreRelease": "إصدار تجريبي",
|
||||
"branchHint": "التبديل بين الإصدار المستقر والإصدار التجريبي",
|
||||
"branchWarning": "تغيير الفرع سيؤدي إلى تحميل وتثبيت نسخة مختلفة من اللعبة",
|
||||
"branchSwitching": "جاري التبديل إلى {branch}...",
|
||||
"branchSwitched": "تم التبديل إلى {branch} بنجاح!",
|
||||
"installRequired": "التثبيت مطلوب",
|
||||
"branchInstallConfirm": "سيتم تثبيت اللعبة لفرع {branch}. هل تريد الاستمرار؟"
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "إدارة UUID",
|
||||
"allPlayerUUIDs": "جميع معرفات UUID للاعبين",
|
||||
"addIdentity": "إضافة هوية",
|
||||
"usernamePlaceholder": "اسم المستخدم",
|
||||
"add": "إضافة",
|
||||
"cancel": "إلغاء",
|
||||
"advanced": "متقدم",
|
||||
"loadingUUIDs": "جاري تحميل الـ UUIDs...",
|
||||
"setCustomUUID": "تعيين UUID مخصص",
|
||||
"customPlaceholder": "أدخل UUID مخصص (الصيغة: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
"setUUID": "تعيين UUID",
|
||||
"warning": "تحذير: تعيين UUID مخصص سيغير هوية اللاعب الحالية",
|
||||
"copyTooltip": "نسخ UUID",
|
||||
"regenerateTooltip": "إنشاء UUID جديد"
|
||||
},
|
||||
"configurations": {
|
||||
"modalTitle": "إدارة التكوينات",
|
||||
"newProfilePlaceholder": "اسم التكوين الجديد",
|
||||
"createProfile": "إنشاء تكوين"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "انضم إلى مجتمعنا على ديسكورد!",
|
||||
"joinButton": "انضم إلى ديسكورد"
|
||||
},
|
||||
"common": {
|
||||
"confirm": "تأكيد",
|
||||
"cancel": "إلغاء",
|
||||
"save": "حفظ",
|
||||
"close": "إغلاق",
|
||||
"delete": "حذف",
|
||||
"edit": "تعديل",
|
||||
"loading": "جاري التحميل...",
|
||||
"apply": "تطبيق",
|
||||
"install": "تثبيت"
|
||||
},
|
||||
"notifications": {
|
||||
"gameDataNotFound": "خطأ: لم يتم العثور على بيانات اللعبة",
|
||||
"gameUpdatedSuccess": "تم تحديث اللعبة بنجاح! 🎉",
|
||||
"updateFailed": "فشل التحديث: {error}",
|
||||
"updateError": "خطأ في التحديث: {error}",
|
||||
"discordEnabled": "تم تفعيل نشاط ديسكورد",
|
||||
"discordDisabled": "تم تعطيل نشاط ديسكورد",
|
||||
"discordSaveFailed": "فشل حفظ إعدادات ديسكورد",
|
||||
"playerNameRequired": "يرجى إدخال اسم لاعب صالح",
|
||||
"playerNameSaved": "تم حفظ اسم اللاعب بنجاح",
|
||||
"playerNameSaveFailed": "فشل حفظ اسم اللاعب",
|
||||
"uuidCopied": "تم نسخ الـ UUID إلى الحافظة!",
|
||||
"uuidCopyFailed": "فشل نسخ الـ UUID",
|
||||
"uuidRegenNotAvailable": "إعادة إنشاء UUID غير متاحة",
|
||||
"uuidRegenFailed": "فشل إعادة إنشاء الـ UUID",
|
||||
"uuidGenerated": "تم إنشاء UUID جديد بنجاح!",
|
||||
"uuidGeneratedShort": "تم إنشاء UUID جديد!",
|
||||
"uuidGenerateFailed": "فشل إنشاء UUID جديد",
|
||||
"uuidRequired": "يرجى إدخال UUID",
|
||||
"uuidInvalidFormat": "صيغة UUID غير صالحة",
|
||||
"uuidSetFailed": "فشل تعيين الـ UUID المخصص",
|
||||
"uuidSetSuccess": "تم تعيين الـ UUID المخصص بنجاح!",
|
||||
"uuidDeleteFailed": "فشل حذف الـ UUID",
|
||||
"uuidDeleteSuccess": "تم حذف الـ UUID بنجاح!",
|
||||
"modsDownloading": "جاري تحميل {name}...",
|
||||
"modsTogglingMod": "جاري تبديل حالة المود...",
|
||||
"modsDeletingMod": "جاري حذف المود...",
|
||||
"modsLoadingMods": "جاري تحميل المودات من CurseForge...",
|
||||
"modsInstalledSuccess": "تم تثبيت {name} بنجاح! 🎉",
|
||||
"modsDeletedSuccess": "تم حذف {name} بنجاح",
|
||||
"modsDownloadFailed": "فشل تحميل المود: {error}",
|
||||
"modsToggleFailed": "فشل تبديل المود: {error}",
|
||||
"modsDeleteFailed": "فشل حذف المود: {error}",
|
||||
"modsModNotFound": "لم يتم العثور على معلومات المود",
|
||||
"hwAccelSaved": "تم حفظ إعداد تسريع الأجهزة",
|
||||
"hwAccelSaveFailed": "فشل حفظ إعداد تسريع الأجهزة",
|
||||
"noUsername": "لم يتم تهيئة اسم مستخدم. يرجى حفظ اسم المستخدم أولاً.",
|
||||
"identityAdded": "تمت إضافة الهوية بنجاح!",
|
||||
"identityAddFailed": "فشل في إضافة الهوية",
|
||||
"switchUsernameSuccess": "تم التبديل إلى المستخدم \"{username}\" بنجاح!",
|
||||
"switchUsernameFailed": "فشل تبديل اسم المستخدم",
|
||||
"playerNameTooLong": "يجب أن يكون اسم اللاعب 16 حرفاً أو أقل"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "تأكيد الإجراء",
|
||||
"regenerateUuidTitle": "إنشاء UUID جديد",
|
||||
"regenerateUuidMessage": "هل أنت متأكد أنك تريد إنشاء UUID جديد؟ سيؤدي ذلك إلى تغيير هوية اللاعب الخاصة بك.",
|
||||
"regenerateUuidButton": "إنشاء",
|
||||
"setCustomUuidTitle": "تعيين UUID مخصص",
|
||||
"setCustomUuidMessage": "هل أنت متأكد أنك تريد تعيين هذا الـ UUID المخصص؟ سيؤدي ذلك إلى تغيير هوية اللاعب الخاصة بك.",
|
||||
"setCustomUuidButton": "تعيين UUID",
|
||||
"deleteUuidTitle": "حذف UUID",
|
||||
"deleteUuidMessage": "هل أنت متأكد أنك تريد حذف الـ UUID الخاص بـ \"{username}\"؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"deleteUuidButton": "حذف",
|
||||
"uninstallGameTitle": "إلغاء تثبيت اللعبة",
|
||||
"uninstallGameMessage": "هل أنت متأكد أنك تريد إلغاء تثبيت Hytale؟ سيتم حذف جميع ملفات اللعبة.",
|
||||
"uninstallGameButton": "إلغاء التثبيت",
|
||||
"switchUsernameTitle": "تبديل الهوية",
|
||||
"switchUsernameMessage": "التبديل إلى اسم المستخدم \"{username}\"؟ سيؤدي هذا إلى تغيير هوية اللاعب الحالية.",
|
||||
"switchUsernameButton": "تبديل"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "جاري التهيئة...",
|
||||
"downloading": "جاري التحميل...",
|
||||
"installing": "جاري التثبيت...",
|
||||
"extracting": "جاري الاستخراج...",
|
||||
"verifying": "جاري التحقق...",
|
||||
"switchingProfile": "جاري تبديل التكوين...",
|
||||
"profileSwitched": "تم تبديل التكوين!",
|
||||
"startingGame": "جاري بدء اللعبة...",
|
||||
"launching": "جاري التشغيل...",
|
||||
"uninstallingGame": "جاري إلغاء تثبيت اللعبة...",
|
||||
"gameUninstalled": "تم إلغاء تثبيت اللعبة بنجاح!",
|
||||
"uninstallFailed": "فشل إلغاء التثبيت: {error}",
|
||||
"startingUpdate": "جاري بدء تحديث اللعبة الإجباري...",
|
||||
"installationComplete": "تم اكتمال التثبيت بنجاح!",
|
||||
"installationFailed": "فشل التثبيت: {error}",
|
||||
"installingGameFiles": "جاري تثبيت ملفات اللعبة...",
|
||||
"installComplete": "اكتمل التثبيت!"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Spieler:",
|
||||
"manageProfiles": "Profile verwalten",
|
||||
"manageProfiles": "Verwalten",
|
||||
"manageIdentities": "Verwalten",
|
||||
"identityTooltip": "Dein Spielername und UUID im Spiel",
|
||||
"configTooltip": "Spielkonfiguration: Mods, Java- und Speichereinstellungen",
|
||||
"defaultProfile": "Standard"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Kopieren",
|
||||
"logsRefresh": "Aktualisieren",
|
||||
"logsFolder": "Ordner öffnen",
|
||||
"logsSend": "Logs senden",
|
||||
"logsSending": "Senden...",
|
||||
"logsSent": "Gesendet!",
|
||||
"logsSendFailed": "Fehlgeschlagen",
|
||||
"logsSubmissionId": "Einreichungs-ID",
|
||||
"logsShareId": "Teilen Sie diese ID dem Support mit, wenn Sie Probleme melden",
|
||||
"logsLoading": "Protokolle werden geladen...",
|
||||
"closeLauncher": "Launcher-Verhalten",
|
||||
"closeOnStart": "Launcher beim Spielstart schließen",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "UUID-Verwaltung",
|
||||
"currentUserUUID": "Aktuelle Benutzer-UUID",
|
||||
"allPlayerUUIDs": "Alle Spieler-UUIDs",
|
||||
"generateNew": "Neue UUID generieren",
|
||||
"addIdentity": "Identität hinzufügen",
|
||||
"usernamePlaceholder": "Benutzername",
|
||||
"add": "Hinzufügen",
|
||||
"cancel": "Abbrechen",
|
||||
"advanced": "Erweitert",
|
||||
"loadingUUIDs": "UUIDs werden geladen...",
|
||||
"setCustomUUID": "Benutzerdefinierte UUID festlegen",
|
||||
"customPlaceholder": "Benutzerdefinierte UUID eingeben (Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"copyTooltip": "UUID kopieren",
|
||||
"regenerateTooltip": "Neue UUID generieren"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Profile verwalten",
|
||||
"newProfilePlaceholder": "Neuer Profilname",
|
||||
"createProfile": "Profil erstellen"
|
||||
"configurations": {
|
||||
"modalTitle": "Konfigurationen verwalten",
|
||||
"newProfilePlaceholder": "Neuer Konfigurationsname",
|
||||
"createProfile": "Konfiguration erstellen"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Tritt unserer Discord-Community bei!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}",
|
||||
"modsModNotFound": "Mod-Informationen nicht gefunden",
|
||||
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
|
||||
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden"
|
||||
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden",
|
||||
"noUsername": "Kein Benutzername konfiguriert. Bitte speichere zuerst deinen Benutzernamen.",
|
||||
"identityAdded": "Identität erfolgreich hinzugefügt!",
|
||||
"identityAddFailed": "Fehler beim Hinzufügen der Identität",
|
||||
"switchUsernameSuccess": "Erfolgreich zu \"{username}\" gewechselt!",
|
||||
"switchUsernameFailed": "Benutzername konnte nicht gewechselt werden",
|
||||
"playerNameTooLong": "Spielername darf maximal 16 Zeichen haben"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Aktion bestätigen",
|
||||
@@ -226,7 +244,10 @@
|
||||
"deleteUuidButton": "Löschen",
|
||||
"uninstallGameTitle": "Spiel deinstallieren",
|
||||
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
|
||||
"uninstallGameButton": "Deinstallieren"
|
||||
"uninstallGameButton": "Deinstallieren",
|
||||
"switchUsernameTitle": "Identität wechseln",
|
||||
"switchUsernameMessage": "Zu Benutzername \"{username}\" wechseln? Dies ändert deine aktuelle Spieleridentität.",
|
||||
"switchUsernameButton": "Wechseln"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Initialisiere...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Installiere...",
|
||||
"extracting": "Entpacke...",
|
||||
"verifying": "Überprüfe...",
|
||||
"switchingProfile": "Profil wird gewechselt...",
|
||||
"profileSwitched": "Profil gewechselt!",
|
||||
"switchingProfile": "Konfiguration wird gewechselt...",
|
||||
"profileSwitched": "Konfiguration gewechselt!",
|
||||
"startingGame": "Spiel wird gestartet...",
|
||||
"launching": "STARTET...",
|
||||
"uninstallingGame": "Spiel wird deinstalliert...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Players:",
|
||||
"manageProfiles": "Manage Profiles",
|
||||
"manageProfiles": "Manage",
|
||||
"manageIdentities": "Manage",
|
||||
"identityTooltip": "Your player name & UUID used in-game",
|
||||
"configTooltip": "Game config: mods, Java & memory settings",
|
||||
"defaultProfile": "Default"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Copy",
|
||||
"logsRefresh": "Refresh",
|
||||
"logsFolder": "Open Folder",
|
||||
"logsSend": "Send Logs",
|
||||
"logsSending": "Sending...",
|
||||
"logsSent": "Sent!",
|
||||
"logsSendFailed": "Failed",
|
||||
"logsSubmissionId": "Submission ID",
|
||||
"logsShareId": "Share this ID with support when reporting issues",
|
||||
"logsLoading": "Loading logs...",
|
||||
"closeLauncher": "Launcher Behavior",
|
||||
"closeOnStart": "Close Launcher on game start",
|
||||
@@ -141,25 +150,39 @@
|
||||
"branchSwitching": "Switching to {branch}...",
|
||||
"branchSwitched": "Switched to {branch} successfully!",
|
||||
"installRequired": "Installation Required",
|
||||
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?"
|
||||
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?",
|
||||
"wrapperConfig": "Java Wrapper Configuration",
|
||||
"wrapperConfigHint": "Configure how the Java wrapper handles JVM flags and arguments at launch time.",
|
||||
"wrapperStripFlags": "JVM Flags to Remove",
|
||||
"wrapperInjectArgs": "Arguments to Inject",
|
||||
"wrapperAddFlagPlaceholder": "e.g. -XX:+SomeFlag",
|
||||
"wrapperAddArgPlaceholder": "e.g. --some-flag",
|
||||
"wrapperAdd": "Add",
|
||||
"wrapperConditionServer": "Server Only",
|
||||
"wrapperConditionAlways": "Always",
|
||||
"wrapperRestoreDefaults": "Restore Defaults",
|
||||
"wrapperAdvancedPreview": "Advanced: Script Preview"
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "UUID Management",
|
||||
"currentUserUUID": "Current User UUID",
|
||||
"allPlayerUUIDs": "All Player UUIDs",
|
||||
"generateNew": "Generate New UUID",
|
||||
"addIdentity": "Add Identity",
|
||||
"usernamePlaceholder": "Username",
|
||||
"add": "Add",
|
||||
"cancel": "Cancel",
|
||||
"advanced": "Advanced",
|
||||
"loadingUUIDs": "Loading UUIDs...",
|
||||
"setCustomUUID": "Set Custom UUID",
|
||||
"setCustomUUID": "Set Custom UUID for Current User",
|
||||
"customPlaceholder": "Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
"setUUID": "Set UUID",
|
||||
"warning": "Warning: Setting a custom UUID will change your current player identity",
|
||||
"copyTooltip": "Copy UUID",
|
||||
"regenerateTooltip": "Generate New UUID"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Manage Profiles",
|
||||
"newProfilePlaceholder": "New Profile Name",
|
||||
"createProfile": "Create Profile"
|
||||
"configurations": {
|
||||
"modalTitle": "Manage Configurations",
|
||||
"newProfilePlaceholder": "New Configuration Name",
|
||||
"createProfile": "Create Configuration"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Join our Discord community!",
|
||||
@@ -211,7 +234,19 @@
|
||||
"modsDeleteFailed": "Failed to delete mod: {error}",
|
||||
"modsModNotFound": "Mod information not found",
|
||||
"hwAccelSaved": "Hardware acceleration setting saved",
|
||||
"hwAccelSaveFailed": "Failed to save hardware acceleration setting"
|
||||
"hwAccelSaveFailed": "Failed to save hardware acceleration setting",
|
||||
"noUsername": "No username configured. Please save your username first.",
|
||||
"identityAdded": "Identity added successfully!",
|
||||
"identityAddFailed": "Failed to add identity",
|
||||
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
|
||||
"switchUsernameFailed": "Failed to switch username",
|
||||
"playerNameTooLong": "Player name must be 16 characters or less",
|
||||
"wrapperConfigSaved": "Wrapper configuration saved",
|
||||
"wrapperConfigSaveFailed": "Failed to save wrapper configuration",
|
||||
"wrapperConfigReset": "Wrapper configuration restored to defaults",
|
||||
"wrapperConfigResetFailed": "Failed to restore wrapper configuration",
|
||||
"wrapperFlagExists": "This flag is already in the list",
|
||||
"wrapperArgExists": "This argument is already in the list"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Confirm action",
|
||||
@@ -226,7 +261,12 @@
|
||||
"deleteUuidButton": "Delete",
|
||||
"uninstallGameTitle": "Uninstall game",
|
||||
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
|
||||
"uninstallGameButton": "Uninstall"
|
||||
"uninstallGameButton": "Uninstall",
|
||||
"switchUsernameTitle": "Switch Identity",
|
||||
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
|
||||
"switchUsernameButton": "Switch",
|
||||
"resetWrapperTitle": "Restore Defaults",
|
||||
"resetWrapperMessage": "Are you sure you want to restore the default wrapper configuration? Your custom changes will be lost."
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Initializing...",
|
||||
@@ -234,8 +274,8 @@
|
||||
"installing": "Installing...",
|
||||
"extracting": "Extracting...",
|
||||
"verifying": "Verifying...",
|
||||
"switchingProfile": "Switching profile...",
|
||||
"profileSwitched": "Profile switched!",
|
||||
"switchingProfile": "Switching configuration...",
|
||||
"profileSwitched": "Configuration switched!",
|
||||
"startingGame": "Starting game...",
|
||||
"launching": "LAUNCHING...",
|
||||
"uninstallingGame": "Uninstalling game...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Jugadores:",
|
||||
"manageProfiles": "Gestionar Perfiles",
|
||||
"manageProfiles": "Gestionar",
|
||||
"manageIdentities": "Gestionar",
|
||||
"identityTooltip": "Tu nombre de jugador y UUID usados en el juego",
|
||||
"configTooltip": "Configuración del juego: mods, Java y memoria",
|
||||
"defaultProfile": "Predeterminado"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Copiar",
|
||||
"logsRefresh": "Actualizar",
|
||||
"logsFolder": "Abrir Carpeta",
|
||||
"logsSend": "Enviar logs",
|
||||
"logsSending": "Enviando...",
|
||||
"logsSent": "Enviado!",
|
||||
"logsSendFailed": "Fallido",
|
||||
"logsSubmissionId": "ID de envío",
|
||||
"logsShareId": "Comparte este ID con soporte al reportar problemas",
|
||||
"logsLoading": "Cargando registros...",
|
||||
"closeLauncher": "Comportamiento del Launcher",
|
||||
"closeOnStart": "Cerrar Launcher al iniciar el juego",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "Gestión de UUID",
|
||||
"currentUserUUID": "UUID del usuario actual",
|
||||
"allPlayerUUIDs": "Todos los UUIDs de jugadores",
|
||||
"generateNew": "Generar nuevo UUID",
|
||||
"addIdentity": "Añadir identidad",
|
||||
"usernamePlaceholder": "Nombre de usuario",
|
||||
"add": "Añadir",
|
||||
"cancel": "Cancelar",
|
||||
"advanced": "Avanzado",
|
||||
"loadingUUIDs": "Cargando UUIDs...",
|
||||
"setCustomUUID": "Establecer UUID personalizado",
|
||||
"customPlaceholder": "Ingresa un UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"copyTooltip": "Copiar UUID",
|
||||
"regenerateTooltip": "Generar nuevo UUID"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Gestionar perfiles",
|
||||
"newProfilePlaceholder": "Nombre del nuevo perfil",
|
||||
"createProfile": "Crear perfil"
|
||||
"configurations": {
|
||||
"modalTitle": "Gestionar Configuraciones",
|
||||
"newProfilePlaceholder": "Nombre de la nueva configuración",
|
||||
"createProfile": "Crear Configuración"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "¡Únete a nuestra comunidad de Discord!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"modsDeleteFailed": "Error al eliminar mod: {error}",
|
||||
"modsModNotFound": "Información del mod no encontrada",
|
||||
"hwAccelSaved": "Configuración de aceleración por hardware guardada",
|
||||
"hwAccelSaveFailed": "Error al guardar la configuración de aceleración por hardware"
|
||||
"hwAccelSaveFailed": "Error al guardar la configuración de aceleración por hardware",
|
||||
"noUsername": "No hay nombre de usuario configurado. Por favor, guarda tu nombre de usuario primero.",
|
||||
"identityAdded": "¡Identidad añadida con éxito!",
|
||||
"identityAddFailed": "Error al añadir identidad",
|
||||
"switchUsernameSuccess": "¡Cambiado a \"{username}\" con éxito!",
|
||||
"switchUsernameFailed": "Error al cambiar nombre de usuario",
|
||||
"playerNameTooLong": "El nombre del jugador debe tener 16 caracteres o menos"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Confirmar acción",
|
||||
@@ -226,7 +244,10 @@
|
||||
"deleteUuidButton": "Eliminar",
|
||||
"uninstallGameTitle": "Desinstalar juego",
|
||||
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
|
||||
"uninstallGameButton": "Desinstalar"
|
||||
"uninstallGameButton": "Desinstalar",
|
||||
"switchUsernameTitle": "Cambiar identidad",
|
||||
"switchUsernameMessage": "¿Cambiar al nombre de usuario \"{username}\"? Esto cambiará tu identidad de jugador actual.",
|
||||
"switchUsernameButton": "Cambiar"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Inicializando...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Instalando...",
|
||||
"extracting": "Extrayendo...",
|
||||
"verifying": "Verificando...",
|
||||
"switchingProfile": "Cambiando perfil...",
|
||||
"profileSwitched": "¡Perfil cambiado!",
|
||||
"switchingProfile": "Cambiando configuración...",
|
||||
"profileSwitched": "¡Configuración cambiada!",
|
||||
"startingGame": "Iniciando juego...",
|
||||
"launching": "INICIANDO...",
|
||||
"uninstallingGame": "Desinstalando juego...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Joueurs:",
|
||||
"manageProfiles": "Gérer les Profils",
|
||||
"manageProfiles": "Gérer",
|
||||
"manageIdentities": "Gérer",
|
||||
"identityTooltip": "Votre nom de joueur et UUID utilisés en jeu",
|
||||
"configTooltip": "Configuration du jeu : mods, Java et mémoire",
|
||||
"defaultProfile": "Par défaut"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Copier",
|
||||
"logsRefresh": "Actualiser",
|
||||
"logsFolder": "Ouvrir le Dossier",
|
||||
"logsSend": "Envoyer les logs",
|
||||
"logsSending": "Envoi...",
|
||||
"logsSent": "Envoyé !",
|
||||
"logsSendFailed": "Échoué",
|
||||
"logsSubmissionId": "ID de soumission",
|
||||
"logsShareId": "Partagez cet ID avec le support pour signaler des problèmes",
|
||||
"logsLoading": "Chargement des journaux...",
|
||||
"closeLauncher": "Comportement du Launcher",
|
||||
"closeOnStart": "Fermer le Launcher au démarrage du jeu",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "Gestion UUID",
|
||||
"currentUserUUID": "UUID Utilisateur Actuel",
|
||||
"allPlayerUUIDs": "Tous les UUIDs Joueurs",
|
||||
"generateNew": "Générer Nouvel UUID",
|
||||
"addIdentity": "Ajouter une identité",
|
||||
"usernamePlaceholder": "Nom d'utilisateur",
|
||||
"add": "Ajouter",
|
||||
"cancel": "Annuler",
|
||||
"advanced": "Avancé",
|
||||
"loadingUUIDs": "Chargement des UUIDs...",
|
||||
"setCustomUUID": "Définir UUID Personnalisé",
|
||||
"customPlaceholder": "Entrez UUID personnalisé (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"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"
|
||||
"configurations": {
|
||||
"modalTitle": "Gérer les Configurations",
|
||||
"newProfilePlaceholder": "Nom de la Nouvelle Configuration",
|
||||
"createProfile": "Créer une Configuration"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Rejoignez notre communauté Discord!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"modsDeleteFailed": "Échec de la suppression du mod: {error}",
|
||||
"modsModNotFound": "Informations du mod introuvables",
|
||||
"hwAccelSaved": "Paramètre d'accélération matérielle sauvegardé",
|
||||
"hwAccelSaveFailed": "Échec de la sauvegarde du paramètre d'accélération matérielle"
|
||||
"hwAccelSaveFailed": "Échec de la sauvegarde du paramètre d'accélération matérielle",
|
||||
"noUsername": "Aucun nom d'utilisateur configuré. Veuillez d'abord enregistrer votre nom d'utilisateur.",
|
||||
"identityAdded": "Identité ajoutée avec succès !",
|
||||
"identityAddFailed": "Échec de l'ajout de l'identité",
|
||||
"switchUsernameSuccess": "Basculé vers \"{username}\" avec succès!",
|
||||
"switchUsernameFailed": "Échec du changement de nom d'utilisateur",
|
||||
"playerNameTooLong": "Le nom du joueur doit comporter 16 caractères ou moins"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Confirmer l'action",
|
||||
@@ -226,7 +244,10 @@
|
||||
"deleteUuidButton": "Supprimer",
|
||||
"uninstallGameTitle": "Désinstaller le jeu",
|
||||
"uninstallGameMessage": "Êtes-vous sûr de vouloir désinstaller Hytale? Tous les fichiers du jeu seront supprimés.",
|
||||
"uninstallGameButton": "Désinstaller"
|
||||
"uninstallGameButton": "Désinstaller",
|
||||
"switchUsernameTitle": "Changer d'identité",
|
||||
"switchUsernameMessage": "Basculer vers le nom d'utilisateur \"{username}\"? Cela changera votre identité de joueur actuelle.",
|
||||
"switchUsernameButton": "Changer"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Initialisation...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Installation...",
|
||||
"extracting": "Extraction...",
|
||||
"verifying": "Vérification...",
|
||||
"switchingProfile": "Changement de profil...",
|
||||
"profileSwitched": "Profil changé!",
|
||||
"switchingProfile": "Changement de configuration...",
|
||||
"profileSwitched": "Configuration changée !",
|
||||
"startingGame": "Démarrage du jeu...",
|
||||
"launching": "LANCEMENT...",
|
||||
"uninstallingGame": "Désinstallation du jeu...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Pemain:",
|
||||
"manageProfiles": "Kelola Profil",
|
||||
"manageProfiles": "Kelola",
|
||||
"manageIdentities": "Kelola",
|
||||
"identityTooltip": "Nama pemain & UUID yang digunakan dalam game",
|
||||
"configTooltip": "Konfigurasi game: mod, Java & pengaturan memori",
|
||||
"defaultProfile": "Default"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Salin",
|
||||
"logsRefresh": "Segarkan",
|
||||
"logsFolder": "Buka Folder",
|
||||
"logsSend": "Kirim Log",
|
||||
"logsSending": "Mengirim...",
|
||||
"logsSent": "Terkirim!",
|
||||
"logsSendFailed": "Gagal",
|
||||
"logsSubmissionId": "ID Pengiriman",
|
||||
"logsShareId": "Bagikan ID ini ke dukungan saat melaporkan masalah",
|
||||
"logsLoading": "Memuat log...",
|
||||
"closeLauncher": "Perilaku Launcher",
|
||||
"closeOnStart": "Tutup launcher saat game dimulai",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "Manajemen UUID",
|
||||
"currentUserUUID": "UUID Pengguna Saat Ini",
|
||||
"allPlayerUUIDs": "Semua UUID Pemain",
|
||||
"generateNew": "Hasilkan UUID Baru",
|
||||
"addIdentity": "Tambah Identitas",
|
||||
"usernamePlaceholder": "Nama Pengguna",
|
||||
"add": "Tambah",
|
||||
"cancel": "Batal",
|
||||
"advanced": "Lanjutan",
|
||||
"loadingUUIDs": "Memuat UUID...",
|
||||
"setCustomUUID": "Setel UUID Kustom",
|
||||
"customPlaceholder": "Masukkan UUID kustom (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"copyTooltip": "Salin UUID",
|
||||
"regenerateTooltip": "Hasilkan UUID Baru"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Kelola Profil",
|
||||
"newProfilePlaceholder": "Nama Profil Baru",
|
||||
"createProfile": "Buat Profil"
|
||||
"configurations": {
|
||||
"modalTitle": "Kelola Konfigurasi",
|
||||
"newProfilePlaceholder": "Nama Konfigurasi Baru",
|
||||
"createProfile": "Buat Konfigurasi"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Gabung komunitas Discord kami!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"modsDeleteFailed": "Gagal menghapus mod: {error}",
|
||||
"modsModNotFound": "Informasi mod tidak ditemukan",
|
||||
"hwAccelSaved": "Pengaturan akselerasi perangkat keras disimpan",
|
||||
"hwAccelSaveFailed": "Gagal menyimpan pengaturan akselerasi perangkat keras"
|
||||
"hwAccelSaveFailed": "Gagal menyimpan pengaturan akselerasi perangkat keras",
|
||||
"noUsername": "Nama pengguna belum dikonfigurasi. Silakan simpan nama pengguna terlebih dahulu.",
|
||||
"identityAdded": "Identitas berhasil ditambahkan!",
|
||||
"identityAddFailed": "Gagal menambahkan identitas",
|
||||
"switchUsernameSuccess": "Berhasil beralih ke \"{username}\"!",
|
||||
"switchUsernameFailed": "Gagal beralih nama pengguna",
|
||||
"playerNameTooLong": "Nama pemain harus 16 karakter atau kurang"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Konfirmasi tindakan",
|
||||
@@ -226,7 +244,10 @@
|
||||
"deleteUuidButton": "Hapus",
|
||||
"uninstallGameTitle": "Hapus instalasi game",
|
||||
"uninstallGameMessage": "Apakah kamu yakin ingin menghapus instalasi Hytale? Semua file game akan dihapus.",
|
||||
"uninstallGameButton": "Hapus Instalasi"
|
||||
"uninstallGameButton": "Hapus Instalasi",
|
||||
"switchUsernameTitle": "Ganti Identitas",
|
||||
"switchUsernameMessage": "Beralih ke nama pengguna \"{username}\"? Ini akan mengubah identitas pemain saat ini.",
|
||||
"switchUsernameButton": "Ganti"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Menginisialisasi...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Menginstal...",
|
||||
"extracting": "Mengekstrak...",
|
||||
"verifying": "Memverifikasi...",
|
||||
"switchingProfile": "Beralih profil...",
|
||||
"profileSwitched": "Profil dialihkan!",
|
||||
"switchingProfile": "Beralih konfigurasi...",
|
||||
"profileSwitched": "Konfigurasi dialihkan!",
|
||||
"startingGame": "Memulai game...",
|
||||
"launching": "MELUNCURKAN...",
|
||||
"uninstallingGame": "Menghapus instalasi game...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Graczy:",
|
||||
"manageProfiles": "Zarządzaj Profilami",
|
||||
"manageProfiles": "Zarządzaj",
|
||||
"manageIdentities": "Zarządzaj",
|
||||
"identityTooltip": "Twoja nazwa gracza i UUID używane w grze",
|
||||
"configTooltip": "Konfiguracja gry: mody, Java i ustawienia pamięci",
|
||||
"defaultProfile": "Domyślny"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Kopiuj",
|
||||
"logsRefresh": "Odśwież",
|
||||
"logsFolder": "Otwórz Folder",
|
||||
"logsSend": "Wyślij logi",
|
||||
"logsSending": "Wysyłanie...",
|
||||
"logsSent": "Wysłano!",
|
||||
"logsSendFailed": "Błąd",
|
||||
"logsSubmissionId": "ID zgłoszenia",
|
||||
"logsShareId": "Udostępnij ten ID wsparciu technicznemu przy zgłaszaniu problemów",
|
||||
"logsLoading": "Ładowanie logów...",
|
||||
"closeLauncher": "Zachowanie Launchera",
|
||||
"closeOnStart": "Zamknij Launcher przy starcie gry",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "Zarządzanie UUID",
|
||||
"currentUserUUID": "Aktualny UUID użytkownika",
|
||||
"allPlayerUUIDs": "Wszystkie identyfikatory UUID graczy",
|
||||
"generateNew": "Wygeneruj nowy UUID",
|
||||
"addIdentity": "Dodaj tożsamość",
|
||||
"usernamePlaceholder": "Nazwa użytkownika",
|
||||
"add": "Dodaj",
|
||||
"cancel": "Anuluj",
|
||||
"advanced": "Zaawansowane",
|
||||
"loadingUUIDs": "Ładowanie UUID...",
|
||||
"setCustomUUID": "Ustaw niestandardowy UUID",
|
||||
"customPlaceholder": "Wprowadź niestandardowy UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"copyTooltip": "Kopiuj UUID",
|
||||
"regenerateTooltip": "Wygeneruj nowy UUID"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Zarządzaj Profilami",
|
||||
"newProfilePlaceholder": "Nowa Nazwa Profilu",
|
||||
"createProfile": "Utwórz Profil"
|
||||
"configurations": {
|
||||
"modalTitle": "Zarządzaj Konfiguracjami",
|
||||
"newProfilePlaceholder": "Nazwa Nowej Konfiguracji",
|
||||
"createProfile": "Utwórz Konfigurację"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Dołącz do naszej społeczności Discord!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"modsDeleteFailed": "Nie udało się usunąć moda: {error}",
|
||||
"modsModNotFound": "Nie znaleziono informacji o modzie",
|
||||
"hwAccelSaved": "Zapisano ustawienie przyspieszenia sprzętowego",
|
||||
"hwAccelSaveFailed": "Nie udało się zapisać ustawienia przyspieszenia sprzętowego"
|
||||
"hwAccelSaveFailed": "Nie udało się zapisać ustawienia przyspieszenia sprzętowego",
|
||||
"noUsername": "Nie skonfigurowano nazwy użytkownika. Najpierw zapisz swoją nazwę użytkownika.",
|
||||
"identityAdded": "Tożsamość dodana pomyślnie!",
|
||||
"identityAddFailed": "Nie udało się dodać tożsamości",
|
||||
"switchUsernameSuccess": "Pomyślnie przełączono na \"{username}\"!",
|
||||
"switchUsernameFailed": "Nie udało się przełączyć nazwy użytkownika",
|
||||
"playerNameTooLong": "Nazwa gracza musi mieć 16 znaków lub mniej"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Potwierdź działanie",
|
||||
@@ -226,7 +244,10 @@
|
||||
"deleteUuidButton": "Usuń",
|
||||
"uninstallGameTitle": "Odinstaluj grę",
|
||||
"uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.",
|
||||
"uninstallGameButton": "Odinstaluj"
|
||||
"uninstallGameButton": "Odinstaluj",
|
||||
"switchUsernameTitle": "Zmień tożsamość",
|
||||
"switchUsernameMessage": "Przełączyć na nazwę użytkownika \"{username}\"? To zmieni Twoją aktualną tożsamość gracza.",
|
||||
"switchUsernameButton": "Przełącz"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Inicjalizacja...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Instalowanie...",
|
||||
"extracting": "Ekstraktowanie...",
|
||||
"verifying": "Weryfikowanie...",
|
||||
"switchingProfile": "Przełączanie profilu...",
|
||||
"profileSwitched": "Profil zmieniony!",
|
||||
"switchingProfile": "Przełączanie konfiguracji...",
|
||||
"profileSwitched": "Konfiguracja zmieniona!",
|
||||
"startingGame": "Uruchamianie gry...",
|
||||
"launching": "URUCHAMIANIE...",
|
||||
"uninstallingGame": "Odinstalowywanie gry...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Jogadores:",
|
||||
"manageProfiles": "Gerenciar Perfis",
|
||||
"manageProfiles": "Gerenciar",
|
||||
"manageIdentities": "Gerenciar",
|
||||
"identityTooltip": "Seu nome de jogador e UUID usados no jogo",
|
||||
"configTooltip": "Configuração do jogo: mods, Java e memória",
|
||||
"defaultProfile": "Padrão"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Copiar",
|
||||
"logsRefresh": "Atualizar",
|
||||
"logsFolder": "Abrir Pasta",
|
||||
"logsSend": "Enviar logs",
|
||||
"logsSending": "Enviando...",
|
||||
"logsSent": "Enviado!",
|
||||
"logsSendFailed": "Falhou",
|
||||
"logsSubmissionId": "ID de envio",
|
||||
"logsShareId": "Compartilhe este ID com o suporte ao relatar problemas",
|
||||
"logsLoading": "Carregando registros...",
|
||||
"closeLauncher": "Comportamento do Lançador",
|
||||
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "Gerenciamento de UUID",
|
||||
"currentUserUUID": "UUID do usuário atual",
|
||||
"allPlayerUUIDs": "Todos os UUIDs de jogadores",
|
||||
"generateNew": "Gerar novo UUID",
|
||||
"addIdentity": "Adicionar identidade",
|
||||
"usernamePlaceholder": "Nome de usuário",
|
||||
"add": "Adicionar",
|
||||
"cancel": "Cancelar",
|
||||
"advanced": "Avançado",
|
||||
"loadingUUIDs": "Carregando UUIDs...",
|
||||
"setCustomUUID": "Definir UUID personalizado",
|
||||
"customPlaceholder": "Digite um UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"copyTooltip": "Copiar UUID",
|
||||
"regenerateTooltip": "Gerar novo UUID"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Gerenciar perfis",
|
||||
"newProfilePlaceholder": "Nome do novo perfil",
|
||||
"createProfile": "Criar perfil"
|
||||
"configurations": {
|
||||
"modalTitle": "Gerenciar Configurações",
|
||||
"newProfilePlaceholder": "Nome da Nova Configuração",
|
||||
"createProfile": "Criar Configuração"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Junte-se à nossa comunidade do Discord!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"modsDeleteFailed": "Falha ao excluir mod: {error}",
|
||||
"modsModNotFound": "Informações do mod não encontradas",
|
||||
"hwAccelSaved": "Configuração de aceleração de hardware salva",
|
||||
"hwAccelSaveFailed": "Falha ao salvar configuração de aceleração de hardware"
|
||||
"hwAccelSaveFailed": "Falha ao salvar configuração de aceleração de hardware",
|
||||
"noUsername": "Nenhum nome de usuário configurado. Por favor, salve seu nome de usuário primeiro.",
|
||||
"identityAdded": "Identidade adicionada com sucesso!",
|
||||
"identityAddFailed": "Falha ao adicionar identidade",
|
||||
"switchUsernameSuccess": "Alterado para \"{username}\" com sucesso!",
|
||||
"switchUsernameFailed": "Falha ao trocar nome de usuário",
|
||||
"playerNameTooLong": "O nome do jogador deve ter 16 caracteres ou menos"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Confirmar ação",
|
||||
@@ -226,7 +244,10 @@
|
||||
"deleteUuidButton": "Excluir",
|
||||
"uninstallGameTitle": "Desinstalar jogo",
|
||||
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
|
||||
"uninstallGameButton": "Desinstalar"
|
||||
"uninstallGameButton": "Desinstalar",
|
||||
"switchUsernameTitle": "Trocar Identidade",
|
||||
"switchUsernameMessage": "Trocar para o nome de usuário \"{username}\"? Isso mudará sua identidade de jogador atual.",
|
||||
"switchUsernameButton": "Trocar"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Inicializando...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Instalando...",
|
||||
"extracting": "Extraindo...",
|
||||
"verifying": "Verificando...",
|
||||
"switchingProfile": "Alternando perfil...",
|
||||
"profileSwitched": "Perfil alternado!",
|
||||
"switchingProfile": "Alternando configuração...",
|
||||
"profileSwitched": "Configuração alternada!",
|
||||
"startingGame": "Iniciando jogo...",
|
||||
"launching": "INICIANDO...",
|
||||
"uninstallingGame": "Desinstalando jogo...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Игроки:",
|
||||
"manageProfiles": "Управлять профилями:",
|
||||
"manageProfiles": "Управление",
|
||||
"manageIdentities": "Управление",
|
||||
"identityTooltip": "Ваш игровой ник и UUID, используемые в игре",
|
||||
"configTooltip": "Конфигурация игры: моды, Java и настройки памяти",
|
||||
"defaultProfile": "По умолчанию"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Копировать",
|
||||
"logsRefresh": "Обновить",
|
||||
"logsFolder": "Открыть папку",
|
||||
"logsSend": "Отправить логи",
|
||||
"logsSending": "Отправка...",
|
||||
"logsSent": "Отправлено!",
|
||||
"logsSendFailed": "Ошибка",
|
||||
"logsSubmissionId": "ID отправки",
|
||||
"logsShareId": "Поделитесь этим ID с поддержкой при обращении",
|
||||
"logsLoading": "Загрузка логов...",
|
||||
"closeLauncher": "Поведение лаунчера",
|
||||
"closeOnStart": "Закрыть лаунчер при старте игры",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "Управление UUID",
|
||||
"currentUserUUID": "UUID текущего пользователя",
|
||||
"allPlayerUUIDs": "UUID всех игроков",
|
||||
"generateNew": "Сгенерировать новый UUID",
|
||||
"addIdentity": "Добавить личность",
|
||||
"usernamePlaceholder": "Имя пользователя",
|
||||
"add": "Добавить",
|
||||
"cancel": "Отмена",
|
||||
"advanced": "Дополнительно",
|
||||
"loadingUUIDs": "Загрузка UUID...",
|
||||
"setCustomUUID": "Установить кастомный UUID",
|
||||
"customPlaceholder": "Ввести кастомный UUID (форматы: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"copyTooltip": "Скопировать UUID",
|
||||
"regenerateTooltip": "Сгенерировать новый UUID"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Управление профилями",
|
||||
"newProfilePlaceholder": "Новое имя профиля",
|
||||
"createProfile": "Создать профиль"
|
||||
"configurations": {
|
||||
"modalTitle": "Управление конфигурациями",
|
||||
"newProfilePlaceholder": "Название новой конфигурации",
|
||||
"createProfile": "Создать конфигурацию"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Присоединитесь к нашему сообществу в Discord!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"modsDeleteFailed": "Не получилось удалить мод: {error}",
|
||||
"modsModNotFound": "Информация по моду не найдена",
|
||||
"hwAccelSaved": "Настройка аппаратного ускорения сохранена!",
|
||||
"hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения"
|
||||
"hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения",
|
||||
"noUsername": "Имя пользователя не настроено. Пожалуйста, сначала сохраните имя пользователя.",
|
||||
"identityAdded": "Личность успешно добавлена!",
|
||||
"identityAddFailed": "Не удалось добавить личность",
|
||||
"switchUsernameSuccess": "Успешно переключено на \"{username}\"!",
|
||||
"switchUsernameFailed": "Не удалось переключить имя пользователя",
|
||||
"playerNameTooLong": "Имя игрока должно быть не более 16 символов"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Подтвердить действие",
|
||||
@@ -226,7 +244,10 @@
|
||||
"deleteUuidButton": "Удалить",
|
||||
"uninstallGameTitle": "Удалить игру",
|
||||
"uninstallGameMessage": "Вы уверены, что хотите удалить Hytale? Все данные игры будут безвозвратно удалены!",
|
||||
"uninstallGameButton": "Удалить"
|
||||
"uninstallGameButton": "Удалить",
|
||||
"switchUsernameTitle": "Сменить личность",
|
||||
"switchUsernameMessage": "Переключиться на имя пользователя \"{username}\"? Это изменит вашу текущую личность игрока.",
|
||||
"switchUsernameButton": "Переключить"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Инициализация...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Установка...",
|
||||
"extracting": "Извлечение...",
|
||||
"verifying": "Проверка...",
|
||||
"switchingProfile": "Смена профиля...",
|
||||
"profileSwitched": "Профиль сменён!",
|
||||
"switchingProfile": "Смена конфигурации...",
|
||||
"profileSwitched": "Конфигурация изменена!",
|
||||
"startingGame": "Запуск игры...",
|
||||
"launching": "ЗАПУСК...",
|
||||
"uninstallingGame": "Удаление игры...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Spelare:",
|
||||
"manageProfiles": "Hantera profiler",
|
||||
"manageProfiles": "Hantera",
|
||||
"manageIdentities": "Hantera",
|
||||
"identityTooltip": "Ditt spelarnamn och UUID som används i spelet",
|
||||
"configTooltip": "Spelkonfiguration: moddar, Java- och minnesinställningar",
|
||||
"defaultProfile": "Standard"
|
||||
},
|
||||
"install": {
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Kopiera",
|
||||
"logsRefresh": "Uppdatera",
|
||||
"logsFolder": "Öppna mapp",
|
||||
"logsSend": "Skicka loggar",
|
||||
"logsSending": "Skickar...",
|
||||
"logsSent": "Skickat!",
|
||||
"logsSendFailed": "Misslyckades",
|
||||
"logsSubmissionId": "Inlämnings-ID",
|
||||
"logsShareId": "Dela detta ID med support vid felanmälan",
|
||||
"logsLoading": "Laddar loggar...",
|
||||
"closeLauncher": "Launcher-beteende",
|
||||
"closeOnStart": "Stäng launcher vid spelstart",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "UUID-hantering",
|
||||
"currentUserUUID": "Nuvarande användar-UUID",
|
||||
"allPlayerUUIDs": "Alla spelare-UUID:er",
|
||||
"generateNew": "Generera ny UUID",
|
||||
"addIdentity": "Lägg till identitet",
|
||||
"usernamePlaceholder": "Användarnamn",
|
||||
"add": "Lägg till",
|
||||
"cancel": "Avbryt",
|
||||
"advanced": "Avancerat",
|
||||
"loadingUUIDs": "Laddar UUID:er...",
|
||||
"setCustomUUID": "Ange anpassad UUID",
|
||||
"customPlaceholder": "Ange anpassad UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"copyTooltip": "Kopiera UUID",
|
||||
"regenerateTooltip": "Generera ny UUID"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Hantera profiler",
|
||||
"newProfilePlaceholder": "Nytt profilnamn",
|
||||
"createProfile": "Skapa profil"
|
||||
"configurations": {
|
||||
"modalTitle": "Hantera konfigurationer",
|
||||
"newProfilePlaceholder": "Nytt konfigurationsnamn",
|
||||
"createProfile": "Skapa konfiguration"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Gå med i vår Discord-gemenskap!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"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"
|
||||
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning",
|
||||
"noUsername": "Inget användarnamn konfigurerat. Vänligen spara ditt användarnamn först.",
|
||||
"identityAdded": "Identitet tillagd!",
|
||||
"identityAddFailed": "Kunde inte lägga till identitet",
|
||||
"switchUsernameSuccess": "Bytte till \"{username}\" framgångsrikt!",
|
||||
"switchUsernameFailed": "Misslyckades med att byta användarnamn",
|
||||
"playerNameTooLong": "Spelarnamnet måste vara 16 tecken eller mindre"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Bekräfta åtgärd",
|
||||
@@ -226,7 +244,10 @@
|
||||
"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"
|
||||
"uninstallGameButton": "Avinstallera",
|
||||
"switchUsernameTitle": "Byt identitet",
|
||||
"switchUsernameMessage": "Byta till användarnamn \"{username}\"? Detta kommer att ändra din nuvarande spelaridentitet.",
|
||||
"switchUsernameButton": "Byt"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Initierar...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Installerar...",
|
||||
"extracting": "Extraherar...",
|
||||
"verifying": "Verifierar...",
|
||||
"switchingProfile": "Byter profil...",
|
||||
"profileSwitched": "Profil bytt!",
|
||||
"switchingProfile": "Byter konfiguration...",
|
||||
"profileSwitched": "Konfiguration bytt!",
|
||||
"startingGame": "Startar spel...",
|
||||
"launching": "STARTAR...",
|
||||
"uninstallingGame": "Avinstallerar spel...",
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Oyuncular:",
|
||||
"manageProfiles": "Profilleri Yönet",
|
||||
"manageProfiles": "Yönet",
|
||||
"manageIdentities": "Yönet",
|
||||
"identityTooltip": "Oyun içinde kullanılan oyuncu adınız ve UUID'niz",
|
||||
"configTooltip": "Oyun yapılandırması: modlar, Java ve bellek ayarları",
|
||||
"defaultProfile": "Varsayılan"
|
||||
},
|
||||
"install": {
|
||||
@@ -22,13 +25,13 @@
|
||||
"installationFolder": "Kurulum Klasörü",
|
||||
"pathPlaceholder": "Varsayılan konum",
|
||||
"browse": "Gözat",
|
||||
"installButton": "HYTALE KURU",
|
||||
"installButton": "HYTALE KUR",
|
||||
"installing": "KURULUYOR..."
|
||||
},
|
||||
"play": {
|
||||
"ready": "OYNAMAYA HAZIR",
|
||||
"subtitle": "Hytale'i başlat ve maceraya başla",
|
||||
"playButton": "HYTALE'YI OYNA",
|
||||
"subtitle": "Hytale'ı başlat ve maceraya başla",
|
||||
"playButton": "HYTALE'I OYNA",
|
||||
"latestNews": "SON HABERLER",
|
||||
"viewAll": "HEPSINI GÖR",
|
||||
"checking": "KONTROL EDİLİYOR...",
|
||||
@@ -47,13 +50,13 @@
|
||||
"noModsInstalled": "Hiçbir Mod Kurulu Değil",
|
||||
"noModsInstalledDesc": "CurseForge'dan modlar ekleyin veya yerel dosyalar içe aktarın",
|
||||
"view": "GÖR",
|
||||
"install": "KURU",
|
||||
"install": "KUR",
|
||||
"installed": "KURULU",
|
||||
"enable": "ETKİNLEŞTİR",
|
||||
"disable": "DEĞİ",
|
||||
"enable": "AÇ",
|
||||
"disable": "KAPAT",
|
||||
"active": "AKTİF",
|
||||
"disabled": "DEĞİ",
|
||||
"delete": "Modı sil",
|
||||
"disabled": "DEVREDIŞI",
|
||||
"delete": "Modu sil",
|
||||
"noDescription": "Açıklama yok",
|
||||
"confirmDelete": "\"{name}\" öğesini silmek istediğinizden emin misiniz?",
|
||||
"confirmDeleteDesc": "Bu işlem geri alınamaz.",
|
||||
@@ -67,7 +70,7 @@
|
||||
},
|
||||
"chat": {
|
||||
"title": "OYUNCU SOHBETI",
|
||||
"pickColor": "Renk",
|
||||
"pickColor": "Renk Seç",
|
||||
"inputPlaceholder": "Mesajınızı yazın...",
|
||||
"send": "Gönder",
|
||||
"online": "çevrimiçi",
|
||||
@@ -116,7 +119,7 @@
|
||||
"manageUUIDsDesc": "Tüm oyuncu UUID'lerini görüntüleyin ve yönetin",
|
||||
"language": "Dil",
|
||||
"selectLanguage": "Dil Seçin",
|
||||
"repairGame": "Oyunu Onarı",
|
||||
"repairGame": "Oyunu Düzelt",
|
||||
"reinstallGame": "Oyun dosyalarını yeniden kur (veri korur)",
|
||||
"gpuPreference": "GPU Tercihi",
|
||||
"gpuHint": "Sadece dizüstü bilgisayarlarda bulunan bir özellik; PC'de kullanılıyorsa Entegre olarak ayarlayın.",
|
||||
@@ -127,6 +130,12 @@
|
||||
"logsCopy": "Kopyala",
|
||||
"logsRefresh": "Yenile",
|
||||
"logsFolder": "Klasörü Aç",
|
||||
"logsSend": "Logları Gönder",
|
||||
"logsSending": "Gönderiliyor...",
|
||||
"logsSent": "Gönderildi!",
|
||||
"logsSendFailed": "Başarısız",
|
||||
"logsSubmissionId": "Gönderim ID",
|
||||
"logsShareId": "Sorun bildirirken bu ID'yi destekle paylaşın",
|
||||
"logsLoading": "Loglar yükleniyor...",
|
||||
"closeLauncher": "Başlatıcı Davranışı",
|
||||
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
|
||||
@@ -145,9 +154,12 @@
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "UUID Yönetimi",
|
||||
"currentUserUUID": "Geçerli Kullanıcı UUID",
|
||||
"allPlayerUUIDs": "Tüm Oyuncu UUID'leri",
|
||||
"generateNew": "Yeni UUID Oluştur",
|
||||
"addIdentity": "Kimlik Ekle",
|
||||
"usernamePlaceholder": "Kullanıcı Adı",
|
||||
"add": "Ekle",
|
||||
"cancel": "İptal",
|
||||
"advanced": "Gelişmiş",
|
||||
"loadingUUIDs": "UUID'ler yükleniyor...",
|
||||
"setCustomUUID": "Özel UUID Ayarla",
|
||||
"customPlaceholder": "Özel UUID girin (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
@@ -156,10 +168,10 @@
|
||||
"copyTooltip": "UUID'yi Kopyala",
|
||||
"regenerateTooltip": "Yeni UUID Oluştur"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Profilleri Yönet",
|
||||
"newProfilePlaceholder": "Yeni Profil Adı",
|
||||
"createProfile": "Profil Oluştur"
|
||||
"configurations": {
|
||||
"modalTitle": "Yapılandırmaları Yönet",
|
||||
"newProfilePlaceholder": "Yeni Yapılandırma Adı",
|
||||
"createProfile": "Yapılandırma Oluştur"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Discord topluluğumuza katılın!",
|
||||
@@ -211,7 +223,13 @@
|
||||
"modsDeleteFailed": "Mod silinemedi: {error}",
|
||||
"modsModNotFound": "Mod bilgileri bulunamadı",
|
||||
"hwAccelSaved": "Donanım hızlandırma ayarı kaydedildi",
|
||||
"hwAccelSaveFailed": "Donanım hızlandırma ayarı kaydedilemedi"
|
||||
"hwAccelSaveFailed": "Donanım hızlandırma ayarı kaydedilemedi",
|
||||
"noUsername": "Kullanıcı adı yapılandırılmadı. Lütfen önce kullanıcı adınızı kaydedin.",
|
||||
"identityAdded": "Kimlik başarıyla eklendi!",
|
||||
"identityAddFailed": "Kimlik eklenemedi",
|
||||
"switchUsernameSuccess": "\"{username}\" adına başarıyla geçildi!",
|
||||
"switchUsernameFailed": "Kullanıcı adı değiştirilemedi",
|
||||
"playerNameTooLong": "Oyuncu adı 16 karakter veya daha az olmalıdır"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Eylemi onayla",
|
||||
@@ -226,7 +244,10 @@
|
||||
"deleteUuidButton": "Sil",
|
||||
"uninstallGameTitle": "Oyunu kaldır",
|
||||
"uninstallGameMessage": "Hytale'yi kaldırmak istediğinizden emin misiniz? Tüm oyun dosyaları silinecektir.",
|
||||
"uninstallGameButton": "Kaldır"
|
||||
"uninstallGameButton": "Kaldır",
|
||||
"switchUsernameTitle": "Kimlik Değiştir",
|
||||
"switchUsernameMessage": "\"{username}\" kullanıcı adına geçilsin mi? Bu mevcut oyuncu kimliğinizi değiştirecektir.",
|
||||
"switchUsernameButton": "Değiştir"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Başlatılıyor...",
|
||||
@@ -234,8 +255,8 @@
|
||||
"installing": "Kuruluyur...",
|
||||
"extracting": "Ayıklanıyor...",
|
||||
"verifying": "Doğrulanıyor...",
|
||||
"switchingProfile": "Profil değiştiriliyor...",
|
||||
"profileSwitched": "Profil değiştirildi!",
|
||||
"switchingProfile": "Yapılandırma değiştiriliyor...",
|
||||
"profileSwitched": "Yapılandırma değiştirildi!",
|
||||
"startingGame": "Oyun başlatılıyor...",
|
||||
"launching": "BAŞLATILIYOR...",
|
||||
"uninstallingGame": "Oyun kaldırılıyor...",
|
||||
@@ -247,4 +268,5 @@
|
||||
"installingGameFiles": "Oyun dosyaları kuruluyor...",
|
||||
"installComplete": "Kurulum tamamlandı!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
209
GUI/style-RTL.css
Normal file
209
GUI/style-RTL.css
Normal file
@@ -0,0 +1,209 @@
|
||||
body.rtl {
|
||||
direction: rtl;
|
||||
font-family: 'Noto Sans Arabic', 'Space Grotesk', sans-serif;
|
||||
}
|
||||
|
||||
body.rtl .sidebar {
|
||||
right: 0;
|
||||
left: auto;
|
||||
border-right: none;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body.rtl .nav-item.active::before {
|
||||
right: -8px;
|
||||
left: auto;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
body.rtl .nav-tooltip {
|
||||
right: 100%;
|
||||
left: auto;
|
||||
margin-right: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
body.rtl .nav-item:hover .nav-tooltip {
|
||||
transform: translateX(-8px);
|
||||
}
|
||||
|
||||
|
||||
body.rtl .main-content {
|
||||
margin-right: 80px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Header Layout*/
|
||||
|
||||
body.rtl .players-counter {
|
||||
order: 2;
|
||||
margin-left: 1.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
body.rtl .profile-selector {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
body.rtl .window-controls {
|
||||
order: 3;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
body.rtl .profile-dropdown {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body.rtl .form-group {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.rtl .radio-label,
|
||||
body.rtl .checkbox-group {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
body.rtl .form-input {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
|
||||
body.rtl .mods-pagination {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
body.rtl .pagination-btn:first-child i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
body.rtl .pagination-btn:last-child i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* UUID Display */
|
||||
|
||||
body.rtl .uuid-display-container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
body.rtl .uuid-btn {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
body.rtl .uuid-input {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
|
||||
body.rtl .segmented-control {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* Mod Grid Layout */
|
||||
body.rtl .mods-search {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.rtl .mods-search-container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
body.rtl .mods-actions {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
body.rtl .mod-card {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
body.rtl .installed-mod-card {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
body.rtl .installed-mod-card .mod-info {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.rtl .mods-header {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
body.rtl .news-section .news-header {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* Settings Layout */
|
||||
|
||||
body.rtl .settings-option {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.rtl .settings-input-group {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.rtl .settings-input {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
body.rtl .settings-section-title i {
|
||||
margin-right: 0;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
body.rtl .settings-hint i {
|
||||
margin-right: 0;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
body.rtl .custom-options,
|
||||
body.rtl .custom-java-options {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.rtl .checkbox-content {
|
||||
margin-right: 2rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
body.rtl .btn-content {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Icons & Transformations */
|
||||
|
||||
body.rtl .news-title i {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
body.rtl .uuid-modal-title i {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
body.rtl .mods-modal-title i {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
body.rtl .view-all-btn i,
|
||||
body.rtl .sidebar-nav div i,
|
||||
body.rtl .logs-header i,
|
||||
body.rtl .home-play-button i {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
body.rtl .play-title i {
|
||||
transform: scaleX(-1);
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
body.rtl .logs-terminal {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body.rtl .version-display-bottom {
|
||||
right: auto;
|
||||
left: 1rem;
|
||||
}
|
||||
571
GUI/style.css
571
GUI/style.css
@@ -873,6 +873,22 @@ body {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.logs-action-btn.logs-send-btn {
|
||||
background: rgba(0, 212, 255, 0.15);
|
||||
border-color: rgba(0, 212, 255, 0.3);
|
||||
color: #00d4ff;
|
||||
}
|
||||
|
||||
.logs-action-btn.logs-send-btn:hover {
|
||||
background: rgba(0, 212, 255, 0.25);
|
||||
border-color: rgba(0, 212, 255, 0.5);
|
||||
}
|
||||
|
||||
.logs-action-btn.logs-send-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.logs-terminal {
|
||||
flex: 1;
|
||||
background: #0d1117;
|
||||
@@ -1005,20 +1021,12 @@ body {
|
||||
}
|
||||
|
||||
/* Featured Servers Styles */
|
||||
.featured-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
height: calc(100vh - 180px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.featured-left,
|
||||
.featured-right {
|
||||
.featured-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 180px);
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.featured-header {
|
||||
@@ -1074,8 +1082,8 @@ body {
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
min-height: 120px;
|
||||
grid-template-columns: 300px 1fr;
|
||||
min-height: 180px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1086,24 +1094,24 @@ body {
|
||||
}
|
||||
|
||||
.featured-server-banner {
|
||||
width: 200px;
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
min-height: 120px;
|
||||
min-height: 180px;
|
||||
object-fit: cover;
|
||||
background: linear-gradient(135deg, #1e293b, #334155);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.featured-server-content {
|
||||
padding: 1.25rem;
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.featured-server-name {
|
||||
font-size: 1.15rem;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
line-height: 1.4;
|
||||
@@ -1118,27 +1126,40 @@ body {
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.server-address-text {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.server-action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copy-address-btn {
|
||||
background: linear-gradient(135deg, #9333ea, #7c3aed);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.375rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1155,6 +1176,31 @@ body {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
}
|
||||
|
||||
.server-discord-btn {
|
||||
background: linear-gradient(135deg, #5865F2, #4752C4);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.server-discord-btn:hover {
|
||||
background: linear-gradient(135deg, #4752C4, #3c45a5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.server-discord-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -4783,6 +4829,140 @@ select.settings-input option {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.wrapper-items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wrapper-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.wrapper-item:hover {
|
||||
border-color: rgba(147, 51, 234, 0.3);
|
||||
background: rgba(147, 51, 234, 0.05);
|
||||
}
|
||||
|
||||
.wrapper-item-text {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 0.82rem;
|
||||
color: #e5e7eb;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wrapper-item-condition select {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.wrapper-item-condition select:focus {
|
||||
outline: none;
|
||||
border-color: rgba(147, 51, 234, 0.5);
|
||||
}
|
||||
|
||||
.wrapper-item-delete {
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.wrapper-item-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.wrapper-items-empty {
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
padding: 12px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wrapper-condition-select {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.82rem;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrapper-condition-select:focus {
|
||||
outline: none;
|
||||
border-color: rgba(147, 51, 234, 0.5);
|
||||
}
|
||||
|
||||
.wrapper-preview-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.wrapper-preview-toggle:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.wrapper-preview-toggle i {
|
||||
transition: transform 0.2s;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.wrapper-preview-toggle i.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.wrapper-preview-content {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.5;
|
||||
padding: 12px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.settings-hint {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
@@ -5236,6 +5416,21 @@ select.settings-input option {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
letter-spacing: 0.5px;
|
||||
-webkit-user-select: text !important;
|
||||
-moz-user-select: text !important;
|
||||
-ms-user-select: text !important;
|
||||
user-select: text !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Ensure all input fields allow text selection and paste */
|
||||
input[type="text"].uuid-input,
|
||||
#customUuidInput {
|
||||
-webkit-user-select: text !important;
|
||||
-moz-user-select: text !important;
|
||||
-ms-user-select: text !important;
|
||||
user-select: text !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.uuid-btn {
|
||||
@@ -5380,9 +5575,8 @@ select.settings-input option {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
}
|
||||
|
||||
.uuid-current-section,
|
||||
.uuid-list-section,
|
||||
.uuid-custom-section {
|
||||
.uuid-advanced-section {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
@@ -5623,6 +5817,102 @@ select.settings-input option {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.uuid-item-btn.switch:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
border-color: rgba(59, 130, 246, 0.4);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.uuid-item-btn.regenerate:hover {
|
||||
background: rgba(249, 115, 22, 0.2);
|
||||
border-color: rgba(249, 115, 22, 0.4);
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
/* Add Identity Form */
|
||||
.uuid-add-form {
|
||||
background: rgba(147, 51, 234, 0.05);
|
||||
border: 1px dashed rgba(147, 51, 234, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.uuid-add-form-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.uuid-add-form-row .uuid-input {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.uuid-add-form-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.uuid-cancel-btn {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.uuid-cancel-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Advanced Section */
|
||||
.uuid-advanced-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem 0;
|
||||
transition: color 0.3s ease;
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
}
|
||||
|
||||
.uuid-advanced-toggle:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.uuid-advanced-chevron {
|
||||
font-size: 0.75rem;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.uuid-advanced-chevron.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.uuid-advanced-content {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.uuid-modal-content {
|
||||
width: 95vw;
|
||||
@@ -5634,8 +5924,8 @@ select.settings-input option {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.uuid-current-display,
|
||||
.uuid-custom-form {
|
||||
.uuid-custom-form,
|
||||
.uuid-add-form-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -6012,6 +6302,155 @@ select.settings-input option {
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Header Tooltip Styles */
|
||||
.header-tooltip {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(4px);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.03em;
|
||||
color: #9ca3af;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.identity-selector:hover .header-tooltip,
|
||||
.profile-selector:hover .header-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.identity-dropdown.show ~ .header-tooltip,
|
||||
.profile-dropdown.show ~ .header-tooltip {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
/* Identity Selector Styles */
|
||||
.identity-selector {
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
margin-left: 1rem;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.identity-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 100000 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
.identity-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.identity-btn i {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.identity-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 0.5rem;
|
||||
width: 200px;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0.5rem;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
z-index: 2000;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
animation: fadeIn 0.1s ease;
|
||||
}
|
||||
|
||||
.identity-dropdown.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.identity-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.identity-item {
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #ccc;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.identity-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.identity-item.active {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.identity-item.active::before {
|
||||
content: '\2022';
|
||||
color: #22c55e;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.identity-divider {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.identity-action {
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #22c55e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.identity-action:hover {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.identity-empty {
|
||||
padding: 0.6rem 0.8rem;
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Profile Selector Styles */
|
||||
.profile-selector {
|
||||
position: relative;
|
||||
@@ -6409,3 +6848,85 @@ select.settings-input option {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Version Selection Styles */
|
||||
.version-list-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.version-list-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.version-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.version-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.version-name {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.version-meta {
|
||||
font-size: 0.8rem;
|
||||
color: #a0a0a0;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.version-meta i {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.version-actions .btn-install {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.version-actions .btn-install:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.3);
|
||||
background: linear-gradient(135deg, #4f93f6, #3b82f6);
|
||||
}
|
||||
|
||||
.loading-versions {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
2
PKGBUILD
2
PKGBUILD
@@ -2,7 +2,7 @@
|
||||
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
||||
# This PKGBUILD is for Github Releases
|
||||
pkgname=Hytale-F2P
|
||||
pkgver=2.2.0
|
||||
pkgver=2.2.1
|
||||
pkgrel=1
|
||||
pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support"
|
||||
arch=('x86_64')
|
||||
|
||||
74
README.md
74
README.md
@@ -7,19 +7,19 @@
|
||||
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support!</small></p>
|
||||
</header>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://github.com/amiayweb/Hytale-F2P/releases)
|
||||
[](https://github.com/amiayweb/Hytale-F2P/releases)
|
||||
[](https://github.com/amiayweb/Hytale-F2P/releases)
|
||||
|
||||
[](https://github.com/amiayweb/Hytale-F2P/stargazers)
|
||||
[](https://github.com/amiayweb/Hytale-F2P/network/members)
|
||||
[](https://github.com/amiayweb/Hytale-F2P/stargazers)
|
||||
[](https://github.com/amiayweb/Hytale-F2P/network/members)
|
||||
[](https://github.com/amiayweb/Hytale-F2P/issues)
|
||||

|
||||
|
||||
⭐ **If you find this project useful, please give it a STAR!** ⭐
|
||||
### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
||||
|
||||
### ⚠️ **READ [QUICK START](README.md#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
||||
|
||||
#### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
|
||||
#### 🛑 **Found a problem? [TG Group](https://t.me/sanhostnet) | [TG Channel](https://t.me/hf2p_og) | [Community Chat](https://chat.sanhost.net/invite/Tfz4jCK4)** 🛑
|
||||
<!-- #### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/Fhbb9Yk5WW) and head to `#-⚠️-community-help`** 🛑 -->
|
||||
|
||||
<p>
|
||||
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
|
||||
@@ -30,9 +30,15 @@
|
||||
<img src="https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExem14OW1tanN3eHlyYmR4NW1sYmJkOTZmbmJxejdjZXB6MXY5cW12MSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9cw/TDQOtnWgsBx99cNoyH/giphy.gif" width="120">
|
||||
</a>
|
||||
|
||||
|
||||
⭐ **If you find this project useful, please give it a STAR!** ⭐
|
||||
|
||||
[](https://www.star-history.com/#amiayweb/Hytale-F2P&type=date&legend=top-left)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<div align="center">
|
||||
@@ -167,17 +173,17 @@
|
||||
### 🪟 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.
|
||||
* or [Alt 1: Adoptium](https://adoptium.net/temurin/releases/?version=25)
|
||||
* or [Alt 2: Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download).
|
||||
* **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/)
|
||||
* Download via [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
|
||||
* Or [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe)
|
||||
|
||||
### 🐧 Linux Prequisites
|
||||
|
||||
* 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:
|
||||
* [Not needed in update v2.2.0+] 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
|
||||
@@ -272,8 +278,8 @@ The `.zip` version is useful for users who prefer a portable installation or nee
|
||||
|
||||
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 UPnP`.
|
||||
4. If your friends can't connect to your hosted Online-Play feature, please follow **Local Dedicated Server** tutorial.
|
||||
3. Check the status `Connected via UPnP`, it means you can use the Invite Codes for your friends.
|
||||
4. If your friends can't connect to your hosted Online-Play feature OR if it's showing `"Restricted (no UPnP)`, please follow the Tailscale/Playit.gg/Radmin tutorial in [SERVER.md](SERVER.md).
|
||||
|
||||
## 🖧 Host a Dedicated Server
|
||||
|
||||
@@ -284,10 +290,10 @@ The `.zip` version is useful for users who prefer a portable installation or nee
|
||||
> 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). Additional: **Linux ARM64** is supported for server only, not client.
|
||||
> `HytaleServer.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). Additional: **Linux ARM64** is supported for server only, not client.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 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.
|
||||
> See detailed information of setting up a server here: [SERVER.md](SERVER.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -305,7 +311,17 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
||||
|
||||
## 📋 Changelog
|
||||
|
||||
### 🆕 v2.2.0
|
||||
### 🆕 v2.2.1
|
||||
- 👚 **Avatar Not Saving Bug Fix:** FINALLY, the long-awaited avatar saves is now working! 🙌 Show off your avatar skin in our Discord `#-media` text channel! 👀
|
||||
- 🚀 **HytaleClient Fails to Launch and Persists in Task Manager Bug Fix:** Major bug fix for all affected Windows users! No more ghost processes of `HytaleClient.exe` in Task Manager! And no more launch fail, that's hella one of an achievement 🔥 (If problem persists please create issue on Github 😢)
|
||||
- 🚦 **EPERM Bug Fix in 'Repair Game' Button:** Repair game will not produce Error Permission (EPERM) any more.
|
||||
- 🚨 **'Server Failed to Boot' Bug Fix:** Happy news for internet-limited countries (e.g. 🇷🇺 Russia, 🇹🇷 Turkey, 🇧🇷 Brazil, etc.)! The launcher now using proxy to access our patched JAR & check game version release status!🎉 Make sure you're already allow the `HytaleClient.exe` on Public & Private Windows Firewall 😉!
|
||||
- ⚡ **GPU Detection System Enhancements:** The detection system will now detect your GPU with `CimInstance` instead of `WmicObject`, which deprecated for most Windows 11 updates. Also, it's show how much your VRAM on each iGPU and dGPU! 🔍
|
||||
- ⚠️ **Failed to Deserialize Packets Bug Fix:** Shared `libzstd` library didn't get detected in Fedora/Bazzite/RHEL-based Linux Distros due to incorrect checking library order. 📑
|
||||
- 📟 **UUID Persistence Bug Fix:** Correlates to the avatar not saving bug, this fixes the persistence UUID when changing username. 🔖
|
||||
- 🌐 **Turkish Translation Fix:** 🇹🇷 Turkey players should feel at home now. 🏠
|
||||
|
||||
### 🔄 v2.2.0
|
||||
- 🔃 **Game Patches Auto-Update Improvement:** No need to install 1.5GB for every updates! Game updates now reduced to almost **~90%** (Hytale Game Update 3 to 4 only take ~150MB).
|
||||
- 🩹 **Improved Patch System Pre-Release JAR:** In previous version, only Release JAR could be patched. Now it also can be used for Pre-Release JAR!
|
||||
- 🔗 **Fix Mods Manager Issue:** Mods now can be downloaded seamlessly from the launcher, use Profiles to install your preferred mod. It will also automatically copy from selected `Profile/<profilename>` to the `Mods` folder.
|
||||
@@ -435,23 +451,13 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
||||
|
||||
---
|
||||
|
||||
## 📊 GitHub Stats
|
||||
## 📞 Contact Information
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
## 📞 Support
|
||||
|
||||
<div align="center">
|
||||
|
||||
**Need help?** Join us: https://discord.gg/gME8rUy3MB
|
||||
**Questions? Ads? Collaboration? Endorsement? Other business-related?**
|
||||
Message the founders at [TG Group](https://t.me/sanhostnet) | [TG Channel](https://t.me/hf2p_og) | [Community Chat](https://chat.sanhost.net/invite/Tfz4jCK4)
|
||||
<!-- Message the founders at https://discord.gg/Fhbb9Yk5WW -->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
132
SERVER.md
132
SERVER.md
@@ -2,19 +2,21 @@
|
||||
|
||||
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
|
||||
|
||||
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/MEyWUxt77m**
|
||||
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://chat.sanhost.net/invite/Tfz4jCK4**
|
||||
<!-- ### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/Fhbb9Yk5WW** -->
|
||||
|
||||
**Table of Contents**
|
||||
|
||||
* [\[NEW!\] Play Online with Official Accounts 🆕](#new-play-online-with-official-accounts-)
|
||||
* ["Server" Term and Definition](#server-term-and-definiton)
|
||||
* [Server Directory Location](#server-directory-location)
|
||||
* [A. Online Play Feature](#a-online-play-feature)
|
||||
* [1. Host Your Singleplayer World using In-Game Invite Code](#1-host-your-singleplayer-world-using-in-game-invite-code)
|
||||
* [A. Host Your Singleplayer World](#a-host-your-singleplayer-world)
|
||||
* [1. Using Online-Play Feature In-Game Invite Code](#1-using-online-play-feature--in-game-invite-code)
|
||||
* [Common Issues (UPnP/NAT/STUN) on Online Play](#common-issues-upnpnatstun-on-online-play)
|
||||
* [2. Host Your Singleplayer World using Tailscale](#2-host-your-singleplayer-world-using-tailscale)
|
||||
* [2. Using Tailscale](#2-using-tailscale)
|
||||
* [3. Using Radmin VPN](#3-using-radmin-vpn)
|
||||
* [B. Local Dedicated Server](#b-local-dedicated-server)
|
||||
* [1. Using Playit.gg (Recommended) ✅](#1-using-playitgg-recommended-)
|
||||
* [2. Using Radmin VPN](#2-using-radmin-vpn)
|
||||
* [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced)
|
||||
* [Step 1: Get the Files Ready](#step-1-get-the-files-ready)
|
||||
* [Step 2: Place HytaleServer.jar in the Server directory](#step-2-place-hytaleserverjar-in-the-server-directory)
|
||||
@@ -32,6 +34,69 @@ Play with friends online! This guide covers both easy in-game hosting and advanc
|
||||
* [10. Getting Help](#10-getting-help)
|
||||
---
|
||||
|
||||
<div align='center'>
|
||||
<h3>
|
||||
<b>
|
||||
Do you want to create Hytale Game Server with EASY SETUP, AFFORDABLE PRICE, AND 24/7 SUPPORT?
|
||||
</b>
|
||||
</h3>
|
||||
<h2>
|
||||
<b>
|
||||
<a href="https://cloudnord.net/hytale-server-hosting">CLOUDNORD</a> is the ANSWER! HF2P Server is available!
|
||||
</b>
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
|
||||
**CloudNord's Hytale, Minecraft, and Game Hosting** is at the core of our Server Hosting business. Join our Gaming community and experience our large choice of premium game servers, we’ve got you covered with super high-performance hardware, fantastic support options, and powerful server hosting to build and explore your worlds without limits!
|
||||
|
||||
**Order your Hytale, Minecraft, or other game servers today!**
|
||||
Choose Java Edition, Bedrock Edition, Cross-Play, or any of our additional supported games.
|
||||
Enjoy **20% OFF** all new game servers, **available now for a limited time!** Don’t miss out.
|
||||
|
||||
### **CloudNord key hosting features include:**
|
||||
- Instant Server Setup ⚡
|
||||
- High Performance Game Servers 🚀
|
||||
- Game DDoS Protection 🛡️
|
||||
- Intelligent Game Backups 🧠
|
||||
- Quick Modpack Installer 🔧
|
||||
- Quick Plugin & Mod Installer 🧰
|
||||
- Full File Access 🗃️
|
||||
- 24/7 Support 📞 🏪
|
||||
- Powerful Game Control Server Panel 💪
|
||||
|
||||
### **Check Us Out:**
|
||||
* 👉 CloudNord Website: https://cloudnord.net/hytalef2p
|
||||
* 👉 CloudNord Discord: https://discord.gg/TYxGrmUz4Y
|
||||
* 👉 CloudNord Reviews: https://www.trustpilot.com/review/cloudnord.net?page=2&stars=5
|
||||
|
||||
---
|
||||
|
||||
### [NEW!] Play Online with Official Accounts 🆕
|
||||
|
||||
**Documentations:**
|
||||
* [Hytale-Server-Docker by Sanasol](https://github.com/sanasol/hytale-server-docker/tree/main?tab=readme-ov-file#dual-authentication)
|
||||
|
||||
**Requirements:**
|
||||
* Using the patched HytaleServer.jar
|
||||
* Has Official Account with Purchased status on Official Hytale Website.
|
||||
* This official account holder can be the server hoster or one of the players.
|
||||
|
||||
**Steps:**
|
||||
1. Running the patched HytaleServer.jar with either [B. Local Dedicated Server](#b-local-dedicated-server) or [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced) successfully.
|
||||
2. On the server's console/terminal/CMD, server admin **MUST RUN THIS EACH BOOT** to allow players with Official Hytale game license to connect on the server:
|
||||
```
|
||||
/auth logout
|
||||
/auth persistence Encrypted
|
||||
/auth login device
|
||||
```
|
||||
3. Server console will show instructions, an URL and a code; these will be revoked after 10 minutes if not authorized.
|
||||
4. The server hoster can open the URL directly to browser by holding Ctrl then Click on it, or copy and send it to the player with official account.
|
||||
5. Once it authorized, the official accounts can join server with F2P players.
|
||||
6. If you want to modify anything, look at the [Hytale-Server-Docker](https://github.com/sanasol/hytale-server-docker/) above, give the repo a STAR too.
|
||||
|
||||
---
|
||||
|
||||
### "Server" Term and Definiton
|
||||
|
||||
"HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (Thanks to Sanasol), handling approximately thousands of players worldwide to play this game for free.
|
||||
@@ -41,14 +106,15 @@ Kindly support us via [our Buy Me a Coffee link](https://buymeacoffee.com/hf2p)
|
||||
|
||||
### Server Directory Location
|
||||
|
||||
Here are the directory locations of Server folder if you have installed
|
||||
Here are the directory locations of Server folder if you have installed it on default instalation location:
|
||||
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
|
||||
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
|
||||
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
|
||||
|
||||
> [!NOTE]
|
||||
> This location only exists if the user installed the game using our launcher. The `Server` folder needed to auth the HytaleClient to play Hytale online
|
||||
> (for now; we planned to add offline mode in later version of our launcher).
|
||||
> This location only exists if the user installed the game using our launcher.
|
||||
> The `Server` folder needed to auth the HytaleClient to play Hytale in Singleplayer/Multiplayer for now.
|
||||
> (We planned to add offline mode in later version of our launcher).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Hosting a dedicated Hytale server will not need the exact similar tree. You can put it anywhere, as long as the directory has `Assets.zip` which
|
||||
@@ -64,6 +130,7 @@ Terms and conditions applies.
|
||||
## 1. Using Online-Play Feature / In-Game Invite Code
|
||||
|
||||
The easiest way to play with friends - no manual server setup required!
|
||||
|
||||
*The game automatically handles networking using UPnP/STUN/NAT traversal.*
|
||||
|
||||
**For Online Play to work, you need:**
|
||||
@@ -112,6 +179,7 @@ Warning: Your network configuration may prevent other players from connecting.
|
||||
</details>
|
||||
|
||||
<details><summary><b>b. "UPnP Failed" or "Port Mapping Failed" Warning</b></summary>
|
||||
|
||||
**Check your router:**
|
||||
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`)
|
||||
2. Find UPnP settings (often under "Advanced" or "NAT")
|
||||
@@ -123,7 +191,8 @@ Warning: Your network configuration may prevent other players from connecting.
|
||||
- See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below
|
||||
</details>
|
||||
|
||||
<details><summary><b>c. "Strict NAT" or "Symmetric NAT" Warning</b></summary>
|
||||
<details><summary><b>c. "Connected via STUN", "Strict NAT" or "Symmetric NAT" Warning</b></summary>
|
||||
|
||||
Some routers have restrictive NAT that blocks peer connections.
|
||||
|
||||
**Try:**
|
||||
@@ -133,6 +202,7 @@ Some routers have restrictive NAT that blocks peer connections.
|
||||
</details>
|
||||
|
||||
## 2. Using Tailscale
|
||||
|
||||
Tailscale creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
|
||||
|
||||
1. All members are required to download [Tailscale](https://tailscale.com/download) on your device.
|
||||
@@ -148,6 +218,17 @@ Tailscale creates mesh VPN service that streamlines connecting devices and servi
|
||||
* Use the new share code to connect
|
||||
* To test your connection, ping the host's ipv4 mentioned in Tailscale
|
||||
|
||||
## 3. Using Radmin VPN
|
||||
|
||||
Creates a virtual LAN - all players need to install it:
|
||||
|
||||
1. Download [Radmin VPN](https://www.radmin-vpn.com/) - All players install it
|
||||
2. One person create a room/network, others join with network name/password
|
||||
3. Host joined the world, others will connect to it.
|
||||
4. Open Hytale Game > Servers > Add Servers > Direct Connect > Type IP Address of the Host from Radmin.
|
||||
|
||||
These options bypass all NAT/CGNAT issues. But for **Windows machines only!**
|
||||
|
||||
---
|
||||
|
||||
# B. Local Dedicated Server
|
||||
@@ -166,12 +247,13 @@ Free tunneling service - only the host needs to install it:
|
||||
* Linux:
|
||||
* Right-click file > Properties > Turn on 'Executable as a Program' | or `chmod +x playit-linux-amd64` on terminal
|
||||
* Run by double-clicking the file or `./playit-linux-amd64` via terminal
|
||||
5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account.
|
||||
6. **WARNING: Do not close the terminal if you are still playing or hosting the server**
|
||||
7. Once it done, download the `run_server_with_tokens` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server`
|
||||
8. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`)
|
||||
9. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder. (TIP: You can use Symlink of that file to reduce disk usage!)
|
||||
10. Double-click the .BAT file to host your server, wait until it shows:
|
||||
5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account.
|
||||
6. Once it done, download the `run_server_with_tokens (1)` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server`
|
||||
7. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`)
|
||||
8. Rename the script file to `run_server_with_tokens` to make it easier if you run it with Terminal, then do Method A or B.
|
||||
9. If you put it in `Server` folder in `HytaleF2P` launcher, change `ASSETS_PATH="${ASSETS_PATH:-./Assets.zip}"` inside the script to be `ASSETS_PATH="${ASSETS_PATH:-../Assets.zip}"`. NOTICE THE `./` and `../` DIFFERENCE.
|
||||
10. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder. (TIP: You can use Symlink of that file to reduce disk usage!)
|
||||
11. Double-click the .BAT file to host your server, wait until it shows:
|
||||
```
|
||||
===================================================
|
||||
Hytale Server Booted! [Multiplayer, Fresh Universe]
|
||||
@@ -180,16 +262,12 @@ Hytale Server Booted! [Multiplayer, Fresh Universe]
|
||||
11. Connect to the server by go to `Servers` in your game client, press `Add Server`, type `localhost` in the address box, use any name for your server.
|
||||
12. Send the public address in Step 3 to your friends.
|
||||
|
||||
## 2. Using Radmin VPN
|
||||
> [!CAUTION]
|
||||
> Do not close the Playit.gg Terminal OR HytaleServer Terminal if you are still playing or hosting the server.
|
||||
|
||||
Creates a virtual LAN - all players need to install it:
|
||||
## 2. Using Tailscale [DRAFT]
|
||||
|
||||
1. Download [Radmin VPN](https://www.radmin-vpn.com/) - All players install it
|
||||
2. One person create a room/network, others join with network name/password
|
||||
3. Host joined the world, others will connect to it.
|
||||
4. Open Hytale Game > Servers > Add Servers > Direct Connect > Type IP Address of the Host from Radmin.
|
||||
|
||||
These options bypass all NAT/CGNAT issues. But for **Windows machines only!**
|
||||
Tailscale
|
||||
|
||||
---
|
||||
|
||||
@@ -228,12 +306,12 @@ For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
|
||||
|
||||
**Windows:**
|
||||
```batch
|
||||
run_server.bat
|
||||
run_server_with_token.bat
|
||||
```
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
./run_server.sh
|
||||
./run_server_with_token.sh
|
||||
```
|
||||
|
||||
---
|
||||
@@ -502,3 +580,7 @@ See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for
|
||||
- [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker)
|
||||
- Auth Server: sanasol.ws
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Hytale F2P Launcher - Troubleshooting Guide
|
||||
|
||||
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/gME8rUy3MB).
|
||||
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [TG Group](https://t.me/sanhostnet) | [TG Channel](https://t.me/hf2p_og) | [Community Chat](https://chat.sanhost.net/invite/Tfz4jCK4).
|
||||
<!-- Discord: https://discord.gg/Fhbb9Yk5WW -->
|
||||
|
||||
---
|
||||
|
||||
@@ -437,7 +438,8 @@ Game sessions have a 10-hour TTL. This is by design for security.
|
||||
If your issue isn't resolved by this guide:
|
||||
|
||||
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
|
||||
2. **Join Discord:** [discord.gg/gME8rUy3MB](https://discord.gg/gME8rUy3MB)
|
||||
2. **Join Community:** [TG Group](https://t.me/sanhostnet) | [TG Channel](https://t.me/hf2p_og) | [Chat](https://chat.sanhost.net/invite/Tfz4jCK4)
|
||||
<!-- Discord: https://discord.gg/Fhbb9Yk5WW -->
|
||||
3. **Open a new issue** with:
|
||||
- Your operating system and version
|
||||
- Launcher version
|
||||
|
||||
@@ -4,6 +4,10 @@ const logger = require('./logger');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const https = require('https');
|
||||
|
||||
const FORGEJO_API = 'https://git.sanhost.net/api/v1';
|
||||
const FORGEJO_REPO = 'sanasol/hytale-f2p';
|
||||
|
||||
class AppUpdater {
|
||||
constructor(mainWindow) {
|
||||
@@ -14,6 +18,34 @@ class AppUpdater {
|
||||
this.setupAutoUpdater();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest non-draft release tag from Forgejo and set the feed URL
|
||||
*/
|
||||
async _resolveUpdateUrl() {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(`${FORGEJO_API}/repos/${FORGEJO_REPO}/releases?limit=5`, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const releases = JSON.parse(data);
|
||||
const latest = releases.find(r => !r.draft && !r.prerelease);
|
||||
if (latest) {
|
||||
const url = `https://git.sanhost.net/${FORGEJO_REPO}/releases/download/${latest.tag_name}`;
|
||||
console.log(`Auto-update URL resolved to: ${url}`);
|
||||
autoUpdater.setFeedURL({ provider: 'generic', url });
|
||||
resolve(url);
|
||||
} else {
|
||||
reject(new Error('No published release found'));
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
setupAutoUpdater() {
|
||||
|
||||
// Configure logger for electron-updater
|
||||
@@ -216,8 +248,10 @@ class AppUpdater {
|
||||
}
|
||||
|
||||
checkForUpdatesAndNotify() {
|
||||
// Check for updates and notify if available
|
||||
autoUpdater.checkForUpdatesAndNotify().catch(err => {
|
||||
// Resolve latest release URL then check for updates
|
||||
this._resolveUpdateUrl().catch(err => {
|
||||
console.warn('Failed to resolve update URL:', err.message);
|
||||
}).then(() => autoUpdater.checkForUpdatesAndNotify()).catch(err => {
|
||||
console.error('Failed to check for updates:', err);
|
||||
|
||||
// Network errors are not critical - just log and continue
|
||||
@@ -245,8 +279,10 @@ class AppUpdater {
|
||||
}
|
||||
|
||||
checkForUpdates() {
|
||||
// Manual check for updates (returns promise)
|
||||
return autoUpdater.checkForUpdates().catch(err => {
|
||||
// Manual check - resolve latest release URL first
|
||||
return this._resolveUpdateUrl().catch(err => {
|
||||
console.warn('Failed to resolve update URL:', err.message);
|
||||
}).then(() => autoUpdater.checkForUpdates()).catch(err => {
|
||||
console.error('Failed to check for updates:', err);
|
||||
|
||||
// Network errors are not critical - just return no update available
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
const FORCE_CLEAN_INSTALL_VERSION = false;
|
||||
const CLEAN_INSTALL_TEST_VERSION = '4.pwr';
|
||||
const CLEAN_INSTALL_TEST_VERSION = 'v4';
|
||||
|
||||
module.exports = {
|
||||
FORCE_CLEAN_INSTALL_VERSION,
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
const {
|
||||
saveUsername,
|
||||
loadUsername,
|
||||
loadUsernameWithDefault,
|
||||
hasUsername,
|
||||
saveJavaPath,
|
||||
loadJavaPath,
|
||||
saveInstallPath,
|
||||
@@ -27,9 +29,11 @@ const {
|
||||
getUuidForUser,
|
||||
isFirstLaunch,
|
||||
markAsLaunched,
|
||||
checkLaunchReady,
|
||||
// UUID Management
|
||||
getCurrentUuid,
|
||||
getAllUuidMappings,
|
||||
getAllUuidMappingsArray,
|
||||
setUuidForUser,
|
||||
generateNewUuid,
|
||||
deleteUuidForUser,
|
||||
@@ -41,7 +45,13 @@ const {
|
||||
saveVersionClient,
|
||||
loadVersionClient,
|
||||
saveVersionBranch,
|
||||
loadVersionBranch
|
||||
loadVersionBranch,
|
||||
// Java Wrapper Config
|
||||
getDefaultWrapperConfig,
|
||||
loadWrapperConfig,
|
||||
saveWrapperConfig,
|
||||
resetWrapperConfig,
|
||||
generateWrapperScript
|
||||
} = require('./core/config');
|
||||
|
||||
const { getResolvedAppDir, getModsPath } = require('./core/paths');
|
||||
@@ -74,7 +84,8 @@ const {
|
||||
loadInstalledMods,
|
||||
downloadMod,
|
||||
uninstallMod,
|
||||
toggleMod
|
||||
toggleMod,
|
||||
getModFiles
|
||||
} = require('./managers/modManager');
|
||||
|
||||
// Services
|
||||
@@ -110,7 +121,10 @@ module.exports = {
|
||||
// User configuration functions
|
||||
saveUsername,
|
||||
loadUsername,
|
||||
loadUsernameWithDefault,
|
||||
hasUsername,
|
||||
getUuidForUser,
|
||||
checkLaunchReady,
|
||||
|
||||
// Java configuration functions
|
||||
saveJavaPath,
|
||||
@@ -162,6 +176,7 @@ module.exports = {
|
||||
// UUID Management functions
|
||||
getCurrentUuid,
|
||||
getAllUuidMappings,
|
||||
getAllUuidMappingsArray,
|
||||
setUuidForUser,
|
||||
generateNewUuid,
|
||||
deleteUuidForUser,
|
||||
@@ -173,6 +188,7 @@ module.exports = {
|
||||
downloadMod,
|
||||
uninstallMod,
|
||||
toggleMod,
|
||||
getModFiles,
|
||||
saveModsToConfig,
|
||||
loadModsFromConfig,
|
||||
|
||||
@@ -189,6 +205,13 @@ module.exports = {
|
||||
proposeGameUpdate,
|
||||
handleFirstLaunchCheck,
|
||||
|
||||
// Java Wrapper Config functions
|
||||
getDefaultWrapperConfig,
|
||||
loadWrapperConfig,
|
||||
saveWrapperConfig,
|
||||
resetWrapperConfig,
|
||||
generateWrapperScript,
|
||||
|
||||
// Path functions
|
||||
getResolvedAppDir
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
const { downloadFile, retryDownload } = require('../utils/fileManager');
|
||||
const { getOS, getArch } = require('../utils/platformUtils');
|
||||
const { validateChecksum, extractVersionDetails, canUseDifferentialUpdate, needsIntermediatePatches, getInstalledClientVersion } = require('../services/versionManager');
|
||||
const { validateChecksum, extractVersionDetails, getInstalledClientVersion, getUpdatePlan, extractVersionNumber, getAllMirrorUrls, getPatchesBaseUrl } = require('../services/versionManager');
|
||||
const { installButler } = require('./butlerManager');
|
||||
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||
const { saveVersionClient } = require('../core/config');
|
||||
@@ -30,16 +30,63 @@ async function acquireGameArchive(downloadUrl, targetPath, checksum, progressCal
|
||||
}
|
||||
|
||||
console.log(`Downloading game archive from: ${downloadUrl}`);
|
||||
|
||||
try {
|
||||
if (allowRetry) {
|
||||
await retryDownload(downloadUrl, targetPath, progressCallback);
|
||||
} else {
|
||||
await downloadFile(downloadUrl, targetPath, progressCallback);
|
||||
|
||||
// Try primary URL first, then mirror URLs on timeout/connection failure
|
||||
const mirrors = await getAllMirrorUrls();
|
||||
const primaryBase = await getPatchesBaseUrl();
|
||||
const urlsToTry = [downloadUrl];
|
||||
|
||||
// Build mirror URLs by replacing the base URL
|
||||
for (const mirror of mirrors) {
|
||||
if (mirror !== primaryBase && downloadUrl.startsWith(primaryBase)) {
|
||||
const mirrorUrl = downloadUrl.replace(primaryBase, mirror);
|
||||
if (!urlsToTry.includes(mirrorUrl)) {
|
||||
urlsToTry.push(mirrorUrl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const enhancedError = new Error(`Archive download failed: ${error.message}`);
|
||||
enhancedError.originalError = error;
|
||||
}
|
||||
|
||||
let lastError;
|
||||
for (let i = 0; i < urlsToTry.length; i++) {
|
||||
const url = urlsToTry[i];
|
||||
try {
|
||||
if (i > 0) {
|
||||
console.log(`[Download] Trying mirror ${i}: ${url}`);
|
||||
if (progressCallback) {
|
||||
progressCallback(`Trying alternative mirror (${i}/${urlsToTry.length - 1})...`, 0, null, null, null);
|
||||
}
|
||||
// Clean up partial download from previous attempt
|
||||
if (fs.existsSync(targetPath)) {
|
||||
try { fs.unlinkSync(targetPath); } catch (e) {}
|
||||
}
|
||||
}
|
||||
if (allowRetry) {
|
||||
await retryDownload(url, targetPath, progressCallback);
|
||||
} else {
|
||||
await downloadFile(url, targetPath, progressCallback);
|
||||
}
|
||||
lastError = null;
|
||||
break; // Success
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const isConnectionError = error.message && (
|
||||
error.message.includes('ETIMEDOUT') ||
|
||||
error.message.includes('ECONNREFUSED') ||
|
||||
error.message.includes('ECONNABORTED') ||
|
||||
error.message.includes('timeout')
|
||||
);
|
||||
if (isConnectionError && i < urlsToTry.length - 1) {
|
||||
console.warn(`[Download] Connection failed (${error.message}), will try mirror...`);
|
||||
continue;
|
||||
}
|
||||
// Non-connection error or last mirror — throw
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastError) {
|
||||
const enhancedError = new Error(`Archive download failed: ${lastError.message}`);
|
||||
enhancedError.originalError = lastError;
|
||||
enhancedError.downloadUrl = downloadUrl;
|
||||
enhancedError.targetPath = targetPath;
|
||||
throw enhancedError;
|
||||
@@ -103,13 +150,13 @@ async function deployGameArchive(archivePath, destinationDir, toolsDir, progress
|
||||
if (error) {
|
||||
const cleanStderr = stderr.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
|
||||
const cleanStdout = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
|
||||
|
||||
|
||||
if (cleanStderr) console.error('Deployment stderr:', cleanStderr);
|
||||
if (cleanStdout) console.error('Deployment stdout:', cleanStdout);
|
||||
|
||||
|
||||
const errorText = (stderr + ' ' + error.message).toLowerCase();
|
||||
let message = 'Game deployment failed';
|
||||
|
||||
|
||||
if (errorText.includes('unexpected eof')) {
|
||||
message = 'Corrupted archive detected. Please retry download.';
|
||||
if (fs.existsSync(archivePath)) {
|
||||
@@ -156,20 +203,20 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
||||
console.log(`Initiating intelligent update to version ${targetVersion}`);
|
||||
|
||||
const currentVersion = getInstalledClientVersion();
|
||||
console.log(`Current version: ${currentVersion || 'none (clean install)'}`);
|
||||
console.log(`Target version: ${targetVersion}`);
|
||||
console.log(`Branch: ${branch}`);
|
||||
const currentBuild = extractVersionNumber(currentVersion) || 0;
|
||||
const targetBuild = extractVersionNumber(targetVersion);
|
||||
console.log(`Current build: ${currentBuild}, Target build: ${targetBuild}, Branch: ${branch}`);
|
||||
|
||||
// For non-release branches, always do full install
|
||||
if (branch !== 'release') {
|
||||
console.log(`Pre-release branch detected - forcing full archive download`);
|
||||
console.log('Pre-release branch detected - forcing full archive download');
|
||||
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||
const archiveName = path.basename(versionDetails.fullUrl);
|
||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||
|
||||
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
|
||||
}
|
||||
|
||||
|
||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||
saveVersionClient(targetVersion);
|
||||
@@ -177,16 +224,16 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentVersion) {
|
||||
// Clean install (no current version)
|
||||
if (currentBuild === 0) {
|
||||
console.log('No existing installation detected - downloading full archive');
|
||||
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||
const archiveName = path.basename(versionDetails.fullUrl);
|
||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||
|
||||
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Downloading full game archive (first install - v${targetVersion})...`, 0, null, null, null);
|
||||
progressCallback(`Downloading full game archive (first install - v${targetBuild})...`, 0, null, null, null);
|
||||
}
|
||||
|
||||
|
||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||
saveVersionClient(targetVersion);
|
||||
@@ -194,59 +241,67 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
||||
return;
|
||||
}
|
||||
|
||||
const patchesToApply = needsIntermediatePatches(currentVersion, targetVersion);
|
||||
|
||||
if (patchesToApply.length === 0) {
|
||||
console.log('Already at target version or invalid version sequence');
|
||||
// Already at target
|
||||
if (currentBuild >= targetBuild) {
|
||||
console.log('Already at target version or newer');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Applying ${patchesToApply.length} differential patch(es): ${patchesToApply.join(' -> ')}`);
|
||||
// Use mirror's update plan for optimal patch routing
|
||||
try {
|
||||
const plan = await getUpdatePlan(currentBuild, targetBuild, branch);
|
||||
|
||||
console.log(`Applying ${plan.steps.length} patch(es): ${plan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')}`);
|
||||
|
||||
for (let i = 0; i < plan.steps.length; i++) {
|
||||
const step = plan.steps[i];
|
||||
const stepName = `${step.from}_to_${step.to}`;
|
||||
const archivePath = path.join(cacheDir, `${branch}_${stepName}.pwr`);
|
||||
const isDifferential = step.from !== 0;
|
||||
|
||||
for (let i = 0; i < patchesToApply.length; i++) {
|
||||
const patchVersion = patchesToApply[i];
|
||||
const versionDetails = await extractVersionDetails(patchVersion, branch);
|
||||
|
||||
const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails);
|
||||
|
||||
if (!canDifferential || !versionDetails.differentialUrl) {
|
||||
console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`);
|
||||
const archiveName = path.basename(versionDetails.fullUrl);
|
||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Downloading full archive for ${patchVersion} (${i + 1}/${patchesToApply.length})...`, 0, null, null, null);
|
||||
progressCallback(`Downloading patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 0, null, null, null);
|
||||
}
|
||||
|
||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||
} else {
|
||||
console.log(`Applying differential patch: ${versionDetails.sourceVersion} -> ${patchVersion}`);
|
||||
const archiveName = path.basename(versionDetails.differentialUrl);
|
||||
const archivePath = path.join(cacheDir, `${branch}_patch_${archiveName}`);
|
||||
|
||||
|
||||
await acquireGameArchive(step.url, archivePath, null, progressCallback);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Applying patch ${i + 1}/${patchesToApply.length}: ${patchVersion}...`, 0, null, null, null);
|
||||
progressCallback(`Applying patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 50, null, null, null);
|
||||
}
|
||||
|
||||
await acquireGameArchive(versionDetails.differentialUrl, archivePath, versionDetails.checksum, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, true);
|
||||
|
||||
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, isDifferential);
|
||||
|
||||
// Clean up patch file
|
||||
if (fs.existsSync(archivePath)) {
|
||||
try {
|
||||
fs.unlinkSync(archivePath);
|
||||
console.log(`Cleaned up patch file: ${archiveName}`);
|
||||
console.log(`Cleaned up: ${stepName}.pwr`);
|
||||
} catch (cleanupErr) {
|
||||
console.warn(`Failed to cleanup patch file: ${cleanupErr.message}`);
|
||||
console.warn(`Failed to cleanup: ${cleanupErr.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveVersionClient(patchVersion);
|
||||
console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`);
|
||||
}
|
||||
|
||||
console.log(`Update completed successfully. Version ${targetVersion} is now installed.`);
|
||||
saveVersionClient(`v${step.to}`);
|
||||
console.log(`Patch ${stepName} applied (${i + 1}/${plan.steps.length})`);
|
||||
}
|
||||
|
||||
console.log(`Update completed. Version ${targetVersion} is now installed.`);
|
||||
} catch (planError) {
|
||||
console.error('Update plan failed:', planError.message);
|
||||
console.log('Falling back to full archive download');
|
||||
|
||||
// Fallback: full install
|
||||
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Downloading full game archive (fallback)...`, 0, null, null, null);
|
||||
}
|
||||
|
||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||
saveVersionClient(targetVersion);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
|
||||
|
||||
@@ -7,7 +7,21 @@ const { spawn } = require('child_process');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { getResolvedAppDir, findClientPath } = require('../core/paths');
|
||||
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils');
|
||||
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain, loadVersionBranch, loadVersionClient, saveVersionClient } = require('../core/config');
|
||||
const {
|
||||
saveInstallPath,
|
||||
loadJavaPath,
|
||||
getUuidForUser,
|
||||
getAuthServerUrl,
|
||||
getAuthDomain,
|
||||
loadVersionBranch,
|
||||
loadVersionClient,
|
||||
saveVersionClient,
|
||||
loadUsername,
|
||||
hasUsername,
|
||||
checkLaunchReady,
|
||||
loadWrapperConfig,
|
||||
generateWrapperScript
|
||||
} = require('../core/config');
|
||||
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
||||
const { getLatestClientVersion } = require('../services/versionManager');
|
||||
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
||||
@@ -15,6 +29,7 @@ const { ensureGameInstalled } = require('./differentialUpdateManager');
|
||||
const { syncModsForCurrentProfile } = require('./modManager');
|
||||
const { getUserDataPath } = require('../utils/userDataMigration');
|
||||
const { syncServerList } = require('../utils/serverListSync');
|
||||
const { killGameProcesses } = require('./gameManager');
|
||||
|
||||
// Client patcher for custom auth server (sanasol.ws)
|
||||
let clientPatcher = null;
|
||||
@@ -49,12 +64,39 @@ async function fetchAuthTokens(uuid, name) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Auth tokens received from server');
|
||||
const identityToken = data.IdentityToken || data.identityToken;
|
||||
const sessionToken = data.SessionToken || data.sessionToken;
|
||||
|
||||
return {
|
||||
identityToken: data.IdentityToken || data.identityToken,
|
||||
sessionToken: data.SessionToken || data.sessionToken
|
||||
};
|
||||
// Verify the identity token has the correct username
|
||||
// This catches cases where the auth server defaults to "Player"
|
||||
try {
|
||||
const parts = identityToken.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
||||
if (payload.username && payload.username !== name && name !== 'Player') {
|
||||
console.warn(`[Auth] Token username mismatch: token has "${payload.username}", expected "${name}". Retrying...`);
|
||||
// Retry once with explicit name
|
||||
const retryResponse = await fetch(`${authServerUrl}/game-session/child`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uuid: uuid, name: name, scopes: ['hytale:server', 'hytale:client'] })
|
||||
});
|
||||
if (retryResponse.ok) {
|
||||
const retryData = await retryResponse.json();
|
||||
console.log('[Auth] Retry successful');
|
||||
return {
|
||||
identityToken: retryData.IdentityToken || retryData.identityToken,
|
||||
sessionToken: retryData.SessionToken || retryData.sessionToken
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (verifyErr) {
|
||||
console.warn('[Auth] Token verification skipped:', verifyErr.message);
|
||||
}
|
||||
|
||||
console.log('Auth tokens received from server');
|
||||
return { identityToken, sessionToken };
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch auth tokens:', error.message);
|
||||
// Fallback to local generation if server unavailable
|
||||
@@ -104,8 +146,56 @@ function generateLocalTokens(uuid, name) {
|
||||
};
|
||||
}
|
||||
|
||||
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||
// Synchronize server list on every game launch
|
||||
async function launchGame(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||
// ==========================================================================
|
||||
// CACHE INVALIDATION: Clear proxyClient module cache to force fresh .env load
|
||||
// This prevents stale cached values from affecting multiple launch attempts
|
||||
// ==========================================================================
|
||||
try {
|
||||
const proxyClientPath = require.resolve('../utils/proxyClient');
|
||||
if (require.cache[proxyClientPath]) {
|
||||
delete require.cache[proxyClientPath];
|
||||
console.log('[Launcher] Cleared proxyClient cache for fresh .env load');
|
||||
}
|
||||
} catch (cacheErr) {
|
||||
console.warn('[Launcher] Could not clear proxyClient cache:', cacheErr.message);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// STEP 1: Validate player identity FIRST (before any other operations)
|
||||
// ==========================================================================
|
||||
const launchState = checkLaunchReady();
|
||||
|
||||
// Load username from config - single source of truth
|
||||
let playerName = loadUsername();
|
||||
|
||||
if (!playerName) {
|
||||
// No username configured - this is a critical error
|
||||
const error = new Error('No username configured. Please set your username in Settings before playing.');
|
||||
console.error('[Launcher] Launch blocked:', error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Allow override only if explicitly provided (for testing/migration)
|
||||
if (playerNameOverride && typeof playerNameOverride === 'string' && playerNameOverride.trim()) {
|
||||
const overrideName = playerNameOverride.trim();
|
||||
if (overrideName !== playerName && overrideName !== 'Player') {
|
||||
console.warn(`[Launcher] Username override requested: "${overrideName}" (saved: "${playerName}")`);
|
||||
// Use override for this session but DON'T save it - config is source of truth
|
||||
playerName = overrideName;
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if using default 'Player' name (likely misconfiguration)
|
||||
if (playerName === 'Player') {
|
||||
console.warn('[Launcher] Warning: Using default username "Player". This may cause cosmetic issues.');
|
||||
}
|
||||
|
||||
console.log(`[Launcher] Launching game for player: "${playerName}"`);
|
||||
|
||||
// ==========================================================================
|
||||
// STEP 2: Synchronize server list
|
||||
// ==========================================================================
|
||||
try {
|
||||
console.log('[Launcher] Synchronizing server list...');
|
||||
await syncServerList();
|
||||
@@ -113,11 +203,14 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
console.warn('[Launcher] Server list sync failed, continuing launch:', syncError.message);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// STEP 3: Setup paths and directories
|
||||
// ==========================================================================
|
||||
const branch = branchOverride || loadVersionBranch();
|
||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||
|
||||
|
||||
// NEW 2.2.0: Use centralized UserData location
|
||||
const userDataDir = getUserDataPath();
|
||||
|
||||
@@ -128,7 +221,10 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
throw new Error('Game is not installed. Please install the game first.');
|
||||
}
|
||||
|
||||
saveUsername(playerName);
|
||||
// NOTE: We do NOT save username here anymore - username is only saved
|
||||
// when user explicitly changes it in Settings. This prevents accidental
|
||||
// overwrites from race conditions or default values.
|
||||
|
||||
if (installPathOverride) {
|
||||
saveInstallPath(installPathOverride);
|
||||
}
|
||||
@@ -157,6 +253,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
}
|
||||
|
||||
const uuid = getUuidForUser(playerName);
|
||||
console.log(`[Launcher] UUID for "${playerName}": ${uuid} (verify this stays constant across launches)`);
|
||||
|
||||
// Fetch tokens from auth server
|
||||
if (progressCallback) {
|
||||
@@ -186,8 +283,8 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
if (patchResult.client) {
|
||||
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
|
||||
}
|
||||
if (patchResult.server) {
|
||||
console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
|
||||
if (patchResult.agent) {
|
||||
console.log(` Agent: ${patchResult.agent.alreadyExists ? 'already present' : patchResult.agent.success ? 'downloaded' : 'failed'}`);
|
||||
}
|
||||
} else {
|
||||
console.warn('Game patching failed:', patchResult.error);
|
||||
@@ -233,23 +330,13 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
console.log('Signed server binaries (after patching)');
|
||||
}
|
||||
|
||||
// Create java wrapper (must be signed on macOS)
|
||||
if (javaBin && fs.existsSync(javaBin)) {
|
||||
const javaWrapperPath = path.join(path.dirname(javaBin), 'java-wrapper');
|
||||
const wrapperScript = `#!/bin/bash
|
||||
# Java wrapper for macOS - adds --disable-sentry to fix Sentry hang issue
|
||||
REAL_JAVA="${javaBin}"
|
||||
ARGS=("$@")
|
||||
for i in "\${!ARGS[@]}"; do
|
||||
if [[ "\${ARGS[$i]}" == *"HytaleServer.jar"* ]]; then
|
||||
ARGS=("\${ARGS[@]:0:$((i+1))}" "--disable-sentry" "\${ARGS[@]:$((i+1))}")
|
||||
break
|
||||
fi
|
||||
done
|
||||
exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
`;
|
||||
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'darwin', javaBin);
|
||||
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
|
||||
await signPath(javaWrapperPath, false);
|
||||
console.log('Created java wrapper with --disable-sentry fix');
|
||||
console.log('Created java wrapper from config template');
|
||||
javaBin = javaWrapperPath;
|
||||
}
|
||||
} catch (signError) {
|
||||
@@ -258,6 +345,40 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
}
|
||||
}
|
||||
|
||||
// Windows: Create java wrapper to strip/inject JVM flags per wrapper config
|
||||
if (process.platform === 'win32' && javaBin && fs.existsSync(javaBin)) {
|
||||
try {
|
||||
const javaDir = path.dirname(javaBin);
|
||||
const javaOriginal = path.join(javaDir, 'java-original.exe');
|
||||
const javaWrapperPath = path.join(javaDir, 'java-wrapper.bat');
|
||||
|
||||
if (!fs.existsSync(javaOriginal)) {
|
||||
fs.copyFileSync(javaBin, javaOriginal);
|
||||
console.log('Backed up java.exe as java-original.exe');
|
||||
}
|
||||
|
||||
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'win32', null);
|
||||
fs.writeFileSync(javaWrapperPath, wrapperScript);
|
||||
console.log('Created Windows java wrapper from config template');
|
||||
javaBin = javaWrapperPath;
|
||||
} catch (wrapperError) {
|
||||
console.log('Notice: Windows java wrapper creation failed:', wrapperError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Linux: Create java wrapper to strip/inject JVM flags per wrapper config
|
||||
if (process.platform === 'linux' && javaBin && fs.existsSync(javaBin)) {
|
||||
try {
|
||||
const javaWrapperPath = path.join(path.dirname(javaBin), 'java-wrapper');
|
||||
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'linux', javaBin);
|
||||
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
|
||||
console.log('Created Linux java wrapper from config template');
|
||||
javaBin = javaWrapperPath;
|
||||
} catch (wrapperError) {
|
||||
console.log('Notice: Linux java wrapper creation failed:', wrapperError.message);
|
||||
}
|
||||
}
|
||||
|
||||
const args = [
|
||||
'--app-dir', gameLatest,
|
||||
'--java-exec', javaBin,
|
||||
@@ -287,14 +408,12 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
console.log('Starting game...');
|
||||
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
|
||||
|
||||
const env = { ...process.env };
|
||||
|
||||
const waylandEnv = setupWaylandEnvironment();
|
||||
Object.assign(env, waylandEnv);
|
||||
|
||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||
Object.assign(env, gpuEnv);
|
||||
const env = { ...process.env };
|
||||
|
||||
const waylandEnv = setupWaylandEnvironment();
|
||||
Object.assign(env, waylandEnv);
|
||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||
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') {
|
||||
@@ -304,9 +423,9 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
|
||||
// Common system libzstd paths
|
||||
const systemLibzstdPaths = [
|
||||
'/usr/lib64/libzstd.so.1', // Fedora/RHEL
|
||||
'/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
|
||||
'/usr/lib/x86_64-linux-gnu/libzstd.so.1' // Debian/Ubuntu
|
||||
];
|
||||
|
||||
let systemLibzstd = null;
|
||||
@@ -344,6 +463,33 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
}
|
||||
}
|
||||
|
||||
// Kill any stalled game processes from a previous launch to prevent file locks
|
||||
// and "game already running" issues
|
||||
await killGameProcesses();
|
||||
|
||||
// Remove AOT cache: generated by official Hytale JRE, incompatible with F2P JRE.
|
||||
// Client adds -XX:AOTCache when this file exists, causing classloading failures.
|
||||
const aotCache = path.join(gameLatest, 'Server', 'HytaleServer.aot');
|
||||
if (fs.existsSync(aotCache)) {
|
||||
try {
|
||||
fs.unlinkSync(aotCache);
|
||||
console.log('Removed incompatible AOT cache (HytaleServer.aot)');
|
||||
} catch (aotErr) {
|
||||
console.warn('Could not remove AOT cache:', aotErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
// DualAuth Agent: Set JAVA_TOOL_OPTIONS so java picks up -javaagent: flag
|
||||
// This enables runtime auth patching without modifying the server JAR
|
||||
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');
|
||||
if (fs.existsSync(agentJar)) {
|
||||
const agentFlag = `-javaagent:"${agentJar}"`;
|
||||
env.JAVA_TOOL_OPTIONS = env.JAVA_TOOL_OPTIONS
|
||||
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag}`
|
||||
: agentFlag;
|
||||
console.log('DualAuth Agent: enabled via JAVA_TOOL_OPTIONS');
|
||||
}
|
||||
|
||||
try {
|
||||
let spawnOptions = {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
@@ -358,23 +504,35 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
|
||||
const child = spawn(clientPath, args, spawnOptions);
|
||||
|
||||
// Release process reference immediately so it's truly independent
|
||||
// This works on all platforms (Windows, macOS, Linux)
|
||||
child.unref();
|
||||
|
||||
console.log(`Game process started with PID: ${child.pid}`);
|
||||
|
||||
let hasExited = false;
|
||||
let outputReceived = false;
|
||||
let launchCheckTimeout;
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
outputReceived = true;
|
||||
console.log(`Game output: ${data.toString().trim()}`);
|
||||
});
|
||||
if (child.stdout) {
|
||||
child.stdout.on('data', (data) => {
|
||||
outputReceived = true;
|
||||
const msg = data.toString().trim();
|
||||
console.log(`Game output: ${msg}`);
|
||||
});
|
||||
}
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
outputReceived = true;
|
||||
console.error(`Game error: ${data.toString().trim()}`);
|
||||
});
|
||||
if (child.stderr) {
|
||||
child.stderr.on('data', (data) => {
|
||||
outputReceived = true;
|
||||
const msg = data.toString().trim();
|
||||
console.error(`Game error: ${msg}`);
|
||||
});
|
||||
}
|
||||
|
||||
child.on('error', (error) => {
|
||||
hasExited = true;
|
||||
clearTimeout(launchCheckTimeout);
|
||||
console.error(`Failed to start game process: ${error.message}`);
|
||||
if (progressCallback) {
|
||||
progressCallback(`Failed to start game: ${error.message}`, -1, null, null, null);
|
||||
@@ -383,30 +541,30 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
hasExited = true;
|
||||
clearTimeout(launchCheckTimeout);
|
||||
|
||||
if (code !== null) {
|
||||
console.log(`Game process exited with code ${code}`);
|
||||
if (code !== 0 && progressCallback) {
|
||||
progressCallback(`Game exited with error code ${code}`, -1, null, null, null);
|
||||
if (code !== 0) {
|
||||
console.error(`[Launcher] Game crashed or exited with error code ${code}`);
|
||||
if (progressCallback) {
|
||||
progressCallback(`Game exited with error code ${code}`, -1, null, null, null);
|
||||
}
|
||||
}
|
||||
} else if (signal) {
|
||||
console.log(`Game process terminated by signal ${signal}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor game process status in background
|
||||
setTimeout(() => {
|
||||
if (!hasExited) {
|
||||
console.log('Game appears to be running successfully');
|
||||
child.unref();
|
||||
if (progressCallback) {
|
||||
progressCallback('Game launched successfully', 100, null, null, null);
|
||||
}
|
||||
} else if (!outputReceived) {
|
||||
console.warn('Game process exited immediately with no output - possible issue with game files or dependencies');
|
||||
}
|
||||
}, 3000);
|
||||
// Process is detached and unref'd - it runs independently from the launcher
|
||||
// We cannot reliably detect if the game window actually appears from here,
|
||||
// so we report success after spawning. stdout/stderr logging above provides debugging info.
|
||||
console.log('Game process spawned and detached successfully');
|
||||
if (progressCallback) {
|
||||
progressCallback('Game launched successfully', 100, null, null, null);
|
||||
}
|
||||
|
||||
// Return immediately, don't wait for setTimeout
|
||||
// Return immediately after spawn
|
||||
return { success: true, installed: true, launched: true, pid: child.pid };
|
||||
} catch (spawnError) {
|
||||
console.error(`Error spawning game process: ${spawnError.message}`);
|
||||
@@ -417,10 +575,26 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
}
|
||||
}
|
||||
|
||||
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||
async function launchGameWithVersionCheck(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||
try {
|
||||
// ==========================================================================
|
||||
// PRE-LAUNCH VALIDATION: Check username is configured
|
||||
// ==========================================================================
|
||||
const launchState = checkLaunchReady();
|
||||
|
||||
if (!launchState.hasUsername) {
|
||||
const error = 'No username configured. Please set your username in Settings before playing.';
|
||||
console.error('[Launcher] Launch blocked:', error);
|
||||
if (progressCallback) {
|
||||
progressCallback(error, -1, null, null, null);
|
||||
}
|
||||
return { success: false, error: error, needsUsername: true };
|
||||
}
|
||||
|
||||
console.log(`[Launcher] Pre-launch check passed. Username: "${launchState.username}"`);
|
||||
|
||||
const branch = branchOverride || loadVersionBranch();
|
||||
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Checking for updates...', 0, null, null, null);
|
||||
}
|
||||
@@ -474,7 +648,7 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
||||
progressCallback('Launching game...', 80, null, null, null);
|
||||
}
|
||||
|
||||
const launchResult = await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
|
||||
const launchResult = await launchGame(playerNameOverride, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
|
||||
|
||||
// Ensure we always return a result
|
||||
if (!launchResult) {
|
||||
|
||||
@@ -1,18 +1,110 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
const { execFile, exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||
const { getOS, getArch } = require('../utils/platformUtils');
|
||||
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
|
||||
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
|
||||
const { getLatestClientVersion, getInstalledClientVersion, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
|
||||
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
||||
const { installButler } = require('./butlerManager');
|
||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
||||
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
||||
const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration');
|
||||
const userDataBackup = require('../utils/userDataBackup');
|
||||
|
||||
async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Helper function to check if game processes are running
|
||||
async function isGameRunning() {
|
||||
try {
|
||||
let command;
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, check for HytaleClient.exe processes
|
||||
command = 'tasklist /FI "IMAGENAME eq HytaleClient.exe" /NH';
|
||||
} else if (process.platform === 'darwin') {
|
||||
// On macOS, check for HytaleClient processes
|
||||
command = 'pgrep -f HytaleClient';
|
||||
} else {
|
||||
// On Linux, check for HytaleClient processes
|
||||
command = 'pgrep -f HytaleClient';
|
||||
}
|
||||
|
||||
const { stdout } = await execAsync(command);
|
||||
return stdout.trim().length > 0;
|
||||
} catch (error) {
|
||||
// If command fails, assume no processes are running
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Force-kill stalled game processes to release file locks before repair/reinstall.
|
||||
// Cross-platform: Windows (taskkill/PowerShell), macOS (pkill), Linux (pkill).
|
||||
async function killGameProcesses() {
|
||||
const killed = [];
|
||||
|
||||
async function tryKill(command, label) {
|
||||
try {
|
||||
await execAsync(command);
|
||||
killed.push(label);
|
||||
} catch (_) { /* process not found is expected */ }
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Kill client
|
||||
await tryKill('taskkill /F /IM "HytaleClient.exe" /T', 'HytaleClient.exe');
|
||||
// Kill java.exe instances running HytaleServer.jar via PowerShell
|
||||
// (Get-CimInstance replaces deprecated wmic, works on Windows 10+)
|
||||
await tryKill(
|
||||
'powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name=\'java.exe\'\\" | Where-Object { $_.CommandLine -like \'*HytaleServer*\' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"',
|
||||
'java.exe(HytaleServer)'
|
||||
);
|
||||
} else {
|
||||
// macOS and Linux
|
||||
await tryKill('pkill -9 -f HytaleClient', 'HytaleClient');
|
||||
await tryKill('pkill -9 -f HytaleServer', 'HytaleServer');
|
||||
}
|
||||
|
||||
if (killed.length > 0) {
|
||||
console.log(`[GameManager] Force-killed stalled processes: ${killed.join(', ')}`);
|
||||
// Wait for OS to release file handles
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
return killed;
|
||||
}
|
||||
|
||||
// Helper function to safely remove directory with retry logic
|
||||
async function safeRemoveDirectory(dirPath, maxRetries = 3) {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
if (fs.existsSync(dirPath)) {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
console.log(`Successfully removed directory: ${dirPath}`);
|
||||
}
|
||||
return; // Success, exit the loop
|
||||
} catch (error) {
|
||||
console.warn(`Attempt ${attempt}/${maxRetries} failed to remove ${dirPath}: ${error.message}`);
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// On EPERM/EBUSY, try killing stalled game processes that hold file locks
|
||||
if (attempt === 1 && (error.code === 'EPERM' || error.code === 'EBUSY')) {
|
||||
console.log('Permission error detected, killing stalled game processes...');
|
||||
await killGameProcesses();
|
||||
}
|
||||
// Wait before retrying (exponential backoff)
|
||||
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
||||
console.log(`Waiting ${delay}ms before retry...`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
} else {
|
||||
// Last attempt failed, throw the error
|
||||
throw new Error(`Failed to remove directory ${dirPath} after ${maxRetries} attempts: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback, cacheDir = CACHE_DIR, manualRetry = false, directUrl = null, expectedSize = null) {
|
||||
const osName = getOS();
|
||||
const arch = getArch();
|
||||
|
||||
@@ -20,28 +112,69 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
|
||||
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}/${branch}/0/${fileName}`;
|
||||
const dest = path.join(cacheDir, `${branch}_${fileName}`);
|
||||
let url;
|
||||
|
||||
// Check if file exists and validate it
|
||||
if (fs.existsSync(dest) && !manualRetry) {
|
||||
console.log('PWR file found in cache:', 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;
|
||||
if (directUrl) {
|
||||
url = directUrl;
|
||||
console.log(`[DownloadPWR] Using direct URL: ${url}`);
|
||||
} else {
|
||||
const { getPWRUrl } = require('../services/versionManager');
|
||||
try {
|
||||
console.log(`[DownloadPWR] Fetching mirror URL for branch: ${branch}, version: ${fileName}`);
|
||||
url = await getPWRUrl(branch, fileName);
|
||||
console.log(`[DownloadPWR] Mirror URL: ${url}`);
|
||||
} catch (error) {
|
||||
console.error(`[DownloadPWR] Failed to get mirror URL: ${error.message}`);
|
||||
const { getPatchesBaseUrl } = require('../services/versionManager');
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
url = `${baseUrl}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`;
|
||||
console.log(`[DownloadPWR] Fallback URL: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Fetching PWR patch file:', url);
|
||||
// Look up expected file size from manifest if not provided
|
||||
if (!expectedSize) {
|
||||
try {
|
||||
const { fetchMirrorManifest } = require('../services/versionManager');
|
||||
const manifest = await fetchMirrorManifest();
|
||||
// Try to match: "0_to_11" format or "v11" format
|
||||
const versionMatch = fileName.match(/^(\d+)_to_(\d+)$/);
|
||||
let manifestKey;
|
||||
if (versionMatch) {
|
||||
manifestKey = `${osName}/${arch}/${branch}/${fileName}.pwr`;
|
||||
} else {
|
||||
const buildNum = extractVersionNumber(fileName);
|
||||
manifestKey = `${osName}/${arch}/${branch}/0_to_${buildNum}.pwr`;
|
||||
}
|
||||
if (manifest.files[manifestKey]) {
|
||||
expectedSize = manifest.files[manifestKey].size;
|
||||
console.log(`[PWR] Expected size from manifest: ${(expectedSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[PWR] Could not fetch expected size from manifest: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`);
|
||||
|
||||
// Check if file exists and validate it
|
||||
if (fs.existsSync(dest) && !manualRetry) {
|
||||
const stats = fs.statSync(dest);
|
||||
if (stats.size > 1024 * 1024) {
|
||||
// Validate against expected size - reject if file is truncated (< 99% of expected)
|
||||
if (expectedSize && stats.size < expectedSize * 0.99) {
|
||||
console.log(`[PWR] Cached file truncated: ${(stats.size / 1024 / 1024).toFixed(2)} MB, expected ${(expectedSize / 1024 / 1024).toFixed(2)} MB. Deleting and re-downloading.`);
|
||||
fs.unlinkSync(dest);
|
||||
} else {
|
||||
console.log(`[PWR] Using cached file: ${dest} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
return dest;
|
||||
}
|
||||
} else {
|
||||
console.log(`[PWR] Cached file too small (${stats.size} bytes), re-downloading`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[DownloadPWR] Downloading from: ${url}`);
|
||||
|
||||
try {
|
||||
if (manualRetry) {
|
||||
@@ -67,7 +200,7 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
|
||||
const retryStats = fs.statSync(dest);
|
||||
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||
|
||||
if (!validatePWRFile(dest)) {
|
||||
if (!validatePWRFile(dest, expectedSize)) {
|
||||
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');
|
||||
@@ -117,8 +250,8 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
|
||||
// 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)) {
|
||||
|
||||
if (!validatePWRFile(dest, expectedSize)) {
|
||||
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');
|
||||
@@ -136,7 +269,7 @@ async function retryPWRDownload(branch, fileName, progressCallback, cacheDir = C
|
||||
return await downloadPWR(branch, fileName, progressCallback, cacheDir, true);
|
||||
}
|
||||
|
||||
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR) {
|
||||
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR, skipExistingCheck = false) {
|
||||
console.log(`[Butler] Starting PWR application with:`);
|
||||
console.log(`[Butler] - PWR file: ${pwrFile}`);
|
||||
console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`);
|
||||
@@ -160,11 +293,12 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
||||
const gameLatest = gameDir;
|
||||
const stagingDir = path.join(gameLatest, 'staging-temp');
|
||||
|
||||
const clientPath = findClientPath(gameLatest);
|
||||
|
||||
if (clientPath) {
|
||||
console.log('Game files detected, skipping patch installation.');
|
||||
return;
|
||||
if (!skipExistingCheck) {
|
||||
const clientPath = findClientPath(gameLatest);
|
||||
if (clientPath) {
|
||||
console.log('Game files detected, skipping patch installation.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and prepare directories
|
||||
@@ -345,57 +479,118 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
||||
}
|
||||
console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`);
|
||||
|
||||
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
||||
// Determine update strategy: intermediate patches vs full reinstall
|
||||
const currentVersion = loadVersionClient();
|
||||
const currentBuild = extractVersionNumber(currentVersion) || 0;
|
||||
const targetBuild = extractVersionNumber(newVersion);
|
||||
|
||||
if (fs.existsSync(tempUpdateDir)) {
|
||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(tempUpdateDir, { recursive: true });
|
||||
let useIntermediatePatches = false;
|
||||
let updatePlan = null;
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Downloading new game version...', 20, null, null, null);
|
||||
}
|
||||
|
||||
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Extracting new files...', 60, null, null, null);
|
||||
}
|
||||
|
||||
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
|
||||
// Delete PWR file from cache after successful update
|
||||
try {
|
||||
if (fs.existsSync(pwrFile)) {
|
||||
fs.unlinkSync(pwrFile);
|
||||
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
|
||||
if (currentBuild > 0 && currentBuild < targetBuild) {
|
||||
try {
|
||||
updatePlan = await getUpdatePlan(currentBuild, targetBuild, branch);
|
||||
useIntermediatePatches = !updatePlan.isFullInstall;
|
||||
if (useIntermediatePatches) {
|
||||
const totalMB = (updatePlan.totalSize / 1024 / 1024).toFixed(0);
|
||||
console.log(`[UpdateGameFiles] Using intermediate patches: ${updatePlan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${totalMB} MB)`);
|
||||
}
|
||||
} catch (planError) {
|
||||
console.warn('[UpdateGameFiles] Could not get update plan, falling back to full install:', planError.message);
|
||||
}
|
||||
} catch (delErr) {
|
||||
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('Replacing game files...', 80, null, null, null);
|
||||
}
|
||||
|
||||
if (fs.existsSync(gameDir)) {
|
||||
console.log('Removing old game files...');
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
if (useIntermediatePatches && updatePlan) {
|
||||
// Apply intermediate patches directly to game dir
|
||||
for (let i = 0; i < updatePlan.steps.length; i++) {
|
||||
const step = updatePlan.steps[i];
|
||||
const stepName = `${step.from}_to_${step.to}`;
|
||||
|
||||
if (progressCallback) {
|
||||
const progress = 20 + Math.round((i / updatePlan.steps.length) * 60);
|
||||
progressCallback(`Downloading patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, progress, null, null, null);
|
||||
}
|
||||
|
||||
const pwrFile = await downloadPWR(branch, stepName, progressCallback, cacheDir, false, step.url, step.size);
|
||||
|
||||
if (!pwrFile) {
|
||||
throw new Error(`Failed to download patch ${stepName}`);
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Applying patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, null, null, null, null);
|
||||
}
|
||||
|
||||
await applyPWR(pwrFile, progressCallback, gameDir, toolsDir, branch, cacheDir, true);
|
||||
|
||||
// Clean up PWR file from cache
|
||||
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;
|
||||
if (fs.existsSync(pwrFile)) {
|
||||
fs.unlinkSync(pwrFile);
|
||||
}
|
||||
} catch (delErr) {
|
||||
console.warn('[UpdateGameFiles] Failed to delete PWR from cache:', delErr.message);
|
||||
}
|
||||
|
||||
// Save intermediate version so we can resume if interrupted
|
||||
saveVersionClient(`v${step.to}`);
|
||||
console.log(`[UpdateGameFiles] Applied patch ${stepName} (${i + 1}/${updatePlan.steps.length})`);
|
||||
}
|
||||
} else {
|
||||
// Full install: download 0->target, apply to temp dir, swap
|
||||
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
||||
|
||||
if (fs.existsSync(tempUpdateDir)) {
|
||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(tempUpdateDir, { recursive: true });
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Downloading new game version...', 20, null, null, null);
|
||||
}
|
||||
|
||||
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Extracting new files...', 60, null, null, null);
|
||||
}
|
||||
|
||||
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(pwrFile)) {
|
||||
fs.unlinkSync(pwrFile);
|
||||
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
|
||||
}
|
||||
} catch (delErr) {
|
||||
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Replacing game files...', 80, null, null, null);
|
||||
}
|
||||
|
||||
if (fs.existsSync(gameDir)) {
|
||||
console.log('Removing old game files...');
|
||||
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);
|
||||
}
|
||||
|
||||
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
|
||||
console.log('HomePage.ui update result after update:', homeUIResult);
|
||||
@@ -472,7 +667,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
||||
}
|
||||
});
|
||||
|
||||
saveUsername(playerName);
|
||||
// NOTE: Do NOT save username here - username should only be saved when user explicitly
|
||||
// changes it in Settings. Saving here could overwrite a good username with 'Player' default.
|
||||
// The username is only needed for launching, not for installing.
|
||||
if (installPathOverride) {
|
||||
saveInstallPath(installPathOverride);
|
||||
}
|
||||
@@ -587,8 +784,14 @@ async function uninstallGame() {
|
||||
throw new Error('Game is not installed');
|
||||
}
|
||||
|
||||
// Check if game is running before attempting to delete files
|
||||
const gameRunning = await isGameRunning();
|
||||
if (gameRunning) {
|
||||
throw new Error('Cannot uninstall game while it is running. Please close the game first.');
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmSync(appDir, { recursive: true, force: true });
|
||||
await safeRemoveDirectory(appDir);
|
||||
console.log('Game uninstalled successfully - removed entire HytaleF2P folder');
|
||||
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
@@ -670,14 +873,34 @@ async function repairGame(progressCallback, branchOverride = null) {
|
||||
progressCallback('Removing old game files...', 30, null, null, null);
|
||||
}
|
||||
|
||||
// Delete Game and Cache Directory
|
||||
// Kill stalled game processes before attempting to delete files
|
||||
const gameRunning = await isGameRunning();
|
||||
if (gameRunning) {
|
||||
console.warn('[RepairGame] Game processes detected. Force-killing to release file locks...');
|
||||
if (progressCallback) {
|
||||
progressCallback('Stopping stalled game processes...', 20, null, null, null);
|
||||
}
|
||||
await killGameProcesses();
|
||||
}
|
||||
|
||||
// Delete Game and Cache Directory with retry logic
|
||||
console.log('Removing corrupted game files...');
|
||||
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||
try {
|
||||
await safeRemoveDirectory(gameDir);
|
||||
} catch (error) {
|
||||
console.error(`[RepairGame] Failed to remove game directory: ${error.message}`);
|
||||
throw new Error(`Cannot repair game: ${error.message}. Please ensure the game is not running and try again.`);
|
||||
}
|
||||
|
||||
const cacheDir = path.join(appDir, 'cache');
|
||||
if (fs.existsSync(cacheDir)) {
|
||||
console.log('Clearing cache directory...');
|
||||
fs.rmSync(cacheDir, { recursive: true, force: true });
|
||||
try {
|
||||
await safeRemoveDirectory(cacheDir);
|
||||
} catch (error) {
|
||||
console.warn(`[RepairGame] Failed to clear cache directory: ${error.message}`);
|
||||
// Don't throw here, cache cleanup is not critical
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Reinstalling game files...');
|
||||
@@ -741,36 +964,30 @@ function validateGameDirectory(gameDir, stagingDir) {
|
||||
}
|
||||
|
||||
// Enhanced PWR file validation
|
||||
function validatePWRFile(filePath) {
|
||||
// Accepts intermediate patches (50+ MB) and full installs (1.5+ GB)
|
||||
function validatePWRFile(filePath, expectedSize = null) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const stats = fs.statSync(filePath);
|
||||
const sizeInMB = stats.size / 1024 / 1024;
|
||||
|
||||
|
||||
// PWR files should be at least 1 MB
|
||||
if (stats.size < 1024 * 1024) {
|
||||
console.log(`[PWR Validation] File too small: ${sizeInMB.toFixed(2)} MB`);
|
||||
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`);
|
||||
|
||||
// Validate against expected size if known (reject if < 99% of expected)
|
||||
if (expectedSize && stats.size < expectedSize * 0.99) {
|
||||
const expectedMB = expectedSize / 1024 / 1024;
|
||||
console.log(`[PWR Validation] File truncated: ${sizeInMB.toFixed(2)} MB, expected ${expectedMB.toFixed(2)} MB`);
|
||||
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}`);
|
||||
|
||||
|
||||
console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[PWR Validation] Error:`, error.message);
|
||||
@@ -790,5 +1007,6 @@ module.exports = {
|
||||
installGame,
|
||||
uninstallGame,
|
||||
checkExistingGameInstallation,
|
||||
repairGame
|
||||
repairGame,
|
||||
killGameProcesses
|
||||
};
|
||||
|
||||
@@ -340,36 +340,70 @@ async function extractJRE(archivePath, destDir) {
|
||||
}
|
||||
|
||||
function extractZip(zipPath, dest) {
|
||||
const zip = new AdmZip(zipPath);
|
||||
const entries = zip.getEntries();
|
||||
try {
|
||||
const zip = new AdmZip(zipPath);
|
||||
const entries = zip.getEntries();
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dest, entry.entryName);
|
||||
|
||||
const resolvedPath = path.resolve(entryPath);
|
||||
const resolvedDest = path.resolve(dest);
|
||||
if (!resolvedPath.startsWith(resolvedDest)) {
|
||||
throw new Error(`Invalid file path detected: ${entryPath}`);
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dest, entry.entryName);
|
||||
|
||||
if (entry.isDirectory) {
|
||||
fs.mkdirSync(entryPath, { recursive: true });
|
||||
} else {
|
||||
fs.mkdirSync(path.dirname(entryPath), { recursive: true });
|
||||
fs.writeFileSync(entryPath, entry.getData());
|
||||
if (process.platform !== 'win32') {
|
||||
fs.chmodSync(entryPath, entry.header.attr >>> 16);
|
||||
// Security check: prevent zip slip attacks
|
||||
const resolvedPath = path.resolve(entryPath);
|
||||
const resolvedDest = path.resolve(dest);
|
||||
if (!resolvedPath.startsWith(resolvedDest)) {
|
||||
throw new Error(`Invalid file path detected: ${entryPath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (entry.isDirectory) {
|
||||
fs.mkdirSync(entryPath, { recursive: true });
|
||||
} else {
|
||||
// Ensure parent directory exists
|
||||
const parentDir = path.dirname(entryPath);
|
||||
fs.mkdirSync(parentDir, { recursive: true });
|
||||
|
||||
// Get file data and write it
|
||||
const data = entry.getData();
|
||||
if (!data) {
|
||||
console.warn(`Warning: No data for file ${entry.entryName}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.writeFileSync(entryPath, data);
|
||||
|
||||
// Set permissions on non-Windows platforms
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
const mode = entry.header.attr >>> 16;
|
||||
if (mode > 0) {
|
||||
fs.chmodSync(entryPath, mode);
|
||||
}
|
||||
} catch (chmodError) {
|
||||
console.warn(`Warning: Could not set permissions for ${entryPath}: ${chmodError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (entryError) {
|
||||
console.error(`Error extracting ${entry.entryName}: ${entryError.message}`);
|
||||
// Continue with other entries rather than failing completely
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to extract ZIP archive: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function extractTarGz(tarGzPath, dest) {
|
||||
return tar.extract({
|
||||
file: tarGzPath,
|
||||
cwd: dest,
|
||||
strip: 0
|
||||
});
|
||||
try {
|
||||
return tar.extract({
|
||||
file: tarGzPath,
|
||||
cwd: dest,
|
||||
strip: 0
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to extract TAR.GZ archive: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function flattenJREDir(jreLatest) {
|
||||
|
||||
@@ -285,6 +285,27 @@ async function toggleMod(modId, modsPath) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getModFiles(modId) {
|
||||
try {
|
||||
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modId}/files`, {
|
||||
headers: {
|
||||
'x-api-key': API_KEY,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params: {
|
||||
pageSize: 20,
|
||||
sortOrder: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching mod files:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function syncModsForCurrentProfile() {
|
||||
try {
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
@@ -455,5 +476,6 @@ module.exports = {
|
||||
syncModsForCurrentProfile,
|
||||
generateModId,
|
||||
extractModName,
|
||||
extractVersion
|
||||
};
|
||||
extractVersion,
|
||||
getModFiles
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { downloadFile, findHomePageUIPath, findLogoPath } = require('../utils/fileManager');
|
||||
const { smartRequest } = require('../utils/proxyClient');
|
||||
|
||||
async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
|
||||
try {
|
||||
@@ -13,7 +14,8 @@ async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
|
||||
const homeUIUrl = 'https://files.hytalef2p.com/api/HomeUI';
|
||||
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
|
||||
|
||||
await downloadFile(homeUIUrl, tempHomePath);
|
||||
const response = await smartRequest(homeUIUrl, { responseType: 'arraybuffer' });
|
||||
fs.writeFileSync(tempHomePath, response.data);
|
||||
|
||||
const existingHomePath = findHomePageUIPath(gameDir);
|
||||
|
||||
@@ -66,7 +68,8 @@ async function downloadAndReplaceLogo(gameDir, progressCallback) {
|
||||
const logoUrl = 'https://files.hytalef2p.com/api/Logo';
|
||||
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
|
||||
|
||||
await downloadFile(logoUrl, tempLogoPath);
|
||||
const response = await smartRequest(logoUrl, { responseType: 'arraybuffer' });
|
||||
fs.writeFileSync(tempLogoPath, response.data);
|
||||
|
||||
const existingLogoPath = findLogoPath(gameDir);
|
||||
|
||||
|
||||
@@ -3,32 +3,117 @@ const path = require('path');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
|
||||
|
||||
function getOrCreatePlayerId() {
|
||||
try {
|
||||
if (!fs.existsSync(APP_DIR)) {
|
||||
fs.mkdirSync(APP_DIR, { recursive: true });
|
||||
}
|
||||
/**
|
||||
* DEPRECATED: This file is kept for backward compatibility.
|
||||
*
|
||||
* The primary UUID system is now in config.js using userUuids.
|
||||
* This player_id.json system was a separate UUID storage that could
|
||||
* cause desync issues.
|
||||
*
|
||||
* New code should use config.js functions:
|
||||
* - getUuidForUser(username) - Get/create UUID for a username
|
||||
* - getCurrentUuid() - Get current user's UUID
|
||||
* - setUuidForUser(username, uuid) - Set UUID for a user
|
||||
*
|
||||
* This function is kept for migration purposes only.
|
||||
*/
|
||||
|
||||
if (fs.existsSync(PLAYER_ID_FILE)) {
|
||||
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
|
||||
if (data.playerId) {
|
||||
return data.playerId;
|
||||
/**
|
||||
* Get or create a legacy player ID
|
||||
* NOTE: This is DEPRECATED - use config.js getUuidForUser() instead
|
||||
*
|
||||
* FIXED: No longer returns random UUID on error - throws instead
|
||||
*/
|
||||
function getOrCreatePlayerId() {
|
||||
const maxRetries = 3;
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
if (!fs.existsSync(APP_DIR)) {
|
||||
fs.mkdirSync(APP_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
if (fs.existsSync(PLAYER_ID_FILE)) {
|
||||
const data = fs.readFileSync(PLAYER_ID_FILE, 'utf8');
|
||||
if (data.trim()) {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.playerId) {
|
||||
return parsed.playerId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No existing ID - create new one atomically
|
||||
const newPlayerId = uuidv4();
|
||||
const tempFile = PLAYER_ID_FILE + '.tmp';
|
||||
const playerData = {
|
||||
playerId: newPlayerId,
|
||||
createdAt: new Date().toISOString(),
|
||||
note: 'DEPRECATED: This file is for legacy compatibility. UUID is now stored in config.json userUuids.'
|
||||
};
|
||||
|
||||
// Write to temp file first
|
||||
fs.writeFileSync(tempFile, JSON.stringify(playerData, null, 2));
|
||||
|
||||
// Atomic rename
|
||||
fs.renameSync(tempFile, PLAYER_ID_FILE);
|
||||
|
||||
console.log(`[PlayerManager] Created new legacy player ID: ${newPlayerId}`);
|
||||
return newPlayerId;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error(`[PlayerManager] Attempt ${attempt}/${maxRetries} failed:`, error.message);
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
// Small delay before retry
|
||||
const delay = attempt * 100;
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < delay) {
|
||||
// Busy wait
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPlayerId = uuidv4();
|
||||
fs.writeFileSync(PLAYER_ID_FILE, JSON.stringify({
|
||||
playerId: newPlayerId,
|
||||
createdAt: new Date().toISOString()
|
||||
}, null, 2));
|
||||
// FIXED: Do NOT return random UUID - throw error instead
|
||||
// Returning random UUID was causing silent identity loss
|
||||
console.error('[PlayerManager] CRITICAL: Failed to get/create player ID after all retries');
|
||||
throw new Error(`Failed to manage player ID: ${lastError.message}`);
|
||||
}
|
||||
|
||||
return newPlayerId;
|
||||
/**
|
||||
* Migrate legacy player_id.json to config.json userUuids
|
||||
* Call this during app startup
|
||||
*/
|
||||
function migrateLegacyPlayerId() {
|
||||
try {
|
||||
if (!fs.existsSync(PLAYER_ID_FILE)) {
|
||||
return null; // No legacy file to migrate
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
|
||||
if (!data.playerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[PlayerManager] Found legacy player_id.json with ID: ${data.playerId}`);
|
||||
|
||||
// Mark file as migrated by renaming
|
||||
const migratedFile = PLAYER_ID_FILE + '.migrated';
|
||||
if (!fs.existsSync(migratedFile)) {
|
||||
fs.renameSync(PLAYER_ID_FILE, migratedFile);
|
||||
console.log('[PlayerManager] Legacy player_id.json marked as migrated');
|
||||
}
|
||||
|
||||
return data.playerId;
|
||||
} catch (error) {
|
||||
console.error('Error managing player ID:', error);
|
||||
return uuidv4();
|
||||
console.error('[PlayerManager] Error during legacy migration:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOrCreatePlayerId
|
||||
getOrCreatePlayerId,
|
||||
migrateLegacyPlayerId
|
||||
};
|
||||
|
||||
@@ -1,134 +1,500 @@
|
||||
const axios = require('axios');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getOS, getArch } = require('../utils/platformUtils');
|
||||
|
||||
const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches';
|
||||
const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest';
|
||||
// Patches base URL fetched dynamically via multi-source fallback chain
|
||||
const AUTH_DOMAIN = process.env.HYTALE_AUTH_DOMAIN || 'auth.sanasol.ws';
|
||||
const PATCHES_CONFIG_SOURCES = [
|
||||
{ type: 'http', url: `https://${AUTH_DOMAIN}/api/patches-config`, name: 'primary' },
|
||||
{ type: 'http', url: 'https://htdwnldsan.top/patches-config', name: 'backup-1' },
|
||||
{ type: 'http', url: 'https://dl1.htdwnldsan.top/patches-config', name: 'backup-2' },
|
||||
{ type: 'doh', name: '_patches.htdwnldsan.top', name_label: 'dns-txt' },
|
||||
];
|
||||
const HARDCODED_FALLBACK = 'https://dl.vboro.de/patches';
|
||||
|
||||
async function getLatestClientVersion(branch = 'release') {
|
||||
// Alternative mirrors (non-Cloudflare) for regions where CF is blocked
|
||||
const NON_CF_MIRRORS = [
|
||||
'https://dl1.htdwnldsan.top',
|
||||
'https://htdwnldsan.top/patches',
|
||||
];
|
||||
|
||||
// Fallback: latest known build number if manifest is unreachable
|
||||
const FALLBACK_LATEST_BUILD = 11;
|
||||
|
||||
let patchesBaseUrl = null;
|
||||
let patchesConfigTime = 0;
|
||||
const PATCHES_CONFIG_CACHE_DURATION = 300000; // 5 minutes
|
||||
|
||||
let manifestCache = null;
|
||||
let manifestCacheTime = 0;
|
||||
const MANIFEST_CACHE_DURATION = 60000; // 1 minute
|
||||
|
||||
// Disk cache path for patches URL (survives restarts)
|
||||
function getDiskCachePath() {
|
||||
const os = require('os');
|
||||
const home = os.homedir();
|
||||
let appDir;
|
||||
if (process.platform === 'win32') {
|
||||
appDir = path.join(home, 'AppData', 'Local', 'HytaleF2P');
|
||||
} else if (process.platform === 'darwin') {
|
||||
appDir = path.join(home, 'Library', 'Application Support', 'HytaleF2P');
|
||||
} else {
|
||||
appDir = path.join(home, '.hytalef2p');
|
||||
}
|
||||
return path.join(appDir, 'patches-url-cache.json');
|
||||
}
|
||||
|
||||
function saveDiskCache(url) {
|
||||
try {
|
||||
console.log(`Fetching latest client version from API (branch: ${branch})...`);
|
||||
const response = await axios.get('https://files.hytalef2p.com/api/version_client', {
|
||||
params: { branch },
|
||||
timeout: 40000,
|
||||
headers: {
|
||||
'User-Agent': 'Hytale-F2P-Launcher'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data && response.data.client_version) {
|
||||
const version = response.data.client_version;
|
||||
console.log(`Latest client version for ${branch}: ${version}`);
|
||||
return version;
|
||||
} else {
|
||||
console.log('Warning: Invalid API response, falling back to latest known version (7.pwr)');
|
||||
return '7.pwr';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching client version:', error.message);
|
||||
console.log('Warning: API unavailable, falling back to latest known version (7.pwr)');
|
||||
return '7.pwr';
|
||||
const cachePath = getDiskCachePath();
|
||||
const dir = path.dirname(cachePath);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(cachePath, JSON.stringify({ patches_url: url, ts: Date.now() }), 'utf8');
|
||||
} catch (e) {
|
||||
// Non-critical, ignore
|
||||
}
|
||||
}
|
||||
|
||||
function buildArchiveUrl(buildNumber, branch = 'release') {
|
||||
function loadDiskCache() {
|
||||
try {
|
||||
const cachePath = getDiskCachePath();
|
||||
if (fs.existsSync(cachePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
||||
if (data && data.patches_url) return data.patches_url;
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-critical, ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch patches URL from a single HTTP config endpoint
|
||||
*/
|
||||
async function fetchFromHttp(url) {
|
||||
const response = await axios.get(url, {
|
||||
timeout: 8000,
|
||||
headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
|
||||
});
|
||||
if (response.data && response.data.patches_url) {
|
||||
return response.data.patches_url.replace(/\/+$/, '');
|
||||
}
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch patches URL from DNS TXT record via DNS-over-HTTPS
|
||||
*/
|
||||
async function fetchFromDoh(recordName) {
|
||||
const dohEndpoints = [
|
||||
{ url: 'https://dns.google/resolve', params: { name: recordName, type: 'TXT' } },
|
||||
{ url: 'https://cloudflare-dns.com/dns-query', params: { name: recordName, type: 'TXT' }, headers: { 'Accept': 'application/dns-json' } },
|
||||
];
|
||||
|
||||
for (const endpoint of dohEndpoints) {
|
||||
try {
|
||||
const response = await axios.get(endpoint.url, {
|
||||
params: endpoint.params,
|
||||
headers: { 'User-Agent': 'Hytale-F2P-Launcher', ...(endpoint.headers || {}) },
|
||||
timeout: 5000
|
||||
});
|
||||
const answers = response.data && response.data.Answer;
|
||||
if (answers && answers.length > 0) {
|
||||
// TXT records are quoted, strip quotes
|
||||
const txt = answers[0].data.replace(/^"|"$/g, '');
|
||||
if (txt.startsWith('http')) return txt.replace(/\/+$/, '');
|
||||
}
|
||||
} catch (e) {
|
||||
// Try next DoH endpoint
|
||||
}
|
||||
}
|
||||
throw new Error('All DoH endpoints failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch patches base URL with hardened multi-source fallback chain:
|
||||
* 1. Memory cache (5 min)
|
||||
* 2. HTTP: auth.sanasol.ws (primary)
|
||||
* 3. HTTP: htdwnldsan.top (backup, different host/domain/registrar)
|
||||
* 4. DNS TXT: _patches.htdwnldsan.top via DoH (different protocol layer)
|
||||
* 5. Disk cache (survives restarts, never expires)
|
||||
* 6. Hardcoded fallback URL (last resort)
|
||||
*/
|
||||
async function getPatchesBaseUrl() {
|
||||
const now = Date.now();
|
||||
|
||||
// 1. Memory cache
|
||||
if (patchesBaseUrl && (now - patchesConfigTime) < PATCHES_CONFIG_CACHE_DURATION) {
|
||||
return patchesBaseUrl;
|
||||
}
|
||||
|
||||
// 2-4. Try all sources: HTTP endpoints first, then DoH
|
||||
for (const source of PATCHES_CONFIG_SOURCES) {
|
||||
try {
|
||||
let url;
|
||||
if (source.type === 'http') {
|
||||
console.log(`[Mirror] Trying ${source.name}: ${source.url}`);
|
||||
url = await fetchFromHttp(source.url);
|
||||
} else if (source.type === 'doh') {
|
||||
console.log(`[Mirror] Trying ${source.name_label}: ${source.name}`);
|
||||
url = await fetchFromDoh(source.name);
|
||||
}
|
||||
if (url) {
|
||||
patchesBaseUrl = url;
|
||||
patchesConfigTime = now;
|
||||
saveDiskCache(url);
|
||||
console.log(`[Mirror] Patches URL (via ${source.name || source.name_label}): ${url}`);
|
||||
return url;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[Mirror] ${source.name || source.name_label} failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Stale memory cache (any age)
|
||||
if (patchesBaseUrl) {
|
||||
console.log('[Mirror] All sources failed, using stale memory cache:', patchesBaseUrl);
|
||||
return patchesBaseUrl;
|
||||
}
|
||||
|
||||
// 6. Disk cache (survives restarts)
|
||||
const diskUrl = loadDiskCache();
|
||||
if (diskUrl) {
|
||||
patchesBaseUrl = diskUrl;
|
||||
console.log('[Mirror] All sources failed, using disk cache:', diskUrl);
|
||||
return diskUrl;
|
||||
}
|
||||
|
||||
// 7. Hardcoded fallback
|
||||
console.warn('[Mirror] All sources + caches exhausted, using hardcoded fallback:', HARDCODED_FALLBACK);
|
||||
patchesBaseUrl = HARDCODED_FALLBACK;
|
||||
return HARDCODED_FALLBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available mirror base URLs (primary + non-Cloudflare fallbacks)
|
||||
* Used by download logic to retry on different mirrors when primary is blocked
|
||||
*/
|
||||
async function getAllMirrorUrls() {
|
||||
const primary = await getPatchesBaseUrl();
|
||||
// Deduplicate: don't include mirrors that match primary
|
||||
const mirrors = NON_CF_MIRRORS.filter(m => m !== primary);
|
||||
return [primary, ...mirrors];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the mirror manifest — tries primary URL first, then non-Cloudflare mirrors
|
||||
*/
|
||||
async function fetchMirrorManifest() {
|
||||
const now = Date.now();
|
||||
|
||||
if (manifestCache && (now - manifestCacheTime) < MANIFEST_CACHE_DURATION) {
|
||||
console.log('[Mirror] Using cached manifest');
|
||||
return manifestCache;
|
||||
}
|
||||
|
||||
const mirrors = await getAllMirrorUrls();
|
||||
|
||||
for (let i = 0; i < mirrors.length; i++) {
|
||||
const baseUrl = mirrors[i];
|
||||
const manifestUrl = `${baseUrl}/manifest.json`;
|
||||
try {
|
||||
console.log(`[Mirror] Fetching manifest from: ${manifestUrl}`);
|
||||
const response = await axios.get(manifestUrl, {
|
||||
timeout: 15000,
|
||||
maxRedirects: 5,
|
||||
headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
|
||||
});
|
||||
|
||||
if (response.data && response.data.files) {
|
||||
manifestCache = response.data;
|
||||
manifestCacheTime = now;
|
||||
// If a non-primary mirror worked, switch to it for downloads too
|
||||
if (i > 0) {
|
||||
console.log(`[Mirror] Primary unreachable, switching to mirror: ${baseUrl}`);
|
||||
patchesBaseUrl = baseUrl;
|
||||
patchesConfigTime = now;
|
||||
saveDiskCache(baseUrl);
|
||||
}
|
||||
console.log('[Mirror] Manifest fetched successfully');
|
||||
return response.data;
|
||||
}
|
||||
throw new Error('Invalid manifest structure');
|
||||
} catch (error) {
|
||||
const isTimeout = error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED' || error.message.includes('timeout');
|
||||
console.error(`[Mirror] Error fetching manifest from ${baseUrl}: ${error.message}${isTimeout ? ' (Cloudflare may be blocked)' : ''}`);
|
||||
if (i < mirrors.length - 1) {
|
||||
console.log(`[Mirror] Trying next mirror...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All mirrors failed — use cached manifest if available
|
||||
if (manifestCache) {
|
||||
console.log('[Mirror] All mirrors failed, using expired cache');
|
||||
return manifestCache;
|
||||
}
|
||||
throw new Error('All mirrors failed and no cached manifest available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse manifest to get available patches for current platform
|
||||
* Returns array of { from, to, key, size }
|
||||
*/
|
||||
function getPlatformPatches(manifest, branch = 'release') {
|
||||
const os = getOS();
|
||||
const arch = getArch();
|
||||
return `${BASE_PATCH_URL}/${os}/${arch}/${branch}/0/${buildNumber}.pwr`;
|
||||
const prefix = `${os}/${arch}/${branch}/`;
|
||||
const patches = [];
|
||||
|
||||
for (const [key, info] of Object.entries(manifest.files)) {
|
||||
if (key.startsWith(prefix) && key.endsWith('.pwr')) {
|
||||
const filename = key.slice(prefix.length, -4); // e.g., "0_to_11"
|
||||
const match = filename.match(/^(\d+)_to_(\d+)$/);
|
||||
if (match) {
|
||||
patches.push({
|
||||
from: parseInt(match[1]),
|
||||
to: parseInt(match[2]),
|
||||
key,
|
||||
size: info.size
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return patches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find optimal patch path using BFS with download size minimization
|
||||
* Returns array of { from, to, url, size, key } steps, or null if no path found
|
||||
*/
|
||||
async function findOptimalPatchPath(currentBuild, targetBuild, patches) {
|
||||
if (currentBuild >= targetBuild) return [];
|
||||
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const edges = {};
|
||||
for (const patch of patches) {
|
||||
if (!edges[patch.from]) edges[patch.from] = [];
|
||||
edges[patch.from].push(patch);
|
||||
}
|
||||
|
||||
const queue = [{ build: currentBuild, path: [], totalSize: 0 }];
|
||||
let bestPath = null;
|
||||
let bestSize = Infinity;
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { build, path, totalSize } = queue.shift();
|
||||
|
||||
if (build === targetBuild) {
|
||||
if (totalSize < bestSize) {
|
||||
bestPath = path;
|
||||
bestSize = totalSize;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (totalSize >= bestSize) continue;
|
||||
|
||||
const nextEdges = edges[build] || [];
|
||||
for (const edge of nextEdges) {
|
||||
if (edge.to <= build || edge.to > targetBuild) continue;
|
||||
if (path.some(p => p.to === edge.to)) continue;
|
||||
|
||||
queue.push({
|
||||
build: edge.to,
|
||||
path: [...path, {
|
||||
from: edge.from,
|
||||
to: edge.to,
|
||||
url: `${baseUrl}/${edge.key}`,
|
||||
size: edge.size,
|
||||
key: edge.key
|
||||
}],
|
||||
totalSize: totalSize + edge.size
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return bestPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the optimal update plan from currentBuild to targetBuild
|
||||
* Returns { steps: [{from, to, url, size}], totalSize, isFullInstall }
|
||||
*/
|
||||
async function getUpdatePlan(currentBuild, targetBuild, branch = 'release') {
|
||||
const manifest = await fetchMirrorManifest();
|
||||
const patches = getPlatformPatches(manifest, branch);
|
||||
|
||||
// Try optimal path
|
||||
const steps = await findOptimalPatchPath(currentBuild, targetBuild, patches);
|
||||
|
||||
if (steps && steps.length > 0) {
|
||||
const totalSize = steps.reduce((sum, s) => sum + s.size, 0);
|
||||
console.log(`[Mirror] Update plan: ${steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${(totalSize / 1024 / 1024).toFixed(0)} MB)`);
|
||||
return { steps, totalSize, isFullInstall: steps.length === 1 && steps[0].from === 0 };
|
||||
}
|
||||
|
||||
// Fallback: full install 0 -> target
|
||||
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
|
||||
if (fullPatch) {
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const step = {
|
||||
from: 0,
|
||||
to: targetBuild,
|
||||
url: `${baseUrl}/${fullPatch.key}`,
|
||||
size: fullPatch.size,
|
||||
key: fullPatch.key
|
||||
};
|
||||
console.log(`[Mirror] Full install: 0\u2192${targetBuild} (${(fullPatch.size / 1024 / 1024).toFixed(0)} MB)`);
|
||||
return { steps: [step], totalSize: fullPatch.size, isFullInstall: true };
|
||||
}
|
||||
|
||||
throw new Error(`No patch path found from build ${currentBuild} to ${targetBuild} for ${getOS()}/${getArch()}`);
|
||||
}
|
||||
|
||||
async function getLatestClientVersion(branch = 'release') {
|
||||
try {
|
||||
console.log(`[Mirror] Fetching latest client version (branch: ${branch})...`);
|
||||
const manifest = await fetchMirrorManifest();
|
||||
const patches = getPlatformPatches(manifest, branch);
|
||||
|
||||
if (patches.length === 0) {
|
||||
console.log(`[Mirror] No patches for branch '${branch}', using fallback`);
|
||||
return `v${FALLBACK_LATEST_BUILD}`;
|
||||
}
|
||||
|
||||
const latestBuild = Math.max(...patches.map(p => p.to));
|
||||
console.log(`[Mirror] Latest client version: v${latestBuild}`);
|
||||
return `v${latestBuild}`;
|
||||
} catch (error) {
|
||||
console.error('[Mirror] Error:', error.message);
|
||||
return `v${FALLBACK_LATEST_BUILD}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get PWR download URL for fresh install (0 -> target)
|
||||
* Backward-compatible with old getPWRUrlFromNewAPI signature
|
||||
* Checks mirror first, then constructs URL for the branch
|
||||
*/
|
||||
async function getPWRUrl(branch = 'release', version = 'v11') {
|
||||
const targetBuild = extractVersionNumber(version);
|
||||
const os = getOS();
|
||||
const arch = getArch();
|
||||
|
||||
try {
|
||||
const manifest = await fetchMirrorManifest();
|
||||
const patches = getPlatformPatches(manifest, branch);
|
||||
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
|
||||
|
||||
if (fullPatch) {
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const url = `${baseUrl}/${fullPatch.key}`;
|
||||
console.log(`[Mirror] PWR URL: ${url}`);
|
||||
return url;
|
||||
}
|
||||
|
||||
if (patches.length > 0) {
|
||||
// Branch exists in mirror but no full patch for this target - construct URL
|
||||
console.log(`[Mirror] No 0->${targetBuild} patch found, constructing URL`);
|
||||
} else {
|
||||
console.log(`[Mirror] Branch '${branch}' not in mirror, constructing URL`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Mirror] Error getting PWR URL:', error.message);
|
||||
}
|
||||
|
||||
// Construct mirror URL (will work if patch was uploaded but manifest is stale)
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
return `${baseUrl}/${os}/${arch}/${branch}/0_to_${targetBuild}.pwr`;
|
||||
}
|
||||
|
||||
// Backward-compatible alias
|
||||
const getPWRUrlFromNewAPI = getPWRUrl;
|
||||
|
||||
// Utility function to extract version number
|
||||
// Supports: "7.pwr", "v8", "v8-windows-amd64.pwr", "5_to_10", etc.
|
||||
function extractVersionNumber(version) {
|
||||
if (!version) return 0;
|
||||
|
||||
// New format: "v8" or "v8-xxx.pwr"
|
||||
const vMatch = version.match(/v(\d+)/);
|
||||
if (vMatch) return parseInt(vMatch[1]);
|
||||
|
||||
// Old format: "7.pwr"
|
||||
const pwrMatch = version.match(/(\d+)\.pwr/);
|
||||
if (pwrMatch) return parseInt(pwrMatch[1]);
|
||||
|
||||
// Fallback
|
||||
const num = parseInt(version);
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
|
||||
async function buildArchiveUrl(buildNumber, branch = 'release') {
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const os = getOS();
|
||||
const arch = getArch();
|
||||
return `${baseUrl}/${os}/${arch}/${branch}/0_to_${buildNumber}.pwr`;
|
||||
}
|
||||
|
||||
async function checkArchiveExists(buildNumber, branch = 'release') {
|
||||
const url = buildArchiveUrl(buildNumber, branch);
|
||||
const url = await buildArchiveUrl(buildNumber, branch);
|
||||
try {
|
||||
const response = await axios.head(url, { timeout: 10000 });
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function discoverAvailableVersions(latestKnown, branch = 'release', maxProbe = 50) {
|
||||
const available = [];
|
||||
const latest = parseInt(latestKnown.replace('.pwr', ''));
|
||||
|
||||
for (let i = latest; i >= Math.max(1, latest - maxProbe); i--) {
|
||||
const exists = await checkArchiveExists(i, branch);
|
||||
if (exists) {
|
||||
available.push(`${i}.pwr`);
|
||||
}
|
||||
}
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
async function fetchPatchManifest(branch = 'release') {
|
||||
async function discoverAvailableVersions(latestKnown, branch = 'release') {
|
||||
try {
|
||||
const os = getOS();
|
||||
const arch = getArch();
|
||||
const response = await axios.get(MANIFEST_API, {
|
||||
params: { branch, os, arch },
|
||||
timeout: 10000
|
||||
});
|
||||
return response.data.patches || {};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch patch manifest:', error.message);
|
||||
return {};
|
||||
const manifest = await fetchMirrorManifest();
|
||||
const patches = getPlatformPatches(manifest, branch);
|
||||
const versions = [...new Set(patches.map(p => p.to))].sort((a, b) => b - a);
|
||||
return versions.map(v => `${v}.pwr`);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function extractVersionDetails(targetVersion, branch = 'release') {
|
||||
const buildNumber = parseInt(targetVersion.replace('.pwr', ''));
|
||||
const previousBuild = buildNumber - 1;
|
||||
|
||||
const manifest = await fetchPatchManifest(branch);
|
||||
const patchInfo = manifest[buildNumber];
|
||||
|
||||
const buildNumber = extractVersionNumber(targetVersion);
|
||||
const fullUrl = await buildArchiveUrl(buildNumber, branch);
|
||||
|
||||
return {
|
||||
version: targetVersion,
|
||||
buildNumber: buildNumber,
|
||||
buildNumber,
|
||||
buildName: `HYTALE-Build-${buildNumber}`,
|
||||
fullUrl: patchInfo?.original_url || buildArchiveUrl(buildNumber, branch),
|
||||
differentialUrl: patchInfo?.patch_url || null,
|
||||
checksum: patchInfo?.patch_hash || null,
|
||||
sourceVersion: patchInfo?.from ? `${patchInfo.from}.pwr` : (previousBuild > 0 ? `${previousBuild}.pwr` : null),
|
||||
isDifferential: !!patchInfo?.proper_patch,
|
||||
releaseNotes: patchInfo?.patch_note || null
|
||||
fullUrl,
|
||||
differentialUrl: null,
|
||||
checksum: null,
|
||||
sourceVersion: null,
|
||||
isDifferential: false,
|
||||
releaseNotes: null
|
||||
};
|
||||
}
|
||||
|
||||
function canUseDifferentialUpdate(currentVersion, targetDetails) {
|
||||
if (!targetDetails) return false;
|
||||
if (!targetDetails.differentialUrl) return false;
|
||||
if (!targetDetails.isDifferential) return false;
|
||||
|
||||
if (!currentVersion) return false;
|
||||
|
||||
const currentBuild = parseInt(currentVersion.replace('.pwr', ''));
|
||||
const expectedSource = parseInt(targetDetails.sourceVersion?.replace('.pwr', '') || '0');
|
||||
|
||||
return currentBuild === expectedSource;
|
||||
function canUseDifferentialUpdate() {
|
||||
// Differential updates are now handled via getUpdatePlan()
|
||||
return false;
|
||||
}
|
||||
|
||||
function needsIntermediatePatches(currentVersion, targetVersion) {
|
||||
if (!currentVersion) return [];
|
||||
|
||||
const current = parseInt(currentVersion.replace('.pwr', ''));
|
||||
const target = parseInt(targetVersion.replace('.pwr', ''));
|
||||
|
||||
const intermediates = [];
|
||||
for (let i = current + 1; i <= target; i++) {
|
||||
intermediates.push(`${i}.pwr`);
|
||||
}
|
||||
|
||||
return intermediates;
|
||||
const current = extractVersionNumber(currentVersion);
|
||||
const target = extractVersionNumber(targetVersion);
|
||||
if (current >= target) return [];
|
||||
return [targetVersion];
|
||||
}
|
||||
|
||||
async function computeFileChecksum(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
|
||||
stream.on('data', data => hash.update(data));
|
||||
stream.on('end', () => resolve(hash.digest('hex')));
|
||||
stream.on('error', reject);
|
||||
@@ -137,7 +503,6 @@ async function computeFileChecksum(filePath) {
|
||||
|
||||
async function validateChecksum(filePath, expectedChecksum) {
|
||||
if (!expectedChecksum) return true;
|
||||
|
||||
const actualChecksum = await computeFileChecksum(filePath);
|
||||
return actualChecksum === expectedChecksum;
|
||||
}
|
||||
@@ -146,7 +511,7 @@ function getInstalledClientVersion() {
|
||||
try {
|
||||
const { loadVersionClient } = require('../core/config');
|
||||
return loadVersionClient();
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -161,5 +526,14 @@ module.exports = {
|
||||
needsIntermediatePatches,
|
||||
computeFileChecksum,
|
||||
validateChecksum,
|
||||
getInstalledClientVersion
|
||||
getInstalledClientVersion,
|
||||
fetchMirrorManifest,
|
||||
getPWRUrl,
|
||||
getPWRUrlFromNewAPI,
|
||||
getUpdatePlan,
|
||||
extractVersionNumber,
|
||||
getPlatformPatches,
|
||||
findOptimalPatchPath,
|
||||
getPatchesBaseUrl,
|
||||
getAllMirrorUrls
|
||||
};
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { smartDownloadStream } = require('./proxyClient');
|
||||
|
||||
// Domain configuration
|
||||
const ORIGINAL_DOMAIN = 'hytale.com';
|
||||
const MIN_DOMAIN_LENGTH = 4;
|
||||
const MAX_DOMAIN_LENGTH = 16;
|
||||
|
||||
// DualAuth ByteBuddy Agent (runtime class transformation, no JAR modification)
|
||||
const DUALAUTH_AGENT_URL = 'https://github.com/sanasol/hytale-auth-server/releases/latest/download/dualauth-agent.jar';
|
||||
const DUALAUTH_AGENT_VERSION_API = 'https://api.github.com/repos/sanasol/hytale-auth-server/releases/latest';
|
||||
const DUALAUTH_AGENT_FILENAME = 'dualauth-agent.jar';
|
||||
const DUALAUTH_AGENT_VERSION_FILE = 'dualauth-agent.version';
|
||||
|
||||
function getTargetDomain() {
|
||||
if (process.env.HYTALE_AUTH_DOMAIN) {
|
||||
return process.env.HYTALE_AUTH_DOMAIN;
|
||||
@@ -22,7 +29,7 @@ const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
|
||||
|
||||
/**
|
||||
* Patches HytaleClient binary to replace hytale.com with custom domain
|
||||
* Server patching is done via pre-patched JAR download from CDN
|
||||
* Server auth is handled by DualAuth ByteBuddy Agent (-javaagent: flag)
|
||||
*
|
||||
* Supports domains from 4 to 16 characters:
|
||||
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
|
||||
@@ -493,227 +500,144 @@ class ClientPatcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if server JAR contains DualAuth classes (was patched)
|
||||
* Get the path to the DualAuth Agent JAR in a directory
|
||||
*/
|
||||
serverJarContainsDualAuth(serverPath) {
|
||||
try {
|
||||
const data = fs.readFileSync(serverPath);
|
||||
// Check for DualAuthContext class signature in JAR
|
||||
const signature = Buffer.from('DualAuthContext', 'utf8');
|
||||
return data.includes(signature);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
getAgentPath(dir) {
|
||||
return path.join(dir, DUALAUTH_AGENT_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate downloaded file is not corrupt/partial
|
||||
* Server JAR should be at least 50MB
|
||||
* Download DualAuth ByteBuddy Agent (replaces old pre-patched JAR approach)
|
||||
* The agent provides runtime class transformation via -javaagent: flag
|
||||
* No server JAR modification needed - original JAR stays pristine
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
async ensureAgentAvailable(serverDir, progressCallback) {
|
||||
const agentPath = this.getAgentPath(serverDir);
|
||||
const versionPath = path.join(serverDir, DUALAUTH_AGENT_VERSION_FILE);
|
||||
|
||||
/**
|
||||
* Patch server JAR by downloading pre-patched version from CDN
|
||||
*/
|
||||
async patchServer(serverPath, progressCallback, branch = 'release') {
|
||||
const newDomain = this.getNewDomain();
|
||||
console.log('=== DualAuth Agent (ByteBuddy) ===');
|
||||
console.log(`Target: ${agentPath}`);
|
||||
|
||||
console.log('=== Server Patcher (Pre-patched Download) ===');
|
||||
console.log(`Target: ${serverPath}`);
|
||||
console.log(`Branch: ${branch}`);
|
||||
console.log(`Domain: ${newDomain}`);
|
||||
|
||||
if (!fs.existsSync(serverPath)) {
|
||||
const error = `Server JAR not found: ${serverPath}`;
|
||||
console.error(error);
|
||||
return { success: false, error };
|
||||
}
|
||||
|
||||
// Check if already patched
|
||||
const patchFlagFile = serverPath + '.dualauth_patched';
|
||||
let needsRestore = false;
|
||||
|
||||
if (fs.existsSync(patchFlagFile)) {
|
||||
// Check local version and whether file exists
|
||||
let localVersion = null;
|
||||
let agentExists = false;
|
||||
if (fs.existsSync(agentPath)) {
|
||||
try {
|
||||
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
||||
if (flagData.domain === newDomain && flagData.branch === branch) {
|
||||
// Verify JAR actually contains DualAuth classes (game may have auto-updated)
|
||||
if (this.serverJarContainsDualAuth(serverPath)) {
|
||||
console.log(`Server already patched for ${newDomain} (${branch}), skipping`);
|
||||
if (progressCallback) progressCallback('Server already patched', 100);
|
||||
return { success: true, alreadyPatched: true };
|
||||
} else {
|
||||
console.log(' Flag exists but JAR not patched (was auto-updated?), will re-download...');
|
||||
// Delete stale flag file
|
||||
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
|
||||
const stats = fs.statSync(agentPath);
|
||||
if (stats.size > 1024) {
|
||||
agentExists = true;
|
||||
if (fs.existsSync(versionPath)) {
|
||||
localVersion = fs.readFileSync(versionPath, 'utf8').trim();
|
||||
}
|
||||
} else {
|
||||
console.log(`Server patched for "${flagData.domain}" (${flagData.branch}), need to change to "${newDomain}" (${branch})`);
|
||||
needsRestore = true;
|
||||
console.log('Agent file appears corrupt, re-downloading...');
|
||||
fs.unlinkSync(agentPath);
|
||||
}
|
||||
} catch (e) {
|
||||
// Flag file corrupt, re-patch
|
||||
console.log(' Flag file corrupt, will re-download');
|
||||
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
|
||||
console.warn('Could not check agent file:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore backup if patched for different domain
|
||||
if (needsRestore) {
|
||||
const backupPath = serverPath + '.original';
|
||||
if (fs.existsSync(backupPath)) {
|
||||
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
|
||||
console.log('Restoring original JAR from backup for re-patching...');
|
||||
fs.copyFileSync(backupPath, serverPath);
|
||||
if (fs.existsSync(patchFlagFile)) {
|
||||
fs.unlinkSync(patchFlagFile);
|
||||
// Check for updates from GitHub
|
||||
let remoteVersion = null;
|
||||
let needsDownload = !agentExists;
|
||||
if (agentExists) {
|
||||
try {
|
||||
if (progressCallback) progressCallback('Checking for agent updates...', 5);
|
||||
const axios = require('axios');
|
||||
const resp = await axios.get(DUALAUTH_AGENT_VERSION_API, {
|
||||
timeout: 5000,
|
||||
headers: { 'Accept': 'application/vnd.github.v3+json' }
|
||||
});
|
||||
remoteVersion = resp.data.tag_name; // e.g. "v1.1.10"
|
||||
if (localVersion && localVersion === remoteVersion) {
|
||||
console.log(`DualAuth Agent up to date (${localVersion})`);
|
||||
if (progressCallback) progressCallback('DualAuth Agent ready', 100);
|
||||
return { success: true, agentPath, alreadyExists: true, version: localVersion };
|
||||
}
|
||||
console.log(`Agent update available: ${localVersion || 'unknown'} → ${remoteVersion}`);
|
||||
needsDownload = true;
|
||||
} catch (e) {
|
||||
// GitHub API failed - use existing agent if available
|
||||
console.warn(`Could not check for updates: ${e.message}`);
|
||||
if (agentExists) {
|
||||
console.log(`Using existing agent (${localVersion || 'unknown version'})`);
|
||||
if (progressCallback) progressCallback('DualAuth Agent ready', 100);
|
||||
return { success: true, agentPath, alreadyExists: true, version: localVersion };
|
||||
}
|
||||
} else {
|
||||
console.warn(' No backup found to restore - will download fresh patched JAR');
|
||||
}
|
||||
}
|
||||
|
||||
// Create backup
|
||||
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');
|
||||
if (!needsDownload) {
|
||||
if (progressCallback) progressCallback('DualAuth Agent ready', 100);
|
||||
return { success: true, agentPath, alreadyExists: true, version: localVersion };
|
||||
}
|
||||
|
||||
// Only support standard domain (auth.sanasol.ws) via pre-patched download
|
||||
if (newDomain !== 'auth.sanasol.ws' && newDomain !== 'sanasol.ws') {
|
||||
console.error(`Domain "${newDomain}" requires DualAuthPatcher - only auth.sanasol.ws is supported via pre-patched download`);
|
||||
return { success: false, error: `Unsupported domain: ${newDomain}. Only auth.sanasol.ws is supported.` };
|
||||
}
|
||||
|
||||
// Download pre-patched JAR
|
||||
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
|
||||
console.log('Downloading pre-patched HytaleServer.jar...');
|
||||
// Download agent from GitHub releases
|
||||
const action = agentExists ? 'Updating' : 'Downloading';
|
||||
if (progressCallback) progressCallback(`${action} DualAuth Agent...`, 20);
|
||||
console.log(`${action} from: ${DUALAUTH_AGENT_URL}`);
|
||||
|
||||
try {
|
||||
const https = require('https');
|
||||
|
||||
// Use different URL for pre-release vs release
|
||||
let url;
|
||||
if (branch === 'pre-release') {
|
||||
url = 'https://patcher.authbp.xyz/download/patched_prerelease';
|
||||
console.log(' Using pre-release patched server from:', url);
|
||||
} else {
|
||||
url = 'https://patcher.authbp.xyz/download/patched_release';
|
||||
console.log(' Using release patched server from:', url);
|
||||
// Ensure server directory exists
|
||||
if (!fs.existsSync(serverDir)) {
|
||||
fs.mkdirSync(serverDir, { recursive: true });
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const tmpPath = agentPath + '.tmp';
|
||||
const file = fs.createWriteStream(tmpPath);
|
||||
|
||||
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);
|
||||
});
|
||||
const stream = await smartDownloadStream(DUALAUTH_AGENT_URL, (chunk, downloadedBytes, total) => {
|
||||
if (progressCallback && total) {
|
||||
const percent = 20 + Math.floor((downloadedBytes / total) * 70);
|
||||
progressCallback(`${action} agent... ${(downloadedBytes / 1024).toFixed(0)} KB`, percent);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(' Download successful');
|
||||
stream.pipe(file);
|
||||
|
||||
// Verify downloaded JAR size and contents
|
||||
if (progressCallback) progressCallback('Verifying downloaded JAR...', 95);
|
||||
await new Promise((resolve, reject) => {
|
||||
file.on('finish', () => { file.close(); resolve(); });
|
||||
file.on('error', reject);
|
||||
stream.on('error', reject);
|
||||
});
|
||||
|
||||
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)' };
|
||||
// Verify download
|
||||
const stats = fs.statSync(tmpPath);
|
||||
if (stats.size < 1024) {
|
||||
fs.unlinkSync(tmpPath);
|
||||
const error = 'Downloaded agent too small (corrupt or failed download)';
|
||||
console.error(error);
|
||||
return { success: false, error };
|
||||
}
|
||||
|
||||
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' };
|
||||
// Atomic move
|
||||
if (fs.existsSync(agentPath)) {
|
||||
fs.unlinkSync(agentPath);
|
||||
}
|
||||
console.log(' Verification successful - DualAuth classes present');
|
||||
fs.renameSync(tmpPath, agentPath);
|
||||
|
||||
// Mark as patched
|
||||
const sourceUrl = branch === 'pre-release'
|
||||
? 'https://patcher.authbp.xyz/download/patched_prerelease'
|
||||
: 'https://patcher.authbp.xyz/download/patched_release';
|
||||
|
||||
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
||||
domain: newDomain,
|
||||
branch: branch,
|
||||
patchedAt: new Date().toISOString(),
|
||||
patcher: 'PrePatchedDownload',
|
||||
source: sourceUrl
|
||||
}));
|
||||
// Save version
|
||||
const version = remoteVersion || 'unknown';
|
||||
fs.writeFileSync(versionPath, version, 'utf8');
|
||||
|
||||
if (progressCallback) progressCallback('Server patching complete', 100);
|
||||
console.log('=== Server Patching Complete ===');
|
||||
return { success: true, patchCount: 1 };
|
||||
console.log(`DualAuth Agent ${agentExists ? 'updated' : 'downloaded'} (${(stats.size / 1024).toFixed(0)} KB, ${version})`);
|
||||
if (progressCallback) progressCallback('DualAuth Agent ready', 100);
|
||||
return { success: true, agentPath, updated: agentExists, version };
|
||||
|
||||
} 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');
|
||||
console.error(`Failed to download DualAuth Agent: ${downloadError.message}`);
|
||||
// Clean up temp file
|
||||
const tmpPath = agentPath + '.tmp';
|
||||
if (fs.existsSync(tmpPath)) {
|
||||
try { fs.unlinkSync(tmpPath); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
return { success: false, error: `Failed to download patched server: ${downloadError.message}` };
|
||||
// If we had an existing agent, still use it
|
||||
if (agentExists) {
|
||||
console.log('Using existing agent despite update failure');
|
||||
return { success: true, agentPath, alreadyExists: true, version: localVersion };
|
||||
}
|
||||
return { success: false, error: downloadError.message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,12 +682,12 @@ class ClientPatcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure both client and server are patched before launching
|
||||
* Ensure client is patched and DualAuth Agent is available before launching
|
||||
*/
|
||||
async ensureClientPatched(gameDir, progressCallback, javaPath = null, branch = 'release') {
|
||||
const results = {
|
||||
client: null,
|
||||
server: null,
|
||||
agent: null,
|
||||
success: true
|
||||
};
|
||||
|
||||
@@ -780,22 +704,23 @@ class ClientPatcher {
|
||||
results.client = { success: false, error: 'Client binary not found' };
|
||||
}
|
||||
|
||||
const serverPath = this.findServerPath(gameDir);
|
||||
if (serverPath) {
|
||||
if (progressCallback) progressCallback('Patching server JAR...', 50);
|
||||
results.server = await this.patchServer(serverPath, (msg, pct) => {
|
||||
// Download DualAuth ByteBuddy Agent (runtime patching, no JAR modification)
|
||||
const serverDir = path.join(gameDir, 'Server');
|
||||
if (fs.existsSync(serverDir)) {
|
||||
if (progressCallback) progressCallback('Checking DualAuth Agent...', 50);
|
||||
results.agent = await this.ensureAgentAvailable(serverDir, (msg, pct) => {
|
||||
if (progressCallback) {
|
||||
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
||||
progressCallback(`Agent: ${msg}`, pct ? 50 + pct / 2 : null);
|
||||
}
|
||||
}, branch);
|
||||
});
|
||||
} else {
|
||||
console.warn('Could not find HytaleServer.jar');
|
||||
results.server = { success: false, error: 'Server JAR not found' };
|
||||
console.warn('Server directory not found, skipping agent download');
|
||||
results.agent = { success: true, skipped: true };
|
||||
}
|
||||
|
||||
results.success = (results.client && results.client.success) || (results.server && results.server.success);
|
||||
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
|
||||
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
|
||||
results.success = (results.client && results.client.success) || (results.agent && results.agent.success);
|
||||
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.agent && results.agent.alreadyExists);
|
||||
results.patchCount = results.client ? results.client.patchCount || 0 : 0;
|
||||
|
||||
if (progressCallback) progressCallback('Patching complete', 100);
|
||||
|
||||
|
||||
405
backend/utils/clientPatcher.localhost.js.bak
Normal file
405
backend/utils/clientPatcher.localhost.js.bak
Normal file
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* Localhost/Local Development Code Backup
|
||||
*
|
||||
* This file contains code removed from clientPatcher.js that was used for local development.
|
||||
* To re-enable local dev mode, merge this code back into clientPatcher.js.
|
||||
*
|
||||
* Backed up: 2026-01-28
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// LOCAL PATCHER PATHS (was in ensurePatcherDownloaded method)
|
||||
// =============================================================================
|
||||
|
||||
// Check for local patcher (for local development)
|
||||
const localPatcherPaths = [
|
||||
path.join(__dirname, '..', '..', '..', 'hytale-auth-server', 'patcher', 'DualAuthPatcher.java'),
|
||||
path.join(__dirname, '..', '..', '..', '..', 'hytale-auth-server', 'patcher', 'DualAuthPatcher.java'),
|
||||
'/Users/sanasol/code/pterodactyl-hytale/hytale-auth-server/patcher/DualAuthPatcher.java'
|
||||
];
|
||||
|
||||
// Check if we should use local patcher (localhost domain = local dev)
|
||||
const domain = getTargetDomain();
|
||||
const isLocalDev = domain.startsWith('localhost') || domain.startsWith('127.0.0.1');
|
||||
|
||||
if (isLocalDev) {
|
||||
for (const localPath of localPatcherPaths) {
|
||||
if (fs.existsSync(localPath)) {
|
||||
console.log(`Using local DualAuthPatcher: ${localPath}`);
|
||||
// Always copy fresh for local dev to pick up changes
|
||||
fs.copyFileSync(localPath, patcherJava);
|
||||
// Delete compiled class to force recompile
|
||||
const patcherClass = path.join(patcherDir, 'DualAuthPatcher.class');
|
||||
if (fs.existsSync(patcherClass)) {
|
||||
fs.unlinkSync(patcherClass);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log('Local patcher not found, falling back to download...');
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// LEGACY SERVER PATCHER (was patchServerLegacy method)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Legacy server patcher (simple domain replacement, no dual auth)
|
||||
* Use patchServer() for full dual auth support
|
||||
*/
|
||||
async patchServerLegacy(serverPath, progressCallback) {
|
||||
const newDomain = this.getNewDomain();
|
||||
const strategy = this.getDomainStrategy(newDomain);
|
||||
|
||||
console.log('=== Legacy Server Patcher ===');
|
||||
console.log(`Target: ${serverPath}`);
|
||||
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
||||
|
||||
if (!fs.existsSync(serverPath)) {
|
||||
return { success: false, error: `Server JAR not found: ${serverPath}` };
|
||||
}
|
||||
|
||||
if (progressCallback) progressCallback('Patching server...', 20);
|
||||
|
||||
console.log('Opening server JAR...');
|
||||
const zip = new AdmZip(serverPath);
|
||||
const entries = zip.getEntries();
|
||||
|
||||
let totalCount = 0;
|
||||
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
||||
|
||||
for (const entry of entries) {
|
||||
const name = entry.entryName;
|
||||
if (name.endsWith('.class') || name.endsWith('.properties') ||
|
||||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
|
||||
const data = entry.getData();
|
||||
if (data.includes(oldUtf8)) {
|
||||
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
||||
if (count > 0) {
|
||||
zip.updateFile(entry.entryName, patchedData);
|
||||
totalCount += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalCount > 0) {
|
||||
zip.writeZip(serverPath);
|
||||
}
|
||||
|
||||
if (progressCallback) progressCallback('Complete', 100);
|
||||
return { success: true, patchCount: totalCount };
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// DUALAUTHPATCHER FLOW FOR NON-STANDARD DOMAINS (was in patchServer method)
|
||||
// =============================================================================
|
||||
|
||||
// For non-standard domains, use DualAuthPatcher for proper bytecode patching
|
||||
const isStandardDomain = newDomain === 'auth.sanasol.ws' || newDomain === 'sanasol.ws';
|
||||
|
||||
if (!isStandardDomain) {
|
||||
console.log(`Non-standard domain "${newDomain}" - using DualAuthPatcher`);
|
||||
|
||||
// Find Java
|
||||
const java = this.findJava();
|
||||
if (!java) {
|
||||
console.error('Java not found - cannot run DualAuthPatcher');
|
||||
console.error('Please install Java or use the bundled JRE');
|
||||
return { success: false, error: 'Java not found for DualAuthPatcher' };
|
||||
}
|
||||
console.log(` Using Java: ${java}`);
|
||||
|
||||
// Setup patcher directory
|
||||
const patcherDir = path.join(path.dirname(serverPath), '.patcher');
|
||||
const libDir = path.join(patcherDir, 'lib');
|
||||
|
||||
try {
|
||||
// Download patcher and libraries
|
||||
if (progressCallback) progressCallback('Downloading DualAuthPatcher...', 20);
|
||||
await this.ensurePatcherDownloaded(patcherDir);
|
||||
|
||||
if (progressCallback) progressCallback('Downloading ASM libraries...', 30);
|
||||
await this.ensureAsmLibraries(libDir);
|
||||
|
||||
// Compile patcher
|
||||
if (progressCallback) progressCallback('Compiling patcher...', 40);
|
||||
const compileResult = await this.compileDualAuthPatcher(java, patcherDir, libDir);
|
||||
if (!compileResult.success) {
|
||||
return { success: false, error: compileResult.error };
|
||||
}
|
||||
|
||||
// Build classpath
|
||||
const classpath = [
|
||||
patcherDir,
|
||||
path.join(libDir, 'asm-9.6.jar'),
|
||||
path.join(libDir, 'asm-tree-9.6.jar'),
|
||||
path.join(libDir, 'asm-util-9.6.jar')
|
||||
].join(process.platform === 'win32' ? ';' : ':');
|
||||
|
||||
// Run DualAuthPatcher with custom domain
|
||||
if (progressCallback) progressCallback('Running DualAuthPatcher...', 60);
|
||||
console.log(` Running DualAuthPatcher with domain: ${newDomain}`);
|
||||
|
||||
const patchResult = await this.runDualAuthPatcher(java, classpath, serverPath, newDomain);
|
||||
|
||||
if (patchResult.success) {
|
||||
// Mark as patched
|
||||
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
||||
domain: newDomain,
|
||||
patchedAt: new Date().toISOString(),
|
||||
patcher: 'DualAuthPatcher',
|
||||
output: patchResult.stdout
|
||||
}, null, 2));
|
||||
|
||||
if (progressCallback) progressCallback('Server patching complete', 100);
|
||||
console.log('=== Server Patching Complete (DualAuthPatcher) ===');
|
||||
return { success: true, patchCount: 1 };
|
||||
} else {
|
||||
console.error('DualAuthPatcher failed:', patchResult.error);
|
||||
return { success: false, error: patchResult.error };
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to run DualAuthPatcher:', err.message);
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// HELPER METHODS FOR DUALAUTHPATCHER (keep if re-enabling non-standard domains)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Find Java executable - uses bundled JRE first (same as game uses)
|
||||
* Falls back to system Java if bundled not available
|
||||
*/
|
||||
findJava() {
|
||||
// 1. Try bundled JRE first (comes with the game)
|
||||
try {
|
||||
const bundled = getBundledJavaPath(JRE_DIR);
|
||||
if (bundled && fs.existsSync(bundled)) {
|
||||
console.log(`Using bundled Java: ${bundled}`);
|
||||
return bundled;
|
||||
}
|
||||
} catch (e) {
|
||||
// Bundled not available
|
||||
}
|
||||
|
||||
// 2. Try javaManager's getJavaExec (handles all fallbacks)
|
||||
try {
|
||||
const javaExec = getJavaExec(JRE_DIR);
|
||||
if (javaExec && fs.existsSync(javaExec)) {
|
||||
console.log(`Using Java from javaManager: ${javaExec}`);
|
||||
return javaExec;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not available
|
||||
}
|
||||
|
||||
// 3. Check JAVA_HOME
|
||||
if (process.env.JAVA_HOME) {
|
||||
const javaHome = process.env.JAVA_HOME;
|
||||
const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
|
||||
if (fs.existsSync(javaBin)) {
|
||||
console.log(`Using Java from JAVA_HOME: ${javaBin}`);
|
||||
return javaBin;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Try 'java' from PATH
|
||||
try {
|
||||
execSync('java -version 2>&1', { encoding: 'utf8' });
|
||||
console.log('Using Java from PATH');
|
||||
return 'java';
|
||||
} catch (e) {
|
||||
// Not in PATH
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get DualAuthPatcher - downloads from GitHub
|
||||
*/
|
||||
async ensurePatcherDownloaded(patcherDir) {
|
||||
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
||||
const patcherUrl = 'https://raw.githubusercontent.com/sanasol/hytale-auth-server/master/patcher/DualAuthPatcher.java';
|
||||
|
||||
if (!fs.existsSync(patcherDir)) {
|
||||
fs.mkdirSync(patcherDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(patcherJava)) {
|
||||
console.log('Downloading DualAuthPatcher from hytale-auth-server...');
|
||||
try {
|
||||
const https = require('https');
|
||||
await new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(patcherJava);
|
||||
https.get(patcherUrl, (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
https.get(response.headers.location, (redirectResponse) => {
|
||||
redirectResponse.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
}).on('error', reject);
|
||||
} else {
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
fs.unlink(patcherJava, () => {});
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
console.log(' Downloaded DualAuthPatcher.java');
|
||||
} catch (e) {
|
||||
console.error(` Failed to download DualAuthPatcher: ${e.message}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download ASM libraries if not present
|
||||
*/
|
||||
async ensureAsmLibraries(libDir) {
|
||||
if (!fs.existsSync(libDir)) {
|
||||
fs.mkdirSync(libDir, { recursive: true });
|
||||
}
|
||||
|
||||
const libs = [
|
||||
{ name: 'asm-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar' },
|
||||
{ name: 'asm-tree-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar' },
|
||||
{ name: 'asm-util-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-util/9.6/asm-util-9.6.jar' }
|
||||
];
|
||||
|
||||
for (const lib of libs) {
|
||||
const libPath = path.join(libDir, lib.name);
|
||||
if (!fs.existsSync(libPath)) {
|
||||
console.log(`Downloading ${lib.name}...`);
|
||||
try {
|
||||
const https = require('https');
|
||||
await new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(libPath);
|
||||
https.get(lib.url, (response) => {
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve();
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
fs.unlink(libPath, () => {});
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
console.log(` Downloaded ${lib.name}`);
|
||||
} catch (e) {
|
||||
console.error(` Failed to download ${lib.name}: ${e.message}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile DualAuthPatcher if needed
|
||||
*/
|
||||
async compileDualAuthPatcher(java, patcherDir, libDir) {
|
||||
const patcherClass = path.join(patcherDir, 'DualAuthPatcher.class');
|
||||
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
||||
|
||||
if (fs.existsSync(patcherClass)) {
|
||||
const classTime = fs.statSync(patcherClass).mtime;
|
||||
const javaTime = fs.statSync(patcherJava).mtime;
|
||||
if (classTime > javaTime) {
|
||||
console.log('DualAuthPatcher already compiled');
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Compiling DualAuthPatcher...');
|
||||
|
||||
const javac = java.replace(/java(\.exe)?$/, 'javac$1');
|
||||
const classpath = [
|
||||
path.join(libDir, 'asm-9.6.jar'),
|
||||
path.join(libDir, 'asm-tree-9.6.jar'),
|
||||
path.join(libDir, 'asm-util-9.6.jar')
|
||||
].join(process.platform === 'win32' ? ';' : ':');
|
||||
|
||||
try {
|
||||
const execOptions = {
|
||||
stdio: 'pipe',
|
||||
cwd: patcherDir,
|
||||
env: { ...process.env }
|
||||
};
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const systemRoot = process.env.SystemRoot || 'C:\\WINDOWS';
|
||||
const systemPath = `${systemRoot}\\system32;${systemRoot};${systemRoot}\\System32\\Wbem`;
|
||||
execOptions.env.PATH = execOptions.env.PATH
|
||||
? `${systemPath};${execOptions.env.PATH}`
|
||||
: systemPath;
|
||||
execOptions.shell = true;
|
||||
}
|
||||
|
||||
execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, execOptions);
|
||||
console.log(' Compilation successful');
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
const error = `Failed to compile DualAuthPatcher: ${e.message}`;
|
||||
console.error(error);
|
||||
if (e.stderr) console.error(e.stderr.toString());
|
||||
return { success: false, error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run DualAuthPatcher on the server JAR
|
||||
*/
|
||||
async runDualAuthPatcher(java, classpath, serverPath, domain) {
|
||||
return new Promise((resolve) => {
|
||||
const args = ['-cp', classpath, 'DualAuthPatcher', serverPath];
|
||||
const env = { ...process.env, HYTALE_AUTH_DOMAIN: domain };
|
||||
|
||||
console.log(`Running: java ${args.join(' ')}`);
|
||||
console.log(` HYTALE_AUTH_DOMAIN=${domain}`);
|
||||
|
||||
const proc = spawn(java, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
proc.stdout.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
stdout += str;
|
||||
console.log(str.trim());
|
||||
});
|
||||
|
||||
proc.stderr.on('data', (data) => {
|
||||
const str = data.toString();
|
||||
stderr += str;
|
||||
console.error(str.trim());
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ success: true, stdout });
|
||||
} else {
|
||||
resolve({ success: false, error: `Patcher exited with code ${code}: ${stderr}` });
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
resolve({ success: false, error: `Failed to run patcher: ${err.message}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
238
backend/utils/logCollector.js
Normal file
238
backend/utils/logCollector.js
Normal file
@@ -0,0 +1,238 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const zlib = require('zlib');
|
||||
const logger = require('../logger');
|
||||
|
||||
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB per file
|
||||
|
||||
/**
|
||||
* Get the HytaleSaves directory (game client logs)
|
||||
*/
|
||||
function getHytaleSavesDir() {
|
||||
const home = os.homedir();
|
||||
if (process.platform === 'win32') {
|
||||
return path.join(home, 'AppData', 'Local', 'HytaleSaves');
|
||||
} else if (process.platform === 'darwin') {
|
||||
return path.join(home, 'Library', 'Application Support', 'HytaleSaves');
|
||||
} else {
|
||||
return path.join(home, '.hytalesaves');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a log file, capping at MAX_FILE_SIZE (keeps tail/most recent lines)
|
||||
*/
|
||||
function readLogFile(filePath) {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.size <= MAX_FILE_SIZE) {
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
// Read only the last MAX_FILE_SIZE bytes
|
||||
const fd = fs.openSync(filePath, 'r');
|
||||
const buffer = Buffer.alloc(MAX_FILE_SIZE);
|
||||
fs.readSync(fd, buffer, 0, MAX_FILE_SIZE, stats.size - MAX_FILE_SIZE);
|
||||
fs.closeSync(fd);
|
||||
|
||||
const content = buffer.toString('utf8');
|
||||
// Skip first partial line
|
||||
const firstNewline = content.indexOf('\n');
|
||||
const trimmed = firstNewline >= 0 ? content.substring(firstNewline + 1) : content;
|
||||
return `[... truncated ${stats.size - MAX_FILE_SIZE} bytes ...]\n` + trimmed;
|
||||
} catch (err) {
|
||||
return `[Error reading file: ${err.message}]`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files matching a date pattern from a directory
|
||||
*/
|
||||
function getFilesForDate(dir, dateStr, pattern) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
|
||||
try {
|
||||
return fs.readdirSync(dir)
|
||||
.filter(f => f.includes(dateStr) && (pattern ? pattern.test(f) : true))
|
||||
.map(f => ({
|
||||
name: f,
|
||||
path: path.join(dir, f),
|
||||
mtime: fs.statSync(path.join(dir, f)).mtime
|
||||
}))
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ZIP BUILDER (pure Node.js, no dependencies)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* CRC32 lookup table
|
||||
*/
|
||||
const crc32Table = new Uint32Array(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
let c = i;
|
||||
for (let j = 0; j < 8; j++) {
|
||||
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
||||
}
|
||||
crc32Table[i] = c;
|
||||
}
|
||||
|
||||
function crc32(buf) {
|
||||
let crc = 0xFFFFFFFF;
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
crc = crc32Table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ZIP file from an array of {name, content} entries
|
||||
* Uses DEFLATE compression via built-in zlib
|
||||
*/
|
||||
function createZipBuffer(files) {
|
||||
const localHeaders = [];
|
||||
const centralEntries = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const nameBytes = Buffer.from(file.name, 'utf8');
|
||||
const contentBytes = Buffer.from(file.content, 'utf8');
|
||||
const compressed = zlib.deflateRawSync(contentBytes);
|
||||
const crcVal = crc32(contentBytes);
|
||||
|
||||
// Local file header (30 bytes + filename)
|
||||
const local = Buffer.alloc(30);
|
||||
local.writeUInt32LE(0x04034b50, 0); // signature
|
||||
local.writeUInt16LE(20, 4); // version needed
|
||||
local.writeUInt16LE(0, 6); // flags
|
||||
local.writeUInt16LE(8, 8); // compression: DEFLATE
|
||||
local.writeUInt16LE(0, 10); // mod time
|
||||
local.writeUInt16LE(0, 12); // mod date
|
||||
local.writeUInt32LE(crcVal, 14); // crc32
|
||||
local.writeUInt32LE(compressed.length, 18); // compressed size
|
||||
local.writeUInt32LE(contentBytes.length, 22); // uncompressed size
|
||||
local.writeUInt16LE(nameBytes.length, 26); // filename length
|
||||
local.writeUInt16LE(0, 28); // extra field length
|
||||
|
||||
localHeaders.push(local, nameBytes, compressed);
|
||||
|
||||
// Central directory entry (46 bytes + filename)
|
||||
const central = Buffer.alloc(46);
|
||||
central.writeUInt32LE(0x02014b50, 0); // signature
|
||||
central.writeUInt16LE(20, 4); // version made by
|
||||
central.writeUInt16LE(20, 6); // version needed
|
||||
central.writeUInt16LE(0, 8); // flags
|
||||
central.writeUInt16LE(8, 10); // compression
|
||||
central.writeUInt16LE(0, 12); // mod time
|
||||
central.writeUInt16LE(0, 14); // mod date
|
||||
central.writeUInt32LE(crcVal, 16); // crc32
|
||||
central.writeUInt32LE(compressed.length, 20); // compressed size
|
||||
central.writeUInt32LE(contentBytes.length, 24); // uncompressed size
|
||||
central.writeUInt16LE(nameBytes.length, 28); // filename length
|
||||
central.writeUInt16LE(0, 30); // extra field length
|
||||
central.writeUInt16LE(0, 32); // comment length
|
||||
central.writeUInt16LE(0, 34); // disk number
|
||||
central.writeUInt16LE(0, 36); // internal attrs
|
||||
central.writeUInt32LE(0, 38); // external attrs
|
||||
central.writeUInt32LE(offset, 42); // local header offset
|
||||
|
||||
centralEntries.push(central, nameBytes);
|
||||
offset += 30 + nameBytes.length + compressed.length;
|
||||
}
|
||||
|
||||
const centralDirBuf = Buffer.concat(centralEntries);
|
||||
|
||||
// End of central directory (22 bytes)
|
||||
const eocd = Buffer.alloc(22);
|
||||
eocd.writeUInt32LE(0x06054b50, 0); // signature
|
||||
eocd.writeUInt16LE(0, 4); // disk number
|
||||
eocd.writeUInt16LE(0, 6); // cd disk number
|
||||
eocd.writeUInt16LE(files.length, 8); // entries on disk
|
||||
eocd.writeUInt16LE(files.length, 10); // total entries
|
||||
eocd.writeUInt32LE(centralDirBuf.length, 12); // cd size
|
||||
eocd.writeUInt32LE(offset, 16); // cd offset
|
||||
eocd.writeUInt16LE(0, 20); // comment length
|
||||
|
||||
return Buffer.concat([...localHeaders, centralDirBuf, eocd]);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Collect all relevant logs for submission
|
||||
* Returns { files: [{name, content}], meta: {username, platform, version} }
|
||||
*/
|
||||
function collectLogs() {
|
||||
const files = [];
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||
|
||||
// 1. Launcher logs
|
||||
const launcherLogDir = logger.getLogDirectory();
|
||||
if (launcherLogDir && fs.existsSync(launcherLogDir)) {
|
||||
// Today's launcher logs
|
||||
const todayLogs = getFilesForDate(launcherLogDir, todayStr, /^launcher-.*\.log$/);
|
||||
for (const f of todayLogs) {
|
||||
files.push({ name: `launcher/${f.name}`, content: readLogFile(f.path) });
|
||||
}
|
||||
|
||||
// Most recent from yesterday (just one)
|
||||
const yesterdayLogs = getFilesForDate(launcherLogDir, yesterdayStr, /^launcher-.*\.log$/);
|
||||
if (yesterdayLogs.length > 0) {
|
||||
files.push({ name: `launcher/${yesterdayLogs[0].name}`, content: readLogFile(yesterdayLogs[0].path) });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Game client logs
|
||||
const savesDir = getHytaleSavesDir();
|
||||
const clientLogDir = path.join(savesDir, 'Logs');
|
||||
if (fs.existsSync(clientLogDir)) {
|
||||
const clientLogs = getFilesForDate(clientLogDir, todayStr, /client\.log$/);
|
||||
for (const f of clientLogs) {
|
||||
files.push({ name: `client/${f.name}`, content: readLogFile(f.path) });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Config snapshot
|
||||
const appDir = logger.getAppDir ? logger.getAppDir() : logger.getInstallPath();
|
||||
const configPath = path.join(appDir, 'config.json');
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const configContent = fs.readFileSync(configPath, 'utf8');
|
||||
files.push({ name: 'config.json', content: configContent });
|
||||
} catch (err) {
|
||||
files.push({ name: 'config.json', content: `[Error reading config: ${err.message}]` });
|
||||
}
|
||||
}
|
||||
|
||||
// Build metadata
|
||||
const { app } = require('electron');
|
||||
let username = 'unknown';
|
||||
try {
|
||||
const configFile = path.join(appDir, 'config.json');
|
||||
if (fs.existsSync(configFile)) {
|
||||
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||||
username = cfg.username || cfg.playerName || 'unknown';
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return {
|
||||
files,
|
||||
meta: {
|
||||
username,
|
||||
platform: `${process.platform}-${process.arch}`,
|
||||
version: app.getVersion()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { collectLogs, createZipBuffer };
|
||||
@@ -1,4 +1,4 @@
|
||||
const { execSync } = require('child_process');
|
||||
const { execSync, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
function getOS() {
|
||||
@@ -116,117 +116,460 @@ function detectGpu() {
|
||||
}
|
||||
|
||||
function detectGpuLinux() {
|
||||
const output = execSync('lspci -nn | grep \'VGA\\|3D\'', { encoding: 'utf8' });
|
||||
let output = '';
|
||||
try {
|
||||
output = execSync('lspci -nn | grep -E "VGA|3D"', { encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
|
||||
}
|
||||
|
||||
const lines = output.split('\n').filter(line => line.trim());
|
||||
|
||||
let integratedName = null;
|
||||
let dedicatedName = null;
|
||||
let hasNvidia = false;
|
||||
let hasAmd = false;
|
||||
let gpus = {
|
||||
integrated: [],
|
||||
dedicated: []
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('VGA') || line.includes('3D')) {
|
||||
const match = line.match(/\[([^\]]+)\]/g);
|
||||
let modelName = null;
|
||||
if (match && match.length >= 2) {
|
||||
modelName = match[1].slice(1, -1);
|
||||
// Example: 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation TU116 [GeForce GTX 1660 Ti] [10de:2182] (rev a1)
|
||||
|
||||
// Matches all content inside [...]
|
||||
const brackets = line.match(/\[([^\]]+)\]/g);
|
||||
|
||||
let name = line; // fallback
|
||||
let vendorId = '';
|
||||
|
||||
if (brackets && brackets.length >= 2) {
|
||||
const idBracket = brackets.find(b => b.includes(':')); // [10de:2182]
|
||||
if (idBracket) {
|
||||
vendorId = idBracket.replace(/[\[\]]/g, '').split(':')[0].toLowerCase();
|
||||
|
||||
// The bracket before the ID bracket is usually the model name.
|
||||
const idIndex = brackets.indexOf(idBracket);
|
||||
if (idIndex > 0) {
|
||||
name = brackets[idIndex - 1].replace(/[\[\]]/g, '');
|
||||
}
|
||||
}
|
||||
} else if (brackets && brackets.length === 1) {
|
||||
name = brackets[0].replace(/[\[\]]/g, '');
|
||||
}
|
||||
|
||||
if (line.includes('10de:') || line.toLowerCase().includes('nvidia')) {
|
||||
hasNvidia = true;
|
||||
dedicatedName = "NVIDIA " + modelName || 'NVIDIA GPU';
|
||||
console.log('Detected NVIDIA GPU:', dedicatedName);
|
||||
} else if (line.includes('1002:') || line.toLowerCase().includes('amd') || line.toLowerCase().includes('radeon')) {
|
||||
hasAmd = true;
|
||||
dedicatedName = "AMD " + modelName || 'AMD GPU';
|
||||
console.log('Detected AMD GPU:', dedicatedName);
|
||||
} else if (line.includes('8086:') || line.toLowerCase().includes('intel')) {
|
||||
integratedName = "Intel " + modelName || 'Intel GPU';
|
||||
console.log('Detected Intel GPU:', integratedName);
|
||||
// Clean name
|
||||
name = name.trim();
|
||||
const lowerName = name.toLowerCase();
|
||||
const lowerLine = line.toLowerCase();
|
||||
|
||||
// Vendor detection
|
||||
const isNvidia = lowerLine.includes('nvidia') || vendorId === '10de';
|
||||
const isAmd = lowerLine.includes('amd') || lowerLine.includes('radeon') || vendorId === '1002';
|
||||
const isIntel = lowerLine.includes('intel') || vendorId === '8086';
|
||||
|
||||
// Intel Arc discrete GPU detection
|
||||
// Discrete Arc cards have specific model numbers: A310, A380, A580, A750, A770, B570, B580
|
||||
// Integrated "Intel Arc Graphics" (Meteor Lake, Lunar Lake, Arrow Lake) have NO model suffix
|
||||
// and sit on bus 00 (slot 00:02.0) — these are iGPUs, not dGPUs
|
||||
const pciSlot = line.split(' ')[0] || '';
|
||||
const pciBus = parseInt(pciSlot.split(':')[0], 16) || 0;
|
||||
const hasArcModelNumber = isIntel && /\b[ab]\d{3}\b/i.test(lowerName);
|
||||
const isIntelArc = isIntel && (hasArcModelNumber || (lowerName.includes('arc') && pciBus > 0));
|
||||
|
||||
let vendor = 'unknown';
|
||||
if (isNvidia) vendor = 'nvidia';
|
||||
else if (isAmd) vendor = 'amd';
|
||||
else if (isIntel) vendor = 'intel';
|
||||
|
||||
let vramMb = 0;
|
||||
|
||||
// VRAM Detection Logic
|
||||
if (isNvidia) {
|
||||
try {
|
||||
// Try nvidia-smi
|
||||
const smiOutput = execSync('nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
||||
const vramVal = parseInt(smiOutput.split('\n')[0]); // Take first if multiple
|
||||
if (!isNaN(vramVal)) {
|
||||
vramMb = vramVal;
|
||||
}
|
||||
} catch (err) {
|
||||
// failed
|
||||
}
|
||||
} else if (isAmd) {
|
||||
// Try /sys/class/drm/card*/device/mem_info_vram_total
|
||||
// This is a bit heuristical, we need to match the card.
|
||||
// But usually checking any card with AMD vendor in /sys is a good guess if we just want "the AMD GPU vram".
|
||||
try {
|
||||
const cards = fs.readdirSync('/sys/class/drm').filter(c => c.startsWith('card') && !c.includes('-'));
|
||||
for (const card of cards) {
|
||||
try {
|
||||
const vendorFile = fs.readFileSync(`/sys/class/drm/${card}/device/vendor`, 'utf8').trim();
|
||||
if (vendorFile === '0x1002') { // AMD vendor ID
|
||||
const vramBytes = fs.readFileSync(`/sys/class/drm/${card}/device/mem_info_vram_total`, 'utf8').trim();
|
||||
vramMb = Math.round(parseInt(vramBytes) / (1024 * 1024));
|
||||
if (vramMb > 0) break;
|
||||
}
|
||||
} catch (e2) {}
|
||||
}
|
||||
} catch (err) {}
|
||||
} else if (isIntel) {
|
||||
// Try lspci -v to get prefetchable memory (stolen/dedicated aperture)
|
||||
try {
|
||||
// Extract slot from line, e.g. "00:02.0"
|
||||
const slot = line.split(' ')[0];
|
||||
if (slot && /^[0-9a-f:.]+$/.test(slot)) {
|
||||
const verbose = execSync(`lspci -v -s ${slot}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
const vLines = verbose.split('\n');
|
||||
for (const vLine of vLines) {
|
||||
// Match "Memory at ... (..., prefetchable) [size=256M]"
|
||||
// Must ensure it is prefetchable and NOT non-prefetchable
|
||||
if (vLine.includes('prefetchable') && !vLine.includes('non-prefetchable')) {
|
||||
const match = vLine.match(/size=([0-9]+)([KMGT])/);
|
||||
if (match) {
|
||||
let size = parseInt(match[1]);
|
||||
const unit = match[2];
|
||||
if (unit === 'G') size *= 1024;
|
||||
else if (unit === 'K') size /= 1024;
|
||||
// M is default
|
||||
if (size > 0) {
|
||||
vramMb = size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const gpuInfo = {
|
||||
name: name,
|
||||
vendor: vendor,
|
||||
vram: vramMb
|
||||
};
|
||||
|
||||
if (isNvidia || isAmd || isIntelArc) {
|
||||
gpus.dedicated.push(gpuInfo);
|
||||
} else if (isIntel) {
|
||||
gpus.integrated.push(gpuInfo);
|
||||
} else {
|
||||
// Unknown vendor or other, fallback to integrated list to be safe
|
||||
gpus.integrated.push(gpuInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNvidia) {
|
||||
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
||||
} else if (hasAmd) {
|
||||
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
||||
} else {
|
||||
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Intel GPU', dedicatedName: null };
|
||||
// Fallback: Attempt to get Integrated VRAM via glxinfo if it's STILL 0 (common for Intel iGPUs if lspci failed)
|
||||
// glxinfo -B usually reports the active renderer's "Video memory" which includes shared memory for iGPUs.
|
||||
if (gpus.integrated.length > 0 && gpus.integrated[0].vram === 0) {
|
||||
try {
|
||||
const glxOut = execSync('glxinfo -B', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
const lines = glxOut.split('\n');
|
||||
let glxVendor = '';
|
||||
let glxMem = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trim = line.trim();
|
||||
if (trim.startsWith('Device:')) {
|
||||
const lower = trim.toLowerCase();
|
||||
if (lower.includes('intel')) glxVendor = 'intel';
|
||||
else if (lower.includes('nvidia')) glxVendor = 'nvidia';
|
||||
else if (lower.includes('amd') || lower.includes('ati')) glxVendor = 'amd';
|
||||
} else if (trim.startsWith('Video memory:')) {
|
||||
// Example: "Video memory: 15861MB"
|
||||
const memStr = trim.split(':')[1].replace('MB', '').trim();
|
||||
glxMem = parseInt(memStr, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// If glxinfo reports Intel and we have an Intel integrated GPU, update it
|
||||
// We check vendor match to ensure we don't accidentally assign Nvidia VRAM to Intel if user is running on dGPU
|
||||
if (glxVendor === 'intel' && gpus.integrated[0].vendor === 'intel' && glxMem > 0) {
|
||||
gpus.integrated[0].vram = glxMem;
|
||||
}
|
||||
} catch (err) {
|
||||
// glxinfo missing or failed, ignore
|
||||
}
|
||||
}
|
||||
|
||||
const primaryDedicated = gpus.dedicated[0] || null;
|
||||
const primaryIntegrated = gpus.integrated[0] || { name: 'Intel GPU', vram: 0 };
|
||||
|
||||
return {
|
||||
mode: primaryDedicated ? 'dedicated' : 'integrated',
|
||||
vendor: primaryDedicated ? primaryDedicated.vendor : (gpus.integrated[0] ? gpus.integrated[0].vendor : 'intel'),
|
||||
integratedName: primaryIntegrated.name,
|
||||
dedicatedName: primaryDedicated ? primaryDedicated.name : null,
|
||||
dedicatedVram: primaryDedicated ? primaryDedicated.vram : 0,
|
||||
integratedVram: primaryIntegrated.vram
|
||||
};
|
||||
}
|
||||
|
||||
function detectGpuWindows() {
|
||||
const output = execSync('wmic path win32_VideoController get name', { encoding: 'utf8' });
|
||||
const lines = output.split('\n').map(line => line.trim()).filter(line => line && line !== 'Name');
|
||||
let output = '';
|
||||
let commandUsed = 'cim'; // Track which command succeeded
|
||||
const POWERSHELL_TIMEOUT = 5000; // 5 second timeout to prevent hanging
|
||||
|
||||
let integratedName = null;
|
||||
let dedicatedName = null;
|
||||
let hasNvidia = false;
|
||||
let hasAmd = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const lowerLine = line.toLowerCase();
|
||||
if (lowerLine.includes('nvidia')) {
|
||||
hasNvidia = true;
|
||||
dedicatedName = line;
|
||||
console.log('Detected NVIDIA GPU:', dedicatedName);
|
||||
} else if (lowerLine.includes('amd') || lowerLine.includes('radeon')) {
|
||||
hasAmd = true;
|
||||
dedicatedName = line;
|
||||
console.log('Detected AMD GPU:', dedicatedName);
|
||||
} else if (lowerLine.includes('intel')) {
|
||||
integratedName = line;
|
||||
console.log('Detected Intel GPU:', integratedName);
|
||||
try {
|
||||
// Use spawnSync with explicit timeout instead of execSync to avoid ghost processes
|
||||
// Fetch Name and AdapterRAM (VRAM in bytes)
|
||||
const result = spawnSync('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-Command',
|
||||
'Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM | ConvertTo-Csv -NoTypeInformation'
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
timeout: POWERSHELL_TIMEOUT,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNvidia) {
|
||||
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
||||
} else if (hasAmd) {
|
||||
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Intel GPU', dedicatedName };
|
||||
} else {
|
||||
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Intel GPU', dedicatedName: null };
|
||||
}
|
||||
}
|
||||
|
||||
function detectGpuMac() {
|
||||
const output = execSync('system_profiler SPDisplaysDataType', { encoding: 'utf8' });
|
||||
const lines = output.split('\n');
|
||||
|
||||
let integratedName = null;
|
||||
let dedicatedName = null;
|
||||
let hasNvidia = false;
|
||||
let hasAmd = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('Chipset Model:')) {
|
||||
const gpuName = line.split('Chipset Model:')[1].trim();
|
||||
const lowerGpu = gpuName.toLowerCase();
|
||||
if (lowerGpu.includes('nvidia')) {
|
||||
hasNvidia = true;
|
||||
dedicatedName = gpuName;
|
||||
console.log('Detected NVIDIA GPU:', dedicatedName);
|
||||
} else if (lowerGpu.includes('amd') || lowerGpu.includes('radeon')) {
|
||||
hasAmd = true;
|
||||
dedicatedName = gpuName;
|
||||
console.log('Detected AMD GPU:', dedicatedName);
|
||||
} else if (lowerGpu.includes('intel') || lowerGpu.includes('iris') || lowerGpu.includes('uhd')) {
|
||||
integratedName = gpuName;
|
||||
console.log('Detected Intel GPU:', integratedName);
|
||||
} else if (!dedicatedName && !integratedName) {
|
||||
// Fallback for Apple Silicon or other
|
||||
integratedName = gpuName;
|
||||
|
||||
if (result.status === 0 && result.stdout) {
|
||||
output = result.stdout;
|
||||
} else {
|
||||
throw new Error(`PowerShell returned status ${result.status || result.signal}`);
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
// Fallback to Get-WmiObject (Older PowerShell)
|
||||
commandUsed = 'wmi';
|
||||
const result = spawnSync('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-Command',
|
||||
'Get-WmiObject Win32_VideoController | Select-Object Name, AdapterRAM | ConvertTo-Csv -NoTypeInformation'
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
timeout: POWERSHELL_TIMEOUT,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (result.status === 0 && result.stdout) {
|
||||
output = result.stdout;
|
||||
} else {
|
||||
throw new Error(`PowerShell WMI returned status ${result.status || result.signal}`);
|
||||
}
|
||||
} catch (e2) {
|
||||
// Fallback to wmic (Deprecated, often missing on newer Windows)
|
||||
// Note: This fallback likely won't provide VRAM in the same reliable CSV format easily,
|
||||
// so we stick to just getting the Name to at least allow the app to launch.
|
||||
try {
|
||||
commandUsed = 'wmic';
|
||||
const result = spawnSync('wmic.exe', ['path', 'win32_VideoController', 'get', 'name'], {
|
||||
encoding: 'utf8',
|
||||
timeout: POWERSHELL_TIMEOUT,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (result.status === 0 && result.stdout) {
|
||||
output = result.stdout;
|
||||
} else {
|
||||
throw new Error(`wmic returned status ${result.status || result.signal}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('All Windows GPU detection methods failed:', err.message);
|
||||
return { mode: 'unknown', vendor: 'none', integratedName: null, dedicatedName: null };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNvidia) {
|
||||
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Integrated GPU', dedicatedName };
|
||||
} else if (hasAmd) {
|
||||
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Integrated GPU', dedicatedName };
|
||||
// Parse lines.
|
||||
// PowerShell CSV output (Get-CimInstance/Get-WmiObject) usually looks like:
|
||||
// "Name","AdapterRAM"
|
||||
// "NVIDIA GeForce RTX 3060","12884901888"
|
||||
//
|
||||
// WMIC output is just plain text lines with the name (if we used the wmic command above).
|
||||
|
||||
const lines = output.split(/\r?\n/).filter(l => l.trim().length > 0);
|
||||
|
||||
let gpus = {
|
||||
integrated: [],
|
||||
dedicated: []
|
||||
};
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip header lines
|
||||
if (line.toLowerCase().includes('name') && (line.includes('AdapterRAM') || commandUsed === 'wmic')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = '';
|
||||
let vramBytes = 0;
|
||||
|
||||
if (commandUsed === 'wmic') {
|
||||
name = line.trim();
|
||||
} else {
|
||||
// Parse CSV: "Name","AdapterRAM"
|
||||
// Simple regex to handle potential quotes.
|
||||
// This assumes simple CSV structure from ConvertTo-Csv.
|
||||
const parts = line.split(',');
|
||||
// Remove surrounding quotes if present
|
||||
const rawName = parts[0] ? parts[0].replace(/^"|"$/g, '') : '';
|
||||
const rawRam = parts[1] ? parts[1].replace(/^"|"$/g, '') : '0';
|
||||
|
||||
name = rawName.trim();
|
||||
vramBytes = parseInt(rawRam, 10) || 0;
|
||||
}
|
||||
|
||||
if (!name) continue;
|
||||
|
||||
const lowerName = name.toLowerCase();
|
||||
const vramMb = Math.round(vramBytes / (1024 * 1024));
|
||||
|
||||
// Logic for dGPU detection; added isIntelArc check
|
||||
const isNvidia = lowerName.includes('nvidia');
|
||||
const isAmd = lowerName.includes('amd') || lowerName.includes('radeon');
|
||||
const isIntelArc = lowerName.includes('arc') && lowerName.includes('intel');
|
||||
|
||||
const gpuInfo = {
|
||||
name: name,
|
||||
vendor: isNvidia ? 'nvidia' : (isAmd ? 'amd' : (isIntelArc ? 'intel' : 'unknown')),
|
||||
vram: vramMb
|
||||
};
|
||||
|
||||
if (isNvidia || isAmd || isIntelArc) {
|
||||
gpus.dedicated.push(gpuInfo);
|
||||
} else if (lowerName.includes('intel') || lowerName.includes('iris') || lowerName.includes('uhd')) {
|
||||
gpus.integrated.push(gpuInfo);
|
||||
} else {
|
||||
// Fallback: If unknown vendor but high VRAM (> 512MB), treat as dedicated?
|
||||
// Or just assume integrated if generic "Microsoft Basic Display Adapter" etc.
|
||||
// For now, if we can't identify it as dedicated vendor, put in integrated/other.
|
||||
gpus.integrated.push(gpuInfo);
|
||||
}
|
||||
}
|
||||
|
||||
const primaryDedicated = gpus.dedicated[0] || null;
|
||||
const primaryIntegrated = gpus.integrated[0] || { name: 'Intel GPU', vram: 0 };
|
||||
|
||||
return {
|
||||
mode: primaryDedicated ? 'dedicated' : 'integrated',
|
||||
vendor: primaryDedicated ? primaryDedicated.vendor : 'intel', // Default to intel if only integrated found
|
||||
integratedName: primaryIntegrated.name,
|
||||
dedicatedName: primaryDedicated ? primaryDedicated.name : null,
|
||||
// Add VRAM info if available (mostly for debug or UI)
|
||||
dedicatedVram: primaryDedicated ? primaryDedicated.vram : 0,
|
||||
integratedVram: primaryIntegrated.vram
|
||||
};
|
||||
}
|
||||
|
||||
function detectGpuMac() {
|
||||
let output = '';
|
||||
try {
|
||||
output = execSync('system_profiler SPDisplaysDataType', { encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
|
||||
}
|
||||
|
||||
const lines = output.split('\n');
|
||||
let gpus = {
|
||||
integrated: [],
|
||||
dedicated: []
|
||||
};
|
||||
|
||||
let currentGpu = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// New block starts with "Chipset Model:"
|
||||
if (trimmed.startsWith('Chipset Model:')) {
|
||||
if (currentGpu) {
|
||||
// Push previous
|
||||
categorizeMacGpu(currentGpu, gpus);
|
||||
}
|
||||
currentGpu = {
|
||||
name: trimmed.split(':')[1].trim(),
|
||||
vendor: 'unknown',
|
||||
vram: 0
|
||||
};
|
||||
} else if (currentGpu) {
|
||||
if (trimmed.startsWith('VRAM (Total):') || trimmed.startsWith('VRAM (Dynamic, Max):')) {
|
||||
// Parse VRAM: "1.5 GB" or "1536 MB"
|
||||
const valParts = trimmed.split(':')[1].trim().split(' ');
|
||||
let val = parseFloat(valParts[0]);
|
||||
if (valParts[1] && valParts[1].toUpperCase() === 'GB') {
|
||||
val = val * 1024;
|
||||
}
|
||||
currentGpu.vram = Math.round(val);
|
||||
} else if (trimmed.startsWith('Vendor:') || trimmed.startsWith('Vendor Name:')) {
|
||||
// "Vendor: NVIDIA (0x10de)"
|
||||
const v = trimmed.split(':')[1].toLowerCase();
|
||||
if (v.includes('nvidia')) currentGpu.vendor = 'nvidia';
|
||||
else if (v.includes('amd') || v.includes('ati')) currentGpu.vendor = 'amd';
|
||||
else if (v.includes('intel')) currentGpu.vendor = 'intel';
|
||||
else if (v.includes('apple')) currentGpu.vendor = 'apple';
|
||||
}
|
||||
}
|
||||
}
|
||||
// Push last one
|
||||
if (currentGpu) {
|
||||
categorizeMacGpu(currentGpu, gpus);
|
||||
}
|
||||
|
||||
// If we have an Apple Silicon GPU (vendor=apple) but VRAM is 0, fetch system memory as it is unified.
|
||||
gpus.dedicated.forEach(gpu => {
|
||||
if (gpu.vendor === 'apple' && gpu.vram === 0) {
|
||||
try {
|
||||
const memSize = execSync('sysctl -n hw.memsize', { encoding: 'utf8' }).trim();
|
||||
// memSize is in bytes
|
||||
const memMb = Math.round(parseInt(memSize, 10) / (1024 * 1024));
|
||||
if (memMb > 0) gpu.vram = memMb;
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const primaryDedicated = gpus.dedicated[0] || null;
|
||||
const primaryIntegrated = gpus.integrated[0] || { name: 'Integrated GPU', vram: 0 };
|
||||
|
||||
return {
|
||||
mode: primaryDedicated ? 'dedicated' : 'integrated',
|
||||
vendor: primaryDedicated ? primaryDedicated.vendor : (gpus.integrated[0] ? gpus.integrated[0].vendor : 'intel'),
|
||||
integratedName: primaryIntegrated.name,
|
||||
dedicatedName: primaryDedicated ? primaryDedicated.name : null,
|
||||
dedicatedVram: primaryDedicated ? primaryDedicated.vram : 0,
|
||||
integratedVram: primaryIntegrated.vram
|
||||
};
|
||||
}
|
||||
|
||||
function categorizeMacGpu(gpu, gpus) {
|
||||
const lowerName = gpu.name.toLowerCase();
|
||||
|
||||
// Refine vendor if still unknown
|
||||
if (gpu.vendor === 'unknown') {
|
||||
if (lowerName.includes('nvidia')) gpu.vendor = 'nvidia';
|
||||
else if (lowerName.includes('amd') || lowerName.includes('radeon')) gpu.vendor = 'amd';
|
||||
else if (lowerName.includes('intel')) gpu.vendor = 'intel';
|
||||
else if (lowerName.includes('apple') || lowerName.includes('m1') || lowerName.includes('m2') || lowerName.includes('m3')) gpu.vendor = 'apple';
|
||||
}
|
||||
|
||||
const isNvidia = gpu.vendor === 'nvidia';
|
||||
const isAmd = gpu.vendor === 'amd';
|
||||
const isApple = gpu.vendor === 'apple';
|
||||
|
||||
// Per user request, "project is not meant for Intel Mac (x86)",
|
||||
// so we treat Apple Silicon as the primary "dedicated-like" GPU for this app's context.
|
||||
|
||||
if (isNvidia || isAmd || isApple) {
|
||||
gpus.dedicated.push(gpu);
|
||||
} else {
|
||||
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Integrated GPU', dedicatedName: null };
|
||||
// Intel or unknown
|
||||
gpus.integrated.push(gpu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,11 +610,108 @@ function setupGpuEnvironment(gpuPreference) {
|
||||
return envVars;
|
||||
}
|
||||
|
||||
function getSystemType() {
|
||||
const platform = getOS();
|
||||
try {
|
||||
if (platform === 'linux') return getSystemTypeLinux();
|
||||
if (platform === 'windows') return getSystemTypeWindows();
|
||||
if (platform === 'darwin') return getSystemTypeMac();
|
||||
return 'desktop'; // Default to desktop if unknown
|
||||
} catch (err) {
|
||||
console.warn('Failed to detect system type, defaulting to desktop:', err.message);
|
||||
return 'desktop';
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemTypeLinux() {
|
||||
try {
|
||||
// Try reliable DMI check first
|
||||
if (fs.existsSync('/sys/class/dmi/id/chassis_type')) {
|
||||
const type = parseInt(fs.readFileSync('/sys/class/dmi/id/chassis_type', 'utf8').trim());
|
||||
// 8=Portable, 9=Laptop, 10=Notebook, 11=Hand Held, 12=Docking Station, 14=Sub Notebook
|
||||
if ([8, 9, 10, 11, 12, 14, 31, 32].includes(type)) {
|
||||
return 'laptop';
|
||||
}
|
||||
}
|
||||
// Fallback to chassis_id for some systems? Usually chassis_type is enough.
|
||||
return 'desktop';
|
||||
} catch (e) {
|
||||
return 'desktop';
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemTypeWindows() {
|
||||
const POWERSHELL_TIMEOUT = 5000; // 5 second timeout
|
||||
|
||||
try {
|
||||
// Use spawnSync instead of execSync to avoid ghost processes
|
||||
const result = spawnSync('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-Command',
|
||||
'Get-CimInstance Win32_SystemEnclosure | Select-Object -ExpandProperty ChassisTypes'
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
timeout: POWERSHELL_TIMEOUT,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
if (result.error || result.status !== 0) {
|
||||
throw new Error(`PowerShell failed: ${result.error?.message || result.signal}`);
|
||||
}
|
||||
|
||||
const output = (result.stdout || '').trim();
|
||||
// Output might be a single number or array.
|
||||
// Clean it up
|
||||
const types = output.split(/\s+/).map(t => parseInt(t)).filter(n => !isNaN(n));
|
||||
|
||||
// Laptop codes: 8, 9, 10, 11, 12, 14, 31, 32
|
||||
const laptopCodes = [8, 9, 10, 11, 12, 14, 31, 32];
|
||||
|
||||
for (const t of types) {
|
||||
if (laptopCodes.includes(t)) return 'laptop';
|
||||
}
|
||||
return 'desktop';
|
||||
} catch (e) {
|
||||
// Fallback wmic
|
||||
try {
|
||||
const result = spawnSync('wmic.exe', ['path', 'win32_systemenclosure', 'get', 'chassistypes'], {
|
||||
encoding: 'utf8',
|
||||
timeout: POWERSHELL_TIMEOUT,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
if (result.status === 0 && result.stdout) {
|
||||
const output = result.stdout.trim();
|
||||
if (output.includes('8') || output.includes('9') || output.includes('10') || output.includes('14')) {
|
||||
return 'laptop';
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('System type detection failed:', err.message);
|
||||
}
|
||||
return 'desktop';
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemTypeMac() {
|
||||
try {
|
||||
const model = execSync('sysctl -n hw.model', { encoding: 'utf8' }).trim().toLowerCase();
|
||||
if (model.includes('book')) return 'laptop';
|
||||
return 'desktop';
|
||||
} catch (e) {
|
||||
return 'desktop';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOS,
|
||||
getArch,
|
||||
isWaylandSession,
|
||||
setupWaylandEnvironment,
|
||||
detectGpu,
|
||||
setupGpuEnvironment
|
||||
setupGpuEnvironment,
|
||||
getSystemType
|
||||
};
|
||||
|
||||
426
backend/utils/proxyClient.js
Normal file
426
backend/utils/proxyClient.js
Normal file
@@ -0,0 +1,426 @@
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
const https = require('https');
|
||||
const { PassThrough } = require('stream');
|
||||
|
||||
const PROXY_URL = process.env.HF2P_PROXY_URL || 'your_proxy_url_here';
|
||||
const SECRET_KEY = process.env.HF2P_SECRET_KEY || 'your_secret_key_here_for_jwt';
|
||||
const USE_DIRECT_FALLBACK = process.env.HF2P_USE_FALLBACK !== 'false';
|
||||
const DIRECT_TIMEOUT = 7000; // 7 seconds timeout
|
||||
|
||||
console.log('[ProxyClient] Initialized with proxy URL:', PROXY_URL ? 'YES' : 'NO');
|
||||
console.log('[ProxyClient] Secret key configured:', SECRET_KEY ? 'YES' : 'NO');
|
||||
console.log('[ProxyClient] Direct connection fallback:', USE_DIRECT_FALLBACK ? 'ENABLED' : 'DISABLED');
|
||||
console.log('[ProxyClient] Direct timeout before fallback:', DIRECT_TIMEOUT / 1000, 'seconds');
|
||||
|
||||
function generateToken() {
|
||||
const timestamp = Date.now().toString();
|
||||
const hash = crypto
|
||||
.createHmac('sha256', SECRET_KEY)
|
||||
.update(timestamp)
|
||||
.digest('hex');
|
||||
const token = `${timestamp}:${hash}`;
|
||||
console.log('[ProxyClient] Generated auth token:', token.substring(0, 20) + '...');
|
||||
return token;
|
||||
}
|
||||
|
||||
// Direct request without proxy
|
||||
async function directRequest(url, options = {}) {
|
||||
console.log('[ProxyClient] Attempting direct request (no proxy)');
|
||||
console.log('[ProxyClient] Direct URL:', url);
|
||||
|
||||
const timeoutMs = options.timeout || DIRECT_TIMEOUT;
|
||||
const controller = new AbortController();
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn('[ProxyClient] TIMEOUT! Aborting direct request after', timeoutMs, 'ms');
|
||||
controller.abort();
|
||||
}, timeoutMs);
|
||||
|
||||
try {
|
||||
const config = {
|
||||
method: options.method || 'GET',
|
||||
url: url,
|
||||
headers: options.headers || {},
|
||||
timeout: timeoutMs,
|
||||
responseType: options.responseType,
|
||||
signal: controller.signal
|
||||
};
|
||||
|
||||
const response = await axios(config);
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy request (original function)
|
||||
async function proxyRequest(url, options = {}) {
|
||||
console.log('[ProxyClient] Starting proxy request');
|
||||
console.log('[ProxyClient] Original URL:', url);
|
||||
console.log('[ProxyClient] Options:', JSON.stringify(options, null, 2));
|
||||
|
||||
try {
|
||||
const token = generateToken();
|
||||
const urlObj = new URL(url);
|
||||
const targetUrl = `${urlObj.protocol}//${urlObj.host}`;
|
||||
|
||||
console.log('[ProxyClient] Parsed URL components:');
|
||||
console.log(' - Protocol:', urlObj.protocol);
|
||||
console.log(' - Host:', urlObj.host);
|
||||
console.log(' - Pathname:', urlObj.pathname);
|
||||
console.log(' - Search:', urlObj.search);
|
||||
console.log(' - Target URL:', targetUrl);
|
||||
|
||||
const proxyEndpoint = `${PROXY_URL}/proxy${urlObj.pathname}${urlObj.search}`;
|
||||
console.log('[ProxyClient] Proxy endpoint:', proxyEndpoint);
|
||||
|
||||
const config = {
|
||||
method: options.method || 'GET',
|
||||
url: proxyEndpoint,
|
||||
headers: {
|
||||
'X-Auth-Token': token,
|
||||
'X-Target-URL': targetUrl,
|
||||
...(options.headers || {})
|
||||
},
|
||||
timeout: options.timeout || 30000,
|
||||
responseType: options.responseType
|
||||
};
|
||||
|
||||
console.log('[ProxyClient] Request config:', JSON.stringify({
|
||||
method: config.method,
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
timeout: config.timeout,
|
||||
responseType: config.responseType
|
||||
}, null, 2));
|
||||
|
||||
const response = await axios(config);
|
||||
console.log('[ProxyClient] Response received - Status:', response.status);
|
||||
console.log('[ProxyClient] Response headers:', JSON.stringify(response.headers, null, 2));
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[ProxyClient] Request failed!');
|
||||
console.error('[ProxyClient] Error type:', error.constructor.name);
|
||||
console.error('[ProxyClient] Error message:', error.message);
|
||||
if (error.response) {
|
||||
console.error('[ProxyClient] Response status:', error.response.status);
|
||||
console.error('[ProxyClient] Response data:', error.response.data);
|
||||
console.error('[ProxyClient] Response headers:', error.response.headers);
|
||||
}
|
||||
if (error.config) {
|
||||
console.error('[ProxyClient] Failed request URL:', error.config.url);
|
||||
console.error('[ProxyClient] Failed request headers:', error.config.headers);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Smart request with automatic fallback
|
||||
async function smartRequest(url, options = {}) {
|
||||
if (!USE_DIRECT_FALLBACK) {
|
||||
console.log('[ProxyClient] Fallback disabled, using proxy directly');
|
||||
return proxyRequest(url, options);
|
||||
}
|
||||
|
||||
console.log('[ProxyClient] Smart request with fallback enabled');
|
||||
console.log('[ProxyClient] Direct timeout configured:', DIRECT_TIMEOUT, 'ms');
|
||||
|
||||
const directStartTime = Date.now();
|
||||
try {
|
||||
console.log('[ProxyClient] [ATTEMPT 1/2] Trying direct connection first...');
|
||||
const response = await directRequest(url, options);
|
||||
const directDuration = Date.now() - directStartTime;
|
||||
console.log('[ProxyClient] [SUCCESS] Direct connection successful in', directDuration, 'ms');
|
||||
return response;
|
||||
} catch (directError) {
|
||||
const directDuration = Date.now() - directStartTime;
|
||||
console.warn('[ProxyClient] [FAILED] Direct connection failed after', directDuration, 'ms');
|
||||
console.warn('[ProxyClient] Error message:', directError.message);
|
||||
console.warn('[ProxyClient] Error code:', directError.code);
|
||||
|
||||
// Always fallback to proxy on any error
|
||||
console.log('[ProxyClient] Attempting proxy fallback for all errors...');
|
||||
|
||||
if (true) {
|
||||
console.log('[ProxyClient] [ATTEMPT 2/2] Falling back to proxy connection...');
|
||||
try {
|
||||
const proxyStartTime = Date.now();
|
||||
const response = await proxyRequest(url, options);
|
||||
const proxyDuration = Date.now() - proxyStartTime;
|
||||
console.log('[ProxyClient] [SUCCESS] Proxy connection successful in', proxyDuration, 'ms');
|
||||
return response;
|
||||
} catch (proxyError) {
|
||||
console.error('[ProxyClient] [FAILED] Both direct and proxy connections failed!');
|
||||
console.error('[ProxyClient] Direct error:', directError.message);
|
||||
console.error('[ProxyClient] Proxy error:', proxyError.message);
|
||||
throw proxyError;
|
||||
}
|
||||
} else {
|
||||
console.log('[ProxyClient] [SKIP] Direct error not related to connectivity, not falling back');
|
||||
throw directError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Direct download stream without proxy
|
||||
function directDownloadStream(url, onData) {
|
||||
console.log('[ProxyClient] Starting direct download stream (no proxy)');
|
||||
console.log('[ProxyClient] Direct download URL:', url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const protocol = urlObj.protocol === 'https:' ? https : require('http');
|
||||
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
timeout: DIRECT_TIMEOUT
|
||||
};
|
||||
|
||||
const handleResponse = (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
const redirectUrl = response.headers.location;
|
||||
console.log('[ProxyClient] Direct redirect to:', redirectUrl);
|
||||
directDownloadStream(redirectUrl, onData).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`Direct HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (onData) {
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
let downloaded = 0;
|
||||
const passThrough = new PassThrough();
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloaded += chunk.length;
|
||||
onData(chunk, downloaded, totalSize);
|
||||
});
|
||||
|
||||
response.pipe(passThrough);
|
||||
resolve(passThrough);
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
};
|
||||
|
||||
const req = protocol.get(options, handleResponse);
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('[ProxyClient] Direct download error:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
console.warn('[ProxyClient] TIMEOUT! Direct download timed out after', DIRECT_TIMEOUT, 'ms');
|
||||
req.destroy();
|
||||
const timeoutError = new Error('ETIMEDOUT: Direct connection timeout');
|
||||
timeoutError.code = 'ETIMEDOUT';
|
||||
reject(timeoutError);
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getProxyDownloadStream(url, onData) {
|
||||
console.log('[ProxyClient] Starting download stream');
|
||||
console.log('[ProxyClient] Download URL:', url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const token = generateToken();
|
||||
const urlObj = new URL(url);
|
||||
const targetUrl = `${urlObj.protocol}//${urlObj.host}`;
|
||||
|
||||
console.log('[ProxyClient] Download URL parsed:');
|
||||
console.log(' - Protocol:', urlObj.protocol);
|
||||
console.log(' - Host:', urlObj.host);
|
||||
console.log(' - Hostname:', urlObj.hostname);
|
||||
console.log(' - Port:', urlObj.port);
|
||||
console.log(' - Pathname:', urlObj.pathname);
|
||||
console.log(' - Search:', urlObj.search);
|
||||
console.log(' - Target URL:', targetUrl);
|
||||
|
||||
const proxyUrl = new URL(PROXY_URL);
|
||||
const requestPath = `/proxy${urlObj.pathname}${urlObj.search}`;
|
||||
|
||||
console.log('[ProxyClient] Proxy configuration:');
|
||||
console.log(' - Proxy URL:', PROXY_URL);
|
||||
console.log(' - Proxy protocol:', proxyUrl.protocol);
|
||||
console.log(' - Proxy hostname:', proxyUrl.hostname);
|
||||
console.log(' - Proxy port:', proxyUrl.port);
|
||||
console.log(' - Request path:', requestPath);
|
||||
|
||||
const options = {
|
||||
hostname: proxyUrl.hostname,
|
||||
port: proxyUrl.port || (proxyUrl.protocol === 'https:' ? 443 : 80),
|
||||
path: requestPath,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Token': token,
|
||||
'X-Target-URL': targetUrl
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[ProxyClient] HTTP request options:', JSON.stringify(options, null, 2));
|
||||
|
||||
const protocol = proxyUrl.protocol === 'https:' ? https : require('http');
|
||||
console.log('[ProxyClient] Using protocol:', proxyUrl.protocol);
|
||||
|
||||
const handleResponse = (response) => {
|
||||
console.log('[ProxyClient] Response received - Status:', response.statusCode);
|
||||
console.log('[ProxyClient] Response headers:', JSON.stringify(response.headers, null, 2));
|
||||
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
const redirectUrl = response.headers.location;
|
||||
console.log('[ProxyClient] Redirect detected to:', redirectUrl);
|
||||
|
||||
if (redirectUrl.startsWith('http')) {
|
||||
console.log('[ProxyClient] Following redirect...');
|
||||
getProxyDownloadStream(redirectUrl, onData).then(resolve).catch(reject);
|
||||
} else {
|
||||
console.error('[ProxyClient] Invalid redirect URL:', redirectUrl);
|
||||
reject(new Error(`Invalid redirect: ${redirectUrl}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
console.error('[ProxyClient] Unexpected status code:', response.statusCode);
|
||||
console.error('[ProxyClient] Response message:', response.statusMessage);
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (onData) {
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
console.log('[ProxyClient] Download starting - Total size:', totalSize, 'bytes');
|
||||
|
||||
let downloaded = 0;
|
||||
const passThrough = new PassThrough();
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloaded += chunk.length;
|
||||
const progress = ((downloaded / totalSize) * 100).toFixed(2);
|
||||
onData(chunk, downloaded, totalSize);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
console.log('[ProxyClient] Download completed -', downloaded, 'bytes received');
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
console.error('[ProxyClient] Response stream error:', error.message);
|
||||
});
|
||||
|
||||
response.pipe(passThrough);
|
||||
console.log('[ProxyClient] Stream piped to PassThrough');
|
||||
resolve(passThrough);
|
||||
} else {
|
||||
console.log('[ProxyClient] Returning raw response stream (no progress callback)');
|
||||
resolve(response);
|
||||
}
|
||||
};
|
||||
|
||||
const request = protocol.get(options, handleResponse);
|
||||
|
||||
request.on('error', (error) => {
|
||||
console.error('[ProxyClient] HTTP request error!');
|
||||
console.error('[ProxyClient] Error type:', error.constructor.name);
|
||||
console.error('[ProxyClient] Error message:', error.message);
|
||||
console.error('[ProxyClient] Error code:', error.code);
|
||||
console.error('[ProxyClient] Error stack:', error.stack);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
console.log('[ProxyClient] HTTP request sent');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ProxyClient] Exception in getProxyDownloadStream!');
|
||||
console.error('[ProxyClient] Error type:', error.constructor.name);
|
||||
console.error('[ProxyClient] Error message:', error.message);
|
||||
console.error('[ProxyClient] Error stack:', error.stack);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Smart download stream with automatic fallback
|
||||
function smartDownloadStream(url, onData) {
|
||||
if (!USE_DIRECT_FALLBACK) {
|
||||
console.log('[ProxyClient] Fallback disabled, using proxy stream directly');
|
||||
return getProxyDownloadStream(url, onData);
|
||||
}
|
||||
|
||||
console.log('[ProxyClient] Smart download stream with fallback enabled');
|
||||
console.log('[ProxyClient] Direct timeout configured:', DIRECT_TIMEOUT, 'ms');
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const directStartTime = Date.now();
|
||||
try {
|
||||
console.log('[ProxyClient] [DOWNLOAD 1/2] Trying direct download first...');
|
||||
const stream = await directDownloadStream(url, onData);
|
||||
const directDuration = Date.now() - directStartTime;
|
||||
console.log('[ProxyClient] [SUCCESS] Direct download stream established in', directDuration, 'ms');
|
||||
resolve(stream);
|
||||
} catch (directError) {
|
||||
const directDuration = Date.now() - directStartTime;
|
||||
console.warn('[ProxyClient] [FAILED] Direct download failed after', directDuration, 'ms');
|
||||
console.warn('[ProxyClient] Error message:', directError.message);
|
||||
console.warn('[ProxyClient] Error code:', directError.code);
|
||||
|
||||
// Always fallback to proxy on any error
|
||||
console.log('[ProxyClient] Attempting proxy fallback for all download errors...');
|
||||
|
||||
if (true) {
|
||||
console.log('[ProxyClient] [DOWNLOAD 2/2] Falling back to proxy download...');
|
||||
try {
|
||||
const proxyStartTime = Date.now();
|
||||
const stream = await getProxyDownloadStream(url, onData);
|
||||
const proxyDuration = Date.now() - proxyStartTime;
|
||||
console.log('[ProxyClient] [SUCCESS] Proxy download stream established in', proxyDuration, 'ms');
|
||||
resolve(stream);
|
||||
} catch (proxyError) {
|
||||
console.error('[ProxyClient] [FAILED] Both direct and proxy downloads failed!');
|
||||
console.error('[ProxyClient] Direct error:', directError.message);
|
||||
console.error('[ProxyClient] Proxy error:', proxyError.message);
|
||||
reject(proxyError);
|
||||
}
|
||||
} else {
|
||||
console.log('[ProxyClient] [SKIP] Direct error not related to connectivity, not falling back');
|
||||
reject(directError);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// Recommended: Smart functions with automatic fallback
|
||||
smartRequest,
|
||||
smartDownloadStream,
|
||||
|
||||
// Legacy: Direct proxy functions (for manual control)
|
||||
proxyRequest,
|
||||
getProxyDownloadStream,
|
||||
|
||||
// Direct functions (no proxy)
|
||||
directRequest,
|
||||
directDownloadStream,
|
||||
|
||||
// Utilities
|
||||
generateToken
|
||||
};
|
||||
@@ -1,3 +1,2 @@
|
||||
provider: github
|
||||
owner: amiayweb # Change to your own GitHub username
|
||||
repo: Hytale-F2P
|
||||
provider: generic
|
||||
url: https://git.sanhost.net/sanasol/hytale-f2p/releases/download/latest
|
||||
|
||||
732
docs/CLIENT_BINARY_ANALYSIS.md
Normal file
732
docs/CLIENT_BINARY_ANALYSIS.md
Normal file
@@ -0,0 +1,732 @@
|
||||
# Hytale Client Binary Analysis
|
||||
|
||||
CSC_LINK="/Users/sanasol/Downloads/Certificates-hytale.p12" CSC_KEY_PASSWORD="YieocpBVP68Rih*" APPLE_ID="sanasol2008rs@gmail.com" APPLE_APP_SPECIFIC_PASSWORD="ihah-lbta-movj-iqni" APPLE_TEAM_ID="9WVL8YG95H" npm run build:mac
|
||||
CSC_LINK="/Users/sanasol/Downloads/Certificates-hytale.p12" CSC_KEY_PASSWORD="YieocpBVP68Rih*" APPLE_ID="sanasol2008rs@gmail.com" APPLE_APP_SPECIFIC_PASSWORD="ihah-lbta-movj-iqni" APPLE_TEAM_ID="9WVL8YG95H" npx electron-builder --mac --arm64
|
||||
|
||||
|
||||
password ihah-lbta-movj-iqni
|
||||
team id 9WVL8YG95H
|
||||
cert pass YieocpBVP68Rih*
|
||||
|
||||
## Overview
|
||||
|
||||
This document contains a comprehensive analysis of the HytaleClient binary, documenting all discovered URLs, API endpoints, service domains, patchable strings, and internal functionality.
|
||||
|
||||
**Binary Analyzed:** `HytaleClient` (macOS .NET AOT compiled)
|
||||
**Analysis Date:** 2026-01-27
|
||||
**String Encoding:** UTF-16LE (Windows .NET string format)
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Service URLs](#1-service-urls)
|
||||
2. [API Endpoints](#2-api-endpoints)
|
||||
3. [World Tools & Builder Tools](#3-world-tools--builder-tools)
|
||||
4. [External Service URLs](#4-external-service-urls)
|
||||
5. [Patchable Strings](#5-patchable-strings)
|
||||
6. [Sentry Error Tracking](#6-sentry-error-tracking)
|
||||
7. [Internal Class References](#7-internal-class-references)
|
||||
8. [Binary Offsets Reference](#8-binary-offsets-reference)
|
||||
9. [Implementation Notes](#9-implementation-notes)
|
||||
|
||||
---
|
||||
|
||||
## 1. Service URLs
|
||||
|
||||
### 1.1 Primary Hytale Services
|
||||
|
||||
The client connects to four main service subdomains:
|
||||
|
||||
| Service | URL Pattern | Purpose | Status |
|
||||
|---------|-------------|---------|--------|
|
||||
| **Sessions** | `https://sessions.{domain}` | Authentication, JWT tokens, session management | Implemented in auth-server |
|
||||
| **Account Data** | `https://account-data.{domain}` | Player profiles, skins, account information | Implemented in auth-server |
|
||||
| **Telemetry** | `https://telemetry.{domain}` | Analytics, error reporting, usage statistics | Implemented (accepts/discards) |
|
||||
| **Tools** | `https://tools.{domain}` | Asset editor, prefab management, world tools | **Not implemented** |
|
||||
|
||||
### 1.2 URL Construction
|
||||
|
||||
The client constructs URLs by combining:
|
||||
1. Protocol: `https://`
|
||||
2. Subdomain: `sessions.`, `account-data.`, `telemetry.`, `tools.`
|
||||
3. Base domain: `hytale.com`
|
||||
|
||||
**Example:** `https://` + `sessions.` + `hytale.com` = `https://sessions.hytale.com`
|
||||
|
||||
The client patcher replaces these components to redirect traffic to the F2P auth server.
|
||||
|
||||
### 1.3 F2P Domain Routing
|
||||
|
||||
For F2P mode, all subdomains route to a single endpoint:
|
||||
- `sessions.{f2p_domain}` → `https://{f2p_domain}`
|
||||
- `account-data.{f2p_domain}` → `https://{f2p_domain}`
|
||||
- `telemetry.{f2p_domain}` → `https://{f2p_domain}`
|
||||
- `tools.{f2p_domain}` → `https://{f2p_domain}`
|
||||
|
||||
---
|
||||
|
||||
## 2. API Endpoints
|
||||
|
||||
### 2.1 Session Management Endpoints
|
||||
|
||||
#### POST /game-session/new
|
||||
Create a new game session.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"clientVersion": "string",
|
||||
"platform": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"token": "JWT token",
|
||||
"refreshToken": "refresh token",
|
||||
"expiresIn": 36000
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /game-session/refresh
|
||||
Refresh an existing session token.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"refreshToken": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"token": "new JWT token",
|
||||
"refreshToken": "new refresh token",
|
||||
"expiresIn": 36000
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /game-session/child
|
||||
Create a child session (for server connections).
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"parentToken": "string",
|
||||
"audience": "server identifier"
|
||||
}
|
||||
```
|
||||
|
||||
#### DELETE /game-session
|
||||
Notify server of session end (player disconnect).
|
||||
|
||||
### 2.2 Server Join Endpoints
|
||||
|
||||
#### POST /server-join/auth-grant
|
||||
Request authorization grant for connecting to a game server.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"serverAddress": "string",
|
||||
"serverPort": number
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"grant": "authorization grant string"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /server-join/auth-token
|
||||
Exchange authorization grant for server-specific token with certificate binding.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"grant": "authorization grant",
|
||||
"clientCertHash": "SHA256 hash of client certificate"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"token": "server-specific JWT with cnf claim"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Account Endpoints
|
||||
|
||||
#### GET /my-account/game-profile
|
||||
Get the player's game profile.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"uuid": "player UUID",
|
||||
"username": "display name",
|
||||
"createdAt": "timestamp"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /my-account/game-profile
|
||||
Update the player's game profile.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"username": "new display name"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /my-account/cosmetics
|
||||
Get list of unlocked cosmetics for the player.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"cosmetics": [
|
||||
{
|
||||
"id": "cosmetic_id",
|
||||
"category": "category_name",
|
||||
"unlockedAt": "timestamp"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /my-account/skin
|
||||
Save player's skin/character customization preferences.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"skinTone": "SkinTone_01",
|
||||
"bodyType": "Default",
|
||||
"parts": {
|
||||
"haircut": "Haircut_ShortMessy.Blue",
|
||||
"eyes": "Eyes_Default.Green",
|
||||
"eyebrows": "Eyebrows_Default",
|
||||
"face": "Face_Default"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 JWKS Endpoint
|
||||
|
||||
#### GET /.well-known/jwks.json
|
||||
Get JSON Web Key Set for JWT verification.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "OKP",
|
||||
"crv": "Ed25519",
|
||||
"x": "base64url-encoded-public-key",
|
||||
"kid": "key-id",
|
||||
"use": "sig",
|
||||
"alg": "EdDSA"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 Profile Lookup Endpoints
|
||||
|
||||
#### GET /profile/uuid/{uuid}
|
||||
Lookup player profile by UUID.
|
||||
|
||||
#### GET /profile/username/{username}
|
||||
Lookup player profile by username (server-scoped).
|
||||
|
||||
---
|
||||
|
||||
## 3. World Tools & Builder Tools
|
||||
|
||||
### 3.1 World Tools (worldtools.*)
|
||||
|
||||
These are in-game tools for world creation and editing in builder/creative mode.
|
||||
|
||||
| Tool | Command | Description |
|
||||
|------|---------|-------------|
|
||||
| **Change Model** | `worldtools.changeModel` | Change the model of an entity in the world |
|
||||
| **Import Image** | `worldtools.importImage` | Import image files into the world as textures |
|
||||
| **Import OBJ** | `worldtools.importObj` | Import 3D OBJ model files into the world |
|
||||
| **Instance** | `worldtools.instance` | Manage world instances and copies |
|
||||
| **Play Sound** | `worldtools.playSound` | Play sound effects in the world |
|
||||
| **Prefab Editor** | `worldtools.prefabEditor` | Open the prefab editor interface |
|
||||
| **Prefab List** | `worldtools.prefabList` | List and manage saved prefabs |
|
||||
| **Spawn Entity** | `worldtools.spawnEntity` | Spawn entities at specified locations |
|
||||
| **Spawn Particle** | `worldtools.spawnParticle` | Spawn particle effects |
|
||||
| **Tint Chunk** | `worldtools.tintChunk` | Apply color tinting to world chunks |
|
||||
|
||||
### 3.2 Builder Tools (buildertools.*)
|
||||
|
||||
Additional tools for the asset editor and builder mode.
|
||||
|
||||
| Tool | Class | Description |
|
||||
|------|-------|-------------|
|
||||
| **Image Import** | `buildertools.imageimport.ImageImportPage` | UI page for importing images |
|
||||
| **OBJ Import** | `buildertools.objimport.ObjImportPage` | UI page for importing OBJ models |
|
||||
| **Prefab Editor** | `buildertools.prefabeditor.ui.PrefabEditorLoadSettings` | Prefab editor with load/save |
|
||||
| **Prefab List** | `buildertools.prefablist.PrefabPage` | Prefab listing and management |
|
||||
|
||||
### 3.3 Machinima Tool
|
||||
|
||||
- **Purpose:** In-game cinematic/video recording tool
|
||||
- **Access:** Available via hotbar slot
|
||||
- **Message:** "Hotbar is full. Clear a slot to receive the Machinima tool."
|
||||
|
||||
### 3.4 Asset Editor
|
||||
|
||||
The client includes an asset editor with these features:
|
||||
- `AssetEditorDownload` - Download assets from tools service
|
||||
- `assetEditor.exportModal` - Export modal for assets
|
||||
- `assetEditor.fileSaveState` - File save state management
|
||||
- `assetEditor.property.tooltip` - Property tooltips
|
||||
|
||||
### 3.5 tools.hytale.com API Requirements
|
||||
|
||||
To fully support builder mode, the tools service would need:
|
||||
|
||||
```
|
||||
POST /assets/upload
|
||||
- Upload asset files (images, models, sounds)
|
||||
- Returns asset ID/URL
|
||||
|
||||
GET /assets/{assetId}
|
||||
- Download asset by ID
|
||||
- Returns asset binary data
|
||||
|
||||
POST /prefabs/save
|
||||
- Save prefab definition
|
||||
- Returns prefab ID
|
||||
|
||||
GET /prefabs/{prefabId}
|
||||
- Load prefab by ID
|
||||
- Returns prefab JSON
|
||||
|
||||
GET /prefabs/list
|
||||
- List user's saved prefabs
|
||||
- Returns array of prefab metadata
|
||||
|
||||
DELETE /prefabs/{prefabId}
|
||||
- Delete a prefab
|
||||
```
|
||||
|
||||
**Note:** The game functions without tools.hytale.com - it's only needed for cloud-based asset sharing in builder mode.
|
||||
|
||||
---
|
||||
|
||||
## 4. External Service URLs
|
||||
|
||||
### 4.1 Hytale Official URLs
|
||||
|
||||
| URL | Purpose | Patchable |
|
||||
|-----|---------|-----------|
|
||||
| `https://store.hytale.com/?upgrade=` | In-game store for purchases | Yes |
|
||||
| `https://hytale.com/help/joining-friends` | Help documentation | Yes |
|
||||
| `https://discord.gg/hytale` | Official Discord invite | Yes |
|
||||
|
||||
### 4.2 Third-Party Service URLs
|
||||
|
||||
| URL | Purpose | Notes |
|
||||
|-----|---------|-------|
|
||||
| `https://blockbench.net/downloads` | Blockbench download page | 3D model editor |
|
||||
| `https://blockbench.net/plugins/hytale_plugin` | Hytale Blockbench plugin | For asset creation |
|
||||
| `https://docs.sentry.io/platforms/dotnet/*` | Sentry documentation | Error tracking docs |
|
||||
| `https://aka.ms/*` | Microsoft .NET documentation | Runtime docs |
|
||||
| `https://learn.microsoft.com/*` | Microsoft Learn | .NET API docs |
|
||||
| `https://go.microsoft.com/*` | Microsoft redirects | Various docs |
|
||||
|
||||
### 4.3 Graphics/Rendering References
|
||||
|
||||
| URL | Purpose |
|
||||
|-----|---------|
|
||||
| `https://www.khronos.org/opengl/wiki/Interface_Block_(GLSL)` | GLSL interface blocks |
|
||||
| `https://www.khronos.org/opengl/wiki/Sampler_(GLSL)` | GLSL texture samplers |
|
||||
| `https://www.shadertoy.com/view/Xd23Dh` | Shader reference |
|
||||
| `https://www.shadertoy.com/view/ltlSRj` | Shader reference |
|
||||
| `https://aras-p.info/texts/CompactNormalStorage.html` | Normal map compression |
|
||||
| `https://mynameismjp.wordpress.com/2009/03/10/reconstructing-position-from-depth/` | Depth reconstruction |
|
||||
| `https://briansharpe.files.wordpress.com/2018/07/moment-transparency-supp-av.pdf` | Transparency rendering |
|
||||
| `https://www.pmavridis.com/research/fbcompression/` | Frame buffer compression |
|
||||
| `https://jcgt.org/published/0002/02/09/` | Graphics technique |
|
||||
| `https://jcgt.org/published/0006/01/03/` | Graphics technique |
|
||||
| `https://graphics.cs.williams.edu/papers/CSSM/` | Graphics paper |
|
||||
| `http://www.humus.name/Articles/Persson_LowLevelThinking.pdf` | Low-level graphics |
|
||||
|
||||
### 4.4 GitHub References
|
||||
|
||||
| URL | Purpose |
|
||||
|-----|---------|
|
||||
| `https://github.com/NLog/NLog.git` | Logging framework |
|
||||
| `https://github.com/Noesis/Managed/tree/master/Src/Noesis/Core` | NoesisGUI core |
|
||||
| `https://github.com/Noesis/Managed/tree/master/Src/NoesisApp/Core` | NoesisGUI app |
|
||||
| `https://github.com/dotnet/dotnet` | .NET runtime |
|
||||
| `https://github.com/ektrah/nsec.git` | NSec cryptography |
|
||||
| `https://github.com/getsentry/sentry-dotnet` | Sentry .NET SDK |
|
||||
|
||||
---
|
||||
|
||||
## 5. Patchable Strings
|
||||
|
||||
### 5.1 Domain Strings
|
||||
|
||||
| Original | Replacement | Purpose |
|
||||
|----------|-------------|---------|
|
||||
| `hytale.com` | `{f2p_domain}` | Base domain (4-16 chars) |
|
||||
| `sessions.` | Stripped or replaced | Session service subdomain |
|
||||
| `account-data.` | Stripped or replaced | Account service subdomain |
|
||||
| `telemetry.` | Stripped or replaced | Telemetry subdomain |
|
||||
| `tools.` | Stripped or replaced | Tools service subdomain |
|
||||
|
||||
### 5.2 URL Strings
|
||||
|
||||
| Original | Can Replace With | Notes |
|
||||
|----------|------------------|-------|
|
||||
| `https://store.hytale.com/?upgrade=` | Custom store URL | In-game purchases |
|
||||
| `https://discord.gg/hytale` | Custom Discord | Community link |
|
||||
| `https://hytale.com/help/joining-friends` | Custom help docs | Help system |
|
||||
| `sentry.hytale.com` | Own Sentry or disable | Error tracking |
|
||||
|
||||
### 5.3 String Encoding Details
|
||||
|
||||
**.NET UTF-16LE Format:**
|
||||
- Each character is 2 bytes (little-endian)
|
||||
- Example: "hytale" = `68 00 79 00 74 00 61 00 6c 00 65 00`
|
||||
- Strings are length-prefixed in the binary
|
||||
|
||||
**Length Prefix Format:**
|
||||
- 1 byte for strings < 128 chars
|
||||
- 2 bytes (varint) for longer strings
|
||||
- Followed by UTF-16LE character data
|
||||
|
||||
### 5.4 Current Patcher Behavior
|
||||
|
||||
The `clientPatcher.js` patches:
|
||||
1. `sessions.hytale.com` → `{f2p_domain}` (single endpoint)
|
||||
2. `account-data.hytale.com` → `{f2p_domain}`
|
||||
3. `telemetry.hytale.com` → `{f2p_domain}`
|
||||
|
||||
**Not currently patched:**
|
||||
- `tools.hytale.com` (builder mode assets)
|
||||
- `store.hytale.com` (in-game store)
|
||||
- `sentry.hytale.com` (error tracking)
|
||||
|
||||
---
|
||||
|
||||
## 6. Sentry Error Tracking
|
||||
|
||||
### 6.1 Sentry Configuration
|
||||
|
||||
**DSN Found in Binary:**
|
||||
```
|
||||
https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/
|
||||
```
|
||||
|
||||
**DSN Components:**
|
||||
- Protocol: `https`
|
||||
- Public Key: `ca900df42fcf57d4dd8401a86ddd7da2`
|
||||
- Host: `sentry.hytale.com`
|
||||
- Project ID: (after trailing slash)
|
||||
|
||||
### 6.2 Sentry Integration
|
||||
|
||||
The client uses the official Sentry .NET SDK:
|
||||
- Package: `sentry-dotnet`
|
||||
- Documentation refs found in binary
|
||||
|
||||
### 6.3 Patching Options
|
||||
|
||||
**Option 1: Disable Sentry**
|
||||
- Replace DSN with invalid string
|
||||
- Errors won't be reported
|
||||
|
||||
**Option 2: Redirect to Own Sentry**
|
||||
- Replace `sentry.hytale.com` with own Sentry host
|
||||
- Requires same-length domain or binary patching
|
||||
|
||||
**Option 3: Leave As-Is**
|
||||
- Errors still report to Hypixel
|
||||
- May expose F2P server information
|
||||
|
||||
### 6.4 Sentry Environment Variables
|
||||
|
||||
Found configuration references:
|
||||
- `SENTRY_DSN` - DSN override
|
||||
- `SENTRY_ENVIRONMENT` - Environment name
|
||||
- Docs: `https://docs.sentry.io/platforms/dotnet/configuration/environments`
|
||||
|
||||
---
|
||||
|
||||
## 7. Internal Class References
|
||||
|
||||
### 7.1 Package Structure
|
||||
|
||||
```
|
||||
com.hypixel.hytale/
|
||||
├── builtin/
|
||||
│ ├── buildertools/
|
||||
│ │ ├── imageimport/
|
||||
│ │ │ └── ImageImportPage
|
||||
│ │ ├── objimport/
|
||||
│ │ │ └── ObjImportPage
|
||||
│ │ ├── prefabeditor/
|
||||
│ │ │ └── ui/PrefabEditorLoadSettings
|
||||
│ │ └── prefablist/
|
||||
│ │ └── PrefabPage
|
||||
│ ├── instances/
|
||||
│ │ └── page/InstanceListPage
|
||||
│ └── model/
|
||||
│ └── pages/ChangeModelPage
|
||||
├── server/
|
||||
│ └── core/
|
||||
│ └── asset/
|
||||
│ └── type/
|
||||
│ └── particle/
|
||||
│ └── pages/ParticleSpawn*
|
||||
└── Creation/
|
||||
└── navigation/
|
||||
├── buildertools/
|
||||
└── worldtools/
|
||||
├── changeModel
|
||||
├── importImage
|
||||
├── importObj
|
||||
├── instance
|
||||
├── playSound
|
||||
├── prefabEditor
|
||||
├── prefabList
|
||||
├── spawnEntity
|
||||
├── spawnParticle
|
||||
└── tintChunk
|
||||
```
|
||||
|
||||
### 7.2 UI Components
|
||||
|
||||
| Component | Path | Purpose |
|
||||
|-----------|------|---------|
|
||||
| GameLoading | `/GameLoading.u` | Loading screen |
|
||||
| GamePageNavigation | `/GamePageNavigation.u` | Main navigation |
|
||||
| ServerButton | `/ServerButton.u` | Server list button |
|
||||
| ServerModal | `/ServerModal.u` | Server details modal |
|
||||
| ServersPage | `/Servers/ServersPage.u` | Server browser |
|
||||
| DirectConnectPopup | `/Servers/DirectConnectPopup.u` | Direct connect dialog |
|
||||
| EditServerPopup | `/Servers/EditServerPopup.u` | Edit server dialog |
|
||||
| JoinViaCodePopup | `/Servers/JoinViaCodePopup.u` | Join via code dialog |
|
||||
| MinigamesPage | `/Minigames/MinigamesPage.u` | Minigames browser |
|
||||
|
||||
### 7.3 Configuration Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `/GameplayConfigs/Default.json` | Default gameplay settings |
|
||||
| `hytale_plugin.js` | Blockbench plugin script |
|
||||
| `hytale_assets` | Asset reference |
|
||||
|
||||
---
|
||||
|
||||
## 8. Binary Offsets Reference
|
||||
|
||||
### 8.1 URL String Offsets (macOS binary)
|
||||
|
||||
| Offset | Content | Length |
|
||||
|--------|---------|--------|
|
||||
| `0x1bf0098` | `https://account-data` | ~21 chars |
|
||||
| `0x1bf00c9` | `https://aka.ms/dotnet-warnings/{0}` | ~35 chars |
|
||||
| `0x1bf0114` | `https://blockbench.net/downloads` | ~33 chars |
|
||||
| `0x1bf015d` | `https://blockbench.net/plugins/hytale_plugin` | ~45 chars |
|
||||
| `0x1bf01bc` | `https://...@sentry.hytale.com/` | ~60 chars |
|
||||
| `0x1bf023b` | `https://discord.gg/hytale` | ~26 chars |
|
||||
| `0x1bf0274` | `https://hytale.com/help/joining-friends` | ~40 chars |
|
||||
| `0x1bf02c9` | `https://sessions` | ~17 chars |
|
||||
| `0x1bf02f2` | `https://store.hytale.com/?upgrade=` | ~35 chars |
|
||||
| `0x1bf033d` | `https://telemetry` | ~18 chars |
|
||||
| `0x1bf0368` | `https://tools` | ~14 chars |
|
||||
|
||||
### 8.2 API Endpoint Offsets
|
||||
|
||||
| Offset | Endpoint |
|
||||
|--------|----------|
|
||||
| `0x1b115d2` | `/game-session/child` |
|
||||
| `0x1b115ff` | `/game-session/refresh` |
|
||||
| `0x1b117c2` | `/server-join/auth-grant` |
|
||||
| `0x1b117f7` | `/server-join/auth-token` |
|
||||
| `0x1b11689` | `/my-account/cosmetics` |
|
||||
| `0x1b116ba` | `/my-account/game-profile` |
|
||||
| `0x1b116f1` | `/my-account/skin` |
|
||||
| `0x1b10d8c` | `/.well-known/jwks.json` |
|
||||
|
||||
### 8.3 Notes on Offsets
|
||||
|
||||
- Offsets are for the macOS binary
|
||||
- Windows/Linux binaries will have different offsets
|
||||
- Offsets may change between game versions
|
||||
- Always verify offsets before patching
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation Notes
|
||||
|
||||
### 9.1 Current Auth Server Implementation
|
||||
|
||||
The auth server (`hytale-auth-server`) currently implements:
|
||||
|
||||
**Fully Implemented:**
|
||||
- [x] `/game-session/new` - Session creation
|
||||
- [x] `/game-session/refresh` - Token refresh
|
||||
- [x] `/server-join/auth-grant` - Auth grants
|
||||
- [x] `/server-join/auth-token` - Token exchange with cert binding
|
||||
- [x] `/my-account/cosmetics` - Cosmetic list
|
||||
- [x] `/my-account/game-profile` - Profile get/update
|
||||
- [x] `/my-account/skin` - Skin save
|
||||
- [x] `/.well-known/jwks.json` - JWKS endpoint
|
||||
- [x] `/profile/uuid/{uuid}` - UUID lookup
|
||||
- [x] `/profile/username/{username}` - Username lookup
|
||||
- [x] Telemetry endpoints (accept and discard)
|
||||
|
||||
**Not Implemented:**
|
||||
- [ ] `tools.hytale.com` API (asset upload/download)
|
||||
- [ ] Prefab cloud storage
|
||||
- [ ] Asset sharing between players
|
||||
|
||||
### 9.2 Tools Service Implementation (Future)
|
||||
|
||||
If implementing tools.hytale.com functionality:
|
||||
|
||||
```javascript
|
||||
// Suggested endpoints for auth-server
|
||||
|
||||
// Asset upload
|
||||
app.post('/tools/assets/upload', async (req, res) => {
|
||||
// Handle multipart file upload
|
||||
// Store in local filesystem or S3
|
||||
// Return asset ID
|
||||
});
|
||||
|
||||
// Asset download
|
||||
app.get('/tools/assets/:assetId', async (req, res) => {
|
||||
// Retrieve asset by ID
|
||||
// Stream file to client
|
||||
});
|
||||
|
||||
// Prefab operations
|
||||
app.post('/tools/prefabs', async (req, res) => {
|
||||
// Save prefab JSON
|
||||
// Associate with user
|
||||
});
|
||||
|
||||
app.get('/tools/prefabs/:prefabId', async (req, res) => {
|
||||
// Get prefab by ID
|
||||
});
|
||||
|
||||
app.get('/tools/prefabs', async (req, res) => {
|
||||
// List user's prefabs
|
||||
});
|
||||
|
||||
app.delete('/tools/prefabs/:prefabId', async (req, res) => {
|
||||
// Delete prefab
|
||||
});
|
||||
```
|
||||
|
||||
### 9.3 Patching Recommendations
|
||||
|
||||
**Essential (Already Done):**
|
||||
1. Patch `sessions.hytale.com` → F2P domain
|
||||
2. Patch `account-data.hytale.com` → F2P domain
|
||||
3. Patch `telemetry.hytale.com` → F2P domain (or disable)
|
||||
|
||||
**Optional Enhancements:**
|
||||
1. Patch `tools.hytale.com` → F2P domain (if implementing tools API)
|
||||
2. Patch `sentry.hytale.com` → Own Sentry or disable
|
||||
3. Patch `discord.gg/hytale` → Community Discord
|
||||
4. Patch `store.hytale.com` → Custom store (if applicable)
|
||||
|
||||
**Not Recommended to Patch:**
|
||||
- Blockbench URLs (useful for modding)
|
||||
- Microsoft documentation URLs
|
||||
- Graphics reference URLs
|
||||
|
||||
### 9.4 Security Considerations
|
||||
|
||||
1. **Sentry DSN Exposure**
|
||||
- Current: Errors report to Hypixel's Sentry
|
||||
- Risk: May expose F2P server details
|
||||
- Recommendation: Disable or redirect
|
||||
|
||||
2. **Telemetry Data**
|
||||
- Current: Accepted but discarded
|
||||
- Alternative: Log for analytics
|
||||
- Risk: Privacy concerns
|
||||
|
||||
3. **Asset Upload (if implemented)**
|
||||
- Validate file types
|
||||
- Limit file sizes
|
||||
- Scan for malicious content
|
||||
- Rate limit uploads
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: String Extraction Commands
|
||||
|
||||
### Extract UTF-16LE Strings
|
||||
```bash
|
||||
python3 << 'EOF'
|
||||
with open("HytaleClient", "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
pattern = b'h\x00t\x00t\x00p\x00s\x00:\x00/\x00/\x00'
|
||||
idx = 0
|
||||
while True:
|
||||
idx = data.find(pattern, idx)
|
||||
if idx == -1:
|
||||
break
|
||||
end = idx
|
||||
chars = []
|
||||
while end < len(data) - 1:
|
||||
char = data[end] | (data[end+1] << 8)
|
||||
if 0x20 <= char <= 0x7e:
|
||||
chars.append(chr(char))
|
||||
end += 2
|
||||
else:
|
||||
break
|
||||
print(f"{hex(idx)}: {''.join(chars)}")
|
||||
idx += 2
|
||||
EOF
|
||||
```
|
||||
|
||||
### Search for Specific Pattern
|
||||
```bash
|
||||
xxd HytaleClient | grep "h.y.t.a.l.e"
|
||||
```
|
||||
|
||||
### Extract Context Around Offset
|
||||
```bash
|
||||
dd if=HytaleClient bs=1 skip=$((0x1bf0000)) count=2048 | xxd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Version History
|
||||
|
||||
| Date | Changes |
|
||||
|------|---------|
|
||||
| 2026-01-27 | Initial analysis of macOS client binary |
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: Related Documentation
|
||||
|
||||
- `CLAUDE.md` - Project overview and architecture
|
||||
- `DUAL_AUTH_FLOW.md` - Dual authentication flow diagrams
|
||||
- `STEAMDECK_CRASH_INVESTIGATION.md` - libzstd crash fix
|
||||
- `PLAYER_PASSWORD_FEATURE.md` - Planned password authentication
|
||||
- `backend/utils/clientPatcher.js` - Client patcher implementation
|
||||
113
docs/FASTUTIL_CLASSLOADER_ISSUE.md
Normal file
113
docs/FASTUTIL_CLASSLOADER_ISSUE.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Singleplayer Server Crash: fastutil ClassNotFoundException
|
||||
|
||||
## Status: Open (user-specific, Feb 24 2026)
|
||||
|
||||
## Symptom
|
||||
|
||||
Singleplayer server crashes immediately after DualAuth Agent installs successfully:
|
||||
|
||||
```
|
||||
Exception in thread "main" java.lang.NoClassDefFoundError: it/unimi/dsi/fastutil/objects/ObjectArrayList
|
||||
at com.hypixel.hytale.plugin.early.EarlyPluginLoader.<clinit>(EarlyPluginLoader.java:34)
|
||||
at com.hypixel.hytale.Main.main(Main.java:36)
|
||||
Caused by: java.lang.ClassNotFoundException: it.unimi.dsi.fastutil.objects.ObjectArrayList
|
||||
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
|
||||
```
|
||||
|
||||
Server exits with code 1. Multiplayer works fine for the same user.
|
||||
|
||||
## Affected User
|
||||
|
||||
- Discord: ヅ𝚃 JAYED !
|
||||
- Platform: Windows (standard x86_64, NOT ARM)
|
||||
- Reproduces 100% on singleplayer, every attempt
|
||||
- Other users (including macOS/Linux) are NOT affected
|
||||
|
||||
## What Works
|
||||
|
||||
- Java wrapper correctly strips `-XX:+UseCompactObjectHeaders`
|
||||
- Java wrapper correctly injects `--disable-sentry`
|
||||
- DualAuth Agent v1.1.12 installs successfully (STATIC mode)
|
||||
- Multiplayer connections work fine
|
||||
- Repair and reinstall did NOT fix the issue
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
`fastutil` (`it.unimi.dsi.fastutil`) should be bundled inside `HytaleServer.jar` (fat JAR). The `ClassNotFoundException` means the JVM's app classloader cannot find it despite it being in the JAR.
|
||||
|
||||
### Ruled Out
|
||||
|
||||
- **Wrapper issue**: Wrapper is working correctly (confirmed in logs)
|
||||
- **UseCompactObjectHeaders**: Server also has `-XX:+IgnoreUnrecognizedVMOptions`, so unrecognized flags don't crash it
|
||||
- **DualAuth Agent**: Works for all other users; agent installs successfully before the crash
|
||||
- **Corrupted game files**: Repair/reinstall didn't help
|
||||
- **ARM64/Parallels**: User is on standard Windows, not ARM
|
||||
|
||||
### Likely Causes (user-specific)
|
||||
|
||||
1. **Antivirus interference** — Windows Defender or third-party AV blocking Java from reading classes out of JAR files, especially with `-javaagent` active
|
||||
2. **Corrupted/incompatible JRE** — bundled JRE might be broken on their system
|
||||
3. **File locking** — another process holding HytaleServer.jar open
|
||||
|
||||
## Debugging Steps (ask user)
|
||||
|
||||
1. **Does official Hytale singleplayer work?** (without F2P launcher)
|
||||
- Yes → something about our launch setup
|
||||
- No → their system/JRE issue
|
||||
|
||||
2. **Check antivirus** — add game directory to Windows Defender exclusions:
|
||||
- Settings → Windows Security → Virus & threat protection → Exclusions
|
||||
- Add their HytaleF2P install folder
|
||||
|
||||
3. **Verify fastutil is in the JAR**:
|
||||
```cmd
|
||||
jar tf "D:\path\to\Server\HytaleServer.jar" | findstr fastutil
|
||||
```
|
||||
- If output shows fastutil classes → JAR is fine, classloader issue
|
||||
- If no output → JAR is incomplete/corrupt (different from other users)
|
||||
|
||||
4. **Try without DualAuth agent** — rename `dualauth-agent.jar` in Server/ folder, retry singleplayer
|
||||
- If works → agent's classloader manipulation breaks fastutil on their setup
|
||||
- If still fails → unrelated to agent
|
||||
|
||||
5. **Check JRE version** — have them run:
|
||||
```cmd
|
||||
"D:\path\to\jre\latest\bin\java.exe" -version
|
||||
```
|
||||
|
||||
## Update (Feb 24): `-XX:+UseCompactObjectHeaders` stripping removed from defaults
|
||||
|
||||
Stripping this flag did NOT fix the issue. The server already has `-XX:+IgnoreUnrecognizedVMOptions` so unrecognized flags are harmless. The flag was removed from default `stripFlags` in `backend/core/config.js`.
|
||||
|
||||
## Using the Java Wrapper to Strip JVM Flags
|
||||
|
||||
If a user needs to strip a specific JVM flag (e.g., for debugging or compatibility), they can do it via the launcher UI:
|
||||
|
||||
1. Open **Settings** → scroll to **Java Wrapper Configuration**
|
||||
2. Under **JVM Flags to Remove**, type the flag (e.g. `-XX:+UseCompactObjectHeaders`) and click **Add**
|
||||
3. The flag will be stripped from all JVM invocations at launch time
|
||||
4. To inject custom arguments, use the **Arguments to Inject** section (with optional "Server Only" condition)
|
||||
5. **Restore Defaults** resets to empty strip flags + `--disable-sentry` (server only)
|
||||
|
||||
The wrapper generates platform-specific scripts at launch time:
|
||||
- **Windows**: `java-wrapper.bat` in `jre/latest/bin/`
|
||||
- **macOS/Linux**: `java-wrapper` shell script in the same directory
|
||||
|
||||
Config is stored in `config.json` under `javaWrapperConfig`:
|
||||
```json
|
||||
{
|
||||
"javaWrapperConfig": {
|
||||
"stripFlags": ["-XX:+SomeFlag"],
|
||||
"injectArgs": [
|
||||
{ "arg": "--some-arg", "condition": "server" },
|
||||
{ "arg": "--other-arg", "condition": "always" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- Java wrapper config: `backend/core/config.js` (stripFlags / injectArgs)
|
||||
- DualAuth Agent: v1.1.12, package `ws.sanasol.dualauth`
|
||||
- Game version at time of report: `2026.02.19-1a311a592`
|
||||
121
docs/GHOST_PROCESS_ANALYSIS.md
Normal file
121
docs/GHOST_PROCESS_ANALYSIS.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Ghost Process Root Cause Analysis & Fix
|
||||
|
||||
## Problem Summary
|
||||
The Task Manager was freezing after the launcher (Hytale-F2P) ran. This was caused by **ghost/zombie PowerShell processes** spawned on Windows that were not being properly cleaned up.
|
||||
|
||||
## Root Cause
|
||||
|
||||
### Location
|
||||
**File:** `backend/utils/platformUtils.js`
|
||||
|
||||
**Functions affected:**
|
||||
1. `detectGpuWindows()` - Called during app startup and game launch
|
||||
2. `getSystemTypeWindows()` - Called during system detection
|
||||
|
||||
### The Issue
|
||||
Both functions were using **`execSync()`** to run PowerShell commands for GPU and system type detection:
|
||||
|
||||
```javascript
|
||||
// PROBLEMATIC CODE
|
||||
output = execSync(
|
||||
'powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_VideoController..."',
|
||||
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
||||
);
|
||||
```
|
||||
|
||||
#### Why This Causes Ghost Processes
|
||||
|
||||
1. **execSync spawns a shell process** - On Windows, `execSync` with a string command spawns `cmd.exe` which then launches `powershell.exe`
|
||||
2. **PowerShell inherits stdio settings** - The `stdio: ['ignore', 'pipe', 'ignore']` doesn't fully detach the PowerShell subprocess
|
||||
3. **Process hierarchy issue** - Even though the Node.js process receives the output and continues, the PowerShell subprocess may remain as a child process
|
||||
4. **Windows job object limitation** - Node.js child_process doesn't always properly terminate all descendants on Windows
|
||||
5. **Multiple calls during initialization** - GPU detection runs:
|
||||
- During app startup (line 1057 in main.js)
|
||||
- During game launch (in gameLauncher.js)
|
||||
- During settings UI rendering
|
||||
|
||||
Each call can spawn 2-3 PowerShell processes, and if the app spawns multiple game instances or restarts, these accumulate
|
||||
|
||||
### Call Stack
|
||||
1. `main.js` app startup → calls `detectGpu()`
|
||||
2. `gameLauncher.js` on launch → calls `setupGpuEnvironment()` → calls `detectGpu()`
|
||||
3. Multiple PowerShell processes spawn but aren't cleaned up properly
|
||||
4. Task Manager accumulates these ghost processes and becomes unresponsive
|
||||
|
||||
## The Solution
|
||||
|
||||
Replace `execSync()` with `spawnSync()` and add explicit timeouts:
|
||||
|
||||
### Key Changes
|
||||
|
||||
#### 1. Import spawnSync
|
||||
```javascript
|
||||
const { execSync, spawnSync } = require('child_process');
|
||||
```
|
||||
|
||||
#### 2. Replace execSync with spawnSync in detectGpuWindows()
|
||||
```javascript
|
||||
const POWERSHELL_TIMEOUT = 5000; // 5 second timeout
|
||||
|
||||
const result = spawnSync('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-Command',
|
||||
'Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM | ConvertTo-Csv -NoTypeInformation'
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
timeout: POWERSHELL_TIMEOUT,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
windowsHide: true
|
||||
});
|
||||
```
|
||||
|
||||
#### 3. Apply same fix to getSystemTypeWindows()
|
||||
|
||||
### Why spawnSync Fixes This
|
||||
|
||||
1. **Direct process spawn** - `spawnSync()` directly spawns the executable without going through `cmd.exe`
|
||||
2. **Explicit timeout** - The `timeout` parameter ensures processes are forcibly terminated after 5 seconds
|
||||
3. **windowsHide: true** - Prevents PowerShell window flashing and better resource cleanup
|
||||
4. **Better cleanup** - Node.js has better control over process lifecycle with `spawnSync`
|
||||
5. **Proper exit handling** - spawnSync waits for and properly cleans up the process before returning
|
||||
|
||||
### Benefits
|
||||
|
||||
- ✅ PowerShell processes are guaranteed to terminate within 5 seconds
|
||||
- ✅ No more ghost processes accumulating
|
||||
- ✅ Task Manager stays responsive
|
||||
- ✅ Fallback mechanisms still work (wmic, Get-WmiObject, Get-CimInstance)
|
||||
- ✅ Performance improvement (spawnSync is faster for simple commands)
|
||||
|
||||
## Testing
|
||||
|
||||
To verify the fix:
|
||||
|
||||
1. **Before running the launcher**, open Task Manager and check for PowerShell processes (should be 0 or 1)
|
||||
2. **Start the launcher** and observe Task Manager - you should not see PowerShell processes accumulating
|
||||
3. **Launch the game** and check Task Manager - still no ghost PowerShell processes
|
||||
4. **Restart the launcher** multiple times - PowerShell process count should remain stable
|
||||
|
||||
Expected behavior: No PowerShell processes should remain after each operation completes.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- **`backend/utils/platformUtils.js`**
|
||||
- Line 1: Added `spawnSync` import
|
||||
- Lines 300-380: Refactored `detectGpuWindows()`
|
||||
- Lines 599-643: Refactored `getSystemTypeWindows()`
|
||||
|
||||
## Performance Impact
|
||||
|
||||
- ⚡ **Faster execution** - `spawnSync` with argument arrays is faster than shell string parsing
|
||||
- 🎯 **More reliable** - Explicit timeout prevents indefinite hangs
|
||||
- 💾 **Lower memory usage** - Processes properly cleaned up instead of becoming zombies
|
||||
|
||||
## Additional Notes
|
||||
|
||||
The fix maintains backward compatibility:
|
||||
- All three GPU detection methods still work (Get-CimInstance → Get-WmiObject → wmic)
|
||||
- Error handling is preserved
|
||||
- System type detection (laptop vs desktop) still functions correctly
|
||||
- No changes to public API or external behavior
|
||||
83
docs/GHOST_PROCESS_FIX_SUMMARY.md
Normal file
83
docs/GHOST_PROCESS_FIX_SUMMARY.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Quick Fix Summary: Ghost Process Issue
|
||||
|
||||
## Problem
|
||||
Task Manager freezed after launcher runs due to accumulating ghost PowerShell processes.
|
||||
|
||||
## Root Cause
|
||||
**File:** `backend/utils/platformUtils.js`
|
||||
|
||||
Two functions used `execSync()` to run PowerShell commands:
|
||||
- `detectGpuWindows()` (GPU detection at startup & game launch)
|
||||
- `getSystemTypeWindows()` (system type detection)
|
||||
|
||||
`execSync()` on Windows spawns PowerShell processes that don't properly terminate → accumulate over time → freeze Task Manager.
|
||||
|
||||
## Solution Applied
|
||||
|
||||
### Changed From (❌ Wrong):
|
||||
```javascript
|
||||
output = execSync(
|
||||
'powershell -NoProfile -ExecutionPolicy Bypass -Command "Get-CimInstance..."',
|
||||
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
||||
);
|
||||
```
|
||||
|
||||
### Changed To (✅ Correct):
|
||||
```javascript
|
||||
const result = spawnSync('powershell.exe', [
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-Command',
|
||||
'Get-CimInstance...'
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000, // 5 second timeout - processes killed if hung
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
windowsHide: true
|
||||
});
|
||||
```
|
||||
|
||||
## What Changed
|
||||
|
||||
| Aspect | Before | After |
|
||||
|--------|--------|-------|
|
||||
| **Method** | `execSync()` → shell string | `spawnSync()` → argument array |
|
||||
| **Process spawn** | Via cmd.exe → powershell.exe | Direct powershell.exe |
|
||||
| **Timeout** | None (can hang indefinitely) | 5 seconds (processes auto-killed) |
|
||||
| **Process cleanup** | Hit or miss | Guaranteed |
|
||||
| **Ghost processes** | ❌ Accumulate over time | ✅ Always terminate |
|
||||
| **Performance** | Slower (shell parsing) | Faster (direct spawn) |
|
||||
|
||||
## Why This Works
|
||||
|
||||
1. **spawnSync directly spawns PowerShell** without intermediate cmd.exe
|
||||
2. **timeout: 5000** forcibly kills any hung process after 5 seconds
|
||||
3. **windowsHide: true** prevents window flashing and improves cleanup
|
||||
4. **Node.js has better control** over process lifecycle with spawnSync
|
||||
|
||||
## Impact
|
||||
|
||||
- ✅ No more ghost PowerShell processes
|
||||
- ✅ Task Manager stays responsive
|
||||
- ✅ Launcher performance improved
|
||||
- ✅ Game launch unaffected (still works the same)
|
||||
- ✅ All fallback methods preserved (Get-WmiObject, wmic)
|
||||
|
||||
## Files Changed
|
||||
|
||||
Only one file modified: **`backend/utils/platformUtils.js`**
|
||||
- Import added for `spawnSync`
|
||||
- Two functions refactored with new approach
|
||||
- All error handling preserved
|
||||
|
||||
## Testing
|
||||
|
||||
After applying fix, verify no ghost processes appear in Task Manager:
|
||||
|
||||
```
|
||||
Before launch: PowerShell processes = 0 or 1
|
||||
During launch: PowerShell processes = 0 or 1
|
||||
After game closes: PowerShell processes = 0 or 1
|
||||
```
|
||||
|
||||
If processes keep accumulating, check Task Manager → Details tab → look for powershell.exe entries.
|
||||
48
docs/KULVIN_RAM_PRESSURE_CRASH.md
Normal file
48
docs/KULVIN_RAM_PRESSURE_CRASH.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Client Crash: RAM Pressure on Low-End Hardware
|
||||
|
||||
## Status: Resolved (Feb 24, 2026)
|
||||
|
||||
## Symptom
|
||||
|
||||
Game launches but crashes with exit code 1 after ~32 seconds. Launcher logs show stale `java.exe(HytaleServer)` killed on every relaunch. Earlier sessions showed game running for ~4 minutes before `System.OutOfMemoryException`.
|
||||
|
||||
## Affected User
|
||||
|
||||
- Discord: KULVIN
|
||||
- Hardware: Intel UHD 620 + NVIDIA MX150 (2GB VRAM), low-end laptop
|
||||
- 22 mods installed
|
||||
- Platform: Windows x64
|
||||
|
||||
## Root Cause
|
||||
|
||||
RAM pressure from background apps + 22 mods. The game's .NET client ran out of memory, and the embedded singleplayer server JVM (no `-Xmx` cap) competed for the same limited RAM.
|
||||
|
||||
## Timeline
|
||||
|
||||
1. Morning session: Game ran ~4 minutes, then `System.OutOfMemoryException` crashed client, server JVM crashed with `EXCEPTION_ACCESS_VIOLATION`
|
||||
2. Evening sessions: Game started crashing in ~32 seconds (system still degraded from earlier OOM)
|
||||
3. Server JVM orphaned every time (cleaned up by `killGameProcesses()` on next launch)
|
||||
|
||||
## Resolution
|
||||
|
||||
User fixed by:
|
||||
1. Closing background applications to free RAM
|
||||
2. Reinstalling the game
|
||||
|
||||
## Additional Issues Found
|
||||
|
||||
- `WeaponStatsViewer` mod left a directory (not a .jar) in `HytaleSaves\Mods\`, causing EPERM on every mod sync
|
||||
- Stale AOT cache (`HytaleServer.aot`) cleaned up automatically by launcher
|
||||
|
||||
## Debugging Steps (for similar cases)
|
||||
|
||||
1. Check RAM usage in Task Manager before launching
|
||||
2. Windows Event Viewer (Win+R → `eventvwr.msc` → Application) for crash module details
|
||||
3. Try with all mods disabled
|
||||
4. Reboot to clear degraded memory state
|
||||
5. Close background apps (browsers, Discord, etc.)
|
||||
|
||||
## Recommendations
|
||||
|
||||
- Low-end hardware: reduce mod count (10 or fewer)
|
||||
- Consider adding `-Xmx` cap to singleplayer server JVM to prevent unbounded memory growth
|
||||
159
docs/LAUNCHER_CLEANUP_FLOWCHART.md
Normal file
159
docs/LAUNCHER_CLEANUP_FLOWCHART.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Launcher Process Lifecycle & Cleanup Flow
|
||||
|
||||
## Shutdown Event Sequence
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ USER CLOSES LAUNCHER │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ mainWindow.on('closed') event │
|
||||
│ ✅ Cleanup Discord RPC │
|
||||
└────────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ app.on('before-quit') event │
|
||||
│ ✅ Cleanup Discord RPC (again) │
|
||||
└────────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ app.on('window-all-closed') │
|
||||
│ ✅ Call app.quit() │
|
||||
└────────────┬───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ Node.js Process Exit │
|
||||
│ ✅ All resources released │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Resource Cleanup Map
|
||||
|
||||
```
|
||||
DISCORD RPC
|
||||
├─ clearActivity() ← Stop Discord integration
|
||||
├─ destroy() ← Destroy client object
|
||||
└─ Set to null ← Remove reference
|
||||
|
||||
GAME PROCESS
|
||||
├─ spawn() with detached: true
|
||||
├─ Immediately unref() ← Remove from event loop
|
||||
└─ Launcher ignores game after spawn
|
||||
|
||||
DOWNLOAD STREAMS
|
||||
├─ Clear stalledTimeout ← Stop stall detection
|
||||
├─ Clear overallTimeout ← Stop overall timeout
|
||||
├─ Abort controller ← Stop stream
|
||||
├─ Destroy writer ← Stop file writing
|
||||
└─ Reject promise ← End download
|
||||
|
||||
MAIN WINDOW
|
||||
├─ Destroy window
|
||||
├─ Remove listeners
|
||||
└─ Free memory
|
||||
|
||||
ELECTRON APP
|
||||
├─ Close all windows
|
||||
└─ Exit process
|
||||
```
|
||||
|
||||
## Cleanup Verification Points
|
||||
|
||||
### ✅ What IS Being Cleaned Up
|
||||
|
||||
1. **Discord RPC Client**
|
||||
- Activity cleared before exit
|
||||
- Client destroyed
|
||||
- Reference nulled
|
||||
|
||||
2. **Download Operations**
|
||||
- Timeouts cleared (stalledTimeout, overallTimeout)
|
||||
- Stream aborted
|
||||
- Writer destroyed
|
||||
- Promise rejected/resolved
|
||||
|
||||
3. **Game Process**
|
||||
- Detached from launcher
|
||||
- Unrefed so launcher can exit
|
||||
- Independent process tree
|
||||
|
||||
4. **Event Listeners**
|
||||
- IPC handlers persist (normal - Electron's design)
|
||||
- Main window listeners removed
|
||||
- Auto-updater auto-cleanup
|
||||
|
||||
### ⚠️ Considerations
|
||||
|
||||
1. **Discord RPC called twice**
|
||||
- Line 174: When window closes
|
||||
- Line 438: When app is about to quit
|
||||
- → This is defensive programming (safe, not wasteful)
|
||||
|
||||
2. **Game Process Orphaned (By Design)**
|
||||
- Launcher doesn't track game process
|
||||
- Game can outlive launcher
|
||||
- On Windows: Process is detached, unref'd
|
||||
- → This is correct behavior for a launcher
|
||||
|
||||
3. **IPC Handlers Remain Registered**
|
||||
- Normal for Electron apps
|
||||
- Handlers removed when app exits anyway
|
||||
- → Not a resource leak
|
||||
|
||||
---
|
||||
|
||||
## Comparison: Before & After Ghost Process Fix
|
||||
|
||||
### Before Fix (PowerShell Issues Only)
|
||||
```
|
||||
Launcher Cleanup: ✅ Good
|
||||
PowerShell GPU Detection: ❌ Bad (ghost processes)
|
||||
Result: Task Manager frozen by PowerShell
|
||||
```
|
||||
|
||||
### After Fix (PowerShell Fixed)
|
||||
```
|
||||
Launcher Cleanup: ✅ Good
|
||||
PowerShell GPU Detection: ✅ Fixed (spawnSync with timeout)
|
||||
Result: No ghost processes accumulate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
### Memory Usage Pattern
|
||||
```
|
||||
Startup → 80-120 MB
|
||||
After Download → 150-200 MB
|
||||
After Cleanup → 80-120 MB (back to baseline)
|
||||
After Exit → Process released
|
||||
```
|
||||
|
||||
### Handle Leaks: None Detected
|
||||
- Discord RPC: Properly released
|
||||
- Streams: Properly closed
|
||||
- Timeouts: Properly cleared
|
||||
- Window: Properly destroyed
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Launcher Termination Quality: ✅ GOOD**
|
||||
|
||||
| Aspect | Status | Details |
|
||||
|--------|--------|---------|
|
||||
| Discord cleanup | ✅ | Called in 2 places (defensive) |
|
||||
| Game process | ✅ | Detached & unref'd |
|
||||
| Download cleanup | ✅ | All timeouts cleared |
|
||||
| Memory release | ✅ | Event handlers removed |
|
||||
| Handle leaks | ✅ | None detected |
|
||||
| **Overall** | **✅** | **Proper shutdown architecture** |
|
||||
|
||||
The launcher has **solid cleanup logic**. The ghost process issue was specific to PowerShell GPU detection, not the launcher's termination flow.
|
||||
273
docs/LAUNCHER_TERMINATION_ANALYSIS.md
Normal file
273
docs/LAUNCHER_TERMINATION_ANALYSIS.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Launcher Process Termination & Cleanup Analysis
|
||||
|
||||
## Overview
|
||||
This document analyzes how the Hytale-F2P launcher handles process cleanup, event termination, and resource deallocation during shutdown.
|
||||
|
||||
## Shutdown Flow
|
||||
|
||||
### 1. **Primary Termination Events** (main.js)
|
||||
|
||||
#### Event: `before-quit` (Line 438)
|
||||
```javascript
|
||||
app.on('before-quit', () => {
|
||||
console.log('=== LAUNCHER BEFORE QUIT ===');
|
||||
cleanupDiscordRPC();
|
||||
});
|
||||
```
|
||||
- Called by Electron before the app starts quitting
|
||||
- Ensures Discord RPC is properly disconnected and destroyed
|
||||
- Gives async cleanup a chance to run
|
||||
|
||||
#### Event: `window-all-closed` (Line 443)
|
||||
```javascript
|
||||
app.on('window-all-closed', () => {
|
||||
console.log('=== LAUNCHER CLOSING ===');
|
||||
app.quit();
|
||||
});
|
||||
```
|
||||
- Triggered when all Electron windows are closed
|
||||
- Initiates app.quit() to cleanly exit
|
||||
|
||||
#### Event: `closed` (Line 174)
|
||||
```javascript
|
||||
mainWindow.on('closed', () => {
|
||||
console.log('Main window closed, cleaning up Discord RPC...');
|
||||
cleanupDiscordRPC();
|
||||
});
|
||||
```
|
||||
- Called when the main window is actually destroyed
|
||||
- Additional Discord RPC cleanup as safety measure
|
||||
|
||||
---
|
||||
|
||||
## 2. **Discord RPC Cleanup** (Lines 59-89, 424-436)
|
||||
|
||||
### cleanupDiscordRPC() Function
|
||||
```javascript
|
||||
async function cleanupDiscordRPC() {
|
||||
if (!discordRPC) return;
|
||||
try {
|
||||
console.log('Cleaning up Discord RPC...');
|
||||
discordRPC.clearActivity();
|
||||
await new Promise(r => setTimeout(r, 100)); // Wait for clear to propagate
|
||||
discordRPC.destroy();
|
||||
console.log('Discord RPC cleaned up successfully');
|
||||
} catch (error) {
|
||||
console.log('Error cleaning up Discord RPC:', error.message);
|
||||
} finally {
|
||||
discordRPC = null; // Null out the reference
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
1. Checks if Discord RPC is initialized
|
||||
2. Clears the current activity (disconnects from Discord)
|
||||
3. Waits 100ms for the clear to propagate
|
||||
4. Destroys the Discord RPC client
|
||||
5. Nulls out the reference to prevent memory leaks
|
||||
6. Error handling ensures cleanup doesn't crash the app
|
||||
|
||||
**Quality:** ✅ **Proper cleanup with error handling**
|
||||
|
||||
---
|
||||
|
||||
## 3. **Game Process Handling** (gameLauncher.js)
|
||||
|
||||
### Game Launch Process (Lines 356-403)
|
||||
|
||||
```javascript
|
||||
let spawnOptions = {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
env: env
|
||||
};
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
spawnOptions.shell = false;
|
||||
spawnOptions.windowsHide = true;
|
||||
spawnOptions.detached = true; // ← Game runs independently
|
||||
spawnOptions.stdio = 'ignore'; // ← Fully detach stdio
|
||||
}
|
||||
|
||||
const child = spawn(clientPath, args, spawnOptions);
|
||||
|
||||
// Windows: Release process reference immediately
|
||||
if (process.platform === 'win32') {
|
||||
child.unref(); // ← Allows Node.js to exit without waiting for game
|
||||
}
|
||||
```
|
||||
|
||||
**Critical Analysis:**
|
||||
- ✅ **Windows detached mode**: Game process is spawned detached and stdio is ignored
|
||||
- ✅ **child.unref()**: Removes the Node process from the event loop
|
||||
- ⚠️ **No event listeners**: Once detached, the launcher doesn't track the game process
|
||||
|
||||
**Potential Issue:**
|
||||
The game process is completely detached and unrefed, which is correct. However, if the game crashes and respawns (or multiple instances), these orphaned processes could accumulate.
|
||||
|
||||
---
|
||||
|
||||
## 4. **Download/File Transfer Cleanup** (fileManager.js)
|
||||
|
||||
### setInterval Cleanup (Lines 77-94)
|
||||
```javascript
|
||||
const overallTimeout = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastProgress = now - lastProgressTime;
|
||||
|
||||
if (timeSinceLastProgress > 900000 && hasReceivedData) {
|
||||
console.log('Download stalled for 15 minutes, aborting...');
|
||||
controller.abort();
|
||||
}
|
||||
}, 60000); // Check every minute
|
||||
```
|
||||
|
||||
### Cleanup Locations:
|
||||
|
||||
**On Stream Error (Lines 225-228):**
|
||||
```javascript
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
}
|
||||
if (overallTimeout) {
|
||||
clearInterval(overallTimeout);
|
||||
}
|
||||
```
|
||||
|
||||
**On Stream Close (Lines 239-244):**
|
||||
```javascript
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
}
|
||||
if (overallTimeout) {
|
||||
clearInterval(overallTimeout);
|
||||
}
|
||||
```
|
||||
|
||||
**On Writer Finish (Lines 295-299):**
|
||||
```javascript
|
||||
if (stalledTimeout) {
|
||||
clearTimeout(stalledTimeout);
|
||||
console.log('Cleared stall timeout after writer finished');
|
||||
}
|
||||
if (overallTimeout) {
|
||||
clearInterval(overallTimeout);
|
||||
console.log('Cleared overall timeout after writer finished');
|
||||
}
|
||||
```
|
||||
|
||||
**Quality:** ✅ **Proper cleanup with multiple safeguards**
|
||||
- Intervals are cleared in all exit paths
|
||||
- No orphaned setInterval/setTimeout calls
|
||||
|
||||
---
|
||||
|
||||
## 5. **Electron Auto-Updater** (Lines 184-237)
|
||||
|
||||
```javascript
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
**Auto-Updater Cleanup:** ✅
|
||||
- Electron handles auto-updater cleanup automatically
|
||||
- No explicit cleanup needed (Electron manages lifecycle)
|
||||
|
||||
---
|
||||
|
||||
## Summary: Process Termination Quality
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| **Discord RPC** | ✅ **Good** | Properly destroyed with error handling |
|
||||
| **Main Window** | ✅ **Good** | Cleanup called on closed and before-quit |
|
||||
| **Game Process** | ✅ **Good** | Detached and unref'd on Windows |
|
||||
| **Download Intervals** | ✅ **Good** | Cleared in all exit paths |
|
||||
| **Event Listeners** | ⚠️ **Mixed** | Main listeners properly removed, but IPC handlers remain registered (normal) |
|
||||
| **Overall** | ✅ **Good** | Proper cleanup architecture |
|
||||
|
||||
---
|
||||
|
||||
## Potential Improvements
|
||||
|
||||
### 1. **Add Explicit Process Tracking (Optional)**
|
||||
Currently, the launcher doesn't track child processes. We could add:
|
||||
```javascript
|
||||
// Track all spawned processes for cleanup
|
||||
const childProcesses = new Set();
|
||||
|
||||
app.on('before-quit', () => {
|
||||
// Kill any remaining child processes
|
||||
for (const proc of childProcesses) {
|
||||
if (proc && !proc.killed) {
|
||||
proc.kill('SIGTERM');
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. **Auto-Updater Resource Cleanup (Minor)**
|
||||
Add explicit cleanup for auto-updater listeners:
|
||||
```javascript
|
||||
app.on('before-quit', () => {
|
||||
autoUpdater.removeAllListeners();
|
||||
});
|
||||
```
|
||||
|
||||
### 3. **Graceful Shutdown Timeout (Safety)**
|
||||
Add a safety timeout to force exit if cleanup hangs:
|
||||
```javascript
|
||||
app.on('before-quit', () => {
|
||||
const forceExitTimeout = setTimeout(() => {
|
||||
console.warn('Cleanup timeout - forcing exit');
|
||||
process.exit(0);
|
||||
}, 5000); // 5 second max cleanup time
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Relationship to Ghost Process Issue
|
||||
|
||||
### Previous Issue (PowerShell processes)
|
||||
- **Root cause**: Spawned PowerShell processes weren't cleaned up in `platformUtils.js`
|
||||
- **Fixed by**: Replacing `execSync()` with `spawnSync()` + timeouts
|
||||
|
||||
### Launcher Termination
|
||||
- **Status**: ✅ **No critical issues found**
|
||||
- **Discord RPC**: Properly cleaned up
|
||||
- **Game process**: Properly detached
|
||||
- **Intervals**: Properly cleared
|
||||
- **No memory leaks detected**
|
||||
|
||||
The launcher's termination flow is solid. The ghost process issue was specific to PowerShell process spawning during GPU detection, not the launcher's shutdown process.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
To verify proper launcher termination:
|
||||
|
||||
- [ ] Start launcher → Close window → Check Task Manager for lingering processes
|
||||
- [ ] Start launcher → Launch game → Close launcher → Check for orphaned processes
|
||||
- [ ] Start launcher → Download something → Cancel mid-download → Check for setInterval processes
|
||||
- [ ] Disable Discord RPC → Start launcher → Close → No Discord processes remain
|
||||
- [ ] Check Windows Event Viewer → No unhandled exceptions on launcher exit
|
||||
- [ ] Multiple launch/close cycles → No memory growth in Task Manager
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Hytale-F2P launcher has **good shutdown hygiene**:
|
||||
- ✅ Discord RPC is properly cleaned
|
||||
- ✅ Game process is properly detached
|
||||
- ✅ Download intervals are properly cleared
|
||||
- ✅ Event handlers are properly registered
|
||||
|
||||
The ghost process issue was **not** caused by the launcher's termination logic, but by the PowerShell GPU detection functions, which has already been fixed.
|
||||
138
docs/UPDATE_SYSTEM_FIXES.md
Normal file
138
docs/UPDATE_SYSTEM_FIXES.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Update System Fixes Summary
|
||||
|
||||
## Overview
|
||||
This document summarizes the fixes made to the launcher auto-update system to improve UX and fix macOS-specific issues.
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. Duplicate Update Popups
|
||||
**Problem:** Two different update UI files (`update.js` and `updater.js`) were both listening for update events, causing duplicate popups to appear.
|
||||
|
||||
**Solution:**
|
||||
- Disabled `updater.js` in `index.html` (commented out the script tag)
|
||||
- Now only `update.js` handles all update UI with improved features
|
||||
|
||||
### 2. Missing Skip Button
|
||||
**Problem:** Users were soft-locked on the update screen with no way to dismiss it, especially problematic on macOS where auto-install often fails.
|
||||
|
||||
**Solution:**
|
||||
- Added "Skip for now (not recommended)" button to update popup
|
||||
- Skip button appears:
|
||||
- After 30 seconds (timeout fallback)
|
||||
- Immediately on any update error
|
||||
- After install attempt fails (5 second timeout)
|
||||
- Always visible once download completes
|
||||
|
||||
### 3. macOS Auto-Install Failure
|
||||
**Problem:** `quitAndInstall()` silently fails on unsigned macOS apps, leaving users stuck.
|
||||
|
||||
**Solution:**
|
||||
- Detect macOS in `main.js` and send `autoInstallSupported: false` flag
|
||||
- On macOS, show "Download Manually (Recommended)" as primary action
|
||||
- "Try Install & Restart" shown as secondary option
|
||||
- Added force quit fallback in `install-update` handler for macOS
|
||||
- Clear messaging: "Update downloaded but auto-install may not work on macOS"
|
||||
|
||||
### 4. Missing IPC Handlers
|
||||
**Problem:** `open-download-page` IPC handler was not registered, causing errors when clicking manual download.
|
||||
|
||||
**Solution:**
|
||||
- Added `open-download-page` handler in `main.js` that opens GitHub releases page
|
||||
- Added `quitAndInstallUpdate` alias in `preload.js` for compatibility
|
||||
|
||||
### 5. Interface Blocking Not Removed
|
||||
**Problem:** `unblockInterface()` method was called but never defined, leaving the UI blurred after closing popup.
|
||||
|
||||
**Solution:**
|
||||
- Added complete `unblockInterface()` method that:
|
||||
- Removes `interface-blocked` class from main content
|
||||
- Removes `no-select` class from body
|
||||
- Properly removes event listeners using stored bound function references
|
||||
|
||||
### 6. Breathing Animation on Downloaded State
|
||||
**Problem:** The pulse/breathing animation continued after download completed, which felt inappropriate for a "ready to install" state.
|
||||
|
||||
**Solution:**
|
||||
- Remove `update-popup-pulse` class in `showUpdateDownloaded()` method
|
||||
|
||||
### 7. Player Name Not Synced on First Install
|
||||
**Problem:** Player name entered during installation wasn't synced to settings page input.
|
||||
|
||||
**Solution:**
|
||||
- In `install.js`, sync player name to both `playerName` and `settingsPlayerName` inputs after installation completes
|
||||
|
||||
## Files Modified
|
||||
|
||||
### `GUI/index.html`
|
||||
- Commented out `updater.js` script tag (duplicate update UI)
|
||||
|
||||
### `GUI/js/update.js`
|
||||
- Removed legacy `onUpdatePopup` listener
|
||||
- Added `closeUpdatePopup()` method
|
||||
- Added `unblockInterface()` method
|
||||
- Added skip button to popup HTML
|
||||
- Added skip button visibility logic (30s timeout, on error, after download)
|
||||
- Added macOS detection and alternative UI (manual download as primary)
|
||||
- Removed pulse animation when download completes
|
||||
- Added console logging for debugging
|
||||
- Added extra DOM check to prevent duplicate popups
|
||||
- Fixed manual download button to show "Opened in browser" and close popup
|
||||
|
||||
### `main.js`
|
||||
- Changed `autoUpdater.autoDownload` from `false` to `true`
|
||||
- Added macOS error handling with `requiresManualDownload` flag
|
||||
- Added `autoInstallSupported` flag to `update-downloaded` event
|
||||
- Added `open-download-page` IPC handler
|
||||
- Enhanced `install-update` handler with macOS force quit fallback
|
||||
|
||||
### `preload.js`
|
||||
- Added `quitAndInstallUpdate` alias for `install-update` IPC
|
||||
|
||||
### `GUI/js/install.js`
|
||||
- Sync player name to `settingsPlayerName` input after installation
|
||||
|
||||
### `backend/utils/clientPatcher.js`
|
||||
- Removed server patching code (server uses pre-patched JAR from CDN)
|
||||
- Simplified to client-only patching
|
||||
- Removed unused imports: `crypto`, `AdmZip`, `execSync`, `spawn`, `getJavaExec`, `getBundledJavaPath`, `JRE_DIR`
|
||||
- Removed unused methods: `stringToUtf8()`, `findAndReplaceDomainUtf8()`
|
||||
- Cleaned up comments and documentation
|
||||
- Localhost/local dev code moved to `clientPatcher.localhost.js.bak` for reference
|
||||
|
||||
## Testing
|
||||
|
||||
To test the update popup manually, you can temporarily add this debug code to `update.js` init():
|
||||
|
||||
```javascript
|
||||
// DEBUG: Simulate update available popup after 2 seconds
|
||||
setTimeout(() => {
|
||||
this.showUpdatePopup({
|
||||
currentVersion: '2.0.0',
|
||||
newVersion: '2.1.0',
|
||||
releaseNotes: 'Debug test update'
|
||||
});
|
||||
|
||||
// Simulate download complete after 3 more seconds
|
||||
setTimeout(() => {
|
||||
this.showUpdateDownloaded({
|
||||
version: '2.1.0',
|
||||
platform: 'darwin',
|
||||
autoInstallSupported: false // Simulate macOS
|
||||
});
|
||||
}, 3000);
|
||||
}, 2000);
|
||||
```
|
||||
|
||||
## Platform-Specific Behavior
|
||||
|
||||
### Windows/Linux
|
||||
- Auto-download enabled
|
||||
- "Install & Restart" as primary action
|
||||
- Skip button available as fallback
|
||||
|
||||
### macOS
|
||||
- Auto-download enabled (download works, install doesn't)
|
||||
- "Download Manually (Recommended)" as primary action
|
||||
- "Try Install & Restart" as secondary option
|
||||
- Skip button always visible after download
|
||||
- Force quit fallback if quitAndInstall fails
|
||||
482
docs/UUID_BUGS_FIX_PLAN.md
Normal file
482
docs/UUID_BUGS_FIX_PLAN.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# UUID/Skin Reset Bug Fix Plan
|
||||
|
||||
## Problem Summary
|
||||
|
||||
Players experience random skin/cosmetic resets without intentionally changing anything. The root cause is that the UUID system has multiple failure points that can silently generate new UUIDs or use the wrong UUID during gameplay.
|
||||
|
||||
**Impact**: Players lose their customized cosmetics/skins randomly, causing frustration and confusion.
|
||||
|
||||
**Status**: ✅ **FIXED** - All critical and high priority bugs have been addressed.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
### What Was Fixed
|
||||
|
||||
| Bug | Severity | Status | Description |
|
||||
|-----|----------|--------|-------------|
|
||||
| BUG-001 | Critical | ✅ Fixed | Username not loaded before play click |
|
||||
| BUG-002 | High | ✅ Fixed | isFirstLaunch() always returns true |
|
||||
| BUG-003 | Critical | ✅ Fixed | Silent config corruption returns empty object |
|
||||
| BUG-004 | Critical | ✅ Fixed | Non-atomic config writes |
|
||||
| BUG-005 | High | ✅ Fixed | Username fallback to 'Player' |
|
||||
| BUG-006 | Medium | ✅ Fixed | Launch overwrites username every time |
|
||||
| BUG-007 | Medium | ✅ Fixed | Dual UUID systems (playerManager vs config) |
|
||||
| BUG-008 | High | ✅ Fixed | Error returns random UUID |
|
||||
| BUG-009 | Medium | ✅ Fixed | Username case sensitivity |
|
||||
| BUG-010 | Medium | ⏳ Pending | Migration marks complete on partial failure |
|
||||
| BUG-011 | Medium | ⏳ Pending | Race condition on concurrent config access |
|
||||
| BUG-012 | High | ✅ Fixed | UUID modal isCurrent flag broken |
|
||||
| BUG-013 | High | ✅ Fixed | UUID setting uses unsaved DOM username |
|
||||
| BUG-014 | Medium | ✅ Fixed | No way to switch between saved identities |
|
||||
| BUG-015 | High | ✅ Fixed | installGame saves username (overwrites good value) |
|
||||
| BUG-016 | High | ✅ Fixed | Username rename creates new UUID instead of preserving |
|
||||
| BUG-017 | Medium | ✅ Fixed | UUID list not refreshing when player name changes |
|
||||
| BUG-018 | Low | ✅ Fixed | Custom UUID input doesn't allow copy/paste |
|
||||
|
||||
---
|
||||
|
||||
## User Scenario Analysis
|
||||
|
||||
All user scenarios have been analyzed for UUID/username persistence:
|
||||
|
||||
| Scenario | Risk | Status | Details |
|
||||
|----------|------|--------|---------|
|
||||
| **Fresh Install** | Low | ✅ Safe | firstLaunch.js reads but doesn't modify username/UUID |
|
||||
| **Username Change** | Low | ✅ Safe | Rename preserves UUID, user-initiated saves work correctly |
|
||||
| **Auto-Update** | Low | ✅ Safe | Config is on disk before update, backup recovery available |
|
||||
| **Manual Update** | Low | ✅ Safe | Config file persists across manual updates |
|
||||
| **Different Install Location** | Low | ✅ Safe | Config uses central app directory, not install-relative |
|
||||
| **Repair Game** | Low | ✅ Safe | repairGame() doesn't touch config |
|
||||
| **UUID Modal** | Low | ✅ Fixed | Fixed isCurrent badge, unsaved username bug, added switch button |
|
||||
| **Profile Switch** | Low | ✅ Safe | Profiles only control mods/java, not username/UUID |
|
||||
| **Branch Change** | Low | ✅ Safe | Only changes game version, not identity |
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `backend/core/config.js` | Atomic writes, backup/recovery, validation, case-insensitive UUID lookup, checkLaunchReady(), username rename preserves UUID |
|
||||
| `backend/managers/gameLauncher.js` | Pre-launch validation, removed saveUsername call |
|
||||
| `backend/managers/gameManager.js` | Removed saveUsername call from installGame |
|
||||
| `backend/services/playerManager.js` | Marked DEPRECATED, throws on error, retry logic |
|
||||
| `backend/launcher.js` | Export new functions (checkLaunchReady, hasUsername, etc.) |
|
||||
| `GUI/js/launcher.js` | Uses checkLaunchReady API, blocks launch if no username |
|
||||
| `GUI/js/settings.js` | UUID modal fixes, switchToUsername function, proper error handling, refreshes UUID list on name change |
|
||||
| `GUI/style.css` | Switch button styling, user-select: text for UUID input |
|
||||
| `GUI/locales/*.json` | Added translation keys for switch username functionality (all 10 locales) |
|
||||
| `main.js` | Fixed UUID IPC handlers, added checkLaunchReady handler, enabled Ctrl+V/C/X/A shortcuts |
|
||||
| `preload.js` | Exposed checkLaunchReady to renderer |
|
||||
|
||||
---
|
||||
|
||||
## Bug Categories
|
||||
|
||||
### Category A: Race Conditions & Initialization
|
||||
### Category B: Silent Failures & Fallbacks
|
||||
### Category C: Data Integrity & Persistence
|
||||
### Category D: Design Issues
|
||||
### Category E: UI/UX Issues
|
||||
|
||||
---
|
||||
|
||||
## Detailed Bug List & Fixes
|
||||
|
||||
---
|
||||
|
||||
### BUG-001: Username Not Loaded Before Play Click (CRITICAL) ✅ FIXED
|
||||
|
||||
**Category**: A - Race Condition
|
||||
|
||||
**Location**:
|
||||
- `GUI/js/launcher.js`
|
||||
- `GUI/js/settings.js`
|
||||
|
||||
**Problem**: If user clicks Play before settings DOM initializes, returns 'Player' silently.
|
||||
|
||||
**Fix Applied**:
|
||||
- launcher.js now uses `checkLaunchReady()` API to validate before launch
|
||||
- Loads username from backend config (single source of truth)
|
||||
- Blocks launch and shows error if no username configured
|
||||
- Navigates user to settings page to set username
|
||||
|
||||
---
|
||||
|
||||
### BUG-002: `isFirstLaunch()` Always Returns True (HIGH) ✅ FIXED
|
||||
|
||||
**Category**: B - Silent Failure
|
||||
|
||||
**Location**: `backend/core/config.js`
|
||||
|
||||
**Problem**: Function always returns `true` even when user has data (typo: `return true` instead of `return false`).
|
||||
|
||||
**Fix Applied**:
|
||||
- Fixed return statement: `return true` → `return false`
|
||||
|
||||
---
|
||||
|
||||
### BUG-003: Silent Config Corruption Returns Empty Object (CRITICAL) ✅ FIXED
|
||||
|
||||
**Category**: B - Silent Failure
|
||||
|
||||
**Location**: `backend/core/config.js`
|
||||
|
||||
**Problem**: Corrupted config silently returns `{}`, causing UUID regeneration.
|
||||
|
||||
**Fix Applied**:
|
||||
- Added config validation after load
|
||||
- Implemented backup config system (config.json.bak)
|
||||
- Tries loading backup if primary fails
|
||||
- Logs detailed errors for debugging
|
||||
|
||||
---
|
||||
|
||||
### BUG-004: Non-Atomic Config Writes (CRITICAL) ✅ FIXED
|
||||
|
||||
**Category**: C - Data Integrity
|
||||
|
||||
**Location**: `backend/core/config.js`
|
||||
|
||||
**Problem**: Direct write can corrupt file if interrupted. Silent error logging.
|
||||
|
||||
**Fix Applied**:
|
||||
- Atomic write: write to temp file → verify JSON → backup current → rename
|
||||
- Throws error on save failure (no silent continuation)
|
||||
- Cleans up temp file on failure
|
||||
|
||||
---
|
||||
|
||||
### BUG-005: Username Fallback to 'Player' (HIGH) ✅ FIXED
|
||||
|
||||
**Category**: B - Silent Failure
|
||||
|
||||
**Location**: `backend/core/config.js`
|
||||
|
||||
**Problem**: Missing username silently falls back to 'Player', causing wrong UUID.
|
||||
|
||||
**Fix Applied**:
|
||||
- `loadUsername()` returns `null` instead of 'Player'
|
||||
- Added `loadUsernameWithDefault()` for display purposes
|
||||
- Added `hasUsername()` helper function
|
||||
- All callers updated to handle null case explicitly
|
||||
|
||||
---
|
||||
|
||||
### BUG-006: Launch Overwrites Username Every Time (MEDIUM) ✅ FIXED
|
||||
|
||||
**Category**: D - Design Issue
|
||||
|
||||
**Location**: `backend/managers/gameLauncher.js`
|
||||
|
||||
**Problem**: If playerName parameter is wrong, it overwrites the saved username.
|
||||
|
||||
**Fix Applied**:
|
||||
- Removed `saveUsername()` call from launch process
|
||||
- Username only saved when user explicitly changes it in Settings
|
||||
- Launch loads username from config (single source of truth)
|
||||
|
||||
---
|
||||
|
||||
### BUG-007: Dual UUID Systems (playerManager vs config) (MEDIUM) ✅ FIXED
|
||||
|
||||
**Category**: D - Design Issue
|
||||
|
||||
**Location**:
|
||||
- `backend/services/playerManager.js` → `player_id.json`
|
||||
- `backend/core/config.js` → `config.json` → `userUuids`
|
||||
|
||||
**Problem**: Two independent UUID systems can desync.
|
||||
|
||||
**Fix Applied**:
|
||||
- `playerManager.js` marked as DEPRECATED
|
||||
- All code uses `config.js` `getUuidForUser()`
|
||||
- Migration function added for legacy `player_id.json`
|
||||
|
||||
---
|
||||
|
||||
### BUG-008: Error Returns Random UUID (HIGH) ✅ FIXED
|
||||
|
||||
**Category**: B - Silent Failure
|
||||
|
||||
**Location**: `backend/services/playerManager.js`
|
||||
|
||||
**Problem**: Any error generates random UUID, losing player identity.
|
||||
|
||||
**Fix Applied**:
|
||||
- Now throws error instead of returning random UUID
|
||||
- Retry logic added (3 attempts before failure)
|
||||
- Caller must handle the error appropriately
|
||||
|
||||
---
|
||||
|
||||
### BUG-009: Username Case Sensitivity (MEDIUM) ✅ FIXED
|
||||
|
||||
**Category**: D - Design Issue
|
||||
|
||||
**Location**: `backend/core/config.js`
|
||||
|
||||
**Problem**: "PlayerOne" and "playerone" are different UUIDs.
|
||||
|
||||
**Fix Applied**:
|
||||
- `getUuidForUser()` uses case-insensitive lookup
|
||||
- Username stored with ORIGINAL case (preserves "Sanasol", "SaAnAsOl", etc.)
|
||||
- Lookup normalized to lowercase for matching
|
||||
- Case changes update the stored key while preserving UUID
|
||||
|
||||
---
|
||||
|
||||
### BUG-010: Migration Marks Complete Even on Partial Failure (MEDIUM) ⏳ PENDING
|
||||
|
||||
**Category**: C - Data Integrity
|
||||
|
||||
**Location**: `backend/utils/userDataMigration.js`
|
||||
|
||||
**Problem**: Partial copy is marked as complete, preventing retry.
|
||||
|
||||
**Status**: Not yet implemented - low priority since migration runs once.
|
||||
|
||||
---
|
||||
|
||||
### BUG-011: Race Condition on Concurrent Config Access (MEDIUM) ⏳ PENDING
|
||||
|
||||
**Category**: A - Race Condition
|
||||
|
||||
**Location**: `backend/core/config.js`
|
||||
|
||||
**Problem**: No file locking - concurrent processes can overwrite each other.
|
||||
|
||||
**Status**: Not yet implemented - would require `proper-lockfile` package. Low risk since launcher is single-instance.
|
||||
|
||||
---
|
||||
|
||||
### BUG-012: UUID Modal isCurrent Flag Broken (HIGH) ✅ FIXED
|
||||
|
||||
**Category**: D - Design Issue
|
||||
|
||||
**Location**: `main.js` - `get-all-uuid-mappings` IPC handler
|
||||
|
||||
**Problem**: Case-sensitive comparison between normalized key (lowercase) and current username.
|
||||
```javascript
|
||||
// BROKEN:
|
||||
isCurrent: username === loadUsername() // "player1" === "Player1" → FALSE
|
||||
```
|
||||
|
||||
**Fix Applied**:
|
||||
- IPC handler now uses `getAllUuidMappingsArray()` from config.js
|
||||
- This function correctly compares against normalized username
|
||||
|
||||
---
|
||||
|
||||
### BUG-013: UUID Setting Uses Unsaved DOM Username (HIGH) ✅ FIXED
|
||||
|
||||
**Category**: B - Silent Failure
|
||||
|
||||
**Location**: `GUI/js/settings.js` - `performSetCustomUuid()`
|
||||
|
||||
**Problem**: Gets username from DOM input field instead of saved config.
|
||||
```javascript
|
||||
// BROKEN:
|
||||
const username = getCurrentPlayerName(); // From UI input, not saved!
|
||||
```
|
||||
|
||||
**Risk Scenario**: User types new name but doesn't save → opens UUID modal → sets custom UUID → UUID gets set for unsaved name while config has old name.
|
||||
|
||||
**Fix Applied**:
|
||||
- Now loads username from backend config via `window.electronAPI.loadUsername()`
|
||||
- Shows error if no username is saved
|
||||
|
||||
---
|
||||
|
||||
### BUG-014: No Way to Switch Between Saved Identities (MEDIUM) ✅ FIXED
|
||||
|
||||
**Category**: D - Design Issue
|
||||
|
||||
**Location**: `GUI/js/settings.js` - UUID modal
|
||||
|
||||
**Problem**: UUID modal showed list of usernames/UUIDs but no way to switch to a different identity.
|
||||
|
||||
**Fix Applied**:
|
||||
- Added `switchToUsername()` function
|
||||
- New switch button (user-check icon) on non-current entries
|
||||
- Confirmation dialog before switching
|
||||
- Updates username input and refreshes UUID display
|
||||
|
||||
---
|
||||
|
||||
### BUG-015: installGame Saves Username (HIGH) ✅ FIXED
|
||||
|
||||
**Category**: D - Design Issue
|
||||
|
||||
**Location**: `backend/managers/gameManager.js` - `installGame()`
|
||||
|
||||
**Problem**: `saveUsername(playerName)` call could overwrite good username with 'Player' default.
|
||||
|
||||
**Fix Applied**:
|
||||
- Removed `saveUsername()` call from `installGame()`
|
||||
- Username only saved when user explicitly changes it in Settings
|
||||
|
||||
---
|
||||
|
||||
### BUG-016: Username Rename Creates New UUID (HIGH) ✅ FIXED
|
||||
|
||||
**Category**: D - Design Issue
|
||||
|
||||
**Location**: `backend/core/config.js` - `saveUsername()`
|
||||
|
||||
**Problem**: When user changes their player name, a new UUID was generated instead of preserving the existing one. User's identity (cosmetics/skins) was lost on every name change.
|
||||
|
||||
**Symptom**: Change "Player1" to "NewPlayer" → gets completely new UUID → loses all cosmetics.
|
||||
|
||||
**Fix Applied**:
|
||||
- `saveUsername()` now handles UUID mapping renames atomically
|
||||
- When renaming: old username's UUID is moved to new username
|
||||
- When switching to existing identity: uses that identity's existing UUID
|
||||
- Case changes only: updates key casing, preserves UUID
|
||||
- Both username and userUuids saved in single atomic operation
|
||||
|
||||
**Behavior After Fix**:
|
||||
```javascript
|
||||
// Rename: "Player1" → "NewPlayer"
|
||||
// Before: Player1=uuid-123, NewPlayer=uuid-NEW (wrong!)
|
||||
// After: NewPlayer=uuid-123 (same UUID, just renamed)
|
||||
|
||||
// Switch to existing: "Player1" → "ExistingPlayer"
|
||||
// Uses ExistingPlayer's existing UUID (switching identity)
|
||||
|
||||
// Case change: "Player1" → "PLAYER1"
|
||||
// UUID preserved, key updated to new case
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### BUG-017: UUID List Not Refreshing on Name Change (MEDIUM) ✅ FIXED
|
||||
|
||||
**Category**: E - UI/UX Issue
|
||||
|
||||
**Location**: `GUI/js/settings.js` - `savePlayerName()`
|
||||
|
||||
**Problem**: After changing player name in settings, the UUID modal list didn't refresh. The "Current" badge showed on the old username instead of the new one.
|
||||
|
||||
**Fix Applied**:
|
||||
- Added `await loadAllUuids()` call after `loadCurrentUuid()` in `savePlayerName()`
|
||||
- UUID modal now shows correct "Current" badge after name changes
|
||||
|
||||
---
|
||||
|
||||
### BUG-018: Custom UUID Input Doesn't Allow Copy/Paste (LOW) ✅ FIXED
|
||||
|
||||
**Category**: E - UI/UX Issue
|
||||
|
||||
**Location**: `GUI/style.css`, `main.js`
|
||||
|
||||
**Problem**: Two issues prevented copy/paste:
|
||||
1. The body element has `select-none` class (Tailwind) which applies `user-select: none` globally
|
||||
2. Electron's `setIgnoreMenuShortcuts(true)` was blocking Ctrl+V/C/X/A shortcuts
|
||||
|
||||
**Fix Applied**:
|
||||
- Added `user-select: text` with all vendor prefixes to `.uuid-input` class
|
||||
- Removed `setIgnoreMenuShortcuts(true)` from main.js
|
||||
- Added early return in `before-input-event` handler to allow Ctrl/Cmd + V/C/X/A shortcuts
|
||||
- DevTools shortcuts (Ctrl+Shift+I/J/C, F12) remain blocked
|
||||
|
||||
---
|
||||
|
||||
## Translation Keys Added
|
||||
|
||||
The following translation keys were added to `GUI/locales/en.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"notifications": {
|
||||
"noUsername": "No username configured. Please save your username first.",
|
||||
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
|
||||
"switchUsernameFailed": "Failed to switch username",
|
||||
"playerNameTooLong": "Player name must be 16 characters or less"
|
||||
},
|
||||
"confirm": {
|
||||
"switchUsernameTitle": "Switch Identity",
|
||||
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
|
||||
"switchUsernameButton": "Switch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After implementing fixes, verify:
|
||||
|
||||
- [x] Launch with freshly installed launcher - UUID persists
|
||||
- [x] Change username in settings - UUID preserved (renamed, not new)
|
||||
- [x] Config corruption - recovers from backup
|
||||
- [x] Click Play immediately after opening - correct UUID used
|
||||
- [x] Manual update from GitHub - UUID persists
|
||||
- [x] Username with different casing - same UUID used, case preserved
|
||||
- [x] UUID modal shows correct "Current" badge
|
||||
- [x] UUID modal refreshes after username change
|
||||
- [x] Switch identity from UUID modal works
|
||||
- [x] Profile switching doesn't affect username/UUID
|
||||
- [x] Custom UUID input allows copy/paste
|
||||
|
||||
---
|
||||
|
||||
## Architecture: How UUID/Username Persistence Works
|
||||
|
||||
**Config Structure** (`config.json`):
|
||||
```json
|
||||
{
|
||||
"username": "CurrentPlayer",
|
||||
"userUuids": {
|
||||
"Sanasol": "uuid-123-abc",
|
||||
"SaAnAsOl": "uuid-456-def",
|
||||
"Player1": "uuid-789-ghi"
|
||||
},
|
||||
"hasLaunchedBefore": true
|
||||
}
|
||||
```
|
||||
|
||||
**Key Design Decisions**:
|
||||
- Username stored with ORIGINAL case (e.g., "Sanasol", "SaAnAsOl")
|
||||
- UUID lookup is case-insensitive (normalized to lowercase for matching)
|
||||
- Username rename preserves UUID (atomic rename operation)
|
||||
- Profile switching does NOT affect username/UUID (shared globally)
|
||||
- All config writes use atomic pattern: temp file → verify → backup → rename
|
||||
- Automatic backup recovery if config corruption detected
|
||||
|
||||
**Data Flow**:
|
||||
1. User sets username in Settings → `saveUsername()` handles rename logic → saves to config.json
|
||||
2. If renaming: UUID moved from old name to new name (same UUID preserved)
|
||||
3. Launch game → `checkLaunchReady()` validates username exists
|
||||
4. Launch game → `getUuidForUser(username)` gets UUID (case-insensitive lookup)
|
||||
5. UUID modal → shows all username→UUID mappings from config
|
||||
6. Switch identity → saves new username → gets that username's UUID
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✅ Zero silent UUID regeneration
|
||||
- ✅ Config corruption recovery working
|
||||
- ✅ No UUID change without explicit user action
|
||||
- ✅ Username rename preserves UUID
|
||||
- ✅ Username case is preserved in display
|
||||
- ✅ UUID modal correctly identifies current user
|
||||
- ✅ UUID modal refreshes on changes
|
||||
- ✅ Users can switch between saved identities
|
||||
- ✅ Copy/paste works in UUID input
|
||||
|
||||
---
|
||||
|
||||
## Remaining Work
|
||||
|
||||
1. **BUG-010**: Verify migration completeness before marking done (low priority)
|
||||
2. **BUG-011**: Add file locking with `proper-lockfile` (low priority - single instance)
|
||||
3. Add telemetry for config load failures and UUID regeneration events
|
||||
|
||||
## Completed Additional Tasks
|
||||
|
||||
- ✅ Added translation keys to all 10 locale files (de-DE, es-ES, fr-FR, id-ID, pl-PL, pt-BR, ru-RU, sv-SE, tr-TR, en)
|
||||
21
local-dev/Dockerfile.debug
Normal file
21
local-dev/Dockerfile.debug
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install openssl for SSL certificate generation
|
||||
RUN apk add --no-cache openssl
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files from hytale-auth-server
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy source files
|
||||
COPY src ./src
|
||||
COPY assets ./assets
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# Use debug wrapper as entry point (will fall back to normal app.js if DEBUG_MODE!=true)
|
||||
CMD ["node", "src/debug-wrapper.js"]
|
||||
252
local-dev/README.md
Normal file
252
local-dev/README.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Hytale F2P Local Development Environment
|
||||
|
||||
Local development setup for researching and testing the Hytale client. Uses the **full hytale-auth-server** with a debug wrapper for capturing all requests.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Start the local auth server
|
||||
cd local-dev
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
|
||||
# 2. In another terminal, start the launcher
|
||||
cd ..
|
||||
HYTALE_AUTH_DOMAIN=localhost:3000 npm start
|
||||
```
|
||||
|
||||
That's it! The auth server will capture all requests for analysis.
|
||||
|
||||
## What's Included
|
||||
|
||||
### Full Auth Server with Debug Wrapper
|
||||
- Uses the complete `hytale-auth-server` (not a minimal implementation)
|
||||
- All endpoints work exactly as in production
|
||||
- Debug wrapper captures ALL requests with full details
|
||||
- Web-based debug dashboard at `/debug`
|
||||
- Requests logged to `data/auth/debug-requests.jsonl`
|
||||
- Unknown endpoints captured for research
|
||||
|
||||
### Services
|
||||
- **Auth Server** (port 3000) - Full hytale-auth-server with debug
|
||||
- **Kvrocks** (port 6666) - Redis-compatible storage for sessions
|
||||
|
||||
## Debug Dashboard
|
||||
|
||||
Access at `http://localhost:3000/debug`
|
||||
|
||||
Features:
|
||||
- Real-time request viewer with auto-refresh
|
||||
- Filter by subdomain (sessions, account-data, telemetry, tools)
|
||||
- View full request/response details (headers, body, timing)
|
||||
- Subdomain summary statistics
|
||||
- Color-coded by request type and status
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
local-dev/
|
||||
├── docker-compose.yml # Docker services config
|
||||
├── Dockerfile.debug # Dockerfile using debug entry point
|
||||
├── debug-wrapper.js # Debug wrapper for auth server
|
||||
├── start.sh # Start script
|
||||
├── README.md # This file
|
||||
└── data/
|
||||
├── auth/
|
||||
│ ├── jwt_keys.json # Generated JWT keys
|
||||
│ └── debug-requests.jsonl # Request log
|
||||
└── kvrocks/ # Redis data
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Debug Endpoints
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/debug` | GET | Web-based debug dashboard |
|
||||
| `/debug/requests` | GET | JSON list of captured requests |
|
||||
| `/debug/requests?subdomain=X` | GET | Filter by subdomain |
|
||||
| `/debug/subdomains` | GET | Request summary by subdomain |
|
||||
| `/debug/requests` | DELETE | Clear captured requests |
|
||||
|
||||
### Auth Server Endpoints
|
||||
All standard hytale-auth-server endpoints work:
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/health` | GET | Health check |
|
||||
| `/.well-known/jwks.json` | GET | JWT public keys |
|
||||
| `/game-session/new` | POST | Create session |
|
||||
| `/game-session/refresh` | POST | Refresh token |
|
||||
| `/game-session/child` | POST | Child session |
|
||||
| `/game-session` | DELETE | End session |
|
||||
| `/server-join/auth-grant` | POST | Auth grant |
|
||||
| `/server-join/auth-token` | POST | Token exchange |
|
||||
| `/my-account/game-profile` | GET/POST | Profile |
|
||||
| `/my-account/cosmetics` | GET | Cosmetics |
|
||||
| `/my-account/skin` | POST | Save skin |
|
||||
| `/profile/uuid/:uuid` | GET | Profile lookup |
|
||||
| `/profile/username/:username` | GET | Username lookup |
|
||||
| `/avatar/:uuid` | GET | Avatar viewer |
|
||||
| `/customizer/:uuid` | GET | Avatar customizer |
|
||||
| `/admin` | GET | Admin dashboard (password: localdev) |
|
||||
|
||||
### Catch-All
|
||||
Unknown endpoints return a debug response with tokens for research.
|
||||
|
||||
## Researching tools.hytale.com
|
||||
|
||||
The `tools.` subdomain is used for cloud-based asset management in builder/editor modes.
|
||||
|
||||
### How to Trigger Tools API
|
||||
|
||||
**Method 1: Builder Mode**
|
||||
1. Start local dev environment
|
||||
2. Launch game
|
||||
3. Look for Creative/Builder mode in game menu
|
||||
4. Use asset import/export features
|
||||
5. Check debug dashboard for captured `tools.` requests
|
||||
|
||||
**Method 2: Server Configuration**
|
||||
Builder mode may need server-side enablement:
|
||||
- Server config for `builderToolsEnabled`
|
||||
- Game mode settings
|
||||
- Player permissions
|
||||
|
||||
**Method 3: Console Commands**
|
||||
Try these worldtools commands in-game:
|
||||
```
|
||||
worldtools.changeModel
|
||||
worldtools.importImage
|
||||
worldtools.importObj
|
||||
worldtools.prefabEditor
|
||||
worldtools.prefabList
|
||||
worldtools.spawnEntity
|
||||
```
|
||||
|
||||
### Expected tools.hytale.com Endpoints
|
||||
|
||||
Based on binary analysis:
|
||||
```
|
||||
POST /assets/upload - Upload asset files
|
||||
GET /assets/{id} - Download asset by ID
|
||||
POST /prefabs - Save prefab
|
||||
GET /prefabs/{id} - Load prefab
|
||||
GET /prefabs - List prefabs
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Start Services
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
docker compose logs -f auth-server
|
||||
```
|
||||
|
||||
### Stop Services
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### View Captured Requests (CLI)
|
||||
```bash
|
||||
# All requests
|
||||
curl http://localhost:3000/debug/requests | jq
|
||||
|
||||
# Filter by subdomain
|
||||
curl "http://localhost:3000/debug/requests?subdomain=tools" | jq
|
||||
|
||||
# Subdomain summary
|
||||
curl http://localhost:3000/debug/subdomains | jq
|
||||
|
||||
# From log file
|
||||
cat data/auth/debug-requests.jsonl | jq -s '.'
|
||||
```
|
||||
|
||||
### Clear Request Log
|
||||
```bash
|
||||
curl -X DELETE http://localhost:3000/debug/requests
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `HYTALE_AUTH_DOMAIN` | - | Set to `localhost:3000` for local dev |
|
||||
| `PORT` | `3000` | Auth server port |
|
||||
| `DATA_DIR` | `/app/data` | Data directory |
|
||||
| `DEBUG_MODE` | `true` | Enable debug features |
|
||||
| `ADMIN_PASSWORD` | `localdev` | Admin dashboard password |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Client won't connect
|
||||
1. Verify auth server is running: `curl http://localhost:3000/health`
|
||||
2. Check environment variable: `echo $HYTALE_AUTH_DOMAIN`
|
||||
3. Check Docker logs: `docker compose logs auth-server`
|
||||
|
||||
### JWT/Token errors
|
||||
1. Check JWKS endpoint: `curl http://localhost:3000/.well-known/jwks.json`
|
||||
2. Ensure `data/auth/jwt_keys.json` exists
|
||||
3. Try restarting: `docker compose restart auth-server`
|
||||
|
||||
### Port 3000 in use
|
||||
Change the port in docker-compose.yml:
|
||||
```yaml
|
||||
ports:
|
||||
- "3001:3000"
|
||||
```
|
||||
Then use `HYTALE_AUTH_DOMAIN=localhost:3001`
|
||||
|
||||
### Server won't start
|
||||
Check for build errors:
|
||||
```bash
|
||||
docker compose logs auth-server
|
||||
docker compose build --no-cache auth-server
|
||||
```
|
||||
|
||||
## Request Log Format
|
||||
|
||||
Each line in `debug-requests.jsonl` is a JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-01-27T12:00:00.000Z",
|
||||
"method": "POST",
|
||||
"path": "/game-session/new",
|
||||
"host": "localhost:3000",
|
||||
"subdomain": null,
|
||||
"headers": {"content-type": "application/json", ...},
|
||||
"query": {},
|
||||
"body": {"uuid": "...", "name": "Player"},
|
||||
"response": {
|
||||
"statusCode": 200,
|
||||
"duration": 15,
|
||||
"body": {"identityToken": "...", ...}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Docker Compose** starts Kvrocks (Redis) and the auth server
|
||||
2. **Dockerfile.debug** builds the auth server with `debug-wrapper.js` as entry point
|
||||
3. **debug-wrapper.js** wraps the original server:
|
||||
- Intercepts all requests before they reach handlers
|
||||
- Captures request details (headers, body, timing)
|
||||
- Intercepts responses to capture response body
|
||||
- Stores everything in memory and log file
|
||||
- Adds `/debug/*` routes for the dashboard
|
||||
|
||||
4. When `DEBUG_MODE=true`:
|
||||
- All requests are logged to console with colors
|
||||
- Debug dashboard available at `/debug`
|
||||
- Unknown endpoints return debug info with valid tokens
|
||||
|
||||
5. When `DEBUG_MODE=false`:
|
||||
- Server runs normally (no debug overhead)
|
||||
- Same as production auth server
|
||||
19
local-dev/data/auth/certs/server.crt
Normal file
19
local-dev/data/auth/certs/server.crt
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDJTCCAg2gAwIBAgIUPhxbMZRuz3jUe6EzJf6yh9dazkcwDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDEyNzIxMzU0OVoXDTI3MDEy
|
||||
NzIxMzU0OVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEAzkqaar2i/RFFkmXjcVC6G8Dvf9vYTMDJXq47qVmE0nDb
|
||||
MsGSZCk4f5a9aG8BIEK/LN1csaGo3C7995R4+tqhEmFvRjHBsY2eJNeamHTEZsiw
|
||||
UqWC98/Mq2EdT/mRVWks00XQAR0yobnPz7nphmxDv3LxddHfvYv1Va010UHO4SrW
|
||||
1twNvM5UwCT2r5shpyzJAqF8U35RmaWSVzkEN7OMZbcg7aYsumdW2jSn539MKpAC
|
||||
Eyr8pRXoDqGh4zp2lEuPryKtPyh2ljNmpYIQYaXQWlyjXDsHbs9U0sHFP2fw6k1X
|
||||
sp4dUcPYzyt7WC6XfbUq2Ynz2no2sOJV1hruKY1niwIDAQABo28wbTAdBgNVHQ4E
|
||||
FgQU/RBh6UbVrO9qkFVNmo+5AnM6mMgwHwYDVR0jBBgwFoAU/RBh6UbVrO9qkFVN
|
||||
mo+5AnM6mMgwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SH
|
||||
BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAIr5gJ2gb079dF/tHeyqx6BxCg9+hsUg
|
||||
38Fsxde0a6lEjSkntla+p20Bl2mdeWEeKFw5wc1mnMQkEJjixEhE5iA6gdswvpgf
|
||||
07bYFRddCUYLrkQjWbo6u7UrkOeJfgZdHFTihMPguZ2tEPcMXitY7Ct24JRC3RRZ
|
||||
BKCp88ix+ns61vAJfgdBZPbrtM0Ky2fIS+m9fWmKHvxag2/TOB67/avFzBkYXVgV
|
||||
Xy79xxcICgIUELAQ8Hz0lXYfW/k+QnSAAtgLRxGh3eONaA/0Ij9sqzSnQzLM5fLd
|
||||
omUKx+p2gzCpprSZGfV221ZtiAm6IvwKpQJMyM2YSbdCJfYAC0RjVyQ=
|
||||
-----END CERTIFICATE-----
|
||||
28
local-dev/data/auth/certs/server.key
Normal file
28
local-dev/data/auth/certs/server.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDOSppqvaL9EUWS
|
||||
ZeNxULobwO9/29hMwMlerjupWYTScNsywZJkKTh/lr1obwEgQr8s3VyxoajcLv33
|
||||
lHj62qESYW9GMcGxjZ4k15qYdMRmyLBSpYL3z8yrYR1P+ZFVaSzTRdABHTKhuc/P
|
||||
uemGbEO/cvF10d+9i/VVrTXRQc7hKtbW3A28zlTAJPavmyGnLMkCoXxTflGZpZJX
|
||||
OQQ3s4xltyDtpiy6Z1baNKfnf0wqkAITKvylFegOoaHjOnaUS4+vIq0/KHaWM2al
|
||||
ghBhpdBaXKNcOwduz1TSwcU/Z/DqTVeynh1Rw9jPK3tYLpd9tSrZifPaejaw4lXW
|
||||
Gu4pjWeLAgMBAAECggEAOXySsYItJGamw5g/HHnJkyhd1XyXNzRWKVtWZuf6WoZ2
|
||||
nxtQRzcxdmS0XaDpaGsRSVhal2mcW9eAkHjAie9ZCX07fA0rk+YKFuw6OZf4j0gH
|
||||
0tAqwhIXT/7dI0dB19JaWnnO8DCJxoW4QoPlbr9G1dgbL6EPv4t8D6cYIzs6goZk
|
||||
LZcj5niASmkPgNoSi6yiBTcvOUEalJm3b+u5BMKL5jaodmo0pcBRa+w2jPdrFB9X
|
||||
9s2ZdztLXdn2JVxS7N0IN9KNJzOhip/0FMiv9MAxQ+rSxsNgaZRKQ5iI8nzCRRv8
|
||||
VDI6FW4+hUFIcieeEqzfORJRmtj2N99AzVZMOomkkQKBgQD3rkRiIB7Q5mFIoRRN
|
||||
YWiTOlGT3o4tYRWBE/p6eFEBTxPlvbezKyZ5OdwsUVMVbWe2ZqAUjUgQQ53VtRZq
|
||||
3VAQbsvsm08IbY9Zrj2zSqhfhm/pyT63fqWqdqHMmJfXCkL1tZJWaijyzmsUA5sO
|
||||
j7zGJtxdb7a0DFD0WYhFQ9nbeQKBgQDVOHES0+UCQsq7XEmGhYBJNnVFtV8bCy8q
|
||||
HuGBX1f+M+mE4DDuNo067EFKlEfiMTrMIKQT/3WNim6YpdCcMIrA/roCmG2cqjmQ
|
||||
gkuEItxU1WGbO3VvK2eN9lG5uiWjg5P/5F4Y6U+DdBqGmNqvH3dvQP1AyEeqYnDv
|
||||
YGkXFygWIwKBgQDUV0k1PwhsXDanR8HaHVrEbkkmFrWZ3hPLl880VBZOovcSDbaC
|
||||
GspfP+Ws8QPj6OnzjMRNGlrf5rhYUWoosBhGHlciQHxfY150qlcncSgszVsA+ZGV
|
||||
SzTIkfBhMalrqNaDROlywIzerW1LuVOkBkL3NrXSPUZL0gtNkbysdWE/MQKBgFLB
|
||||
IUHJc+y4t66YVwEa93ty43k2t77rTFbwvV1U//XteAjWaqdKDO59m6mye2PS75si
|
||||
YAxS7fENdXdRg/Ha9T+Kne878e8IMmdf6qdSUGmsl8GEBkQreHmkzHtlQA6ClwKO
|
||||
Q+cvRmkiutjaoqWtdNF9S83E7eu3YVXG+YK4vho5AoGBANt5fp1In+BLcToV+MoK
|
||||
gf6gNfwq6N3as5V4/JsB5Uem53dNE1E37rKNZqpeKHgBfrRi4FcfXNQFtNsk1Hty
|
||||
tLLuNJtt6kTFKuurccCtxvexU7oItR7Jph6U7pWjKV9zcoReX817QZQeVjq2ZYAx
|
||||
bGmSCyK9LMAI/TGYwu9G2DMA
|
||||
-----END PRIVATE KEY-----
|
||||
488
local-dev/data/auth/debug-requests.jsonl
Normal file
488
local-dev/data/auth/debug-requests.jsonl
Normal file
File diff suppressed because one or more lines are too long
5
local-dev/data/auth/jwt_keys.json
Normal file
5
local-dev/data/auth/jwt_keys.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"privateKey": "MC4CAQAwBQYDK2VwBCIEID/rEs35asZTitPu/73KBIoI4mb3dzH4QoBElz8IX29x",
|
||||
"publicKey": "MCowBQYDK2VwAyEAg1HEi1XUdUZAYc+/9ioijFkkGdulbxT3eVpM+i8Mdr4=",
|
||||
"createdAt": "2026-01-27T23:22:58.667Z"
|
||||
}
|
||||
19
local-dev/data/auth/ssl/server.crt
Normal file
19
local-dev/data/auth/ssl/server.crt
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCTCCAfGgAwIBAgIUHQdNViKmIqxQLoob3FV9wsVi7EYwDQYJKoZIhvcNAQEL
|
||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDEyNzIyMDA0MFoXDTI3MDEy
|
||||
NzIyMDA0MFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||
AAOCAQ8AMIIBCgKCAQEAtt+6Fk7s1tunvn1rCB4HislzRl38OCHw8OS6LcJQgqat
|
||||
qJMtfm/UQd2i97URmX6/RAimAEKAPd2njZXhtZ5pyGkURW9K99PhKx1yW7YPVnDs
|
||||
bkyC3TvIFG6kBtUqwdawSHigUMKaOUMlDnHLlrobIoxGt1EplyPXRykLfzWO37Wo
|
||||
3WNp/3tXcSvL7Osq/82f6jJhyTlo6UngUInMXpm3bq/k2ikEsIge7yJUqoLJwdCL
|
||||
lIkm+Q0jI+p0YSiA+b17iLJBpvQHKkhLASKfK78lggZdcZJIc0J7Oqflzy3mG9e2
|
||||
qqeSSCUKEpvvh95ep5kk5AkP0wYlgVO9bh+y9MEA9wIDAQABo1MwUTAdBgNVHQ4E
|
||||
FgQUeK7lS9jHG9wkYTasyg14fNLo3bQwHwYDVR0jBBgwFoAUeK7lS9jHG9wkYTas
|
||||
yg14fNLo3bQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAk4p1
|
||||
wxBa1j156zE6ZAQAZT9axh+nuHjA6JyKPO82npOrjjMRDgkvvmvEjMQDL+YSwngD
|
||||
RA5sJGwKiU9boaTYQEuM2uaqlNzBENv3GpWzkbfzKbqcgOOA/WFmXbogDjkknlee
|
||||
/jgirTl1hM8Kih0avDjKLwRvSSAUZB2Z0tjpK1ZdLp7L8jIGzYdke0Qt4wDyucxS
|
||||
C1BIPe8r12cSnNeP9krbdj7+P3YEWi9UgZWKAY8HbSjZSzXZvVlxcUO4ViSvF+lh
|
||||
kGfAg22af46U0TKtZB2X++xO7aNrcRbqKNSEpibG510BN9ym4P/D1+QhFcz5guco
|
||||
tvenNmwdgwY9viSnIw==
|
||||
-----END CERTIFICATE-----
|
||||
28
local-dev/data/auth/ssl/server.key
Normal file
28
local-dev/data/auth/ssl/server.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC237oWTuzW26e+
|
||||
fWsIHgeKyXNGXfw4IfDw5LotwlCCpq2oky1+b9RB3aL3tRGZfr9ECKYAQoA93aeN
|
||||
leG1nmnIaRRFb0r30+ErHXJbtg9WcOxuTILdO8gUbqQG1SrB1rBIeKBQwpo5QyUO
|
||||
ccuWuhsijEa3USmXI9dHKQt/NY7ftajdY2n/e1dxK8vs6yr/zZ/qMmHJOWjpSeBQ
|
||||
icxembdur+TaKQSwiB7vIlSqgsnB0IuUiSb5DSMj6nRhKID5vXuIskGm9AcqSEsB
|
||||
Ip8rvyWCBl1xkkhzQns6p+XPLeYb17aqp5JIJQoSm++H3l6nmSTkCQ/TBiWBU71u
|
||||
H7L0wQD3AgMBAAECggEANC/zjHM4jnY/0BI9jaL4NwiXP4EJvcEd37j+Upmt3SMQ
|
||||
0tcxd/AU7DkCTVtcaufrUFCBgvh3XXBYZQTdVTWgbYNjOA9zHvdVvjaYkIiLpvjx
|
||||
1+Y4wEbTqdULNTH3EjsgsoXOBk4KsgWx2QXrehehL8JFWgIs8sdVI6cYc1SG8dsK
|
||||
ekWB14zwaA4bBMejpGtDV0q2/bgECq6R9vU9pMD46LXkjHli+xSnYdwrimMlckg/
|
||||
VX94Tedi23mdEmxZBZ+m3fhzrSqs8C0JmTezGEO7awDl3NnAhK/t1cSfLz0o1YJv
|
||||
1SOC/JSLET4rMDMHxRlUfhM5CI9NeSXS3n4HWKcBRQKBgQD3drS+sZ/pqUxjUbv+
|
||||
LPUCLHar6ax0ge9bw3nS6LPRyp1uqmYiVRfruEWWZnCGIU+ST3zokZrroQA5jGPG
|
||||
DB5PLsopqoE217G2514WEW8hZzW3395VtZqphA+o9SCmIA1logEYdN+ZDG6KQtiS
|
||||
Ml1BdRTuyrUAC4QG4PNkb8xmSwKBgQC9LqTP7rU7p8RttN/f6q9kJCjsVrfj8rIs
|
||||
fSPzYhVDHSNwJufwMjqLJxmNtrg0yIZfPfXKki8yX5ms28xh7d2je61rBBajI/lY
|
||||
8cYsgP7bebruvClb4Rgyi9//bACWaEca/hQUte/8OG37wCSvotWMe0wZst1vLXrY
|
||||
4i1Qo3YUhQKBgCIy3H2yDHh9NhpPtFxHGEEJpgjSDUw4nLygwhg8ooUurB0cHWtH
|
||||
OTwRRkSnuYs/1UBSDeASJ0fDA0XwueUnzZSB0dx6PMs4ec3eIamJFUOXgNv9azuL
|
||||
Emm033kpDy8GJPPTtLUNic0b47bl0Ao8PZkLNi5eAy+TZ9aPhfNuY+ALAoGBAILT
|
||||
Y8QrlocHkK4xO/R0LhA5dVdi7M4lApQDgj4IIR4FY4FmVlTj11ptkp1Os3pFBa+N
|
||||
gJEyoJLcS8CfS2qZfQFWQOnVrPXqpb+diucx2YzbVKtN0ego9HvYAPJ4tLtOp4WC
|
||||
GU7tNmWcfGpxSK1xFO6OQWUwLTN6Jw8e8ilmsPylAoGBAK89GUz02qJrdbN8TMsI
|
||||
vXBVpJX27wjFFwLHcsQ09CJ1Fppa8tZrqO/BtYTPhOncsmiOSjuFCnh6d3meEUHQ
|
||||
BSYSjQWtJ+pilEMp2EiO55s4k/iFhKUeRsCjrTSGor21HqgRNaxsEfoieNWWWFv+
|
||||
rcDAyW1WdJaydbVfXisAtzOL
|
||||
-----END PRIVATE KEY-----
|
||||
BIN
local-dev/data/kvrocks/db/000014.sst
Normal file
BIN
local-dev/data/kvrocks/db/000014.sst
Normal file
Binary file not shown.
BIN
local-dev/data/kvrocks/db/000023.sst
Normal file
BIN
local-dev/data/kvrocks/db/000023.sst
Normal file
Binary file not shown.
BIN
local-dev/data/kvrocks/db/000032.sst
Normal file
BIN
local-dev/data/kvrocks/db/000032.sst
Normal file
Binary file not shown.
BIN
local-dev/data/kvrocks/db/000033.sst
Normal file
BIN
local-dev/data/kvrocks/db/000033.sst
Normal file
Binary file not shown.
BIN
local-dev/data/kvrocks/db/000034.log
Normal file
BIN
local-dev/data/kvrocks/db/000034.log
Normal file
Binary file not shown.
1
local-dev/data/kvrocks/db/CURRENT
Normal file
1
local-dev/data/kvrocks/db/CURRENT
Normal file
@@ -0,0 +1 @@
|
||||
MANIFEST-000035
|
||||
1
local-dev/data/kvrocks/db/IDENTITY
Normal file
1
local-dev/data/kvrocks/db/IDENTITY
Normal file
@@ -0,0 +1 @@
|
||||
7a0fd32b-70d3-447e-90d3-74fc57ad6651
|
||||
0
local-dev/data/kvrocks/db/LOCK
Normal file
0
local-dev/data/kvrocks/db/LOCK
Normal file
2434
local-dev/data/kvrocks/db/LOG
Normal file
2434
local-dev/data/kvrocks/db/LOG
Normal file
File diff suppressed because it is too large
Load Diff
1440
local-dev/data/kvrocks/db/LOG.old.1769550932829617
Normal file
1440
local-dev/data/kvrocks/db/LOG.old.1769550932829617
Normal file
File diff suppressed because it is too large
Load Diff
1448
local-dev/data/kvrocks/db/LOG.old.1769551238558585
Normal file
1448
local-dev/data/kvrocks/db/LOG.old.1769551238558585
Normal file
File diff suppressed because it is too large
Load Diff
391
local-dev/data/kvrocks/db/LOG.old.1769551238561527
Normal file
391
local-dev/data/kvrocks/db/LOG.old.1769551238561527
Normal file
@@ -0,0 +1,391 @@
|
||||
2026/01/27-22:00:38.559438 1 RocksDB version: 10.6.2
|
||||
2026/01/27-22:00:38.559502 1 Git sha 0
|
||||
2026/01/27-22:00:38.559503 1 Compile date 2025-11-08 14:59:16
|
||||
2026/01/27-22:00:38.559504 1 DB SUMMARY
|
||||
2026/01/27-22:00:38.559511 1 Host name (Env): 50e4104391a6
|
||||
2026/01/27-22:00:38.559513 1 DB Session ID: Y90KK2H3WYG203E6436J
|
||||
2026/01/27-22:00:38.559609 1 CURRENT file: CURRENT
|
||||
2026/01/27-22:00:38.559610 1 IDENTITY file: IDENTITY
|
||||
2026/01/27-22:00:38.559618 1 MANIFEST file: MANIFEST-000011 size: 751 Bytes
|
||||
2026/01/27-22:00:38.559619 1 SST files in /data/db dir, Total Num: 0, files:
|
||||
2026/01/27-22:00:38.559620 1 Write Ahead Log file in /data/db: 000010.log size: 1329 ;
|
||||
2026/01/27-22:00:38.559621 1 Options.error_if_exists: 0
|
||||
2026/01/27-22:00:38.559621 1 Options.create_if_missing: 1
|
||||
2026/01/27-22:00:38.559622 1 Options.paranoid_checks: 1
|
||||
2026/01/27-22:00:38.559623 1 Options.flush_verify_memtable_count: 1
|
||||
2026/01/27-22:00:38.559623 1 Options.compaction_verify_record_count: 1
|
||||
2026/01/27-22:00:38.559624 1 Options.track_and_verify_wals_in_manifest: 0
|
||||
2026/01/27-22:00:38.559624 1 Options.track_and_verify_wals: 0
|
||||
2026/01/27-22:00:38.559625 1 Options.verify_sst_unique_id_in_manifest: 1
|
||||
2026/01/27-22:00:38.559625 1 Options.env: 0xffffa561d0c0
|
||||
2026/01/27-22:00:38.559626 1 Options.fs: PosixFileSystem
|
||||
2026/01/27-22:00:38.559627 1 Options.info_log: 0xffffa570f600
|
||||
2026/01/27-22:00:38.559627 1 Options.max_file_opening_threads: 16
|
||||
2026/01/27-22:00:38.559628 1 Options.statistics: 0xffffa56135b0
|
||||
2026/01/27-22:00:38.559628 1 Options.statistics stats level: 3
|
||||
2026/01/27-22:00:38.559629 1 Options.use_fsync: 0
|
||||
2026/01/27-22:00:38.559629 1 Options.max_log_file_size: 268435456
|
||||
2026/01/27-22:00:38.559630 1 Options.max_manifest_file_size: 67108864
|
||||
2026/01/27-22:00:38.559630 1 Options.log_file_time_to_roll: 0
|
||||
2026/01/27-22:00:38.559631 1 Options.keep_log_file_num: 12
|
||||
2026/01/27-22:00:38.559631 1 Options.recycle_log_file_num: 0
|
||||
2026/01/27-22:00:38.559632 1 Options.allow_fallocate: 1
|
||||
2026/01/27-22:00:38.559632 1 Options.allow_mmap_reads: 0
|
||||
2026/01/27-22:00:38.559633 1 Options.allow_mmap_writes: 0
|
||||
2026/01/27-22:00:38.559633 1 Options.use_direct_reads: 0
|
||||
2026/01/27-22:00:38.559634 1 Options.use_direct_io_for_flush_and_compaction: 0
|
||||
2026/01/27-22:00:38.559634 1 Options.create_missing_column_families: 1
|
||||
2026/01/27-22:00:38.559634 1 Options.db_log_dir:
|
||||
2026/01/27-22:00:38.559635 1 Options.wal_dir:
|
||||
2026/01/27-22:00:38.559636 1 Options.table_cache_numshardbits: 6
|
||||
2026/01/27-22:00:38.559636 1 Options.WAL_ttl_seconds: 10800
|
||||
2026/01/27-22:00:38.559637 1 Options.WAL_size_limit_MB: 16384
|
||||
2026/01/27-22:00:38.559637 1 Options.max_write_batch_group_size_bytes: 1048576
|
||||
2026/01/27-22:00:38.559638 1 Options.manifest_preallocation_size: 4194304
|
||||
2026/01/27-22:00:38.559638 1 Options.is_fd_close_on_exec: 1
|
||||
2026/01/27-22:00:38.559639 1 Options.advise_random_on_open: 1
|
||||
2026/01/27-22:00:38.559639 1 Options.db_write_buffer_size: 0
|
||||
2026/01/27-22:00:38.559640 1 Options.write_buffer_manager: 0xffffa566bfc0
|
||||
2026/01/27-22:00:38.559640 1 Options.use_adaptive_mutex: 0
|
||||
2026/01/27-22:00:38.559640 1 Options.rate_limiter: 0xffffa5636000
|
||||
2026/01/27-22:00:38.559641 1 Options.sst_file_manager.rate_bytes_per_sec: 0
|
||||
2026/01/27-22:00:38.559642 1 Options.wal_recovery_mode: 2
|
||||
2026/01/27-22:00:38.559642 1 Options.enable_thread_tracking: 0
|
||||
2026/01/27-22:00:38.559643 1 Options.enable_pipelined_write: 0
|
||||
2026/01/27-22:00:38.559643 1 Options.unordered_write: 0
|
||||
2026/01/27-22:00:38.559644 1 Options.allow_concurrent_memtable_write: 1
|
||||
2026/01/27-22:00:38.559644 1 Options.enable_write_thread_adaptive_yield: 1
|
||||
2026/01/27-22:00:38.559645 1 Options.write_thread_max_yield_usec: 100
|
||||
2026/01/27-22:00:38.559645 1 Options.write_thread_slow_yield_usec: 3
|
||||
2026/01/27-22:00:38.559646 1 Options.row_cache: None
|
||||
2026/01/27-22:00:38.559646 1 Options.wal_filter: None
|
||||
2026/01/27-22:00:38.559647 1 Options.avoid_flush_during_recovery: 0
|
||||
2026/01/27-22:00:38.559647 1 Options.allow_ingest_behind: 0
|
||||
2026/01/27-22:00:38.559648 1 Options.two_write_queues: 0
|
||||
2026/01/27-22:00:38.559648 1 Options.manual_wal_flush: 0
|
||||
2026/01/27-22:00:38.559648 1 Options.wal_compression: 0
|
||||
2026/01/27-22:00:38.559649 1 Options.background_close_inactive_wals: 0
|
||||
2026/01/27-22:00:38.559649 1 Options.atomic_flush: 0
|
||||
2026/01/27-22:00:38.559650 1 Options.avoid_unnecessary_blocking_io: 1
|
||||
2026/01/27-22:00:38.559650 1 Options.prefix_seek_opt_in_only: 0
|
||||
2026/01/27-22:00:38.559651 1 Options.persist_stats_to_disk: 0
|
||||
2026/01/27-22:00:38.559651 1 Options.write_dbid_to_manifest: 1
|
||||
2026/01/27-22:00:38.559652 1 Options.write_identity_file: 1
|
||||
2026/01/27-22:00:38.559652 1 Options.log_readahead_size: 0
|
||||
2026/01/27-22:00:38.559653 1 Options.file_checksum_gen_factory: Unknown
|
||||
2026/01/27-22:00:38.559653 1 Options.best_efforts_recovery: 0
|
||||
2026/01/27-22:00:38.559654 1 Options.max_bgerror_resume_count: 2147483647
|
||||
2026/01/27-22:00:38.559654 1 Options.bgerror_resume_retry_interval: 1000000
|
||||
2026/01/27-22:00:38.559655 1 Options.allow_data_in_errors: 0
|
||||
2026/01/27-22:00:38.559655 1 Options.db_host_id: __hostname__
|
||||
2026/01/27-22:00:38.559656 1 Options.enforce_single_del_contracts: true
|
||||
2026/01/27-22:00:38.559657 1 Options.metadata_write_temperature: kUnknown
|
||||
2026/01/27-22:00:38.559657 1 Options.wal_write_temperature: kUnknown
|
||||
2026/01/27-22:00:38.559658 1 Options.max_background_jobs: 4
|
||||
2026/01/27-22:00:38.559658 1 Options.max_background_compactions: -1
|
||||
2026/01/27-22:00:38.559659 1 Options.max_subcompactions: 2
|
||||
2026/01/27-22:00:38.559659 1 Options.avoid_flush_during_shutdown: 0
|
||||
2026/01/27-22:00:38.559660 1 Options.writable_file_max_buffer_size: 1048576
|
||||
2026/01/27-22:00:38.559661 1 Options.delayed_write_rate : 536870912000
|
||||
2026/01/27-22:00:38.559661 1 Options.max_total_wal_size: 536870912
|
||||
2026/01/27-22:00:38.559662 1 Options.delete_obsolete_files_period_micros: 21600000000
|
||||
2026/01/27-22:00:38.559663 1 Options.stats_dump_period_sec: 0
|
||||
2026/01/27-22:00:38.559663 1 Options.stats_persist_period_sec: 600
|
||||
2026/01/27-22:00:38.559664 1 Options.stats_history_buffer_size: 1048576
|
||||
2026/01/27-22:00:38.559664 1 Options.max_open_files: 8096
|
||||
2026/01/27-22:00:38.559665 1 Options.bytes_per_sync: 1048576
|
||||
2026/01/27-22:00:38.559665 1 Options.wal_bytes_per_sync: 0
|
||||
2026/01/27-22:00:38.559666 1 Options.strict_bytes_per_sync: 0
|
||||
2026/01/27-22:00:38.559666 1 Options.compaction_readahead_size: 2097152
|
||||
2026/01/27-22:00:38.559667 1 Options.max_background_flushes: -1
|
||||
2026/01/27-22:00:38.559667 1 Options.daily_offpeak_time_utc:
|
||||
2026/01/27-22:00:38.559668 1 Compression algorithms supported:
|
||||
2026/01/27-22:00:38.559669 1 kCustomCompressionFE supported: 0
|
||||
2026/01/27-22:00:38.559669 1 kCustomCompressionFC supported: 0
|
||||
2026/01/27-22:00:38.559670 1 kCustomCompressionF8 supported: 0
|
||||
2026/01/27-22:00:38.559670 1 kCustomCompressionF7 supported: 0
|
||||
2026/01/27-22:00:38.559671 1 kCustomCompressionB2 supported: 0
|
||||
2026/01/27-22:00:38.559671 1 kLZ4Compression supported: 1
|
||||
2026/01/27-22:00:38.559672 1 kCustomCompression88 supported: 0
|
||||
2026/01/27-22:00:38.559673 1 kCustomCompressionD8 supported: 0
|
||||
2026/01/27-22:00:38.559673 1 kCustomCompression9F supported: 0
|
||||
2026/01/27-22:00:38.559674 1 kCustomCompressionD6 supported: 0
|
||||
2026/01/27-22:00:38.559674 1 kCustomCompressionA9 supported: 0
|
||||
2026/01/27-22:00:38.559675 1 kCustomCompressionEC supported: 0
|
||||
2026/01/27-22:00:38.559675 1 kCustomCompressionA3 supported: 0
|
||||
2026/01/27-22:00:38.559676 1 kCustomCompressionCB supported: 0
|
||||
2026/01/27-22:00:38.559676 1 kCustomCompression90 supported: 0
|
||||
2026/01/27-22:00:38.559677 1 kCustomCompressionA0 supported: 0
|
||||
2026/01/27-22:00:38.559677 1 kCustomCompressionC6 supported: 0
|
||||
2026/01/27-22:00:38.559678 1 kCustomCompression9D supported: 0
|
||||
2026/01/27-22:00:38.559678 1 kCustomCompression8B supported: 0
|
||||
2026/01/27-22:00:38.559679 1 kCustomCompressionA8 supported: 0
|
||||
2026/01/27-22:00:38.559679 1 kCustomCompression8D supported: 0
|
||||
2026/01/27-22:00:38.559680 1 kCustomCompression97 supported: 0
|
||||
2026/01/27-22:00:38.559680 1 kCustomCompression98 supported: 0
|
||||
2026/01/27-22:00:38.559681 1 kCustomCompressionAC supported: 0
|
||||
2026/01/27-22:00:38.559681 1 kCustomCompressionE9 supported: 0
|
||||
2026/01/27-22:00:38.559682 1 kCustomCompression96 supported: 0
|
||||
2026/01/27-22:00:38.559682 1 kCustomCompressionB1 supported: 0
|
||||
2026/01/27-22:00:38.559683 1 kCustomCompression95 supported: 0
|
||||
2026/01/27-22:00:38.559683 1 kCustomCompression84 supported: 0
|
||||
2026/01/27-22:00:38.559684 1 kCustomCompression91 supported: 0
|
||||
2026/01/27-22:00:38.559686 1 kCustomCompressionAB supported: 0
|
||||
2026/01/27-22:00:38.559687 1 kCustomCompressionB3 supported: 0
|
||||
2026/01/27-22:00:38.559687 1 kCustomCompression81 supported: 0
|
||||
2026/01/27-22:00:38.559688 1 kCustomCompressionDC supported: 0
|
||||
2026/01/27-22:00:38.559689 1 kBZip2Compression supported: 0
|
||||
2026/01/27-22:00:38.559689 1 kCustomCompressionBB supported: 0
|
||||
2026/01/27-22:00:38.559690 1 kCustomCompression9C supported: 0
|
||||
2026/01/27-22:00:38.559690 1 kCustomCompressionC9 supported: 0
|
||||
2026/01/27-22:00:38.559691 1 kCustomCompressionCC supported: 0
|
||||
2026/01/27-22:00:38.559691 1 kCustomCompression92 supported: 0
|
||||
2026/01/27-22:00:38.559692 1 kCustomCompressionB9 supported: 0
|
||||
2026/01/27-22:00:38.559694 1 kCustomCompression8F supported: 0
|
||||
2026/01/27-22:00:38.559695 1 kCustomCompression8A supported: 0
|
||||
2026/01/27-22:00:38.559695 1 kCustomCompression9B supported: 0
|
||||
2026/01/27-22:00:38.559696 1 kZSTD supported: 1
|
||||
2026/01/27-22:00:38.559696 1 kCustomCompressionAA supported: 0
|
||||
2026/01/27-22:00:38.559697 1 kCustomCompressionA2 supported: 0
|
||||
2026/01/27-22:00:38.559697 1 kZlibCompression supported: 1
|
||||
2026/01/27-22:00:38.559698 1 kXpressCompression supported: 0
|
||||
2026/01/27-22:00:38.559699 1 kCustomCompressionFD supported: 0
|
||||
2026/01/27-22:00:38.559699 1 kCustomCompressionE2 supported: 0
|
||||
2026/01/27-22:00:38.559700 1 kLZ4HCCompression supported: 1
|
||||
2026/01/27-22:00:38.559700 1 kCustomCompressionA6 supported: 0
|
||||
2026/01/27-22:00:38.559701 1 kCustomCompression85 supported: 0
|
||||
2026/01/27-22:00:38.559701 1 kCustomCompressionA4 supported: 0
|
||||
2026/01/27-22:00:38.559702 1 kCustomCompression86 supported: 0
|
||||
2026/01/27-22:00:38.559702 1 kCustomCompression83 supported: 0
|
||||
2026/01/27-22:00:38.559703 1 kCustomCompression87 supported: 0
|
||||
2026/01/27-22:00:38.559703 1 kCustomCompression89 supported: 0
|
||||
2026/01/27-22:00:38.559704 1 kCustomCompression8C supported: 0
|
||||
2026/01/27-22:00:38.559704 1 kCustomCompressionDB supported: 0
|
||||
2026/01/27-22:00:38.559705 1 kCustomCompressionF3 supported: 0
|
||||
2026/01/27-22:00:38.559705 1 kCustomCompressionE6 supported: 0
|
||||
2026/01/27-22:00:38.559705 1 kCustomCompression8E supported: 0
|
||||
2026/01/27-22:00:38.559706 1 kCustomCompressionDA supported: 0
|
||||
2026/01/27-22:00:38.559706 1 kCustomCompression93 supported: 0
|
||||
2026/01/27-22:00:38.559707 1 kCustomCompression94 supported: 0
|
||||
2026/01/27-22:00:38.559709 1 kCustomCompression9E supported: 0
|
||||
2026/01/27-22:00:38.559710 1 kCustomCompressionB4 supported: 0
|
||||
2026/01/27-22:00:38.559710 1 kCustomCompressionFB supported: 0
|
||||
2026/01/27-22:00:38.559711 1 kCustomCompressionB5 supported: 0
|
||||
2026/01/27-22:00:38.559711 1 kCustomCompressionD5 supported: 0
|
||||
2026/01/27-22:00:38.559712 1 kCustomCompressionB8 supported: 0
|
||||
2026/01/27-22:00:38.559712 1 kCustomCompressionD1 supported: 0
|
||||
2026/01/27-22:00:38.559713 1 kCustomCompressionBA supported: 0
|
||||
2026/01/27-22:00:38.559713 1 kCustomCompressionBC supported: 0
|
||||
2026/01/27-22:00:38.559714 1 kCustomCompressionCE supported: 0
|
||||
2026/01/27-22:00:38.559714 1 kCustomCompressionBD supported: 0
|
||||
2026/01/27-22:00:38.559715 1 kCustomCompressionC4 supported: 0
|
||||
2026/01/27-22:00:38.559715 1 kCustomCompression9A supported: 0
|
||||
2026/01/27-22:00:38.559716 1 kCustomCompression99 supported: 0
|
||||
2026/01/27-22:00:38.559716 1 kCustomCompressionBE supported: 0
|
||||
2026/01/27-22:00:38.559717 1 kCustomCompressionE5 supported: 0
|
||||
2026/01/27-22:00:38.559717 1 kCustomCompressionD9 supported: 0
|
||||
2026/01/27-22:00:38.559718 1 kCustomCompressionC1 supported: 0
|
||||
2026/01/27-22:00:38.559718 1 kCustomCompressionC5 supported: 0
|
||||
2026/01/27-22:00:38.559719 1 kCustomCompressionC2 supported: 0
|
||||
2026/01/27-22:00:38.559719 1 kCustomCompressionA5 supported: 0
|
||||
2026/01/27-22:00:38.559720 1 kCustomCompressionC7 supported: 0
|
||||
2026/01/27-22:00:38.559720 1 kCustomCompressionBF supported: 0
|
||||
2026/01/27-22:00:38.559721 1 kCustomCompressionE8 supported: 0
|
||||
2026/01/27-22:00:38.559721 1 kCustomCompressionC8 supported: 0
|
||||
2026/01/27-22:00:38.559722 1 kCustomCompressionAF supported: 0
|
||||
2026/01/27-22:00:38.559722 1 kCustomCompressionCA supported: 0
|
||||
2026/01/27-22:00:38.559723 1 kCustomCompressionCD supported: 0
|
||||
2026/01/27-22:00:38.559723 1 kCustomCompressionC0 supported: 0
|
||||
2026/01/27-22:00:38.559724 1 kCustomCompressionCF supported: 0
|
||||
2026/01/27-22:00:38.559724 1 kCustomCompressionF9 supported: 0
|
||||
2026/01/27-22:00:38.559724 1 kCustomCompressionD0 supported: 0
|
||||
2026/01/27-22:00:38.559725 1 kCustomCompressionD2 supported: 0
|
||||
2026/01/27-22:00:38.559726 1 kCustomCompressionAD supported: 0
|
||||
2026/01/27-22:00:38.559726 1 kCustomCompressionD3 supported: 0
|
||||
2026/01/27-22:00:38.559727 1 kCustomCompressionD4 supported: 0
|
||||
2026/01/27-22:00:38.559727 1 kCustomCompressionD7 supported: 0
|
||||
2026/01/27-22:00:38.559728 1 kCustomCompression82 supported: 0
|
||||
2026/01/27-22:00:38.559728 1 kCustomCompressionDD supported: 0
|
||||
2026/01/27-22:00:38.559729 1 kCustomCompressionC3 supported: 0
|
||||
2026/01/27-22:00:38.559729 1 kCustomCompressionEE supported: 0
|
||||
2026/01/27-22:00:38.559730 1 kCustomCompressionDE supported: 0
|
||||
2026/01/27-22:00:38.559730 1 kCustomCompressionDF supported: 0
|
||||
2026/01/27-22:00:38.559731 1 kCustomCompressionA7 supported: 0
|
||||
2026/01/27-22:00:38.559731 1 kCustomCompressionE0 supported: 0
|
||||
2026/01/27-22:00:38.559732 1 kCustomCompressionF1 supported: 0
|
||||
2026/01/27-22:00:38.559732 1 kCustomCompressionE1 supported: 0
|
||||
2026/01/27-22:00:38.559733 1 kCustomCompressionF5 supported: 0
|
||||
2026/01/27-22:00:38.559733 1 kCustomCompression80 supported: 0
|
||||
2026/01/27-22:00:38.559734 1 kCustomCompressionE3 supported: 0
|
||||
2026/01/27-22:00:38.559734 1 kCustomCompressionE4 supported: 0
|
||||
2026/01/27-22:00:38.559735 1 kCustomCompressionB0 supported: 0
|
||||
2026/01/27-22:00:38.559735 1 kCustomCompressionEA supported: 0
|
||||
2026/01/27-22:00:38.559736 1 kCustomCompressionFA supported: 0
|
||||
2026/01/27-22:00:38.559736 1 kCustomCompressionE7 supported: 0
|
||||
2026/01/27-22:00:38.559737 1 kCustomCompressionAE supported: 0
|
||||
2026/01/27-22:00:38.559737 1 kCustomCompressionEB supported: 0
|
||||
2026/01/27-22:00:38.559738 1 kCustomCompressionED supported: 0
|
||||
2026/01/27-22:00:38.559738 1 kCustomCompressionB6 supported: 0
|
||||
2026/01/27-22:00:38.559739 1 kCustomCompressionEF supported: 0
|
||||
2026/01/27-22:00:38.559739 1 kCustomCompressionF0 supported: 0
|
||||
2026/01/27-22:00:38.559740 1 kCustomCompressionB7 supported: 0
|
||||
2026/01/27-22:00:38.559740 1 kCustomCompressionF2 supported: 0
|
||||
2026/01/27-22:00:38.559741 1 kCustomCompressionA1 supported: 0
|
||||
2026/01/27-22:00:38.559741 1 kCustomCompressionF4 supported: 0
|
||||
2026/01/27-22:00:38.559742 1 kSnappyCompression supported: 1
|
||||
2026/01/27-22:00:38.559742 1 kCustomCompressionF6 supported: 0
|
||||
2026/01/27-22:00:38.559743 1 Fast CRC32 supported: Supported on Arm64
|
||||
2026/01/27-22:00:38.559744 1 DMutex implementation: pthread_mutex_t
|
||||
2026/01/27-22:00:38.559744 1 Jemalloc supported: 1
|
||||
2026/01/27-22:00:38.560596 1 [db/version_set.cc:6190] Recovering from manifest file: /data/db/MANIFEST-000011
|
||||
2026/01/27-22:00:38.560798 1 [db/column_family.cc:693] --------------- Options for column family [default]:
|
||||
2026/01/27-22:00:38.560800 1 Options.comparator: leveldb.BytewiseComparator
|
||||
2026/01/27-22:00:38.560801 1 Options.merge_operator: None
|
||||
2026/01/27-22:00:38.560802 1 Options.compaction_filter: None
|
||||
2026/01/27-22:00:38.560802 1 Options.compaction_filter_factory: None
|
||||
2026/01/27-22:00:38.560803 1 Options.sst_partitioner_factory: None
|
||||
2026/01/27-22:00:38.560803 1 Options.memtable_factory: SkipListFactory
|
||||
2026/01/27-22:00:38.560804 1 Options.table_factory: BlockBasedTable
|
||||
2026/01/27-22:00:38.560817 1 table_factory options: flush_block_policy_factory: FlushBlockBySizePolicyFactory (0xffffa56d15a0)
|
||||
cache_index_and_filter_blocks: 0
|
||||
cache_index_and_filter_blocks_with_high_priority: 1
|
||||
pin_l0_filter_and_index_blocks_in_cache: 0
|
||||
pin_top_level_index_and_filter: 1
|
||||
index_type: 0
|
||||
data_block_index_type: 0
|
||||
index_shortening: 1
|
||||
data_block_hash_table_util_ratio: 0.750000
|
||||
checksum: 4
|
||||
no_block_cache: 0
|
||||
block_cache: 0xffffa566bd90
|
||||
block_cache_name: LRUCache
|
||||
block_cache_options:
|
||||
capacity : 33554432
|
||||
num_shard_bits : 6
|
||||
strict_capacity_limit : 0
|
||||
memory_allocator : None
|
||||
high_pri_pool_ratio: 0.500
|
||||
low_pri_pool_ratio: 0.000
|
||||
persistent_cache: (nil)
|
||||
block_size: 4096
|
||||
block_size_deviation: 10
|
||||
block_restart_interval: 16
|
||||
index_block_restart_interval: 1
|
||||
metadata_block_size: 4096
|
||||
partition_filters: 0
|
||||
use_delta_encoding: 1
|
||||
filter_policy: nullptr
|
||||
user_defined_index_factory: nullptr
|
||||
fail_if_no_udi_on_open: 0
|
||||
whole_key_filtering: 1
|
||||
verify_compression: 0
|
||||
read_amp_bytes_per_bit: 0
|
||||
format_version: 6
|
||||
enable_index_compression: 1
|
||||
block_align: 0
|
||||
max_auto_readahead_size: 262144
|
||||
prepopulate_block_cache: 0
|
||||
initial_auto_readahead_size: 8192
|
||||
num_file_reads_for_auto_readahead: 2
|
||||
2026/01/27-22:00:38.560818 1 Options.write_buffer_size: 67108864
|
||||
2026/01/27-22:00:38.560819 1 Options.max_write_buffer_number: 4
|
||||
2026/01/27-22:00:38.560820 1 Options.compression[0]: NoCompression
|
||||
2026/01/27-22:00:38.560820 1 Options.compression[1]: NoCompression
|
||||
2026/01/27-22:00:38.560821 1 Options.compression[2]: Snappy
|
||||
2026/01/27-22:00:38.560822 1 Options.compression[3]: Snappy
|
||||
2026/01/27-22:00:38.560822 1 Options.compression[4]: Snappy
|
||||
2026/01/27-22:00:38.560823 1 Options.compression[5]: Snappy
|
||||
2026/01/27-22:00:38.560823 1 Options.compression[6]: Snappy
|
||||
2026/01/27-22:00:38.560824 1 Options.bottommost_compression: Disabled
|
||||
2026/01/27-22:00:38.560824 1 Options.prefix_extractor: nullptr
|
||||
2026/01/27-22:00:38.560825 1 Options.memtable_insert_with_hint_prefix_extractor: nullptr
|
||||
2026/01/27-22:00:38.560825 1 Options.num_levels: 7
|
||||
2026/01/27-22:00:38.560826 1 Options.min_write_buffer_number_to_merge: 1
|
||||
2026/01/27-22:00:38.560826 1 Options.max_write_buffer_size_to_maintain: 0
|
||||
2026/01/27-22:00:38.560827 1 Options.bottommost_compression_opts.window_bits: -14
|
||||
2026/01/27-22:00:38.560827 1 Options.bottommost_compression_opts.level: 32767
|
||||
2026/01/27-22:00:38.560828 1 Options.bottommost_compression_opts.strategy: 0
|
||||
2026/01/27-22:00:38.560828 1 Options.bottommost_compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-22:00:38.560829 1 Options.bottommost_compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-22:00:38.560829 1 Options.bottommost_compression_opts.parallel_threads: 1
|
||||
2026/01/27-22:00:38.560830 1 Options.bottommost_compression_opts.enabled: false
|
||||
2026/01/27-22:00:38.560830 1 Options.bottommost_compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-22:00:38.560831 1 Options.bottommost_compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-22:00:38.560831 1 Options.compression_opts.window_bits: -14
|
||||
2026/01/27-22:00:38.560832 1 Options.compression_opts.level: 32767
|
||||
2026/01/27-22:00:38.560832 1 Options.compression_opts.strategy: 0
|
||||
2026/01/27-22:00:38.560833 1 Options.compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-22:00:38.560833 1 Options.compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-22:00:38.560834 1 Options.compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-22:00:38.560834 1 Options.compression_opts.parallel_threads: 1
|
||||
2026/01/27-22:00:38.560835 1 Options.compression_opts.enabled: false
|
||||
2026/01/27-22:00:38.560835 1 Options.compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-22:00:38.560836 1 Options.level0_file_num_compaction_trigger: 4
|
||||
2026/01/27-22:00:38.560836 1 Options.level0_slowdown_writes_trigger: 20
|
||||
2026/01/27-22:00:38.560837 1 Options.level0_stop_writes_trigger: 40
|
||||
2026/01/27-22:00:38.560837 1 Options.target_file_size_base: 134217728
|
||||
2026/01/27-22:00:38.560838 1 Options.target_file_size_multiplier: 1
|
||||
2026/01/27-22:00:38.560838 1 Options.max_bytes_for_level_base: 268435456
|
||||
2026/01/27-22:00:38.560838 1 Options.level_compaction_dynamic_level_bytes: 1
|
||||
2026/01/27-22:00:38.560840 1 Options.max_bytes_for_level_multiplier: 10.000000
|
||||
2026/01/27-22:00:38.560840 1 Options.max_bytes_for_level_multiplier_addtl[0]: 1
|
||||
2026/01/27-22:00:38.560841 1 Options.max_bytes_for_level_multiplier_addtl[1]: 1
|
||||
2026/01/27-22:00:38.560841 1 Options.max_bytes_for_level_multiplier_addtl[2]: 1
|
||||
2026/01/27-22:00:38.560842 1 Options.max_bytes_for_level_multiplier_addtl[3]: 1
|
||||
2026/01/27-22:00:38.560842 1 Options.max_bytes_for_level_multiplier_addtl[4]: 1
|
||||
2026/01/27-22:00:38.560843 1 Options.max_bytes_for_level_multiplier_addtl[5]: 1
|
||||
2026/01/27-22:00:38.560843 1 Options.max_bytes_for_level_multiplier_addtl[6]: 1
|
||||
2026/01/27-22:00:38.560844 1 Options.max_sequential_skip_in_iterations: 8
|
||||
2026/01/27-22:00:38.560844 1 Options.memtable_op_scan_flush_trigger: 0
|
||||
2026/01/27-22:00:38.560844 1 Options.memtable_avg_op_scan_flush_trigger: 0
|
||||
2026/01/27-22:00:38.560845 1 Options.max_compaction_bytes: 3355443200
|
||||
2026/01/27-22:00:38.560845 1 Options.arena_block_size: 1048576
|
||||
2026/01/27-22:00:38.560846 1 Options.soft_pending_compaction_bytes_limit: 68719476736
|
||||
2026/01/27-22:00:38.560846 1 Options.hard_pending_compaction_bytes_limit: 274877906944
|
||||
2026/01/27-22:00:38.560847 1 Options.disable_auto_compactions: 0
|
||||
2026/01/27-22:00:38.560848 1 Options.compaction_style: kCompactionStyleLevel
|
||||
2026/01/27-22:00:38.560849 1 Options.compaction_pri: kMinOverlappingRatio
|
||||
2026/01/27-22:00:38.560849 1 Options.compaction_options_universal.size_ratio: 1
|
||||
2026/01/27-22:00:38.560850 1 Options.compaction_options_universal.min_merge_width: 2
|
||||
2026/01/27-22:00:38.560850 1 Options.compaction_options_universal.max_merge_width: 4294967295
|
||||
2026/01/27-22:00:38.560851 1 Options.compaction_options_universal.max_size_amplification_percent: 200
|
||||
2026/01/27-22:00:38.560851 1 Options.compaction_options_universal.compression_size_percent: -1
|
||||
2026/01/27-22:00:38.560852 1 Options.compaction_options_universal.stop_style: kCompactionStopStyleTotalSize
|
||||
2026/01/27-22:00:38.560852 1 Options.compaction_options_universal.max_read_amp: -1
|
||||
2026/01/27-22:00:38.560853 1 Options.compaction_options_universal.reduce_file_locking: 0
|
||||
2026/01/27-22:00:38.560853 1 Options.compaction_options_fifo.max_table_files_size: 1073741824
|
||||
2026/01/27-22:00:38.560854 1 Options.compaction_options_fifo.allow_compaction: 0
|
||||
2026/01/27-22:00:38.560855 1 Options.table_properties_collectors:
|
||||
2026/01/27-22:00:38.560856 1 Options.inplace_update_support: 0
|
||||
2026/01/27-22:00:38.560857 1 Options.inplace_update_num_locks: 10000
|
||||
2026/01/27-22:00:38.560857 1 Options.memtable_prefix_bloom_size_ratio: 0.000000
|
||||
2026/01/27-22:00:38.560858 1 Options.memtable_whole_key_filtering: 0
|
||||
2026/01/27-22:00:38.560858 1 Options.memtable_huge_page_size: 0
|
||||
2026/01/27-22:00:38.560859 1 Options.bloom_locality: 0
|
||||
2026/01/27-22:00:38.560859 1 Options.max_successive_merges: 0
|
||||
2026/01/27-22:00:38.560860 1 Options.strict_max_successive_merges: 0
|
||||
2026/01/27-22:00:38.560860 1 Options.optimize_filters_for_hits: 0
|
||||
2026/01/27-22:00:38.560861 1 Options.paranoid_file_checks: 0
|
||||
2026/01/27-22:00:38.560861 1 Options.force_consistency_checks: 1
|
||||
2026/01/27-22:00:38.560862 1 Options.report_bg_io_stats: 0
|
||||
2026/01/27-22:00:38.560862 1 Options.disallow_memtable_writes: 0
|
||||
2026/01/27-22:00:38.560863 1 Options.ttl: 2592000
|
||||
2026/01/27-22:00:38.560863 1 Options.periodic_compaction_seconds: 0
|
||||
2026/01/27-22:00:38.560864 1 Options.default_temperature: kUnknown
|
||||
2026/01/27-22:00:38.560864 1 Options.preclude_last_level_data_seconds: 0
|
||||
2026/01/27-22:00:38.560865 1 Options.preserve_internal_time_seconds: 0
|
||||
2026/01/27-22:00:38.560865 1 Options.enable_blob_files: false
|
||||
2026/01/27-22:00:38.560866 1 Options.min_blob_size: 0
|
||||
2026/01/27-22:00:38.560866 1 Options.blob_file_size: 268435456
|
||||
2026/01/27-22:00:38.560867 1 Options.blob_compression_type: NoCompression
|
||||
2026/01/27-22:00:38.560867 1 Options.enable_blob_garbage_collection: false
|
||||
2026/01/27-22:00:38.560879 1 Options.blob_garbage_collection_age_cutoff: 0.250000
|
||||
2026/01/27-22:00:38.560880 1 Options.blob_garbage_collection_force_threshold: 1.000000
|
||||
2026/01/27-22:00:38.560880 1 Options.blob_compaction_readahead_size: 0
|
||||
2026/01/27-22:00:38.560881 1 Options.blob_file_starting_level: 0
|
||||
2026/01/27-22:00:38.560881 1 Options.experimental_mempurge_threshold: 0.000000
|
||||
2026/01/27-22:00:38.560882 1 Options.memtable_max_range_deletions: 0
|
||||
2026/01/27-22:00:38.560882 1 Options.cf_allow_ingest_behind: false
|
||||
2026/01/27-22:00:38.561145 1 [WARN] [db/db_impl/db_impl_open.cc:2688] DB::Open() failed: Invalid argument: Column families not opened: index, search, stream, propagate, pubsub, zset_score, metadata
|
||||
2026/01/27-22:00:38.561190 1 [db/db_impl/db_impl.cc:467] Shutdown: canceling all background work
|
||||
2026/01/27-22:00:38.561240 1 [db/db_impl/db_impl.cc:681] Shutdown complete
|
||||
1449
local-dev/data/kvrocks/db/LOG.old.1769551520856746
Normal file
1449
local-dev/data/kvrocks/db/LOG.old.1769551520856746
Normal file
File diff suppressed because it is too large
Load Diff
391
local-dev/data/kvrocks/db/LOG.old.1769551520859596
Normal file
391
local-dev/data/kvrocks/db/LOG.old.1769551520859596
Normal file
@@ -0,0 +1,391 @@
|
||||
2026/01/27-22:05:20.858109 1 RocksDB version: 10.6.2
|
||||
2026/01/27-22:05:20.858266 1 Git sha 0
|
||||
2026/01/27-22:05:20.858267 1 Compile date 2025-11-08 14:59:16
|
||||
2026/01/27-22:05:20.858268 1 DB SUMMARY
|
||||
2026/01/27-22:05:20.858269 1 Host name (Env): 83baffb0044b
|
||||
2026/01/27-22:05:20.858270 1 DB Session ID: KLAN4OO9759DRR73WKX0
|
||||
2026/01/27-22:05:20.858345 1 CURRENT file: CURRENT
|
||||
2026/01/27-22:05:20.858346 1 IDENTITY file: IDENTITY
|
||||
2026/01/27-22:05:20.858357 1 MANIFEST file: MANIFEST-000016 size: 941 Bytes
|
||||
2026/01/27-22:05:20.858358 1 SST files in /data/db dir, Total Num: 1, files: 000014.sst
|
||||
2026/01/27-22:05:20.858359 1 Write Ahead Log file in /data/db: 000015.log size: 0 ;
|
||||
2026/01/27-22:05:20.858360 1 Options.error_if_exists: 0
|
||||
2026/01/27-22:05:20.858361 1 Options.create_if_missing: 1
|
||||
2026/01/27-22:05:20.858361 1 Options.paranoid_checks: 1
|
||||
2026/01/27-22:05:20.858362 1 Options.flush_verify_memtable_count: 1
|
||||
2026/01/27-22:05:20.858362 1 Options.compaction_verify_record_count: 1
|
||||
2026/01/27-22:05:20.858363 1 Options.track_and_verify_wals_in_manifest: 0
|
||||
2026/01/27-22:05:20.858363 1 Options.track_and_verify_wals: 0
|
||||
2026/01/27-22:05:20.858364 1 Options.verify_sst_unique_id_in_manifest: 1
|
||||
2026/01/27-22:05:20.858364 1 Options.env: 0xffff8221d0c0
|
||||
2026/01/27-22:05:20.858365 1 Options.fs: PosixFileSystem
|
||||
2026/01/27-22:05:20.858366 1 Options.info_log: 0xffff8230f600
|
||||
2026/01/27-22:05:20.858366 1 Options.max_file_opening_threads: 16
|
||||
2026/01/27-22:05:20.858366 1 Options.statistics: 0xffff822135b0
|
||||
2026/01/27-22:05:20.858367 1 Options.statistics stats level: 3
|
||||
2026/01/27-22:05:20.858367 1 Options.use_fsync: 0
|
||||
2026/01/27-22:05:20.858368 1 Options.max_log_file_size: 268435456
|
||||
2026/01/27-22:05:20.858369 1 Options.max_manifest_file_size: 67108864
|
||||
2026/01/27-22:05:20.858369 1 Options.log_file_time_to_roll: 0
|
||||
2026/01/27-22:05:20.858370 1 Options.keep_log_file_num: 12
|
||||
2026/01/27-22:05:20.858370 1 Options.recycle_log_file_num: 0
|
||||
2026/01/27-22:05:20.858371 1 Options.allow_fallocate: 1
|
||||
2026/01/27-22:05:20.858371 1 Options.allow_mmap_reads: 0
|
||||
2026/01/27-22:05:20.858371 1 Options.allow_mmap_writes: 0
|
||||
2026/01/27-22:05:20.858372 1 Options.use_direct_reads: 0
|
||||
2026/01/27-22:05:20.858372 1 Options.use_direct_io_for_flush_and_compaction: 0
|
||||
2026/01/27-22:05:20.858373 1 Options.create_missing_column_families: 1
|
||||
2026/01/27-22:05:20.858373 1 Options.db_log_dir:
|
||||
2026/01/27-22:05:20.858374 1 Options.wal_dir:
|
||||
2026/01/27-22:05:20.858374 1 Options.table_cache_numshardbits: 6
|
||||
2026/01/27-22:05:20.858375 1 Options.WAL_ttl_seconds: 10800
|
||||
2026/01/27-22:05:20.858375 1 Options.WAL_size_limit_MB: 16384
|
||||
2026/01/27-22:05:20.858376 1 Options.max_write_batch_group_size_bytes: 1048576
|
||||
2026/01/27-22:05:20.858376 1 Options.manifest_preallocation_size: 4194304
|
||||
2026/01/27-22:05:20.858377 1 Options.is_fd_close_on_exec: 1
|
||||
2026/01/27-22:05:20.858377 1 Options.advise_random_on_open: 1
|
||||
2026/01/27-22:05:20.858378 1 Options.db_write_buffer_size: 0
|
||||
2026/01/27-22:05:20.858378 1 Options.write_buffer_manager: 0xffff8226bfc0
|
||||
2026/01/27-22:05:20.858379 1 Options.use_adaptive_mutex: 0
|
||||
2026/01/27-22:05:20.858379 1 Options.rate_limiter: 0xffff82236000
|
||||
2026/01/27-22:05:20.858380 1 Options.sst_file_manager.rate_bytes_per_sec: 0
|
||||
2026/01/27-22:05:20.858380 1 Options.wal_recovery_mode: 2
|
||||
2026/01/27-22:05:20.858381 1 Options.enable_thread_tracking: 0
|
||||
2026/01/27-22:05:20.858381 1 Options.enable_pipelined_write: 0
|
||||
2026/01/27-22:05:20.858382 1 Options.unordered_write: 0
|
||||
2026/01/27-22:05:20.858382 1 Options.allow_concurrent_memtable_write: 1
|
||||
2026/01/27-22:05:20.858383 1 Options.enable_write_thread_adaptive_yield: 1
|
||||
2026/01/27-22:05:20.858383 1 Options.write_thread_max_yield_usec: 100
|
||||
2026/01/27-22:05:20.858384 1 Options.write_thread_slow_yield_usec: 3
|
||||
2026/01/27-22:05:20.858384 1 Options.row_cache: None
|
||||
2026/01/27-22:05:20.858384 1 Options.wal_filter: None
|
||||
2026/01/27-22:05:20.858385 1 Options.avoid_flush_during_recovery: 0
|
||||
2026/01/27-22:05:20.858385 1 Options.allow_ingest_behind: 0
|
||||
2026/01/27-22:05:20.858386 1 Options.two_write_queues: 0
|
||||
2026/01/27-22:05:20.858386 1 Options.manual_wal_flush: 0
|
||||
2026/01/27-22:05:20.858387 1 Options.wal_compression: 0
|
||||
2026/01/27-22:05:20.858387 1 Options.background_close_inactive_wals: 0
|
||||
2026/01/27-22:05:20.858388 1 Options.atomic_flush: 0
|
||||
2026/01/27-22:05:20.858388 1 Options.avoid_unnecessary_blocking_io: 1
|
||||
2026/01/27-22:05:20.858389 1 Options.prefix_seek_opt_in_only: 0
|
||||
2026/01/27-22:05:20.858389 1 Options.persist_stats_to_disk: 0
|
||||
2026/01/27-22:05:20.858390 1 Options.write_dbid_to_manifest: 1
|
||||
2026/01/27-22:05:20.858390 1 Options.write_identity_file: 1
|
||||
2026/01/27-22:05:20.858391 1 Options.log_readahead_size: 0
|
||||
2026/01/27-22:05:20.858391 1 Options.file_checksum_gen_factory: Unknown
|
||||
2026/01/27-22:05:20.858392 1 Options.best_efforts_recovery: 0
|
||||
2026/01/27-22:05:20.858392 1 Options.max_bgerror_resume_count: 2147483647
|
||||
2026/01/27-22:05:20.858393 1 Options.bgerror_resume_retry_interval: 1000000
|
||||
2026/01/27-22:05:20.858393 1 Options.allow_data_in_errors: 0
|
||||
2026/01/27-22:05:20.858394 1 Options.db_host_id: __hostname__
|
||||
2026/01/27-22:05:20.858394 1 Options.enforce_single_del_contracts: true
|
||||
2026/01/27-22:05:20.858395 1 Options.metadata_write_temperature: kUnknown
|
||||
2026/01/27-22:05:20.858396 1 Options.wal_write_temperature: kUnknown
|
||||
2026/01/27-22:05:20.858396 1 Options.max_background_jobs: 4
|
||||
2026/01/27-22:05:20.858396 1 Options.max_background_compactions: -1
|
||||
2026/01/27-22:05:20.858397 1 Options.max_subcompactions: 2
|
||||
2026/01/27-22:05:20.858397 1 Options.avoid_flush_during_shutdown: 0
|
||||
2026/01/27-22:05:20.858398 1 Options.writable_file_max_buffer_size: 1048576
|
||||
2026/01/27-22:05:20.858399 1 Options.delayed_write_rate : 536870912000
|
||||
2026/01/27-22:05:20.858399 1 Options.max_total_wal_size: 536870912
|
||||
2026/01/27-22:05:20.858400 1 Options.delete_obsolete_files_period_micros: 21600000000
|
||||
2026/01/27-22:05:20.858400 1 Options.stats_dump_period_sec: 0
|
||||
2026/01/27-22:05:20.858401 1 Options.stats_persist_period_sec: 600
|
||||
2026/01/27-22:05:20.858401 1 Options.stats_history_buffer_size: 1048576
|
||||
2026/01/27-22:05:20.858402 1 Options.max_open_files: 8096
|
||||
2026/01/27-22:05:20.858402 1 Options.bytes_per_sync: 1048576
|
||||
2026/01/27-22:05:20.858403 1 Options.wal_bytes_per_sync: 0
|
||||
2026/01/27-22:05:20.858403 1 Options.strict_bytes_per_sync: 0
|
||||
2026/01/27-22:05:20.858404 1 Options.compaction_readahead_size: 2097152
|
||||
2026/01/27-22:05:20.858405 1 Options.max_background_flushes: -1
|
||||
2026/01/27-22:05:20.858405 1 Options.daily_offpeak_time_utc:
|
||||
2026/01/27-22:05:20.858406 1 Compression algorithms supported:
|
||||
2026/01/27-22:05:20.858406 1 kCustomCompressionFE supported: 0
|
||||
2026/01/27-22:05:20.858407 1 kCustomCompressionFC supported: 0
|
||||
2026/01/27-22:05:20.858407 1 kCustomCompressionF8 supported: 0
|
||||
2026/01/27-22:05:20.858408 1 kCustomCompressionF7 supported: 0
|
||||
2026/01/27-22:05:20.858408 1 kCustomCompressionB2 supported: 0
|
||||
2026/01/27-22:05:20.858409 1 kLZ4Compression supported: 1
|
||||
2026/01/27-22:05:20.858410 1 kCustomCompression88 supported: 0
|
||||
2026/01/27-22:05:20.858410 1 kCustomCompressionD8 supported: 0
|
||||
2026/01/27-22:05:20.858411 1 kCustomCompression9F supported: 0
|
||||
2026/01/27-22:05:20.858411 1 kCustomCompressionD6 supported: 0
|
||||
2026/01/27-22:05:20.858412 1 kCustomCompressionA9 supported: 0
|
||||
2026/01/27-22:05:20.858412 1 kCustomCompressionEC supported: 0
|
||||
2026/01/27-22:05:20.858413 1 kCustomCompressionA3 supported: 0
|
||||
2026/01/27-22:05:20.858413 1 kCustomCompressionCB supported: 0
|
||||
2026/01/27-22:05:20.858414 1 kCustomCompression90 supported: 0
|
||||
2026/01/27-22:05:20.858414 1 kCustomCompressionA0 supported: 0
|
||||
2026/01/27-22:05:20.858415 1 kCustomCompressionC6 supported: 0
|
||||
2026/01/27-22:05:20.858415 1 kCustomCompression9D supported: 0
|
||||
2026/01/27-22:05:20.858415 1 kCustomCompression8B supported: 0
|
||||
2026/01/27-22:05:20.858416 1 kCustomCompressionA8 supported: 0
|
||||
2026/01/27-22:05:20.858416 1 kCustomCompression8D supported: 0
|
||||
2026/01/27-22:05:20.858417 1 kCustomCompression97 supported: 0
|
||||
2026/01/27-22:05:20.858417 1 kCustomCompression98 supported: 0
|
||||
2026/01/27-22:05:20.858418 1 kCustomCompressionAC supported: 0
|
||||
2026/01/27-22:05:20.858418 1 kCustomCompressionE9 supported: 0
|
||||
2026/01/27-22:05:20.858419 1 kCustomCompression96 supported: 0
|
||||
2026/01/27-22:05:20.858419 1 kCustomCompressionB1 supported: 0
|
||||
2026/01/27-22:05:20.858420 1 kCustomCompression95 supported: 0
|
||||
2026/01/27-22:05:20.858420 1 kCustomCompression84 supported: 0
|
||||
2026/01/27-22:05:20.858421 1 kCustomCompression91 supported: 0
|
||||
2026/01/27-22:05:20.858421 1 kCustomCompressionAB supported: 0
|
||||
2026/01/27-22:05:20.858422 1 kCustomCompressionB3 supported: 0
|
||||
2026/01/27-22:05:20.858422 1 kCustomCompression81 supported: 0
|
||||
2026/01/27-22:05:20.858423 1 kCustomCompressionDC supported: 0
|
||||
2026/01/27-22:05:20.858423 1 kBZip2Compression supported: 0
|
||||
2026/01/27-22:05:20.858424 1 kCustomCompressionBB supported: 0
|
||||
2026/01/27-22:05:20.858424 1 kCustomCompression9C supported: 0
|
||||
2026/01/27-22:05:20.858425 1 kCustomCompressionC9 supported: 0
|
||||
2026/01/27-22:05:20.858425 1 kCustomCompressionCC supported: 0
|
||||
2026/01/27-22:05:20.858426 1 kCustomCompression92 supported: 0
|
||||
2026/01/27-22:05:20.858426 1 kCustomCompressionB9 supported: 0
|
||||
2026/01/27-22:05:20.858427 1 kCustomCompression8F supported: 0
|
||||
2026/01/27-22:05:20.858427 1 kCustomCompression8A supported: 0
|
||||
2026/01/27-22:05:20.858427 1 kCustomCompression9B supported: 0
|
||||
2026/01/27-22:05:20.858428 1 kZSTD supported: 1
|
||||
2026/01/27-22:05:20.858428 1 kCustomCompressionAA supported: 0
|
||||
2026/01/27-22:05:20.858429 1 kCustomCompressionA2 supported: 0
|
||||
2026/01/27-22:05:20.858429 1 kZlibCompression supported: 1
|
||||
2026/01/27-22:05:20.858430 1 kXpressCompression supported: 0
|
||||
2026/01/27-22:05:20.858431 1 kCustomCompressionFD supported: 0
|
||||
2026/01/27-22:05:20.858431 1 kCustomCompressionE2 supported: 0
|
||||
2026/01/27-22:05:20.858432 1 kLZ4HCCompression supported: 1
|
||||
2026/01/27-22:05:20.858432 1 kCustomCompressionA6 supported: 0
|
||||
2026/01/27-22:05:20.858433 1 kCustomCompression85 supported: 0
|
||||
2026/01/27-22:05:20.858433 1 kCustomCompressionA4 supported: 0
|
||||
2026/01/27-22:05:20.858434 1 kCustomCompression86 supported: 0
|
||||
2026/01/27-22:05:20.858434 1 kCustomCompression83 supported: 0
|
||||
2026/01/27-22:05:20.858434 1 kCustomCompression87 supported: 0
|
||||
2026/01/27-22:05:20.858435 1 kCustomCompression89 supported: 0
|
||||
2026/01/27-22:05:20.858435 1 kCustomCompression8C supported: 0
|
||||
2026/01/27-22:05:20.858436 1 kCustomCompressionDB supported: 0
|
||||
2026/01/27-22:05:20.858436 1 kCustomCompressionF3 supported: 0
|
||||
2026/01/27-22:05:20.858437 1 kCustomCompressionE6 supported: 0
|
||||
2026/01/27-22:05:20.858437 1 kCustomCompression8E supported: 0
|
||||
2026/01/27-22:05:20.858438 1 kCustomCompressionDA supported: 0
|
||||
2026/01/27-22:05:20.858438 1 kCustomCompression93 supported: 0
|
||||
2026/01/27-22:05:20.858439 1 kCustomCompression94 supported: 0
|
||||
2026/01/27-22:05:20.858440 1 kCustomCompression9E supported: 0
|
||||
2026/01/27-22:05:20.858441 1 kCustomCompressionB4 supported: 0
|
||||
2026/01/27-22:05:20.858441 1 kCustomCompressionFB supported: 0
|
||||
2026/01/27-22:05:20.858442 1 kCustomCompressionB5 supported: 0
|
||||
2026/01/27-22:05:20.858442 1 kCustomCompressionD5 supported: 0
|
||||
2026/01/27-22:05:20.858443 1 kCustomCompressionB8 supported: 0
|
||||
2026/01/27-22:05:20.858443 1 kCustomCompressionD1 supported: 0
|
||||
2026/01/27-22:05:20.858444 1 kCustomCompressionBA supported: 0
|
||||
2026/01/27-22:05:20.858444 1 kCustomCompressionBC supported: 0
|
||||
2026/01/27-22:05:20.858445 1 kCustomCompressionCE supported: 0
|
||||
2026/01/27-22:05:20.858445 1 kCustomCompressionBD supported: 0
|
||||
2026/01/27-22:05:20.858445 1 kCustomCompressionC4 supported: 0
|
||||
2026/01/27-22:05:20.858446 1 kCustomCompression9A supported: 0
|
||||
2026/01/27-22:05:20.858446 1 kCustomCompression99 supported: 0
|
||||
2026/01/27-22:05:20.858447 1 kCustomCompressionBE supported: 0
|
||||
2026/01/27-22:05:20.858447 1 kCustomCompressionE5 supported: 0
|
||||
2026/01/27-22:05:20.858448 1 kCustomCompressionD9 supported: 0
|
||||
2026/01/27-22:05:20.858448 1 kCustomCompressionC1 supported: 0
|
||||
2026/01/27-22:05:20.858449 1 kCustomCompressionC5 supported: 0
|
||||
2026/01/27-22:05:20.858449 1 kCustomCompressionC2 supported: 0
|
||||
2026/01/27-22:05:20.858450 1 kCustomCompressionA5 supported: 0
|
||||
2026/01/27-22:05:20.858450 1 kCustomCompressionC7 supported: 0
|
||||
2026/01/27-22:05:20.858451 1 kCustomCompressionBF supported: 0
|
||||
2026/01/27-22:05:20.858451 1 kCustomCompressionE8 supported: 0
|
||||
2026/01/27-22:05:20.858452 1 kCustomCompressionC8 supported: 0
|
||||
2026/01/27-22:05:20.858452 1 kCustomCompressionAF supported: 0
|
||||
2026/01/27-22:05:20.858453 1 kCustomCompressionCA supported: 0
|
||||
2026/01/27-22:05:20.858453 1 kCustomCompressionCD supported: 0
|
||||
2026/01/27-22:05:20.858454 1 kCustomCompressionC0 supported: 0
|
||||
2026/01/27-22:05:20.858454 1 kCustomCompressionCF supported: 0
|
||||
2026/01/27-22:05:20.858455 1 kCustomCompressionF9 supported: 0
|
||||
2026/01/27-22:05:20.858455 1 kCustomCompressionD0 supported: 0
|
||||
2026/01/27-22:05:20.858456 1 kCustomCompressionD2 supported: 0
|
||||
2026/01/27-22:05:20.858456 1 kCustomCompressionAD supported: 0
|
||||
2026/01/27-22:05:20.858457 1 kCustomCompressionD3 supported: 0
|
||||
2026/01/27-22:05:20.858457 1 kCustomCompressionD4 supported: 0
|
||||
2026/01/27-22:05:20.858458 1 kCustomCompressionD7 supported: 0
|
||||
2026/01/27-22:05:20.858458 1 kCustomCompression82 supported: 0
|
||||
2026/01/27-22:05:20.858459 1 kCustomCompressionDD supported: 0
|
||||
2026/01/27-22:05:20.858459 1 kCustomCompressionC3 supported: 0
|
||||
2026/01/27-22:05:20.858459 1 kCustomCompressionEE supported: 0
|
||||
2026/01/27-22:05:20.858460 1 kCustomCompressionDE supported: 0
|
||||
2026/01/27-22:05:20.858460 1 kCustomCompressionDF supported: 0
|
||||
2026/01/27-22:05:20.858461 1 kCustomCompressionA7 supported: 0
|
||||
2026/01/27-22:05:20.858461 1 kCustomCompressionE0 supported: 0
|
||||
2026/01/27-22:05:20.858462 1 kCustomCompressionF1 supported: 0
|
||||
2026/01/27-22:05:20.858462 1 kCustomCompressionE1 supported: 0
|
||||
2026/01/27-22:05:20.858463 1 kCustomCompressionF5 supported: 0
|
||||
2026/01/27-22:05:20.858463 1 kCustomCompression80 supported: 0
|
||||
2026/01/27-22:05:20.858464 1 kCustomCompressionE3 supported: 0
|
||||
2026/01/27-22:05:20.858464 1 kCustomCompressionE4 supported: 0
|
||||
2026/01/27-22:05:20.858465 1 kCustomCompressionB0 supported: 0
|
||||
2026/01/27-22:05:20.858465 1 kCustomCompressionEA supported: 0
|
||||
2026/01/27-22:05:20.858466 1 kCustomCompressionFA supported: 0
|
||||
2026/01/27-22:05:20.858466 1 kCustomCompressionE7 supported: 0
|
||||
2026/01/27-22:05:20.858467 1 kCustomCompressionAE supported: 0
|
||||
2026/01/27-22:05:20.858467 1 kCustomCompressionEB supported: 0
|
||||
2026/01/27-22:05:20.858468 1 kCustomCompressionED supported: 0
|
||||
2026/01/27-22:05:20.858468 1 kCustomCompressionB6 supported: 0
|
||||
2026/01/27-22:05:20.858469 1 kCustomCompressionEF supported: 0
|
||||
2026/01/27-22:05:20.858469 1 kCustomCompressionF0 supported: 0
|
||||
2026/01/27-22:05:20.858470 1 kCustomCompressionB7 supported: 0
|
||||
2026/01/27-22:05:20.858470 1 kCustomCompressionF2 supported: 0
|
||||
2026/01/27-22:05:20.858470 1 kCustomCompressionA1 supported: 0
|
||||
2026/01/27-22:05:20.858471 1 kCustomCompressionF4 supported: 0
|
||||
2026/01/27-22:05:20.858472 1 kSnappyCompression supported: 1
|
||||
2026/01/27-22:05:20.858472 1 kCustomCompressionF6 supported: 0
|
||||
2026/01/27-22:05:20.858476 1 Fast CRC32 supported: Supported on Arm64
|
||||
2026/01/27-22:05:20.858477 1 DMutex implementation: pthread_mutex_t
|
||||
2026/01/27-22:05:20.858477 1 Jemalloc supported: 1
|
||||
2026/01/27-22:05:20.858838 1 [db/version_set.cc:6190] Recovering from manifest file: /data/db/MANIFEST-000016
|
||||
2026/01/27-22:05:20.858987 1 [db/column_family.cc:693] --------------- Options for column family [default]:
|
||||
2026/01/27-22:05:20.858989 1 Options.comparator: leveldb.BytewiseComparator
|
||||
2026/01/27-22:05:20.858990 1 Options.merge_operator: None
|
||||
2026/01/27-22:05:20.858990 1 Options.compaction_filter: None
|
||||
2026/01/27-22:05:20.858991 1 Options.compaction_filter_factory: None
|
||||
2026/01/27-22:05:20.858991 1 Options.sst_partitioner_factory: None
|
||||
2026/01/27-22:05:20.858992 1 Options.memtable_factory: SkipListFactory
|
||||
2026/01/27-22:05:20.858992 1 Options.table_factory: BlockBasedTable
|
||||
2026/01/27-22:05:20.859006 1 table_factory options: flush_block_policy_factory: FlushBlockBySizePolicyFactory (0xffff822d15a0)
|
||||
cache_index_and_filter_blocks: 0
|
||||
cache_index_and_filter_blocks_with_high_priority: 1
|
||||
pin_l0_filter_and_index_blocks_in_cache: 0
|
||||
pin_top_level_index_and_filter: 1
|
||||
index_type: 0
|
||||
data_block_index_type: 0
|
||||
index_shortening: 1
|
||||
data_block_hash_table_util_ratio: 0.750000
|
||||
checksum: 4
|
||||
no_block_cache: 0
|
||||
block_cache: 0xffff8226bd90
|
||||
block_cache_name: LRUCache
|
||||
block_cache_options:
|
||||
capacity : 33554432
|
||||
num_shard_bits : 6
|
||||
strict_capacity_limit : 0
|
||||
memory_allocator : None
|
||||
high_pri_pool_ratio: 0.500
|
||||
low_pri_pool_ratio: 0.000
|
||||
persistent_cache: (nil)
|
||||
block_size: 4096
|
||||
block_size_deviation: 10
|
||||
block_restart_interval: 16
|
||||
index_block_restart_interval: 1
|
||||
metadata_block_size: 4096
|
||||
partition_filters: 0
|
||||
use_delta_encoding: 1
|
||||
filter_policy: nullptr
|
||||
user_defined_index_factory: nullptr
|
||||
fail_if_no_udi_on_open: 0
|
||||
whole_key_filtering: 1
|
||||
verify_compression: 0
|
||||
read_amp_bytes_per_bit: 0
|
||||
format_version: 6
|
||||
enable_index_compression: 1
|
||||
block_align: 0
|
||||
max_auto_readahead_size: 262144
|
||||
prepopulate_block_cache: 0
|
||||
initial_auto_readahead_size: 8192
|
||||
num_file_reads_for_auto_readahead: 2
|
||||
2026/01/27-22:05:20.859009 1 Options.write_buffer_size: 67108864
|
||||
2026/01/27-22:05:20.859009 1 Options.max_write_buffer_number: 4
|
||||
2026/01/27-22:05:20.859010 1 Options.compression[0]: NoCompression
|
||||
2026/01/27-22:05:20.859011 1 Options.compression[1]: NoCompression
|
||||
2026/01/27-22:05:20.859011 1 Options.compression[2]: Snappy
|
||||
2026/01/27-22:05:20.859012 1 Options.compression[3]: Snappy
|
||||
2026/01/27-22:05:20.859012 1 Options.compression[4]: Snappy
|
||||
2026/01/27-22:05:20.859013 1 Options.compression[5]: Snappy
|
||||
2026/01/27-22:05:20.859013 1 Options.compression[6]: Snappy
|
||||
2026/01/27-22:05:20.859014 1 Options.bottommost_compression: Disabled
|
||||
2026/01/27-22:05:20.859015 1 Options.prefix_extractor: nullptr
|
||||
2026/01/27-22:05:20.859015 1 Options.memtable_insert_with_hint_prefix_extractor: nullptr
|
||||
2026/01/27-22:05:20.859016 1 Options.num_levels: 7
|
||||
2026/01/27-22:05:20.859016 1 Options.min_write_buffer_number_to_merge: 1
|
||||
2026/01/27-22:05:20.859017 1 Options.max_write_buffer_size_to_maintain: 0
|
||||
2026/01/27-22:05:20.859017 1 Options.bottommost_compression_opts.window_bits: -14
|
||||
2026/01/27-22:05:20.859018 1 Options.bottommost_compression_opts.level: 32767
|
||||
2026/01/27-22:05:20.859018 1 Options.bottommost_compression_opts.strategy: 0
|
||||
2026/01/27-22:05:20.859019 1 Options.bottommost_compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-22:05:20.859019 1 Options.bottommost_compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-22:05:20.859020 1 Options.bottommost_compression_opts.parallel_threads: 1
|
||||
2026/01/27-22:05:20.859020 1 Options.bottommost_compression_opts.enabled: false
|
||||
2026/01/27-22:05:20.859021 1 Options.bottommost_compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-22:05:20.859021 1 Options.bottommost_compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-22:05:20.859022 1 Options.compression_opts.window_bits: -14
|
||||
2026/01/27-22:05:20.859022 1 Options.compression_opts.level: 32767
|
||||
2026/01/27-22:05:20.859023 1 Options.compression_opts.strategy: 0
|
||||
2026/01/27-22:05:20.859023 1 Options.compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-22:05:20.859024 1 Options.compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-22:05:20.859024 1 Options.compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-22:05:20.859025 1 Options.compression_opts.parallel_threads: 1
|
||||
2026/01/27-22:05:20.859025 1 Options.compression_opts.enabled: false
|
||||
2026/01/27-22:05:20.859026 1 Options.compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-22:05:20.859026 1 Options.level0_file_num_compaction_trigger: 4
|
||||
2026/01/27-22:05:20.859026 1 Options.level0_slowdown_writes_trigger: 20
|
||||
2026/01/27-22:05:20.859027 1 Options.level0_stop_writes_trigger: 40
|
||||
2026/01/27-22:05:20.859027 1 Options.target_file_size_base: 134217728
|
||||
2026/01/27-22:05:20.859028 1 Options.target_file_size_multiplier: 1
|
||||
2026/01/27-22:05:20.859028 1 Options.max_bytes_for_level_base: 268435456
|
||||
2026/01/27-22:05:20.859029 1 Options.level_compaction_dynamic_level_bytes: 1
|
||||
2026/01/27-22:05:20.859030 1 Options.max_bytes_for_level_multiplier: 10.000000
|
||||
2026/01/27-22:05:20.859030 1 Options.max_bytes_for_level_multiplier_addtl[0]: 1
|
||||
2026/01/27-22:05:20.859031 1 Options.max_bytes_for_level_multiplier_addtl[1]: 1
|
||||
2026/01/27-22:05:20.859031 1 Options.max_bytes_for_level_multiplier_addtl[2]: 1
|
||||
2026/01/27-22:05:20.859032 1 Options.max_bytes_for_level_multiplier_addtl[3]: 1
|
||||
2026/01/27-22:05:20.859032 1 Options.max_bytes_for_level_multiplier_addtl[4]: 1
|
||||
2026/01/27-22:05:20.859033 1 Options.max_bytes_for_level_multiplier_addtl[5]: 1
|
||||
2026/01/27-22:05:20.859033 1 Options.max_bytes_for_level_multiplier_addtl[6]: 1
|
||||
2026/01/27-22:05:20.859034 1 Options.max_sequential_skip_in_iterations: 8
|
||||
2026/01/27-22:05:20.859034 1 Options.memtable_op_scan_flush_trigger: 0
|
||||
2026/01/27-22:05:20.859035 1 Options.memtable_avg_op_scan_flush_trigger: 0
|
||||
2026/01/27-22:05:20.859035 1 Options.max_compaction_bytes: 3355443200
|
||||
2026/01/27-22:05:20.859036 1 Options.arena_block_size: 1048576
|
||||
2026/01/27-22:05:20.859036 1 Options.soft_pending_compaction_bytes_limit: 68719476736
|
||||
2026/01/27-22:05:20.859037 1 Options.hard_pending_compaction_bytes_limit: 274877906944
|
||||
2026/01/27-22:05:20.859037 1 Options.disable_auto_compactions: 0
|
||||
2026/01/27-22:05:20.859038 1 Options.compaction_style: kCompactionStyleLevel
|
||||
2026/01/27-22:05:20.859038 1 Options.compaction_pri: kMinOverlappingRatio
|
||||
2026/01/27-22:05:20.859039 1 Options.compaction_options_universal.size_ratio: 1
|
||||
2026/01/27-22:05:20.859039 1 Options.compaction_options_universal.min_merge_width: 2
|
||||
2026/01/27-22:05:20.859040 1 Options.compaction_options_universal.max_merge_width: 4294967295
|
||||
2026/01/27-22:05:20.859040 1 Options.compaction_options_universal.max_size_amplification_percent: 200
|
||||
2026/01/27-22:05:20.859041 1 Options.compaction_options_universal.compression_size_percent: -1
|
||||
2026/01/27-22:05:20.859042 1 Options.compaction_options_universal.stop_style: kCompactionStopStyleTotalSize
|
||||
2026/01/27-22:05:20.859042 1 Options.compaction_options_universal.max_read_amp: -1
|
||||
2026/01/27-22:05:20.859043 1 Options.compaction_options_universal.reduce_file_locking: 0
|
||||
2026/01/27-22:05:20.859043 1 Options.compaction_options_fifo.max_table_files_size: 1073741824
|
||||
2026/01/27-22:05:20.859043 1 Options.compaction_options_fifo.allow_compaction: 0
|
||||
2026/01/27-22:05:20.859044 1 Options.table_properties_collectors:
|
||||
2026/01/27-22:05:20.859045 1 Options.inplace_update_support: 0
|
||||
2026/01/27-22:05:20.859046 1 Options.inplace_update_num_locks: 10000
|
||||
2026/01/27-22:05:20.859046 1 Options.memtable_prefix_bloom_size_ratio: 0.000000
|
||||
2026/01/27-22:05:20.859049 1 Options.memtable_whole_key_filtering: 0
|
||||
2026/01/27-22:05:20.859049 1 Options.memtable_huge_page_size: 0
|
||||
2026/01/27-22:05:20.859050 1 Options.bloom_locality: 0
|
||||
2026/01/27-22:05:20.859050 1 Options.max_successive_merges: 0
|
||||
2026/01/27-22:05:20.859051 1 Options.strict_max_successive_merges: 0
|
||||
2026/01/27-22:05:20.859051 1 Options.optimize_filters_for_hits: 0
|
||||
2026/01/27-22:05:20.859052 1 Options.paranoid_file_checks: 0
|
||||
2026/01/27-22:05:20.859052 1 Options.force_consistency_checks: 1
|
||||
2026/01/27-22:05:20.859053 1 Options.report_bg_io_stats: 0
|
||||
2026/01/27-22:05:20.859053 1 Options.disallow_memtable_writes: 0
|
||||
2026/01/27-22:05:20.859054 1 Options.ttl: 2592000
|
||||
2026/01/27-22:05:20.859054 1 Options.periodic_compaction_seconds: 0
|
||||
2026/01/27-22:05:20.859055 1 Options.default_temperature: kUnknown
|
||||
2026/01/27-22:05:20.859055 1 Options.preclude_last_level_data_seconds: 0
|
||||
2026/01/27-22:05:20.859056 1 Options.preserve_internal_time_seconds: 0
|
||||
2026/01/27-22:05:20.859056 1 Options.enable_blob_files: false
|
||||
2026/01/27-22:05:20.859057 1 Options.min_blob_size: 0
|
||||
2026/01/27-22:05:20.859058 1 Options.blob_file_size: 268435456
|
||||
2026/01/27-22:05:20.859058 1 Options.blob_compression_type: NoCompression
|
||||
2026/01/27-22:05:20.859059 1 Options.enable_blob_garbage_collection: false
|
||||
2026/01/27-22:05:20.859059 1 Options.blob_garbage_collection_age_cutoff: 0.250000
|
||||
2026/01/27-22:05:20.859060 1 Options.blob_garbage_collection_force_threshold: 1.000000
|
||||
2026/01/27-22:05:20.859060 1 Options.blob_compaction_readahead_size: 0
|
||||
2026/01/27-22:05:20.859061 1 Options.blob_file_starting_level: 0
|
||||
2026/01/27-22:05:20.859061 1 Options.experimental_mempurge_threshold: 0.000000
|
||||
2026/01/27-22:05:20.859062 1 Options.memtable_max_range_deletions: 0
|
||||
2026/01/27-22:05:20.859062 1 Options.cf_allow_ingest_behind: false
|
||||
2026/01/27-22:05:20.859382 1 [WARN] [db/db_impl/db_impl_open.cc:2688] DB::Open() failed: Invalid argument: Column families not opened: index, search, stream, propagate, pubsub, zset_score, metadata
|
||||
2026/01/27-22:05:20.859417 1 [db/db_impl/db_impl.cc:467] Shutdown: canceling all background work
|
||||
2026/01/27-22:05:20.859456 1 [db/db_impl/db_impl.cc:681] Shutdown complete
|
||||
1456
local-dev/data/kvrocks/db/LOG.old.1769552965239568
Normal file
1456
local-dev/data/kvrocks/db/LOG.old.1769552965239568
Normal file
File diff suppressed because it is too large
Load Diff
391
local-dev/data/kvrocks/db/LOG.old.1769552965242971
Normal file
391
local-dev/data/kvrocks/db/LOG.old.1769552965242971
Normal file
@@ -0,0 +1,391 @@
|
||||
2026/01/27-22:29:25.240257 1 RocksDB version: 10.6.2
|
||||
2026/01/27-22:29:25.240319 1 Git sha 0
|
||||
2026/01/27-22:29:25.240320 1 Compile date 2025-11-08 14:59:16
|
||||
2026/01/27-22:29:25.240321 1 DB SUMMARY
|
||||
2026/01/27-22:29:25.240322 1 Host name (Env): 5872b7ec3319
|
||||
2026/01/27-22:29:25.240323 1 DB Session ID: NHD5FTT50V3BVJ0V5F19
|
||||
2026/01/27-22:29:25.240426 1 CURRENT file: CURRENT
|
||||
2026/01/27-22:29:25.240428 1 IDENTITY file: IDENTITY
|
||||
2026/01/27-22:29:25.240430 1 MANIFEST file: MANIFEST-000020 size: 941 Bytes
|
||||
2026/01/27-22:29:25.240432 1 SST files in /data/db dir, Total Num: 1, files: 000014.sst
|
||||
2026/01/27-22:29:25.240433 1 Write Ahead Log file in /data/db: 000019.log size: 101968 ;
|
||||
2026/01/27-22:29:25.240437 1 Options.error_if_exists: 0
|
||||
2026/01/27-22:29:25.240437 1 Options.create_if_missing: 1
|
||||
2026/01/27-22:29:25.240438 1 Options.paranoid_checks: 1
|
||||
2026/01/27-22:29:25.240438 1 Options.flush_verify_memtable_count: 1
|
||||
2026/01/27-22:29:25.240439 1 Options.compaction_verify_record_count: 1
|
||||
2026/01/27-22:29:25.240440 1 Options.track_and_verify_wals_in_manifest: 0
|
||||
2026/01/27-22:29:25.240440 1 Options.track_and_verify_wals: 0
|
||||
2026/01/27-22:29:25.240441 1 Options.verify_sst_unique_id_in_manifest: 1
|
||||
2026/01/27-22:29:25.240441 1 Options.env: 0xffff8981d0c0
|
||||
2026/01/27-22:29:25.240442 1 Options.fs: PosixFileSystem
|
||||
2026/01/27-22:29:25.240442 1 Options.info_log: 0xffff8990f600
|
||||
2026/01/27-22:29:25.240443 1 Options.max_file_opening_threads: 16
|
||||
2026/01/27-22:29:25.240443 1 Options.statistics: 0xffff898135b0
|
||||
2026/01/27-22:29:25.240444 1 Options.statistics stats level: 3
|
||||
2026/01/27-22:29:25.240444 1 Options.use_fsync: 0
|
||||
2026/01/27-22:29:25.240445 1 Options.max_log_file_size: 268435456
|
||||
2026/01/27-22:29:25.240446 1 Options.max_manifest_file_size: 67108864
|
||||
2026/01/27-22:29:25.240448 1 Options.log_file_time_to_roll: 0
|
||||
2026/01/27-22:29:25.240449 1 Options.keep_log_file_num: 12
|
||||
2026/01/27-22:29:25.240450 1 Options.recycle_log_file_num: 0
|
||||
2026/01/27-22:29:25.240450 1 Options.allow_fallocate: 1
|
||||
2026/01/27-22:29:25.240451 1 Options.allow_mmap_reads: 0
|
||||
2026/01/27-22:29:25.240451 1 Options.allow_mmap_writes: 0
|
||||
2026/01/27-22:29:25.240452 1 Options.use_direct_reads: 0
|
||||
2026/01/27-22:29:25.240453 1 Options.use_direct_io_for_flush_and_compaction: 0
|
||||
2026/01/27-22:29:25.240453 1 Options.create_missing_column_families: 1
|
||||
2026/01/27-22:29:25.240454 1 Options.db_log_dir:
|
||||
2026/01/27-22:29:25.240455 1 Options.wal_dir:
|
||||
2026/01/27-22:29:25.240455 1 Options.table_cache_numshardbits: 6
|
||||
2026/01/27-22:29:25.240456 1 Options.WAL_ttl_seconds: 10800
|
||||
2026/01/27-22:29:25.240456 1 Options.WAL_size_limit_MB: 16384
|
||||
2026/01/27-22:29:25.240457 1 Options.max_write_batch_group_size_bytes: 1048576
|
||||
2026/01/27-22:29:25.240458 1 Options.manifest_preallocation_size: 4194304
|
||||
2026/01/27-22:29:25.240458 1 Options.is_fd_close_on_exec: 1
|
||||
2026/01/27-22:29:25.240459 1 Options.advise_random_on_open: 1
|
||||
2026/01/27-22:29:25.240460 1 Options.db_write_buffer_size: 0
|
||||
2026/01/27-22:29:25.240460 1 Options.write_buffer_manager: 0xffff8986bfc0
|
||||
2026/01/27-22:29:25.240461 1 Options.use_adaptive_mutex: 0
|
||||
2026/01/27-22:29:25.240461 1 Options.rate_limiter: 0xffff89836000
|
||||
2026/01/27-22:29:25.240462 1 Options.sst_file_manager.rate_bytes_per_sec: 0
|
||||
2026/01/27-22:29:25.240465 1 Options.wal_recovery_mode: 2
|
||||
2026/01/27-22:29:25.240466 1 Options.enable_thread_tracking: 0
|
||||
2026/01/27-22:29:25.240467 1 Options.enable_pipelined_write: 0
|
||||
2026/01/27-22:29:25.240468 1 Options.unordered_write: 0
|
||||
2026/01/27-22:29:25.240468 1 Options.allow_concurrent_memtable_write: 1
|
||||
2026/01/27-22:29:25.240469 1 Options.enable_write_thread_adaptive_yield: 1
|
||||
2026/01/27-22:29:25.240469 1 Options.write_thread_max_yield_usec: 100
|
||||
2026/01/27-22:29:25.240470 1 Options.write_thread_slow_yield_usec: 3
|
||||
2026/01/27-22:29:25.240471 1 Options.row_cache: None
|
||||
2026/01/27-22:29:25.240471 1 Options.wal_filter: None
|
||||
2026/01/27-22:29:25.240472 1 Options.avoid_flush_during_recovery: 0
|
||||
2026/01/27-22:29:25.240472 1 Options.allow_ingest_behind: 0
|
||||
2026/01/27-22:29:25.240473 1 Options.two_write_queues: 0
|
||||
2026/01/27-22:29:25.240474 1 Options.manual_wal_flush: 0
|
||||
2026/01/27-22:29:25.240474 1 Options.wal_compression: 0
|
||||
2026/01/27-22:29:25.240475 1 Options.background_close_inactive_wals: 0
|
||||
2026/01/27-22:29:25.240475 1 Options.atomic_flush: 0
|
||||
2026/01/27-22:29:25.240476 1 Options.avoid_unnecessary_blocking_io: 1
|
||||
2026/01/27-22:29:25.240476 1 Options.prefix_seek_opt_in_only: 0
|
||||
2026/01/27-22:29:25.240477 1 Options.persist_stats_to_disk: 0
|
||||
2026/01/27-22:29:25.240480 1 Options.write_dbid_to_manifest: 1
|
||||
2026/01/27-22:29:25.240481 1 Options.write_identity_file: 1
|
||||
2026/01/27-22:29:25.240481 1 Options.log_readahead_size: 0
|
||||
2026/01/27-22:29:25.240482 1 Options.file_checksum_gen_factory: Unknown
|
||||
2026/01/27-22:29:25.240483 1 Options.best_efforts_recovery: 0
|
||||
2026/01/27-22:29:25.240483 1 Options.max_bgerror_resume_count: 2147483647
|
||||
2026/01/27-22:29:25.240484 1 Options.bgerror_resume_retry_interval: 1000000
|
||||
2026/01/27-22:29:25.240484 1 Options.allow_data_in_errors: 0
|
||||
2026/01/27-22:29:25.240485 1 Options.db_host_id: __hostname__
|
||||
2026/01/27-22:29:25.240485 1 Options.enforce_single_del_contracts: true
|
||||
2026/01/27-22:29:25.240486 1 Options.metadata_write_temperature: kUnknown
|
||||
2026/01/27-22:29:25.240487 1 Options.wal_write_temperature: kUnknown
|
||||
2026/01/27-22:29:25.240487 1 Options.max_background_jobs: 4
|
||||
2026/01/27-22:29:25.240488 1 Options.max_background_compactions: -1
|
||||
2026/01/27-22:29:25.240488 1 Options.max_subcompactions: 2
|
||||
2026/01/27-22:29:25.240489 1 Options.avoid_flush_during_shutdown: 0
|
||||
2026/01/27-22:29:25.240489 1 Options.writable_file_max_buffer_size: 1048576
|
||||
2026/01/27-22:29:25.240492 1 Options.delayed_write_rate : 536870912000
|
||||
2026/01/27-22:29:25.240493 1 Options.max_total_wal_size: 536870912
|
||||
2026/01/27-22:29:25.240494 1 Options.delete_obsolete_files_period_micros: 21600000000
|
||||
2026/01/27-22:29:25.240494 1 Options.stats_dump_period_sec: 0
|
||||
2026/01/27-22:29:25.240495 1 Options.stats_persist_period_sec: 600
|
||||
2026/01/27-22:29:25.240495 1 Options.stats_history_buffer_size: 1048576
|
||||
2026/01/27-22:29:25.240496 1 Options.max_open_files: 8096
|
||||
2026/01/27-22:29:25.240496 1 Options.bytes_per_sync: 1048576
|
||||
2026/01/27-22:29:25.240497 1 Options.wal_bytes_per_sync: 0
|
||||
2026/01/27-22:29:25.240498 1 Options.strict_bytes_per_sync: 0
|
||||
2026/01/27-22:29:25.240498 1 Options.compaction_readahead_size: 2097152
|
||||
2026/01/27-22:29:25.240499 1 Options.max_background_flushes: -1
|
||||
2026/01/27-22:29:25.240499 1 Options.daily_offpeak_time_utc:
|
||||
2026/01/27-22:29:25.240500 1 Compression algorithms supported:
|
||||
2026/01/27-22:29:25.240500 1 kCustomCompressionFE supported: 0
|
||||
2026/01/27-22:29:25.240501 1 kCustomCompressionFC supported: 0
|
||||
2026/01/27-22:29:25.240502 1 kCustomCompressionF8 supported: 0
|
||||
2026/01/27-22:29:25.240502 1 kCustomCompressionF7 supported: 0
|
||||
2026/01/27-22:29:25.240503 1 kCustomCompressionB2 supported: 0
|
||||
2026/01/27-22:29:25.240503 1 kLZ4Compression supported: 1
|
||||
2026/01/27-22:29:25.240506 1 kCustomCompression88 supported: 0
|
||||
2026/01/27-22:29:25.240506 1 kCustomCompressionD8 supported: 0
|
||||
2026/01/27-22:29:25.240507 1 kCustomCompression9F supported: 0
|
||||
2026/01/27-22:29:25.240507 1 kCustomCompressionD6 supported: 0
|
||||
2026/01/27-22:29:25.240508 1 kCustomCompressionA9 supported: 0
|
||||
2026/01/27-22:29:25.240508 1 kCustomCompressionEC supported: 0
|
||||
2026/01/27-22:29:25.240509 1 kCustomCompressionA3 supported: 0
|
||||
2026/01/27-22:29:25.240509 1 kCustomCompressionCB supported: 0
|
||||
2026/01/27-22:29:25.240510 1 kCustomCompression90 supported: 0
|
||||
2026/01/27-22:29:25.240510 1 kCustomCompressionA0 supported: 0
|
||||
2026/01/27-22:29:25.240511 1 kCustomCompressionC6 supported: 0
|
||||
2026/01/27-22:29:25.240513 1 kCustomCompression9D supported: 0
|
||||
2026/01/27-22:29:25.240514 1 kCustomCompression8B supported: 0
|
||||
2026/01/27-22:29:25.240514 1 kCustomCompressionA8 supported: 0
|
||||
2026/01/27-22:29:25.240515 1 kCustomCompression8D supported: 0
|
||||
2026/01/27-22:29:25.240515 1 kCustomCompression97 supported: 0
|
||||
2026/01/27-22:29:25.240516 1 kCustomCompression98 supported: 0
|
||||
2026/01/27-22:29:25.240516 1 kCustomCompressionAC supported: 0
|
||||
2026/01/27-22:29:25.240517 1 kCustomCompressionE9 supported: 0
|
||||
2026/01/27-22:29:25.240517 1 kCustomCompression96 supported: 0
|
||||
2026/01/27-22:29:25.240518 1 kCustomCompressionB1 supported: 0
|
||||
2026/01/27-22:29:25.240518 1 kCustomCompression95 supported: 0
|
||||
2026/01/27-22:29:25.240519 1 kCustomCompression84 supported: 0
|
||||
2026/01/27-22:29:25.240519 1 kCustomCompression91 supported: 0
|
||||
2026/01/27-22:29:25.240520 1 kCustomCompressionAB supported: 0
|
||||
2026/01/27-22:29:25.240520 1 kCustomCompressionB3 supported: 0
|
||||
2026/01/27-22:29:25.240521 1 kCustomCompression81 supported: 0
|
||||
2026/01/27-22:29:25.240521 1 kCustomCompressionDC supported: 0
|
||||
2026/01/27-22:29:25.240522 1 kBZip2Compression supported: 0
|
||||
2026/01/27-22:29:25.240523 1 kCustomCompressionBB supported: 0
|
||||
2026/01/27-22:29:25.240523 1 kCustomCompression9C supported: 0
|
||||
2026/01/27-22:29:25.240524 1 kCustomCompressionC9 supported: 0
|
||||
2026/01/27-22:29:25.240525 1 kCustomCompressionCC supported: 0
|
||||
2026/01/27-22:29:25.240525 1 kCustomCompression92 supported: 0
|
||||
2026/01/27-22:29:25.240526 1 kCustomCompressionB9 supported: 0
|
||||
2026/01/27-22:29:25.240526 1 kCustomCompression8F supported: 0
|
||||
2026/01/27-22:29:25.240527 1 kCustomCompression8A supported: 0
|
||||
2026/01/27-22:29:25.240528 1 kCustomCompression9B supported: 0
|
||||
2026/01/27-22:29:25.240528 1 kZSTD supported: 1
|
||||
2026/01/27-22:29:25.240529 1 kCustomCompressionAA supported: 0
|
||||
2026/01/27-22:29:25.240529 1 kCustomCompressionA2 supported: 0
|
||||
2026/01/27-22:29:25.240530 1 kZlibCompression supported: 1
|
||||
2026/01/27-22:29:25.240531 1 kXpressCompression supported: 0
|
||||
2026/01/27-22:29:25.240532 1 kCustomCompressionFD supported: 0
|
||||
2026/01/27-22:29:25.240532 1 kCustomCompressionE2 supported: 0
|
||||
2026/01/27-22:29:25.240533 1 kLZ4HCCompression supported: 1
|
||||
2026/01/27-22:29:25.240534 1 kCustomCompressionA6 supported: 0
|
||||
2026/01/27-22:29:25.240534 1 kCustomCompression85 supported: 0
|
||||
2026/01/27-22:29:25.240535 1 kCustomCompressionA4 supported: 0
|
||||
2026/01/27-22:29:25.240536 1 kCustomCompression86 supported: 0
|
||||
2026/01/27-22:29:25.240536 1 kCustomCompression83 supported: 0
|
||||
2026/01/27-22:29:25.240537 1 kCustomCompression87 supported: 0
|
||||
2026/01/27-22:29:25.240537 1 kCustomCompression89 supported: 0
|
||||
2026/01/27-22:29:25.240538 1 kCustomCompression8C supported: 0
|
||||
2026/01/27-22:29:25.240539 1 kCustomCompressionDB supported: 0
|
||||
2026/01/27-22:29:25.240539 1 kCustomCompressionF3 supported: 0
|
||||
2026/01/27-22:29:25.240540 1 kCustomCompressionE6 supported: 0
|
||||
2026/01/27-22:29:25.240541 1 kCustomCompression8E supported: 0
|
||||
2026/01/27-22:29:25.240541 1 kCustomCompressionDA supported: 0
|
||||
2026/01/27-22:29:25.240542 1 kCustomCompression93 supported: 0
|
||||
2026/01/27-22:29:25.240542 1 kCustomCompression94 supported: 0
|
||||
2026/01/27-22:29:25.240545 1 kCustomCompression9E supported: 0
|
||||
2026/01/27-22:29:25.240545 1 kCustomCompressionB4 supported: 0
|
||||
2026/01/27-22:29:25.240546 1 kCustomCompressionFB supported: 0
|
||||
2026/01/27-22:29:25.240547 1 kCustomCompressionB5 supported: 0
|
||||
2026/01/27-22:29:25.240547 1 kCustomCompressionD5 supported: 0
|
||||
2026/01/27-22:29:25.240548 1 kCustomCompressionB8 supported: 0
|
||||
2026/01/27-22:29:25.240548 1 kCustomCompressionD1 supported: 0
|
||||
2026/01/27-22:29:25.240549 1 kCustomCompressionBA supported: 0
|
||||
2026/01/27-22:29:25.240550 1 kCustomCompressionBC supported: 0
|
||||
2026/01/27-22:29:25.240550 1 kCustomCompressionCE supported: 0
|
||||
2026/01/27-22:29:25.240551 1 kCustomCompressionBD supported: 0
|
||||
2026/01/27-22:29:25.240551 1 kCustomCompressionC4 supported: 0
|
||||
2026/01/27-22:29:25.240552 1 kCustomCompression9A supported: 0
|
||||
2026/01/27-22:29:25.240553 1 kCustomCompression99 supported: 0
|
||||
2026/01/27-22:29:25.240553 1 kCustomCompressionBE supported: 0
|
||||
2026/01/27-22:29:25.240554 1 kCustomCompressionE5 supported: 0
|
||||
2026/01/27-22:29:25.240554 1 kCustomCompressionD9 supported: 0
|
||||
2026/01/27-22:29:25.240555 1 kCustomCompressionC1 supported: 0
|
||||
2026/01/27-22:29:25.240555 1 kCustomCompressionC5 supported: 0
|
||||
2026/01/27-22:29:25.240556 1 kCustomCompressionC2 supported: 0
|
||||
2026/01/27-22:29:25.240557 1 kCustomCompressionA5 supported: 0
|
||||
2026/01/27-22:29:25.240557 1 kCustomCompressionC7 supported: 0
|
||||
2026/01/27-22:29:25.240558 1 kCustomCompressionBF supported: 0
|
||||
2026/01/27-22:29:25.240558 1 kCustomCompressionE8 supported: 0
|
||||
2026/01/27-22:29:25.240559 1 kCustomCompressionC8 supported: 0
|
||||
2026/01/27-22:29:25.240559 1 kCustomCompressionAF supported: 0
|
||||
2026/01/27-22:29:25.240560 1 kCustomCompressionCA supported: 0
|
||||
2026/01/27-22:29:25.240561 1 kCustomCompressionCD supported: 0
|
||||
2026/01/27-22:29:25.240561 1 kCustomCompressionC0 supported: 0
|
||||
2026/01/27-22:29:25.240562 1 kCustomCompressionCF supported: 0
|
||||
2026/01/27-22:29:25.240562 1 kCustomCompressionF9 supported: 0
|
||||
2026/01/27-22:29:25.240563 1 kCustomCompressionD0 supported: 0
|
||||
2026/01/27-22:29:25.240564 1 kCustomCompressionD2 supported: 0
|
||||
2026/01/27-22:29:25.240564 1 kCustomCompressionAD supported: 0
|
||||
2026/01/27-22:29:25.240565 1 kCustomCompressionD3 supported: 0
|
||||
2026/01/27-22:29:25.240566 1 kCustomCompressionD4 supported: 0
|
||||
2026/01/27-22:29:25.240566 1 kCustomCompressionD7 supported: 0
|
||||
2026/01/27-22:29:25.240567 1 kCustomCompression82 supported: 0
|
||||
2026/01/27-22:29:25.240567 1 kCustomCompressionDD supported: 0
|
||||
2026/01/27-22:29:25.240568 1 kCustomCompressionC3 supported: 0
|
||||
2026/01/27-22:29:25.240568 1 kCustomCompressionEE supported: 0
|
||||
2026/01/27-22:29:25.240569 1 kCustomCompressionDE supported: 0
|
||||
2026/01/27-22:29:25.240569 1 kCustomCompressionDF supported: 0
|
||||
2026/01/27-22:29:25.240570 1 kCustomCompressionA7 supported: 0
|
||||
2026/01/27-22:29:25.240570 1 kCustomCompressionE0 supported: 0
|
||||
2026/01/27-22:29:25.240570 1 kCustomCompressionF1 supported: 0
|
||||
2026/01/27-22:29:25.240571 1 kCustomCompressionE1 supported: 0
|
||||
2026/01/27-22:29:25.240571 1 kCustomCompressionF5 supported: 0
|
||||
2026/01/27-22:29:25.240572 1 kCustomCompression80 supported: 0
|
||||
2026/01/27-22:29:25.240572 1 kCustomCompressionE3 supported: 0
|
||||
2026/01/27-22:29:25.240573 1 kCustomCompressionE4 supported: 0
|
||||
2026/01/27-22:29:25.240573 1 kCustomCompressionB0 supported: 0
|
||||
2026/01/27-22:29:25.240574 1 kCustomCompressionEA supported: 0
|
||||
2026/01/27-22:29:25.240574 1 kCustomCompressionFA supported: 0
|
||||
2026/01/27-22:29:25.240575 1 kCustomCompressionE7 supported: 0
|
||||
2026/01/27-22:29:25.240575 1 kCustomCompressionAE supported: 0
|
||||
2026/01/27-22:29:25.240576 1 kCustomCompressionEB supported: 0
|
||||
2026/01/27-22:29:25.240576 1 kCustomCompressionED supported: 0
|
||||
2026/01/27-22:29:25.240577 1 kCustomCompressionB6 supported: 0
|
||||
2026/01/27-22:29:25.240579 1 kCustomCompressionEF supported: 0
|
||||
2026/01/27-22:29:25.240580 1 kCustomCompressionF0 supported: 0
|
||||
2026/01/27-22:29:25.240580 1 kCustomCompressionB7 supported: 0
|
||||
2026/01/27-22:29:25.240581 1 kCustomCompressionF2 supported: 0
|
||||
2026/01/27-22:29:25.240581 1 kCustomCompressionA1 supported: 0
|
||||
2026/01/27-22:29:25.240582 1 kCustomCompressionF4 supported: 0
|
||||
2026/01/27-22:29:25.240582 1 kSnappyCompression supported: 1
|
||||
2026/01/27-22:29:25.240583 1 kCustomCompressionF6 supported: 0
|
||||
2026/01/27-22:29:25.240584 1 Fast CRC32 supported: Supported on Arm64
|
||||
2026/01/27-22:29:25.240584 1 DMutex implementation: pthread_mutex_t
|
||||
2026/01/27-22:29:25.240585 1 Jemalloc supported: 1
|
||||
2026/01/27-22:29:25.241055 1 [db/version_set.cc:6190] Recovering from manifest file: /data/db/MANIFEST-000020
|
||||
2026/01/27-22:29:25.241240 1 [db/column_family.cc:693] --------------- Options for column family [default]:
|
||||
2026/01/27-22:29:25.241243 1 Options.comparator: leveldb.BytewiseComparator
|
||||
2026/01/27-22:29:25.241244 1 Options.merge_operator: None
|
||||
2026/01/27-22:29:25.241245 1 Options.compaction_filter: None
|
||||
2026/01/27-22:29:25.241246 1 Options.compaction_filter_factory: None
|
||||
2026/01/27-22:29:25.241246 1 Options.sst_partitioner_factory: None
|
||||
2026/01/27-22:29:25.241247 1 Options.memtable_factory: SkipListFactory
|
||||
2026/01/27-22:29:25.241248 1 Options.table_factory: BlockBasedTable
|
||||
2026/01/27-22:29:25.241278 1 table_factory options: flush_block_policy_factory: FlushBlockBySizePolicyFactory (0xffff898d15a0)
|
||||
cache_index_and_filter_blocks: 0
|
||||
cache_index_and_filter_blocks_with_high_priority: 1
|
||||
pin_l0_filter_and_index_blocks_in_cache: 0
|
||||
pin_top_level_index_and_filter: 1
|
||||
index_type: 0
|
||||
data_block_index_type: 0
|
||||
index_shortening: 1
|
||||
data_block_hash_table_util_ratio: 0.750000
|
||||
checksum: 4
|
||||
no_block_cache: 0
|
||||
block_cache: 0xffff8986bd90
|
||||
block_cache_name: LRUCache
|
||||
block_cache_options:
|
||||
capacity : 33554432
|
||||
num_shard_bits : 6
|
||||
strict_capacity_limit : 0
|
||||
memory_allocator : None
|
||||
high_pri_pool_ratio: 0.500
|
||||
low_pri_pool_ratio: 0.000
|
||||
persistent_cache: (nil)
|
||||
block_size: 4096
|
||||
block_size_deviation: 10
|
||||
block_restart_interval: 16
|
||||
index_block_restart_interval: 1
|
||||
metadata_block_size: 4096
|
||||
partition_filters: 0
|
||||
use_delta_encoding: 1
|
||||
filter_policy: nullptr
|
||||
user_defined_index_factory: nullptr
|
||||
fail_if_no_udi_on_open: 0
|
||||
whole_key_filtering: 1
|
||||
verify_compression: 0
|
||||
read_amp_bytes_per_bit: 0
|
||||
format_version: 6
|
||||
enable_index_compression: 1
|
||||
block_align: 0
|
||||
max_auto_readahead_size: 262144
|
||||
prepopulate_block_cache: 0
|
||||
initial_auto_readahead_size: 8192
|
||||
num_file_reads_for_auto_readahead: 2
|
||||
2026/01/27-22:29:25.241289 1 Options.write_buffer_size: 67108864
|
||||
2026/01/27-22:29:25.241289 1 Options.max_write_buffer_number: 4
|
||||
2026/01/27-22:29:25.241291 1 Options.compression[0]: NoCompression
|
||||
2026/01/27-22:29:25.241291 1 Options.compression[1]: NoCompression
|
||||
2026/01/27-22:29:25.241292 1 Options.compression[2]: Snappy
|
||||
2026/01/27-22:29:25.241292 1 Options.compression[3]: Snappy
|
||||
2026/01/27-22:29:25.241293 1 Options.compression[4]: Snappy
|
||||
2026/01/27-22:29:25.241293 1 Options.compression[5]: Snappy
|
||||
2026/01/27-22:29:25.241294 1 Options.compression[6]: Snappy
|
||||
2026/01/27-22:29:25.241294 1 Options.bottommost_compression: Disabled
|
||||
2026/01/27-22:29:25.241295 1 Options.prefix_extractor: nullptr
|
||||
2026/01/27-22:29:25.241295 1 Options.memtable_insert_with_hint_prefix_extractor: nullptr
|
||||
2026/01/27-22:29:25.241296 1 Options.num_levels: 7
|
||||
2026/01/27-22:29:25.241296 1 Options.min_write_buffer_number_to_merge: 1
|
||||
2026/01/27-22:29:25.241297 1 Options.max_write_buffer_size_to_maintain: 0
|
||||
2026/01/27-22:29:25.241297 1 Options.bottommost_compression_opts.window_bits: -14
|
||||
2026/01/27-22:29:25.241298 1 Options.bottommost_compression_opts.level: 32767
|
||||
2026/01/27-22:29:25.241298 1 Options.bottommost_compression_opts.strategy: 0
|
||||
2026/01/27-22:29:25.241299 1 Options.bottommost_compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-22:29:25.241299 1 Options.bottommost_compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-22:29:25.241300 1 Options.bottommost_compression_opts.parallel_threads: 1
|
||||
2026/01/27-22:29:25.241301 1 Options.bottommost_compression_opts.enabled: false
|
||||
2026/01/27-22:29:25.241301 1 Options.bottommost_compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-22:29:25.241302 1 Options.bottommost_compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-22:29:25.241302 1 Options.compression_opts.window_bits: -14
|
||||
2026/01/27-22:29:25.241303 1 Options.compression_opts.level: 32767
|
||||
2026/01/27-22:29:25.241303 1 Options.compression_opts.strategy: 0
|
||||
2026/01/27-22:29:25.241304 1 Options.compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-22:29:25.241304 1 Options.compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-22:29:25.241304 1 Options.compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-22:29:25.241305 1 Options.compression_opts.parallel_threads: 1
|
||||
2026/01/27-22:29:25.241305 1 Options.compression_opts.enabled: false
|
||||
2026/01/27-22:29:25.241306 1 Options.compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-22:29:25.241306 1 Options.level0_file_num_compaction_trigger: 4
|
||||
2026/01/27-22:29:25.241307 1 Options.level0_slowdown_writes_trigger: 20
|
||||
2026/01/27-22:29:25.241307 1 Options.level0_stop_writes_trigger: 40
|
||||
2026/01/27-22:29:25.241308 1 Options.target_file_size_base: 134217728
|
||||
2026/01/27-22:29:25.241308 1 Options.target_file_size_multiplier: 1
|
||||
2026/01/27-22:29:25.241309 1 Options.max_bytes_for_level_base: 268435456
|
||||
2026/01/27-22:29:25.241309 1 Options.level_compaction_dynamic_level_bytes: 1
|
||||
2026/01/27-22:29:25.241310 1 Options.max_bytes_for_level_multiplier: 10.000000
|
||||
2026/01/27-22:29:25.241311 1 Options.max_bytes_for_level_multiplier_addtl[0]: 1
|
||||
2026/01/27-22:29:25.241311 1 Options.max_bytes_for_level_multiplier_addtl[1]: 1
|
||||
2026/01/27-22:29:25.241312 1 Options.max_bytes_for_level_multiplier_addtl[2]: 1
|
||||
2026/01/27-22:29:25.241312 1 Options.max_bytes_for_level_multiplier_addtl[3]: 1
|
||||
2026/01/27-22:29:25.241313 1 Options.max_bytes_for_level_multiplier_addtl[4]: 1
|
||||
2026/01/27-22:29:25.241313 1 Options.max_bytes_for_level_multiplier_addtl[5]: 1
|
||||
2026/01/27-22:29:25.241314 1 Options.max_bytes_for_level_multiplier_addtl[6]: 1
|
||||
2026/01/27-22:29:25.241314 1 Options.max_sequential_skip_in_iterations: 8
|
||||
2026/01/27-22:29:25.241315 1 Options.memtable_op_scan_flush_trigger: 0
|
||||
2026/01/27-22:29:25.241315 1 Options.memtable_avg_op_scan_flush_trigger: 0
|
||||
2026/01/27-22:29:25.241316 1 Options.max_compaction_bytes: 3355443200
|
||||
2026/01/27-22:29:25.241316 1 Options.arena_block_size: 1048576
|
||||
2026/01/27-22:29:25.241317 1 Options.soft_pending_compaction_bytes_limit: 68719476736
|
||||
2026/01/27-22:29:25.241317 1 Options.hard_pending_compaction_bytes_limit: 274877906944
|
||||
2026/01/27-22:29:25.241318 1 Options.disable_auto_compactions: 0
|
||||
2026/01/27-22:29:25.241318 1 Options.compaction_style: kCompactionStyleLevel
|
||||
2026/01/27-22:29:25.241319 1 Options.compaction_pri: kMinOverlappingRatio
|
||||
2026/01/27-22:29:25.241319 1 Options.compaction_options_universal.size_ratio: 1
|
||||
2026/01/27-22:29:25.241320 1 Options.compaction_options_universal.min_merge_width: 2
|
||||
2026/01/27-22:29:25.241320 1 Options.compaction_options_universal.max_merge_width: 4294967295
|
||||
2026/01/27-22:29:25.241321 1 Options.compaction_options_universal.max_size_amplification_percent: 200
|
||||
2026/01/27-22:29:25.241322 1 Options.compaction_options_universal.compression_size_percent: -1
|
||||
2026/01/27-22:29:25.241322 1 Options.compaction_options_universal.stop_style: kCompactionStopStyleTotalSize
|
||||
2026/01/27-22:29:25.241323 1 Options.compaction_options_universal.max_read_amp: -1
|
||||
2026/01/27-22:29:25.241323 1 Options.compaction_options_universal.reduce_file_locking: 0
|
||||
2026/01/27-22:29:25.241324 1 Options.compaction_options_fifo.max_table_files_size: 1073741824
|
||||
2026/01/27-22:29:25.241324 1 Options.compaction_options_fifo.allow_compaction: 0
|
||||
2026/01/27-22:29:25.241326 1 Options.table_properties_collectors:
|
||||
2026/01/27-22:29:25.241327 1 Options.inplace_update_support: 0
|
||||
2026/01/27-22:29:25.241327 1 Options.inplace_update_num_locks: 10000
|
||||
2026/01/27-22:29:25.241328 1 Options.memtable_prefix_bloom_size_ratio: 0.000000
|
||||
2026/01/27-22:29:25.241329 1 Options.memtable_whole_key_filtering: 0
|
||||
2026/01/27-22:29:25.241329 1 Options.memtable_huge_page_size: 0
|
||||
2026/01/27-22:29:25.241329 1 Options.bloom_locality: 0
|
||||
2026/01/27-22:29:25.241330 1 Options.max_successive_merges: 0
|
||||
2026/01/27-22:29:25.241330 1 Options.strict_max_successive_merges: 0
|
||||
2026/01/27-22:29:25.241331 1 Options.optimize_filters_for_hits: 0
|
||||
2026/01/27-22:29:25.241331 1 Options.paranoid_file_checks: 0
|
||||
2026/01/27-22:29:25.241332 1 Options.force_consistency_checks: 1
|
||||
2026/01/27-22:29:25.241332 1 Options.report_bg_io_stats: 0
|
||||
2026/01/27-22:29:25.241333 1 Options.disallow_memtable_writes: 0
|
||||
2026/01/27-22:29:25.241333 1 Options.ttl: 2592000
|
||||
2026/01/27-22:29:25.241334 1 Options.periodic_compaction_seconds: 0
|
||||
2026/01/27-22:29:25.241334 1 Options.default_temperature: kUnknown
|
||||
2026/01/27-22:29:25.241335 1 Options.preclude_last_level_data_seconds: 0
|
||||
2026/01/27-22:29:25.241335 1 Options.preserve_internal_time_seconds: 0
|
||||
2026/01/27-22:29:25.241336 1 Options.enable_blob_files: false
|
||||
2026/01/27-22:29:25.241336 1 Options.min_blob_size: 0
|
||||
2026/01/27-22:29:25.241337 1 Options.blob_file_size: 268435456
|
||||
2026/01/27-22:29:25.241337 1 Options.blob_compression_type: NoCompression
|
||||
2026/01/27-22:29:25.241338 1 Options.enable_blob_garbage_collection: false
|
||||
2026/01/27-22:29:25.241338 1 Options.blob_garbage_collection_age_cutoff: 0.250000
|
||||
2026/01/27-22:29:25.241339 1 Options.blob_garbage_collection_force_threshold: 1.000000
|
||||
2026/01/27-22:29:25.241340 1 Options.blob_compaction_readahead_size: 0
|
||||
2026/01/27-22:29:25.241340 1 Options.blob_file_starting_level: 0
|
||||
2026/01/27-22:29:25.241341 1 Options.experimental_mempurge_threshold: 0.000000
|
||||
2026/01/27-22:29:25.241341 1 Options.memtable_max_range_deletions: 0
|
||||
2026/01/27-22:29:25.241342 1 Options.cf_allow_ingest_behind: false
|
||||
2026/01/27-22:29:25.242680 1 [WARN] [db/db_impl/db_impl_open.cc:2688] DB::Open() failed: Invalid argument: Column families not opened: index, search, stream, propagate, pubsub, zset_score, metadata
|
||||
2026/01/27-22:29:25.242737 1 [db/db_impl/db_impl.cc:467] Shutdown: canceling all background work
|
||||
2026/01/27-22:29:25.242782 1 [db/db_impl/db_impl.cc:681] Shutdown complete
|
||||
1449
local-dev/data/kvrocks/db/LOG.old.1769552979167350
Normal file
1449
local-dev/data/kvrocks/db/LOG.old.1769552979167350
Normal file
File diff suppressed because it is too large
Load Diff
391
local-dev/data/kvrocks/db/LOG.old.1769552979169205
Normal file
391
local-dev/data/kvrocks/db/LOG.old.1769552979169205
Normal file
@@ -0,0 +1,391 @@
|
||||
2026/01/27-22:29:39.168333 1 RocksDB version: 10.6.2
|
||||
2026/01/27-22:29:39.168394 1 Git sha 0
|
||||
2026/01/27-22:29:39.168395 1 Compile date 2025-11-08 14:59:16
|
||||
2026/01/27-22:29:39.168396 1 DB SUMMARY
|
||||
2026/01/27-22:29:39.168397 1 Host name (Env): db98b7caa3d9
|
||||
2026/01/27-22:29:39.168398 1 DB Session ID: M5A8FG11TXBDE6GONB6Y
|
||||
2026/01/27-22:29:39.168483 1 CURRENT file: CURRENT
|
||||
2026/01/27-22:29:39.168484 1 IDENTITY file: IDENTITY
|
||||
2026/01/27-22:29:39.168485 1 MANIFEST file: MANIFEST-000025 size: 1092 Bytes
|
||||
2026/01/27-22:29:39.168487 1 SST files in /data/db dir, Total Num: 2, files: 000014.sst 000023.sst
|
||||
2026/01/27-22:29:39.168487 1 Write Ahead Log file in /data/db: 000024.log size: 0 ;
|
||||
2026/01/27-22:29:39.168488 1 Options.error_if_exists: 0
|
||||
2026/01/27-22:29:39.168489 1 Options.create_if_missing: 1
|
||||
2026/01/27-22:29:39.168489 1 Options.paranoid_checks: 1
|
||||
2026/01/27-22:29:39.168490 1 Options.flush_verify_memtable_count: 1
|
||||
2026/01/27-22:29:39.168490 1 Options.compaction_verify_record_count: 1
|
||||
2026/01/27-22:29:39.168491 1 Options.track_and_verify_wals_in_manifest: 0
|
||||
2026/01/27-22:29:39.168491 1 Options.track_and_verify_wals: 0
|
||||
2026/01/27-22:29:39.168492 1 Options.verify_sst_unique_id_in_manifest: 1
|
||||
2026/01/27-22:29:39.168492 1 Options.env: 0xffff9a21d0c0
|
||||
2026/01/27-22:29:39.168493 1 Options.fs: PosixFileSystem
|
||||
2026/01/27-22:29:39.168493 1 Options.info_log: 0xffff9a30f600
|
||||
2026/01/27-22:29:39.168494 1 Options.max_file_opening_threads: 16
|
||||
2026/01/27-22:29:39.168494 1 Options.statistics: 0xffff9a2135b0
|
||||
2026/01/27-22:29:39.168495 1 Options.statistics stats level: 3
|
||||
2026/01/27-22:29:39.168495 1 Options.use_fsync: 0
|
||||
2026/01/27-22:29:39.168496 1 Options.max_log_file_size: 268435456
|
||||
2026/01/27-22:29:39.168496 1 Options.max_manifest_file_size: 67108864
|
||||
2026/01/27-22:29:39.168497 1 Options.log_file_time_to_roll: 0
|
||||
2026/01/27-22:29:39.168497 1 Options.keep_log_file_num: 12
|
||||
2026/01/27-22:29:39.168498 1 Options.recycle_log_file_num: 0
|
||||
2026/01/27-22:29:39.168498 1 Options.allow_fallocate: 1
|
||||
2026/01/27-22:29:39.168499 1 Options.allow_mmap_reads: 0
|
||||
2026/01/27-22:29:39.168499 1 Options.allow_mmap_writes: 0
|
||||
2026/01/27-22:29:39.168500 1 Options.use_direct_reads: 0
|
||||
2026/01/27-22:29:39.168500 1 Options.use_direct_io_for_flush_and_compaction: 0
|
||||
2026/01/27-22:29:39.168501 1 Options.create_missing_column_families: 1
|
||||
2026/01/27-22:29:39.168501 1 Options.db_log_dir:
|
||||
2026/01/27-22:29:39.168502 1 Options.wal_dir:
|
||||
2026/01/27-22:29:39.168502 1 Options.table_cache_numshardbits: 6
|
||||
2026/01/27-22:29:39.168503 1 Options.WAL_ttl_seconds: 10800
|
||||
2026/01/27-22:29:39.168503 1 Options.WAL_size_limit_MB: 16384
|
||||
2026/01/27-22:29:39.168504 1 Options.max_write_batch_group_size_bytes: 1048576
|
||||
2026/01/27-22:29:39.168504 1 Options.manifest_preallocation_size: 4194304
|
||||
2026/01/27-22:29:39.168505 1 Options.is_fd_close_on_exec: 1
|
||||
2026/01/27-22:29:39.168505 1 Options.advise_random_on_open: 1
|
||||
2026/01/27-22:29:39.168506 1 Options.db_write_buffer_size: 0
|
||||
2026/01/27-22:29:39.168506 1 Options.write_buffer_manager: 0xffff9a26bfc0
|
||||
2026/01/27-22:29:39.168507 1 Options.use_adaptive_mutex: 0
|
||||
2026/01/27-22:29:39.168507 1 Options.rate_limiter: 0xffff9a236000
|
||||
2026/01/27-22:29:39.168508 1 Options.sst_file_manager.rate_bytes_per_sec: 0
|
||||
2026/01/27-22:29:39.168508 1 Options.wal_recovery_mode: 2
|
||||
2026/01/27-22:29:39.168509 1 Options.enable_thread_tracking: 0
|
||||
2026/01/27-22:29:39.168509 1 Options.enable_pipelined_write: 0
|
||||
2026/01/27-22:29:39.168509 1 Options.unordered_write: 0
|
||||
2026/01/27-22:29:39.168510 1 Options.allow_concurrent_memtable_write: 1
|
||||
2026/01/27-22:29:39.168510 1 Options.enable_write_thread_adaptive_yield: 1
|
||||
2026/01/27-22:29:39.168511 1 Options.write_thread_max_yield_usec: 100
|
||||
2026/01/27-22:29:39.168511 1 Options.write_thread_slow_yield_usec: 3
|
||||
2026/01/27-22:29:39.168512 1 Options.row_cache: None
|
||||
2026/01/27-22:29:39.168512 1 Options.wal_filter: None
|
||||
2026/01/27-22:29:39.168513 1 Options.avoid_flush_during_recovery: 0
|
||||
2026/01/27-22:29:39.168513 1 Options.allow_ingest_behind: 0
|
||||
2026/01/27-22:29:39.168514 1 Options.two_write_queues: 0
|
||||
2026/01/27-22:29:39.168514 1 Options.manual_wal_flush: 0
|
||||
2026/01/27-22:29:39.168515 1 Options.wal_compression: 0
|
||||
2026/01/27-22:29:39.168515 1 Options.background_close_inactive_wals: 0
|
||||
2026/01/27-22:29:39.168516 1 Options.atomic_flush: 0
|
||||
2026/01/27-22:29:39.168516 1 Options.avoid_unnecessary_blocking_io: 1
|
||||
2026/01/27-22:29:39.168516 1 Options.prefix_seek_opt_in_only: 0
|
||||
2026/01/27-22:29:39.168517 1 Options.persist_stats_to_disk: 0
|
||||
2026/01/27-22:29:39.168517 1 Options.write_dbid_to_manifest: 1
|
||||
2026/01/27-22:29:39.168518 1 Options.write_identity_file: 1
|
||||
2026/01/27-22:29:39.168518 1 Options.log_readahead_size: 0
|
||||
2026/01/27-22:29:39.168519 1 Options.file_checksum_gen_factory: Unknown
|
||||
2026/01/27-22:29:39.168519 1 Options.best_efforts_recovery: 0
|
||||
2026/01/27-22:29:39.168520 1 Options.max_bgerror_resume_count: 2147483647
|
||||
2026/01/27-22:29:39.168520 1 Options.bgerror_resume_retry_interval: 1000000
|
||||
2026/01/27-22:29:39.168521 1 Options.allow_data_in_errors: 0
|
||||
2026/01/27-22:29:39.168521 1 Options.db_host_id: __hostname__
|
||||
2026/01/27-22:29:39.168522 1 Options.enforce_single_del_contracts: true
|
||||
2026/01/27-22:29:39.168523 1 Options.metadata_write_temperature: kUnknown
|
||||
2026/01/27-22:29:39.168523 1 Options.wal_write_temperature: kUnknown
|
||||
2026/01/27-22:29:39.168523 1 Options.max_background_jobs: 4
|
||||
2026/01/27-22:29:39.168524 1 Options.max_background_compactions: -1
|
||||
2026/01/27-22:29:39.168524 1 Options.max_subcompactions: 2
|
||||
2026/01/27-22:29:39.168525 1 Options.avoid_flush_during_shutdown: 0
|
||||
2026/01/27-22:29:39.168525 1 Options.writable_file_max_buffer_size: 1048576
|
||||
2026/01/27-22:29:39.168526 1 Options.delayed_write_rate : 536870912000
|
||||
2026/01/27-22:29:39.168527 1 Options.max_total_wal_size: 536870912
|
||||
2026/01/27-22:29:39.168528 1 Options.delete_obsolete_files_period_micros: 21600000000
|
||||
2026/01/27-22:29:39.168528 1 Options.stats_dump_period_sec: 0
|
||||
2026/01/27-22:29:39.168528 1 Options.stats_persist_period_sec: 600
|
||||
2026/01/27-22:29:39.168529 1 Options.stats_history_buffer_size: 1048576
|
||||
2026/01/27-22:29:39.168530 1 Options.max_open_files: 8096
|
||||
2026/01/27-22:29:39.168530 1 Options.bytes_per_sync: 1048576
|
||||
2026/01/27-22:29:39.168531 1 Options.wal_bytes_per_sync: 0
|
||||
2026/01/27-22:29:39.168531 1 Options.strict_bytes_per_sync: 0
|
||||
2026/01/27-22:29:39.168532 1 Options.compaction_readahead_size: 2097152
|
||||
2026/01/27-22:29:39.168532 1 Options.max_background_flushes: -1
|
||||
2026/01/27-22:29:39.168533 1 Options.daily_offpeak_time_utc:
|
||||
2026/01/27-22:29:39.168533 1 Compression algorithms supported:
|
||||
2026/01/27-22:29:39.168534 1 kCustomCompressionFE supported: 0
|
||||
2026/01/27-22:29:39.168534 1 kCustomCompressionFC supported: 0
|
||||
2026/01/27-22:29:39.168535 1 kCustomCompressionF8 supported: 0
|
||||
2026/01/27-22:29:39.168535 1 kCustomCompressionF7 supported: 0
|
||||
2026/01/27-22:29:39.168536 1 kCustomCompressionB2 supported: 0
|
||||
2026/01/27-22:29:39.168537 1 kLZ4Compression supported: 1
|
||||
2026/01/27-22:29:39.168537 1 kCustomCompression88 supported: 0
|
||||
2026/01/27-22:29:39.168538 1 kCustomCompressionD8 supported: 0
|
||||
2026/01/27-22:29:39.168538 1 kCustomCompression9F supported: 0
|
||||
2026/01/27-22:29:39.168539 1 kCustomCompressionD6 supported: 0
|
||||
2026/01/27-22:29:39.168539 1 kCustomCompressionA9 supported: 0
|
||||
2026/01/27-22:29:39.168540 1 kCustomCompressionEC supported: 0
|
||||
2026/01/27-22:29:39.168540 1 kCustomCompressionA3 supported: 0
|
||||
2026/01/27-22:29:39.168541 1 kCustomCompressionCB supported: 0
|
||||
2026/01/27-22:29:39.168541 1 kCustomCompression90 supported: 0
|
||||
2026/01/27-22:29:39.168542 1 kCustomCompressionA0 supported: 0
|
||||
2026/01/27-22:29:39.168542 1 kCustomCompressionC6 supported: 0
|
||||
2026/01/27-22:29:39.168543 1 kCustomCompression9D supported: 0
|
||||
2026/01/27-22:29:39.168543 1 kCustomCompression8B supported: 0
|
||||
2026/01/27-22:29:39.168544 1 kCustomCompressionA8 supported: 0
|
||||
2026/01/27-22:29:39.168544 1 kCustomCompression8D supported: 0
|
||||
2026/01/27-22:29:39.168545 1 kCustomCompression97 supported: 0
|
||||
2026/01/27-22:29:39.168545 1 kCustomCompression98 supported: 0
|
||||
2026/01/27-22:29:39.168546 1 kCustomCompressionAC supported: 0
|
||||
2026/01/27-22:29:39.168546 1 kCustomCompressionE9 supported: 0
|
||||
2026/01/27-22:29:39.168547 1 kCustomCompression96 supported: 0
|
||||
2026/01/27-22:29:39.168547 1 kCustomCompressionB1 supported: 0
|
||||
2026/01/27-22:29:39.168548 1 kCustomCompression95 supported: 0
|
||||
2026/01/27-22:29:39.168548 1 kCustomCompression84 supported: 0
|
||||
2026/01/27-22:29:39.168549 1 kCustomCompression91 supported: 0
|
||||
2026/01/27-22:29:39.168549 1 kCustomCompressionAB supported: 0
|
||||
2026/01/27-22:29:39.168550 1 kCustomCompressionB3 supported: 0
|
||||
2026/01/27-22:29:39.168550 1 kCustomCompression81 supported: 0
|
||||
2026/01/27-22:29:39.168551 1 kCustomCompressionDC supported: 0
|
||||
2026/01/27-22:29:39.168551 1 kBZip2Compression supported: 0
|
||||
2026/01/27-22:29:39.168552 1 kCustomCompressionBB supported: 0
|
||||
2026/01/27-22:29:39.168552 1 kCustomCompression9C supported: 0
|
||||
2026/01/27-22:29:39.168553 1 kCustomCompressionC9 supported: 0
|
||||
2026/01/27-22:29:39.168553 1 kCustomCompressionCC supported: 0
|
||||
2026/01/27-22:29:39.168554 1 kCustomCompression92 supported: 0
|
||||
2026/01/27-22:29:39.168554 1 kCustomCompressionB9 supported: 0
|
||||
2026/01/27-22:29:39.168554 1 kCustomCompression8F supported: 0
|
||||
2026/01/27-22:29:39.168555 1 kCustomCompression8A supported: 0
|
||||
2026/01/27-22:29:39.168555 1 kCustomCompression9B supported: 0
|
||||
2026/01/27-22:29:39.168556 1 kZSTD supported: 1
|
||||
2026/01/27-22:29:39.168556 1 kCustomCompressionAA supported: 0
|
||||
2026/01/27-22:29:39.168557 1 kCustomCompressionA2 supported: 0
|
||||
2026/01/27-22:29:39.168558 1 kZlibCompression supported: 1
|
||||
2026/01/27-22:29:39.168558 1 kXpressCompression supported: 0
|
||||
2026/01/27-22:29:39.168559 1 kCustomCompressionFD supported: 0
|
||||
2026/01/27-22:29:39.168559 1 kCustomCompressionE2 supported: 0
|
||||
2026/01/27-22:29:39.168560 1 kLZ4HCCompression supported: 1
|
||||
2026/01/27-22:29:39.168560 1 kCustomCompressionA6 supported: 0
|
||||
2026/01/27-22:29:39.168561 1 kCustomCompression85 supported: 0
|
||||
2026/01/27-22:29:39.168561 1 kCustomCompressionA4 supported: 0
|
||||
2026/01/27-22:29:39.168562 1 kCustomCompression86 supported: 0
|
||||
2026/01/27-22:29:39.168562 1 kCustomCompression83 supported: 0
|
||||
2026/01/27-22:29:39.168562 1 kCustomCompression87 supported: 0
|
||||
2026/01/27-22:29:39.168563 1 kCustomCompression89 supported: 0
|
||||
2026/01/27-22:29:39.168563 1 kCustomCompression8C supported: 0
|
||||
2026/01/27-22:29:39.168564 1 kCustomCompressionDB supported: 0
|
||||
2026/01/27-22:29:39.168564 1 kCustomCompressionF3 supported: 0
|
||||
2026/01/27-22:29:39.168565 1 kCustomCompressionE6 supported: 0
|
||||
2026/01/27-22:29:39.168565 1 kCustomCompression8E supported: 0
|
||||
2026/01/27-22:29:39.168566 1 kCustomCompressionDA supported: 0
|
||||
2026/01/27-22:29:39.168566 1 kCustomCompression93 supported: 0
|
||||
2026/01/27-22:29:39.168567 1 kCustomCompression94 supported: 0
|
||||
2026/01/27-22:29:39.168570 1 kCustomCompression9E supported: 0
|
||||
2026/01/27-22:29:39.168570 1 kCustomCompressionB4 supported: 0
|
||||
2026/01/27-22:29:39.168571 1 kCustomCompressionFB supported: 0
|
||||
2026/01/27-22:29:39.168571 1 kCustomCompressionB5 supported: 0
|
||||
2026/01/27-22:29:39.168572 1 kCustomCompressionD5 supported: 0
|
||||
2026/01/27-22:29:39.168572 1 kCustomCompressionB8 supported: 0
|
||||
2026/01/27-22:29:39.168572 1 kCustomCompressionD1 supported: 0
|
||||
2026/01/27-22:29:39.168573 1 kCustomCompressionBA supported: 0
|
||||
2026/01/27-22:29:39.168573 1 kCustomCompressionBC supported: 0
|
||||
2026/01/27-22:29:39.168574 1 kCustomCompressionCE supported: 0
|
||||
2026/01/27-22:29:39.168574 1 kCustomCompressionBD supported: 0
|
||||
2026/01/27-22:29:39.168575 1 kCustomCompressionC4 supported: 0
|
||||
2026/01/27-22:29:39.168575 1 kCustomCompression9A supported: 0
|
||||
2026/01/27-22:29:39.168577 1 kCustomCompression99 supported: 0
|
||||
2026/01/27-22:29:39.168578 1 kCustomCompressionBE supported: 0
|
||||
2026/01/27-22:29:39.168578 1 kCustomCompressionE5 supported: 0
|
||||
2026/01/27-22:29:39.168579 1 kCustomCompressionD9 supported: 0
|
||||
2026/01/27-22:29:39.168579 1 kCustomCompressionC1 supported: 0
|
||||
2026/01/27-22:29:39.168580 1 kCustomCompressionC5 supported: 0
|
||||
2026/01/27-22:29:39.168580 1 kCustomCompressionC2 supported: 0
|
||||
2026/01/27-22:29:39.168581 1 kCustomCompressionA5 supported: 0
|
||||
2026/01/27-22:29:39.168581 1 kCustomCompressionC7 supported: 0
|
||||
2026/01/27-22:29:39.168581 1 kCustomCompressionBF supported: 0
|
||||
2026/01/27-22:29:39.168582 1 kCustomCompressionE8 supported: 0
|
||||
2026/01/27-22:29:39.168582 1 kCustomCompressionC8 supported: 0
|
||||
2026/01/27-22:29:39.168583 1 kCustomCompressionAF supported: 0
|
||||
2026/01/27-22:29:39.168583 1 kCustomCompressionCA supported: 0
|
||||
2026/01/27-22:29:39.168584 1 kCustomCompressionCD supported: 0
|
||||
2026/01/27-22:29:39.168584 1 kCustomCompressionC0 supported: 0
|
||||
2026/01/27-22:29:39.168585 1 kCustomCompressionCF supported: 0
|
||||
2026/01/27-22:29:39.168585 1 kCustomCompressionF9 supported: 0
|
||||
2026/01/27-22:29:39.168586 1 kCustomCompressionD0 supported: 0
|
||||
2026/01/27-22:29:39.168586 1 kCustomCompressionD2 supported: 0
|
||||
2026/01/27-22:29:39.168587 1 kCustomCompressionAD supported: 0
|
||||
2026/01/27-22:29:39.168587 1 kCustomCompressionD3 supported: 0
|
||||
2026/01/27-22:29:39.168588 1 kCustomCompressionD4 supported: 0
|
||||
2026/01/27-22:29:39.168588 1 kCustomCompressionD7 supported: 0
|
||||
2026/01/27-22:29:39.168589 1 kCustomCompression82 supported: 0
|
||||
2026/01/27-22:29:39.168589 1 kCustomCompressionDD supported: 0
|
||||
2026/01/27-22:29:39.168590 1 kCustomCompressionC3 supported: 0
|
||||
2026/01/27-22:29:39.168590 1 kCustomCompressionEE supported: 0
|
||||
2026/01/27-22:29:39.168591 1 kCustomCompressionDE supported: 0
|
||||
2026/01/27-22:29:39.168591 1 kCustomCompressionDF supported: 0
|
||||
2026/01/27-22:29:39.168592 1 kCustomCompressionA7 supported: 0
|
||||
2026/01/27-22:29:39.168592 1 kCustomCompressionE0 supported: 0
|
||||
2026/01/27-22:29:39.168593 1 kCustomCompressionF1 supported: 0
|
||||
2026/01/27-22:29:39.168593 1 kCustomCompressionE1 supported: 0
|
||||
2026/01/27-22:29:39.168594 1 kCustomCompressionF5 supported: 0
|
||||
2026/01/27-22:29:39.168594 1 kCustomCompression80 supported: 0
|
||||
2026/01/27-22:29:39.168595 1 kCustomCompressionE3 supported: 0
|
||||
2026/01/27-22:29:39.168595 1 kCustomCompressionE4 supported: 0
|
||||
2026/01/27-22:29:39.168596 1 kCustomCompressionB0 supported: 0
|
||||
2026/01/27-22:29:39.168596 1 kCustomCompressionEA supported: 0
|
||||
2026/01/27-22:29:39.168597 1 kCustomCompressionFA supported: 0
|
||||
2026/01/27-22:29:39.168597 1 kCustomCompressionE7 supported: 0
|
||||
2026/01/27-22:29:39.168598 1 kCustomCompressionAE supported: 0
|
||||
2026/01/27-22:29:39.168598 1 kCustomCompressionEB supported: 0
|
||||
2026/01/27-22:29:39.168598 1 kCustomCompressionED supported: 0
|
||||
2026/01/27-22:29:39.168599 1 kCustomCompressionB6 supported: 0
|
||||
2026/01/27-22:29:39.168599 1 kCustomCompressionEF supported: 0
|
||||
2026/01/27-22:29:39.168600 1 kCustomCompressionF0 supported: 0
|
||||
2026/01/27-22:29:39.168600 1 kCustomCompressionB7 supported: 0
|
||||
2026/01/27-22:29:39.168601 1 kCustomCompressionF2 supported: 0
|
||||
2026/01/27-22:29:39.168601 1 kCustomCompressionA1 supported: 0
|
||||
2026/01/27-22:29:39.168602 1 kCustomCompressionF4 supported: 0
|
||||
2026/01/27-22:29:39.168602 1 kSnappyCompression supported: 1
|
||||
2026/01/27-22:29:39.168603 1 kCustomCompressionF6 supported: 0
|
||||
2026/01/27-22:29:39.168604 1 Fast CRC32 supported: Supported on Arm64
|
||||
2026/01/27-22:29:39.168605 1 DMutex implementation: pthread_mutex_t
|
||||
2026/01/27-22:29:39.168605 1 Jemalloc supported: 1
|
||||
2026/01/27-22:29:39.168746 1 [db/version_set.cc:6190] Recovering from manifest file: /data/db/MANIFEST-000025
|
||||
2026/01/27-22:29:39.168844 1 [db/column_family.cc:693] --------------- Options for column family [default]:
|
||||
2026/01/27-22:29:39.168846 1 Options.comparator: leveldb.BytewiseComparator
|
||||
2026/01/27-22:29:39.168846 1 Options.merge_operator: None
|
||||
2026/01/27-22:29:39.168847 1 Options.compaction_filter: None
|
||||
2026/01/27-22:29:39.168847 1 Options.compaction_filter_factory: None
|
||||
2026/01/27-22:29:39.168848 1 Options.sst_partitioner_factory: None
|
||||
2026/01/27-22:29:39.168848 1 Options.memtable_factory: SkipListFactory
|
||||
2026/01/27-22:29:39.168849 1 Options.table_factory: BlockBasedTable
|
||||
2026/01/27-22:29:39.168863 1 table_factory options: flush_block_policy_factory: FlushBlockBySizePolicyFactory (0xffff9a2d15a0)
|
||||
cache_index_and_filter_blocks: 0
|
||||
cache_index_and_filter_blocks_with_high_priority: 1
|
||||
pin_l0_filter_and_index_blocks_in_cache: 0
|
||||
pin_top_level_index_and_filter: 1
|
||||
index_type: 0
|
||||
data_block_index_type: 0
|
||||
index_shortening: 1
|
||||
data_block_hash_table_util_ratio: 0.750000
|
||||
checksum: 4
|
||||
no_block_cache: 0
|
||||
block_cache: 0xffff9a26bd90
|
||||
block_cache_name: LRUCache
|
||||
block_cache_options:
|
||||
capacity : 33554432
|
||||
num_shard_bits : 6
|
||||
strict_capacity_limit : 0
|
||||
memory_allocator : None
|
||||
high_pri_pool_ratio: 0.500
|
||||
low_pri_pool_ratio: 0.000
|
||||
persistent_cache: (nil)
|
||||
block_size: 4096
|
||||
block_size_deviation: 10
|
||||
block_restart_interval: 16
|
||||
index_block_restart_interval: 1
|
||||
metadata_block_size: 4096
|
||||
partition_filters: 0
|
||||
use_delta_encoding: 1
|
||||
filter_policy: nullptr
|
||||
user_defined_index_factory: nullptr
|
||||
fail_if_no_udi_on_open: 0
|
||||
whole_key_filtering: 1
|
||||
verify_compression: 0
|
||||
read_amp_bytes_per_bit: 0
|
||||
format_version: 6
|
||||
enable_index_compression: 1
|
||||
block_align: 0
|
||||
max_auto_readahead_size: 262144
|
||||
prepopulate_block_cache: 0
|
||||
initial_auto_readahead_size: 8192
|
||||
num_file_reads_for_auto_readahead: 2
|
||||
2026/01/27-22:29:39.168867 1 Options.write_buffer_size: 67108864
|
||||
2026/01/27-22:29:39.168873 1 Options.max_write_buffer_number: 4
|
||||
2026/01/27-22:29:39.168874 1 Options.compression[0]: NoCompression
|
||||
2026/01/27-22:29:39.168875 1 Options.compression[1]: NoCompression
|
||||
2026/01/27-22:29:39.168875 1 Options.compression[2]: Snappy
|
||||
2026/01/27-22:29:39.168876 1 Options.compression[3]: Snappy
|
||||
2026/01/27-22:29:39.168876 1 Options.compression[4]: Snappy
|
||||
2026/01/27-22:29:39.168877 1 Options.compression[5]: Snappy
|
||||
2026/01/27-22:29:39.168877 1 Options.compression[6]: Snappy
|
||||
2026/01/27-22:29:39.168878 1 Options.bottommost_compression: Disabled
|
||||
2026/01/27-22:29:39.168878 1 Options.prefix_extractor: nullptr
|
||||
2026/01/27-22:29:39.168879 1 Options.memtable_insert_with_hint_prefix_extractor: nullptr
|
||||
2026/01/27-22:29:39.168879 1 Options.num_levels: 7
|
||||
2026/01/27-22:29:39.168880 1 Options.min_write_buffer_number_to_merge: 1
|
||||
2026/01/27-22:29:39.168880 1 Options.max_write_buffer_size_to_maintain: 0
|
||||
2026/01/27-22:29:39.168881 1 Options.bottommost_compression_opts.window_bits: -14
|
||||
2026/01/27-22:29:39.168881 1 Options.bottommost_compression_opts.level: 32767
|
||||
2026/01/27-22:29:39.168882 1 Options.bottommost_compression_opts.strategy: 0
|
||||
2026/01/27-22:29:39.168882 1 Options.bottommost_compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-22:29:39.168883 1 Options.bottommost_compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-22:29:39.168883 1 Options.bottommost_compression_opts.parallel_threads: 1
|
||||
2026/01/27-22:29:39.168884 1 Options.bottommost_compression_opts.enabled: false
|
||||
2026/01/27-22:29:39.168884 1 Options.bottommost_compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-22:29:39.168885 1 Options.bottommost_compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-22:29:39.168885 1 Options.compression_opts.window_bits: -14
|
||||
2026/01/27-22:29:39.168886 1 Options.compression_opts.level: 32767
|
||||
2026/01/27-22:29:39.168886 1 Options.compression_opts.strategy: 0
|
||||
2026/01/27-22:29:39.168887 1 Options.compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-22:29:39.168887 1 Options.compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-22:29:39.168887 1 Options.compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-22:29:39.168888 1 Options.compression_opts.parallel_threads: 1
|
||||
2026/01/27-22:29:39.168888 1 Options.compression_opts.enabled: false
|
||||
2026/01/27-22:29:39.168889 1 Options.compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-22:29:39.168889 1 Options.level0_file_num_compaction_trigger: 4
|
||||
2026/01/27-22:29:39.168890 1 Options.level0_slowdown_writes_trigger: 20
|
||||
2026/01/27-22:29:39.168890 1 Options.level0_stop_writes_trigger: 40
|
||||
2026/01/27-22:29:39.168891 1 Options.target_file_size_base: 134217728
|
||||
2026/01/27-22:29:39.168891 1 Options.target_file_size_multiplier: 1
|
||||
2026/01/27-22:29:39.168892 1 Options.max_bytes_for_level_base: 268435456
|
||||
2026/01/27-22:29:39.168892 1 Options.level_compaction_dynamic_level_bytes: 1
|
||||
2026/01/27-22:29:39.168893 1 Options.max_bytes_for_level_multiplier: 10.000000
|
||||
2026/01/27-22:29:39.168894 1 Options.max_bytes_for_level_multiplier_addtl[0]: 1
|
||||
2026/01/27-22:29:39.168895 1 Options.max_bytes_for_level_multiplier_addtl[1]: 1
|
||||
2026/01/27-22:29:39.168895 1 Options.max_bytes_for_level_multiplier_addtl[2]: 1
|
||||
2026/01/27-22:29:39.168895 1 Options.max_bytes_for_level_multiplier_addtl[3]: 1
|
||||
2026/01/27-22:29:39.168896 1 Options.max_bytes_for_level_multiplier_addtl[4]: 1
|
||||
2026/01/27-22:29:39.168896 1 Options.max_bytes_for_level_multiplier_addtl[5]: 1
|
||||
2026/01/27-22:29:39.168897 1 Options.max_bytes_for_level_multiplier_addtl[6]: 1
|
||||
2026/01/27-22:29:39.168897 1 Options.max_sequential_skip_in_iterations: 8
|
||||
2026/01/27-22:29:39.168898 1 Options.memtable_op_scan_flush_trigger: 0
|
||||
2026/01/27-22:29:39.168898 1 Options.memtable_avg_op_scan_flush_trigger: 0
|
||||
2026/01/27-22:29:39.168899 1 Options.max_compaction_bytes: 3355443200
|
||||
2026/01/27-22:29:39.168899 1 Options.arena_block_size: 1048576
|
||||
2026/01/27-22:29:39.168900 1 Options.soft_pending_compaction_bytes_limit: 68719476736
|
||||
2026/01/27-22:29:39.168900 1 Options.hard_pending_compaction_bytes_limit: 274877906944
|
||||
2026/01/27-22:29:39.168901 1 Options.disable_auto_compactions: 0
|
||||
2026/01/27-22:29:39.168901 1 Options.compaction_style: kCompactionStyleLevel
|
||||
2026/01/27-22:29:39.168902 1 Options.compaction_pri: kMinOverlappingRatio
|
||||
2026/01/27-22:29:39.168902 1 Options.compaction_options_universal.size_ratio: 1
|
||||
2026/01/27-22:29:39.168903 1 Options.compaction_options_universal.min_merge_width: 2
|
||||
2026/01/27-22:29:39.168903 1 Options.compaction_options_universal.max_merge_width: 4294967295
|
||||
2026/01/27-22:29:39.168904 1 Options.compaction_options_universal.max_size_amplification_percent: 200
|
||||
2026/01/27-22:29:39.168904 1 Options.compaction_options_universal.compression_size_percent: -1
|
||||
2026/01/27-22:29:39.168905 1 Options.compaction_options_universal.stop_style: kCompactionStopStyleTotalSize
|
||||
2026/01/27-22:29:39.168905 1 Options.compaction_options_universal.max_read_amp: -1
|
||||
2026/01/27-22:29:39.168906 1 Options.compaction_options_universal.reduce_file_locking: 0
|
||||
2026/01/27-22:29:39.168906 1 Options.compaction_options_fifo.max_table_files_size: 1073741824
|
||||
2026/01/27-22:29:39.168907 1 Options.compaction_options_fifo.allow_compaction: 0
|
||||
2026/01/27-22:29:39.168908 1 Options.table_properties_collectors:
|
||||
2026/01/27-22:29:39.168908 1 Options.inplace_update_support: 0
|
||||
2026/01/27-22:29:39.168909 1 Options.inplace_update_num_locks: 10000
|
||||
2026/01/27-22:29:39.168910 1 Options.memtable_prefix_bloom_size_ratio: 0.000000
|
||||
2026/01/27-22:29:39.168911 1 Options.memtable_whole_key_filtering: 0
|
||||
2026/01/27-22:29:39.168911 1 Options.memtable_huge_page_size: 0
|
||||
2026/01/27-22:29:39.168911 1 Options.bloom_locality: 0
|
||||
2026/01/27-22:29:39.168912 1 Options.max_successive_merges: 0
|
||||
2026/01/27-22:29:39.168912 1 Options.strict_max_successive_merges: 0
|
||||
2026/01/27-22:29:39.168913 1 Options.optimize_filters_for_hits: 0
|
||||
2026/01/27-22:29:39.168913 1 Options.paranoid_file_checks: 0
|
||||
2026/01/27-22:29:39.168914 1 Options.force_consistency_checks: 1
|
||||
2026/01/27-22:29:39.168914 1 Options.report_bg_io_stats: 0
|
||||
2026/01/27-22:29:39.168915 1 Options.disallow_memtable_writes: 0
|
||||
2026/01/27-22:29:39.168915 1 Options.ttl: 2592000
|
||||
2026/01/27-22:29:39.168916 1 Options.periodic_compaction_seconds: 0
|
||||
2026/01/27-22:29:39.168916 1 Options.default_temperature: kUnknown
|
||||
2026/01/27-22:29:39.168917 1 Options.preclude_last_level_data_seconds: 0
|
||||
2026/01/27-22:29:39.168917 1 Options.preserve_internal_time_seconds: 0
|
||||
2026/01/27-22:29:39.168918 1 Options.enable_blob_files: false
|
||||
2026/01/27-22:29:39.168918 1 Options.min_blob_size: 0
|
||||
2026/01/27-22:29:39.168919 1 Options.blob_file_size: 268435456
|
||||
2026/01/27-22:29:39.168919 1 Options.blob_compression_type: NoCompression
|
||||
2026/01/27-22:29:39.168920 1 Options.enable_blob_garbage_collection: false
|
||||
2026/01/27-22:29:39.168920 1 Options.blob_garbage_collection_age_cutoff: 0.250000
|
||||
2026/01/27-22:29:39.168921 1 Options.blob_garbage_collection_force_threshold: 1.000000
|
||||
2026/01/27-22:29:39.168921 1 Options.blob_compaction_readahead_size: 0
|
||||
2026/01/27-22:29:39.168922 1 Options.blob_file_starting_level: 0
|
||||
2026/01/27-22:29:39.168922 1 Options.experimental_mempurge_threshold: 0.000000
|
||||
2026/01/27-22:29:39.168923 1 Options.memtable_max_range_deletions: 0
|
||||
2026/01/27-22:29:39.168923 1 Options.cf_allow_ingest_behind: false
|
||||
2026/01/27-22:29:39.168977 1 [WARN] [db/db_impl/db_impl_open.cc:2688] DB::Open() failed: Invalid argument: Column families not opened: index, search, stream, propagate, pubsub, zset_score, metadata
|
||||
2026/01/27-22:29:39.169006 1 [db/db_impl/db_impl.cc:467] Shutdown: canceling all background work
|
||||
2026/01/27-22:29:39.169031 1 [db/db_impl/db_impl.cc:681] Shutdown complete
|
||||
1468
local-dev/data/kvrocks/db/LOG.old.1769556178501276
Normal file
1468
local-dev/data/kvrocks/db/LOG.old.1769556178501276
Normal file
File diff suppressed because it is too large
Load Diff
391
local-dev/data/kvrocks/db/LOG.old.1769556178503672
Normal file
391
local-dev/data/kvrocks/db/LOG.old.1769556178503672
Normal file
@@ -0,0 +1,391 @@
|
||||
2026/01/27-23:22:58.502039 1 RocksDB version: 10.6.2
|
||||
2026/01/27-23:22:58.502469 1 Git sha 0
|
||||
2026/01/27-23:22:58.502470 1 Compile date 2025-11-08 14:59:16
|
||||
2026/01/27-23:22:58.502472 1 DB SUMMARY
|
||||
2026/01/27-23:22:58.502473 1 Host name (Env): aae070ac4a88
|
||||
2026/01/27-23:22:58.502474 1 DB Session ID: SI1XGUJ3JZXKO7QMB6X0
|
||||
2026/01/27-23:22:58.502586 1 CURRENT file: CURRENT
|
||||
2026/01/27-23:22:58.502587 1 IDENTITY file: IDENTITY
|
||||
2026/01/27-23:22:58.502589 1 MANIFEST file: MANIFEST-000029 size: 1104 Bytes
|
||||
2026/01/27-23:22:58.502590 1 SST files in /data/db dir, Total Num: 2, files: 000014.sst 000023.sst
|
||||
2026/01/27-23:22:58.502591 1 Write Ahead Log file in /data/db: 000028.log size: 86377 ;
|
||||
2026/01/27-23:22:58.502592 1 Options.error_if_exists: 0
|
||||
2026/01/27-23:22:58.502593 1 Options.create_if_missing: 1
|
||||
2026/01/27-23:22:58.502593 1 Options.paranoid_checks: 1
|
||||
2026/01/27-23:22:58.502594 1 Options.flush_verify_memtable_count: 1
|
||||
2026/01/27-23:22:58.502595 1 Options.compaction_verify_record_count: 1
|
||||
2026/01/27-23:22:58.502595 1 Options.track_and_verify_wals_in_manifest: 0
|
||||
2026/01/27-23:22:58.502596 1 Options.track_and_verify_wals: 0
|
||||
2026/01/27-23:22:58.502596 1 Options.verify_sst_unique_id_in_manifest: 1
|
||||
2026/01/27-23:22:58.502597 1 Options.env: 0xffff9341d0c0
|
||||
2026/01/27-23:22:58.502598 1 Options.fs: PosixFileSystem
|
||||
2026/01/27-23:22:58.502599 1 Options.info_log: 0xffff9350f600
|
||||
2026/01/27-23:22:58.502599 1 Options.max_file_opening_threads: 16
|
||||
2026/01/27-23:22:58.502600 1 Options.statistics: 0xffff934135b0
|
||||
2026/01/27-23:22:58.502600 1 Options.statistics stats level: 3
|
||||
2026/01/27-23:22:58.502601 1 Options.use_fsync: 0
|
||||
2026/01/27-23:22:58.502602 1 Options.max_log_file_size: 268435456
|
||||
2026/01/27-23:22:58.502602 1 Options.max_manifest_file_size: 67108864
|
||||
2026/01/27-23:22:58.502603 1 Options.log_file_time_to_roll: 0
|
||||
2026/01/27-23:22:58.502603 1 Options.keep_log_file_num: 12
|
||||
2026/01/27-23:22:58.502604 1 Options.recycle_log_file_num: 0
|
||||
2026/01/27-23:22:58.502605 1 Options.allow_fallocate: 1
|
||||
2026/01/27-23:22:58.502605 1 Options.allow_mmap_reads: 0
|
||||
2026/01/27-23:22:58.502606 1 Options.allow_mmap_writes: 0
|
||||
2026/01/27-23:22:58.502606 1 Options.use_direct_reads: 0
|
||||
2026/01/27-23:22:58.502607 1 Options.use_direct_io_for_flush_and_compaction: 0
|
||||
2026/01/27-23:22:58.502607 1 Options.create_missing_column_families: 1
|
||||
2026/01/27-23:22:58.502608 1 Options.db_log_dir:
|
||||
2026/01/27-23:22:58.502609 1 Options.wal_dir:
|
||||
2026/01/27-23:22:58.502609 1 Options.table_cache_numshardbits: 6
|
||||
2026/01/27-23:22:58.502610 1 Options.WAL_ttl_seconds: 10800
|
||||
2026/01/27-23:22:58.502611 1 Options.WAL_size_limit_MB: 16384
|
||||
2026/01/27-23:22:58.502611 1 Options.max_write_batch_group_size_bytes: 1048576
|
||||
2026/01/27-23:22:58.502612 1 Options.manifest_preallocation_size: 4194304
|
||||
2026/01/27-23:22:58.502612 1 Options.is_fd_close_on_exec: 1
|
||||
2026/01/27-23:22:58.502613 1 Options.advise_random_on_open: 1
|
||||
2026/01/27-23:22:58.502614 1 Options.db_write_buffer_size: 0
|
||||
2026/01/27-23:22:58.502614 1 Options.write_buffer_manager: 0xffff9346bfc0
|
||||
2026/01/27-23:22:58.502615 1 Options.use_adaptive_mutex: 0
|
||||
2026/01/27-23:22:58.502615 1 Options.rate_limiter: 0xffff93436000
|
||||
2026/01/27-23:22:58.502616 1 Options.sst_file_manager.rate_bytes_per_sec: 0
|
||||
2026/01/27-23:22:58.502617 1 Options.wal_recovery_mode: 2
|
||||
2026/01/27-23:22:58.502617 1 Options.enable_thread_tracking: 0
|
||||
2026/01/27-23:22:58.502618 1 Options.enable_pipelined_write: 0
|
||||
2026/01/27-23:22:58.502618 1 Options.unordered_write: 0
|
||||
2026/01/27-23:22:58.502619 1 Options.allow_concurrent_memtable_write: 1
|
||||
2026/01/27-23:22:58.502619 1 Options.enable_write_thread_adaptive_yield: 1
|
||||
2026/01/27-23:22:58.502620 1 Options.write_thread_max_yield_usec: 100
|
||||
2026/01/27-23:22:58.502621 1 Options.write_thread_slow_yield_usec: 3
|
||||
2026/01/27-23:22:58.502621 1 Options.row_cache: None
|
||||
2026/01/27-23:22:58.502622 1 Options.wal_filter: None
|
||||
2026/01/27-23:22:58.502622 1 Options.avoid_flush_during_recovery: 0
|
||||
2026/01/27-23:22:58.502623 1 Options.allow_ingest_behind: 0
|
||||
2026/01/27-23:22:58.502623 1 Options.two_write_queues: 0
|
||||
2026/01/27-23:22:58.502624 1 Options.manual_wal_flush: 0
|
||||
2026/01/27-23:22:58.502625 1 Options.wal_compression: 0
|
||||
2026/01/27-23:22:58.502625 1 Options.background_close_inactive_wals: 0
|
||||
2026/01/27-23:22:58.502626 1 Options.atomic_flush: 0
|
||||
2026/01/27-23:22:58.502626 1 Options.avoid_unnecessary_blocking_io: 1
|
||||
2026/01/27-23:22:58.502627 1 Options.prefix_seek_opt_in_only: 0
|
||||
2026/01/27-23:22:58.502628 1 Options.persist_stats_to_disk: 0
|
||||
2026/01/27-23:22:58.502628 1 Options.write_dbid_to_manifest: 1
|
||||
2026/01/27-23:22:58.502629 1 Options.write_identity_file: 1
|
||||
2026/01/27-23:22:58.502629 1 Options.log_readahead_size: 0
|
||||
2026/01/27-23:22:58.502630 1 Options.file_checksum_gen_factory: Unknown
|
||||
2026/01/27-23:22:58.502630 1 Options.best_efforts_recovery: 0
|
||||
2026/01/27-23:22:58.502631 1 Options.max_bgerror_resume_count: 2147483647
|
||||
2026/01/27-23:22:58.502632 1 Options.bgerror_resume_retry_interval: 1000000
|
||||
2026/01/27-23:22:58.502632 1 Options.allow_data_in_errors: 0
|
||||
2026/01/27-23:22:58.502633 1 Options.db_host_id: __hostname__
|
||||
2026/01/27-23:22:58.502633 1 Options.enforce_single_del_contracts: true
|
||||
2026/01/27-23:22:58.502634 1 Options.metadata_write_temperature: kUnknown
|
||||
2026/01/27-23:22:58.502635 1 Options.wal_write_temperature: kUnknown
|
||||
2026/01/27-23:22:58.502635 1 Options.max_background_jobs: 4
|
||||
2026/01/27-23:22:58.502636 1 Options.max_background_compactions: -1
|
||||
2026/01/27-23:22:58.502637 1 Options.max_subcompactions: 2
|
||||
2026/01/27-23:22:58.502637 1 Options.avoid_flush_during_shutdown: 0
|
||||
2026/01/27-23:22:58.502638 1 Options.writable_file_max_buffer_size: 1048576
|
||||
2026/01/27-23:22:58.502638 1 Options.delayed_write_rate : 536870912000
|
||||
2026/01/27-23:22:58.502640 1 Options.max_total_wal_size: 536870912
|
||||
2026/01/27-23:22:58.502640 1 Options.delete_obsolete_files_period_micros: 21600000000
|
||||
2026/01/27-23:22:58.502641 1 Options.stats_dump_period_sec: 0
|
||||
2026/01/27-23:22:58.502642 1 Options.stats_persist_period_sec: 600
|
||||
2026/01/27-23:22:58.502643 1 Options.stats_history_buffer_size: 1048576
|
||||
2026/01/27-23:22:58.502644 1 Options.max_open_files: 8096
|
||||
2026/01/27-23:22:58.502644 1 Options.bytes_per_sync: 1048576
|
||||
2026/01/27-23:22:58.502645 1 Options.wal_bytes_per_sync: 0
|
||||
2026/01/27-23:22:58.502645 1 Options.strict_bytes_per_sync: 0
|
||||
2026/01/27-23:22:58.502646 1 Options.compaction_readahead_size: 2097152
|
||||
2026/01/27-23:22:58.502646 1 Options.max_background_flushes: -1
|
||||
2026/01/27-23:22:58.502647 1 Options.daily_offpeak_time_utc:
|
||||
2026/01/27-23:22:58.502647 1 Compression algorithms supported:
|
||||
2026/01/27-23:22:58.502648 1 kCustomCompressionFE supported: 0
|
||||
2026/01/27-23:22:58.502649 1 kCustomCompressionFC supported: 0
|
||||
2026/01/27-23:22:58.502649 1 kCustomCompressionF8 supported: 0
|
||||
2026/01/27-23:22:58.502650 1 kCustomCompressionF7 supported: 0
|
||||
2026/01/27-23:22:58.502651 1 kCustomCompressionB2 supported: 0
|
||||
2026/01/27-23:22:58.502651 1 kLZ4Compression supported: 1
|
||||
2026/01/27-23:22:58.502652 1 kCustomCompression88 supported: 0
|
||||
2026/01/27-23:22:58.502653 1 kCustomCompressionD8 supported: 0
|
||||
2026/01/27-23:22:58.502653 1 kCustomCompression9F supported: 0
|
||||
2026/01/27-23:22:58.502654 1 kCustomCompressionD6 supported: 0
|
||||
2026/01/27-23:22:58.502654 1 kCustomCompressionA9 supported: 0
|
||||
2026/01/27-23:22:58.502655 1 kCustomCompressionEC supported: 0
|
||||
2026/01/27-23:22:58.502655 1 kCustomCompressionA3 supported: 0
|
||||
2026/01/27-23:22:58.502656 1 kCustomCompressionCB supported: 0
|
||||
2026/01/27-23:22:58.502657 1 kCustomCompression90 supported: 0
|
||||
2026/01/27-23:22:58.502657 1 kCustomCompressionA0 supported: 0
|
||||
2026/01/27-23:22:58.502658 1 kCustomCompressionC6 supported: 0
|
||||
2026/01/27-23:22:58.502659 1 kCustomCompression9D supported: 0
|
||||
2026/01/27-23:22:58.502659 1 kCustomCompression8B supported: 0
|
||||
2026/01/27-23:22:58.502660 1 kCustomCompressionA8 supported: 0
|
||||
2026/01/27-23:22:58.502660 1 kCustomCompression8D supported: 0
|
||||
2026/01/27-23:22:58.502661 1 kCustomCompression97 supported: 0
|
||||
2026/01/27-23:22:58.502661 1 kCustomCompression98 supported: 0
|
||||
2026/01/27-23:22:58.502662 1 kCustomCompressionAC supported: 0
|
||||
2026/01/27-23:22:58.502662 1 kCustomCompressionE9 supported: 0
|
||||
2026/01/27-23:22:58.502663 1 kCustomCompression96 supported: 0
|
||||
2026/01/27-23:22:58.502663 1 kCustomCompressionB1 supported: 0
|
||||
2026/01/27-23:22:58.502664 1 kCustomCompression95 supported: 0
|
||||
2026/01/27-23:22:58.502664 1 kCustomCompression84 supported: 0
|
||||
2026/01/27-23:22:58.502665 1 kCustomCompression91 supported: 0
|
||||
2026/01/27-23:22:58.502665 1 kCustomCompressionAB supported: 0
|
||||
2026/01/27-23:22:58.502666 1 kCustomCompressionB3 supported: 0
|
||||
2026/01/27-23:22:58.502666 1 kCustomCompression81 supported: 0
|
||||
2026/01/27-23:22:58.502667 1 kCustomCompressionDC supported: 0
|
||||
2026/01/27-23:22:58.502667 1 kBZip2Compression supported: 0
|
||||
2026/01/27-23:22:58.502668 1 kCustomCompressionBB supported: 0
|
||||
2026/01/27-23:22:58.502668 1 kCustomCompression9C supported: 0
|
||||
2026/01/27-23:22:58.502668 1 kCustomCompressionC9 supported: 0
|
||||
2026/01/27-23:22:58.502669 1 kCustomCompressionCC supported: 0
|
||||
2026/01/27-23:22:58.502669 1 kCustomCompression92 supported: 0
|
||||
2026/01/27-23:22:58.502670 1 kCustomCompressionB9 supported: 0
|
||||
2026/01/27-23:22:58.502670 1 kCustomCompression8F supported: 0
|
||||
2026/01/27-23:22:58.502671 1 kCustomCompression8A supported: 0
|
||||
2026/01/27-23:22:58.502671 1 kCustomCompression9B supported: 0
|
||||
2026/01/27-23:22:58.502671 1 kZSTD supported: 1
|
||||
2026/01/27-23:22:58.502672 1 kCustomCompressionAA supported: 0
|
||||
2026/01/27-23:22:58.502672 1 kCustomCompressionA2 supported: 0
|
||||
2026/01/27-23:22:58.502673 1 kZlibCompression supported: 1
|
||||
2026/01/27-23:22:58.502673 1 kXpressCompression supported: 0
|
||||
2026/01/27-23:22:58.502674 1 kCustomCompressionFD supported: 0
|
||||
2026/01/27-23:22:58.502674 1 kCustomCompressionE2 supported: 0
|
||||
2026/01/27-23:22:58.502675 1 kLZ4HCCompression supported: 1
|
||||
2026/01/27-23:22:58.502675 1 kCustomCompressionA6 supported: 0
|
||||
2026/01/27-23:22:58.502676 1 kCustomCompression85 supported: 0
|
||||
2026/01/27-23:22:58.502676 1 kCustomCompressionA4 supported: 0
|
||||
2026/01/27-23:22:58.502676 1 kCustomCompression86 supported: 0
|
||||
2026/01/27-23:22:58.502677 1 kCustomCompression83 supported: 0
|
||||
2026/01/27-23:22:58.502677 1 kCustomCompression87 supported: 0
|
||||
2026/01/27-23:22:58.502678 1 kCustomCompression89 supported: 0
|
||||
2026/01/27-23:22:58.502678 1 kCustomCompression8C supported: 0
|
||||
2026/01/27-23:22:58.502679 1 kCustomCompressionDB supported: 0
|
||||
2026/01/27-23:22:58.502679 1 kCustomCompressionF3 supported: 0
|
||||
2026/01/27-23:22:58.502679 1 kCustomCompressionE6 supported: 0
|
||||
2026/01/27-23:22:58.502680 1 kCustomCompression8E supported: 0
|
||||
2026/01/27-23:22:58.502680 1 kCustomCompressionDA supported: 0
|
||||
2026/01/27-23:22:58.502681 1 kCustomCompression93 supported: 0
|
||||
2026/01/27-23:22:58.502681 1 kCustomCompression94 supported: 0
|
||||
2026/01/27-23:22:58.502685 1 kCustomCompression9E supported: 0
|
||||
2026/01/27-23:22:58.502685 1 kCustomCompressionB4 supported: 0
|
||||
2026/01/27-23:22:58.502686 1 kCustomCompressionFB supported: 0
|
||||
2026/01/27-23:22:58.502686 1 kCustomCompressionB5 supported: 0
|
||||
2026/01/27-23:22:58.502686 1 kCustomCompressionD5 supported: 0
|
||||
2026/01/27-23:22:58.502687 1 kCustomCompressionB8 supported: 0
|
||||
2026/01/27-23:22:58.502687 1 kCustomCompressionD1 supported: 0
|
||||
2026/01/27-23:22:58.502688 1 kCustomCompressionBA supported: 0
|
||||
2026/01/27-23:22:58.502688 1 kCustomCompressionBC supported: 0
|
||||
2026/01/27-23:22:58.502689 1 kCustomCompressionCE supported: 0
|
||||
2026/01/27-23:22:58.502689 1 kCustomCompressionBD supported: 0
|
||||
2026/01/27-23:22:58.502689 1 kCustomCompressionC4 supported: 0
|
||||
2026/01/27-23:22:58.502690 1 kCustomCompression9A supported: 0
|
||||
2026/01/27-23:22:58.502690 1 kCustomCompression99 supported: 0
|
||||
2026/01/27-23:22:58.502691 1 kCustomCompressionBE supported: 0
|
||||
2026/01/27-23:22:58.502691 1 kCustomCompressionE5 supported: 0
|
||||
2026/01/27-23:22:58.502692 1 kCustomCompressionD9 supported: 0
|
||||
2026/01/27-23:22:58.502692 1 kCustomCompressionC1 supported: 0
|
||||
2026/01/27-23:22:58.502693 1 kCustomCompressionC5 supported: 0
|
||||
2026/01/27-23:22:58.502694 1 kCustomCompressionC2 supported: 0
|
||||
2026/01/27-23:22:58.502694 1 kCustomCompressionA5 supported: 0
|
||||
2026/01/27-23:22:58.502695 1 kCustomCompressionC7 supported: 0
|
||||
2026/01/27-23:22:58.502695 1 kCustomCompressionBF supported: 0
|
||||
2026/01/27-23:22:58.502696 1 kCustomCompressionE8 supported: 0
|
||||
2026/01/27-23:22:58.502696 1 kCustomCompressionC8 supported: 0
|
||||
2026/01/27-23:22:58.502697 1 kCustomCompressionAF supported: 0
|
||||
2026/01/27-23:22:58.502698 1 kCustomCompressionCA supported: 0
|
||||
2026/01/27-23:22:58.502698 1 kCustomCompressionCD supported: 0
|
||||
2026/01/27-23:22:58.502699 1 kCustomCompressionC0 supported: 0
|
||||
2026/01/27-23:22:58.502699 1 kCustomCompressionCF supported: 0
|
||||
2026/01/27-23:22:58.502700 1 kCustomCompressionF9 supported: 0
|
||||
2026/01/27-23:22:58.502700 1 kCustomCompressionD0 supported: 0
|
||||
2026/01/27-23:22:58.502701 1 kCustomCompressionD2 supported: 0
|
||||
2026/01/27-23:22:58.502701 1 kCustomCompressionAD supported: 0
|
||||
2026/01/27-23:22:58.502702 1 kCustomCompressionD3 supported: 0
|
||||
2026/01/27-23:22:58.502703 1 kCustomCompressionD4 supported: 0
|
||||
2026/01/27-23:22:58.502703 1 kCustomCompressionD7 supported: 0
|
||||
2026/01/27-23:22:58.502704 1 kCustomCompression82 supported: 0
|
||||
2026/01/27-23:22:58.502704 1 kCustomCompressionDD supported: 0
|
||||
2026/01/27-23:22:58.502705 1 kCustomCompressionC3 supported: 0
|
||||
2026/01/27-23:22:58.502705 1 kCustomCompressionEE supported: 0
|
||||
2026/01/27-23:22:58.502706 1 kCustomCompressionDE supported: 0
|
||||
2026/01/27-23:22:58.502706 1 kCustomCompressionDF supported: 0
|
||||
2026/01/27-23:22:58.502707 1 kCustomCompressionA7 supported: 0
|
||||
2026/01/27-23:22:58.502707 1 kCustomCompressionE0 supported: 0
|
||||
2026/01/27-23:22:58.502708 1 kCustomCompressionF1 supported: 0
|
||||
2026/01/27-23:22:58.502708 1 kCustomCompressionE1 supported: 0
|
||||
2026/01/27-23:22:58.502709 1 kCustomCompressionF5 supported: 0
|
||||
2026/01/27-23:22:58.502709 1 kCustomCompression80 supported: 0
|
||||
2026/01/27-23:22:58.502710 1 kCustomCompressionE3 supported: 0
|
||||
2026/01/27-23:22:58.502711 1 kCustomCompressionE4 supported: 0
|
||||
2026/01/27-23:22:58.502711 1 kCustomCompressionB0 supported: 0
|
||||
2026/01/27-23:22:58.502712 1 kCustomCompressionEA supported: 0
|
||||
2026/01/27-23:22:58.502712 1 kCustomCompressionFA supported: 0
|
||||
2026/01/27-23:22:58.502713 1 kCustomCompressionE7 supported: 0
|
||||
2026/01/27-23:22:58.502713 1 kCustomCompressionAE supported: 0
|
||||
2026/01/27-23:22:58.502714 1 kCustomCompressionEB supported: 0
|
||||
2026/01/27-23:22:58.502714 1 kCustomCompressionED supported: 0
|
||||
2026/01/27-23:22:58.502715 1 kCustomCompressionB6 supported: 0
|
||||
2026/01/27-23:22:58.502715 1 kCustomCompressionEF supported: 0
|
||||
2026/01/27-23:22:58.502716 1 kCustomCompressionF0 supported: 0
|
||||
2026/01/27-23:22:58.502716 1 kCustomCompressionB7 supported: 0
|
||||
2026/01/27-23:22:58.502717 1 kCustomCompressionF2 supported: 0
|
||||
2026/01/27-23:22:58.502717 1 kCustomCompressionA1 supported: 0
|
||||
2026/01/27-23:22:58.502718 1 kCustomCompressionF4 supported: 0
|
||||
2026/01/27-23:22:58.502719 1 kSnappyCompression supported: 1
|
||||
2026/01/27-23:22:58.502719 1 kCustomCompressionF6 supported: 0
|
||||
2026/01/27-23:22:58.502720 1 Fast CRC32 supported: Supported on Arm64
|
||||
2026/01/27-23:22:58.502721 1 DMutex implementation: pthread_mutex_t
|
||||
2026/01/27-23:22:58.502722 1 Jemalloc supported: 1
|
||||
2026/01/27-23:22:58.503054 1 [db/version_set.cc:6190] Recovering from manifest file: /data/db/MANIFEST-000029
|
||||
2026/01/27-23:22:58.503246 1 [db/column_family.cc:693] --------------- Options for column family [default]:
|
||||
2026/01/27-23:22:58.503249 1 Options.comparator: leveldb.BytewiseComparator
|
||||
2026/01/27-23:22:58.503250 1 Options.merge_operator: None
|
||||
2026/01/27-23:22:58.503250 1 Options.compaction_filter: None
|
||||
2026/01/27-23:22:58.503251 1 Options.compaction_filter_factory: None
|
||||
2026/01/27-23:22:58.503251 1 Options.sst_partitioner_factory: None
|
||||
2026/01/27-23:22:58.503252 1 Options.memtable_factory: SkipListFactory
|
||||
2026/01/27-23:22:58.503253 1 Options.table_factory: BlockBasedTable
|
||||
2026/01/27-23:22:58.503273 1 table_factory options: flush_block_policy_factory: FlushBlockBySizePolicyFactory (0xffff934d15a0)
|
||||
cache_index_and_filter_blocks: 0
|
||||
cache_index_and_filter_blocks_with_high_priority: 1
|
||||
pin_l0_filter_and_index_blocks_in_cache: 0
|
||||
pin_top_level_index_and_filter: 1
|
||||
index_type: 0
|
||||
data_block_index_type: 0
|
||||
index_shortening: 1
|
||||
data_block_hash_table_util_ratio: 0.750000
|
||||
checksum: 4
|
||||
no_block_cache: 0
|
||||
block_cache: 0xffff9346bd90
|
||||
block_cache_name: LRUCache
|
||||
block_cache_options:
|
||||
capacity : 33554432
|
||||
num_shard_bits : 6
|
||||
strict_capacity_limit : 0
|
||||
memory_allocator : None
|
||||
high_pri_pool_ratio: 0.500
|
||||
low_pri_pool_ratio: 0.000
|
||||
persistent_cache: (nil)
|
||||
block_size: 4096
|
||||
block_size_deviation: 10
|
||||
block_restart_interval: 16
|
||||
index_block_restart_interval: 1
|
||||
metadata_block_size: 4096
|
||||
partition_filters: 0
|
||||
use_delta_encoding: 1
|
||||
filter_policy: nullptr
|
||||
user_defined_index_factory: nullptr
|
||||
fail_if_no_udi_on_open: 0
|
||||
whole_key_filtering: 1
|
||||
verify_compression: 0
|
||||
read_amp_bytes_per_bit: 0
|
||||
format_version: 6
|
||||
enable_index_compression: 1
|
||||
block_align: 0
|
||||
max_auto_readahead_size: 262144
|
||||
prepopulate_block_cache: 0
|
||||
initial_auto_readahead_size: 8192
|
||||
num_file_reads_for_auto_readahead: 2
|
||||
2026/01/27-23:22:58.503278 1 Options.write_buffer_size: 67108864
|
||||
2026/01/27-23:22:58.503279 1 Options.max_write_buffer_number: 4
|
||||
2026/01/27-23:22:58.503280 1 Options.compression[0]: NoCompression
|
||||
2026/01/27-23:22:58.503281 1 Options.compression[1]: NoCompression
|
||||
2026/01/27-23:22:58.503281 1 Options.compression[2]: Snappy
|
||||
2026/01/27-23:22:58.503282 1 Options.compression[3]: Snappy
|
||||
2026/01/27-23:22:58.503282 1 Options.compression[4]: Snappy
|
||||
2026/01/27-23:22:58.503283 1 Options.compression[5]: Snappy
|
||||
2026/01/27-23:22:58.503284 1 Options.compression[6]: Snappy
|
||||
2026/01/27-23:22:58.503284 1 Options.bottommost_compression: Disabled
|
||||
2026/01/27-23:22:58.503285 1 Options.prefix_extractor: nullptr
|
||||
2026/01/27-23:22:58.503286 1 Options.memtable_insert_with_hint_prefix_extractor: nullptr
|
||||
2026/01/27-23:22:58.503286 1 Options.num_levels: 7
|
||||
2026/01/27-23:22:58.503287 1 Options.min_write_buffer_number_to_merge: 1
|
||||
2026/01/27-23:22:58.503287 1 Options.max_write_buffer_size_to_maintain: 0
|
||||
2026/01/27-23:22:58.503288 1 Options.bottommost_compression_opts.window_bits: -14
|
||||
2026/01/27-23:22:58.503289 1 Options.bottommost_compression_opts.level: 32767
|
||||
2026/01/27-23:22:58.503290 1 Options.bottommost_compression_opts.strategy: 0
|
||||
2026/01/27-23:22:58.503290 1 Options.bottommost_compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-23:22:58.503291 1 Options.bottommost_compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-23:22:58.503292 1 Options.bottommost_compression_opts.parallel_threads: 1
|
||||
2026/01/27-23:22:58.503292 1 Options.bottommost_compression_opts.enabled: false
|
||||
2026/01/27-23:22:58.503293 1 Options.bottommost_compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-23:22:58.503294 1 Options.bottommost_compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-23:22:58.503294 1 Options.compression_opts.window_bits: -14
|
||||
2026/01/27-23:22:58.503295 1 Options.compression_opts.level: 32767
|
||||
2026/01/27-23:22:58.503296 1 Options.compression_opts.strategy: 0
|
||||
2026/01/27-23:22:58.503296 1 Options.compression_opts.max_dict_bytes: 0
|
||||
2026/01/27-23:22:58.503297 1 Options.compression_opts.zstd_max_train_bytes: 0
|
||||
2026/01/27-23:22:58.503297 1 Options.compression_opts.use_zstd_dict_trainer: true
|
||||
2026/01/27-23:22:58.503298 1 Options.compression_opts.parallel_threads: 1
|
||||
2026/01/27-23:22:58.503299 1 Options.compression_opts.enabled: false
|
||||
2026/01/27-23:22:58.503299 1 Options.compression_opts.max_dict_buffer_bytes: 0
|
||||
2026/01/27-23:22:58.503300 1 Options.level0_file_num_compaction_trigger: 4
|
||||
2026/01/27-23:22:58.503300 1 Options.level0_slowdown_writes_trigger: 20
|
||||
2026/01/27-23:22:58.503301 1 Options.level0_stop_writes_trigger: 40
|
||||
2026/01/27-23:22:58.503301 1 Options.target_file_size_base: 134217728
|
||||
2026/01/27-23:22:58.503302 1 Options.target_file_size_multiplier: 1
|
||||
2026/01/27-23:22:58.503303 1 Options.max_bytes_for_level_base: 268435456
|
||||
2026/01/27-23:22:58.503303 1 Options.level_compaction_dynamic_level_bytes: 1
|
||||
2026/01/27-23:22:58.503304 1 Options.max_bytes_for_level_multiplier: 10.000000
|
||||
2026/01/27-23:22:58.503305 1 Options.max_bytes_for_level_multiplier_addtl[0]: 1
|
||||
2026/01/27-23:22:58.503306 1 Options.max_bytes_for_level_multiplier_addtl[1]: 1
|
||||
2026/01/27-23:22:58.503307 1 Options.max_bytes_for_level_multiplier_addtl[2]: 1
|
||||
2026/01/27-23:22:58.503307 1 Options.max_bytes_for_level_multiplier_addtl[3]: 1
|
||||
2026/01/27-23:22:58.503308 1 Options.max_bytes_for_level_multiplier_addtl[4]: 1
|
||||
2026/01/27-23:22:58.503308 1 Options.max_bytes_for_level_multiplier_addtl[5]: 1
|
||||
2026/01/27-23:22:58.503309 1 Options.max_bytes_for_level_multiplier_addtl[6]: 1
|
||||
2026/01/27-23:22:58.503309 1 Options.max_sequential_skip_in_iterations: 8
|
||||
2026/01/27-23:22:58.503310 1 Options.memtable_op_scan_flush_trigger: 0
|
||||
2026/01/27-23:22:58.503310 1 Options.memtable_avg_op_scan_flush_trigger: 0
|
||||
2026/01/27-23:22:58.503311 1 Options.max_compaction_bytes: 3355443200
|
||||
2026/01/27-23:22:58.503311 1 Options.arena_block_size: 1048576
|
||||
2026/01/27-23:22:58.503312 1 Options.soft_pending_compaction_bytes_limit: 68719476736
|
||||
2026/01/27-23:22:58.503312 1 Options.hard_pending_compaction_bytes_limit: 274877906944
|
||||
2026/01/27-23:22:58.503313 1 Options.disable_auto_compactions: 0
|
||||
2026/01/27-23:22:58.503314 1 Options.compaction_style: kCompactionStyleLevel
|
||||
2026/01/27-23:22:58.503314 1 Options.compaction_pri: kMinOverlappingRatio
|
||||
2026/01/27-23:22:58.503315 1 Options.compaction_options_universal.size_ratio: 1
|
||||
2026/01/27-23:22:58.503315 1 Options.compaction_options_universal.min_merge_width: 2
|
||||
2026/01/27-23:22:58.503316 1 Options.compaction_options_universal.max_merge_width: 4294967295
|
||||
2026/01/27-23:22:58.503316 1 Options.compaction_options_universal.max_size_amplification_percent: 200
|
||||
2026/01/27-23:22:58.503317 1 Options.compaction_options_universal.compression_size_percent: -1
|
||||
2026/01/27-23:22:58.503318 1 Options.compaction_options_universal.stop_style: kCompactionStopStyleTotalSize
|
||||
2026/01/27-23:22:58.503318 1 Options.compaction_options_universal.max_read_amp: -1
|
||||
2026/01/27-23:22:58.503319 1 Options.compaction_options_universal.reduce_file_locking: 0
|
||||
2026/01/27-23:22:58.503319 1 Options.compaction_options_fifo.max_table_files_size: 1073741824
|
||||
2026/01/27-23:22:58.503320 1 Options.compaction_options_fifo.allow_compaction: 0
|
||||
2026/01/27-23:22:58.503321 1 Options.table_properties_collectors:
|
||||
2026/01/27-23:22:58.503321 1 Options.inplace_update_support: 0
|
||||
2026/01/27-23:22:58.503322 1 Options.inplace_update_num_locks: 10000
|
||||
2026/01/27-23:22:58.503323 1 Options.memtable_prefix_bloom_size_ratio: 0.000000
|
||||
2026/01/27-23:22:58.503323 1 Options.memtable_whole_key_filtering: 0
|
||||
2026/01/27-23:22:58.503324 1 Options.memtable_huge_page_size: 0
|
||||
2026/01/27-23:22:58.503324 1 Options.bloom_locality: 0
|
||||
2026/01/27-23:22:58.503325 1 Options.max_successive_merges: 0
|
||||
2026/01/27-23:22:58.503325 1 Options.strict_max_successive_merges: 0
|
||||
2026/01/27-23:22:58.503326 1 Options.optimize_filters_for_hits: 0
|
||||
2026/01/27-23:22:58.503326 1 Options.paranoid_file_checks: 0
|
||||
2026/01/27-23:22:58.503327 1 Options.force_consistency_checks: 1
|
||||
2026/01/27-23:22:58.503327 1 Options.report_bg_io_stats: 0
|
||||
2026/01/27-23:22:58.503328 1 Options.disallow_memtable_writes: 0
|
||||
2026/01/27-23:22:58.503328 1 Options.ttl: 2592000
|
||||
2026/01/27-23:22:58.503329 1 Options.periodic_compaction_seconds: 0
|
||||
2026/01/27-23:22:58.503329 1 Options.default_temperature: kUnknown
|
||||
2026/01/27-23:22:58.503330 1 Options.preclude_last_level_data_seconds: 0
|
||||
2026/01/27-23:22:58.503330 1 Options.preserve_internal_time_seconds: 0
|
||||
2026/01/27-23:22:58.503331 1 Options.enable_blob_files: false
|
||||
2026/01/27-23:22:58.503331 1 Options.min_blob_size: 0
|
||||
2026/01/27-23:22:58.503332 1 Options.blob_file_size: 268435456
|
||||
2026/01/27-23:22:58.503332 1 Options.blob_compression_type: NoCompression
|
||||
2026/01/27-23:22:58.503333 1 Options.enable_blob_garbage_collection: false
|
||||
2026/01/27-23:22:58.503333 1 Options.blob_garbage_collection_age_cutoff: 0.250000
|
||||
2026/01/27-23:22:58.503334 1 Options.blob_garbage_collection_force_threshold: 1.000000
|
||||
2026/01/27-23:22:58.503335 1 Options.blob_compaction_readahead_size: 0
|
||||
2026/01/27-23:22:58.503335 1 Options.blob_file_starting_level: 0
|
||||
2026/01/27-23:22:58.503336 1 Options.experimental_mempurge_threshold: 0.000000
|
||||
2026/01/27-23:22:58.503336 1 Options.memtable_max_range_deletions: 0
|
||||
2026/01/27-23:22:58.503337 1 Options.cf_allow_ingest_behind: false
|
||||
2026/01/27-23:22:58.503427 1 [WARN] [db/db_impl/db_impl_open.cc:2688] DB::Open() failed: Invalid argument: Column families not opened: index, search, stream, propagate, pubsub, zset_score, metadata
|
||||
2026/01/27-23:22:58.503463 1 [db/db_impl/db_impl.cc:467] Shutdown: canceling all background work
|
||||
2026/01/27-23:22:58.503492 1 [db/db_impl/db_impl.cc:681] Shutdown complete
|
||||
BIN
local-dev/data/kvrocks/db/MANIFEST-000035
Normal file
BIN
local-dev/data/kvrocks/db/MANIFEST-000035
Normal file
Binary file not shown.
1071
local-dev/data/kvrocks/db/OPTIONS-000031
Normal file
1071
local-dev/data/kvrocks/db/OPTIONS-000031
Normal file
File diff suppressed because it is too large
Load Diff
1071
local-dev/data/kvrocks/db/OPTIONS-000037
Normal file
1071
local-dev/data/kvrocks/db/OPTIONS-000037
Normal file
File diff suppressed because it is too large
Load Diff
143
local-dev/data/kvrocks/kvrocks_2026-01-27.log
Normal file
143
local-dev/data/kvrocks/kvrocks_2026-01-27.log
Normal file
@@ -0,0 +1,143 @@
|
||||
[2026-01-27T21:55:32.811461+00:00][I][main.cc:168] kvrocks version 2.14.0 (commit a71eb42f)
|
||||
[2026-01-27T21:55:32.841671+00:00][I][storage.cc:407] [storage] Success to load the data from disk: 12 ms
|
||||
[2026-01-27T21:55:32.843628+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T21:55:32.843938+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T21:55:32.844064+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T21:55:32.844141+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T21:55:32.844201+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T21:55:32.844268+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T21:55:32.844338+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T21:55:32.844401+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T21:55:32.844625+00:00][I][worker.cc:596] [worker] Thread #281473273094752 started
|
||||
[2026-01-27T21:55:32.844653+00:00][I][worker.cc:596] [worker] Thread #281473281548896 started
|
||||
[2026-01-27T21:55:32.844671+00:00][I][worker.cc:596] [worker] Thread #281473290003040 started
|
||||
[2026-01-27T21:55:32.844708+00:00][I][worker.cc:596] [worker] Thread #281473366090336 started
|
||||
[2026-01-27T21:55:32.844742+00:00][I][worker.cc:596] [worker] Thread #281473357636192 started
|
||||
[2026-01-27T21:55:32.844787+00:00][I][worker.cc:596] [worker] Thread #281473349182048 started
|
||||
[2026-01-27T21:55:32.844825+00:00][I][worker.cc:596] [worker] Thread #281473340727904 started
|
||||
[2026-01-27T21:55:32.844861+00:00][I][worker.cc:596] [worker] Thread #281473332273760 started
|
||||
[2026-01-27T21:55:32.844960+00:00][I][server.cc:245] [server] Ready to accept connections
|
||||
[2026-01-27T21:56:34.369240+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: pubsub
|
||||
[2026-01-27T21:56:34.377799+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: pubsub finished, result: OK
|
||||
[2026-01-27T21:56:34.378156+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: propagate
|
||||
[2026-01-27T21:56:34.378349+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: propagate finished, result: OK
|
||||
[2026-01-27T21:59:36.368485+00:00][I][main.cc:53] Signal Terminated (15) received, stopping the server
|
||||
[2026-01-27T22:00:38.556809+00:00][I][main.cc:168] kvrocks version 2.14.0 (commit a71eb42f)
|
||||
[2026-01-27T22:00:38.571773+00:00][I][event_listener.cc:187] [event_listener/table_file_created] column family: metadata, file path: /data/db/000014.sst, file size: 2427, job_id: 1, reason: recovery, status: OK
|
||||
[2026-01-27T22:00:38.579255+00:00][I][storage.cc:407] [storage] Success to load the data from disk: 17 ms
|
||||
[2026-01-27T22:00:38.581394+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:00:38.581635+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:00:38.581731+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:00:38.581905+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:00:38.582059+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:00:38.582166+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:00:38.582287+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:00:38.582369+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:00:38.582500+00:00][I][worker.cc:596] [worker] Thread #281473264837216 started
|
||||
[2026-01-27T22:00:38.582607+00:00][I][worker.cc:596] [worker] Thread #281473273291360 started
|
||||
[2026-01-27T22:00:38.582648+00:00][I][worker.cc:596] [worker] Thread #281473281745504 started
|
||||
[2026-01-27T22:00:38.582698+00:00][I][worker.cc:596] [worker] Thread #281473336730208 started
|
||||
[2026-01-27T22:00:38.582731+00:00][I][worker.cc:596] [worker] Thread #281473328276064 started
|
||||
[2026-01-27T22:00:38.582767+00:00][I][worker.cc:596] [worker] Thread #281473319821920 started
|
||||
[2026-01-27T22:00:38.582805+00:00][I][worker.cc:596] [worker] Thread #281473311367776 started
|
||||
[2026-01-27T22:00:38.582859+00:00][I][worker.cc:596] [worker] Thread #281473302913632 started
|
||||
[2026-01-27T22:00:38.582953+00:00][I][server.cc:245] [server] Ready to accept connections
|
||||
[2026-01-27T22:01:40.207616+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: pubsub
|
||||
[2026-01-27T22:01:40.208939+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: pubsub finished, result: OK
|
||||
[2026-01-27T22:01:40.209035+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: propagate
|
||||
[2026-01-27T22:01:40.209124+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: propagate finished, result: OK
|
||||
[2026-01-27T22:04:28.316771+00:00][I][main.cc:53] Signal Terminated (15) received, stopping the server
|
||||
[2026-01-27T22:05:20.855690+00:00][I][main.cc:168] kvrocks version 2.14.0 (commit a71eb42f)
|
||||
[2026-01-27T22:05:20.871111+00:00][I][storage.cc:407] [storage] Success to load the data from disk: 11 ms
|
||||
[2026-01-27T22:05:20.872974+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:05:20.873484+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:05:20.873699+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:05:20.874015+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:05:20.874074+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:05:20.874129+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:05:20.874184+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:05:20.874242+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:05:20.874356+00:00][I][worker.cc:596] [worker] Thread #281472658825824 started
|
||||
[2026-01-27T22:05:20.874371+00:00][I][worker.cc:596] [worker] Thread #281472667279968 started
|
||||
[2026-01-27T22:05:20.874386+00:00][I][worker.cc:596] [worker] Thread #281472675734112 started
|
||||
[2026-01-27T22:05:20.874415+00:00][I][worker.cc:596] [worker] Thread #281472739041888 started
|
||||
[2026-01-27T22:05:20.874436+00:00][I][worker.cc:596] [worker] Thread #281472726458976 started
|
||||
[2026-01-27T22:05:20.874450+00:00][I][worker.cc:596] [worker] Thread #281472718004832 started
|
||||
[2026-01-27T22:05:20.874464+00:00][I][worker.cc:596] [worker] Thread #281472709550688 started
|
||||
[2026-01-27T22:05:20.874485+00:00][I][worker.cc:596] [worker] Thread #281472701096544 started
|
||||
[2026-01-27T22:05:20.874543+00:00][I][server.cc:245] [server] Ready to accept connections
|
||||
[2026-01-27T22:06:22.367592+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: pubsub
|
||||
[2026-01-27T22:06:22.370793+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: pubsub finished, result: OK
|
||||
[2026-01-27T22:06:22.371106+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: propagate
|
||||
[2026-01-27T22:06:22.371313+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: propagate finished, result: OK
|
||||
[2026-01-27T22:29:24.678219+00:00][I][main.cc:53] Signal Terminated (15) received, stopping the server
|
||||
[2026-01-27T22:29:25.238671+00:00][I][main.cc:168] kvrocks version 2.14.0 (commit a71eb42f)
|
||||
[2026-01-27T22:29:25.251917+00:00][I][event_listener.cc:187] [event_listener/table_file_created] column family: metadata, file path: /data/db/000023.sst, file size: 2476, job_id: 1, reason: recovery, status: OK
|
||||
[2026-01-27T22:29:25.259557+00:00][I][storage.cc:407] [storage] Success to load the data from disk: 16 ms
|
||||
[2026-01-27T22:29:25.262110+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:25.262274+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:25.262372+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:25.262449+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:25.262518+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:25.262618+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:25.262684+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:25.262746+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:25.262900+00:00][I][worker.cc:596] [worker] Thread #281472793043552 started
|
||||
[2026-01-27T22:29:25.262932+00:00][I][worker.cc:596] [worker] Thread #281472801497696 started
|
||||
[2026-01-27T22:29:25.262956+00:00][I][worker.cc:596] [worker] Thread #281472809951840 started
|
||||
[2026-01-27T22:29:25.262989+00:00][I][worker.cc:596] [worker] Thread #281472873259616 started
|
||||
[2026-01-27T22:29:25.263017+00:00][I][worker.cc:596] [worker] Thread #281472860676704 started
|
||||
[2026-01-27T22:29:25.263051+00:00][I][worker.cc:596] [worker] Thread #281472852222560 started
|
||||
[2026-01-27T22:29:25.263086+00:00][I][worker.cc:596] [worker] Thread #281472843768416 started
|
||||
[2026-01-27T22:29:25.263115+00:00][I][worker.cc:596] [worker] Thread #281472835314272 started
|
||||
[2026-01-27T22:29:25.263201+00:00][I][server.cc:245] [server] Ready to accept connections
|
||||
[2026-01-27T22:29:38.668808+00:00][I][main.cc:53] Signal Terminated (15) received, stopping the server
|
||||
[2026-01-27T22:29:39.166069+00:00][I][main.cc:168] kvrocks version 2.14.0 (commit a71eb42f)
|
||||
[2026-01-27T22:29:39.182475+00:00][I][storage.cc:407] [storage] Success to load the data from disk: 13 ms
|
||||
[2026-01-27T22:29:39.183886+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:39.184001+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:39.184110+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:39.184542+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:39.184647+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:39.184722+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:39.184822+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:39.184900+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T22:29:39.185008+00:00][I][worker.cc:596] [worker] Thread #281473065673312 started
|
||||
[2026-01-27T22:29:39.185045+00:00][I][worker.cc:596] [worker] Thread #281473074127456 started
|
||||
[2026-01-27T22:29:39.185068+00:00][I][worker.cc:596] [worker] Thread #281473082581600 started
|
||||
[2026-01-27T22:29:39.185101+00:00][I][worker.cc:596] [worker] Thread #281473147986528 started
|
||||
[2026-01-27T22:29:39.185127+00:00][I][worker.cc:596] [worker] Thread #281473133306464 started
|
||||
[2026-01-27T22:29:39.185153+00:00][I][worker.cc:596] [worker] Thread #281473124852320 started
|
||||
[2026-01-27T22:29:39.185193+00:00][I][worker.cc:596] [worker] Thread #281473116398176 started
|
||||
[2026-01-27T22:29:39.185221+00:00][I][worker.cc:596] [worker] Thread #281473107944032 started
|
||||
[2026-01-27T22:29:39.185302+00:00][I][server.cc:245] [server] Ready to accept connections
|
||||
[2026-01-27T22:30:40.725701+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: pubsub
|
||||
[2026-01-27T22:30:40.726451+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: pubsub finished, result: OK
|
||||
[2026-01-27T22:30:40.726546+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: propagate
|
||||
[2026-01-27T22:30:40.726624+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: propagate finished, result: OK
|
||||
[2026-01-27T23:22:53.541798+00:00][I][main.cc:53] Signal Terminated (15) received, stopping the server
|
||||
[2026-01-27T23:22:58.500203+00:00][I][main.cc:168] kvrocks version 2.14.0 (commit a71eb42f)
|
||||
[2026-01-27T23:22:58.511777+00:00][I][event_listener.cc:187] [event_listener/table_file_created] column family: default, file path: /data/db/000032.sst, file size: 2104, job_id: 1, reason: recovery, status: OK
|
||||
[2026-01-27T23:22:58.513628+00:00][I][event_listener.cc:187] [event_listener/table_file_created] column family: metadata, file path: /data/db/000033.sst, file size: 6606, job_id: 1, reason: recovery, status: OK
|
||||
[2026-01-27T23:22:58.525506+00:00][I][storage.cc:407] [storage] Success to load the data from disk: 21 ms
|
||||
[2026-01-27T23:22:58.527352+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T23:22:58.527574+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T23:22:58.527688+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T23:22:58.527767+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T23:22:58.527848+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T23:22:58.527949+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T23:22:58.528244+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T23:22:58.528341+00:00][I][worker.cc:76] [worker] Listening on: 0.0.0.0:6666
|
||||
[2026-01-27T23:22:58.528525+00:00][I][worker.cc:596] [worker] Thread #281472916578912 started
|
||||
[2026-01-27T23:22:58.528585+00:00][I][worker.cc:596] [worker] Thread #281472958849632 started
|
||||
[2026-01-27T23:22:58.528618+00:00][I][worker.cc:596] [worker] Thread #281472967303776 started
|
||||
[2026-01-27T23:22:58.528648+00:00][I][worker.cc:596] [worker] Thread #281473036837472 started
|
||||
[2026-01-27T23:22:58.528952+00:00][I][worker.cc:596] [worker] Thread #281473022157408 started
|
||||
[2026-01-27T23:22:58.528990+00:00][I][worker.cc:596] [worker] Thread #281473009574496 started
|
||||
[2026-01-27T23:22:58.529021+00:00][I][worker.cc:596] [worker] Thread #281473001120352 started
|
||||
[2026-01-27T23:22:58.529044+00:00][I][worker.cc:596] [worker] Thread #281472992666208 started
|
||||
[2026-01-27T23:22:58.529123+00:00][I][server.cc:245] [server] Ready to accept connections
|
||||
[2026-01-27T23:24:00.045680+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: pubsub
|
||||
[2026-01-27T23:24:00.046224+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: pubsub finished, result: OK
|
||||
[2026-01-27T23:24:00.046312+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: propagate
|
||||
[2026-01-27T23:24:00.046352+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: propagate finished, result: OK
|
||||
4
local-dev/data/kvrocks/kvrocks_2026-01-28.log
Normal file
4
local-dev/data/kvrocks/kvrocks_2026-01-28.log
Normal file
@@ -0,0 +1,4 @@
|
||||
[2026-01-28T00:00:56.433217+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: pubsub
|
||||
[2026-01-28T00:00:56.435397+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: pubsub finished, result: OK
|
||||
[2026-01-28T00:00:56.435483+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: propagate
|
||||
[2026-01-28T00:00:56.435537+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: propagate finished, result: OK
|
||||
4
local-dev/data/kvrocks/kvrocks_2026-01-29.log
Normal file
4
local-dev/data/kvrocks/kvrocks_2026-01-29.log
Normal file
@@ -0,0 +1,4 @@
|
||||
[2026-01-29T00:00:32.264105+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: pubsub
|
||||
[2026-01-29T00:00:32.270121+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: pubsub finished, result: OK
|
||||
[2026-01-29T00:00:32.270189+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: propagate
|
||||
[2026-01-29T00:00:32.270251+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: propagate finished, result: OK
|
||||
5
local-dev/data/kvrocks/kvrocks_2026-01-30.log
Normal file
5
local-dev/data/kvrocks/kvrocks_2026-01-30.log
Normal file
@@ -0,0 +1,5 @@
|
||||
[2026-01-30T00:00:53.731511+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: pubsub
|
||||
[2026-01-30T00:00:53.732305+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: pubsub finished, result: OK
|
||||
[2026-01-30T00:00:53.732942+00:00][I][compaction_checker.cc:35] [compaction checker] Start to compact the column family: propagate
|
||||
[2026-01-30T00:00:53.733051+00:00][I][compaction_checker.cc:38] [compaction checker] Compact the column family: propagate finished, result: OK
|
||||
[2026-01-30T00:17:42.821542+00:00][I][main.cc:53] Signal Terminated (15) received, stopping the server
|
||||
169
local-dev/debug-middleware.js
Normal file
169
local-dev/debug-middleware.js
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Debug Middleware for Local Development
|
||||
* Captures and logs all requests for research
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || '/app/data';
|
||||
const LOG_FILE = path.join(DATA_DIR, 'debug-requests.jsonl');
|
||||
const capturedRequests = [];
|
||||
const MAX_CAPTURED = 500;
|
||||
|
||||
function extractSubdomain(host) {
|
||||
if (!host) return null;
|
||||
const hostname = host.split(':')[0];
|
||||
const match = hostname.match(/^(sessions|account-data|telemetry|tools|oauth\.accounts|accounts)\./);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug middleware - logs requests (skip internal paths)
|
||||
*/
|
||||
function debugMiddleware(req, res, next) {
|
||||
// Skip debug/health/admin endpoints
|
||||
if (req.path.startsWith('/debug') || req.path === '/health' || req.path === '/favicon.ico') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const subdomain = extractSubdomain(req.headers.host);
|
||||
|
||||
// Color logging
|
||||
const colors = { reset: '\x1b[0m', green: '\x1b[32m', blue: '\x1b[34m', yellow: '\x1b[33m', magenta: '\x1b[35m', cyan: '\x1b[36m', red: '\x1b[31m' };
|
||||
const subColor = { 'sessions': colors.green, 'account-data': colors.blue, 'telemetry': colors.yellow, 'tools': colors.magenta }[subdomain] || colors.cyan;
|
||||
|
||||
console.log(`${subColor}[${subdomain || 'main'}]${colors.reset} ${req.method} ${req.url}`);
|
||||
|
||||
// Capture response
|
||||
const origSend = res.send.bind(res);
|
||||
const origJson = res.json.bind(res);
|
||||
let responseBody = null;
|
||||
|
||||
res.send = function(body) {
|
||||
responseBody = typeof body === 'string' && body.length > 2000 ? body.substring(0, 2000) + '...' : body;
|
||||
return origSend(body);
|
||||
};
|
||||
|
||||
res.json = function(body) {
|
||||
try {
|
||||
const str = JSON.stringify(body);
|
||||
responseBody = str.length > 2000 ? { _truncated: true, _len: str.length } : body;
|
||||
} catch (e) {
|
||||
responseBody = { _error: 'serialize failed' };
|
||||
}
|
||||
return origJson(body);
|
||||
};
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - startTime;
|
||||
const statusColor = res.statusCode >= 400 ? colors.red : colors.green;
|
||||
console.log(` ${statusColor}-> ${res.statusCode}${colors.reset} (${duration}ms)`);
|
||||
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
host: req.headers.host,
|
||||
subdomain,
|
||||
query: req.query,
|
||||
body: req.body,
|
||||
response: { statusCode: res.statusCode, duration, body: responseBody }
|
||||
};
|
||||
|
||||
capturedRequests.unshift(entry);
|
||||
if (capturedRequests.length > MAX_CAPTURED) capturedRequests.pop();
|
||||
|
||||
try { fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n'); } catch (e) {}
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup debug routes
|
||||
*/
|
||||
function setupDebugRoutes(app) {
|
||||
// Debug dashboard
|
||||
app.get('/debug', (req, res) => {
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html><head><title>Debug</title>
|
||||
<style>
|
||||
body{font-family:monospace;background:#1a1a2e;color:#eee;padding:20px}
|
||||
h1{color:#00d4ff}.section{background:#16213e;padding:15px;margin:10px 0;border-radius:8px}
|
||||
.req{background:#0f3460;padding:8px;margin:4px 0;border-radius:4px;font-size:12px}
|
||||
.GET{color:#4ade80}.POST{color:#60a5fa}.DELETE{color:#f87171}
|
||||
.sub{color:#c084fc;font-weight:bold}pre{background:#0a0a1a;padding:8px;overflow-x:auto;font-size:11px}
|
||||
button{background:#00d4ff;color:#000;border:none;padding:6px 12px;cursor:pointer;margin:4px;border-radius:4px}
|
||||
select{padding:6px;background:#0f3460;border:1px solid #00d4ff;color:#fff;border-radius:4px}
|
||||
details summary{cursor:pointer;color:#60a5fa}
|
||||
</style></head><body>
|
||||
<h1>Debug Dashboard</h1>
|
||||
<div class="section"><button onclick="refresh()">Refresh</button><button onclick="clearReqs()">Clear</button>
|
||||
<select id="filter" onchange="refresh()"><option value="">All</option><option value="sessions">sessions</option>
|
||||
<option value="account-data">account-data</option><option value="telemetry">telemetry</option><option value="tools">tools</option></select>
|
||||
<span id="count"></span></div>
|
||||
<div class="section"><h2>Subdomains</h2><div id="subs"></div></div>
|
||||
<div class="section"><h2>Requests</h2><div id="reqs" style="max-height:500px;overflow-y:auto"></div></div>
|
||||
<script>
|
||||
async function refresh(){
|
||||
const f=document.getElementById('filter').value;
|
||||
const r=await fetch('/debug/requests'+(f?'?subdomain='+f:''));
|
||||
const d=await r.json();
|
||||
document.getElementById('count').textContent=d.total+' total';
|
||||
document.getElementById('reqs').innerHTML=d.requests.map(r=>\`<div class="req">
|
||||
<span class="\${r.method}">\${r.method}</span> <span class="sub">\${r.subdomain||'main'}</span> \${r.path}
|
||||
<span style="color:\${r.response?.statusCode<400?'#4ade80':'#f87171'}">\${r.response?.statusCode}</span>
|
||||
<details><summary>Details</summary><pre>\${JSON.stringify(r,null,2)}</pre></details></div>\`).join('');
|
||||
const s=await fetch('/debug/subdomains');
|
||||
const sd=await s.json();
|
||||
document.getElementById('subs').innerHTML=Object.entries(sd).map(([k,v])=>'<span class="sub">'+k+'</span>: '+v.count).join(' | ');
|
||||
}
|
||||
async function clearReqs(){await fetch('/debug/requests',{method:'DELETE'});refresh();}
|
||||
setInterval(refresh,5000);refresh();
|
||||
</script></body></html>`);
|
||||
});
|
||||
|
||||
app.get('/debug/requests', (req, res) => {
|
||||
let filtered = capturedRequests;
|
||||
if (req.query.subdomain) filtered = filtered.filter(r => r.subdomain === req.query.subdomain);
|
||||
res.json({ total: capturedRequests.length, filtered: filtered.length, requests: filtered.slice(0, 100) });
|
||||
});
|
||||
|
||||
app.get('/debug/subdomains', (req, res) => {
|
||||
const summary = {};
|
||||
for (const r of capturedRequests) {
|
||||
const sub = r.subdomain || 'main';
|
||||
if (!summary[sub]) summary[sub] = { count: 0 };
|
||||
summary[sub].count++;
|
||||
}
|
||||
res.json(summary);
|
||||
});
|
||||
|
||||
app.delete('/debug/requests', (req, res) => {
|
||||
capturedRequests.length = 0;
|
||||
res.json({ cleared: true });
|
||||
});
|
||||
|
||||
// Catch-all for unknown endpoints (must be registered last)
|
||||
app.all('*', (req, res, next) => {
|
||||
// Only catch truly unknown paths
|
||||
if (res.headersSent) return;
|
||||
|
||||
const subdomain = extractSubdomain(req.headers.host);
|
||||
console.log(`\x1b[31m[UNKNOWN]\x1b[0m ${req.method} ${req.path} (subdomain: ${subdomain})`);
|
||||
|
||||
res.status(200).json({
|
||||
debug: true,
|
||||
message: 'Unknown endpoint captured',
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
subdomain,
|
||||
headers: req.headers,
|
||||
body: req.body
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { debugMiddleware, setupDebugRoutes, capturedRequests };
|
||||
800
local-dev/debug-wrapper.js
Normal file
800
local-dev/debug-wrapper.js
Normal file
@@ -0,0 +1,800 @@
|
||||
/**
|
||||
* Debug Wrapper Entry Point
|
||||
* Wraps the existing hytale-auth-server with debug capabilities
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Only load debug functionality if DEBUG_MODE is enabled
|
||||
const DEBUG_MODE = process.env.DEBUG_MODE === 'true';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || '/app/data';
|
||||
const LOG_FILE = path.join(DATA_DIR, 'debug-requests.jsonl');
|
||||
const SSL_DIR = path.join(DATA_DIR, 'ssl');
|
||||
const SSL_KEY = path.join(SSL_DIR, 'server.key');
|
||||
const SSL_CERT = path.join(SSL_DIR, 'server.crt');
|
||||
const HTTPS_PORT = parseInt(process.env.HTTPS_PORT || '3443');
|
||||
|
||||
/**
|
||||
* Generate self-signed SSL certificates if they don't exist
|
||||
*/
|
||||
function ensureSSLCertificates() {
|
||||
if (!fs.existsSync(SSL_DIR)) {
|
||||
fs.mkdirSync(SSL_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(SSL_KEY) || !fs.existsSync(SSL_CERT)) {
|
||||
console.log('Generating self-signed SSL certificates...');
|
||||
try {
|
||||
execSync(`openssl req -x509 -newkey rsa:2048 -keyout "${SSL_KEY}" -out "${SSL_CERT}" -days 365 -nodes -subj "/CN=localhost" 2>/dev/null`);
|
||||
console.log('SSL certificates generated successfully');
|
||||
} catch (err) {
|
||||
console.error('Failed to generate SSL certificates:', err.message);
|
||||
console.error('HTTPS will not be available. Install openssl or provide certificates manually.');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
key: fs.readFileSync(SSL_KEY),
|
||||
cert: fs.readFileSync(SSL_CERT)
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to read SSL certificates:', err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// In-memory request storage for debug dashboard
|
||||
const capturedRequests = [];
|
||||
const MAX_CAPTURED = 500;
|
||||
|
||||
// Color codes for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
green: '\x1b[32m',
|
||||
blue: '\x1b[34m',
|
||||
yellow: '\x1b[33m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m',
|
||||
red: '\x1b[31m'
|
||||
};
|
||||
|
||||
function extractSubdomain(host) {
|
||||
if (!host) return null;
|
||||
const hostname = host.split(':')[0];
|
||||
const match = hostname.match(/^(sessions|account-data|telemetry|tools|oauth\.accounts|accounts)\./);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function getSubdomainColor(subdomain) {
|
||||
const subColors = {
|
||||
'sessions': colors.green,
|
||||
'account-data': colors.blue,
|
||||
'telemetry': colors.yellow,
|
||||
'tools': colors.magenta
|
||||
};
|
||||
return subColors[subdomain] || colors.cyan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the debug dashboard HTML
|
||||
*/
|
||||
function serveDebugDashboard(res) {
|
||||
const html = `<!DOCTYPE html>
|
||||
<html><head><title>Debug Dashboard - Local Dev</title>
|
||||
<style>
|
||||
body{font-family:monospace;background:#1a1a2e;color:#eee;padding:20px;margin:0}
|
||||
h1{color:#00d4ff;margin-bottom:10px}
|
||||
.info{color:#888;font-size:12px;margin-bottom:20px}
|
||||
.section{background:#16213e;padding:15px;margin:10px 0;border-radius:8px}
|
||||
.req{background:#0f3460;padding:10px;margin:5px 0;border-radius:4px;font-size:12px;border-left:3px solid #00d4ff}
|
||||
.req.error{border-left-color:#f87171}
|
||||
.GET{color:#4ade80}.POST{color:#60a5fa}.DELETE{color:#f87171}.PUT{color:#fbbf24}
|
||||
.sub{color:#c084fc;font-weight:bold}
|
||||
.status{padding:2px 6px;border-radius:3px;font-size:11px}
|
||||
.status.ok{background:#166534;color:#4ade80}
|
||||
.status.error{background:#7f1d1d;color:#f87171}
|
||||
pre{background:#0a0a1a;padding:10px;overflow-x:auto;font-size:11px;border-radius:4px;max-height:300px;overflow-y:auto}
|
||||
button{background:#00d4ff;color:#000;border:none;padding:8px 16px;cursor:pointer;margin:4px;border-radius:4px;font-weight:bold}
|
||||
button:hover{background:#00b8e6}
|
||||
button.danger{background:#dc2626}
|
||||
button.danger:hover{background:#b91c1c}
|
||||
button.paused{background:#fbbf24;color:#000}
|
||||
select{padding:8px;background:#0f3460;border:1px solid #00d4ff;color:#fff;border-radius:4px}
|
||||
details summary{cursor:pointer;color:#60a5fa;padding:5px 0}
|
||||
.timestamp{color:#666;font-size:10px}
|
||||
.duration{color:#fbbf24}
|
||||
.path{color:#fff}
|
||||
#count{margin-left:10px;color:#4ade80}
|
||||
#pauseStatus{margin-left:10px;color:#fbbf24;font-weight:bold}
|
||||
.controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
</style></head><body>
|
||||
<h1>Debug Dashboard - Local Development</h1>
|
||||
<div class="info">Capturing all requests to auth server for research purposes</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="controls">
|
||||
<button onclick="refresh()">Refresh</button>
|
||||
<button id="pauseBtn" onclick="togglePause()">Pause</button>
|
||||
<button class="danger" onclick="clearReqs()">Clear All</button>
|
||||
<select id="filter" onchange="refresh()">
|
||||
<option value="">All Subdomains</option>
|
||||
<option value="sessions">sessions</option>
|
||||
<option value="account-data">account-data</option>
|
||||
<option value="telemetry">telemetry</option>
|
||||
<option value="tools">tools</option>
|
||||
<option value="main">main (no subdomain)</option>
|
||||
</select>
|
||||
<span id="count"></span>
|
||||
<span id="pauseStatus"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Subdomain Summary</h2>
|
||||
<div id="subs"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Recent Requests</h2>
|
||||
<div id="reqs" style="max-height:600px;overflow-y:auto"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let isPaused = false;
|
||||
let refreshInterval = null;
|
||||
|
||||
async function refresh() {
|
||||
const f = document.getElementById('filter').value;
|
||||
const r = await fetch('/debug/requests' + (f ? '?subdomain=' + f : ''));
|
||||
const d = await r.json();
|
||||
document.getElementById('count').textContent = d.total + ' total, ' + d.filtered + ' shown';
|
||||
|
||||
document.getElementById('reqs').innerHTML = d.requests.map((r, i) => {
|
||||
const statusClass = r.response?.statusCode >= 400 ? 'error' : 'ok';
|
||||
const reqClass = r.response?.statusCode >= 400 ? 'error' : '';
|
||||
return \`<div class="req \${reqClass}">
|
||||
<div>
|
||||
<span class="timestamp">\${r.timestamp}</span>
|
||||
<span class="\${r.method}">\${r.method}</span>
|
||||
<span class="sub">\${r.subdomain || 'main'}</span>
|
||||
<span class="path">\${r.path}</span>
|
||||
<span class="status \${statusClass}">\${r.response?.statusCode || '?'}</span>
|
||||
<span class="duration">\${r.response?.duration || 0}ms</span>
|
||||
</div>
|
||||
<details ontoggle="handleDetailsToggle(this)">
|
||||
<summary>Request Details</summary>
|
||||
<pre>\${JSON.stringify({
|
||||
host: r.host,
|
||||
query: r.query,
|
||||
body: r.body,
|
||||
headers: r.headers
|
||||
}, null, 2)}</pre>
|
||||
</details>
|
||||
<details ontoggle="handleDetailsToggle(this)">
|
||||
<summary>Response</summary>
|
||||
<pre>\${JSON.stringify(r.response, null, 2)}</pre>
|
||||
</details>
|
||||
</div>\`;
|
||||
}).join('');
|
||||
|
||||
const s = await fetch('/debug/subdomains');
|
||||
const sd = await s.json();
|
||||
document.getElementById('subs').innerHTML = Object.entries(sd)
|
||||
.map(([k, v]) => '<span class="sub">' + k + '</span>: ' + v.count)
|
||||
.join(' | ') || '<em>No requests yet</em>';
|
||||
}
|
||||
|
||||
function handleDetailsToggle(el) {
|
||||
// Auto-pause when any details block is opened
|
||||
if (el.open && !isPaused) {
|
||||
togglePause();
|
||||
}
|
||||
}
|
||||
|
||||
function togglePause() {
|
||||
isPaused = !isPaused;
|
||||
const btn = document.getElementById('pauseBtn');
|
||||
const status = document.getElementById('pauseStatus');
|
||||
if (isPaused) {
|
||||
btn.textContent = 'Resume';
|
||||
btn.classList.add('paused');
|
||||
status.textContent = '(auto-refresh paused)';
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
} else {
|
||||
btn.textContent = 'Pause';
|
||||
btn.classList.remove('paused');
|
||||
status.textContent = '';
|
||||
refreshInterval = setInterval(autoRefresh, 3000);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function autoRefresh() {
|
||||
if (!isPaused) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async function clearReqs() {
|
||||
if (confirm('Clear all captured requests?')) {
|
||||
await fetch('/debug/requests', { method: 'DELETE' });
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
refreshInterval = setInterval(autoRefresh, 3000);
|
||||
refresh();
|
||||
</script>
|
||||
</body></html>`;
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle debug API endpoints
|
||||
*/
|
||||
function handleDebugApi(req, res, urlPath, url) {
|
||||
if (urlPath === '/debug' || urlPath === '/debug/') {
|
||||
serveDebugDashboard(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (urlPath === '/debug/requests') {
|
||||
if (req.method === 'DELETE') {
|
||||
capturedRequests.length = 0;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ cleared: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
let filtered = capturedRequests;
|
||||
const subdomain = url.searchParams.get('subdomain');
|
||||
if (subdomain) {
|
||||
if (subdomain === 'main') {
|
||||
filtered = filtered.filter(r => !r.subdomain);
|
||||
} else {
|
||||
filtered = filtered.filter(r => r.subdomain === subdomain);
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
total: capturedRequests.length,
|
||||
filtered: filtered.length,
|
||||
requests: filtered.slice(0, 100)
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (urlPath === '/debug/subdomains') {
|
||||
const summary = {};
|
||||
for (const r of capturedRequests) {
|
||||
const sub = r.subdomain || 'main';
|
||||
if (!summary[sub]) summary[sub] = { count: 0 };
|
||||
summary[sub].count++;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(summary));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture request/response for debugging
|
||||
*/
|
||||
function captureRequest(req, res, url, startTime) {
|
||||
const subdomain = extractSubdomain(req.headers.host);
|
||||
const originalEnd = res.end.bind(res);
|
||||
let responseBody = null;
|
||||
|
||||
// Intercept res.end to capture response
|
||||
res.end = function(chunk, encoding, callback) {
|
||||
if (chunk) {
|
||||
try {
|
||||
const str = chunk.toString();
|
||||
if (str.length > 2000) {
|
||||
responseBody = { _truncated: true, _length: str.length, _preview: str.substring(0, 500) };
|
||||
} else {
|
||||
try {
|
||||
responseBody = JSON.parse(str);
|
||||
} catch {
|
||||
responseBody = str;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
responseBody = { _error: 'Could not capture response' };
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Log with colors
|
||||
const subColor = getSubdomainColor(subdomain);
|
||||
const statusColor = res.statusCode >= 400 ? colors.red : colors.green;
|
||||
console.log(`${subColor}[${subdomain || 'main'}]${colors.reset} ${req.method} ${url.pathname} ${statusColor}${res.statusCode}${colors.reset} (${duration}ms)`);
|
||||
|
||||
// Store in memory
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
method: req.method,
|
||||
path: url.pathname,
|
||||
host: req.headers.host,
|
||||
subdomain,
|
||||
query: Object.fromEntries(url.searchParams),
|
||||
headers: sanitizeHeaders(req.headers),
|
||||
response: {
|
||||
statusCode: res.statusCode,
|
||||
duration,
|
||||
body: responseBody
|
||||
}
|
||||
};
|
||||
|
||||
capturedRequests.unshift(entry);
|
||||
if (capturedRequests.length > MAX_CAPTURED) {
|
||||
capturedRequests.pop();
|
||||
}
|
||||
|
||||
// Also write to file
|
||||
try {
|
||||
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
||||
} catch (e) {
|
||||
// Ignore file write errors
|
||||
}
|
||||
|
||||
return originalEnd(chunk, encoding, callback);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize headers for logging (remove sensitive data)
|
||||
*/
|
||||
function sanitizeHeaders(headers) {
|
||||
const sanitized = { ...headers };
|
||||
// Keep Authorization header but might want to truncate in future
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point - wraps the existing server
|
||||
*/
|
||||
async function main() {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('\x1b[33m=== DEBUG MODE ENABLED ===\x1b[0m');
|
||||
console.log(`Debug dashboard: http://localhost:${process.env.PORT || 3000}/debug`);
|
||||
console.log(`Request log: ${LOG_FILE}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Load the original app module
|
||||
const app = require('./app');
|
||||
|
||||
// If not debug mode, just run normally
|
||||
if (!DEBUG_MODE) {
|
||||
app.run();
|
||||
return;
|
||||
}
|
||||
|
||||
// In debug mode, we need to wrap the request handler
|
||||
// Re-implement the server startup with debug wrapper
|
||||
const config = require('./config');
|
||||
const { connect: connectRedis, isConnected } = require('./services/redis');
|
||||
const assets = require('./services/assets');
|
||||
|
||||
console.log('=== Hytale Auth Server (Debug Mode) ===');
|
||||
console.log(`Domain: ${config.domain}`);
|
||||
console.log(`Data directory: ${config.dataDir}`);
|
||||
|
||||
// Pre-load cosmetics
|
||||
assets.preloadCosmetics();
|
||||
|
||||
// Connect to Redis
|
||||
await connectRedis();
|
||||
|
||||
// Create the original request handler reference
|
||||
const originalHandler = require('./app').handleRequest || null;
|
||||
|
||||
// Since handleRequest is not exported, we need a different approach
|
||||
// Load all the modules manually
|
||||
const middleware = require('./middleware');
|
||||
const routes = require('./routes');
|
||||
const { sendJson } = require('./utils/response');
|
||||
const auth = require('./services/auth');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Recreate the request handler with debug wrapping
|
||||
async function debugWrappedHandler(req, res) {
|
||||
const startTime = Date.now();
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
const urlPath = url.pathname;
|
||||
|
||||
// Skip debug endpoints from capture (but still handle them)
|
||||
if (urlPath.startsWith('/debug')) {
|
||||
if (handleDebugApi(req, res, urlPath, url)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip favicon and health from debug capture
|
||||
const skipCapture = urlPath === '/favicon.ico' || urlPath === '/health';
|
||||
|
||||
// Capture the request/response if not skipped
|
||||
if (!skipCapture) {
|
||||
captureRequest(req, res, url, startTime);
|
||||
}
|
||||
|
||||
// Now delegate to original handling logic
|
||||
// CORS headers
|
||||
middleware.corsHeaders(res);
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
middleware.handleOptions(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle binary uploads
|
||||
const headCacheMatch = urlPath.match(/^\/avatar\/([^/]+)\/head-cache$/);
|
||||
if (headCacheMatch && req.method === 'POST') {
|
||||
routes.avatar.handleAvatarRoutes(req, res, urlPath, {});
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse JSON body
|
||||
const body = await middleware.parseBody(req);
|
||||
|
||||
// Store body for debug capture
|
||||
if (!skipCapture && capturedRequests.length > 0 && !capturedRequests[0].body) {
|
||||
capturedRequests[0].body = body;
|
||||
}
|
||||
|
||||
// Extract user context
|
||||
const { uuid, name, tokenScope } = middleware.extractUserContext(body, req.headers);
|
||||
|
||||
// Route the request (copied from app.js to avoid modification)
|
||||
await routeRequestDebug(req, res, url, urlPath, body, uuid, name, tokenScope, middleware, routes, sendJson, auth, crypto);
|
||||
}
|
||||
|
||||
// Create HTTP server with debug wrapper
|
||||
const server = http.createServer(debugWrappedHandler);
|
||||
server.listen(config.port, '0.0.0.0', () => {
|
||||
console.log(`HTTP Server running on port ${config.port}`);
|
||||
console.log(`Redis: ${isConnected() ? 'connected' : 'NOT CONNECTED'}`);
|
||||
console.log(`\n\x1b[36mDebug Dashboard: http://localhost:${config.port}/debug\x1b[0m`);
|
||||
});
|
||||
|
||||
// Create HTTPS server if certificates are available
|
||||
const sslOptions = ensureSSLCertificates();
|
||||
if (sslOptions) {
|
||||
const httpsServer = https.createServer(sslOptions, debugWrappedHandler);
|
||||
httpsServer.listen(HTTPS_PORT, '0.0.0.0', () => {
|
||||
console.log(`\x1b[32mHTTPS Server running on port ${HTTPS_PORT}\x1b[0m`);
|
||||
console.log(`\x1b[33mNote: Self-signed cert - add to keychain or use NODE_TLS_REJECT_UNAUTHORIZED=0\x1b[0m\n`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Route request - copied from app.js to avoid modifying original
|
||||
*/
|
||||
async function routeRequestDebug(req, res, url, urlPath, body, uuid, name, tokenScope, middleware, routes, sendJson, auth, crypto) {
|
||||
const headers = req.headers;
|
||||
|
||||
// Avatar viewer routes
|
||||
if (urlPath.startsWith('/avatar/')) {
|
||||
await routes.avatar.handleAvatarRoutes(req, res, urlPath, body);
|
||||
return;
|
||||
}
|
||||
|
||||
// Customizer route
|
||||
if (urlPath.startsWith('/customizer')) {
|
||||
routes.avatar.handleCustomizerRoute(req, res, urlPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cosmetics list API
|
||||
if (urlPath === '/cosmetics/list') {
|
||||
routes.assets.handleCosmeticsList(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single cosmetic item data API
|
||||
if (urlPath.startsWith('/cosmetics/item/')) {
|
||||
routes.assets.handleCosmeticItem(req, res, urlPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets route
|
||||
if (urlPath.startsWith('/assets/')) {
|
||||
routes.assets.handleStaticAssets(req, res, urlPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Asset extraction route
|
||||
if (urlPath.startsWith('/asset/')) {
|
||||
routes.assets.handleAssetRoute(req, res, urlPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download route
|
||||
if (urlPath.startsWith('/download/')) {
|
||||
routes.assets.handleDownload(req, res, urlPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Health check
|
||||
if (urlPath === '/health' || urlPath === '/') {
|
||||
routes.health.handleHealth(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Favicon
|
||||
if (urlPath === '/favicon.ico') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// JWKS endpoint
|
||||
if (urlPath === '/.well-known/jwks.json' || urlPath === '/jwks.json') {
|
||||
routes.health.handleJwks(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Server auto-auth
|
||||
if (urlPath === '/server/auto-auth') {
|
||||
routes.server.handleServerAutoAuth(req, res, body);
|
||||
return;
|
||||
}
|
||||
|
||||
// Server game profiles
|
||||
if (urlPath === '/server/game-profiles' || urlPath === '/game-profiles') {
|
||||
routes.server.handleServerGameProfiles(req, res, headers);
|
||||
return;
|
||||
}
|
||||
|
||||
// OAuth device authorization
|
||||
if (urlPath === '/oauth2/device/auth') {
|
||||
routes.server.handleOAuthDeviceAuth(req, res, body);
|
||||
return;
|
||||
}
|
||||
|
||||
// OAuth device verification
|
||||
if (urlPath === '/oauth2/device/verify') {
|
||||
const query = Object.fromEntries(url.searchParams);
|
||||
routes.server.handleOAuthDeviceVerify(req, res, query);
|
||||
return;
|
||||
}
|
||||
|
||||
// OAuth token endpoint
|
||||
if (urlPath === '/oauth2/token') {
|
||||
routes.server.handleOAuthToken(req, res, body);
|
||||
return;
|
||||
}
|
||||
|
||||
// Game session endpoints
|
||||
if (urlPath === '/game-session/new') {
|
||||
routes.session.handleGameSessionNew(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/game-session/refresh') {
|
||||
await routes.session.handleGameSessionRefresh(req, res, body, uuid, name, headers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/game-session/child' || urlPath.includes('/game-session/child')) {
|
||||
routes.session.handleGameSessionChild(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Authorization grant
|
||||
if (urlPath === '/game-session/authorize' || urlPath.includes('/authorize') || urlPath.includes('/auth-grant')) {
|
||||
routes.session.handleAuthorizationGrant(req, res, body, uuid, name, headers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Token exchange
|
||||
if (urlPath === '/server-join/auth-token' || urlPath === '/game-session/exchange' || urlPath.includes('/auth-token')) {
|
||||
routes.session.handleTokenExchange(req, res, body, uuid, name, headers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Session/Auth endpoints
|
||||
if ((urlPath.includes('/session') || urlPath.includes('/child')) && !urlPath.startsWith('/admin')) {
|
||||
routes.session.handleSession(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath.includes('/auth')) {
|
||||
routes.session.handleAuth(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath.includes('/token')) {
|
||||
routes.session.handleToken(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath.includes('/validate') || urlPath.includes('/verify')) {
|
||||
routes.session.handleValidate(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath.includes('/refresh')) {
|
||||
routes.session.handleRefresh(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Account data endpoints
|
||||
if (urlPath === '/my-account/game-profile' || urlPath.includes('/game-profile')) {
|
||||
await routes.account.handleGameProfile(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/my-account/skin') {
|
||||
await routes.account.handleSkin(req, res, body, uuid, name, routes.avatar.invalidateHeadCache);
|
||||
return;
|
||||
}
|
||||
|
||||
// Account-data skin endpoint
|
||||
if (urlPath.startsWith('/account-data/skin/')) {
|
||||
const skinUuid = urlPath.replace('/account-data/skin/', '');
|
||||
await routes.account.handleSkin(req, res, body, skinUuid, name, routes.avatar.invalidateHeadCache);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/my-account/cosmetics' || urlPath.includes('/my-account/cosmetics')) {
|
||||
routes.account.handleCosmetics(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/my-account/get-launcher-data') {
|
||||
routes.account.handleLauncherData(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/my-account/get-profiles') {
|
||||
routes.account.handleGetProfiles(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bug reports and feedback
|
||||
if (urlPath === '/bugs/create' || urlPath === '/feedback/create') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Game session delete
|
||||
if (urlPath === '/game-session' && req.method === 'DELETE') {
|
||||
await routes.session.handleGameSessionDelete(req, res, headers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin login
|
||||
if (urlPath === '/admin/login' && req.method === 'POST') {
|
||||
await routes.admin.handleAdminLogin(req, res, body);
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin verify
|
||||
if (urlPath === '/admin/verify') {
|
||||
const token = headers['x-admin-token'] || url.searchParams.get('token');
|
||||
await routes.admin.handleAdminVerify(req, res, token);
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin dashboard
|
||||
if (urlPath === '/admin' || urlPath === '/admin/') {
|
||||
routes.admin.handleAdminDashboard(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test page for head embed
|
||||
if (urlPath === '/test/head') {
|
||||
routes.avatar.handleTestHeadPage(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Protected admin routes
|
||||
if (urlPath.startsWith('/admin/')) {
|
||||
const validToken = await middleware.verifyAdminAuth(headers);
|
||||
if (!validToken) {
|
||||
sendJson(res, 401, { error: 'Unauthorized. Please login at /admin' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Admin API endpoints
|
||||
if (urlPath === '/admin/sessions' || urlPath === '/sessions/active') {
|
||||
await routes.admin.handleActiveSessions(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/admin/stats') {
|
||||
await routes.admin.handleAdminStats(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath.startsWith('/admin/servers')) {
|
||||
await routes.admin.handleAdminServers(req, res, url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/admin/search') {
|
||||
await routes.admin.handlePlayerSearch(req, res, url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlPath === '/admin/prerender-queue') {
|
||||
await routes.admin.handlePrerenderQueue(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Profile lookup by UUID
|
||||
if (urlPath.startsWith('/profile/uuid/')) {
|
||||
const lookupUuid = urlPath.replace('/profile/uuid/', '');
|
||||
await routes.account.handleProfileLookupByUuid(req, res, lookupUuid, headers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Profile lookup by username
|
||||
if (urlPath.startsWith('/profile/username/')) {
|
||||
const lookupUsername = decodeURIComponent(urlPath.replace('/profile/username/', ''));
|
||||
await routes.account.handleProfileLookupByUsername(req, res, lookupUsername, headers);
|
||||
return;
|
||||
}
|
||||
|
||||
// Profile endpoint
|
||||
if (urlPath.includes('/profile') || urlPath.includes('/user') || urlPath.includes('/me')) {
|
||||
routes.account.handleProfile(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cosmetics endpoint
|
||||
if (urlPath.includes('/cosmetic') || urlPath.includes('/unlocked') || urlPath.includes('/inventory')) {
|
||||
routes.account.handleCosmetics(req, res, body, uuid, name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Telemetry endpoint
|
||||
if (urlPath.includes('/telemetry') || urlPath.includes('/analytics') || urlPath.includes('/event')) {
|
||||
sendJson(res, 200, { success: true, received: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Catch-all for unknown endpoints - important for research!
|
||||
console.log(`\x1b[31m[UNKNOWN ENDPOINT]\x1b[0m ${req.method} ${urlPath}`);
|
||||
const requestHost = req.headers.host;
|
||||
const authGrant = auth.generateAuthorizationGrant(uuid, name, crypto.randomUUID(), null, requestHost);
|
||||
const accessToken = auth.generateIdentityToken(uuid, name, null, ['game.base'], requestHost);
|
||||
sendJson(res, 200, {
|
||||
debug: true,
|
||||
message: 'Unknown endpoint captured for research',
|
||||
endpoint: urlPath,
|
||||
method: req.method,
|
||||
identityToken: accessToken,
|
||||
sessionToken: auth.generateSessionToken(uuid, requestHost),
|
||||
authorizationGrant: authGrant,
|
||||
accessToken: accessToken,
|
||||
tokenType: 'Bearer',
|
||||
user: { uuid, name, premium: true }
|
||||
});
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('Failed to start debug server:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
43
local-dev/docker-compose.yml
Normal file
43
local-dev/docker-compose.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
services:
|
||||
# Redis/Kvrocks for session storage
|
||||
kvrocks:
|
||||
image: apache/kvrocks:latest
|
||||
command: --bind 0.0.0.0 --dir /data
|
||||
ports:
|
||||
- "6666:6666"
|
||||
volumes:
|
||||
- ./data/kvrocks:/data
|
||||
|
||||
# Use the real hytale-auth-server with debug wrapper
|
||||
auth-server:
|
||||
build:
|
||||
context: ../../hytale-auth-server
|
||||
dockerfile: ../Hytale-F2P/local-dev/Dockerfile.debug
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3443:3443"
|
||||
environment:
|
||||
- PORT=3000
|
||||
- HTTPS_PORT=3443
|
||||
- DOMAIN=localhost:3443
|
||||
- USE_TLS=false
|
||||
- DATA_DIR=/app/data
|
||||
- REDIS_URL=redis://kvrocks:6666
|
||||
- ADMIN_PASSWORD=localdev
|
||||
- DEBUG_MODE=true
|
||||
- KEY_ID=2025-10-01-sanasol
|
||||
volumes:
|
||||
- ./data/auth:/app/data
|
||||
# Mount debug wrapper as entry point
|
||||
- ./debug-wrapper.js:/app/src/debug-wrapper.js:ro
|
||||
depends_on:
|
||||
- kvrocks
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: hytale-local-dev
|
||||
88
local-dev/start.sh
Executable file
88
local-dev/start.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Hytale F2P Local Development - Start Script
|
||||
# Usage: ./start.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
CYAN='\033[0;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}========================================"
|
||||
echo "Hytale F2P Local Dev Environment"
|
||||
echo -e "========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Create data directories
|
||||
mkdir -p "$SCRIPT_DIR/data/auth" "$SCRIPT_DIR/data/kvrocks"
|
||||
|
||||
# Start Docker services
|
||||
echo -e "${GREEN}Building and starting services...${NC}"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Stop existing
|
||||
docker compose down 2>/dev/null || true
|
||||
|
||||
# Build and start
|
||||
docker compose up -d --build
|
||||
|
||||
# Wait for ready
|
||||
echo -n "Waiting for server"
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:3000/health > /dev/null 2>&1; then
|
||||
echo ""
|
||||
echo -e "${GREEN}Server ready!${NC}"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 30 ]; then
|
||||
echo ""
|
||||
echo -e "${RED}Server failed to start! Check logs:${NC}"
|
||||
docker compose logs auth-server
|
||||
exit 1
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}========================================"
|
||||
echo "Services Running"
|
||||
echo -e "========================================${NC}"
|
||||
echo ""
|
||||
docker compose ps
|
||||
echo ""
|
||||
echo -e "${CYAN}Endpoints:${NC}"
|
||||
echo -e " Auth Server HTTP: ${GREEN}http://localhost:3000${NC}"
|
||||
echo -e " Auth Server HTTPS: ${GREEN}https://localhost:3443${NC}"
|
||||
echo -e " Debug Dashboard: ${GREEN}http://localhost:3000/debug${NC}"
|
||||
echo -e " Admin Dashboard: ${GREEN}http://localhost:3000/admin${NC} (password: localdev)"
|
||||
echo -e " JWKS: ${GREEN}http://localhost:3000/.well-known/jwks.json${NC}"
|
||||
echo -e " Kvrocks: localhost:6666"
|
||||
echo ""
|
||||
echo -e "${CYAN}========================================"
|
||||
echo "Using with Launcher"
|
||||
echo -e "========================================${NC}"
|
||||
echo ""
|
||||
echo -e "Start launcher with HTTPS (recommended):"
|
||||
echo -e " ${GREEN}cd $PROJECT_ROOT${NC}"
|
||||
echo -e " ${GREEN}HYTALE_AUTH_DOMAIN=localhost:3443 NODE_TLS_REJECT_UNAUTHORIZED=0 npm start${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}First time SSL setup (macOS):${NC}"
|
||||
echo -e " Add cert to keychain to avoid NODE_TLS_REJECT_UNAUTHORIZED:"
|
||||
echo -e " ${GREEN}sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain data/auth/ssl/server.crt${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}========================================"
|
||||
echo "Useful Commands"
|
||||
echo -e "========================================${NC}"
|
||||
echo ""
|
||||
echo -e "View logs: ${GREEN}docker compose logs -f auth-server${NC}"
|
||||
echo -e "Stop services: ${GREEN}docker compose down${NC}"
|
||||
echo -e "Restart: ${GREEN}docker compose restart auth-server${NC}"
|
||||
echo -e "Check request log: ${GREEN}cat data/auth/debug-requests.jsonl | jq${NC}"
|
||||
echo ""
|
||||
270
main.js
270
main.js
@@ -3,7 +3,7 @@ require('dotenv').config({ path: path.join(__dirname, '.env') });
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const fs = require('fs');
|
||||
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched, loadConfig, saveConfig } = require('./backend/launcher');
|
||||
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched, loadConfig, saveConfig, checkLaunchReady } = require('./backend/launcher');
|
||||
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
||||
|
||||
@@ -84,12 +84,13 @@ function setDiscordActivity() {
|
||||
largeImageText: 'Hytale F2P Launcher',
|
||||
buttons: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
url: 'https://github.com/amiayweb/Hytale-F2P'
|
||||
label: 'Download',
|
||||
url: 'https://git.sanhost.net/sanasol/hytale-f2p/releases'
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
url: 'https://discord.gg/hf2pdc'
|
||||
label: 'Community',
|
||||
// url: 'https://discord.gg/Fhbb9Yk5WW'
|
||||
url: 'https://chat.sanhost.net/invite/Tfz4jCK4'
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -107,9 +108,41 @@ async function toggleDiscordRPC(enabled) {
|
||||
} else if (!enabled && discordRPC) {
|
||||
try {
|
||||
console.log('Disconnecting Discord RPC...');
|
||||
discordRPC.clearActivity();
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
discordRPC.destroy();
|
||||
|
||||
// Check if Discord RPC is still connected before trying to use it
|
||||
if (discordRPC && discordRPC.transport && discordRPC.transport.socket) {
|
||||
// Add timeout to prevent hanging
|
||||
const clearActivityPromise = discordRPC.clearActivity();
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Discord RPC clearActivity timeout')), 1000)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.race([clearActivityPromise, timeoutPromise]);
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
} catch (timeoutErr) {
|
||||
console.log('Discord RPC clearActivity timed out:', timeoutErr.message);
|
||||
}
|
||||
} else {
|
||||
console.log('Discord RPC already disconnected');
|
||||
}
|
||||
|
||||
// Destroy - wrap in try-catch to handle library errors
|
||||
if (discordRPC) {
|
||||
try {
|
||||
if (typeof discordRPC.destroy === 'function') {
|
||||
const destroyPromise = discordRPC.destroy();
|
||||
if (destroyPromise && typeof destroyPromise.catch === 'function') {
|
||||
destroyPromise.catch(err => {
|
||||
console.log('Discord RPC destroy error (ignored):', err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (destroyErr) {
|
||||
console.log('Error destroying Discord RPC (ignored):', destroyErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Discord RPC disconnected successfully');
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting Discord RPC:', error.message);
|
||||
@@ -257,6 +290,17 @@ function createWindow() {
|
||||
});
|
||||
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
// Allow standard copy/paste/cut/select-all shortcuts
|
||||
const isMac = process.platform === 'darwin';
|
||||
const modKey = isMac ? input.meta : input.control;
|
||||
const key = input.key.toLowerCase();
|
||||
|
||||
// Allow Ctrl/Cmd + V (paste), C (copy), X (cut), A (select all)
|
||||
if (modKey && !input.shift && ['v', 'c', 'x', 'a'].includes(key)) {
|
||||
return; // Don't block these
|
||||
}
|
||||
|
||||
// Block devtools shortcuts
|
||||
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -274,7 +318,6 @@ function createWindow() {
|
||||
}
|
||||
|
||||
// Close application shortcuts
|
||||
const isMac = process.platform === 'darwin';
|
||||
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
|
||||
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
|
||||
(!isMac && input.alt && input.key === 'F4');
|
||||
@@ -290,7 +333,7 @@ function createWindow() {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
mainWindow.webContents.setIgnoreMenuShortcuts(true);
|
||||
// Note: Not using setIgnoreMenuShortcuts to allow copy/paste to work
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
@@ -414,9 +457,43 @@ async function cleanupDiscordRPC() {
|
||||
if (!discordRPC) return;
|
||||
try {
|
||||
console.log('Cleaning up Discord RPC...');
|
||||
discordRPC.clearActivity();
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
discordRPC.destroy();
|
||||
|
||||
// Check if Discord RPC is still connected before trying to use it
|
||||
if (discordRPC && discordRPC.transport && discordRPC.transport.socket) {
|
||||
// Add timeout to prevent hanging if Discord is unresponsive
|
||||
const clearActivityPromise = discordRPC.clearActivity();
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Discord RPC clearActivity timeout')), 1000)
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.race([clearActivityPromise, timeoutPromise]);
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
} catch (timeoutErr) {
|
||||
console.log('Discord RPC clearActivity timed out, proceeding with cleanup:', timeoutErr.message);
|
||||
}
|
||||
} else {
|
||||
console.log('Discord RPC already disconnected, skipping clearActivity');
|
||||
}
|
||||
|
||||
// Destroy and cleanup - wrap in try-catch to handle library errors
|
||||
if (discordRPC) {
|
||||
try {
|
||||
if (typeof discordRPC.destroy === 'function') {
|
||||
// destroy() may return a promise that rejects, so handle it
|
||||
const destroyPromise = discordRPC.destroy();
|
||||
if (destroyPromise && typeof destroyPromise.catch === 'function') {
|
||||
// If it's a promise, catch any rejections silently
|
||||
destroyPromise.catch(err => {
|
||||
console.log('Discord RPC destroy error (ignored):', err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (destroyErr) {
|
||||
console.log('Error destroying Discord RPC client (ignored):', destroyErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Discord RPC cleaned up successfully');
|
||||
} catch (error) {
|
||||
console.log('Error cleaning up Discord RPC:', error.message);
|
||||
@@ -551,7 +628,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
||||
console.log('[Main] Processing Butler error with retry context');
|
||||
errorData.retryData = {
|
||||
branch: error.branch || 'release',
|
||||
fileName: error.fileName || '7.pwr',
|
||||
fileName: error.fileName || 'v8',
|
||||
cacheDir: error.cacheDir
|
||||
};
|
||||
errorData.canRetry = error.canRetry !== false;
|
||||
@@ -571,7 +648,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
||||
console.log('[Main] Processing generic error, creating default retry data');
|
||||
errorData.retryData = {
|
||||
branch: 'release',
|
||||
fileName: '7.pwr'
|
||||
fileName: 'v8'
|
||||
};
|
||||
// For generic errors, assume it's retryable unless specified
|
||||
errorData.canRetry = error.canRetry !== false;
|
||||
@@ -596,14 +673,26 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
||||
});
|
||||
|
||||
ipcMain.handle('save-username', (event, username) => {
|
||||
saveUsername(username);
|
||||
return { success: true };
|
||||
try {
|
||||
saveUsername(username);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[Main] Failed to save username:', error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('load-username', () => {
|
||||
// Returns null if no username configured (no silent 'Player' fallback)
|
||||
return loadUsername();
|
||||
});
|
||||
|
||||
ipcMain.handle('check-launch-ready', () => {
|
||||
// Returns launch readiness state with detailed info
|
||||
// { ready: boolean, hasUsername: boolean, username: string|null, issues: string[] }
|
||||
return checkLaunchReady();
|
||||
});
|
||||
|
||||
ipcMain.handle('save-java-path', (event, javaPath) => {
|
||||
saveJavaPath(javaPath);
|
||||
return { success: true };
|
||||
@@ -799,7 +888,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
|
||||
console.log('[IPC] Invalid retry data, using PWR defaults');
|
||||
retryData = {
|
||||
branch: 'release',
|
||||
fileName: '7.pwr'
|
||||
fileName: 'v8'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -833,7 +922,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
|
||||
} :
|
||||
{
|
||||
branch: retryData?.branch || 'release',
|
||||
fileName: retryData?.fileName || '7.pwr',
|
||||
fileName: retryData?.fileName || 'v8',
|
||||
cacheDir: retryData?.cacheDir
|
||||
};
|
||||
|
||||
@@ -876,8 +965,8 @@ 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');
|
||||
// Open Forgejo releases page for manual download
|
||||
await shell.openExternal('https://git.sanhost.net/sanasol/hytale-f2p/releases/latest');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to open download page:', error);
|
||||
@@ -980,7 +1069,7 @@ ipcMain.handle('load-settings', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
|
||||
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getModFiles, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
|
||||
const os = require('os');
|
||||
|
||||
ipcMain.handle('get-local-app-data', async () => {
|
||||
@@ -1030,6 +1119,15 @@ ipcMain.handle('download-mod', async (event, modInfo) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-mod-files', async (event, modId) => {
|
||||
try {
|
||||
return await getModFiles(modId);
|
||||
} catch (error) {
|
||||
console.error('Error getting mod files:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('uninstall-mod', async (event, modId, modsPath) => {
|
||||
try {
|
||||
return await uninstallMod(modId, modsPath);
|
||||
@@ -1235,12 +1333,9 @@ ipcMain.handle('get-current-uuid', async () => {
|
||||
|
||||
ipcMain.handle('get-all-uuid-mappings', async () => {
|
||||
try {
|
||||
const mappings = getAllUuidMappings();
|
||||
return Object.entries(mappings).map(([username, uuid]) => ({
|
||||
username,
|
||||
uuid,
|
||||
isCurrent: username === require('./backend/launcher').loadUsername()
|
||||
}));
|
||||
// Use getAllUuidMappingsArray which correctly normalizes username for comparison
|
||||
const { getAllUuidMappingsArray } = require('./backend/launcher');
|
||||
return getAllUuidMappingsArray();
|
||||
} catch (error) {
|
||||
console.error('Error getting UUID mappings:', error);
|
||||
return [];
|
||||
@@ -1322,6 +1417,83 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
|
||||
|
||||
|
||||
|
||||
ipcMain.handle('send-logs', async () => {
|
||||
try {
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const { collectLogs, createZipBuffer } = require('./backend/utils/logCollector');
|
||||
|
||||
const { files, meta } = collectLogs();
|
||||
if (files.length === 0) {
|
||||
return { success: false, error: 'No log files found' };
|
||||
}
|
||||
|
||||
// Create ZIP with individual log files
|
||||
const zipBuffer = createZipBuffer(files);
|
||||
|
||||
// Get auth server URL from core config
|
||||
const { getAuthServerUrl } = require('./backend/core/config');
|
||||
const authUrl = getAuthServerUrl();
|
||||
|
||||
// Build file names list
|
||||
const fileNames = files.map(f => f.name).join(',');
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
const url = new URL(authUrl + '/logs/submit');
|
||||
const transport = url.protocol === 'https:' ? https : http;
|
||||
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||
path: url.pathname,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/zip',
|
||||
'Content-Length': zipBuffer.length,
|
||||
'X-Log-Username': meta.username || 'unknown',
|
||||
'X-Log-Platform': meta.platform || 'unknown',
|
||||
'X-Log-Version': meta.version || 'unknown',
|
||||
'X-Log-File-Count': String(files.length),
|
||||
'X-Log-Files': fileNames
|
||||
},
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
const req = transport.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => body += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
if (res.statusCode === 200) {
|
||||
resolve({ success: true, id: data.id, message: data.message });
|
||||
} else {
|
||||
resolve({ success: false, error: data.error || `Server error ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
resolve({ success: false, error: `Invalid response: ${res.statusCode}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
resolve({ success: false, error: 'Request timed out' });
|
||||
});
|
||||
|
||||
req.write(zipBuffer);
|
||||
req.end();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending logs:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('open-logs-folder', async () => {
|
||||
try {
|
||||
const logDir = logger.getLogDirectory();
|
||||
@@ -1377,3 +1549,45 @@ ipcMain.handle('profile-update', async (event, id, updates) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Java Wrapper Config IPC
|
||||
ipcMain.handle('load-wrapper-config', () => {
|
||||
const { loadWrapperConfig } = require('./backend/launcher');
|
||||
return loadWrapperConfig();
|
||||
});
|
||||
|
||||
ipcMain.handle('save-wrapper-config', (event, config) => {
|
||||
try {
|
||||
const { saveWrapperConfig } = require('./backend/launcher');
|
||||
saveWrapperConfig(config);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error saving wrapper config:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('reset-wrapper-config', () => {
|
||||
try {
|
||||
const { resetWrapperConfig } = require('./backend/launcher');
|
||||
const config = resetWrapperConfig();
|
||||
return { success: true, config };
|
||||
} catch (error) {
|
||||
console.error('Error resetting wrapper config:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-default-wrapper-config', () => {
|
||||
const { getDefaultWrapperConfig } = require('./backend/launcher');
|
||||
return getDefaultWrapperConfig();
|
||||
});
|
||||
|
||||
ipcMain.handle('preview-wrapper-script', (event, config, platform) => {
|
||||
const { generateWrapperScript } = require('./backend/launcher');
|
||||
return generateWrapperScript(config || require('./backend/launcher').loadWrapperConfig(), platform || process.platform, '/path/to/java');
|
||||
});
|
||||
|
||||
ipcMain.handle('get-current-platform', () => {
|
||||
return process.platform;
|
||||
});
|
||||
|
||||
|
||||
676
package-lock.json
generated
676
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user