Compare commits

..

11 Commits

Author SHA1 Message Date
sanasol
ab6f932245 Add HYTALE_NOOP_TEST to test read/write without patching
Tests if our file read/write process itself causes corruption.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 04:05:08 +01:00
sanasol
6333263ef9 Fix: Don't fallback to legacy mode when using skip list
When HYTALE_PATCH_SKIP is set, the legacy fallback was ignoring
the skip list and patching all occurrences anyway.

Now if skip list is active or HYTALE_NO_LEGACY_FALLBACK=1,
the legacy fallback is disabled.

Also found 4th occurrence at 0x1bc5d67 with metadata byte 0x89
that legacy mode was patching - this may be the crash culprit.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 03:51:32 +01:00
sanasol
654deca933 Add HYTALE_PATCH_SKIP to skip specific occurrences by index
HYTALE_PATCH_SKIP=0 - skip first occurrence only
HYTALE_PATCH_SKIP=0,2 - skip first and third
HYTALE_PATCH_SKIP=0 HYTALE_PATCH_LIMIT=1 - patch only second occurrence

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 03:38:09 +01:00
sanasol
aab67e8e28 Add HYTALE_PATCH_LIMIT to patch only first N occurrences
Use HYTALE_PATCH_LIMIT=1 to patch only first occurrence,
HYTALE_PATCH_LIMIT=2 for first two, etc.

Helps isolate which specific occurrence causes the crash.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 03:34:36 +01:00
sanasol
3953827f4a Add offset logging to debug which locations are being patched
Shows hex offset, bytes before/after each patch location to help
identify if we're patching false positives.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 03:31:51 +01:00
sanasol
81d1e7c113 Add HYTALE_PATCH_MODE env var to test different string formats
HYTALE_PATCH_MODE=utf16le - Use pure UTF-16LE (no length prefix)
HYTALE_PATCH_MODE=length-prefixed - Use length-prefixed format (default)

This helps debug if the length-prefixed format is causing crashes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 03:00:29 +01:00
sanasol
d8f90bd1ff Disable Discord URL patching entirely
The Discord URL patch was causing buffer overflow crashes and has no
practical effect on F2P functionality anyway.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 02:44:13 +01:00
sanasol
50491abc69 Fix buffer overflow in Discord URL patch - likely cause of crashes
The Discord URL patch was writing 28 bytes (.gg/MHkEjepMQ7, 14 chars)
where only 20 bytes existed (.gg/hytale, 10 chars), corrupting 8 bytes
of adjacent data in the binary.

Changes:
- Use same-length Discord URL: .gg/santop (10 chars)
- Add length check to UTF-16LE fallback path
- Add length check and zero-fill to findAndReplaceDomainSmart

This buffer overflow explains why the crash happened on some systems
(Steam Deck, Ubuntu LTS) but not others - depending on what data
was adjacent to the patched string.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 02:41:38 +01:00
sanasol
c92c5bec3c Add debug options and jemalloc support for Linux memory issues
Debug env vars for clientPatcher:
- HYTALE_SKIP_SENTRY_PATCH=1 - Skip sentry URL patch (60->26 chars)
- HYTALE_SKIP_SUBDOMAIN_PATCH=1 - Skip subdomain prefix patches

Game launcher Linux options:
- HYTALE_USE_JEMALLOC=1 - Use jemalloc allocator instead of glibc

This helps isolate which patch causes "free(): invalid pointer"
on Steam Deck and Ubuntu LTS.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 02:31:38 +01:00
sanasol
73f67f2fec Add MALLOC_CHECK_=0 for Linux to bypass glibc heap validation
Attempts to fix "free(): invalid pointer" crashes on Steam Deck and
Ubuntu LTS. The same patched binary works on macOS, Windows, and Arch
Linux, suggesting the issue is glibc's strict heap validation rather
than actual memory corruption.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 02:27:04 +01:00
sanasol
2582d9b6d1 Fix memory corruption by null-padding shorter replacement patterns
When replacing domain strings with shorter ones, the replaceBytes function
was only copying the new bytes without clearing the leftover bytes from
the old pattern. This caused "free(): invalid pointer" crashes on Steam
Deck and Ubuntu due to corrupted string metadata in the .NET AOT binary.

Fix: Fill the entire old pattern region with 0x00 before writing the
new bytes. This ensures no leftover data remains that could corrupt
the binary structure.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 02:10:31 +01:00
121 changed files with 4345 additions and 33651 deletions

View File

@@ -1,2 +0,0 @@
HF2P_SECRET_KEY=YOUR_KEY_HERE
HF2P_PROXY_URL=YOUR_PROXY

View File

@@ -36,8 +36,7 @@ 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 [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.
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.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
@@ -81,4 +80,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

View File

@@ -1,6 +1,6 @@
name: Bug Report
description: Create a report to help us improve
title: "[BUG] <Insert Bug Title Here>"
title: "[BUG] "
labels: ["bug"]
body:
- type: markdown
@@ -41,17 +41,17 @@ body:
required: true
- type: textarea
id: proof
id: screenshots
attributes:
label: Screenshots/Recordings
description: If applicable, add Screenshots/Recordings to help explain your problem.
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
- type: input
id: version
attributes:
label: Version
description: What version of the launcher are you running?
placeholder: "e.g. \"v2.2.1\""
placeholder: "e.g. \"v2.0.11 stable/pre-release\""
validations:
required: true
@@ -59,10 +59,8 @@ body:
id: hardwarespec
attributes:
label: Hardware Specification
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"
description: Tell us your CPU, iGPU, dGPU, VRAM, and RAM information.
placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 | VRAM: 24 GB | RAM: 32 GB"
validations:
required: true
@@ -72,11 +70,13 @@ body:
label: Operating System
description: What operating system are you using?
options:
- Windows 11/10
- macOS (Apple Silicon, M1/M2/M3)
- 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.)
- Windows 10
- Windows 11
- macOS (Apple Silicon)
- macOS (Intel)
- Linux Ubuntu/Debian-based
- Linux Fedora/RHEL-based
- Linux Arch-based
validations:
required: true

View File

@@ -1,28 +1,14 @@
name: Support Request
description: Request help or support
title: "[SUPPORT] <ADD YOUR TITLE HERE>"
title: "[SUPPORT] "
labels: ["support"]
body:
- type: dropdown
id: acknowledge
attributes:
label: Checklist
options:
- label: I have read the README.md before asking Support Request.
required: true
- label: I have read the TROUBLESHOOTING.md before asking Support Request.
required: true
- label: I have added title before submitting this Support Request.
required: true
- label: I acknowledge that my Support Request will not be responded as quick as in Discord Open-A-Ticket, I prefer this way.
required: true
- type: markdown
attributes:
value: |
If you need help or support with using the launcher, please fill out this support request.
Provide as much detail as possible so we can assist you effectively.
**Need a quick assistance?** Join our [TG Group](https://t.me/sanhostnet) | [TG Channel](https://t.me/hf2p_og) | [Community Chat](https://chat.sanhost.net/invite/Tfz4jCK4)!
**Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/gME8rUy3MB)!
- type: textarea
id: question
@@ -38,16 +24,10 @@ body:
attributes:
label: Context
description: Provide any relevant context or background information.
placeholder: "I've tried these steps, but got..."
placeholder: "I've tried..., but got..."
validations:
required: true
- type: textarea
id: proof
attributes:
label: Screenshots/Recordings
description: If applicable, add Screenshots/Recordings to help explain your problem.
- type: textarea
id: hardwarespec
attributes:
@@ -57,18 +37,12 @@ body:
validations:
required: true
- type: dropdown
- type: input
id: version
attributes:
label: Version
description: What launcher version are you using?
options:
- v2.2.1
- v2.2.0
- v2.1.1
- v2.1.0
- v2.0.11
- v2.0.2
description: What version are you using?
placeholder: "e.g. v2.0.11 stable/pre-release"
validations:
required: true
@@ -78,11 +52,13 @@ body:
label: Platform
description: What platform are you using?
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.)
- Windows 10
- Windows 11
- macOS (Apple Silicon)
- macOS (Intel)
- Linux Ubuntu/Debian-based
- Linux Fedora/RHEL-based
- Linux Arch-based
validations:
required: true

View File

@@ -2,143 +2,171 @@ name: Build and Release
on:
push:
branches:
- main
tags:
- '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:
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
runs-on: windows-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 --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
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
build-macos:
needs: [create-release]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- name: Build macOS Packages
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
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 --publish never
- 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
- uses: actions/upload-artifact@v4
with:
name: macos-builds
path: |
dist/*.dmg
dist/*.zip
dist/latest-mac.yml
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 rpm
sudo apt-get install -y libarchive-tools
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- name: Build Linux Packages
run: npx electron-builder --linux AppImage deb rpm pacman --publish never
- 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/*.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
npx electron-builder --linux AppImage deb rpm --x64 --arm64 --publish never
- uses: actions/upload-artifact@v4
with:
name: linux-builds
path: |
dist/*.AppImage
dist/*.AppImage.blockmap
dist/*.deb
dist/*.rpm
dist/latest-linux.yml
build-arch:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install base packages
run: |
pacman -Syu --noconfirm
pacman -S --noconfirm \
base-devel \
git \
nodejs \
npm \
rpm-tools \
libxcrypt-compat
- name: Create build user
run: |
useradd -m builder
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
- name: Fix Permissions
run: chown -R builder:builder .
- name: Build Arch Package
run: |
sudo -u builder bash << 'EOF'
set -e
makepkg --printsrcinfo > .SRCINFO
makepkg -s --noconfirm
EOF
- name: Upload Arch Package
uses: actions/upload-artifact@v4
with:
name: arch-package
path: |
*.pkg.tar.zst
*.src.tar.zst
.SRCINFO
release:
needs: [build-windows, build-macos, build-linux, build-arch]
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:
# FIX: './package.json' Module Not Found in `Get version` step
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- 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 }}
# If it's a tag, use the tag.
# tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
# If it's the 'release' branch, use 'v2.0.2-beta.r42'
# name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}-beta.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
files: |
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

3
.gitignore vendored
View File

@@ -17,9 +17,6 @@ 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

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,500 @@
let socket = null;
let isAuthenticated = false;
let messageQueue = [];
let chatUsername = '';
let userColor = '#3498db';
let userBadge = null;
const SOCKET_URL = 'https://chat.hytalef2p.com';
const MAX_MESSAGE_LENGTH = 500;
async function getOrCreatePlayerId() {
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
export async function initChat() {
if (window.electronAPI?.loadChatUsername) {
chatUsername = await window.electronAPI.loadChatUsername();
}
if (window.electronAPI?.loadChatColor) {
const savedColor = await window.electronAPI.loadChatColor();
if (savedColor) {
userColor = savedColor;
}
}
if (!chatUsername || chatUsername.trim() === '') {
showUsernameModal();
return;
}
setupChatUI();
setupColorSelector();
await connectToChat();
}
function showUsernameModal() {
const modal = document.getElementById('chatUsernameModal');
if (modal) {
modal.style.display = 'flex';
const input = document.getElementById('chatUsernameInput');
if (input) {
setTimeout(() => input.focus(), 100);
}
}
}
function hideUsernameModal() {
const modal = document.getElementById('chatUsernameModal');
if (modal) {
modal.style.display = 'none';
}
}
async function submitChatUsername() {
const input = document.getElementById('chatUsernameInput');
const errorMsg = document.getElementById('chatUsernameError');
if (!input) return;
const username = input.value.trim();
if (username.length === 0) {
if (errorMsg) errorMsg.textContent = 'Username cannot be empty';
return;
}
if (username.length < 3) {
if (errorMsg) errorMsg.textContent = 'Username must be at least 3 characters';
return;
}
if (username.length > 20) {
if (errorMsg) errorMsg.textContent = 'Username must be 20 characters or less';
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
if (errorMsg) errorMsg.textContent = 'Username can only contain letters, numbers, - and _';
return;
}
chatUsername = username;
if (window.electronAPI?.saveChatUsername) {
await window.electronAPI.saveChatUsername(username);
}
hideUsernameModal();
setupChatUI();
await connectToChat();
}
function setupChatUI() {
const sendBtn = document.getElementById('chatSendBtn');
const chatInput = document.getElementById('chatInput');
const chatMessages = document.getElementById('chatMessages');
if (!sendBtn || !chatInput || !chatMessages) {
console.warn('Chat UI elements not found');
return;
}
sendBtn.addEventListener('click', () => {
sendMessage();
});
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
chatInput.addEventListener('input', () => {
if (chatInput.value.length > MAX_MESSAGE_LENGTH) {
chatInput.value = chatInput.value.substring(0, MAX_MESSAGE_LENGTH);
}
updateCharCounter();
});
updateCharCounter();
}
async function connectToChat() {
try {
if (!window.io) {
await loadSocketIO();
}
const userId = await window.electronAPI?.getUserId();
if (!userId) {
console.error('User ID not available');
addSystemMessage('Error: Could not connect to chat');
return;
}
if (!chatUsername || chatUsername.trim() === '') {
console.error('Chat username not set');
addSystemMessage('Error: Username not set');
return;
}
socket = io(SOCKET_URL, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
socket.on('connect', async () => {
console.log('Connected to chat server');
const uuid = await window.electronAPI?.getCurrentUuid();
socket.emit('authenticate', {
username: chatUsername,
userId,
uuid: uuid,
userColor: userColor
});
});
socket.on('authenticated', (data) => {
isAuthenticated = true;
userBadge = data.badge;
addSystemMessage(`Connected as ${data.username}`);
while (messageQueue.length > 0) {
const msg = messageQueue.shift();
socket.emit('send_message', { message: msg });
}
});
socket.on('message', (data) => {
if (data.type === 'system') {
addSystemMessage(data.message);
} else if (data.type === 'user') {
addUserMessage(data.username, data.message, data.timestamp, data.userColor, data.badge);
}
});
socket.on('users_update', (data) => {
updateOnlineCount(data.count);
});
socket.on('error', (data) => {
addSystemMessage(`Error: ${data.message}`, 'error');
});
socket.on('clear_chat', (data) => {
clearAllMessages();
addSystemMessage(data.message || 'Chat cleared by server', 'warning');
});
socket.on('disconnect', () => {
isAuthenticated = false;
console.log('Disconnected from chat server');
addSystemMessage('Disconnected from chat', 'error');
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
addSystemMessage('Connection error. Retrying...', 'error');
});
} catch (error) {
console.error('Error connecting to chat:', error);
addSystemMessage('Failed to connect to chat server', 'error');
}
}
function loadSocketIO() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.socket.io/4.6.1/socket.io.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function sendMessage() {
const chatInput = document.getElementById('chatInput');
const message = chatInput.value.trim();
if (!message || message.length === 0) {
return;
}
if (message.length > MAX_MESSAGE_LENGTH) {
addSystemMessage(`Message too long (max ${MAX_MESSAGE_LENGTH} characters)`, 'error');
return;
}
if (!socket || !isAuthenticated) {
messageQueue.push(message);
addSystemMessage('Connecting... Your message will be sent soon.', 'warning');
chatInput.value = '';
updateCharCounter();
return;
}
socket.emit('send_message', { message });
chatInput.value = '';
updateCharCounter();
}
function addUserMessage(username, message, timestamp, userColor = '#3498db', badge = null) {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
const messageDiv = document.createElement('div');
messageDiv.className = 'chat-message user-message';
const time = new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
let badgeHTML = '';
if (badge) {
let badgeStyle = '';
if (badge.style === 'rainbow') {
badgeStyle = `background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #ffeaa7, #fab1a0, #fd79a8); background-size: 400% 400%; animation: rainbow 3s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`;
} else if (badge.style === 'gradient') {
if (badge.badge === 'CONTRIBUTOR') {
badgeStyle = `background: linear-gradient(45deg, #22c55e, #16a34a); background-size: 200% 200%; animation: contributorGlow 2s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`;
} else {
badgeStyle = `color: ${badge.color}; font-weight: bold; display: inline;`;
}
}
badgeHTML = `<span class="user-badge" style="${badgeStyle}">[${badge.badge}]</span> `;
}
messageDiv.innerHTML = `
<div class="message-header">
<span class="message-user-info">${badgeHTML}<span class="message-username" style="font-weight: bold;" data-username-color="${userColor}">${escapeHtml(username)}</span></span>
<span class="message-time">${time}</span>
</div>
<div class="message-content">${message}</div>
`;
const usernameElement = messageDiv.querySelector('.message-username');
if (usernameElement) {
applyUserColorStyle(usernameElement, userColor);
}
chatMessages.appendChild(messageDiv);
scrollToBottom();
}
function addSystemMessage(message, type = 'info') {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message system-message system-${type}`;
messageDiv.innerHTML = `
<div class="message-content">
<i class="fas fa-info-circle"></i> ${escapeHtml(message)}
</div>
`;
chatMessages.appendChild(messageDiv);
scrollToBottom();
}
function updateOnlineCount(count) {
const onlineCountElement = document.getElementById('chatOnlineCount');
if (onlineCountElement) {
onlineCountElement.textContent = count;
}
}
function updateCharCounter() {
const chatInput = document.getElementById('chatInput');
const charCounter = document.getElementById('chatCharCounter');
if (chatInput && charCounter) {
const length = chatInput.value.length;
charCounter.textContent = `${length}/${MAX_MESSAGE_LENGTH}`;
if (length > MAX_MESSAGE_LENGTH * 0.9) {
charCounter.classList.add('warning');
} else {
charCounter.classList.remove('warning');
}
}
}
function scrollToBottom() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
function clearAllMessages() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.innerHTML = '';
console.log('Chat cleared');
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.addEventListener('beforeunload', () => {
if (socket && socket.connected) {
socket.disconnect();
}
});
document.addEventListener('DOMContentLoaded', () => {
const usernameSubmitBtn = document.getElementById('chatUsernameSubmit');
const usernameCancelBtn = document.getElementById('chatUsernameCancel');
const usernameInput = document.getElementById('chatUsernameInput');
if (usernameSubmitBtn) {
usernameSubmitBtn.addEventListener('click', submitChatUsername);
}
if (usernameCancelBtn) {
usernameCancelBtn.addEventListener('click', () => {
hideUsernameModal();
const playNavItem = document.querySelector('[data-page="play"]');
if (playNavItem) playNavItem.click();
});
}
if (usernameInput) {
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
submitChatUsername();
}
});
}
const chatNavItem = document.querySelector('[data-page="chat"]');
if (chatNavItem) {
chatNavItem.addEventListener('click', () => {
if (!socket) {
initChat();
}
});
}
});
function setupColorSelector() {
const colorBtn = document.getElementById('chatColorBtn');
if (colorBtn) {
colorBtn.addEventListener('click', showChatColorModal);
}
const colorOptions = document.querySelectorAll('.color-option');
colorOptions.forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
option.classList.add('selected');
updateColorPreview();
});
});
const customColor = document.getElementById('customColor');
if (customColor) {
customColor.addEventListener('input', () => {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
updateColorPreview();
});
}
}
function showChatColorModal() {
const modal = document.getElementById('chatColorModal');
if (modal) {
modal.style.display = 'flex';
updateColorPreview();
}
}
window.closeChatColorModal = function() {
const modal = document.getElementById('chatColorModal');
if (modal) {
modal.style.display = 'none';
}
}
function updateColorPreview() {
const preview = document.getElementById('colorPreview');
if (!preview) return;
const selectedOption = document.querySelector('.color-option.selected');
let color = '#3498db';
if (selectedOption) {
color = selectedOption.dataset.color;
} else {
const customColor = document.getElementById('customColor');
if (customColor) color = customColor.value;
}
preview.style.color = color;
preview.style.background = 'transparent';
preview.style.webkitBackgroundClip = 'initial';
preview.style.webkitTextFillColor = 'initial';
}
window.applyChatColor = async function() {
let newColor;
const selectedOption = document.querySelector('.color-option.selected');
if (selectedOption) {
newColor = selectedOption.dataset.color;
} else {
const customColor = document.getElementById('customColor');
newColor = customColor ? customColor.value : '#3498db';
}
userColor = newColor;
if (window.electronAPI?.saveChatColor) {
await window.electronAPI.saveChatColor(newColor);
}
if (socket && isAuthenticated) {
const uuid = await window.electronAPI?.getCurrentUuid();
socket.emit('authenticate', {
username: chatUsername,
userId: await getOrCreatePlayerId(),
uuid: uuid,
userColor: userColor
});
addSystemMessage('Username color updated successfully', 'success');
}
closeChatColorModal();
}
function applyUserColorStyle(element, color) {
element.style.color = color;
element.style.background = 'transparent';
element.style.webkitBackgroundClip = 'initial';
element.style.webkitTextFillColor = 'initial';
}
window.ChatAPI = {
send: sendMessage,
disconnect: () => socket?.disconnect()
};

View File

@@ -1,191 +0,0 @@
// Featured Servers Management
const FEATURED_SERVERS_API = 'https://assets.authbp.xyz/featured.json';
/**
* Safely escape HTML while preserving UTF-8 characters
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Load and display featured servers
*/
async function loadFeaturedServers() {
const featuredContainer = document.getElementById('featuredServersList');
try {
console.log('[FeaturedServers] Fetching from', FEATURED_SERVERS_API);
// Fetch featured servers from API (no cache)
const response = await fetch(FEATURED_SERVERS_API, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Accept-Charset': 'utf-8'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
const data = JSON.parse(text);
const featuredServers = data.featuredServers || [];
console.log('[FeaturedServers] Loaded', featuredServers.length, 'featured servers');
// Render featured servers
if (featuredServers.length === 0) {
featuredContainer.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-info-circle fa-2x"></i>
<p>No featured servers</p>
</div>
`;
} else {
const featuredHTML = featuredServers.map((server, index) => {
console.log(`[FeaturedServers] Building featured card ${index + 1}:`, server.Name);
const escapedName = escapeHtml(server.Name || 'Unknown Server');
const escapedAddress = escapeHtml(server.Address || '');
const bannerUrl = server.img_Banner || 'https://via.placeholder.com/400x240/1e293b/ffffff?text=Server+Banner';
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">
<img
src="${bannerUrl}"
alt="${escapedName}"
class="featured-server-banner"
onerror="this.src='https://via.placeholder.com/400x240/1e293b/ffffff?text=Server'"
/>
<div class="featured-server-content">
<h3 class="featured-server-name">${escapedName}</h3>
<div class="featured-server-address">
<span class="server-address-text">${escapedAddress}</span>
<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>
`;
}).join('');
featuredContainer.innerHTML = featuredHTML;
}
} catch (error) {
console.error('[FeaturedServers] Error loading servers:', error);
featuredContainer.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-exclamation-triangle fa-2x" style="color: #ef4444;"></i>
<p>Failed to load servers</p>
<p style="font-size: 0.9rem; color: #64748b;">${error.message}</p>
</div>
`;
}
}
/**
* Copy server address to clipboard
*/
async function copyServerAddress(address, button) {
try {
await navigator.clipboard.writeText(address);
// Visual feedback
const originalHTML = button.innerHTML;
button.classList.add('copied');
button.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = originalHTML;
}, 2000);
console.log('[FeaturedServers] Copied address:', address);
} catch (error) {
console.error('[FeaturedServers] Failed to copy address:', error);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = address;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
const originalHTML = button.innerHTML;
button.classList.add('copied');
button.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = originalHTML;
}, 2000);
} catch (err) {
console.error('[FeaturedServers] Fallback copy also failed:', err);
}
document.body.removeChild(textArea);
}
}
/**
* 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) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const featuredPage = document.getElementById('featured-page');
if (featuredPage && featuredPage.classList.contains('active')) {
loadFeaturedServers();
}
}
});
});
const featuredPage = document.getElementById('featured-page');
if (featuredPage) {
observer.observe(featuredPage, { attributes: true });
// Load immediately if already visible
if (featuredPage.classList.contains('active')) {
loadFeaturedServers();
}
}
});

View File

@@ -4,26 +4,12 @@ const i18n = (() => {
let translations = {};
const availableLanguages = [
{ code: 'en', name: 'English' },
{ code: 'de-DE', name: 'German (Germany)' },
{ code: 'es-ES', name: 'Spanish (Spain)' },
{ code: 'fr-FR', name: 'French (France)' },
{ code: 'pl-PL', name: 'Polish (Poland)' },
{ code: 'es-ES', name: 'Español (España)' },
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
{ code: 'ru-RU', name: 'Russian (Russia)' },
{ code: 'sv-SE', name: 'Swedish (Sweden)' },
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
{ code: 'id-ID', name: 'Indonesian (Indonesia)' },
{ code: 'ar-SA', name: 'Arabic (Saudi Arabia)' }
{ code: 'pl-PL', name: 'Polish (Poland)' }
];
// 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;
@@ -82,24 +68,6 @@ 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
@@ -115,8 +83,7 @@ const i18n = (() => {
t,
setLanguage,
getAvailableLanguages: () => availableLanguages,
getCurrentLanguage: () => currentLang,
isRTL
getCurrentLanguage: () => currentLang
};
})();

View File

@@ -45,17 +45,9 @@ export function setupInstallation() {
export async function installGame() {
if (isDownloading || (installBtn && installBtn.disabled)) return;
let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
const installPath = installPathInput ? installPathInput.value.trim() : '';
// Limit player name to 16 characters
if (playerName.length > 16) {
playerName = playerName.substring(0, 16);
if (installPlayerName) {
installPlayerName.value = playerName;
}
}
const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked');
const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release';
@@ -80,11 +72,8 @@ export async function installGame() {
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
// Sync player name to both launcher and settings inputs
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
const settingsPlayerName = document.getElementById('settingsPlayerName');
if (settingsPlayerName) settingsPlayerName.value = playerName;
resetInstallButton();
}, 2000);
}
@@ -136,11 +125,8 @@ function simulateInstallation(playerName) {
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
// Sync player name to both launcher and settings inputs
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
const settingsPlayerName = document.getElementById('settingsPlayerName');
if (settingsPlayerName) settingsPlayerName.value = playerName;
resetInstallButton();
}, 2000);
}
@@ -202,16 +188,7 @@ export async function browseInstallPath() {
async function savePlayerName() {
try {
if (window.electronAPI && window.electronAPI.saveSettings) {
let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
// Limit player name to 16 characters
if (playerName.length > 16) {
playerName = playerName.substring(0, 16);
if (installPlayerName) {
installPlayerName.value = playerName;
}
}
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
await window.electronAPI.saveSettings({ playerName });
}
} catch (error) {
@@ -269,3 +246,9 @@ document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});
window.browseInstallPath = browseInstallPath;
document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});

View File

@@ -35,21 +35,13 @@ export function setupLauncher() {
// Initial Profile Load
loadProfiles();
// Initial Identity Load
loadIdentities();
// Close dropdowns on outside click
// Close dropdown on outside click
document.addEventListener('click', (e) => {
const profileSelector = document.getElementById('profileSelector');
if (profileSelector && !profileSelector.contains(e.target)) {
const selector = document.getElementById('profileSelector');
if (selector && !selector.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');
}
});
}
@@ -91,7 +83,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-sliders-h text-xl text-gray-400"></i>
<i class="fas fa-user-circle text-xl text-gray-400"></i>
<div>
<div class="font-bold">${p.name}</div>
<div class="text-xs text-gray-500">ID: ${p.id.substring(0, 8)}...</div>
@@ -114,6 +106,13 @@ 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) {
@@ -147,7 +146,7 @@ window.createNewProfile = async () => {
};
window.deleteProfile = async (id) => {
if (!confirm('Are you sure you want to delete this configuration? Mod settings will be lost.')) return;
if (!confirm('Are you sure you want to delete this profile? parameters and mods configuration will be lost.')) return;
try {
await window.electronAPI.profile.delete(id);
@@ -161,7 +160,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 configuration...';
const switchingMsg = window.i18n ? window.i18n.t('progress.switchingProfile') : 'Switching Profile...';
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: switchingMsg });
await window.electronAPI.profile.activate(id);
@@ -180,7 +179,7 @@ window.switchProfile = async (id) => {
if (dropdown) dropdown.classList.remove('show');
if (window.LauncherUI) {
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Configuration switched!';
const switchedMsg = window.i18n ? window.i18n.t('progress.profileSwitched') : 'Profile Switched!';
window.LauncherUI.updateProgress({ message: switchedMsg });
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
}
@@ -195,81 +194,27 @@ window.switchProfile = async (id) => {
export async function launch() {
if (isDownloading || (playBtn && playBtn.disabled)) return;
// ==========================================================================
// 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 playerName = 'Player';
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
playerName = window.SettingsAPI.getCurrentPlayerName();
} else if (playerNameInput && playerNameInput.value.trim()) {
playerName = playerNameInput.value.trim();
}
// 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);
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
javaPath = window.SettingsAPI.getCurrentJavaPath();
}
let gpuPreference = 'auto';
try {
if (window.electronAPI && window.electronAPI.loadGpuPreference) {
gpuPreference = await window.electronAPI.loadGpuPreference();
}
} catch (error) {
console.error('[Launcher] Error loading GPU preference:', error);
console.error('Error loading GPU preference:', error);
}
// ==========================================================================
// STEP 3: Start launch process
// ==========================================================================
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
if (playBtn) {
@@ -282,9 +227,8 @@ 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) {
@@ -299,35 +243,7 @@ export async function launch() {
}, 500);
}
} else {
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);
}
}
console.error('Launch failed:', result.error);
}
} else {
isDownloading = false;
@@ -344,13 +260,7 @@ export async function launch() {
window.LauncherUI.hideProgress();
}
resetPlayButton();
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);
}
console.error('Launch error:', error);
}
}
@@ -677,121 +587,6 @@ 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, "&#39;")}')">
<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;

View File

@@ -66,113 +66,6 @@ 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');
@@ -184,8 +77,6 @@ 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

View File

@@ -49,18 +49,6 @@ 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) => {
@@ -90,30 +78,12 @@ 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();
}
}
@@ -122,10 +92,6 @@ function closeMyModsModal() {
const modal = document.getElementById('myModsModal');
if (modal) {
modal.classList.remove('active');
const searchInput = document.getElementById('myModsSearch');
if (searchInput) {
searchInput.value = '';
}
}
}
@@ -147,39 +113,19 @@ 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-${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>
<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>
</div>
`;
if (window.i18n && !isSearching) {
if (window.i18n) {
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');
@@ -219,7 +165,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">${mod.fileName || 'v' + mod.version}</span>
<span class="installed-mod-version">v${mod.version}</span>
</div>
<p class="installed-mod-description">${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}</p>
</div>
@@ -349,6 +295,13 @@ 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) {
@@ -397,12 +350,12 @@ function createBrowseModCard(mod) {
${window.i18n ? window.i18n.t('mods.view') : 'VIEW'}
</button>
${!isInstalled ?
`<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>
`<button id="install-${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>`
}
@@ -411,104 +364,6 @@ 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}...`;
@@ -907,10 +762,7 @@ window.modsManager = {
closeMyModsModal,
viewModPage,
loadInstalledMods,
loadBrowseMods,
openVersionSelectModal
loadBrowseMods
};
// Remove auto-init since we are now calling it from script.js explicitly
// which guarantees order and environment readiness
// document.addEventListener('DOMContentLoaded', initModsManager);
document.addEventListener('DOMContentLoaded', initModsManager);

View File

@@ -2,75 +2,25 @@ import './ui.js';
import './install.js';
import './launcher.js';
import './news.js';
import { initModsManager } from './mods.js';
import './mods.js';
import './players.js';
import './chat.js';
import './settings.js';
import './logs.js';
// Initialize i18n immediately (before DOMContentLoaded)
let i18nInitialized = false;
(async () => {
const savedLang = await window.electronAPI?.loadLanguage();
await i18n.init(savedLang);
i18nInitialized = true;
// Update language selector if DOM is already loaded
if (document.readyState === 'complete' || document.readyState === 'interactive') {
updateLanguageSelector();
initModsManager();
} else {
document.addEventListener('DOMContentLoaded', () => {
updateLanguageSelector();
initModsManager();
});
}
})();
async function checkDiscordPopup() {
try {
const config = await window.electronAPI?.loadConfig();
if (!config || config.discordPopup === undefined || config.discordPopup === false) {
const modal = document.getElementById('discordPopupModal');
if (modal) {
const buttons = modal.querySelectorAll('.discord-popup-btn');
buttons.forEach(btn => btn.disabled = true);
setTimeout(() => {
modal.style.display = 'flex';
modal.classList.add('active');
setTimeout(() => {
buttons.forEach(btn => btn.disabled = false);
}, 2000);
}, 1000);
}
}
} catch (error) {
console.error('Failed to check Discord popup:', error);
}
}
window.closeDiscordPopup = function() {
const modal = document.getElementById('discordPopupModal');
if (modal) {
modal.classList.remove('active');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
}
};
window.joinDiscord = async function() {
// await window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
await window.electronAPI?.openExternal('https://chat.sanhost.net/invite/Tfz4jCK4');
try {
await window.electronAPI?.saveConfig({ discordPopup: true });
} catch (error) {
console.error('Failed to save Discord popup state:', error);
}
closeDiscordPopup();
};
function updateLanguageSelector() {
const langSelect = document.getElementById('languageSelect');
if (langSelect) {
@@ -101,9 +51,32 @@ function updateLanguageSelector() {
}
document.addEventListener('DOMContentLoaded', () => {
// Populate language selector (wait for i18n if needed)
if (i18nInitialized) {
updateLanguageSelector();
}
checkDiscordPopup();
});
// Discord notification
const notification = document.getElementById('discordNotification');
if (notification) {
const dismissed = localStorage.getItem('discordNotificationDismissed');
if (!dismissed) {
setTimeout(() => {
notification.style.display = 'flex';
}, 3000);
} else {
notification.style.display = 'none';
}
}
});
window.closeDiscordNotification = function() {
const notification = document.getElementById('discordNotification');
if (notification) {
notification.classList.add('hidden');
setTimeout(() => {
notification.style.display = 'none';
}, 300);
}
localStorage.setItem('discordNotificationDismissed', 'true');
};

View File

@@ -18,15 +18,10 @@ let regenerateUuidBtn;
let manageUuidsBtn;
let uuidModal;
let uuidModalClose;
let addIdentityBtn;
let uuidAddForm;
let addIdentityUsername;
let addIdentityUuid;
let addIdentityRegenerateBtn;
let addIdentityConfirmBtn;
let addIdentityCancelBtn;
let uuidAdvancedToggle;
let uuidAdvancedContent;
let modalCurrentUuid;
let modalCopyUuidBtn;
let modalRegenerateUuidBtn;
let generateNewUuidBtn;
let uuidList;
let customUuidInput;
let setCustomUuidBtn;
@@ -185,15 +180,10 @@ function setupSettingsElements() {
manageUuidsBtn = document.getElementById('manageUuidsBtn');
uuidModal = document.getElementById('uuidModal');
uuidModalClose = document.getElementById('uuidModalClose');
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');
modalCurrentUuid = document.getElementById('modalCurrentUuid');
modalCopyUuidBtn = document.getElementById('modalCopyUuidBtn');
modalRegenerateUuidBtn = document.getElementById('modalRegenerateUuidBtn');
generateNewUuidBtn = document.getElementById('generateNewUuidBtn');
uuidList = document.getElementById('uuidList');
customUuidInput = document.getElementById('customUuidInput');
setCustomUuidBtn = document.getElementById('setCustomUuidBtn');
@@ -240,24 +230,16 @@ function setupSettingsElements() {
uuidModalClose.addEventListener('click', closeUuidModal);
}
if (addIdentityBtn) {
addIdentityBtn.addEventListener('click', showAddIdentityForm);
if (modalCopyUuidBtn) {
modalCopyUuidBtn.addEventListener('click', copyCurrentUuid);
}
if (addIdentityRegenerateBtn) {
addIdentityRegenerateBtn.addEventListener('click', regenerateAddIdentityUuid);
if (modalRegenerateUuidBtn) {
modalRegenerateUuidBtn.addEventListener('click', regenerateCurrentUuid);
}
if (addIdentityConfirmBtn) {
addIdentityConfirmBtn.addEventListener('click', confirmAddIdentity);
}
if (addIdentityCancelBtn) {
addIdentityCancelBtn.addEventListener('click', hideAddIdentityForm);
}
if (uuidAdvancedToggle) {
uuidAdvancedToggle.addEventListener('click', toggleAdvancedSection);
if (generateNewUuidBtn) {
generateNewUuidBtn.addEventListener('click', generateNewUuid);
}
if (setCustomUuidBtn) {
@@ -457,37 +439,10 @@ async function savePlayerName() {
return;
}
if (playerName.length > 16) {
const msg = window.i18n ? window.i18n.t('notifications.playerNameTooLong') : 'Player name must be 16 characters or less';
showNotification(msg, 'error');
settingsPlayerName.value = playerName.substring(0, 16);
return;
}
const result = await window.electronAPI.saveUsername(playerName);
// Check if save was successful
if (result && result.success === false) {
console.error('[Settings] Failed to save username:', result.error);
const errorMsg = window.i18n
? window.i18n.t('notifications.playerNameSaveFailed')
: `Failed to save player name: ${result.error || 'Unknown error'}`;
showNotification(errorMsg, 'error');
return;
}
await window.electronAPI.saveUsername(playerName);
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';
@@ -590,7 +545,6 @@ async function loadAllSettings() {
await loadLauncherHwAccel();
await loadGpuPreference();
await loadVersionBranch();
await loadWrapperConfigUI();
}
@@ -612,26 +566,11 @@ 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 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';
return 'Player';
}
window.openGameLocation = openGameLocation;
@@ -641,7 +580,6 @@ document.addEventListener('DOMContentLoaded', initSettings);
window.SettingsAPI = {
getCurrentJavaPath,
getCurrentPlayerName,
getCurrentPlayerNameForDisplay,
reloadBranch: loadVersionBranch
};
@@ -651,6 +589,7 @@ async function loadCurrentUuid() {
const uuid = await window.electronAPI.getCurrentUuid();
if (uuid) {
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
}
}
} catch (error) {
@@ -660,7 +599,7 @@ async function loadCurrentUuid() {
async function copyCurrentUuid() {
try {
const uuid = currentUuidDisplay ? currentUuidDisplay.value : null;
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
if (uuid && navigator.clipboard) {
await navigator.clipboard.writeText(uuid);
const msg = window.i18n ? window.i18n.t('notifications.uuidCopied') : 'UUID copied to clipboard!';
@@ -708,13 +647,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');
}
@@ -737,10 +676,6 @@ 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');
@@ -749,7 +684,6 @@ function closeUuidModal() {
}, 300);
}
}
window.closeUuidModal = closeUuidModal;
async function loadAllUuids() {
try {
@@ -788,15 +722,9 @@ 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>` : ''}
@@ -819,119 +747,23 @@ async function loadAllUuids() {
}
}
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() {
async function generateNewUuid() {
try {
if (window.electronAPI && window.electronAPI.generateNewUuid) {
const newUuid = await window.electronAPI.generateNewUuid();
if (newUuid && addIdentityUuid) {
addIdentityUuid.value = newUuid;
}
}
} catch (error) {
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!';
if (newUuid) {
if (customUuidInput) customUuidInput.value = newUuid;
const msg = window.i18n ? window.i18n.t('notifications.uuidGeneratedShort') : 'New UUID generated!';
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';
console.error('Error generating new UUID:', error);
const msg = window.i18n ? window.i18n.t('notifications.uuidGenerateFailed') : 'Failed to generate new UUID';
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()) {
@@ -974,28 +806,18 @@ async function setCustomUuid() {
async function performSetCustomUuid(uuid) {
try {
if (window.electronAPI && window.electronAPI.setUuidForUser) {
// 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 username = getCurrentPlayerName();
const result = await window.electronAPI.setUuidForUser(username, uuid);
if (result.success) {
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
if (customUuidInput) customUuidInput.value = '';
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');
}
@@ -1021,76 +843,6 @@ 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.`;
@@ -1124,7 +876,6 @@ 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');
}
@@ -1383,235 +1134,3 @@ 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;
}
}

View File

@@ -63,10 +63,8 @@ function handleNavigation() {
navItems.forEach(item => {
item.addEventListener('click', () => {
const page = item.getAttribute('data-page');
if (page) {
showPage(`${page}-page`);
setActiveNav(page);
}
showPage(`${page}-page`);
setActiveNav(page);
});
});
}
@@ -79,18 +77,12 @@ 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';
@@ -104,9 +96,6 @@ function setupWindowControls() {
if (profileSelector) {
profileSelector.style.webkitAppRegion = 'no-drag';
}
if (identitySelector) {
identitySelector.style.webkitAppRegion = 'no-drag';
}
}
if (window.electronAPI) {
@@ -854,7 +843,7 @@ function getErrorMessage(technicalMessage, errorType) {
case 'stall':
return 'Download stalled due to slow connection. Please retry.';
case 'file':
return 'Unable to save file. Check permissions. Please retry.';
return 'Unable to save file. Check disk space and permissions. Please retry.';
case 'permission':
return 'Permission denied. Check if launcher has write access. Please retry.';
case 'server':
@@ -983,7 +972,7 @@ function setupRetryButton() {
if (!currentDownloadState.retryData || currentDownloadState.errorType === 'jre') {
currentDownloadState.retryData = {
branch: 'release',
fileName: '7.pwr'
fileName: '4.pwr'
};
console.log('[UI] Created default PWR retry data:', currentDownloadState.retryData);
}
@@ -1051,7 +1040,7 @@ function setupRetryButton() {
} else {
currentDownloadState.retryData = {
branch: 'release',
fileName: '7.pwr'
fileName: '4.pwr'
};
}
console.log('[UI] Created default retry data:', currentDownloadState.retryData);
@@ -1111,57 +1100,7 @@ function getRetryContextMessage() {
}
}
window.openDiscordExternal = function() {
// window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
window.electronAPI?.openExternal('https://chat.sanhost.net/invite/Tfz4jCK4');
};
// Make toggleMaximize globally available
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);

View File

@@ -6,12 +6,12 @@ class ClientUpdateManager {
}
init() {
console.log('🔧 ClientUpdateManager initializing...');
window.electronAPI.onUpdatePopup((updateInfo) => {
this.showUpdatePopup(updateInfo);
});
// Listen for electron-updater events from main.js
// This is the primary update trigger - main.js checks for updates on startup
// Listen for electron-updater events
window.electronAPI.onUpdateAvailable((updateInfo) => {
console.log('📥 update-available event received:', updateInfo);
this.showUpdatePopup(updateInfo);
});
@@ -20,30 +20,18 @@ class ClientUpdateManager {
});
window.electronAPI.onUpdateDownloaded((updateInfo) => {
console.log('📦 update-downloaded event received:', updateInfo);
this.showUpdateDownloaded(updateInfo);
});
window.electronAPI.onUpdateError((errorInfo) => {
console.log('❌ update-error event received:', errorInfo);
this.handleUpdateError(errorInfo);
});
console.log('✅ ClientUpdateManager initialized');
// Note: Don't call checkForUpdatesOnDemand() here - main.js already checks
// for updates after 3 seconds and sends 'update-available' event.
// Calling it here would cause duplicate popups.
this.checkForUpdatesOnDemand();
}
showUpdatePopup(updateInfo) {
console.log('🔔 showUpdatePopup called, updatePopupVisible:', this.updatePopupVisible);
// Check if popup already exists in DOM (extra safety)
if (this.updatePopupVisible || document.getElementById('update-popup-overlay')) {
console.log('⚠️ Update popup already visible, skipping');
return;
}
if (this.updatePopupVisible) return;
this.updatePopupVisible = true;
@@ -104,10 +92,7 @@ class ClientUpdateManager {
</div>
<div class="update-popup-footer">
<span id="update-footer-text">Downloading update...</span>
<button id="update-skip-btn" class="update-skip-btn" style="display: none; margin-top: 0.5rem; background: transparent; border: 1px solid rgba(255,255,255,0.2); color: #9ca3af; padding: 0.5rem 1rem; border-radius: 0.25rem; cursor: pointer; font-size: 0.75rem;">
Skip for now (not recommended)
</button>
This popup cannot be closed until you update the launcher
</div>
</div>
</div>
@@ -128,43 +113,16 @@ class ClientUpdateManager {
installBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
installBtn.disabled = true;
installBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Installing...';
try {
await window.electronAPI.quitAndInstallUpdate();
// If we're still here after 5 seconds, the install probably failed
setTimeout(() => {
console.log('⚠️ Install may have failed - showing skip option');
installBtn.disabled = false;
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Again';
// Show skip button
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Install not working? Skip for now:';
}
}
}, 5000);
} catch (error) {
console.error('❌ Error installing update:', error);
installBtn.disabled = false;
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Install & Restart';
// Show skip button on error
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Install failed. Skip for now:';
}
}
}
});
}
@@ -180,15 +138,10 @@ class ClientUpdateManager {
try {
await window.electronAPI.openDownloadPage();
console.log('✅ Download page opened');
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Opened in browser';
// Close the popup after opening download page
setTimeout(() => {
this.closeUpdatePopup();
}, 1500);
console.log('✅ Download page opened, launcher will close...');
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Launcher closing...';
} catch (error) {
console.error('❌ Error opening download page:', error);
downloadBtn.disabled = false;
@@ -208,39 +161,9 @@ class ClientUpdateManager {
});
}
// Show skip button after 30 seconds as fallback (in case update is stuck)
setTimeout(() => {
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Update taking too long?';
}
}
}, 30000);
const skipBtn = document.getElementById('update-skip-btn');
if (skipBtn) {
skipBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.closeUpdatePopup();
});
}
console.log('🔔 Update popup displayed with new style');
}
closeUpdatePopup() {
const overlay = document.getElementById('update-popup-overlay');
if (overlay) {
overlay.remove();
}
this.updatePopupVisible = false;
this.unblockInterface();
}
updateDownloadProgress(progress) {
const progressBar = document.getElementById('update-progress-bar');
const progressPercent = document.getElementById('update-progress-percent');
@@ -274,96 +197,35 @@ class ClientUpdateManager {
const statusText = document.getElementById('update-status-text');
const progressContainer = document.getElementById('update-progress-container');
const buttonsContainer = document.getElementById('update-buttons-container');
const installBtn = document.getElementById('update-install-btn');
const downloadBtn = document.getElementById('update-download-btn');
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
const popupContainer = document.querySelector('.update-popup-container');
// Remove breathing/pulse animation when download is complete
if (popupContainer) {
popupContainer.classList.remove('update-popup-pulse');
if (statusText) {
statusText.textContent = 'Update downloaded! Ready to install.';
}
if (progressContainer) {
progressContainer.style.display = 'none';
}
// Use platform info from main process if available, fallback to browser detection
const autoInstallSupported = updateInfo.autoInstallSupported !== undefined
? updateInfo.autoInstallSupported
: navigator.platform.toUpperCase().indexOf('MAC') < 0;
if (!autoInstallSupported) {
// macOS: Show manual download as primary since auto-update doesn't work
if (statusText) {
statusText.textContent = 'Update downloaded but auto-install may not work on macOS.';
}
if (installBtn) {
// Still show install button but as secondary option
installBtn.classList.add('update-download-btn-secondary');
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Install & Restart';
}
if (downloadBtn) {
// Make manual download primary
downloadBtn.classList.remove('update-download-btn-secondary');
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Manually (Recommended)';
}
if (footerText) {
footerText.textContent = 'Auto-install often fails on macOS:';
}
} else {
// Windows/Linux: Auto-install should work
if (statusText) {
statusText.textContent = 'Update downloaded! Ready to install.';
}
if (footerText) {
footerText.textContent = 'Click to install the update:';
}
}
if (buttonsContainer) {
buttonsContainer.style.display = 'block';
}
// Always show skip button in downloaded state
if (skipBtn) {
skipBtn.style.display = 'inline-block';
console.log('✅ Skip button made visible');
} else {
console.error('❌ Skip button not found in DOM!');
}
console.log('✅ Update downloaded, ready to install. autoInstallSupported:', autoInstallSupported);
console.log('✅ Update downloaded, ready to install');
}
handleUpdateError(errorInfo) {
console.error('Update error:', errorInfo);
// Show skip button immediately on any error
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Update failed. You can skip for now.';
}
}
// If manual download is required, update the UI (this will handle status text)
if (errorInfo.requiresManualDownload) {
this.showManualDownloadRequired(errorInfo);
return; // Don't do anything else, showManualDownloadRequired handles everything
}
// For non-critical errors, just show error message without changing status
const errorMessage = document.getElementById('update-error-message');
const errorText = document.getElementById('update-error-text');
if (errorMessage && errorText) {
let message = errorInfo.message || 'An error occurred during the update process.';
if (errorInfo.isMacSigningError) {
@@ -427,16 +289,6 @@ class ClientUpdateManager {
buttonsContainer.style.display = 'block';
}
// Show skip button for manual download errors
const skipBtn = document.getElementById('update-skip-btn');
const footerText = document.getElementById('update-footer-text');
if (skipBtn) {
skipBtn.style.display = 'inline-block';
if (footerText) {
footerText.textContent = 'Or continue without updating:';
}
}
console.log('⚠️ Manual download required due to update error');
}
@@ -448,35 +300,13 @@ class ClientUpdateManager {
document.body.classList.add('no-select');
// Store bound functions so we can remove them later
this._boundBlockKeyEvents = this.blockKeyEvents.bind(this);
this._boundBlockContextMenu = this.blockContextMenu.bind(this);
document.addEventListener('keydown', this._boundBlockKeyEvents, true);
document.addEventListener('contextmenu', this._boundBlockContextMenu, true);
document.addEventListener('keydown', this.blockKeyEvents.bind(this), true);
document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true);
console.log('🚫 Interface blocked for update');
}
unblockInterface() {
const mainContent = document.querySelector('.flex.w-full.h-screen');
if (mainContent) {
mainContent.classList.remove('interface-blocked');
}
document.body.classList.remove('no-select');
// Remove event listeners
if (this._boundBlockKeyEvents) {
document.removeEventListener('keydown', this._boundBlockKeyEvents, true);
}
if (this._boundBlockContextMenu) {
document.removeEventListener('contextmenu', this._boundBlockContextMenu, true);
}
console.log('✅ Interface unblocked');
}
blockKeyEvents(event) {
if (event.target.closest('#update-popup-overlay')) {
if ((event.key === 'Enter' || event.key === ' ') &&

View File

@@ -1,271 +0,0 @@
{
"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": "اكتمل التثبيت!"
}
}

View File

@@ -1,271 +0,0 @@
{
"nav": {
"play": "Spielen",
"mods": "Mods",
"news": "Neuigkeiten",
"chat": "Spieler-Chat",
"settings": "Einstellungen"
},
"header": {
"playersLabel": "Spieler:",
"manageProfiles": "Verwalten",
"manageIdentities": "Verwalten",
"identityTooltip": "Dein Spielername und UUID im Spiel",
"configTooltip": "Spielkonfiguration: Mods, Java- und Speichereinstellungen",
"defaultProfile": "Standard"
},
"install": {
"title": "KOSTENLOSER LAUNCHER",
"playerName": "Spielername",
"playerNamePlaceholder": "Namen eingeben",
"gameBranch": "Spielversion",
"releaseVersion": "Release (Stabil)",
"preReleaseVersion": "Pre-Release (Experimentell)",
"customInstallation": "Benutzerdefinierte Installation",
"installationFolder": "Installationsordner",
"pathPlaceholder": "Standardspeicherort",
"browse": "Durchsuchen",
"installButton": "HYTALE INSTALLIEREN",
"installing": "INSTALLIERE..."
},
"play": {
"ready": "BEREIT ZUM SPIELEN",
"subtitle": "Starte Hytale und beginne das Abenteuer",
"playButton": "HYTALE SPIELEN",
"latestNews": "NEUESTE NACHRICHTEN",
"viewAll": "ALLE ANZEIGEN",
"checking": "ÜBERPRÜFE...",
"play": "SPIELEN"
},
"mods": {
"searchPlaceholder": "Mods suchen...",
"myMods": "MEINE MODS",
"previous": "ZURÜCK",
"next": "WEITER",
"page": "Seite",
"of": "von",
"modalTitle": "MEINE MODS",
"noModsFound": "Keine Mods gefunden",
"noModsFoundDesc": "Versuche deine Suche anzupassen",
"noModsInstalled": "Keine Mods installiert",
"noModsInstalledDesc": "Füge Mods von CurseForge hinzu oder importiere lokale Dateien",
"view": "ANZEIGEN",
"install": "INSTALLIEREN",
"installed": "INSTALLIERT",
"enable": "AKTIVIEREN",
"disable": "DEAKTIVIEREN",
"active": "AKTIV",
"disabled": "DEAKTIVIERT",
"delete": "Mod löschen",
"noDescription": "Keine Beschreibung verfügbar",
"confirmDelete": "Möchtest du \"{name}\" wirklich löschen?",
"confirmDeleteDesc": "Diese Aktion kann nicht rückgängig gemacht werden.",
"confirmDeletion": "Löschung bestätigen",
"apiKeyRequired": "API-Schlüssel erforderlich",
"apiKeyRequiredDesc": "CurseForge API-Schlüssel wird benötigt, um Mods zu durchsuchen"
},
"news": {
"title": "ALLE NACHRICHTEN",
"readMore": "Mehr lesen"
},
"chat": {
"title": "SPIELER-CHAT",
"pickColor": "Farbe",
"inputPlaceholder": "Nachricht eingeben...",
"send": "Senden",
"online": "online",
"charCounter": "{current}/{max}",
"secureChat": "Sicherer Chat - Links werden zensiert",
"joinChat": "Chat beitreten",
"chooseUsername": "Wähle einen Benutzernamen, um dem Spieler-Chat beizutreten",
"username": "Benutzername",
"usernamePlaceholder": "Benutzernamen eingeben...",
"usernameHint": "3-20 Zeichen, nur Buchstaben, Zahlen, - und _",
"joinButton": "Chat beitreten",
"colorModal": {
"title": "Benutzernamenfarbe anpassen",
"chooseSolid": "Wähle eine einfarbige Farbe:",
"customColor": "Benutzerdefinierte Farbe:",
"preview": "Vorschau:",
"previewUsername": "Benutzername",
"apply": "Farbe anwenden"
}
},
"settings": {
"title": "EINSTELLUNGEN",
"java": "Java Runtime",
"useCustomJava": "Benutzerdefinierten Java-Pfad verwenden",
"javaDescription": "Ersetze die mitgelieferte Java-Installation durch deine eigene",
"javaPath": "Java-Ausführungsdatei-Pfad",
"javaPathPlaceholder": "Java-Pfad auswählen...",
"javaBrowse": "Durchsuchen",
"javaHint": "Wähle den Java-Installationsordner (unterstützt Windows, Mac, Linux)",
"discord": "Discord-Integration",
"enableRPC": "Discord Rich Presence aktivieren",
"discordDescription": "Zeige deine Launcher-Aktivität auf Discord",
"game": "Spieloptionen",
"playerName": "Spielername",
"playerNamePlaceholder": "Spielernamen eingeben",
"playerNameHint": "Dieser Name wird im Spiel verwendet (1-16 Zeichen)",
"openGameLocation": "Spielordner öffnen",
"openGameLocationDesc": "Öffne den Spielinstallationsordner",
"account": "Spieler-UUID-Verwaltung",
"currentUUID": "Aktuelle UUID",
"uuidPlaceholder": "UUID wird geladen...",
"copyUUID": "UUID kopieren",
"regenerateUUID": "UUID neu generieren",
"uuidHint": "Deine eindeutige Spielerkennung für diesen Benutzernamen",
"manageUUIDs": "Alle UUIDs verwalten",
"manageUUIDsDesc": "Alle Spieler-UUIDs anzeigen und verwalten",
"language": "Sprache",
"selectLanguage": "Sprache auswählen",
"repairGame": "Spiel reparieren",
"reinstallGame": "Spieldateien neu installieren (behält Daten)",
"gpuPreference": "GPU-Präferenz",
"gpuHint": "Funktion nur für Laptops; auf „Integriert“ stellen, wenn auf einem PC.",
"gpuAuto": "Auto",
"gpuIntegrated": "Integriert",
"gpuDedicated": "Dediziert",
"logs": "SYSTEMPROTOKOLLE",
"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",
"closeOnStartDescription": "Schließe den Launcher automatisch, nachdem Hytale gestartet wurde",
"hwAccel": "Hardware-Beschleunigung",
"hwAccelDescription": "Hardware-Beschleunigung für den Launcher aktivieren",
"gameBranch": "Spiel-Branch",
"branchRelease": "Release",
"branchPreRelease": "Pre-Release",
"branchHint": "Wechsel zwischen stabiler Release- und experimenteller Pre-Release-Version",
"branchWarning": "Das Ändern des Branches lädt eine andere Spielversion herunter und installiert sie",
"branchSwitching": "Wechsle zu {branch}...",
"branchSwitched": "Erfolgreich zu {branch} gewechselt!",
"installRequired": "Installation erforderlich",
"branchInstallConfirm": "Das Spiel wird für den {branch}-Branch installiert. Fortfahren?"
},
"uuid": {
"modalTitle": "UUID-Verwaltung",
"allPlayerUUIDs": "Alle Spieler-UUIDs",
"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)",
"setUUID": "UUID festlegen",
"warning": "Warnung: Das Festlegen einer benutzerdefinierten UUID ändert deine aktuelle Spieleridentität",
"copyTooltip": "UUID kopieren",
"regenerateTooltip": "Neue UUID generieren"
},
"configurations": {
"modalTitle": "Konfigurationen verwalten",
"newProfilePlaceholder": "Neuer Konfigurationsname",
"createProfile": "Konfiguration erstellen"
},
"discord": {
"notificationText": "Tritt unserer Discord-Community bei!",
"joinButton": "Discord beitreten"
},
"common": {
"confirm": "Bestätigen",
"cancel": "Abbrechen",
"save": "Speichern",
"close": "Schließen",
"delete": "Löschen",
"edit": "Bearbeiten",
"loading": "Lädt...",
"apply": "Anwenden",
"install": "Installieren"
},
"notifications": {
"gameDataNotFound": "Fehler: Spieldaten nicht gefunden",
"gameUpdatedSuccess": "Spiel erfolgreich aktualisiert! 🎉",
"updateFailed": "Update fehlgeschlagen: {error}",
"updateError": "Update-Fehler: {error}",
"discordEnabled": "Discord Rich Presence aktiviert",
"discordDisabled": "Discord Rich Presence deaktiviert",
"discordSaveFailed": "Discord-Einstellung konnte nicht gespeichert werden",
"playerNameRequired": "Bitte gib einen gültigen Spielernamen ein",
"playerNameSaved": "Spielername erfolgreich gespeichert",
"playerNameSaveFailed": "Spielername konnte nicht gespeichert werden",
"uuidCopied": "UUID in die Zwischenablage kopiert!",
"uuidCopyFailed": "UUID konnte nicht kopiert werden",
"uuidRegenNotAvailable": "UUID-Neugenerierung nicht verfügbar",
"uuidRegenFailed": "UUID konnte nicht neu generiert werden",
"uuidGenerated": "Neue UUID erfolgreich generiert!",
"uuidGeneratedShort": "Neue UUID generiert!",
"uuidGenerateFailed": "Neue UUID konnte nicht generiert werden",
"uuidRequired": "Bitte gib eine UUID ein",
"uuidInvalidFormat": "Ungültiges UUID-Format",
"uuidSetFailed": "Benutzerdefinierte UUID konnte nicht festgelegt werden",
"uuidSetSuccess": "Benutzerdefinierte UUID erfolgreich festgelegt!",
"uuidDeleteFailed": "UUID konnte nicht gelöscht werden",
"uuidDeleteSuccess": "UUID erfolgreich gelöscht!",
"modsDownloading": "{name} wird heruntergeladen...",
"modsTogglingMod": "Mod wird umgeschaltet...",
"modsDeletingMod": "Mod wird gelöscht...",
"modsLoadingMods": "Mods von CurseForge werden geladen...",
"modsInstalledSuccess": "{name} erfolgreich installiert! 🎉",
"modsDeletedSuccess": "{name} erfolgreich gelöscht",
"modsDownloadFailed": "Mod konnte nicht heruntergeladen werden: {error}",
"modsToggleFailed": "Mod konnte nicht umgeschaltet werden: {error}",
"modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}",
"modsModNotFound": "Mod-Informationen nicht gefunden",
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden",
"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",
"regenerateUuidTitle": "Neue UUID generieren",
"regenerateUuidMessage": "Möchtest du wirklich eine neue UUID generieren? Dies ändert deine Spieleridentität.",
"regenerateUuidButton": "Generieren",
"setCustomUuidTitle": "Benutzerdefinierte UUID festlegen",
"setCustomUuidMessage": "Möchtest du wirklich diese benutzerdefinierte UUID festlegen? Dies ändert deine Spieleridentität.",
"setCustomUuidButton": "UUID festlegen",
"deleteUuidTitle": "UUID löschen",
"deleteUuidMessage": "Möchtest du wirklich die UUID für \"{username}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"deleteUuidButton": "Löschen",
"uninstallGameTitle": "Spiel deinstallieren",
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
"uninstallGameButton": "Deinstallieren",
"switchUsernameTitle": "Identität wechseln",
"switchUsernameMessage": "Zu Benutzername \"{username}\" wechseln? Dies ändert deine aktuelle Spieleridentität.",
"switchUsernameButton": "Wechseln"
},
"progress": {
"initializing": "Initialisiere...",
"downloading": "Lädt herunter...",
"installing": "Installiere...",
"extracting": "Entpacke...",
"verifying": "Überprüfe...",
"switchingProfile": "Konfiguration wird gewechselt...",
"profileSwitched": "Konfiguration gewechselt!",
"startingGame": "Spiel wird gestartet...",
"launching": "STARTET...",
"uninstallingGame": "Spiel wird deinstalliert...",
"gameUninstalled": "Spiel erfolgreich deinstalliert!",
"uninstallFailed": "Deinstallation fehlgeschlagen: {error}",
"startingUpdate": "Obligatorisches Spiel-Update wird gestartet...",
"installationComplete": "Installation erfolgreich abgeschlossen!",
"installationFailed": "Installation fehlgeschlagen: {error}",
"installingGameFiles": "Spieldateien werden installiert...",
"installComplete": "Installation abgeschlossen!"
}
}

View File

@@ -8,10 +8,7 @@
},
"header": {
"playersLabel": "Players:",
"manageProfiles": "Manage",
"manageIdentities": "Manage",
"identityTooltip": "Your player name & UUID used in-game",
"configTooltip": "Game config: mods, Java & memory settings",
"manageProfiles": "Manage Profiles",
"defaultProfile": "Default"
},
"install": {
@@ -122,7 +119,7 @@
"repairGame": "Repair Game",
"reinstallGame": "Reinstall game files (preserves data)",
"gpuPreference": "GPU Preference",
"gpuHint": "Laptop-only feature; set to Integrated if on PC",
"gpuHint": "Select your preferred GPU (Linux: affects DRI_PRIME)",
"gpuAuto": "Auto",
"gpuIntegrated": "Integrated",
"gpuDedicated": "Dedicated",
@@ -130,12 +127,6 @@
"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",
@@ -150,39 +141,25 @@
"branchSwitching": "Switching to {branch}...",
"branchSwitched": "Switched to {branch} successfully!",
"installRequired": "Installation Required",
"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"
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?"
},
"uuid": {
"modalTitle": "UUID Management",
"currentUserUUID": "Current User UUID",
"allPlayerUUIDs": "All Player UUIDs",
"addIdentity": "Add Identity",
"usernamePlaceholder": "Username",
"add": "Add",
"cancel": "Cancel",
"advanced": "Advanced",
"generateNew": "Generate New UUID",
"loadingUUIDs": "Loading UUIDs...",
"setCustomUUID": "Set Custom UUID for Current User",
"setCustomUUID": "Set Custom UUID",
"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"
},
"configurations": {
"modalTitle": "Manage Configurations",
"newProfilePlaceholder": "New Configuration Name",
"createProfile": "Create Configuration"
"profiles": {
"modalTitle": "Manage Profiles",
"newProfilePlaceholder": "New Profile Name",
"createProfile": "Create Profile"
},
"discord": {
"notificationText": "Join our Discord community!",
@@ -234,19 +211,7 @@
"modsDeleteFailed": "Failed to delete mod: {error}",
"modsModNotFound": "Mod information not found",
"hwAccelSaved": "Hardware acceleration setting saved",
"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"
"hwAccelSaveFailed": "Failed to save hardware acceleration setting"
},
"confirm": {
"defaultTitle": "Confirm action",
@@ -261,12 +226,7 @@
"deleteUuidButton": "Delete",
"uninstallGameTitle": "Uninstall game",
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
"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."
"uninstallGameButton": "Uninstall"
},
"progress": {
"initializing": "Initializing...",
@@ -274,8 +234,8 @@
"installing": "Installing...",
"extracting": "Extracting...",
"verifying": "Verifying...",
"switchingProfile": "Switching configuration...",
"profileSwitched": "Configuration switched!",
"switchingProfile": "Switching profile...",
"profileSwitched": "Profile switched!",
"startingGame": "Starting game...",
"launching": "LAUNCHING...",
"uninstallingGame": "Uninstalling game...",

View File

@@ -8,10 +8,7 @@
},
"header": {
"playersLabel": "Jugadores:",
"manageProfiles": "Gestionar",
"manageIdentities": "Gestionar",
"identityTooltip": "Tu nombre de jugador y UUID usados en el juego",
"configTooltip": "Configuración del juego: mods, Java y memoria",
"manageProfiles": "Gestionar Perfiles",
"defaultProfile": "Predeterminado"
},
"install": {
@@ -122,7 +119,7 @@
"repairGame": "Reparar juego",
"reinstallGame": "Reinstalar archivos del juego (conserva los datos)",
"gpuPreference": "Preferencia de GPU",
"gpuHint": "Función exclusiva para computadora portátil; configúrela como Integrada si está en una PC",
"gpuHint": "Selecciona tu GPU preferida (Linux: afecta DRI_PRIME)",
"gpuAuto": "Automático",
"gpuIntegrated": "Integrada",
"gpuDedicated": "Dedicada",
@@ -130,18 +127,10 @@
"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",
"closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado",
"hwAccel": "Aceleración por Hardware",
"hwAccelDescription": "Habilitar aceleración por hardware para el launcher",
"gameBranch": "Rama del Juego",
"branchRelease": "Lanzamiento",
"branchPreRelease": "Pre-Lanzamiento",
@@ -154,12 +143,9 @@
},
"uuid": {
"modalTitle": "Gestión de UUID",
"currentUserUUID": "UUID del usuario actual",
"allPlayerUUIDs": "Todos los UUIDs de jugadores",
"addIdentity": "Añadir identidad",
"usernamePlaceholder": "Nombre de usuario",
"add": "Añadir",
"cancel": "Cancelar",
"advanced": "Avanzado",
"generateNew": "Generar nuevo UUID",
"loadingUUIDs": "Cargando UUIDs...",
"setCustomUUID": "Establecer UUID personalizado",
"customPlaceholder": "Ingresa un UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
@@ -168,10 +154,10 @@
"copyTooltip": "Copiar UUID",
"regenerateTooltip": "Generar nuevo UUID"
},
"configurations": {
"modalTitle": "Gestionar Configuraciones",
"newProfilePlaceholder": "Nombre de la nueva configuración",
"createProfile": "Crear Configuración"
"profiles": {
"modalTitle": "Gestionar perfiles",
"newProfilePlaceholder": "Nombre del nuevo perfil",
"createProfile": "Crear perfil"
},
"discord": {
"notificationText": "¡Únete a nuestra comunidad de Discord!",
@@ -221,15 +207,7 @@
"modsDownloadFailed": "Error al descargar mod: {error}",
"modsToggleFailed": "Error al alternar mod: {error}",
"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",
"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"
"modsModNotFound": "Información del mod no encontrada"
},
"confirm": {
"defaultTitle": "Confirmar acción",
@@ -244,10 +222,7 @@
"deleteUuidButton": "Eliminar",
"uninstallGameTitle": "Desinstalar juego",
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
"uninstallGameButton": "Desinstalar",
"switchUsernameTitle": "Cambiar identidad",
"switchUsernameMessage": "¿Cambiar al nombre de usuario \"{username}\"? Esto cambiará tu identidad de jugador actual.",
"switchUsernameButton": "Cambiar"
"uninstallGameButton": "Desinstalar"
},
"progress": {
"initializing": "Inicializando...",
@@ -255,8 +230,8 @@
"installing": "Instalando...",
"extracting": "Extrayendo...",
"verifying": "Verificando...",
"switchingProfile": "Cambiando configuración...",
"profileSwitched": Configuración cambiada!",
"switchingProfile": "Cambiando perfil...",
"profileSwitched": Perfil cambiado!",
"startingGame": "Iniciando juego...",
"launching": "INICIANDO...",
"uninstallingGame": "Desinstalando juego...",
@@ -268,4 +243,4 @@
"installingGameFiles": "Instalando archivos del juego...",
"installComplete": "¡Instalación completa!"
}
}
}

View File

@@ -1,271 +0,0 @@
{
"nav": {
"play": "Jouer",
"mods": "Mods",
"news": "Actualités",
"chat": "Chat Joueurs",
"settings": "Paramètres"
},
"header": {
"playersLabel": "Joueurs:",
"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": {
"title": "LAUNCHER GRATUIT",
"playerName": "Nom du Joueur",
"playerNamePlaceholder": "Entrez votre nom",
"gameBranch": "Version du Jeu",
"releaseVersion": "Release (Stable)",
"preReleaseVersion": "Pré-Release (Expérimental)",
"customInstallation": "Installation Personnalisée",
"installationFolder": "Dossier d'Installation",
"pathPlaceholder": "Emplacement par défaut",
"browse": "Parcourir",
"installButton": "INSTALLER HYTALE",
"installing": "INSTALLATION..."
},
"play": {
"ready": "PRÊT À JOUER",
"subtitle": "Lancez Hytale et entrez dans l'aventure",
"playButton": "JOUER À HYTALE",
"latestNews": "DERNIÈRES ACTUALITÉS",
"viewAll": "VOIR TOUT",
"checking": "VÉRIFICATION...",
"play": "JOUER"
},
"mods": {
"searchPlaceholder": "Rechercher des mods...",
"myMods": "MES MODS",
"previous": "PRÉCÉDENT",
"next": "SUIVANT",
"page": "Page",
"of": "sur",
"modalTitle": "MES MODS",
"noModsFound": "Aucun Mod Trouvé",
"noModsFoundDesc": "Essayez d'ajuster votre recherche",
"noModsInstalled": "Aucun Mod Installé",
"noModsInstalledDesc": "Ajoutez des mods depuis CurseForge ou importez des fichiers locaux",
"view": "VOIR",
"install": "INSTALLER",
"installed": "INSTALLÉ",
"enable": "ACTIVER",
"disable": "DÉSACTIVER",
"active": "ACTIF",
"disabled": "DÉSACTIVÉ",
"delete": "Supprimer le mod",
"noDescription": "Aucune description disponible",
"confirmDelete": "Êtes-vous sûr de vouloir supprimer \"{name}\" ?",
"confirmDeleteDesc": "Cette action est irréversible.",
"confirmDeletion": "Confirmer la Suppression",
"apiKeyRequired": "Clé API Requise",
"apiKeyRequiredDesc": "Une clé API CurseForge est nécessaire pour parcourir les mods"
},
"news": {
"title": "TOUTES LES ACTUALITÉS",
"readMore": "Lire Plus"
},
"chat": {
"title": "CHAT JOUEURS",
"pickColor": "Couleur",
"inputPlaceholder": "Tapez votre message...",
"send": "Envoyer",
"online": "en ligne",
"charCounter": "{current}/{max}",
"secureChat": "Chat sécurisé - Les liens sont censurés",
"joinChat": "Rejoindre le Chat",
"chooseUsername": "Choisissez un nom d'utilisateur pour rejoindre le Chat Joueurs",
"username": "Nom d'utilisateur",
"usernamePlaceholder": "Entrez votre nom d'utilisateur...",
"usernameHint": "3-20 caractères, lettres, chiffres, - et _ uniquement",
"joinButton": "Rejoindre le Chat",
"colorModal": {
"title": "Personnaliser la Couleur du Nom",
"chooseSolid": "Choisissez une couleur unie:",
"customColor": "Couleur personnalisée:",
"preview": "Aperçu:",
"previewUsername": "Nom d'utilisateur",
"apply": "Appliquer la Couleur"
}
},
"settings": {
"title": "PARAMÈTRES",
"java": "Java Runtime",
"useCustomJava": "Utiliser un Chemin Java Personnalisé",
"javaDescription": "Remplacer le Java intégré par votre propre installation",
"javaPath": "Chemin de l'Exécutable Java",
"javaPathPlaceholder": "Sélectionnez le chemin Java...",
"javaBrowse": "Parcourir",
"javaHint": "Sélectionnez le dossier d'installation de Java (compatible Windows, Mac, Linux)",
"discord": "Intégration Discord",
"enableRPC": "Activer Discord Rich Presence",
"discordDescription": "Afficher votre activité du launcher sur Discord",
"game": "Options de Jeu",
"playerName": "Nom du Joueur",
"playerNamePlaceholder": "Entrez le nom du joueur",
"playerNameHint": "Ce nom sera utilisé en jeu (1-16 caractères)",
"openGameLocation": "Ouvrir l'Emplacement du Jeu",
"openGameLocationDesc": "Ouvrir le dossier d'installation du jeu",
"account": "Gestion UUID Joueur",
"currentUUID": "UUID Actuel",
"uuidPlaceholder": "Chargement UUID...",
"copyUUID": "Copier UUID",
"regenerateUUID": "Régénérer UUID",
"uuidHint": "Votre identifiant unique de joueur pour ce nom d'utilisateur",
"manageUUIDs": "Gérer Tous les UUIDs",
"manageUUIDsDesc": "Voir et gérer tous les UUIDs de joueurs",
"language": "Langue",
"selectLanguage": "Sélectionner la Langue",
"repairGame": "Réparer le Jeu",
"reinstallGame": "Réinstaller les fichiers du jeu (préserve les données)",
"gpuPreference": "Préférence GPU",
"gpuHint": "Fonctionnalité exclusive aux ordinateurs portables; à définir sur Intégré sur PC",
"gpuAuto": "Auto",
"gpuIntegrated": "Intégré",
"gpuDedicated": "Dédié",
"logs": "JOURNAUX SYSTÈME",
"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",
"closeOnStartDescription": "Fermer automatiquement le launcher après le lancement d'Hytale",
"hwAccel": "Accélération Matérielle",
"hwAccelDescription": "Activer l'accélération matérielle pour le launcher",
"gameBranch": "Branche du Jeu",
"branchRelease": "Release",
"branchPreRelease": "Pré-Release",
"branchHint": "Basculer entre la version stable release et la pré-release expérimentale",
"branchWarning": "Changer de branche téléchargera et installera une version différente du jeu",
"branchSwitching": "Passage à {branch}...",
"branchSwitched": "Passage à {branch} réussi!",
"installRequired": "Installation Requise",
"branchInstallConfirm": "Le jeu sera installé pour la branche {branch}. Continuer?"
},
"uuid": {
"modalTitle": "Gestion UUID",
"allPlayerUUIDs": "Tous les UUIDs Joueurs",
"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)",
"setUUID": "Définir UUID",
"warning": "Attention: Définir un UUID personnalisé changera votre identité de joueur actuelle",
"copyTooltip": "Copier UUID",
"regenerateTooltip": "Générer Nouvel UUID"
},
"configurations": {
"modalTitle": "Gérer les Configurations",
"newProfilePlaceholder": "Nom de la Nouvelle Configuration",
"createProfile": "Créer une Configuration"
},
"discord": {
"notificationText": "Rejoignez notre communauté Discord!",
"joinButton": "Rejoindre Discord"
},
"common": {
"confirm": "Confirmer",
"cancel": "Annuler",
"save": "Sauvegarder",
"close": "Fermer",
"delete": "Supprimer",
"edit": "Modifier",
"loading": "Chargement...",
"apply": "Appliquer",
"install": "Installer"
},
"notifications": {
"gameDataNotFound": "Erreur: Données du jeu introuvables",
"gameUpdatedSuccess": "Jeu mis à jour avec succès! 🎉",
"updateFailed": "Mise à jour échouée: {error}",
"updateError": "Erreur de mise à jour: {error}",
"discordEnabled": "Discord Rich Presence activé",
"discordDisabled": "Discord Rich Presence désactivé",
"discordSaveFailed": "Échec de la sauvegarde des paramètres Discord",
"playerNameRequired": "Veuillez entrer un nom de joueur valide",
"playerNameSaved": "Nom du joueur sauvegardé avec succès",
"playerNameSaveFailed": "Échec de la sauvegarde du nom du joueur",
"uuidCopied": "UUID copié dans le presse-papiers!",
"uuidCopyFailed": "Échec de la copie de l'UUID",
"uuidRegenNotAvailable": "Régénération UUID non disponible",
"uuidRegenFailed": "Échec de la régénération de l'UUID",
"uuidGenerated": "Nouvel UUID généré avec succès!",
"uuidGeneratedShort": "Nouvel UUID généré!",
"uuidGenerateFailed": "Échec de la génération du nouvel UUID",
"uuidRequired": "Veuillez entrer un UUID",
"uuidInvalidFormat": "Format UUID invalide",
"uuidSetFailed": "Échec de la définition de l'UUID personnalisé",
"uuidSetSuccess": "UUID personnalisé défini avec succès!",
"uuidDeleteFailed": "Échec de la suppression de l'UUID",
"uuidDeleteSuccess": "UUID supprimé avec succès!",
"modsDownloading": "Téléchargement de {name}...",
"modsTogglingMod": "Basculement du mod...",
"modsDeletingMod": "Suppression du mod...",
"modsLoadingMods": "Chargement des mods depuis CurseForge...",
"modsInstalledSuccess": "{name} installé avec succès! 🎉",
"modsDeletedSuccess": "{name} supprimé avec succès",
"modsDownloadFailed": "Échec du téléchargement du mod: {error}",
"modsToggleFailed": "Échec du basculement du mod: {error}",
"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",
"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",
"regenerateUuidTitle": "Générer un nouvel UUID",
"regenerateUuidMessage": "Êtes-vous sûr de vouloir générer un nouvel UUID? Cela changera votre identité de joueur.",
"regenerateUuidButton": "Générer",
"setCustomUuidTitle": "Définir UUID personnalisé",
"setCustomUuidMessage": "Êtes-vous sûr de vouloir définir cet UUID personnalisé? Cela changera votre identité de joueur.",
"setCustomUuidButton": "Définir UUID",
"deleteUuidTitle": "Supprimer UUID",
"deleteUuidMessage": "Êtes-vous sûr de vouloir supprimer l'UUID de \"{username}\"? Cette action est irréversible.",
"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",
"switchUsernameTitle": "Changer d'identité",
"switchUsernameMessage": "Basculer vers le nom d'utilisateur \"{username}\"? Cela changera votre identité de joueur actuelle.",
"switchUsernameButton": "Changer"
},
"progress": {
"initializing": "Initialisation...",
"downloading": "Téléchargement...",
"installing": "Installation...",
"extracting": "Extraction...",
"verifying": "Vérification...",
"switchingProfile": "Changement de configuration...",
"profileSwitched": "Configuration changée !",
"startingGame": "Démarrage du jeu...",
"launching": "LANCEMENT...",
"uninstallingGame": "Désinstallation du jeu...",
"gameUninstalled": "Jeu désinstallé avec succès!",
"uninstallFailed": "Échec de la désinstallation: {error}",
"startingUpdate": "Démarrage de la mise à jour obligatoire du jeu...",
"installationComplete": "Installation terminée avec succès!",
"installationFailed": "Échec de l'installation: {error}",
"installingGameFiles": "Installation des fichiers du jeu...",
"installComplete": "Installation terminée!"
}
}

View File

@@ -1,271 +0,0 @@
{
"nav": {
"play": "Main",
"mods": "Mod",
"news": "Berita",
"chat": "Obrolan Pemain",
"settings": "Pengaturan"
},
"header": {
"playersLabel": "Pemain:",
"manageProfiles": "Kelola",
"manageIdentities": "Kelola",
"identityTooltip": "Nama pemain & UUID yang digunakan dalam game",
"configTooltip": "Konfigurasi game: mod, Java & pengaturan memori",
"defaultProfile": "Default"
},
"install": {
"title": "LAUNCHER GRATIS UNTUK DIMAINKAN",
"playerName": "Nama Pemain",
"playerNamePlaceholder": "Masukkan namamu",
"gameBranch": "Versi Game",
"releaseVersion": "Rilis (Stabil)",
"preReleaseVersion": "Pra-Rilis (Eksperimental)",
"customInstallation": "Instalasi Kustom",
"installationFolder": "Folder Instalasi",
"pathPlaceholder": "Lokasi default",
"browse": "Telusuri",
"installButton": "INSTAL HYTALE",
"installing": "MENGINSTAL..."
},
"play": {
"ready": "SIAP BERMAIN",
"subtitle": "Luncurkan Hytale dan mulai petualanganmu",
"playButton": "MAIN HYTALE",
"latestNews": "BERITA TERBARU",
"viewAll": "LIHAT SEMUA",
"checking": "MEMERIKSA...",
"play": "MAIN"
},
"mods": {
"searchPlaceholder": "Cari mod...",
"myMods": "MOD SAYA",
"previous": "SEBELUMNYA",
"next": "BERIKUTNYA",
"page": "Halaman",
"of": "dari",
"modalTitle": "MOD SAYA",
"noModsFound": "Mod Tidak Ditemukan",
"noModsFoundDesc": "Coba sesuaikan pencarianmu",
"noModsInstalled": "Tidak ada Mod Terinstal",
"noModsInstalledDesc": "Tambahkan mod dari CurseForge atau impor file lokal",
"view": "LIHAT",
"install": "INSTAL",
"installed": "TERINSTAL",
"enable": "AKTIFKAN",
"disable": "NONAKTIFKAN",
"active": "AKTIF",
"disabled": "NONAKTIF",
"delete": "Hapus mod",
"noDescription": "Tidak ada deskripsi tersedia",
"confirmDelete": "Apakah kamu yakin ingin menghapus \"{name}\"?",
"confirmDeleteDesc": "Tindakan ini tidak dapat dibatalkan.",
"confirmDeletion": "Konfirmasi Penghapusan",
"apiKeyRequired": "Kunci API Diperlukan",
"apiKeyRequiredDesc": "Kunci API CurseForge diperlukan untuk menelusuri mod"
},
"news": {
"title": "SEMUA BERITA",
"readMore": "Baca Selengkapnya"
},
"chat": {
"title": "OBROLAN PEMAIN",
"pickColor": "Warna",
"inputPlaceholder": "Ketik pesanmu...",
"send": "Kirim",
"online": "aktif",
"charCounter": "{current}/{max}",
"secureChat": "Obrolan aman - Tautan disensor",
"joinChat": "Gabung Obrolan",
"chooseUsername": "Pilih nama pengguna untuk bergabung ke Obrolan Pemain",
"username": "Nama Pengguna",
"usernamePlaceholder": "Masukkan nama penggunamu...",
"usernameHint": "3-20 karakter, huruf, angka, - dan _ saja",
"joinButton": "Gabung Obrolan",
"colorModal": {
"title": "Kustomisasi Warna Nama Pengguna",
"chooseSolid": "Pilih warna solid:",
"customColor": "Warna kustom:",
"preview": "Pratinjau:",
"previewUsername": "Nama Pengguna",
"apply": "Terapkan Warna"
}
},
"settings": {
"title": "PENGATURAN",
"java": "Runtime Java",
"useCustomJava": "Gunakan lokasi Java Kustom",
"javaDescription": "Ganti runtime Java bawaan dengan instalasi milikmu",
"javaPath": "Lokasi Eksekutabel Java",
"javaPathPlaceholder": "Pilih lokasi Java...",
"javaBrowse": "Telusuri",
"javaHint": "Pilih folder instalasi Java (mendukung Windows, Mac, Linux)",
"discord": "Integrasi Discord",
"enableRPC": "Aktifkan Discord Rich Presence",
"discordDescription": "Tampilkan aktivitas launchermu di Discord",
"game": "Opsi Game",
"playerName": "Nama Pemain",
"playerNamePlaceholder": "Masukkan nama pemainmu",
"playerNameHint": "Nama ini akan digunakan di dalam game (1-16 karakter)",
"openGameLocation": "Buka Lokasi Game",
"openGameLocationDesc": "Buka folder instalasi game",
"account": "Manajemen UUID Pemain",
"currentUUID": "UUID Saat Ini",
"uuidPlaceholder": "Memuat UUID...",
"copyUUID": "Salin UUID",
"regenerateUUID": "Regenerasi UUID",
"uuidHint": "Pengidentifikasi pemain unikmu untuk nama pengguna ini",
"manageUUIDs": "Kelola Semua UUID",
"manageUUIDsDesc": "Lihat dan kelola semua UUID pemain",
"language": "Bahasa",
"selectLanguage": "Pilih Bahasa",
"repairGame": "Perbaiki Game",
"reinstallGame": "Instal ulang file game (tetap menyimpan data)",
"gpuPreference": "Preferensi GPU",
"gpuHint": "Fitur khusus laptop; setel ke Terintegrasi jika di PC",
"gpuAuto": "Otomatis",
"gpuIntegrated": "Terintegrasi",
"gpuDedicated": "Terdedikasi",
"logs": "LOG SISTEM",
"logsCopy": "Salin",
"logsRefresh": "Segarkan",
"logsFolder": "Buka Folder",
"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",
"closeOnStartDescription": "Tutup launcher secara otomatis setelah Hytale diluncurkan",
"hwAccel": "Akselerasi Perangkat Keras",
"hwAccelDescription": "Aktifkan akselerasi perangkat keras untuk launcher`",
"gameBranch": "Cabang Game",
"branchRelease": "Rilis",
"branchPreRelease": "Pra-Rilis",
"branchHint": "Beralih antara rilis stabil dan versi pra-rilis eksperimental",
"branchWarning": "Mengubah cabang akan mengunduh dan menginstal versi game yang berbeda",
"branchSwitching": "Beralih ke {branch}...",
"branchSwitched": "Berhasil beralih ke {branch}!",
"installRequired": "Instalasi Diperlukan",
"branchInstallConfirm": "Game akan diinstal untuk cabang {branch}. Lanjutkan?"
},
"uuid": {
"modalTitle": "Manajemen UUID",
"allPlayerUUIDs": "Semua UUID Pemain",
"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)",
"setUUID": "Setel UUID",
"warning": "Peringatan: Menyetel UUID secara kustom akan mengubah identitas pemainmu saat ini",
"copyTooltip": "Salin UUID",
"regenerateTooltip": "Hasilkan UUID Baru"
},
"configurations": {
"modalTitle": "Kelola Konfigurasi",
"newProfilePlaceholder": "Nama Konfigurasi Baru",
"createProfile": "Buat Konfigurasi"
},
"discord": {
"notificationText": "Gabung komunitas Discord kami!",
"joinButton": "Gabung Discord"
},
"common": {
"confirm": "Konfirmasi",
"cancel": "Batal",
"save": "Simpan",
"close": "Tutup",
"delete": "Hapus",
"edit": "Edit",
"loading": "Memuat...",
"apply": "Terapkan",
"install": "Instal"
},
"notifications": {
"gameDataNotFound": "Kesalahan: Data game tidak ditemukan",
"gameUpdatedSuccess": "Game berhasil diperbarui! 🎉",
"updateFailed": "Pembaruan gagal: {error}",
"updateError": "Kesalahan pembaruan: {error}",
"discordEnabled": "Discord Rich Presence diaktifkan",
"discordDisabled": "Discord Rich Presence dinonaktifkan",
"discordSaveFailed": "Gagal menyimpan pengaturan Discord",
"playerNameRequired": "Silakan masukkan nama pemain yang valid",
"playerNameSaved": "Nama pemain berhasil disimpan",
"playerNameSaveFailed": "Gagal menyimpan nama pemain",
"uuidCopied": "UUID disalin ke papan klip!",
"uuidCopyFailed": "Gagal menyalin UUID",
"uuidRegenNotAvailable": "Regenerasi UUID tidak tersedia",
"uuidRegenFailed": "Gagal meregenerasi UUID",
"uuidGenerated": "UUID baru berhasil dihasilkan!",
"uuidGeneratedShort": "UUID baru dihasilkan!",
"uuidGenerateFailed": "Gagal menghasilkan UUID baru",
"uuidRequired": "Silakan masukkan UUID",
"uuidInvalidFormat": "Format UUID tidak valid",
"uuidSetFailed": "Gagal menyetel UUID kustom",
"uuidSetSuccess": "UUID kustom berhasil disetel!",
"uuidDeleteFailed": "Gagal menghapus UUID",
"uuidDeleteSuccess": "UUID berhasil dihapus!",
"modsDownloading": "Mengunduh {name}...",
"modsTogglingMod": "Beralih mod...",
"modsDeletingMod": "Menghapus mod...",
"modsLoadingMods": "Memuat mod dari CurseForge...",
"modsInstalledSuccess": "{name} berhasil diinstal! 🎉",
"modsDeletedSuccess": "{name} berhasil dihapus",
"modsDownloadFailed": "Gagal mengunduh mod: {error}",
"modsToggleFailed": "Gagal beralih mod: {error}",
"modsDeleteFailed": "Gagal menghapus mod: {error}",
"modsModNotFound": "Informasi mod tidak ditemukan",
"hwAccelSaved": "Pengaturan akselerasi perangkat keras disimpan",
"hwAccelSaveFailed": "Gagal menyimpan pengaturan akselerasi perangkat keras",
"noUsername": "Nama pengguna belum dikonfigurasi. Silakan simpan nama pengguna terlebih dahulu.",
"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",
"regenerateUuidTitle": "Hasilkan UUID baru",
"regenerateUuidMessage": "Apakah kamu yakin ingin menghasilkan UUID baru? Ini akan mengubah identitas pemainmu.",
"regenerateUuidButton": "Hasilkan",
"setCustomUuidTitle": "Setel UUID kustom",
"setCustomUuidMessage": "Apakah kamu yakin ingin menyetel UUID kustom ini? Ini akan mengubah identitas pemainmu.",
"setCustomUuidButton": "Setel UUID",
"deleteUuidTitle": "Hapus UUID",
"deleteUuidMessage": "Apakah kamu yakin ingin menghapus UUID untuk \"{username}\"? Tindakan ini tidak dapat dibatalkan.",
"deleteUuidButton": "Hapus",
"uninstallGameTitle": "Hapus instalasi game",
"uninstallGameMessage": "Apakah kamu yakin ingin menghapus instalasi Hytale? Semua file game akan dihapus.",
"uninstallGameButton": "Hapus Instalasi",
"switchUsernameTitle": "Ganti Identitas",
"switchUsernameMessage": "Beralih ke nama pengguna \"{username}\"? Ini akan mengubah identitas pemain saat ini.",
"switchUsernameButton": "Ganti"
},
"progress": {
"initializing": "Menginisialisasi...",
"downloading": "Mengunduh...",
"installing": "Menginstal...",
"extracting": "Mengekstrak...",
"verifying": "Memverifikasi...",
"switchingProfile": "Beralih konfigurasi...",
"profileSwitched": "Konfigurasi dialihkan!",
"startingGame": "Memulai game...",
"launching": "MELUNCURKAN...",
"uninstallingGame": "Menghapus instalasi game...",
"gameUninstalled": "Instalasi game berhasil dihapus!",
"uninstallFailed": "Penghapusan instalasi gagal: {error}",
"startingUpdate": "Memulai pembaruan game wajib...",
"installationComplete": "Instalasi berhasil diselesaikan!",
"installationFailed": "Instalasi gagal: {error}",
"installingGameFiles": "Menginstal file game...",
"installComplete": "Instalasi selesai!"
}
}

View File

@@ -4,23 +4,19 @@
"mods": "Mody",
"news": "Wiadomości",
"chat": "Chat z graczami",
"settings": "Ustawienia"
"settings": "Ustawienia",
"skins": "Skiny"
},
"header": {
"playersLabel": "Graczy:",
"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"
"manageProfiles": "Zarządzaj Profilami",
"defaultProfile": "Domyślny",
"f2p": "FREE TO PLAY"
},
"install": {
"title": "DARMOWY LAUNCHER",
"title": "FREE TO PLAY LAUNCHER",
"playerName": "Nazwa Gracza",
"playerNamePlaceholder": "Wprowadź Nazwę",
"gameBranch": "Wersja Gry",
"releaseVersion": "Wydanie (Stabilna)",
"preReleaseVersion": "Przed-Wydaniem (Eksperymentalna)",
"customInstallation": "Dostosuj Instalacje",
"installationFolder": "Folder docelowy",
"pathPlaceholder": "Domyślna lokalizacja",
@@ -60,9 +56,7 @@
"noDescription": "Brak opisu",
"confirmDelete": "Czy na pewno chcesz usunąć \"{name}\"?",
"confirmDeleteDesc": "Tej czynności nie można cofnąć.",
"confirmDeletion": "Potwierdź",
"apiKeyRequired": "Wymagany Klucz API",
"apiKeyRequiredDesc": "Klucz API CurseForge jest potrzebny do przeglądania modów"
"confirmDeletion": "Potwierdź"
},
"news": {
"title": "WSZYSTKIE WIADOMOŚCI",
@@ -122,44 +116,21 @@
"repairGame": "Napraw Grę",
"reinstallGame": "Zainstaluj ponownie pliki gry (zachowuje dane)",
"gpuPreference": "Preferencje GPU",
"gpuHint": "Funkcja dostępna tylko na laptopie; ustaw na Zintegrowaną, jeśli na komputerze PC",
"gpuHint": "Wybierz preferowany procesor graficzny (Linux: wpływa na DRI_PRIME)",
"gpuAuto": "Auto",
"gpuIntegrated": "Zintegrowana",
"gpuDedicated": "Dedykowana",
"logs": "DZIENNIKI SYSTEMOWE",
"logs": "SYSTEM LOGS",
"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",
"closeOnStartDescription": "Automatycznie zamknij launcher po uruchomieniu Hytale",
"hwAccel": "Przyspieszenie Sprzętowe",
"hwAccelDescription": "Włącz przyspieszenie sprzętowe dla launchera",
"gameBranch": "Gałąź Gry",
"branchRelease": "Wydanie",
"branchPreRelease": "Przed-Wydaniem",
"branchHint": "Przełączaj między stabilnym wydaniem a eksperymentalną wersją przed-wydaniem",
"branchWarning": "Zmiana gałęzi spowoduje pobranie i instalację innej wersji gry",
"branchSwitching": "Przełączanie na {branch}...",
"branchSwitched": "Pomyślnie przełączono na {branch}!",
"installRequired": "Wymagana Instalacja",
"branchInstallConfirm": "Gra zostanie zainstalowana dla gałęzi {branch}. Kontynuować?"
"logsLoading": "Ładowanie logów..."
},
"uuid": {
"modalTitle": "Zarządzanie UUID",
"currentUserUUID": "Aktualny UUID użytkownika",
"allPlayerUUIDs": "Wszystkie identyfikatory UUID graczy",
"addIdentity": "Dodaj tożsamość",
"usernamePlaceholder": "Nazwa użytkownika",
"add": "Dodaj",
"cancel": "Anuluj",
"advanced": "Zaawansowane",
"generateNew": "Wygeneruj nowy UUID",
"loadingUUIDs": "Ładowanie UUID...",
"setCustomUUID": "Ustaw niestandardowy UUID",
"customPlaceholder": "Wprowadź niestandardowy UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
@@ -168,15 +139,19 @@
"copyTooltip": "Kopiuj UUID",
"regenerateTooltip": "Wygeneruj nowy UUID"
},
"configurations": {
"modalTitle": "Zarządzaj Konfiguracjami",
"newProfilePlaceholder": "Nazwa Nowej Konfiguracji",
"createProfile": "Utwórz Konfigurację"
"profiles": {
"modalTitle": "Zarządzaj Profilami",
"newProfilePlaceholder": "Nowa Nazwa Profilu",
"createProfile": "Utwórz Profil"
},
"discord": {
"notificationText": "Dołącz do naszej społeczności Discord!",
"joinButton": "Dołącz Discord"
},
"skins": {
"title": "Skiny",
"comingSoon": "Personalizacja skórek już wkrótce..."
},
"common": {
"confirm": "Potwierdź",
"cancel": "Anuluj",
@@ -185,8 +160,7 @@
"delete": "Usuń",
"edit": "Edytuj",
"loading": "Ładowanie...",
"apply": "Zastosuj",
"install": "Zainstaluj"
"apply": "Zastosuj"
},
"notifications": {
"gameDataNotFound": "Błąd: Nie znaleziono danych gry",
@@ -221,15 +195,7 @@
"modsDownloadFailed": "Nie udało się pobrać moda: {error}",
"modsToggleFailed": "Nie udało się przełączyć moda: {error}",
"modsDeleteFailed": "Nie udało się usunąć moda: {error}",
"modsModNotFound": "Nie znaleziono informacji o modzie",
"hwAccelSaved": "Zapisano ustawienie przyspieszenia sprzętowego",
"hwAccelSaveFailed": "Nie udało się zapisać ustawienia przyspieszenia sprzętowego",
"noUsername": "Nie skonfigurowano nazwy użytkownika. Najpierw zapisz swoją nazwę użytkownika.",
"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"
"modsModNotFound": "Nie znaleziono informacji o modzie"
},
"confirm": {
"defaultTitle": "Potwierdź działanie",
@@ -244,10 +210,7 @@
"deleteUuidButton": "Usuń",
"uninstallGameTitle": "Odinstaluj grę",
"uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.",
"uninstallGameButton": "Odinstaluj",
"switchUsernameTitle": "Zmień tożsamość",
"switchUsernameMessage": "Przełączyć na nazwę użytkownika \"{username}\"? To zmieni Twoją aktualną tożsamość gracza.",
"switchUsernameButton": "Przełącz"
"uninstallGameButton": "Odinstaluj"
},
"progress": {
"initializing": "Inicjalizacja...",
@@ -255,8 +218,8 @@
"installing": "Instalowanie...",
"extracting": "Ekstraktowanie...",
"verifying": "Weryfikowanie...",
"switchingProfile": "Przełączanie konfiguracji...",
"profileSwitched": "Konfiguracja zmieniona!",
"switchingProfile": "Przełączanie profilu...",
"profileSwitched": "Profil zmieniony!",
"startingGame": "Uruchamianie gry...",
"launching": "URUCHAMIANIE...",
"uninstallingGame": "Odinstalowywanie gry...",
@@ -268,4 +231,4 @@
"installingGameFiles": "Instalowanie plików gry...",
"installComplete": "Instalacja zakończona!"
}
}
}

View File

@@ -8,20 +8,15 @@
},
"header": {
"playersLabel": "Jogadores:",
"manageProfiles": "Gerenciar",
"manageIdentities": "Gerenciar",
"identityTooltip": "Seu nome de jogador e UUID usados no jogo",
"configTooltip": "Configuração do jogo: mods, Java e memória",
"manageProfiles": "Gerenciar Perfis",
"defaultProfile": "Padrão"
},
"install": {
"title": "LANÇADOR JOGO GRATUITO",
"playerName": "Nome do Jogador",
"playerNamePlaceholder": "Digite seu nome",
"gameBranch": "Versão do Jogo",
"playerNamePlaceholder": "Digite seu nome", "gameBranch": "Versão do Jogo",
"releaseVersion": "Lançamento (Estável)",
"preReleaseVersion": "Pré-Lançamento (Experimental)",
"customInstallation": "Instalação Personalizada",
"preReleaseVersion": "Pré-Lançamento (Experimental)", "customInstallation": "Instalação Personalizada",
"installationFolder": "Pasta de Instalação",
"pathPlaceholder": "Local padrão",
"browse": "Procurar",
@@ -122,7 +117,7 @@
"repairGame": "Reparar jogo",
"reinstallGame": "Reinstalar arquivos do jogo (mantém os dados)",
"gpuPreference": "Preferência de GPU",
"gpuHint": "Recurso exclusivo para laptops; defina como Integrado se estiver em um PC.",
"gpuHint": "Selecione sua GPU preferida (Linux: afeta o DRI_PRIME)",
"gpuAuto": "Automático",
"gpuIntegrated": "Integrada",
"gpuDedicated": "Dedicada",
@@ -130,18 +125,10 @@
"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",
"closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado",
"hwAccel": "Aceleração de Hardware",
"hwAccelDescription": "Ativar aceleração de hardware para o lançador",
"gameBranch": "Versão do Jogo",
"branchRelease": "Lançamento",
"branchPreRelease": "Pré-Lançamento",
@@ -154,12 +141,9 @@
},
"uuid": {
"modalTitle": "Gerenciamento de UUID",
"currentUserUUID": "UUID do usuário atual",
"allPlayerUUIDs": "Todos os UUIDs de jogadores",
"addIdentity": "Adicionar identidade",
"usernamePlaceholder": "Nome de usuário",
"add": "Adicionar",
"cancel": "Cancelar",
"advanced": "Avançado",
"generateNew": "Gerar novo UUID",
"loadingUUIDs": "Carregando UUIDs...",
"setCustomUUID": "Definir UUID personalizado",
"customPlaceholder": "Digite um UUID personalizado (formato: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
@@ -168,15 +152,16 @@
"copyTooltip": "Copiar UUID",
"regenerateTooltip": "Gerar novo UUID"
},
"configurations": {
"modalTitle": "Gerenciar Configurações",
"newProfilePlaceholder": "Nome da Nova Configuração",
"createProfile": "Criar Configuração"
"profiles": {
"modalTitle": "Gerenciar perfis",
"newProfilePlaceholder": "Nome do novo perfil",
"createProfile": "Criar perfil"
},
"discord": {
"notificationText": "Junte-se à nossa comunidade do Discord!",
"joinButton": "Entrar no Discord"
},
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",
@@ -221,15 +206,7 @@
"modsDownloadFailed": "Falha ao baixar mod: {error}",
"modsToggleFailed": "Falha ao alternar mod: {error}",
"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",
"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"
"modsModNotFound": "Informações do mod não encontradas"
},
"confirm": {
"defaultTitle": "Confirmar ação",
@@ -244,10 +221,7 @@
"deleteUuidButton": "Excluir",
"uninstallGameTitle": "Desinstalar jogo",
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
"uninstallGameButton": "Desinstalar",
"switchUsernameTitle": "Trocar Identidade",
"switchUsernameMessage": "Trocar para o nome de usuário \"{username}\"? Isso mudará sua identidade de jogador atual.",
"switchUsernameButton": "Trocar"
"uninstallGameButton": "Desinstalar"
},
"progress": {
"initializing": "Inicializando...",
@@ -255,8 +229,8 @@
"installing": "Instalando...",
"extracting": "Extraindo...",
"verifying": "Verificando...",
"switchingProfile": "Alternando configuração...",
"profileSwitched": "Configuração alternada!",
"switchingProfile": "Alternando perfil...",
"profileSwitched": "Perfil alternado!",
"startingGame": "Iniciando jogo...",
"launching": "INICIANDO...",
"uninstallingGame": "Desinstalando jogo...",

View File

@@ -1,271 +0,0 @@
{
"nav": {
"play": "Играть",
"mods": "Моды",
"news": "Новости",
"chat": "Чат игроков",
"settings": "Настройки"
},
"header": {
"playersLabel": "Игроки:",
"manageProfiles": "Управление",
"manageIdentities": "Управление",
"identityTooltip": "Ваш игровой ник и UUID, используемые в игре",
"configTooltip": "Конфигурация игры: моды, Java и настройки памяти",
"defaultProfile": "По умолчанию"
},
"install": {
"title": "FREE TO PLAY LAUNCHER",
"playerName": "Ник игрока",
"playerNamePlaceholder": "Введите ваш ник",
"gameBranch": "Версия игры",
"releaseVersion": "Релиз (Стабильная)",
"preReleaseVersion": "Пре-Релиз (Экспериментально)",
"customInstallation": "Модифицированная установка",
"installationFolder": "Папка установки",
"pathPlaceholder": "Путь по умолчанию",
"browse": "Обзор",
"installButton": "УСТАНОВИТЬ HYTALE",
"installing": "УСТАНОВКА..."
},
"play": {
"ready": "ГОТОВ К ИГРЕ",
"subtitle": "Запусти Hytale и приготовься к приключению!",
"playButton": "ЗАПУСТИТЬ HYTALE",
"latestNews": "ПОСЛЕДНИЕ НОВОСТИ",
"viewAll": "ПОСМОТРЕТЬ ВСЁ",
"checking": "ПРОВЕРКА...",
"play": "ЗАПУСТИТЬ"
},
"mods": {
"searchPlaceholder": "Искать моды...",
"myMods": "Мои моды",
"previous": "Предыдущая",
"next": "Вперёд",
"page": "Страница",
"of": "",
"modalTitle": "МОИ МОДЫ",
"noModsFound": "Моды не найдены",
"noModsFoundDesc": "Попробуйте изменить свой запрос",
"noModsInstalled": "Нет установленных модов",
"noModsInstalledDesc": "Добавьте моды с CurseForge или импортируйте свои!",
"view": "Посмотреть",
"install": "Установить",
"installed": "УСТАНОВЛЕННЫЕ",
"enable": "ВКЛЮЧИТЬ",
"disable": "ВЫКЛЮЧИТЬ",
"active": "ВКЛЮЧЁН",
"disabled": "ВЫКЛЮЧЕН",
"delete": "Удалить мод",
"noDescription": "Нет доступного описания",
"confirmDelete": "Вы точно уверены, что хотите удалить \"{name}\"?",
"confirmDeleteDesc": "Это действие не отменить.",
"confirmDeletion": "Подтвердите удаление",
"apiKeyRequired": "Требуется ключ API",
"apiKeyRequiredDesc": "Ключ CurseForge API требуется для просмотра модов"
},
"news": {
"title": "ВСЕ НОВОСТИ",
"readMore": "Читать дальше"
},
"chat": {
"title": "ЧАТ ИГРОКОВ",
"pickColor": "Цвет",
"inputPlaceholder": "Введите своё сообщение...",
"send": "Отправить",
"online": "онлайн",
"charCounter": "{current}/{max}",
"secureChat": "Безопасный чат - все ссылки зацензурены",
"joinChat": "Присоединиться к чату",
"chooseUsername": "Выберите имя пользователя для входа в чат игроков",
"username": "Ник",
"usernamePlaceholder": "Введите ваш ник...",
"usernameHint": "3-20 символов, букв, цифр, только - и _",
"joinButton": "Присоединиться к чату",
"colorModal": {
"title": "Выберите цвет ника",
"chooseSolid": "Выберите цвет:",
"customColor": "Модифицированный цвет:",
"preview": "Предварительный просмотр:",
"previewUsername": "Ник",
"apply": "Применить цвет"
}
},
"settings": {
"title": "НАСТРОЙКИ",
"java": "Java Runtime",
"useCustomJava": "Укажите свой путь Java",
"javaDescription": "Переопределить встроенный Java Runtime с вашей установкой",
"javaPath": "Путь исполняемого файла Java",
"javaPathPlaceholder": "Выберите путь Java...",
"javaBrowse": "Обзор",
"javaHint": "Выберите папку установки Java (поддерживается Windows, Mac, Linux)",
"discord": "Интеграция Discord",
"enableRPC": "Включить Discord Rich Presence",
"discordDescription": "Показывать вашу активность лаунчера в Discord",
"game": "Настройки игры",
"playerName": "Ник игрока",
"playerNamePlaceholder": "Введите ваш ник",
"playerNameHint": "Этот ник будет использован в игре (1-16 символов)",
"openGameLocation": "Открыть местоположение игры",
"openGameLocationDesc": "Открыть папку установки игры",
"account": "Управление UUID игрока",
"currentUUID": "Текущий UUID",
"uuidPlaceholder": "Загрузка UUID...",
"copyUUID": "Копировать UUID",
"regenerateUUID": "Перегенерировать UUID",
"uuidHint": "Уникальный идентификатор игрока для этого ника",
"manageUUIDs": "Управление всеми UUID",
"manageUUIDsDesc": "Смотреть и управлять всеми UUID игрока",
"language": "Язык",
"selectLanguage": "Выберите язык",
"repairGame": "Починить игру",
"reinstallGame": "Переустановить файлы игры (сохраняет данные)",
"gpuPreference": "Предпочтение GPU",
"gpuHint": "Функция доступна только на ноутбуках; при использовании на ПК выберите встроенную видеокарту.",
"gpuAuto": "Автоматический выбор",
"gpuIntegrated": "Интегрированная видеокарта",
"gpuDedicated": "Дискретная видеокарта",
"logs": "ЛОГИ",
"logsCopy": "Копировать",
"logsRefresh": "Обновить",
"logsFolder": "Открыть папку",
"logsSend": "Отправить логи",
"logsSending": "Отправка...",
"logsSent": "Отправлено!",
"logsSendFailed": "Ошибка",
"logsSubmissionId": "ID отправки",
"logsShareId": "Поделитесь этим ID с поддержкой при обращении",
"logsLoading": "Загрузка логов...",
"closeLauncher": "Поведение лаунчера",
"closeOnStart": "Закрыть лаунчер при старте игры",
"closeOnStartDescription": "Автоматически закрыть лаунчер после запуска Hytale",
"hwAccel": "Аппаратное ускорение",
"hwAccelDescription": "Включить аппаратное ускорение для лаунчера",
"gameBranch": "Ветка игры",
"branchRelease": "Релиз",
"branchPreRelease": "Пре-Релиз",
"branchHint": "Переключает между релизом и пре-релизом игры",
"branchWarning": "Изменение ветки скачает и установит другую версию игры",
"branchSwitching": "Переключение на {branch}...",
"branchSwitched": "Переключение на {branch} выполнено успешно!",
"installRequired": "Необходима установка",
"branchInstallConfirm": "Игра будет установлена для ветки {branch}. Продолжить?"
},
"uuid": {
"modalTitle": "Управление UUID",
"allPlayerUUIDs": "UUID всех игроков",
"addIdentity": "Добавить личность",
"usernamePlaceholder": "Имя пользователя",
"add": "Добавить",
"cancel": "Отмена",
"advanced": "Дополнительно",
"loadingUUIDs": "Загрузка UUID...",
"setCustomUUID": "Установить кастомный UUID",
"customPlaceholder": "Ввести кастомный UUID (форматы: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Установить UUID",
"warning": "Внимание! Установка кастомного UUID изменит вашу текущую личность игрока!",
"copyTooltip": "Скопировать UUID",
"regenerateTooltip": "Сгенерировать новый UUID"
},
"configurations": {
"modalTitle": "Управление конфигурациями",
"newProfilePlaceholder": "Название новой конфигурации",
"createProfile": "Создать конфигурацию"
},
"discord": {
"notificationText": "Присоединитесь к нашему сообществу в Discord!",
"joinButton": "Присоединиться к Discord"
},
"common": {
"confirm": "Подтвердить",
"cancel": "Отменить",
"save": "Сохранить",
"close": "Закрыть",
"delete": "Удалить",
"edit": "Редактировать",
"loading": "Загружается...",
"apply": "Применить",
"install": "Установить"
},
"notifications": {
"gameDataNotFound": "Ошибка: данные игры не найдены",
"gameUpdatedSuccess": "Игра успешно обновлена! Ура! 🎉",
"updateFailed": "Обновление прервалось с ошибкой: {error}",
"updateError": "Ошибка обновления: {error}",
"discordEnabled": "Discord Rich Presence включен",
"discordDisabled": "Discord Rich Presence выключен",
"discordSaveFailed": "Не удалось сохранить настройку Discord",
"playerNameRequired": "Пожалуйста, введите действительное имя игрока",
"playerNameSaved": "Имя игрока успешно сохранено!",
"playerNameSaveFailed": "Не удалось сохранить имя игрока",
"uuidCopied": "UUID скопирован в буфер обмена!",
"uuidCopyFailed": "Не удалось скопировать UUID",
"uuidRegenNotAvailable": "UUID перегенерация к сожалению не доступна",
"uuidRegenFailed": "Не удалось перегенерировать UUID",
"uuidGenerated": "Новый UUID сгенерирован успешно!",
"uuidGeneratedShort": "Новый UUID сгенерирован!",
"uuidGenerateFailed": "Не получилось сгенерировать новый UUID",
"uuidRequired": "Пожалуйста введите UUID",
"uuidInvalidFormat": "Неправильный формат UUID",
"uuidSetFailed": "Не удалось поставить кастомный UUID",
"uuidSetSuccess": "Кастомный UUID успешно установлен!",
"uuidDeleteFailed": "Не удалось удалить UUID",
"uuidDeleteSuccess": "Удаление UUID успешно завершено!",
"modsDownloading": "Скачивание {name}...",
"modsTogglingMod": "Включение мода...",
"modsDeletingMod": "Удаление мода...",
"modsLoadingMods": "Загрузка модов с CurseForge...",
"modsInstalledSuccess": "{name} успешно установлен! 🎉",
"modsDeletedSuccess": "{name} удалён успешно!",
"modsDownloadFailed": "Не получилось скачать мод: {error}",
"modsToggleFailed": "Не получилось включить мод: {error}",
"modsDeleteFailed": "Не получилось удалить мод: {error}",
"modsModNotFound": "Информация по моду не найдена",
"hwAccelSaved": "Настройка аппаратного ускорения сохранена!",
"hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения",
"noUsername": "Имя пользователя не настроено. Пожалуйста, сначала сохраните имя пользователя.",
"identityAdded": "Личность успешно добавлена!",
"identityAddFailed": "Не удалось добавить личность",
"switchUsernameSuccess": "Успешно переключено на \"{username}\"!",
"switchUsernameFailed": "Не удалось переключить имя пользователя",
"playerNameTooLong": "Имя игрока должно быть не более 16 символов"
},
"confirm": {
"defaultTitle": "Подтвердить действие",
"regenerateUuidTitle": "Сгенерировать новый UUID",
"regenerateUuidMessage": "Вы уверены, что хотите сгенерировать новый UUID? Генерация нового UUID изменит вашу текущую личность игрока!",
"regenerateUuidButton": "Сгенерировать",
"setCustomUuidTitle": "Установить кастомный UUID",
"setCustomUuidMessage": "Вы уверены, что хотите установить кастомный UUID? Установка кастомного UUID изменит вашу текущую личность игрока!",
"setCustomUuidButton": "Установить UUID",
"deleteUuidTitle": "Удалить UUID",
"deleteUuidMessage": "Вы уверены, что хотите удалить UUID для \"{username}\"? Это действие необратимо!",
"deleteUuidButton": "Удалить",
"uninstallGameTitle": "Удалить игру",
"uninstallGameMessage": "Вы уверены, что хотите удалить Hytale? Все данные игры будут безвозвратно удалены!",
"uninstallGameButton": "Удалить",
"switchUsernameTitle": "Сменить личность",
"switchUsernameMessage": "Переключиться на имя пользователя \"{username}\"? Это изменит вашу текущую личность игрока.",
"switchUsernameButton": "Переключить"
},
"progress": {
"initializing": "Инициализация...",
"downloading": "Скачивание...",
"installing": "Установка...",
"extracting": "Извлечение...",
"verifying": "Проверка...",
"switchingProfile": "Смена конфигурации...",
"profileSwitched": "Конфигурация изменена!",
"startingGame": "Запуск игры...",
"launching": "ЗАПУСК...",
"uninstallingGame": "Удаление игры...",
"gameUninstalled": "Игра успешно удалена!",
"uninstallFailed": "Удаление игры прервано с ошибкой: {error}",
"startingUpdate": "Начало обязательного обновления игры...",
"installationComplete": "Установка успешно завершена!",
"installationFailed": "Установка прервана с ошибкой: {error}",
"installingGameFiles": "Установка файлов игры...",
"installComplete": "Установка завершена!"
}
}

View File

@@ -1,271 +0,0 @@
{
"nav": {
"play": "Spela",
"mods": "Moddar",
"news": "Nyheter",
"chat": "Spelarchatt",
"settings": "Inställningar"
},
"header": {
"playersLabel": "Spelare:",
"manageProfiles": "Hantera",
"manageIdentities": "Hantera",
"identityTooltip": "Ditt spelarnamn och UUID som används i spelet",
"configTooltip": "Spelkonfiguration: moddar, Java- och minnesinställningar",
"defaultProfile": "Standard"
},
"install": {
"title": "GRATIS LAUNCHER",
"playerName": "Spelarnamn",
"playerNamePlaceholder": "Ange ditt namn",
"gameBranch": "Spelversion",
"releaseVersion": "Release (Stabil)",
"preReleaseVersion": "Pre-Release (Experimentell)",
"customInstallation": "Anpassad installation",
"installationFolder": "Installationsmapp",
"pathPlaceholder": "Standardplats",
"browse": "Bläddra",
"installButton": "INSTALLERA HYTALE",
"installing": "INSTALLERAR..."
},
"play": {
"ready": "REDO ATT SPELA",
"subtitle": "Starta Hytale och börja äventyret",
"playButton": "SPELA HYTALE",
"latestNews": "SENASTE NYHETERNA",
"viewAll": "VISA ALLA",
"checking": "KONTROLLERAR...",
"play": "SPELA"
},
"mods": {
"searchPlaceholder": "Sök moddar...",
"myMods": "MINA MODDAR",
"previous": "FÖREGÅENDE",
"next": "NÄSTA",
"page": "Sida",
"of": "av",
"modalTitle": "MINA MODDAR",
"noModsFound": "Inga moddar hittades",
"noModsFoundDesc": "Försök justera din sökning",
"noModsInstalled": "Inga moddar installerade",
"noModsInstalledDesc": "Lägg till moddar från CurseForge eller importera lokala filer",
"view": "VISA",
"install": "INSTALLERA",
"installed": "INSTALLERAD",
"enable": "AKTIVERA",
"disable": "INAKTIVERA",
"active": "AKTIV",
"disabled": "INAKTIVERAD",
"delete": "Ta bort modd",
"noDescription": "Ingen beskrivning tillgänglig",
"confirmDelete": "Är du säker på att du vill ta bort \"{name}\"?",
"confirmDeleteDesc": "Denna åtgärd kan inte ångras.",
"confirmDeletion": "Bekräfta borttagning",
"apiKeyRequired": "API-nyckel krävs",
"apiKeyRequiredDesc": "CurseForge API-nyckel behövs för att bläddra bland moddar"
},
"news": {
"title": "ALLA NYHETER",
"readMore": "Läs mer"
},
"chat": {
"title": "SPELARCHATT",
"pickColor": "Färg",
"inputPlaceholder": "Skriv ditt meddelande...",
"send": "Skicka",
"online": "online",
"charCounter": "{current}/{max}",
"secureChat": "Säker chatt - Länkar är censurerade",
"joinChat": "Gå med i chatten",
"chooseUsername": "Välj ett användarnamn för att gå med i spelarchartten",
"username": "Användarnamn",
"usernamePlaceholder": "Ange ditt användarnamn...",
"usernameHint": "3-20 tecken, endast bokstäver, siffror, - och _",
"joinButton": "Gå med i chatten",
"colorModal": {
"title": "Anpassa användarnamnsfargen",
"chooseSolid": "Välj en enfärgad färg:",
"customColor": "Anpassad färg:",
"preview": "Förhandsvisning:",
"previewUsername": "Användarnamn",
"apply": "Använd färg"
}
},
"settings": {
"title": "INSTÄLLNINGAR",
"java": "Java Runtime",
"useCustomJava": "Använd anpassad Java-sökväg",
"javaDescription": "Ersätt den medföljande Java-installationen med din egen",
"javaPath": "Java-körbar fil-sökväg",
"javaPathPlaceholder": "Välj Java-sökväg...",
"javaBrowse": "Bläddra",
"javaHint": "Välj Java-installationsmappen (stöder Windows, Mac, Linux)",
"discord": "Discord-integration",
"enableRPC": "Aktivera Discord Rich Presence",
"discordDescription": "Visa din launcher-aktivitet på Discord",
"game": "Spelalternativ",
"playerName": "Spelarnamn",
"playerNamePlaceholder": "Ange spelarnamn",
"playerNameHint": "Detta namn kommer att användas i spelet (1-16 tecken)",
"openGameLocation": "Öppna spelplats",
"openGameLocationDesc": "Öppna spelinstallationsmappen",
"account": "Spelare UUID-hantering",
"currentUUID": "Nuvarande UUID",
"uuidPlaceholder": "Laddar UUID...",
"copyUUID": "Kopiera UUID",
"regenerateUUID": "Återskapa UUID",
"uuidHint": "Din unika spelaridentifierare för detta användarnamn",
"manageUUIDs": "Hantera alla UUID:er",
"manageUUIDsDesc": "Visa och hantera alla spelare-UUID:er",
"language": "Språk",
"selectLanguage": "Välj språk",
"repairGame": "Reparera spel",
"reinstallGame": "Ominstallera spelfiler (bevarar data)",
"gpuPreference": "GPU-preferens",
"gpuHint": "Endast för bärbar dator; inställd på Integrerad om den är på datorn",
"gpuAuto": "Auto",
"gpuIntegrated": "Integrerad",
"gpuDedicated": "Dedikerad",
"logs": "SYSTEMLOGGAR",
"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",
"closeOnStartDescription": "Stäng automatiskt launcher efter att Hytale har startats",
"hwAccel": "Hårdvaruacceleration",
"hwAccelDescription": "Aktivera hårdvaruacceleration för launchern",
"gameBranch": "Spelgren",
"branchRelease": "Release",
"branchPreRelease": "Pre-Release",
"branchHint": "Växla mellan stabil release- och experimentell pre-release-version",
"branchWarning": "Att byta gren kommer att ladda ner och installera en annan spelversion",
"branchSwitching": "Byter till {branch}...",
"branchSwitched": "Bytte framgångsrikt till {branch}!",
"installRequired": "Installation krävs",
"branchInstallConfirm": "Spelet kommer att installeras för {branch}-grenen. Fortsätt?"
},
"uuid": {
"modalTitle": "UUID-hantering",
"allPlayerUUIDs": "Alla spelare-UUID:er",
"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)",
"setUUID": "Ange UUID",
"warning": "Varning: Att ange en anpassad UUID kommer att ändra din nuvarande spelaridentitet",
"copyTooltip": "Kopiera UUID",
"regenerateTooltip": "Generera ny UUID"
},
"configurations": {
"modalTitle": "Hantera konfigurationer",
"newProfilePlaceholder": "Nytt konfigurationsnamn",
"createProfile": "Skapa konfiguration"
},
"discord": {
"notificationText": "Gå med i vår Discord-gemenskap!",
"joinButton": "Gå med i Discord"
},
"common": {
"confirm": "Bekräfta",
"cancel": "Avbryt",
"save": "Spara",
"close": "Stäng",
"delete": "Ta bort",
"edit": "Redigera",
"loading": "Laddar...",
"apply": "Verkställ",
"install": "Installera"
},
"notifications": {
"gameDataNotFound": "Fel: Speldata hittades inte",
"gameUpdatedSuccess": "Spelet uppdaterades framgångsrikt! 🎉",
"updateFailed": "Uppdatering misslyckades: {error}",
"updateError": "Uppdateringsfel: {error}",
"discordEnabled": "Discord Rich Presence aktiverad",
"discordDisabled": "Discord Rich Presence inaktiverad",
"discordSaveFailed": "Misslyckades med att spara Discord-inställning",
"playerNameRequired": "Ange ett giltigt spelarnamn",
"playerNameSaved": "Spelarnamn sparat framgångsrikt",
"playerNameSaveFailed": "Misslyckades med att spara spelarnamn",
"uuidCopied": "UUID kopierad till urklipp!",
"uuidCopyFailed": "Misslyckades med att kopiera UUID",
"uuidRegenNotAvailable": "UUID-återgenerering ej tillgänglig",
"uuidRegenFailed": "Misslyckades med att återgenerera UUID",
"uuidGenerated": "Ny UUID genererad framgångsrikt!",
"uuidGeneratedShort": "Ny UUID genererad!",
"uuidGenerateFailed": "Misslyckades med att generera ny UUID",
"uuidRequired": "Ange en UUID",
"uuidInvalidFormat": "Ogiltigt UUID-format",
"uuidSetFailed": "Misslyckades med att ange anpassad UUID",
"uuidSetSuccess": "Anpassad UUID angiven framgångsrikt!",
"uuidDeleteFailed": "Misslyckades med att ta bort UUID",
"uuidDeleteSuccess": "UUID borttagen framgångsrikt!",
"modsDownloading": "Laddar ner {name}...",
"modsTogglingMod": "Växlar modd...",
"modsDeletingMod": "Tar bort modd...",
"modsLoadingMods": "Laddar moddar från CurseForge...",
"modsInstalledSuccess": "{name} installerad framgångsrikt! 🎉",
"modsDeletedSuccess": "{name} borttagen framgångsrikt",
"modsDownloadFailed": "Misslyckades med att ladda ner modd: {error}",
"modsToggleFailed": "Misslyckades med att växla modd: {error}",
"modsDeleteFailed": "Misslyckades med att ta bort modd: {error}",
"modsModNotFound": "Moddinformation hittades inte",
"hwAccelSaved": "Hårdvaruaccelerationsinställning sparad",
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning",
"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",
"regenerateUuidTitle": "Generera ny UUID",
"regenerateUuidMessage": "Är du säker på att du vill generera en ny UUID? Detta kommer att ändra din spelaridentitet.",
"regenerateUuidButton": "Generera",
"setCustomUuidTitle": "Ange anpassad UUID",
"setCustomUuidMessage": "Är du säker på att du vill ange denna anpassade UUID? Detta kommer att ändra din spelaridentitet.",
"setCustomUuidButton": "Ange UUID",
"deleteUuidTitle": "Ta bort UUID",
"deleteUuidMessage": "Är du säker på att du vill ta bort UUID:n för \"{username}\"? Denna åtgärd kan inte ångras.",
"deleteUuidButton": "Ta bort",
"uninstallGameTitle": "Avinstallera spel",
"uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.",
"uninstallGameButton": "Avinstallera",
"switchUsernameTitle": "Byt identitet",
"switchUsernameMessage": "Byta till användarnamn \"{username}\"? Detta kommer att ändra din nuvarande spelaridentitet.",
"switchUsernameButton": "Byt"
},
"progress": {
"initializing": "Initierar...",
"downloading": "Laddar ner...",
"installing": "Installerar...",
"extracting": "Extraherar...",
"verifying": "Verifierar...",
"switchingProfile": "Byter konfiguration...",
"profileSwitched": "Konfiguration bytt!",
"startingGame": "Startar spel...",
"launching": "STARTAR...",
"uninstallingGame": "Avinstallerar spel...",
"gameUninstalled": "Spel avinstallerat framgångsrikt!",
"uninstallFailed": "Avinstallation misslyckades: {error}",
"startingUpdate": "Startar obligatorisk speluppdatering...",
"installationComplete": "Installation slutförd framgångsrikt!",
"installationFailed": "Installation misslyckades: {error}",
"installingGameFiles": "Installerar spelfiler...",
"installComplete": "Installation slutförd!"
}
}

View File

@@ -8,10 +8,7 @@
},
"header": {
"playersLabel": "Oyuncular:",
"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ı",
"manageProfiles": "Profilleri Yönet",
"defaultProfile": "Varsayılan"
},
"install": {
@@ -25,13 +22,13 @@
"installationFolder": "Kurulum Klasörü",
"pathPlaceholder": "Varsayılan konum",
"browse": "Gözat",
"installButton": "HYTALE KUR",
"installButton": "HYTALE KURU",
"installing": "KURULUYOR..."
},
"play": {
"ready": "OYNAMAYA HAZIR",
"subtitle": "Hytale'ı başlat ve maceraya başla",
"playButton": "HYTALE'I OYNA",
"subtitle": "Hytale'i başlat ve maceraya başla",
"playButton": "HYTALE'YI OYNA",
"latestNews": "SON HABERLER",
"viewAll": "HEPSINI GÖR",
"checking": "KONTROL EDİLİYOR...",
@@ -50,13 +47,13 @@
"noModsInstalled": "Hiçbir Mod Kurulu Değil",
"noModsInstalledDesc": "CurseForge'dan modlar ekleyin veya yerel dosyalar içe aktarın",
"view": "GÖR",
"install": "KUR",
"install": "KURU",
"installed": "KURULU",
"enable": "",
"disable": "KAPAT",
"enable": "ETKİNLEŞTİR",
"disable": "DEĞİ",
"active": "AKTİF",
"disabled": "DEVREDIŞI",
"delete": "Modu sil",
"disabled": "DEĞİ",
"delete": "Modı sil",
"noDescription": "Açıklama yok",
"confirmDelete": "\"{name}\" öğesini silmek istediğinizden emin misiniz?",
"confirmDeleteDesc": "Bu işlem geri alınamaz.",
@@ -70,7 +67,7 @@
},
"chat": {
"title": "OYUNCU SOHBETI",
"pickColor": "Renk Seç",
"pickColor": "Renk",
"inputPlaceholder": "Mesajınızı yazın...",
"send": "Gönder",
"online": "çevrimiçi",
@@ -119,10 +116,10 @@
"manageUUIDsDesc": "Tüm oyuncu UUID'lerini görüntüleyin ve yönetin",
"language": "Dil",
"selectLanguage": "Dil Seçin",
"repairGame": "Oyunu Düzelt",
"repairGame": "Oyunu Onarı",
"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.",
"gpuHint": "Tercih ettiğiniz GPU'yu seçin (Linux: DRI_PRIME'ı etkiler)",
"gpuAuto": "Otomatik",
"gpuIntegrated": "Entegre",
"gpuDedicated": "Ayrılmış",
@@ -130,18 +127,10 @@
"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",
"closeOnStartDescription": "Hytale başlatıldıktan sonra başlatıcıyı otomatik olarak kapatın",
"hwAccel": "Donanım Hızlandırma",
"hwAccelDescription": "Başlatıcı için donanım hızlandırmasını etkinleştir",
"gameBranch": "Oyun Dalı",
"branchRelease": "Yayın",
"branchPreRelease": "Ön-Yayın",
@@ -154,12 +143,9 @@
},
"uuid": {
"modalTitle": "UUID Yönetimi",
"currentUserUUID": "Geçerli Kullanıcı UUID",
"allPlayerUUIDs": "Tüm Oyuncu UUID'leri",
"addIdentity": "Kimlik Ekle",
"usernamePlaceholder": "Kullanıcı Adı",
"add": "Ekle",
"cancel": "İptal",
"advanced": "Gelişmiş",
"generateNew": "Yeni UUID Oluştur",
"loadingUUIDs": "UUID'ler yükleniyor...",
"setCustomUUID": "Özel UUID Ayarla",
"customPlaceholder": "Özel UUID girin (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
@@ -168,10 +154,10 @@
"copyTooltip": "UUID'yi Kopyala",
"regenerateTooltip": "Yeni UUID Oluştur"
},
"configurations": {
"modalTitle": "Yapılandırmaları Yönet",
"newProfilePlaceholder": "Yeni Yapılandırma Adı",
"createProfile": "Yapılandırma Oluştur"
"profiles": {
"modalTitle": "Profilleri Yönet",
"newProfilePlaceholder": "Yeni Profil Adı",
"createProfile": "Profil Oluştur"
},
"discord": {
"notificationText": "Discord topluluğumuza katılın!",
@@ -221,15 +207,7 @@
"modsDownloadFailed": "Mod indirilemedi: {error}",
"modsToggleFailed": "Mod değiştirilemedi: {error}",
"modsDeleteFailed": "Mod silinemedi: {error}",
"modsModNotFound": "Mod bilgileri bulunamadı",
"hwAccelSaved": "Donanım hızlandırma ayarı kaydedildi",
"hwAccelSaveFailed": "Donanım hızlandırma ayarı kaydedilemedi",
"noUsername": "Kullanıcı adı yapılandırılmadı. Lütfen önce kullanıcı adınızı kaydedin.",
"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"
"modsModNotFound": "Mod bilgileri bulunamadı"
},
"confirm": {
"defaultTitle": "Eylemi onayla",
@@ -244,10 +222,7 @@
"deleteUuidButton": "Sil",
"uninstallGameTitle": "Oyunu kaldır",
"uninstallGameMessage": "Hytale'yi kaldırmak istediğinizden emin misiniz? Tüm oyun dosyaları silinecektir.",
"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"
"uninstallGameButton": "Kaldır"
},
"progress": {
"initializing": "Başlatılıyor...",
@@ -255,8 +230,8 @@
"installing": "Kuruluyur...",
"extracting": "Ayıklanıyor...",
"verifying": "Doğrulanıyor...",
"switchingProfile": "Yapılandırma değiştiriliyor...",
"profileSwitched": "Yapılandırma değiştirildi!",
"switchingProfile": "Profil değiştiriliyor...",
"profileSwitched": "Profil değiştirildi!",
"startingGame": "Oyun başlatılıyor...",
"launching": "BAŞLATILIYOR...",
"uninstallingGame": "Oyun kaldırılıyor...",
@@ -269,4 +244,3 @@
"installComplete": "Kurulum tamamlandı!"
}
}

View File

@@ -1,209 +0,0 @@
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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
# Maintainer: Fazri Gading <fazrigading@gmail.com>
# This PKGBUILD is for Github Releases
pkgname=Hytale-F2P
pkgver=2.2.1
pkgver=2.1.1
pkgrel=1
pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support"
arch=('x86_64')
@@ -10,8 +10,6 @@ url="https://github.com/amiayweb/Hytale-F2P"
license=('custom')
depends=('gtk3' 'nss' 'libxcrypt-compat')
makedepends=('npm')
provides=('Hytale-F2P-git' 'hytale-f2p-git')
conflicts=('Hytale-F2P-git' 'hytale-f2p-git')
source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')

View File

@@ -10,25 +10,25 @@ url="https://github.com/amiayweb/Hytale-F2P"
license=('custom')
depends=('gtk3' 'nss' 'libxcrypt-compat')
makedepends=('git' 'npm')
provides=('Hytale-F2P' 'hytale-f2p-git')
conflicts=('Hytale-F2P' 'hytale-f2p-git')
source=("git+$url.git" "$_pkgname.desktop")
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
pkgver() {
cd "$_pkgname"
cd "$srcdir/$_pkgname"
git describe --tags --long | sed 's/^v//;s/-/.r/;s/-/./'
}
build() {
cd "$_pkgname"
cd "$srcdir/$_pkgname"
npm ci
npm run build:arch
}
package() {
mkdir -p "$pkgdir/opt/$_pkgname"
cd "$srcdir/$_pkgname"
install -d "$pkgdir/opt/$_pkgname"
cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname"
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
install -Dm644 "$_pkgname/GUI/icon.png" "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png"
install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png"
}

203
README.md
View File

@@ -4,22 +4,21 @@
<h1>🎮 Hytale F2P Launcher 🚀</h1>
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support!</small></p>
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)</small></p>
</header>
[![GitHub Downloads](https://img.shields.io/github/downloads/amiayweb/Hytale-F2P/total?style=for-the-badge)](https://github.com/amiayweb/Hytale-F2P/releases)
[![Version](https://img.shields.io/badge/Version-2.2.1-red?style=for-the-badge)](https://github.com/amiayweb/Hytale-F2P/releases)
[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-teal?style=for-the-badge)](https://github.com/amiayweb/Hytale-F2P/releases)
![Version](https://img.shields.io/badge/Version-2.1.1-green?style=for-the-badge)
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-orange?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
[![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/network/members)
[![GitHub issues](https://img.shields.io/github/issues/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/issues)
![License](https://img.shields.io/badge/License-Educational-purple?style=for-the-badge)
[![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members)
### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️
**If you find this project useful, please give it a STAR!**
#### 🛑 **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`** 🛑 -->
### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️
#### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
<p>
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
@@ -30,49 +29,43 @@
<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!**
[![Star History Chart](https://api.star-history.com/svg?repos=amiayweb/Hytale-F2P&type=date&legend=top-left)](https://www.star-history.com/#amiayweb/Hytale-F2P&type=date&legend=top-left)
</div>
---
## 📸 Screenshots
<div align="center">
<img src="https://i.imgur.com/wwuuMUf.png" alt="Hytale F2P Launcher" width="1000">
<img src="https://i.imgur.com/xW9do3d.png" alt="Hytale F2P Launcher" width="1000">
<details>
<summary><b>View Gallery</b></summary>
<table style="width: 100%; border-spacing: 15px; border-collapse: separate;">
<tr>
<td align="center" style="vertical-align: top; width: 50%;">
<b>Featured Servers 🆕</b><br>
<img src="https://i.imgur.com/fEu9y3Z.png" alt="Hytale F2P Featured Servers" width="100%">
<b>Mods Preview</b><br>
<img src="https://i.imgur.com/f8qyIJy.png" alt="Hytale F2P Mods" width="100%">
</td>
<td align="center" style="vertical-align: top; width: 50%;">
<b>Settings Page ⚙️</b><br>
<img src="https://i.imgur.com/l5iBzxc.png" alt="Hytale F2P Settings Page" width="100%">
<b>Latest News</b><br>
<img src="https://i.imgur.com/qu0HltD.png" alt="Hytale F2P News" width="100%">
</td>
</tr>
<tr>
<td align="center" style="vertical-align: top; width: 50%;">
<b>Downloadable Mods from CurseForge 🛠️</b><br>
<img src="https://i.imgur.com/QIDbqYn.png" alt="Hytale F2P Mods Download" width="100%">
<b>Social & Chat</b><br>
<img src="https://i.imgur.com/t3GmbfF.png" alt="Hytale F2P Chat" width="100%">
</td>
<td align="center" style="vertical-align: top; width: 50%;">
<b>My Mods Menu 🔧</b><br>
<img src="https://i.imgur.com/rjvwUfq.png" alt="Hytale F2P My Mods Menu" width="100%">
<b>Settings</b><br>
<img src="https://i.imgur.com/uUD7lDB.png" alt="Hytale F2P Settings" width="100%">
</td>
</tr>
<tr>
<td align="center" style="vertical-align: top; width: 50%;">
<b>In-Game Screenshot - Spawn Point 🎮</b><br>
<b>In-Game Screenshot - Spawn Point</b><br>
<img src="https://i.imgur.com/X8lNFQ7.png" alt="Hytale F2P In-Game Screenshot-1" width="100%">
</td>
<td align="center" style="vertical-align: top; width: 50%;">
<b>In-Game Screenshot - Gameplay Terrain 🌳</b><br>
<b>In-Game Screenshot - Gameplay Terrain</b><br>
<img src="https://i.imgur.com/3iRScPa.png" alt="Hytale F2P In-Game Screenshot-2" width="100%">
</td>
</tr>
@@ -86,7 +79,7 @@
🎯 **Core Features**
- 🔄 **Automatic Updates** - Smart version checking and seamless game updates
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
- 🌐 **Cross-Platform** - Full support for Windows x64, Linux x64 (X11/Wayland, SteamDeck), and macOS Silicon
- 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS
-**Java Management** - Automatic Java runtime detection and installation
- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows, macOS & Linux !)
@@ -94,6 +87,7 @@
- 📁 **Custom Installation** - Choose your own installation directory
- 🔍 **Smart Detection** - Automatic game and dependency detection
- 🗂️ **Mod Support** - Built-in mod management system
- 💬 **Player Chat** - Integrated chat system for community interaction
- 📰 **News Feed** - Stay updated with the latest Hytale news
- 🎨 **Modern UI** - Clean, responsive interface with dark theme
@@ -124,9 +118,9 @@
<tr>
<td><b>🖥️ OS</b></td>
<td colspan="3" align="center">
Windows 10/11 (64-bit X64) | Linux (x64) | macOS (ARM64/Apple Silicon)
Windows 10/11 (64-bit; X64/ARM64) | Linux (x64/ARM64) | macOS (Apple Silicon only)
<br />
<small><i>⚠️ Note: ARM64 (Windows & Linux), macOS (x86/Intel) <b>are not supported!</b> ⚠️</i></small>
<small><i>⚠️ Note: macOS Intel (x86) is not yet supported <sup><a href="#fn1" id="ref1">1</a></sup></i></small>
</td>
</tr>
<tr>
@@ -137,7 +131,7 @@
</tr>
<tr>
<td><b>🧠 RAM</b></td>
<td>8GB (dGPU) / 12GB (iGPU)<sup><a href="#fn1" id="ref1">1</a></sup></td>
<td>8GB (dGPU)<sup><a href="#fn1" id="ref2">2</a></sup> /<br>12GB (iGPU)<sup><a href="#fn1" id="ref3">3</a></sup></td>
<td>16 GB</td>
<td>32 GB</td>
</tr>
@@ -162,28 +156,31 @@
</tbody>
</table>
</div>
<p id="fn1"><sup>Note 1</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum, while using Integrated GPU (iGPU) must have 12 GB RAM.</p>
<p id="fn1"><sup>Note 1</sup> Hytale did not provide game files for macOS Intel, yet.</p>
<p id="fn2"><sup>Note 2</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.</p>
<p id="fn3"><sup>Note 3</sup> Using Integrated GPU (dGPU) must have 12 GB RAM minimum.</p>
> [!WARNING]
> Our launcher has **not yet** supported Offline Mode (playing Hytale without internet).
> We will surely add the feature as soon as possible. Kindly wait for the update.
---
### 🪟 Windows Prequisites
* **
* **Java JDK 25:**
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows)
* or [Alt 1: Adoptium](https://adoptium.net/temurin/releases/?version=25)
* or [Alt 2: Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download).
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21.
* [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only.
* [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25.
* Download from any vendor if your OS is not Windows with ARM64 architecture.
* **Latest Visual Studio Redist:**
* 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)
* 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/)
### 🐧 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.
* [Not needed in update v2.2.0+] Install `libpng` package to avoid `SDL3_Image` error:
> [!WARNING]
> Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973).
* Make sure you have already installed newest **GPU driver**, consult your distro docs or wiki.
* 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
@@ -197,12 +194,11 @@
1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above.
2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
* Click **More info**, then click **Run anyway**.
* Click **More info**.
* Click **Run anyway**.
4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu.
5. **Whitelist in Windows Firewall** [#192](https://github.com/amiayweb/Hytale-F2P/issues/192#issuecomment-3803042908)
* Open the Windows Start Menu and search for `Allow an app through Windows Firewall`
* Click "Change settings" (you may need Admin privileges) and Locate `HytaleClient.exe` in the list.
* Ensure both the Private and Public checkboxes are checked. Click OK to save.
---
### 🐧 Linux Installation
@@ -223,7 +219,7 @@
# Fedora/RHEL-based
sudo dnf install hytale-f2p-launcher.rpm
# Debian/Ubuntu
sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76 # Not needed in v2.2.0+
sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76
sudo dpkg -i hytale-f2p-launcher.deb
```
* **Arch Linux (pacman):** Install the package using:
@@ -242,6 +238,13 @@
> [!NOTE]
> Make sure to adjust the filename correctly with the version and the architecture type. TIP: Use `cd` command to the package location.
4. **Troubleshooting:**
* **FUSE:** If the AppImage fails to launch on newer distributions, ensure `libfuse2` (or `fuse2` on Arch/Fedora) is installed.
* **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid.
* Missing libxcrypt.so.1: Install `libxcrypt-compat` using your package manager
---
### 🍎 macOS Installation
> [!NOTE]
@@ -256,7 +259,7 @@
* Look for the message regarding "Hytale F2P Launcher" and click **Open Anyway**.
* Authenticate with your password and click **Open**.
#### **Advanced macOS: Manual Installation (.zip)**
#### **Advanced: Manual Installation (.zip)**
The `.zip` version is useful for users who prefer a portable installation or need to bypass specific permission issues.
1. **Extract:** Download and unzip the file to your desired location (e.g., `~/Applications`).
@@ -269,41 +272,34 @@ The `.zip` version is useful for users who prefer a portable installation or nee
---
# 📢 How to Host a Server
# How to Host a Server
## 🌐 Host your Singleplayer Server (Online-Play Feature)
## Host your Singleplayer Server (Online-Play Feature)
> [!NOTE]
> You have to play the game to host the server. See Dedicated Server section below if you want to host it without you playing as the host.
1. Open your Singleplayer World
2. Pause the game (Esc) > select Online Play > Turn on `Allow Other Players to Join` > Set password if needed > Press `Save`.
3. Check the status `Connected via 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).
3. Check the status `Connected via STUN` or `Connected via UPnP`.
## 🖧 Host a Dedicated Server
## Dedicated Server
> [!NOTE]
> If you already have the patched `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server. Put the `.bat`/`.sh` script from our Discord server inside your `.../latest/Server` folder.
> If you have already `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
> [!TIP]
> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
> [!WARNING]
> `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.
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting).
> [!IMPORTANT]
> See detailed information of setting up a server here: [SERVER.md](SERVER.md).
> See detailed information of setting up a server here: [SERVER.md](SERVER.md)
---
## 🔧 Troubleshooting
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed Troubleshooting guide.
---
## 🔨 Building from Source
## 🛠️ Building from Source
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
@@ -311,38 +307,15 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
## 📋 Changelog
### 🆕 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.
- 💾 **New User Data Location:** UserData Migration to Centralized Location. User data now preserves in `HytaleSaves` located beside `HytaleF2P` folder.
- 🎮 **SteamDeck and Ubuntu/Debian-based Library Fix:** Replace bundled `libzstd.so` with system version to fix `glibc 2.41+` crash.
- 🍎 **Launcher auto-update Improvement for macOS:** Fix auto-install fails on unsigned app. Added option to download the new launcher version on Github website.
- 🌎 **New Translations**: Added France 🇲🇫, German 🇩🇪, Indonesian 🇮🇩, Russia 🇷🇺, and Swedish 🇸🇪 translations to the launcher.
- 🔐 **Fixes Tar Vulnerability:** Updates `tar` from version `6.2.1` to `7.5.7` for vulnerability issue.
- ⚙️ **Improved Settings Pane UI:** Settings are now shown in two columns instead of one. No more doom scrolling just to change your language.
- ⭐ **Added Features Servers:** Don't know which one to play? Join our Featured Servers!
- 💬 **Removed Chat Pane and Add Discord Feature:** Useless chat feature, we got Discord. Join it, NOW. Also added Discord RPC features to Github and our Discord Server. SHOW OFF TO YOUR FRIENDS.
- 🔍 **Investigation on Avatar Not Saving Bug:** We are currently investigating this issue.
<details><summary>Click here to see older Changelogs</summary>
### 🔄 v2.1.1
### 🆕 v2.1.1
- 🛠️ **Fix Bug EPERM**: EPERM or Error Permission in creating/removing process in reinstalling is now fixed.
- 🅰️ **Adds .pkg.tar.zst Build for Arch Users**: This Arch-package has been needed since the first release.
- ❎ **Removes .pacman Build for Arch**: Based on the established conventions within the Arch Linux community, the file extension .pacman should not be used for package files.
- 🌎 **New Translation**: New Polish 🇵🇱 Translation added to the Launcher.
<details>
<summary>Click here to see older Changelogs</summary>
### 🔄 v2.1.0
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
- ⚡ **Hardware Acceleration** —
@@ -410,7 +383,6 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
- 🎨 **Modern Interface** - Clean, intuitive design
- 🌟 **First Release** - Core launcher functionality
</details>
---
## 👥 Contributors
@@ -424,40 +396,39 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
</div>
### 🏆 Project Creator
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator | Windows*
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
### 🌟 Main Contributors
### 🌟 Contributors
- [**@sanasol**](https://github.com/sanasol) - *Main Issues fixer | Multiplayer Patcher*
- [**@Terromur**](https://github.com/Terromur) - *Main Issues fixer | Beta tester*
- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester | Github Release Maintainer*
- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester*
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
- [**@crimera**](https://github.com/crimera) - *Issues fixer*
- [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer*
- [**@Rahul-Sahani04**](https://github.com/Rahul-Sahani04) - *Issues fixer*
- [**@xSamiVS**](https://github.com/xSamiVS) - *Issues fixer | Language Translator*
#### 🎟️ Fresh Contributors
- [**@GreenKod**](https://github.com/GreenKod) - *Code refractor*
- [**@Citeli-py**](https://github.com/Citeli-py) - *Linux fix & packages version in early release*
- [**@crimera**](https://github.com/crimera) - *Generate new UUID for new username string feature*
- [**@letha11**](https://github.com/letha11) - *CSS filename typo fix*
- [**@colbster937**](https://github.com/colbster937) - *Icon upscaler*
- [**@ArnavSingh77**](https://github.com/ArnavSingh77) - *Close game launcher on start feature, improve app termination behavior*
- [**@TalesAmaral**](https://github.com/TalesAmaral) - *BUILD.md link fix in README.md*
#### 🌐 Language Translators
- [**@BlackSystemCoder**](https://github.com/BlackSystemCoder) - *Russian Language Translator*
- [**@walti0**](https://github.com/walti0) - *Polish Language Translator*
- [**@xSamiVS**](https://github.com/xSamiVS) - *Language Translator*
---
## 📞 Contact Information
## 📊 GitHub Stats
<div align="center">
**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 -->
![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub issues](https://img.shields.io/github/issues/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub downloads](https://img.shields.io/github/downloads/amiayweb/Hytale-F2P/total?style=for-the-badge&logo=github)
</div>
## 📞 Support
<div align="center">
**Need help?** Join us: https://discord.gg/gME8rUy3MB
</div>

437
SERVER.md
View File

@@ -1,166 +1,34 @@
# 🎮 Hytale F2P Server Guide
# Hytale F2P Server Guide
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
### **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. 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. 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-)
* [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced)
* [Step 1: Get the Files Ready](#step-1-get-the-files-ready)
* [Step 2: Place HytaleServer.jar in the Server directory](#step-2-place-hytaleserverjar-in-the-server-directory)
* [Step 3: Run the Server](#step-3-run-the-server)
* [D. Tinkering Guides](#d-tinkering-guides)
* [1. Network Setup](#1-network-setup)
* [2. Configuration](#2-configuration)
* [3. RAM Allocation Guide](#3-ram-allocation-guide)
* [4. Server Commands](#4-server-commands)
* [5. Command Line Options](#5-command-line-options)
* [6. File Structure](#6-file-structure)
* [7. Backups](#7-backups)
* [8. Troubleshooting](#8-troubleshooting)
* [9. Docker Deployment (Advanced)](#9-docker-deployment-advanced)
* [10. Getting Help](#10-getting-help)
---
<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, weve 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!** Dont 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.
DOWNLOAD SERVER FILES HERE: https://discord.gg/MEyWUxt77m
---
### "Server" Term and Definiton
"HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (Thanks to Sanasol), handling approximately thousands of players worldwide to play this game for free.
Kindly support us via [our Buy Me a Coffee link](https://buymeacoffee.com/hf2p) if you think our launcher took a big part of developing this Hytale community for the love of the game itself.
**We will always advertise, always pushing, and always asking, to every users of this launcher to purchase the original game to help the official development of Hytale**.
### Server Directory Location
Here are the directory locations of Server folder if you have installed 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 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
> can be acquired from our launcher via our `HytaleServer.rar` server file (which contains patched `HytaleServer.jar`, `Assets.zip`, and `run_server` scripts in `.sh & .bat`.
---
# A. Host Your Singleplayer World
This feature is perfect for 1-5 players that want to just play instantly with friends.
Terms and conditions applies.
## 1. Using Online-Play Feature / In-Game Invite Code
## Part 1: Playing with Friends (Online Play)
The easiest way to play with friends - no manual server setup required!
*The game automatically handles networking using UPnP/STUN/NAT traversal.*
### How It Works
1. **Start the game** via F2P Launcher
2. **Click "Online Play"** in the main menu
3. **Share the invite code** with your friends
4. Friends enter your invite code to join
The game automatically handles networking using UPnP/STUN/NAT traversal.
### Network Requirements
For Online Play to work, you need:
**For Online Play to work, you need:**
- **UPnP enabled** on your router (most routers have this on by default)
- **Public IP address** from your ISP (not behind CGNAT)
> [!TIP]
> Hoster need to make sure that the router can use UPnP: read router docs, wiki, or watch Youtube tutorials.
>
> If you encounter any problem, check Common Issues section below!
### Common Issues
1. Press **Worlds** on the Main Menu.
2. Select which world you want to play with your friend.
3. Once you get in the world, press **ESC**/Pause the game.
4. Press **Online Play** in the Pause Menu.
5. Set option "Allow Other Players to Join" from OFF to **ON**. You can set Password if you want.
6. Press **Save**, the Invite Code will appear.
7. Press **Copy to Clipboard** and **Share the Invite Code** to your friends!
8. Friends: Press **Servers** in the Main Menu > Press **Join via Code** > Paste the Code > Join.
> [!WARNING]
> If other players can't join the Hoster with error: `Failed to connect to any available address. The host may be offline or behind a strict firewall.`
>
> **AND ALSO** the Hoster "Online Play" menu shows `Connected to STUN - NAT Type: Restricted (No UPnP)`,
>
> this means the Online Play is **unavailable** on the Hoster machine, and it is neccessary to use services to host your world. **We recommend Playit.gg!**
### Common Issues (UPnP/NAT/STUN) on Online Play
<details><summary><b>a. "NAT Type: Carrier-Grade NAT (CGNAT)" Warning</b></summary>
#### "NAT Type: Carrier-Grade NAT (CGNAT)" Warning
If you see this message:
```
@@ -172,14 +40,14 @@ Warning: Your network configuration may prevent other players from connecting.
**What this means:** Your ISP doesn't give you a public IP address. Multiple customers share one public IP, which blocks incoming connections.
**Solutions:**
1. **Contact your ISP** - Request a public/static IP address (may cost extra)
2. **Use a VPN with port forwarding** - Services like Mullvad, PIA, or AirVPN offer this
3. **Use Playit.gg / Tailscale / Radmin VPN** - Create a virtual LAN with friends (see below)
3. **Use Radmin VPN or Playit.gg** - Create a virtual LAN with friends (see below)
4. **Have a friend with public IP host instead**
</details>
<details><summary><b>b. "UPnP Failed" or "Port Mapping Failed" Warning</b></summary>
#### "UPnP Failed" or "Port Mapping Failed"
**Check your router:**
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`)
2. Find UPnP settings (often under "Advanced" or "NAT")
@@ -188,148 +56,117 @@ Warning: Your network configuration may prevent other players from connecting.
**If UPnP isn't available:**
- Manually forward **port 5520 UDP** to your computer's local IP
- See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below
</details>
- See "Port Forwarding" section below
<details><summary><b>c. "Connected via STUN", "Strict NAT" or "Symmetric NAT" Warning</b></summary>
#### "Strict NAT" or "Symmetric NAT"
Some routers have restrictive NAT that blocks peer connections.
**Try:**
1. Enable "NAT Passthrough" or "NAT Filtering: Open" in router settings
2. Put your device in router's DMZ (temporary test only)
3. Use Playit.gg / Tailscale / Radmin VPN as workaround
</details>
3. Use Radmin VPN as workaround
## 2. Using Tailscale
### Workarounds for NAT/CGNAT Issues
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.
[Once installed, Tailscale starts and live inside your hidden icon section in Windows, Mac and Linux]
2. Create a **common Tailscale** account which will shared among your friends to log in.
3. Ask your **host to login in to thier Tailscale client first**, then the other members.
* Host
* Open your singleplayer world
* Go to Online Play settings
* Re-save your settings to generate a new share code
* Friends
* Ensure Tailscale is connected
* Use the new share code to connect
* To test your connection, ping the host's ipv4 mentioned in Tailscale
## 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
This option is perfect for any players size. From small to high.
## 1. Using Playit.gg (Recommended) ✅
#### Option 1: playit.gg (Recommended)
Free tunneling service - only the host needs to install it:
1. Go to https://playit.gg/login and **Log In** with your existing account, **Create Account** if you don't have one
2. Press "Add a tunnel" > Select `UDP` > Tunnel description of `Hytale Server` > Port count `1` > and Local Port `5520`
3. Press **Start the tunnel** (or you can just run the Playit.gg.EXE if you already installed it on your machine) - You'll get a public address like `xx-xx.gl.at.ply.gg:5520`
4. Go to https://playit.gg/download : `Installer` (Windows) or `x86-64` (Linux) or follow `Debian Install Script` (Debian-based only)
* Windows: Install the `playit-windows.msi`
* Linux:
* Right-click file > Properties > Turn on 'Executable as a Program' | or `chmod +x playit-linux-amd64` on terminal
* Run by double-clicking the file or `./playit-linux-amd64` via terminal
5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account.
6. 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]
===================================================
```
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.
1. **Download [playit.gg](https://playit.gg/)** and run it - Connect your account from the terminal (do not close it when playing on the server)
2. **Add a tunnel** - Select "UDP", tunnel description of "Hytale Server", port count `1`, and local port `5520`
3. **Start the tunnel** - You'll get a public address like `xx-xx.gl.at.ply.gg:5520`
4. **Share the address** - Friends connect directly using this address
> [!CAUTION]
> Do not close the Playit.gg Terminal OR HytaleServer Terminal if you are still playing or hosting the server.
Works with both Online Play and dedicated servers. No software needed for players joining.
## 2. Using Tailscale [DRAFT]
#### Option 2: Radmin VPN
Tailscale
Creates a virtual LAN - all players need to install it:
1. **Download [Radmin VPN](https://www.radmin-vpn.com/)** - All players install it
2. **Create a network** - One person creates, others join with network name/password
3. **Host via Online Play** - Use your Radmin VPN IP instead
4. **Friends connect** - They'll see you on the virtual LAN
Both options bypass all NAT/CGNAT issues. But for **Windows machines only!**
#### Option 3: Tailscale
It creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
1. All member's are required to download [Tailscale](https://tailscale.com/download) on your device.
[Once installed, Tailwind starts and live inside your hidden icon section in Windows, Mac and Linux]
2. Create a **common tailscale** account which will shared among your friends to log in.
3. Ask your **host to login in to thier tailscale client first**, then the other members.
##### Host
1. Open your singleplayer world
2. Go to Online Play settings
3. Re-save your settings to generate a new share code
##### Friends
1. Ensure Tailscale is connected
2. Use the new share code to connect
[To test your connection, ping the host's ipv4 mentioned in tailwind]
---
# C. 24/7 Dedicated Server (Advanced)
## Part 2: Dedicated Server (Advanced)
For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
## Step 1: Get the Files Ready
### Quick Start
### Prequisites
#### Step 1: Get the Server JAR
1. `HytaleServer.jar` (pre-patched for F2P players; dual-auth soon for Official + F2P play)
2. `Assets.zip`
3. `run_scripts_with_token.bat` for Windows or `run_scripts_with_token.sh` for macOS/Linux
The server scripts will automatically download the pre-patched server JAR if it's not present.
> [!NOTE]
> The `HytaleServer.rar` available on our Discord Server (`#open-public-server` channel; typo on the Discord, not `zip`) includes all of the prequisites.
> Unfortunately, the JAR inside the RAR isn't updated so you'll need to download the JAR from the link on Discord.
**Option A:** Let the scripts download automatically (requires `HYTALE_SERVER_URL` to be configured)
> [!TIP]
> You can copy `Assets.zip` generated from the launcher to be used for the dedicated server. It's located in `HytaleF2P/release/package/game/latest`.
**Option B:** Manually place `HytaleServer.jar` (pre-patched for F2P) in the Server directory:
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
## Step 2: Place `HytaleServer.jar` in the `Server` directory
If you have a custom install path, the Server folder is inside your custom location under `HytaleF2P/release/package/game/latest/Server`.
* Windows
* `%localappdata%\HytaleF2P\release\package\game\latest\Server`
* macOS
* `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
* Linux
* `~/.hytalef2p/release/package/game/latest/Server`
* If you have a custom install path, the Server folder is inside your custom location under
* `HytaleF2P/release/package/game/latest/Server`.
## Step 3: Run the Server
#### Step 2: Run the Server
**Windows:**
```batch
run_server_with_token.bat
cd scripts
run_server.bat
```
**macOS / Linux:**
```bash
./run_server_with_token.sh
cd scripts
./run_server.sh
```
The scripts will:
1. Find your game installation automatically
2. Download the pre-patched server JAR if needed
3. Fetch session tokens from the auth server
4. Start the server
#### Step 3: Connect Players
Share your server IP address with players. They connect via the F2P Launcher's server browser or direct connect.
---
# D. Tinkering Guides
## Network Setup (Dedicated Server)
## 1. Network Setup
### a. Local Network (LAN)
### Local Network (LAN)
If all players are on the same network:
1. Find your local IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux)
2. Share this IP with players on your network
3. Default port is `5520`
### b. Port Forwarding (Internet Play)
### Port Forwarding (Internet Play)
To allow direct internet connections:
1. Forward **port 5520 (UDP)** in your router
2. Find your public IP at [whatismyip.com](https://whatismyip.com)
3. Share your public IP with players
@@ -342,35 +179,36 @@ netsh advfirewall firewall add rule name="Hytale Server" dir=in action=allow pro
---
## 2. Configuration
## Configuration
### a. Environment Variables
### Environment Variables
Write this in the script file `.BAT`/`.SH` or set these manually in command before running to customize your server:
Set these before running to customize your server:
| Variable | Default | Description |
|----------|---------|-------------|
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
| `HYTALE_AUTH_DOMAIN` | `auth.sanasol.ws` | Auth server domain (4-16 chars) |
| `BIND_ADRESS` | `0.0.0.0:5520` | Server IP and port |
| `AUTH_MODE` | `authenticated` | Auth mode (see below) |
| `SERVER_NAME` | `My Hytale Server` | Server display name |
| `ASSETS_PATH` | `./Assets.zip` | Assets file location |
| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port |
| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) |
| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name |
| `HYTALE_GAME_PATH` | (auto-detected) | Override game location |
| `JVM_XMS` | `2G` | Minimum Java memory |
| `JVM_XMX` | `4G` | Maximum Java memory |
**Example (Windows):**
```batch
set SERVER_NAME=My Awesome Server
set HYTALE_SERVER_NAME=My Awesome Server
set JVM_XMX=8G
run_server.bat
```
**Example (Linux/macOS):**
```bash
SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
HYTALE_SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
```
### b. Authentication Modes
### Authentication Modes
| Mode | Description | Use Case |
|------|-------------|----------|
@@ -380,7 +218,7 @@ SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
---
## 3. RAM Allocation Guide
## RAM Allocation Guide
Adjust memory based on your system:
@@ -404,7 +242,7 @@ JVM_XMS=4G JVM_XMX=12G ./run_server.sh
---
## 4. Server Commands
## Server Commands
Once running, use these commands in the console:
@@ -421,12 +259,9 @@ Once running, use these commands in the console:
| `unban <player>` | Unban a player |
| `tp <player> <x> <y> <z>` | Teleport player |
Use `/` slash for these commands.
---
## 5. Command Line Options
## Command Line Options
Pass these when starting the server:
@@ -455,7 +290,7 @@ Pass these when starting the server:
---
## 6. File Structure
## File Structure
```
<game_path>/
@@ -473,21 +308,21 @@ Pass these when starting the server:
---
## 7. Backups
## Backups
### a. Automatic Backups
### Automatic Backups
```bash
./run_server.sh --backup --backup-dir ./backups --backup-frequency 30
```
### b. Manual Backup
### Manual Backup
1. Use `save` command or stop the server
2. Copy the `universe/` folder
3. Store in a safe location
### c. Restore
### Restore
1. Stop the server
2. Delete/rename current `universe/`
@@ -496,9 +331,9 @@ Pass these when starting the server:
---
## 8. Troubleshooting
## Troubleshooting
### a. "Java not found" or "Java version too old"
### "Java not found" or "Java version too old"
**Java 21 is REQUIRED** (the server uses class file version 65.0).
@@ -517,20 +352,30 @@ export PATH="$JAVA_HOME/bin:$PATH"
```
Add these lines to `~/.zshrc` or `~/.bash_profile` to make permanent.
### b. "Port already in use"
### "Game directory not found"
- Download game via F2P Launcher first
- Or set `HYTALE_GAME_PATH` environment variable
- Check custom install path in launcher settings
### "Assets.zip not found"
Game files incomplete. Re-download via the launcher.
### "Port already in use"
```bash
./run_server.sh --bind 0.0.0.0:5521
```
### c. "Out of memory"
### "Out of memory"
Increase JVM_XMX:
```bash
JVM_XMX=6G ./run_server.sh
```
### d. Players can't connect
### Players can't connect
1. Server shows "Server Ready"?
2. Using F2P Launcher (not official)?
@@ -538,7 +383,7 @@ JVM_XMX=6G ./run_server.sh
4. Port forwarding configured (for internet)?
5. Try `--auth-mode unauthenticated` for testing
### e. "Authentication failed"
### "Authentication failed"
- Ensure players use F2P Launcher
- Auth server may be temporarily down
@@ -546,7 +391,7 @@ JVM_XMX=6G ./run_server.sh
---
## 9. Docker Deployment (Advanced)
## Docker Deployment (Advanced)
For production servers, use Docker:
@@ -565,7 +410,40 @@ See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for
---
## 10. Getting Help
## Server Settings Summary
### Minimal Setup
```bash
./run_server.sh
```
### Custom Memory
```bash
JVM_XMS=2G JVM_XMX=8G ./run_server.sh
```
### Custom Port
```bash
HYTALE_BIND=0.0.0.0:25565 ./run_server.sh
```
### LAN Party (No Auth)
```bash
./run_server.sh --auth-mode unauthenticated
```
### Full Custom Setup
```bash
HYTALE_SERVER_NAME="Epic Server" \
HYTALE_BIND=0.0.0.0:5520 \
JVM_XMS=2G \
JVM_XMX=8G \
./run_server.sh --backup --backup-frequency 15 --allow-op
```
---
## Getting Help
- Check server console logs for errors
- Test with `--auth-mode unauthenticated` first
@@ -574,13 +452,8 @@ See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for
---
# Credits
## Credits
- Hytale F2P Project
- [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker)
- Auth Server: sanasol.ws

View File

@@ -1,462 +0,0 @@
# 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 [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 -->
---
## Table of Contents
- [Windows Issues](#windows-issues)
- [Linux Issues](#linux-issues)
- [macOS Issues](#macos-issues)
- [Connection & Server Issues](#connection--server-issues)
- [Authentication & Token Issues](#authentication--token-issues)
- [Avatar & Cosmetics Issues](#avatar--cosmetics-issues)
- [General Issues](#general-issues)
- [Known Limitations](#known-limitations)
---
## Windows Issues
### "Failed to connect to server" / Server won't boot
**Symptoms:** Singleplayer world fails to load, "Server failed to boot" error.
**Solution - Whitelist in Windows Firewall:**
1. Open **Windows Settings** > **Privacy & Security** > **Windows Security**
2. Click **Firewall & network protection** > **Allow an app through firewall**
3. Click **Change settings**
4. Find `HytaleClient.exe` and check both **Private** and **Public**
5. If not listed, click **Allow another app** and browse to:
```
%localappdata%\HytaleF2P\release\package\game\latest\Client\HytaleClient.exe
```
### Duplicate mod error
**Symptoms:** `java.lang.IllegalArgumentException: Tried to load duplicate plugin`
**Solution:**
1. Navigate to your mods folder:
```
%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\Mods
```
2. Remove any duplicate `.jar` files
3. Restart the launcher
### SmartScreen blocks the launcher
**Solution:**
1. Click **More info**
2. Click **Run anyway**
---
## Linux Issues
### GPU not detected / Using software rendering (llvmpipe)
**Symptoms:**
- Game uses integrated GPU instead of dedicated GPU
- Very low FPS / unplayable performance
- Play button not clickable
- Log shows `llvmpipe` instead of your GPU
**Solution for NVIDIA:**
```bash
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json ./HytaleF2P.AppImage
```
**Solution for AMD (hybrid GPU systems):**
```bash
DRI_PRIME=1 ./HytaleF2P.AppImage
```
**Permanent fix - Create a launcher script:**
```bash
#!/bin/bash
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json
export DRI_PRIME=1
./HytaleF2P.AppImage
```
**Note:** On some desktop systems with AMD iGPU + dGPU, the GPU selector may be inverted (selecting iGPU actually uses dGPU). Use whichever option works.
### SDL3_image / libpng errors
**Symptoms:**
- `DllNotFoundException: Unable to load shared library 'SDL3_image'`
- `libpng` related errors
- Game crashes on startup
**Solution - Install dependencies:**
**Fedora / RHEL:**
```bash
sudo dnf install libpng libpng-devel
```
**Debian / Ubuntu:**
```bash
sudo apt install libpng16-16 libpng-dev libgdiplus libc6-dev
```
**Arch Linux:**
```bash
sudo pacman -S libpng
```
**Alternative - Replace corrupted library:**
```bash
cd ~/.hytalef2p/release/package/game/latest/Client/
mv libSDL3_image.so libSDL3_image.so.bak
wget https://github.com/user-attachments/files/24710966/libSDL3_image.zip
unzip libSDL3_image.zip
chmod 644 libSDL3_image.so
rm libSDL3_image.zip
```
### AppImage won't launch / FUSE error
**Solution:**
```bash
# Debian/Ubuntu
sudo apt install libfuse2
# Fedora
sudo dnf install fuse
# Arch
sudo pacman -S fuse2
```
### Missing libxcrypt.so.1
**Solution:**
```bash
# Fedora/RHEL
sudo dnf install libxcrypt-compat
# Arch
sudo pacman -S libxcrypt-compat
```
### Wayland display issues
**Symptoms:** Game doesn't launch, stuck at loading, or display glitches on Wayland.
**Solution - Force X11:**
```bash
GDK_BACKEND=x11 ./HytaleF2P.AppImage
```
**Alternative - Electron Wayland hint:**
```bash
ELECTRON_OZONE_PLATFORM_HINT=auto ./HytaleF2P.AppImage
```
### Steam Deck / Gamescope issues
**Solution 1 - Add custom launch options in Steam:**
```
ELECTRON_OZONE_PLATFORM_HINT=x11 %command%
```
**Solution 2 - Launch from Desktop Mode** instead of Game Mode.
**Solution 3 - Force X11:**
```bash
GDK_BACKEND=x11 ./HytaleF2P.AppImage
```
### Ubuntu LTS-based distros (Linux Mint, Zorin OS, Pop!_OS)
These distributions may have compatibility issues due to older system packages. This is a limitation of the Hytale game client, not the launcher.
**Workarounds:**
1. Install all dependencies listed above
2. Try the SDL3_image replacement
3. Consider using a more recent distribution or Flatpak/AppImage with bundled dependencies
---
## macOS Issues
### "Butler system error -86" (Apple Silicon)
**Symptoms:** `Butler execution failed: spawn Unknown system error -86` (EXC_BAD_CPU_TYPE)
**Cause:** Butler (the update tool) is x86_64 only.
**Solution - Install Rosetta 2:**
```bash
softwareupdate --install-rosetta
```
Then restart the launcher.
### Auto-update fails with code signature error
**Symptoms:**
```
Code signature at URL did not pass validation
domain: 'SQRLCodeSignatureErrorDomain'
```
**Solution - Manual update:**
1. Download the latest version manually from [Releases](https://github.com/amiayweb/Hytale-F2P/releases/latest)
2. Backup your data first (see [Backup Locations](#backup-locations))
3. Install the fresh download
### "Unidentified developer" warning
**Solution:**
1. Open **System Settings** > **Privacy & Security**
2. Scroll to **Security** section
3. Find the message about "Hytale F2P Launcher"
4. Click **Open Anyway**
5. Authenticate and click **Open**
### App won't open (quarantine)
**Solution:**
```bash
xattr -rd com.apple.quarantine /Applications/Hytale-F2P-Launcher.app
```
---
## Connection & Server Issues
### "Failed to connect to server" in Singleplayer
**Possible causes:**
1. Windows Firewall blocking (see [Windows section](#failed-to-connect-to-server--server-wont-boot))
2. Patched server JAR download failed
3. Regional network restrictions
**Solution - Check patched JAR:**
1. Look for `HytaleServer.jar` in:
- Windows: `%localappdata%\HytaleF2P\release\package\game\latest\Server\`
- Linux: `~/.hytalef2p/release/package/game/latest/Server/`
- macOS: `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server/`
2. If missing or very small, the download may have failed
**Solution - Regional restrictions:**
Some countries (Russia, Turkey, Indonesia, etc.) may have issues accessing download servers.
- Try using a VPN for the initial download
- Once downloaded, the patched JAR is cached locally
### "Infinite Booting Server" / Server stuck loading
**Solution:**
1. Check if the patched JAR downloaded successfully (see above)
2. Ensure your GPU meets minimum requirements
3. Check launcher logs for specific errors
4. Try with a VPN if in a restricted region
### "Connection timed out from inactivity"
**This is expected behavior.** Sessions have a 10-hour TTL and will timeout after extended inactivity. Simply reconnect to continue playing.
---
## Authentication & Token Issues
### "Invalid identity token" / "Failed to start Hytale"
**Solution:**
1. **Restart the launcher** - This fetches fresh tokens
2. **Check system time** - JWT validation requires accurate system time
3. **Clear cached tokens:**
- Delete `config.json` from your HytaleF2P folder
- Restart the launcher
- Re-enter your username
**Locations:**
- Windows: `%localappdata%\HytaleF2P\config.json`
- Linux: `~/.hytalef2p/config.json`
- macOS: `~/Library/Application Support/HytaleF2P/config.json`
### Token refresh errors
If you see issuer mismatch errors in logs:
1. Delete `config.json` and `player_id.json`
2. Restart the launcher
3. This forces a fresh authentication
---
## Avatar & Cosmetics Issues
### Avatar/skin changes not saving
**This is a known F2P limitation:**
- F2P mode has no password protection for usernames
- Anyone can use any username
- Cosmetics are stored server-side by username
- If someone else uses your username, they can change your cosmetics
**Workaround:** Use a unique username that others are unlikely to choose.
### Character invisible / Customization crashes
**Solution:**
1. Use **Repair Game** in launcher Settings
2. Verify `Assets.zip` exists in your game folder
3. Clear cached assets:
- Windows: Delete `%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\CachedAssets\`
4. Restart the launcher
### Avatar creator shows "Offline Mode"
**Cause:** Cannot connect to auth server.
**Solution:**
1. Check your internet connection
2. Test connectivity: Open `https://auth.sanasol.ws/health` in browser (should show "OK")
3. Check if firewall is blocking the connection
4. Try disabling VPN (or enabling one if in restricted region)
---
## General Issues
### Mods not showing up
**Solution:**
1. Ensure mods are placed in the correct folder:
- Windows: `%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\Mods\`
- Linux: `~/.hytalef2p/release/package/game/latest/Client/UserData/Mods/`
- macOS: `~/Library/Application Support/HytaleF2P/release/package/game/latest/Client/UserData/Mods/`
2. Verify mod files are `.jar` format
3. Check launcher logs for mod loading errors
### Game updates delete configurations/mods
**This is a known issue being worked on.**
**Prevention - Always backup before updating:**
- Server configs and worlds
- Mods folder
- `config.json` and `player_id.json`
See [Backup Locations](#backup-locations) below.
### Play button not clickable
Usually caused by GPU detection failure. See [GPU not detected](#gpu-not-detected--using-software-rendering-llvmpipe).
**Alternative:**
1. Go to **Settings** > **Graphics**
2. Manually select your GPU
3. Restart the launcher
### Read timeout errors
**Cause:** Network connectivity issues.
**Solutions:**
1. Check your internet connection stability
2. Try using a VPN
3. Check firewall settings
4. Try at a different time (server load varies)
---
## Known Limitations
### Linux ARM64 not supported
Hytale does not provide ARM64 game client builds. The launcher downloads from official Hytale servers which only provide:
- Windows x64
- macOS (Universal/Intel)
- Linux x64
This is outside our control.
### F2P Username System
- No password protection for usernames
- Anyone can claim any username
- Cosmetics shared by username
- UUIDs generated based on username
A per-player password system is planned for future versions.
### Session Timeout
Game sessions have a 10-hour TTL. This is by design for security.
---
## Backup Locations
### Windows
```
%localappdata%\HytaleF2P\
├── config.json # Launcher settings
├── player_id.json # Player identity
└── release\package\game\latest\
├── Client\UserData\ # Saves, settings, mods
└── Server\
├── universe\ # World data
└── config.json # Server config
```
### Linux
```
~/.hytalef2p/
├── config.json
├── player_id.json
└── release/package/game/latest/
├── Client/UserData/
└── Server/
├── universe/
└── config.json
```
### macOS
```
~/Library/Application Support/HytaleF2P/
├── config.json
├── player_id.json
└── release/package/game/latest/
├── Client/UserData/
└── Server/
├── universe/
└── config.json
```
---
## Getting Help
If your issue isn't resolved by this guide:
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
2. **Join 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
- Full launcher logs from:
- Windows: `%localappdata%\HytaleF2P\logs\`
- Linux: `~/.hytalef2p/logs/`
- macOS: `~/Library/Application Support/HytaleF2P/logs/`
- Steps to reproduce the issue
---
## Logs Location
For bug reports, please include logs from:
| OS | Path |
|----|------|
| Windows | `%localappdata%\HytaleF2P\logs\` |
| Linux | `~/.hytalef2p/logs/` |
| macOS | `~/Library/Application Support/HytaleF2P/logs/` |

View File

@@ -4,10 +4,6 @@ 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) {
@@ -18,34 +14,6 @@ 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
@@ -248,10 +216,8 @@ class AppUpdater {
}
checkForUpdatesAndNotify() {
// 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 => {
// Check for updates and notify if available
autoUpdater.checkForUpdatesAndNotify().catch(err => {
console.error('Failed to check for updates:', err);
// Network errors are not critical - just log and continue
@@ -279,10 +245,8 @@ class AppUpdater {
}
checkForUpdates() {
// 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 => {
// Manual check for updates (returns promise)
return 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

View File

@@ -15,7 +15,7 @@ function getAppDir() {
}
/**
* Get centralized UserData saves directory (NEW in 2.2.0)
* Get centralized UserData saves directory (NEW in 2.1.2)
* UserData is now stored separately from game installation
*/
function getHytaleSavesDir() {
@@ -233,7 +233,7 @@ async function getModsPath(customInstallPath = null) {
function getProfilesDir(customInstallPath = null) {
try {
// NEW 2.2.0: Use centralized UserData location
// NEW 2.1.2: Use centralized UserData location
const userDataPath = getHytaleSavesDir();
const profilesDir = path.join(userDataPath, 'Profiles');

View File

@@ -1,7 +0,0 @@
const FORCE_CLEAN_INSTALL_VERSION = false;
const CLEAN_INSTALL_TEST_VERSION = 'v4';
module.exports = {
FORCE_CLEAN_INSTALL_VERSION,
CLEAN_INSTALL_TEST_VERSION
};

View File

@@ -5,8 +5,10 @@
const {
saveUsername,
loadUsername,
loadUsernameWithDefault,
hasUsername,
saveChatUsername,
loadChatUsername,
saveChatColor,
loadChatColor,
saveJavaPath,
loadJavaPath,
saveInstallPath,
@@ -18,22 +20,19 @@ const {
saveCloseLauncherOnStart,
loadCloseLauncherOnStart,
// Hardware Acceleration
saveLauncherHardwareAcceleration,
loadLauncherHardwareAcceleration,
loadConfig,
saveConfig,
saveModsToConfig,
loadModsFromConfig,
getUuidForUser,
isFirstLaunch,
markAsLaunched,
checkLaunchReady,
// UUID Management
getCurrentUuid,
getAllUuidMappings,
getAllUuidMappingsArray,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
@@ -45,13 +44,7 @@ const {
saveVersionClient,
loadVersionClient,
saveVersionBranch,
loadVersionBranch,
// Java Wrapper Config
getDefaultWrapperConfig,
loadWrapperConfig,
saveWrapperConfig,
resetWrapperConfig,
generateWrapperScript
loadVersionBranch
} = require('./core/config');
const { getResolvedAppDir, getModsPath } = require('./core/paths');
@@ -84,8 +77,7 @@ const {
loadInstalledMods,
downloadMod,
uninstallMod,
toggleMod,
getModFiles
toggleMod
} = require('./managers/modManager');
// Services
@@ -121,10 +113,11 @@ module.exports = {
// User configuration functions
saveUsername,
loadUsername,
loadUsernameWithDefault,
hasUsername,
saveChatUsername,
loadChatUsername,
saveChatColor,
loadChatColor,
getUuidForUser,
checkLaunchReady,
// Java configuration functions
saveJavaPath,
@@ -151,10 +144,6 @@ module.exports = {
saveLauncherHardwareAcceleration,
loadLauncherHardwareAcceleration,
// Config functions
loadConfig,
saveConfig,
// GPU Preference functions
saveGpuPreference,
loadGpuPreference,
@@ -176,7 +165,6 @@ module.exports = {
// UUID Management functions
getCurrentUuid,
getAllUuidMappings,
getAllUuidMappingsArray,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
@@ -188,7 +176,6 @@ module.exports = {
downloadMod,
uninstallMod,
toggleMod,
getModFiles,
saveModsToConfig,
loadModsFromConfig,
@@ -205,13 +192,6 @@ module.exports = {
proposeGameUpdate,
handleFirstLaunchCheck,
// Java Wrapper Config functions
getDefaultWrapperConfig,
loadWrapperConfig,
saveWrapperConfig,
resetWrapperConfig,
generateWrapperScript,
// Path functions
getResolvedAppDir
};

View File

@@ -1,327 +0,0 @@
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const { downloadFile, retryDownload } = require('../utils/fileManager');
const { getOS, getArch } = require('../utils/platformUtils');
const { validateChecksum, extractVersionDetails, 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');
async function acquireGameArchive(downloadUrl, targetPath, checksum, progressCallback, allowRetry = true) {
const osName = getOS();
const arch = getArch();
if (osName === 'darwin' && arch === 'amd64') {
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
}
if (fs.existsSync(targetPath)) {
const stats = fs.statSync(targetPath);
if (stats.size > 1024 * 1024) {
const isValid = await validateChecksum(targetPath, checksum);
if (isValid) {
console.log(`Valid archive found in cache: ${targetPath}`);
return targetPath;
}
console.log('Cached archive checksum mismatch, re-downloading');
fs.unlinkSync(targetPath);
}
}
console.log(`Downloading game archive from: ${downloadUrl}`);
// Try 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);
}
}
}
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;
}
const stats = fs.statSync(targetPath);
console.log(`Archive downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
const isValid = await validateChecksum(targetPath, checksum);
if (!isValid) {
console.log('Downloaded archive checksum validation failed, removing corrupted file');
fs.unlinkSync(targetPath);
throw new Error('Downloaded archive is corrupted or invalid. Please retry');
}
console.log(`Archive validation passed: ${targetPath}`);
return targetPath;
}
async function deployGameArchive(archivePath, destinationDir, toolsDir, progressCallback, isDifferential = false) {
if (!archivePath || !fs.existsSync(archivePath)) {
throw new Error(`Archive not found: ${archivePath || 'undefined'}`);
}
const stats = fs.statSync(archivePath);
console.log(`Deploying archive: ${archivePath}`);
console.log(`Archive size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
console.log(`Deployment mode: ${isDifferential ? 'differential' : 'full'}`);
const butlerPath = await installButler(toolsDir);
const stagingDir = path.join(destinationDir, 'staging-temp');
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir, { recursive: true });
}
if (fs.existsSync(stagingDir)) {
fs.rmSync(stagingDir, { recursive: true, force: true });
}
fs.mkdirSync(stagingDir, { recursive: true });
if (progressCallback) {
progressCallback(isDifferential ? 'Applying differential update...' : 'Installing game files...', null, null, null, null);
}
const args = [
'apply',
'--staging-dir',
stagingDir,
archivePath,
destinationDir
];
console.log(`Executing deployment: ${butlerPath} ${args.join(' ')}`);
return new Promise((resolve, reject) => {
const child = execFile(butlerPath, args, {
maxBuffer: 1024 * 1024 * 10,
timeout: 600000
}, (error, stdout, stderr) => {
if (error) {
const cleanStderr = stderr.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
const cleanStdout = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
if (cleanStderr) console.error('Deployment stderr:', cleanStderr);
if (cleanStdout) console.error('Deployment stdout:', cleanStdout);
const errorText = (stderr + ' ' + error.message).toLowerCase();
let message = 'Game deployment failed';
if (errorText.includes('unexpected eof')) {
message = 'Corrupted archive detected. Please retry download.';
if (fs.existsSync(archivePath)) {
fs.unlinkSync(archivePath);
}
} else if (errorText.includes('permission denied')) {
message = 'Permission denied. Check file permissions and try again.';
} else if (errorText.includes('no space left') || errorText.includes('device full')) {
message = 'Insufficient disk space. Free up space and try again.';
}
const deployError = new Error(message);
deployError.originalError = error;
deployError.stderr = cleanStderr;
deployError.stdout = cleanStdout;
return reject(deployError);
}
console.log('Game deployment completed successfully');
const cleanOutput = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
if (cleanOutput) {
console.log(cleanOutput);
}
if (fs.existsSync(stagingDir)) {
try {
fs.rmSync(stagingDir, { recursive: true, force: true });
} catch (cleanupErr) {
console.warn('Failed to cleanup staging directory:', cleanupErr.message);
}
}
resolve();
});
child.on('error', (err) => {
console.error('Deployment process error:', err);
reject(new Error(`Failed to execute deployment tool: ${err.message}`));
});
});
}
async function performIntelligentUpdate(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
console.log(`Initiating intelligent update to version ${targetVersion}`);
const currentVersion = getInstalledClientVersion();
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');
const versionDetails = await extractVersionDetails(targetVersion, branch);
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);
console.log(`Pre-release installation completed. Version ${targetVersion} is now installed.`);
return;
}
// Clean install (no current version)
if (currentBuild === 0) {
console.log('No existing installation detected - downloading full archive');
const versionDetails = await extractVersionDetails(targetVersion, branch);
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
if (progressCallback) {
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);
console.log(`Initial installation completed. Version ${targetVersion} is now installed.`);
return;
}
// Already at target
if (currentBuild >= targetBuild) {
console.log('Already at target version or newer');
return;
}
// 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;
if (progressCallback) {
progressCallback(`Downloading patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 0, null, null, null);
}
await acquireGameArchive(step.url, archivePath, null, progressCallback);
if (progressCallback) {
progressCallback(`Applying patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 50, null, null, null);
}
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, isDifferential);
// Clean up patch file
if (fs.existsSync(archivePath)) {
try {
fs.unlinkSync(archivePath);
console.log(`Cleaned up: ${stepName}.pwr`);
} catch (cleanupErr) {
console.warn(`Failed to cleanup: ${cleanupErr.message}`);
}
}
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) {
const { findClientPath } = require('../core/paths');
const clientPath = findClientPath(gameDir);
if (clientPath) {
const currentVersion = getInstalledClientVersion();
if (currentVersion === targetVersion) {
console.log(`Game already installed at correct version: ${targetVersion}`);
return;
}
}
await performIntelligentUpdate(targetVersion, branch, progressCallback, gameDir, cacheDir, toolsDir);
}
module.exports = {
acquireGameArchive,
deployGameArchive,
performIntelligentUpdate,
ensureGameInstalled
};

View File

@@ -7,29 +7,12 @@ const { spawn } = require('child_process');
const { v4: uuidv4 } = require('uuid');
const { getResolvedAppDir, findClientPath } = require('../core/paths');
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils');
const {
saveInstallPath,
loadJavaPath,
getUuidForUser,
getAuthServerUrl,
getAuthDomain,
loadVersionBranch,
loadVersionClient,
saveVersionClient,
loadUsername,
hasUsername,
checkLaunchReady,
loadWrapperConfig,
generateWrapperScript
} = require('../core/config');
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain, loadVersionBranch, loadVersionClient, saveVersionClient } = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getLatestClientVersion } = require('../services/versionManager');
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
const { ensureGameInstalled } = require('./differentialUpdateManager');
const { updateGameFiles } = require('./gameManager');
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;
@@ -64,39 +47,12 @@ async function fetchAuthTokens(uuid, name) {
}
const data = await response.json();
const identityToken = data.IdentityToken || data.identityToken;
const 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 };
return {
identityToken: data.IdentityToken || data.identityToken,
sessionToken: data.SessionToken || data.sessionToken
};
} catch (error) {
console.error('Failed to fetch auth tokens:', error.message);
// Fallback to local generation if server unavailable
@@ -146,72 +102,13 @@ function generateLocalTokens(uuid, name) {
};
}
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();
} catch (syncError) {
console.warn('[Launcher] Server list sync failed, continuing launch:', syncError.message);
}
// ==========================================================================
// STEP 3: Setup paths and directories
// ==========================================================================
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
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
// NEW 2.1.2: Use centralized UserData location
const userDataDir = getUserDataPath();
const gameLatest = customGameDir;
@@ -221,10 +118,7 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
throw new Error('Game is not installed. Please install the game first.');
}
// 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.
saveUsername(playerName);
if (installPathOverride) {
saveInstallPath(installPathOverride);
}
@@ -253,7 +147,6 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
}
const uuid = getUuidForUser(playerName);
console.log(`[Launcher] UUID for "${playerName}": ${uuid} (verify this stays constant across launches)`);
// Fetch tokens from auth server
if (progressCallback) {
@@ -276,15 +169,15 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
if (progressCallback && msg) {
progressCallback(msg, percent, null, null, null);
}
}, null, branch);
});
if (patchResult.success) {
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
if (patchResult.client) {
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
}
if (patchResult.agent) {
console.log(` Agent: ${patchResult.agent.alreadyExists ? 'already present' : patchResult.agent.success ? 'downloaded' : 'failed'}`);
if (patchResult.server) {
console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
}
} else {
console.warn('Game patching failed:', patchResult.error);
@@ -330,13 +223,23 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
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 = generateWrapperScript(loadWrapperConfig(), 'darwin', javaBin);
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[@]}"
`;
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
await signPath(javaWrapperPath, false);
console.log('Created java wrapper from config template');
console.log('Created java wrapper with --disable-sentry fix');
javaBin = javaWrapperPath;
}
} catch (signError) {
@@ -345,40 +248,6 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
}
}
// 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,
@@ -408,87 +277,39 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
console.log('Starting game...');
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
const env = { ...process.env };
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') {
const clientDir = path.dirname(clientPath);
const bundledLibzstd = path.join(clientDir, 'libzstd.so');
const backupLibzstd = path.join(clientDir, 'libzstd.so.bundled');
const waylandEnv = setupWaylandEnvironment();
Object.assign(env, waylandEnv);
// 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
];
const gpuEnv = setupGpuEnvironment(gpuPreference);
Object.assign(env, gpuEnv);
let systemLibzstd = null;
for (const p of systemLibzstdPaths) {
if (fs.existsSync(p)) {
systemLibzstd = p;
break;
}
}
// Linux memory allocator fixes for "free(): invalid pointer" crashes
// on Steam Deck (glibc 2.41) and Ubuntu LTS
if (process.platform === 'linux') {
// Option 1: Disable glibc heap validation
env.MALLOC_CHECK_ = '0';
if (systemLibzstd && fs.existsSync(bundledLibzstd)) {
try {
const stats = fs.lstatSync(bundledLibzstd);
// Option 2: Try to use jemalloc if available (more robust allocator)
// User can set HYTALE_USE_JEMALLOC=1 to enable
if (process.env.HYTALE_USE_JEMALLOC === '1') {
const jemalloc = require('fs').existsSync('/usr/lib/libjemalloc.so.2')
? '/usr/lib/libjemalloc.so.2'
: require('fs').existsSync('/usr/lib/x86_64-linux-gnu/libjemalloc.so.2')
? '/usr/lib/x86_64-linux-gnu/libjemalloc.so.2'
: null;
// Only replace if it's not already a symlink to system version
if (!stats.isSymbolicLink()) {
// Backup bundled version
if (!fs.existsSync(backupLibzstd)) {
fs.renameSync(bundledLibzstd, backupLibzstd);
console.log(`Linux: Backed up bundled libzstd.so`);
} else {
fs.unlinkSync(bundledLibzstd);
}
// Create symlink to system version
fs.symlinkSync(systemLibzstd, bundledLibzstd);
console.log(`Linux: Linked libzstd.so to system version (${systemLibzstd}) for glibc 2.41+ compatibility`);
} else {
const linkTarget = fs.readlinkSync(bundledLibzstd);
console.log(`Linux: libzstd.so already linked to ${linkTarget}`);
}
} catch (libzstdError) {
console.warn(`Linux: Could not replace libzstd.so: ${libzstdError.message}`);
}
}
}
// 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');
}
if (jemalloc) {
env.LD_PRELOAD = jemalloc + (env.LD_PRELOAD ? ':' + env.LD_PRELOAD : '');
console.log(`Linux: Using jemalloc allocator (${jemalloc})`);
} else {
console.log('Linux: jemalloc not found, using glibc with MALLOC_CHECK_=0');
}
} else {
console.log('Linux: Using glibc with MALLOC_CHECK_=0 (set HYTALE_USE_JEMALLOC=1 to try jemalloc)');
}
}
try {
let spawnOptions = {
@@ -504,35 +325,23 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
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;
if (child.stdout) {
child.stdout.on('data', (data) => {
outputReceived = true;
const msg = data.toString().trim();
console.log(`Game output: ${msg}`);
});
}
child.stdout.on('data', (data) => {
outputReceived = true;
console.log(`Game output: ${data.toString().trim()}`);
});
if (child.stderr) {
child.stderr.on('data', (data) => {
outputReceived = true;
const msg = data.toString().trim();
console.error(`Game error: ${msg}`);
});
}
child.stderr.on('data', (data) => {
outputReceived = true;
console.error(`Game error: ${data.toString().trim()}`);
});
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);
@@ -541,30 +350,30 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
child.on('exit', (code, signal) => {
hasExited = true;
clearTimeout(launchCheckTimeout);
if (code !== null) {
console.log(`Game process exited with code ${code}`);
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);
}
if (code !== 0 && progressCallback) {
progressCallback(`Game exited with error code ${code}`, -1, null, null, null);
}
} else if (signal) {
console.log(`Game process terminated by signal ${signal}`);
}
});
// 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);
}
// 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);
// Return immediately after spawn
// Return immediately, don't wait for setTimeout
return { success: true, installed: true, launched: true, pid: child.pid };
} catch (spawnError) {
console.error(`Error spawning game process: ${spawnError.message}`);
@@ -575,26 +384,10 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
}
}
async function launchGameWithVersionCheck(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
async function launchGameWithVersionCheck(playerName = 'Player', 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);
}
@@ -621,13 +414,7 @@ async function launchGameWithVersionCheck(playerNameOverride = null, progressCal
const customCacheDir = path.join(customAppDir, 'cache');
try {
let versionToInstall = latestVersion;
if (FORCE_CLEAN_INSTALL_VERSION && !installedVersion) {
versionToInstall = CLEAN_INSTALL_TEST_VERSION;
console.log(`TESTING MODE: Clean install detected, forcing version ${versionToInstall} instead of ${latestVersion}`);
}
await ensureGameInstalled(versionToInstall, branch, progressCallback, customGameDir, customCacheDir, customToolsDir);
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir, branch);
console.log('Game updated successfully, patching will be forced on launch...');
if (progressCallback) {
@@ -648,7 +435,7 @@ async function launchGameWithVersionCheck(playerNameOverride = null, progressCal
progressCallback('Launching game...', 80, null, null, null);
}
const launchResult = await launchGame(playerNameOverride, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
const launchResult = await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
// Ensure we always return a result
if (!launchResult) {

View File

@@ -1,110 +1,17 @@
const fs = require('fs');
const path = require('path');
const { execFile, exec } = require('child_process');
const { promisify } = require('util');
const { execFile } = require('child_process');
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, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
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');
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) {
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
const osName = getOS();
const arch = getArch();
@@ -112,69 +19,28 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
}
let url;
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}`);
}
}
// 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`);
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}`;
const dest = path.join(cacheDir, `${branch}_${fileName}`);
// 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) {
// 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`);
if (stats.size < 1024 * 1024) {
return false;
}
// Check if file is under 1.5 GB (incomplete download)
const sizeInMB = stats.size / 1024 / 1024;
if (sizeInMB < 1500) {
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
return false;
}
}
console.log(`[DownloadPWR] Downloading from: ${url}`);
console.log('Fetching PWR patch file:', url);
try {
if (manualRetry) {
@@ -200,7 +66,7 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
const retryStats = fs.statSync(dest);
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
if (!validatePWRFile(dest, expectedSize)) {
if (!validatePWRFile(dest)) {
console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`);
fs.unlinkSync(dest);
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
@@ -250,8 +116,8 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
// 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, expectedSize)) {
if (!validatePWRFile(dest)) {
console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`);
fs.unlinkSync(dest);
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
@@ -269,7 +135,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, skipExistingCheck = false) {
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR) {
console.log(`[Butler] Starting PWR application with:`);
console.log(`[Butler] - PWR file: ${pwrFile}`);
console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`);
@@ -293,12 +159,11 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
const gameLatest = gameDir;
const stagingDir = path.join(gameLatest, 'staging-temp');
if (!skipExistingCheck) {
const clientPath = findClientPath(gameLatest);
if (clientPath) {
console.log('Game files detected, skipping patch installation.');
return;
}
const clientPath = findClientPath(gameLatest);
if (clientPath) {
console.log('Game files detected, skipping patch installation.');
return;
}
// Validate and prepare directories
@@ -435,16 +300,6 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
fs.rmSync(stagingDir, { recursive: true, force: true });
}
// Delete PWR file from cache after successful installation
try {
if (fs.existsSync(pwrFile)) {
fs.unlinkSync(pwrFile);
console.log('[Butler] PWR file deleted from cache after successful installation:', pwrFile);
}
} catch (delErr) {
console.warn('[Butler] Failed to delete PWR file from cache:', delErr.message);
}
if (progressCallback) {
progressCallback('Installation complete', null, null, null, null);
}
@@ -461,7 +316,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
try {
// NEW 2.2.0: Ensure UserData migration to centralized location
// NEW 2.1.2: Ensure UserData migration to centralized location
try {
console.log('[UpdateGameFiles] Ensuring UserData migration...');
const migrationResult = await migrateUserDataToCentralized();
@@ -479,126 +334,57 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
}
console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`);
// Determine update strategy: intermediate patches vs full reinstall
const currentVersion = loadVersionClient();
const currentBuild = extractVersionNumber(currentVersion) || 0;
const targetBuild = extractVersionNumber(newVersion);
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
let useIntermediatePatches = false;
let updatePlan = null;
if (fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
}
fs.mkdirSync(tempUpdateDir, { recursive: true });
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);
}
if (progressCallback) {
progressCallback('Downloading new game version...', 20, null, null, null);
}
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}`;
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
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);
}
if (progressCallback) {
progressCallback('Extracting new files...', 60, null, null, null);
}
const pwrFile = await downloadPWR(branch, stepName, progressCallback, cacheDir, false, step.url, step.size);
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
if (!pwrFile) {
throw new Error(`Failed to download patch ${stepName}`);
}
if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null);
}
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
if (fs.existsSync(gameDir)) {
console.log('Removing old game files...');
let retries = 3;
while (retries > 0) {
try {
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.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);
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
console.log('Logo@2x.png update result after update:', logoResult);
// NEW 2.2.0: No longer create UserData in game installation
// NEW 2.1.2: No longer create UserData in game installation
// UserData is now in centralized location (getUserDataPath())
console.log('[UpdateGameFiles] UserData is managed in centralized location');
@@ -648,7 +434,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
// NEW 2.2.0: Ensure UserData migration to centralized location
// NEW 2.1.2: Ensure UserData migration to centralized location
try {
console.log('[InstallGame] Ensuring UserData migration...');
const migrationResult = await migrateUserDataToCentralized();
@@ -667,9 +453,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
}
});
// 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.
saveUsername(playerName);
if (installPathOverride) {
saveInstallPath(installPathOverride);
}
@@ -726,33 +510,31 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
console.log(`Installing game files for branch: ${branch}...`);
const latestVersion = await getLatestClientVersion(branch);
const targetVersion = FORCE_CLEAN_INSTALL_VERSION ? CLEAN_INSTALL_TEST_VERSION : latestVersion;
if (FORCE_CLEAN_INSTALL_VERSION) {
console.log(`TESTING MODE: Forcing installation of ${targetVersion} instead of ${latestVersion}`);
}
let pwrFile;
try {
pwrFile = await downloadPWR(branch, targetVersion, progressCallback, customCacheDir);
pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir);
// If downloadPWR returns false, it means the file doesn't exist or is invalid
// We should retry the download with a manual retry flag
if (!pwrFile) {
console.log('[Install] PWR file not found or invalid, attempting retry...');
pwrFile = await retryPWRDownload(branch, targetVersion, progressCallback, customCacheDir);
pwrFile = await retryPWRDownload(branch, latestVersion, progressCallback, customCacheDir);
}
// Double-check we have a valid file path
if (!pwrFile || typeof pwrFile !== 'string') {
throw new Error(`PWR file download failed: received invalid path ${pwrFile}. Please retry download.`);
}
} catch (downloadError) {
console.error('[Install] PWR download failed:', downloadError.message);
throw downloadError;
throw downloadError; // Re-throw to be handled by the main installGame error handler
}
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir, branch, customCacheDir);
saveVersionClient(targetVersion);
// Save the installed version and branch to config
saveVersionClient(latestVersion);
const { saveVersionBranch } = require('../core/config');
saveVersionBranch(branch);
@@ -762,7 +544,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
console.log('Logo@2x.png update result after installation:', logoResult);
// NEW 2.2.0: No longer create UserData in game installation
// NEW 2.1.2: No longer create UserData in game installation
// UserData is managed in centralized location (getUserDataPath())
console.log('[InstallGame] UserData is managed in centralized location');
@@ -784,14 +566,8 @@ 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 {
await safeRemoveDirectory(appDir);
fs.rmSync(appDir, { recursive: true, force: true });
console.log('Game uninstalled successfully - removed entire HytaleF2P folder');
if (fs.existsSync(CONFIG_FILE)) {
@@ -873,34 +649,14 @@ async function repairGame(progressCallback, branchOverride = null) {
progressCallback('Removing old game files...', 30, null, null, null);
}
// 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
// Delete Game and Cache Directory
console.log('Removing corrupted game files...');
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.`);
}
fs.rmSync(gameDir, { recursive: true, force: true });
const cacheDir = path.join(appDir, 'cache');
if (fs.existsSync(cacheDir)) {
console.log('Clearing cache directory...');
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
}
fs.rmSync(cacheDir, { recursive: true, force: true });
}
console.log('Reinstalling game files...');
@@ -964,30 +720,36 @@ function validateGameDirectory(gameDir, stagingDir) {
}
// Enhanced PWR file validation
// Accepts intermediate patches (50+ MB) and full installs (1.5+ GB)
function validatePWRFile(filePath, expectedSize = null) {
function validatePWRFile(filePath) {
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;
}
// 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`);
// Check if file is under 1.5 GB (incomplete download)
if (sizeInMB < 1500) {
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
return false;
}
console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
// Basic file header validation (PWR files should have specific headers)
const buffer = fs.readFileSync(filePath, { start: 0, end: 20 });
if (buffer.length < 10) {
return false;
}
// Check for common PWR magic bytes or patterns
// This is a basic check - could be enhanced with actual PWR format specification
const header = buffer.toString('hex', 0, 10);
console.log(`[PWR Validation] File header: ${header}`);
return true;
} catch (error) {
console.error(`[PWR Validation] Error:`, error.message);
@@ -1007,6 +769,5 @@ module.exports = {
installGame,
uninstallGame,
checkExistingGameInstallation,
repairGame,
killGameProcesses
repairGame
};

View File

@@ -340,70 +340,36 @@ async function extractJRE(archivePath, destDir) {
}
function extractZip(zipPath, dest) {
try {
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
for (const entry of entries) {
const entryPath = path.join(dest, entry.entryName);
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}`);
}
// 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;
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);
}
}
} catch (error) {
throw new Error(`Failed to extract ZIP archive: ${error.message}`);
}
}
function extractTarGz(tarGzPath, dest) {
try {
return tar.extract({
file: tarGzPath,
cwd: dest,
strip: 0
});
} catch (error) {
throw new Error(`Failed to extract TAR.GZ archive: ${error.message}`);
}
return tar.extract({
file: tarGzPath,
cwd: dest,
strip: 0
});
}
function flattenJREDir(jreLatest) {

View File

@@ -3,7 +3,7 @@ const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const { getOS } = require('../utils/platformUtils');
const { getModsPath, getProfilesDir, getHytaleSavesDir } = require('../core/paths');
const { getModsPath, getProfilesDir } = require('../core/paths');
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
const profileManager = require('./profileManager');
@@ -285,27 +285,6 @@ 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();
@@ -317,9 +296,8 @@ async function syncModsForCurrentProfile() {
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`);
// 1. Resolve Paths
// centralModsPath is HytaleSaves\Mods (centralized location for active mods)
const hytaleSavesDir = getHytaleSavesDir();
const centralModsPath = path.join(hytaleSavesDir, 'Mods');
// globalModsPath is the one the game uses (symlink target)
const globalModsPath = await getModsPath();
// profileModsPath is the real storage for this profile
const profileModsPath = getProfileModsPath(activeProfile.id);
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
@@ -328,51 +306,96 @@ async function syncModsForCurrentProfile() {
fs.mkdirSync(profileDisabledModsPath, { recursive: true });
}
// 2. Copy-based Mod Sync (No symlinks - avoids permission issues)
// Ensure HytaleSaves\Mods directory exists
if (!fs.existsSync(centralModsPath)) {
fs.mkdirSync(centralModsPath, { recursive: true });
console.log(`[ModManager] Created centralized mods directory: ${centralModsPath}`);
}
// Check for old symlink and convert to real directory if needed (one-time migration)
// 2. Symlink / Migration Logic
let needsLink = false;
let globalStats = null;
try {
const centralStats = fs.lstatSync(centralModsPath);
if (centralStats.isSymbolicLink()) {
console.log('[ModManager] Removing old symlink, converting to copy-based system...');
fs.unlinkSync(centralModsPath);
fs.mkdirSync(centralModsPath, { recursive: true });
}
globalStats = fs.lstatSync(globalModsPath);
} catch (e) {
// Path doesn't exist, will be created above
// Path doesn't exist
}
// Copy enabled mods from profile to HytaleSaves\Mods (for game to use)
console.log(`[ModManager] Copying enabled mods from ${profileModsPath} to ${centralModsPath}`);
// First, clear central mods folder
const existingCentralMods = fs.existsSync(centralModsPath) ? fs.readdirSync(centralModsPath) : [];
for (const file of existingCentralMods) {
const filePath = path.join(centralModsPath, file);
if (globalStats) {
if (globalStats.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(globalModsPath);
// Normalize paths for comparison
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
console.log(`[ModManager] Updating symlink from ${linkTarget} to ${profileModsPath}`);
fs.unlinkSync(globalModsPath);
needsLink = true;
}
} else if (globalStats.isDirectory()) {
// MIGRATION: It's a real directory. Move contents to profile.
console.log('[ModManager] Migrating global mods folder to profile folder...');
const files = fs.readdirSync(globalModsPath);
for (const file of files) {
const src = path.join(globalModsPath, file);
const dest = path.join(profileModsPath, file);
// Only move if dest doesn't exist to avoid overwriting
if (!fs.existsSync(dest)) {
fs.renameSync(src, dest);
}
}
// Also migrate DisabledMods if it exists globally
const globalDisabledPath = path.join(path.dirname(globalModsPath), 'DisabledMods');
if (fs.existsSync(globalDisabledPath) && fs.lstatSync(globalDisabledPath).isDirectory()) {
const dFiles = fs.readdirSync(globalDisabledPath);
for (const file of dFiles) {
const src = path.join(globalDisabledPath, file);
const dest = path.join(profileDisabledModsPath, file);
if (!fs.existsSync(dest)) {
fs.renameSync(src, dest);
}
}
// We can remove global DisabledMods now, as it's not used by game
try { fs.rmSync(globalDisabledPath, { recursive: true, force: true }); } catch(e) {}
}
// Remove the directory so we can link it
try {
let retries = 3;
while (retries > 0) {
try {
fs.rmSync(globalModsPath, { recursive: true, force: true });
break;
} catch (err) {
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
retries--;
await new Promise(resolve => setTimeout(resolve, 500));
} else {
throw err;
}
}
}
needsLink = true;
} catch (e) {
console.error('Failed to remove global mods dir:', e);
// Throw error to stop.
throw new Error('Failed to migrate mods directory. Please clear ' + globalModsPath);
}
}
} else {
needsLink = true;
}
if (needsLink) {
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
try {
fs.unlinkSync(filePath);
} catch (e) {
console.warn(`Failed to remove ${file} from central mods:`, e.message);
}
}
// Copy enabled mods to HytaleSaves\Mods
const enabledModFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
for (const file of enabledModFiles) {
const src = path.join(profileModsPath, file);
const dest = path.join(centralModsPath, file);
try {
fs.copyFileSync(src, dest);
console.log(`[ModManager] Copied ${file} to HytaleSaves\\Mods`);
} catch (e) {
console.error(`Failed to copy ${file}:`, e.message);
const symlinkType = getOS() === 'windows' ? 'junction' : 'dir';
fs.symlinkSync(profileModsPath, globalModsPath, symlinkType);
} catch (err) {
// If we can't create the symlink, try creating the directory first
console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.');
console.error(err.message);
// Fallback: create a real directory so the game still works
if (!fs.existsSync(globalModsPath)) {
fs.mkdirSync(globalModsPath, { recursive: true });
}
}
}
// 3. Auto-Repair (Download missing mods)
const profileModsSnapshot = activeProfile.mods || [];
@@ -437,7 +460,7 @@ async function syncModsForCurrentProfile() {
}
// 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods)
// Note: Enabled mods are copied to HytaleSaves\Mods, disabled mods stay in Profile/DisabledMods
// Note: Since Global/Mods IS Profile/Mods (via symlink), moving out of Profile/Mods disables it for the game.
const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
@@ -476,6 +499,5 @@ module.exports = {
syncModsForCurrentProfile,
generateModId,
extractModName,
extractVersion,
getModFiles
};
extractVersion
};

View File

@@ -1,7 +1,6 @@
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 {
@@ -14,8 +13,7 @@ async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
const homeUIUrl = 'https://files.hytalef2p.com/api/HomeUI';
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
const response = await smartRequest(homeUIUrl, { responseType: 'arraybuffer' });
fs.writeFileSync(tempHomePath, response.data);
await downloadFile(homeUIUrl, tempHomePath);
const existingHomePath = findHomePageUIPath(gameDir);
@@ -68,8 +66,7 @@ async function downloadAndReplaceLogo(gameDir, progressCallback) {
const logoUrl = 'https://files.hytalef2p.com/api/Logo';
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
const response = await smartRequest(logoUrl, { responseType: 'arraybuffer' });
fs.writeFileSync(tempLogoPath, response.data);
await downloadFile(logoUrl, tempLogoPath);
const existingLogoPath = findLogoPath(gameDir);

View File

@@ -3,117 +3,32 @@ const path = require('path');
const { v4: uuidv4 } = require('uuid');
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
/**
* DEPRECATED: This file is kept for backward compatibility.
*
* The primary UUID system is now in config.js using userUuids.
* This player_id.json system was a separate UUID storage that could
* cause desync issues.
*
* New code should use config.js functions:
* - getUuidForUser(username) - Get/create UUID for a username
* - getCurrentUuid() - Get current user's UUID
* - setUuidForUser(username, uuid) - Set UUID for a user
*
* This function is kept for migration purposes only.
*/
/**
* Get or create a legacy player ID
* NOTE: This is DEPRECATED - use config.js getUuidForUser() instead
*
* FIXED: No longer returns random UUID on error - throws instead
*/
function getOrCreatePlayerId() {
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
}
}
}
}
// FIXED: Do NOT return random UUID - throw error instead
// Returning random UUID was causing silent identity loss
console.error('[PlayerManager] CRITICAL: Failed to get/create player ID after all retries');
throw new Error(`Failed to manage player ID: ${lastError.message}`);
}
/**
* Migrate legacy player_id.json to config.json userUuids
* Call this during app startup
*/
function migrateLegacyPlayerId() {
try {
if (!fs.existsSync(PLAYER_ID_FILE)) {
return null; // No legacy file to migrate
if (!fs.existsSync(APP_DIR)) {
fs.mkdirSync(APP_DIR, { recursive: true });
}
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
if (!data.playerId) {
return null;
if (fs.existsSync(PLAYER_ID_FILE)) {
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
if (data.playerId) {
return data.playerId;
}
}
console.log(`[PlayerManager] Found legacy player_id.json with ID: ${data.playerId}`);
const newPlayerId = uuidv4();
fs.writeFileSync(PLAYER_ID_FILE, JSON.stringify({
playerId: newPlayerId,
createdAt: new Date().toISOString()
}, null, 2));
// 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;
return newPlayerId;
} catch (error) {
console.error('[PlayerManager] Error during legacy migration:', error.message);
return null;
console.error('Error managing player ID:', error);
return uuidv4();
}
}
module.exports = {
getOrCreatePlayerId,
migrateLegacyPlayerId
getOrCreatePlayerId
};

View File

@@ -1,539 +1,31 @@
const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { getOS, getArch } = require('../utils/platformUtils');
// 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';
// 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 {
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 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();
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);
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: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
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`);
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(`[Mirror] Branch '${branch}' not in mirror, constructing URL`);
console.log('Warning: Invalid API response, falling back to default version');
return '4.pwr';
}
} 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 = await buildArchiveUrl(buildNumber, branch);
try {
const response = await axios.head(url, { timeout: 10000 });
return response.status === 200;
} catch {
return false;
}
}
async function discoverAvailableVersions(latestKnown, branch = 'release') {
try {
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 = extractVersionNumber(targetVersion);
const fullUrl = await buildArchiveUrl(buildNumber, branch);
return {
version: targetVersion,
buildNumber,
buildName: `HYTALE-Build-${buildNumber}`,
fullUrl,
differentialUrl: null,
checksum: null,
sourceVersion: null,
isDifferential: false,
releaseNotes: null
};
}
function canUseDifferentialUpdate() {
// Differential updates are now handled via getUpdatePlan()
return false;
}
function needsIntermediatePatches(currentVersion, targetVersion) {
if (!currentVersion) return [];
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);
});
}
async function validateChecksum(filePath, expectedChecksum) {
if (!expectedChecksum) return true;
const actualChecksum = await computeFileChecksum(filePath);
return actualChecksum === expectedChecksum;
}
function getInstalledClientVersion() {
try {
const { loadVersionClient } = require('../core/config');
return loadVersionClient();
} catch {
return null;
console.error('Error fetching client version:', error.message);
console.log('Warning: API unavailable, falling back to default version');
return '4.pwr';
}
}
module.exports = {
getLatestClientVersion,
buildArchiveUrl,
checkArchiveExists,
discoverAvailableVersions,
extractVersionDetails,
canUseDifferentialUpdate,
needsIntermediatePatches,
computeFileChecksum,
validateChecksum,
getInstalledClientVersion,
fetchMirrorManifest,
getPWRUrl,
getPWRUrlFromNewAPI,
getUpdatePlan,
extractVersionNumber,
getPlatformPatches,
findOptimalPatchPath,
getPatchesBaseUrl,
getAllMirrorUrls
getLatestClientVersion
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,405 +0,0 @@
/**
* 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}` });
});
});
}

