Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ShopkeeperDiscounts v0.1.2
BepInEx/plugins/ShopkeeperDiscounts/ShopkeeperDiscounts.dll
Decompiled 6 days agousing 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); } } } }