Skip to content

Nautilus compatibility for GameInput keybinds#2663

Open
JackNytely wants to merge 9 commits intoSubnauticaNitrox:masterfrom
JackNytely:master
Open

Nautilus compatibility for GameInput keybinds#2663
JackNytely wants to merge 9 commits intoSubnauticaNitrox:masterfrom
JackNytely:master

Conversation

@JackNytely
Copy link

@JackNytely JackNytely commented Feb 5, 2026

1. Keybind compatibility with Nautilus

Problem

When Nitrox and Nautilus run together, Nitrox buttons may not get InputAction entries. That can cause RegisterKeybindsActions to fail because gameInputSystem.actions[NitroxButton] does not exist.

When Nitrox runs without Nautilus, the game was showing duplicate keybinds in the input settings ("Open chat" and "Focus on the Discord invite window" twice) because Nitrox buttons were added in two places: GameInput.AllActions and Enum.GetValues(GameInput.Button). The game builds the binding list from both, so each Nitrox keybind appeared twice.

Solution

  • Extend GameInput.AllActions: In the Initialize prefix, extend AllActions with Nitrox buttons so the game builds the action map for them. A duplicate check prevents adding them again when AllActions was already initialized from the patched Enum.GetValues.
  • Cleanup on deinitialize: Add a prefix on GameInputSystem.Deinitialize() to restore the original AllActions array.
  • Fallback for missing actions: If the game doesn't create an action (e.g. some builds don't use AllActions), RegisterKeybindsActions creates and registers InputActions for Nitrox buttons in the same way as Nautilus's InitializePostfix.
  • Duplicate-safe enum extension: Enum_GetValues_Patch still extends Enum.GetValues(GameInput.Button) with Nitrox buttons (required for the rebinding UI to recognize them as valid), but now checks for duplicates so Nautilus or repeated calls don't add them twice. Combined with the AllActions duplicate check, each keybind appears once in the settings UI.

Changes

  • GameInputSystem_Initialize_Patch:
    • Add transpiler/patch logic for Deinitialize to restore AllActions.
    • Add duplicate check before extending AllActions (prevents double entries when already initialized from patched Enum.GetValues).
    • Add TryGetValue check in RegisterKeybindsActions with manual InputAction creation when an entry is missing.
    • Wire Nitrox actions to gameInputSystem.OnActionStarted, add bindings, and enable them to mirror Nautilus behavior.
    • Register "Option{buttonId}" localization entries in Language.main so the binding conflict dialog shows translated labels (e.g. "Open chat") instead of "Option1000".
  • Enum_GetValues_Patch: Add duplicate check so Nitrox buttons are only appended when not already present (Nautilus compatibility).
  • uGUI_TabbedControlsPanel_AddBindingOption_Patch: Fix infinite recursion that prevented Nitrox keybinds from being rebindable. The old code checked button ID, translated the label, then recursively called AddBindingOption — which triggered the prefix again on the same button, recursing infinitely. Changed to a void prefix that modifies the label parameter by reference, so the original method runs exactly once with the correct display text.
  • ChatKeyBindingAction / DiscordFocusBindingAction: Add default controller bindings (dpad/up and dpad/down) so the game creates proper controller binding slots during initialization, enabling controller rebinding.

2. Server shutdown improvements

Problem

When stopping a server through the launcher, the shutdown could hang indefinitely if the server process didn't respond to the quit command. Additionally, embedded servers were left running when the launcher was closed.

Solution

Add timeout-based shutdown with force-kill fallback, and stop all embedded servers on launcher dispose.

Changes

  • ServerEntry.cs: Add a 10-second CancellationTokenSource timeout to the shutdown loop. If the server doesn't respond to quit within the timeout, force-terminate the process. Also handle OperationCanceledException in StopAsync by force-killing the process when the graceful shutdown times out.
  • ServerService.cs: On Dispose, iterate all online embedded servers and call StopAsync, waiting up to 30 seconds for all to finish. Non-embedded servers (with their own console window) are left running.

3. Release build: keep dependencies in output root

Problem

