Skip to content

Fix Release launcher dependency loading from lib#2749

Open
mindless2831 wants to merge 2 commits into
SubnauticaNitrox:masterfrom
mindless2831:fix-launcher-release-lib-loading
Open

Fix Release launcher dependency loading from lib#2749
mindless2831 wants to merge 2 commits into
SubnauticaNitrox:masterfrom
mindless2831:fix-launcher-release-lib-loading

Conversation

@mindless2831
Copy link
Copy Markdown
Contributor

@mindless2831 mindless2831 commented May 18, 2026

Summary

Fixes Release launcher dependency loading so the launcher/server side resolves the correct assemblies from the Release output layout.

This PR is specifically about the Release build/package output, not Debug. In the broken Release output, the launcher/server path could load stale or incompatible assemblies from the wrong deployed location. That caused runtime MissingMethodExceptions even though the DLL files existed.

Problem

The failure was not a simple missing-file issue. The runtime was finding assemblies, but some of them were the wrong version / wrong target asset for the code that was calling them.

The concrete mismatches observed were:

Grpc.Core.Api.dll

The stale/bad loaded version was:

  • Grpc.Core.Api.dll version 2.57.0

The dependent library involved was:

  • MagicOnion 7.0.6

The concrete missing method was:

  • Grpc.Core.IAsyncStreamWriter<T>.WriteAsync(T, CancellationToken)

This means the runtime had loaded a gRPC API assembly, but not one with the API surface expected by the newer MagicOnion dependency.

Nitrox.Model.dll

The launcher/server path also resolved a stale or wrong Nitrox.Model.dll.

The expected methods included:

  • Nitrox.Model.Serialization.NitroxConfig.Load(string, ILogger)
  • Nitrox.Model.Serialization.NitroxConfig.Load<T>(string, ILogger)

The failure mode was a MissingMethodException because the assembly that got loaded did not contain the overloads expected by the current launcher/server code.

Release layout detail

The important layout issue is that the root lib side needs the current net10/server-facing assets, while lib/net472 needs to keep the legacy/net472-facing assets.

The layout must not collapse those into one effective resolution pool where the launcher/server can accidentally load stale net472-era assets or incompatible dependency versions.

The observed distinction included:

  • correct root lib asset: Grpc.Core.Api.dll from the newer netstandard2.1 dependency asset path;
  • correct root/server-facing Nitrox.Model.dll;
  • separate legacy lib/net472/Nitrox.Model.dll for the net472-facing side.

The practical problem was that the Release deployment/layout allowed the launcher/server runtime path to resolve incompatible assemblies, producing MissingMethodExceptions instead of clear file-not-found errors.

What this PR changes

This PR fixes the Release dependency layout/resolution behavior so the launcher/server side loads the intended assemblies from the Release output.

It is not intended to dynamically reshuffle files at runtime. The goal is for the Release package/output to place and resolve the correct assemblies consistently.

Symptoms fixed locally

This fixed the Release-only behavior where:

  • saved/created servers did not appear correctly in the launcher;
  • creating a server could crash;
  • launcher/server communication failed because the runtime loaded incompatible dependency assemblies;
  • the server command path failed due to dependency MissingMethodExceptions.

Why this matters

Debug builds did not expose the issue in the same way because the Debug output layout and probing behavior did not match the broken Release package behavior.

The Release build/package is what users actually run, so fixing the Release output layout is necessary even if Debug appears fine.

Testing

Tested on fresh Release deployments.

Before this fix:

  • Release launcher could fail to see server saves / server entries.
  • Creating a server could crash.
  • Runtime logs showed MissingMethodExceptions caused by incompatible loaded assemblies.

After this fix:

  • Release launcher sees server saves / server entries correctly.
  • Server creation succeeds.
  • The launcher/server command path works.
  • The assembly mismatch / missing method failures are resolved locally.

Clarification from follow-up discussion

The earlier wording around "Release layout" was confusing. By "Release layout," I mean the physical output produced by the Release build/package and the way that output causes assemblies to be resolved at runtime.

The important point is:

  • this PR fixes which assemblies are deployed/resolved for the Release launcher/server path;
  • it is not trying to make an arbitrary runtime workaround for unrelated DLL loading;
  • the concrete failures were wrong-version/wrong-target assemblies being loaded, specifically involving Grpc.Core.Api.dll / MagicOnion and Nitrox.Model.dll overload mismatches.

@mindless2831
Copy link
Copy Markdown
Contributor Author

I also created a temporary draft integration/testing PR that combines the current related fixes for anyone who wants to test them together before they land individually:

mindless2831:integration/all-current-fixes

This is not intended to replace the focused PRs. It is only a convenience branch for combined Release/modded testing.

@dartasen dartasen added the Status: Waiting for reviews Pull Request is waiting for code review label May 18, 2026
@Measurity
Copy link
Copy Markdown
Collaborator

Measurity commented May 18, 2026

Is it possible to add test steps to this PR? Or at least an error / crash log?

We've never seen the server crash on latest master so taking the word of AI on these changes is a hard sell as it is.

@mindless2831
Copy link
Copy Markdown
Contributor Author

mindless2831 commented May 18, 2026

Is it possible to add test steps to this PR? Or at least an error / crash log?

We've never seen the server crash on latest master so taking the word of AI on these changes is a hard sell as it is.

I get it, I write documentation all day for a living so I use AI to help me with that part as it adds years back to my life I feel. I hope the following helps though, as I have used AI to keep track of all my steps and logs to make this documentation process easier, so it also helps me answer this easier. Please do not take offense to that or see it as me being lazy, just faster.

This was not based on AI speculation. I reproduced this locally in a Release build from the packaged launcher output layout.

Before this change, the Release launcher started, but existing saves were skipped because the launcher hit runtime missing-method errors while reading server entries. Example from my local log:

Error while initializing save from directory "C:\Users\mindl\AppData\Roaming\Nitrox\saves\NautTest". Skipping...
System.MissingMethodException: Method not found:
'!!0 Nitrox.Model.Serialization.NitroxConfig.Load(System.String, Microsoft.Extensions.Logging.ILogger)'.
   at Nitrox.Launcher.Models.Design.ServerEntry.RefreshFromDirectoryAsync(String saveDir)
   at Nitrox.Launcher.Models.Design.ServerEntry.FromDirectoryAsync(String saveDir)
   at Nitrox.Launcher.Models.Services.ServerService.GetSavesOnStorageAsync(...)

Because the save entries failed to initialize, existing servers did not show correctly in the launcher. The same Release dependency-loading issue also caused server creation/startup paths to fail because the launcher/runtime could see mismatched Nitrox assembly API surfaces.

Test steps I used:

  1. Build the launcher in Release.
  2. Run from the packaged Release output layout.
  3. Open the launcher with existing saves under %APPDATA%\Nitrox\saves.
  4. Before this change, observe existing saves being skipped with MissingMethodException from NitroxConfig.Load(...).
  5. Confirm Creating New Server crashes launcher
  6. Apply this PR.
  7. Build Release again and run from the same packaged output layout.
  8. Confirm existing servers appear in the launcher.
  9. Confirm creating a new server no longer crashes.
  10. Confirm the previous Release-only missing-method errors no longer appear.
  11. Confirm that the folder structure is now no longer messed up and completely different that the 1.8.1.0 release. All the libs are inside the main folder and make the master branch EXTREMELY ugly and unprofessional looking. After this fix, everything works AND it is pretty :-)

@Measurity
Copy link
Copy Markdown
Collaborator

I've followed the steps without PR changes but I'm on Linux.

With a dotnet build Nitrox.Launcher -c Release build. No crashes, no errors. I can create a new server save. Existing saves load fine too.

This is what a release build output looks like already:
image

Is there something wrong with the build environment on Windows? Can anyone else on the team please confirm the issues this PR is trying to solve?

@dartasen
Copy link
Copy Markdown
Member

Unable to reproduce this on Windows and Mac :/

@mindless2831
Copy link
Copy Markdown
Contributor Author

That just doesn't make any sense. I have tried deploying master on 3 different Windows 11 Pro builds, and every single one of them the master Release build does not show server saves, and will crash if you try to create one. Period. I get that a lot of y'all are building in debug, but release build should work as well. Also, just simply the fact that the libs aren't where they should be is obviously going to cause a ton of problems. As it stands right now, this is the single most important fix I have made, and I am unable to create a working Release build on a fresh copy of master without it.

@Measurity
Copy link
Copy Markdown
Collaborator

The problem for me is that we don't know what's causing the "MissingMethodException".

Maybe we can list the assembly versions + locations it loads compared to without the change? That would be helpful to get an understanding.

And if the versions are different, why? Assembly.LoadFile allows use to specifically load each library ourselves going by the MSDN docs. In other words, why should a DLL that gets loaded outside of Nitrox files be better (avoid MissingMethodException) compared to its own files?

The screenshot shows the files in release mode. How is it possible that the files are suddenly changing position when loading them differently at runtime (done by this PR change)? Or do you mean a different PR?

@dartasen dartasen added Status: Needs answers Pull Request needs answers from author and removed Status: Waiting for reviews Pull Request is waiting for code review labels May 20, 2026
@mindless2831
Copy link
Copy Markdown
Contributor Author

mindless2831 commented May 21, 2026

The problem for me is that we don't know what's causing the "MissingMethodException".

Maybe we can list the assembly versions + locations it loads compared to without the change? That would be helpful to get an understanding.

And if the versions are different, why? Assembly.LoadFile allows use to specifically load each library ourselves going by the MSDN docs. In other words, why should a DLL that gets loaded outside of Nitrox files be better (avoid MissingMethodException) compared to its own files?

The screenshot shows the files in release mode. How is it possible that the files are suddenly changing position when loading them differently at runtime (done by this PR change)? Or do you mean a different PR?