View File

@@ -1,238 +0,0 @@
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 };

View File

@@ -1,4 +1,4 @@
const { execSync, spawnSync } = require('child_process');
const { execSync } = require('child_process');
const fs = require('fs');
function getOS() {
@@ -18,16 +18,11 @@ function isWaylandSession() {
}
const sessionType = process.env.XDG_SESSION_TYPE;
const waylandDisplay = process.env.WAYLAND_DISPLAY;
// Debug logging
console.log(`[PlatformUtils] Checking Wayland: XDG_SESSION_TYPE=${sessionType}, WAYLAND_DISPLAY=${waylandDisplay}`);
if (sessionType && sessionType.toLowerCase() === 'wayland') {
return true;
}
if (waylandDisplay) {
if (process.env.WAYLAND_DISPLAY) {
return true;
}
@@ -49,48 +44,19 @@ function setupWaylandEnvironment() {
if (process.platform !== 'linux') {
return {};
}
// If the user has manually set SDL_VIDEODRIVER (e.g. to 'x11'), strictly respect it.
if (process.env.SDL_VIDEODRIVER) {
console.log(`User manually set SDL_VIDEODRIVER=${process.env.SDL_VIDEODRIVER}, ignoring internal Wayland configuration.`);
return {};
}
if (!isWaylandSession()) {
console.log('Detected X11 session, using default environment');
return {};
}
console.log('Detected Wayland session, checking for Gamescope/Steam Deck...');
console.log('Detected Wayland session, configuring environment...');
const envVars = {};
// Only set Ozone hint if not already set by user
if (!process.env.ELECTRON_OZONE_PLATFORM_HINT) {
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
}
// 2. DETECT GAMESCOPE / STEAM DECK
// Native Wayland often fails for SDL games in Gaming Mode (gamescope), so we force X11 (XWayland).
// Checks:
// - XDG_CURRENT_DESKTOP == 'gamescope'
// - SteamDeck=1 (often set in SteamOS)
const currentDesktop = process.env.XDG_CURRENT_DESKTOP || '';
const isGamescope = currentDesktop.toLowerCase() === 'gamescope' || process.env.SteamDeck === '1';
const envVars = {
SDL_VIDEODRIVER: 'wayland'
};
if (isGamescope) {
console.log('Gamescope / Steam Deck detected, forcing SDL_VIDEODRIVER=x11 for compatibility');
envVars.SDL_VIDEODRIVER = 'x11';
} else {
// For standard desktop Wayland (GNOME, KDE), we leave SDL_VIDEODRIVER unset.
// This allows SDL3/SDL2 to use its internal preference (Wayland > X11).
// EXCEPT if it was somehow force-set to 'wayland' by the parent process (rare but possible),
// we strictly want to allow fallback, so we might unset it if it was 'wayland'.
// But since we checked process.env.SDL_VIDEODRIVER at the start, we know it's NOT set manually.
// So we effectively do nothing for standard Wayland, letting SDL decide.
console.log('Standard Wayland session detected, letting SDL decide backend (auto-fallback enabled).');
}
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
console.log('Wayland environment variables:', envVars);
return envVars;
@@ -116,460 +82,117 @@ function detectGpu() {
}
function detectGpuLinux() {
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 output = execSync('lspci -nn | grep \'VGA\\|3D\'', { encoding: 'utf8' });
const lines = output.split('\n').filter(line => line.trim());
let gpus = {
integrated: [],
dedicated: []
};
let integratedName = null;
let dedicatedName = null;
let hasNvidia = false;
let hasAmd = false;
for (const line of lines) {
// 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, '');
}
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);
}
} else if (brackets && brackets.length === 1) {
name = brackets[0].replace(/[\[\]]/g, '');
}
// 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
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);
}
} 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);
}
}
// 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
}
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 };
}
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() {
let output = '';
let commandUsed = 'cim'; // Track which command succeeded
const POWERSHELL_TIMEOUT = 5000; // 5 second timeout to prevent hanging
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');
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 (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 };
}
}
}
// 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: []
};
let integratedName = null;
let dedicatedName = null;
let hasNvidia = false;
let hasAmd = false;
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 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);
}
}
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
};
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() {
let output = '';
try {
output = execSync('system_profiler SPDisplaysDataType', { encoding: 'utf8' });
} catch (e) {
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
}
const output = execSync('system_profiler SPDisplaysDataType', { encoding: 'utf8' });
const lines = output.split('\n');
let gpus = {
integrated: [],
dedicated: []
};
let currentGpu = null;
let integratedName = null;
let dedicatedName = null;
let hasNvidia = false;
let hasAmd = false;
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';
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;
}
}
}
// 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);
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 };
} else {
// Intel or unknown
gpus.integrated.push(gpu);
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Integrated GPU', dedicatedName: null };
}
}
@@ -610,108 +233,11 @@ 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,
getSystemType
setupGpuEnvironment
};

View File

@@ -1,426 +0,0 @@
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
};

View File

@@ -1,120 +0,0 @@
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const { getHytaleSavesDir } = require('../core/paths');
const SERVER_LIST_URL = 'https://assets.authbp.xyz/server.json';
function getLocalDateTime() {
return formatLocalDateTime(new Date());
}
function formatLocalDateTime(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
const offsetMinutes = -date.getTimezoneOffset();
const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
const offsetMins = Math.abs(offsetMinutes) % 60;
const offsetSign = offsetMinutes >= 0 ? '+' : '-';
const offset = `${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(offsetMins).padStart(2, '0')}`;
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}0000${offset}`;
}
async function syncServerList() {
try {
const hytaleSavesDir = getHytaleSavesDir();
const serverListPath = path.join(hytaleSavesDir, 'ServerList.json');
console.log('[ServerListSync] Fetching server list from', SERVER_LIST_URL);
let remoteData;
try {
const response = await axios.get(SERVER_LIST_URL, {
timeout: 40000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
remoteData = response.data;
} catch (fetchError) {
console.warn('[ServerListSync] Failed to fetch remote server list:', fetchError.message);
remoteData = { SavedServers: [] };
}
let localData = { SavedServers: [] };
if (fs.existsSync(serverListPath)) {
try {
const localContent = fs.readFileSync(serverListPath, 'utf-8');
localData = JSON.parse(localContent);
console.log('[ServerListSync] Loaded existing local server list with', localData.SavedServers?.length || 0, 'servers');
} catch (parseError) {
console.warn('[ServerListSync] Failed to parse local server list, creating new one:', parseError.message);
localData = { SavedServers: [] };
}
} else {
console.log('[ServerListSync] Local server list does not exist, creating new one');
}
if (!localData.SavedServers) {
localData.SavedServers = [];
}
if (!remoteData.SavedServers) {
remoteData.SavedServers = [];
}
const existingServersByAddress = new Map();
const userServers = [];
for (const server of localData.SavedServers) {
existingServersByAddress.set(server.Address.toLowerCase(), server);
}
const remoteAddresses = new Set(remoteData.SavedServers.map(s => s.Address.toLowerCase()));
for (const server of localData.SavedServers) {
if (!remoteAddresses.has(server.Address.toLowerCase())) {
userServers.push(server);
}
}
const currentDate = getLocalDateTime();
const apiServers = [];
for (const remoteServer of remoteData.SavedServers) {
const serverToAdd = {
Id: uuidv4(),
Name: "@ " + remoteServer.Name,
Address: remoteServer.Address,
DateSaved: currentDate,
img_Banner: remoteServer.img_Banner || null // Copy banner if exists
};
apiServers.push(serverToAdd);
console.log('[ServerListSync] Added/Updated server with new ID:', remoteServer.Name);
}
localData.SavedServers = [...apiServers, ...userServers];
const addedCount = apiServers.length;
if (!fs.existsSync(hytaleSavesDir)) {
fs.mkdirSync(hytaleSavesDir, { recursive: true });
}
fs.writeFileSync(serverListPath, JSON.stringify(localData, null, 2), 'utf-8');
console.log('[ServerListSync] Server list synchronized:', addedCount, 'API servers added, total:', localData.SavedServers.length);
return { success: true, added: addedCount, total: localData.SavedServers.length };
} catch (error) {
console.error('[ServerListSync] Failed to synchronize server list:', error.message);
return { success: false, error: error.message };
}
}
module.exports = {
syncServerList
};

View File

@@ -4,7 +4,7 @@ const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths');
const { loadConfig, saveConfig } = require('../core/config');
/**
* NEW SYSTEM (2.2.0+): UserData Migration to Centralized Location
* NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location
*
* UserData is now stored in a centralized location instead of inside game installation:
* - Windows: %LOCALAPPDATA%\HytaleSaves\
@@ -31,7 +31,7 @@ function markMigrationCompleted() {
}
/**
* Find old UserData location (pre-2.2.0)
* Find old UserData location (pre-2.1.2)
* Searches in: installPath/branch/package/game/latest/Client/UserData
*/
function findOldUserDataPath() {
@@ -77,7 +77,7 @@ function findOldUserDataPath() {
/**
* Migrate UserData from old location to new centralized location
* One-time operation when upgrading to 2.2.0
* One-time operation when upgrading to 2.1.2
*/
async function migrateUserDataToCentralized() {
// Check if already migrated
@@ -149,7 +149,7 @@ async function migrateUserDataToCentralized() {
}
/**
* Get the centralized UserData path (always use this in 2.2.0+)
* Get the centralized UserData path (always use this in 2.1.2+)
* Ensures directory exists
*/
function getUserDataPath() {

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

View File

@@ -1,2 +1,3 @@
provider: generic
url: https://git.sanhost.net/sanasol/hytale-f2p/releases/download/latest
provider: github
owner: amiayweb # Change to your own GitHub username
repo: Hytale-F2P

View File

@@ -1,732 +0,0 @@
# 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

View File

@@ -1,113 +0,0 @@
# 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`

View File

@@ -1,121 +0,0 @@
# 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

View File

@@ -1,83 +0,0 @@
# 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.

View File

@@ -1,48 +0,0 @@
# 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

View File

@@ -1,159 +0,0 @@
# 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.

View File

@@ -1,273 +0,0 @@
# 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.

View File

@@ -1,123 +0,0 @@
# Steam Deck / Ubuntu LTS Crash Investigation
## Status: SOLVED
**Last updated:** 2026-01-27
**Solution:** Replace bundled `libzstd.so` with system version.
---
## Problem Summary
The Hytale F2P launcher's client patcher causes crashes on Steam Deck and Ubuntu LTS with the error:
```
free(): invalid pointer
```
or
```
SIGSEGV (Segmentation fault)
```
The crash occurs after successful authentication, specifically right after "Finished handling RequiredAssets".
**Affected Systems:**
- Steam Deck (glibc 2.41)
- Ubuntu LTS
**Working Systems:**
- macOS
- Windows
- Older Arch Linux (glibc < 2.41)
---
## Root Cause
The **bundled `libzstd.so`** in the game client is incompatible with glibc 2.41's stricter heap validation. When the game decompresses assets using this library, it triggers heap corruption detected by glibc 2.41.
The crash occurs in `libzstd.so` during `free()` after "Finished handling RequiredAssets" (asset decompression).
---
## Solution
Replace the bundled `libzstd.so` with the system's `libzstd.so.1`.
### Automatic (Launcher)
The launcher automatically detects and replaces `libzstd.so` on Linux systems. No manual action needed.
### Manual
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
# Backup bundled version
mv libzstd.so libzstd.so.bundled
# Link to system version
# Steam Deck / Arch Linux:
ln -s /usr/lib/libzstd.so.1 libzstd.so
# Debian / Ubuntu:
ln -s /usr/lib/x86_64-linux-gnu/libzstd.so.1 libzstd.so
# Fedora / RHEL:
ln -s /usr/lib64/libzstd.so.1 libzstd.so
```
### Restore Original
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
rm libzstd.so
mv libzstd.so.bundled libzstd.so
```
---
## Why This Works
1. The bundled `libzstd.so` was likely compiled with different allocator settings or an older toolchain
2. glibc 2.41 has stricter heap validation that catches invalid memory operations
3. The system `libzstd.so.1` is compiled with the system's glibc and uses compatible memory allocation patterns
4. By using the system library, we avoid the incompatibility entirely
---
## Previous Investigation (for reference)
### What Was Tried Before Finding Solution
| Approach | Result |
|----------|--------|
| jemalloc allocator | Worked ~30% of time, not stable |
| GLIBC_TUNABLES | No effect |
| taskset (CPU pinning) | Single core too slow |
| nice/chrt (scheduling) | No effect |
| Various patching approaches | All crashed |
### Key Insight
The crash was in `libzstd.so`, not in our patched code. The patching just changed timing enough to expose the libzstd incompatibility more frequently.
---
## GDB Stack Trace (Historical)
```
#0 0x00007ffff7d3f5a4 in ?? () from /usr/lib/libc.so.6
#1 raise () from /usr/lib/libc.so.6
#2 abort () from /usr/lib/libc.so.6
#3-#4 ?? () from /usr/lib/libc.so.6
#5 free () from /usr/lib/libc.so.6
#6 ?? () from libzstd.so <-- CRASH POINT (bundled library)
#7-#24 HytaleClient code (asset decompression)
```
---
## Branch
`fix/steamdeck-libzstd`

View File

@@ -1,65 +0,0 @@
# Steam Deck / Linux Crash Fix
## SOLUTION: Use system libzstd
The crash is caused by the bundled `libzstd.so` being incompatible with glibc 2.41's stricter heap validation.
### Automatic Fix
The launcher automatically replaces `libzstd.so` with the system version. No manual action needed.
### Manual Fix
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
# Backup and replace
mv libzstd.so libzstd.so.bundled
ln -s /usr/lib/libzstd.so.1 libzstd.so
```
### Restore Original
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
rm libzstd.so
mv libzstd.so.bundled libzstd.so
```
---
## Debug Commands (for troubleshooting)
### Check libzstd Status
```bash
# Check if symlinked
ls -la ~/.hytalef2p/release/package/game/latest/Client/libzstd.so
# Find system libzstd
find /usr/lib -name "libzstd.so*"
```
### Binary Validation
```bash
file ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
ldd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
```
### Restore Client Binary
```bash
cd ~/.hytalef2p/release/package/game/latest/Client
cp HytaleClient.original HytaleClient
rm -f HytaleClient.patched_custom
```
---
## Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `HYTALE_AUTH_DOMAIN` | Custom auth domain | `auth.sanasol.ws` |
| `HYTALE_NO_LIBZSTD_FIX` | Disable libzstd replacement | `1` |

