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

14
NitroxPatcher/App.config Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Configuration" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

158
NitroxPatcher/Main.cs Normal file
View File

@@ -0,0 +1,158 @@
extern alias JB;
global using NitroxModel.Logger;
global using static NitroxClient.Helpers.NitroxEntityExtensions;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using JB::JetBrains.Annotations;
using Microsoft.Win32;
using NitroxModel.Helper;
using NitroxModel_Subnautica.Logger;
using UnityEngine;
namespace NitroxPatcher;
public static class Main
{
/// <summary>
/// Lazily (i.e. when called, unlike immediately on class load) gets the path to the Nitrox Launcher folder.
/// This path can be anywhere on the system because it's placed somewhere the user likes.
/// </summary>
private static readonly Lazy<string> nitroxLauncherDir = new(() =>
{
// Get path from command args.
string[] args = Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length - 1; i++)
{
if (args[i].Equals("--nitrox", StringComparison.OrdinalIgnoreCase) && Directory.Exists(args[i + 1]))
{
return Path.GetFullPath(args[i + 1]);
}
}
// Get path from environment variable.
string envPath = Environment.GetEnvironmentVariable("NITROX_LAUNCHER_PATH", EnvironmentVariableTarget.Process);
if (Directory.Exists(envPath))
{
return envPath;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Get path from windows registry.
using RegistryKey nitroxRegKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Nitrox");
if (nitroxRegKey == null)
{
return null;
}
string path = nitroxRegKey.GetValue("LauncherPath") as string;
return Directory.Exists(path) ? path : null;
}
return null;
});
private static readonly char[] newLineChars = Environment.NewLine.ToCharArray();
/// <summary>
/// Entrypoint of Nitrox. Code in this method cannot use other dependencies (DLLs) without crashing
/// due to <see cref="AppDomain.AssemblyResolve" /> not being called.
/// Use the <see cref="Init" /> method or later before using dependency code.
/// </summary>
[UsedImplicitly]
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Execute()
{
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomainOnAssemblyResolve;
if (!Directory.Exists(Environment.GetEnvironmentVariable("NITROX_LAUNCHER_PATH")))
{
Environment.SetEnvironmentVariable("NITROX_LAUNCHER_PATH", nitroxLauncherDir.Value, EnvironmentVariableTarget.Process);
}
if (!Directory.Exists(Environment.GetEnvironmentVariable("NITROX_LAUNCHER_PATH")))
{
Console.WriteLine("Nitrox will not load because launcher path was not provided.");
return;
}
InitWithDependencies();
}
/// <summary>
/// This method must not be inlined since the AppDomain dependency resolve will be triggered when the JIT compiles this method. If it's inlined it will cause dependencies to
/// resolve in <see cref="Execute" /> *before* the dependency resolve listener is applied.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void InitWithDependencies()
{
Log.Setup(gameLogger: new SubnauticaInGameLogger(), useConsoleLogging: false);
// Capture unity errors to be logged by our logging framework.
Application.logMessageReceived += (condition, stackTrace, type) =>
{
switch (type)
{
case LogType.Error:
case LogType.Exception:
string toWrite = condition;
if (!string.IsNullOrWhiteSpace(stackTrace))
{
toWrite += $"{Environment.NewLine}{stackTrace}";
}
Log.ErrorUnity(toWrite.Trim(newLineChars));
break;
case LogType.Warning:
case LogType.Log:
case LogType.Assert:
// These logs from Unity spam too much uninteresting stuff
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
};
Log.Info($"Using Nitrox {NitroxEnvironment.ReleasePhase} V{NitroxEnvironment.Version} built on {NitroxEnvironment.BuildDate}");
try
{
Patcher.Initialize();
}
catch (Exception ex)
{
// Placeholder for popup gui
Log.Error(ex, "Unhandled exception occurred while initializing Nitrox:");
}
}
/// <summary>
/// Nitrox DLL location resolver.
/// <p/>
/// Required to load the files from the Nitrox Launcher subfolder which would otherwise not be found.
/// </summary>
private static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args)
{
string dllFileName = args.Name.Split(',')[0];
if (!dllFileName.EndsWith(".dll"))
{
dllFileName += ".dll";
}
// Load DLLs where Nitrox launcher is first, if not found, use Subnautica's DLLs.
string dllPath = Path.Combine(nitroxLauncherDir.Value, "lib", "net472", dllFileName);
if (!File.Exists(dllPath))
{
Console.Write($"Did not find '{dllFileName}' at '{dllPath}'");
dllPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), dllFileName);
Console.WriteLine($", looking at {dllPath}");
}
if (!File.Exists(dllPath))
{
Console.WriteLine($"Nitrox dll missing: {dllPath}");
}
return Assembly.LoadFile(dllPath);
}
}

View File

@@ -0,0 +1,24 @@
using System.Reflection;
using Autofac;
using NitroxPatcher.Patches;
namespace NitroxPatcher.Modules;
/// <summary>
/// Simple Dependency Injection (DI) container for registering the patch classes with AutoFac.
/// </summary>
public class NitroxPatchesModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
builder
.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AssignableTo<IPersistentPatch>()
.AsImplementedInterfaces().SingleInstance();
builder
.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AssignableTo<IDynamicPatch>()
.AsImplementedInterfaces().SingleInstance();
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;net9.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HarmonyX" Version="2.10.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NitroxClient\NitroxClient.csproj" />
<ProjectReference Include="..\NitroxModel-Subnautica\NitroxModel-Subnautica.csproj" />
</ItemGroup>
<Target Name="IncludeGameReferences" AfterTargets="FindGameAndIncludeReferences">
<ItemGroup>
<Reference Include="FMODUnity">
<HintPath>$(GameManagedDir)\FMODUnity.dll</HintPath>
</Reference>
<Reference Include="Sentry">
<HintPath>$(GameManagedDir)\Sentry.dll</HintPath>
</Reference>
</ItemGroup>
</Target>
</Project>

161
NitroxPatcher/Patcher.cs Normal file
View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Autofac;
using HarmonyLib;
using HarmonyLib.Tools;
using NitroxClient;
using NitroxClient.MonoBehaviours;
using NitroxModel.Core;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxPatcher.Modules;
using NitroxPatcher.Patches;
using UnityEngine;
namespace NitroxPatcher;
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "DIMA001:Dependency Injection container is used directly")]
internal static class Patcher
{
/// <summary>
/// Dependency Injection container used by NitroxPatcher only.
/// </summary>
private static IContainer container;
private static readonly Harmony harmony = new("com.nitroxmod.harmony");
private static bool isApplied;
/// <summary>
/// Applies all the dynamic patches defined in Patches namespace.
/// Persistent patches are applied when the game initializes (i.e. before main menu).
/// See <see cref="Patcher.Initialize"/>.
/// </summary>
public static void Apply()
{
Validate.NotNull(container, "No patches have been discovered yet! Run Execute() first.");
if (isApplied)
{
return;
}
foreach (IDynamicPatch patch in container.Resolve<IDynamicPatch[]>())
{
Log.Debug($"Applying dynamic patch {patch.GetType().Name}");
try
{
patch.Patch(harmony);
}
catch (HarmonyException e)
{
Exception innerMost = e;
while (innerMost.InnerException != null)
{
innerMost = innerMost.InnerException;
}
Log.Error($"Error patching {patch.GetType().Name}{Environment.NewLine}{innerMost}");
}
catch (Exception e)
{
Log.Error($"Error patching {patch.GetType().Name}{Environment.NewLine}{e}");
}
}
isApplied = true;
}
/// <summary>
/// Removes all the dynamic patches defined by <see cref="NitroxPatcher"/>.
/// <p/>
/// If the player starts the main menu for the first time, or returns from a (multiplayer) session, get rid of all the
/// patches if applicable.
/// </summary>
public static void Restore()
{
Validate.NotNull(container, "No patches have been discovered yet! Run Execute() first.");
if (!isApplied)
{
return;
}
foreach (IDynamicPatch patch in container.Resolve<IDynamicPatch[]>())
{
Log.Debug($"Restoring dynamic patch {patch.GetType().Name}");
patch.Restore(harmony);
}
isApplied = false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void Initialize()
{
Optional.ApplyHasValueCondition<UnityEngine.Object>(o => (bool)o);
if (container != null)
{
throw new Exception($"Patches have already been detected! Call {nameof(Apply)} or {nameof(Restore)} instead.");
}
Log.Info("Registering dependencies");
container = CreatePatchingContainer();
try
{
NitroxServiceLocator.InitializeDependencyContainer(new ClientAutoFacRegistrar());
}
catch (ReflectionTypeLoadException ex)
{
Log.Error($"Failed to load one or more dependency types for Nitrox. Assembly: {ex.Types.FirstOrDefault()?.Assembly.FullName ?? "unknown"}");
foreach (Exception loaderEx in ex.LoaderExceptions)
{
Log.Error(loaderEx);
}
throw;
}
catch (Exception ex)
{
Log.Error(ex, "Error while initializing and loading dependencies.");
throw;
}
InitPatches();
ApplyNitroxBehaviours();
}
private static void InitPatches()
{
Log.Info("Patching Subnautica...");
// Enabling this creates a log file on your desktop (why there?), showing the emitted IL instructions.
HarmonyFileLog.Enabled = false;
foreach (IPersistentPatch patch in container.Resolve<IEnumerable<IPersistentPatch>>())
{
Log.Debug($"Applying persistent patch {patch.GetType().Name}");
patch.Patch(harmony);
}
Multiplayer.OnBeforeMultiplayerStart += Apply;
Multiplayer.OnAfterMultiplayerEnd += Restore;
Log.Info("Completed patching");
}
private static IContainer CreatePatchingContainer()
{
ContainerBuilder builder = new();
builder.RegisterModule(new NitroxPatchesModule());
return builder.Build();
}
private static void ApplyNitroxBehaviours()
{
Log.Info("Applying Nitrox behaviours..");
GameObject nitroxRoot = new();
nitroxRoot.name = "Nitrox";
nitroxRoot.AddComponent<NitroxBootstrapper>();
Log.Info("Behaviours applied.");
}
}

View File

@@ -0,0 +1,31 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Allow creatures to choose remote players as targets only if they can be attacked (<see cref="RemotePlayer.CanBeAttacked"/>)
/// </summary>
public sealed partial class AggressiveWhenSeeTarget_IsTargetValid_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((AggressiveWhenSeeTarget t) => t.IsTargetValid(default(GameObject)));
public static bool Prefix(GameObject target, ref bool __result)
{
if (!target)
{
return false;
}
// We only want to cancel if the target is a remote player which can't be attacked
if (target.TryGetComponent(out RemotePlayerIdentifier remotePlayerIdentifier) &&
!remotePlayerIdentifier.RemotePlayer.CanBeAttacked())
{
__result = false;
return false;
}
return true;
}
}

View File

