Skip to content

Commit

Permalink
PacificDrive impl
Browse files Browse the repository at this point in the history
  • Loading branch information
ZeeWanderer committed Aug 7, 2024
1 parent d7e9a5f commit 0dcbc7f
Show file tree
Hide file tree
Showing 12 changed files with 504 additions and 1 deletion.
7 changes: 7 additions & 0 deletions NexusMods.App.sln
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Mnem
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.DataModel.Synchronizer.Tests", "tests\NexusMods.DataModel.Synchronizer.Tests\NexusMods.DataModel.Synchronizer.Tests.csproj", "{11718918-CF45-4A6E-9B87-33A16DDF6F2B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NexusMods.Games.UnrealEngine", "src\Games\NexusMods.Games.UnrealEngine\NexusMods.Games.UnrealEngine.csproj", "{EC1D0F00-BFA9-40A6-AC61-41A5292DF163}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -610,6 +612,10 @@ Global
{11718918-CF45-4A6E-9B87-33A16DDF6F2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11718918-CF45-4A6E-9B87-33A16DDF6F2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11718918-CF45-4A6E-9B87-33A16DDF6F2B}.Release|Any CPU.Build.0 = Release|Any CPU
{EC1D0F00-BFA9-40A6-AC61-41A5292DF163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC1D0F00-BFA9-40A6-AC61-41A5292DF163}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC1D0F00-BFA9-40A6-AC61-41A5292DF163}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC1D0F00-BFA9-40A6-AC61-41A5292DF163}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -718,6 +724,7 @@ Global
{F6482055-698C-492A-9FC2-0FCDC9FC2E23} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{162D8E65-C513-4C51-9947-400914483FB4} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{11718918-CF45-4A6E-9B87-33A16DDF6F2B} = {52AF9D62-7D5B-4AD0-BA12-86F2AA67428B}
{EC1D0F00-BFA9-40A6-AC61-41A5292DF163} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F9F8352-34DD-42C0-8564-EE9AF34A3501}
Expand Down
37 changes: 37 additions & 0 deletions src/Games/NexusMods.Games.UnrealEngine/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Text.RegularExpressions;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
using NexusMods.Abstractions.GameLocators;

namespace NexusMods.Games.UnrealEngine;
public static partial class Constants
{
/// <summary>
/// {Game}/{GameMainUE}/Content/Paks
/// </summary>
public static readonly LocationId GameMainUE = LocationId.From("GameMainUE");
/// <summary>
/// Relative to GameMainUE
/// </summary>
public static readonly RelativePath ContentModsPath = "Content/Paks/~mods".ToRelativePath();
/// <summary>
/// Relative to GameMainUE
/// </summary>
public static readonly RelativePath InjectorModsPath = "Binaries/Win64".ToRelativePath();
public static readonly Extension PakExt = new(".pak");
public static readonly Extension UcasExt = new(".ucas");
public static readonly Extension UtocExt = new(".utoc");
public static readonly Extension SigExt = new(".sig");
public static readonly Extension DLLExt = new(".dll");
public static readonly Extension ExeExt = new(".exe");
public static readonly Extension ConfigExt = new(".ini");

public static readonly HashSet<Extension> ContentExts = [PakExt, UcasExt, UtocExt, SigExt];
public static readonly HashSet<Extension> ArchiveExts = [new(".zip"), new(".rar")];

[GeneratedRegex(@"^(?<modName>.+?)-(?<id>\d+)-(?<version>(?:\d+-?)+)-(?<uniqueId>\d+)\.(zip|rar)$", RegexOptions.IgnoreCase)]
public static partial Regex DefaultUEModArchiveNameRegex();

[GeneratedRegex(@"^(?<modName>.*?)\s?[-_]?\s?(?<version>[\d.]+)\.(zip|rar)$", RegexOptions.IgnoreCase)]
public static partial Regex ModArchiveNameRegexFallback();
}
268 changes: 268 additions & 0 deletions src/Games/NexusMods.Games.UnrealEngine/Installers/SmartUEInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using NexusMods.Abstractions.DiskState;
using NexusMods.Abstractions.FileStore.Trees;
using NexusMods.Abstractions.GameLocators;
using NexusMods.Abstractions.Installers;
using NexusMods.Abstractions.Loadouts;
using NexusMods.Abstractions.Loadouts.Extensions;
using NexusMods.Abstractions.Loadouts.Files;
using NexusMods.Abstractions.Loadouts.Mods;
using NexusMods.Abstractions.Loadouts.Synchronizers;
using File = NexusMods.Abstractions.Loadouts.Files.File;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.Models;
using NexusMods.Paths;
using NexusMods.Paths.Extensions;
using NexusMods.Paths.Trees.Traits;
using System.Text.RegularExpressions;
using NexusMods.Abstractions.Library.Installers;
using NexusMods.Abstractions.Library.Models;
using System.Xml.Linq;
using NexusMods.Extensions.BCL;


namespace NexusMods.Games.UnrealEngine.Installers;

[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "IdentifierTypo")]
public class SmartUEInstaller : ALibraryArchiveInstaller
{
private readonly IConnection _connection;

public SmartUEInstaller(ILogger<SmartUEInstaller> logger, IConnection connection, IServiceProvider serviceProvider) : base(serviceProvider, logger)
{
_connection = connection;
}

/// <summary>
/// A collextion of <see cref="Regex"/>es to try an parse Archive filename.
/// </summary>
private static IEnumerable<Regex> ModArchiveNameRegexes =>
[
Constants.DefaultUEModArchiveNameRegex(),
Constants.ModArchiveNameRegexFallback(),
];

// This is here for reference, to be removed when loadout items reach parity with this implementation
public async ValueTask<IEnumerable<ModInstallerResult>> GetModsAsync(
ModInstallerInfo info,
CancellationToken cancellationToken = default)
{
// TODO: add support for executable files
// TODO: add support for config ini files, preferably merge and not replace
// TODO: test with mod downloaded with metadata, i.e. via website
// TODO: see what can be done with IDs extracted from archive filename

var gameFolderPath = info.Locations[LocationId.Game];
var gameMainUEFolderPath = info.Locations[Constants.GameMainUE];

var achiveFiles = info.ArchiveFiles.GetFiles();

if (achiveFiles.Length == 0)
{
Logger.LogError("Archive contains 0 files");
return [];
}

var loadout = Loadout.All(_connection.Db)
.First(x => x.Installation.Path == gameFolderPath.ToString());

var gameFilesLookup = loadout.Mods
.First(mod => mod.Category == ModCategory.GameFiles).Files
.Select(file => file.To).ToLookup(x => x.Path.FileName);

var modFiles = achiveFiles.Select(kv =>
{
switch (kv.Path().Extension)
{
case Extension ext when Constants.ContentExts.Contains(ext):
{
var matchesGameContentFles = gameFilesLookup[kv.FileName()];
if (matchesGameContentFles.Any()) // if Content file exists in game dir replace it
{
var matchedContentFIle = matchesGameContentFles.First();
return kv.ToStoredFile(
matchedContentFIle
);
}
else
return kv.ToStoredFile(
new GamePath(Constants.GameMainUE, Constants.ContentModsPath.Join(kv.FileName()))
);
}
case Extension ext when ext == Constants.DLLExt:
{
var matchesGameDlls = gameFilesLookup[kv.FileName()];
if (matchesGameDlls.Any()) // if DLL exists in game dir replace it
{
var matchedDll = matchesGameDlls.First();
return kv.ToStoredFile(
matchedDll
);
}
else
return kv.ToStoredFile(
new GamePath(Constants.GameMainUE, Constants.InjectorModsPath.Join(kv.FileName()))
);
}
default:
{
var matchesGameFles = gameFilesLookup[kv.FileName()];
if (matchesGameFles.Any()) // if File exists in game dir replace it
{
var matchedFile = matchesGameFles.First();
return kv.ToStoredFile(
matchedFile
);
}
else
{
Logger.LogWarning("File {} is of unrecognized type {}, skipping", kv.Path().FileName, kv.Path().Extension);
return null;
}
}
}
}).OfType<TempEntity>().ToArray();

if (modFiles.Length == 0)
{
Logger.LogError("0 files were processed");
return [];
}
else if (modFiles.Length != achiveFiles.Length)
{
Logger.LogWarning("Of {} files in archive only {} were processed", achiveFiles.Length, modFiles.Length);
}

var Name = info.ModName;
var Id = info.BaseModId;
string? Version = null;

// If ModName ends with archive extension try to parse name and version out of the archive name
if (info.ModName != null && Constants.ArchiveExts.Contains(info.ModName.ToRelativePath().Extension))
{
foreach (var regex in ModArchiveNameRegexes)
{
var match = regex.Match(info.ModName);
if (match.Success)
{
if (match.Groups.ContainsKey("modName"))
{
Name = match.Groups["modName"].Value
.Replace('_', ' ')
.Trim();
}

if (match.Groups.ContainsKey("version"))
{
Version = match.Groups["version"].Value
.Replace('-', '.');
}

break;
}
}
}

return [ new ModInstallerResult
{
Id = Id,
Files = modFiles,
Name = Name,
Version = Version,
}];
}

public override async ValueTask<InstallerResult> ExecuteAsync(
LibraryArchive.ReadOnly libraryArchive,
LoadoutItemGroup.New loadoutGroup,
ITransaction transaction,
Loadout.ReadOnly loadout,
CancellationToken cancellationToken)
{
var achiveFiles = libraryArchive.GetTree().EnumerateChildrenBfs().ToArray();

if (achiveFiles.Length == 0)
{
Logger.LogError("Archive contains 0 files");
return new NotSupported();
}

var foundGameFilesGroup = LoadoutGameFilesGroup
.FindByGameMetadata(loadout.Db, loadout.Installation.GameMetadataId)
.TryGetFirst(x => x.AsLoadoutItemGroup().AsLoadoutItem().LoadoutId == loadout.LoadoutId, out var gameFilesGroup);

if (!foundGameFilesGroup)
{
Logger.LogError("Unable to find game files group!");
return new NotSupported();
}

var gameFilesLookup = gameFilesGroup.AsLoadoutItemGroup().Children
.Select(gameFile => gameFile.TryGetAsLoadoutItemWithTargetPath(out var targeted) ? targeted.TargetPath : default)
.Where(x => x != default)
.ToLookup(x => x.FileName);

var modFiles = achiveFiles.Select(kv =>
{
var filePath = kv.Value.Item.Path;
switch (filePath.Extension)
{
case Extension ext when Constants.ContentExts.Contains(ext):
{
var matchesGameContentFles = gameFilesLookup[filePath.FileName];
if (matchesGameContentFles.Any()) // if Content file exists in game dir replace it
{
var matchedContentFIle = matchesGameContentFles.First();
return kv.Value.ToLoadoutFile(loadout.Id, loadoutGroup.Id, transaction, matchedContentFIle);
}
else
return kv.Value.ToLoadoutFile(
loadout.Id, loadoutGroup.Id, transaction, new GamePath(Constants.GameMainUE, Constants.ContentModsPath.Join(filePath.FileName))
);
}
case Extension ext when ext == Constants.DLLExt:
{
var matchesGameDlls = gameFilesLookup[filePath.FileName];
if (matchesGameDlls.Any()) // if DLL exists in game dir replace it
{
var matchedDll = matchesGameDlls.First();
return kv.Value.ToLoadoutFile(loadout.Id, loadoutGroup.Id, transaction, matchedDll);
}
else
return kv.Value.ToLoadoutFile(
loadout.Id, loadoutGroup.Id, transaction, new GamePath(Constants.GameMainUE, Constants.InjectorModsPath.Join(filePath.FileName))
);
}
default:
{
var matchesGameFles = gameFilesLookup[filePath.FileName];
if (matchesGameFles.Any()) // if File exists in game dir replace it
{
var matchedFile = matchesGameFles.First();
return kv.Value.ToLoadoutFile(loadout.Id, loadoutGroup.Id, transaction, matchedFile);
}
else
{
Logger.LogWarning("File {} is of unrecognized type {}, skipping", filePath.FileName, filePath.Extension);
return null;
}
}
}
}).OfType<LoadoutFile.New>().ToArray();

if (modFiles.Length == 0)
{
Logger.LogError("0 files were processed");
return new NotSupported();
}
else if (modFiles.Length != achiveFiles.Length)
{
Logger.LogWarning("Of {} files in archive only {} were processed", achiveFiles.Length, modFiles.Length);
}

return new Success();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<None Remove="Resources\PacificDrive\game_image.jpg" />
<None Remove="Resources\PacificDrive\icon.png" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Resources\PacificDrive\game_image.jpg" />
<EmbeddedResource Include="Resources\PacificDrive\icon.png" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Abstractions\NexusMods.Abstractions.Games\NexusMods.Abstractions.Games.csproj" />
<ProjectReference Include="..\..\Abstractions\NexusMods.Abstractions.Loadouts\NexusMods.Abstractions.Loadouts.csproj" />
<ProjectReference Include="..\..\Extensions\NexusMods.Extensions.DependencyInjection\NexusMods.Extensions.DependencyInjection.csproj" />
<PackageReference Include="NexusMods.MnemonicDB.SourceGenerator" PrivateAssets="all" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

</Project>
Loading

0 comments on commit 0dcbc7f

Please sign in to comment.