In Release, dependencies were moved into lib/ (Debug only copied). Some code paths load assemblies before or outside the custom AssemblyResolve (e.g. Release vs Debug), so when DLLs existed only in lib/, the launcher could fail to load them. That led to empty server list, missing types, or other assembly-load issues in Release only.

Solution

Use Copy for both Debug and Release instead of Move in Release, so dependencies stay in the output root as well as in lib/.

Changes

  • Nitrox.Shared.targets: Always copy FilesToMove into lib/ (remove the Release-only Move). Update comment to explain that keeping DLLs in the output root avoids empty UI / missing types when they would otherwise be only in lib/.

4. Minor improvements

  • PrefabPlaceholderGroupsResource.cs: Combine the cache version check into the if (cache.HasValue) condition so an outdated cache falls through to the rebuild path directly. Log message now distinguishes between outdated cache rebuild and fresh build.
  • Reflect.cs: Add nullable annotation to implementingType parameter.

Testing

Keybinds

  • Nitrox alone — no duplicate keybinds, rebinding works for both keyboard and controller
  • Nitrox + Nautilus * Nitrox bindings cant be rebound with nautilus enabled *
  • Nitrox + Nautilus + (Epic Weather Mod + Prototype Sub + Deathrun Remade) * Nitrox bindings cant be rebound with nautilus enabled *
  • Binding conflict dialog shows correct translated labels

Server shutdown

  • Embedded server stops gracefully via quit command
  • Embedded server is force-killed after timeout
  • Embedded servers stop when launcher is closed

Release build

  • Release build runs without empty server list or assembly-load errors
  • Server list displays correctly; create server works

@Measurity
Copy link
Collaborator

Measurity commented Feb 5, 2026

I'm not sure what this PR fixes since the input already works after PR #2571

Can you take a screenshot or paste output of the error you're seeing?

I see now thx to @misterbubb - the new update to Nautilus broke the compatibility:

NullReferenceException: Object reference not set to an instance of an object
  at GameInput.GetBinding (GameInput+Device device, GameInput+Button action, GameInput+BindingSet bindingSet) [0x00000] in <c055c80802d14a9db359fbdc91eb371c>:0 
  at GameInput.AppendDisplayText (GameInput+Button action, System.Text.StringBuilder sb, System.Boolean allBindingSets) [0x0001c] in <c055c80802d14a9db359fbdc91eb371c>:0 
  at GameInput.FormatButton (GameInput+Button action, System.Boolean allBindingSets) [0x0000d] in <c055c80802d14a9db359fbdc91eb371c>:0 
  at uGUI_CameraCyclops.UpdateBindings () [0x00000] in <c055c80802d14a9db359fbdc91eb371c>:0 
  at uGUI_CameraCyclops.UpdateTexts () [0x00000] in <c055c80802d14a9db359fbdc91eb371c>:0 
  at uGUI_CameraCyclops.OnEnable () [0x00000] in <c055c80802d14a9db359fbdc91eb371c>:0 
UnityEngine.Object:Internal_InstantiateSingle_Injected(Object, Vector3&, Quaternion&)
UnityEngine.Object:Internal_InstantiateSingle(Object, Vector3, Quaternion)
UnityEngine.Object:Instantiate(Object, Vector3, Quaternion)
UnityEngine.Object:Instantiate(GameObject, Vector3, Quaternion)
UWE.Utils:InstantiateWrap(GameObject, Vector3, Quaternion)
<InitializeAsync>d__43:MoveNext()
UnityEngine.SetupCoroutine:DMD<UnityEngine.SetupCoroutine::InvokeMoveNext>(IEnumerator, IntPtr)

Copy link
Collaborator

@Measurity Measurity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitrox keybindings appears twice with these changes:

Image

Minor: when BepInEx mods are loaded, I'm unable to change Nitrox keybindings as well.

@JackNytely JackNytely requested a review from Measurity February 5, 2026 22:25
@JackNytely
Copy link
Author

Nitrox keybindings appears twice with these changes:
Image

Minor: when BepInEx mods are loaded, I'm unable to change Nitrox keybindings as well.

Will look into fixing this, not sure why it appears as double, will investigate asap

Copy link
Collaborator

@Measurity Measurity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes to support AsyncBarrier in net472 code is quite involved. But AsyncBarrier is not used right now except for (net10) server.

Would you mind reverting changes made for AsyncBarrier in an effort to keep complexity to the minimum?

Keeping DLLs in the output root avoids assembly load failures when
AssemblyResolve is not used (e.g. some load order/contexts in Release).
Fixes empty server list and related issues in Release-only builds.
@JackNytely
Copy link
Author

The changes to support AsyncBarrier in net472 code is quite involved. But AsyncBarrier is not used right now except for (net10) server.

Would you mind reverting changes made for AsyncBarrier in an effort to keep complexity to the minimum?

I was attempting to fix an issue where the server list in the launcher itself was empty and crashing if you tried creating a server, however, I found out that the issue was in the way the app builds a release, publishing works, but if you run from direct release, it moves the dlls over instead of copying which causes it to break in some areas.

I removed my changes and fixed the issue in the build script instead

Kept the fixes for nautilus to work though.

@JackNytely
Copy link
Author

Nitrox keybindings appears twice with these changes:
Image
Minor: when BepInEx mods are loaded, I'm unable to change Nitrox keybindings as well.

Will look into fixing this, not sure why it appears as double, will investigate asap

I am not getting duplicates on my side and keybinds seem to work now

@JackNytely JackNytely requested a review from Measurity February 8, 2026 11:33
Copy link
Collaborator

@Measurity Measurity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks ❤️

Copy link
Collaborator

@Measurity Measurity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh still duplicated input when starting normally without mods... :(

I think you've handled Nautilus happy path, but without Nautilus the logic now duplicates the input.

…te keybinds

The game builds the input binding list from both Enum.GetValues and
GameInput.AllActions. Adding Nitrox buttons in both places caused
'Open chat' and 'Focus on the Discord invite window' to appear twice
when running Nitrox without Nautilus. Nitrox keys are now added only
via AllActions in GameInputSystem_Initialize_Patch so each keybind
shows once.
@JackNytely
Copy link
Author

Oh still duplicated input when starting normally without mods... :(

I think you've handled Nautilus happy path, but without Nautilus the logic now duplicates the input.

The changes I made caused the UI to build the input bindings from both Enum.GetValues and GameInput.AllActions, by adding nitrox in both it caused the duplication when running without nautilus enabled

I have fixed this issue now, thanks for pointing it out <3

@JackNytely JackNytely requested a review from Measurity February 8, 2026 14:10
@Measurity
Copy link
Collaborator

Thanks for working on this.

I noticed the input bindings cannot be rebound when starting without mods. Please verify behavior doesn't regress when playing Nitrox without BepInEx / Nautilus.

Server shutdown could hang indefinitely when the process didn't respond
to the quit command. Added timeout-based shutdown with force-kill fallback,
and ensured embedded servers stop when the launcher closes.

Nitrox keybinds (Open Chat, Focus Discord) could not be rebound in the
settings UI due to infinite recursion in the AddBindingOption patch.
Changed to modify the label by reference instead of recursively calling
AddBindingOption. Added duplicate checks for Nautilus compatibility when
extending both Enum.GetValues and GameInput.AllActions, registered
'Option{id}' localization entries for the binding conflict dialog, and
added default controller bindings (D-Pad Up/Down).
@JackNytely
Copy link
Author

Thanks for working on this.

I noticed the input bindings cannot be rebound when starting without mods. Please verify behavior doesn't regress when playing Nitrox without BepInEx / Nautilus.

I fixed the issue with rebinds not working on nitrox without mods, found the issue in my code and fixed it ( nautilus with bepinex still has the binding issues, I will need to investigate this in the future)

I also ran into issues with nitrox servers sometimes not starting, and the stop buttong sometimes does nothing, forcing me to close the game and nitrox and task manager the server (I also fixed this issue in my PR)

Copy link
Collaborator

@Measurity Measurity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Job well done, thanks.

Setting input bindings works with Nitrox. And mods load now at least.

That said, I'm not getting the "server close not working" issue, but would like the GameInput keybinds fixes to be merged.

Can the server related bug fixing be split in a separate PR?

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert file changes.

Comment on lines +250 to 285

// Stop all embedded servers when launcher closes
// Non-embedded servers are left running as they have their own console window
lock (serversLock)
{
List<Task> stopTasks = [];
foreach (ServerEntry server in servers)
{
if (server.IsOnline && server.IsEmbedded && server.Process != null)
{
try
{
Log.Info($"Stopping embedded server {server.Name} during launcher shutdown");
stopTasks.Add(server.StopAsync());
}
catch (Exception ex)
{
Log.Error(ex, $"Error stopping server {server.Name} during shutdown");
}
}
}

// Wait for all servers to stop with a timeout
if (stopTasks.Count > 0)
{
try
{
Task.WaitAll([.. stopTasks], TimeSpan.FromSeconds(30));
}
catch (Exception ex)
{
Log.Error(ex, "Error waiting for servers to stop during shutdown");
}
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this code ever called? No debugger breakpoint hits on launcher exit. I think the code can be removed.

MainWindowViewModel also already handles server exit.

Comment on lines +393 to +396
Log.Warn($"Server {Name} did not respond to quit command, attempting force terminate");
using ProcessEx? proc = ProcessEx.GetFirstProcess(GetServerExeName(), ex => ex.Id == LastProcessId);
proc?.Terminate();
break;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Terminating the server on timeout can break server saving. I'd prefer we investigate why the server doesn't exit and fix instead of terminating it.

If you know how to trigger the bug, please create a new issue with steps. Then we fix this in a separate PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, I think it could also help with speed of merging the prs. Here it feels like three prs in one

logger.ZLogWarning($"An error occurred while deserializing the prefab cache. Re-creating it: {ex.Message:@Error}");
}
if (cache.HasValue)
if (cache.HasValue && cache.Value.Version == CACHE_VERSION)
Copy link
Collaborator

@Measurity Measurity Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for noticing this bug. Fixed in #2674 in case your PR stays unmerged for a while.

@Measurity Measurity added this to the 1.9 milestone Feb 16, 2026
Comment on lines +90 to 97
<!-- Copy (don't Move) so dependencies stay in output root. Some code paths load assemblies before/when AssemblyResolve runs or from contexts that don't use it (e.g. Release vs Debug), causing empty UI or missing types when DLLs are only in lib/. -->
<Copy SourceFiles="@(FilesToMove)"
DestinationFolder="$(OutputDirectory)"
OverwriteReadOnlyFiles="True"
SkipUnchangedFiles="True"
Retries="3"
RetryDelayMilliseconds="100"
Condition="'$(Configuration)' == 'Debug'"
ContinueOnError="ErrorAndContinue" />

<!-- Move every matching files to OutputDirectory -->
<Move SourceFiles="@(FilesToMove)"
DestinationFolder="$(OutputDirectory)"
OverwriteReadOnlyFiles="True"
Condition="'$(Configuration)' == 'Release'"
ContinueOnError="ErrorAndContinue" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intended behavior. Avalonia doesn't use our AppDomain.CurrentDomain.AssemblyResolve. In Debug we're fine with duplicating the files so Avalonia Previewer works.

Comment on lines +393 to +396
Log.Warn($"Server {Name} did not respond to quit command, attempting force terminate");
using ProcessEx? proc = ProcessEx.GetFirstProcess(GetServerExeName(), ex => ex.Id == LastProcessId);
proc?.Terminate();
break;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, I think it could also help with speed of merging the prs. Here it feels like three prs in one

public class ChatKeyBindingAction : KeyBinding
{
public ChatKeyBindingAction() : base("Nitrox_Settings_Keybind_OpenChat", "y") { }
public ChatKeyBindingAction() : base("Nitrox_Settings_Keybind_OpenChat", "y", "dpad/up") { }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these universal across all unity controllers? Will it only work for say playstation but not Xbox?

}

private static MemberInfo GetMemberInfo(LambdaExpression expression, Type implementingType = null)
private static MemberInfo GetMemberInfo(LambdaExpression expression, Type? implementingType = null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for improving this code. Added in #2674 in case your PR stays unmerged for a while.

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.

4 participants