Thanks, that makes sense. I’ll try to explain the distinction more clearly.

The Release/Debug file layout is not being changed at runtime by this PR. That layout is determined by the project/MSBuild/publish configuration before the launcher ever runs: the .csproj settings, shared props/targets, copy-local behavior, publish/build configuration, and whatever packaging logic places dependencies in the root versus /lib, /Resources, etc.

So when I say “Release layout,” I mean the physical output produced by the Release build/package. In the Release output I was testing, most dependency assemblies were already placed under /lib, while the launcher executable and a smaller set of core files were in the root. This PR does not move those files around at runtime. It only changes how the launcher resolves assemblies when the runtime asks for one that is not already loadable from the default probing path.

On the MissingMethodException: the issue was not visible in the same way as the UWE prefab probability issue. The UWE issue produced a direct visible error in the normal flow once the bad call path was hit. For this launcher/lib-loading issue, the visible symptom was higher level: fresh Release builds from current master could start, but server saves were not discovered correctly and creating a server could crash/fail. To see the underlying assembly issue clearly, we had to add/enable more assembly-resolution logging around what the launcher was actually loading and where from. That is what showed the important part: the runtime was not consistently loading the intended Nitrox assemblies from the Release layout.

A reproducible test should be:

Start from a fresh clone/current master.
Build the launcher/server in Release.
Use the produced Release output as-is, with dependencies in the generated layout.
Launch the Release launcher.
Check whether existing saves/servers are listed correctly.
Try creating a server from the launcher.
Compare the loaded assembly names, versions, and physical locations against the same test with this PR applied.

The important comparison is not just “does a DLL exist somewhere,” but “which physical DLL path did the process actually load for this assembly identity?” That is why listing assembly versions plus locations would be useful. I agree that it would make the diagnosis clearer.

On Assembly.LoadFile: switching to LoadFile is not a viable fix here. We already tested that path, and it does not resolve the Release launcher problem correctly.

The issue is not simply “open this DLL from this exact path.” The launcher needs failed assembly resolution to be satisfied from the Release layout’s /lib folder while still allowing the CLR/runtime to bind dependencies coherently. LoadFile bypasses the normal load context and can load an assembly into a separate context, which creates exactly the class of problems we are trying to avoid: duplicate assembly identities, wrong dependency binding, type identity mismatches, and MissingMethodException-style failures when the runtime ends up using a different assembly than the one the code was built/tested against.

That is why the PR uses the assembly resolution event and returns the resolved assembly from /lib instead of replacing resolution with direct LoadFile calls. In testing, the Release launcher did not work correctly with the LoadFile approach, while the resolver-based approach restored the expected Release behavior.

Edit: This totally changes files and my freaking Notetaker was on drugs when it wrote this. I have update the PR with the relevant info but am leaving this here so Measurity doesn't look like they are crazy lol.

@Measurity
Copy link
Copy Markdown
Collaborator

Measurity commented May 21, 2026

Can you stop using AI and use your own words please?

I have a lot of patience for humans, but almost zero for AI. So using AI isn't in your favor replying to human messages.

@mindless2831
Copy link
Copy Markdown
Contributor Author

mindless2831 commented May 21, 2026

Can you stop using AI and use your own words please?

I have a lot of patience for humans, but almost zero for AI. So using AI isn't in your favor replying to human messages.

I use AI draft my long technical responses so that they do not leave anything out since I use it to take notes. Makes life easier for something I do all day in my real work and this is something I do for fun. Have messaged you about this in discord.

@mindless2831
Copy link
Copy Markdown
Contributor Author

Whoops, wrong button! Reopened! Yikes!

@github-actions
Copy link
Copy Markdown

Test Results

255 tests  ±0   252 ✅ ±0   22s ⏱️ -1s
  1 suites ±0     3 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit b501421. ± Comparison against base commit a89227d.

@mindless2831
Copy link
Copy Markdown
Contributor Author

Updated the PR body to clarify the concrete assembly/version/method failures and to remove the confusing wording around "Release layout."

The important specifics now included are:

  • Grpc.Core.Api.dll was resolving as the stale/bad 2.57.0 API surface while MagicOnion 7.0.6 expected Grpc.Core.IAsyncStreamWriter<T>.WriteAsync(T, CancellationToken).
  • Nitrox.Model.dll was also resolving from the wrong/stale deployed asset path, causing missing overloads such as NitroxConfig.Load(string, ILogger) / NitroxConfig.Load<T>(string, ILogger).
  • The issue is Release-package/output layout and assembly resolution: the launcher/server side needs the correct root lib net10/server-facing assets, while lib/net472 remains separate for legacy/net472-facing assets.
  • This explains why the failures were MissingMethodExceptions: assemblies were found, but they were incompatible versions/target assets for the code calling them.

Also clarified that the PR is not intended to arbitrarily move files around at runtime; it is intended to make the Release output/package resolve the correct assemblies consistently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Needs answers Pull Request needs answers from author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants