Skip to content
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

Check optional dependencies when downloading missing dependencies #848

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion Celeste.Mod.mm/Mod/Everest/Everest.Loader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ internal static void LoadAuto() {
.Select(Path.GetFileName)
.Where(file => file.EndsWith(".zip") && ShouldLoadFile(file))
.ToArray();

string[] dirs = Directory
.GetDirectories(PathMods)
.OrderBy(f => f) //Prevent inode loading jank
Expand Down Expand Up @@ -921,6 +921,35 @@ public static bool TryGetDependency(EverestModuleMetadata dep, out EverestModule
return false;
}

/// <summary>
/// Fetch a optional dependency if it is loaded.
/// Can be used by mods manually to f.e. activate / disable functionality.
/// </summary>
/// <param name="dep">Dependency to check for. Only Name will be checked.</param>
/// <param name="module">EverestModule for the dependency if found, null if not.</param>
/// <returns>True if the dependency has already been loaded by Everest, false otherwise.</returns>
public static bool TryGetDependencyIgnoreVersion(EverestModuleMetadata dep, out EverestModule module) {
string depName = dep.Name;
Version depVersion = dep.Version;

// Harcode EverestCore as an alias for the core module
if (depName == CoreModule.NETCoreMetaName)
depName = CoreModule.Instance.Metadata.Name;

lock (_Modules) {
foreach (EverestModule other in _Modules) {
EverestModuleMetadata meta = other.Metadata;
if (meta.Name != depName)
continue;

module = other;
return true;
}
}
module = null;
return false;
}

/// <summary>
/// Checks if the given version number is "compatible" with the one required as a dependency.
/// </summary>
Expand Down
159 changes: 130 additions & 29 deletions Celeste.Mod.mm/Mod/UI/OuiDependencyDownloader.cs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Celeste.Mod.UI {
class OuiDependencyDownloader : OuiLoggedProgress {
public static List<EverestModuleMetadata> MissingDependencies;

public List<string> ModPathToLoad;
private bool shouldAutoExit;
private bool shouldRestart;

Expand All @@ -25,6 +26,7 @@ public override IEnumerator Enter(Oui from) {

Title = Dialog.Clean("DEPENDENCYDOWNLOADER_TITLE");
task = new Task(downloadAllDependencies);
ModPathToLoad = new List<string>();
Lines = new List<string>();
Progress = 0;
ProgressMax = 0;
Expand Down Expand Up @@ -70,7 +72,16 @@ private void downloadAllDependencies() {
if (modDependencyGraph != null) {
addTransitiveDependencies(modDependencyGraph);
}

lock (Everest._Modules) {
if (Everest._Modules.SelectMany(mod => mod.Metadata.OptionalDependencies).Any(install => {
if (MissingDependencies.Any(todo => todo.Name == install.Name)) {
return true;
}
return false;
})) {
shouldRestart = true;
}
}
// load information on all installed mods, so that we can spot blacklisted ones easily.
LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_LOADING_INSTALLED_MODS"));

Expand Down Expand Up @@ -206,11 +217,8 @@ private void downloadAllDependencies() {
Everest.Loader._Blacklist.RemoveWhere(item => item == modFilename);

// hot load the mod
if (modFilename.EndsWith(".zip")) {
Everest.Loader.LoadZip(Path.Combine(Everest.Loader.PathMods, modFilename));
} else {
Everest.Loader.LoadDir(Path.Combine(Everest.Loader.PathMods, modFilename));
}
string path = Path.Combine(Everest.Loader.PathMods, modFilename);
ModPathToLoad.Add(path);
} catch (Exception e) {
// something bad happened during the mod hot loading, log it and prompt to restart the game to load the mod.
LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_UNBLACKLIST_FAILED"));
Expand All @@ -222,6 +230,23 @@ private void downloadAllDependencies() {
}
}

if (!shouldRestart && everestVersionToInstall == null) {
foreach (string path in ModPathToLoad) {
try {
if (File.Exists(path)) {
Everest.Loader.LoadZip(path);
} else {
Everest.Loader.LoadDir(path);
}
} catch (Exception e) {
LogLine(string.Format(Dialog.Get("DEPENDENCYDOWNLOADER_INSTALL_FAILED"), Path.GetFileName(path)));
Logger.LogDetailed(e);
shouldAutoExit = false;
break;
}
}
}

// display all mods that couldn't be accounted for
if (shouldUpdateEverestManually)
LogLine(Dialog.Clean("DEPENDENCYDOWNLOADER_MUST_UPDATE_EVEREST"));
Expand Down Expand Up @@ -275,34 +300,110 @@ private void downloadAllDependencies() {
}
}

class DependencyNode {
public bool Optional;
public EverestModuleMetadata metadata;
public List<DependencyNode> Dependencies = new List<DependencyNode>();
public int RefCount = 1;
public DependencyNode(EverestModuleMetadata from, bool opt) {
Optional = opt;
metadata = from;
}
}
private static void addTransitiveDependencies(Dictionary<string, EverestModuleMetadata> modDependencyGraph) {
List<EverestModuleMetadata> newlyMissing = new List<EverestModuleMetadata>();
do {
Logger.Verbose("OuiDependencyDownloader", "Checking for transitive dependencies...");

newlyMissing.Clear();

// All transitive dependencies must be either loaded or missing. If not, they're added as missing as well.
foreach (EverestModuleMetadata metadata in MissingDependencies) {
if (!modDependencyGraph.TryGetValue(metadata.Name, out EverestModuleMetadata graphEntry)) {
Logger.Verbose("OuiDependencyDownloader", $"{metadata.Name} was not found in the graph");
} else {
foreach (EverestModuleMetadata dependency in graphEntry.Dependencies) {
if (Everest.Loader.DependencyLoaded(dependency)) {
Logger.Verbose("OuiDependencyDownloader", $"{dependency.Name} is loaded");
} else if (MissingDependencies.Any(dep => dep.Name == dependency.Name) || newlyMissing.Any(dep => dep.Name == dependency.Name)) {
Logger.Verbose("OuiDependencyDownloader", $"{dependency.Name} is already missing");
} else {
Logger.Verbose("OuiDependencyDownloader", $"{dependency.Name} was added to the missing dependencies!");
newlyMissing.Add(dependency);
//Topological Sorting
List<DependencyNode> missingDependencies = MissingDependencies.Select(x => new DependencyNode(x, false) { RefCount = 0, }).ToList();
int index;
Logger.Verbose("OuiDependencyDownloader", "Checking for transitive dependencies...");
for (int i = 0; i < missingDependencies.Count; i++) {
// all mods in fromt of i is
// | optional -> no dependencies was added [a]
// | not opt -> all dependencies was added [b]
//
// all mods behind i is
// | optional -> no dependencies was added [c]
// | not opt -> no dependencies was added [d]

DependencyNode metadatanode = missingDependencies[i];
if (metadatanode.Optional) {
// we're moving [c] to [a]
// no-op
continue;
}
// we're moving [d] to [b]
EverestModuleMetadata metadata = metadatanode.metadata;
if (!modDependencyGraph.TryGetValue(metadata.Name, out EverestModuleMetadata graphEntry)) {
Logger.Verbose("OuiDependencyDownloader", $"{metadata.Name} was not found in the graph");
} else {
foreach ((EverestModuleMetadata dependency, bool _opt) in graphEntry.Dependencies.Select(x => (x, false)).Concat(graphEntry.OptionalDependencies.Select(x => (x, true)))) {
bool opt = _opt;
bool has = Everest.Loader.TryGetDependencyIgnoreVersion(dependency, out EverestModule module);
bool version = has && Everest.Loader.VersionSatisfiesDependency(dependency.Version, module.Metadata.Version);
bool loaded = false;
if (opt && has) {
opt = false;
}
if (has && version) {
loaded = true;
}
//if (opt) {
// loaded = false;
//}
if (loaded) {
// it's already loaded, so we can ignore it.
// if other mods are not satisfied with its version, we need reboot.
// in this case, it's not important if it's not ordered.
Logger.Verbose("OuiDependencyDownloader", $"{dependency.Name} is loaded");
} else if ((index = missingDependencies.FindIndex(dep => dep.metadata.Name == dependency.Name)) > 0) {
DependencyNode found = missingDependencies[index];
found.RefCount++;
Logger.Verbose("OuiDependencyDownloader", $"{dependency.Name} is already missing");
if (!Everest.Loader.VersionSatisfiesDependency(dependency.Version, found.metadata.Version)) {
Logger.Verbose("OuiDependencyDownloader", $"but is outdated.");
found.metadata = dependency;
}
if (found.Optional && !opt) {
found.Optional = false;
if (index < i) {
// [a] -> [b]
// actually it's [a] -> [d]
missingDependencies.RemoveAt(index);
missingDependencies.Add(found);
i--;
} // else { [c] -> [d] nop }
}
metadatanode.Dependencies.Add(found);
} else {
EverestModuleMetadata toadd = dependency;
Logger.Verbose("OuiDependencyDownloader", $"{toadd.Name} was added to the missing dependencies!");
DependencyNode item = new DependencyNode(toadd, opt);
missingDependencies.Add(item);
metadatanode.Dependencies.Add(item);
}
}
}
}
List<EverestModuleMetadata> Result = new List<EverestModuleMetadata>();
List<DependencyNode> Prepared = missingDependencies.Where(x => x.RefCount == 0).ToList();
for (int i = 0; i < Prepared.Count; i++) {
DependencyNode metadatanode = Prepared[i];
if (metadatanode.Optional) {
continue;
}
Result.Add(metadatanode.metadata);
foreach (DependencyNode dep in metadatanode.Dependencies) {
dep.RefCount--;
if (dep.RefCount == 0) {
Prepared.Add(dep);
}
}
}
if (Prepared.Count != missingDependencies.Count) {
Logger.Verbose("OuiDependencyDownloader", $"WA");
}
MissingDependencies = Result;

MissingDependencies.AddRange(newlyMissing);

} while (newlyMissing.Count > 0);
MissingDependencies.Reverse();
}

private static bool tryUnblacklist(EverestModuleMetadata dependency, Dictionary<EverestModuleMetadata, string> allModsInformation, HashSet<string> modsToUnblacklist) {
Expand Down Expand Up @@ -480,7 +581,7 @@ private void downloadDependency(ModUpdateInfo mod, EverestModuleMetadata install
File.Delete(installDestination);
}
File.Move(downloadDestination, installDestination);
Everest.Loader.LoadZip(installDestination);
ModPathToLoad.Add(installDestination);
}

} catch (Exception e) {
Expand Down