Decompiled source of ShopkeeperDiscounts v0.1.2

BepInEx/plugins/ShopkeeperDiscounts/ShopkeeperDiscounts.dll

Decompiled 6 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("artrointel")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Configure shopkeeper discount chance, attempts per shop, and per-item failure tolerance. Host-only, with REPOConfig support.")]
[assembly: AssemblyFileVersion("0.1.2.0")]
[assembly: AssemblyInformationalVersion("0.1.2+891441b8bbe725edbe030d1be773a718f5aa4bbf")]
[assembly: AssemblyProduct("ShopkeeperDiscounts")]
[assembly: AssemblyTitle("ShopkeeperDiscounts")]
[assembly: AssemblyVersion("0.1.2.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace ShopkeeperDiscounts
{
	internal static class DiscountIlRewriter
	{
		internal static IEnumerable<CodeInstruction> AttemptLimit(IEnumerable<CodeInstruction> instructions, FieldInfo attempts, MethodInfo getter, string methodName)
		{
			List<CodeInstruction> list = instructions.Select((Func<CodeInstruction, CodeInstruction>)((CodeInstruction i) => new CodeInstruction(i))).ToList();
			int num = 0;
			for (int num2 = 1; num2 < list.Count; num2++)
			{
				if (CodeInstructionExtensions.LoadsField(list[num2 - 1], attempts, false) && CodeInstructionExtensions.LoadsConstant(list[num2], 3L))
				{
					list[num2].opcode = OpCodes.Call;
					list[num2].operand = getter;
					num++;
				}
			}
			if (num != 1)
			{
				throw new InvalidOperationException($"Expected one native discount limit in {methodName}, found {num}. Game API changed.");
			}
			return list;
		}

		internal static IEnumerable<CodeInstruction> Verdict(IEnumerable<CodeInstruction> instructions, MethodInfo nativeRange, MethodInfo draw)
		{
			List<CodeInstruction> list = instructions.Select((Func<CodeInstruction, CodeInstruction>)((CodeInstruction i) => new CodeInstruction(i))).ToList();
			int num = 0;
			for (int num2 = 2; num2 < list.Count; num2++)
			{
				if (CodeInstructionExtensions.Calls(list[num2], nativeRange) && CodeInstructionExtensions.LoadsConstant(list[num2 - 2], 0L) && CodeInstructionExtensions.LoadsConstant(list[num2 - 1], 3L))
				{
					list[num2].operand = draw;
					num++;
				}
			}
			if (num != 1)
			{
				throw new InvalidOperationException($"Expected one native discount verdict draw, found {num}. Game API changed.");
			}
			return list;
		}
	}
	internal static class DiscountPolicy
	{
		internal static int AttemptLimit()
		{
			if (!SemiFunc.IsMasterClientOrSingleplayer())
			{
				return 3;
			}
			return Plugin.Settings.AttemptsPerShop.Value;
		}

		internal static int DrawVerdict(int minimum, int maximum)
		{
			if (!SemiFunc.IsMasterClientOrSingleplayer())
			{
				return Random.Range(minimum, maximum);
			}
			return (!DiscountRules.Succeeds(Plugin.Settings.ChancePercent.Value, Random.Range(0, 100))) ? 1 : 0;
		}
	}
	internal static class DiscountRules
	{
		internal static bool Succeeds(int chancePercent, int roll)
		{
			if (roll >= 0 && roll < 100)
			{
				return roll < Math.Max(0, Math.Min(100, chancePercent));
			}
			return false;
		}
	}
	internal sealed class DiscountSettings
	{
		internal ConfigEntry<int> ChancePercent { get; }

		internal ConfigEntry<int> AttemptsPerShop { get; }

		internal ConfigEntry<int> BreakAfterFailures { get; }

		internal DiscountSettings(ConfigFile config)
		{
			//IL_0026: Unknown result type (might be due to invalid IL or missing references)
			//IL_0030: Expected O, but got Unknown
			//IL_0054: Unknown result type (might be due to invalid IL or missing references)
			//IL_005e: Expected O, but got Unknown
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_008b: Expected O, but got Unknown
			ChancePercent = config.Bind<int>("Discounts", "ChancePercent", 70, new ConfigDescription("Host only. Chance of a positive shopkeeper discount verdict, in percent. 0 always fails; 100 always succeeds for a valid item. Applies to the next verdict. Vanilla is approximately 33.33%.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			AttemptsPerShop = config.Bind<int>("Discounts", "AttemptsPerShop", 5, new ConfigDescription("Host only. Total discount attempts shared by the entire lobby per shop, not successful discounts or attempts per player. 0 disables attempts. Changes affect new requests without refunding spent attempts; an already accepted scan may finish.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 10), Array.Empty<object>()));
			BreakAfterFailures = config.Bind<int>("Discounts", "BreakAfterFailures", 1, new ConfigDescription("Host only. Cumulative failed discount verdicts on the same physical item before the native destruction effect is applied. Shared by all players; successful discounts do not reset the failure count. Applies to the next verdict. Default 1 preserves native first-failure destruction; 1-5 allowed.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 5), Array.Empty<object>()));
		}
	}
	internal sealed class FailedDiscountCounter
	{
		private int _lastVerdictSequence;

		internal int Failures { get; private set; }

		internal bool BreakAllowed { get; private set; }

		internal void ObserveVerdict(int sequence, bool failed, int breakAfterFailures)
		{
			if (sequence <= _lastVerdictSequence)
			{
				return;
			}
			_lastVerdictSequence = sequence;
			BreakAllowed = false;
			if (failed)
			{
				if (Failures < int.MaxValue)
				{
					Failures++;
				}
				int num = Math.Max(1, Math.Min(5, breakAfterFailures));
				BreakAllowed = Failures >= num;
			}
		}
	}
	internal sealed class ItemFailureTracker : MonoBehaviour
	{
		internal readonly FailedDiscountCounter Counter = new FailedDiscountCounter();
	}
	[HarmonyPatch(typeof(ShopKeeper), "ItemShownSet")]
	internal static class ItemFailureVerdictPatch
	{
		[HarmonyPrefix]
		private static void Prefix(PhysGrabObject _physGrabObject, int _timesShown, bool _disliked)
		{
			if (SemiFunc.IsMasterClientOrSingleplayer() && !((Object)(object)_physGrabObject == (Object)null))
			{
				ItemFailureTracker itemFailureTracker = ((Component)_physGrabObject).GetComponent<ItemFailureTracker>();
				if ((Object)(object)itemFailureTracker == (Object)null)
				{
					itemFailureTracker = ((Component)_physGrabObject).gameObject.AddComponent<ItemFailureTracker>();
				}
				itemFailureTracker.Counter.ObserveVerdict(_timesShown, _disliked, Plugin.Settings.BreakAfterFailures.Value);
			}
		}
	}
	[HarmonyPatch(typeof(ShopKeeper), "ItemDislikeAffectApply")]
	internal static class ItemFailureDestructionPatch
	{
		[HarmonyPrefix]
		private static bool Prefix(PhysGrabObject _physGrabObject)
		{
			if (!SemiFunc.IsMasterClientOrSingleplayer() || (Object)(object)_physGrabObject == (Object)null)
			{
				return true;
			}
			ItemFailureTracker component = ((Component)_physGrabObject).GetComponent<ItemFailureTracker>();
			if (!((Object)(object)component == (Object)null))
			{
				return component.Counter.BreakAllowed;
			}
			return true;
		}
	}
	[BepInPlugin("artrointel.ShopkeeperDiscounts", "ShopkeeperDiscounts", "0.1.2")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "artrointel.ShopkeeperDiscounts";

		public const string PluginName = "ShopkeeperDiscounts";

		public const string PluginVersion = "0.1.2";

		private Harmony? _harmony;

		internal static DiscountSettings Settings { get; private set; }

		private void Awake()
		{
			//IL_0016: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Expected O, but got Unknown
			Settings = new DiscountSettings(((BaseUnityPlugin)this).Config);
			_harmony = new Harmony("artrointel.ShopkeeperDiscounts");
			try
			{
				_ = ShopBindings.Attempts;
				_harmony.PatchAll();
				((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} v{1} loaded. Host settings: {2}% chance, {3} attempts per shop, break after {4} failures per item.", "ShopkeeperDiscounts", "0.1.2", Settings.ChancePercent.Value, Settings.AttemptsPerShop.Value, Settings.BreakAfterFailures.Value));
			}
			catch (Exception arg)
			{
				_harmony.UnpatchSelf();
				((BaseUnityPlugin)this).Logger.LogError((object)$"Initialization failed; all ShopkeeperDiscounts hooks were removed: {arg}");
				((Behaviour)this).enabled = false;
			}
		}

		private void OnDestroy()
		{
			Harmony? harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
	}
	internal static class ShopBindings
	{
		internal static readonly FieldInfo Attempts = AccessTools.DeclaredField(typeof(ShopKeeper), "itemDiscountAttempts") ?? throw new MissingFieldException(typeof(ShopKeeper).FullName, "itemDiscountAttempts");

		internal static int ReadAttempts(ShopKeeper keeper)
		{
			return (int)Attempts.GetValue(keeper);
		}

		internal static void RestoreAttempts(ShopKeeper keeper, int value)
		{
			Attempts.SetValue(keeper, value);
		}
	}
	[HarmonyPatch]
	internal static class AttemptLimitPatch
	{
		internal static readonly string[] MethodNames = new string[4] { "ItemPresentationTargetValid", "ItemNoticeStart", "TryStartItemDiscount", "ItemDenyPhysics" };

		[HarmonyTargetMethods]
		private static IEnumerable<MethodBase> TargetMethods()
		{
			string[] methodNames = MethodNames;
			foreach (string text in methodNames)
			{
				yield return AccessTools.DeclaredMethod(typeof(ShopKeeper), text, (Type[])null, (Type[])null) ?? throw new MissingMethodException(typeof(ShopKeeper).FullName, text);
			}
		}

		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, MethodBase __originalMethod)
		{
			return DiscountIlRewriter.AttemptLimit(instructions, AccessTools.DeclaredField(typeof(ShopKeeper), "itemDiscountAttempts"), AccessTools.DeclaredMethod(typeof(DiscountPolicy), "AttemptLimit", (Type[])null, (Type[])null), __originalMethod.Name);
		}
	}
	[HarmonyPatch(typeof(ShopKeeper), "ItemVerdictPositiveGet")]
	internal static class VerdictChancePatch
	{
		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			return DiscountIlRewriter.Verdict(instructions, AccessTools.DeclaredMethod(typeof(Random), "Range", new Type[2]
			{
				typeof(int),
				typeof(int)
			}, (Type[])null), AccessTools.DeclaredMethod(typeof(DiscountPolicy), "DrawVerdict", (Type[])null, (Type[])null));
		}
	}
	[HarmonyPatch(typeof(ShopKeeper), "UpdateStateRPC")]
	internal static class HostAttemptCounterPatch
	{
		[HarmonyPrefix]
		private static void Prefix(ShopKeeper __instance, out int __state)
		{
			__state = (SemiFunc.IsMasterClientOrSingleplayer() ? ShopBindings.ReadAttempts(__instance) : (-1));
		}

		[HarmonyPostfix]
		private static void Postfix(ShopKeeper __instance, int __state)
		{
			if (__state >= 0)
			{
				ShopBindings.RestoreAttempts(__instance, __state);
			}
		}
	}
}