From 3df6eca8b0efd42018ae1398db7ae8d7ba26459c Mon Sep 17 00:00:00 2001 From: std-external <201375710+std-external@users.noreply.github.com> Date: Sat, 31 Oct 2026 19:04:39 +0800 Subject: [PATCH] fix(profile): keep Mods enabled outside CeleMod enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 预设模式在每次启动时都会把 blacklist.txt 重建成白名单。之前的修复只 收养 CeleMod「没见过」的 Mod,而 CeleMod 见过的 Mod 若是在游戏里的 Toggle Mods 菜单(或别的启动器)里被重新启用,下一次启动应用预设时 又会被关掉。 现在区分两种情况:当前 blacklist.txt 仍加载、且没有任何预设启用的 Mod,说明这次启用来自 CeleMod 之外,按当前状态收养进正在应用的预设; 若另一个预设本来就启用它,则保持预设切换的语义不变。「下载后默认 行为」只决定新加入的 Mod 的状态,不再影响这种外部启用。 --- src/celemod-ui/src/modAdoption.test.ts | 33 ++++++++++++++++++++++++-- src/celemod-ui/src/modAdoption.ts | 31 ++++++++++++++++++++---- src/celemod-ui/src/states.ts | 1 + 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/celemod-ui/src/modAdoption.test.ts b/src/celemod-ui/src/modAdoption.test.ts index 7330108..0964507 100644 --- a/src/celemod-ui/src/modAdoption.test.ts +++ b/src/celemod-ui/src/modAdoption.test.ts @@ -8,6 +8,7 @@ const settings = { { name: "Copied", file: "Copied.zip" }, ], profileEnabledNames: ["Enabled"], + allProfileEnabledNames: ["Enabled"], blacklistEnabledNames: ["Enabled", "Copied"], knownNames: ["Enabled"], alwaysOnNames: [], @@ -18,9 +19,37 @@ test("adopts a Mod that was copied into the Mods folder", () => { assert.deepEqual(selectAdoptableModNames(settings), ["Copied"]); }); -test("keeps Mods CeleMod already saw out of the adoption", () => { +test("adopts a known Mod that was enabled outside CeleMod", () => { + // The game's "Toggle Mods" menu only edits blacklist.txt, so a Mod CeleMod + // already knows can still be enabled while no profile enables it. Applying + // the profiles must not turn it off again. assert.deepEqual( - selectAdoptableModNames({ ...settings, knownNames: ["Enabled", "Copied"] }), + selectAdoptableModNames({ + ...settings, + knownNames: ["Enabled", "Copied"], + }), + ["Copied"], + ); +}); + +test("adopts a known Mod enabled outside CeleMod whatever the download default is", () => { + assert.deepEqual( + selectAdoptableModNames({ + ...settings, + knownNames: ["Enabled", "Copied"], + enableNewMods: false, + }), + ["Copied"], + ); +}); + +test("keeps a known Mod another profile enables out of the adoption", () => { + assert.deepEqual( + selectAdoptableModNames({ + ...settings, + knownNames: ["Enabled", "Copied"], + allProfileEnabledNames: ["Enabled", "Copied"], + }), [], ); }); diff --git a/src/celemod-ui/src/modAdoption.ts b/src/celemod-ui/src/modAdoption.ts index 7d3d4fe..b83f810 100644 --- a/src/celemod-ui/src/modAdoption.ts +++ b/src/celemod-ui/src/modAdoption.ts @@ -19,6 +19,12 @@ export interface ModAdoptionInput { installed: readonly InstalledModLike[]; /** Mod names the profiles that are about to be applied enable. */ profileEnabledNames: readonly string[]; + /** + * Mod names every stored profile enables, including the inactive ones. A + * Mod that is only enabled on disk because another profile owns it keeps + * that profile's state and is not adopted here. + */ + allProfileEnabledNames: readonly string[]; /** * Mod names the current `blacklist.txt` still loads. Every other installed * Mod was disabled by CeleMod on purpose and must stay disabled. @@ -26,7 +32,9 @@ export interface ModAdoptionInput { blacklistEnabledNames: readonly string[]; /** * Mod names CeleMod already saw. They are not newly added, so they keep the - * state the profiles and the blacklist gave them. + * state the profiles and the blacklist gave them, except for a Mod that is + * enabled on disk although no profile enables it: something outside CeleMod + * turned that one on (see the helper below). */ knownNames: readonly string[]; alwaysOnNames: readonly string[]; @@ -38,19 +46,26 @@ const normalize = (name: string) => name.trim().toLowerCase(); /** * Names of the installed Mods that CeleMod should adopt into the applied - * profiles. Returns an empty list when the default is "disable" - applying - * the profiles then disables those Mods, which is exactly that setting. + * profiles. New Mods follow the "Mod 下载后默认行为" setting: an empty list + * when the default is "disable", which is exactly that setting. + * + * A Mod that CeleMod already saw is not new, so it normally keeps the state the + * profiles gave it. The exception is a Mod that `blacklist.txt` still loads + * while no profile enables it: Everest's "Toggle Mods" menu and other launchers + * only write `blacklist.txt`, so that enabled state came from outside CeleMod + * and applying the profiles would silently turn it off again. */ export const selectAdoptableModNames = ({ installed, profileEnabledNames, + allProfileEnabledNames, blacklistEnabledNames, knownNames, alwaysOnNames, enableNewMods, }: ModAdoptionInput): string[] => { - if (!enableNewMods) return []; const profileEnabled = new Set(profileEnabledNames.map(normalize)); + const anyProfileEnabled = new Set(allProfileEnabledNames.map(normalize)); const blacklistEnabled = new Set(blacklistEnabledNames.map(normalize)); const known = new Set(knownNames.map(normalize)); const alwaysOn = new Set(alwaysOnNames.map(normalize)); @@ -63,7 +78,13 @@ export const selectAdoptableModNames = ({ handled.add(key); if (profileEnabled.has(key)) continue; if (!blacklistEnabled.has(key)) continue; - if (known.has(key) || alwaysOn.has(key)) continue; + if (alwaysOn.has(key)) continue; + if (known.has(key)) { + if (anyProfileEnabled.has(key)) continue; + adopted.push(mod.name); + continue; + } + if (!enableNewMods) continue; adopted.push(mod.name); } return adopted; diff --git a/src/celemod-ui/src/states.ts b/src/celemod-ui/src/states.ts index 432ebe2..af78713 100644 --- a/src/celemod-ui/src/states.ts +++ b/src/celemod-ui/src/states.ts @@ -532,6 +532,7 @@ export const adoptUntrackedMods = async ( profileEnabledNames: selectedProfiles.flatMap( (profile) => profile.enabled_mods, ), + allProfileEnabledNames: profiles.flatMap((profile) => profile.enabled_mods), blacklistEnabledNames: direct.enabled_mods, knownNames: state.knownMods[gamePath] ?? [], alwaysOnNames: state.alwaysOnMods,