Fix auto removal when installing w/ deps #3643
Merged
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.
Background
If you install a mod with dependencies, by default they are marked as auto-installed, and if you later uninstall the original mod, the auto-installed dependencies are supposed to be uninstalled as well automatically, see #2753.
Problems
While investigating #1268, I noticed that sometimes my auto-removable modules would not show up in the changeset. It happened when I was both removing and installing mods, but only if the installing mods had some dependencies of their own. The auto-removable modules were left in place, and after the install finished, the Apply changes button was still enabled on the main mod list, with a changeset to remove them.
Distantly relatedly (this is the deleted
TODO
inModList.cs
), if you remove the only module that depends on an auto-installed mod X and also install a new module that depends on X in the same changeset, then X will be removed in the uninstall step and re-installed in the install step. It should just be left alone.Causes
I found this bug particularly complicated and obscure, so I will try to spell it out as carefully as possible.
The auto-removal logic works by first calculating an auto-installed module's reverse dependencies within {the changeset combined with the installed modules}, i.e., the mods that depend on this mod, directly or indirectly, after the changeset is performed. Then it checks to see whether those reverse dependencies are a subset of the auto-installed mods; if so, then the module can be removed. If any user-selected mod depends on an auto-installed mod, then it is not auto-removed. This algorithm is sound (but there was a problem with the implementation; keep reading).
To calculate the reverse dependencies of a mod, we take a copy of the set of mods and remove that mod from it. Then we check whether any of the remaining mods have unsatisfied dependencies, and if they do, we remove them as well. The logic then repeats until none of the remaining mods have unsatisfied dependencies; at that point, the mods that have been removed up to that point are considered the reverse dependencies.
The unstated assumption here is that the changeset used to build the set of mods is the full changeset. Otherwise, you could have a set of mods that had unsatisfied dependencies before any mods were removed from it!
That unstated assumption was not satisfied by
Registry.FindRemovableAutoInstalled
. The list of mods that it used for finding reverse dependencies was the installed modules combined with the user-selected changeset, not including dependencies. This explains why installing a mod with dependencies broke the auto-remove feature: the mod with dependencies (which were always unsatisfied unless the user selected them manually) was erroneously counted as a reverse dependency of every auto-installed mod, and it wasn't auto-installed, so we determined that nothing could be auto-removed in that pass.Changes
Registry.FindRemovableAutoInstalled
calculates the full changeset associated with its inputs, which it then checks for auto-removable modulesRelationshipResolver
, which shouldn't be used inside ofRegistry
becauseRegistry
is supposed to be a data object that doesn't need to know the complexities of relationship resolution. (It might also create a circular reference, I didn't check that.) Luckily we already have a class that acts as a thin app logic layer aroundRegistry
, and in fact already usesRelationshipResolver
:IRegistryQuerier
; henceRegistry.FindRemovableAutoInstalled
has been moved toIRegistryQuerier.FindRemovableAutoInstalled
RelationshipResolver
needs aGameVersionCriteria
, so nowIRegistryQuerier.FindRemovableAutoInstalled
does as wellGUI.Main.Instance
withinGUI.Main
itself are changed tothis
(or simply omitted, when referencing a class member)GUI.Main.FetchMasterRepositoryList
is no longerstatic
because its logic depends on using an instance ofMain
, which is better expressed as a member functionIEnumerable<>
is nice because it's capable of holding a reference to an enumerable collection of any underlying type, but it has the pitfall of also being C#'s mechanism for implementing generators (see Eliminate duplicate network calls in Netkan #2928 and Memoize lazily evaluated sequences #2953). For example, if I setIEnumerable<string> s = someArray.Select(x => x.ToString())
, and then save a reference tos
or pass it to other functions, that expression might be evaluated multiple times, repeating logic and slowing down the app. Each function that uses such a value has to be written carefully to avoid this, which requires a fairly deep understanding of C#.The ideal use of
IEnumerable<>
is for a function that legitimately can/should be a generator, possibly using another generator as input, on which the calling function can then call.ToList()
,.ToHashSet()
,.ToDictionary()
, etc., depending on which type it needs.Now many of our uses of
IEnumerable<>
for member variables and function parameters are changed toList
orHashSet
(or changed between those types where appropriate), to ensure that we hold a reference to a real object rather than a generator that might be lazily evaluated at some point in the future.GUI.MainTabControl
was a class that existed just to set focus after the main tabstrip firedOnSelectedIndexChanged
. This logic is now moved toMain.MainTabControl_OnSelectedIndexChanged
, and the class is deleted and replaced byThemedTabControl
, its parent class.Main.ManageMods_OnRefresh
is renamed toMain.RefreshModList
becauseManageMods
wasn't actually using it(I had an issue with DLCs breaking the resolver, which I couldn't figure out because my
log.Debug
messages were accidentally suppressed by mylog4net.xml
configuration file. By the time I fixed that, I ended up with messages that more clearly illuminated the proceedings.)