@@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class AggressiveWhenSeeTarget_ScanForAggressionTarget_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((AggressiveWhenSeeTarget t) => t.ScanForAggressionTarget());
public static bool Prefix(AggressiveWhenSeeTarget __instance)
{
if (!__instance.TryGetNitroxId(out NitroxId creatureId) ||
Resolve<SimulationOwnership>().HasAnyLockType(creatureId))
{
return true;
}
return false;
}
/*
*
* Debug.DrawLine(aggressionTarget.transform.position, base.transform.position, Color.white);
* this.creature.Aggression.Add(num6);
* BroadcastTargetChange(this, aggressionTarget); <--- [INSERTED LINE]
* this.lastTarget.SetTarget(aggressionTarget);
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).MatchStartForward(new CodeMatch(OpCodes.Callvirt, Reflect.Method((CreatureTrait t) => t.Add(default))))
.Advance(1)
.InsertAndAdvance([
new CodeInstruction(OpCodes.Ldarg_0),
new CodeInstruction(OpCodes.Ldloc_0),
new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastTargetChange(default, default)))
]).InstructionEnumeration();
}
public static void BroadcastTargetChange(AggressiveWhenSeeTarget aggressiveWhenSeeTarget, GameObject aggressionTarget)
{
if (!Resolve<AI>().IsCreatureWhitelisted(aggressiveWhenSeeTarget.creature))
{
return;
}
// If the function was called to this point, either it'll return because it doesn't have an id or it'll be evident that we have ownership over the aggressive creature
LastTarget lastTarget = aggressiveWhenSeeTarget.lastTarget;
// If there's already (likely another) locked target, we get its id over aggressionTarget
GameObject realTarget = lastTarget.targetLocked ? lastTarget.target : aggressionTarget;
if (realTarget && realTarget.TryGetNitroxId(out NitroxId targetId) &&
aggressiveWhenSeeTarget.TryGetNitroxId(out NitroxId creatureId))
{
float aggressionAmount = aggressiveWhenSeeTarget.creature.Aggression.Value;
Resolve<IPacketSender>().Send(new AggressiveWhenSeeTargetChanged(creatureId, targetId, lastTarget.targetLocked, aggressionAmount));
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Once multiplayer is initiated, prevent game logic from sleeping (i.e. freezing).
/// </summary>
public sealed partial class Application_IsFocused_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Property(() => Application.isFocused).GetMethod;
public static bool Prefix(ref bool __result)
{
__result = true;
return false;
}
}

View File

@@ -0,0 +1,14 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class ArmsController_Start_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((ArmsController t) => t.Start());
public static void Postfix(ArmsController __instance)
{
__instance.Reconfigure(null);
}
}

View File

@@ -0,0 +1,28 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class ArmsController_Update_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((ArmsController t) => t.Update());
public static bool Prefix(ArmsController __instance)
{
if (__instance.smoothSpeedAboveWater == 0)
{
if (__instance.reconfigureWorldTarget)
{
__instance.Reconfigure(null);
__instance.reconfigureWorldTarget = false;
}
__instance.leftAim.Update(__instance.ikToggleTime);
__instance.rightAim.Update(__instance.ikToggleTime);
__instance.UpdateHandIKWeights();
return false;
}
return true;
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Cancels <see cref="AttackCyclops.OnCollisionEnter"/> on players not simulating the creature.
/// Replaces the bad cyclops detection to also find out about remote players in the collisioned cyclops.
/// </summary>
public sealed partial class AttackCyclops_OnCollisionEnter_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((AttackCyclops t) => t.OnCollisionEnter(default));
public static bool Prefix(AttackCyclops __instance)
{
return !__instance.TryGetNitroxId(out NitroxId creatureId) ||
Resolve<SimulationOwnership>().HasAnyLockType(creatureId);
}
/*
* REPLACE:
* if (Player.main != null && Player.main.currentSub != null && Player.main.currentSub.isCyclops && Player.main.currentSub.gameObject == collision.gameObject)
* {
* if (this.isActive)
* BY:
* if (AttackCyclops_OnCollisionEnter_Patch.ShouldCollisionAnnoyCreature(collision))
* {
* if (this.isActive)
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).Advance(1)
.RemoveInstructions(19)
.InsertAndAdvance([
new CodeInstruction(OpCodes.Ldarg_1),
new CodeInstruction(OpCodes.Call, Reflect.Method(() => ShouldCollisionAnnoyCreature(default)))
]).InstructionEnumeration();
}
public static bool ShouldCollisionAnnoyCreature(Collision collision)
{
return AttackCyclops_UpdateAggression_Patch.IsTargetAValidInhabitedCyclops(collision.gameObject);
}
}

View File

@@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours.Cyclops;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Patch responsible for broadcasting <see cref="AttackCyclops"/>' latest data and cancelling <see cref="AttackCyclops.UpdateAggression"/> on players not simulating the creature.
/// </summary>
public sealed partial class AttackCyclops_UpdateAggression_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((AttackCyclops t) => t.UpdateAggression());
// TODO: Sync attacksub command
public static bool Prefix(AttackCyclops __instance)
{
return !__instance.TryGetNitroxId(out NitroxId creatureId) ||
Resolve<SimulationOwnership>().HasAnyLockType(creatureId);
}
/*
* REPLACE:
* if (Player.main != null && Player.main.currentSub != null && Player.main.currentSub.isCyclops)
* {
* cyclopsNoiseManager = Player.main.currentSub.noiseManager;
* }
* else if (this.forcedNoiseManager != null)
* {
* cyclopsNoiseManager = this.forcedNoiseManager;
* }
* BY:
* cyclopsNoiseManager = AttackCyclops_UpdateAggression_Patch.FindClosestCyclopsNoiseManagerIfAny(this);
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).MatchStartForward([
new CodeMatch(OpCodes.Ldsfld),
new CodeMatch(OpCodes.Ldnull),
new CodeMatch(OpCodes.Call),
new CodeMatch(OpCodes.Brfalse),
new CodeMatch(OpCodes.Ldsfld),
new CodeMatch(OpCodes.Callvirt, Reflect.Property((Player t) => t.currentSub).GetGetMethod())
]).RemoveInstructions(25)
.InsertAndAdvance([
new CodeInstruction(OpCodes.Ldarg_0),
new CodeInstruction(OpCodes.Call, Reflect.Method(() => FindClosestCyclopsNoiseManagerIfAny(default)))
]).InstructionEnumeration();
}
public static void Postfix(AttackCyclops __instance)
{
if (__instance.currentTarget && __instance.currentTarget.TryGetNitroxId(out NitroxId targetId) &&
__instance.TryGetNitroxId(out NitroxId creatureId) && Resolve<SimulationOwnership>().HasAnyLockType(creatureId))
{
float aggressiveToNoise = __instance.aggressiveToNoise.Value;
Resolve<IPacketSender>().Send(new AttackCyclopsTargetChanged(creatureId, targetId, aggressiveToNoise));
}
}
/// <summary>
/// This is executed every 0.5s for each spawned leviathan so we can focus on optimization
/// </summary>
public static CyclopsNoiseManager FindClosestCyclopsNoiseManagerIfAny(AttackCyclops attackCyclops)
{
// Limit for optimization
if (NitroxCyclops.ScaledNoiseByCyclops.Count > 100)
{
// Cyclops are marked with EcoTargetType.Whale
IEcoTarget ecoTarget = EcoRegionManager.main.FindNearestTarget(EcoTargetType.Whale, attackCyclops.transform.position, IsTargetAValidInhabitedCyclops, 3);
if (ecoTarget == null)
{
return null;
}
return ecoTarget.GetGameObject().GetComponent<CyclopsNoiseManager>();
}
float minDistance = float.MaxValue;
NitroxCyclops closest = null;
foreach (KeyValuePair<NitroxCyclops, float> cyclopsEntry in NitroxCyclops.ScaledNoiseByCyclops)
{
if (cyclopsEntry.Key.Pawns.Count == 0)
{
continue;
}
// Calculations from the "if (closestDecoy != null || cyclopsNoiseManager != null)" part
float distance = Vector3.Distance(cyclopsEntry.Key.transform.position, attackCyclops.transform.position);
if (distance < cyclopsEntry.Value && distance < minDistance)
{
minDistance = distance;
closest = cyclopsEntry.Key;
}
}
return closest.AliveOrNull()?.GetComponent<CyclopsNoiseManager>();
}
public static bool IsTargetAValidInhabitedCyclops(IEcoTarget target)
{
return IsTargetAValidInhabitedCyclops(target.GetGameObject());
}
public static bool IsTargetAValidInhabitedCyclops(GameObject targetObject)
{
return targetObject.TryGetComponent(out NitroxCyclops nitroxCyclops) && nitroxCyclops.Pawns.Count > 0;
}
}

View File

@@ -0,0 +1,31 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Allows creatures to choose remote players as targets only if they can be attacked (<see cref="RemotePlayer.CanBeAttacked"/>)
/// </summary>
public sealed partial class AttackLastTarget_CanAttackTarget_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((AttackLastTarget t) => t.CanAttackTarget(default));
public static bool Prefix(GameObject target, ref bool __result)
{
if (!target)
{
return false;
}
// We only want to cancel if the target is a remote player which can't be attacked
if (target.TryGetComponent(out RemotePlayerIdentifier remotePlayerIdentifier) &&
(!remotePlayerIdentifier.RemotePlayer.LiveMixin.IsAlive() || !remotePlayerIdentifier.RemotePlayer.CanBeAttacked()))
{
__result = false;
return false;
}
return true;
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <remarks>
/// Prevents <see cref="AuroraWarnings.Update"/> from occurring before initial sync has completed.
/// It lets us avoid a very weird edge case in which warnings are triggered way too early. Linked to <see cref="CrashedShipExploder_Update_Patch"/>
/// </remarks>
public sealed partial class AuroraWarnings_Update_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((AuroraWarnings t) => t.Update());
public static bool Prefix()
{
return Multiplayer.Main && Multiplayer.Main.InitialSyncCompleted;
}
}

View File

@@ -0,0 +1,201 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Spawning.Bases;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Bases;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxPatcher.PatternMatching;
using UnityEngine;
using static System.Reflection.Emit.OpCodes;
using static NitroxClient.GameLogic.Bases.BuildingHandler;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class BaseDeconstructable_Deconstruct_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((BaseDeconstructable t) => t.Deconstruct());
private static TemporaryBuildData Temp => BuildingHandler.Main.Temp;
private static BuildPieceIdentifier cachedPieceIdentifier;
public static readonly InstructionsPattern BaseDeconstructInstructionPattern1 = new()
{
Callvirt,
Call,
Ldloc_3,
{ new() { OpCode = Callvirt, Operand = new(nameof(BaseGhost), nameof(BaseGhost.ClearTargetBase)) }, "Insert1" }
};
public static readonly InstructionsPattern BaseDeconstructInstructionPattern2 = new()
{
Ldloc_0,
new() { OpCode = Callvirt, Operand = new(nameof(Base), nameof(Base.FixCorridorLinks)) },
Ldloc_0,
{ new() { OpCode = Callvirt, Operand = new(nameof(Base), nameof(Base.RebuildGeometry)) }, "Insert2" },
};
public static IEnumerable<CodeInstruction> InstructionsToAdd(bool destroyed)
{
yield return new(Ldarg_0);
yield return new(Ldloc_2);
yield return new(Ldloc_0);
yield return new(destroyed ? Ldc_I4_1 : Ldc_I4_0);
yield return new(Call, Reflect.Method(() => PieceDeconstructed(default, default, default, default)));
}
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
instructions.Transform(BaseDeconstructInstructionPattern1, (label, instruction) =>
{
if (label.Equals("Insert1"))
{
return InstructionsToAdd(true);
}
return null;
}).Transform(BaseDeconstructInstructionPattern2, (label, instruction) =>
{
if (label.Equals("Insert2"))
{
return InstructionsToAdd(false);
}
return null;
});
public static void Prefix(BaseDeconstructable __instance)
{
BuildUtils.TryGetIdentifier(__instance, out cachedPieceIdentifier, null, __instance.face);
}
public static void PieceDeconstructed(BaseDeconstructable baseDeconstructable, ConstructableBase constructableBase, Base @base, bool destroyed)
{
if (!@base.TryGetNitroxId(out NitroxId baseId))
{
Log.Error("Couldn't find NitroxEntity on a deconstructed base, which is really problematic");
return;
}
GhostEntity ghostEntity = GhostEntitySpawner.From(constructableBase);
ghostEntity.Id = baseId;
if (destroyed)
{
// Base was destroyed and replaced with a simple ghost
Log.Verbose("Transferring id from base to the new ghost");
NitroxEntity.SetNewId(constructableBase.gameObject, baseId);
Log.Verbose("Base destroyed and replaced by a simple ghost");
Resolve<IPacketSender>().Send(new BaseDeconstructed(baseId, ghostEntity));
return;
}
if (!baseDeconstructable.GetComponentInParent<BaseCell>())
{
Log.Error("Couldn't find a BaseCell parent to the BaseDeconstructable");
return;
}
// If deconstruction was ordered by BuildingHandler, then we simply take the provided id
if (Temp.Id != null)
{
// If it had an attached module, we'll also delete the NitroxEntity from the said module similarly to the code below
if (NitroxEntity.TryGetObjectFrom(Temp.Id, out GameObject moduleObject) &&
moduleObject.TryGetComponent(out IBaseModule baseModule) &&
constructableBase.moduleFace.HasValue && constructableBase.moduleFace.Value.Equals(baseModule.moduleFace))
{
Object.Destroy(moduleObject.GetComponent<NitroxEntity>());
}
else if (constructableBase.techType.Equals(TechType.BaseMoonpool) && @base.TryGetComponent(out MoonpoolManager moonpoolManager))
{
moonpoolManager.DeregisterMoonpool(constructableBase.transform);
}
NitroxEntity.SetNewId(constructableBase.gameObject, Temp.Id);
// We don't need to go any further
return;
}
NitroxId pieceId = null;
// If the destructed piece has an attached module, we'll transfer the NitroxEntity from it
if (constructableBase.moduleFace.HasValue)
{
Base.Face moduleFace = constructableBase.moduleFace.Value;
moduleFace.cell += @base.GetAnchor();
Component geometryObject = @base.GetModule(moduleFace).AliveOrNull();
if (geometryObject && geometryObject.TryGetNitroxEntity(out NitroxEntity moduleEntity))
{
pieceId = moduleEntity.Id;
Object.Destroy(moduleEntity);
Log.Verbose($"Successfully transferred NitroxEntity from module geometry {moduleEntity.Id}");
}
}
else
{
switch (constructableBase.techType)
{
case TechType.BaseMoonpool:
if (@base.TryGetComponent(out MoonpoolManager moonpoolManager))
{
pieceId = moonpoolManager.DeregisterMoonpool(constructableBase.transform); // pieceId can still be null
}
break;
case TechType.BaseMapRoom:
Int3 mapRoomFunctionalityCell = BuildUtils.GetMapRoomFunctionalityCell(constructableBase.model.GetComponent<BaseGhost>());
MapRoomFunctionality mapRoomFunctionality = @base.GetMapRoomFunctionalityForCell(mapRoomFunctionalityCell);
if (mapRoomFunctionality && mapRoomFunctionality.TryGetNitroxId(out NitroxId mapRoomId))
{
pieceId = mapRoomId;
}
else
{
Log.Error("Either couldn't find a MapRoomFunctionality associated with destroyed piece or couldn't find a NitroxEntity onto it.");
}
break;
case TechType.BaseWaterPark:
// When a BaseWaterPark doesn't have a moduleFace, it means that there's still another WaterPark so we don't need to destroy its id and it won't be an error
break;
default:
if (baseDeconstructable.GetComponent<IBaseModuleGeometry>() != null)
{
Log.Error("Couldn't find the module's GameObject of IBaseModuleGeometry when transferring the NitroxEntity");
}
break;
}
}
// Else, if it's a local client deconstruction, we generate a new one
pieceId ??= new();
NitroxEntity.SetNewId(constructableBase.gameObject, pieceId);
ghostEntity.Id = pieceId;
ghostEntity.ParentId = baseId;
if (cachedPieceIdentifier == default)
{
BuildingHandler.Main.EnsureTracker(baseId).FailedOperations++;
Log.Error($"[{nameof(PieceDeconstructed)}] Couldn't find a CachedPieceIdentifier for deconstructed object {constructableBase.gameObject}");
return;
}
BuildingHandler.Main.EnsureTracker(baseId).LocalOperations++;
int operationId = BuildingHandler.Main.GetCurrentOperationIdOrDefault(baseId);
PieceDeconstructed pieceDeconstructed;
if (Temp.MovedChildrenIdsByNewHostId != null)
{
pieceDeconstructed = new LargeWaterParkDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), Temp.MovedChildrenIdsByNewHostId, operationId);
}
else
{
pieceDeconstructed = Temp.NewWaterPark == null ?
new PieceDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), operationId) :
new WaterParkDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), Temp.NewWaterPark, Temp.MovedChildrenIds, Temp.Transfer, operationId);
}
Log.Verbose($"Base is not empty, sending packet {pieceDeconstructed}");
Resolve<IPacketSender>().Send(pieceDeconstructed);
Temp.Dispose();
}
}

View File

@@ -0,0 +1,20 @@
using NitroxClient.GameLogic.Bases;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
using System.Reflection;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class BaseDeconstructable_DeconstructionAllowed_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((BaseDeconstructable t) => t.DeconstructionAllowed(out Reflect.Ref<string>.Field));
public static void Postfix(BaseDeconstructable __instance, ref bool __result, ref string reason)
{
if (!__result || !BuildingHandler.Main || !__instance.deconstructedBase.TryGetComponent(out NitroxEntity parentEntity))
{
return;
}
Constructable_DeconstructionAllowed_Patch.DeconstructionAllowed(parentEntity.Id, ref __result, ref reason);
}
}

View File

@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class BaseHullStrength_CrushDamageUpdate_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((BaseHullStrength t) => t.CrushDamageUpdate());
public static bool Prefix(BaseHullStrength __instance)
{
return __instance.TryGetNitroxId(out NitroxId baseId) && Resolve<SimulationOwnership>().HasAnyLockType(baseId);
}
/*
* }
* ErrorMessage.AddMessage(Language.main.GetFormat<float>("BaseHullStrDamageDetected", this.totalStrength));
* BroadcastChange(this, random); <------ Inserted line
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
// We add instructions right before the ret which is equivalent to inserting at last offset
return new CodeMatcher(instructions).End()
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_0))
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_0))
.Insert(new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastChange(default, default))))
.InstructionEnumeration();
}
public static void BroadcastChange(BaseHullStrength baseHullStrength, LiveMixin liveMixin)
{
if (!baseHullStrength.TryGetNitroxId(out NitroxId baseId))
{
return;
}
BaseCell baseCell = liveMixin.GetComponent<BaseCell>();
Int3 relativeCell = baseCell.cell - baseHullStrength.baseComp.anchor;
BaseLeakManager baseLeakManager = baseHullStrength.gameObject.EnsureComponent<BaseLeakManager>();
if (!baseLeakManager.TryGetAbsoluteCellId(baseCell.cell, out NitroxId leakId))
{
leakId = new();
}
baseLeakManager.EnsureLeak(relativeCell, leakId, liveMixin.health);
if (!Resolve<LiveMixinManager>().IsRemoteHealthChanging)
{
BaseLeakEntity baseLeakEntity = new(liveMixin.health, relativeCell.ToDto(), leakId, baseId);
Resolve<Entities>().BroadcastEntitySpawnedByClient(baseLeakEntity, true);
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Reflection;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Before a base gets destroyed, we eventually detach/exit any remote player's object that would be inside so that their GameObjects don't get destroyed
/// </summary>
public sealed partial class Base_OnPreDestroy_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Base t) => t.OnPreDestroy());
public static void Prefix(Base __instance)
{
foreach (RemotePlayerIdentifier remotePlayerIdentifier in __instance.GetComponentsInChildren<RemotePlayerIdentifier>(true))
{
remotePlayerIdentifier.RemotePlayer.ResetStates();
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Adds a callback to broadcast beacon label change when edited.
/// </summary>
public sealed partial class BeaconLabel_OnHandClick_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((BeaconLabel t) => t.OnHandClick(default));
public static void Postfix(BeaconLabel __instance)
{
uGUI.main.userInput.callback += _ =>
{
if (__instance.transform.parent && __instance.transform.parent.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Entities>().EntityMetadataChanged(__instance.transform.parent.GetComponent<Beacon>(), id);
}
};
}
}

View File

@@ -0,0 +1,17 @@
using System.Reflection;
using NitroxClient.Communication.Abstract;
using NitroxModel.Core;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Bed_EnterInUseMode_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Bed t) => t.EnterInUseMode(default(Player)));
public static void Postfix()
{
Resolve<IPacketSender>().Send(new BedEnter());
}
}

View File

@@ -0,0 +1,43 @@
using System.Collections;
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.ChatUI;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Bench_ExitSittingMode_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((Bench t) => t.ExitSittingMode(default, default));
public static void Prefix(ref bool __runOriginal)
{
__runOriginal = !Resolve<PlayerChatManager>().IsChatSelected && !DevConsole.instance.selected;
}
public static void Postfix(Bench __instance, bool __runOriginal)
{
if (!__runOriginal)
{
return;
}
if (__instance.TryGetIdOrWarn(out NitroxId id))
{
// Request to be downgraded to a transient lock so we can still simulate the positioning.
Resolve<SimulationOwnership>().RequestSimulationLock(id, SimulationLockType.TRANSIENT);
Resolve<LocalPlayer>().AnimationChange(AnimChangeType.BENCH, AnimChangeState.OFF);
__instance.StartCoroutine(ResetAnimationDelayed(__instance.standUpCinematicController.interpolationTimeOut));
}
}
private static IEnumerator ResetAnimationDelayed(float delay)
{
yield return new WaitForSeconds(delay);
Resolve<LocalPlayer>().AnimationChange(AnimChangeType.BENCH, AnimChangeState.UNSET);
}
}

View File

@@ -0,0 +1,59 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.Simulation;
using NitroxClient.MonoBehaviours;
using NitroxClient.MonoBehaviours.Gui.HUD;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Bench_OnHandClick_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((Bench t) => t.OnHandClick(default(GUIHand)));
private static bool skipPrefix;
public static bool Prefix(Bench __instance, GUIHand hand)
{
if (skipPrefix)
{
return true;
}
if (!__instance.TryGetIdOrWarn(out NitroxId id))
{
return true;
}
if (Resolve<SimulationOwnership>().HasExclusiveLock(id))
{
Log.Debug($"Already have an exclusive lock on the bench/chair: {id}");
return true;
}
HandInteraction<Bench> context = new(__instance, hand);
LockRequest<HandInteraction<Bench>> lockRequest = new(id, SimulationLockType.EXCLUSIVE, ReceivedSimulationLockResponse, context);
Resolve<SimulationOwnership>().RequestSimulationLock(lockRequest);
return false;
}
private static void ReceivedSimulationLockResponse(NitroxId id, bool lockAcquired, HandInteraction<Bench> context)
{
Bench bench = context.Target;
if (lockAcquired)
{
skipPrefix = true;
bench.OnHandClick(context.GuiHand);
Resolve<LocalPlayer>().AnimationChange(AnimChangeType.BENCH, AnimChangeState.ON);
skipPrefix = false;
}
else
{
bench.gameObject.AddComponent<DenyOwnershipHand>();
bench.isValidHandTarget = false;
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Bench_OnPlayerDeath_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((Bench t) => t.OnPlayerDeath(default(Player)));
public static void Postfix(Bench __instance)
{
if (__instance.TryGetIdOrWarn(out NitroxId id))
{
// Request to be downgraded to a transient lock so we can still simulate the positioning.
Resolve<SimulationOwnership>().RequestSimulationLock(id, SimulationLockType.TRANSIENT);
}
Resolve<LocalPlayer>().AnimationChange(AnimChangeType.BENCH, AnimChangeState.UNSET);
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using NitroxClient.Communication.Abstract;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class BreakableResource_BreakIntoResources_Patch : NitroxPatch, IDynamicPatch
{
private static MethodInfo TARGET_METHOD = Reflect.Method((BreakableResource t) => t.BreakIntoResources());
public static void Prefix(BreakableResource __instance)
{
if (!__instance.TryGetNitroxId(out NitroxId destroyedId))
{
Log.Warn($"[{nameof(BreakableResource_BreakIntoResources_Patch)}] Could not find {nameof(NitroxEntity)} for breakable entity {__instance.gameObject.GetFullHierarchyPath()}.");
return;
}
// Case by case handling
// Sea Treaders spawn resource chunks but we don't register them on server-side as they're auto destroyed after 60s
// So we need to broadcast their deletion differently
if (__instance.GetComponent<SinkingGroundChunk>())
{
Resolve<IPacketSender>().Send(new SeaTreaderChunkPickedUp(destroyedId));
}
// Generic case
else
{
Resolve<IPacketSender>().Send(new EntityDestroyed(destroyedId));
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
using NitroxPatcher.PatternMatching;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using static System.Reflection.Emit.OpCodes;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Synchronizes entities that can be broken and that will drop material, such as limestones...
/// </summary>
public sealed partial class BreakableResource_SpawnResourceFromPrefab_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method(() => BreakableResource.SpawnResourceFromPrefab(default, default, default)));
private static readonly InstructionsPattern SpawnResFromPrefPattern = new()
{
{ Reflect.Method((Rigidbody b) => b.AddForce(default(Vector3))), "DropItemInstance" },
Ldc_I4_0
};
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
return instructions.InsertAfterMarker(SpawnResFromPrefPattern, "DropItemInstance", new CodeInstruction[]
{
new(Ldloc_1),
new(Call, ((Action<GameObject>)Callback).Method)
});
}
private static void Callback(GameObject __instance)
{
NitroxEntity.SetNewId(__instance, new());
Resolve<Items>().Dropped(__instance);
}
}

View File

@@ -0,0 +1,32 @@
using System.Reflection;
using NitroxClient.GameLogic.Bases;
using NitroxClient.Helpers;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class BuilderTool_Construct_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((BuilderTool t) => t.Construct(default, default, default));
public static bool Prefix(Constructable c)
{
if (!BuildingHandler.Main || !c.tr.parent || !c.tr.parent.TryGetNitroxId(out NitroxId parentId))
{
return true;
}
bool isAllowed = true;
string message = string.Empty;
Constructable_DeconstructionAllowed_Patch.DeconstructionAllowed(parentId, ref isAllowed, ref message);
if (!isAllowed)
{
Log.InGame(message);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,106 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic.Spawning.Bases;
using NitroxClient.MonoBehaviours;
using NitroxClient.MonoBehaviours.Cyclops;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxPatcher.PatternMatching;
using UnityEngine;
using static System.Reflection.Emit.OpCodes;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Builder_TryPlace_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => Builder.TryPlace());
public static readonly InstructionsPattern AddInstructionPattern1 = new()
{
Ldloc_0,
Ldc_I4_0,
Ldc_I4_1,
new() { OpCode = Callvirt, Operand = new(nameof(Constructable), nameof(Constructable.SetState)) },
{ Pop, "Insert1" }
};
public static readonly List<CodeInstruction> InstructionsToAdd1 = new()
{
new(Ldloc_0),
new(Call, Reflect.Method(() => GhostCreated(default)))
};
public static readonly InstructionsPattern AddInstructionPattern2 = new()
{
Ldloc_S,
Ldloc_3,
Ldloc_S,
Or,
{ new() { OpCode = Callvirt, Operand = new(nameof(Constructable), nameof(Constructable.SetIsInside)) }, "Insert2" }
};
public static readonly List<CodeInstruction> InstructionsToAdd2 = new()
{
TARGET_METHOD.Ldloc<Constructable>(),
new(Call, Reflect.Method(() => GhostCreated(default)))
};
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
instructions.Transform(AddInstructionPattern1, (label, instruction) =>
{
if (label.Equals("Insert1"))
{
return InstructionsToAdd1;
}
return null;
}).Transform(AddInstructionPattern2, (label, instruction) =>
{
if (label.Equals("Insert2"))
{
return InstructionsToAdd2;
}
return null;
});
public static void GhostCreated(Constructable constructable)
{
GameObject ghostObject = constructable.gameObject;
NitroxId parentId = null;
if (ghostObject.TryGetComponentInParent(out SubRoot subRoot, true) && (subRoot.isBase || subRoot.isCyclops) &&
subRoot.TryGetNitroxId(out NitroxId entityId))
{
parentId = entityId;
}
// Assign a NitroxId to the ghost now
NitroxId ghostId = new();
NitroxEntity.SetNewId(ghostObject, ghostId);
if (constructable is ConstructableBase constructableBase)
{
GhostEntity ghost = GhostEntitySpawner.From(constructableBase);
ghost.Id = ghostId;
ghost.ParentId = parentId;
Resolve<IPacketSender>().Send(new PlaceGhost(ghost));
}
else
{
ModuleEntitySpawner.MoveToGlobalRoot(ghostObject);
ModuleEntity module = ModuleEntitySpawner.From(constructable);
module.Id = ghostId;
module.ParentId = parentId;
Resolve<IPacketSender>().Send(new PlaceModule(module));
if (constructable.transform.parent && constructable.transform.parent.TryGetComponent(out NitroxCyclops nitroxCyclops) && nitroxCyclops.Virtual)
{
nitroxCyclops.Virtual.ReplicateConstructable(constructable);
}
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Reflection;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Settings;
using NitroxClient.Helpers;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Changes the piece color during the placing process if the current base is desynced.
/// </summary>
public sealed partial class Builder_Update_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => Builder.Update());
private static readonly Color DESYNCED_COLOR = Color.magenta;
public static void Postfix()
{
if (!Builder.canPlace || !BuildingHandler.Main || !NitroxPrefs.SafeBuilding.Value)
{
return;
}
BaseGhost baseGhost = Builder.ghostModel.GetComponent<BaseGhost>();
GameObject parentBase;
if (baseGhost && baseGhost.targetBase)
{
parentBase = baseGhost.targetBase.gameObject;
}
// In case it's a simple Constructable
else
{
parentBase = Builder.placementTarget;
}
if (parentBase && parentBase.TryGetNitroxId(out NitroxId parentId) &&
BuildingHandler.Main.EnsureTracker(parentId).IsDesynced())
{
Builder.canPlace = false;
Builder.ghostStructureMaterial.SetColor(ShaderPropertyID._Tint, DESYNCED_COLOR);
}
}
}

View File

@@ -0,0 +1,65 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Replaces local use of <see cref="Time.deltaTime"/> by <see cref="TimeManager.DeltaTime"/> and prevents remote bullets from detecting collisions
/// </summary>
public sealed partial class Bullet_Update_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((Bullet t) => t.Update());
/*
* RaycastHit raycastHit;
* REPLACE:
* if (Physics.SphereCast(this.tr.position, this.shellRadius, this.tr.forward, out raycastHit, num, this.layerMask.value))
* {
* num = raycastHit.distance;
* BY:
* if (!Bullet_Update_Patch.IsRemoteObject(this) && Physics.SphereCast(this.tr.position, this.shellRadius, this.tr.forward, out raycastHit, num, this.layerMask.value))
* {
* num = raycastHit.distance;
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
Label label = generator.DefineLabel();
// Replace the two occurences of Time.deltaTime
return new CodeMatcher(instructions).ReplaceDeltaTime()
.ReplaceDeltaTime()
.MatchStartForward([
new CodeMatch(OpCodes.Ldarg_0),
new CodeMatch(OpCodes.Call, Reflect.Property((Bullet t) => t.tr).GetGetMethod()),
new CodeMatch(OpCodes.Callvirt, Reflect.Property((Transform t) => t.position).GetGetMethod()),
new CodeMatch(OpCodes.Ldarg_0),
])
// Skip the Ldarg_0 because it is the previous ifs' jump target
.Advance(1)
// Insert if (!Bullet_Update_Patch.IsRemoteObject(this)) before the condition
.InsertAndAdvance([
new CodeInstruction(OpCodes.Call, Reflect.Method(() => IsRemoteObject(default))),
new CodeInstruction(OpCodes.Brtrue_S, label),
new CodeInstruction(OpCodes.Ldarg_0),
])
// Find the destination of the position to go to
.MatchStartForward([
new CodeMatch(OpCodes.Ldarg_0),
new CodeMatch(OpCodes.Call, Reflect.Property((Bullet t) => t.tr).GetGetMethod()),
new CodeMatch(OpCodes.Dup),
new CodeMatch(OpCodes.Callvirt, Reflect.Property((Transform t) => t.position).GetGetMethod())
])
.AddLabels([label])
.InstructionEnumeration();
}
public static bool IsRemoteObject(Bullet bullet)
{
return bullet.GetComponent<BulletManager.RemotePlayerBullet>();
}
}

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxPatcher.PatternMatching;
using UnityEngine;
using static System.Reflection.Emit.OpCodes;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class ConstructableBase_SetState_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((ConstructableBase t) => t.SetState(default, default));
/*
* Make it become
* if (Builder.CanDestroyObject(gameObject))
* {
* ConstructableBase_SetState_Patch.BeforeDestroy(gameObject); <==========
* UnityEngine.Object.Destroy(gameObject);
* }
*/
public static readonly InstructionsPattern InstructionPattern = new()
{
new() { OpCode = Call, Operand = new(nameof(Builder), nameof(Builder.CanDestroyObject)) },
{ Brfalse, "Insert" }
};
public static readonly List<CodeInstruction> InstructionsToAdd = new()
{
TARGET_METHOD.Ldloc<GameObject>(),
new(Call, Reflect.Method(() => BeforeDestroy(default)))
};
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
instructions.Transform(InstructionPattern, (label, instruction) =>
{
if (label.Equals("Insert"))
{
return InstructionsToAdd;
}
return null;
});
public static void BeforeDestroy(GameObject gameObject)
{
if (gameObject && gameObject.TryGetNitroxId(out NitroxId nitroxId))
{
Resolve<IPacketSender>().Send(new EntityDestroyed(nitroxId));
}
}
}

View File

@@ -0,0 +1,203 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Spawning.Bases;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.Helpers;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Bases;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxPatcher.PatternMatching;
using UnityEngine;
using UWE;
using static System.Reflection.Emit.OpCodes;
using static NitroxClient.GameLogic.Bases.BuildingHandler;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Constructable_Construct_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Constructable t) => t.Construct());
private static TemporaryBuildData Temp => BuildingHandler.Main.Temp;
public static readonly InstructionsPattern InstructionsPattern = new()
{
Div,
Stfld,
Ldc_I4_0,
Ret,
Ldarg_0,
{ InstructionPattern.Call(nameof(Constructable), nameof(Constructable.UpdateMaterial)), "Insert" }
};
public static readonly List<CodeInstruction> InstructionsToAdd = new()
{
new(Ldarg_0),
new(Ldc_I4_1), // True for "constructing"
new(Call, Reflect.Method(() => ConstructionAmountModified(default, default)))
};
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
instructions.Transform(InstructionsPattern, (label, instruction) =>
{
if (label.Equals("Insert"))
{
return InstructionsToAdd;
}
return null;
});
public static void ConstructionAmountModified(Constructable constructable, bool constructing)
{
// We only manage the amount change, not the deconstruction/construction action
if (!constructable.TryGetNitroxId(out NitroxId entityId))
{
Log.ErrorOnce($"[{nameof(ConstructionAmountModified)}] Couldn't find a NitroxEntity on {constructable.name}");
return;
}
float amount = NitroxModel.Helper.Mathf.Clamp01(constructable.constructedAmount);
// An object is destroyed when amount = 0 AND if we are destructing
// so we don't need the broadcast if we are trying to construct with not enough resources (amount = 0)
if (amount == 0f && constructing)
{
return;
}
/*
* Different cases:
* - Normal module (only Constructable), just let it go to 1.0f normally
* - ConstructableBase:
* - if it's already in a base, simply update the current base
* - else, create a new base
*/
if (amount == 1f)
{
Resolve<ThrottledPacketSender>().RemovePendingPackets(entityId);
if (constructable is ConstructableBase constructableBase)
{
CoroutineHost.StartCoroutine(BroadcastObjectBuilt(constructableBase, entityId));
return;
}
IEnumerator postSpawner = BuildingPostSpawner.ApplyPostSpawner(constructable.gameObject, entityId);
// Can be null if no post spawner is set for the constructable's techtype
if (postSpawner != null)
{
CoroutineHost.StartCoroutine(postSpawner);
}
// To avoid any unrequired throttled packet to be sent we clean the pending throttled packets for this object
Resolve<IPacketSender>().Send(new ModifyConstructedAmount(entityId, 1f));
return;
}
// update as a normal module
Resolve<ThrottledPacketSender>().SendThrottled(new ModifyConstructedAmount(entityId, amount),
(packet) => { return packet.GhostId; }, 0.1f);
}
public static IEnumerator BroadcastObjectBuilt(ConstructableBase constructableBase, NitroxId entityId)
{
BaseGhost baseGhost = constructableBase.model.GetComponent<BaseGhost>();
constructableBase.SetState(true, true);
if (!baseGhost.targetBase)
{
Log.Error("Something wrong happened, couldn't find base after finishing building ghost");
yield break;
}
Base targetBase = baseGhost.targetBase;
Base parentBase = null;
if (constructableBase.tr.parent)
{
parentBase = constructableBase.GetComponentInParent<Base>(true);
}
// If a module was spawned we need to transfer the ghost id to it for further recognition
BuildUtils.TryTransferIdFromGhostToModule(baseGhost, entityId, constructableBase, out GameObject moduleObject);
// Have a delay for baseGhost to be actually destroyed
yield return null;
if (parentBase)
{
// update existing base
if (!parentBase.TryGetNitroxId(out NitroxId parentId))
{
BuildingHandler.Main.FailedOperations++;
Log.Error($"[{nameof(BroadcastObjectBuilt)}] Parent base doesn't have a NitroxEntity, which is not normal");
yield break;
}
MoonpoolManager moonpoolManager = parentBase.gameObject.EnsureComponent<MoonpoolManager>();
GlobalRootEntity builtPiece = null;
if (moduleObject)
{
if (moduleObject.TryGetComponent(out IBaseModule builtModule))
{
builtPiece = InteriorPieceEntitySpawner.From(builtModule, Resolve<EntityMetadataManager>());
}
else if (moduleObject.GetComponent<VehicleDockingBay>())
{
builtPiece = moonpoolManager.LatestRegisteredMoonpool;
}
else if (moduleObject.TryGetComponent(out MapRoomFunctionality mapRoomFunctionality))
{
builtPiece = BuildUtils.CreateMapRoomEntityFrom(mapRoomFunctionality, parentBase, entityId, parentId);
}
}
SendUpdateBase(parentBase, parentId, entityId, builtPiece, moonpoolManager);
}
else
{
// Must happen before NitroxEntity.SetNewId because else, if a moonpool was marked with the same id, this id be will unlinked from the base object
if (baseGhost.targetBase.TryGetComponent(out MoonpoolManager moonpoolManager))
{
moonpoolManager.LateAssignNitroxEntity(entityId);
moonpoolManager.OnPostRebuildGeometry(baseGhost.targetBase);
}
// create a new base
NitroxEntity.SetNewId(baseGhost.targetBase.gameObject, entityId);
BuildingHandler.Main.EnsureTracker(entityId).ResetToId();
Resolve<IPacketSender>().Send(new PlaceBase(entityId, BuildEntitySpawner.From(targetBase, Resolve<EntityMetadataManager>())));
}
if (moduleObject)
{
yield return BuildingPostSpawner.ApplyPostSpawner(moduleObject, entityId);
}
Temp.Dispose();
}
private static void SendUpdateBase(Base @base, NitroxId baseId, NitroxId pieceId, GlobalRootEntity builtPieceEntity, MoonpoolManager moonpoolManager)
{
List<Entity> childEntities = BuildUtils.GetChildEntities(@base, baseId, Resolve<EntityMetadataManager>());
// We get InteriorPieceEntity children from the base and make up a dictionary with their updated data (their BaseFace)
Dictionary<NitroxId, NitroxBaseFace> updatedChildren = childEntities.OfType<InteriorPieceEntity>()
.ToDictionary(entity => entity.Id, entity => entity.BaseFace);
// Same for MapRooms
Dictionary<NitroxId, NitroxInt3> updatedMapRooms = childEntities.OfType<MapRoomEntity>()
.ToDictionary(entity => entity.Id, entity => entity.Cell);
BuildingHandler.Main.EnsureTracker(baseId).LocalOperations++;
int operationId = BuildingHandler.Main.GetCurrentOperationIdOrDefault(baseId);
UpdateBase updateBase = new(baseId, pieceId, BuildEntitySpawner.GetBaseData(@base), builtPieceEntity, updatedChildren, moonpoolManager.GetMoonpoolsUpdate(), updatedMapRooms, Temp.ChildrenTransfer, operationId);
// TODO: (for server-side) Find a way to optimize this (maybe by copying BaseGhost.Finish() => Base.CopyFrom)
Resolve<IPacketSender>().Send(updateBase);
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using NitroxModel.Helper;
using NitroxPatcher.PatternMatching;
using static System.Reflection.Emit.OpCodes;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Constructable_DeconstructAsync_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((Constructable t) => t.DeconstructAsync(default, default)));
public static readonly InstructionsPattern InstructionsPattern = new()
{
Ldc_I4_0,
Ret,
Ldloc_1,
{ InstructionPattern.Call(nameof(Constructable), nameof(Constructable.UpdateMaterial)), "InsertDestruction" }
};
public static readonly List<CodeInstruction> InstructionsToAdd = new()
{
new(Ldloc_1),
new(Ldc_I4_0), // False for "constructing"
new(Call, Reflect.Method(() => Constructable_Construct_Patch.ConstructionAmountModified(default, default)))
};
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
instructions.Transform(InstructionsPattern, (label, instruction) =>
{
if (label.Equals("InsertDestruction"))
{
return InstructionsToAdd;
}
return null;
});
}

