Skip to content

fold gamebryo plugin management into core and harden plugin sorting - #24296

Merged
IDCs merged 98 commits into
masterfrom
integration/gamebryo
Sep 28, 2026
Merged

IDCs merged 98 commits into
masterfrom
integration/gamebryo

Conversation

@IDCs

@IDCs IDCs commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Brings the Gamebryo/Bethesda refactor from integration/gamebryo to master. The gamebryo plugin management extension is folded into the renderer, covered by characterization and contract suites on the production reducer composition, and its libloot, persistor and collection paths are fixed. Each change was reviewed
in its own PR into the integration branch:

  • #24104 pin plugin persistor and load order reducer behaviour
  • #24118 fold gamebryo-plugin-indexlock into the plugin management extension
  • #24121 retire the legacy gamebryo bsa support extension
  • #24129 rebuild the api test harness on the production reducer composition
  • #24136 extend the api harness for the gamebryo test tranche
  • #24146 add characterization and desired-contract suites for autosort
  • #24181 pin the libloot lifecycle contract for autosort
  • #24188 key plugin index locks by plugin id and keep file names in the plugins table
  • #24190 defer plugin sorting behind mod activity and report the sort outcome honestly
  • #24193 fix the lootSortAsync api contract
  • #24196 load the plugins into libloot before every sort
  • #24238 forward the Gamebryo and libloot failures that only reached the log
  • #24255 enable every plugin of a collection whose manifest lists none
  • #24258 ask the plugin-management toggle before a state sort
  • #24260 enable the plugins a collection ships and count ghosted masters as disabled
  • #24261 pin node-loot to the libloot 0.29.6 build
  • #24269 load gamebryo plugin management on Linux
  • #24271 extract the plugin sync lifecycle from the gamebryo entry module
  • #24273 read plugin names through one rule in gamebryo plugin management
  • #24275 read the store lazily in the plugin sync
  • #24294 fix plugin state issues found in live testing

IDCs added 30 commits September 7, 2026 05:50
Covers:
- missing plugins.txt handling.
- plugin format.
- blueprint exclusion.
- native load order offsets.
- the setPluginOrder clean slate and updatePluginOrder semantics.
- reducer tests drive the real action creators.

part of LAZ-1025
Partial<typeof import(util/fs)> links every member to the real
declaration so signature drift fails the typecheck; member annotations
are inferred, utimesAsync wraps in Bluebird to match the real return
type, and the watch stub casts through the linked declaration.

part of LAZ-1025
loot becomes an optionalDependency of the renderer only: it must stay out
of @vortex/main's tree or electron-rebuild force-rebuilds it against
electron headers and the Linux link fails; the install-time napi build
loads in electron without a rebuild. copy-assets lays out index.js,
async.js and the binding with libloot.dll co-located under
build/assets/loot; electron-builder unpacks the assets copy; packaging
fails if the loot assets are missing from the dist tree; prepare-dist
resolves optionalDependencies versions.

part of LAZ-1026
The extension registers under its pre-fold name (the public requireExtension
contract and i18n namespace), Windows-only until LAZ-1061. The loot binding
loads lazily from the unpacked assets tree, typed through types/ILoot.ts.
The stylesheet compiles as a built-in partial, the loot icon ships with core
assets, the plural strings move into the bundled locales, and the leftover
bundled-extension project is removed so the extension cannot load twice.

part of LAZ-1026
Fold gamebryo-plugin-management into the renderer
The locked-indices reducer and table attribute register alongside the
extension's own, the once wiring shares the extension's deploying flag
instead of tracking its own, and lock-index strings resolve through the
extension namespace with a fallback to the pre-fold one so existing
translation packs keep working. The bundled extension project is removed.

part of LAZ-1026
IPluginLoadOrderEntry describes one entry of the root loadOrder hive; the
old ILoadOrder name read as the whole order and invited confusion with the
file based load order types.

part of LAZ-1026
The incompatible-archives test registers inline beside the extension's
other health tests, state reads go through the extension's typed state and
the existing selectors, and the bundled extension project is removed.

part of LAZ-1026
Fold gamebryo-archive-check into the plugin management extension
gamebryo-archive-support provides the bsa handler, including version-aware
archive creation; the bsatk-based duplicate registered the same archive type
and ignored the version option. Also removes the now-unused bsatk dependency.

fixes LAZ-1050
retire the legacy gamebryo bsa support extension
makeApiHarness drives a real redux store: addToTree/deriveReducer (now
exported) compose the bound specs exactly as production does, thunk
middleware matches the production store, actions are recorded by
middleware, and slices seed through the production __hydrate_replace
action. Bindings are plain IExtensionReducer entries; setState applies
the draft mutation as a cloned whole-state replacement dispatch, and
pure seeding moved into harness overrides.

part of LAZ-1025
rebuild the api test harness on the production reducer composition
api.onStateChange is backed by the production ReduxWatcher over the harness
store; the api gains runExecutable/genMd5Hash and history captures, and
notifications record the full payload. The extension's reducer registrations
move to an exported REDUCER_BINDINGS list that register() loops and the
gamebryo harness binds alongside the core transaction/activity slices its
handlers read. makePlugin builds pluginList entries.

part of LAZ-1025
extend the api harness for the gamebryo test tranche
Three suites drive LootInterface through the gamebryo harness: the sort
event, the doSort error matrix, and plugin-details. Five tests assert
the desired contracts from the sorting bug tickets as test.fails, so
the suite stays green and each fix flips its test to a plain one.

