Compare commits

...

16 Commits

Author SHA1 Message Date
Docker Build 8ad6a2aca3 Paragon: cascade guard for class skill lines + panel catalog backfill
The skill-line cascade in Player::learnSkillRewardedSpells 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 pass re-grants
every SkillLineAbility-tagged class ability on the matching skill line,
which leaks Blood Presence / Death Coil / Death Grip / etc. back into
the spellbook within seconds even after the player intentionally
refunded them via the Character Advancement panel.

Path B fix: a 5-line guard at the top of learnSkillRewardedSpells skips
the cascade for class-category skill lines on CLASS_PARAGON characters.
mod-paragon already calls Player::learnSpell directly for the abilities
the player actually purchased (and their attached passives), so the
panel becomes the sole authority over class abilities. Profession,
weapon, language, and racial cascades stay enabled so recipe auto-learn,
weapon proficiencies, and racial perks still work.

Side effect: passives that previously rode along on the cascade
(Forceful Deflection on Blood Strike, Runic Focus on Icy Touch) must be
force-attached the same way Blood Plague / Frost Fever already are.
Extend kAttached and kFixup in Paragon_Essence.cpp to do that; existing
characters self-heal on next login.

Backfill paragon_spell_ae_cost for 42 spells newly exposed by the panel
after the ClassMask=0 filter was removed from the client catalog
generator (Lava Burst, Hex, Evocation, Kill Shot, Path of Frost,
Horn of Winter, Rune Strike, Raise Ally, Dark Command, etc.). Migration
is INSERT IGNORE so any per-spell tuning on existing rows is preserved.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 23:53:13 -04:00
Docker Build 36ac3dbd1d fix(launcher): force .MPQ extension uppercase on disk for WoW compatibility
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 22:09:01 -05:00
Docker Build 24d1ae71d9 fix(launcher): install release MPQs under Data/enUS (not Data root)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 22:06:48 -05:00
Docker Build 9cef99f0ff feat(launcher): sync release assets from manifest or attachment list (no fixed exe name)
- default files []: resolve sync list from patch-manifest keys, else discover
  release attachments (exclude launcher artifacts).
- Explicit files[] still overrides; strip deprecated Wow-patched.exe on merge.
- listReleaseAttachmentNames + fetchGiteaReleaseRecord helpers.
- Version 1.0.7; README config docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 22:04:48 -05:00
Docker Build f409ffad12 fix(launcher): Gitea http URL; Wine Z: path + Wow.exe case check
- baked-gitea-channel: http:// for brassnet mirror.
- win-game-dir: map Unix /home/... to Z:\ under win32 (Wine folder picker).
- resolveGameDir + saveGameDir + patch paths use it; Wow.exe resolved case-insensitively.
- Version 1.0.6; README checklist for Wine.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:54:04 -05:00
Docker Build c1f7eaa153 fix(launcher): clearer fetch errors for Gitea TLS/DNS (fetch failed)
- fetchOrThrow wraps global fetch with TLS/DNS/refused hints + URL (sanitized).
- Use in gitea-release, github paths; fetchToFile already benefits.
- README checklist for sync Wow.exe fetch failed; version 1.0.5.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:46:48 -05:00
Docker Build b455db0db8 fix(launcher): drop patch-Z.MPQ from default files and migrate old configs
- default-launcher.json files: only Wow-patched.exe from release.
- config-store: strip deprecated patch-Z.MPQ from merged files; rewrite
  launcher.json on load if user still had that entry.
- Docs/scripts examples updated; version 1.0.4.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:36:50 -05:00
Docker Build 1fb284cb5c docs(launcher): clarify userData path wording for Linux config
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:33:07 -05:00
Docker Build ebd8d81924 fix(launcher): Linux/macOS packaged config in userData (AppImage EROFS)
AppImage mounts read-only at /tmp/.mount_*; writing launcher.json beside
execPath failed. Use app.getPath('userData') for linux/darwin when packaged.
Bump version to 1.0.3.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:32:43 -05:00
Docker Build 362084b829 ci(gitea-sync): validate workflow_dispatch tag; reject release title as ref
- Trim input; fail fast if tag contains whitespace (common mistake: pasting
  release title instead of git tag).
- Multiline GITHUB_OUTPUT for tag value safety.
- README checklist + input description clarify tag vs title.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 21:21:45 -05:00
Docker Build 656cf2d07d Paragon panel: keep cascade passives, strip free actives; DK passive DBC fix
- PanelLearnSpellChain: record every non-chain passive as panel_spell_child;
  only revoke non-passive (Blood Presence, Death Coil, Death Grip, etc.).
- RevokeUnwantedCascadeSpellsForPlayer: skip passive rewards on login sweep.
- RevokeBlockedSpellsForPlayer: migrate legacy passive revoke rows to
  children; walk (parent, revoked) pairs from DB.
- PruneSkillLineCascadeChildrenFromDb: only strip actives wrongly stored as
  children; never strip passives.
- SpellInfoCorrections: set SPELL_ATTR0_PASSIVE on Forceful Deflection (49410)
  and Runic Focus (61455) so IsPassive() matches spellbook behavior.
- PanelUnlearnTalentPurchase: mirror resetTalents (_removeTalentAurasAndSpells,
  _removeTalent, SendTalentsInfoData) so Beast Mastery loss triggers pet reset.
- OnPlayerLogin: run legacy passive attach before scoped cascade sweep.
- Add .paragon recalibrate GM command (RBAC modify): full panel reset + AE/TE
  reconciliation for selected player or self.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 22:20:13 -04:00
Docker Build bfe51f6ad4 docs(launcher): note manual Windows pack for local test
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 20:50:16 -05:00
Docker Build 2a3107a78d feat(launcher): Linux AppImage 1.0.2, Gitea sync + CI, manual pack script
- Add pack:linux (AppImage x64), linux/appImage artifact names in package.json.
- Gitea sync: parallel build-electron-linux, merge Windows+Linux into Gitea upload;
  rename Windows artifact to electron-dist-windows.
- Fractured launcher CI: electron-launcher-windows + electron-launcher-linux jobs.
- scripts/manual-pack-linux.sh for local test builds from current tree.
- Normalize Gitea base_url (prepend https if missing); baked channel uses full URL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 20:50:06 -05:00
Docker Build 48826e21d6 refactor(launcher): hardcode Gitea channel in lib/baked-gitea-channel.js
- Merge baked base_url/owner/repo/release_tag at load time (no inject script,
  no fractured-release-channel.json, no CI env for pack).
- Fix mergeConfig deep-merge for gitea, patch_manifest, launcher_updates_from_github.
- Remove inject-release-channel.js and fractured-release-channel.json.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 20:15:38 -05:00
Docker Build 15c476c12d ci(gitea-sync): overlay launcher from default branch before pack
Release tags can point at commits older than launcher lib additions; building
only from the tag omitted gitea-release.js etc. Fetch default branch and
checkout tools/fractured-launcher-electron from it before npm ci/pack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 19:15:27 -05:00
Docker Build 6c4d7244c3 fix(launcher): add missing gitea-release and patch-manifest to repo
These modules were required by main.js / auto-update.js / github.js but never
committed, so packaged builds lacked them and crashed at startup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-10 19:08:55 -05:00
25 changed files with 1837 additions and 249 deletions
+30 -5
View File
@@ -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
+92 -15
View File
@@ -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 repos 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,
+37 -22
View File
@@ -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 pages 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\...`** (Wines 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 Giteas 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,
};
+31 -11
View File
@@ -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,
};
+39 -5
View File
@@ -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 };
+3 -1
View File
@@ -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'}`);
+20 -3
View File
@@ -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