View File

@@ -0,0 +1,40 @@
using System.Reflection;
using NitroxClient.GameLogic.Bases;
using NitroxClient.GameLogic.Settings;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents deconstruction if the target base is desynced.
/// </summary>
public sealed partial class Constructable_DeconstructionAllowed_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Constructable t) => t.DeconstructionAllowed(out Reflect.Ref<string>.Field));
public static void Postfix(Constructable __instance, ref bool __result, ref string reason)
{
if (!__result || !BuildingHandler.Main || !__instance.TryGetComponentInParent(out NitroxEntity parentEntity, true))
{
return;
}
DeconstructionAllowed(parentEntity.Id, ref __result, ref reason);
}
public static void DeconstructionAllowed(NitroxId baseId, ref bool __result, ref string reason)
{
if (BuildingHandler.Main.BasesCooldown.ContainsKey(baseId))
{
__result = false;
reason = Language.main.Get("Nitrox_ErrorRecentBuildUpdate");
}
else if (BuildingHandler.Main.EnsureTracker(baseId).IsDesynced() && NitroxPrefs.SafeBuilding.Value)
{
__result = false;
reason = Language.main.Get("Nitrox_ErrorDesyncDetected");
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Reflection;
using NitroxClient.MonoBehaviours.Cyclops;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Unregisters constructables from virtual cyclops when they're fully deconstructed.
/// </summary>
public sealed partial class Constructable_ProgressDeconstruction_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((Constructable t) => t.ProgressDeconstruction());
public static void Prefix(Constructable __instance)
{
if (__instance.constructedAmount <= 0f && __instance.transform.parent &&
__instance.transform.parent.TryGetComponent(out NitroxCyclops nitroxCyclops) && nitroxCyclops.Virtual)
{
nitroxCyclops.Virtual.UnregisterConstructable(__instance.gameObject);
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic
{
public sealed partial class ConstructorInput_OnCraftingBegin_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((ConstructorInput t) => t.OnCraftingBeginAsync(default(TechType), default(float))));
public static readonly OpCode INJECTION_OPCODE = OpCodes.Call;
public static readonly object INJECTION_OPERAND = Reflect.Method(() => CrafterLogic.NotifyCraftEnd(default(GameObject), default(TechType)));
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
Validate.NotNull(INJECTION_OPERAND);
foreach (CodeInstruction instruction in instructions)
{
yield return instruction;
if (instruction.opcode.Equals(INJECTION_OPCODE) && instruction.operand.Equals(INJECTION_OPERAND))
{
/*
* Callback(constructor, gameObject, techType, duration);
*/
yield return original.Ldloc<ConstructorInput>(0);
yield return original.Ldloc<GameObject>(0);
yield return new CodeInstruction(OpCodes.Ldarg_0);
yield return new CodeInstruction(OpCodes.Ldfld, TARGET_METHOD.DeclaringType.GetField("techType", BindingFlags.Instance | BindingFlags.Public));
yield return new CodeInstruction(OpCodes.Ldarg_0);
yield return new CodeInstruction(OpCodes.Ldfld, TARGET_METHOD.DeclaringType.GetField("duration", BindingFlags.Instance | BindingFlags.Public));
yield return new CodeInstruction(OpCodes.Call, ((Action<ConstructorInput, GameObject, TechType, float>)Callback).Method);
}
}
}
public static void Callback(ConstructorInput constructor, GameObject constructedObject, TechType techType, float duration)
{
Resolve<MobileVehicleBay>().BeginCrafting(constructor, constructedObject, techType, duration);
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Constructor_Deploy_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Constructor t) => t.Deploy(default(bool)));
public static void Prefix(Constructor __instance, bool value)
{
// only trigger updates when there is a valid state change.
if (value != __instance.deployed)
{
// We need to set this early so that the extracted metadata has the right value for "deployed"
__instance.deployed = value;
if (__instance.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Entities>().EntityMetadataChanged(__instance, id);
}
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/**
* When a player is finished crafting an item, we need to let the server know we spawned the items. We also
* let other players know to close out the crafter and consider it empty.
*/
public sealed partial class CrafterLogic_TryPickupSingleAsync_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((CrafterLogic t) => t.TryPickupSingleAsync(default(TechType), default(IOut<bool>))));
public static readonly OpCode INJECTION_OPCODE = OpCodes.Call;
public static readonly object INJECTION_OPERAND = Reflect.Method(() => CrafterLogic.NotifyCraftEnd(default(GameObject), default(TechType)));
private static readonly MethodInfo COMPONENT_GAMEOBJECT_GETTER = Reflect.Property((Component t) => t.gameObject).GetMethod;
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
foreach (CodeInstruction instruction in instructions)
{
yield return instruction;
if (instruction.opcode.Equals(INJECTION_OPCODE) && instruction.operand.Equals(INJECTION_OPERAND))
{
/*
* Injects: Callback(this.gameObject, gameObject);
*/
yield return original.Ldloc<CrafterLogic>();
yield return new CodeInstruction(OpCodes.Callvirt, COMPONENT_GAMEOBJECT_GETTER);
yield return new CodeInstruction(OpCodes.Ldloc_S, (byte)5);
yield return new CodeInstruction(OpCodes.Call, ((Action<GameObject, GameObject>)Callback).Method);
}
}
}
public static void Callback(GameObject crafter, GameObject item)
{
if (crafter.TryGetIdOrWarn(out NitroxId crafterId))
{
// Tell the other players to consider this crafter to no longer contain a tech type.
Resolve<Entities>().BroadcastMetadataUpdate(crafterId, new CrafterMetadata(null, DayNightCycle.main.timePassedAsFloat, 0));
}
// The Pickup() item codepath will inform the server that the item was added to the inventory.
}
}

View File

@@ -0,0 +1,25 @@
using System.Reflection;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CrashHome_OnDestroy_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CrashHome t) => t.OnDestroy());
public static void Prefix(CrashHome __instance)
{
if (!__instance.TryGetNitroxId(out NitroxId crashHomeId) ||
!Resolve<SimulationOwnership>().HasAnyLockType(crashHomeId) ||
!__instance.crash ||
!__instance.crash.TryGetNitroxId(out NitroxId crashId))
{
return;
}
Resolve<IPacketSender>().Send(new EntityDestroyed(crashId));
}
}

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CrashHome_Spawn_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((CrashHome t) => t.Spawn());
public static bool Prefix(CrashHome __instance)
{
if (__instance.TryGetNitroxId(out NitroxId crashHomeId) &&
Resolve<SimulationOwnership>().HasAnyLockType(crashHomeId))
{
return true;
}
return false;
}
/*
* this.spawnTime = -1f;
* BroadcastFishCreated(gameObject); [INSERTED LINE]
* if (LargeWorldStreamer.main != null)
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).MatchStartForward(new CodeMatch(OpCodes.Stfld, Reflect.Field((CrashHome t) => t.spawnTime)))
.Advance(1)
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_0))
.InsertAndAdvance(new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastFishCreated(default))))
.InstructionEnumeration();
}
public static void BroadcastFishCreated(GameObject crashFishObject)
{
if (!crashFishObject.TryGetComponentInParent(out CrashHome crashHome, true) ||
!crashHome.TryGetNitroxId(out NitroxId crashHomeId) || !DayNightCycle.main)
{
return;
}
NitroxId crashFishId = NitroxEntity.GenerateNewId(crashFishObject);
LargeWorldEntity largeWorldEntity = crashFishObject.GetComponent<LargeWorldEntity>();
UniqueIdentifier uniqueIdentifier = crashFishObject.GetComponent<UniqueIdentifier>();
// Broadcast the new CrashHome's metadata (spawnTime = -1)
Optional<EntityMetadata> metadata = Resolve<EntityMetadataManager>().Extract(crashHome);
if (metadata.HasValue)
{
Resolve<Entities>().BroadcastMetadataUpdate(crashHomeId, metadata.Value);
}
// Create the entity
WorldEntity crashFishEntity = new(crashFishObject.transform.ToWorldDto(), (int)largeWorldEntity.cellLevel, uniqueIdentifier.classId, false, crashFishId, TechType.Crash.ToDto(), null, crashHomeId, new List<Entity>());
Resolve<Entities>().BroadcastEntitySpawnedByClient(crashFishEntity);
}
}

View File

@@ -0,0 +1,18 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// We don't want <see cref="CrashHome.Start"/> accidentally spawning a Crash so we prevent it from happening.
/// Instead, the spawning functionality will happen in <see cref="CrashHome.Update"/>
/// </summary>
public sealed partial class CrashHome_Start_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((CrashHome t) => t.Start());
public static bool Prefix()
{
return false;
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CrashHome_Update_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((CrashHome t) => t.Update());
/*
* if (!this.crash && this.spawnTime < 0f)
* {
* this.spawnTime = (float)(main.timePassed + 1200.0); [REMOVED LINE]
* UpdateSpawnTimeAndBroadcast(this); [INSERTED LINE]
* }
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).MatchEndForward([
new CodeMatch(OpCodes.Ldfld),
new CodeMatch(OpCodes.Ldc_R4),
new CodeMatch(OpCodes.Bge_Un)
])
.Advance(1)
.RemoveInstructions(7)
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_0))
.InsertAndAdvance(new CodeInstruction(OpCodes.Call, Reflect.Method(() => UpdateSpawnTimeAndBroadcast(default))))
.InstructionEnumeration();
}
public static void UpdateSpawnTimeAndBroadcast(CrashHome crashHome)
{
// We udpate and broadcast the spawn time only if we're simulating the home
if (!crashHome.TryGetNitroxId(out NitroxId crashHomeId) ||
!Resolve<SimulationOwnership>().HasAnyLockType(crashHomeId))
{
return;
}
crashHome.spawnTime = DayNightCycle.main.timePassedAsFloat + (float)CrashHome.respawnDelay;
// Set spawn time before broadcast the new CrashHome's metadata
Optional<EntityMetadata> metadata = Resolve<EntityMetadataManager>().Extract(crashHome);
if (metadata.HasValue)
{
Resolve<Entities>().BroadcastMetadataUpdate(crashHomeId, metadata.Value);
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using System.Reflection;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
/// <remarks>
/// We just want to disable all these commands on client-side and redirect them as ConsoleCommand
/// TODO: Remove this file when we'll have the command system
/// </remarks>
public sealed class CrashedShipExploder_OnConsoleCommand_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD_COUNTDOWNSHIP = Reflect.Method((CrashedShipExploder t) => t.OnConsoleCommand_countdownship());
private static readonly MethodInfo TARGET_METHOD_EXPLODEFORCE = Reflect.Method((CrashedShipExploder t) => t.OnConsoleCommand_explodeforce());
private static readonly MethodInfo TARGET_METHOD_EXPLODESHIP = Reflect.Method((CrashedShipExploder t) => t.OnConsoleCommand_explodeship());
private static readonly MethodInfo TARGET_METHOD_RESTORESHIP = Reflect.Method((CrashedShipExploder t) => t.OnConsoleCommand_restoreship());
public static bool PrefixCountdownShip()
{
Resolve<IPacketSender>().Send(new ServerCommand("aurora countdown"));
return false;
}
// This command's purpose is just to show FX, we don't need to sync it
public static bool PrefixExplodeForce()
{
return true;
}
public static bool PrefixExplodeShip()
{
Resolve<IPacketSender>().Send(new ServerCommand("aurora explode"));
return false;
}
public static bool PrefixRestoreShip()
{
Resolve<IPacketSender>().Send(new ServerCommand("aurora restore"));
return false;
}
public override void Patch(Harmony harmony)
{
PatchPrefix(harmony, TARGET_METHOD_COUNTDOWNSHIP, ((Func<bool>)PrefixCountdownShip).Method);
PatchPrefix(harmony, TARGET_METHOD_EXPLODEFORCE, ((Func<bool>)PrefixExplodeForce).Method);
PatchPrefix(harmony, TARGET_METHOD_EXPLODESHIP, ((Func<bool>)PrefixExplodeShip).Method);
PatchPrefix(harmony, TARGET_METHOD_RESTORESHIP, ((Func<bool>)PrefixRestoreShip).Method);
}
}

View File

@@ -0,0 +1,21 @@
using System.Reflection;
using NitroxClient.MonoBehaviours;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <remarks>
/// Prevents <see cref="CrashedShipExploder.Update"/> from occurring before initial sync has completed.
/// It lets us avoid a very weird edge case in which SetExplodeTime happens before server time is set on the client,
/// after what some event in this Update method might be triggered because there's a dead frame before the StoryGoalInitialSyncProcessor step
/// which sets up all the aurora story-related stuff locally.
/// </remarks>
public sealed partial class CrashedShipExploder_Update_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CrashedShipExploder t) => t.Update());
public static bool Prefix()
{
return Multiplayer.Main && Multiplayer.Main.InitialSyncCompleted;
}
}

View File

@@ -0,0 +1,25 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents <see cref="CreatureAction.Perform"/> from happening if local player doesn't have lock on creature
/// </summary>
public sealed partial class CreatureAction_Perform_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CreatureAction t) => t.Perform(default, default, default));
public static bool Prefix(CreatureAction __instance)
{
if (!__instance.TryGetNitroxId(out NitroxId id) || Resolve<SimulationOwnership>().HasAnyLockType(id))
{
return true;
}
// Perform is too specific for each action so it should always be synced case by case (and never run directly on remote players)
return false;
}
}

View File

@@ -0,0 +1,24 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents <see cref="CreatureAction.StartPerform"/> from happening if local player doesn't have lock on creature or if the action is not whitelisted
/// </summary>
public sealed partial class CreatureAction_StartPerform_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CreatureAction t) => t.StartPerform(default, default));
public static bool Prefix(CreatureAction __instance)
{
if (!__instance.TryGetNitroxId(out NitroxId id) || Resolve<SimulationOwnership>().HasAnyLockType(id))
{
return true;
}
return Resolve<AI>().IsCreatureActionWhitelisted(__instance);
}
}

View File

@@ -0,0 +1,14 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents <see cref="CreatureAction.StopPerform"/> from happening if local player doesn't have lock on creature or if the action is not whitelisted
/// </summary>
public sealed partial class CreatureAction_StopPerform_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CreatureAction t) => t.StopPerform(default, default));
public static bool Prefix(CreatureAction __instance) => CreatureAction_StartPerform_Patch.Prefix(__instance);
}

View File

@@ -0,0 +1,24 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents <see cref="CreatureDeath.OnAttackByCreature"/> from happening on non-simulated entities
/// </summary>
public sealed partial class CreatureDeath_OnAttackByCreature_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CreatureDeath t) => t.OnAttackByCreature());
public static bool Prefix(CreatureDeath __instance)
{
if (__instance.TryGetNitroxId(out NitroxId creatureId) &&
Resolve<SimulationOwnership>().HasAnyLockType(creatureId))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,114 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Broadcasts creature death consequences:<br/>
/// - Converted to a cooked item<br/>
/// - Dead but still has its corpse floating in the water<br/>
/// - Eatable decomposition metadata
/// </summary>
public sealed partial class CreatureDeath_OnKillAsync_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((CreatureDeath t) => t.OnKillAsync()));
/*
* 1st injection:
* gameObject.GetComponent<Rigidbody>().angularDrag = base.gameObject.GetComponent<Rigidbody>().angularDrag * 3f;
* UnityEngine.Object.Destroy(base.gameObject);
* result = null;
* CreatureDeath_OnKillAsync_Patch.BroadcastCookedSpawned(this, gameObject, cookedData); <---- INSERTED LINE
*
* 2nd injection:
* base.Invoke("RemoveCorpse", this.removeCorpseAfterSeconds);
* CreatureDeath_OnKillAsync_Patch.BroadcastRemoveCorpse(this); <---- INSERTED LINE
*
* 3rd injection:
* this.eatable.SetDecomposes(true);
* CreatureDeath_OnKillAsync_Patch.BroadcastCookedSpawned(this.eatable); <---- INSERTED LINE
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
// First injection
return new CodeMatcher(instructions).MatchEndForward([
new CodeMatch(OpCodes.Ldarg_0),
new CodeMatch(OpCodes.Ldnull),
new CodeMatch(OpCodes.Stfld),
new CodeMatch(OpCodes.Br),
])
.Advance(1)
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_0))
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_2))
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_3))
.Insert(new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastCookedSpawned(default, default, default))))
// Second injection
.MatchEndForward([
new CodeMatch(OpCodes.Ldloc_1),
new CodeMatch(OpCodes.Ldfld, Reflect.Field((CreatureDeath t) => t.removeCorpseAfterSeconds)),
new CodeMatch(OpCodes.Call),
])
.Advance(1)
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_0))
.Insert(new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastRemoveCorpse(default))))
// Third injection
.MatchEndForward([
new CodeMatch(OpCodes.Ldloc_1),
new CodeMatch(OpCodes.Ldfld, Reflect.Field((CreatureDeath t) => t.eatable)),
new CodeMatch(OpCodes.Ldc_I4_1),
new CodeMatch(OpCodes.Callvirt),
])
.Advance(1)
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_1))
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldfld, Reflect.Field((CreatureDeath t) => t.eatable)))
.Insert(new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastEatableMetadata(default))))
.InstructionEnumeration();
}
public static void BroadcastCookedSpawned(CreatureDeath creatureDeath, GameObject gameObject, TechType cookedTechType)
{
if (creatureDeath.TryGetNitroxId(out NitroxId creatureId))
{
NitroxEntity.SetNewId(gameObject, creatureId);
}
Resolve<Items>().Dropped(gameObject, cookedTechType);
}
public static void BroadcastRemoveCorpse(CreatureDeath creatureDeath)
{
if (creatureDeath.TryGetNitroxId(out NitroxId creatureId))
{
Resolve<SimulationOwnership>().StopSimulatingEntity(creatureId);
EntityPositionBroadcaster.RemoveEntityMovementControl(creatureDeath.gameObject, creatureId);
Resolve<IPacketSender>().Send(new RemoveCreatureCorpse(creatureId, creatureDeath.transform.localPosition.ToDto(), creatureDeath.transform.localRotation.ToDto()));
}
}
public static void BroadcastEatableMetadata(Eatable eatable)
{
if (!eatable.TryGetNitroxId(out NitroxId eatableId))
{
return;
}
Optional<EntityMetadata> metadata = Resolve<EntityMetadataManager>().Extract(eatable);
if (metadata.HasValue)
{
Resolve<Entities>().BroadcastMetadataUpdate(eatableId, metadata.Value);
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents <see cref="CreatureDeath.OnKill"/> from happening on non-simulated entities
/// </summary>
public sealed partial class CreatureDeath_OnKill_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CreatureDeath t) => t.OnKill());
public static bool Prefix(CreatureDeath __instance)
{
if (__instance.TryGetNitroxId(out NitroxId creatureId) &&
Resolve<SimulationOwnership>().HasAnyLockType(creatureId))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,24 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents <see cref="CreatureDeath.OnPickedUp"/> from happening on non-simulated entities
/// </summary>
public sealed partial class CreatureDeath_OnPickedUp_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CreatureDeath t) => t.OnPickedUp(default));
public static bool Prefix(CreatureDeath __instance)
{
if (__instance.TryGetNitroxId(out NitroxId creatureId) &&
Resolve<SimulationOwnership>().HasAnyLockType(creatureId))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CreatureDeath_SpawnRespawner_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((CreatureDeath t) => t.SpawnRespawner());
public static bool Prefix(CreatureDeath __instance)
{
if (__instance.TryGetNitroxId(out NitroxId creatureId) &&
Resolve<SimulationOwnership>().HasAnyLockType(creatureId))
{
return true;
}
return false;
}
/*
* this.hasSpawnedRespawner = true;
* CreatureDeath_SpawnRespawner_Patch.BroadcastRespawnSpawned(this); [INSERTED LINE]
*/
public static IEnumerable<CodeInstruction> Transpiler(MethodBase methodBase, IEnumerable<CodeInstruction> instructions)
{
// We add instructions right before the ret which is equivalent to inserting at last offset
return new CodeMatcher(instructions).End()
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldloc_3))
.Insert(new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastSpawnedRespawner(default))))
.InstructionEnumeration();
}
public static void BroadcastSpawnedRespawner(Respawn respawn)
{
int cellLevel = respawn.TryGetComponent(out LargeWorldEntity largeWorldEntity) ? (int)largeWorldEntity.cellLevel : 0;
string classId = respawn.GetComponent<UniqueIdentifier>().ClassId;
NitroxId respawnId = NitroxEntity.GenerateNewId(respawn.gameObject);
NitroxId parentId = null;
if (respawn.transform.parent)
{
respawn.transform.parent.TryGetNitroxId(out parentId);
}
CreatureRespawnEntity creatureSpawner = new(respawn.transform.ToWorldDto(), cellLevel, classId, false, respawnId, NitroxTechType.None, null, parentId, [],
respawn.spawnTime, respawn.techType.ToDto(), respawn.addComponents);
Resolve<Entities>().BroadcastEntitySpawnedByClient(creatureSpawner, true);
// We don't need this object as the respawner only works when we load its cell
// and it won't activate right now so we'll just delete the entity locally
GameObject.Destroy(respawn.gameObject);
}
}

