Skip to content

refactor(unrealengine): extract UE plugin + wire DI into IPluginContext#25

Merged
dhkatz merged 2 commits into
mainfrom
refactor/unrealengine-plugin
May 25, 2026
Merged

refactor(unrealengine): extract UE plugin + wire DI into IPluginContext#25
dhkatz merged 2 commits into
mainfrom
refactor/unrealengine-plugin

Conversation

@dhkatz
Copy link
Copy Markdown
Contributor

@dhkatz dhkatz commented May 25, 2026

Summary

Continuation of #24's plugin-foundation work. Unreal Engine 4/5 support now lives in a standalone GMConverter.UnrealEngine plugin assembly. The core CLI and UI ship without CUE4Parse or MessagePack — those move with the plugin.

Plugin extraction

  • New GMConverter.UnrealEngine project referencing only GMConverter.SDK, CUE4Parse, MessagePack, and ImageSharp. 33 files moved from Core: PSKImporter, UE4Explorer, UE2Explorer, UE4ScanCache, UE4ExportCache, all of Formats/{Unreal,PSK,PSA}/, plus the UE-specific helpers ExportSessionCache, DecodedImage, BarycentricRasterizer.
  • UnrealEnginePlugin : IPlugin entry registers PSK + UE4/UE2 explorers via DI. plugin.json manifest at the plugin root.
  • Core GMConverter.csproj drops CUE4Parse ProjectReferences and the MessagePack PackageReference — both move to the plugin.

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) to preserve 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 instead.

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 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.

Test plan

  • dotnet build GMConverter.slnx --configuration Release → 0 warnings, 0 errors
  • dotnet format GMConverter.slnx --verify-no-changes --severity warn → clean
  • Empirical: bin/Release/net10.0/plugins/unrealengine/ populated under both CLI and UI with all expected DLLs including CUE4Parse-Natives.dll
  • Manual runtime verification required: PSKImporter is ~1300 lines and is the most-exercised real-world code path. The refactor is mechanical (move + constructor injection + replace 2 new ImageSharpTexture(...) sites with ITextureFactory.FromRgba) but I could not run it against a real Fortnite/UE4 archive. Before merge, please:
    • Open the UI, browse a UE4 archive via the Explorer, confirm assets list correctly
    • Convert a sample asset to glTF, confirm output matches a pre-refactor baseline
    • Convert to MDL, confirm Source compile workspace generates

Notes

This unblocks the eventual extraction of GMConverter.UnrealEngine into its own repo under the new GMConverter org. The plugin's csproj is self-contained — no shared Plugin.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

…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>
Comment thread GMConverter/Plugins/PluginLoadContext.cs Fixed
Comment thread GMConverter/Plugins/PluginLoadContext.cs Fixed
…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>
Comment thread GMConverter/Plugins/PluginLoadContext.cs Dismissed
@dhkatz dhkatz merged commit e27ac7b into main May 25, 2026
4 checks passed
@dhkatz dhkatz deleted the refactor/unrealengine-plugin branch May 25, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant