Conversation
… fails to load (#188) The install popup asked the ModDB for a Mod's releases and had no way to say that the answer never came: a rejected query left it on the loading spinner forever, under a description that read MOD NOT FOUND for a Mod the user had just clicked on a list. The versions query now reports failure the way the game version catalog already does, with a loading/failed/retry shape, and the popup renders one sentence and a reload button in place of the endless spinner. The description falls back to the name the clicked row already carries, then to the ModID, so the not-found wording cannot show up for a Mod that was picked off a list. Refs #186
#189) The launcher downloaded its own updates silently. autoDownload was left at its default, so finding an update was the same event as fetching it, and the only thing the user ever saw was a toast saying a download they never agreed to had already happened. Finding an update now only offers it. The toast names the version and carries two buttons, and only the accepting one reaches the new DOWNLOAD_UPDATE channel, which is the single path to autoUpdater.downloadUpdate anywhere in the app. That channel refuses anything but the trusted renderer, refuses to run before the main process has actually found an update, and refuses to start a second download of the same one. The accepted download reports itself. Every tick of electron-updater's download-progress becomes a task in the task manager, which means the update draws the same progress bar in the same list as every other download the launcher runs, rather than a widget that only exists for this one case. The task completes when the update lands and fails if the download dies, so the bar always has somewhere to end. The restart flow is untouched: the same downloaded flag, the same trusted UPDATE_AND_RESTART channel, the same quitAndInstall.
…be retried (#190) A toast with actions could be answered more than once. removeNotification takes it out of the list straight away, but AnimatePresence keeps the node on screen for the length of its exit animation and the buttons stay live the whole way out, so a double click on "Update now" sent DOWNLOAD_UPDATE twice and raised two "Starting download" toasts. The main process refused the second real download, so this was cosmetic, but visible. The handler now refuses to run twice for the same toast, synchronously on the click. The startup update check had no catch, so launching a packaged build with no network threw an unhandled rejection in the main process. It logs now, and tests/security-boundaries.test.ts holds the call and its catch together the way it already holds the other main-process properties. A download that failed left a red task and nothing else until the launcher was restarted, despite the previous description claiming otherwise. The updater's error event now clears the re-entrancy guard in the handlers and the renderer offers the same version again, so a retry is possible without relaunching. An error that arrives before anyone accepted anything is a failed check rather than a failed download and offers nothing.
* fix(account): carry the service's refusal reason into the login verdict and log it * fix(account): stop reporting a network failure as invalid credentials
A player who deliberately keeps a Mod at an older version, because the newer one is a release candidate or because updating it would touch an existing world, had two options: update it with everything else, or never press Update all. Suspending one Mod is the missing middle. The suspension is a modid list in the config, same shape as favMods, and it is read in exactly one place: the list Update all builds. The row keeps its update notice and keeps its own update button, because watching for the new version is the reason to suspend in the first place, and an update the player asks for by hand leaves the suspension standing.
The name field and the data folder on the Add Installation page were independent: you typed a name, then went and appended it to the folder path yourself, and renaming before saving left the folder on the old text. The Add Version page already solved this with useVersionInstallFolder, so this is the same hook shape applied to installations. useInstallationFolder derives the folder from the configured installations folder plus the cleaned-up name, re-derives on either side changing, and stops for good once the user takes the field over. It differs from the versions hook on one point: typing in the folder input counts as taking it over. On the versions page the other half of the suggestion is a version picked from a list, so it settles early and typing can stay harmless. Here the name is free text the user keeps editing, and a folder they typed themselves would be thrown away on the next keystroke in the name field. The base folder is now part of what the suggestion tracks. It arrives empty on the first render and only fills in once the config has loaded, so a suggestion built before that has to be rebuilt after. cleanFolderName picks up the last two things the path layer refuses in assertSafeFileName: names longer than 255 characters, and dot-only segments such as "." and "..". Both come back empty now, and the installation folder appends nothing rather than a folder named after punctuation. The backup file names built from the same helper already had a fallback for an empty result, so they gain the same protection. useCleanFolderName and useDefaultInstallationPath were wrappers with no React state in them and one consumer each, so they go away. Co-authored-by: Pixnop <pixnop@users.noreply.github.com>
* test: rename the mods grid suite to what it actually pins The suite mocks ModListCard and wraps its own spy in memo(), so removing memo() from the real component leaves it green. What it does catch is a prop losing referential stability between ListMods and the cards: adding an unstable dependency to onSelectMod's useCallback fails it. Renamed the file and the assertions accordingly, and pointed the comments at modListCardMemo.test.tsx for the memo pin on the real export. * docs(styles): correct the skip-offscreen-render comment Table.tsx uses AnimatePresence but never useInView; only Grid.tsx imports it. The comment credited both files with both, so it named a reason for keeping items mounted that Table.tsx does not have. * fix(versions): trim a hand-typed game version before registering it A version made only of spaces is falsy to a person but truthy to the guard, so it passed the missing-folder-or-version check and got written to the config with its whitespace intact, where it matched nothing. Trimming before the guard also keeps a padded version from being stored padded.
PR #169 recompressed this file but left it at 3840x2160, and the review pointed out that the dimensions are where the memory actually is: Chromium decodes to the intrinsic size, so every window paid for a 4K bitmap no matter how small it was. The decoded ceiling drops from 31.6 MiB (3840*2160*4) to 14.1 MiB (2560*1440*4). Re-exported from the pre-#169 quality-99 original recovered from git history rather than from the shipped quality-76 file, which avoids a second generation of JPEG loss. Lanczos in linear light, then quality 92 with 4:4:4 chroma. Measured at 40.49 dB PSNR against an uncompressed Lanczos downscale of that original, matching the 40.53 dB bar #169 held itself to. File size 1,581,834 -> 1,381,147 bytes. 2560x1440 lands 1:1 on a maximized window on a 1440p panel and still has headroom for a 1280-wide window at devicePixelRatio 2. A maximized window on a 4K panel upscales 1.5x, which sits under the 2px backdrop-blur the root div already applies over this image.
…182) RUN_INSTALLER's payload read called runTrackedWorker directly while every other archive-shaped operation went through archiveConcurrency. That let an install start a third CPU-bound archive reader alongside two already-running extractions or compressions, against a limit written to allow two. The read is the same kind of work the limit describes even though it spawns no 7-Zip: src/domain/inno decodes the installer's LZMA2 payload in plain TypeScript and CRC32s every chunk, so it saturates a core inside its worker thread for the whole read. Queueing it cannot expire it. ConcurrencyLimiter.run awaits acquire() before calling the task, and runTrackedWorker arms its WORKER_TIMEOUTS_MS bound inside the promise it builds when called, so the clock starts on the slot and not on the call. The spawnInstaller fallback stays outside the limiter on purpose, documented next to it: it waits on a real installer process rather than a decode, it only runs after the reader already released its slot, and it resolves on every outcome where the limiter would introduce a rejection.
The open-mods-folder action reused the update-all tooltip. Give it its own string in en-US and pt-BR.
The install popup and the installed-mods scan both map or iterate releases directly, so a detail response without the array crashed them instead of reaching their failure states.
#206) COPY_TO_ICONS answered eight different refusals with the same bare { status: false } and wrote nothing to the log, so a player whose icon was rejected got one sentence that named no reason and left no trace to read afterwards. The renderer made it worse in the two places it awaited the bridge without a catch: a rejected invoke travelled back up through the popup's own async onClick, where nothing handled it, and the flow ended with no notification at all. The channel now returns a typed reason (unsupported-format, source-unavailable, copy-failed) and logs the error behind it at debug. describeAddCustomIconFailure maps those plus the two renderer-side refusals onto their own en-US sentences, following the same shape as the delete, uninstall and backup flows. The extension gate was checked for case sensitivity and is already case-insensitive at all four points that compare .png, so an icon named ICON.PNG has never been the problem. There is an acceptance row pinning that now. Closes #202
… lands (#200) (#208) The main process rounds the download percentage before it reaches the renderer, so a tick at 99.6 percent arrived as 100 and the task listener treated that as the download finishing. Bytes were still moving at that point, and on Windows the last stretch overlaps the installer signature check, so the task list could show Completed while the update was not yet on disk. The progress listener now only ever moves the bar. update-downloaded, which fires when electron-updater has the verified file, is the only route to the completed state.
…ting it (#209) The launcher wrote its stored session into an installation's clientsettings.json on every launch. Once that stored session was invalidated by a login somewhere else, the game prompted, the player logged in, the game wrote a working session into the same file, and the next launch put the dead one straight back. A prompt every launch, forever. writeClientSettingsSession now reads the file first, as it already did, and steps aside when it finds a session key that is not ours on an account whose playeruid is. That can only have come from the game storing what the auth service accepted, so it is newer and it works. The new "adopted" outcome carries the secrets back to EXECUTE_GAME, which puts them in the same encrypted store LOGIN writes to. A session belonging to a different playeruid is still overwritten: adopting one would leave the launcher showing one account while holding another account's credentials. Refs #204
The background stops being a fixed asset and becomes a choice with three answers: the scene bundled with the app, a scene downloaded from the repository's backgrounds branch, or a picture the player supplies. The bundled scene stays the default and the offline answer, so a first launch with no network looks exactly as it did before. Closes #192.
… (#212) The Installations list renders the config array in order, with nothing sorting it on the way to the screen, so the stored order already was the display order. It just had no way to change it once an Installation was added. Each row gains an up and a down icon button dispatching a single MOVE_INSTALLATION action that swaps the entry with its neighbour. Up is disabled on the first row and down on the last, so neither wraps around. ConfigProvider's existing save effect persists the new order like any other config change.
COPY_TO_ICONS decided whether a picked file was a PNG by reading its extension, so a JPEG renamed to .png was copied into the Icons folder and served through the icons: protocol without anything ever looking at its bytes. The background flow stopped doing that in #207, where isJpegBytes confirms the SOI marker before a picture is cached or served. isPngBytes joins isJpegBytes in src/domain/backgrounds.ts and answers the same question about the same kind of file. The handler reads the first eight bytes through a file handle rather than pulling the whole picture into memory, since a custom icon has no size ceiling of its own, and a mismatch comes back as unsupported-format, the reason #206 already gave a file that is not a PNG.
Zaldaryon
approved these changes
Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything merged since the beta.2 promotion, aligned onto main for the third prerelease.
Field reports answered: custom icons name their refusal instead of failing silently (twelve refusal points had no message and no log line between them), installations can be reordered, and the launcher no longer overwrites the session the game refreshed, which was turning a single invalidated login into a prompt on every launch. Requested features: per-mod update suspension, the data folder following the typed installation name, and the background selector with its repository-hosted catalog plus a custom image option. Reliability: consent before an update downloads with a real progress bar, the install popup failing honestly instead of spinning forever, the update task completing only when the download actually lands, image validation by signature rather than file name, and the test suites no longer writing log files into the developer's home directory.
After this merges: tag v1.7.0-beta.3, let the release workflow build the artifact set, review the draft, publish as a prerelease.