Skip to content
Open
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
1 change: 1 addition & 0 deletions Editors/Ipc/IpcEditor/DependencyInjectionContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public override void Register(IServiceCollection serviceCollection)
serviceCollection.AddTransient<IIpcUserNotifier, IpcUserNotifier>();
serviceCollection.AddTransient<IExternalFileOpenExecutor, ExternalFileOpenExecutor>();
serviceCollection.AddTransient<IIpcRequestHandler, IpcRequestHandler>();
serviceCollection.AddSingleton<IUiDispatcher, WpfUiDispatcher>();
serviceCollection.AddSingleton<AssetEditorIpcServer>();
}

Expand Down
70 changes: 30 additions & 40 deletions Editors/Ipc/IpcEditor/ExternalPackLoader.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Windows;
using Shared.Core.PackFiles;
using Shared.Core.PackFiles;
using Shared.Core.PackFiles.Models;
using Shared.Core.PackFiles.Utility;

Expand All @@ -10,59 +9,60 @@ public class ExternalPackLoader : IExternalPackLoader
private readonly ILogger _logger = Logging.Create<ExternalPackLoader>();
private readonly IPackFileService _packFileService;
private readonly IPackFileContainerLoader _packFileContainerLoader;
private readonly IUiDispatcher _uiDispatcher;

public ExternalPackLoader(IPackFileService packFileService, IPackFileContainerLoader packFileContainerLoader)
public ExternalPackLoader(IPackFileService packFileService, IPackFileContainerLoader packFileContainerLoader, IUiDispatcher uiDispatcher)
{
_packFileService = packFileService;
_packFileContainerLoader = packFileContainerLoader;
_uiDispatcher = uiDispatcher;
}

public Task<PackLoadResult> EnsureLoadedAsync(string packPathOnDisk, CancellationToken cancellationToken)
public async Task<PackLoadResult> EnsureLoadedAsync(string packPathOnDisk, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

if (string.IsNullOrWhiteSpace(packPathOnDisk))
return Task.FromResult(PackLoadResult.Ok());
return PackLoadResult.Ok();

var normalizedDiskPath = NormalizeDiskPath(packPathOnDisk);
if (string.IsNullOrWhiteSpace(normalizedDiskPath))
return Task.FromResult(PackLoadResult.Fail("Pack path is empty"));

var alreadyLoaded = _packFileService
.GetAllPackfileContainers()
.Any(x => x.GetAllFiles().ContainsKey(normalizedDiskPath));


if (alreadyLoaded)
return Task.FromResult(PackLoadResult.Ok());
return PackLoadResult.Fail("Pack path is empty");

try
{
var container = _packFileContainerLoader.CreateFromPackFile(PackFileContainerType.Normal, normalizedDiskPath, true);
if (container == null)
return Task.FromResult(PackLoadResult.Fail("Pack file could not be loaded"));

var added = AddContainerOnUiThread(container);
if (added == null)
return Task.FromResult(PackLoadResult.Fail("Pack file could not be added"));

_logger.Here().Information($"Externally loaded pack file {normalizedDiskPath}");
return Task.FromResult(PackLoadResult.Ok());
// The pack loader owns WPF wait-cursor handling, so both loading and
// publishing the new container must run on the application dispatcher.
return await _uiDispatcher.InvokeAsync(
() => EnsureLoadedOnUiThread(normalizedDiskPath),
cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
_logger.Here().Error(ex, $"Failed loading external pack file {normalizedDiskPath}");
return Task.FromResult(PackLoadResult.Fail("Pack file load failed"));
return PackLoadResult.Fail("Pack file load failed");
}
}

private IPackFileContainer AddContainerOnUiThread(IPackFileContainer container)
private PackLoadResult EnsureLoadedOnUiThread(string normalizedDiskPath)
{
var app = Application.Current;
if (app?.Dispatcher == null || app.Dispatcher.CheckAccess())
return _packFileService.AddContainer(container, false);
if (_packFileService.IsPackFileLoaded(normalizedDiskPath))
return PackLoadResult.Ok();

return app.Dispatcher.Invoke(() => _packFileService.AddContainer(container, false));
var container = _packFileContainerLoader.CreateFromPackFile(PackFileContainerType.Normal, normalizedDiskPath, true);
if (container == null)
return PackLoadResult.Fail("Pack file could not be loaded");

var added = _packFileService.AddContainer(container, false);
if (added == null)
return PackLoadResult.Fail("Pack file could not be added");

_logger.Here().Information($"Externally loaded pack file {normalizedDiskPath}");
return PackLoadResult.Ok();
}

private static string NormalizeDiskPath(string input)
Expand All @@ -89,15 +89,5 @@ private static string NormalizeDiskPath(string input)
return path;
}
}

private static bool PathsEqual(string left, string right)
{
if (string.IsNullOrWhiteSpace(left) || string.IsNullOrWhiteSpace(right))
return false;

var normalizedLeft = left.Replace('/', '\\').Trim();
var normalizedRight = right.Replace('/', '\\').Trim();
return string.Equals(normalizedLeft, normalizedRight, StringComparison.OrdinalIgnoreCase);
}
}
}
24 changes: 24 additions & 0 deletions Editors/Ipc/IpcEditor/IUiDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Windows;
using System.Windows.Threading;

namespace Editors.Ipc
{
public interface IUiDispatcher
{
Task<T> InvokeAsync<T>(Func<T> action, CancellationToken cancellationToken);
}

internal sealed class WpfUiDispatcher : IUiDispatcher
{
public async Task<T> InvokeAsync<T>(Func<T> action, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var dispatcher = Application.Current?.Dispatcher;
if (dispatcher == null || dispatcher.CheckAccess())
return action();

return await dispatcher.InvokeAsync(action, DispatcherPriority.Normal, cancellationToken);
}
}
}
97 changes: 97 additions & 0 deletions Editors/Ipc/Test.Ipc/ExternalPackLoaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using Editors.Ipc;
using Moq;
using Shared.Core.PackFiles;
using Shared.Core.PackFiles.Models;
using Shared.Core.PackFiles.Utility;

namespace Test.Ipc
{
public class ExternalPackLoaderTests
{
[Test]
public async Task EnsureLoadedAsync_LoadsAndAddsPackInsideUiDispatcher()
{
var packPath = Path.GetFullPath("external.pack");
var container = new Mock<IPackFileContainer>().Object;
var packFileService = new Mock<IPackFileService>();
var containerLoader = new Mock<IPackFileContainerLoader>();
var dispatcher = new RecordingUiDispatcher();

packFileService
.Setup(x => x.IsPackFileLoaded(packPath))
.Returns(false);
containerLoader
.Setup(x => x.CreateFromPackFile(PackFileContainerType.Normal, packPath, true))
.Returns(() =>
{
Assert.That(dispatcher.IsExecuting, Is.True);
return container;
});
packFileService
.Setup(x => x.AddContainer(container, false))
.Returns(() =>
{
Assert.That(dispatcher.IsExecuting, Is.True);
return container;
});

var sut = new ExternalPackLoader(packFileService.Object, containerLoader.Object, dispatcher);

var result = await sut.EnsureLoadedAsync(packPath, CancellationToken.None);

Assert.That(result.Success, Is.True);
Assert.That(dispatcher.InvocationCount, Is.EqualTo(1));
containerLoader.Verify(
x => x.CreateFromPackFile(PackFileContainerType.Normal, packPath, true),
Times.Once);
packFileService.Verify(x => x.AddContainer(container, false), Times.Once);
}

[Test]
public async Task EnsureLoadedAsync_DoesNotReloadPack_WhenSourcePackIsAlreadyLoaded()
{
var packPath = Path.GetFullPath("already-loaded.pack");
var packFileService = new Mock<IPackFileService>();
var containerLoader = new Mock<IPackFileContainerLoader>();
var dispatcher = new RecordingUiDispatcher();

packFileService
.Setup(x => x.IsPackFileLoaded(packPath))
.Returns(true);

var sut = new ExternalPackLoader(packFileService.Object, containerLoader.Object, dispatcher);

var result = await sut.EnsureLoadedAsync(packPath, CancellationToken.None);

Assert.That(result.Success, Is.True);
Assert.That(dispatcher.InvocationCount, Is.EqualTo(1));
containerLoader.Verify(
x => x.CreateFromPackFile(It.IsAny<PackFileContainerType>(), It.IsAny<string>(), It.IsAny<bool>()),
Times.Never);
packFileService.Verify(
x => x.AddContainer(It.IsAny<IPackFileContainer>(), It.IsAny<bool>()),
Times.Never);
}

private sealed class RecordingUiDispatcher : IUiDispatcher
{
public int InvocationCount { get; private set; }
public bool IsExecuting { get; private set; }

public Task<T> InvokeAsync<T>(Func<T> action, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
InvocationCount++;
IsExecuting = true;
try
{
return Task.FromResult(action());
}
finally
{
IsExecuting = false;
}
}
}
}
}
3 changes: 2 additions & 1 deletion Editors/Ipc/Test.Ipc/Test.Ipc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.7.0" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
Expand All @@ -22,4 +23,4 @@
<Using Include="NUnit.Framework" />
</ItemGroup>

</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public interface IPackFileService
void DeleteFolder(IPackFileContainer pf, string folder);
PackFile? FindFile(string path, IPackFileContainer? container = null);
List<IPackFileContainer> GetAllPackfileContainers();
bool IsPackFileLoaded(string packFilePath);
IPackFileContainer? GetEditablePack();
string GetFullPath(PackFile file, IPackFileContainer? container = null);
IPackFileContainer? GetPackFileContainer(PackFile file);
Expand Down
31 changes: 31 additions & 0 deletions Shared/SharedCore/Shared.Core/PackFiles/PackFileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,31 @@ public PackFileService(IGlobalEventHub? globalEventHub)

public List<IPackFileContainer> GetAllPackfileContainers() => _packFileContainers.Cast<IPackFileContainer>().ToList();

public bool IsPackFileLoaded(string packFilePath)
{
if (string.IsNullOrWhiteSpace(packFilePath))
return false;

var normalizedPath = NormalizeSystemPath(packFilePath);
foreach (var container in _packFileContainers)
{
if (PathsEqual(container.SystemFilePath, normalizedPath))
return true;

var sourcePackFilePaths = container switch
{
PackFileContainer packFileContainer => packFileContainer.SourcePackFilePaths,
CachedPackFileContainer cachedPackFileContainer => cachedPackFileContainer.SourcePackFilePaths,
_ => []
};

if (sourcePackFilePaths.Any(path => PathsEqual(path, normalizedPath)))
return true;
}

return false;
}

public IPackFileContainer? AddContainer(IPackFileContainer container, bool setToMainPackIfFirst = false)
{
var pf = CastContainer(container);
Expand Down Expand Up @@ -388,6 +413,12 @@ private static string NormalizeSystemPath(string path)
}
}

private static bool PathsEqual(string? path, string normalizedPath)
{
return string.IsNullOrWhiteSpace(path) == false
&& NormalizeSystemPath(path) == normalizedPath;
}

private static string DescribeFile(IPackFileContainer container, PackFile file)
{
var concreteContainer = CastContainer(container);
Expand Down
30 changes: 30 additions & 0 deletions Shared/SharedCore/Shared.CoreTest/PackFiles/PackFileServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,36 @@ public void AddContainers_Duplicate()
dialogProvider.Verify(m => m.ShowDialogBox(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}

[Test]
public void IsPackFileLoaded_ReturnsTrue_ForContainerSystemPath()
{
var pfs = CreateService();
var packPath = Path.GetFullPath("direct.pack");
pfs.AddContainer(PackFileContainer.CreateCaPackFile("Direct", packPath));

var result = pfs.IsPackFileLoaded(packPath.ToUpperInvariant());

Assert.That(result, Is.True);
}

[Test]
public void IsPackFileLoaded_ReturnsTrue_ForSourcePackInCachedAggregate()
{
var pfs = CreateService();
var sourcePackPath = Path.GetFullPath("variants.pack");
using var cached = CachedPackFileContainer.CreateFromFileList(
"All Game Packs",
[],
useInMemoryDb: true,
systemFilePath: Path.GetDirectoryName(sourcePackPath),
sourcePackFilePath: sourcePackPath);
pfs.AddContainer(cached);

var result = pfs.IsPackFileLoaded(sourcePackPath);

Assert.That(result, Is.True);
}

[Test]
public void CreateNewPackFileContainer()
{
Expand Down
Loading