Skip to content

Commit ef8d6d5

Browse files
authored
Intrinsic files (#3894)
* Add intrinsic file support for Fallout 4 and Skyrim SE synchronizers (#3830) * Add intrinsic file handling for Skyrim SE and Fallout 4 synchronizers, refactor `Write` method to remove unused transaction parameter, and extend plugin file handling logic. * Refactor `KnownPaths` to use target-typed `new` and add missing `using` directive in `PluginsFile`. * Add stream source architecture with prioritization and improve plugin file handling - Introduced `IReadOnlyStreamSource`, `IStreamSourceDispatcher`, and `SourcePriority` to enable prioritized, read-only stream sources. - Implemented `NxFileStore`, `GameFileStreamSource`, and `StreamSourceDispatcher` to handle stream retrieval by source type. - Refactored `PluginsFile` to include sorting logic and enhanced plugin dependency handling. - Updated `ACreationEngineSynchronizer` constructor to support the new stream architecture. * Refactor stream handling and remove `IFileStore` from SkyrimSE, Fallout 4, and PluginFile implementations - Replaced `IFileStore` usage with `IStreamSourceDispatcher` for streamlined and prioritized stream access. - Updated synchronizers and constructors for SkyrimSE and Fallout 4 to reflect the new stream architecture. - Enhanced `PluginsFile.Write` logic to generate dynamic plugin lists, removed unused `transaction` parameter, and added intrinsic file support. - Refactored error handling and conditions in `NxFileStore` and `GameFileStreamSource`. * WIP updating the syncronizer code to support file types * Expand ActionMapping to support additional shorthand variants * Add handling for intrinsic file actions in synchronizer rules - Introduced `WriteIntrinsic` and `AdaptLoadout` actions for intrinsic file types. - Updated test cases to validate intrinsic file scenarios. - Refactored `ActionMapping` to correctly map actions for intrinsic files. - Adjusted flag ordering in `Actions` enum to ensure proper action sequence. * Add `AdaptLoadout` and `WriteIntrinsic` actions for intrinsic files - Implemented handling for `AdaptLoadout` and `WriteIntrinsic` actions in synchronizers. - Updated tests to cover intrinsic file scenarios and associated validations. - Enhanced `PluginsFile` with intrinsic file handling and logging. - Adjusted `ActionMapping` to correctly map shorthand to new actions. * Ensure directories exist before writing resolved paths and add `WriteIntrinsic` action validation - Added directory creation for resolved paths in synchronizers to avoid file write failures. - Implemented validation for `WriteIntrinsic` action in debug mode. - Prevented empty plugin lists from being sorted in `PluginsFile` logic. * Remove redundant signature shorthand cases - Reduced signature count from 276 to 256 in verified output. * Refactor `IntrinsicFiles` to use `Dictionary` for improved lookup efficiency and update related logic. * Refactor `IntrinsicFiles` to a method and update synchronization logic for improved flexibility and context handling.
1 parent 4974ab3 commit ef8d6d5

28 files changed

+2717
-734
lines changed

src/NexusMods.Abstractions.GameLocators/GamePath.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ public GamePath(LocationId locationId, RelativePath path)
5252

5353
/// <summary/>
5454
public static bool operator !=(GamePath a, GamePath b) => a.LocationId != b.LocationId || a.Path != b.Path;
55+
56+
/// <summary>
57+
/// Joins the current path with a relative path.
58+
/// </summary>
59+
public static GamePath operator /(GamePath a, RelativePath b) => new(a.LocationId, a.Path / b);
60+
61+
/// <summary>
62+
/// Joins the current path with a string.
63+
/// </summary>
64+
public static GamePath operator /(GamePath a, string b) => new(a.LocationId, a.Path / b);
5565

5666
/// <inheritdoc />
5767
public override bool Equals(object? obj) => obj is GamePath other && Equals(other);

src/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,28 @@ public Dictionary<GamePath, SyncNode> BuildSyncTree<T>(T latestDiskState, T prev
333333
}
334334
}
335335
}
336+
337+
// Add in the intrinsic files
338+
foreach (var file in IntrinsicFiles(loadout).Values)
339+
{
340+
ref var found = ref CollectionsMarshal.GetValueRefOrAddDefault(syncTree, file.Path, out var exists);
341+
if (exists)
342+
{
343+
Logger.LogWarning("Found duplicate intrinsic file `{Path}` in Loadout {LoadoutName} for game {Game}", file.Path, loadout.Name, loadout.InstallationInstance.Game.Name);
344+
}
345+
found.SourceItemType = LoadoutSourceItemType.Intrinsic;
346+
var stream = new MemoryStream();
347+
file.Write(stream, loadout, syncTree);
348+
stream.Position = 0;
349+
var span = stream.GetBuffer().AsSpan(0, (int)stream.Length);
350+
found.Loadout = new SyncNodePart()
351+
{
352+
Hash = span.xxHash3(),
353+
Size = Size.From((ulong)span.Length),
354+
LastModifiedTicks = 0,
355+
};
356+
found.SourceItemType = LoadoutSourceItemType.Intrinsic;
357+
}
336358