View File

@@ -0,0 +1,39 @@
using System.Reflection;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Syncs egg deletion when hatching.
/// </summary>
public sealed partial class CreatureEgg_Hatch_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CreatureEgg t) => t.Hatch());
public static void Prefix(CreatureEgg __instance)
{
// This case always destroys the creature egg (see original code)
if (!__instance.TryGetComponent(out WaterParkItem waterParkItem))
{
return;
}
// We don't manage eggs with no id here
if (!__instance.TryGetNitroxId(out NitroxId eggId))
{
return;
}
// It is VERY IMPORTANT to check for simulation ownership on the water park and not on the egg
// since that's the convention we chose
if (waterParkItem.currentWaterPark.TryGetNitroxId(out NitroxId waterParkId) &&
Resolve<SimulationOwnership>().HasAnyLockType(waterParkId))
{
Resolve<IPacketSender>().Send(new EntityDestroyed(eggId));
}
}
}

View File

@@ -0,0 +1,43 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// For players without lock: apply remote creature actions and prevent the original call.
/// For players with lock: broadcast new creature actions
/// </summary>
public sealed partial class Creature_ChooseBestAction_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Creature t) => t.ChooseBestAction(default));
public static bool Prefix(Creature __instance, out NitroxId __state, ref CreatureAction __result)
{
if (!__instance.TryGetIdOrWarn(out __state) || Resolve<SimulationOwnership>().HasAnyLockType(__state))
{
return true;
}
// If we have received any order
if (Resolve<AI>().TryGetActionForCreature(__instance, out CreatureAction action))
{
__result = action;
}
return false;
}
public static void Postfix(Creature __instance, bool __runOriginal, NitroxId __state, ref CreatureAction __result)
{
if (!__runOriginal || __state == null)
{
return;
}
if (Resolve<SimulationOwnership>().HasAnyLockType(__state))
{
Resolve<AI>().BroadcastNewAction(__state, __instance, __result);
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// <see cref="CyclopsExternalDamageManager.RepairPoint(CyclopsDamagePoint)"/> would seem like the correct method to patch, but adding to its postfix will
/// execute before <see cref="CyclopsDamagePoint.OnRepair"/> is finished working. Both owners and non-owners will be able to repair damage points on a ship.
/// </summary>
public sealed partial class CyclopsDamagePoint_OnRepair_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsDamagePoint t) => t.OnRepair());
public static void Postfix(CyclopsDamagePoint __instance)
{
// If the amount is high enough, it'll heal full
Resolve<Cyclops>().OnDamagePointRepaired(__instance.GetComponentInParent<SubRoot>(), __instance, 999);
}
}

View File

@@ -0,0 +1,20 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Core;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsDecoyLaunchButton_OnClick_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsDecoyLaunchButton t) => t.OnClick());
public static void Postfix(CyclopsHornButton __instance)
{
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
NitroxServiceLocator.LocateService<Cyclops>().BroadcastLaunchDecoy(id);
}
}
}

View File

@@ -0,0 +1,79 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours.Cyclops;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Broadcasts the cyclops destruction, and safely removes every player from it. Also broadcasts the creation of the beacon.
/// </summary>
public sealed partial class CyclopsDestructionEvent_DestroyCyclops_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsDestructionEvent t) => t.DestroyCyclops());
public static void Prefix(CyclopsDestructionEvent __instance)
{
bool wasInCyclops = Player.main.currentSub == __instance.subRoot;
// Before the cyclops destruction, we move out the remote players so that they aren't stuck in its hierarchy
if (__instance.subRoot && __instance.subRoot.TryGetComponent(out NitroxCyclops nitroxCyclops))
{
nitroxCyclops.RemoveAllPlayers();
}
if (wasInCyclops)
{
// Particular case here, this is not broadcasted and should not be, it's just there to have player be really inside the cyclops while not being registered by NitroxCyclops
Player.main._currentSub = __instance.subRoot;
}
__instance.subLiveMixin.Kill();
}
public static void Postfix(CyclopsDestructionEvent __instance)
{
if (__instance.TryGetNitroxId(out NitroxId nitroxId))
{
Resolve<Vehicles>().BroadcastDestroyedCyclops(__instance.gameObject, nitroxId);
}
}
/*
* ADD at the end of the method:
* CyclopsDestructionEvent_DestroyCyclops_Patch.ManageBeacon(component, this);
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).End() // Move before Ret
.InsertAndAdvance([
new CodeInstruction(OpCodes.Ldloc_2),
new CodeInstruction(OpCodes.Ldarg_0),
new CodeInstruction(OpCodes.Call, Reflect.Method(() => ManageBeacon(default, default)))
]).InstructionEnumeration();
}
public static void ManageBeacon(Beacon beacon, CyclopsDestructionEvent cyclopsDestructionEvent)
{
if (!cyclopsDestructionEvent.TryGetNitroxId(out NitroxId nitroxId))
{
return;
}
// We let the simulating player spawn it for everyone
if (!Resolve<SimulationOwnership>().HasAnyLockType(nitroxId))
{
Object.Destroy(beacon.gameObject);
return;
}
// We need to force this state for beaconLabel to wear the correct tag
beacon.Start();
Resolve<Items>().Dropped(beacon.gameObject, TechType.Beacon);
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Reflection;
using HarmonyLib;
using NitroxModel.Helper;
using static NitroxModel.Helper.Reflect;
namespace NitroxPatcher.Patches.Dynamic;
public sealed class CyclopsDestructionEvent_OnConsoleCommand_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD_RESTORE = Reflect.Method((CyclopsDestructionEvent t) => t.OnConsoleCommand_restorecyclops(default));
private static readonly MethodInfo TARGET_METHOD_DESTROY = Reflect.Method((CyclopsDestructionEvent t) => t.OnConsoleCommand_destroycyclops(default));
public static bool PrefixRestore()
{
// TODO: add support for "restorecyclops" command
Log.InGame(Language.main.Get("Nitrox_CommandNotAvailable"));
return false;
}
public static bool PrefixDestroy(CyclopsDestructionEvent __instance, out bool __state)
{
// We only apply the destroy to the current Cyclops
__state = Player.main.currentSub == __instance.subRoot;
return __state;
}
public override void Patch(Harmony harmony)
{
MethodInfo destroyPrefixInfo = Method(() => PrefixDestroy(default, out Ref<bool>.Field));
PatchPrefix(harmony, TARGET_METHOD_RESTORE, ((Func<bool>)PrefixRestore).Method);
PatchPrefix(harmony, TARGET_METHOD_DESTROY, destroyPrefixInfo);
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.Communication;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.GameLogic.Entities;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using NitroxPatcher.PatternMatching;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsDestructionEvent_SpawnLootAsync_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((CyclopsDestructionEvent t) => t.SpawnLootAsync()));
// Matches twice, once for scrap metal and once for computer chips
public static readonly InstructionsPattern PATTERN = new(expectedMatches: 2)
{
{ Reflect.Method(() => UnityEngine.Object.Instantiate(default(GameObject), default(Vector3), default(Quaternion))), "SpawnObject" }
};
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions)
.MatchStartForward(new CodeMatch(OpCodes.Switch))
.InsertAndAdvance(new CodeInstruction(OpCodes.Call, Reflect.Method(() => TrampolineCallback(default))))
.InstructionEnumeration()
.InsertAfterMarker(PATTERN, "SpawnObject", [
new(OpCodes.Dup),
new(OpCodes.Ldloc_1),
new(OpCodes.Call, ((Action<GameObject, CyclopsDestructionEvent>)SpawnObjectCallback).Method)
]);
}
public static void SpawnObjectCallback(GameObject gameObject, CyclopsDestructionEvent __instance)
{
NitroxId lootId = NitroxEntity.GenerateNewId(gameObject);
LargeWorldEntity largeWorldEntity = gameObject.GetComponent<LargeWorldEntity>();
PrefabIdentifier prefabIdentifier = gameObject.GetComponent<PrefabIdentifier>();
Pickupable pickupable = gameObject.GetComponent<Pickupable>();
WorldEntity lootEntity = new(gameObject.transform.ToWorldDto(), (int)largeWorldEntity.cellLevel, prefabIdentifier.classId, false, lootId, pickupable.GetTechType().ToDto(), null, null, []);
Resolve<Entities>().BroadcastEntitySpawnedByClient(lootEntity);
}
public static int TrampolineCallback(int originalIndex)
{
// Immediately return from iterator block if called from within CyclopsMetadataProcessor
return PacketSuppressor<EntitySpawnedByClient>.IsSuppressed ? int.MaxValue : originalIndex;
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsEngineChangeState_OnClick_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsEngineChangeState t) => t.OnClick());
public static void Postfix(CyclopsEngineChangeState __instance)
{
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Cyclops>().BroadcastMetadataChange(id);
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsExternalDamageManager_CreatePoint_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsExternalDamageManager t) => t.CreatePoint());
public static bool Prefix(CyclopsExternalDamageManager __instance, out bool __state)
{
// Block from creating points if they aren't the owner of the sub
__state = __instance.subRoot.TryGetNitroxId(out NitroxId id) && Resolve<SimulationOwnership>().HasAnyLockType(id);
return __state;
}
public static void Postfix(CyclopsExternalDamageManager __instance, bool __state)
{
if (__state)
{
Resolve<Cyclops>().OnCreateDamagePoint(__instance.subRoot);
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Core;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/*
* Relays Cyclops FireSuppressionSystem to other players
* This method was used instead of the OnClick to ensure, that the the suppression really started
*/
public sealed partial class CyclopsFireSuppressionButton_StartCooldown_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsFireSuppressionSystemButton t) => t.StartCooldown());
public static void Postfix(CyclopsFireSuppressionSystemButton __instance)
{
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
NitroxServiceLocator.LocateService<Cyclops>().BroadcastActivateFireSuppression(id);
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsHelmHUDManager_StopPiloting_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsHelmHUDManager t) => t.StopPiloting());
public static void Postfix(CyclopsHelmHUDManager __instance)
{
__instance.hudActive = true;
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Cyclops>().BroadcastMetadataChange(id);
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsHelmHUDManager_Update_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsHelmHUDManager t) => t.Update());
public static void Postfix(CyclopsHelmHUDManager __instance)
{
// To show the Cyclops HUD every time "hudActive" have to be true. "hornObject" is a good indicator to check if the player piloting the cyclops.
if (!__instance.hornObject.activeSelf && __instance.hudActive)
{
__instance.canvasGroup.interactable = false;
}
else if (!__instance.hudActive)
{
__instance.hudActive = true;
}
if (__instance.subLiveMixin.IsAlive())
{
if (__instance.motorMode.engineOn)
{
__instance.engineToggleAnimator.SetTrigger("EngineOn");
}
else
{
__instance.engineToggleAnimator.SetTrigger("EngineOff");
}
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsLightingPanel_ToggleFloodlights_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsLightingPanel t) => t.ToggleFloodlights());
public static bool Prefix(CyclopsLightingPanel __instance, out bool __state)
{
__state = __instance.floodlightsOn;
return true;
}
public static void Postfix(CyclopsLightingPanel __instance, bool __state)
{
if (__state != __instance.floodlightsOn && __instance.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Entities>().EntityMetadataChanged(__instance, id);
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsLightingPanel_ToggleInternalLighting_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsLightingPanel t) => t.ToggleInternalLighting());
public static bool Prefix(CyclopsLightingPanel __instance, out bool __state)
{
__state = __instance.lightingOn;
return true;
}
public static void Postfix(CyclopsLightingPanel __instance, bool __state)
{
if (__state != __instance.lightingOn && __instance.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Entities>().EntityMetadataChanged(__instance, id);
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxClient.Unity.Helper;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsMotorModeButton_OnClick_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsMotorModeButton t) => t.OnClick());
public static bool Prefix(CyclopsMotorModeButton __instance, out bool __state)
{
SubRoot cyclops = __instance.subRoot;
if (cyclops != null && cyclops == Player.main.currentSub)
{
CyclopsHelmHUDManager cyclops_HUD = cyclops.gameObject.RequireComponentInChildren<CyclopsHelmHUDManager>();
// To show the Cyclops HUD every time "hudActive" have to be true. "hornObject" is a good indicator to check if the player piloting the cyclops.
if (cyclops_HUD.hudActive)
{
__state = cyclops_HUD.hornObject.activeSelf;
return cyclops_HUD.hornObject.activeSelf;
}
}
__state = false;
return false;
}
public static void Postfix(CyclopsMotorModeButton __instance, bool __state)
{
if (__state && __instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Cyclops>().BroadcastMetadataChange(id);
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsMotorMode_RestoreEngineState_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsMotorMode t) => t.RestoreEngineState());
public static bool Prefix()
{
// We don't want this to happen because it will prevent players that were far of the cyclops when spawning to see its actual engine state
return false;
}
}

View File

@@ -0,0 +1,16 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsMotorMode_SaveEngineStateAndPowerDown_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsMotorMode t) => t.SaveEngineStateAndPowerDown());
public static bool Prefix(CyclopsMotorMode __instance)
{
// SN disable the engine if the player leave the cyclops. So this must be avoided.
__instance.engineOnOldState = __instance.engineOn;
return false;
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsShieldButton_OnClick_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsShieldButton t) => t.OnClick());
public static readonly OpCode START_CUT_CODE = OpCodes.Ldsfld;
public static readonly OpCode START_CUT_CODE_CALL = OpCodes.Callvirt;
public static readonly FieldInfo PLAYER_MAIN_FIELD = Reflect.Field(() => Player.main);
public static readonly OpCode END_CUT_CODE = OpCodes.Ret;
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
List<CodeInstruction> instructionList = instructions.ToList();
int startCut = 0;
int endCut = instructionList.Count;
/* Cut out
* if (Player.main.currentSub != this.subRoot)
* {
* return;
* }
*/
for (int i = 1; i < instructionList.Count; i++)
{
if (instructionList[i - 1].opcode.Equals(START_CUT_CODE) && instructionList[i - 1].operand.Equals(PLAYER_MAIN_FIELD) && instructionList[i].opcode == START_CUT_CODE_CALL)
{
startCut = i - 1;
}
// Cut at the first return encountered
if (endCut == instructionList.Count && instructionList[i].opcode.Equals(END_CUT_CODE))
{
endCut = i;
}
}
instructionList.RemoveRange(startCut, endCut + 1);
if (startCut == 0)
{
instructionList.Insert(0, new CodeInstruction(OpCodes.Nop));
}
return instructionList;
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsShieldButton_StartShield_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsShieldButton t) => t.StartShield());
public static void Postfix(CyclopsShieldButton __instance)
{
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Cyclops>().BroadcastMetadataChange(id);
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsShieldButton_StopShield_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsShieldButton t) => t.StopShield());
public static void Postfix(CyclopsShieldButton __instance)
{
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Cyclops>().BroadcastMetadataChange(id);
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsSilentRunningAbilityButton_TurnOffSilentRunning_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsSilentRunningAbilityButton t) => t.TurnOffSilentRunning());
public static void Postfix(CyclopsSilentRunningAbilityButton __instance)
{
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Cyclops>().BroadcastMetadataChange(id);
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsSilentRunningAbilityButton_TurnOnSilentRunning_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsSilentRunningAbilityButton t) => t.TurnOnSilentRunning());
public static void Postfix(CyclopsSilentRunningAbilityButton __instance)
{
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Cyclops>().BroadcastMetadataChange(id);
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsSonarButton_OnClick_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsSonarButton t) => t.OnClick());
public static void Postfix(CyclopsSonarButton __instance)
{
if (__instance.subRoot.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Cyclops>().BroadcastMetadataChange(id);
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// The sonar will stay on until the player leaves the vehicle and automatically turns on when they enter again (if sonar was on at that time).
/// </summary>
public sealed partial class CyclopsSonarButton_SonarPing_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsSonarButton t) => t.SonarPing());
public static bool Prefix(CyclopsSonarButton __instance)
{
SubRoot subRoot = __instance.subRoot;
if (Player.main.currentSub != subRoot)
{
return false;
}
if (LocalPlayerHasLock(subRoot) && !subRoot.powerRelay.ConsumeEnergy(subRoot.sonarPowerCost, out float _))
{
__instance.TurnOffSonar();
return false;
}
SNCameraRoot.main.SonarPing();
__instance.soundFX.Play();
return false;
}
private static bool LocalPlayerHasLock(SubRoot subRoot)
{
return subRoot.TryGetNitroxId(out NitroxId entityId) && Resolve<SimulationOwnership>().HasExclusiveLock(entityId);
}
}

View File

@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.MonoBehaviours.Cyclops;
using NitroxClient.MonoBehaviours.Vehicles;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents sonar from turning off automatically for the player that isn't currently piloting the Cyclops.
/// </summary>
public sealed partial class CyclopsSonarButton_Update_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsSonarButton t) => t.Update());
internal static readonly OpCode INJECTION_OPCODE = OpCodes.Ldsfld;
internal static readonly object INJECTION_OPERAND = Reflect.Field(() => Player.main);
public static IEnumerable<CodeInstruction> Transpiler(MethodBase methodBase, IEnumerable<CodeInstruction> instructions, ILGenerator il)
{
/* Normally in the Update()
* if (Player.main.GetMode() == Player.Mode.Normal && this.sonarActive)
* {
* this.TurnOffSonar();
* }
* this part will be changed into:
* if (CyclopsSonarButton_Update_Patch.ShouldTurnOff(this) && Player.main.GetMode() == Player.Mode.Normal && this.sonarActive)
*/
List<CodeInstruction> codeInstructions = new(instructions);
Label brLabel = il.DefineLabel();
CodeInstruction loadInstruction = new(OpCodes.Ldarg_0);
CodeInstruction callInstruction = new(OpCodes.Call, Reflect.Method(() => ShouldTurnoff(default)));
CodeInstruction brInstruction = new(OpCodes.Brfalse, brLabel);
codeInstructions.Last().labels.Add(brLabel);
for (int i = 0; i < codeInstructions.Count; i++)
{
CodeInstruction instruction = codeInstructions[i];
if (instruction.opcode.Equals(INJECTION_OPCODE) && instruction.operand.Equals(INJECTION_OPERAND))
{
// The second line after the current instruction should be a Brtrue, we need its operand to have the same jump label for our brfalse
CodeInstruction nextBr = codeInstructions[i + 2];
// The new instruction will be the first of the if statement, so it should take the jump labels that the former first part of the statement had
instruction.MoveLabelsTo(loadInstruction);
yield return loadInstruction;
yield return callInstruction;
yield return brInstruction;
}
yield return instruction;
}
}
/// <returns>true (sonar should be turned off) if local player is simulating the cyclops (there's no replicator in this case)</returns>
public static bool ShouldTurnoff(CyclopsSonarButton cyclopsSonarButton)
{
return !cyclopsSonarButton.subRoot.GetComponent<CyclopsMovementReplicator>();
}
}

View File

@@ -0,0 +1,17 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Persistent;
public sealed partial class CyclopsSonarCreatureDetector_CheckForCreaturesInRange_Patch : NitroxPatch, IPersistentPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsSonarCreatureDetector t) => t.CheckForCreaturesInRange());
public const CyclopsSonarDisplay.EntityType PLAYER_TYPE = (CyclopsSonarDisplay.EntityType)2;
public static void Postfix(CyclopsSonarCreatureDetector __instance)
{
__instance.ChekItemsOnHashSet(Resolve<PlayerManager>().GetAllPlayerObjects(), PLAYER_TYPE);
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.PlayerLogic;
using NitroxModel.Helper;
using NitroxModel_Subnautica.DataStructures;
using NitroxPatcher.Patches.Persistent;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class CyclopsSonarDisplay_NewEntityOnSonar_Patch : NitroxPatch, IPersistentPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((CyclopsSonarDisplay t) => t.NewEntityOnSonar(default));
/*
* }
* this.entitysOnSonar.Add(entityPing2);
* SetupPing(component, entityData); <----- INSERTED LINE
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).End()
.InsertAndAdvance(TARGET_METHOD.Ldloc<CyclopsHUDSonarPing>())
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_1))
.InsertAndAdvance(new CodeInstruction(OpCodes.Call, Reflect.Method(() => SetupPing(default, default))))
.InstructionEnumeration();
}
public static void SetupPing(CyclopsHUDSonarPing ping, CyclopsSonarCreatureDetector.EntityData entityData)
{
if (entityData.entityType != CyclopsSonarCreatureDetector_CheckForCreaturesInRange_Patch.PLAYER_TYPE)
{
return;
}
Color color;
if (entityData.gameObject == Player.mainObject)
{
color = Resolve<LocalPlayer>().PlayerSettings.PlayerColor.ToUnity();
}
else if (entityData.gameObject.TryGetComponent(out RemotePlayerIdentifier remotePlayerIdentifier))
{
color = remotePlayerIdentifier.RemotePlayer.PlayerSettings.PlayerColor.ToUnity();
}
else
{
return;
}
CyclopsHUDSonarPing sonarPing = ping.GetComponent<CyclopsHUDSonarPing>();
// Set isCreaturePing to true so that CyclopsHUDSonarPing.Start runs the SetColor code
sonarPing.isCreaturePing = true;
sonarPing.passiveColor = color;
sonarPing.Start();
sonarPing.isCreaturePing = false;
// We remove the pulse to be able to differentiate those signals from the creatures and decoy ones
GameObject.Destroy(sonarPing.transform.Find("Ping/PingPulse").gameObject);
}
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using NitroxClient.Communication.Abstract;
using NitroxModel.Core;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class DayNightCycle_OnConsoleCommand_day_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((DayNightCycle t) => t.OnConsoleCommand_day(default(NotificationCenter.Notification)));
public static bool Prefix()
{
IPacketSender packetSender = NitroxServiceLocator.LocateService<IPacketSender>();
packetSender.Send(new ServerCommand("time day"));
return false;
}
}

View File

@@ -0,0 +1,16 @@
using System.Reflection;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class DayNightCycle_OnConsoleCommand_daynightspeed_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((DayNightCycle t) => t.OnConsoleCommand_daynightspeed(default));
// The command is skipped because simulating speed reliable on the server is out of scope
public static bool Prefix()
{
Log.InGame(Language.main.Get("Nitrox_CommandNotAvailable"));
return false;
}
}

View File

@@ -0,0 +1,17 @@
using System.Reflection;
using NitroxClient.Communication.Abstract;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class DayNightCycle_OnConsoleCommand_night_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((DayNightCycle t) => t.OnConsoleCommand_night(default(NotificationCenter.Notification)));
public static bool Prefix()
{
Resolve<IPacketSender>().Send(new ServerCommand("time night"));
return false;
}
}

View File

@@ -0,0 +1,22 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Replace the local time calculations by the real server time.
/// </summary>
public sealed partial class DayNightCycle_Update_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((DayNightCycle t) => t.Update());
public static bool Prefix(DayNightCycle __instance)
{
// Essential part of the Update() method to have it running all of the time and have the local time set to the real server time
__instance.timePassedAsDouble = Resolve<TimeManager>().CalculateCurrentTime();
__instance.UpdateAtmosphere();
__instance.UpdateDayNightMessage();
return false;
}
}

View File

@@ -0,0 +1,20 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Replace the deltaTime calculations by one that is not capped by <see cref="Time.maximumDeltaTime"/>
/// </summary>
public sealed partial class DayNightCycle_deltaTime_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Property((DayNightCycle t) => t.deltaTime).GetGetMethod();
public static bool Prefix(DayNightCycle __instance, out float __result)
{
__result = Resolve<TimeManager>().DeltaTime;
return false;
}
}

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using NitroxModel.Helper;
using NitroxPatcher.PatternMatching;
using UnityEngine;
using static System.Reflection.Emit.OpCodes;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Keeps DevConsole disabled when enter is pressed.
/// </summary>
public sealed partial class DevConsole_Update_Patch : NitroxPatch, IDynamicPatch
{
private static readonly InstructionsPattern devConsoleSetStateTruePattern = new()
{
Reflect.Method(() => Input.GetKeyDown(default(KeyCode))),
Brfalse,
Ldarg_0,
Ldfld,
Brtrue,
Ldarg_0,
{ Ldc_I4_1, "ConsoleEnableFlag" },
Reflect.Method((DevConsole t) => t.SetState(default(bool)))
};
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((DevConsole t) => t.Update());
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
return instructions.ChangeAtMarker(devConsoleSetStateTruePattern, "ConsoleEnableFlag", i => i.opcode = Ldc_I4_0);
}
}

View File

@@ -0,0 +1,67 @@
using System.Reflection;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.Simulation;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class DockedVehicleHandTarget_OnHandClick_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo targetMethod = Reflect.Method((DockedVehicleHandTarget t) => t.OnHandClick(default(GUIHand)));
private static bool skipPrefix;
public static bool Prefix(DockedVehicleHandTarget __instance, GUIHand hand)
{
Vehicle vehicle = __instance.dockingBay.GetDockedVehicle();
if (skipPrefix || !vehicle.TryGetIdOrWarn(out NitroxId vehicleId))
{
return true;
}
if (Resolve<SimulationOwnership>().HasExclusiveLock(vehicleId))
{
Log.Debug($"Already have an exclusive lock on this vehicle: {vehicleId}");
return true;
}
HandInteraction<DockedVehicleHandTarget> context = new(__instance, hand);
LockRequest<HandInteraction<DockedVehicleHandTarget>> lockRequest = new(vehicleId, SimulationLockType.EXCLUSIVE, ReceivedSimulationLockResponse, context);
Resolve<SimulationOwnership>().RequestSimulationLock(lockRequest);
return false;
}
private static void ReceivedSimulationLockResponse(NitroxId vehicleId, bool lockAcquired, HandInteraction<DockedVehicleHandTarget> context)
{
if (lockAcquired)
{
VehicleDockingBay dockingBay = context.Target.dockingBay;
Vehicle vehicle = dockingBay.GetDockedVehicle();
if (!dockingBay.TryGetIdOrWarn(out NitroxId dockId))
{
return;
}
Vehicles.EngagePlayerMovementSuppressor(vehicle);
Resolve<IPacketSender>().Send(new VehicleUndocking(vehicleId, dockId, Resolve<IMultiplayerSession>().Reservation.PlayerId, true));
skipPrefix = true;
context.Target.OnHandClick(context.GuiHand);
skipPrefix = false;
}
else
{
//TODO: Check if this should be Hand
HandReticle.main.SetText(HandReticle.TextType.Hand, "Another player is using this vehicle!", false, GameInput.Button.None);
HandReticle.main.SetText(HandReticle.TextType.HandSubscript, string.Empty, false, GameInput.Button.None);
HandReticle.main.SetIcon(HandReticle.IconType.HandDeny, 1f);
context.Target.isValidHandTarget = false;
}
}
}

View File

@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents <see cref="Eatable.IterateDespawn"/> from happening on non-simulated entities and broadcast it for simulated entities
/// </summary>
public sealed partial class Eatable_IterateDespawn_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((Eatable t) => t.IterateDespawn());
public static bool Prefix(CreatureDeath __instance)
{
if (__instance.TryGetNitroxId(out NitroxId creatureId) &&
Resolve<SimulationOwnership>().HasAnyLockType(creatureId))
{
return true;
}
return false;
}
/*
* if (DayNightCycle.main.timePassedAsFloat - this.timeDespawnStart > this.despawnDelay)
* {
* base.CancelInvoke();
* UnityEngine.Object.Destroy(base.gameObject);
* Eatable_IterateDespawn_Patch.BroadcastEatableDestroyed(this); [INSERTED LINE]
* }
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
// We add instructions right before the ret which is equivalent to inserting at last offset
return new CodeMatcher(instructions).End()
.InsertAndAdvance(new CodeInstruction(OpCodes.Ldarg_0))
.Insert(new CodeInstruction(OpCodes.Call, Reflect.Method(() => BroadcastEatableDestroyed(default))))
.InstructionEnumeration();
}
public static void BroadcastEatableDestroyed(Eatable eatable)
{
if (eatable.TryGetNitroxId(out NitroxId objectId))
{
Resolve<IPacketSender>().Send(new EntityDestroyed(objectId));
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// When items are spawned in Subnautica will automatically add batteries to them async. This is challenging to deal with because it is
/// difficult to discriminate between these async/default additions and a user purposfully placing batteries into an object. Instead, we
/// disable the default behavior and allow Nitrox to always spawn the batteries. This guarentees we can capture the ids correctly.
/// </summary>
public sealed partial class EnergyMixin_SpawnDefaultAsync_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((EnergyMixin t) => t.SpawnDefaultAsync(default(float), default(TaskResult<bool>))));
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
// Blanks out the generated MoveNext() and replaces it with:
//
// result.set(false);
// return false;
//
yield return new CodeInstruction(OpCodes.Ldarg_0);
yield return new CodeInstruction(OpCodes.Ldfld, TARGET_METHOD.DeclaringType.GetField("result", BindingFlags.Instance | BindingFlags.Public));
yield return new CodeInstruction(OpCodes.Ldc_I4_0);
yield return new CodeInstruction(OpCodes.Callvirt, Reflect.Method((TaskResult<bool> result) => result.Set(default)));
yield return new CodeInstruction(OpCodes.Ldc_I4_0);
yield return new CodeInstruction(OpCodes.Ret);
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class EnergyMixin_ModifyCharge_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((EnergyMixin t) => t.ModifyCharge(default(float)));
public static void Postfix(EnergyMixin __instance, float __result)
{
GameObject batteryGo = __instance.GetBatteryGameObject();
if (batteryGo && batteryGo.TryGetComponent(out Battery battery) &&
Math.Abs(Math.Floor(__instance.charge) - Math.Floor(__instance.charge - __result)) > 0.0 && //Send package if power changed to next natural number
batteryGo.TryGetIdOrWarn(out NitroxId id))
{
Resolve<Entities>().EntityMetadataChanged(battery, id);
}
}
}

View File

@@ -0,0 +1,18 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class EnergyMixin_OnAddItem_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((EnergyMixin t) => t.OnAddItem(default(InventoryItem)));
public static void Postfix(EnergyMixin __instance, InventoryItem item)
{
if (item != null)
{
Resolve<ItemContainers>().BroadcastBatteryAdd(item.item.gameObject, __instance.gameObject, item.techType);
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Registers the awakening of a cell for <see cref="Terrain"/>, and prevents the cell from loading serialized data of any sort.
/// </summary>
public sealed partial class EntityCell_AwakeAsync_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((EntityCell t) => t.AwakeAsync(default)));
/*
* this.state = EntityCell.State.InAwakeAsync;
* EntityCell_AwakeAsync_Patch.Callback(this); <--- INSERTED LINE
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).MatchEndForward([
new CodeMatch(OpCodes.Ldloc_2),
new CodeMatch(OpCodes.Ldc_I4_6),
new CodeMatch(OpCodes.Stfld, Reflect.Field((EntityCell t) => t.state))
])
.Advance(1)
.InsertAndAdvance([
new CodeInstruction(OpCodes.Ldloc_2),
new CodeInstruction(OpCodes.Call, ((Action<EntityCell>)Callback).Method)
]).InstructionEnumeration();
}
public static void Callback(EntityCell __instance)
{
Resolve<Terrain>().CellLoaded(__instance.BatchId, __instance.CellId, __instance.Level);
__instance.ClearWaiterQueue();
__instance.serialData.Clear();
__instance.legacyData.Clear();
__instance.waiterData.Clear();
}
}

View File

@@ -0,0 +1,18 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// An important source of cell unload which is different than <see cref="EntityCell.SleepAsync"/> but must also be taken into account.
/// </summary>
public sealed partial class EntityCell_Reset_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((EntityCell t) => t.Reset());
public static void Prefix(EntityCell __instance)
{
Resolve<Terrain>().CellUnloaded(__instance.batchId, __instance.cellId, __instance.level);
}
}

View File

@@ -0,0 +1,18 @@
using System.Reflection;
using HarmonyLib;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents caching cells GameObjects.
/// </summary>
public sealed partial class EntityCell_SerializeAsyncImpl_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((EntityCell t) => t.SerializeAsyncImpl(default, default)));
public static bool Prefix()
{
return false;
}
}

View File

@@ -0,0 +1,18 @@
using System.Reflection;
using HarmonyLib;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Prevents caching cells GameObjects.
/// </summary>
public sealed partial class EntityCell_SerializeWaiterDataAsync_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((EntityCell t) => t.SerializeWaiterDataAsync(default)));
public static bool Prefix()
{
return false;
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Entity cells will go sleep when the player gets out of range. This needs to be reported to the server so they can lose simulation locks.
/// </summary>
public sealed partial class EntityCell_SleepAsync_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((EntityCell t) => t.SleepAsync(default)));
/*
* this.state = EntityCell.State.InSleepAsync;
* EntityCell_SleepAsync_Patch.Callback(this); <--- INSERTED LINE
*/
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(instructions).MatchEndForward([
new CodeMatch(OpCodes.Ldloc_1),
new CodeMatch(OpCodes.Ldc_I4_7),
new CodeMatch(OpCodes.Stfld, Reflect.Field((EntityCell t) => t.state))
])
.Advance(1)
.InsertAndAdvance([
new CodeInstruction(OpCodes.Ldloc_1),
new CodeInstruction(OpCodes.Call, ((Action<EntityCell>)Callback).Method)
]).InstructionEnumeration();
}
public static void Callback(EntityCell entityCell)
{
Resolve<Terrain>().CellUnloaded(entityCell.BatchId, entityCell.CellId, entityCell.Level);
}
}

View File

@@ -0,0 +1,18 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Equipment_AddItem_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((Equipment t) => t.AddItem(default, default, default));
public static void Postfix(Equipment __instance, bool __result, string slot, InventoryItem newItem)
{
if (__result)
{
Resolve<EquipmentSlots>().BroadcastEquip(newItem.item, __instance.owner, slot);
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class Equipment_RemoveItem_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((Equipment t) => t.RemoveItem(default(string), default(bool), default(bool)));
public static readonly OpCode INJECTION_OPCODE = OpCodes.Stloc_1;
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
foreach (CodeInstruction instruction in instructions)
{
yield return instruction;
if (instruction.opcode.Equals(INJECTION_OPCODE))
{
/*
* Multiplayer.Logic.EquipmentSlots.Unequip(pickupable, this.owner, slot)
*/
yield return TranspilerHelper.LocateService<EquipmentSlots>();
yield return new CodeInstruction(OpCodes.Ldloc_0);
yield return new CodeInstruction(OpCodes.Callvirt, Reflect.Property((InventoryItem t) => t.item).GetMethod);
yield return new CodeInstruction(OpCodes.Ldarg_0);
yield return new CodeInstruction(OpCodes.Call, Reflect.Property((Equipment t) => t.owner).GetMethod);
yield return new CodeInstruction(OpCodes.Ldarg_1);
yield return new CodeInstruction(OpCodes.Callvirt, Reflect.Method((EquipmentSlots t) => t.BroadcastUnequip(default(Pickupable), default(GameObject), default(string))));
}
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Reflection;
using NitroxModel.Helper;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class EscapePodFirstUseCinematicsController_ReleaseCreature_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((EscapePodFirstUseCinematicsController t) => t.ReleaseCreature(default));
/**
* Avoid cinematics from spawning unsynced entites.
* As soon as the cinematics are over, we'll kill them.
*/
public static bool Prefix(GameObject creatureGO)
{
if (creatureGO)
{
UnityEngine.Object.Destroy(creatureGO);
}
return false;
}
}

View File

@@ -0,0 +1,15 @@
using System.Reflection;
using NitroxClient.GameLogic.Spawning.WorldEntities;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class EscapePod_Awake_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((EscapePod t) => t.Awake());
public static bool Prefix(EscapePod __instance)
{
return !EscapePodWorldEntitySpawner.SuppressEscapePodAwakeMethod;
}
}

View File

@@ -0,0 +1,22 @@
using System.Reflection;
using NitroxClient.GameLogic;
using NitroxClient.GameLogic.Spawning.Metadata;
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class EscapePod_OnRepair_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((EscapePod t) => t.OnRepair());
public static void Prefix(EscapePod __instance)
{
if (__instance.TryGetIdOrWarn(out NitroxId id) &&
Resolve<EntityMetadataManager>().TryExtract(__instance, out EntityMetadata metadata))
{
Resolve<Entities>().BroadcastMetadataUpdate(id, metadata);
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Reflection;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
namespace NitroxPatcher.Patches.Dynamic;
public sealed partial class EscapePod_RespawnPlayer_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo TARGET_METHOD = Reflect.Method((EscapePod t) => t.RespawnPlayer());
public static void Postfix(EscapePod __instance)
{
// EscapePod.RespawnPlayer() runs both for player respawn (Player.MovePlayerToRespawnPoint()) and for warpme command
Optional<NitroxId> id = __instance.GetId();
Resolve<LocalPlayer>().BroadcastEscapePodChange(id);
}
}

Some files were not shown because too many files have changed in this diff Show More