-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d7e9a5f
commit 0dcbc7f
Showing
12 changed files
with
504 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
268
src/Games/NexusMods.Games.UnrealEngine/Installers/SmartUEInstaller.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
src/Games/NexusMods.Games.UnrealEngine/NexusMods.Games.UnrealEngine.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.