View File

@@ -1,138 +0,0 @@
# 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

View File

@@ -1,482 +0,0 @@
# 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)

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -1,21 +0,0 @@
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"]

View File

@@ -1,252 +0,0 @@
# 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

View File

@@ -1,19 +0,0 @@
-----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-----

View File

@@ -1,28 +0,0 @@
-----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-----

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +0,0 @@
{
"privateKey": "MC4CAQAwBQYDK2VwBCIEID/rEs35asZTitPu/73KBIoI4mb3dzH4QoBElz8IX29x",
"publicKey": "MCowBQYDK2VwAyEAg1HEi1XUdUZAYc+/9ioijFkkGdulbxT3eVpM+i8Mdr4=",
"createdAt": "2026-01-27T23:22:58.667Z"
}

View File

@@ -1,19 +0,0 @@
-----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-----

View File

@@ -1,28 +0,0 @@
-----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-----

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1 +0,0 @@
MANIFEST-000035

View File

@@ -1 +0,0 @@
7a0fd32b-70d3-447e-90d3-74fc57ad6651

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,391 +0,0 @@
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,391 +0,0 @@
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,391 +0,0 @@
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,391 +0,0 @@
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

File diff suppressed because it is too large Load Diff

View File

@@ -1,391 +0,0 @@
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

Some files were not shown because too many files have changed in this diff Show More