part of LAZ-1025
part of LAZ-1048
part of LAZ-1049
part of LAZ-1092
add characterization and desired-contract suites for autosort
IDCs added 25 commits September 22, 2026 10:07
gameSupported honors the profile's toggle; knownGame only checks the support
table. LOOT init and a lootSortAsync file sort need a known game, since a game
extension in drag-and-drop mode sorts with the toggle off. A state sort works the
loadOrder hive, which nothing hydrates while the toggle is off, so it now asks
gameSupported and is refused instead of sorting an empty hive.

fixes LAZ-1047
enable every plugin of a collection whose manifest lists none
ask the plugin-management toggle before a state sort
The missing-masters check stats a deployed master at the path its plugin-list
entry holds, which for a ghosted plugin ends in .ghost, instead of the bare
master name in the game folder. A disabled master is reported as not enabled
again rather than removed externally, which turned the warning red and sent a
span per dependent.

part of LAZ-1030
The gamebryo collection parser switches on every plugin a member mod ships
unless the curator listed it as off. A plugin the manifest does not name was
dispatched disabled, and with autoEnable off by default it stayed that way
through every re-run of the collection rules.

fixes LAZ-1030
libloot 0.29.5 stopped hashing archive contents, so a BSA whose stored file
hashes collide parses instead of failing with "unexpected collision for file
name hash" on every plugin-info request and sort. Verified against
KhajiitChildren.bsa on a Skyrim SE install with 675 plugins: 0.29.3 logs the
error at the metadata and sort steps, 0.29.6 logs nothing.

fixes LAZ-1029
enable the plugins a collection ships and count ghosted masters as disabled
pin node-loot to the libloot 0.29.6 build
Pins node-loot to the build that compiles on Linux and copies libloot.so.0
next to the binding, where its rpath looks, the way libloot.dll is copied
on Windows.

part of LAZ-1061
load gamebryo plugin management on Linux
disable() waits its turn in the write queue, so a load order write that
lands in between still schedules a write whose timer fires after the paths
are gone. That write resolves without touching the files.

fixes LAZ-1111
pluginSync.ts owns starting, stopping and refreshing the sync between the
plugin list and the game's plugin files: it loads the persistors for the
active game, watches its Data folder and rescans once a plugin file appears
or vanishes outside a deployment, debounced through the shared Debouncer.
Stopping also drops a rescan still pending.

util/profileUserlist.ts owns the per-profile userlist swap, with the active
file's path taken from the metadata list layout and the profile folder from
profile management; the profile feature toggle handler shares its paths and
its missing-source-tolerant copy.

part of LAZ-1037
The sync suite drives makePluginSync against a real Data folder: the
persistors load behind a cleared load order, a plugin appearing rescans once
after the debounce, changes during a deployment and non-plugin files are
ignored, stop closes the watcher and disables the persistor, and refresh
asks for details of the listed plugins.

The swap suite covers saving, restoring, first-time seeding, the global
backup and a profile pending removal, on a per-test userData folder.

makeFakePersistor and settle join test-utils; the persistor and plugin list
suites use them instead of their own copies. The autosort suites and
lootMocks no longer carry a note about mock paths moving with the
decomposition, which ends here.

fixes LAZ-1037
A folder watcher on a path holding an 8.3 short name, the GitHub Windows
runners' RUNNER~1 temp folder among them, aborts node with a libuv
assertion, taking the vitest worker with it. makeTempDir resolves the
folder it creates so every suite watching one is spared.

part of LAZ-1037
extract the plugin sync lifecycle from the gamebryo entry module
isGhosted recognises the suffix in any casing, unghost strips it with the
directory kept and ghost adds it once. The plugin id, the plugin history,
the sort input, the plugins table and the ghost rename all go through them,
so a .GHOST file is disabled everywhere at once.

pluginFlags derives the flags the plugins table shows from the parsed header,
widened by the name conventions: an .esm is a master, an .esl a light master
where the game has light plugins, the medium flag passes through, and a
ghosted plugin counts by its real extension. The table calls it on the
header it already parsed, so no plugin is opened a second time and a ghosted
plugin keeps its flags, author and masters.

part of LAZ-1038
isPluginName answers whether a name carries one of the game's plugin
extensions; the plugin file check, the Data folder watcher and the mod
enabled handler all ask it, and the plugin file check reads a ghosted name
through the ghost rule. The "could be light" label checks the plugin's id,
which never carries the ghost suffix, so a ghosted .esp shows it too.

fixes LAZ-1038
read plugin names through one rule in gamebryo plugin management
part of LAZ-1037
read the store lazily in the plugin sync
- plugins without a load order position are written after the natives
- turning plugin management off for a profile stops the plugin sync; the
  toggle check reads the setting through pluginManagementEnabled
- a lootSortAsync sort leaves the load order and the pending sort marker to
  the caller
- a plugin sort requested during a profile switch waits for the next
  profile's load order
- the plugin list's loading spinner says what it is waiting for
fix plugin state issues found in live testing
@IDCs
IDCs requested a review from a team as a code owner September 24, 2026 13:44
@IDCs IDCs self-assigned this Sep 24, 2026
@IDCs
IDCs merged commit 91c2a3b into master Sep 28, 2026
14 checks passed
@IDCs
IDCs deleted the integration/gamebryo branch September 28, 2026 03:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants