first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
using System.Diagnostics;
using System.Reflection;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Persistent;
/// <summary>
/// Ensures <see cref="Application.runInBackground"/> is set to true at all times.
/// </summary>
/// <remarks>
/// Nitrox needs to be running updates and processing packets at all times to work properly.
/// </remarks>
public sealed partial class Application_runInBackground_Patch : NitroxPatch, IPersistentPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Property(() => Application.runInBackground).GetSetMethod();
public static bool Prefix(bool value)
{
if (!value)
{
Log.WarnOnce($"An attempt to set {nameof(Application.runInBackground)} to \"false\" was issued but it was ignored.\n{new StackTrace()}");
Application.runInBackground = true;
return false;
}
return true;
}
}

View File

@@ -0,0 +1,17 @@
using System.Reflection;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
internal partial class CellManager_GetPrefabForSlot_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CellManager t) => t.GetPrefabForSlot(default(EntitySlot)));
public static bool Prefix(IEntitySlot slot, out EntitySlot.Filler __result)
{
__result = default;
return !Multiplayer.Active;
}
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
public partial class CellManager_TryLoadCacheBatchCells_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CellManager t) => t.TryLoadCacheBatchCells(default(BatchCells)));
private static readonly MethodInfo PATH_PREFIX_GETTER = Reflect.Property((LargeWorldStreamer t) => t.pathPrefix).GetMethod;
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
List<CodeInstruction> instrList = instructions.ToList();
Label pathPrefixJmp = generator.DefineLabel();
Label labeledPathInstructionJmp = generator.DefineLabel();
Label fallbackPrefixJmp = generator.DefineLabel();
Label labeledFallbackInstructionJmp = generator.DefineLabel();
for (int i = 0; i < instrList.Count; i++)
{
CodeInstruction instruction = instrList[i];
if (instrList.Count > i + 2 && instrList[i + 2].opcode == OpCodes.Callvirt && ReferenceEquals(instrList[i + 2].operand, PATH_PREFIX_GETTER))
{
foreach (CodeInstruction instr in TranspilerHelper.IsMultiplayer(pathPrefixJmp, generator))
{
yield return instr;
}
yield return new CodeInstruction(OpCodes.Ldstr, ""); // Replace pathPrefix with an empty string
yield return new CodeInstruction(OpCodes.Br, labeledPathInstructionJmp);
instrList[i].labels.Add(pathPrefixJmp);
yield return instrList[i];
yield return instrList[i + 1];
yield return instrList[i + 2];
CodeInstruction labeledCodeInstruction = new(instrList[i + 3].opcode, instrList[i + 3].operand);
labeledCodeInstruction.labels.Add(labeledPathInstructionJmp);
yield return labeledCodeInstruction;
i += 3;
}
else
{
yield return instruction;
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Reflection;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
/// <summary>
/// Patch to disable the EscapePodFirstUseCinematicsController.OnSceneObjectsLoaded method when in multiplayer.
/// Initialize will be called OnSceneLoad to setup cinematics or first use cinematics
/// </summary>
public sealed partial class EscapePodFirstUseCinematicsController_OnSceneObjectsLoaded_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((EscapePodFirstUseCinematicsController t) => t.OnSceneObjectsLoaded());
public static bool Prefix(EscapePodFirstUseCinematicsController __instance) => !Multiplayer.Active;
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Reflection;
using HarmonyLib;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
/// <summary>
/// Patch to disable any analytics uploads to UWE servers. Since game is modded it's not useful for UWE and it's less
/// code to run for us
/// </summary>
public sealed class GameAnalytics_Patch : NitroxPatch, IPersistentPatch
{
/// <summary>
/// Disables analytics uploads
/// </summary>
private static readonly MethodInfo TARGET_METHOD_SDK = Reflect.Method((SentrySdk t) => t.Start());
/// <summary>
/// Skip manager create as well to remove NRE in Player.log on exit (OnDestroy will try access null SentrySdk instance)
/// </summary>
private static readonly MethodInfo TARGET_METHOD_MANAGER = Reflect.Method((SentrySdkManager t) => t.Awake());
private static readonly MethodInfo TARGET_METHOD_MANAGER_ONENABLE = Reflect.Method((SentrySdkManager t) => t.OnEnable());
/// <summary>
/// Skip controller create as well to remove NRE in Player.log
/// </summary>
private static readonly MethodInfo TARGET_METHOD_CONTROLLER = Reflect.Method((AnalyticsController t) => t.Awake());
private static readonly MethodInfo TARGET_METHOD_CONTROLLER_ONENABLE = Reflect.Method((AnalyticsController t) => t.OnEnable());
private static readonly MethodInfo TARGET_METHOD_TELEMETRY = Reflect.Method((Telemetry t) => t.Awake());
private static readonly MethodInfo TARGET_METHOD_TELEMETRY_ONENABLE = Reflect.Method((Telemetry t) => t.OnEnable());
private static readonly MethodInfo TARGET_METHOD_TELEMETRY_QUIT = Reflect.Method(() => Telemetry.SendGameQuit(default));
private static readonly MethodInfo TARGET_METHOD_GAMEANALYTICS_SEND_EVENT = Reflect.Method(() => GameAnalytics.Send(default(GameAnalytics.Event), default(bool), default(string)));
private static readonly MethodInfo TARGET_METHOD_GAMEANALYTICS_SEND_EVENTINFO = Reflect.Method(() => GameAnalytics.Send(default(GameAnalytics.EventInfo), default(bool), default(string)));
private static readonly MethodInfo TARGET_METHOD_GAMEANALYTICS_SEND_LEGACYEVENT = Reflect.Method(() => GameAnalytics.LegacyEvent(default(GameAnalytics.Event), default(string)));
private static readonly MethodInfo TARGET_METHOD_SPAWNER_ANALYTICS = Reflect.Method((SystemsSpawner t) => t.Awake());
public static bool Prefix(SentrySdk __instance)
{
FieldInfo initializedField = __instance.GetType().GetField("_initialized", BindingFlags.NonPublic | BindingFlags.Instance);
if (initializedField == null)
{
Log.WarnOnce($"{nameof(SentrySdk)} could not be properly disabled because the field '_initialized' is missing. Expect NRE in logs where Telemetry events are processed.");
return false;
}
initializedField.SetValue(__instance, false);
return false;
}
public static bool Prefix(AnalyticsController __instance)
{
UnityEngine.Object.Destroy(__instance);
return false;
}
public static bool Prefix(Telemetry __instance)
{
UnityEngine.Object.Destroy(__instance);
return false;
}
public static bool Prefix()
{
return false;
}
public override void Patch(Harmony harmony)
{
PatchPrefix(harmony, TARGET_METHOD_SDK, ((Func<SentrySdk, bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_CONTROLLER, ((Func<AnalyticsController, bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_CONTROLLER_ONENABLE, ((Func<bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_MANAGER, ((Func<bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_MANAGER_ONENABLE, ((Func<bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_TELEMETRY, ((Func<Telemetry, bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_TELEMETRY_ONENABLE, ((Func<bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_TELEMETRY_QUIT, ((Func<bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_GAMEANALYTICS_SEND_EVENT, ((Func<bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_GAMEANALYTICS_SEND_EVENTINFO, ((Func<bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_GAMEANALYTICS_SEND_LEGACYEVENT, ((Func<bool>)Prefix).Method);
PatchPrefix(harmony, TARGET_METHOD_SPAWNER_ANALYTICS, ((Func<bool>)Prefix).Method);
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.MonoBehaviours.Gui.Input;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
public partial class GameInput_Initialize_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((GameInput t) => t.Initialize());
private static readonly OpCode INJECTION_OPCODE = OpCodes.Stsfld;
private static readonly object INJECTION_OPERAND = Reflect.Field(() => GameInput.numButtons);
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
Validate.NotNull(INJECTION_OPERAND);
foreach (CodeInstruction instruction in instructions)
{
if (instruction.opcode.Equals(INJECTION_OPCODE) && instruction.operand.Equals(INJECTION_OPERAND))
{
/*
* int prev = GameInput.GetMaximumEnumValue(typeof(GameInput.Button)) + 1;
* // ^ This value is already calculated by the original code, it's stored on top of the stack.
* KeyBindingManager keyBindingManager = new KeyBindingManager();
* GameButton.numButtons = Math.Max(keyBindingManager.GetHighestKeyBindingValue() + 1, prev);
*/
yield return new CodeInstruction(OpCodes.Newobj, Reflect.Constructor(() => new KeyBindingManager()));
yield return new CodeInstruction(OpCodes.Callvirt, Reflect.Method((KeyBindingManager t) => t.GetHighestKeyBindingValue()));
yield return new CodeInstruction(OpCodes.Ldc_I4_1);
yield return new CodeInstruction(OpCodes.Add);
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => Math.Max(default(int), default(int))));
}
yield return instruction;
}
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Reflection;
using NitroxClient.MonoBehaviours.Gui.Input;
using NitroxClient.MonoBehaviours.Gui.Input.KeyBindings;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
public partial class GameInput_SetupDefaultKeyboardBindings_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => GameInput.SetupDefaultKeyboardBindings());
public static void Postfix()
{
KeyBindingManager keyBindingManager = new();
foreach (KeyBinding keyBinding in keyBindingManager.KeyboardKeyBindings)
{
GameInput.SetBindingInternal(keyBinding.Device, keyBinding.Button, GameInput.BindingSet.Primary, keyBinding.PrimaryKey);
// if the options.bin gets deleted (when loading SP) and the client.cfg has secondary keybinds, repopulate them.
if (!string.IsNullOrEmpty(keyBinding.SecondaryKey))
{
GameInput.SetBindingInternal(keyBinding.Device, keyBinding.Button, GameInput.BindingSet.Secondary, keyBinding.SecondaryKey);
}
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Reflection;
using NitroxClient.MonoBehaviours.Gui.Input;
using NitroxClient.MonoBehaviours.Gui.Input.KeyBindings;
using NitroxClient.Serialization;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
public partial class GameSettings_SerializeInputSettings_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => GameSettings.SerializeInputSettings(default(GameSettings.ISerializer)));
public static void Postfix(GameSettings.ISerializer serializer)
{
ClientConfig cfg = ClientConfig.Load(NitroxUser.AppDataPath);
KeyBindingManager keyBindingManager = new();
foreach (GameInput.BindingSet bindingSet in Enum.GetValues(typeof(GameInput.BindingSet)))
{
foreach (KeyBinding keyBinding in keyBindingManager.KeyboardKeyBindings)
{
Log.Debug($"Getting keybinding: {keyBinding.Device}, {keyBinding.Label} ({keyBinding.Button}), {bindingSet}");
string binding = GameInput.GetBinding(keyBinding.Device, keyBinding.Button, bindingSet);
// We need to assign the correct binding for primary and secondary binding sets to the relevant area of the config.
switch ((KeyBindingValues)keyBinding.Button)
{
case KeyBindingValues.CHAT when bindingSet == GameInput.BindingSet.Primary:
cfg.OpenChatKeybindPrimary = binding;
break;
case KeyBindingValues.FOCUS_DISCORD when bindingSet == GameInput.BindingSet.Primary:
cfg.FocusDiscordKeybindPrimary = binding;
break;
case KeyBindingValues.CHAT when bindingSet == GameInput.BindingSet.Secondary:
cfg.OpenChatKeybindSecondary = binding;
break;
case KeyBindingValues.FOCUS_DISCORD when bindingSet == GameInput.BindingSet.Secondary:
cfg.FocusDiscordKeybindSecondary = binding;
break;
}
}
}
cfg.Serialize(NitroxUser.AppDataPath);
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using LitJson;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
public class Language_LoadLanguageFile_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((Language t) => t.LoadLanguageFile(default));
private static readonly Dictionary<string, Tuple<string, string>> languageToIsoCode = new(); // First Tuple item is region specific (en-US), second isn't (en)
public static void Postfix(string language, Dictionary<string, string> ___strings)
{
if (!TryLoadLanguageFile("en", ___strings)) //Loading english as fallback for missing files or keys
{
Log.Error($"The English language file could not be loaded");
return;
}
if (!TryLoadLanguageFile(languageToIsoCode[language].Item1, ___strings))
{
if (!TryLoadLanguageFile(languageToIsoCode[language].Item2, ___strings))
{
Log.Warn($"No language file was found for {language}. Using English as fallback");
}
}
Language.main.ParseMetaData();
}
private static bool TryLoadLanguageFile(string fileName, IDictionary<string, string> strings)
{
string filePath = Path.Combine(NitroxUser.LanguageFilesPath, $"{fileName}.json");
if (!File.Exists(filePath))
{
return false;
}
using StreamReader streamReader = new(filePath);
try
{
JsonData json = JsonMapper.ToObject(streamReader);
foreach (string key in json.Keys)
{
JsonData entry = json[key];
if (entry.IsString)
{
strings[key] = (string)entry;
}
}
return true;
}
catch (Exception ex)
{
Log.Error(ex, $"Error while reading language file {fileName}.json");
}
return false;
}
public override void Patch(Harmony harmony)
{
List<string> existingLanguageNames = Directory.EnumerateFiles(SNUtils.InsideUnmanaged("LanguageFiles"), "*.json").Select(Path.GetFileNameWithoutExtension).ToList();
foreach (CultureInfo culture in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
if (!languageToIsoCode.ContainsKey(culture.EnglishName) && !string.IsNullOrEmpty(culture.Name) && existingLanguageNames.Contains(culture.EnglishName))
{
languageToIsoCode.Add(culture.EnglishName, new Tuple<string, string>(culture.Name, culture.TwoLetterISOLanguageName));
}
}
// This language isn't registered in CultureInfo
if (existingLanguageNames.Contains("Spanish (Latin America)"))
{
languageToIsoCode.Add("Spanish (Latin America)", new Tuple<string, string>("es-419", "es"));
}
PatchPostfix(harmony, TARGET_METHOD, ((Action<string, Dictionary<string, string>>)Postfix).Method);
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
public partial class MainGameController_StartGame_Patch : NitroxPatch, IPersistentPatch
{
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((MainGameController t) => t.StartGame()));
public static readonly OpCode INJECTION_OPCODE = OpCodes.Call;
public static readonly object INJECTION_OPERAND = Reflect.Method(() => WaitScreen.Remove(default));
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
Validate.NotNull(INJECTION_OPERAND);
int injectSeenCounter = 0;
foreach (CodeInstruction instruction in instructions)
{
yield return instruction;
if (instruction.opcode.Equals(INJECTION_OPCODE) && instruction.operand.Equals(INJECTION_OPERAND))
{
injectSeenCounter++;
if (injectSeenCounter == 3)
{
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => Multiplayer.SubnauticaLoadingCompleted()));
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxModel.Helper;
using UnityEngine.UI;
namespace NitroxPatcher.Patches.Persistent;
/// <summary>
/// MainMenuLoadButton.RestoreParentsSettings() is throwing null refs because we copy it for our MainMenu and then destroy it.
/// Unfortunately OnDestroy can't be prevented when destroying MBs so we fix the NRE here.
/// </summary>
public sealed partial class MainMenuLoadButton_RestoreParentsSettings_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((MainMenuLoadButton lb) => lb.RestoreParentsSettings());
public static bool Prefix(GridLayoutGroup ___gridLayoutGroup, ScrollRect ___scrollRect)
{
return ___gridLayoutGroup && ___scrollRect;
}
}

View File

@@ -0,0 +1,20 @@
using System.Reflection;
using NitroxClient.MonoBehaviours.Gui.MainMenu;
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
public partial class MainMenuRightSide_OpenGroup_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((MainMenuRightSide t) => t.OpenGroup(default(string)));
public static void Prefix(string target)
{
// Stopping the client if we leave the joining process
if (target is not (MainMenuJoinServerPanel.NAME or MainMenuEnterPasswordPanel.NAME or MainMenuNotificationPanel.NAME))
{
JoinServerBackend.StopMultiplayerClient();
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.IO;
using System.Reflection;
using NitroxClient.Helpers;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
public partial class ProtobufSerializer_Deserialize_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((ProtobufSerializer t) => t.Deserialize(default(Stream), default(object), default(Type), default(bool)));
private static readonly NitroxProtobufSerializer serializer = Resolve<NitroxProtobufSerializer>(true);
public static bool Prefix(Stream stream, object target, Type type, bool verbose)
{
if (Multiplayer.Active && serializer.NitroxTypes.ContainsKey(type))
{
serializer.Deserialize(stream, target, type);
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.IO;
using System.Reflection;
using NitroxClient.Helpers;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
public partial class ProtobufSerializer_Serialize_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((ProtobufSerializer t) => t.Serialize(default(Stream), default(object), default(Type)));
/// <summary>
/// This patch is in a hot path so it needs this optimization.
/// </summary>
private static readonly NitroxProtobufSerializer serializer = Resolve<NitroxProtobufSerializer>(true);
public static bool Prefix(Stream stream, object source, Type type)
{
if (Multiplayer.Active && serializer.NitroxTypes.ContainsKey(type))
{
serializer.Serialize(stream, source);
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,86 @@
// Set "false" to "true" then start any game and look into game.log to find the code to paste in ReefbackSpawnData.cs
#if false
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Persistent;
public sealed partial class ReefbackLife_OnEnable_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((ReefbackLife t) => t.OnEnable());
private static bool printed;
public static void Postfix(ReefbackLife __instance)
{
if (printed)
{
return;
}
printed = true;
// Make sure to switch culture so that formatted string use "." instead of "," for numbers
CultureInfo previousCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
string text = $"\npublic const int CREATURE_SLOTS_COUNT = {__instance.creatureSlots.Length};";
text += $"\npublic const int PLANT_SLOTS_COUNT = {__instance.plantSlots.Length};";
text += $"\npublic const int GRASS_VARIANTS_COUNT = {__instance.grassVariants.Length};";
text += $"\n\npublic static readonly NitroxTransform CreatureSlotsRootTransform = {TransformToString(__instance.creatureSlots[0].parent)};";
text += $"\npublic static readonly NitroxTransform PlantSlotsRootTransform = {TransformToString(__instance.plantSlotsRoot)};";
text += "\n\npublic static List<ReefbackSlotCreature> SpawnableCreatures { get; } =\n[\n";
foreach (ReefbackSlotsData.ReefbackSlotCreature reefbackSlotCreature in __instance.reefbackSlotsData.creatures)
{
string classId = reefbackSlotCreature.prefab.GetComponent<UniqueIdentifier>().ClassId;
text += $"new() {{ Probability = {reefbackSlotCreature.probability}f, MinNumber = {reefbackSlotCreature.minNumber}, MaxNumber = {reefbackSlotCreature.maxNumber}, ClassId = \"{classId}\" }},\n";
}
text += "];\n\npublic static List<ReefbackSlotPlant> SpawnablePlants { get; } =\n[\n";
foreach (ReefbackSlotsData.ReefbackSlotPlant reefbackSlotPlant in __instance.reefbackSlotsData.plants)
{
List<string> list = [];
foreach (GameObject gameObject in reefbackSlotPlant.prefabVariants)
{
list.Add(gameObject.GetComponent<UniqueIdentifier>().ClassId);
}
text += $"new() {{ ClassIds = [\"{string.Join("\", \"", list)}\"], Probability = {reefbackSlotPlant.probability}f, StartRotation = {Vector3ToString(reefbackSlotPlant.startRotation)} }},\n";
}
text += "];\n\npublic static List<NitroxTransform> CreatureSlotsCoordinates { get; } =\n[\n";
foreach (Transform transform in __instance.creatureSlots)
{
text += $"{TransformToString(transform)},\n";
}
text += "];\n\npublic static List<NitroxTransform> PlantSlotsCoordinates { get; } =\n[\n";
foreach (Transform transform in __instance.plantSlots)
{
text += $"{TransformToString(transform)},\n";
}
text += "];\n";
Log.Info(text);
CultureInfo.CurrentCulture = previousCulture;
}
private static string TransformToString(Transform transform)
{
return $"new({Vector3ToString(transform.localPosition)}, {QuaternionToString(transform.localRotation)}, {Vector3ToString(transform.localScale)})";
}
private static string Vector3ToString(Vector3 vector3)
{
return $"new({vector3.x}f, {vector3.y}f, {vector3.z}f)";
}
private static string QuaternionToString(Quaternion quaternion)
{
return $"new({quaternion.x}f, {quaternion.y}f, {quaternion.z}f, {quaternion.w}f)";
}
}
#endif

View File

@@ -0,0 +1,16 @@
using System.IO;
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
public partial class ScreenshotManager_Initialise : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => ScreenshotManager.Initialize(default(string)));
public static void Prefix(ScreenshotManager __instance, ref string _savePath)
{
_savePath = Path.GetFullPath(NitroxUser.LauncherPath ?? ".");
}
}
}

View File

@@ -0,0 +1,16 @@
#if DEBUG
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
internal sealed partial class StartScreen_TryToShowDisclaimer_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((StartScreen t) => t.TryToShowDisclaimer());
/// <summary>
/// Speed up startup in development by skipping disclaimer screen.
/// </summary>
public static bool Prefix() => false;
}
#endif

View File

@@ -0,0 +1,25 @@
using System;
using System.Reflection;
using UnityEngine;
namespace NitroxPatcher.Patches.Persistent;
/// <summary>
/// Makes sure that Coroutine aren't fully blocked once they encounter an Exception
/// </summary>
public sealed partial class UnityEngine_SetupCoroutine_Patch : NitroxPatch, IPersistentPatch
{
// UnityEngine DLLs aren't publicized so we can't access this class as done in other patches
public static readonly MethodInfo TARGET_METHOD = Assembly.GetAssembly(typeof(GameObject))
.GetType("UnityEngine.SetupCoroutine")
.GetMethod("InvokeMoveNext", BindingFlags.Public | BindingFlags.Static);
public static Exception Finalizer(Exception __exception)
{
if (__exception != null)
{
Log.Error(__exception);
}
return null;
}
}

View File

@@ -0,0 +1,16 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
public partial class uGUI_FeedbackCollector_IsEnabled_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((uGUI_FeedbackCollector t) => t.IsEnabled());
public static bool Prefix(ref bool __result)
{
__result = false;
return false;
}
}
}

View File

@@ -0,0 +1,79 @@
#if DEBUG
using System;
using System.Net;
using System.Numerics;
using System.Reflection;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.MultiplayerSession;
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServerJoin;
using NitroxModel;
using NitroxModel.DataStructures.Unity;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel.MultiplayerSession;
namespace NitroxPatcher.Patches.Persistent;
// TODO: Rework this to be less ad hoc and more robust with command line arguments
public sealed partial class uGUI_MainMenu_Start_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((uGUI_MainMenu t) => t.Start()));
private static bool applied;
private static string playerName;
public static void Postfix()
{
if (applied)
{
return;
}
applied = true;
string[] args = Environment.GetCommandLineArgs();
Log.Info($"CommandLineArgs: {string.Join(" ", args)}");
for (int i = 0; i < args.Length; i++)
{
if (args[i].Equals("--instantlaunch", StringComparison.OrdinalIgnoreCase) && args.Length > i + 1)
{
playerName = args[i + 1];
Log.Info($"Detected instant launch, connecting to 127.0.0.1:11000 as {playerName}");
_ = JoinServerBackend.StartDetachedMultiplayerClientAsync(IPAddress.Loopback, 11000, SessionConnectionStateChangedHandler);
}
}
}
private static void SessionConnectionStateChangedHandler(IMultiplayerSessionConnectionState state)
{
switch (state.CurrentStage)
{
case MultiplayerSessionConnectionStage.AWAITING_RESERVATION_CREDENTIALS:
if (Resolve<IMultiplayerSession>().SessionPolicy.RequiresServerPassword)
{
Log.Error("Local server requires a password which is not supported with instant launch.");
Log.InGame("Local server requires a password which is not supported with instant launch.");
break;
}
NitroxColor playerColor = new(1,1,1);
byte[] nameHash = playerName.AsMd5Hash();
if (nameHash.Length >= 8)
{
float hue = BitConverter.ToUInt64([nameHash[0], nameHash[1], nameHash[2], nameHash[3], nameHash[4], nameHash[5], nameHash[6], nameHash[7]], 0) / (float)ulong.MaxValue;
playerColor = NitroxColor.FromHsb(hue);
}
PlayerSettings playerSettings = new(playerColor);
AuthenticationContext authenticationContext = new(playerName, Optional.Empty);
Resolve<IMultiplayerSession>().RequestSessionReservation(playerSettings, authenticationContext);
break;
case MultiplayerSessionConnectionStage.SESSION_RESERVED:
Resolve<IMultiplayerSession>().ConnectionStateChanged -= SessionConnectionStateChangedHandler;
JoinServerBackend.StartGame();
break;
}
}
}
#endif

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
/// <summary>
/// Remove the option for PDA pause
/// </summary>
public sealed partial class uGUI_OptionsPanel_AddAccessibilityTab_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((uGUI_OptionsPanel t) => t.AddAccessibilityTab());
/// <summary>
/// Simply removes following line
/// AddToggleOption(num, "PDAPause" ...
/// </summary>
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).MatchStartForward([
new(OpCodes.Ldarg_0),
new(OpCodes.Ldloc_0),
new(OpCodes.Ldstr, "PDAPause")
])
.RemoveInstructions(10)
.InstructionEnumeration();
}
}

View File

@@ -0,0 +1,24 @@
using System.Reflection;
using NitroxClient.MonoBehaviours.Gui.Input;
using NitroxClient.MonoBehaviours.Gui.Input.KeyBindings;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent
{
public partial class uGUI_OptionsPanel_AddBindings_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((uGUI_OptionsPanel t) => t.AddBindings(default(int), default(GameInput.Device)));
public static void Postfix(uGUI_OptionsPanel __instance, int tabIndex, GameInput.Device device)
{
KeyBindingManager keyBindingManager = new();
if (device == GameInput.Device.Keyboard)
{
foreach (KeyBinding keyBinding in keyBindingManager.KeyboardKeyBindings)
{
__instance.AddBindingOption(tabIndex, keyBinding.Label, keyBinding.Device, keyBinding.Button);
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Reflection;
using NitroxClient.GameLogic.Settings;
using NitroxModel.Helper;
using UnityEngine.Events;
namespace NitroxPatcher.Patches.Persistent
{
public partial class uGUI_OptionsPanel_AddTabs_Patch : NitroxPatch, IPersistentPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((uGUI_OptionsPanel t) => t.AddTabs());
private static NitroxSettingsManager nitroxSettingsManager;
public static void Postfix(uGUI_OptionsPanel __instance)
{
nitroxSettingsManager ??= Resolve<NitroxSettingsManager>(true);
int tabIndex = __instance.AddTab("Nitrox");
foreach (KeyValuePair<string, List<NitroxSettingsManager.Setting>> settingEntries in nitroxSettingsManager.NitroxSettings)
{
__instance.AddHeading(tabIndex, settingEntries.Key);
foreach (NitroxSettingsManager.Setting setting in settingEntries.Value)
{
switch (setting.SettingType)
{
case NitroxSettingsManager.SettingType.TOGGLE:
__instance.AddToggleOption(tabIndex, setting.Label, setting.GetValue<bool>(), (UnityAction<bool>)setting.Callback);
break;
case NitroxSettingsManager.SettingType.SLIDER:
__instance.AddSliderOption(tabIndex, setting.Label, setting.GetValue<float>(), setting.SliderMinValue, setting.SliderMaxValue, setting.SliderDefaultValue, setting.SliderStep, (UnityAction<float>)setting.Callback, setting.LabelMode, setting.FloatFormat, setting.Tooltip);
break;
case NitroxSettingsManager.SettingType.LIST:
__instance.AddChoiceOption(tabIndex, setting.Label, setting.ListItems, setting.GetValue<int>(), (UnityAction<int>)setting.Callback);
break;
case NitroxSettingsManager.SettingType.BUTTON:
__instance.AddButton(tabIndex, setting.Label, (UnityAction)setting.Callback);
break;
}
}
}
}
}
}