337359
// Remove deleted files. I'm not super happy with this requiring a full scan of
338360
// the loadout, but we have to somehow mark the deleted files and then delete them.
@@ -526,7 +548,8 @@ public void ProcessSyncTree(Dictionary<GamePath, SyncNode> tree)
526548
diskArchived: item.HaveDisk && HaveArchive(item.Disk.Hash),
527549
prevArchived: item.HavePrevious && HaveArchive(item.Previous.Hash),
528550
loadoutArchived: item.Loadout.Hash != Hash.Zero && HaveArchive(item.Loadout.Hash),
529-
pathIsIgnored: IsIgnoredBackupPath(path));
551+
pathIsIgnored: IsIgnoredBackupPath(path),
552+
item.SourceItemType);
530553

531554

532555
item.Signature = signature;
@@ -560,6 +583,11 @@ public void ProcessSyncTree(Dictionary<GamePath, SyncNode> tree)
560583
job?.SetStatus("Adding external changes");
561584
ActionIngestFromDisk(syncTree, loadout, tx, ref overridesGroup);
562585
break;
586+
587+
case Actions.AdaptLoadout:
588+
job?.SetStatus("Updating loadout");
589+
await AdaptLoadout(syncTree, register, loadout, tx);
590+
break;
563591

564592
case Actions.DeleteFromDisk:
565593
job?.SetStatus("Deleting files");
@@ -570,6 +598,11 @@ public void ProcessSyncTree(Dictionary<GamePath, SyncNode> tree)
570598
job?.SetStatus("Extracting files");
571599
await ActionExtractToDisk(syncTree, register, tx, gameMetadataId, job);
572600
break;
601+
602+
case Actions.WriteIntrinsic:
603+
job?.SetStatus("Writing intrinsic files");
604+
await ActionWriteIntrinsics(syncTree, register, tx, loadout, job);
605+
break;
573606

574607
case Actions.AddReifiedDelete:
575608
job?.SetStatus("Updating deleted files");
@@ -583,7 +616,7 @@ public void ProcessSyncTree(Dictionary<GamePath, SyncNode> tree)
583616
case Actions.WarnOfConflict:
584617
WarnOfConflict(syncTree);
585618
break;
586-
619+
587620
default:
588621
throw new InvalidOperationException($"Unknown action: {action}");
589622
}
@@ -615,6 +648,45 @@ public void ProcessSyncTree(Dictionary<GamePath, SyncNode> tree)
615648
return loadout;
616649
}
617650

