diff --git a/src/renderer/src/extensions/gamebryo_plugin_management/util/PluginPersistor.test.ts b/src/renderer/src/extensions/gamebryo_plugin_management/util/PluginPersistor.test.ts index 0f9a973fdb..d1239a50f7 100644 --- a/src/renderer/src/extensions/gamebryo_plugin_management/util/PluginPersistor.test.ts +++ b/src/renderer/src/extensions/gamebryo_plugin_management/util/PluginPersistor.test.ts @@ -344,6 +344,38 @@ describe("PluginPersistor", () => { expect(persistor.entry("old.esp").enabled).toBe(true); }); + it("does not report an empty plugins.txt when only the game's own plugins are known", async () => { + paths.native = ["skyrim.esm"]; + paths.pluginDir = tmpDir("plugin-vanilla-"); + nodeFs.writeFileSync(path.join(paths.pluginDir, "plugins.txt"), "", { encoding: "latin1" }); + const record = vi.fn(); + const vanilla = new PluginPersistor(vi.fn(), () => false, record); + persistors.push(vanilla); + vanilla.setResetCallback(vi.fn(() => Promise.resolve())); + vanilla.setKnownPlugins({ "skyrim.esm": "Skyrim.esm" }); + + await vanilla.loadFiles("skyrimse"); + await settle(SETTLE_MS); + + expect(record).not.toHaveBeenCalled(); + }); + + it("reports an empty plugins.txt when mod plugins are known", async () => { + paths.native = ["skyrim.esm"]; + paths.pluginDir = tmpDir("plugin-emptied-"); + nodeFs.writeFileSync(path.join(paths.pluginDir, "plugins.txt"), "", { encoding: "latin1" }); + const record = vi.fn(); + const modded = new PluginPersistor(vi.fn(), () => false, record); + persistors.push(modded); + modded.setResetCallback(vi.fn(() => Promise.resolve())); + modded.setKnownPlugins({ "skyrim.esm": "Skyrim.esm", "old.esp": "Old.esp" }); + + await modded.loadFiles("skyrimse"); + await settle(SETTLE_MS); + + expect(record.mock.calls[0]?.[0]).toBe("the plugin list on disk is empty"); + }); + it("does not offer the keep/revert choice for an empty plugins.txt", async () => { const onExternalChange = vi.fn(() => Promise.resolve("keep" as const)); persistor.setExternalChangeCallback(onExternalChange); diff --git a/src/renderer/src/extensions/gamebryo_plugin_management/util/PluginPersistor.ts b/src/renderer/src/extensions/gamebryo_plugin_management/util/PluginPersistor.ts index 4bae16858e..690638d639 100644 --- a/src/renderer/src/extensions/gamebryo_plugin_management/util/PluginPersistor.ts +++ b/src/renderer/src/extensions/gamebryo_plugin_management/util/PluginPersistor.ts @@ -695,12 +695,14 @@ class PluginPersistor implements IPersistor { // not even a header? I don't trust this. Read once more in case we caught a write // mid-flight, then leave the current state alone: a truncated file is not a if (retry) { - // still empty on the second read, so nobody was mid-write and the load order the - // file held is gone - this.recordFailure( - "the plugin list on disk is empty", - new Error("plugins.txt has no content, not even a header"), - ); + // still empty on the second read, so nobody was mid-write; a game with only its own + // plugins leaves the file empty, so it only held a load order if mods add plugins + if (Object.keys(this.mKnownPlugins).length > this.mInstalledNative.length) { + this.recordFailure( + "the plugin list on disk is empty", + new Error("plugins.txt has no content, not even a header"), + ); + } // The persistor must still count as loaded, or serialize() drops every write this.mLoaded = true; return Promise.resolve();