-
Notifications
You must be signed in to change notification settings - Fork 97
Creation games loadorder #3963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Creation games loadorder #3963
Conversation
…on, and refine file indexing logic.
- Added SQL macros for plugin load order management (`load_order_plugin_files` and `plugin_sort_order`). - Implemented `PluginLoadOrderVariety` for sorting and managing plugins. - Extended `SkyrimSE` with sort order support via `ISortOrderVariety`. - Introduced models and attributes (`PluginSortEntry`, `PluginReactiveSortItem`, `ModKeyAttribute`) to enhance plugin data representation. - Updated `NexusMods.MnemonicDB` packages to version `0.28.1` for compatibility. - Adjusted project files to include SQL resources and align with the new load order functionality.
- Replaced unused and commented-out code in `PluginsFile.Write` with a simplified implementation based on database queries to generate sorted plugin lists. - Refined SQL macros and added ordering in `plugin_sort_order`. - Updated `PluginLoadOrderVariety` to improve item sorting and persist logic, including ordering and deletion checks.
…ntrinsic file path handling. This was causing a bug where disabled files would never show up as disabled in these queries
…plementation to ensure compatibility with future file scanner changes.
…sort logic - Introduced `PluginLoadOrderIntegrationTests` to validate plugin sorting and load order management behaviors. - Updated `plugin_sort_order` SQL to include explicit `ORDER BY SortIndex` for consistent results. - Extended test framework with additional dependencies for plugin file handling.
| { | ||
| private const string Namespace = "NexusMods.Games.CreationEngine.PluginSortEntry"; | ||
|
|
||
| public static readonly ModKeyAttribute ModKey = new(Namespace, nameof(ModKey)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a comment to clarify what ModKey refers to in this context, as it could easily be equivocated to mean an Id for the LoadoutItemGroup mod, rather than an Id for the plugin.
I'm considering whether to use different names and aliases on top of the Mutagen ModKey, to avoid equivocation since in our code domain Mod means something different.
|
|
||
| namespace NexusMods.Games.CreationEngine.LoadOrder; | ||
|
|
||
| public class PluginLoadOrderVariety : ASortOrderVariety< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be named PluginSortOrderVariety for consistency
| public class PluginLoadOrderVariety : ASortOrderVariety< | |
| public class PluginSortOrderVariety : ASortOrderVariety< |
| public override async ValueTask<SortOrderId> GetOrCreateSortOrderFor(LoadoutId loadoutId, OneOf<LoadoutId, CollectionGroupId> parentEntity, CancellationToken token = default) | ||
| { | ||
| var optionalSortOrderId = GetSortOrderIdFor(parentEntity); | ||
| if (optionalSortOrderId.HasValue) | ||
| return optionalSortOrderId.Value; | ||
|
|
||
| token.ThrowIfCancellationRequested(); | ||
|
|
||
| using var ts = Connection.BeginTransaction(); | ||
| var newSortOrder = new SortOrder.New(ts) | ||
| { | ||
| LoadoutId = loadoutId, | ||
| ParentEntity = parentEntity, | ||
| SortOrderTypeId = SortOrderVarietyId.Value, | ||
| }; | ||
|
|
||
| var commitResult = await ts.Commit(); | ||
|
|
||
| var sortOrder = commitResult.Remap(newSortOrder); | ||
| return sortOrder; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should move this into ASortOrderVariety directly.
Only reason RedMods have an override is that they have the extra RedModSortOrder model from previous iteration of the system. It's superfluous, but it was simpler to keep it rather than do a migration to remove it.
| $""" | ||
| SELECT ModKey, GroupName, GroupId, SortIndex | ||
| FROM creation_engine.plugin_sort_order({Connection}) WHERE SortOrderId = {sortOrderId.Value} | ||
| """) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As final cleanup, I moved all the queries out from the SortOrderVariety file into a separate file (RedModExtensions, but maybe PluginQueries.cs would be better here).
I think it makes it easier to find them and update them if we change something with the query system (e.g. we want to use something like Dapper to generate the SQL or some other change).
In general I like to keep the implementation details of how the data is accessed in the DB separate from the logic.
| // Insert new items at the start, sorted by newest creation order (ModGroupId) | ||
| // For cyberpunk RedMods, lower index wins, so we add new items at the start | ||
| results.InsertRange(0, itemsToAdd); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New items should be added at the end of the sort order for Bethesda games, rather than at the start.
We want newly added items to win by default, and for bethesda games we need to put them at the end of the order.
src/NexusMods.Games.CreationEngine/LoadOrder/PluginLoadOrderVariety.cs
Outdated
Show resolved
Hide resolved
| file.Size, | ||
| file.ItemType, | ||
| file.Id, | ||
| regexp_extract(file.Path.Path, '([^/]+)$') ModKey, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you interested in the entire path or just the filename? I think you might need to specify to capture group 1 of the pattern if just looking for the filename, as it should default to capturing the entire string otherwise.
| regexp_extract(file.Path.Path, '([^/]+)$') ModKey, | |
| regexp_extract(file.Path.Path, '([^/]+)$', 1) ModKey, |
| regexp_extract(file.Path.Path, '([^/]+)$') ModKey, | ||
| grp.Id GroupId, | ||
| grp.Name GroupName | ||
| FROM synchronizer.WinningFiles(db) file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only querying the winning plugins here is problematic because it is excluding all the plugins in disabled mods and collections.
Including plugins from disabled mods in the sorting has been an explicit design decision.
This was the solution Design came to when considering the issue of users being able to Toggle collections on and off.
If we don't include disabled items in the sorting, users would immediately lose all their sorting information upon disabling a collection.
Enabling the collection again would result in all the items getting sorted in random or alphabetical order.
Similarly, users don't want to have to reposition a plugin in the load order after having toggled the parent off and on again.
I would agree that users don't necessarily expect plugins from disabled mods to show up in the sorting view, but not including them here at the data layer would result in a very bad user experience.
MO2 has a system in place to remember the position of recently disabled items in the load order. I would need check again how the implementation worked (my impression was that it was more of a hack), but it is there to address the case of users toggling mods on and off.
I would start by including disabled items in the sorting, and I would instead look at options later to potentially filter out disabled items in the view proposed to users if we have an issue with too many plugins being shown.
| if (!x.ModId.HasValue) | ||
| return model; | ||
|
|
||
| model.ModGroupId = Optional<LoadoutItemGroupId>.Create(x.ModId!.Value); | ||
| var loadoutData = new SortItemLoadoutData<SortItemKey<ModKey>>( | ||
| model.Key, | ||
| true, | ||
| x.GroupName, | ||
| LoadoutItemGroupId.From(x.ModId!.Value)); | ||
|
|
||
| model.LoadoutData = loadoutData; | ||
| return model; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a bit of an example of what I mentioned the other day regarding GameFiles not being Loadout Files.
It would be preferrable to be able to reference them in a homogeneous way like we do with other items in the loadout, either as LoadoutItems themselves, or by using some abstraction over LoadoutItems (which sounds less doable).
Not much to do for this PR specifically, but more of a note for how we want to handle game files in the future.
| WHERE file.Path.Location = nma_fnv1a_hash_short('Game') | ||
| AND file.Path.Path SIMILAR TO '(?i)^Data/[^/]+\.(esp|esl|esm)$'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rows of this query should be deterministically sorted.
Ideally items that have been added most recently to the loadout should appear at the bottom, in case of parity I would go by plugin alphabetical order.
We want to ensure that new entries appear at the bottom of the order.
| LEFT JOIN mdb_LoadoutItem() itm on itm.Id = file.Id | ||
| LEFT JOIN mdb_LoadoutITemGroup() grp on grp.Id = itm.Parent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to pass in the db parameter to mdb_LoadoutItem() and mdb_LoadoutITemGroup(), or we might be fetching from the most recent DB rather than the one we are interested in.
| CREATE MACRO creation_engine.plugin_sort_order(db) AS TABLE | ||
| SELECT sortOrder.Id SortOrderId, items.ModKey, sortItem.SortIndex, items.GroupId, items.GroupName | ||
| FROM MDB_SORTORDER() sortOrder | ||
| INNER JOIN creation_engine.load_order_plugin_files(null) items ON items.Loadout = sortOrder.Loadout | ||
| INNER JOIN MDB_PLUGINSORTENTRY() sortItem ON sortItem.ParentSortOrder = sortOrder.Id AND sortItem.ModKey = items.ModKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is used when writing Plugins.txt during synchronizer step, it needs to access the correct Db revision.
So MDB_SORTORDER(), MDB_PLUGINSORTENTRY() should take the db parameter as well I think.
Not sure what the null is doing in this case creation_engine.load_order_plugin_files(null), I would have expected that to also need to have the db parameter passed to it.
…st framework with new test projects for SkyrimSE. Update `FileHashesService` for in-memory file system compatibility and adjust NexusMods library package versions.
…image error handling, and enhance SkyrimSE path resolution with `DataPath`.
…`.nx` archives and update integration test constructor visibility.
…`PackManifestToNx` helpers to simplify logic and reduce code duplication.
… error handling for missing environment variable, and add developer documentation for running integration tests.
…appings for Linux and Windows
…n tests as part of the main tests
…mprove filtering logic
|
This PR doesn't conflict with |
… `System.IO.Compression` in `ManagedZipExtractor`
|
This PR conflicts with |
| throw new NotSupportedException(); | ||
| using var owner = MemoryPool<byte>.Shared.Rent(buffer.Length); | ||
| var sliced = owner.Memory[..buffer.Length]; | ||
| Task.Run(() => ReadChunkAsync(sliced, chunkIndex)).Wait(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should use .GetAwaiter().GetResult() instead of .Wait() for better handling of exceptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Task.Run(() => ReadChunkAsync(sliced, chunkIndex)).Wait(); | |
| Task.Run(() => ReadChunkAsync(sliced, chunkIndex)).GetAwaiter().GetResult(); |
| public class IntegrationTestDataSource : DependencyInjectionDataSourceAttribute<IServiceProvider> | ||
| { | ||
| public override IServiceProvider CreateScope(DataGeneratorMetadata dataGeneratorMetadata) | ||
| { | ||
| throw new NotImplementedException(); | ||
| } | ||
|
|
||
| public override object? Create(IServiceProvider scope, Type type) | ||
| { | ||
| throw new NotImplementedException(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would this be for?
…riety.cs Co-authored-by: Al <[email protected]>
|
This PR doesn't conflict with |
|
This PR conflicts with |
|
This PR has been marked as stale due to inactivity. |
This implements plugin sorting for SkyrimSE/FO4. But right now tests don't pass as we need a way to efficiently test against game files and have a valid game hash database