Skip to content
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
30 changes: 26 additions & 4 deletions src/Core/API/Config.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Core.Games;
using Core.Games;
using Core.Mods;
using Core.Mods.Installation.Installers;
using Core.SoftwareUpdates;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -35,10 +36,31 @@ public class GameConfig : Game.IConfig
public string ProcessName { get; set; } = "Undefined";
}

public class ModInstallConfig : ModInstaller.IConfig
public class ModInstallConfig : ModInstaller.IConfig, BootfilesInstaller.IConfig, PrefixBootfilesNaming.IConfig
{
public IEnumerable<string> DirsAtRoot { get; set; } = Array.Empty<string>();
public IEnumerable<string> ExcludedFromInstall { get; set; } = Array.Empty<string>();
public IEnumerable<string> DirsAtRoot { get; set; } = new[]
{
"cameras", "characters", "effects", "gui", "pakfiles", "render",
"text", "tracks", "userdata", "upgrade", "vehicles"
};
public IEnumerable<string> ExcludedFromInstall { get; set; } = new[]
{
@"**\*.orig",
@"**\*.dll",
@"**\*.exe"
};
public string GameSupportedModDir { get; set; } = Path.Combine("UserData", "Mods");

public IEnumerable<string> ExcludedFromConfig { get; set; } = Array.Empty<string>();
public bool GenerateModDetails { get; set; } = true;

public string BootfilesVehicleListDir { get; set; } = "vehicles";
public string BootfilesTrackListDir { get; set; } = Path.Combine("tracks", "_data");
public string BootfilesDrivelineDir { get; set; } = Path.Combine("vehicles", "physics", "driveline");

public string VehicleListFileName { get; set; } = "vehiclelist.lst";
public string TrackListFileName { get; set; } = "tracklist.lst";
public string DrivelineFileName { get; set; } = "driveline.rg";

public string BootfilesPrefix { get; set; } = "__bootfiles";
}
5 changes: 3 additions & 2 deletions src/Core/API/IEventHandler.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Core.Mods.Installation;
using Core.Mods.Installation.Installers;
using Core.Packages.Installation;

namespace Core.API;

public interface IEventHandler : ModPackagesUpdater.IEventHandler
public interface IEventHandler : PackagesUpdater.IEventHandler, BootfilesInstaller.IEventHandler
{
}
25 changes: 16 additions & 9 deletions src/Core/API/Init.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Core.Games;
using Core.IO;
using Core.Mods;
using Core.Mods.Installation;
using Core.Mods.Installation.Installers;
using Core.Packages.Installation;
using Core.Packages.Installation.Backup;
using Core.Packages.Repository;
Expand All @@ -17,23 +19,28 @@ public static IModManager CreateModManager(Config config)
{
var game = new Game(config.Game);
var modsDir = Path.Combine(game.InstallationDirectory, ModsDirName);
var tempDir = new SubdirectoryTempDir(modsDir);
var statePersistence = new JsonFileStatePersistence(modsDir);
var modRepository = new FileSystemRepository(modsDir);
var statePersistence = new JsonFileStatePersistence(modsDir);
var safeFileDelete = new WindowsRecyclingBin();
var packagesUpdater = CreatePackagesUpdater(config.ModInstall, game, tempDir);
return new ModManager(game, modRepository, packagesUpdater, statePersistence, safeFileDelete, tempDir);
var tempDir = new SubdirectoryTempDir(modsDir);
return CreateModManager(game, modRepository, statePersistence, safeFileDelete, tempDir, config.ModInstall);
}

internal static IPackagesUpdater<IEventHandler> CreatePackagesUpdater(
ModInstallConfig installerConfig,
public static IModManager CreateModManager(
IGame game,
ITempDir tempDir)
IPackageRepository modRepository,
IStatePersistence statePersistence,
ISafeFileDelete safeFileDelete,
ITempDir tempDir,
ModInstallConfig modInstallConfig)
{
var backupStrategyProvider = new SkipUpdatedBackupStrategy.Provider<IEventHandler>(
new SuffixBackupStrategy.Provider<PackageInstallationState, IEventHandler>());
return new ModPackagesUpdater<IEventHandler>(
var bootfilesNaming = new PrefixBootfilesNaming(modInstallConfig);
var modInstallerFactory = new ModInstallerFactory<ModInstallConfig>(game, tempDir, bootfilesNaming, modInstallConfig);
var modPackagesUpdater = new ModPackagesUpdater<IEventHandler>(
new FileSystemInstallerFactory(), backupStrategyProvider,
TimeProvider.System, game, tempDir, installerConfig);
TimeProvider.System, bootfilesNaming, modInstallerFactory);
return new ModManager(game, modRepository, bootfilesNaming, modPackagesUpdater, statePersistence, safeFileDelete, tempDir);
}
}
16 changes: 13 additions & 3 deletions src/Core/API/ModManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Core.Games;
using Core.IO;
using Core.Mods.Installation;
using Core.Mods;
using Core.Mods.Installation.Installers;
using Core.Packages.Installation;
using Core.Packages.Repository;
using Core.State;
Expand All @@ -12,16 +13,25 @@ internal class ModManager : IModManager
{
private readonly IGame game;
private readonly IPackageRepository packageRepository;
private readonly IBootfilesNaming bootfilesNaming;
private readonly IStatePersistence statePersistence;
private readonly ISafeFileDelete safeFileDelete;
private readonly ITempDir tempDir;

private readonly IPackagesUpdater<IEventHandler> packagesUpdater;

internal ModManager(IGame game, IPackageRepository packageRepository, IPackagesUpdater<IEventHandler> packagesUpdater, IStatePersistence statePersistence, ISafeFileDelete safeFileDelete, ITempDir tempDir)
internal ModManager(
IGame game,
IPackageRepository packageRepository,
IBootfilesNaming bootfilesNaming,
IPackagesUpdater<IEventHandler> packagesUpdater,
IStatePersistence statePersistence,
ISafeFileDelete safeFileDelete,
ITempDir tempDir)
{
this.game = game;
this.packageRepository = packageRepository;
this.bootfilesNaming = bootfilesNaming;
this.statePersistence = statePersistence;
this.safeFileDelete = safeFileDelete;
this.tempDir = tempDir;
Expand Down Expand Up @@ -56,7 +66,7 @@ public List<ModState> FetchState()
return IsOutOfDate(modPackage, modInstallationState);
});

var allPackageNames = installedMods.Keys.Where(packageName => !ModPackagesUpdater.IsBootFiles(packageName))
var allPackageNames = installedMods.Keys.Where(packageName => !bootfilesNaming.IsBootfiles(packageName))
.Concat(enabledModPackages.Keys)
.Concat(disabledModPackages.Keys)
.Distinct();
Expand Down
8 changes: 8 additions & 0 deletions src/Core/Mods/IBootfilesNaming.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Core.Mods;

public interface IBootfilesNaming
{
public bool IsBootfiles(string packageName);
public string GeneratedBootfilesName { get; }
public bool IsGeneratedBootfiles(string packageName);
}
182 changes: 146 additions & 36 deletions src/Core/Mods/Installation/Installers/BaseModInstaller.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Collections.Immutable;
using Core.Games;
using System.Collections.Immutable;
using System.IO.Abstractions;
using Core.Packages.Installation;
using Core.Packages.Installation.Backup;
using Core.Packages.Installation.Installers;
Expand All @@ -12,33 +12,39 @@ public abstract class BaseModInstaller : IInstaller
{
public interface IConfig
{
IEnumerable<string> DirsAtRoot
{
get;
}

IEnumerable<string> ExcludedFromInstall
{
get;
}
IEnumerable<string> DirsAtRoot { get; }
IEnumerable<string> ExcludedFromInstall { get; }
string GameSupportedModDir { get; }
string VehicleListFileName { get; }
string TrackListFileName { get; }
string DrivelineFileName { get; }
}

protected readonly string VehicleListFileName;
protected readonly string TrackListFileName;
protected readonly string DrivelineFileName;

protected readonly IFileSystem FileSystem;
protected readonly IInstaller Inner;
protected readonly IGame Game;
protected readonly DirectoryInfo StagingDir;
protected readonly string StagingFullPath;
protected readonly string GameSupportedModRelativeDir;

private readonly Lazy<IRootFinder.RootPaths> rootPaths;
private readonly Matcher filesToInstallMatcher;

private bool postProcessingDone;

private readonly List<RootedPath> localInstalledFiles = new();
private readonly HashSet<RootedPath> localInstalledFiles = new();

protected BaseModInstaller(IInstaller inner, IGame game, ITempDir tempDir, IConfig config)
protected BaseModInstaller(IFileSystem fileSystem, IInstaller inner, string tempDir, IConfig config)
{
FileSystem = fileSystem;
Inner = inner;
Game = game;
StagingDir = new DirectoryInfo(Path.Combine(tempDir.BasePath, inner.PackageName));
StagingFullPath = Path.GetFullPath(Path.Combine(tempDir, inner.PackageName));
GameSupportedModRelativeDir = config.GameSupportedModDir;
VehicleListFileName = config.VehicleListFileName;
TrackListFileName = config.TrackListFileName;
DrivelineFileName = config.DrivelineFileName;
var rootFinder = new ContainedDirsRootFinder(config.DirsAtRoot);
rootPaths = new Lazy<IRootFinder.RootPaths>(
() => rootFinder.FromDirectoryList(Inner.RelativeDirectoryPaths));
Expand All @@ -50,13 +56,13 @@ protected BaseModInstaller(IInstaller inner, IGame game, ITempDir tempDir, IConf

public int? PackageFsHash => Inner.PackageFsHash;

public abstract IReadOnlyCollection<string> PackageDependencies { get; }
public abstract IReadOnlySet<string> PackageDependencies { get; }

public IReadOnlyCollection<RootedPath> InstalledFiles =>
public IReadOnlySet<RootedPath> InstalledFiles =>
Inner.InstalledFiles
.Concat(localInstalledFiles)
.Where(RootIsNotStagingDir)
.ToImmutableArray();
.ToImmutableHashSet();

public IInstallation.State Installed =>
Inner.Installed == IInstallation.State.Installed && !postProcessingDone
Expand All @@ -66,33 +72,27 @@ protected BaseModInstaller(IInstaller inner, IGame game, ITempDir tempDir, IConf
public void Install(IInstaller.Destination destination, IBackupStrategy backupStrategy,
ProcessingCallbacks<RootedPath> callbacks)
{
Install(() => Inner.Install(
ConfigToStagingDir(destination),
backupStrategy,
IgnoreForStagedFiles(callbacks.AndAccept(Whitelisted))));
Install(
() => Inner.Install(
ConfigToStagingDir(destination),
backupStrategy,
IgnoreForStagedFiles(callbacks.AndAccept(Whitelisted))),
callbacks);

postProcessingDone = true;
}

public IEnumerable<string> RelativeDirectoryPaths =>
Inner.RelativeDirectoryPaths.SelectNotNull(rootPaths.Value.GetPathFromRoot);

protected abstract void Install(Action innerInstall);

protected void AddToInstalledFiles(RootedPath? installedFile)
{
if (installedFile is not null)
{
localInstalledFiles.Add(installedFile);
}
}
protected abstract void Install(Action innerInstall, ProcessingCallbacks<RootedPath> callbacks);

private IInstaller.Destination ConfigToStagingDir(IInstaller.Destination destination) =>
pathInPackage =>
{
var relativePathFromRoot = rootPaths.Value.GetPathFromRoot(pathInPackage);
return relativePathFromRoot is null
? new RootedPath(StagingDir.FullName, pathInPackage)
? new RootedPath(StagingFullPath, pathInPackage)
// If part of a game root, return the destination relative to that root
: destination(relativePathFromRoot);
};
Expand Down Expand Up @@ -121,8 +121,118 @@ private Action<RootedPath> IgnoreForStagedFiles(Action<RootedPath> action) => rp
};

private bool RootIsNotStagingDir(RootedPath rp) =>
rp.Root != StagingDir.FullName;
rp.Root != StagingFullPath;

private bool RootIsStagingDir(RootedPath rp) =>
rp.Root == StagingDir.FullName;
rp.Root == StagingFullPath;

protected void AppendCrdFileEntries(IEnumerable<string> crdFileEntries,
ProcessingCallbacks<RootedPath> callbacks) =>
AppendEntryList(VehicleListDir.SubPath(VehicleListFileName), crdFileEntries, callbacks);

protected abstract RootedPath VehicleListDir { get; }

protected void AppendTrdFileEntries(IEnumerable<string> trdFileEntries,
ProcessingCallbacks<RootedPath> callbacks) =>
AppendEntryList(TrackListDir.SubPath(TrackListFileName), trdFileEntries, callbacks);

protected abstract RootedPath TrackListDir { get; }

protected void InsertDrivelineRecords(IEnumerable<string> recordBlocks,
ProcessingCallbacks<RootedPath> callbacks)
{
var recordsTextBlock = DrivelineBlock(recordBlocks);
if (recordsTextBlock.Length == 0)
{
return;
}

var driveLineFilePath = DrivelineDir.SubPath(DrivelineFileName);
var newContents = DrivelineFileContents(driveLineFilePath, WrapConfigBlock(recordsTextBlock));

SafeWriteAllText(driveLineFilePath, newContents, callbacks);
}

protected abstract RootedPath DrivelineDir { get; }

private static string DrivelineBlock(IEnumerable<string> recordBlocks)
{
var dedupedRecordBlocks = DedupeRecordBlocks(recordBlocks);
return string.Join($"{Environment.NewLine}{Environment.NewLine}", dedupedRecordBlocks);
}

internal static IEnumerable<string> DedupeRecordBlocks(IEnumerable<string> recordBlocks)
{
var seen = new HashSet<string>();
var deduped = new List<string>();
foreach (var rb in recordBlocks.Reverse())
{
var key = rb.Split(Environment.NewLine, 2).First().NormalizeWhitespaces();
if (seen.Contains(key))
{
continue;
}
seen.Add(key);
deduped.Add(rb);
}
return deduped.Reverse<string>();
}

private string DrivelineFileContents(RootedPath driveLineFilePath, string recordsTextBlock)
{
if (!FileSystem.File.Exists(driveLineFilePath.Full))
{
return recordsTextBlock;
}

var contents = FileSystem.File.ReadAllText(driveLineFilePath.Full);
var endIndex = contents.LastIndexOf("END", StringComparison.Ordinal);
if (endIndex < 0)
{
throw new Exception("Could not find insertion point in driveline file");
}
return contents.Insert(endIndex, recordsTextBlock);
}

private void AppendEntryList(
RootedPath filePath,
IEnumerable<string> entries,
ProcessingCallbacks<RootedPath> callbacks)
{
var entriesBlock = string.Join(Environment.NewLine, entries);
if (entriesBlock.Length == 0)
{
return;
}
var contents = WrapConfigBlock(entriesBlock);

SafeAppendAllText(filePath, contents, callbacks);
}

protected virtual string WrapConfigBlock(string configBlock) => configBlock;

protected void SafeWriteAllText(RootedPath filePath, string contents,
ProcessingCallbacks<RootedPath> callbacks) =>
SafeFileOperation(FileSystem.File.WriteAllText, filePath, contents, callbacks);

protected void SafeAppendAllText(RootedPath filePath, string contents,
ProcessingCallbacks<RootedPath> callbacks) =>
SafeFileOperation(FileSystem.File.AppendAllText, filePath, contents, callbacks);

private void SafeFileOperation(Action<string, string> fileOperation, RootedPath filePath,
string contents, ProcessingCallbacks<RootedPath> callbacks)
{
var fullFilePath = filePath.Full;

(InstalledFiles.Contains(filePath) ? new ProcessingCallbacks<RootedPath>() : callbacks).Wrap(() =>
{
var dirPath = Path.GetDirectoryName(fullFilePath);
if (dirPath is not null)
{
FileSystem.Directory.CreateDirectory(dirPath);
}
fileOperation(fullFilePath, contents);
localInstalledFiles.Add(filePath);
}, filePath);
}
}
Loading
Loading