Skip to content

Commit b3f2973

Browse files
authored
Merge pull request #4066 from erri120/task/4059
Fix file conflict priorities when cloning collections
2 parents 6e8e066 + 139c0d0 commit b3f2973

File tree

8 files changed

+216
-191
lines changed

8 files changed

+216
-191
lines changed

src/NexusMods.Abstractions.Collections/NexusCollectionLoadoutGroup.cs

Lines changed: 0 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -44,105 +44,4 @@ public partial struct ReadOnly
4444
.Where(static model => model.IsValid());
4545
}
4646
}
47-
48-
public static async Task<EntityId> MakeEditableLocalCollection(IConnection conn, EntityId collId, string newName)
49-
{
50-
var cloneId = await CollectionGroup.Clone(conn, collId);
51-
var cloneEnt = Load(conn.Db, cloneId);
52-
53-
using var tx = conn.BeginTransaction();
54-
// Remap the name
55-
tx.Add(cloneId, LoadoutItem.Name, newName);
56-
// Make it editable
57-
tx.Add(cloneId, CollectionGroup.IsReadOnly, false);
58-
// Retract the Nexus references as this is no longer associated with the official collection
59-
tx.Retract(cloneId, RevisionId, RevisionId.Get(cloneEnt));
60-
tx.Retract(cloneId, CollectionId, CollectionId.Get(cloneEnt));
61-
tx.Retract(cloneId, LibraryFileId, LibraryFileId.Get(cloneEnt));
62-
63-
// Retract the Nexus references in items so that this is no longer associated with the official collection
64-
var db = conn.Db;
65-
foreach (var item in conn.Query<EntityId>($"SELECT Id FROM mdb_NexusCollectionItemLoadoutGroup(Db=>{db}) WHERE Parent = {cloneId}"))
66-
{
67-
var ent = NexusCollectionItemLoadoutGroup.Load(db, item);
68-
tx.Retract(item, NexusCollectionItemLoadoutGroup.Download, NexusCollectionItemLoadoutGroup.Download.Get(ent));
69-
tx.Retract(item, NexusCollectionItemLoadoutGroup.IsRequired, NexusCollectionItemLoadoutGroup.IsRequired.Get(ent));
70-
71-
if (NexusCollectionReplicatedLoadoutGroup.Replicated.TryGetValue(ent, out var replicated))
72-
tx.Retract(item, NexusCollectionReplicatedLoadoutGroup.Replicated, replicated);
73-
74-
if (NexusCollectionBundledLoadoutGroup.CollectionLibraryFileId.TryGetValue(ent, out var bundleLibraryFileId))
75-
tx.Retract(item, NexusCollectionBundledLoadoutGroup.CollectionLibraryFileId, bundleLibraryFileId);
76-
77-
if (NexusCollectionBundledLoadoutGroup.BundleDownload.TryGetValue(ent, out var bundleDownload))
78-
{
79-
tx.Retract(item, NexusCollectionBundledLoadoutGroup.BundleDownload, bundleDownload);
80-
81-
// We've now orphaned the bundled files, so we'll now create a download archive that contains the files this loadout group needs.
82-
83-
var fileName = "Bundled Files - " + ent.AsLoadoutItemGroup().AsLoadoutItem().Name;
84-
85-
// Create a new library item,
86-
var libraryFile = new ManuallyCreatedArchive.New(tx, out var libraryFileId)
87-
{
88-
Source = ManuallyCreatedArchive.CreationSource.CollectionBundled,
89-
LibraryArchive = new LibraryArchive.New(tx, libraryFileId)
90-
{
91-
IsArchive = true,
92-
LibraryFile = new LibraryFile.New(tx, libraryFileId)
93-
{
94-
Hash = Hash.Zero,
95-
// We'll update the size later with the total size of all the files
96-
Size = Size.Zero,
97-
FileName = fileName,
98-
LibraryItem = new LibraryItem.New(tx, libraryFileId)
99-
{
100-
Name = fileName,
101-
},
102-
},
103-
104-
},
105-
};
106-
107-
// Link the mod group to the archive
108-
tx.Add(item, LibraryLinkedLoadoutItem.LibraryItemId, libraryFileId);
109-
110-
var added = new HashSet<Hash>();
111-
112-
var sum = Size.Zero;
113-
// Now link up all the required items
114-
foreach (var child in ent.AsLoadoutItemGroup().Children.OfTypeLoadoutItemWithTargetPath().OfTypeLoadoutFile())
115-
{
116-
// Don't add duplicates
117-
if (added.Contains(child.Hash))
118-
continue;
119-
120-
var name = child.AsLoadoutItemWithTargetPath().AsLoadoutItem().Name;
121-
_ = new LibraryArchiveFileEntry.New(tx, out var fileId)
122-
{
123-
ParentId = libraryFileId,
124-
Path = child.AsLoadoutItemWithTargetPath().TargetPath.Item3,
125-
LibraryFile = new LibraryFile.New(tx, fileId)
126-
{
127-
Hash = child.Hash,
128-
Size = child.Size,
129-
FileName = name,
130-
LibraryItem = new LibraryItem.New(tx, fileId)
131-
{
132-
Name = name,
133-
},
134-
},
135-
};
136-
sum += child.Size;
137-
added.Add(child.Hash);
138-
}
139-
140-
tx.Add(libraryFileId, NexusMods.Abstractions.Library.Models.LibraryFile.Size, sum);
141-
}
142-
143-
}
144-
145-
await tx.Commit();
146-
return cloneId;
147-
}
14847
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ IJobTask<IInstallLoadoutItemJob, InstallLoadoutItemJobResult> InstallItem(
7878
/// </summary>
7979
ValueTask RemoveCollection(CollectionGroupId collection);
8080

81+
/// <summary>
82+
/// Clones a collection.
83+
/// </summary>
84+
ValueTask<CollectionGroup.ReadOnly> CloneCollection(CollectionGroupId collection);
85+
8186
/// <summary>
8287
/// Removes all groups and installs the new library item.
8388
/// </summary>

src/NexusMods.Abstractions.Loadouts/Models/CollectionGroup.cs

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
1-
using System.Runtime.InteropServices;
2-
using DynamicData.Kernel;
31
using NexusMods.HyperDuck;
42
using NexusMods.MnemonicDB.Abstractions;
53
using NexusMods.MnemonicDB.Abstractions.Attributes;
6-
using NexusMods.MnemonicDB.Abstractions.ElementComparers;
7-
using NexusMods.MnemonicDB.Abstractions.IndexSegments;
84
using NexusMods.MnemonicDB.Abstractions.Models;
9-
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
105

116
namespace NexusMods.Abstractions.Loadouts;
127

@@ -22,84 +17,6 @@ public partial class CollectionGroup : IModelDefinition
2217
/// If the collection is read-only it won't support adding new mods or modifying the existing files.
2318
/// </summary>
2419
public static readonly BooleanAttribute IsReadOnly = new(Namespace, nameof(IsReadOnly)) { IsIndexed = true };
25-
26-
27-
28-
private const string CollectionEntities = """
29-
30-
""";
31-
32-
/// <summary>
33-
/// Performs a deep clone of the collection, and includes all child loadout items and sortable items and lists
34-
/// </summary>
35-
public static async Task<EntityId> Clone(IConnection conn, EntityId id)
36-
{
37-
Span<byte> refScratch = stackalloc byte[8];
38-
using var writer = new PooledMemoryBufferWriter();
39-
40-
var basisDb = conn.Db;
41-
42-
Dictionary<EntityId, EntityId> remappedIds = new();
43-
var query = conn.Query<EntityId>($"""
44-
WITH RECURSIVE ChildLoadoutItems (Id) AS
45-
(SELECT {id}
46-
UNION
47-
SELECT Id FROM (SELECT Id, Parent FROM mdb_LoadoutItem(Db=>{basisDb})
48-
UNION ALL
49-
SELECT Id, ParentEntity FROM mdb_SortOrder(Db=>{basisDb})
50-
UNION ALL
51-
SELECT Id, ParentSortOrder FROM mdb_SortOrderItem(Db=>{basisDb}))
52-
WHERE Parent in (SELECT Id FROM ChildLoadoutItems))
53-
SELECT DISTINCT Id FROM ChildLoadoutItems
54-
""");
55-
using var tx = conn.BeginTransaction();
56-
foreach (var itemId in query)
57-
{
58-
remappedIds.TryAdd(itemId, tx.TempId());
59-
}
60-
61-
foreach (var (oldId, newId) in remappedIds)
62-
{
63-
var entity = basisDb.Get(oldId);
64-
foreach (var datom in entity)
65-
{
66-
// Remap the value part of references
67-
if (datom.Prefix.ValueTag == ValueTag.Reference)
68-
{
69-
var oldRef = EntityId.From(UInt64Serializer.Read(datom.ValueSpan));
70-
if (!remappedIds.TryGetValue(oldRef, out var newRef))
71-
{
72-
tx.Add(newId, datom.A, datom.Prefix.ValueTag, datom.ValueSpan);
73-
continue;
74-
}
75-
MemoryMarshal.Write(refScratch, newRef);
76-
tx.Add(newId, datom.A, datom.Prefix.ValueTag, refScratch);
77-
}
78-
// It's rare, but the Ref,UShort/String tuple type may include a ref that needs to be remapped
79-
else if (datom.Prefix.ValueTag == ValueTag.Tuple3_Ref_UShort_Utf8I)
80-
{
81-
var (r, s, str) = Tuple3_Ref_UShort_Utf8I_Serializer.Read(datom.ValueSpan);
82-
if (!remappedIds.TryGetValue(r, out var newR))
83-
{
84-
tx.Add(newId, datom.A, datom.Prefix.ValueTag, datom.ValueSpan);
85-
continue;
86-
}
87-
writer.Reset();
88-
var newTuple = (newR, s, str);
89-
Tuple3_Ref_UShort_Utf8I_Serializer.Write(newTuple, writer);
90-
tx.Add(newId, datom.A, datom.Prefix.ValueTag, writer.GetWrittenSpan());
91-
}
92-
// Otherwise just remap the E value
93-
else
94-
{
95-
tx.Add(newId, datom.A, datom.Prefix.ValueTag, datom.ValueSpan);
96-
}
97-
}
98-
}
99-
100-
var result = await tx.Commit();
101-
return result[remappedIds[id]];
102-
}
10320
}
10421

