Files
Nitrox/NitroxPatcher/Patches/Dynamic/PDAScanner_Scan_Patch.cs
2025-07-06 00:23:46 +02:00

93 lines
4.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.Communication.Abstract;
using NitroxModel.DataStructures;
using NitroxModel.Helper;
using NitroxModel.Packets;
using NitroxModel_Subnautica.DataStructures;
using UnityEngine;
namespace NitroxPatcher.Patches.Dynamic;
/// <summary>
/// Broadcasts entry progress and entry unlockings when they happen.
/// </summary>
/// <remarks>
/// This is the only method that needs to be tracked to sync and persist PDAScanner's data, because the other methods
/// are either unused (found out that they have no actual calls in SN's code, just as <see cref="PDAScanner.RemoveAllEntriesWhichUnlocks"/>)
/// or simply called automatically by other events (just as <see cref="PDAScanner.CompleteAllEntriesWhichUnlocks"/>).
/// </remarks>
public sealed partial class PDAScanner_Scan_Patch : NitroxPatch, IDynamicPatch
{
internal static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => PDAScanner.Scan());
internal static readonly OpCode INJECTION_OPCODE = OpCodes.Callvirt;
internal static readonly object INJECTION_OPERAND = Reflect.Method((GameObject t) => t.SendMessage(default, default, default));
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))
{
yield return original.Ldloc<PDAScanner.Result>();
yield return new(OpCodes.Ldloc_S, original.GetLocalVariableIndex<bool>(4));
yield return new(OpCodes.Call, ((Action<PDAScanner.Result, bool>)Callback).Method);
}
}
}
// To determine which exact path the code took, we just need those two parameters
public static void Callback(PDAScanner.Result result, bool alreadyComplete)
{
PDAScanner.ScanTarget scanTarget = PDAScanner.scanTarget;
TechType techType = scanTarget.techType;
PDAScanner.EntryData entryData = PDAScanner.GetEntryData(techType);
if (entryData == null || (result != PDAScanner.Result.Done && result != PDAScanner.Result.Researched))
{
return;
}
// In the case the player has already fully researched the target
if (alreadyComplete)
{
// In the case that the target is destroyable, the player is given 2 titanium and the object is destroyed
// We want to broadcast the destruction event before the object is destroyed and corresponding scan data is invalidated.
if (scanTarget.hasUID)
{
PDAScanFinished packet = new(new(scanTarget.uid), techType.ToDto(), entryData.totalFragments, true, true, true);
Resolve<IPacketSender>().Send(packet);
}
return;
}
else
{
NitroxId targetId = scanTarget.hasUID ? new(scanTarget.uid) : null;
// Case in which the unlocked progress is incremented
if (result == PDAScanner.Result.Done)
{
if (!PDAScanner.GetPartialEntryByKey(techType, out PDAScanner.Entry entry))
{
// Something wrong happened
return;
}
PDAScanFinished packet = new(targetId, techType.ToDto(), entry.unlocked, false, entryData.destroyAfterScan);
Resolve<IPacketSender>().Send(packet);
}
// Case in which the scanned entry is fully unlocked
else if (result == PDAScanner.Result.Researched)
{
PDAScanFinished packet = new(targetId, techType.ToDto(), entryData.totalFragments, true, entryData.destroyAfterScan);
Resolve<IPacketSender>().Send(packet);
}
}
}
}