651+
private async Task ActionWriteIntrinsics(Dictionary<GamePath, SyncNode> syncTree, IGameLocationsRegister register, IMainTransaction tx, Loadout.ReadOnly loadout, SynchronizeLoadoutJob? job)
652+
{
653+
var intrinsicFiles = IntrinsicFiles(loadout);
654+
foreach (var (path, node) in syncTree)
655+
{
656+
if (!node.Actions.HasFlag(Actions.WriteIntrinsic))
657+
continue;
658+
659+
if (node.SourceItemType != LoadoutSourceItemType.Intrinsic)
660+
throw new Exception("WriteIntrinsic should only be called on intrinsic files");
661+
662+
var instance = intrinsicFiles[path];
663+
var resolvedPath = register.GetResolvedPath(path);
664+
resolvedPath.Parent.CreateDirectory();
665+
await using var stream = resolvedPath.Create();
666+
stream.SetLength(0);
667+
await instance.Write(stream, loadout, syncTree);
668+
}
669+
}
670+
671+
private async Task AdaptLoadout(Dictionary<GamePath, SyncNode> syncTree, IGameLocationsRegister register, Loadout.ReadOnly loadout, IMainTransaction tx)
672+
{
673+
var intrinsicFiles = IntrinsicFiles(loadout);
674+
foreach (var (path, node) in syncTree)
675+
{
676+
if (!node.Actions.HasFlag(Actions.AdaptLoadout))
677+
continue;
678+
679+
if (node.SourceItemType != LoadoutSourceItemType.Intrinsic)
680+
throw new Exception("AdaptLoadout should only be called on intrinsic files");
681+
682+
var instance = intrinsicFiles[path];
683+
var resolvedPath = register.GetResolvedPath(path);
684+
await using var stream = resolvedPath.Read();
685+
await instance.Ingest(stream, loadout, syncTree, tx);
686+
}
687+
688+
}
689+
618690
private async ValueTask<Loadout.ReadOnly> ReprocessOverrides(Loadout.ReadOnly loadout)
619691
{
620692
loadout = await ReprocessGameUpdates(loadout);
@@ -739,6 +811,16 @@ public async Task RunActions(Dictionary<GamePath, SyncNode> syncTree, GameInstal
739811
case Actions.BackupFile:
740812
await ActionBackupNewFiles(gameInstallation, syncTree);
741813
break;
814+
815+
case Actions.AdaptLoadout:
816+
if (ApplicationConstants.IsDebug && syncTree.Any(n => n.Value.Actions.HasFlag(Actions.AdaptLoadout)))
817+
throw new InvalidOperationException("Cannot adapt loadout when not in a loadout context");
818+
break;
819+
820+
case Actions.WriteIntrinsic:
821+
if (ApplicationConstants.IsDebug && syncTree.Any(n => n.Value.Actions.HasFlag(Actions.AdaptLoadout)))
822+
throw new InvalidOperationException("Cannot adapt loadout when not in a loadout context");
823+
break;
742824

743825
case Actions.IngestFromDisk:
744826
if (ApplicationConstants.IsDebug && syncTree.Any(n => n.Value.Actions.HasFlag(Actions.IngestFromDisk)))
@@ -1849,6 +1931,16 @@ EntityId RemapFn(EntityId entityId)
18491931
}
18501932
}
18511933

1934+
/// <summary>
1935+
/// Gets a set of files intrinsic to this game. Such as mod order files, preference files, etc.
1936+
/// These files will not be backed up and will not be included in the loadout directly. Instead, they are
1937+
/// generated at sync time by calling the implementations of the files themselves.
1938+
/// </summary>
1939+
protected virtual Dictionary<GamePath, IIntrinsicFile> IntrinsicFiles(Loadout.ReadOnly loadout)
1940+
{
1941+
return new();
1942+
}
1943+
18521944
private static string GetNewShortName(IDb db, GameInstallMetadataId installationId)
18531945
{
18541946
var existingShortNames = Loadout.All(db)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.GameLocators;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
4+
namespace NexusMods.Abstractions.Loadouts.Synchronizers;
5+
6+
public interface IIntrinsicFile
7+
{
8+
/// <summary>
9+
/// The game path of this file.
10+
/// </summary>
11+
public GamePath Path { get; }
12+
13+
/// <summary>
14+
/// Write the contents of this file to the stream.
15+
/// </summary>
16+
public Task Write(Stream stream, Loadout.ReadOnly loadout, Dictionary<GamePath, SyncNode> syncTree);
17+
18+
/// <summary>
19+
/// Ingest the contents of the stream into the loadout
20+
/// </summary>
21+
public Task Ingest(Stream stream, Loadout.ReadOnly loadout, Dictionary<GamePath, SyncNode> syncTree, ITransaction tx);
22+
}

src/NexusMods.Abstractions.Loadouts.Synchronizers/LoadoutSourceItem.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ public enum LoadoutSourceItemType : byte
2626
/// This is a deleted file
2727
/// </summary>
2828
Deleted,
29+
30+
/// <summary>
31+
/// A file intrinsic to the game. This file cannot be deleted, and will not show
32+
/// up in the loadout as a file that can be manipluated. Instead the game provides
33+
/// special handling for this file.
34+
/// </summary>
35+
Intrinsic,
2936
}
3037

3138
public readonly record struct LoadoutSourceItem

0 commit comments

Comments
 (0)