refactor(unrealengine): extract UE plugin + wire DI into IPluginContext#25
Merged
Conversation
…embly + wire DI into IPluginContext Continues the plugin-based architecture refactor (after #24's foundation). Unreal Engine 4/5 support now lives in a standalone `GMConverter.UnrealEngine` plugin; core CLI/UI built-ins no longer ship CUE4Parse or MessagePack. Plugin extraction - New GMConverter.UnrealEngine project (net10.0) referencing only GMConverter.SDK, CUE4Parse, MessagePack, ImageSharp. 33 files moved from Core: PSKImporter, UE4Explorer, UE2Explorer, UE4ScanCache, UE4ExportCache, all of Formats/{Unreal,PSK,PSA}/, plus the UE-specific helpers ExportSessionCache, DecodedImage, and BarycentricRasterizer. - UnrealEnginePlugin : IPlugin entry registers the PSK importer + UE4/UE2 explorers via DI. plugin.json manifest at the plugin root. - Core's GMConverter.csproj drops CUE4Parse ProjectReferences and the MessagePack PackageReference; both move to the plugin's csproj. DI on IPluginContext (slim API surface) - SDK adds Microsoft.Extensions.DependencyInjection.Abstractions. - IPluginContext now exposes `IServiceProvider Services` plus generic Register<T>() overloads alongside the direct-instance overloads. The generics activate the type via ActivatorUtilities.CreateInstance(provider), resolving constructor parameters from the host's service container. - Named TextureFactory/LoggerFactory context properties removed — single source of truth via Services. Adding a new host service is now a one-line AddSingleton in PluginLoader with zero impact on existing plugins. - IPlugin stays an interface (not a base class): preserves single-inheritance, testability, SDK-as-pure-contract, and consistency with IImporter/IExporter/ IExplorer. - PSKImporter, UE4Explorer, UE2Explorer marked public so reflection-based activation works cleanly across the assembly boundary. Shared utilities promoted to SDK - PerfTimer (75+ call sites split across host and UE plugin) moved to GMConverter.SDK.Common.PerfTimer. - ExplorerFileSystem (used by Core's MOW/Generic explorers and the UE plugin) moved to GMConverter.SDK.Explorer.ExplorerFileSystem. Host bundling - GMConverter.CLI.csproj and GMConverter.UI.csproj opt into the plugin via a <BundledPlugin> item with a PluginId metadata. A loose ProjectReference (ReferenceOutputAssembly=false) enforces build order; a CopyBundledPlugins AfterTargets="Build" target stages the plugin's bin/ into the host's bin/plugins/<id>/ folder. Plugin csproj knows nothing about host paths — after a plugin moves to its own repo, the host removes the BundledPlugin entry; end users get the plugin via a release artifact. Native library handling - PluginLoadContext.LoadUnmanagedDll now falls back to a sibling-folder probe (with platform-conventional shared-library names) when AssemblyDependencyResolver returns null. CUE4Parse-Natives.dll is emitted by CUE4Parse.csproj as Content rather than the runtimes/<rid>/native/ NuGet layout, so deps.json does not list it as a native asset; the fallback closes that gap. Empirically verified: the deployed bin/.../plugins/unrealengine/ contains CUE4Parse-Natives.dll alongside the managed assemblies. Host-side surface cleanup - ConversionService (UI) and Program.cs (CLI) drop the hardcoded "psk" case from their input-format switches; resolution falls through to PluginHost.Registry.GetImporter() and surfaces a clearer "plugin may contribute additional formats" error when the format is not recognized. - ExplorerService drops UE4Explorer/UE2Explorer from its built-in list; both come through PluginHost.Registry.Explorers now. - ConvertViewModel keeps a hardcoded "psk" display entry behind a TODO — dynamic projection over built-ins + plugin registry is out of scope for this PR. Verification gap - PSKImporter is ~1300 lines of pixel-path code and the most-exercised real-world path. The refactor is mechanical (move + constructor injection + replace 2 `new ImageSharpTexture(...)` sites with `ITextureFactory.FromRgba`) but I cannot run it against a real Fortnite/UE4 archive from CI. Reviewer should do at least one UE4 -> glTF and one UE4 -> MDL roundtrip manually before merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fallback Address two github-code-quality bot comments on #25: - Path.Combine -> Path.Join in the sibling-folder probe. The candidate names come from EnumerateNativeFileNames which never produces a rooted path, but Path.Join is the right primitive for "append a known file name to a trusted directory" and silences the analyzer's silent-drop concern. - foreach now iterates a single Select projection rather than mapping the iteration variable inside the loop body. Stylistic cleanup; identical behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Continuation of #24's plugin-foundation work. Unreal Engine 4/5 support now lives in a standalone
GMConverter.UnrealEngineplugin assembly. The core CLI and UI ship without CUE4Parse or MessagePack — those move with the plugin.Plugin extraction
GMConverter.UnrealEngineproject referencing onlyGMConverter.SDK, CUE4Parse, MessagePack, and ImageSharp. 33 files moved from Core:PSKImporter,UE4Explorer,UE2Explorer,UE4ScanCache,UE4ExportCache, all ofFormats/{Unreal,PSK,PSA}/, plus the UE-specific helpersExportSessionCache,DecodedImage,BarycentricRasterizer.UnrealEnginePlugin : IPluginentry registers PSK + UE4/UE2 explorers via DI.plugin.jsonmanifest at the plugin root.GMConverter.csprojdrops CUE4ParseProjectReferences and the MessagePackPackageReference— both move to the plugin.DI on IPluginContext (slim API surface)
Microsoft.Extensions.DependencyInjection.Abstractions.IPluginContextnow exposesIServiceProvider Servicesplus genericRegister<T>()overloads alongside the direct-instance overloads. The generics activate the type viaActivatorUtilities.CreateInstance(provider), resolving constructor parameters from the host's service container.TextureFactory/LoggerFactorycontext properties removed — single source of truth viaServices. Adding a new host service is now a one-lineAddSingletoninPluginLoaderwith zero impact on existing plugins.IPluginstays an interface (not a base class) to preserve single-inheritance, testability, SDK-as-pure-contract, and consistency withIImporter/IExporter/IExplorer.PSKImporter,UE4Explorer,UE2Explorermarkedpublicso reflection-based activation works cleanly across the assembly boundary.Shared utilities promoted to SDK
PerfTimer(75+ call sites split across host and UE plugin) moved toGMConverter.SDK.Common.PerfTimer.ExplorerFileSystem(used by Core's MOW/Generic explorers and the UE plugin) moved toGMConverter.SDK.Explorer.ExplorerFileSystem.Host bundling
GMConverter.CLI.csprojandGMConverter.UI.csprojopt into the plugin via a<BundledPlugin>item with aPluginIdmetadata. A looseProjectReference(ReferenceOutputAssembly=false) enforces build order; aCopyBundledPluginsAfterTargets="Build"target stages the plugin'sbin/into the host'sbin/plugins/<id>/folder.BundledPluginentry; end users get the plugin via a release artifact instead.Native library handling
PluginLoadContext.LoadUnmanagedDllnow falls back to a sibling-folder probe (with platform-conventional shared-library names) whenAssemblyDependencyResolverreturnsnull.CUE4Parse-Natives.dllis emitted by CUE4Parse.csproj asContentrather than theruntimes/<rid>/native/NuGet layout, sodeps.jsondoes not list it as a native asset; the fallback closes that gap. Empirically verified: the deployedbin/.../plugins/unrealengine/containsCUE4Parse-Natives.dllalongside the managed assemblies.Host-side surface cleanup
ConversionService(UI) andProgram.cs(CLI) drop the hardcoded"psk"case from their input-format switches; resolution falls through toPluginHost.Registry.GetImporter()and surfaces a clearer error when the format is not recognized.ExplorerServicedropsUE4Explorer/UE2Explorerfrom its built-in list; both come throughPluginHost.Registry.Explorersnow.ConvertViewModelkeeps a hardcoded"psk"display entry behind aTODO— dynamic projection over built-ins + plugin registry is out of scope for this PR.Test plan
dotnet build GMConverter.slnx --configuration Release→ 0 warnings, 0 errorsdotnet format GMConverter.slnx --verify-no-changes --severity warn→ cleanbin/Release/net10.0/plugins/unrealengine/populated under both CLI and UI with all expected DLLs includingCUE4Parse-Natives.dllnew ImageSharpTexture(...)sites withITextureFactory.FromRgba) but I could not run it against a real Fortnite/UE4 archive. Before merge, please:Notes
This unblocks the eventual extraction of
GMConverter.UnrealEngineinto its own repo under the newGMConverterorg. The plugin's csproj is self-contained — no sharedPlugin.props, no host-path coupling. The remaining migrations (Source as an output plugin, Frostbite from its parked branch) follow the same shape.🤖 Generated with Claude Code