Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ad6a2aca3 | |||
| 36ac3dbd1d | |||
| 24d1ae71d9 | |||
| 9cef99f0ff | |||
| f409ffad12 | |||
| c1f7eaa153 | |||
| b455db0db8 | |||
| 1fb284cb5c | |||
| ebd8d81924 | |||
| 362084b829 | |||
| 656cf2d07d | |||
| bfe51f6ad4 | |||
| 2a3107a78d | |||
| 48826e21d6 | |||
| 15c476c12d | |||
| 6c4d7244c3 |
@@ -22,7 +22,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
electron-launcher:
|
||||
electron-launcher-windows:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
@@ -38,10 +38,6 @@ jobs:
|
||||
cache-dependency-path: tools/fractured-launcher-electron/package-lock.json
|
||||
|
||||
- name: Install and pack (NSIS + portable)
|
||||
env:
|
||||
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
|
||||
GITEA_OWNER: ${{ secrets.GITEA_OWNER }}
|
||||
GITEA_REPO: ${{ secrets.GITEA_REPO }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run pack:win
|
||||
@@ -54,3 +50,32 @@ jobs:
|
||||
tools/fractured-launcher-electron/dist/*.exe
|
||||
tools/fractured-launcher-electron/dist/latest.yml
|
||||
tools/fractured-launcher-electron/dist/*.blockmap
|
||||
|
||||
electron-launcher-linux:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
defaults:
|
||||
run:
|
||||
working-directory: tools/fractured-launcher-electron
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
cache-dependency-path: tools/fractured-launcher-electron/package-lock.json
|
||||
|
||||
- name: Install and pack (AppImage)
|
||||
run: |
|
||||
npm ci
|
||||
npm run pack:linux
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fractured-launcher-electron-linux-${{ github.run_id }}
|
||||
if-no-files-found: warn
|
||||
path: |
|
||||
tools/fractured-launcher-electron/dist/*.AppImage
|
||||
tools/fractured-launcher-electron/dist/*.yml
|
||||
tools/fractured-launcher-electron/dist/*.blockmap
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
# Release on github.com (Releases → Draft/new release → Publish). The workflow
|
||||
# definition must exist on the repo DEFAULT branch (GitHub runs it from there).
|
||||
#
|
||||
# Steps: build Electron from tag → download this repo’s release attachments → upload all to Gitea.
|
||||
# Steps: Windows (NSIS+portable) + Linux (AppImage) in parallel, launcher from DEFAULT BRANCH
|
||||
# overlay on tag checkout → merge with GitHub release assets → upload all to Gitea.
|
||||
#
|
||||
# Secrets: GITEA_BASE_URL, GITEA_TOKEN, GITEA_OWNER, GITEA_REPO
|
||||
# Optional variable: GITEA_TARGET_REF (see tools/fractured-launcher-electron/README.md)
|
||||
@@ -28,7 +29,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag on this GitHub repo (must exist; e.g. v1.0.0)'
|
||||
description: 'Git tag only (e.g. v0.7.11-paragon-foo). NOT the release title — open the release and copy the tag next to the title.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
@@ -50,11 +51,28 @@ jobs:
|
||||
id: t
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
|
||||
RAW="${{ github.event.inputs.tag }}"
|
||||
else
|
||||
echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
|
||||
RAW="${{ github.event.release.tag_name }}"
|
||||
fi
|
||||
TAG="$(printf '%s' "$RAW" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
if [ -z "$TAG" ]; then
|
||||
echo '::error::Tag input is empty. Paste the git tag (e.g. v0.7.11-…).'
|
||||
exit 1
|
||||
fi
|
||||
if printf '%s' "$TAG" | grep -q '[[:space:]]'; then
|
||||
echo '::error::Tag contains whitespace — that is usually the **release title**, not the tag. On GitHub → Releases → open the release → copy the **tag** (short ref like v0.7.11-…), not the long title line.'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
{
|
||||
echo "tag<<__TAG_EOF__"
|
||||
echo "$TAG"
|
||||
echo "__TAG_EOF__"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-electron:
|
||||
needs: meta
|
||||
@@ -66,6 +84,16 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.tag }}
|
||||
|
||||
# Release tags often point at server/game commits that predate launcher lib fixes.
|
||||
# Always pack the launcher from default branch so app.asar includes the full tree.
|
||||
- name: Overlay launcher from default branch
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB="${{ github.event.repository.default_branch }}"
|
||||
git fetch --no-tags --depth=1 origin "+refs/heads/${DB}:refs/remotes/origin/${DB}"
|
||||
git checkout "origin/${DB}" -- tools/fractured-launcher-electron
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
@@ -74,14 +102,8 @@ jobs:
|
||||
|
||||
- name: Install and pack (NSIS + portable)
|
||||
working-directory: tools/fractured-launcher-electron
|
||||
env:
|
||||
# Same values as upload step — baked into default-launcher.json (no token).
|
||||
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
|
||||
GITEA_OWNER: ${{ secrets.GITEA_OWNER }}
|
||||
GITEA_REPO: ${{ secrets.GITEA_REPO }}
|
||||
run: |
|
||||
npm ci
|
||||
# pack:win runs inject-release-channel.js then electron-builder --publish never
|
||||
npm run pack:win
|
||||
|
||||
- name: Stage launcher files for upload
|
||||
@@ -97,11 +119,61 @@ jobs:
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: electron-dist
|
||||
name: electron-dist-windows
|
||||
path: launcher-publish/
|
||||
|
||||
build-electron-linux:
|
||||
needs: meta
|
||||
if: github.repository == 'Dawnforger/Fractured'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.meta.outputs.tag }}
|
||||
|
||||
- name: Overlay launcher from default branch
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DB="${{ github.event.repository.default_branch }}"
|
||||
git fetch --no-tags --depth=1 origin "+refs/heads/${DB}:refs/remotes/origin/${DB}"
|
||||
git checkout "origin/${DB}" -- tools/fractured-launcher-electron
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: npm
|
||||
cache-dependency-path: tools/fractured-launcher-electron/package-lock.json
|
||||
|
||||
- name: Install and pack (AppImage)
|
||||
working-directory: tools/fractured-launcher-electron
|
||||
run: |
|
||||
npm ci
|
||||
npm run pack:linux
|
||||
|
||||
- name: Stage Linux launcher for upload
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p launcher-linux-publish
|
||||
shopt -s nullglob
|
||||
cp -f tools/fractured-launcher-electron/dist/*.AppImage launcher-linux-publish/ 2>/dev/null || true
|
||||
cp -f tools/fractured-launcher-electron/dist/*.yml launcher-linux-publish/ 2>/dev/null || true
|
||||
cp -f tools/fractured-launcher-electron/dist/*.blockmap launcher-linux-publish/ 2>/dev/null || true
|
||||
ls -la launcher-linux-publish/
|
||||
if ! compgen -G "launcher-linux-publish/*.AppImage" > /dev/null; then
|
||||
echo "No AppImage under dist/ — electron-builder linux target failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: electron-dist-linux
|
||||
path: launcher-linux-publish/
|
||||
|
||||
sync-gitea:
|
||||
needs: [meta, build-electron]
|
||||
needs: [meta, build-electron, build-electron-linux]
|
||||
if: github.repository == 'Dawnforger/Fractured'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -121,8 +193,13 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: electron-dist
|
||||
path: /tmp/electron
|
||||
name: electron-dist-windows
|
||||
path: /tmp/electron-win
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: electron-dist-linux
|
||||
path: /tmp/electron-linux
|
||||
|
||||
- name: Merge GitHub release assets + Electron build
|
||||
env:
|
||||
@@ -145,7 +222,7 @@ jobs:
|
||||
cat /tmp/dl.err || true
|
||||
fi
|
||||
shopt -s nullglob
|
||||
for f in /tmp/electron/*; do
|
||||
for f in /tmp/electron-win/* /tmp/electron-linux/*; do
|
||||
if [ -f "$f" ]; then
|
||||
cp -f "$f" combined/
|
||||
fi
|
||||
|
||||
@@ -21,6 +21,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(10, 1),
|
||||
(17, 1),
|
||||
(53, 1),
|
||||
(66, 1),
|
||||
(72, 1),
|
||||
(75, 1),
|
||||
(78, 1),
|
||||
@@ -30,6 +31,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(118, 1),
|
||||
(120, 1),
|
||||
(122, 1),
|
||||
(126, 1),
|
||||
(130, 1),
|
||||
(131, 1),
|
||||
(132, 1),
|
||||
@@ -52,6 +54,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(469, 1),
|
||||
(475, 1),
|
||||
(498, 1),
|
||||
(526, 1),
|
||||
(527, 1),
|
||||
(528, 1),
|
||||
(543, 1),
|
||||
@@ -73,22 +76,28 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(676, 1),
|
||||
(686, 1),
|
||||
(687, 1),
|
||||
(688, 1),
|
||||
(689, 1),
|
||||
(691, 1),
|
||||
(693, 1),
|
||||
(694, 1),
|
||||
(697, 1),
|
||||
(698, 1),
|
||||
(702, 1),
|
||||
(703, 1),
|
||||
(706, 1),
|
||||
(710, 1),
|
||||
(712, 1),
|
||||
(740, 1),
|
||||
(755, 1),
|
||||
(759, 1),
|
||||
(768, 1),
|
||||
(770, 1),
|
||||
(772, 1),
|
||||
(774, 1),
|
||||
(779, 1),
|
||||
(781, 1),
|
||||
(783, 1),
|
||||
(845, 1),
|
||||
(853, 1),
|
||||
(871, 1),
|
||||
@@ -104,6 +113,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(1038, 1),
|
||||
(1044, 1),
|
||||
(1064, 1),
|
||||
(1066, 1),
|
||||
(1079, 1),
|
||||
(1082, 1),
|
||||
(1098, 1),
|
||||
@@ -176,6 +186,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(2812, 1),
|
||||
(2825, 1),
|
||||
(2893, 1),
|
||||
(2894, 1),
|
||||
(2908, 1),
|
||||
(2912, 1),
|
||||
(2944, 1),
|
||||
@@ -194,6 +205,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(3565, 1),
|
||||
(3566, 1),
|
||||
(3567, 1),
|
||||
(3714, 1),
|
||||
(3738, 1),
|
||||
(4987, 1),
|
||||
(5116, 1),
|
||||
@@ -205,7 +217,9 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(5185, 1),
|
||||
(5209, 1),
|
||||
(5211, 1),
|
||||
(5215, 1),
|
||||
(5215, 1);
|
||||
|
||||
INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(5217, 1),
|
||||
(5221, 1),
|
||||
(5225, 1),
|
||||
@@ -215,11 +229,10 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(5308, 1),
|
||||
(5384, 1),
|
||||
(5484, 1),
|
||||
(5487, 1),
|
||||
(5500, 1),
|
||||
(5502, 1),
|
||||
(5504, 1);
|
||||
|
||||
INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(5504, 1),
|
||||
(5675, 1),
|
||||
(5676, 1),
|
||||
(5697, 1),
|
||||
@@ -244,6 +257,8 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(6770, 1),
|
||||
(6785, 1),
|
||||
(6789, 1),
|
||||
(6795, 1),
|
||||
(6807, 1),
|
||||
(6940, 1),
|
||||
(7294, 1),
|
||||
(7302, 1),
|
||||
@@ -283,6 +298,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(11418, 1),
|
||||
(11419, 1),
|
||||
(11420, 1),
|
||||
(12051, 1),
|
||||
(13159, 1),
|
||||
(13161, 1),
|
||||
(13163, 1),
|
||||
@@ -297,6 +313,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(16857, 1),
|
||||
(16914, 1),
|
||||
(18499, 1),
|
||||
(19263, 1),
|
||||
(19740, 1),
|
||||
(19742, 1),
|
||||
(19746, 1),
|
||||
@@ -323,7 +340,6 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(20252, 1),
|
||||
(20484, 1),
|
||||
(20736, 1),
|
||||
(21084, 1),
|
||||
(21562, 1),
|
||||
(21849, 1),
|
||||
(22568, 1),
|
||||
@@ -331,6 +347,8 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(22812, 1),
|
||||
(22842, 1),
|
||||
(23028, 1),
|
||||
(23161, 1),
|
||||
(23214, 1),
|
||||
(23920, 1),
|
||||
(23922, 1),
|
||||
(24275, 1),
|
||||
@@ -349,6 +367,7 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(29722, 1),
|
||||
(29858, 1),
|
||||
(29893, 1),
|
||||
(30449, 1),
|
||||
(30451, 1),
|
||||
(30455, 1),
|
||||
(30482, 1),
|
||||
@@ -372,12 +391,14 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(33745, 1),
|
||||
(33763, 1),
|
||||
(33786, 1),
|
||||
(33943, 1),
|
||||
(34026, 1),
|
||||
(34074, 1),
|
||||
(34428, 1),
|
||||
(34433, 1),
|
||||
(34477, 1),
|
||||
(34600, 1),
|
||||
(34767, 1),
|
||||
(35715, 1),
|
||||
(35717, 1),
|
||||
(36936, 1),
|
||||
@@ -398,7 +419,9 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(47541, 1),
|
||||
(47568, 1),
|
||||
(47897, 1),
|
||||
(48018, 1),
|
||||
(48018, 1);
|
||||
|
||||
INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(48020, 1),
|
||||
(48045, 1),
|
||||
(48263, 1),
|
||||
@@ -416,14 +439,19 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(49576, 1),
|
||||
(49998, 1),
|
||||
(50464, 1),
|
||||
(50769, 1),
|
||||
(50842, 1),
|
||||
(51505, 1),
|
||||
(51514, 1),
|
||||
(51722, 1),
|
||||
(51723, 1),
|
||||
(52610, 1);
|
||||
|
||||
INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(51730, 1),
|
||||
(52127, 1),
|
||||
(52610, 1),
|
||||
(53140, 1),
|
||||
(53142, 1),
|
||||
(53271, 1),
|
||||
(53351, 1),
|
||||
(53407, 1),
|
||||
(53408, 1),
|
||||
(53600, 1),
|
||||
@@ -432,15 +460,23 @@ INSERT INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(54428, 1),
|
||||
(55342, 1),
|
||||
(55694, 1),
|
||||
(56222, 1),
|
||||
(56641, 1),
|
||||
(56815, 1),
|
||||
(57330, 1),
|
||||
(57755, 1),
|
||||
(57934, 1),
|
||||
(57994, 1),
|
||||
(60192, 1),
|
||||
(61846, 1),
|
||||
(61999, 1),
|
||||
(62078, 1),
|
||||
(62124, 1),
|
||||
(62757, 1),
|
||||
(64382, 1),
|
||||
(64843, 1);
|
||||
(64843, 1),
|
||||
(64901, 1),
|
||||
(66842, 1),
|
||||
(66843, 1),
|
||||
(66844, 1);
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
-- mod-paragon: backfill paragon_spell_ae_cost rows for spells newly exposed
|
||||
-- by the Character Advancement panel after removing the over-aggressive
|
||||
-- ClassMask=0 filter from tools/_gen_paragon_advancement_spells_lua.py.
|
||||
--
|
||||
-- The base file (data/sql/db-world/base/paragon_spell_ae_cost.sql) was
|
||||
-- regenerated alongside this migration so fresh deployments already have
|
||||
-- these rows. Existing servers do not re-run base files on content change,
|
||||
-- so this update inserts the new (spell_id, ae_cost) pairs idempotently.
|
||||
-- INSERT IGNORE keeps any per-row tuning a server operator may have already
|
||||
-- applied to spell_ids that happen to overlap.
|
||||
--
|
||||
-- New ids include: 51505 Lava Burst (Shaman), 12051 Evocation / 1066 Aqueous
|
||||
-- Form / Hex / Mage Ward / Spellsteal (Mage), 53351 Kill Shot / 19263
|
||||
-- Deterrence / 53271 Master's Call (Hunter), 3714 Path of Frost / 57330
|
||||
-- Horn of Winter / 56815 Rune Strike / 61999 Raise Ally / 56222 Dark Command
|
||||
-- (DK), and 39 other trainer-taught class abilities whose stock
|
||||
-- SkillLineAbility.dbc rows have ClassMask=0 (the skill line itself pins the
|
||||
-- class for these rows; ClassMask is redundant on class-spec lines).
|
||||
|
||||
INSERT IGNORE INTO `paragon_spell_ae_cost` (`spell_id`, `ae_cost`) VALUES
|
||||
(66, 1), -- Invisibility (Mage)
|
||||
(126, 1), -- Eye of Kilrogg (Warlock)
|
||||
(526, 1), -- Cure Toxins (Shaman)
|
||||
(688, 1), -- Summon Imp (Warlock)
|
||||
(691, 1), -- Summon Felhunter (Warlock)
|
||||
(697, 1), -- Summon Voidwalker (Warlock)
|
||||
(712, 1), -- Summon Succubus (Warlock)
|
||||
(768, 1), -- Cat Form (Druid)
|
||||
(783, 1), -- Travel Form (Druid)
|
||||
(1066, 1), -- Aqueous Form (Mage)
|
||||
(2894, 1), -- Fire Resistance Totem (Shaman)
|
||||
(3714, 1), -- Path of Frost (DK)
|
||||
(5215, 1), -- Prowl (Druid)
|
||||
(5487, 1), -- Bear Form (Druid)
|
||||
(5504, 1), -- Conjure Refreshment (Mage)
|
||||
(6795, 1), -- Growl (Druid)
|
||||
(6807, 1), -- Maul (Druid)
|
||||
(12051, 1), -- Evocation (Mage)
|
||||
(19263, 1), -- Deterrence (Hunter)
|
||||
(23161, 1), -- Summon Dreadsteed (Warlock)
|
||||
(23214, 1), -- Summon Charger (Paladin)
|
||||
(30449, 1), -- Spellsteal (Mage)
|
||||
(33943, 1), -- Flight Form (Druid)
|
||||
(34767, 1), -- Summon Felguard (Warlock)
|
||||
(48018, 1), -- Demonic Circle: Summon (Warlock)
|
||||
(50769, 1), -- Revive (Druid)
|
||||
(51505, 1), -- Lava Burst (Shaman)
|
||||
(51514, 1), -- Hex (Shaman)
|
||||
(51730, 1), -- Earthliving Weapon (Shaman)
|
||||
(52127, 1), -- Water Shield (Shaman)
|
||||
(52610, 1), -- Savage Roar (Druid)
|
||||
(53271, 1), -- Master's Call (Hunter)
|
||||
(53351, 1), -- Kill Shot (Hunter)
|
||||
(56222, 1), -- Dark Command (DK)
|
||||
(56815, 1), -- Rune Strike (DK)
|
||||
(57330, 1), -- Horn of Winter (DK)
|
||||
(61999, 1), -- Raise Ally (DK)
|
||||
(64843, 1), -- Divine Hymn (Priest)
|
||||
(64901, 1), -- Hymn of Hope (Priest)
|
||||
(66842, 1), -- Call of the Elements (Shaman totem set)
|
||||
(66843, 1), -- Call of the Ancestors (Shaman totem set)
|
||||
(66844, 1); -- Call of the Spirits (Shaman totem set)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12017,6 +12017,28 @@ void Player::learnSkillRewardedSpells(uint32 skill_id, uint32 skill_value)
|
||||
uint32 raceMask = getRaceMask();
|
||||
uint32 classMask = getClassMask();
|
||||
|
||||
// Fractured / Paragon: the Character Advancement panel is the sole
|
||||
// authority over which class abilities a Paragon owns. The skill-line
|
||||
// cascade re-fires from _LoadSkills (every login), UpdateSkillsForLevel
|
||||
// (every level-up), UpdateSkillPro (every weapon-skill tick on a
|
||||
// training dummy), and SetSkill (first time a class skill is granted).
|
||||
// Each of those re-grants every SLA-tagged class ability on the
|
||||
// matching skill line — leaking Blood Presence / Death Coil / Death
|
||||
// Grip / etc. back into the spellbook within seconds even after the
|
||||
// player intentionally refunded them via the panel. Skip the cascade
|
||||
// for class-category skill lines on Paragon characters; mod-paragon
|
||||
// calls Player::learnSpell directly for the abilities the player
|
||||
// actually purchased, including their attached passives. Profession,
|
||||
// weapon, language, and racial skill cascades stay enabled so things
|
||||
// like recipe auto-learn, weapon proficiencies, and racial perks
|
||||
// still work.
|
||||
if (getClass() == CLASS_PARAGON)
|
||||
{
|
||||
if (SkillLineEntry const* sl = sSkillLineStore.LookupEntry(skill_id))
|
||||
if (sl->categoryId == SKILL_CATEGORY_CLASS)
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all abilities for this skill and sort by MinSkillLineRank (lowest to highest)
|
||||
auto abilities = GetSkillLineAbilitiesBySkillLine(skill_id);
|
||||
std::vector<SkillLineAbilityEntry const*> sortedAbilities(abilities.begin(), abilities.end());
|
||||
|
||||
@@ -5368,6 +5368,18 @@ void SpellMgr::LoadSpellInfoCorrections()
|
||||
LockEntry* key = const_cast<LockEntry*>(sLockStore.LookupEntry(36)); // 3366 Opening, allows to open without proper key
|
||||
key->Type[2] = LOCK_KEY_NONE;
|
||||
|
||||
// Fractured / Paragon: DK weapon-line "passives" Forceful Deflection and
|
||||
// Runic Focus ship in 3.3.5a Spell.dbc without SPELL_ATTR0_PASSIVE set.
|
||||
// SpellInfo::IsPassive() is therefore false, and mod-paragon's panel-learn
|
||||
// diff treats them as castable actives and revokes them — while true
|
||||
// actives (Blood Presence, Death Coil, Death Grip, ...) must stay
|
||||
// stripped. Mark these two passive in-memory so the panel policy matches
|
||||
// the spellbook UX for every class (stock DK benefits too).
|
||||
ApplySpellFix({ 49410, 61455 }, [](SpellInfo* spellInfo)
|
||||
{
|
||||
spellInfo->Attributes |= SPELL_ATTR0_PASSIVE;
|
||||
});
|
||||
|
||||
// Fractured: strip reagent requirements from every player-class spell at
|
||||
// load time. Filtered by SpellFamilyName != 0 so that profession spells
|
||||
// (cooking, alchemy, enchanting, blacksmithing, jewelcrafting, leatherworking,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Fractured Launcher (Electron)
|
||||
|
||||
Windows launcher with **no extra console window**, **native Browse folder** dialog, **Gitea or GitHub** release assets + GitHub repo file sync, **realmlist**, optional **auth**, **Play**, and **auto-update** (via `electron-updater`). This is the **only** supported client launcher in this repo.
|
||||
**Windows** and **Linux (AppImage)** launcher with **no extra console window**, **native Browse folder** dialog, **Gitea or GitHub** release assets + GitHub repo file sync, **realmlist**, optional **auth**, **Play**, and **auto-update** (via `electron-updater`). This is the **only** supported client launcher in this repo.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -14,7 +14,7 @@ npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
On first run, `launcher.json` is created next to the app (dev: in this folder).
|
||||
On first run, **`launcher.json`** is created: **dev** — next to the app in this folder; **Windows packaged** — beside the `.exe`; **Linux AppImage / macOS packaged** — under Electron **`app.getPath('userData')`** (typically under **`~/.config/`**, folder name from the app; AppImage mount is read-only so config cannot live beside the binary).
|
||||
|
||||
### Where patches download from
|
||||
|
||||
@@ -35,14 +35,25 @@ Produces under **`dist/`**:
|
||||
| `Fractured-Launcher-${version}-Setup.exe` (NSIS) | **Recommended for players** — supports seamless **auto-update** and restart. |
|
||||
| `Fractured-Launcher-${version}-Windows-Portable.exe` | No installer; players replace the file manually. Auto-update is **less reliable** than NSIS. |
|
||||
|
||||
### Baked Gitea channel (non-token)
|
||||
## Build Linux AppImage
|
||||
|
||||
**`npm run pack:win`** runs **`scripts/inject-release-channel.js`** first. It merges **`gitea.base_url`**, **`owner`**, **`repo`**, and optional **`release_tag`** into **`default-launcher.json`** for that build only (then **electron-builder** packs that file).
|
||||
```bash
|
||||
cd tools/fractured-launcher-electron
|
||||
npm install
|
||||
npm run pack:linux
|
||||
```
|
||||
|
||||
- **GitHub Actions** — **Sync release to Gitea** and **Fractured launcher CI** export **`GITEA_BASE_URL`**, **`GITEA_OWNER`**, **`GITEA_REPO`** (same names as your upload secrets) for the pack step, so installers match the repo you sync to. Nothing embeds **`GITEA_TOKEN`**.
|
||||
- **Local packs** — put the same values in **`fractured-release-channel.json`** (committed or personal copy) **or** export those env vars before **`npm run pack:win`**.
|
||||
Produces **`dist/Fractured-Launcher-${version}-Linux-x86_64.AppImage`**. Same **`lib/baked-gitea-channel.js`** and **`default-launcher.json`** as Windows; run on **Linux** (or use **Fractured launcher CI** / **Sync release to Gitea**, which upload this file to Gitea with the Windows installers).
|
||||
|
||||
First launch still copies **`default-launcher.json`** → **`launcher.json`** beside the exe, so players get the baked **`gitea.*`** without editing unless they override.
|
||||
**Quick local test (avoids tag snapshot / CI):**
|
||||
- **Linux:** from repo root, **`bash tools/fractured-launcher-electron/scripts/manual-pack-linux.sh`** → **`dist/*.AppImage`**.
|
||||
- **Windows:** on a Windows machine, **`cd tools/fractured-launcher-electron`**, **`npm ci`**, **`npm run pack:win`** → **`dist/*.exe`**.
|
||||
|
||||
### Hardcoded Gitea channel (non-token)
|
||||
|
||||
**`lib/baked-gitea-channel.js`** exports **`base_url`**, **`owner`**, **`repo`**, **`release_tag`**. Set those strings once in the repo (same values you use for CI upload — not secret). At runtime **`config-store`** merges them into **`gitea.*`** so **`launcher.json`** does not need those fields; **`GITEA_TOKEN`** (or **`gitea.token_env`**) is still only for **private** Gitea. Leave a field **`''`** in the baked file to fall back to **`default-launcher.json`** / user **`launcher.json`** for that key.
|
||||
|
||||
**`npm run pack:win`** is plain **electron-builder** — no inject step, no extra JSON beside the app.
|
||||
|
||||
## Auto-update behaviour
|
||||
|
||||
@@ -67,15 +78,15 @@ First launch still copies **`default-launcher.json`** → **`launcher.json`** be
|
||||
### Publishing a new launcher version
|
||||
|
||||
1. Bump **`version`** in `package.json` on `main` (or your release branch) and merge.
|
||||
2. Create a **GitHub release** (tag + attach patches / `Wow.exe` if needed) and click **Publish** — **Sync release to Gitea** builds Windows installers and mirrors everything to Gitea.
|
||||
3. Local check: `npm run pack:win` then **`scripts/upload-release-to-gitea.sh`** with the same **`GITEA_*`** env vars as CI if you need a manual upload.
|
||||
2. Create a **GitHub release** (tag + attach patches / `Wow.exe` if needed) and click **Publish** — **Sync release to Gitea** builds **Windows + Linux** launcher artifacts and mirrors everything to Gitea.
|
||||
3. Local check: **`npm run pack:win`** (on Windows) or **`npm run pack:linux`** / **`scripts/manual-pack-linux.sh`**, then **`scripts/upload-release-to-gitea.sh`** with the same **`GITEA_*`** env vars as CI if you need a manual upload.
|
||||
|
||||
## Sync to Gitea (patches + launcher binaries)
|
||||
|
||||
CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml`) runs on **every published GitHub release** on this repo:
|
||||
|
||||
1. Triggers on **release published** on **`Dawnforger/Fractured`** (or **workflow_dispatch** with a tag).
|
||||
2. Builds the **Electron** app from that tag on Windows.
|
||||
2. Builds **Windows** (NSIS + portable) and **Linux** (AppImage) in parallel, each using **`tools/fractured-launcher-electron` from the default branch** (overlaid onto the tag checkout), so older release tags never ship a launcher missing new **`lib/*.js`** files.
|
||||
3. Downloads **all assets** attached to that **GitHub** release (MPQs, patched `Wow.exe`, etc.).
|
||||
4. Merges with the built launcher artifacts and uploads everything to a **Gitea release** with the **same tag** (existing attachments on that Gitea release are replaced).
|
||||
|
||||
@@ -107,12 +118,15 @@ CI workflow **Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml
|
||||
### Sync did not run / Gitea unchanged — checklist
|
||||
|
||||
1. **Git tag ≠ GitHub Release** — Only **Releases** (published on the GitHub **Releases** page) trigger this workflow. If your teammate only **`git push --tags`**, create a **Release** from that tag and click **Publish** (or run **Actions → Sync release to Gitea → Run workflow** and enter the tag).
|
||||
2. **Draft release** — Must click **Publish release**; drafts do not mirror.
|
||||
3. **Workflow on default branch** — GitHub runs `release` workflows from the **default branch** (e.g. `main`). Ensure `.github/workflows/gitea-release-sync.yml` is merged there.
|
||||
4. **Repo name guard** — Jobs use `if: github.repository == 'Dawnforger/Fractured'`. Forks or renames must change that line or runs are skipped.
|
||||
5. **Secrets** — **`GITEA_BASE_URL`**, **`GITEA_TOKEN`**, **`GITEA_OWNER`**, **`GITEA_REPO`** must be set under **Settings → Secrets and variables → Actions**. A failed “Upload to Gitea” step usually prints which is missing.
|
||||
6. **Actions tab** — Open the latest **Sync release to Gitea** run; a red **build-electron** (old tag without `package-lock.json`, etc.) or **Upload to Gitea** step shows the real error.
|
||||
7. **HTTP 422 `repo is empty`** — The Gitea repo has **no commits** yet. Push any initial commit (e.g. **Add README** in the Gitea web UI, or `git push` to **`main`**). Optionally set **`GITEA_TARGET_REF`** to match your real default branch if it is not **`main`**. From this repo you can run **`scripts/bootstrap-gitea-repo.sh`** (see script header for `GITEA_*` env or pass the HTTPS/SSH clone URL as the first argument).
|
||||
2. **Manual run: tag vs title** — **Run workflow** must receive the **git tag** (e.g. `v0.7.11-paragon-…`), copied from the release page’s tag badge. Pasting the **release title** (long line with spaces/parentheses) breaks `git fetch` with `invalid refspec`.
|
||||
3. **Draft release** — Must click **Publish release**; drafts do not mirror.
|
||||
4. **Workflow on default branch** — GitHub runs `release` workflows from the **default branch** (e.g. `main`). Ensure `.github/workflows/gitea-release-sync.yml` is merged there.
|
||||
5. **Repo name guard** — Jobs use `if: github.repository == 'Dawnforger/Fractured'`. Forks or renames must change that line or runs are skipped.
|
||||
6. **Secrets** — **`GITEA_BASE_URL`**, **`GITEA_TOKEN`**, **`GITEA_OWNER`**, **`GITEA_REPO`** must be set under **Settings → Secrets and variables → Actions**. A failed “Upload to Gitea” step usually prints which is missing.
|
||||
7. **Actions tab** — Open the latest **Sync release to Gitea** run; a red **build-electron** (old tag without `package-lock.json`, etc.) or **Upload to Gitea** step shows the real error.
|
||||
8. **HTTP 422 `repo is empty`** — The Gitea repo has **no commits** yet. Push any initial commit (e.g. **Add README** in the Gitea web UI, or `git push` to **`main`**). Optionally set **`GITEA_TARGET_REF`** to match your real default branch if it is not **`main`**. From this repo you can run **`scripts/bootstrap-gitea-repo.sh`** (see script header for `GITEA_*` env or pass the HTTPS/SSH clone URL as the first argument).
|
||||
9. **`sync Wow.exe: fetch failed`** — Often **HTTPS/TLS** to Gitea; use **`http://…`** in **`lib/baked-gitea-channel.js`** if you only serve plain HTTP, or fix certs / **`NODE_EXTRA_CA_CERTS`**. Ensure **`Wow-patched.exe`** exists on the release (**`release_tag`**: `latest` vs pinned). Errors include the failing URL when possible.
|
||||
10. **Wine + Windows portable** — If the folder picker returns **`/home/...`**, the launcher maps it to **`Z:\home\...`** (Wine’s Unix root). **`Wow.exe`** is matched case-insensitively for Linux-backed folders. Re-save the WoW folder after upgrading if validation still fails.
|
||||
|
||||
### Private Gitea token for players
|
||||
|
||||
@@ -122,7 +136,7 @@ Do **not** embed a shared admin PAT in a shipped `launcher.json`. Prefer read-on
|
||||
|
||||
## Patch versions (same filenames, different bytes)
|
||||
|
||||
The launcher does **not** read Git commits. For **turn-key** updates when asset names stay fixed (`patch-Z.MPQ`, `Wow-patched.exe`, …):
|
||||
The launcher does **not** read Git commits. For **turn-key** updates when asset names stay fixed (e.g. **`Wow-patched.exe`** — add more **`files`** entries for any extra MPQs you ship):
|
||||
|
||||
1. Ship **`patch-manifest.json`** next to those files on **every** release (Gitea/GitHub attachment). It lists a **`version`** label (any string you bump per release, e.g. `v0.9.0-client`) and a **`sha256`** per **`files[].source`** name.
|
||||
2. With **`patch_manifest.enabled`**: true (default in **`default-launcher.json`**), **Download updates** first fetches the manifest from the same release channel. If the files already on disk match those checksums, the player sees **“already match build … (nothing to download)”** — no redundant downloads.
|
||||
@@ -134,20 +148,20 @@ If **`patch-manifest.json`** is missing on a release, the launcher falls back to
|
||||
|
||||
```bash
|
||||
cd /path/to/staging
|
||||
node tools/fractured-launcher-electron/scripts/generate-patch-manifest.js v0.9.0-client patch-Z.MPQ Wow-patched.exe > patch-manifest.json
|
||||
node tools/fractured-launcher-electron/scripts/generate-patch-manifest.js v0.9.0-client Wow-patched.exe > patch-manifest.json
|
||||
```
|
||||
|
||||
Attach **`patch-manifest.json`** together with the MPQ/exe to the GitHub release (CI sync copies it to Gitea with everything else).
|
||||
|
||||
## CI
|
||||
|
||||
Workflow **Fractured launcher CI** (`.github/workflows/fractured-launcher-ci.yml`) runs on pushes/PRs under `tools/fractured-launcher-electron/`: Windows pack uses **`electron-builder … --publish never`** (not `npm run pack:win`, so tagged checkouts never require `GH_TOKEN`). **Actions → Fractured launcher CI → Run workflow** runs it manually.
|
||||
Workflow **Fractured launcher CI** (`.github/workflows/fractured-launcher-ci.yml`) runs on pushes/PRs under `tools/fractured-launcher-electron/`: **Windows** (`npm run pack:win`) and **Linux** (`npm run pack:linux`) jobs, each **`electron-builder … --publish never`**. **Actions → Fractured launcher CI → Run workflow** runs it manually.
|
||||
|
||||
**Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml`) uses the same pack command. If you see `GH_TOKEN` / `GitHubPublisher` errors in logs, the job is almost certainly an old **Re-run failed jobs** — open **Actions → Sync release to Gitea → Run workflow**, enter the tag, and start a **new** run instead.
|
||||
**Sync release to Gitea** (`.github/workflows/gitea-release-sync.yml`) uses the same pack commands. If you see `GH_TOKEN` / `GitHubPublisher` errors in logs, the job is almost certainly an old **Re-run failed jobs** — open **Actions → Sync release to Gitea → Run workflow**, enter the tag, and start a **new** run instead.
|
||||
|
||||
## Config
|
||||
|
||||
Schema is defined by **`default-launcher.json`** (shipped in the app; first run copies to `launcher.json` beside the executable):
|
||||
Schema is defined by **`default-launcher.json`** (shipped in the app; first run copies to **`launcher.json`** — beside the **Windows** exe, or under **`userData`** on **Linux/macOS** packaged builds):
|
||||
|
||||
- **`game_dir`**: WoW 3.3.5a root (contains `Wow.exe`).
|
||||
- **`update_feed_url`**: optional generic HTTPS base for launcher auto-update.
|
||||
@@ -155,4 +169,5 @@ Schema is defined by **`default-launcher.json`** (shipped in the app; first run
|
||||
- **`gitea`**: **`base_url`**, **`owner`**, **`repo`**, **`release_tag`**, **`token_env`** — when **`base_url`** is set (and owner/repo set), **`from_release`** downloads and (with token if needed) the **generic** updater feed use **Gitea**. **Required** for players if your CI mirrors patches/launchers to Gitea only.
|
||||
- **`github`**: used for **non-release** repo paths (`from_release`: false) and for **GitHub** **`from_release`** when **`gitea.base_url`** is empty.
|
||||
- **`patch_manifest`**: **`enabled`**, **`source`** (default `patch-manifest.json`), **`from_release`** — checksum-based skip + verify (see above).
|
||||
- **`files`**, **`realmlist`**, **`auth`**, **`launch`**.
|
||||
- **`files`**: default **`[]`**. **Download updates** resolves what to pull in order: (**1**) non-empty **`files`** if you set explicit **`source`** → **`dest`** pairs; (**2**) else each key in **`patch-manifest.json`** on the release (recommended); (**3**) else release attachments except launcher artifacts (`Fractured-Launcher*`, `*.blockmap`, `latest*.yml`, `.AppImage`, `patch-manifest.json`): **`.MPQ`** → **`Data/enUS/<name>.MPQ`** (extension forced to **`.MPQ`** caps for client compatibility), one **`.exe`** → **`launch.exe`**. Multiple `.exe` attachments require a manifest. Legacy **`Wow-patched.exe`** entries are removed when merging **`launcher.json`**.
|
||||
- **`realmlist`**, **`auth`**, **`launch`**.
|
||||
|
||||
@@ -21,20 +21,7 @@
|
||||
"source": "patch-manifest.json",
|
||||
"from_release": true
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"source": "patch-Z.MPQ",
|
||||
"dest": "Data/patch-Z.MPQ",
|
||||
"backup": true,
|
||||
"from_release": true
|
||||
},
|
||||
{
|
||||
"source": "Wow-patched.exe",
|
||||
"dest": "Wow.exe",
|
||||
"backup": true,
|
||||
"from_release": true
|
||||
}
|
||||
],
|
||||
"files": [],
|
||||
"realmlist": {
|
||||
"enabled": true,
|
||||
"line": "set realmlist fracturedwow.ddns.net:47497",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"gitea": {
|
||||
"base_url": "",
|
||||
"owner": "",
|
||||
"repo": "",
|
||||
"release_tag": "latest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Production Gitea mirror (non-secret). Edit here and ship — no inject script,
|
||||
* no fractured-release-channel.json, no CI env needed for these fields.
|
||||
* Token stays in env: GITEA_TOKEN or launcher.json → gitea.token_env.
|
||||
*/
|
||||
module.exports = {
|
||||
// http:// kept as-is; bare host gets https in gitea-release.js
|
||||
base_url: 'http://brassnet.ddns.net:33983',
|
||||
owner: 'Dawnsorrow',
|
||||
repo: 'Fractured-Distro',
|
||||
release_tag: 'latest',
|
||||
};
|
||||
@@ -2,6 +2,26 @@
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { normalizeWinGameDir } = require('./win-game-dir');
|
||||
|
||||
/** Sources no longer shipped; drop from merged files so old launcher.json does not keep fetching them. */
|
||||
const DEPRECATED_FILE_SOURCES = new Set(['patch-Z.MPQ', 'Wow-patched.exe']);
|
||||
|
||||
function mergeFilesList(defaults, user) {
|
||||
const dep = (e) => DEPRECATED_FILE_SOURCES.has(String(e && e.source ? e.source : '').trim());
|
||||
if (Array.isArray(user.files) && user.files.length) {
|
||||
const filtered = user.files.map((e) => ({ ...e })).filter((e) => !dep(e));
|
||||
if (filtered.length) return filtered;
|
||||
}
|
||||
const defList = Array.isArray(defaults.files) ? defaults.files : [];
|
||||
return defList.map((e) => ({ ...e })).filter((e) => !dep(e));
|
||||
}
|
||||
|
||||
function userFilesContainDeprecated(user) {
|
||||
const files = user && user.files;
|
||||
if (!Array.isArray(files)) return false;
|
||||
return files.some((e) => DEPRECATED_FILE_SOURCES.has(String(e && e.source ? e.source : '').trim()));
|
||||
}
|
||||
|
||||
function mergeConfig(defaults, user) {
|
||||
return {
|
||||
@@ -11,17 +31,44 @@ function mergeConfig(defaults, user) {
|
||||
user.update_feed_url != null && user.update_feed_url !== ''
|
||||
? user.update_feed_url
|
||||
: defaults.update_feed_url,
|
||||
launcher_updates_from_github:
|
||||
user.launcher_updates_from_github != null
|
||||
? user.launcher_updates_from_github
|
||||
: defaults.launcher_updates_from_github,
|
||||
github: { ...defaults.github, ...(user.github || {}) },
|
||||
gitea: { ...defaults.gitea, ...(user.gitea || {}) },
|
||||
patch_manifest: { ...defaults.patch_manifest, ...(user.patch_manifest || {}) },
|
||||
launch: { ...defaults.launch, ...(user.launch || {}) },
|
||||
auth: user.auth != null ? { ...defaults.auth, ...user.auth } : defaults.auth,
|
||||
realmlist: user.realmlist != null ? { ...defaults.realmlist, ...user.realmlist } : defaults.realmlist,
|
||||
files: Array.isArray(user.files) && user.files.length ? user.files : defaults.files,
|
||||
files: mergeFilesList(defaults, user),
|
||||
};
|
||||
}
|
||||
|
||||
/** Hardcoded Gitea host/repo (see lib/baked-gitea-channel.js). Non-empty baked values win. */
|
||||
function applyBakedGitea(cfg) {
|
||||
let baked;
|
||||
try {
|
||||
baked = require('./baked-gitea-channel');
|
||||
} catch {
|
||||
return cfg;
|
||||
}
|
||||
if (!baked || typeof baked !== 'object') return cfg;
|
||||
cfg.gitea = { ...(cfg.gitea || {}) };
|
||||
for (const k of ['base_url', 'owner', 'repo', 'release_tag']) {
|
||||
const v = baked[k];
|
||||
if (v != null && String(v).trim() !== '') cfg.gitea[k] = String(v).trim();
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
function getConfigPath(app) {
|
||||
if (process.env.FRACTURED_LAUNCHER_CONFIG) return process.env.FRACTURED_LAUNCHER_CONFIG;
|
||||
if (app && app.isPackaged) {
|
||||
// AppImage (and macOS .app) run from a read-only mount — cannot write beside execPath.
|
||||
if (process.platform === 'linux' || process.platform === 'darwin') {
|
||||
return path.join(app.getPath('userData'), 'launcher.json');
|
||||
}
|
||||
return path.join(path.dirname(process.execPath), 'launcher.json');
|
||||
}
|
||||
return path.join(__dirname, '..', 'launcher.json');
|
||||
@@ -33,11 +80,16 @@ async function loadConfig(app) {
|
||||
const defaults = JSON.parse(await fs.readFile(defPath, 'utf8'));
|
||||
try {
|
||||
const user = JSON.parse(await fs.readFile(p, 'utf8'));
|
||||
return { configPath: p, config: mergeConfig(defaults, user) };
|
||||
const config = applyBakedGitea(mergeConfig(defaults, user));
|
||||
if (userFilesContainDeprecated(user)) {
|
||||
await fs.writeFile(p, JSON.stringify(config, null, 2), 'utf8');
|
||||
}
|
||||
return { configPath: p, config };
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
await fs.writeFile(p, JSON.stringify(defaults, null, 2), 'utf8');
|
||||
return { configPath: p, config: JSON.parse(JSON.stringify(defaults)) };
|
||||
const initial = applyBakedGitea(mergeConfig(defaults, {}));
|
||||
await fs.writeFile(p, JSON.stringify(initial, null, 2), 'utf8');
|
||||
return { configPath: p, config: JSON.parse(JSON.stringify(initial)) };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
@@ -48,7 +100,7 @@ async function saveGameDir(configPath, gameDir) {
|
||||
const defaults = JSON.parse(await fs.readFile(defPath, 'utf8'));
|
||||
const user = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
||||
user.game_dir = gameDir;
|
||||
const merged = mergeConfig(defaults, user);
|
||||
const merged = applyBakedGitea(mergeConfig(defaults, user));
|
||||
await fs.writeFile(configPath, JSON.stringify(merged, null, 2), 'utf8');
|
||||
return merged;
|
||||
}
|
||||
@@ -56,8 +108,9 @@ async function saveGameDir(configPath, gameDir) {
|
||||
function resolveGameDir(cfg, configPath) {
|
||||
const gd = cfg.game_dir;
|
||||
if (!gd) return '';
|
||||
if (path.isAbsolute(gd)) return path.normalize(gd);
|
||||
return path.normalize(path.join(path.dirname(configPath), gd));
|
||||
const abs = path.isAbsolute(gd) ? path.normalize(gd) : path.normalize(path.join(path.dirname(configPath), gd));
|
||||
if (process.platform === 'win32') return normalizeWinGameDir(abs);
|
||||
return abs;
|
||||
}
|
||||
|
||||
module.exports = { getConfigPath, loadConfig, saveGameDir, resolveGameDir, mergeConfig };
|
||||
module.exports = { getConfigPath, loadConfig, saveGameDir, resolveGameDir, mergeConfig, applyBakedGitea };
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
const { downloadBodyToFile, fetchOrThrow } = require('./http-download');
|
||||
|
||||
function normalizeGiteaBaseUrl(raw) {
|
||||
let b = String(raw || '').trim().replace(/\/+$/, '');
|
||||
if (!b) return '';
|
||||
if (!/^https?:\/\//i.test(b)) b = `https://${b}`;
|
||||
return b;
|
||||
}
|
||||
|
||||
function giteaApiBase(cfg) {
|
||||
const base = normalizeGiteaBaseUrl(cfg.gitea.base_url);
|
||||
return `${base}/api/v1`;
|
||||
}
|
||||
|
||||
function giteaToken(cfg) {
|
||||
const name = cfg.gitea && cfg.gitea.token_env;
|
||||
if (name && process.env[name]) return String(process.env[name]).trim();
|
||||
return String(process.env.GITEA_TOKEN || '').trim();
|
||||
}
|
||||
|
||||
function giteaHeaders(token, json = false) {
|
||||
const h = { 'User-Agent': 'Fractured-Launcher-Electron' };
|
||||
if (json) h.Accept = 'application/json';
|
||||
if (token) h.Authorization = `token ${token}`;
|
||||
return h;
|
||||
}
|
||||
|
||||
function useGiteaReleases(cfg) {
|
||||
const g = cfg.gitea;
|
||||
if (!g) return false;
|
||||
return !!(String(g.base_url || '').trim() && String(g.owner || '').trim() && String(g.repo || '').trim());
|
||||
}
|
||||
|
||||
async function fetchGiteaReleaseRecord(cfg) {
|
||||
const api = giteaApiBase(cfg);
|
||||
const { owner, repo } = cfg.gitea;
|
||||
const tag = (cfg.gitea.release_tag || 'latest').trim() || 'latest';
|
||||
const token = giteaToken(cfg);
|
||||
|
||||
let listUrl;
|
||||
if (tag.toLowerCase() === 'latest') {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/latest`;
|
||||
} else {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
|
||||
const res = await fetchOrThrow(listUrl, { headers: giteaHeaders(token, true) });
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
let hint = '';
|
||||
if (res.status === 404) hint = ' (wrong tag / no release / check base_url owner repo)';
|
||||
if (res.status === 401 || res.status === 403) hint = ' (set GITEA_TOKEN or gitea.token_env)';
|
||||
throw new Error(`Gitea release ${res.status}${hint}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
async function listGiteaReleaseAttachmentNames(cfg) {
|
||||
const rel = await fetchGiteaReleaseRecord(cfg);
|
||||
const list = rel.attachments || rel.assets || [];
|
||||
return list.map((x) => x.name).filter(Boolean);
|
||||
}
|
||||
|
||||
async function downloadGiteaReleaseAsset(cfg, assetName, destPath) {
|
||||
const token = giteaToken(cfg);
|
||||
const rel = await fetchGiteaReleaseRecord(cfg);
|
||||
const list = rel.attachments || rel.assets || [];
|
||||
let downloadUrl = '';
|
||||
for (const a of list) {
|
||||
if (a.name !== assetName) continue;
|
||||
downloadUrl = a.browser_download_url || a.download_url || '';
|
||||
break;
|
||||
}
|
||||
if (!downloadUrl) {
|
||||
const names = list.map((x) => x.name).filter(Boolean);
|
||||
throw new Error(`Gitea release asset "${assetName}" not found; attachments: ${names.join(', ') || '(none)'}`);
|
||||
}
|
||||
|
||||
const h = { Accept: 'application/octet-stream' };
|
||||
if (token) h.Authorization = `token ${token}`;
|
||||
const dl = await fetchOrThrow(downloadUrl, { headers: h, redirect: 'follow' });
|
||||
await downloadBodyToFile(dl, destPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base URL for electron-updater generic provider (expects latest.yml under this path).
|
||||
* Matches Gitea’s pattern: …/owner/repo/releases/download/{tag}/latest.yml
|
||||
*/
|
||||
async function getGiteaUpdaterFeedBase(cfg) {
|
||||
if (!useGiteaReleases(cfg)) return null;
|
||||
const api = giteaApiBase(cfg);
|
||||
const { owner, repo } = cfg.gitea;
|
||||
const tag = (cfg.gitea.release_tag || 'latest').trim() || 'latest';
|
||||
const token = giteaToken(cfg);
|
||||
let listUrl;
|
||||
if (tag.toLowerCase() === 'latest') {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/latest`;
|
||||
} else {
|
||||
listUrl = `${api}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/tags/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
const res = await fetchOrThrow(listUrl, { headers: giteaHeaders(token, true) });
|
||||
if (!res.ok) return null;
|
||||
const rel = await res.json();
|
||||
const tagName = rel.tag_name;
|
||||
if (!tagName || typeof tagName !== 'string') return null;
|
||||
const root = normalizeGiteaBaseUrl(cfg.gitea.base_url);
|
||||
const url = `${root}/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases/download/${encodeURIComponent(tagName)}/`;
|
||||
return { url, token };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
downloadGiteaReleaseAsset,
|
||||
fetchGiteaReleaseRecord,
|
||||
listGiteaReleaseAttachmentNames,
|
||||
giteaToken,
|
||||
useGiteaReleases,
|
||||
getGiteaUpdaterFeedBase,
|
||||
};
|
||||
@@ -3,8 +3,8 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { githubToken } = require('./github-token');
|
||||
const { downloadGiteaReleaseAsset, useGiteaReleases } = require('./gitea-release');
|
||||
const { fetchToFile, downloadBodyToFile } = require('./http-download');
|
||||
const { downloadGiteaReleaseAsset, useGiteaReleases, listGiteaReleaseAttachmentNames } = require('./gitea-release');
|
||||
const { fetchToFile, downloadBodyToFile, fetchOrThrow } = require('./http-download');
|
||||
|
||||
function encodeRepoPath(repoPath) {
|
||||
let p = String(repoPath || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
@@ -35,7 +35,7 @@ async function downloadGitHubRepoFile(cfg, repoPath, destPath) {
|
||||
}
|
||||
|
||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${enc}?ref=${encodeURIComponent(ref)}`;
|
||||
const res = await fetch(apiUrl, { headers: ghHeaders(token, true) });
|
||||
const res = await fetchOrThrow(apiUrl, { headers: ghHeaders(token, true) });
|
||||
const body = await res.text();
|
||||
if (!res.ok) {
|
||||
throw new Error(`GitHub contents API ${res.status}: ${body.slice(0, 800)}`);
|
||||
@@ -65,10 +65,7 @@ async function downloadGitHubRepoFile(cfg, repoPath, destPath) {
|
||||
throw new Error(`unexpected GitHub response for ${repoPath}`);
|
||||
}
|
||||
|
||||
async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
if (useGiteaReleases(cfg)) {
|
||||
return downloadGiteaReleaseAsset(cfg, assetName, destPath);
|
||||
}
|
||||
async function fetchGitHubReleaseJson(cfg) {
|
||||
const token = githubToken(cfg);
|
||||
const tag = (cfg.github.release_tag || 'latest').trim() || 'latest';
|
||||
const { owner, repo } = cfg.github;
|
||||
@@ -78,7 +75,7 @@ async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
} else {
|
||||
listUrl = `https://api.github.com/repos/${owner}/${repo}/releases/tags/${encodeURIComponent(tag)}`;
|
||||
}
|
||||
const res = await fetch(listUrl, { headers: ghHeaders(token, true) });
|
||||
const res = await fetchOrThrow(listUrl, { headers: ghHeaders(token, true) });
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
let hint = '';
|
||||
@@ -89,7 +86,24 @@ async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
if (res.status === 401 || res.status === 403) hint = ' (set GITHUB_TOKEN or token_env PAT)';
|
||||
throw new Error(`releases list ${res.status}${hint}: ${text.slice(0, 600)}`);
|
||||
}
|
||||
const rel = JSON.parse(text);
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
async function listReleaseAttachmentNames(cfg) {
|
||||
if (useGiteaReleases(cfg)) {
|
||||
return listGiteaReleaseAttachmentNames(cfg);
|
||||
}
|
||||
const rel = await fetchGitHubReleaseJson(cfg);
|
||||
const assets = rel.assets || [];
|
||||
return assets.map((a) => a.name).filter(Boolean);
|
||||
}
|
||||
|
||||
async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
if (useGiteaReleases(cfg)) {
|
||||
return downloadGiteaReleaseAsset(cfg, assetName, destPath);
|
||||
}
|
||||
const token = githubToken(cfg);
|
||||
const rel = await fetchGitHubReleaseJson(cfg);
|
||||
const assets = rel.assets || [];
|
||||
let assetURL = '';
|
||||
for (const a of assets) {
|
||||
@@ -114,8 +128,14 @@ async function downloadReleaseAsset(cfg, assetName, destPath) {
|
||||
h.Authorization = `Bearer ${token}`;
|
||||
h['X-GitHub-Api-Version'] = '2022-11-28';
|
||||
}
|
||||
const dl = await fetch(assetURL, { headers: h, redirect: 'follow' });
|
||||
const dl = await fetchOrThrow(assetURL, { headers: h, redirect: 'follow' });
|
||||
await downloadBodyToFile(dl, destPath);
|
||||
}
|
||||
|
||||
module.exports = { downloadGitHubRepoFile, downloadReleaseAsset, encodeRepoPath };
|
||||
module.exports = {
|
||||
downloadGitHubRepoFile,
|
||||
downloadReleaseAsset,
|
||||
encodeRepoPath,
|
||||
fetchGitHubReleaseJson,
|
||||
listReleaseAttachmentNames,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,42 @@ const { createWriteStream } = require('fs');
|
||||
const { pipeline } = require('stream/promises');
|
||||
const { Readable } = require('stream');
|
||||
|
||||
function safeUrlForLog(url) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return `${u.origin}${u.pathname}`;
|
||||
} catch {
|
||||
return String(url || '').split('?')[0].slice(0, 200);
|
||||
}
|
||||
}
|
||||
|
||||
function explainFetchFailure(err, url) {
|
||||
const msg = err && err.message ? err.message : String(err);
|
||||
const cause = err && err.cause;
|
||||
const code = cause && cause.code ? cause.code : '';
|
||||
const combined = `${msg} ${code}`;
|
||||
const hints = [];
|
||||
if (/CERT|TLS|SSL|UNABLE_TO_VERIFY|SELF_SIGNED|certificate|unknown ca|unable to verify/i.test(combined)) {
|
||||
hints.push(
|
||||
'TLS certificate not trusted — install a valid cert on Gitea, or trust your CA system-wide, or set NODE_EXTRA_CA_CERTS to a .pem bundle (self-signed mirrors)'
|
||||
);
|
||||
}
|
||||
if (/ECONNREFUSED/.test(combined)) hints.push('connection refused (wrong host/port or server down)');
|
||||
if (/ENOTFOUND|EAI_AGAIN/.test(combined)) hints.push('DNS lookup failed');
|
||||
if (/ETIMEDOUT|TIMEOUT/i.test(combined)) hints.push('connection timed out');
|
||||
const hintStr = hints.length ? ` ${hints.join(' ')}` : '';
|
||||
return new Error(`${msg}${hintStr} — ${safeUrlForLog(url)}`);
|
||||
}
|
||||
|
||||
/** Wrap global fetch with clearer errors for TLS/DNS/refused (Electron reports bare "fetch failed"). */
|
||||
async function fetchOrThrow(url, init) {
|
||||
try {
|
||||
return await fetch(url, init);
|
||||
} catch (e) {
|
||||
throw explainFetchFailure(e, url);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBodyToFile(res, destPath) {
|
||||
if (!res.ok) {
|
||||
const errText = await res.text().catch(() => '');
|
||||
@@ -30,11 +66,11 @@ async function downloadBodyToFile(res, destPath) {
|
||||
}
|
||||
|
||||
async function fetchToFile(url, headers, destPath) {
|
||||
const res = await fetch(url, {
|
||||
const res = await fetchOrThrow(url, {
|
||||
headers,
|
||||
redirect: 'follow',
|
||||
});
|
||||
await downloadBodyToFile(res, destPath);
|
||||
}
|
||||
|
||||
module.exports = { fetchToFile, downloadBodyToFile };
|
||||
module.exports = { fetchToFile, downloadBodyToFile, fetchOrThrow, safeUrlForLog };
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { downloadReleaseAsset, downloadGitHubRepoFile } = require('./github');
|
||||
|
||||
async function sha256File(absPath) {
|
||||
const buf = await fs.readFile(absPath);
|
||||
return createHash('sha256').update(buf).digest('hex');
|
||||
}
|
||||
|
||||
function stateDir(gameDir) {
|
||||
return path.join(gameDir, '.fractured');
|
||||
}
|
||||
|
||||
function statePath(gameDir) {
|
||||
return path.join(stateDir(gameDir), 'patch-state.json');
|
||||
}
|
||||
|
||||
async function readPatchState(gameDir) {
|
||||
if (!gameDir) return null;
|
||||
try {
|
||||
const t = await fs.readFile(statePath(gameDir), 'utf8');
|
||||
return JSON.parse(t);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writePatchState(gameDir, manifestVersion, fileShas) {
|
||||
const p = statePath(gameDir);
|
||||
await fs.mkdir(path.dirname(p), { recursive: true });
|
||||
const body = {
|
||||
client_build: manifestVersion,
|
||||
updated_at: new Date().toISOString(),
|
||||
files: fileShas,
|
||||
};
|
||||
const tmp = p + '.tmp';
|
||||
await fs.writeFile(tmp, JSON.stringify(body, null, 2), 'utf8');
|
||||
await fs.rename(tmp, p);
|
||||
}
|
||||
|
||||
function validateManifest(m) {
|
||||
if (!m || m.version == null || String(m.version).trim() === '') return false;
|
||||
if (!m.files || typeof m.files !== 'object') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and parse patch-manifest.json (or custom name). Returns null on any failure.
|
||||
*/
|
||||
async function loadManifest(cfg) {
|
||||
const pm = cfg.patch_manifest;
|
||||
if (!pm || !pm.enabled || !String(pm.source || '').trim()) return null;
|
||||
const tmp = path.join(os.tmpdir(), `fr-patch-manifest-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
||||
try {
|
||||
if (pm.from_release) {
|
||||
await downloadReleaseAsset(cfg, String(pm.source).trim(), tmp);
|
||||
} else {
|
||||
await downloadGitHubRepoFile(cfg, String(pm.source).trim(), tmp);
|
||||
}
|
||||
const raw = await fs.readFile(tmp, 'utf8');
|
||||
await fs.unlink(tmp).catch(() => {});
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
await fs.unlink(tmp).catch(() => {});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True if every from_release file on disk matches manifest sha256.
|
||||
*/
|
||||
async function patchesMatchManifest(cfg, manifest, onStatus) {
|
||||
if (!validateManifest(manifest)) return false;
|
||||
const { buildResolvedReleaseFiles } = require('./release-sync');
|
||||
const entries = await buildResolvedReleaseFiles(cfg, manifest);
|
||||
const gameDir = cfg.game_dir;
|
||||
for (const entry of entries) {
|
||||
if (!entry.from_release) continue;
|
||||
const spec = manifest.files[entry.source];
|
||||
if (!spec || !spec.sha256) return false;
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(gameDir, ...parts);
|
||||
let disk;
|
||||
try {
|
||||
disk = await sha256File(destAbs);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (disk.toLowerCase() !== String(spec.sha256).trim().toLowerCase()) return false;
|
||||
}
|
||||
if (onStatus) {
|
||||
onStatus(`Client files already match build ${manifest.version} (nothing to download).`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function verifyInstalledAgainstManifest(cfg, manifest) {
|
||||
if (!validateManifest(manifest)) return;
|
||||
const { buildResolvedReleaseFiles } = require('./release-sync');
|
||||
const entries = await buildResolvedReleaseFiles(cfg, manifest);
|
||||
for (const entry of entries) {
|
||||
if (!entry.from_release) continue;
|
||||
const spec = manifest.files[entry.source];
|
||||
if (!spec || !spec.sha256) {
|
||||
throw new Error(
|
||||
`patch-manifest.json is missing a sha256 for "${entry.source}" — regenerate the manifest for this release.`
|
||||
);
|
||||
}
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(cfg.game_dir, ...parts);
|
||||
const disk = await sha256File(destAbs);
|
||||
if (disk.toLowerCase() !== String(spec.sha256).trim().toLowerCase()) {
|
||||
throw new Error(
|
||||
`${entry.source}: checksum mismatch after install (expected ${spec.sha256.slice(0, 12)}…, got ${disk.slice(0, 12)}…). Try syncing again.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function recordPatchState(cfg, manifest) {
|
||||
if (!validateManifest(manifest)) return;
|
||||
const { buildResolvedReleaseFiles } = require('./release-sync');
|
||||
const entries = await buildResolvedReleaseFiles(cfg, manifest);
|
||||
const shas = {};
|
||||
for (const entry of entries) {
|
||||
if (!entry.from_release) continue;
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(cfg.game_dir, ...parts);
|
||||
try {
|
||||
shas[entry.source] = await sha256File(destAbs);
|
||||
} catch {
|
||||
/* skip */
|
||||
}
|
||||
}
|
||||
await writePatchState(cfg.game_dir, String(manifest.version), shas);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadManifest,
|
||||
validateManifest,
|
||||
patchesMatchManifest,
|
||||
verifyInstalledAgainstManifest,
|
||||
recordPatchState,
|
||||
readPatchState,
|
||||
statePath,
|
||||
};
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const fsSync = require('fs');
|
||||
const { downloadGitHubRepoFile, downloadReleaseAsset } = require('./github');
|
||||
const { normalizeWinGameDir } = require('./win-game-dir');
|
||||
const { loadManifest } = require('./patch-manifest');
|
||||
const { buildResolvedReleaseFiles } = require('./release-sync');
|
||||
|
||||
function pad2(n) {
|
||||
return String(n).padStart(2, '0');
|
||||
@@ -13,19 +17,44 @@ function backupSuffix() {
|
||||
}
|
||||
|
||||
function wowExePath(cfg) {
|
||||
const gd = normalizeWinGameDir(cfg.game_dir || '');
|
||||
const exe = (cfg.launch && cfg.launch.exe) || 'Wow.exe';
|
||||
const parts = exe.replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
return path.join(cfg.game_dir, ...parts);
|
||||
const primary = path.join(gd, ...parts);
|
||||
if (process.platform === 'win32' && gd && fsSync.existsSync(primary)) return primary;
|
||||
if (process.platform === 'win32' && gd) {
|
||||
try {
|
||||
const base = path.basename(primary);
|
||||
const dir = path.dirname(primary);
|
||||
const names = fsSync.readdirSync(dir);
|
||||
const hit = names.find((n) => n.toLowerCase() === base.toLowerCase());
|
||||
if (hit) {
|
||||
const alt = path.join(dir, hit);
|
||||
if (fsSync.statSync(alt).isFile()) return alt;
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
function wowInstallValid(cfg) {
|
||||
if (!cfg.game_dir) return false;
|
||||
return require('fs').existsSync(wowExePath(cfg));
|
||||
const p = wowExePath(cfg);
|
||||
return fsSync.existsSync(p) && fsSync.statSync(p).isFile();
|
||||
}
|
||||
|
||||
/** WoW expects patch MPQ names with a literal .MPQ extension (case-sensitive clients). */
|
||||
function normalizeMpqDestinationPath(absPath) {
|
||||
const s = String(absPath || '');
|
||||
return /\.mpq$/i.test(s) ? s.replace(/\.mpq$/i, '.MPQ') : s;
|
||||
}
|
||||
|
||||
async function installFile(cfg, entry) {
|
||||
const parts = String(entry.dest).replace(/\\/g, '/').split('/').filter(Boolean);
|
||||
const destAbs = path.join(cfg.game_dir, ...parts);
|
||||
const root = normalizeWinGameDir(cfg.game_dir || '');
|
||||
const destAbs = normalizeMpqDestinationPath(path.join(root, ...parts));
|
||||
if (entry.backup) {
|
||||
try {
|
||||
const st = await fs.stat(destAbs);
|
||||
@@ -66,14 +95,19 @@ async function applyRealmlist(cfg) {
|
||||
const r = String(rel).trim().replace(/\\/g, '/');
|
||||
if (!r) continue;
|
||||
const segs = r.split('/').filter(Boolean);
|
||||
const abs = path.join(cfg.game_dir, ...segs);
|
||||
const abs = path.join(normalizeWinGameDir(cfg.game_dir || ''), ...segs);
|
||||
await fs.mkdir(path.dirname(abs), { recursive: true });
|
||||
await fs.writeFile(abs, content, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
async function applyPatches(cfg, onStatus) {
|
||||
for (const f of cfg.files || []) {
|
||||
let manifest = null;
|
||||
if (cfg.patch_manifest && cfg.patch_manifest.enabled) {
|
||||
manifest = await loadManifest(cfg);
|
||||
}
|
||||
const entries = await buildResolvedReleaseFiles(cfg, manifest);
|
||||
for (const f of entries) {
|
||||
if (onStatus) onStatus(`Updating ${f.dest} …`);
|
||||
try {
|
||||
await installFile(cfg, f);
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { listReleaseAttachmentNames } = require('./github');
|
||||
|
||||
/** Legacy launcher.json rows — ignored when merging explicit files. */
|
||||
const DEPRECATED_SOURCES = new Set(['patch-Z.MPQ', 'Wow-patched.exe']);
|
||||
|
||||
function filterExplicitFiles(files) {
|
||||
if (!Array.isArray(files)) return [];
|
||||
return files
|
||||
.filter((e) => e && String(e.source || '').trim())
|
||||
.filter((e) => !DEPRECATED_SOURCES.has(String(e.source).trim()))
|
||||
.map((e) => ({
|
||||
source: String(e.source).trim(),
|
||||
dest: String(e.dest || '').trim(),
|
||||
backup: e.backup !== false,
|
||||
from_release: e.from_release !== false,
|
||||
}))
|
||||
.filter((e) => e.dest);
|
||||
}
|
||||
|
||||
function manifestLooksUsable(m) {
|
||||
return !!(m && m.files && typeof m.files === 'object' && Object.keys(m.files).length > 0);
|
||||
}
|
||||
|
||||
/** Launcher / updater attachments — never copied into the WoW folder. */
|
||||
function isExcludedFromGameSync(fileName) {
|
||||
const n = String(fileName || '');
|
||||
const lower = n.toLowerCase();
|
||||
if (lower === 'patch-manifest.json') return true;
|
||||
if (/^fractured-launcher/i.test(n)) return true;
|
||||
if (/\.blockmap$/i.test(n)) return true;
|
||||
if (/^latest.*\.ya?ml$/i.test(n) || lower === 'latest.yml') return true;
|
||||
if (lower.includes('builder-debug')) return true;
|
||||
if (/\.appimage$/i.test(n)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function mpqDestFromSource(source) {
|
||||
const base = path.basename(String(source || ''));
|
||||
const stem = base.replace(/\.mpq$/i, '');
|
||||
return `Data/enUS/${stem}.MPQ`;
|
||||
}
|
||||
|
||||
function destForReleaseSource(source, cfg) {
|
||||
const base = path.basename(String(source || ''));
|
||||
if (/\.mpq$/i.test(base)) return mpqDestFromSource(source);
|
||||
if (/\.exe$/i.test(base)) return (cfg.launch && cfg.launch.exe) || 'Wow.exe';
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit `files` in config wins. Otherwise use patch-manifest keys if present,
|
||||
* else discover attachments on the release (excluding launcher artifacts).
|
||||
*/
|
||||
async function buildResolvedReleaseFiles(cfg, manifestMaybeNull) {
|
||||
const explicit = filterExplicitFiles(cfg.files);
|
||||
if (explicit.length) return explicit;
|
||||
|
||||
const manifest = manifestMaybeNull;
|
||||
if (manifestLooksUsable(manifest)) {
|
||||
const keys = Object.keys(manifest.files).filter((k) => k && !isExcludedFromGameSync(k));
|
||||
if (!keys.length) {
|
||||
throw new Error('patch-manifest.json has no file entries — add files or attach assets to the release.');
|
||||
}
|
||||
return keys.map((source) => ({
|
||||
source,
|
||||
dest: destForReleaseSource(source, cfg),
|
||||
backup: true,
|
||||
from_release: true,
|
||||
}));
|
||||
}
|
||||
|
||||
const names = await listReleaseAttachmentNames(cfg);
|
||||
const game = names.filter((n) => n && !isExcludedFromGameSync(n));
|
||||
if (!game.length) {
|
||||
throw new Error(
|
||||
'No patch files on this release (after excluding launcher installers). ' +
|
||||
'Attach MPQ/exe assets or ship patch-manifest.json listing filenames.'
|
||||
);
|
||||
}
|
||||
|
||||
const exes = game.filter((n) => /\.exe$/i.test(n));
|
||||
const mpqs = game.filter((n) => /\.mpq$/i.test(n));
|
||||
const rest = game.filter((n) => !/\.(exe|mpq)$/i.test(n));
|
||||
|
||||
if (exes.length > 1) {
|
||||
throw new Error(
|
||||
`Release has multiple .exe files (${exes.join(', ')}). ` +
|
||||
'Remove extras or publish patch-manifest.json with the exact filenames to install.'
|
||||
);
|
||||
}
|
||||
|
||||
const out = [];
|
||||
for (const n of mpqs) {
|
||||
out.push({
|
||||
source: n,
|
||||
dest: mpqDestFromSource(n),
|
||||
backup: true,
|
||||
from_release: true,
|
||||
});
|
||||
}
|
||||
if (exes.length === 1) {
|
||||
out.push({
|
||||
source: exes[0],
|
||||
dest: (cfg.launch && cfg.launch.exe) || 'Wow.exe',
|
||||
backup: true,
|
||||
from_release: true,
|
||||
});
|
||||
}
|
||||
for (const n of rest) {
|
||||
out.push({
|
||||
source: n,
|
||||
dest: path.basename(n),
|
||||
backup: true,
|
||||
from_release: true,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildResolvedReleaseFiles,
|
||||
filterExplicitFiles,
|
||||
isExcludedFromGameSync,
|
||||
DEPRECATED_SOURCES,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Under Wine, the folder picker often returns a Unix absolute path (/home/...).
|
||||
* Windows Node does not resolve that to the WoW install; map to Wine's Z: drive
|
||||
* (Z: == / on typical Wine prefixes).
|
||||
*/
|
||||
function normalizeWinGameDir(gameDir) {
|
||||
if (process.platform !== 'win32') return String(gameDir || '').trim();
|
||||
let s = String(gameDir || '').trim();
|
||||
if (!s) return s;
|
||||
s = s.replace(/\//g, path.win32.sep);
|
||||
if (s.startsWith('\\\\')) return path.normalize(s);
|
||||
if (/^[A-Za-z]:/.test(s)) return path.normalize(s);
|
||||
if (s.startsWith(path.win32.sep)) return path.win32.normalize(`Z:${s}`);
|
||||
return path.normalize(s);
|
||||
}
|
||||
|
||||
module.exports = { normalizeWinGameDir };
|
||||
@@ -4,6 +4,7 @@ const { app, BrowserWindow, ipcMain, dialog, Menu } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const { loadConfig, saveGameDir, resolveGameDir } = require('./lib/config-store');
|
||||
const { normalizeWinGameDir } = require('./lib/win-game-dir');
|
||||
const { applyPatches, wowExePath, wowInstallValid, doAuth } = require('./lib/patch');
|
||||
const { readPatchState } = require('./lib/patch-manifest');
|
||||
const { setupAutoUpdater } = require('./lib/auto-update');
|
||||
@@ -95,7 +96,8 @@ ipcMain.handle('launcher:saveGameDir', async (_e, dir) => {
|
||||
const trimmed = String(dir || '').trim();
|
||||
if (!trimmed) throw new Error('folder path is empty');
|
||||
const { configPath } = await loadConfig(app);
|
||||
const norm = path.normalize(trimmed);
|
||||
const norm =
|
||||
process.platform === 'win32' ? normalizeWinGameDir(path.normalize(trimmed)) : path.normalize(trimmed);
|
||||
const probe = { ...(await readMergedConfig()).config, game_dir: norm };
|
||||
if (!wowInstallValid(probe)) {
|
||||
throw new Error(`That folder does not contain ${(probe.launch && probe.launch.exe) || 'Wow.exe'}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fractured-launcher-electron",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.9",
|
||||
"description": "Fractured WoW launcher (Electron) — no console window, native folder picker, auto-update",
|
||||
"main": "main.js",
|
||||
"repository": {
|
||||
@@ -9,8 +9,9 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"pack:win": "node scripts/inject-release-channel.js && electron-builder --win nsis portable --x64 --publish never",
|
||||
"publish:win": "node scripts/inject-release-channel.js && electron-builder --win nsis portable --x64 --publish never"
|
||||
"pack:win": "electron-builder --win nsis portable --x64 --publish never",
|
||||
"pack:linux": "electron-builder --linux AppImage --x64 --publish never",
|
||||
"publish:win": "electron-builder --win nsis portable --x64 --publish never"
|
||||
},
|
||||
"author": "",
|
||||
"license": "GPL-3.0",
|
||||
@@ -35,6 +36,10 @@
|
||||
"renderer.js",
|
||||
"styles.css",
|
||||
"default-launcher.json",
|
||||
"lib/win-game-dir.js",
|
||||
"lib/baked-gitea-channel.js",
|
||||
"lib/gitea-release.js",
|
||||
"lib/patch-manifest.js",
|
||||
"lib/**/*"
|
||||
],
|
||||
"win": {
|
||||
@@ -56,6 +61,18 @@
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "Fractured-Launcher-${version}-Windows-Portable.${ext}"
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": ["x64"]
|
||||
}
|
||||
],
|
||||
"category": "Game"
|
||||
},
|
||||
"appImage": {
|
||||
"artifactName": "Fractured-Launcher-${version}-Linux-x86_64.${ext}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build patch-manifest.json for a release (same names as files[].source in launcher.json).
|
||||
*
|
||||
* Usage (from a folder containing the patch binaries — list every files[].source name):
|
||||
* node generate-patch-manifest.js v0.9.0-client Wow-patched.exe
|
||||
*
|
||||
* Prints JSON to stdout — redirect to file:
|
||||
* node generate-patch-manifest.js v0.9.0-client Wow-patched.exe > patch-manifest.json
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const version = process.argv[2];
|
||||
const names = process.argv.slice(3);
|
||||
if (!version || names.length === 0) {
|
||||
console.error('Usage: generate-patch-manifest.js <version-label> <file1> [file2 ...]');
|
||||
console.error(' Example: generate-patch-manifest.js v0.9.0-client Wow-patched.exe');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const out = { version, files: {} };
|
||||
for (const f of names) {
|
||||
const base = path.basename(f);
|
||||
const buf = fs.readFileSync(f);
|
||||
const sha256 = crypto.createHash('sha256').update(buf).digest('hex');
|
||||
out.files[base] = { sha256 };
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Merge Gitea release channel (non-token) into default-launcher.json before pack.
|
||||
* Precedence: env → fractured-release-channel.json → leave existing default-launcher values.
|
||||
*
|
||||
* Env (any of these names):
|
||||
* FRACTURED_LAUNCHER_GITEA_BASE_URL | GITEA_BASE_URL
|
||||
* FRACTURED_LAUNCHER_GITEA_OWNER | GITEA_OWNER
|
||||
* FRACTURED_LAUNCHER_GITEA_REPO | GITEA_REPO
|
||||
* FRACTURED_LAUNCHER_GITEA_RELEASE_TAG | GITEA_RELEASE_TAG
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
const defPath = path.join(root, 'default-launcher.json');
|
||||
const channelPath = path.join(root, 'fractured-release-channel.json');
|
||||
|
||||
function pickEnv() {
|
||||
return {
|
||||
base_url: String(
|
||||
process.env.FRACTURED_LAUNCHER_GITEA_BASE_URL || process.env.GITEA_BASE_URL || ''
|
||||
).trim(),
|
||||
owner: String(
|
||||
process.env.FRACTURED_LAUNCHER_GITEA_OWNER || process.env.GITEA_OWNER || ''
|
||||
).trim(),
|
||||
repo: String(
|
||||
process.env.FRACTURED_LAUNCHER_GITEA_REPO || process.env.GITEA_REPO || ''
|
||||
).trim(),
|
||||
release_tag: String(
|
||||
process.env.FRACTURED_LAUNCHER_GITEA_RELEASE_TAG || process.env.GITEA_RELEASE_TAG || ''
|
||||
).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const cfg = JSON.parse(fs.readFileSync(defPath, 'utf8'));
|
||||
cfg.gitea = cfg.gitea && typeof cfg.gitea === 'object' ? cfg.gitea : {};
|
||||
|
||||
let fileGitea = {};
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(channelPath, 'utf8'));
|
||||
if (raw && raw.gitea && typeof raw.gitea === 'object') fileGitea = raw.gitea;
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT') throw e;
|
||||
}
|
||||
|
||||
const env = pickEnv();
|
||||
const keys = ['base_url', 'owner', 'repo', 'release_tag'];
|
||||
let changed = false;
|
||||
|
||||
for (const k of keys) {
|
||||
const fromEnv = env[k];
|
||||
const fromFile =
|
||||
fileGitea[k] != null && String(fileGitea[k]).trim() !== '' ? String(fileGitea[k]).trim() : '';
|
||||
const val = (fromEnv && String(fromEnv).trim()) || fromFile;
|
||||
if (!val) continue;
|
||||
if (cfg.gitea[k] !== val) {
|
||||
cfg.gitea[k] = val;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
console.log(
|
||||
'inject-release-channel: no overrides (set GITEA_* env and/or fill fractured-release-channel.json)'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(defPath, `${JSON.stringify(cfg, null, 2)}\n`, 'utf8');
|
||||
console.log('inject-release-channel: wrote gitea.* into default-launcher.json for this build');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local Linux AppImage build (uses current tree — no tag snapshot). Run from repo root or this dir.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
echo "==> npm ci"
|
||||
npm ci
|
||||
echo "==> npm run pack:linux (AppImage x64)"
|
||||
npm run pack:linux
|
||||
echo "==> dist/:"
|
||||
ls -la dist/
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# Usage (from repo root or this directory):
|
||||
# export GH_TOKEN=ghp_... # PAT with repo/releases on the distro repo
|
||||
# ./tools/fractured-launcher-electron/scripts/publish-to-distro.sh v1.0.0 patch-Z.MPQ Wow-patched.exe
|
||||
# ./tools/fractured-launcher-electron/scripts/publish-to-distro.sh v1.0.0 Wow-patched.exe
|
||||
#
|
||||
# Optional:
|
||||
# DISTRO_REPO=YourOrg/Fratured-Distro # if your GitHub slug differs
|
||||
|
||||
Reference in New Issue
Block a user