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

Fix skyrim start issues #499

Merged
merged 8 commits into from
Aug 2, 2023
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public Hash GetFingerprint(ModFilePair self, Plan plan)
fingerprinter.Add(f.DataStoreId);
});


return fingerprinter.Digest();
}
}
2 changes: 2 additions & 0 deletions src/Games/NexusMods.Games.BethesdaGameStudios/Services.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public static IServiceCollection AddBethesdaGameStudios(this IServiceCollection
services.AddAllSingleton<IModInstaller, SkyrimInstaller>();
services.AddAllSingleton<IGame, SkyrimSpecialEdition>();
services.AddAllSingleton<IGame, SkyrimLegendaryEdition>();
services.AddSingleton<ITool, RunGameTool<SkyrimLegendaryEdition>>();
services.AddSingleton<ITool, RunGameTool<SkyrimSpecialEdition>>();
services.AddAllSingleton<IFileAnalyzer, PluginAnalyzer>();
services.AddAllSingleton<ITypeFinder, TypeFinder>();
return services;
Expand Down
23 changes: 18 additions & 5 deletions src/NexusMods.DataModel/Games/RunGameTool.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Diagnostics;
using System.Text;
using CliWrap;
using Microsoft.Extensions.Logging;
using NexusMods.Common;
using NexusMods.DataModel.Loadouts;
using NexusMods.Paths;

Expand All @@ -22,15 +25,17 @@
{
private readonly ILogger<RunGameTool<T>> _logger;
private readonly T _game;
private readonly IProcessFactory _processFactory;

/// <summary/>
/// <param name="logger">The logger used to log execution.</param>
/// <param name="game">The game to execute.</param>
/// <remarks>
/// This constructor is usually called from DI.
/// </remarks>
public RunGameTool(ILogger<RunGameTool<T>> logger, T game)
public RunGameTool(ILogger<RunGameTool<T>> logger, T game, IProcessFactory processFactory)

Check warning on line 36 in src/NexusMods.DataModel/Games/RunGameTool.cs

View workflow job for this annotation

GitHub Actions / build

Parameter 'processFactory' has no matching param tag in the XML comment for 'RunGameTool<T>.RunGameTool(ILogger<RunGameTool<T>>, T, IProcessFactory)' (but other parameters do)
{
_processFactory = processFactory;
_game = game;
_logger = logger;
}
Expand All @@ -47,10 +52,18 @@
var program = _game.GetPrimaryFile(loadout.Installation.Store).Combine(loadout.Installation.Locations[GameFolderType.Game]);
_logger.LogInformation("Running {Program}", program);

// TODO: use IProcessFactory
var psi = new ProcessStartInfo(program.ToString());
var process = Process.Start(psi);
await process!.WaitForExitAsync();
var stdOut = new StringBuilder();
var stdErr = new StringBuilder();
var command = new Command(program.ToString())
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOut))
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErr))
.WithValidation(CommandResultValidation.None)
.WithWorkingDirectory(program.Parent.ToString());


var result = await _processFactory.ExecuteAsync(command);
if (result.ExitCode != 0)
_logger.LogError("While Running {Filename} : {Error} {Output}", program, stdErr, stdOut);

_logger.LogInformation("Finished running {Program}", program);
}
Expand Down
18 changes: 13 additions & 5 deletions src/NexusMods.DataModel/Loadouts/LoadoutManager.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO.Compression;
using Microsoft.Extensions.Logging;
using NexusMods.Common;
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Abstractions.Ids;
using NexusMods.DataModel.ArchiveContents;
using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Interprocess.Jobs;
using NexusMods.DataModel.Interprocess.Messages;
using NexusMods.DataModel.Loadouts.Cursors;
using NexusMods.DataModel.ModInstallers;
using NexusMods.DataModel.Loadouts.Markers;
using NexusMods.DataModel.Loadouts.ModFiles;
using NexusMods.DataModel.Loadouts.Mods;
using NexusMods.DataModel.RateLimiting;
using NexusMods.DataModel.Sorting.Rules;
Expand Down Expand Up @@ -131,7 +129,8 @@ public async Task<LoadoutMarker> ManageGameAsync(GameInstallation installation,
Files = new EntityDictionary<ModFileId, AModFile>(Store),
Version = installation.Version.ToString(),
ModCategory = Mod.GameFilesCategory,
SortRules = ImmutableList<ISortRule<Mod, ModId>>.Empty.Add(new First<Mod, ModId>())
SortRules = ImmutableList<ISortRule<Mod, ModId>>.Empty.Add(new First<Mod, ModId>()),
Enabled = true
}.WithPersist(Store);