10522
public static partial class CollectionGroupLoaderExtensions

src/NexusMods.App.UI/Pages/LoadoutPage/CollectionLoadoutViewModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using NexusMods.App.UI.Pages.CollectionDownload;
1717
using NexusMods.App.UI.Windows;
1818
using NexusMods.App.UI.WorkspaceSystem;
19+
using NexusMods.Collections;
1920
using NexusMods.MnemonicDB.Abstractions;
2021
using NexusMods.MnemonicDB.Abstractions.ElementComparers;
2122
using NexusMods.MnemonicDB.Abstractions.Query;
@@ -167,8 +168,8 @@ public CollectionLoadoutViewModel(
167168
if (result.ButtonId != ButtonDefinitionId.Accept || string.IsNullOrWhiteSpace(result.InputText))
168169
return;
169170

170-
var cloneId = await NexusCollectionLoadoutGroup.MakeEditableLocalCollection(group.Db.Connection, group.Id, result.InputText);
171-
171+
var cloneId = await CollectionCreator.MakeEditableLocalCollection(serviceProvider.GetRequiredService<ILoadoutManager>(), group.Db.Connection, group.Id, result.InputText);
172+
172173
var pageData = new PageData
173174
{
174175
FactoryId = LoadoutPageFactory.StaticId,

src/NexusMods.Collections/CollectionCreator.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
using NexusMods.Abstractions.Games.FileHashes;
1212
using NexusMods.Abstractions.Library.Models;
1313
using NexusMods.Abstractions.Loadouts;
14+
using NexusMods.Abstractions.Loadouts.Synchronizers;
1415
using NexusMods.Abstractions.NexusModsLibrary;
1516
using NexusMods.Abstractions.NexusModsLibrary.Models;
1617
using NexusMods.Abstractions.NexusWebApi;
1718
using NexusMods.Abstractions.NexusWebApi.Types;
19+
using NexusMods.Hashing.xxHash3;
1820
using NexusMods.MnemonicDB.Abstractions;
1921
using NexusMods.Networking.NexusWebApi;
2022
using NexusMods.Networking.NexusWebApi.Errors;
@@ -61,6 +63,111 @@ private static string GenerateNewCollectionName(string[] allNames)
6163
string TemplatedName() => string.Format(template, ++count);
6264
}
6365

66+
public static async Task<EntityId> MakeEditableLocalCollection(
67+
ILoadoutManager loadoutManager,
68+
IConnection conn,
69+
CollectionGroupId collectionGroupId,
70+
string newName)
71+
{
72+
var cloneId = await loadoutManager.CloneCollection(collectionGroupId);
73+
var cloneEnt = NexusCollectionLoadoutGroup.Load(conn.Db, cloneId);
74+
75+
using var tx = conn.BeginTransaction();
76+
// Remap the name
77+
tx.Add(cloneId, LoadoutItem.Name, newName);
78+
// Make it editable
79+
tx.Add(cloneId, CollectionGroup.IsReadOnly, false);
80+
// Retract the Nexus references as this is no longer associated with the official collection
81+
tx.Retract(cloneId, NexusCollectionLoadoutGroup.RevisionId, NexusCollectionLoadoutGroup.RevisionId.Get(cloneEnt));
82+
tx.Retract(cloneId, NexusCollectionLoadoutGroup.CollectionId, NexusCollectionLoadoutGroup.CollectionId.Get(cloneEnt));
83+
tx.Retract(cloneId, NexusCollectionLoadoutGroup.LibraryFileId, NexusCollectionLoadoutGroup.LibraryFileId.Get(cloneEnt));
84+
85+
// Retract the Nexus references in items so that this is no longer associated with the official collection
86+
var db = conn.Db;
87+
foreach (var item in conn.Query<EntityId>($"SELECT Id FROM mdb_NexusCollectionItemLoadoutGroup(Db=>{db}) WHERE Parent = {cloneId.Id}"))
88+
{
89+
var ent = NexusCollectionItemLoadoutGroup.Load(db, item);
90+
tx.Retract(item, NexusCollectionItemLoadoutGroup.Download, NexusCollectionItemLoadoutGroup.Download.Get(ent));
91+
tx.Retract(item, NexusCollectionItemLoadoutGroup.IsRequired, NexusCollectionItemLoadoutGroup.IsRequired.Get(ent));
92+
93+
if (NexusCollectionReplicatedLoadoutGroup.Replicated.TryGetValue(ent, out var replicated))
94+
tx.Retract(item, NexusCollectionReplicatedLoadoutGroup.Replicated, replicated);
95+
96+
if (NexusCollectionBundledLoadoutGroup.CollectionLibraryFileId.TryGetValue(ent, out var bundleLibraryFileId))
97+
tx.Retract(item, NexusCollectionBundledLoadoutGroup.CollectionLibraryFileId, bundleLibraryFileId);
98+
99+
if (NexusCollectionBundledLoadoutGroup.BundleDownload.TryGetValue(ent, out var bundleDownload))
100+
{
101+
tx.Retract(item, NexusCollectionBundledLoadoutGroup.BundleDownload, bundleDownload);
102+
103+
// We've now orphaned the bundled files, so we'll now create a download archive that contains the files this loadout group needs.
104+
105+
var fileName = "Bundled Files - " + ent.AsLoadoutItemGroup().AsLoadoutItem().Name;
106+
107+
// Create a new library item,
108+
var libraryFile = new ManuallyCreatedArchive.New(tx, out var libraryFileId)
109+
{
110+
Source = ManuallyCreatedArchive.CreationSource.CollectionBundled,
111+
LibraryArchive = new LibraryArchive.New(tx, libraryFileId)
112+
{
113+
IsArchive = true,
114+
LibraryFile = new LibraryFile.New(tx, libraryFileId)
115+
{
116+
Hash = Hash.Zero,
117+
// We'll update the size later with the total size of all the files
118+
Size = Size.Zero,
119+
FileName = fileName,
120+
LibraryItem = new LibraryItem.New(tx, libraryFileId)
121+
{
122+
Name = fileName,
123+
},
124+
},
125+
126+
},
127+
};
128+
129+
// Link the mod group to the archive
130+
tx.Add(item, LibraryLinkedLoadoutItem.LibraryItemId, libraryFileId);
131+
132+
var added = new HashSet<Hash>();
133+
134+
var sum = Size.Zero;
135+
// Now link up all the required items
136+
foreach (var child in ent.AsLoadoutItemGroup().Children.OfTypeLoadoutItemWithTargetPath().OfTypeLoadoutFile())
137+
{
138+
// Don't add duplicates
139+
if (added.Contains(child.Hash))
140+
continue;
141+
142+
var name = child.AsLoadoutItemWithTargetPath().AsLoadoutItem().Name;
143+
_ = new LibraryArchiveFileEntry.New(tx, out var fileId)
144+
{
145+
ParentId = libraryFileId,
146+
Path = child.AsLoadoutItemWithTargetPath().TargetPath.Item3,
147+
LibraryFile = new LibraryFile.New(tx, fileId)
148+
{
149+
Hash = child.Hash,
150+
Size = child.Size,
151+
FileName = name,
152+
LibraryItem = new LibraryItem.New(tx, fileId)
153+
{
154+
Name = name,
155+
},
156+
},
157+
};
158+
sum += child.Size;
159+
added.Add(child.Hash);
160+
}
161+
162+
tx.Add(libraryFileId, NexusMods.Abstractions.Library.Models.LibraryFile.Size, sum);
163+
}
164+
165+
}
166+
167+
await tx.Commit();
168+
return cloneId;
169+
}
170+
64171
/// <summary>
65172
/// Creates a new collection group in the loadout.
66173
/// </summary>

0 commit comments

Comments
 (0)