/// <summary> /// Installs the specific version of an ASI to the specified target /// </summary> /// <param name="modVersion"></param> /// <param name="target"></param> /// <param name="forceSource">Null to let application choose the source, true to force online, false to force local cache. This parameter is used for testing</param> /// <returns></returns> public static bool InstallASIToTarget(ASIModVersion asi, GameTarget target, bool?forceSource = null) { if (asi.Game != target.Game) { throw new Exception($@"ASI {asi.Name} cannot be installed to game {target.Game}"); } Log.Information($@"Processing ASI installation request: {asi.Name} v{asi.Version} -> {target.TargetPath}"); string destinationFilename = $@"{asi.InstalledPrefix}-v{asi.Version}.asi"; string cachedPath = Path.Combine(CachedASIsFolder, destinationFilename); string destinationDirectory = MEDirectories.ASIPath(target); if (!Directory.Exists(destinationDirectory)) { Log.Information(@"Creating ASI directory in game: " + destinationDirectory); Directory.CreateDirectory(destinationDirectory); } string finalPath = Path.Combine(destinationDirectory, destinationFilename); // Delete existing ASIs from the same group to ensure we don't install the same mod var existingSameGroupMods = target.GetInstalledASIs().OfType <KnownInstalledASIMod>().Where(x => x.AssociatedManifestItem.OwningMod == asi.OwningMod).ToList(); bool hasExistingVersionOfModInstalled = false; if (existingSameGroupMods.Any()) { foreach (var v in existingSameGroupMods) { if (v.Hash == asi.Hash && !forceSource.HasValue && !hasExistingVersionOfModInstalled) //If we are forcing a source, we should always install. Delete duplicates past the first one { Log.Information($@"{v.AssociatedManifestItem.Name} is already installed. We will not remove the existing correct installed ASI for this install request"); hasExistingVersionOfModInstalled = true; continue; //Don't delete this one. We are already installed. There is no reason to install it again. } Log.Information($@"Deleting existing ASI from same group: {v.InstalledPath}"); v.Uninstall(); } } if (hasExistingVersionOfModInstalled && !forceSource.HasValue) //Let app decide { return(true); // This asi was "Installed" (because it was already installed). } // Install the ASI if (forceSource == null || forceSource.Value == false) { Debug.WriteLine("Hit me"); } string md5; bool useLocal = forceSource.HasValue && !forceSource.Value; // false (forceLocal) if (!useLocal && !forceSource.HasValue) { useLocal = File.Exists(cachedPath); } if (useLocal) { //Check hash first md5 = Utilities.CalculateMD5(cachedPath); if (md5 == asi.Hash) { Log.Information($@"Copying ASI from cached library to destination: {cachedPath} -> {finalPath}"); File.Copy(cachedPath, finalPath, true); Log.Information($@"Installed ASI to {finalPath}"); Analytics.TrackEvent(@"Installed ASI", new Dictionary <string, string>() { { @"Filename", Path.GetFileNameWithoutExtension(finalPath) } }); return(true); } } if (!forceSource.HasValue || forceSource.Value) { WebRequest request = WebRequest.Create(asi.DownloadLink); Log.Information(@"Fetching remote ASI from server"); using WebResponse response = request.GetResponse(); var memoryStream = new MemoryStream(); response.GetResponseStream().CopyTo(memoryStream); //MD5 check on file for security md5 = Utilities.CalculateMD5(memoryStream); if (md5 != asi.Hash) { //ERROR! Log.Error(@"Downloaded ASI did not match the manifest! It has the wrong hash."); return(false); } Log.Information(@"Fetched remote ASI from server. Installing ASI to " + finalPath); memoryStream.WriteToFile(finalPath); Log.Information(@"ASI successfully installed."); Analytics.TrackEvent(@"Installed ASI", new Dictionary <string, string>() { { @"Filename", Path.GetFileNameWithoutExtension(finalPath) } }); //Cache ASI if (!Directory.Exists(CachedASIsFolder)) { Log.Information(@"Creating cached ASIs folder"); Directory.CreateDirectory(CachedASIsFolder); } Log.Information(@"Caching ASI to local ASI library: " + cachedPath); memoryStream.WriteToFile(cachedPath); return(true); } // We could not install the ASI return(false); }
public void TestASIManager() { GlobalTest.Init(); Random random = new Random(); Console.WriteLine(@"Loading ASI Manager Manifest"); ASIManager.LoadManifest(); var games = new[] { Mod.MEGame.ME1, Mod.MEGame.ME2, Mod.MEGame.ME3 }; foreach (var game in games) { var root = GlobalTest.GetTestGameFoldersDirectory(game); var normal = Path.Combine(root, "normal"); GameTarget gt = new GameTarget(game, normal, true, false); var asiDir = MEDirectories.ASIPath(gt); if (Directory.Exists(asiDir)) { // Clean slate Utilities.DeleteFilesAndFoldersRecursively(asiDir); } var asisForGame = ASIManager.GetASIModsByGame(game); // 1: Test Installs of upgrades of versions foreach (var asi in asisForGame) { // Install every version of an ASI and then ensure only one ASI of that type exists in the directory. foreach (var v in asi.Versions) { var sourceBools = new bool?[] { true, false, null }; //online, local, let app decide foreach (var sourceBool in sourceBools) { // INSTALL FROM SOURCE Console.WriteLine($@"Install source variable: {sourceBool}"); Assert.IsTrue(ASIManager.InstallASIToTarget(v, gt, sourceBool), $"Installation of ASI failed: {v.Name}"); Assert.AreEqual(1, Directory.GetFiles(asiDir).Length, "The count of files in the ASI directory is not 1 after install of an ASI!"); // Check is installed var installedASIs = gt.GetInstalledASIs().OfType <KnownInstalledASIMod>().ToList(); Assert.AreEqual(1, installedASIs.Count, "The amount of installed ASIs as fetched by GameTarget GetInstalledASIs() is not equal to 1!"); // Check it maps to the correct one. var instASI = installedASIs.First(); Assert.AreEqual(v, instASI.AssociatedManifestItem, "The parsed installed ASI does not match the one we fed to ASIManager.InstallASIToTarget()!"); // Rename it to something random so the next version has to find it by the hash and not the filename. var newPath = Path.Combine(asiDir, Guid.NewGuid() + ".asi"); File.Move(instASI.InstalledPath, newPath, false); // Ensure it still can be found. installedASIs = gt.GetInstalledASIs().OfType <KnownInstalledASIMod>().ToList(); Assert.AreEqual(1, installedASIs.Count, "The amount of installed ASIs as fetched by GameTarget GetInstalledASIs() is not equal to 1 after renaming the file!"); // Make multiple clones, to ensure all old ones get deleted on upgrades. for (int i = 0; i < 5; i++) { var clonePath = Path.Combine(asiDir, instASI.AssociatedManifestItem.InstalledPrefix + i + ".asi"); File.Copy(newPath, clonePath, true); } installedASIs = gt.GetInstalledASIs().OfType <KnownInstalledASIMod>().ToList(); Assert.AreEqual(6, installedASIs.Count, "The amount of installed ASIs as fetched by GameTarget GetInstalledASIs() is not equal to 6 after cloning the file 5 times!"); } } var finalASIsPreRandomization = gt.GetInstalledASIs(); int randomCount = 0; foreach (var iam in finalASIsPreRandomization) { // Test randomly editing it. byte[] randomData = new byte[256]; random.NextBytes(randomData); File.WriteAllBytes(iam.InstalledPath, randomData); randomCount++; var unknownInstalledASIs = gt.GetInstalledASIs().OfType <UnknownInstalledASIMod>().ToList(); Assert.AreEqual(randomCount, unknownInstalledASIs.Count, "Writing random bytes to installed ASI made amount of installed ASIs not correct!"); } foreach (var v in finalASIsPreRandomization) { // Test uninstall and remove Assert.IsTrue(v.Uninstall(), $"ASI failed to uninstall: {v.InstalledPath}"); } Assert.AreEqual(0, Directory.GetFiles(asiDir).Length, "Leftover files remain after uninstalling all ASIs from target"); } } }