var loadoutId = LoadoutId.Create();
Expand Down Expand Up @@ -206,7 +205,16 @@ private async Task IndexAndAddGameFiles(GameInstallation installation,
}

managementJob.Progress = new Percent(0.5);
gameFiles.AddRange(installation.Game.GetGameFiles(installation, Store));
var generatedFiles = installation.Game.GetGameFiles(installation, Store).ToArray();

// Generated files should override any existing files that were indexed
var byTo = gameFiles.OfType<IToFile>().ToLookup(l => l.To);
foreach (var generatedFile in generatedFiles.OfType<IToFile>())
{
foreach (var conflict in byTo[generatedFile.To])
gameFiles.Remove((AModFile)conflict);
}
gameFiles.AddRange(generatedFiles);

Registry.Alter(loadout.LoadoutId, mod.Id, "Add game files",
m => m! with
Expand Down
28 changes: 10 additions & 18 deletions src/NexusMods.DataModel/Loadouts/LoadoutSyncronizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,15 @@ public LoadoutSynchronizer(ILogger<LoadoutSynchronizer> logger,
/// <returns></returns>
public async Task<IEnumerable<Mod>> SortMods(Loadout loadout)
{
var modRules = await loadout.Mods.Values
var mods = loadout.Mods.Values.Where(mod => mod.Enabled).ToList();
_logger.LogInformation("Sorting {ModCount} mods in loadout {LoadoutName}", mods.Count, loadout.Name);
var modRules = await mods
.SelectAsync(async mod => (mod.Id, await ModSortRules(loadout, mod).ToListAsync()))
.ToDictionaryAsync(r => r.Id, r => r.Item2);
var sorted = Sorter.Sort<Mod, ModId>(loadout.Mods.Values.ToList(), m => m.Id, m => modRules[m.Id]);
if (modRules.Count == 0)
return Array.Empty<Mod>();

var sorted = Sorter.Sort<Mod, ModId>(mods, m => m.Id, m => modRules[m.Id]);
return sorted;
}

Expand Down Expand Up @@ -288,20 +293,6 @@ private async ValueTask EmitReplacePlan(List<IApplyStep> plan, HashedEntry exist
await EmitCreatePlan(plan, pair, tmpPlan, existing.Path);
}

/// <summary>
/// Gets the metadata for the given file, if the file is from an archive then the metadata is returned
/// </summary>
/// <param name="file"></param>
/// <param name="path"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public ValueTask<FileMetaData?> GetMetaData(AModFile file, AbsolutePath path)
{
if (file is IFromArchive fa)
return ValueTask.FromResult<FileMetaData?>(new FileMetaData(path, fa.Hash, fa.Size));
throw new NotImplementedException();
}

/// <summary>
/// Compares the game folders to the loadout and returns a plan of what needs to be done to make the loadout match the game folders
/// </summary>
Expand All @@ -327,11 +318,12 @@ public async ValueTask<IngestPlan> MakeIngestPlan(Loadout loadout, Func<Absolute

if (flattenedLoadout.TryGetValue(gamePath, out var planFile))
{
var planMetadata = await GetMetaData(planFile.File, existing.Path);
if (planMetadata == null || planMetadata.Hash != existing.Hash || planMetadata.Size != existing.Size)
if (planFile.File is IFromArchive fa && (fa.Hash != existing.Hash || fa.Size != existing.Size))
{
await EmitIngestReplacePlan(plan, planFile, existing);
}
// TODO: Fix this, it doesn't contain support for IGeneratedFile,
// once we re-design apply/ingest this should be implemented
continue;
}

Expand Down
20 changes: 16 additions & 4 deletions src/NexusMods.DataModel/NxArchiveManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,18 @@ private unsafe void UpdateIndexes(NxUnpacker unpacker, (IStreamFactory, Hash, Si
AbsolutePath finalPath)
{
Span<byte> buffer = stackalloc byte[sizeof(NativeFileEntryV1)];

var paths = unpacker.GetPathedFileEntries();

foreach (var entry in unpacker.GetFileEntriesRaw())
{
fixed (byte* ptr = buffer)
{
var writer = new LittleEndianWriter(ptr);
entry.WriteAsV1(ref writer);

var dbId = IdFor((Hash)entry.Hash, guid);

var hash = Hash.FromHex(paths[entry.FilePathIndex].FileName);
var dbId = IdFor(hash, guid);
var dbEntry = new ArchivedFiles
{
File = finalPath.FileName,
Expand Down Expand Up @@ -136,13 +140,21 @@ public async Task ExtractFiles(IEnumerable<(Hash Src, AbsolutePath Dest)> files,
await using var file = group.Key.Read();
var provider = new FromStreamProvider(file);
var unpacker = new NxUnpacker(provider);

var toExtract = group
.Select(entry =>
(IOutputDataProvider)new OutputFileProvider(entry.Dest.Parent.GetFullPath(), entry.Dest.FileName, entry.FileEntry))
.ToArray();

unpacker.ExtractFiles(toExtract, settings);
try
{
unpacker.ExtractFiles(toExtract, settings);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}

foreach (var toDispose in toExtract)
{
Expand Down
2 changes: 1 addition & 1 deletion src/NexusMods.DataModel/Services.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ IDataModelSettings Settings(IServiceProvider provider)
coll.AddSingleton(typeof(EntityLinkConverter<>));

coll.AddSingleton<IDataStore, SqliteDataStore>();
coll.AddAllSingleton<IArchiveManager, NxArchiveManager>();
coll.AddAllSingleton<IArchiveManager, ZipArchiveManager>();
coll.AddAllSingleton<IResource, IResource<FileHashCache, Size>>(s =>
new Resource<FileHashCache, Size>("File Hashing",
Settings(s).MaxHashingJobs,
Expand Down
9 changes: 7 additions & 2 deletions src/NexusMods.DataModel/ToolManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ public async Task<Loadout> RunTool(ITool tool, Loadout loadout, ModId? generated
generatedFilesMod = mod.Id;
}

var ingestPlan = await _loadoutSynchronizer.MakeIngestPlan(loadout, _ => generatedFilesMod.Value, token);
return await _loadoutSynchronizer.Ingest(ingestPlan, $"Updating {tool.Name} Generated Files");
// We don't yet properly support ingesting data. The issue is if a bad apply occurs, the ingest can
// delete files we don't yet have a way of recreating. Also we have no way to create branches, roll back the
// ingest, etc. in the loadout. So for now we just don't ingest.

//var ingestPlan = await _loadoutSynchronizer.MakeIngestPlan(loadout, _ => generatedFilesMod.Value, token);
//return await _loadoutSynchronizer.Ingest(ingestPlan, $"Updating {tool.Name} Generated Files");
return loadout;
erri120 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"FilePath": "plugin_test.7z",
"Source": "RealFileSystem",
"Name": "Plugin Test"
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"FilePath": "Skyrim.7z",
"Source": "RealFileSystem",
"Name": "Skyrim Base"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@
<None Update="Assets\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\DownloadableMods\PluginTest\manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\DownloadableMods\SkyrimBase\manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,32 @@
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Loadouts;
using NexusMods.DataModel.Loadouts.LoadoutSynchronizerDTOs;
using NexusMods.DataModel.Loadouts.ModFiles;
using NexusMods.DataModel.Loadouts.Mods;
using NexusMods.Games.TestFramework;
using NexusMods.Hashing.xxHash64;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
using Noggog;

namespace NexusMods.Games.BethesdaGameStudios.Tests.SkyrimSpecialEditionTests;

public class SkyrimSpecialEditionTests : AGameTest<SkyrimSpecialEdition>
{
private readonly TestModDownloader _downloader;

/// <summary>
/// DI Constructor
/// </summary>
/// <param name="serviceProvider"></param>
public SkyrimSpecialEditionTests(IServiceProvider serviceProvider) : base(serviceProvider)
{
_downloader = serviceProvider.GetRequiredService<TestModDownloader>();

}

Expand Down Expand Up @@ -195,4 +202,87 @@ public async Task CanGeneratePluginsFile()
opt => opt.WithStrictOrdering());
}
}

[Fact]
public async Task EnablingAndDisablingModsModifiesThePluginsFile()
{
var loadout = await CreateLoadout(indexGameFiles: false);

var pluginFile = (from mod in loadout.Value.Mods.Values
from file in mod.Files.Values
where file is PluginOrderFile
select file)
.OfType<PluginOrderFile>()
.First();


var pluginFilePath = pluginFile.To.CombineChecked(loadout.Value.Installation);

var path = BethesdaTestHelpers.GetDownloadableModFolder(FileSystem, "SkyrimBase");
var downloaded = await _downloader.DownloadFromManifestAsync(path, FileSystem);

var skyrimBase = await InstallModFromArchiveIntoLoadout(
loadout,
downloaded.Path,
downloaded.Manifest.Name);

await Apply(loadout.Value);

pluginFilePath.FileExists.Should().BeTrue("the loadout is applied");


var text = await GetPluginOrder(pluginFilePath);

text.Should().Contain("Skyrim.esm");
text.Should().NotContain("plugin_test.esp", "plugin_test.esp is not installed");

path = BethesdaTestHelpers.GetDownloadableModFolder(FileSystem, "PluginTest");
downloaded = await _downloader.DownloadFromManifestAsync(path, FileSystem);
var pluginTest = await InstallModFromArchiveIntoLoadout(
loadout,
downloaded.Path,
downloaded.Manifest.Name);

await Apply(loadout.Value);

pluginFilePath.FileExists.Should().BeTrue("the loadout is applied");
text = await GetPluginOrder(pluginFilePath);

text.Should().Contain("Skyrim.esm");
text.Should().Contain("plugin_test.esp", "plugin_test.esp is installed");

LoadoutRegistry.Alter(loadout.Value.LoadoutId, pluginTest.Id, "disable plugin",
mod => mod with {Enabled = false});

text = await GetPluginOrder(pluginFilePath);

text.Should().Contain("Skyrim.esm");
text.Should().Contain("plugin_test.esp", "new loadout has not been applied yet");

await Apply(loadout.Value);

text = await GetPluginOrder(pluginFilePath);

text.Should().Contain("Skyrim.esm");
text.Should().NotContain("plugin_test.esp", "plugin_test.esp is disabled");

LoadoutRegistry.Alter(loadout.Value.LoadoutId, pluginTest.Id, "enable plugin",
mod => mod with {Enabled = true});

await Apply(loadout.Value);

text = await GetPluginOrder(pluginFilePath);

text.Should().Contain("Skyrim.esm");
text.Should().Contain("plugin_test.esp", "plugin_test.esp is enabled again");

}

private static async Task<string[]> GetPluginOrder(AbsolutePath pluginFilePath)
{
return (await pluginFilePath.ReadAllTextAsync())
.Split(new []{"\r","\n"}, StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.TrimStart("*"))
.ToArray();
}
}
10 changes: 10 additions & 0 deletions tests/Games/NexusMods.Games.TestFramework/AGameTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,14 @@ protected async Task<TemporaryPath> CreateTestFile(byte[] contents, Extension? e

protected Task<TemporaryPath> CreateTestFile(string contents, Extension? extension, Encoding? encoding = null)
=> CreateTestFile((encoding ?? Encoding.UTF8).GetBytes(contents), extension);

/// <summary>
/// Helper method to create create a apply plan and apply it.
/// </summary>
/// <param name="loadout"></param>
protected async Task Apply(Loadout loadout)
{
var plan = await LoadoutSynchronizer.MakeApplySteps(loadout);
await LoadoutSynchronizer.Apply(plan);
}
}
Loading
Loading