Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d17c1bc0a2 | |||
| b968ace8fa | |||
| 2cf5623186 | |||
| 5e7786d66e | |||
| 9a52d6b8fc | |||
| 255befd1b8 | |||
| 691b363f88 | |||
| 8972c3e363 | |||
| 5a8d77c494 | |||
| 2240ca38bd | |||
| d2696a510f | |||
| 4aecb402d6 | |||
| 088db29377 | |||
| d9512a9040 | |||
| 5a18242f72 | |||
| ae359497be | |||
| db89352ed8 | |||
| c1d1f6bce3 | |||
| 6582ddf93a | |||
| fba25a9081 | |||
| ca5c0491b7 | |||
| 2b2ff703c2 | |||
| 13e42e4374 | |||
| 3c7c559748 | |||
| 9e466ea9c4 | |||
| 645a93d3c4 | |||
| d311bf4a7e |
@@ -4,6 +4,17 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release_type:
|
||||||
|
description: 'Override release type (auto-detected by default)'
|
||||||
|
required: false
|
||||||
|
default: 'auto'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- auto
|
||||||
|
- patch
|
||||||
|
- full
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -14,18 +25,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Check if version already released
|
- name: Check if version already released
|
||||||
id: version-check
|
id: version-check
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
||||||
TAG="v$VERSION"
|
TAG="v$VERSION"
|
||||||
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
|
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
echo "TAG=$TAG" >> "$GITHUB_OUTPUT"
|
echo "TAG=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||||
|
"https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client-Test/releases/tags/$TAG" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN")
|
||||||
|
|
||||||
|
if [ "$STATUS" = "200" ]; then
|
||||||
echo "Release $TAG already exists, skipping build"
|
echo "Release $TAG already exists, skipping build"
|
||||||
echo "SKIP=true" >> "$GITHUB_OUTPUT"
|
echo "SKIP=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
@@ -33,6 +50,55 @@ jobs:
|
|||||||
echo "SKIP=false" >> "$GITHUB_OUTPUT"
|
echo "SKIP=false" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Detect release type
|
||||||
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
|
id: release-type
|
||||||
|
run: |
|
||||||
|
OVERRIDE="${{ github.event.inputs.release_type || 'auto' }}"
|
||||||
|
|
||||||
|
if [ "$OVERRIDE" != "auto" ]; then
|
||||||
|
echo "Release type overridden to: $OVERRIDE"
|
||||||
|
echo "TYPE=$OVERRIDE" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-detect: find what changed since the last release tag
|
||||||
|
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$PREV_TAG" ]; then
|
||||||
|
echo "No previous tag found — defaulting to full build"
|
||||||
|
echo "TYPE=full" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Comparing changes since $PREV_TAG"
|
||||||
|
CHANGED=$(git diff --name-only "$PREV_TAG"..HEAD)
|
||||||
|
echo "$CHANGED"
|
||||||
|
|
||||||
|
# Files that require a full (installer-only) release
|
||||||
|
FULL_TRIGGERS="electron-builder.yml|scripts/download-electron.js"
|
||||||
|
|
||||||
|
if echo "$CHANGED" | grep -qE "^($FULL_TRIGGERS)$"; then
|
||||||
|
echo "Full build trigger detected in changed files"
|
||||||
|
echo "TYPE=full" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if package.json deps changed (not just version field)
|
||||||
|
if echo "$CHANGED" | grep -q "^package.json$"; then
|
||||||
|
DEP_DIFF=$(git diff "$PREV_TAG"..HEAD -- package.json \
|
||||||
|
| grep -E '^\+' | grep -vE '^\+\+\+' \
|
||||||
|
| grep -E '"(dependencies|devDependencies|electron|electron-nightly)"' || true)
|
||||||
|
if [ -n "$DEP_DIFF" ]; then
|
||||||
|
echo "Dependency changes detected in package.json"
|
||||||
|
echo "TYPE=full" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Only source changes detected — patch release"
|
||||||
|
echo "TYPE=patch" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: steps.version-check.outputs.SKIP == 'false'
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -42,9 +108,9 @@ jobs:
|
|||||||
- name: Install system dependencies
|
- name: Install system dependencies
|
||||||
if: steps.version-check.outputs.SKIP == 'false'
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
run: |
|
run: |
|
||||||
sudo dpkg --add-architecture i386
|
dpkg --add-architecture i386
|
||||||
sudo apt-get update -qq
|
apt-get update -qq
|
||||||
sudo apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
wine wine32 wine64 \
|
wine wine32 wine64 \
|
||||||
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2t64 \
|
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2t64 \
|
||||||
libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \
|
libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \
|
||||||
@@ -70,46 +136,133 @@ jobs:
|
|||||||
if: steps.version-check.outputs.SKIP == 'false'
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
run: npx electron-builder --linux -c.electronDist=node_modules/electron/dist-linux --publish never
|
run: npx electron-builder --linux -c.electronDist=node_modules/electron/dist-linux --publish never
|
||||||
|
|
||||||
|
- name: Extract asar for patch updates
|
||||||
|
if: steps.version-check.outputs.SKIP == 'false' && steps.release-type.outputs.TYPE == 'patch'
|
||||||
|
run: |
|
||||||
|
# The asar is a byproduct of the Linux build — platform-independent (just JS)
|
||||||
|
ASAR_SRC="out/linux-unpacked/resources/app.asar"
|
||||||
|
ASAR_DIR="out/asar"
|
||||||
|
mkdir -p "$ASAR_DIR"
|
||||||
|
cp "$ASAR_SRC" "$ASAR_DIR/app.asar"
|
||||||
|
sha256sum "$ASAR_DIR/app.asar" | awk '{print $1 " app.asar"}' > "$ASAR_DIR/checksums.sha256"
|
||||||
|
echo "Extracted asar: $(du -h "$ASAR_DIR/app.asar" | cut -f1)"
|
||||||
|
cat "$ASAR_DIR/checksums.sha256"
|
||||||
|
|
||||||
- name: Report build sizes
|
- name: Report build sizes
|
||||||
if: steps.version-check.outputs.SKIP == 'false'
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
run: |
|
run: |
|
||||||
echo "=== Build output sizes ==="
|
echo "=== Release type: ${{ steps.release-type.outputs.TYPE }} ==="
|
||||||
ls -lh out/*.exe out/*.AppImage out/*.deb 2>/dev/null || true
|
ls -lh out/*.exe out/*.AppImage out/*.deb 2>/dev/null || true
|
||||||
|
ls -lh out/asar/* 2>/dev/null || true
|
||||||
|
|
||||||
- name: Generate release notes
|
- name: Generate release notes
|
||||||
if: steps.version-check.outputs.SKIP == 'false'
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
git fetch --unshallow 2>/dev/null || true
|
|
||||||
chmod +x scripts/generate-release-notes.sh
|
chmod +x scripts/generate-release-notes.sh
|
||||||
scripts/generate-release-notes.sh "${{ steps.version-check.outputs.TAG }}" > /tmp/release-notes.md
|
|
||||||
|
PREV_SHA=$(curl -s "https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client-Test/releases?limit=1" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
| jq -r '.[0].target_commitish // empty' 2>/dev/null || true)
|
||||||
|
|
||||||
|
echo "Previous release SHA: ${PREV_SHA:-none}"
|
||||||
|
scripts/generate-release-notes.sh "${{ steps.version-check.outputs.TAG }}" "$PREV_SHA" > /tmp/release-notes.md
|
||||||
echo "--- Generated release notes ---"
|
echo "--- Generated release notes ---"
|
||||||
cat /tmp/release-notes.md
|
cat /tmp/release-notes.md
|
||||||
|
|
||||||
- name: Create GitHub release and upload assets
|
- name: Create release and upload assets
|
||||||
if: steps.version-check.outputs.SKIP == 'false'
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }}
|
||||||
|
TAG: ${{ steps.version-check.outputs.TAG }}
|
||||||
|
RELEASE_TYPE: ${{ steps.release-type.outputs.TYPE }}
|
||||||
run: |
|
run: |
|
||||||
# Collect built artifacts
|
GITEA_BASE="https://gitea.crjlab.net"
|
||||||
ASSETS=()
|
REPO="bigjakk/Krunker-Civilian-Client-Test"
|
||||||
for file in out/*.exe out/*.AppImage out/*.deb; do
|
|
||||||
[ -f "$file" ] || continue
|
|
||||||
ASSETS+=("$file")
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ ${#ASSETS[@]} -eq 0 ]; then
|
# Create tag
|
||||||
echo "ERROR: No build artifacts found in out/"
|
curl -s -X POST "$GITEA_BASE/api/v1/repos/$REPO/tags" \
|
||||||
ls -la out/ 2>/dev/null || echo "out/ directory does not exist"
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"$TAG\", \"message\": \"$TAG\", \"target\": \"$GITHUB_SHA\"}"
|
||||||
|
|
||||||
|
# Read release notes
|
||||||
|
NOTES=$(cat /tmp/release-notes.md)
|
||||||
|
BODY=$(echo "$NOTES" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')
|
||||||
|
|
||||||
|
# Create release
|
||||||
|
RESPONSE=$(curl -s -X POST "$GITEA_BASE/api/v1/repos/$REPO/releases" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"$TAG\",
|
||||||
|
\"name\": \"$TAG\",
|
||||||
|
\"body\": $BODY,
|
||||||
|
\"draft\": false,
|
||||||
|
\"prerelease\": false
|
||||||
|
}")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id')
|
||||||
|
echo "Created release ID: $RELEASE_ID"
|
||||||
|
|
||||||
|
if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Failed to create release:"
|
||||||
|
echo "$RESPONSE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Uploading ${#ASSETS[@]} assets:"
|
# Upload asar + checksums (patch releases only — signals client to use fast path)
|
||||||
printf ' %s\n' "${ASSETS[@]}"
|
if [ "$RELEASE_TYPE" = "patch" ]; then
|
||||||
|
for file in out/asar/app.asar out/asar/checksums.sha256; do
|
||||||
|
[ -f "$file" ] || continue
|
||||||
|
FILENAME=$(basename "$file")
|
||||||
|
echo "Uploading: $FILENAME ($(du -h "$file" | cut -f1))"
|
||||||
|
curl -s -X POST \
|
||||||
|
"$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$FILENAME" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@$file" \
|
||||||
|
| jq -r '" -> \(.name) (\(.size) bytes)"'
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Full release — skipping asar upload (forces installer update)"
|
||||||
|
fi
|
||||||
|
|
||||||
gh release create "${{ steps.version-check.outputs.TAG }}" \
|
# Upload installers (always — for new users and major updates)
|
||||||
--repo "$GITHUB_REPOSITORY" \
|
for file in out/*.exe out/*.AppImage out/*.deb; do
|
||||||
--title "${{ steps.version-check.outputs.TAG }}" \
|
[ -f "$file" ] || continue
|
||||||
--notes-file /tmp/release-notes.md \
|
FILENAME=$(basename "$file")
|
||||||
--draft=false \
|
SAFE_NAME=$(echo "$FILENAME" | tr ' ' '_')
|
||||||
--latest \
|
echo "Uploading: $SAFE_NAME ($(du -h "$file" | cut -f1))"
|
||||||
"${ASSETS[@]}"
|
curl -s -X POST \
|
||||||
|
"$GITEA_BASE/api/v1/repos/$REPO/releases/$RELEASE_ID/assets?name=$SAFE_NAME" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@$file" \
|
||||||
|
| jq -r '" -> \(.name) (\(.size) bytes)"'
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "All assets uploaded"
|
||||||
|
|
||||||
|
- name: Prune old releases
|
||||||
|
if: steps.version-check.outputs.SKIP == 'false'
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.DEST_GITEA_TOKEN }}
|
||||||
|
KEEP: 5
|
||||||
|
run: |
|
||||||
|
GITEA_BASE="https://gitea.crjlab.net"
|
||||||
|
REPO="bigjakk/Krunker-Civilian-Client-Test"
|
||||||
|
echo "=== Pruning $REPO (keeping last $KEEP) ==="
|
||||||
|
RELEASES=$(curl -s "$GITEA_BASE/api/v1/repos/$REPO/releases?limit=50" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN")
|
||||||
|
|
||||||
|
DELETE_IDS=$(echo "$RELEASES" | jq -r "sort_by(.created_at) | reverse | .[$KEEP:][] | .id")
|
||||||
|
|
||||||
|
for ID in $DELETE_IDS; do
|
||||||
|
TAG_NAME=$(echo "$RELEASES" | jq -r ".[] | select(.id == $ID) | .tag_name")
|
||||||
|
echo "Deleting release $TAG_NAME (id=$ID)"
|
||||||
|
curl -s -X DELETE "$GITEA_BASE/api/v1/repos/$REPO/releases/$ID" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN"
|
||||||
|
curl -s -X DELETE "$GITEA_BASE/api/v1/repos/$REPO/tags/$TAG_NAME" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN"
|
||||||
|
done
|
||||||
|
echo "Prune complete"
|
||||||
|
|||||||
Generated
+139
-43
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "krunker-civilian-client",
|
"name": "krunker-civilian-client",
|
||||||
"version": "0.7.1",
|
"version": "0.7.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "krunker-civilian-client",
|
"name": "krunker-civilian-client",
|
||||||
"version": "0.7.1",
|
"version": "0.7.9",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -44,37 +44,6 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/asar": {
|
|
||||||
"version": "3.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
|
||||||
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^5.0.0",
|
|
||||||
"glob": "^7.1.6",
|
|
||||||
"minimatch": "^3.0.4"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"asar": "bin/asar.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/asar/node_modules/minimatch": {
|
|
||||||
"version": "3.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
|
||||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/fuses": {
|
"node_modules/@electron/fuses": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz",
|
||||||
@@ -338,6 +307,48 @@
|
|||||||
"node": ">=16.4"
|
"node": ">=16.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/@electron/asar": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^5.0.0",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"asar": "bin/asar.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/@electron/asar/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/@electron/asar/node_modules/minimatch": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/universal/node_modules/brace-expansion": {
|
"node_modules/@electron/universal/node_modules/brace-expansion": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
@@ -348,6 +359,16 @@
|
|||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/commander": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/universal/node_modules/fs-extra": {
|
"node_modules/@electron/universal/node_modules/fs-extra": {
|
||||||
"version": "11.3.3",
|
"version": "11.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
||||||
@@ -2359,6 +2380,37 @@
|
|||||||
"electron-builder-squirrel-windows": "26.8.1"
|
"electron-builder-squirrel-windows": "26.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/app-builder-lib/node_modules/@electron/asar": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^5.0.0",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"asar": "bin/asar.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/app-builder-lib/node_modules/@electron/asar/node_modules/minimatch": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/app-builder-lib/node_modules/@electron/get": {
|
"node_modules/app-builder-lib/node_modules/@electron/get": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz",
|
||||||
@@ -2422,6 +2474,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/app-builder-lib/node_modules/commander": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/app-builder-lib/node_modules/fs-extra": {
|
"node_modules/app-builder-lib/node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
@@ -3031,16 +3093,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/compare-version": {
|
"node_modules/compare-version": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
|
||||||
@@ -3715,6 +3767,36 @@
|
|||||||
"@electron/windows-sign": "^1.1.2"
|
"@electron/windows-sign": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/@electron/asar": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^5.0.0",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"asar": "bin/asar.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/commander": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-winstaller/node_modules/fs-extra": {
|
"node_modules/electron-winstaller/node_modules/fs-extra": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||||
@@ -3731,6 +3813,20 @@
|
|||||||
"node": ">=6 <7 || >=8"
|
"node": ">=6 <7 || >=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/minimatch": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron/node_modules/@types/node": {
|
"node_modules/electron/node_modules/@types/node": {
|
||||||
"version": "24.11.0",
|
"version": "24.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "krunker-civilian-client",
|
"name": "krunker-civilian-client",
|
||||||
"version": "0.7.2",
|
"version": "0.8.0",
|
||||||
"description": "Cross-platform Krunker game client",
|
"description": "Cross-platform Krunker game client",
|
||||||
"main": "dist/main/index.js",
|
"main": "dist/main/index.js",
|
||||||
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
||||||
|
|||||||
+9
-7
@@ -33,6 +33,7 @@ export interface AppConfig {
|
|||||||
game: {
|
game: {
|
||||||
lastServer: string;
|
lastServer: string;
|
||||||
socialTabBehaviour: 'New Window' | 'Same Window';
|
socialTabBehaviour: 'New Window' | 'Same Window';
|
||||||
|
rememberTabs: boolean;
|
||||||
joinAsSpectator: boolean;
|
joinAsSpectator: boolean;
|
||||||
rawInput: boolean;
|
rawInput: boolean;
|
||||||
betterChat: boolean;
|
betterChat: boolean;
|
||||||
@@ -53,7 +54,7 @@ export interface AppConfig {
|
|||||||
maxPlayers: number;
|
maxPlayers: number;
|
||||||
minRemainingTime: number;
|
minRemainingTime: number;
|
||||||
openServerBrowser: boolean;
|
openServerBrowser: boolean;
|
||||||
autoJoin: boolean;
|
sortByPlayers: boolean;
|
||||||
};
|
};
|
||||||
keybinds: {
|
keybinds: {
|
||||||
reload: Keybind;
|
reload: Keybind;
|
||||||
@@ -62,7 +63,6 @@ export interface AppConfig {
|
|||||||
joinFromClipboard: Keybind;
|
joinFromClipboard: Keybind;
|
||||||
devTools: Keybind;
|
devTools: Keybind;
|
||||||
matchmaker: Keybind;
|
matchmaker: Keybind;
|
||||||
matchmakerAccept: Keybind;
|
|
||||||
matchmakerCancel: Keybind;
|
matchmakerCancel: Keybind;
|
||||||
fullscreenToggle: Keybind;
|
fullscreenToggle: Keybind;
|
||||||
};
|
};
|
||||||
@@ -108,6 +108,7 @@ export interface AppConfig {
|
|||||||
y: number | undefined;
|
y: number | undefined;
|
||||||
maximized: boolean;
|
maximized: boolean;
|
||||||
};
|
};
|
||||||
|
savedTabs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_KEYBINDS: AppConfig['keybinds'] = {
|
export const DEFAULT_KEYBINDS: AppConfig['keybinds'] = {
|
||||||
@@ -117,7 +118,6 @@ export const DEFAULT_KEYBINDS: AppConfig['keybinds'] = {
|
|||||||
joinFromClipboard: { key: 'j', ctrl: true, shift: false, alt: false },
|
joinFromClipboard: { key: 'j', ctrl: true, shift: false, alt: false },
|
||||||
devTools: { key: 'F12', ctrl: false, shift: false, alt: false },
|
devTools: { key: 'F12', ctrl: false, shift: false, alt: false },
|
||||||
matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false },
|
matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false },
|
||||||
matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false },
|
|
||||||
matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false },
|
matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false },
|
||||||
fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
|
fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
|
||||||
};
|
};
|
||||||
@@ -145,6 +145,7 @@ export const config = new Store<AppConfig>({
|
|||||||
game: {
|
game: {
|
||||||
lastServer: '',
|
lastServer: '',
|
||||||
socialTabBehaviour: 'New Window',
|
socialTabBehaviour: 'New Window',
|
||||||
|
rememberTabs: false,
|
||||||
joinAsSpectator: false,
|
joinAsSpectator: false,
|
||||||
rawInput: true,
|
rawInput: true,
|
||||||
betterChat: true,
|
betterChat: true,
|
||||||
@@ -153,7 +154,7 @@ export const config = new Store<AppConfig>({
|
|||||||
hpEnemyCounter: true,
|
hpEnemyCounter: true,
|
||||||
},
|
},
|
||||||
swapper: {
|
swapper: {
|
||||||
enabled: true,
|
enabled: false,
|
||||||
path: '',
|
path: '',
|
||||||
},
|
},
|
||||||
matchmaker: {
|
matchmaker: {
|
||||||
@@ -165,11 +166,11 @@ export const config = new Store<AppConfig>({
|
|||||||
maxPlayers: 6,
|
maxPlayers: 6,
|
||||||
minRemainingTime: 120,
|
minRemainingTime: 120,
|
||||||
openServerBrowser: true,
|
openServerBrowser: true,
|
||||||
autoJoin: false,
|
sortByPlayers: false,
|
||||||
},
|
},
|
||||||
keybinds: DEFAULT_KEYBINDS,
|
keybinds: DEFAULT_KEYBINDS,
|
||||||
userscripts: {
|
userscripts: {
|
||||||
enabled: true,
|
enabled: false,
|
||||||
path: '',
|
path: '',
|
||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
@@ -185,7 +186,7 @@ export const config = new Store<AppConfig>({
|
|||||||
lastSeenVersion: '',
|
lastSeenVersion: '',
|
||||||
},
|
},
|
||||||
discord: {
|
discord: {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
},
|
},
|
||||||
translator: {
|
translator: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -210,5 +211,6 @@ export const config = new Store<AppConfig>({
|
|||||||
y: undefined,
|
y: undefined,
|
||||||
maximized: true,
|
maximized: true,
|
||||||
},
|
},
|
||||||
|
savedTabs: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+61
-16
@@ -1,6 +1,7 @@
|
|||||||
import { app, BrowserWindow, Menu, clipboard, ipcMain, safeStorage, session, shell } from 'electron';
|
import { app, BrowserWindow, Menu, clipboard, ipcMain, safeStorage, session, shell } from 'electron';
|
||||||
import { join } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { existsSync, mkdirSync, promises as fsp } from 'fs';
|
import { existsSync, mkdirSync, promises as fsp } from 'fs';
|
||||||
|
import { existsSync as origExistsSync, unlinkSync as origUnlinkSync } from 'original-fs';
|
||||||
import { get as httpsGet } from 'https';
|
import { get as httpsGet } from 'https';
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
@@ -10,7 +11,7 @@ import { initSwapperProtocol, registerSwapperFileProtocol, ResourceSwapper } fro
|
|||||||
import { UserscriptManager } from './userscripts';
|
import { UserscriptManager } from './userscripts';
|
||||||
import { ALL_CLIENT_CSS } from './client-ui';
|
import { ALL_CLIENT_CSS } from './client-ui';
|
||||||
import { electronLog, getLogPath, closeLogStreams } from './logger';
|
import { electronLog, getLogPath, closeLogStreams } from './logger';
|
||||||
import { checkForUpdate, downloadUpdate, installUpdate } from './updater';
|
import { checkForUpdate, downloadUpdate, installUpdate, applyMinorUpdate } from './updater';
|
||||||
import { showUpdateWindow } from './update-window';
|
import { showUpdateWindow } from './update-window';
|
||||||
import { DiscordRPC } from './discord-rpc';
|
import { DiscordRPC } from './discord-rpc';
|
||||||
import { listThemes, getThemeCSS, listLoadingThemes, getLoadingScreenCSS } from './css-themes';
|
import { listThemes, getThemeCSS, listLoadingThemes, getLoadingScreenCSS } from './css-themes';
|
||||||
@@ -164,45 +165,87 @@ function saveWindowState(win: BrowserWindow): void {
|
|||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
electronLog.log('[KCC] App ready');
|
electronLog.log('[KCC] App ready');
|
||||||
|
electronLog.log('[KCC] Minor update test — asar swap successful');
|
||||||
|
|
||||||
// ── Auto-update check (mandatory, Windows NSIS install only) ──
|
// ── Auto-update check (2-stage: minor asar swap or major installer) ──
|
||||||
const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR;
|
const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR;
|
||||||
const isAppImage = !!process.env.APPIMAGE;
|
const isAppImage = !!process.env.APPIMAGE;
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
if (isDev || process.platform !== 'win32' || isPortable || isAppImage) {
|
const canMajorUpdate = process.platform === 'win32' && !isPortable;
|
||||||
electronLog.log('[KCC] Skipping auto-update (portable or non-Windows)');
|
const canMinorUpdate = !isPortable && !isAppImage;
|
||||||
|
|
||||||
|
if (isDev || (!canMajorUpdate && !canMinorUpdate)) {
|
||||||
|
electronLog.log('[KCC] Skipping auto-update');
|
||||||
} else {
|
} else {
|
||||||
|
// Clean up stale pending asar from a previous failed swap
|
||||||
|
const resourcesDir = join(dirname(app.getPath('exe')), 'resources');
|
||||||
|
const stalePending = join(resourcesDir, 'app-pending.asar');
|
||||||
|
if (origExistsSync(stalePending)) {
|
||||||
|
try { origUnlinkSync(stalePending); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
electronLog.log('[KCC] Checking for updates...');
|
electronLog.log('[KCC] Checking for updates...');
|
||||||
const update = await checkForUpdate(appVersion);
|
const update = await checkForUpdate(appVersion);
|
||||||
if (update) {
|
if (update) {
|
||||||
electronLog.log(`[KCC] Update available: v${update.version}`);
|
electronLog.log(`[KCC] Update available: v${update.version} (${update.updateType})`);
|
||||||
const { window: updateWin, sendProgress } = showUpdateWindow();
|
const { window: updateWin, sendProgress } = showUpdateWindow();
|
||||||
sendProgress(`Update available (v${update.version})`, 0);
|
|
||||||
|
|
||||||
const tempDir = join(app.getPath('temp'), 'kcc-update');
|
|
||||||
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
|
|
||||||
const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`);
|
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
updateWin.on('closed', () => { cancelled = true; });
|
updateWin.on('closed', () => { cancelled = true; });
|
||||||
|
|
||||||
|
if (update.updateType === 'minor' && canMinorUpdate) {
|
||||||
|
// Minor update: download app.asar, swap via external script, restart
|
||||||
|
sendProgress(`Patch available (v${update.version})`, 0);
|
||||||
|
const pendingPath = join(resourcesDir, 'app-pending.asar');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadUpdate(update.downloadUrl, pendingPath, (pct) => {
|
||||||
|
if (!cancelled && !updateWin.isDestroyed()) {
|
||||||
|
sendProgress(`Downloading patch... ${pct}%`, pct);
|
||||||
|
}
|
||||||
|
}, update.sha256 || undefined);
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
sendProgress('Applying patch...', 100);
|
||||||
|
applyMinorUpdate(pendingPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
electronLog.error('[KCC] Patch download failed:', err);
|
||||||
|
// Clean up failed download
|
||||||
|
if (origExistsSync(pendingPath)) {
|
||||||
|
try { origUnlinkSync(pendingPath); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (!updateWin.isDestroyed()) updateWin.close();
|
||||||
|
}
|
||||||
|
} else if (update.updateType === 'major' && canMajorUpdate) {
|
||||||
|
// Major update: download Setup.exe, run installer
|
||||||
|
sendProgress(`Update available (v${update.version})`, 0);
|
||||||
|
const tempDir = join(app.getPath('temp'), 'kcc-update');
|
||||||
|
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
|
||||||
|
const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await downloadUpdate(update.downloadUrl, installerPath, (pct) => {
|
await downloadUpdate(update.downloadUrl, installerPath, (pct) => {
|
||||||
if (!cancelled && !updateWin.isDestroyed()) {
|
if (!cancelled && !updateWin.isDestroyed()) {
|
||||||
sendProgress(`Downloading update... ${pct}%`, pct);
|
sendProgress(`Downloading update... ${pct}%`, pct);
|
||||||
}
|
}
|
||||||
}, update.sha256);
|
}, update.sha256 || undefined);
|
||||||
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
sendProgress('Installing update...', 100);
|
sendProgress('Installing update...', 100);
|
||||||
installUpdate(installerPath);
|
installUpdate(installerPath);
|
||||||
return; // app.quit() called by installUpdate
|
return;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
electronLog.error('[KCC] Update download failed:', err);
|
electronLog.error('[KCC] Update download failed:', err);
|
||||||
if (!updateWin.isDestroyed()) updateWin.close();
|
if (!updateWin.isDestroyed()) updateWin.close();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
electronLog.log('[KCC] Update available but cannot auto-install on this platform');
|
||||||
|
if (!updateWin.isDestroyed()) updateWin.close();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
electronLog.log('[KCC] No updates available');
|
electronLog.log('[KCC] No updates available');
|
||||||
}
|
}
|
||||||
@@ -307,7 +350,7 @@ async function launchApp(): Promise<void> {
|
|||||||
session: ses,
|
session: ses,
|
||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
sandbox: false,
|
sandbox: true,
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
backgroundThrottling: false,
|
backgroundThrottling: false,
|
||||||
},
|
},
|
||||||
@@ -389,7 +432,6 @@ async function launchApp(): Promise<void> {
|
|||||||
if (mm.enabled) {
|
if (mm.enabled) {
|
||||||
win.webContents.send('matchmaker-find', {
|
win.webContents.send('matchmaker-find', {
|
||||||
...mm,
|
...mm,
|
||||||
acceptKey: binds.matchmakerAccept,
|
|
||||||
cancelKey: binds.matchmakerCancel,
|
cancelKey: binds.matchmakerCancel,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -411,7 +453,6 @@ async function launchApp(): Promise<void> {
|
|||||||
if (mm.enabled) {
|
if (mm.enabled) {
|
||||||
win.webContents.send('matchmaker-find', {
|
win.webContents.send('matchmaker-find', {
|
||||||
...mm,
|
...mm,
|
||||||
acceptKey: binds.matchmakerAccept,
|
|
||||||
cancelKey: binds.matchmakerCancel,
|
cancelKey: binds.matchmakerCancel,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -459,10 +500,14 @@ async function launchApp(): Promise<void> {
|
|||||||
// ── Tab Manager ──
|
// ── Tab Manager ──
|
||||||
const preloadPath = join(__dirname, '..', 'preload', 'index.js');
|
const preloadPath = join(__dirname, '..', 'preload', 'index.js');
|
||||||
let tabMode: 'same' | 'new' = getGameConf().socialTabBehaviour === 'Same Window' ? 'same' : 'new';
|
let tabMode: 'same' | 'new' = getGameConf().socialTabBehaviour === 'Same Window' ? 'same' : 'new';
|
||||||
|
let sessionTabs: string[] = [];
|
||||||
let tabManager = new TabManager(
|
let tabManager = new TabManager(
|
||||||
win, ses, preloadPath, tabMode, isGameURL,
|
win, ses, preloadPath, tabMode, isGameURL,
|
||||||
() => config.get('tabWindow'),
|
() => config.get('tabWindow'),
|
||||||
(state) => config.set('tabWindow', state),
|
(state) => config.set('tabWindow', state),
|
||||||
|
() => sessionTabs,
|
||||||
|
(urls) => { sessionTabs = urls; },
|
||||||
|
() => config.get('game.rememberTabs') ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Intercept in-page navigation (e.g. window.location = '/social.html')
|
// Intercept in-page navigation (e.g. window.location = '/social.html')
|
||||||
|
|||||||
+61
-3
@@ -42,7 +42,11 @@ export class TabManager {
|
|||||||
private recentlyClosed: { url: string; title: string }[] = [];
|
private recentlyClosed: { url: string; title: string }[] = [];
|
||||||
private getTabWindowState: () => TabWindowState;
|
private getTabWindowState: () => TabWindowState;
|
||||||
private saveTabWindowState: (state: TabWindowState) => void;
|
private saveTabWindowState: (state: TabWindowState) => void;
|
||||||
|
private getSavedTabs: () => string[];
|
||||||
|
private saveTabs: (urls: string[]) => void;
|
||||||
|
private isRememberEnabled: () => boolean;
|
||||||
private tabSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
private tabSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
private restoredTabs = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
win: BrowserWindow,
|
win: BrowserWindow,
|
||||||
@@ -52,6 +56,9 @@ export class TabManager {
|
|||||||
isGameURL: (url: string) => boolean,
|
isGameURL: (url: string) => boolean,
|
||||||
getTabWindowState: () => TabWindowState,
|
getTabWindowState: () => TabWindowState,
|
||||||
saveTabWindowState: (state: TabWindowState) => void,
|
saveTabWindowState: (state: TabWindowState) => void,
|
||||||
|
getSavedTabs: () => string[],
|
||||||
|
saveTabs: (urls: string[]) => void,
|
||||||
|
isRememberEnabled: () => boolean,
|
||||||
) {
|
) {
|
||||||
this.mainWin = win;
|
this.mainWin = win;
|
||||||
this.ses = ses;
|
this.ses = ses;
|
||||||
@@ -60,6 +67,9 @@ export class TabManager {
|
|||||||
this.isGameURL = isGameURL;
|
this.isGameURL = isGameURL;
|
||||||
this.getTabWindowState = getTabWindowState;
|
this.getTabWindowState = getTabWindowState;
|
||||||
this.saveTabWindowState = saveTabWindowState;
|
this.saveTabWindowState = saveTabWindowState;
|
||||||
|
this.getSavedTabs = getSavedTabs;
|
||||||
|
this.saveTabs = saveTabs;
|
||||||
|
this.isRememberEnabled = isRememberEnabled;
|
||||||
|
|
||||||
// ── Tab bar view (shared between both modes) ──
|
// ── Tab bar view (shared between both modes) ──
|
||||||
this.tabBarView = new WebContentsView({
|
this.tabBarView = new WebContentsView({
|
||||||
@@ -185,8 +195,30 @@ export class TabManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Open a new tab ──
|
// ── Restore saved tabs on first open, then open the requested tab ──
|
||||||
openTab(url: string): number {
|
openTab(url: string): number {
|
||||||
|
if (!this.restoredTabs) {
|
||||||
|
this.restoredTabs = true;
|
||||||
|
const saved = this.isRememberEnabled() ? this.getSavedTabs() : [];
|
||||||
|
this.saveTabs([]);
|
||||||
|
if (saved.length > 0) {
|
||||||
|
for (const savedUrl of saved) {
|
||||||
|
this.openSingleTab(savedUrl);
|
||||||
|
}
|
||||||
|
// If the requested URL is already among the restored tabs, just activate it
|
||||||
|
const existing = this.tabs.find(t => t.url === url);
|
||||||
|
if (existing) {
|
||||||
|
this.switchToTab(existing.id);
|
||||||
|
this.showTabs();
|
||||||
|
return existing.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.openSingleTab(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Open a single new tab ──
|
||||||
|
private openSingleTab(url: string): number {
|
||||||
if (this.tabs.length >= MAX_TABS) {
|
if (this.tabs.length >= MAX_TABS) {
|
||||||
const existing = this.tabs.find(t => t.url === url);
|
const existing = this.tabs.find(t => t.url === url);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -221,7 +253,7 @@ export class TabManager {
|
|||||||
session: this.ses,
|
session: this.ses,
|
||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
sandbox: false,
|
sandbox: true,
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -307,19 +339,39 @@ export class TabManager {
|
|||||||
const tab = this.tabs.find(t => t.id === id);
|
const tab = this.tabs.find(t => t.id === id);
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
if (this.activeTabId !== null) {
|
if (this.activeTabId !== null && this.activeTabId !== id) {
|
||||||
const prev = this.tabs.find(t => t.id === this.activeTabId);
|
const prev = this.tabs.find(t => t.id === this.activeTabId);
|
||||||
if (prev) {
|
if (prev) {
|
||||||
this.containerView.removeChildView(prev.view);
|
this.containerView.removeChildView(prev.view);
|
||||||
|
this.freezeTab(prev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeTabId = id;
|
this.activeTabId = id;
|
||||||
|
this.unfreezeTab(tab);
|
||||||
this.containerView.addChildView(tab.view);
|
this.containerView.addChildView(tab.view);
|
||||||
this.updateLayout();
|
this.updateLayout();
|
||||||
this.broadcastTabState();
|
this.broadcastTabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Freeze/unfreeze background tabs via CDP Page Lifecycle ──
|
||||||
|
private freezeTab(tab: TabInfo): void {
|
||||||
|
const wc = tab.view.webContents;
|
||||||
|
if (wc.isDestroyed()) return;
|
||||||
|
this.stopTitleWatcher(tab.id);
|
||||||
|
try { wc.debugger.attach('1.3'); } catch { /* already attached (DevTools open) */ }
|
||||||
|
wc.debugger.sendCommand('Page.setWebLifecycleState', { state: 'frozen' }).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private unfreezeTab(tab: TabInfo): void {
|
||||||
|
const wc = tab.view.webContents;
|
||||||
|
if (wc.isDestroyed()) return;
|
||||||
|
wc.debugger.sendCommand('Page.setWebLifecycleState', { state: 'active' }).catch(() => {}).finally(() => {
|
||||||
|
try { wc.debugger.detach(); } catch { /* not attached */ }
|
||||||
|
if (!wc.isDestroyed()) this.startTitleWatcher(tab.id, wc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Close a tab ──
|
// ── Close a tab ──
|
||||||
closeTab(id: number): void {
|
closeTab(id: number): void {
|
||||||
const idx = this.tabs.findIndex(t => t.id === id);
|
const idx = this.tabs.findIndex(t => t.id === id);
|
||||||
@@ -489,6 +541,12 @@ export class TabManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private destroyAllTabs(): void {
|
private destroyAllTabs(): void {
|
||||||
|
// Persist tab URLs so they can be restored later
|
||||||
|
if (this.tabs.length > 0 && this.isRememberEnabled()) {
|
||||||
|
this.saveTabs(this.tabs.map(t => t.url));
|
||||||
|
this.restoredTabs = false;
|
||||||
|
}
|
||||||
|
|
||||||
for (const tab of this.tabs) {
|
for (const tab of this.tabs) {
|
||||||
this.stopTitleWatcher(tab.id);
|
this.stopTitleWatcher(tab.id);
|
||||||
if (this.activeTabId === tab.id) {
|
if (this.activeTabId === tab.id) {
|
||||||
|
|||||||
+236
-26
@@ -1,12 +1,20 @@
|
|||||||
import { get as httpsGet } from 'https';
|
import { get as httpsGet } from 'https';
|
||||||
import { createReadStream, createWriteStream, renameSync, unlinkSync, existsSync } from 'fs';
|
// Use original-fs to bypass Electron's asar interception — required for
|
||||||
|
// writing/renaming .asar files in the resources directory.
|
||||||
|
import { createReadStream, createWriteStream, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'original-fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { electronLog } from './logger';
|
import { electronLog } from './logger';
|
||||||
|
|
||||||
|
// ── Types ──
|
||||||
|
|
||||||
|
export type UpdateType = 'minor' | 'major';
|
||||||
|
|
||||||
export interface UpdateInfo {
|
export interface UpdateInfo {
|
||||||
version: string;
|
version: string;
|
||||||
|
updateType: UpdateType;
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
sha256: string;
|
sha256: string;
|
||||||
@@ -14,18 +22,102 @@ export interface UpdateInfo {
|
|||||||
|
|
||||||
export type ProgressCallback = (percent: number) => void;
|
export type ProgressCallback = (percent: number) => void;
|
||||||
|
|
||||||
const UPDATE_CONFIG = {
|
// ── Build-time update source (injected by Vite define) ──
|
||||||
|
|
||||||
|
declare const __UPDATE_SOURCE__: 'github' | 'gitea';
|
||||||
|
|
||||||
|
interface UpdateSourceConfig {
|
||||||
|
checkUrl: string;
|
||||||
|
allowedHosts: string[];
|
||||||
|
checksumSource: 'digest' | 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
const UPDATE_SOURCES: Record<string, UpdateSourceConfig> = {
|
||||||
|
github: {
|
||||||
checkUrl: 'https://api.github.com/repos/bigjakk/Krunker-Civilian-Client/releases/latest',
|
checkUrl: 'https://api.github.com/repos/bigjakk/Krunker-Civilian-Client/releases/latest',
|
||||||
assetPattern: /Setup\.exe$/i,
|
|
||||||
allowedHosts: ['github.com', 'githubusercontent.com'],
|
allowedHosts: ['github.com', 'githubusercontent.com'],
|
||||||
|
checksumSource: 'digest',
|
||||||
|
},
|
||||||
|
gitea: {
|
||||||
|
checkUrl: 'https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client-Test/releases/latest',
|
||||||
|
allowedHosts: ['gitea.crjlab.net'],
|
||||||
|
checksumSource: 'file',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceKey = typeof __UPDATE_SOURCE__ !== 'undefined' ? __UPDATE_SOURCE__ : 'github';
|
||||||
|
const UPDATE_CONFIG = UPDATE_SOURCES[sourceKey] || UPDATE_SOURCES.github;
|
||||||
|
|
||||||
|
const ASSET_PATTERNS = {
|
||||||
|
asar: /^app\.asar$/i,
|
||||||
|
setup: /Setup\.exe$/i,
|
||||||
|
checksums: /^checksums\.sha256$/i,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CHECK_TIMEOUT_MS = 10000;
|
const CHECK_TIMEOUT_MS = 10000;
|
||||||
const DOWNLOAD_TIMEOUT_MS = 300000; // 5 minutes
|
const DOWNLOAD_TIMEOUT_MS = 300000; // 5 minutes
|
||||||
|
|
||||||
/**
|
// ── Swap scripts (embedded, written to temp at runtime) ──
|
||||||
* Validate that a redirect URL stays on an allowed host.
|
|
||||||
*/
|
const SWAP_SCRIPT_PS1 = `param(
|
||||||
|
[int]$ProcessId,
|
||||||
|
[string]$ResourcesDir,
|
||||||
|
[string]$ExePath
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
|
||||||
|
if ($proc) { $proc.WaitForExit(30000) | Out-Null }
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 500
|
||||||
|
|
||||||
|
$asar = Join-Path $ResourcesDir "app.asar"
|
||||||
|
$pending = Join-Path $ResourcesDir "app-pending.asar"
|
||||||
|
$backup = Join-Path $ResourcesDir "app-backup.asar"
|
||||||
|
|
||||||
|
if (-not (Test-Path $pending)) { exit 1 }
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Test-Path $backup) { Remove-Item $backup -Force }
|
||||||
|
Move-Item $asar $backup -Force
|
||||||
|
Move-Item $pending $asar -Force
|
||||||
|
if (Test-Path $backup) { Remove-Item $backup -Force }
|
||||||
|
} catch {
|
||||||
|
if ((Test-Path $backup) -and -not (Test-Path $asar)) {
|
||||||
|
Move-Item $backup $asar -Force
|
||||||
|
}
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Process $ExePath
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SWAP_SCRIPT_BASH = `#!/bin/bash
|
||||||
|
PID="$1"
|
||||||
|
RESOURCES_DIR="$2"
|
||||||
|
EXE_PATH="$3"
|
||||||
|
|
||||||
|
while kill -0 "$PID" 2>/dev/null; do sleep 0.2; done
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
ASAR="$RESOURCES_DIR/app.asar"
|
||||||
|
PENDING="$RESOURCES_DIR/app-pending.asar"
|
||||||
|
BACKUP="$RESOURCES_DIR/app-backup.asar"
|
||||||
|
|
||||||
|
[ -f "$PENDING" ] || exit 1
|
||||||
|
|
||||||
|
rm -f "$BACKUP"
|
||||||
|
mv "$ASAR" "$BACKUP" && mv "$PENDING" "$ASAR" && rm -f "$BACKUP" || {
|
||||||
|
[ -f "$BACKUP" ] && [ ! -f "$ASAR" ] && mv "$BACKUP" "$ASAR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
"$EXE_PATH" &
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ── Helpers ──
|
||||||
|
|
||||||
function isAllowedRedirect(url: string): boolean {
|
function isAllowedRedirect(url: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
@@ -35,10 +127,6 @@ function isAllowedRedirect(url: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple semver comparison: returns true if a < b.
|
|
||||||
* Handles versions like "0.1.0", "1.2.3".
|
|
||||||
*/
|
|
||||||
function versionLessThan(a: string, b: string): boolean {
|
function versionLessThan(a: string, b: string): boolean {
|
||||||
const pa = a.split('.').map(Number);
|
const pa = a.split('.').map(Number);
|
||||||
const pb = b.split('.').map(Number);
|
const pb = b.split('.').map(Number);
|
||||||
@@ -52,6 +140,66 @@ function versionLessThan(a: string, b: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function simpleGet(url: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
function doGet(getUrl: string, redirectCount = 0): void {
|
||||||
|
if (redirectCount > 5) {
|
||||||
|
reject(new Error('Too many redirects'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const req = httpsGet(getUrl, {
|
||||||
|
headers: { 'User-Agent': 'KrunkerCivilianClient' },
|
||||||
|
}, (res) => {
|
||||||
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
if (!isAllowedRedirect(res.headers.location)) {
|
||||||
|
reject(new Error('Redirect to untrusted host: ' + res.headers.location));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doGet(res.headers.location, redirectCount + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
reject(new Error('HTTP ' + res.statusCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk: string) => { data += chunk; });
|
||||||
|
res.on('end', () => resolve(data));
|
||||||
|
res.on('error', reject);
|
||||||
|
});
|
||||||
|
req.setTimeout(CHECK_TIMEOUT_MS, () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Request timed out'));
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
}
|
||||||
|
doGet(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and parse a checksums.sha256 file from a release asset URL.
|
||||||
|
* Format: "<sha256> <filename>" per line.
|
||||||
|
*/
|
||||||
|
async function fetchChecksums(url: string): Promise<Map<string, string>> {
|
||||||
|
const text = await simpleGet(url);
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
const match = line.trim().match(/^([a-f0-9]{64})\s+(.+)$/i);
|
||||||
|
if (match) map.set(match[2].trim(), match[1].toLowerCase());
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Update check ──
|
||||||
|
|
||||||
|
interface ReleaseAsset {
|
||||||
|
name: string;
|
||||||
|
browser_download_url: string;
|
||||||
|
size: number;
|
||||||
|
digest?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | null> {
|
export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | null> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
electronLog.log('[KCC-Update] Checking for updates at:', UPDATE_CONFIG.checkUrl);
|
electronLog.log('[KCC-Update] Checking for updates at:', UPDATE_CONFIG.checkUrl);
|
||||||
@@ -61,7 +209,6 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
|
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
electronLog.log('[KCC-Update] Check response status:', res.statusCode);
|
electronLog.log('[KCC-Update] Check response status:', res.statusCode);
|
||||||
// Follow redirects (with domain validation)
|
|
||||||
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
const redirectUrl = res.headers.location;
|
const redirectUrl = res.headers.location;
|
||||||
electronLog.log('[KCC-Update] Redirected to:', redirectUrl);
|
electronLog.log('[KCC-Update] Redirected to:', redirectUrl);
|
||||||
@@ -84,7 +231,7 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
handleResponse(res);
|
handleResponse(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleResponse(res: import('http').IncomingMessage): void {
|
async function handleResponse(res: import('http').IncomingMessage): Promise<void> {
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
electronLog.error('[KCC-Update] Check returned status', res.statusCode);
|
electronLog.error('[KCC-Update] Check returned status', res.statusCode);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@@ -93,7 +240,7 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
|
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', (chunk: string) => { data += chunk; });
|
res.on('data', (chunk: string) => { data += chunk; });
|
||||||
res.on('end', () => {
|
res.on('end', async () => {
|
||||||
try {
|
try {
|
||||||
const release = JSON.parse(data);
|
const release = JSON.parse(data);
|
||||||
const tagName: string = release.tag_name || '';
|
const tagName: string = release.tag_name || '';
|
||||||
@@ -106,34 +253,60 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assets: Array<{ name: string; browser_download_url: string; size: number; digest: string }> = release.assets || [];
|
const assets: ReleaseAsset[] = release.assets || [];
|
||||||
const setupAsset = assets.find((a) => UPDATE_CONFIG.assetPattern.test(a.name));
|
|
||||||
if (!setupAsset) {
|
// Determine update type: prefer minor (asar) over major (setup)
|
||||||
electronLog.error('[KCC-Update] No Setup.exe asset found in release', remoteVersion);
|
const asarAsset = assets.find((a) => ASSET_PATTERNS.asar.test(a.name));
|
||||||
|
const setupAsset = assets.find((a) => ASSET_PATTERNS.setup.test(a.name));
|
||||||
|
const chosenAsset = asarAsset || setupAsset;
|
||||||
|
const updateType: UpdateType = asarAsset ? 'minor' : 'major';
|
||||||
|
|
||||||
|
if (!chosenAsset) {
|
||||||
|
electronLog.error('[KCC-Update] No app.asar or Setup.exe asset found in release', remoteVersion);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the download URL points to an allowed host
|
if (!isAllowedRedirect(chosenAsset.browser_download_url)) {
|
||||||
if (!isAllowedRedirect(setupAsset.browser_download_url)) {
|
electronLog.error('[KCC-Update] Download URL points to untrusted host:', chosenAsset.browser_download_url);
|
||||||
electronLog.error('[KCC-Update] Download URL points to untrusted host:', setupAsset.browser_download_url);
|
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract SHA-256 digest from GitHub API (format: "sha256:<hex>")
|
// Resolve SHA-256 checksum
|
||||||
const sha256 = (setupAsset.digest || '').replace(/^sha256:/i, '');
|
let sha256 = '';
|
||||||
|
if (UPDATE_CONFIG.checksumSource === 'digest') {
|
||||||
|
sha256 = (chosenAsset.digest || '').replace(/^sha256:/i, '');
|
||||||
if (!sha256) {
|
if (!sha256) {
|
||||||
electronLog.error('[KCC-Update] No SHA-256 digest found for asset');
|
electronLog.error('[KCC-Update] No SHA-256 digest found for asset');
|
||||||
resolve(null);
|
resolve(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Fetch checksums.sha256 companion file
|
||||||
|
const checksumAsset = assets.find((a) => ASSET_PATTERNS.checksums.test(a.name));
|
||||||
|
if (checksumAsset) {
|
||||||
|
try {
|
||||||
|
const checksums = await fetchChecksums(checksumAsset.browser_download_url);
|
||||||
|
sha256 = checksums.get(chosenAsset.name) || '';
|
||||||
|
} catch (err) {
|
||||||
|
electronLog.error('[KCC-Update] Failed to fetch checksums:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sha256) {
|
||||||
|
electronLog.warn('[KCC-Update] No checksum available — proceeding without verification');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
electronLog.log('[KCC-Update] Update available:', remoteVersion,
|
||||||
|
'| Type:', updateType,
|
||||||
|
'| SHA-256:', sha256 ? sha256.substring(0, 16) + '...' : 'none');
|
||||||
|
|
||||||
electronLog.log('[KCC-Update] Update available:', remoteVersion, '| SHA-256:', sha256.substring(0, 16) + '...');
|
|
||||||
resolve({
|
resolve({
|
||||||
version: remoteVersion,
|
version: remoteVersion,
|
||||||
downloadUrl: setupAsset.browser_download_url,
|
updateType,
|
||||||
fileSize: setupAsset.size,
|
downloadUrl: chosenAsset.browser_download_url,
|
||||||
|
fileSize: chosenAsset.size,
|
||||||
sha256,
|
sha256,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -160,6 +333,8 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Download ──
|
||||||
|
|
||||||
function verifyChecksum(filePath: string, expectedSha256: string): Promise<boolean> {
|
function verifyChecksum(filePath: string, expectedSha256: string): Promise<boolean> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const hash = createHash('sha256');
|
const hash = createHash('sha256');
|
||||||
@@ -188,7 +363,6 @@ export function downloadUpdate(url: string, destPath: string, onProgress: Progre
|
|||||||
const req = httpsGet(downloadUrl, {
|
const req = httpsGet(downloadUrl, {
|
||||||
headers: { 'User-Agent': 'KrunkerCivilianClient' },
|
headers: { 'User-Agent': 'KrunkerCivilianClient' },
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
// Follow redirects (with domain validation)
|
|
||||||
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
const redirectUrl = res.headers.location;
|
const redirectUrl = res.headers.location;
|
||||||
electronLog.log('[KCC-Update] Download redirected to:', redirectUrl);
|
electronLog.log('[KCC-Update] Download redirected to:', redirectUrl);
|
||||||
@@ -268,6 +442,8 @@ export function downloadUpdate(url: string, destPath: string, onProgress: Progre
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Install / Apply ──
|
||||||
|
|
||||||
export function installUpdate(installerPath: string): void {
|
export function installUpdate(installerPath: string): void {
|
||||||
electronLog.log('[KCC-Update] Launching installer:', installerPath);
|
electronLog.log('[KCC-Update] Launching installer:', installerPath);
|
||||||
const child = spawn(installerPath, [], {
|
const child = spawn(installerPath, [], {
|
||||||
@@ -277,3 +453,37 @@ export function installUpdate(installerPath: string): void {
|
|||||||
child.unref();
|
child.unref();
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyMinorUpdate(pendingAsarPath: string): void {
|
||||||
|
const resourcesDir = dirname(pendingAsarPath);
|
||||||
|
const exePath = app.getPath('exe');
|
||||||
|
const tempDir = join(app.getPath('temp'), 'kcc-update');
|
||||||
|
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
electronLog.log('[KCC-Update] Applying minor update via swap script');
|
||||||
|
electronLog.log('[KCC-Update] Resources dir:', resourcesDir);
|
||||||
|
electronLog.log('[KCC-Update] Exe path:', exePath);
|
||||||
|
electronLog.log('[KCC-Update] PID:', process.pid);
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const scriptPath = join(tempDir, 'swap-asar.ps1');
|
||||||
|
writeFileSync(scriptPath, SWAP_SCRIPT_PS1);
|
||||||
|
const child = spawn('powershell.exe', [
|
||||||
|
'-ExecutionPolicy', 'Bypass',
|
||||||
|
'-File', scriptPath,
|
||||||
|
'-ProcessId', String(process.pid),
|
||||||
|
'-ResourcesDir', resourcesDir,
|
||||||
|
'-ExePath', exePath,
|
||||||
|
], { detached: true, stdio: 'ignore' });
|
||||||
|
child.unref();
|
||||||
|
} else {
|
||||||
|
const scriptPath = join(tempDir, 'swap-asar.sh');
|
||||||
|
writeFileSync(scriptPath, SWAP_SCRIPT_BASH, { mode: 0o755 });
|
||||||
|
const child = spawn('bash', [
|
||||||
|
scriptPath, String(process.pid), resourcesDir, exePath,
|
||||||
|
], { detached: true, stdio: 'ignore' });
|
||||||
|
child.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|||||||
+174
-120
@@ -321,20 +321,23 @@ function createNumberRow(opts: {
|
|||||||
max: number;
|
max: number;
|
||||||
value: number;
|
value: number;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => void;
|
||||||
|
step?: number;
|
||||||
safety?: number;
|
safety?: number;
|
||||||
restart?: boolean;
|
restart?: boolean;
|
||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
refreshOnly?: boolean;
|
refreshOnly?: boolean;
|
||||||
}): HTMLElement {
|
}): HTMLElement {
|
||||||
const s = opts.safety || 0;
|
const s = opts.safety || 0;
|
||||||
|
const step = opts.step || 1;
|
||||||
|
const parse = step < 1 ? parseFloat : parseInt;
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'setting settName safety-' + s + ' num';
|
row.className = 'setting settName safety-' + s + ' num';
|
||||||
row.innerHTML =
|
row.innerHTML =
|
||||||
settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
|
settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
|
||||||
'<span class="setting-title">' + escapeHtml(opts.label) + '</span>' +
|
'<span class="setting-title">' + escapeHtml(opts.label) + '</span>' +
|
||||||
'<span class="setting-input-wrapper">' +
|
'<span class="setting-input-wrapper">' +
|
||||||
'<div class="slidecontainer"><input type="range" class="sliderM s-update-secondary" min="' + opts.min + '" max="' + opts.max + '" value="' + opts.value + '"></div>' +
|
'<div class="slidecontainer"><input type="range" class="sliderM s-update-secondary" min="' + opts.min + '" max="' + opts.max + '" step="' + step + '" value="' + opts.value + '"></div>' +
|
||||||
'<input type="number" class="rb-input s-update sliderVal" min="' + opts.min + '" max="' + opts.max + '" value="' + opts.value + '">' +
|
'<input type="number" class="rb-input s-update sliderVal" min="' + opts.min + '" max="' + opts.max + '" step="' + step + '" value="' + opts.value + '">' +
|
||||||
'</span>' +
|
'</span>' +
|
||||||
'<div class="setting-desc-new">' + escapeHtml(opts.desc) + '</div>';
|
'<div class="setting-desc-new">' + escapeHtml(opts.desc) + '</div>';
|
||||||
const rangeInput = row.querySelector('input[type="range"]') as HTMLInputElement;
|
const rangeInput = row.querySelector('input[type="range"]') as HTMLInputElement;
|
||||||
@@ -343,7 +346,7 @@ function createNumberRow(opts: {
|
|||||||
numInput.value = rangeInput.value;
|
numInput.value = rangeInput.value;
|
||||||
});
|
});
|
||||||
rangeInput.addEventListener('change', () => {
|
rangeInput.addEventListener('change', () => {
|
||||||
const v = Math.max(opts.min, Math.min(opts.max, parseInt(rangeInput.value) || 0));
|
const v = Math.max(opts.min, Math.min(opts.max, parse(rangeInput.value) || 0));
|
||||||
rangeInput.value = String(v);
|
rangeInput.value = String(v);
|
||||||
numInput.value = String(v);
|
numInput.value = String(v);
|
||||||
opts.onChange(v);
|
opts.onChange(v);
|
||||||
@@ -351,7 +354,7 @@ function createNumberRow(opts: {
|
|||||||
else if (opts.refreshOnly) onSettingChanged('refresh');
|
else if (opts.refreshOnly) onSettingChanged('refresh');
|
||||||
});
|
});
|
||||||
numInput.addEventListener('change', () => {
|
numInput.addEventListener('change', () => {
|
||||||
const v = Math.max(opts.min, Math.min(opts.max, parseInt(numInput.value) || 0));
|
const v = Math.max(opts.min, Math.min(opts.max, parse(numInput.value) || 0));
|
||||||
numInput.value = String(v);
|
numInput.value = String(v);
|
||||||
rangeInput.value = String(v);
|
rangeInput.value = String(v);
|
||||||
opts.onChange(v);
|
opts.onChange(v);
|
||||||
@@ -543,19 +546,9 @@ interface SettingsBag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildGeneralSection(
|
function buildGeneralSection(
|
||||||
body: HTMLElement, gameConf: any, uiConfRaw: any, perfConf: any, bag: SettingsBag,
|
body: HTMLElement, gameConf: any, uiConfRaw: any, bag: SettingsBag,
|
||||||
): void {
|
): void {
|
||||||
const perfDefaults = { fpsUnlocked: true };
|
const gameDefaults = { lastServer: '', socialTabBehaviour: 'New Window', rememberTabs: false };
|
||||||
const perf = { ...perfDefaults, ...perfConf };
|
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
|
||||||
label: 'Unlimited FPS',
|
|
||||||
desc: 'Uncap the frame rate (requires restart)',
|
|
||||||
checked: perf.fpsUnlocked, restart: true,
|
|
||||||
onChange: (v) => { perf.fpsUnlocked = v; ipcRenderer.invoke('set-config', 'performance', perf); },
|
|
||||||
}));
|
|
||||||
|
|
||||||
const gameDefaults = { lastServer: '', socialTabBehaviour: 'New Window' };
|
|
||||||
const game = { ...gameDefaults, ...gameConf };
|
const game = { ...gameDefaults, ...gameConf };
|
||||||
|
|
||||||
body.appendChild(createSelectRow({
|
body.appendChild(createSelectRow({
|
||||||
@@ -566,6 +559,13 @@ function buildGeneralSection(
|
|||||||
onChange: (v) => { game.socialTabBehaviour = v; ipcRenderer.invoke('set-config', 'game', game); },
|
onChange: (v) => { game.socialTabBehaviour = v; ipcRenderer.invoke('set-config', 'game', game); },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
body.appendChild(createToggleRow({
|
||||||
|
label: 'Remember Tabs',
|
||||||
|
desc: 'Restore your open tabs when you reopen the social/hub window',
|
||||||
|
checked: game.rememberTabs, instant: true,
|
||||||
|
onChange: (v) => { game.rememberTabs = v; ipcRenderer.invoke('set-config', 'game', game); },
|
||||||
|
}));
|
||||||
|
|
||||||
const uiDefaults = { showExitButton: true, deathscreenAnimation: false, hideMenuPopups: false };
|
const uiDefaults = { showExitButton: true, deathscreenAnimation: false, hideMenuPopups: false };
|
||||||
const ui = { ...uiDefaults, ...uiConfRaw };
|
const ui = { ...uiDefaults, ...uiConfRaw };
|
||||||
|
|
||||||
@@ -584,6 +584,72 @@ function buildGeneralSection(
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
body.appendChild(createToggleRow({
|
||||||
|
label: 'Join as Spectator',
|
||||||
|
desc: 'Automatically enable spectate mode when joining a game',
|
||||||
|
checked: game.joinAsSpectator, instant: true,
|
||||||
|
onChange: (v) => { game.joinAsSpectator = v; ipcRenderer.invoke('set-config', 'game', game); },
|
||||||
|
}));
|
||||||
|
|
||||||
|
body.appendChild(createToggleRow({
|
||||||
|
label: 'Show Changelog',
|
||||||
|
desc: 'Show release notes popup when the client updates',
|
||||||
|
checked: ui.showChangelog ?? true, instant: true,
|
||||||
|
onChange: (v) => { ui.showChangelog = v; saveUI(); },
|
||||||
|
}));
|
||||||
|
|
||||||
|
body.appendChild(createKeybindRow('Toggle Fullscreen', 'Fullscreen the game window (default F11)', bag.binds.fullscreenToggle, (b) => {
|
||||||
|
bag.binds.fullscreenToggle = b;
|
||||||
|
bag.saveBinds();
|
||||||
|
}, undefined, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGameSection(
|
||||||
|
body: HTMLElement, gameConf: any, uiConfRaw: any, bag: SettingsBag,
|
||||||
|
): void {
|
||||||
|
const game = { rawInput: true, showPing: true, hpEnemyCounter: true, ...gameConf };
|
||||||
|
const ui = { deathscreenAnimation: false, hideMenuPopups: false, menuTimer: true, doublePing: true, ...uiConfRaw };
|
||||||
|
|
||||||
|
function saveGame(): void {
|
||||||
|
ipcRenderer.invoke('set-config', 'game', game);
|
||||||
|
}
|
||||||
|
function saveUI(): void {
|
||||||
|
ipcRenderer.invoke('set-config', 'ui', ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bag.isWindows) {
|
||||||
|
body.appendChild(createToggleRow({
|
||||||
|
label: 'Raw Input',
|
||||||
|
desc: 'Bypass OS mouse acceleration for direct 1:1 sensor input (Windows only)',
|
||||||
|
checked: game.rawInput ?? true, refreshOnly: true,
|
||||||
|
onChange: (v) => { game.rawInput = v; saveGame(); },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
body.appendChild(createToggleRow({
|
||||||
|
label: 'Show Ping in Player List',
|
||||||
|
desc: 'Replace the ping icon with numeric millisecond values in the player list',
|
||||||
|
checked: game.showPing ?? true, refreshOnly: true,
|
||||||
|
onChange: (v) => { game.showPing = v; saveGame(); },
|
||||||
|
}));
|
||||||
|
|
||||||
|
body.appendChild(createToggleRow({
|
||||||
|
label: 'Double Ping Display',
|
||||||
|
desc: 'Show the real ping value (Krunker displays half the actual latency)',
|
||||||
|
checked: ui.doublePing ?? true, refreshOnly: true,
|
||||||
|
onChange: (v) => { ui.doublePing = v; saveUI(); },
|
||||||
|
}));
|
||||||
|
|
||||||
|
body.appendChild(createToggleRow({
|
||||||
|
label: 'Hardpoint Enemy Counter',
|
||||||
|
desc: 'Show enemy capture points in Hardpoint mode',
|
||||||
|
checked: game.hpEnemyCounter ?? true, refreshOnly: true,
|
||||||
|
onChange: (v) => {
|
||||||
|
game.hpEnemyCounter = v; saveGame();
|
||||||
|
if (v) initHPCounter(); else destroyHPCounter();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
body.appendChild(createToggleRow({
|
||||||
label: 'Block Death Screen Animation',
|
label: 'Block Death Screen Animation',
|
||||||
desc: 'Disable the slide-in animation on the death screen',
|
desc: 'Disable the slide-in animation on the death screen',
|
||||||
@@ -601,13 +667,6 @@ function buildGeneralSection(
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
|
||||||
label: 'Join as Spectator',
|
|
||||||
desc: 'Automatically enable spectate mode when joining a game',
|
|
||||||
checked: game.joinAsSpectator, instant: true,
|
|
||||||
onChange: (v) => { game.joinAsSpectator = v; ipcRenderer.invoke('set-config', 'game', game); },
|
|
||||||
}));
|
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
body.appendChild(createToggleRow({
|
||||||
label: 'Menu Timer',
|
label: 'Menu Timer',
|
||||||
desc: 'Show the game/spectate timer on the menu screen',
|
desc: 'Show the game/spectate timer on the menu screen',
|
||||||
@@ -615,64 +674,59 @@ function buildGeneralSection(
|
|||||||
onChange: (v) => { ui.menuTimer = v; saveUI(); setMenuTimer(v); },
|
onChange: (v) => { ui.menuTimer = v; saveUI(); setMenuTimer(v); },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
|
||||||
label: 'Double Ping Display',
|
|
||||||
desc: 'Show the real ping value (Krunker displays half the actual latency)',
|
|
||||||
checked: ui.doublePing ?? true, refreshOnly: true,
|
|
||||||
onChange: (v) => { ui.doublePing = v; saveUI(); },
|
|
||||||
}));
|
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
|
||||||
label: 'Show Ping in Player List',
|
|
||||||
desc: 'Replace the ping icon with numeric millisecond values in the player list',
|
|
||||||
checked: game.showPing ?? true, refreshOnly: true,
|
|
||||||
onChange: (v) => { game.showPing = v; ipcRenderer.invoke('set-config', 'game', game); },
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (bag.isWindows) {
|
|
||||||
body.appendChild(createToggleRow({
|
|
||||||
label: 'Raw Input',
|
|
||||||
desc: 'Bypass OS mouse acceleration for direct 1:1 sensor input (Windows only)',
|
|
||||||
checked: game.rawInput ?? true, refreshOnly: true,
|
|
||||||
onChange: (v) => { game.rawInput = v; ipcRenderer.invoke('set-config', 'game', game); },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
|
||||||
label: 'Hardpoint Enemy Counter',
|
|
||||||
desc: 'Show enemy capture points in Hardpoint mode',
|
|
||||||
checked: game.hpEnemyCounter ?? true, refreshOnly: true,
|
|
||||||
onChange: (v) => {
|
|
||||||
game.hpEnemyCounter = v; ipcRenderer.invoke('set-config', 'game', game);
|
|
||||||
if (v) initHPCounter(); else destroyHPCounter();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
|
||||||
label: 'Show Changelog',
|
|
||||||
desc: 'Show release notes popup when the client updates',
|
|
||||||
checked: ui.showChangelog ?? true, instant: true,
|
|
||||||
onChange: (v) => { ui.showChangelog = v; saveUI(); },
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (ui.deathscreenAnimation) setDeathAnimBlock(true);
|
if (ui.deathscreenAnimation) setDeathAnimBlock(true);
|
||||||
if (ui.menuTimer ?? true) setMenuTimer(true);
|
if (ui.menuTimer ?? true) setMenuTimer(true);
|
||||||
if (ui.hideMenuPopups) startHidePopups();
|
if (ui.hideMenuPopups) startHidePopups();
|
||||||
|
|
||||||
body.appendChild(createKeybindRow('Toggle Fullscreen', 'Fullscreen the game window (default F11)', bag.binds.fullscreenToggle, (b) => {
|
|
||||||
bag.binds.fullscreenToggle = b;
|
|
||||||
bag.saveBinds();
|
|
||||||
}, undefined, true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSwapperSection(body: HTMLElement, swapperConf: any, uiConfRaw: any): void {
|
function buildPerformanceSection(
|
||||||
const swapEnabled = swapperConf ? swapperConf.enabled : true;
|
body: HTMLElement, perfConf: any, isWindows: boolean,
|
||||||
const ui = { cssTheme: 'disabled', loadingTheme: 'disabled', backgroundUrl: '', ...uiConfRaw };
|
): void {
|
||||||
|
const perf = { fpsUnlocked: true, cpuThrottleGame: 1, cpuThrottleMenu: 1.5, processPriority: 'Normal', ...perfConf };
|
||||||
|
|
||||||
function saveUI(): void {
|
function savePerf(): void {
|
||||||
ipcRenderer.invoke('set-config', 'ui', ui);
|
ipcRenderer.invoke('set-config', 'performance', perf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.appendChild(createToggleRow({
|
||||||
|
label: 'Unlimited FPS',
|
||||||
|
desc: 'Uncap the frame rate (requires restart)',
|
||||||
|
checked: perf.fpsUnlocked, restart: true,
|
||||||
|
onChange: (v) => { perf.fpsUnlocked = v; savePerf(); },
|
||||||
|
}));
|
||||||
|
|
||||||
|
body.appendChild(createNumberRow({
|
||||||
|
label: 'CPU Throttle (Game)', desc: 'CPU throttle rate during gameplay (1 = no throttle, 3 = heavy throttle)',
|
||||||
|
min: 1, max: 3, step: 0.01, value: perf.cpuThrottleGame, instant: true, safety: 2,
|
||||||
|
onChange: (v) => { perf.cpuThrottleGame = v; savePerf(); },
|
||||||
|
}));
|
||||||
|
|
||||||
|
body.appendChild(createNumberRow({
|
||||||
|
label: 'CPU Throttle (Menu)', desc: 'CPU throttle rate on menu screens (1 = no throttle, 3 = heavy throttle)',
|
||||||
|
min: 1, max: 3, step: 0.01, value: perf.cpuThrottleMenu, instant: true, safety: 1,
|
||||||
|
onChange: (v) => { perf.cpuThrottleMenu = v; savePerf(); },
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
body.appendChild(createSelectRow({
|
||||||
|
label: 'Process Priority',
|
||||||
|
desc: 'OS-level process priority for the client (Windows only)',
|
||||||
|
options: [
|
||||||
|
{ value: 'Normal', label: 'Normal' },
|
||||||
|
{ value: 'Above Normal', label: 'Above Normal' },
|
||||||
|
{ value: 'High', label: 'High' },
|
||||||
|
{ value: 'Below Normal', label: 'Below Normal' },
|
||||||
|
{ value: 'Low', label: 'Low' },
|
||||||
|
],
|
||||||
|
value: perf.processPriority, restart: true, safety: 2,
|
||||||
|
onChange: (v) => { perf.processPriority = v; savePerf(); },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSwapperSection(body: HTMLElement, swapperConf: any): void {
|
||||||
|
const swapEnabled = swapperConf ? swapperConf.enabled : true;
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
body.appendChild(createToggleRow({
|
||||||
label: 'Resource Swapper',
|
label: 'Resource Swapper',
|
||||||
desc: 'Replace game textures, sounds, and models with local files',
|
desc: 'Replace game textures, sounds, and models with local files',
|
||||||
@@ -697,6 +751,14 @@ function buildSwapperSection(body: HTMLElement, swapperConf: any, uiConfRaw: any
|
|||||||
swapFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-swap-folder'));
|
swapFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-swap-folder'));
|
||||||
folderRow.appendChild(swapFolderBtn);
|
folderRow.appendChild(swapFolderBtn);
|
||||||
body.appendChild(folderRow);
|
body.appendChild(folderRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAppearanceSection(body: HTMLElement, uiConfRaw: any): void {
|
||||||
|
const ui = { cssTheme: 'disabled', loadingTheme: 'disabled', backgroundUrl: '', ...uiConfRaw };
|
||||||
|
|
||||||
|
function saveUI(): void {
|
||||||
|
ipcRenderer.invoke('set-config', 'ui', ui);
|
||||||
|
}
|
||||||
|
|
||||||
// ── CSS Theme selector (populated from swap/themes/) ──
|
// ── CSS Theme selector (populated from swap/themes/) ──
|
||||||
const themeRow = document.createElement('div');
|
const themeRow = document.createElement('div');
|
||||||
@@ -767,10 +829,31 @@ function buildSwapperSection(body: HTMLElement, swapperConf: any, uiConfRaw: any
|
|||||||
saveUI();
|
saveUI();
|
||||||
onSettingChanged('refresh');
|
onSettingChanged('refresh');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Background URL (overrides loading theme selection) ──
|
||||||
|
const urlRow = document.createElement('div');
|
||||||
|
urlRow.className = 'setting settName safety-0';
|
||||||
|
urlRow.innerHTML =
|
||||||
|
refreshIcon('refresh-icon') +
|
||||||
|
'<span class="setting-title">Background URL</span>' +
|
||||||
|
'<div class="setting-desc-new">Direct image URL for loading screen (overrides dropdown above)</div>';
|
||||||
|
const urlInput = document.createElement('input');
|
||||||
|
urlInput.type = 'text';
|
||||||
|
urlInput.className = 'inputGrey2';
|
||||||
|
urlInput.placeholder = 'https://example.com/image.png';
|
||||||
|
urlInput.value = ui.backgroundUrl || '';
|
||||||
|
urlInput.style.width = '300px';
|
||||||
|
urlInput.addEventListener('change', () => {
|
||||||
|
ui.backgroundUrl = urlInput.value.trim();
|
||||||
|
saveUI();
|
||||||
|
onSettingChanged('refresh');
|
||||||
|
});
|
||||||
|
urlRow.appendChild(urlInput);
|
||||||
|
body.appendChild(urlRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void {
|
function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void {
|
||||||
const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true, autoJoin: false };
|
const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true, sortByPlayers: false };
|
||||||
|
|
||||||
function saveMM(): void {
|
function saveMM(): void {
|
||||||
ipcRenderer.invoke('set-config', 'matchmaker', mm);
|
ipcRenderer.invoke('set-config', 'matchmaker', mm);
|
||||||
@@ -791,20 +874,16 @@ function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
body.appendChild(createToggleRow({
|
||||||
label: 'Auto-Join',
|
label: 'Prioritize Player Count',
|
||||||
desc: 'Automatically join the best match without showing the popup',
|
desc: 'Sort results by most players first, then by ping (default is ping first)',
|
||||||
checked: mm.autoJoin ?? false, instant: true,
|
checked: mm.sortByPlayers ?? false, instant: true,
|
||||||
onChange: (v) => { mm.autoJoin = v; saveMM(); },
|
onChange: (v) => { mm.sortByPlayers = v; saveMM(); },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
body.appendChild(createKeybindRow('Matchmaker Hotkey', 'Key to trigger the custom matchmaker', bag.binds.matchmaker, (b) => {
|
body.appendChild(createKeybindRow('Matchmaker Hotkey', 'Key to trigger the custom matchmaker', bag.binds.matchmaker, (b) => {
|
||||||
bag.binds.matchmaker = b;
|
bag.binds.matchmaker = b;
|
||||||
bag.saveBinds();
|
bag.saveBinds();
|
||||||
}, undefined, true));
|
}, undefined, true));
|
||||||
body.appendChild(createKeybindRow('Matchmaker Accept', 'Key to accept a found game', bag.binds.matchmakerAccept, (b) => {
|
|
||||||
bag.binds.matchmakerAccept = b;
|
|
||||||
bag.saveBinds();
|
|
||||||
}, undefined, true));
|
|
||||||
body.appendChild(createKeybindRow('Matchmaker Cancel', 'Key to dismiss the matchmaker popup', bag.binds.matchmakerCancel, (b) => {
|
body.appendChild(createKeybindRow('Matchmaker Cancel', 'Key to dismiss the matchmaker popup', bag.binds.matchmakerCancel, (b) => {
|
||||||
bag.binds.matchmakerCancel = b;
|
bag.binds.matchmakerCancel = b;
|
||||||
bag.saveBinds();
|
bag.saveBinds();
|
||||||
@@ -1071,7 +1150,7 @@ function buildChatSection(body: HTMLElement, gameConf: any, translatorConf: any)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildAdvancedSection(
|
function buildAdvancedSection(
|
||||||
body: HTMLElement, advConf: any, perfConf: any, isWindows: boolean,
|
body: HTMLElement, advConf: any, isWindows: boolean,
|
||||||
): void {
|
): void {
|
||||||
const advDefaults = {
|
const advDefaults = {
|
||||||
removeUselessFeatures: true,
|
removeUselessFeatures: true,
|
||||||
@@ -1084,11 +1163,6 @@ function buildAdvancedSection(
|
|||||||
verboseLogging: false,
|
verboseLogging: false,
|
||||||
};
|
};
|
||||||
const adv = { ...advDefaults, ...advConf };
|
const adv = { ...advDefaults, ...advConf };
|
||||||
const perf = { cpuThrottleGame: 1, cpuThrottleMenu: 1.5, processPriority: 'Normal', ...perfConf };
|
|
||||||
|
|
||||||
function savePerf(): void {
|
|
||||||
ipcRenderer.invoke('set-config', 'performance', perf);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveAdv(): void {
|
function saveAdv(): void {
|
||||||
ipcRenderer.invoke('set-config', 'advanced', adv);
|
ipcRenderer.invoke('set-config', 'advanced', adv);
|
||||||
@@ -1135,34 +1209,6 @@ function buildAdvancedSection(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
body.appendChild(createNumberRow({
|
|
||||||
label: 'CPU Throttle (Game)', desc: 'CPU throttle rate during gameplay (1 = no throttle, 3 = heavy throttle)',
|
|
||||||
min: 1, max: 3, value: perf.cpuThrottleGame, instant: true, safety: 2,
|
|
||||||
onChange: (v) => { perf.cpuThrottleGame = v; savePerf(); },
|
|
||||||
}));
|
|
||||||
|
|
||||||
body.appendChild(createNumberRow({
|
|
||||||
label: 'CPU Throttle (Menu)', desc: 'CPU throttle rate on menu screens (1 = no throttle, 3 = heavy throttle)',
|
|
||||||
min: 1, max: 3, value: perf.cpuThrottleMenu, instant: true, safety: 1,
|
|
||||||
onChange: (v) => { perf.cpuThrottleMenu = v; savePerf(); },
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (isWindows) {
|
|
||||||
body.appendChild(createSelectRow({
|
|
||||||
label: 'Process Priority',
|
|
||||||
desc: 'OS-level process priority for the client (Windows only)',
|
|
||||||
options: [
|
|
||||||
{ value: 'Normal', label: 'Normal' },
|
|
||||||
{ value: 'Above Normal', label: 'Above Normal' },
|
|
||||||
{ value: 'High', label: 'High' },
|
|
||||||
{ value: 'Below Normal', label: 'Below Normal' },
|
|
||||||
{ value: 'Low', label: 'Low' },
|
|
||||||
],
|
|
||||||
value: perf.processPriority, restart: true, safety: 2,
|
|
||||||
onChange: (v) => { perf.processPriority = v; savePerf(); },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
body.appendChild(createToggleRow({
|
body.appendChild(createToggleRow({
|
||||||
label: 'Verbose Logging',
|
label: 'Verbose Logging',
|
||||||
desc: 'Forward all preload console output to the Electron log file',
|
desc: 'Forward all preload console output to the Electron log file',
|
||||||
@@ -1270,8 +1316,14 @@ function renderSettings(searchQuery?: string): void {
|
|||||||
// ── Create section shells ──
|
// ── Create section shells ──
|
||||||
const genSec = createSection('General');
|
const genSec = createSection('General');
|
||||||
container.appendChild(genSec.section);
|
container.appendChild(genSec.section);
|
||||||
|
const gameSec = createSection('Game');
|
||||||
|
container.appendChild(gameSec.section);
|
||||||
|
const perfSec = createSection('Performance');
|
||||||
|
container.appendChild(perfSec.section);
|
||||||
const swapSec = createSection('Swapper');
|
const swapSec = createSection('Swapper');
|
||||||
container.appendChild(swapSec.section);
|
container.appendChild(swapSec.section);
|
||||||
|
const appearSec = createSection('Appearance');
|
||||||
|
container.appendChild(appearSec.section);
|
||||||
const mmSec = createSection('Matchmaker');
|
const mmSec = createSection('Matchmaker');
|
||||||
container.appendChild(mmSec.section);
|
container.appendChild(mmSec.section);
|
||||||
const chatSec = createSection('Chat');
|
const chatSec = createSection('Chat');
|
||||||
@@ -1300,7 +1352,6 @@ function renderSettings(searchQuery?: string): void {
|
|||||||
const translatorConf = allConf.translator;
|
const translatorConf = allConf.translator;
|
||||||
const defaultBinds = {
|
const defaultBinds = {
|
||||||
matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false },
|
matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false },
|
||||||
matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false },
|
|
||||||
matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false },
|
matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false },
|
||||||
fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
|
fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
|
||||||
};
|
};
|
||||||
@@ -1314,13 +1365,16 @@ function renderSettings(searchQuery?: string): void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Populate each section
|
// Populate each section
|
||||||
buildGeneralSection(genSec.body, gameConf, uiConfRaw, allConf.performance, bag);
|
buildGeneralSection(genSec.body, gameConf, uiConfRaw, bag);
|
||||||
buildSwapperSection(swapSec.body, swapperConf, uiConfRaw);
|
buildGameSection(gameSec.body, gameConf, uiConfRaw, bag);
|
||||||
|
buildPerformanceSection(perfSec.body, allConf.performance, isWindows);
|
||||||
|
buildSwapperSection(swapSec.body, swapperConf);
|
||||||
|
buildAppearanceSection(appearSec.body, uiConfRaw);
|
||||||
buildMatchmakerSection(mmSec.body, mmConf, bag);
|
buildMatchmakerSection(mmSec.body, mmConf, bag);
|
||||||
buildChatSection(chatSec.body, gameConf, translatorConf);
|
buildChatSection(chatSec.body, gameConf, translatorConf);
|
||||||
buildDiscordSection(discordSec.body, discordConf);
|
buildDiscordSection(discordSec.body, discordConf);
|
||||||
buildAccountsSection(accSec.body, allConf.accounts);
|
buildAccountsSection(accSec.body, allConf.accounts);
|
||||||
buildAdvancedSection(advSec.body, advConf, allConf.performance, isWindows);
|
buildAdvancedSection(advSec.body, advConf, isWindows);
|
||||||
renderUserscriptsSection(usSec.body);
|
renderUserscriptsSection(usSec.body);
|
||||||
|
|
||||||
if (searchQuery) applySearchFilter(container, holder, searchQuery);
|
if (searchQuery) applySearchFilter(container, holder, searchQuery);
|
||||||
|
|||||||
+17
-96
@@ -17,8 +17,8 @@ export const MATCHMAKER_GAMEMODE_FILTER = [
|
|||||||
'Domination', 'Kranked FFA', 'Team Defender', 'Deposit FFA', 'Chaos Snipers',
|
'Domination', 'Kranked FFA', 'Team Defender', 'Deposit FFA', 'Chaos Snipers',
|
||||||
'Bighead FFA',
|
'Bighead FFA',
|
||||||
];
|
];
|
||||||
export const MATCHMAKER_REGIONS = ['MBI', 'NY', 'FRA', 'SIN', 'DAL', 'SYD', 'MIA', 'BHN', 'TOK', 'BRZ', 'AFR', 'LON', 'CHI', 'SV', 'STL', 'MX'];
|
export const MATCHMAKER_REGIONS = ['SV', 'TOK', 'FRA', 'MBI', 'SYD', 'SIN', 'DAL', 'BHN', 'BRZ', 'NY'];
|
||||||
export const MATCHMAKER_REGION_NAMES: Record<string, string> = { MBI: 'Mumbai', NY: 'New York', FRA: 'Frankfurt', SIN: 'Singapore', DAL: 'Dallas', SYD: 'Sydney', MIA: 'Miami', BHN: 'Middle East', TOK: 'Tokyo', BRZ: 'Brazil', AFR: 'South Africa', LON: 'London', CHI: 'China', SV: 'Silicon Valley', STL: 'Seattle', MX: 'Mexico' };
|
export const MATCHMAKER_REGION_NAMES: Record<string, string> = { SV: 'Silicon Valley', TOK: 'Tokyo', FRA: 'Frankfurt', MBI: 'Mumbai', SYD: 'Sydney', SIN: 'Singapore', DAL: 'Dallas', BHN: 'Bahrain', BRZ: 'Brazil', NY: 'New York' };
|
||||||
export const MAP_ICON_INDICES = ['Burg', 'Littletown', 'Sandstorm', 'Subzero', 'Undergrowth', 'Shipment', 'Freight', 'Lostworld', 'Citadel', 'Oasis', 'Kanji', 'Industry', 'Lumber', 'Evacuation', 'Site', 'SkyTemple', 'Lagoon', 'Bureau', 'Tortuga', 'Tropicano', 'Krunk_Plaza', 'Arena', 'Habitat', 'Atomic', 'Old_Burg', 'Throwback', 'Stockade', 'Facility', 'Clockwork', 'Laboratory', 'Shipyard', 'Soul Sanctum', 'Bazaar', 'Erupt', 'HQ', 'Khepri', 'Lush', 'Vivo', 'Slide Moonlight', 'Eterno Sim'];
|
export const MAP_ICON_INDICES = ['Burg', 'Littletown', 'Sandstorm', 'Subzero', 'Undergrowth', 'Shipment', 'Freight', 'Lostworld', 'Citadel', 'Oasis', 'Kanji', 'Industry', 'Lumber', 'Evacuation', 'Site', 'SkyTemple', 'Lagoon', 'Bureau', 'Tortuga', 'Tropicano', 'Krunk_Plaza', 'Arena', 'Habitat', 'Atomic', 'Old_Burg', 'Throwback', 'Stockade', 'Facility', 'Clockwork', 'Laboratory', 'Shipyard', 'Soul Sanctum', 'Bazaar', 'Erupt', 'HQ', 'Khepri', 'Lush', 'Vivo', 'Slide Moonlight', 'Eterno Sim'];
|
||||||
export const MATCHMAKER_MAP_NAMES: Record<string, string> = {
|
export const MATCHMAKER_MAP_NAMES: Record<string, string> = {
|
||||||
SkyTemple: 'Sky Temple', Krunk_Plaza: 'Krunk Plaza', Old_Burg: 'Old Burg',
|
SkyTemple: 'Sky Temple', Krunk_Plaza: 'Krunk Plaza', Old_Burg: 'Old Burg',
|
||||||
@@ -65,18 +65,10 @@ export interface MatchmakerConfig {
|
|||||||
maxPlayers: number;
|
maxPlayers: number;
|
||||||
minRemainingTime: number;
|
minRemainingTime: number;
|
||||||
openServerBrowser: boolean;
|
openServerBrowser: boolean;
|
||||||
autoJoin: boolean;
|
sortByPlayers: boolean;
|
||||||
acceptKey: Keybind;
|
|
||||||
cancelKey: Keybind;
|
cancelKey: Keybind;
|
||||||
}
|
}
|
||||||
|
|
||||||
function secondsToTimestring(num: number): string {
|
|
||||||
const minutes = Math.floor(num / 60);
|
|
||||||
const seconds = num % 60;
|
|
||||||
if (minutes < 1) return `${num}s`;
|
|
||||||
return `${minutes}m ${seconds}s`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesKey(bind: Keybind, event: KeyboardEvent): boolean {
|
function matchesKey(bind: Keybind, event: KeyboardEvent): boolean {
|
||||||
if ((document.activeElement as HTMLElement)?.tagName === 'INPUT') return false;
|
if ((document.activeElement as HTMLElement)?.tagName === 'INPUT') return false;
|
||||||
return event.key === bind.key
|
return event.key === bind.key
|
||||||
@@ -102,21 +94,17 @@ popupElement.appendChild(popupDescription);
|
|||||||
const popupOptions = document.createElement('div');
|
const popupOptions = document.createElement('div');
|
||||||
popupOptions.id = 'matchmakerPopupOptions';
|
popupOptions.id = 'matchmakerPopupOptions';
|
||||||
|
|
||||||
const popupConfirmBtn = document.createElement('div');
|
|
||||||
popupConfirmBtn.id = 'matchmakerConfirmButton';
|
|
||||||
popupConfirmBtn.className = 'matchmakerPopupButton bigShadowT';
|
|
||||||
popupConfirmBtn.textContent = 'Join';
|
|
||||||
popupConfirmBtn.setAttribute('onmouseenter', 'playTick()');
|
|
||||||
popupConfirmBtn.addEventListener('click', () => decideMatchmakerDecision(true));
|
|
||||||
|
|
||||||
const popupCancelBtn = document.createElement('div');
|
const popupCancelBtn = document.createElement('div');
|
||||||
popupCancelBtn.id = 'matchmakerCancelButton';
|
popupCancelBtn.id = 'matchmakerCancelButton';
|
||||||
popupCancelBtn.className = 'matchmakerPopupButton bigShadowT';
|
popupCancelBtn.className = 'matchmakerPopupButton bigShadowT';
|
||||||
popupCancelBtn.textContent = 'Cancel';
|
popupCancelBtn.textContent = 'Cancel';
|
||||||
popupCancelBtn.setAttribute('onmouseenter', 'playTick()');
|
popupCancelBtn.setAttribute('onmouseenter', 'playTick()');
|
||||||
popupCancelBtn.addEventListener('click', () => decideMatchmakerDecision(false));
|
popupCancelBtn.addEventListener('click', () => {
|
||||||
|
const w = window as any;
|
||||||
|
if (typeof w.playSelect === 'function') w.playSelect();
|
||||||
|
dismissPopup();
|
||||||
|
});
|
||||||
|
|
||||||
popupOptions.appendChild(popupConfirmBtn);
|
|
||||||
popupOptions.appendChild(popupCancelBtn);
|
popupOptions.appendChild(popupCancelBtn);
|
||||||
popupElement.appendChild(popupOptions);
|
popupElement.appendChild(popupOptions);
|
||||||
|
|
||||||
@@ -146,10 +134,8 @@ searchContainer.appendChild(searchCancelBtn);
|
|||||||
popupElement.appendChild(searchContainer);
|
popupElement.appendChild(searchContainer);
|
||||||
|
|
||||||
// ── State ──
|
// ── State ──
|
||||||
let popupGameID = '';
|
|
||||||
let popupCandidates: MatchmakerGame[] = [];
|
let popupCandidates: MatchmakerGame[] = [];
|
||||||
let openServerBrowser = true;
|
let openServerBrowser = true;
|
||||||
let confirmKey: Keybind = { key: 'Enter', ctrl: false, shift: false, alt: false };
|
|
||||||
let cancelKey: Keybind = { key: 'Escape', ctrl: false, shift: false, alt: false };
|
let cancelKey: Keybind = { key: 'Escape', ctrl: false, shift: false, alt: false };
|
||||||
let searchAborted = false;
|
let searchAborted = false;
|
||||||
|
|
||||||
@@ -191,25 +177,11 @@ async function verifyAndJoin(gameID: string): Promise<void> {
|
|||||||
|
|
||||||
function dismissPopup(): void {
|
function dismissPopup(): void {
|
||||||
document.removeEventListener('keydown', handleSearchBind, true);
|
document.removeEventListener('keydown', handleSearchBind, true);
|
||||||
document.removeEventListener('keydown', handleMatchmakerBind, true);
|
|
||||||
if (popupElement.parentNode) popupElement.remove();
|
if (popupElement.parentNode) popupElement.remove();
|
||||||
popupElement.classList.remove('searching');
|
popupElement.classList.remove('searching');
|
||||||
}
|
}
|
||||||
|
|
||||||
function decideMatchmakerDecision(accept: boolean): void {
|
|
||||||
const w = window as any;
|
|
||||||
if (typeof w.playSelect === 'function') w.playSelect();
|
|
||||||
|
|
||||||
if (accept && popupGameID !== 'none') {
|
|
||||||
verifyAndJoin(popupGameID);
|
|
||||||
} else {
|
|
||||||
dismissPopup();
|
|
||||||
if (popupGameID === 'none' && openServerBrowser && typeof w.openServerWindow === 'function') {
|
|
||||||
w.openServerWindow(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSearchBind(event: KeyboardEvent): void {
|
function handleSearchBind(event: KeyboardEvent): void {
|
||||||
if (document.pointerLockElement) return;
|
if (document.pointerLockElement) return;
|
||||||
if (matchesKey(cancelKey, event)) {
|
if (matchesKey(cancelKey, event)) {
|
||||||
@@ -219,42 +191,6 @@ function handleSearchBind(event: KeyboardEvent): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMatchmakerBind(event: KeyboardEvent): void {
|
|
||||||
if (document.pointerLockElement) return;
|
|
||||||
const isAccept = matchesKey(confirmKey, event);
|
|
||||||
const isCancel = matchesKey(cancelKey, event);
|
|
||||||
if (isAccept || isCancel) {
|
|
||||||
document.removeEventListener('keydown', handleMatchmakerBind, true);
|
|
||||||
decideMatchmakerDecision(isAccept);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showResultPopup(game: MatchmakerGame): void {
|
|
||||||
popupElement.classList.remove('searching');
|
|
||||||
const mapIdx = MAP_ICON_INDICES.indexOf(game.map);
|
|
||||||
popupElement.style.backgroundImage = `url(https://assets.krunker.io/img/maps/map_${mapIdx >= 0 ? mapIdx : 0}.png)`;
|
|
||||||
|
|
||||||
popupGameID = game.gameID;
|
|
||||||
if (game.gameID === 'none') {
|
|
||||||
popupTitle.innerText = 'No Games Found...';
|
|
||||||
popupDescription.innerHTML = 'Check the server browser to see other lobbies.';
|
|
||||||
popupConfirmBtn.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
popupTitle.innerText = 'Game Found!';
|
|
||||||
const regionName = MATCHMAKER_REGION_NAMES[game.region] ?? 'Unknown Region';
|
|
||||||
popupDescription.innerHTML = `${escapeHtml(game.gamemode)} on ${escapeHtml(game.map)} (${escapeHtml(regionName)})<br/>${game.playerCount}/${game.playerLimit} Players, ${secondsToTimestring(game.remainingTime)} Left`;
|
|
||||||
popupConfirmBtn.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-trigger slide animation
|
|
||||||
popupElement.style.animation = 'none';
|
|
||||||
void popupElement.offsetWidth;
|
|
||||||
popupElement.style.animation = '';
|
|
||||||
|
|
||||||
document.removeEventListener('keydown', handleSearchBind, true);
|
|
||||||
document.addEventListener('keydown', handleMatchmakerBind, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSearchPopup(): void {
|
function showSearchPopup(): void {
|
||||||
searchAborted = false;
|
searchAborted = false;
|
||||||
popupElement.classList.add('searching');
|
popupElement.classList.add('searching');
|
||||||
@@ -263,7 +199,7 @@ function showSearchPopup(): void {
|
|||||||
searchFeed.innerHTML = '';
|
searchFeed.innerHTML = '';
|
||||||
searchCounter.textContent = '';
|
searchCounter.textContent = '';
|
||||||
|
|
||||||
document.removeEventListener('keydown', handleMatchmakerBind, true);
|
|
||||||
document.addEventListener('keydown', handleSearchBind, true);
|
document.addEventListener('keydown', handleSearchBind, true);
|
||||||
|
|
||||||
const uiBase = document.getElementById('uiBase');
|
const uiBase = document.getElementById('uiBase');
|
||||||
@@ -358,8 +294,12 @@ async function fetchAllGames(mmConfig: MatchmakerConfig): Promise<{ all: RawLobb
|
|||||||
return { all, filtered };
|
return { all, filtered };
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortByPingThenPlayers(games: MatchmakerGame[], pings: Record<string, number>): MatchmakerGame[] {
|
function sortGames(games: MatchmakerGame[], pings: Record<string, number>, sortByPlayers: boolean): MatchmakerGame[] {
|
||||||
return games.sort((a, b) => {
|
return games.sort((a, b) => {
|
||||||
|
if (sortByPlayers) {
|
||||||
|
if (a.playerCount !== b.playerCount) return b.playerCount - a.playerCount;
|
||||||
|
return (pings[a.region] ?? 999) - (pings[b.region] ?? 999);
|
||||||
|
}
|
||||||
const pingA = pings[a.region] ?? 999;
|
const pingA = pings[a.region] ?? 999;
|
||||||
const pingB = pings[b.region] ?? 999;
|
const pingB = pings[b.region] ?? 999;
|
||||||
if (pingA !== pingB) return pingA - pingB;
|
if (pingA !== pingB) return pingA - pingB;
|
||||||
@@ -369,7 +309,6 @@ function sortByPingThenPlayers(games: MatchmakerGame[], pings: Record<string, nu
|
|||||||
|
|
||||||
export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole): Promise<void> {
|
export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole): Promise<void> {
|
||||||
openServerBrowser = mmConfig.openServerBrowser;
|
openServerBrowser = mmConfig.openServerBrowser;
|
||||||
confirmKey = mmConfig.acceptKey;
|
|
||||||
cancelKey = mmConfig.cancelKey;
|
cancelKey = mmConfig.cancelKey;
|
||||||
|
|
||||||
// Dismiss existing popup if active (also aborts in-flight search)
|
// Dismiss existing popup if active (also aborts in-flight search)
|
||||||
@@ -406,7 +345,7 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
|
|||||||
_con?.log('[KCC-MM]', filtered.length, '/', allLobbies.length, 'games passed filters');
|
_con?.log('[KCC-MM]', filtered.length, '/', allLobbies.length, 'games passed filters');
|
||||||
|
|
||||||
// Sort immediately — result is ready
|
// Sort immediately — result is ready
|
||||||
if (filtered.length > 0) sortByPingThenPlayers(filtered, pings);
|
if (filtered.length > 0) sortGames(filtered, pings, mmConfig.sortByPlayers);
|
||||||
popupCandidates = filtered;
|
popupCandidates = filtered;
|
||||||
|
|
||||||
// Fire animation in background (non-blocking eye candy)
|
// Fire animation in background (non-blocking eye candy)
|
||||||
@@ -429,8 +368,7 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
|
|||||||
const best = pool[Math.floor(Math.random() * pool.length)];
|
const best = pool[Math.floor(Math.random() * pool.length)];
|
||||||
_con?.log('[KCC-MM] Best match:', best.gameID, best.region, best.map, `(${pings[best.region] ?? '?'}ms, pool: ${pool.length})`);
|
_con?.log('[KCC-MM] Best match:', best.gameID, best.region, best.map, `(${pings[best.region] ?? '?'}ms, pool: ${pool.length})`);
|
||||||
|
|
||||||
if (mmConfig.autoJoin) {
|
// Brief "Lobby Found!" flash before auto-joining
|
||||||
// Brief "Lobby Found!" flash before joining
|
|
||||||
const regionName = MATCHMAKER_REGION_NAMES[best.region] ?? best.region;
|
const regionName = MATCHMAKER_REGION_NAMES[best.region] ?? best.region;
|
||||||
searchStatus.textContent = 'Lobby Found!';
|
searchStatus.textContent = 'Lobby Found!';
|
||||||
searchFeed.innerHTML = '';
|
searchFeed.innerHTML = '';
|
||||||
@@ -445,29 +383,12 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
|
|||||||
searchCounter.textContent = `${best.gamemode} \u00B7 ${regionName} \u00B7 ${pings[best.region] ?? '?'}ms`;
|
searchCounter.textContent = `${best.gamemode} \u00B7 ${regionName} \u00B7 ${pings[best.region] ?? '?'}ms`;
|
||||||
await new Promise(r => setTimeout(r, 1200));
|
await new Promise(r => setTimeout(r, 1200));
|
||||||
await verifyAndJoin(best.gameID);
|
await verifyAndJoin(best.gameID);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showResultPopup(best);
|
|
||||||
} else {
|
} else {
|
||||||
_con?.log('[KCC-MM] No matching games found');
|
_con?.log('[KCC-MM] No matching games found');
|
||||||
|
|
||||||
if (mmConfig.autoJoin) {
|
|
||||||
dismissPopup();
|
dismissPopup();
|
||||||
if (openServerBrowser && typeof (window as any).openServerWindow === 'function') {
|
if (openServerBrowser && typeof (window as any).openServerWindow === 'function') {
|
||||||
(window as any).openServerWindow(0);
|
(window as any).openServerWindow(0);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showResultPopup({
|
|
||||||
gameID: 'none',
|
|
||||||
region: 'none',
|
|
||||||
playerCount: 0,
|
|
||||||
playerLimit: 0,
|
|
||||||
map: MAP_ICON_INDICES[0],
|
|
||||||
gamemode: MATCHMAKER_GAMEMODES[0],
|
|
||||||
remainingTime: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const FALSE_POSITIVE_LANGS = new Set([
|
|||||||
|
|
||||||
// ── Auto-suppression (repeated short phrases) ──
|
// ── Auto-suppression (repeated short phrases) ──
|
||||||
|
|
||||||
const suppressionCounts = new Map<string, number>();
|
let suppressionCounts = new Map<string, number>();
|
||||||
const SUPPRESS_THRESHOLD = 3;
|
const SUPPRESS_THRESHOLD = 3;
|
||||||
const MIN_LATIN_WORDS = 3;
|
const MIN_LATIN_WORDS = 3;
|
||||||
const SHORT_TEXT_THRESHOLD = 15;
|
const SHORT_TEXT_THRESHOLD = 15;
|
||||||
@@ -113,6 +113,7 @@ const SHORT_TEXT_THRESHOLD = 15;
|
|||||||
|
|
||||||
let activeRequests = 0;
|
let activeRequests = 0;
|
||||||
const MAX_CONCURRENT = 3;
|
const MAX_CONCURRENT = 3;
|
||||||
|
const MAX_QUEUE = 15;
|
||||||
const pendingQueue: Array<() => void> = [];
|
const pendingQueue: Array<() => void> = [];
|
||||||
|
|
||||||
function enqueue(fn: () => Promise<void>): void {
|
function enqueue(fn: () => Promise<void>): void {
|
||||||
@@ -123,10 +124,45 @@ function enqueue(fn: () => Promise<void>): void {
|
|||||||
if (pendingQueue.length > 0) pendingQueue.shift()!();
|
if (pendingQueue.length > 0) pendingQueue.shift()!();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// Drop oldest if queue is full — old messages have already scrolled off-screen
|
||||||
|
if (pendingQueue.length >= MAX_QUEUE) pendingQueue.shift();
|
||||||
pendingQueue.push(() => enqueue(fn));
|
pendingQueue.push(() => enqueue(fn));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Periodic cleanup (prevents unbounded memory growth in long sessions) ──
|
||||||
|
|
||||||
|
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
||||||
|
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
function startCleanup(): void {
|
||||||
|
if (cleanupTimer) return;
|
||||||
|
cleanupTimer = setInterval(() => {
|
||||||
|
// Reset suppression counts (re-learned naturally from fresh messages)
|
||||||
|
suppressionCounts = new Map();
|
||||||
|
|
||||||
|
// Prune expired sessionStorage cache entries
|
||||||
|
const now = Date.now();
|
||||||
|
const keysToRemove: string[] = [];
|
||||||
|
for (let i = 0; i < sessionStorage.length; i++) {
|
||||||
|
const key = sessionStorage.key(i);
|
||||||
|
if (!key?.startsWith(CACHE_KEY_PREFIX)) continue;
|
||||||
|
try {
|
||||||
|
const entry: CacheEntry = JSON.parse(sessionStorage.getItem(key) || '');
|
||||||
|
if (now - entry.ts > CACHE_EXPIRY_MS) keysToRemove.push(key);
|
||||||
|
} catch { keysToRemove.push(key); }
|
||||||
|
}
|
||||||
|
for (const key of keysToRemove) sessionStorage.removeItem(key);
|
||||||
|
}, CLEANUP_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopCleanup(): void {
|
||||||
|
if (cleanupTimer) {
|
||||||
|
clearInterval(cleanupTimer);
|
||||||
|
cleanupTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── System message patterns to skip ──
|
// ── System message patterns to skip ──
|
||||||
|
|
||||||
const SYSTEM_PATTERNS = [
|
const SYSTEM_PATTERNS = [
|
||||||
@@ -281,8 +317,10 @@ function processMessage(node: HTMLElement): void {
|
|||||||
|
|
||||||
const { message, username } = extracted;
|
const { message, username } = extracted;
|
||||||
enqueue(async () => {
|
enqueue(async () => {
|
||||||
|
// Node may have been removed by chat history trimming while queued
|
||||||
|
if (!node.isConnected) return;
|
||||||
const result = await translateText(message);
|
const result = await translateText(message);
|
||||||
if (result) appendTranslation(node, username, result.translation, result.srcLang);
|
if (result && node.isConnected) appendTranslation(node, username, result.translation, result.srcLang);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +354,7 @@ function startObserver(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
chatObserver.observe(chatList, { childList: true });
|
chatObserver.observe(chatList, { childList: true });
|
||||||
|
startCleanup();
|
||||||
_con.log('[KCC-TL] Chat observer active');
|
_con.log('[KCC-TL] Chat observer active');
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
@@ -329,6 +368,8 @@ function stopObserver(): void {
|
|||||||
chatObserver.disconnect();
|
chatObserver.disconnect();
|
||||||
chatObserver = null;
|
chatObserver = null;
|
||||||
}
|
}
|
||||||
|
stopCleanup();
|
||||||
|
pendingQueue.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public API ──
|
// ── Public API ──
|
||||||
|
|||||||
+4
-1
@@ -8,6 +8,9 @@ const nodeBuiltins = builtinModules.flatMap((m) => [m, `node:${m}`]);
|
|||||||
const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--mode');
|
const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--mode');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
__UPDATE_SOURCE__: JSON.stringify(process.env.UPDATE_SOURCE || 'gitea'),
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/main/index.ts'),
|
entry: resolve(__dirname, 'src/main/index.ts'),
|
||||||
@@ -17,7 +20,7 @@ export default defineConfig({
|
|||||||
outDir: 'dist/main',
|
outDir: 'dist/main',
|
||||||
emptyDirBefore: true,
|
emptyDirBefore: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: ['electron', 'electron-store', ...nodeBuiltins],
|
external: ['electron', 'electron-store', 'original-fs', ...nodeBuiltins],
|
||||||
},
|
},
|
||||||
target: 'node20',
|
target: 'node20',
|
||||||
minify: isProd,
|
minify: isProd,
|
||||||
|
|||||||
Reference in New Issue
Block a user